diff --git a/src/modules/registratura/components/registratura-module.tsx b/src/modules/registratura/components/registratura-module.tsx index 170837b..9a96bc6 100644 --- a/src/modules/registratura/components/registratura-module.tsx +++ b/src/modules/registratura/components/registratura-module.tsx @@ -5,10 +5,14 @@ import { Plus } from 'lucide-react'; import { Button } from '@/shared/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card'; import { Badge } from '@/shared/components/ui/badge'; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, +} from '@/shared/components/ui/dialog'; import { useRegistry } from '../hooks/use-registry'; import { RegistryFilters } from './registry-filters'; import { RegistryTable } from './registry-table'; import { RegistryEntryForm } from './registry-entry-form'; +import { getOverdueDays } from '../services/registry-service'; import type { RegistryEntry } from '../types'; type ViewMode = 'list' | 'add' | 'edit'; @@ -16,11 +20,12 @@ type ViewMode = 'list' | 'add' | 'edit'; export function RegistraturaModule() { const { entries, allEntries, loading, filters, updateFilter, - addEntry, updateEntry, removeEntry, + addEntry, updateEntry, removeEntry, closeEntry, } = useRegistry(); const [viewMode, setViewMode] = useState('list'); const [editingEntry, setEditingEntry] = useState(null); + const [closingId, setClosingId] = useState(null); const handleAdd = async (data: Omit) => { await addEntry(data); @@ -43,6 +48,22 @@ export function RegistraturaModule() { await removeEntry(id); }; + const handleCloseRequest = (id: string) => { + const entry = allEntries.find((e) => e.id === id); + if (entry && entry.linkedEntryIds.length > 0) { + setClosingId(id); + } else { + closeEntry(id, false); + } + }; + + const handleCloseConfirm = (closeLinked: boolean) => { + if (closingId) { + closeEntry(closingId, closeLinked); + setClosingId(null); + } + }; + const handleCancel = () => { setViewMode('list'); setEditingEntry(null); @@ -50,18 +71,24 @@ export function RegistraturaModule() { // Stats const total = allEntries.length; - const incoming = allEntries.filter((e) => e.type === 'incoming').length; - const outgoing = allEntries.filter((e) => e.type === 'outgoing').length; - const inProgress = allEntries.filter((e) => e.status === 'in-progress').length; + const open = allEntries.filter((e) => e.status === 'deschis').length; + const overdue = allEntries.filter((e) => { + if (e.status !== 'deschis') return false; + const days = getOverdueDays(e.deadline); + return days !== null && days > 0; + }).length; + const intrat = allEntries.filter((e) => e.direction === 'intrat').length; + + const closingEntry = closingId ? allEntries.find((e) => e.id === closingId) : null; return (
{/* Stats */}
- - - + + 0 ? 'destructive' : undefined} /> +
{viewMode === 'list' && ( @@ -78,6 +105,7 @@ export function RegistraturaModule() { loading={loading} onEdit={handleEdit} onDelete={handleDelete} + onClose={handleCloseRequest} /> {!loading && ( @@ -97,7 +125,11 @@ export function RegistraturaModule() { - + )} @@ -108,20 +140,51 @@ export function RegistraturaModule() { Editare — {editingEntry.number} - + )} + + {/* Close confirmation dialog */} + { if (!open) setClosingId(null); }}> + + + Închide înregistrarea + +
+

+ Această înregistrare are {closingEntry?.linkedEntryIds.length ?? 0} înregistrări legate. + Vrei să le închizi și pe acestea? +

+
+ + + + + +
+
); } -function StatCard({ label, value }: { label: string; value: number }) { +function StatCard({ label, value, variant }: { label: string; value: number; variant?: 'destructive' }) { return (

{label}

-

{value}

+

0 ? 'text-destructive' : ''}`}> + {value} +

); diff --git a/src/modules/registratura/components/registry-entry-form.tsx b/src/modules/registratura/components/registry-entry-form.tsx index 5281056..a0412b0 100644 --- a/src/modules/registratura/components/registry-entry-form.tsx +++ b/src/modules/registratura/components/registry-entry-form.tsx @@ -1,40 +1,116 @@ 'use client'; -import { useState } from 'react'; +import { useState, useMemo, useRef } from 'react'; +import { Paperclip, X } from 'lucide-react'; import type { CompanyId } from '@/core/auth/types'; -import type { RegistryEntry, RegistryEntryType, RegistryEntryStatus } from '../types'; +import type { RegistryEntry, RegistryDirection, RegistryStatus, DocumentType, RegistryAttachment } from '../types'; import { Input } from '@/shared/components/ui/input'; import { Label } from '@/shared/components/ui/label'; import { Textarea } from '@/shared/components/ui/textarea'; import { Button } from '@/shared/components/ui/button'; +import { Badge } from '@/shared/components/ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select'; +import { useContacts } from '@/modules/address-book/hooks/use-contacts'; +import { v4 as uuid } from 'uuid'; interface RegistryEntryFormProps { initial?: RegistryEntry; + allEntries?: RegistryEntry[]; onSubmit: (data: Omit) => void; onCancel: () => void; } -export function RegistryEntryForm({ initial, onSubmit, onCancel }: RegistryEntryFormProps) { - const [type, setType] = useState(initial?.type ?? 'incoming'); +const DOC_TYPE_LABELS: Record = { + contract: 'Contract', + oferta: 'Ofertă', + factura: 'Factură', + scrisoare: 'Scrisoare', + aviz: 'Aviz', + 'nota-de-comanda': 'Notă de comandă', + raport: 'Raport', + cerere: 'Cerere', + altele: 'Altele', +}; + +export function RegistryEntryForm({ initial, allEntries, onSubmit, onCancel }: RegistryEntryFormProps) { + const { allContacts } = useContacts(); + const fileInputRef = useRef(null); + + const [direction, setDirection] = useState(initial?.direction ?? 'intrat'); + const [documentType, setDocumentType] = useState(initial?.documentType ?? 'scrisoare'); const [subject, setSubject] = useState(initial?.subject ?? ''); const [date, setDate] = useState(initial?.date ?? new Date().toISOString().slice(0, 10)); const [sender, setSender] = useState(initial?.sender ?? ''); + const [senderContactId, setSenderContactId] = useState(initial?.senderContactId ?? ''); const [recipient, setRecipient] = useState(initial?.recipient ?? ''); + const [recipientContactId, setRecipientContactId] = useState(initial?.recipientContactId ?? ''); const [company, setCompany] = useState(initial?.company ?? 'beletage'); - const [status, setStatus] = useState(initial?.status ?? 'registered'); + const [status, setStatus] = useState(initial?.status ?? 'deschis'); + const [deadline, setDeadline] = useState(initial?.deadline ?? ''); const [notes, setNotes] = useState(initial?.notes ?? ''); + const [linkedEntryIds, setLinkedEntryIds] = useState(initial?.linkedEntryIds ?? []); + const [attachments, setAttachments] = useState(initial?.attachments ?? []); + + // ── Sender/Recipient autocomplete suggestions ── + const [senderFocused, setSenderFocused] = useState(false); + const [recipientFocused, setRecipientFocused] = useState(false); + + const senderSuggestions = useMemo(() => { + if (!sender || sender.length < 2) return []; + const q = sender.toLowerCase(); + return allContacts.filter((c) => c.name.toLowerCase().includes(q) || c.company.toLowerCase().includes(q)).slice(0, 5); + }, [allContacts, sender]); + + const recipientSuggestions = useMemo(() => { + if (!recipient || recipient.length < 2) return []; + const q = recipient.toLowerCase(); + return allContacts.filter((c) => c.name.toLowerCase().includes(q) || c.company.toLowerCase().includes(q)).slice(0, 5); + }, [allContacts, recipient]); + + const handleFileUpload = (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files) return; + for (const file of Array.from(files)) { + const reader = new FileReader(); + reader.onload = () => { + const base64 = reader.result as string; + setAttachments((prev) => [ + ...prev, + { + id: uuid(), + name: file.name, + data: base64, + type: file.type, + size: file.size, + addedAt: new Date().toISOString(), + }, + ]); + }; + reader.readAsDataURL(file); + } + if (fileInputRef.current) fileInputRef.current.value = ''; + }; + + const removeAttachment = (id: string) => { + setAttachments((prev) => prev.filter((a) => a.id !== id)); + }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSubmit({ - type, + direction, + documentType, subject, date, sender, + senderContactId: senderContactId || undefined, recipient, + recipientContactId: recipientContactId || undefined, company, status, + deadline: deadline || undefined, + linkedEntryIds, + attachments, notes, tags: initial?.tags ?? [], visibility: initial?.visibility ?? 'all', @@ -43,15 +119,26 @@ export function RegistryEntryForm({ initial, onSubmit, onCancel }: RegistryEntry return (
-
+ {/* Row 1: Direction + Document type + Date */} +
- - setDirection(v as RegistryDirection)}> - Intrare - Ieșire - Intern + Intrat + Ieșit + + +
+
+ +
@@ -61,23 +148,78 @@ export function RegistryEntryForm({ initial, onSubmit, onCancel }: RegistryEntry
+ {/* Subject */}
- + setSubject(e.target.value)} className="mt-1" required />
+ {/* Sender / Recipient with autocomplete */}
-
+
- setSender(e.target.value)} className="mt-1" /> + { setSender(e.target.value); setSenderContactId(''); }} + onFocus={() => setSenderFocused(true)} + onBlur={() => setTimeout(() => setSenderFocused(false), 200)} + className="mt-1" + placeholder="Nume sau companie..." + /> + {senderFocused && senderSuggestions.length > 0 && ( +
+ {senderSuggestions.map((c) => ( + + ))} +
+ )}
-
+
- setRecipient(e.target.value)} className="mt-1" /> + { setRecipient(e.target.value); setRecipientContactId(''); }} + onFocus={() => setRecipientFocused(true)} + onBlur={() => setTimeout(() => setRecipientFocused(false), 200)} + className="mt-1" + placeholder="Nume sau companie..." + /> + {recipientFocused && recipientSuggestions.length > 0 && ( +
+ {recipientSuggestions.map((c) => ( + + ))} +
+ )}
-
+ {/* Company + Status + Deadline */} +
setStatus(v as RegistryEntryStatus)}> +
+
+ + setDeadline(e.target.value)} className="mt-1" /> +
+ {/* Linked entries */} + {allEntries && allEntries.length > 0 && ( +
+ +
+ {allEntries + .filter((e) => e.id !== initial?.id) + .slice(0, 20) + .map((e) => ( + + ))} +
+
+ )} + + {/* Attachments */} +
+
+ + + +
+ {attachments.length > 0 && ( +
+ {attachments.map((att) => ( +
+ + {att.name} + + {(att.size / 1024).toFixed(0)} KB + + +
+ ))} +
+ )} +
+ + {/* Notes */}