Files
ArchiTools/src/modules/registratura/components/registry-entry-form.tsx
T
AI Assistant a2b9ff75b5 feat(registratura): restructure Autorizare deadlines — no tacit approval
- ac-verificare (5z lucr.) now auto-track, created automatically with
  any AC emitere type. Informational: authority notifies if incomplete.
- ac-emitere (30z cal.) now chains to ac-emitere-dupa-completari when
  interrupted — term recalculates from completion submission date.
- ac-emitere-urgenta (7z lucr.) and ac-emitere-anexe (15z cal.) kept.
- New: ac-prelungire-emitere (15z lucr.) — authority communicates
  decision on AC extension within 15 working days.
- Info box in DeadlineAddDialog for autorizare category explaining
  auto-tracked verification + interruption mechanism.
- None of the autorizare deadlines have tacit approval.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 23:55:39 +02:00

1817 lines
66 KiB
TypeScript

"use client";
import { useState, useMemo, useRef, useCallback } from "react";
import {
Paperclip,
X,
Clock,
Plus,
UserPlus,
Info,
GitBranch,
Loader2,
AlertTriangle,
Calendar,
Globe,
ArrowDownToLine,
ArrowUpFromLine,
HardDrive,
FolderOpen,
Link2,
} from "lucide-react";
import type { CompanyId } from "@/core/auth/types";
import {
extractTemplates,
assembleSubject,
filterTemplates,
filterSubjects,
getRecommendedTemplates,
getSubjectPlaceholder,
createDynamicTemplate,
} from "../services/subject-template-service";
import type { SubjectTemplate } from "../services/subject-template-service";
import { SubjectTemplateInput } from "./subject-template-input";
import { SubjectAutocompleteDropdown } from "./subject-autocomplete-dropdown";
import type {
RegistryEntry,
RegistryDirection,
DocumentType,
RegistryAttachment,
TrackedDeadline,
DeadlineResolution,
ACValidityTracking,
} from "../types";
import { DEFAULT_DOC_TYPE_LABELS } from "../types";
import {
isNetworkPath,
toUncPath,
pathFileName,
shortDisplayPath,
shareLabelFor,
} from "@/config/nas-paths";
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 { Switch } from "@/shared/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/shared/components/ui/tooltip";
import { useContacts } from "@/modules/address-book/hooks/use-contacts";
import { useTags } from "@/core/tagging";
import type { AddressContact } from "@/modules/address-book/types";
import { v4 as uuid } from "uuid";
import { DeadlineCard } from "./deadline-card";
import { DeadlineAddDialog } from "./deadline-add-dialog";
import { DeadlineResolveDialog } from "./deadline-resolve-dialog";
import { QuickContactDialog } from "./quick-contact-dialog";
import { ThreadView } from "./thread-view";
import { ClosureBanner } from "./closure-banner";
import { ACValidityTracker } from "./ac-validity-tracker";
import { cn } from "@/shared/lib/utils";
import {
createTrackedDeadline,
resolveDeadline as resolveDeadlineFn,
} from "../services/deadline-service";
import { getDeadlineType } from "../services/deadline-catalog";
interface RegistryEntryFormProps {
initial?: RegistryEntry;
/** Pre-fill as reply to this entry — sets threadParentId, flips direction (used by "Inchide") */
replyTo?: RegistryEntry;
/** Pre-fill as related/conex entry — adds to linkedEntryIds, keeps direction (used by "Conex") */
conexTo?: RegistryEntry;
allEntries?: RegistryEntry[];
onSubmit: (
data: Omit<RegistryEntry, "id" | "number" | "createdAt" | "updatedAt">,
) => void | Promise<void>;
onCancel: () => void;
/** Callback to create a new Address Book contact */
onCreateContact?: (data: {
name: string;
phone: string;
email: string;
}) => Promise<AddressContact | undefined>;
/** Callback to create a new document type tag in Tag Manager */
onCreateDocType?: (label: string) => Promise<void>;
/** Navigate to an entry (for thread clicks) */
onNavigateEntry?: (entry: RegistryEntry) => void;
}
export function RegistryEntryForm({
initial,
replyTo,
conexTo,
allEntries,
onSubmit,
onCancel,
onCreateContact,
onCreateDocType,
onNavigateEntry,
}: RegistryEntryFormProps) {
const { allContacts, refresh: refreshContacts } = useContacts();
const { tags: docTypeTags } = useTags("document-type");
const { tags: projectTags } = useTags("project");
const fileInputRef = useRef<HTMLInputElement>(null);
// Track locally-added custom types that may not yet be in Tag Manager
const [localCustomTypes, setLocalCustomTypes] = useState<Map<string, string>>(
new Map(),
);
// ── Build dynamic doc type list from defaults + Tag Manager + local ──
const allDocTypes = useMemo(() => {
const map = new Map<string, string>();
// Add defaults
for (const [key, label] of Object.entries(DEFAULT_DOC_TYPE_LABELS)) {
map.set(key, label);
}
// Add from Tag Manager (document-type category)
for (const tag of docTypeTags) {
const key = tag.label.toLowerCase().replace(/\s+/g, "-");
if (!map.has(key)) {
map.set(key, tag.label);
}
}
// Add locally-created types (before Tag Manager syncs)
for (const [key, label] of localCustomTypes) {
if (!map.has(key)) {
map.set(key, label);
}
}
// Preserve initial doc type even if not in defaults or Tag Manager
// (e.g., custom type whose tag was deleted, or tag not yet loaded)
if (initial?.documentType && !map.has(initial.documentType)) {
const fallbackLabel =
initial.documentType.charAt(0).toUpperCase() +
initial.documentType.slice(1).replace(/-/g, " ");
map.set(initial.documentType, fallbackLabel);
}
// Sort alphabetically by label
const sorted = new Map(
[...map.entries()].sort((a, b) => a[1].localeCompare(b[1], "ro")),
);
return sorted;
}, [docTypeTags, localCustomTypes, initial?.documentType]);
// When replyTo is provided, flip direction (intrat→iesit, iesit→intrat)
const replyDirection: RegistryDirection | undefined = replyTo
? replyTo.direction === "intrat" ? "iesit" : "intrat"
: undefined;
const [direction, setDirection] = useState<RegistryDirection>(
initial?.direction ?? replyDirection ?? "intrat",
);
const defaultDocType = initial?.documentType ?? (direction === "intrat" ? "aviz" : "cerere");
const [documentType, setDocumentType] = useState<DocumentType>(defaultDocType);
const [customDocType, setCustomDocType] = useState("");
const [subject, setSubject] = useState(initial?.subject ?? "");
const [subjectQuery, setSubjectQuery] = useState(initial?.subject ?? "");
const [subjectFocused, setSubjectFocused] = useState(false);
const [activeTemplate, setActiveTemplate] = useState<SubjectTemplate | null>(null);
const [templateFieldValues, setTemplateFieldValues] = useState<Record<string, string>>({});
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<CompanyId>(
initial?.company ?? "beletage",
);
const [isClosed, setIsClosed] = useState(initial?.status === "inchis");
const [deadline, setDeadline] = useState(initial?.deadline ?? "");
const [assignee, setAssignee] = useState(initial?.assignee ?? "");
const [assigneeContactId, setAssigneeContactId] = useState(
initial?.assigneeContactId ?? "",
);
const [threadParentId, setThreadParentId] = useState(
initial?.threadParentId ?? replyTo?.id ?? "",
);
const [notes, setNotes] = useState(initial?.notes ?? "");
const [linkedEntryIds, setLinkedEntryIds] = useState<string[]>(
initial?.linkedEntryIds ?? (conexTo ? [conexTo.id] : []),
);
const [attachments, setAttachments] = useState<RegistryAttachment[]>(
initial?.attachments ?? [],
);
const [trackedDeadlines, setTrackedDeadlines] = useState<TrackedDeadline[]>(
initial?.trackedDeadlines ?? [],
);
const [linkedSearch, setLinkedSearch] = useState("");
const [threadSearch, setThreadSearch] = useState("");
// ── 3.03 new fields ──
const [recipientRegNumber, setRecipientRegNumber] = useState(
initial?.recipientRegNumber ?? "",
);
const [recipientRegDate, setRecipientRegDate] = useState(
initial?.recipientRegDate ?? "",
);
const [expiryDate, setExpiryDate] = useState(initial?.expiryDate ?? "");
const [expiryAlertDays, setExpiryAlertDays] = useState(
initial?.expiryAlertDays ?? 30,
);
const [externalStatusUrl, setExternalStatusUrl] = useState(
initial?.externalStatusUrl ?? "",
);
const [externalTrackingId, setExternalTrackingId] = useState(
initial?.externalTrackingId ?? "",
);
const [acValidity, setAcValidity] = useState<ACValidityTracking | undefined>(
initial?.acValidity,
);
// ── Submission lock + file upload tracking ──
const [isSubmitting, setIsSubmitting] = useState(false);
const [uploadingCount, setUploadingCount] = useState(0);
const isUploading = uploadingCount > 0;
// ── Deadline dialogs ──
const [deadlineAddOpen, setDeadlineAddOpen] = useState(false);
const [resolvingDeadline, setResolvingDeadline] =
useState<TrackedDeadline | null>(null);
// ── Quick contact creation ──
const [quickContactOpen, setQuickContactOpen] = useState(false);
const [quickContactField, setQuickContactField] = useState<
"sender" | "recipient" | "assignee"
>("sender");
const [quickContactName, setQuickContactName] = useState("");
const handleAddDeadline = (
typeId: string,
startDate: string,
options?: { isCJ?: boolean; isComisie?: boolean; chainParentId?: string },
) => {
const tracked = createTrackedDeadline(
typeId,
startDate,
options?.chainParentId,
);
if (tracked) {
const newDeadlines = [tracked];
// Auto-create verification deadline for CU emitere types
const isCUEmitere =
typeId === "cu-emitere-l50" || typeId === "cu-emitere-l350";
if (isCUEmitere) {
const verification = createTrackedDeadline(
"cu-verificare",
startDate,
);
if (verification) newDeadlines.push(verification);
}
// Auto-create CJ sub-deadlines if CJ toggle is on
if (isCUEmitere && options?.isCJ) {
const cjRequest = createTrackedDeadline(
"cu-cj-solicitare-aviz",
startDate,
);
if (cjRequest) {
newDeadlines.push(cjRequest);
// Chain: primar responds 5 days from when arhitect-sef requests (3 days from start)
const requestDue = new Date(startDate);
requestDue.setDate(requestDue.getDate() + 3);
const y = requestDue.getFullYear();
const m = String(requestDue.getMonth() + 1).padStart(2, "0");
const d = String(requestDue.getDate()).padStart(2, "0");
const cjResponse = createTrackedDeadline(
"cu-cj-aviz-primar",
`${y}-${m}-${d}`,
cjRequest.id,
);
if (cjResponse) newDeadlines.push(cjResponse);
}
}
// Auto-create verification deadline for AC emitere types
const isACEmitere =
typeId === "ac-emitere" ||
typeId === "ac-emitere-urgenta" ||
typeId === "ac-emitere-anexe";
if (isACEmitere) {
const acVerification = createTrackedDeadline(
"ac-verificare",
startDate,
);
if (acVerification) newDeadlines.push(acVerification);
}
// Auto-create completari limit for avize (when Comisie toggle is OFF)
const addedDef = getDeadlineType(typeId);
if (addedDef?.category === "avize" && !options?.isComisie) {
const completariLimit = createTrackedDeadline(
"aviz-completari-limit",
startDate,
);
if (completariLimit) newDeadlines.push(completariLimit);
}
// Auto-create Cultura Phase 1 (depunere la comisie) when adding cultura-comisie
if (typeId === "aviz-cultura-comisie") {
const culturaPhase1 = createTrackedDeadline(
"aviz-cultura-depunere-comisie",
startDate,
);
if (culturaPhase1) newDeadlines.push(culturaPhase1);
}
// Auto-create comunicare catre beneficiar for all iesit deadlines
// (legal obligation: institution must communicate on the day of issuance)
if (direction === "iesit") {
const comunicare = createTrackedDeadline(
"comunicare-aviz-beneficiar",
startDate,
);
if (comunicare) newDeadlines.push(comunicare);
}
setTrackedDeadlines((prev) => [...prev, ...newDeadlines]);
}
};
const handleResolveDeadline = (
resolution: DeadlineResolution,
note: string,
chainNext: boolean,
) => {
if (!resolvingDeadline) return;
const resolved = resolveDeadlineFn(resolvingDeadline, resolution, note);
setTrackedDeadlines((prev) =>
prev.map((d) => (d.id === resolved.id ? resolved : d)),
);
// Standard chain (completed / aprobat-tacit)
if (chainNext && resolution !== "intrerupt") {
const def = getDeadlineType(resolvingDeadline.typeId);
if (def?.chainNextTypeId) {
const resolvedDate = new Date().toISOString().slice(0, 10);
const chained = createTrackedDeadline(
def.chainNextTypeId,
resolvedDate,
resolvingDeadline.id,
);
if (chained) setTrackedDeadlines((prev) => [...prev, chained]);
}
}
// Interruption chain — institution requested completions, term interrupted
// Creates chained deadline with today as placeholder start date
// User must update start date when completions are actually submitted
if (resolution === "intrerupt") {
const def = getDeadlineType(resolvingDeadline.typeId);
if (def?.chainNextTypeId) {
const today = new Date().toISOString().slice(0, 10);
const chained = createTrackedDeadline(
def.chainNextTypeId,
today,
resolvingDeadline.id,
);
if (chained) {
chained.auditLog = [
...(chained.auditLog ?? []),
{
action: "modified",
timestamp: new Date().toISOString(),
detail:
"ATENTIE: Actualizati data start cand se depun completarile/clarificarile",
},
];
setTrackedDeadlines((prev) => [...prev, chained]);
}
}
}
setResolvingDeadline(null);
};
const handleRemoveDeadline = (deadlineId: string) => {
setTrackedDeadlines((prev) => prev.filter((d) => d.id !== deadlineId));
};
// ── Contact autocomplete ──
const [senderFocused, setSenderFocused] = useState(false);
const [recipientFocused, setRecipientFocused] = useState(false);
const [assigneeFocused, setAssigneeFocused] = useState(false);
const filterContacts = useCallback(
(query: string) => {
if (!query || query.length < 2) return [];
const q = query.toLowerCase();
return allContacts
.filter(
(c) =>
c.name.toLowerCase().includes(q) ||
c.company.toLowerCase().includes(q),
)
.slice(0, 5);
},
[allContacts],
);
const senderSuggestions = useMemo(
() => filterContacts(sender),
[filterContacts, sender],
);
const recipientSuggestions = useMemo(
() => filterContacts(recipient),
[filterContacts, recipient],
);
const assigneeSuggestions = useMemo(
() => filterContacts(assignee),
[filterContacts, assignee],
);
// ── Subject autocomplete ──
const allSubjects = useMemo(() => {
if (!allEntries) return [];
const unique = new Set<string>();
for (const e of allEntries) {
if (e.subject && !e.isReserved) unique.add(e.subject);
}
return Array.from(unique);
}, [allEntries]);
const allTemplates = useMemo(
() => extractTemplates(allSubjects),
[allSubjects],
);
const matchingTemplates = useMemo(
() =>
subjectQuery.length >= 2
? filterTemplates(allTemplates, subjectQuery).slice(0, 5)
: [],
[allTemplates, subjectQuery],
);
const matchingSuggestions = useMemo(
() =>
subjectQuery.length >= 2
? filterSubjects(allSubjects, subjectQuery, 5)
: [],
[allSubjects, subjectQuery],
);
// Recommended templates for current doc type (seeds + DB-learned)
const recommendedTemplates = useMemo(
() => getRecommendedTemplates(documentType, allTemplates),
[documentType, allTemplates],
);
// Recent subjects from DB (last 5 unique, sorted by newest first)
const recentSubjects = useMemo(() => {
if (!allEntries || allEntries.length === 0) return [];
const sorted = [...allEntries]
.filter((e) => e.subject && !e.isReserved)
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
const seen = new Set<string>();
const result: string[] = [];
for (const e of sorted) {
if (result.length >= 5) break;
const lower = e.subject.toLowerCase();
if (!seen.has(lower)) {
seen.add(lower);
result.push(e.subject);
}
}
return result;
}, [allEntries]);
// Dynamic placeholder based on document type + direction
const subjectPlaceholder = useMemo(
() => getSubjectPlaceholder(documentType, direction),
[documentType, direction],
);
// ── Quick contact creation handler ──
const openQuickContact = (
field: "sender" | "recipient" | "assignee",
name: string,
) => {
setQuickContactField(field);
setQuickContactName(name);
setQuickContactOpen(true);
};
const handleQuickContactConfirm = async (data: {
name: string;
phone: string;
email: string;
}) => {
if (!onCreateContact) return;
const contact = await onCreateContact(data);
if (contact) {
const displayName = contact.company
? `${contact.name} (${contact.company})`
: contact.name;
if (quickContactField === "sender") {
setSender(displayName);
setSenderContactId(contact.id);
} else if (quickContactField === "recipient") {
setRecipient(displayName);
setRecipientContactId(contact.id);
} else {
setAssignee(displayName);
setAssigneeContactId(contact.id);
}
await refreshContacts();
}
setQuickContactOpen(false);
};
// ── Custom doc type creation — adds to local state immediately + Tag Manager ──
const handleAddCustomDocType = async () => {
const label = customDocType.trim();
if (!label) return;
const key = label.toLowerCase().replace(/\s+/g, "-");
// Add to local types immediately so it appears in the select
setLocalCustomTypes((prev) => new Map(prev).set(key, label));
// Select the newly created type
setDocumentType(key);
setCustomDocType("");
if (onCreateDocType) {
await onCreateDocType(label);
}
};
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files) return;
const fileArr = Array.from(files);
setUploadingCount((prev) => prev + fileArr.length);
for (const file of fileArr) {
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(),
},
]);
setUploadingCount((prev) => Math.max(0, prev - 1));
};
reader.onerror = () => {
setUploadingCount((prev) => Math.max(0, prev - 1));
};
reader.readAsDataURL(file);
}
if (fileInputRef.current) fileInputRef.current.value = "";
};
const removeAttachment = (id: string) => {
setAttachments((prev) => prev.filter((a) => a.id !== id));
};
// Network path support
const [networkPathInput, setNetworkPathInput] = useState("");
const [showNetworkInput, setShowNetworkInput] = useState(false);
const handleAddNetworkPath = () => {
const raw = networkPathInput.trim();
if (!raw) return;
const unc = toUncPath(raw);
const fileName = pathFileName(raw);
setAttachments((prev) => [
...prev,
{
id: uuid(),
name: fileName,
data: "__network__",
type: "network/path",
size: 0,
addedAt: new Date().toISOString(),
networkPath: unc,
},
]);
setNetworkPathInput("");
setShowNetworkInput(false);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (isSubmitting || isUploading) return;
setIsSubmitting(true);
try {
await onSubmit({
direction,
documentType,
subject,
date,
sender,
senderContactId: senderContactId || undefined,
recipient,
recipientContactId: recipientContactId || undefined,
company,
status: isClosed ? "inchis" : "deschis",
deadline: deadline || undefined,
assignee: assignee || undefined,
assigneeContactId: assigneeContactId || undefined,
threadParentId: threadParentId || "",
recipientRegNumber: recipientRegNumber || undefined,
recipientRegDate: recipientRegDate || undefined,
expiryDate: expiryDate || undefined,
expiryAlertDays: expiryDate ? expiryAlertDays : undefined,
externalStatusUrl: externalStatusUrl || undefined,
externalTrackingId: externalTrackingId || undefined,
acValidity: acValidity,
linkedEntryIds,
attachments,
trackedDeadlines:
trackedDeadlines.length > 0 ? trackedDeadlines : undefined,
notes,
tags: initial?.tags ?? [],
visibility: initial?.visibility ?? "all",
});
} finally {
setIsSubmitting(false);
}
};
// ── Contact autocomplete dropdown renderer ──
const renderContactDropdown = (
suggestions: AddressContact[],
focused: boolean,
fieldName: "sender" | "recipient" | "assignee",
currentValue: string,
onSelect: (c: AddressContact) => void,
) => {
if (!focused) return null;
const hasExactMatch = suggestions.some(
(c) =>
c.name.toLowerCase() === currentValue.toLowerCase() ||
`${c.name} (${c.company})`.toLowerCase() === currentValue.toLowerCase(),
);
const showCreateButton =
currentValue.length >= 2 && !hasExactMatch && onCreateContact;
if (suggestions.length === 0 && !showCreateButton) return null;
return (
<div className="absolute z-10 mt-1 w-full rounded-md border bg-popover p-1 shadow-md">
{suggestions.map((c) => (
<button
key={c.id}
type="button"
className="w-full rounded px-2 py-1.5 text-left text-sm hover:bg-accent"
onMouseDown={() => onSelect(c)}
>
<span className="font-medium">{c.name}</span>
{c.company && (
<span className="ml-1 text-muted-foreground text-xs">
{c.company}
</span>
)}
</button>
))}
{showCreateButton && (
<button
type="button"
className="w-full rounded px-2 py-1.5 text-left text-sm hover:bg-accent text-primary flex items-center gap-1.5 border-t mt-1 pt-1.5"
onMouseDown={() => openQuickContact(fieldName, currentValue)}
>
<UserPlus className="h-3.5 w-3.5" />
Creează contact &quot;{currentValue}&quot;
</button>
)}
</div>
);
};
// Thread parent entry for display
const threadParent =
threadParentId && allEntries
? allEntries.find((e) => e.id === threadParentId)
: null;
return (
<form onSubmit={handleSubmit} className="space-y-4">
{/* Closure info banner (if editing a closed entry) */}
{initial?.closureInfo && (
<ClosureBanner
closureInfo={initial.closureInfo}
allEntries={allEntries}
onNavigateLinked={onNavigateEntry}
/>
)}
{/* Thread view (if editing an entry that's in a thread) */}
{initial &&
allEntries &&
(initial.threadParentId ||
allEntries.some((e) => e.threadParentId === initial.id)) && (
<ThreadView
entry={initial}
allEntries={allEntries}
onNavigate={onNavigateEntry}
/>
)}
{/* Row 1: Direction + Document type + Date */}
<div className="grid gap-4 sm:grid-cols-3">
<div>
<Label className="flex items-center gap-1.5">
Direcție
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p className="text-xs">
Intrat = document primit de la terți. Ieșit = document
emis de noi către terți.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<div className="mt-1 flex rounded-lg border bg-muted/30 p-1">
<button
type="button"
onClick={() => {
setDirection("intrat");
// Auto-switch doc type if user hasn't customized it
if (documentType === "cerere") setDocumentType("aviz");
}}
className={cn(
"flex flex-1 items-center justify-center gap-1.5 rounded-md px-3 py-2 text-sm font-medium transition-all",
direction === "intrat"
? "bg-blue-500 text-white shadow-sm"
: "text-muted-foreground hover:text-foreground hover:bg-background",
)}
>
<ArrowDownToLine className="h-4 w-4" />
Intrat
</button>
<button
type="button"
onClick={() => {
setDirection("iesit");
// Auto-switch doc type if user hasn't customized it
if (documentType === "aviz") setDocumentType("cerere");
}}
className={cn(
"flex flex-1 items-center justify-center gap-1.5 rounded-md px-3 py-2 text-sm font-medium transition-all",
direction === "iesit"
? "bg-orange-500 text-white shadow-sm"
: "text-muted-foreground hover:text-foreground hover:bg-background",
)}
>
<ArrowUpFromLine className="h-4 w-4" />
Ieșit
</button>
</div>
</div>
<div>
<Label className="flex items-center gap-1.5">
Tip document
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p className="text-xs">
Alege tipul care descrie cel mai bine documentul. Poți
adăuga tipuri noi cu câmpul de mai jos.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<div className="mt-1">
<Select
value={allDocTypes.has(documentType) ? documentType : "altele"}
onValueChange={(v) => setDocumentType(v as DocumentType)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{Array.from(allDocTypes.entries()).map(([key, label]) => (
<SelectItem key={key} value={key}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Add custom type inline */}
<div className="mt-1.5 flex gap-1">
<Input
value={customDocType}
onChange={(e) => setCustomDocType(e.target.value)}
placeholder="Tip nou..."
className="text-xs h-7"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleAddCustomDocType();
}
}}
/>
<Button
type="button"
variant="outline"
size="sm"
className="h-7 px-2"
onClick={handleAddCustomDocType}
disabled={!customDocType.trim()}
>
<Plus className="h-3 w-3" />
</Button>
</div>
</div>
<div>
<Label className="flex items-center gap-1.5">
Data document
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p className="text-xs">
Data reală de pe document. Poate fi în trecut dacă
înregistrezi retroactiv.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<Input
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
className="mt-1"
/>
{!initial && date !== new Date().toISOString().slice(0, 10) && (
<p className="text-[10px] text-amber-600 dark:text-amber-400 mt-0.5">
Data diferă de azi înregistrarea retroactivă va primi următorul
nr. secvențial.
</p>
)}
{/* Late receipt indicator for incoming admin acts */}
{direction === "intrat" &&
documentType === "aviz" &&
date &&
(() => {
const docDate = new Date(date);
const today = new Date();
docDate.setHours(0, 0, 0, 0);
today.setHours(0, 0, 0, 0);
const diffDays = Math.floor(
(today.getTime() - docDate.getTime()) / (1000 * 60 * 60 * 24),
);
if (diffDays > 0) {
return (
<p className="text-[10px] text-orange-600 dark:text-orange-400 mt-0.5 flex items-center gap-1">
<span>
Actul are data emiterii cu {diffDays}{" "}
{diffDays === 1 ? "zi" : "zile"} in urma primit cu
intarziere fata de obligatia de comunicare in ziua
emiterii (L350 art. 44).
</span>
</p>
);
}
return null;
})()}
</div>
</div>
{/* Subject with autocomplete + template mode */}
<div className="relative">
<Label className="flex items-center gap-1.5">
Subiect *
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
{direction === "intrat" ? (
<div className="text-xs space-y-1">
<p className="font-medium">Acte primite:</p>
<ul className="list-disc pl-3 space-y-0.5">
<li>CU nr. 312/2026 Proiect X</li>
<li>AC nr. 89/2026 Proiect X</li>
<li>Aviz ISU nr. 55/2026 Proiect X</li>
<li>Contract nr. 15/2026 Proiect X</li>
</ul>
<p className="text-muted-foreground">Selecteaza un sablon sau tasteaza liber.</p>
<p className="text-muted-foreground mt-1">Tip: scrie <code className="font-mono text-amber-300 bg-white/10 px-1 rounded">{"{proiect}"}</code> pentru dropdown de proiecte.</p>
</div>
) : (
<div className="text-xs space-y-1">
<p className="font-medium">Documente trimise:</p>
<ul className="list-disc pl-3 space-y-0.5">
<li>Cerere CU nr. 123/2026 Proiect X</li>
<li>Solicitare aviz DSP Proiect X</li>
<li>Oferta proiectare DTAC Proiect X</li>
<li>Notificare incepere lucrari Proiect X</li>
</ul>
<p className="text-muted-foreground">Selecteaza un sablon sau tasteaza liber.</p>
<p className="text-muted-foreground mt-1">Tip: scrie <code className="font-mono text-amber-300 bg-white/10 px-1 rounded">{"{proiect}"}</code> pentru dropdown de proiecte.</p>
</div>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
{activeTemplate ? (
<SubjectTemplateInput
template={activeTemplate}
fieldValues={templateFieldValues}
onFieldChange={(fieldId, value) => {
const next = { ...templateFieldValues, [fieldId]: value };
setTemplateFieldValues(next);
setSubject(assembleSubject(activeTemplate, next));
}}
onClear={() => {
setActiveTemplate(null);
setSubjectQuery(subject);
}}
projectTags={projectTags}
/>
) : (
<Input
value={subjectQuery}
onChange={(e) => {
const val = e.target.value;
setSubjectQuery(val);
setSubject(val);
// Auto-detect {proiect}, {nr}, {an}, {detalii}, {text} placeholders
// and switch to template mode when a closing } is typed
if (val.includes("{") && val.includes("}")) {
const dynTemplate = createDynamicTemplate(val);
if (dynTemplate) {
// Pre-fill default values (e.g. current year for {an})
const prefill: Record<string, string> = {};
for (const field of dynTemplate.fields) {
if (field.defaultValue) {
prefill[field.id] = field.defaultValue;
}
}
setActiveTemplate(dynTemplate);
setTemplateFieldValues(prefill);
setSubject(assembleSubject(dynTemplate, prefill));
setSubjectFocused(false);
return;
}
}
}}
onFocus={() => setSubjectFocused(true)}
onBlur={() => setTimeout(() => setSubjectFocused(false), 200)}
className="mt-1"
required
placeholder={subjectPlaceholder}
/>
)}
<SubjectAutocompleteDropdown
templates={matchingTemplates}
suggestions={matchingSuggestions}
recommendedTemplates={recommendedTemplates}
recentSubjects={recentSubjects}
visible={
subjectFocused &&
!activeTemplate
}
onSelectTemplate={(t) => {
// Pre-fill year fields with current year
const prefill: Record<string, string> = {};
for (const field of t.fields) {
if (field.defaultValue) {
prefill[field.id] = field.defaultValue;
}
}
setActiveTemplate(t);
setTemplateFieldValues(prefill);
setSubject(assembleSubject(t, prefill));
setSubjectFocused(false);
}}
onSelectSuggestion={(s) => {
setSubject(s);
setSubjectQuery(s);
setSubjectFocused(false);
}}
/>
</div>
{/* Sender / Recipient with autocomplete + quick create */}
<div className="grid gap-4 sm:grid-cols-2">
{/* Sender */}
<div className="relative">
<Label className="flex items-center gap-1.5">
Expeditor
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p className="text-xs">
Cine a trimis documentul. Caută în Agenda de contacte sau
creează rapid un contact nou.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<Input
value={sender}
onChange={(e) => {
setSender(e.target.value);
setSenderContactId("");
}}
onFocus={() => setSenderFocused(true)}
onBlur={() => setTimeout(() => setSenderFocused(false), 200)}
className="mt-1"
placeholder="Nume sau companie..."
/>
{renderContactDropdown(
senderSuggestions,
senderFocused,
"sender",
sender,
(c) => {
setSender(c.company ? `${c.name} (${c.company})` : c.name);
setSenderContactId(c.id);
setSenderFocused(false);
},
)}
</div>
{/* Recipient */}
<div className="relative">
<Label className="flex items-center gap-1.5">
Destinatar
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p className="text-xs">
Cui trimitem documentul. Caută în Agenda de contacte sau
creează rapid un contact nou.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<Input
value={recipient}
onChange={(e) => {
setRecipient(e.target.value);
setRecipientContactId("");
}}
onFocus={() => setRecipientFocused(true)}
onBlur={() => setTimeout(() => setRecipientFocused(false), 200)}
className="mt-1"
placeholder="Nume sau companie..."
/>
{renderContactDropdown(
recipientSuggestions,
recipientFocused,
"recipient",
recipient,
(c) => {
setRecipient(c.company ? `${c.name} (${c.company})` : c.name);
setRecipientContactId(c.id);
setRecipientFocused(false);
},
)}
</div>
</div>
{/* Recipient registration fields — only for outgoing (iesit) documents */}
{direction === "iesit" && (
<div className="rounded-md border border-blue-500/30 bg-blue-500/5 p-3 space-y-3">
<div className="flex items-center gap-2">
<AlertTriangle className="h-4 w-4 text-blue-600 dark:text-blue-400" />
<Label className="text-sm font-medium text-blue-700 dark:text-blue-300">
Înregistrare la destinatar
</Label>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p className="text-xs">
Termenul legal pentru ieșiri curge DOAR de la data
înregistrării la destinatar, nu de la data trimiterii.
Completează aceste câmpuri când primești confirmarea.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div>
<Label className="text-xs">Nr. înregistrare destinatar</Label>
<Input
value={recipientRegNumber}
onChange={(e) => setRecipientRegNumber(e.target.value)}
className="mt-1"
placeholder="Ex: 12345/2026"
/>
</div>
<div>
<Label className="text-xs">Data înregistrare destinatar</Label>
<Input
type="date"
value={recipientRegDate}
onChange={(e) => setRecipientRegDate(e.target.value)}
className="mt-1"
/>
</div>
</div>
{!recipientRegNumber && !recipientRegDate && (
<p className="text-[10px] text-amber-600 dark:text-amber-400">
Atenție: Datele de la destinatar nu sunt completate. Termenele
legale atașate nu vor porni până la completarea lor.
</p>
)}
</div>
)}
{/* Assignee (Responsabil) — kept below */}
<div className="relative">
<Label className="flex items-center gap-1.5">
Responsabil
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p className="text-xs">
Persoana din echipă responsabilă cu gestionarea acestei
înregistrări. Câmp pregătit pentru integrare viitoare cu ERP.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<Input
value={assignee}
onChange={(e) => {
setAssignee(e.target.value);
setAssigneeContactId("");
}}
onFocus={() => setAssigneeFocused(true)}
onBlur={() => setTimeout(() => setAssigneeFocused(false), 200)}
className="mt-1"
placeholder="Persoana responsabilă..."
/>
{renderContactDropdown(
assigneeSuggestions,
assigneeFocused,
"assignee",
assignee,
(c) => {
setAssignee(c.company ? `${c.name} (${c.company})` : c.name);
setAssigneeContactId(c.id);
setAssigneeFocused(false);
},
)}
</div>
{/* Company + Closed switch + Deadline */}
<div className="grid gap-4 sm:grid-cols-3">
<div>
<Label>Companie</Label>
<Select
value={company}
onValueChange={(v) => setCompany(v as CompanyId)}
>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="beletage">Beletage</SelectItem>
<SelectItem value="urban-switch">Urban Switch</SelectItem>
<SelectItem value="studii-de-teren">Studii de Teren</SelectItem>
<SelectItem value="group">Grup</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label className="flex items-center gap-1.5">
Status
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p className="text-xs">
Implicit, toate înregistrările sunt deschise. Bifează doar
când dosarul este finalizat.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<div className="mt-2.5 flex items-center gap-2">
<Switch checked={isClosed} onCheckedChange={setIsClosed} />
<span className="text-sm text-muted-foreground">
{isClosed ? "Închis" : "Deschis"}
</span>
</div>
</div>
<div>
<Label className="flex items-center gap-1.5">
Termen limită intern
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p className="text-xs">
Termen intern pentru a răspunde sau a acționa pe această
înregistrare. Nu este termen legal termenele legale se
adaugă separat mai jos.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<Input
type="date"
value={deadline}
onChange={(e) => setDeadline(e.target.value)}
className="mt-1"
/>
</div>
</div>
{/* Document Expiry — CU/AC validity tracking (only for act administrativ) */}
{documentType === "aviz" && (
<>
<div className="rounded-md border border-muted p-3 space-y-3">
<Label className="flex items-center gap-1.5 text-sm font-medium">
<Calendar className="h-3.5 w-3.5" />
Valabilitate act administrativ
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p className="text-xs">
Pentru acte administrative cu termen de valabilitate (CU,
AC, avize). Sistemul genereaza alerte inainte de expirare.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<div className="grid gap-3 sm:grid-cols-2">
<div>
<Label className="text-xs">Data expirare</Label>
<Input
type="date"
value={expiryDate}
onChange={(e) => setExpiryDate(e.target.value)}
className="mt-1"
/>
</div>
{expiryDate && (
<div>
<Label className="text-xs">Alertă cu X zile înainte</Label>
<Input
type="number"
value={expiryAlertDays}
onChange={(e) =>
setExpiryAlertDays(parseInt(e.target.value, 10) || 30)
}
className="mt-1"
min={1}
max={365}
/>
</div>
)}
</div>
{expiryDate &&
(() => {
const expiry = new Date(expiryDate);
const now = new Date();
now.setHours(0, 0, 0, 0);
expiry.setHours(0, 0, 0, 0);
const daysLeft = Math.ceil(
(expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),
);
if (daysLeft < 0) {
return (
<p className="text-[10px] text-red-600 dark:text-red-400 font-medium">
Document expirat de {Math.abs(daysLeft)} zile!
</p>
);
}
if (daysLeft <= expiryAlertDays) {
return (
<p className="text-[10px] text-amber-600 dark:text-amber-400">
Expiră în {daysLeft} zile inițiați procedurile de
prelungire.
</p>
);
}
return null;
})()}
</div>
{/* AC Validity Tracker */}
<ACValidityTracker
value={acValidity}
onChange={setAcValidity}
entryDate={date}
/>
</>
)}
{/* Web scraping prep — external tracking */}
<div className="grid gap-4 sm:grid-cols-2">
<div>
<Label className="flex items-center gap-1.5 text-xs">
<Globe className="h-3 w-3" />
URL verificare status (opțional)
</Label>
<Input
value={externalStatusUrl}
onChange={(e) => setExternalStatusUrl(e.target.value)}
className="mt-1 text-xs"
placeholder="https://portal.primaria.ro/..."
/>
</div>
<div>
<Label className="text-xs">ID urmărire extern (opțional)</Label>
<Input
value={externalTrackingId}
onChange={(e) => setExternalTrackingId(e.target.value)}
className="mt-1 text-xs"
placeholder="Ex: REF-2026-001"
/>
</div>
</div>
{/* Thread parent — reply to another entry */}
{allEntries && allEntries.length > 0 && (
<div>
<Label className="flex items-center gap-1.5">
<GitBranch className="h-3.5 w-3.5" />
Răspuns la (Thread)
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p className="text-xs">
Leagă această înregistrare ca răspuns la alta, creând un fir
de conversație instituțională. O intrare poate genera mai
multe ieșiri (branching).
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
{threadParentId && (
<div className="mt-1.5 flex items-center gap-2 rounded border border-primary/30 bg-primary/5 px-2 py-1.5 text-sm">
{threadParent ? (
<>
<Badge
variant={
threadParent.direction === "intrat" ? "default" : "secondary"
}
className="text-[10px]"
>
{threadParent.direction === "intrat" ? "\u2193" : "\u2191"}
</Badge>
<span className="font-mono text-xs">{threadParent.number}</span>
<span className="truncate text-muted-foreground text-xs">
{threadParent.subject}
</span>
</>
) : (
<span className="text-xs text-muted-foreground italic">
Legat de o inregistrare (ID: {threadParentId.slice(0, 8)}...)
</span>
)}
<Button
type="button"
variant="ghost"
size="sm"
className="ml-auto h-7 px-2 text-xs text-destructive hover:text-destructive"
onClick={() => setThreadParentId("")}
title="Sterge legatura"
>
<X className="h-3.5 w-3.5 mr-0.5" />
Sterge
</Button>
</div>
)}
{!threadParentId && (
<>
<Input
className="mt-1.5"
placeholder="Caută după număr sau subiect..."
value={threadSearch}
onChange={(e) => setThreadSearch(e.target.value)}
/>
{threadSearch.trim().length >= 2 && (
<div className="mt-1.5 max-h-32 overflow-y-auto space-y-1">
{allEntries
.filter((e) => {
if (e.id === initial?.id) return false;
const q = threadSearch.toLowerCase();
return (
e.number.toLowerCase().includes(q) ||
e.subject.toLowerCase().includes(q)
);
})
.slice(0, 8)
.map((e) => (
<button
key={e.id}
type="button"
onClick={() => {
setThreadParentId(e.id);
setThreadSearch("");
}}
className="flex w-full items-center gap-2 rounded border px-2 py-1 text-xs transition-colors hover:bg-accent"
>
<Badge
variant={
e.direction === "intrat" ? "default" : "secondary"
}
className="text-[9px] px-1 py-0"
>
{e.direction === "intrat" ? "↓" : "↑"}
</Badge>
<span className="font-mono">{e.number}</span>
<span className="truncate text-muted-foreground">
{e.subject.length > 40
? e.subject.slice(0, 40) + "…"
: e.subject}
</span>
</button>
))}
</div>
)}
</>
)}
</div>
)}
{/* Linked entries */}
{allEntries && allEntries.length > 0 && (
<div>
<Label>Înregistrări legate</Label>
<Input
className="mt-1.5"
placeholder="Caută după număr, subiect sau expeditor…"
value={linkedSearch}
onChange={(e) => setLinkedSearch(e.target.value)}
/>
<div className="mt-1.5 flex flex-wrap gap-1.5">
{allEntries
.filter((e) => {
if (e.id === initial?.id) return false;
if (!linkedSearch.trim()) return true;
const q = linkedSearch.toLowerCase();
return (
e.number.toLowerCase().includes(q) ||
e.subject.toLowerCase().includes(q) ||
(e.sender ?? "").toLowerCase().includes(q)
);
})
.map((e) => (
<button
key={e.id}
type="button"
onClick={() => {
setLinkedEntryIds((prev) =>
prev.includes(e.id)
? prev.filter((id) => id !== e.id)
: [...prev, e.id],
);
}}
className={`rounded border px-2 py-0.5 text-xs transition-colors ${
linkedEntryIds.includes(e.id)
? "border-primary bg-primary/10 text-primary"
: "border-muted-foreground/30 text-muted-foreground hover:border-primary/50"
}`}
>
{e.number}
{e.subject && (
<span className="ml-1 opacity-60">
·{" "}
{e.subject.length > 30
? e.subject.slice(0, 30) + "…"
: e.subject}
</span>
)}
</button>
))}
</div>
</div>
)}
{/* Tracked Deadlines */}
<div>
<div className="flex items-center justify-between">
<Label className="flex items-center gap-1.5">
<Clock className="h-3.5 w-3.5" />
Termene legale
</Label>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setDeadlineAddOpen(true)}
>
<Plus className="mr-1 h-3.5 w-3.5" /> Adaugă termen
</Button>
</div>
{trackedDeadlines.length > 0 && (
<div className="mt-2 space-y-2">
{trackedDeadlines.map((dl) => (
<DeadlineCard
key={dl.id}
deadline={dl}
onResolve={setResolvingDeadline}
onRemove={handleRemoveDeadline}
/>
))}
</div>
)}
{trackedDeadlines.length === 0 && (
<p className="mt-2 text-xs text-muted-foreground">
Niciun termen legal. Apăsați &quot;Adaugă termen&quot; pentru a
urmări un termen.
</p>
)}
</div>
<DeadlineAddDialog
open={deadlineAddOpen}
onOpenChange={setDeadlineAddOpen}
entryDate={date}
direction={direction}
documentType={documentType}
onAdd={handleAddDeadline}
/>
<DeadlineResolveDialog
open={resolvingDeadline !== null}
deadline={resolvingDeadline}
onOpenChange={(open) => {
if (!open) setResolvingDeadline(null);
}}
onResolve={handleResolveDeadline}
/>
{/* Attachments */}
<div>
<div className="flex items-center justify-between">
<Label className="flex items-center gap-1.5">
Atașamente
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p className="text-xs">
Atașează fișiere sau introdu căi de rețea NAS
(\\newamun\...). Preferă NAS pentru fișiere mari (&gt;5MB).
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{isUploading && (
<span className="flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400 font-normal">
<Loader2 className="h-3 w-3 animate-spin" />
Se încarcă {uploadingCount} fișier
{uploadingCount > 1 ? "e" : ""}
</span>
)}
</Label>
<div className="flex gap-1.5">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setShowNetworkInput((v) => !v)}
disabled={isSubmitting}
title="Link către fișier pe NAS"
>
<HardDrive className="mr-1 h-3.5 w-3.5" /> Link NAS
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => fileInputRef.current?.click()}
disabled={isSubmitting}
>
<Paperclip className="mr-1 h-3.5 w-3.5" /> Adaugă fișier
</Button>
</div>
<input
ref={fileInputRef}
type="file"
multiple
accept=".pdf,.jpg,.jpeg,.png,.doc,.docx,.xls,.xlsx"
onChange={handleFileUpload}
className="hidden"
/>
</div>
{/* Network path input */}
{showNetworkInput && (
<div className="mt-2 rounded-md border border-blue-200 bg-blue-50/50 dark:border-blue-800 dark:bg-blue-950/30 p-3 space-y-2">
<Label className="text-xs flex items-center gap-1">
<HardDrive className="h-3 w-3" />
Cale fișier pe NAS (A:\ O:\ P:\ T:\ sau \\newamun\\...)
</Label>
<div className="flex gap-2">
<Input
value={networkPathInput}
onChange={(e) => setNetworkPathInput(e.target.value)}
placeholder="P:\\095 - 2020 - Duplex\\99_DOC\\CU 1348-2024.pdf"
className="flex-1 font-mono text-xs"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleAddNetworkPath();
}
}}
/>
<Button
type="button"
size="sm"
onClick={handleAddNetworkPath}
disabled={!networkPathInput.trim()}
>
<Link2 className="mr-1 h-3.5 w-3.5" /> Adaugă
</Button>
</div>
{networkPathInput.trim() && isNetworkPath(networkPathInput) && (
<p className="text-[10px] text-muted-foreground">
{shortDisplayPath(networkPathInput)}
</p>
)}
{networkPathInput.trim() && !isNetworkPath(networkPathInput) && (
<p className="text-[10px] text-amber-600 dark:text-amber-400">
Calea nu pare a fi pe NAS. Introdu o cale de tip A:\ O:\ P:\ T:\
sau \\newamun\\...
</p>
)}
</div>
)}
{attachments.length > 0 && (
<div className="mt-2 space-y-1">
{attachments.map((att) =>
att.networkPath ? (
// Network path attachment — distinct visual
<div
key={att.id}
className="flex items-center gap-2 rounded border border-blue-200 dark:border-blue-800 bg-blue-50/50 dark:bg-blue-950/20 px-2 py-1.5 text-sm group"
>
<HardDrive className="h-3.5 w-3.5 text-blue-600 dark:text-blue-400 shrink-0" />
<button
type="button"
className="flex-1 min-w-0 flex items-center gap-1 text-blue-700 dark:text-blue-300 hover:underline cursor-pointer text-left"
title="Click copiază calea — lipește în Explorer"
onClick={() => {
navigator.clipboard.writeText(att.networkPath!);
}}
>
<FolderOpen className="h-3 w-3 shrink-0" />
<span className="truncate font-mono text-xs">
{shortDisplayPath(att.networkPath)}
</span>
</button>
<Badge
variant="outline"
className="text-[10px] border-blue-300 dark:border-blue-700 text-blue-600 dark:text-blue-400 shrink-0"
>
{shareLabelFor(att.networkPath) ?? "NAS"}
</Badge>
<button
type="button"
onClick={() => removeAttachment(att.id)}
className="text-destructive opacity-0 group-hover:opacity-100 transition-opacity"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
) : (
// Normal uploaded file attachment
<div
key={att.id}
className="flex items-center gap-2 rounded border px-2 py-1 text-sm"
>
<Paperclip className="h-3 w-3 text-muted-foreground" />
<span className="flex-1 truncate">{att.name}</span>
<Badge variant="outline" className="text-[10px]">
{(att.size / 1024).toFixed(0)} KB
</Badge>
<button
type="button"
onClick={() => removeAttachment(att.id)}
className="text-destructive"
>
<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={3}
className="mt-1"
/>
</div>
<div className="flex justify-end gap-2 pt-2">
<Button
type="button"
variant="outline"
onClick={onCancel}
disabled={isSubmitting}
>
Anulează
</Button>
<Button type="submit" disabled={isSubmitting || isUploading}>
{isSubmitting ? (
<>
<Loader2 className="mr-1.5 h-4 w-4 animate-spin" />
Se salvează
</>
) : isUploading ? (
<>
<Loader2 className="mr-1.5 h-4 w-4 animate-spin" />
Se încarcă fișiere
</>
) : initial ? (
"Actualizează"
) : (
"Adaugă"
)}
</Button>
</div>
{/* Quick contact creation dialog */}
<QuickContactDialog
open={quickContactOpen}
onOpenChange={setQuickContactOpen}
initialName={quickContactName}
onConfirm={handleQuickContactConfirm}
/>
</form>
);
}