From 9d73697fb04686fb23a61fa326762f96c32c9a31 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Fri, 13 Mar 2026 21:07:06 +0200 Subject: [PATCH] feat: address book - require name OR company, add department to contact persons - Either name or company/organization is now required (not just name) - When only company is set, it shows as primary display name - Added department field to ContactPerson sub-entities - Department shown as badge in card and detail views - Updated vCard export to handle nameless contacts and department field - Sort contacts by name or company (whichever is set) Co-Authored-By: Claude Opus 4.6 --- .../components/address-book-module.tsx | 37 +++++++++++++++---- .../address-book/hooks/use-contacts.ts | 4 +- .../address-book/services/vcard-export.ts | 11 +++--- src/modules/address-book/types.ts | 1 + 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/modules/address-book/components/address-book-module.tsx b/src/modules/address-book/components/address-book-module.tsx index 809a8f6..9b049bc 100644 --- a/src/modules/address-book/components/address-book-module.tsx +++ b/src/modules/address-book/components/address-book-module.tsx @@ -286,9 +286,11 @@ function ContactCard({
-

{contact.name}

+

+ {contact.name || contact.company} +

- {contact.company && ( + {contact.company && contact.name && (

{contact.company}

@@ -352,6 +354,7 @@ function ContactCard({ {contact.contactPersons.slice(0, 2).map((cp, i) => (

{cp.name} + {cp.department ? ` · ${cp.department}` : ""} {cp.role ? ` — ${cp.role}` : ""}

))} @@ -404,7 +407,7 @@ function ContactDetailDialog({ - {contact.name} + {contact.name || contact.company} {getTypeLabel(contact.type)} @@ -494,6 +497,11 @@ function ContactDetailDialog({ className="flex flex-wrap items-center gap-2 text-sm" > {cp.name} + {cp.department && ( + + {cp.department} + + )} {cp.role && ( {cp.role} @@ -720,7 +728,7 @@ function ContactForm({ const addContactPerson = () => { setContactPersons([ ...contactPersons, - { name: "", role: "", email: "", phone: "" }, + { name: "", department: "", role: "", email: "", phone: "" }, ]); }; @@ -752,6 +760,7 @@ function ContactForm({
{ e.preventDefault(); + if (!name.trim() && !company.trim()) return; onSubmit({ name, company, @@ -774,22 +783,28 @@ function ContactForm({ className="space-y-4" > {/* Row 1: Name + Company + Type */} + {!name.trim() && !company.trim() && ( +

+ Completează cel puțin Numele sau Compania/Organizația. +

+ )}
- + setName(e.target.value)} className="mt-1" - required + required={!company.trim()} />
- + setCompany(e.target.value)} className="mt-1" + required={!name.trim()} />
@@ -934,6 +949,14 @@ function ContactForm({ } className="min-w-[150px] flex-1 text-sm" /> + + updateContactPerson(i, "department", e.target.value) + } + className="w-[140px] text-sm" + /> a.name.localeCompare(b.name)); + results.sort((a, b) => + (a.name || a.company).localeCompare(b.name || b.company, "ro"), + ); setContacts(results); setLoading(false); }, [storage]); diff --git a/src/modules/address-book/services/vcard-export.ts b/src/modules/address-book/services/vcard-export.ts index 7da07a8..07598cf 100644 --- a/src/modules/address-book/services/vcard-export.ts +++ b/src/modules/address-book/services/vcard-export.ts @@ -6,11 +6,12 @@ import type { AddressContact } from "../types"; export function downloadVCard(contact: AddressContact): void { const lines: string[] = ["BEGIN:VCARD", "VERSION:3.0"]; - // Full name - lines.push(`FN:${esc(contact.name)}`); + // Full name — use company as fallback if no personal name + const displayName = contact.name || contact.company || "Contact"; + lines.push(`FN:${esc(displayName)}`); // Structured name — try to split first/last (best-effort) - const nameParts = contact.name.trim().split(/\s+/); + const nameParts = displayName.trim().split(/\s+/); const last = nameParts.length > 1 ? (nameParts[nameParts.length - 1] ?? "") : ""; const first = @@ -64,7 +65,7 @@ export function downloadVCard(contact: AddressContact): void { const cpNote = contact.contactPersons .map( (cp) => - `${cp.name}${cp.role ? ` (${cp.role})` : ""}${cp.email ? ` <${cp.email}>` : ""}${cp.phone ? ` ${cp.phone}` : ""}`, + `${cp.name}${cp.department ? ` [${cp.department}]` : ""}${cp.role ? ` (${cp.role})` : ""}${cp.email ? ` <${cp.email}>` : ""}${cp.phone ? ` ${cp.phone}` : ""}`, ) .join("; "); lines.push(`NOTE:Persoane contact: ${esc(cpNote)}`); @@ -78,7 +79,7 @@ export function downloadVCard(contact: AddressContact): void { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; - a.download = `${contact.name + a.download = `${displayName .replace(/[^a-zA-Z0-9\s-]/g, "") .trim() .replace(/\s+/g, "_")}.vcf`; diff --git a/src/modules/address-book/types.ts b/src/modules/address-book/types.ts index 9d0fd4b..b8f2b29 100644 --- a/src/modules/address-book/types.ts +++ b/src/modules/address-book/types.ts @@ -11,6 +11,7 @@ export type ContactType = /** A contact person within an organization/entity */ export interface ContactPerson { name: string; + department: string; role: string; email: string; phone: string;