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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,19 +1,29 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
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 { Button } from '@/shared/components/ui/button';
|
||||||
import { Input } from '@/shared/components/ui/input';
|
import { Input } from '@/shared/components/ui/input';
|
||||||
import { Label } from '@/shared/components/ui/label';
|
import { Label } from '@/shared/components/ui/label';
|
||||||
import { Textarea } from '@/shared/components/ui/textarea';
|
import { Textarea } from '@/shared/components/ui/textarea';
|
||||||
import { Badge } from '@/shared/components/ui/badge';
|
import { Badge } from '@/shared/components/ui/badge';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
|
import {
|
||||||
import type { AddressContact, ContactType } from '../types';
|
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||||
|
} from '@/shared/components/ui/select';
|
||||||
|
import type { AddressContact, ContactType, ContactPerson } from '../types';
|
||||||
import { useContacts } from '../hooks/use-contacts';
|
import { useContacts } from '../hooks/use-contacts';
|
||||||
|
import { useTags } from '@/core/tagging';
|
||||||
|
|
||||||
const TYPE_LABELS: Record<ContactType, string> = {
|
const TYPE_LABELS: Record<ContactType, string> = {
|
||||||
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';
|
type ViewMode = 'list' | 'add' | 'edit';
|
||||||
@@ -23,7 +33,7 @@ export function AddressBookModule() {
|
|||||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||||
const [editingContact, setEditingContact] = useState<AddressContact | null>(null);
|
const [editingContact, setEditingContact] = useState<AddressContact | null>(null);
|
||||||
|
|
||||||
const handleSubmit = async (data: Omit<AddressContact, 'id' | 'createdAt'>) => {
|
const handleSubmit = async (data: Omit<AddressContact, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||||
if (viewMode === 'edit' && editingContact) {
|
if (viewMode === 'edit' && editingContact) {
|
||||||
await updateContact(editingContact.id, data);
|
await updateContact(editingContact.id, data);
|
||||||
} else {
|
} else {
|
||||||
@@ -36,9 +46,9 @@ export function AddressBookModule() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-5">
|
||||||
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Total</p><p className="text-2xl font-bold">{allContacts.length}</p></CardContent></Card>
|
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Total</p><p className="text-2xl font-bold">{allContacts.length}</p></CardContent></Card>
|
||||||
{(Object.keys(TYPE_LABELS) as ContactType[]).slice(0, 3).map((type) => (
|
{(Object.keys(TYPE_LABELS) as ContactType[]).slice(0, 4).map((type) => (
|
||||||
<Card key={type}><CardContent className="p-4">
|
<Card key={type}><CardContent className="p-4">
|
||||||
<p className="text-xs text-muted-foreground">{TYPE_LABELS[type]}</p>
|
<p className="text-xs text-muted-foreground">{TYPE_LABELS[type]}</p>
|
||||||
<p className="text-2xl font-bold">{allContacts.filter((c) => c.type === type).length}</p>
|
<p className="text-2xl font-bold">{allContacts.filter((c) => c.type === type).length}</p>
|
||||||
@@ -74,42 +84,12 @@ export function AddressBookModule() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{contacts.map((contact) => (
|
{contacts.map((contact) => (
|
||||||
<Card key={contact.id} className="group relative">
|
<ContactCard
|
||||||
<CardContent className="p-4">
|
key={contact.id}
|
||||||
<div className="absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
contact={contact}
|
||||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setEditingContact(contact); setViewMode('edit'); }}>
|
onEdit={() => { setEditingContact(contact); setViewMode('edit'); }}
|
||||||
<Pencil className="h-3.5 w-3.5" />
|
onDelete={() => removeContact(contact.id)}
|
||||||
</Button>
|
/>
|
||||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => removeContact(contact.id)}>
|
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">{contact.name}</p>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{contact.company && <p className="text-xs text-muted-foreground">{contact.company}</p>}
|
|
||||||
<Badge variant="outline" className="text-[10px]">{TYPE_LABELS[contact.type]}</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{contact.email && (
|
|
||||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
||||||
<Mail className="h-3 w-3" /><span>{contact.email}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{contact.phone && (
|
|
||||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
||||||
<Phone className="h-3 w-3" /><span>{contact.phone}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{contact.address && (
|
|
||||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
||||||
<MapPin className="h-3 w-3" /><span className="truncate">{contact.address}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -120,7 +100,11 @@ export function AddressBookModule() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader><CardTitle>{viewMode === 'edit' ? 'Editare contact' : 'Contact nou'}</CardTitle></CardHeader>
|
<CardHeader><CardTitle>{viewMode === 'edit' ? 'Editare contact' : 'Contact nou'}</CardTitle></CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<ContactForm initial={editingContact ?? undefined} onSubmit={handleSubmit} onCancel={() => { setViewMode('list'); setEditingContact(null); }} />
|
<ContactForm
|
||||||
|
initial={editingContact ?? undefined}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onCancel={() => { setViewMode('list'); setEditingContact(null); }}
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
@@ -128,37 +112,240 @@ export function AddressBookModule() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Contact Card ──
|
||||||
|
|
||||||
|
function ContactCard({ contact, onEdit, onDelete }: {
|
||||||
|
contact: AddressContact;
|
||||||
|
onEdit: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card className="group relative">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
|
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={onEdit}>
|
||||||
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={onDelete}>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{contact.name}</p>
|
||||||
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
|
{contact.company && <p className="text-xs text-muted-foreground">{contact.company}</p>}
|
||||||
|
<Badge variant="outline" className="text-[10px]">{TYPE_LABELS[contact.type]}</Badge>
|
||||||
|
{contact.department && (
|
||||||
|
<Badge variant="secondary" className="text-[10px]">{contact.department}</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{contact.role && (
|
||||||
|
<p className="text-xs text-muted-foreground italic">{contact.role}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{contact.email && (
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
|
<Mail className="h-3 w-3 shrink-0" /><span className="truncate">{contact.email}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{contact.email2 && (
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
|
<Mail className="h-3 w-3 shrink-0" /><span className="truncate">{contact.email2}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{contact.phone && (
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
|
<Phone className="h-3 w-3 shrink-0" /><span>{contact.phone}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{contact.phone2 && (
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
|
<Phone className="h-3 w-3 shrink-0" /><span>{contact.phone2}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{contact.address && (
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
|
<MapPin className="h-3 w-3 shrink-0" /><span className="truncate">{contact.address}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{contact.website && (
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
|
<Globe className="h-3 w-3 shrink-0" /><span className="truncate">{contact.website}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{contact.contactPersons.length > 0 && (
|
||||||
|
<div className="mt-1 border-t pt-1">
|
||||||
|
<p className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider mb-1">
|
||||||
|
Persoane de contact ({contact.contactPersons.length})
|
||||||
|
</p>
|
||||||
|
{contact.contactPersons.slice(0, 2).map((cp, i) => (
|
||||||
|
<p key={i} className="text-xs text-muted-foreground">
|
||||||
|
{cp.name}{cp.role ? ` — ${cp.role}` : ''}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
{contact.contactPersons.length > 2 && (
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
+{contact.contactPersons.length - 2} altele
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Contact Form ──
|
||||||
|
|
||||||
function ContactForm({ initial, onSubmit, onCancel }: {
|
function ContactForm({ initial, onSubmit, onCancel }: {
|
||||||
initial?: AddressContact;
|
initial?: AddressContact;
|
||||||
onSubmit: (data: Omit<AddressContact, 'id' | 'createdAt'>) => void;
|
onSubmit: (data: Omit<AddressContact, 'id' | 'createdAt' | 'updatedAt'>) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { tags: projectTags } = useTags('project');
|
||||||
const [name, setName] = useState(initial?.name ?? '');
|
const [name, setName] = useState(initial?.name ?? '');
|
||||||
const [company, setCompany] = useState(initial?.company ?? '');
|
const [company, setCompany] = useState(initial?.company ?? '');
|
||||||
const [type, setType] = useState<ContactType>(initial?.type ?? 'client');
|
const [type, setType] = useState<ContactType>(initial?.type ?? 'client');
|
||||||
const [email, setEmail] = useState(initial?.email ?? '');
|
const [email, setEmail] = useState(initial?.email ?? '');
|
||||||
|
const [email2, setEmail2] = useState(initial?.email2 ?? '');
|
||||||
const [phone, setPhone] = useState(initial?.phone ?? '');
|
const [phone, setPhone] = useState(initial?.phone ?? '');
|
||||||
|
const [phone2, setPhone2] = useState(initial?.phone2 ?? '');
|
||||||
const [address, setAddress] = useState(initial?.address ?? '');
|
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 [notes, setNotes] = useState(initial?.notes ?? '');
|
||||||
|
const [projectIds, setProjectIds] = useState<string[]>(initial?.projectIds ?? []);
|
||||||
|
const [contactPersons, setContactPersons] = useState<ContactPerson[]>(
|
||||||
|
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 (
|
return (
|
||||||
<form onSubmit={(e) => { e.preventDefault(); onSubmit({ name, company, type, email, phone, address, notes, tags: initial?.tags ?? [], visibility: initial?.visibility ?? 'all' }); }} className="space-y-4">
|
<form
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
onSubmit={(e) => {
|
||||||
<div><Label>Nume</Label><Input value={name} onChange={(e) => setName(e.target.value)} className="mt-1" required /></div>
|
e.preventDefault();
|
||||||
<div><Label>Companie/Organizație</Label><Input value={company} onChange={(e) => setCompany(e.target.value)} className="mt-1" /></div>
|
onSubmit({
|
||||||
</div>
|
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 */}
|
||||||
<div className="grid gap-4 sm:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-3">
|
||||||
|
<div><Label>Nume *</Label><Input value={name} onChange={(e) => setName(e.target.value)} className="mt-1" required /></div>
|
||||||
|
<div><Label>Companie/Organizație</Label><Input value={company} onChange={(e) => setCompany(e.target.value)} className="mt-1" /></div>
|
||||||
<div><Label>Tip</Label>
|
<div><Label>Tip</Label>
|
||||||
<Select value={type} onValueChange={(v) => setType(v as ContactType)}>
|
<Select value={type} onValueChange={(v) => setType(v as ContactType)}>
|
||||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
||||||
<SelectContent>{(Object.keys(TYPE_LABELS) as ContactType[]).map((t) => (<SelectItem key={t} value={t}>{TYPE_LABELS[t]}</SelectItem>))}</SelectContent>
|
<SelectContent>
|
||||||
|
{(Object.keys(TYPE_LABELS) as ContactType[]).map((t) => (
|
||||||
|
<SelectItem key={t} value={t}>{TYPE_LABELS[t]}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div><Label>Email</Label><Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} className="mt-1" /></div>
|
|
||||||
<div><Label>Telefon</Label><Input type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} className="mt-1" /></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Row 2: Department + Role + Website */}
|
||||||
|
<div className="grid gap-4 sm:grid-cols-3">
|
||||||
|
<div><Label>Departament</Label><Input value={department} onChange={(e) => setDepartment(e.target.value)} className="mt-1" /></div>
|
||||||
|
<div><Label>Funcție/Rol</Label><Input value={role} onChange={(e) => setRole(e.target.value)} className="mt-1" /></div>
|
||||||
|
<div><Label>Website</Label><Input type="url" value={website} onChange={(e) => setWebsite(e.target.value)} className="mt-1" placeholder="https://" /></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row 3: Emails + Phones */}
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<div><Label>Email principal</Label><Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} className="mt-1" /></div>
|
||||||
|
<div><Label>Email secundar</Label><Input type="email" value={email2} onChange={(e) => setEmail2(e.target.value)} className="mt-1" /></div>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<div><Label>Telefon principal</Label><Input type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} className="mt-1" /></div>
|
||||||
|
<div><Label>Telefon secundar</Label><Input type="tel" value={phone2} onChange={(e) => setPhone2(e.target.value)} className="mt-1" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Address */}
|
||||||
<div><Label>Adresă</Label><Input value={address} onChange={(e) => setAddress(e.target.value)} className="mt-1" /></div>
|
<div><Label>Adresă</Label><Input value={address} onChange={(e) => setAddress(e.target.value)} className="mt-1" /></div>
|
||||||
|
|
||||||
|
{/* Project links */}
|
||||||
|
{projectTags.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<Label>Proiecte asociate</Label>
|
||||||
|
<div className="mt-1.5 flex flex-wrap gap-1.5">
|
||||||
|
{projectTags.map((pt) => (
|
||||||
|
<button
|
||||||
|
key={pt.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleProject(pt.id)}
|
||||||
|
className={`rounded-full border px-2.5 py-0.5 text-xs transition-colors ${
|
||||||
|
projectIds.includes(pt.id)
|
||||||
|
? 'border-primary bg-primary/10 text-primary'
|
||||||
|
: 'border-muted-foreground/30 text-muted-foreground hover:border-primary/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{pt.projectCode ? `${pt.projectCode} ` : ''}{pt.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Contact Persons */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label>Persoane de contact</Label>
|
||||||
|
<Button type="button" variant="outline" size="sm" onClick={addContactPerson}>
|
||||||
|
<UserPlus className="mr-1 h-3.5 w-3.5" /> Adaugă persoană
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{contactPersons.length > 0 && (
|
||||||
|
<div className="mt-2 space-y-2">
|
||||||
|
{contactPersons.map((cp, i) => (
|
||||||
|
<div key={i} className="flex flex-wrap items-start gap-2 rounded border p-2">
|
||||||
|
<Input placeholder="Nume" value={cp.name} onChange={(e) => updateContactPerson(i, 'name', e.target.value)} className="min-w-[150px] flex-1 text-sm" />
|
||||||
|
<Input placeholder="Funcție" value={cp.role} onChange={(e) => updateContactPerson(i, 'role', e.target.value)} className="w-[140px] text-sm" />
|
||||||
|
<Input placeholder="Email" value={cp.email} onChange={(e) => updateContactPerson(i, 'email', e.target.value)} className="w-[180px] text-sm" />
|
||||||
|
<Input placeholder="Telefon" value={cp.phone} onChange={(e) => updateContactPerson(i, 'phone', e.target.value)} className="w-[140px] text-sm" />
|
||||||
|
<Button type="button" variant="ghost" size="icon" className="h-8 w-8 shrink-0 text-destructive" onClick={() => removeContactPerson(i)}>
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Notes */}
|
||||||
<div><Label>Note</Label><Textarea value={notes} onChange={(e) => setNotes(e.target.value)} rows={2} className="mt-1" /></div>
|
<div><Label>Note</Label><Textarea value={notes} onChange={(e) => setNotes(e.target.value)} rows={2} className="mt-1" /></div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
<Button type="button" variant="outline" onClick={onCancel}>Anulează</Button>
|
<Button type="button" variant="outline" onClick={onCancel}>Anulează</Button>
|
||||||
<Button type="submit">{initial ? 'Actualizează' : 'Adaugă'}</Button>
|
<Button type="submit">{initial ? 'Actualizează' : 'Adaugă'}</Button>
|
||||||
|
|||||||
@@ -36,8 +36,9 @@ export function useContacts() {
|
|||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
useEffect(() => { refresh(); }, [refresh]);
|
useEffect(() => { refresh(); }, [refresh]);
|
||||||
|
|
||||||
const addContact = useCallback(async (data: Omit<AddressContact, 'id' | 'createdAt'>) => {
|
const addContact = useCallback(async (data: Omit<AddressContact, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||||
const contact: AddressContact = { ...data, id: uuid(), createdAt: new Date().toISOString() };
|
const now = new Date().toISOString();
|
||||||
|
const contact: AddressContact = { ...data, id: uuid(), createdAt: now, updatedAt: now };
|
||||||
await storage.set(`${PREFIX}${contact.id}`, contact);
|
await storage.set(`${PREFIX}${contact.id}`, contact);
|
||||||
await refresh();
|
await refresh();
|
||||||
return contact;
|
return contact;
|
||||||
@@ -46,7 +47,13 @@ export function useContacts() {
|
|||||||
const updateContact = useCallback(async (id: string, updates: Partial<AddressContact>) => {
|
const updateContact = useCallback(async (id: string, updates: Partial<AddressContact>) => {
|
||||||
const existing = contacts.find((c) => c.id === id);
|
const existing = contacts.find((c) => c.id === id);
|
||||||
if (!existing) return;
|
if (!existing) return;
|
||||||
const updated = { ...existing, ...updates, id: existing.id, createdAt: existing.createdAt };
|
const updated: AddressContact = {
|
||||||
|
...existing,
|
||||||
|
...updates,
|
||||||
|
id: existing.id,
|
||||||
|
createdAt: existing.createdAt,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
await storage.set(`${PREFIX}${id}`, updated);
|
await storage.set(`${PREFIX}${id}`, updated);
|
||||||
await refresh();
|
await refresh();
|
||||||
}, [storage, refresh, contacts]);
|
}, [storage, refresh, contacts]);
|
||||||
@@ -64,7 +71,14 @@ export function useContacts() {
|
|||||||
if (filters.type !== 'all' && c.type !== filters.type) return false;
|
if (filters.type !== 'all' && c.type !== filters.type) return false;
|
||||||
if (filters.search) {
|
if (filters.search) {
|
||||||
const q = filters.search.toLowerCase();
|
const q = filters.search.toLowerCase();
|
||||||
return c.name.toLowerCase().includes(q) || c.company.toLowerCase().includes(q) || c.email.toLowerCase().includes(q) || c.phone.includes(q);
|
return (
|
||||||
|
c.name.toLowerCase().includes(q) ||
|
||||||
|
c.company.toLowerCase().includes(q) ||
|
||||||
|
c.email.toLowerCase().includes(q) ||
|
||||||
|
c.phone.includes(q) ||
|
||||||
|
c.department.toLowerCase().includes(q) ||
|
||||||
|
c.role.toLowerCase().includes(q)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,17 +1,44 @@
|
|||||||
import type { Visibility } from '@/core/module-registry/types';
|
import type { Visibility } from '@/core/module-registry/types';
|
||||||
|
|
||||||
export type ContactType = 'client' | 'supplier' | 'institution' | 'collaborator';
|
export type ContactType = 'client' | 'supplier' | 'institution' | 'collaborator' | 'internal';
|
||||||
|
|
||||||
|
/** A contact person within an organization/entity */
|
||||||
|
export interface ContactPerson {
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AddressContact {
|
export interface AddressContact {
|
||||||
id: string;
|
id: string;
|
||||||
|
/** Primary name (person or organization) */
|
||||||
name: string;
|
name: string;
|
||||||
|
/** Organization/company name */
|
||||||
company: string;
|
company: string;
|
||||||
type: ContactType;
|
type: ContactType;
|
||||||
|
/** Primary email */
|
||||||
email: string;
|
email: string;
|
||||||
|
/** Secondary email */
|
||||||
|
email2: string;
|
||||||
|
/** Primary phone */
|
||||||
phone: string;
|
phone: string;
|
||||||
|
/** Secondary phone */
|
||||||
|
phone2: string;
|
||||||
address: string;
|
address: string;
|
||||||
|
/** Department within the organization */
|
||||||
|
department: string;
|
||||||
|
/** Role / job title */
|
||||||
|
role: string;
|
||||||
|
/** Website URL */
|
||||||
|
website: string;
|
||||||
|
/** Linked project tag IDs */
|
||||||
|
projectIds: string[];
|
||||||
|
/** Additional contact persons for this entity */
|
||||||
|
contactPersons: ContactPerson[];
|
||||||
tags: string[];
|
tags: string[];
|
||||||
notes: string;
|
notes: string;
|
||||||
visibility: Visibility;
|
visibility: Visibility;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user