From 84d9db4515ef5b3a6ae01aabdae43e17da36fe74 Mon Sep 17 00:00:00 2001 From: Marius Tarau Date: Wed, 18 Feb 2026 06:35:17 +0200 Subject: [PATCH] feat(address-book): rebuild with multi-contact, project links, and extended fields - Add ContactPerson sub-entities for multi-contact per company - Add department, role, website, secondary email/phone, projectIds fields - Add internal contact type alongside client/supplier/institution/collaborator - Project tag picker using core TagService project tags - Updated search to include department and role Co-Authored-By: Claude Opus 4.6 --- .../components/address-book-module.tsx | 293 ++++++++++++++---- .../address-book/hooks/use-contacts.ts | 22 +- src/modules/address-book/types.ts | 29 +- 3 files changed, 286 insertions(+), 58 deletions(-) diff --git a/src/modules/address-book/components/address-book-module.tsx b/src/modules/address-book/components/address-book-module.tsx index f7a7b3d..6b5f686 100644 --- a/src/modules/address-book/components/address-book-module.tsx +++ b/src/modules/address-book/components/address-book-module.tsx @@ -1,19 +1,29 @@ 'use client'; import { useState } from 'react'; -import { Plus, Pencil, Trash2, Search, Mail, Phone, MapPin } from 'lucide-react'; +import { + Plus, Pencil, Trash2, Search, Mail, Phone, MapPin, + Globe, Building2, UserPlus, X, +} from 'lucide-react'; import { Button } from '@/shared/components/ui/button'; import { Input } from '@/shared/components/ui/input'; import { Label } from '@/shared/components/ui/label'; import { Textarea } from '@/shared/components/ui/textarea'; import { Badge } from '@/shared/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select'; -import type { AddressContact, ContactType } from '../types'; +import { + Select, SelectContent, SelectItem, SelectTrigger, SelectValue, +} from '@/shared/components/ui/select'; +import type { AddressContact, ContactType, ContactPerson } from '../types'; import { useContacts } from '../hooks/use-contacts'; +import { useTags } from '@/core/tagging'; const TYPE_LABELS: Record = { - client: 'Client', supplier: 'Furnizor', institution: 'Instituție', collaborator: 'Colaborator', + client: 'Client', + supplier: 'Furnizor', + institution: 'Instituție', + collaborator: 'Colaborator', + internal: 'Intern', }; type ViewMode = 'list' | 'add' | 'edit'; @@ -23,7 +33,7 @@ export function AddressBookModule() { const [viewMode, setViewMode] = useState('list'); const [editingContact, setEditingContact] = useState(null); - const handleSubmit = async (data: Omit) => { + const handleSubmit = async (data: Omit) => { if (viewMode === 'edit' && editingContact) { await updateContact(editingContact.id, data); } else { @@ -36,9 +46,9 @@ export function AddressBookModule() { return (
{/* Stats */} -
+

Total

{allContacts.length}

- {(Object.keys(TYPE_LABELS) as ContactType[]).slice(0, 3).map((type) => ( + {(Object.keys(TYPE_LABELS) as ContactType[]).slice(0, 4).map((type) => (

{TYPE_LABELS[type]}

{allContacts.filter((c) => c.type === type).length}

@@ -74,42 +84,12 @@ export function AddressBookModule() { ) : (
{contacts.map((contact) => ( - - -
- - -
-
-
-

{contact.name}

-
- {contact.company &&

{contact.company}

} - {TYPE_LABELS[contact.type]} -
-
- {contact.email && ( -
- {contact.email} -
- )} - {contact.phone && ( -
- {contact.phone} -
- )} - {contact.address && ( -
- {contact.address} -
- )} -
-
-
+ { setEditingContact(contact); setViewMode('edit'); }} + onDelete={() => removeContact(contact.id)} + /> ))}
)} @@ -120,7 +100,11 @@ export function AddressBookModule() { {viewMode === 'edit' ? 'Editare contact' : 'Contact nou'} - { setViewMode('list'); setEditingContact(null); }} /> + { setViewMode('list'); setEditingContact(null); }} + /> )} @@ -128,37 +112,240 @@ export function AddressBookModule() { ); } +// ── Contact Card ── + +function ContactCard({ contact, onEdit, onDelete }: { + contact: AddressContact; + onEdit: () => void; + onDelete: () => void; +}) { + return ( + + +
+ + +
+
+
+

{contact.name}

+
+ {contact.company &&

{contact.company}

} + {TYPE_LABELS[contact.type]} + {contact.department && ( + {contact.department} + )} +
+ {contact.role && ( +

{contact.role}

+ )} +
+ {contact.email && ( +
+ {contact.email} +
+ )} + {contact.email2 && ( +
+ {contact.email2} +
+ )} + {contact.phone && ( +
+ {contact.phone} +
+ )} + {contact.phone2 && ( +
+ {contact.phone2} +
+ )} + {contact.address && ( +
+ {contact.address} +
+ )} + {contact.website && ( +
+ {contact.website} +
+ )} + {contact.contactPersons.length > 0 && ( +
+

+ Persoane de contact ({contact.contactPersons.length}) +

+ {contact.contactPersons.slice(0, 2).map((cp, i) => ( +

+ {cp.name}{cp.role ? ` — ${cp.role}` : ''} +

+ ))} + {contact.contactPersons.length > 2 && ( +

+ +{contact.contactPersons.length - 2} altele +

+ )} +
+ )} +
+
+
+ ); +} + +// ── Contact Form ── + function ContactForm({ initial, onSubmit, onCancel }: { initial?: AddressContact; - onSubmit: (data: Omit) => void; + onSubmit: (data: Omit) => void; onCancel: () => void; }) { + const { tags: projectTags } = useTags('project'); const [name, setName] = useState(initial?.name ?? ''); const [company, setCompany] = useState(initial?.company ?? ''); const [type, setType] = useState(initial?.type ?? 'client'); const [email, setEmail] = useState(initial?.email ?? ''); + const [email2, setEmail2] = useState(initial?.email2 ?? ''); const [phone, setPhone] = useState(initial?.phone ?? ''); + const [phone2, setPhone2] = useState(initial?.phone2 ?? ''); const [address, setAddress] = useState(initial?.address ?? ''); + const [department, setDepartment] = useState(initial?.department ?? ''); + const [role, setRole] = useState(initial?.role ?? ''); + const [website, setWebsite] = useState(initial?.website ?? ''); const [notes, setNotes] = useState(initial?.notes ?? ''); + const [projectIds, setProjectIds] = useState(initial?.projectIds ?? []); + const [contactPersons, setContactPersons] = useState( + initial?.contactPersons ?? [] + ); + + const addContactPerson = () => { + setContactPersons([...contactPersons, { name: '', role: '', email: '', phone: '' }]); + }; + + const updateContactPerson = (index: number, field: keyof ContactPerson, value: string) => { + setContactPersons(contactPersons.map((cp, i) => + i === index ? { ...cp, [field]: value } : cp + )); + }; + + const removeContactPerson = (index: number) => { + setContactPersons(contactPersons.filter((_, i) => i !== index)); + }; + + const toggleProject = (projectId: string) => { + setProjectIds((prev) => + prev.includes(projectId) ? prev.filter((id) => id !== projectId) : [...prev, projectId] + ); + }; return ( -
{ e.preventDefault(); onSubmit({ name, company, type, email, phone, address, notes, tags: initial?.tags ?? [], visibility: initial?.visibility ?? 'all' }); }} className="space-y-4"> -
-
setName(e.target.value)} className="mt-1" required />
-
setCompany(e.target.value)} className="mt-1" />
-
+ { + e.preventDefault(); + onSubmit({ + name, company, type, email, email2, phone, phone2, + address, department, role, website, notes, + projectIds, + contactPersons: contactPersons.filter((cp) => cp.name.trim()), + tags: initial?.tags ?? [], + visibility: initial?.visibility ?? 'all', + }); + }} + className="space-y-4" + > + {/* Row 1: Name + Company + Type */}
+
setName(e.target.value)} className="mt-1" required />
+
setCompany(e.target.value)} className="mt-1" />
-
setEmail(e.target.value)} className="mt-1" />
-
setPhone(e.target.value)} className="mt-1" />
+ + {/* Row 2: Department + Role + Website */} +
+
setDepartment(e.target.value)} className="mt-1" />
+
setRole(e.target.value)} className="mt-1" />
+
setWebsite(e.target.value)} className="mt-1" placeholder="https://" />
+
+ + {/* Row 3: Emails + Phones */} +
+
+
setEmail(e.target.value)} className="mt-1" />
+
setEmail2(e.target.value)} className="mt-1" />
+
+
+
setPhone(e.target.value)} className="mt-1" />
+
setPhone2(e.target.value)} className="mt-1" />
+
+
+ + {/* Address */}
setAddress(e.target.value)} className="mt-1" />
+ + {/* Project links */} + {projectTags.length > 0 && ( +
+ +
+ {projectTags.map((pt) => ( + + ))} +
+
+ )} + + {/* Contact Persons */} +
+
+ + +
+ {contactPersons.length > 0 && ( +
+ {contactPersons.map((cp, i) => ( +
+ updateContactPerson(i, 'name', e.target.value)} className="min-w-[150px] flex-1 text-sm" /> + updateContactPerson(i, 'role', e.target.value)} className="w-[140px] text-sm" /> + updateContactPerson(i, 'email', e.target.value)} className="w-[180px] text-sm" /> + updateContactPerson(i, 'phone', e.target.value)} className="w-[140px] text-sm" /> + +
+ ))} +
+ )} +
+ + {/* Notes */}