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 <noreply@anthropic.com>
This commit is contained in:
@@ -286,9 +286,11 @@ function ContactCard({
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">{contact.name}</p>
|
<p className="font-medium">
|
||||||
|
{contact.name || contact.company}
|
||||||
|
</p>
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
{contact.company && (
|
{contact.company && contact.name && (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{contact.company}
|
{contact.company}
|
||||||
</p>
|
</p>
|
||||||
@@ -352,6 +354,7 @@ function ContactCard({
|
|||||||
{contact.contactPersons.slice(0, 2).map((cp, i) => (
|
{contact.contactPersons.slice(0, 2).map((cp, i) => (
|
||||||
<p key={i} className="text-xs text-muted-foreground">
|
<p key={i} className="text-xs text-muted-foreground">
|
||||||
{cp.name}
|
{cp.name}
|
||||||
|
{cp.department ? ` · ${cp.department}` : ""}
|
||||||
{cp.role ? ` — ${cp.role}` : ""}
|
{cp.role ? ` — ${cp.role}` : ""}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
@@ -404,7 +407,7 @@ function ContactDetailDialog({
|
|||||||
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
|
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-3">
|
<DialogTitle className="flex items-center gap-3">
|
||||||
<span>{contact.name}</span>
|
<span>{contact.name || contact.company}</span>
|
||||||
<Badge variant="outline">{getTypeLabel(contact.type)}</Badge>
|
<Badge variant="outline">{getTypeLabel(contact.type)}</Badge>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -494,6 +497,11 @@ function ContactDetailDialog({
|
|||||||
className="flex flex-wrap items-center gap-2 text-sm"
|
className="flex flex-wrap items-center gap-2 text-sm"
|
||||||
>
|
>
|
||||||
<span className="font-medium">{cp.name}</span>
|
<span className="font-medium">{cp.name}</span>
|
||||||
|
{cp.department && (
|
||||||
|
<Badge variant="secondary" className="text-[10px]">
|
||||||
|
{cp.department}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
{cp.role && (
|
{cp.role && (
|
||||||
<span className="text-muted-foreground text-xs">
|
<span className="text-muted-foreground text-xs">
|
||||||
{cp.role}
|
{cp.role}
|
||||||
@@ -720,7 +728,7 @@ function ContactForm({
|
|||||||
const addContactPerson = () => {
|
const addContactPerson = () => {
|
||||||
setContactPersons([
|
setContactPersons([
|
||||||
...contactPersons,
|
...contactPersons,
|
||||||
{ name: "", role: "", email: "", phone: "" },
|
{ name: "", department: "", role: "", email: "", phone: "" },
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -752,6 +760,7 @@ function ContactForm({
|
|||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!name.trim() && !company.trim()) return;
|
||||||
onSubmit({
|
onSubmit({
|
||||||
name,
|
name,
|
||||||
company,
|
company,
|
||||||
@@ -774,22 +783,28 @@ function ContactForm({
|
|||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
{/* Row 1: Name + Company + Type */}
|
{/* Row 1: Name + Company + Type */}
|
||||||
|
{!name.trim() && !company.trim() && (
|
||||||
|
<p className="text-xs text-destructive">
|
||||||
|
Completează cel puțin Numele sau Compania/Organizația.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<div className="grid gap-4 sm:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-3">
|
||||||
<div>
|
<div>
|
||||||
<Label>Nume *</Label>
|
<Label>Nume {!company.trim() ? "*" : ""}</Label>
|
||||||
<Input
|
<Input
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
required
|
required={!company.trim()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>Companie/Organizație</Label>
|
<Label>Companie/Organizație {!name.trim() ? "*" : ""}</Label>
|
||||||
<Input
|
<Input
|
||||||
value={company}
|
value={company}
|
||||||
onChange={(e) => setCompany(e.target.value)}
|
onChange={(e) => setCompany(e.target.value)}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
|
required={!name.trim()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -934,6 +949,14 @@ function ContactForm({
|
|||||||
}
|
}
|
||||||
className="min-w-[150px] flex-1 text-sm"
|
className="min-w-[150px] flex-1 text-sm"
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Departament"
|
||||||
|
value={cp.department ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateContactPerson(i, "department", e.target.value)
|
||||||
|
}
|
||||||
|
className="w-[140px] text-sm"
|
||||||
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Funcție"
|
placeholder="Funcție"
|
||||||
value={cp.role}
|
value={cp.role}
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ export function useContacts() {
|
|||||||
results.push(value as AddressContact);
|
results.push(value as AddressContact);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results.sort((a, b) => a.name.localeCompare(b.name));
|
results.sort((a, b) =>
|
||||||
|
(a.name || a.company).localeCompare(b.name || b.company, "ro"),
|
||||||
|
);
|
||||||
setContacts(results);
|
setContacts(results);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [storage]);
|
}, [storage]);
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import type { AddressContact } from "../types";
|
|||||||
export function downloadVCard(contact: AddressContact): void {
|
export function downloadVCard(contact: AddressContact): void {
|
||||||
const lines: string[] = ["BEGIN:VCARD", "VERSION:3.0"];
|
const lines: string[] = ["BEGIN:VCARD", "VERSION:3.0"];
|
||||||
|
|
||||||
// Full name
|
// Full name — use company as fallback if no personal name
|
||||||
lines.push(`FN:${esc(contact.name)}`);
|
const displayName = contact.name || contact.company || "Contact";
|
||||||
|
lines.push(`FN:${esc(displayName)}`);
|
||||||
|
|
||||||
// Structured name — try to split first/last (best-effort)
|
// Structured name — try to split first/last (best-effort)
|
||||||
const nameParts = contact.name.trim().split(/\s+/);
|
const nameParts = displayName.trim().split(/\s+/);
|
||||||
const last =
|
const last =
|
||||||
nameParts.length > 1 ? (nameParts[nameParts.length - 1] ?? "") : "";
|
nameParts.length > 1 ? (nameParts[nameParts.length - 1] ?? "") : "";
|
||||||
const first =
|
const first =
|
||||||
@@ -64,7 +65,7 @@ export function downloadVCard(contact: AddressContact): void {
|
|||||||
const cpNote = contact.contactPersons
|
const cpNote = contact.contactPersons
|
||||||
.map(
|
.map(
|
||||||
(cp) =>
|
(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("; ");
|
.join("; ");
|
||||||
lines.push(`NOTE:Persoane contact: ${esc(cpNote)}`);
|
lines.push(`NOTE:Persoane contact: ${esc(cpNote)}`);
|
||||||
@@ -78,7 +79,7 @@ export function downloadVCard(contact: AddressContact): void {
|
|||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = `${contact.name
|
a.download = `${displayName
|
||||||
.replace(/[^a-zA-Z0-9\s-]/g, "")
|
.replace(/[^a-zA-Z0-9\s-]/g, "")
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/\s+/g, "_")}.vcf`;
|
.replace(/\s+/g, "_")}.vcf`;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type ContactType =
|
|||||||
/** A contact person within an organization/entity */
|
/** A contact person within an organization/entity */
|
||||||
export interface ContactPerson {
|
export interface ContactPerson {
|
||||||
name: string;
|
name: string;
|
||||||
|
department: string;
|
||||||
role: string;
|
role: string;
|
||||||
email: string;
|
email: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user