317 lines
14 KiB
TypeScript
317 lines
14 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { Users, Star, Mail, User, Activity, Plus, X, Save } from 'lucide-react'
|
|
import clsx from 'clsx'
|
|
|
|
export type ContactRole = 'Operativer Entscheider' | 'Infrastruktur-Verantwortlicher' | 'Wirtschaftlicher Entscheider' | 'Innovations-Treiber'
|
|
|
|
export type ContactStatus =
|
|
| '' // Leer
|
|
// Manual
|
|
| 'Soft Denied' | 'Bounced' | 'Redirect' | 'Interested' | 'Hard denied'
|
|
// Auto
|
|
| 'Init' | '1st Step' | '2nd Step' | 'Not replied'
|
|
|
|
export interface Contact {
|
|
id?: number
|
|
gender: 'männlich' | 'weiblich'
|
|
title: string
|
|
first_name: string
|
|
last_name: string
|
|
email: string
|
|
job_title: string
|
|
language: 'De' | 'En'
|
|
role: ContactRole
|
|
status: ContactStatus
|
|
is_primary: boolean
|
|
}
|
|
|
|
interface ContactsManagerProps {
|
|
contacts?: Contact[]
|
|
initialContactId?: number | null // NEW
|
|
onAddContact?: (contact: Contact) => void
|
|
onEditContact?: (contact: Contact) => void
|
|
}
|
|
|
|
export function ContactsManager({ contacts = [], initialContactId, onAddContact, onEditContact }: ContactsManagerProps) {
|
|
const [editingContact, setEditingContact] = useState<Contact | null>(null)
|
|
const [isFormOpen, setIsFormOpen] = useState(false)
|
|
|
|
// Auto-open edit form if initialContactId is provided
|
|
useEffect(() => {
|
|
if (initialContactId && contacts.length > 0) {
|
|
const contact = contacts.find(c => c.id === initialContactId)
|
|
if (contact) {
|
|
setEditingContact({ ...contact })
|
|
setIsFormOpen(true)
|
|
}
|
|
}
|
|
}, [initialContactId, contacts])
|
|
|
|
const roleColors: Record<ContactRole, string> = {
|
|
'Operativer Entscheider': 'text-blue-400 border-blue-400/30 bg-blue-900/20',
|
|
'Infrastruktur-Verantwortlicher': 'text-orange-400 border-orange-400/30 bg-orange-900/20',
|
|
'Wirtschaftlicher Entscheider': 'text-green-400 border-green-400/30 bg-green-900/20',
|
|
'Innovations-Treiber': 'text-purple-400 border-purple-400/30 bg-purple-900/20'
|
|
}
|
|
|
|
const statusColors: Record<string, string> = {
|
|
'': 'text-slate-600 italic',
|
|
'Soft Denied': 'text-slate-400',
|
|
'Bounced': 'text-red-500',
|
|
'Redirect': 'text-yellow-500',
|
|
'Interested': 'text-green-500',
|
|
'Hard denied': 'text-red-700',
|
|
'Init': 'text-slate-300',
|
|
'1st Step': 'text-blue-300',
|
|
'2nd Step': 'text-blue-400',
|
|
'Not replied': 'text-slate-500',
|
|
}
|
|
|
|
const handleAddNew = () => {
|
|
setEditingContact({
|
|
gender: 'männlich',
|
|
title: '',
|
|
first_name: '',
|
|
last_name: '',
|
|
email: '',
|
|
job_title: '',
|
|
language: 'De',
|
|
role: 'Operativer Entscheider',
|
|
status: '',
|
|
is_primary: false
|
|
})
|
|
setIsFormOpen(true)
|
|
}
|
|
|
|
const handleEdit = (contact: Contact) => {
|
|
setEditingContact({ ...contact })
|
|
setIsFormOpen(true)
|
|
}
|
|
|
|
const handleSave = () => {
|
|
if (editingContact) {
|
|
if (editingContact.id) {
|
|
onEditContact && onEditContact(editingContact)
|
|
} else {
|
|
onAddContact && onAddContact(editingContact)
|
|
}
|
|
}
|
|
setIsFormOpen(false)
|
|
setEditingContact(null)
|
|
}
|
|
|
|
if (isFormOpen && editingContact) {
|
|
return (
|
|
<div className="bg-slate-900/50 rounded-lg p-4 border border-slate-700 space-y-4 animate-in fade-in slide-in-from-bottom-2">
|
|
<div className="flex justify-between items-center border-b border-slate-700 pb-2 mb-2">
|
|
<h3 className="text-sm font-bold text-white">
|
|
{editingContact.id ? 'Edit Contact' : 'New Contact'}
|
|
</h3>
|
|
<button onClick={() => setIsFormOpen(false)} className="text-slate-400 hover:text-white">
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Salutation / Address Section */}
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] uppercase text-slate-500 font-bold">Gender / Salutation</label>
|
|
<select
|
|
className="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:border-blue-500 outline-none"
|
|
value={editingContact.gender}
|
|
onChange={e => setEditingContact({...editingContact, gender: e.target.value as any})}
|
|
>
|
|
<option value="männlich">Male / Herr</option>
|
|
<option value="weiblich">Female / Frau</option>
|
|
</select>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] uppercase text-slate-500 font-bold">Academic Title</label>
|
|
<input
|
|
className="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:border-blue-500 outline-none"
|
|
value={editingContact.title}
|
|
placeholder="e.g. Dr., Prof."
|
|
onChange={e => setEditingContact({...editingContact, title: e.target.value})}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] uppercase text-slate-500 font-bold">First Name</label>
|
|
<input
|
|
className="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:border-blue-500 outline-none"
|
|
value={editingContact.first_name}
|
|
onChange={e => setEditingContact({...editingContact, first_name: e.target.value})}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] uppercase text-slate-500 font-bold">Last Name</label>
|
|
<input
|
|
className="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:border-blue-500 outline-none"
|
|
value={editingContact.last_name}
|
|
onChange={e => setEditingContact({...editingContact, last_name: e.target.value})}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] uppercase text-slate-500 font-bold">Email</label>
|
|
<input
|
|
className="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:border-blue-500 outline-none"
|
|
value={editingContact.email}
|
|
onChange={e => setEditingContact({...editingContact, email: e.target.value})}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] uppercase text-slate-500 font-bold">Job Title (Card)</label>
|
|
<input
|
|
className="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:border-blue-500 outline-none"
|
|
value={editingContact.job_title}
|
|
onChange={e => setEditingContact({...editingContact, job_title: e.target.value})}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] uppercase text-slate-500 font-bold">Our Role Interpretation</label>
|
|
<select
|
|
className="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:border-blue-500 outline-none"
|
|
value={editingContact.role}
|
|
onChange={e => setEditingContact({...editingContact, role: e.target.value as ContactRole})}
|
|
>
|
|
{Object.keys(roleColors).map(r => <option key={r} value={r}>{r}</option>)}
|
|
</select>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] uppercase text-slate-500 font-bold">Marketing Status</label>
|
|
<select
|
|
className="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:border-blue-500 outline-none"
|
|
value={editingContact.status}
|
|
onChange={e => setEditingContact({...editingContact, status: e.target.value as ContactStatus})}
|
|
>
|
|
<option value=""><leer></option>
|
|
{Object.keys(statusColors).filter(s => s !== '').map(s => <option key={s} value={s}>{s}</option>)}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] uppercase text-slate-500 font-bold">Language</label>
|
|
<select
|
|
className="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:border-blue-500 outline-none"
|
|
value={editingContact.language}
|
|
onChange={e => setEditingContact({...editingContact, language: e.target.value as any})}
|
|
>
|
|
<option value="De">De</option>
|
|
<option value="En">En</option>
|
|
</select>
|
|
</div>
|
|
<div className="flex items-center pt-5">
|
|
<label className="flex items-center gap-2 cursor-pointer text-sm text-slate-300 hover:text-white">
|
|
<input
|
|
type="checkbox"
|
|
checked={editingContact.is_primary}
|
|
onChange={e => setEditingContact({...editingContact, is_primary: e.target.checked})}
|
|
className="rounded border-slate-700 bg-slate-800 text-blue-500 focus:ring-blue-500"
|
|
/>
|
|
Primary Contact
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-2 pt-2">
|
|
<button
|
|
onClick={handleSave}
|
|
className="flex-1 bg-blue-600 hover:bg-blue-500 text-white text-sm font-bold py-2 rounded flex items-center justify-center gap-2"
|
|
>
|
|
<Save className="h-4 w-4" /> Save Contact
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-semibold text-slate-400 uppercase tracking-wider flex items-center gap-2">
|
|
<Users className="h-4 w-4" /> Contacts List
|
|
</h3>
|
|
<button
|
|
onClick={handleAddNew}
|
|
className="flex items-center gap-1 px-3 py-1 bg-blue-600/20 text-blue-400 border border-blue-500/30 rounded hover:bg-blue-600 hover:text-white transition-all text-xs font-bold"
|
|
>
|
|
<Plus className="h-3.5 w-3.5" /> ADD
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{contacts.length === 0 ? (
|
|
<div className="p-8 rounded-xl border border-dashed border-slate-800 text-center text-slate-600">
|
|
<Users className="h-8 w-8 mx-auto mb-3 opacity-20" />
|
|
<p className="text-sm font-medium">No contacts yet.</p>
|
|
<p className="text-xs mt-1 opacity-70">Click "ADD" to create the first contact for this account.</p>
|
|
</div>
|
|
) : (
|
|
contacts.map(contact => (
|
|
<div
|
|
key={contact.id}
|
|
className={clsx(
|
|
"relative bg-slate-800/30 border rounded-lg p-3 transition-all hover:bg-slate-800/50 group cursor-pointer",
|
|
contact.is_primary ? "border-blue-500/30 shadow-lg shadow-blue-900/10" : "border-slate-800"
|
|
)}
|
|
onClick={() => handleEdit(contact)}
|
|
>
|
|
{contact.is_primary && (
|
|
<div className="absolute top-2 right-2 text-blue-500" title="Primary Contact">
|
|
<Star className="h-3 w-3 fill-current" />
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-start gap-3">
|
|
<div className="p-2 bg-slate-900 rounded-full text-slate-400 shrink-0 mt-1">
|
|
<User className="h-4 w-4" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-0.5">
|
|
<span className="text-sm font-bold text-slate-200 truncate">
|
|
{contact.title ? `${contact.title} ` : ''}{contact.first_name} {contact.last_name}
|
|
</span>
|
|
<span className="text-[10px] text-slate-500 border border-slate-700 px-1 rounded">
|
|
{contact.language}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="text-xs text-slate-400 mb-2 truncate font-medium">
|
|
{contact.job_title}
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2 mb-2">
|
|
<span className={clsx("text-[10px] px-1.5 py-0.5 rounded border font-medium", roleColors[contact.role] || "text-slate-400 border-slate-700")}>
|
|
{contact.role}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3 text-[10px] text-slate-500 font-mono">
|
|
<div className="flex items-center gap-1 truncate">
|
|
<Mail className="h-3 w-3" />
|
|
{contact.email}
|
|
</div>
|
|
<div className={clsx("flex items-center gap-1 font-bold ml-auto mr-8", statusColors[contact.status])}>
|
|
<Activity className="h-3 w-3" />
|
|
{contact.status || '<leer>'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
} |