feat(registratura): 3.02 bidirectional integration, simplified status, threads
- Dynamic document types: string-based DocumentType synced with Tag Manager (new types auto-create tags under 'document-type' category) - Added default types: 'Apel telefonic', 'Videoconferinta' - Bidirectional Address Book: quick-create contacts from sender/recipient/ assignee fields via QuickContactDialog popup - Simplified status: Switch toggle replaces dropdown (default open) - Responsabil (Assignee) field with contact autocomplete (ERP-ready) - Entry threads: threadParentId links entries as replies, ThreadView shows parent/current/children tree with branching support - Info tooltips on deadline, status, and assignee fields - New Resp. column and thread icon in registry table - All changes backward-compatible with existing data
This commit is contained in:
@@ -1,22 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo, useRef } from "react";
|
||||
import { Paperclip, X, Clock, Plus } from "lucide-react";
|
||||
import { useState, useMemo, useRef, useCallback } from "react";
|
||||
import {
|
||||
Paperclip,
|
||||
X,
|
||||
Clock,
|
||||
Plus,
|
||||
UserPlus,
|
||||
Info,
|
||||
GitBranch,
|
||||
} from "lucide-react";
|
||||
import type { CompanyId } from "@/core/auth/types";
|
||||
import type {
|
||||
RegistryEntry,
|
||||
RegistryDirection,
|
||||
RegistryStatus,
|
||||
DocumentType,
|
||||
RegistryAttachment,
|
||||
TrackedDeadline,
|
||||
DeadlineResolution,
|
||||
} from "../types";
|
||||
import { DEFAULT_DOC_TYPE_LABELS } 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 { Switch } from "@/shared/components/ui/switch";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -24,11 +33,21 @@ import {
|
||||
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 {
|
||||
createTrackedDeadline,
|
||||
resolveDeadline as resolveDeadlineFn,
|
||||
@@ -42,35 +61,55 @@ interface RegistryEntryFormProps {
|
||||
data: Omit<RegistryEntry, "id" | "number" | "createdAt" | "updatedAt">,
|
||||
) => 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;
|
||||
}
|
||||
|
||||
const DOC_TYPE_LABELS: Record<DocumentType, string> = {
|
||||
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,
|
||||
onCreateContact,
|
||||
onCreateDocType,
|
||||
onNavigateEntry,
|
||||
}: RegistryEntryFormProps) {
|
||||
const { allContacts } = useContacts();
|
||||
const { allContacts, refresh: refreshContacts } = useContacts();
|
||||
const { tags: docTypeTags } = useTags("document-type");
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// ── Build dynamic doc type list from defaults + Tag Manager ──
|
||||
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);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [docTypeTags]);
|
||||
|
||||
const [direction, setDirection] = useState<RegistryDirection>(
|
||||
initial?.direction ?? "intrat",
|
||||
);
|
||||
const [documentType, setDocumentType] = useState<DocumentType>(
|
||||
initial?.documentType ?? "scrisoare",
|
||||
);
|
||||
const [customDocType, setCustomDocType] = useState("");
|
||||
const [subject, setSubject] = useState(initial?.subject ?? "");
|
||||
const [date, setDate] = useState(
|
||||
initial?.date ?? new Date().toISOString().slice(0, 10),
|
||||
@@ -86,10 +125,15 @@ export function RegistryEntryForm({
|
||||
const [company, setCompany] = useState<CompanyId>(
|
||||
initial?.company ?? "beletage",
|
||||
);
|
||||
const [status, setStatus] = useState<RegistryStatus>(
|
||||
initial?.status ?? "deschis",
|
||||
);
|
||||
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 ?? "",
|
||||
);
|
||||
const [notes, setNotes] = useState(initial?.notes ?? "");
|
||||
const [linkedEntryIds, setLinkedEntryIds] = useState<string[]>(
|
||||
initial?.linkedEntryIds ?? [],
|
||||
@@ -101,12 +145,20 @@ export function RegistryEntryForm({
|
||||
initial?.trackedDeadlines ?? [],
|
||||
);
|
||||
const [linkedSearch, setLinkedSearch] = useState("");
|
||||
const [threadSearch, setThreadSearch] = useState("");
|
||||
|
||||
// ── 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,
|
||||
@@ -127,7 +179,6 @@ export function RegistryEntryForm({
|
||||
prev.map((d) => (d.id === resolved.id ? resolved : d)),
|
||||
);
|
||||
|
||||
// Handle chain
|
||||
if (chainNext) {
|
||||
const def = getDeadlineType(resolvingDeadline.typeId);
|
||||
if (def?.chainNextTypeId) {
|
||||
@@ -147,33 +198,86 @@ export function RegistryEntryForm({
|
||||
setTrackedDeadlines((prev) => prev.filter((d) => d.id !== deadlineId));
|
||||
};
|
||||
|
||||
// ── Sender/Recipient autocomplete suggestions ──
|
||||
// ── Contact autocomplete ──
|
||||
const [senderFocused, setSenderFocused] = useState(false);
|
||||
const [recipientFocused, setRecipientFocused] = useState(false);
|
||||
const [assigneeFocused, setAssigneeFocused] = 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 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 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 senderSuggestions = useMemo(
|
||||
() => filterContacts(sender),
|
||||
[filterContacts, sender],
|
||||
);
|
||||
const recipientSuggestions = useMemo(
|
||||
() => filterContacts(recipient),
|
||||
[filterContacts, recipient],
|
||||
);
|
||||
const assigneeSuggestions = useMemo(
|
||||
() => filterContacts(assignee),
|
||||
[filterContacts, assignee],
|
||||
);
|
||||
|
||||
// ── 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 ──
|
||||
const handleAddCustomDocType = async () => {
|
||||
const label = customDocType.trim();
|
||||
if (!label) return;
|
||||
const key = label.toLowerCase().replace(/\s+/g, "-");
|
||||
setDocumentType(key);
|
||||
setCustomDocType("");
|
||||
if (onCreateDocType) {
|
||||
await onCreateDocType(label);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
@@ -215,8 +319,11 @@ export function RegistryEntryForm({
|
||||
recipient,
|
||||
recipientContactId: recipientContactId || undefined,
|
||||
company,
|
||||
status,
|
||||
status: isClosed ? "inchis" : "deschis",
|
||||
deadline: deadline || undefined,
|
||||
assignee: assignee || undefined,
|
||||
assigneeContactId: assigneeContactId || undefined,
|
||||
threadParentId: threadParentId || undefined,
|
||||
linkedEntryIds,
|
||||
attachments,
|
||||
trackedDeadlines:
|
||||
@@ -227,8 +334,78 @@ export function RegistryEntryForm({
|
||||
});
|
||||
};
|
||||
|
||||
// ── 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 "{currentValue}"
|
||||
</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">
|
||||
{/* 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>
|
||||
@@ -248,23 +425,48 @@ export function RegistryEntryForm({
|
||||
</div>
|
||||
<div>
|
||||
<Label>Tip document</Label>
|
||||
<Select
|
||||
value={documentType}
|
||||
onValueChange={(v) => setDocumentType(v as DocumentType)}
|
||||
>
|
||||
<SelectTrigger className="mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(
|
||||
Object.entries(DOC_TYPE_LABELS) as [DocumentType, string][]
|
||||
).map(([key, label]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<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>Data</Label>
|
||||
@@ -288,8 +490,9 @@ export function RegistryEntryForm({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Sender / Recipient with autocomplete */}
|
||||
{/* Sender / Recipient with autocomplete + quick create */}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{/* Sender */}
|
||||
<div className="relative">
|
||||
<Label>Expeditor</Label>
|
||||
<Input
|
||||
@@ -303,30 +506,19 @@ export function RegistryEntryForm({
|
||||
className="mt-1"
|
||||
placeholder="Nume sau companie..."
|
||||
/>
|
||||
{senderFocused && senderSuggestions.length > 0 && (
|
||||
<div className="absolute z-10 mt-1 w-full rounded-md border bg-popover p-1 shadow-md">
|
||||
{senderSuggestions.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={() => {
|
||||
setSender(c.company ? `${c.name} (${c.company})` : c.name);
|
||||
setSenderContactId(c.id);
|
||||
setSenderFocused(false);
|
||||
}}
|
||||
>
|
||||
<span className="font-medium">{c.name}</span>
|
||||
{c.company && (
|
||||
<span className="ml-1 text-muted-foreground text-xs">
|
||||
{c.company}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{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>Destinatar</Label>
|
||||
<Input
|
||||
@@ -340,35 +532,63 @@ export function RegistryEntryForm({
|
||||
className="mt-1"
|
||||
placeholder="Nume sau companie..."
|
||||
/>
|
||||
{recipientFocused && recipientSuggestions.length > 0 && (
|
||||
<div className="absolute z-10 mt-1 w-full rounded-md border bg-popover p-1 shadow-md">
|
||||
{recipientSuggestions.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={() => {
|
||||
setRecipient(
|
||||
c.company ? `${c.name} (${c.company})` : c.name,
|
||||
);
|
||||
setRecipientContactId(c.id);
|
||||
setRecipientFocused(false);
|
||||
}}
|
||||
>
|
||||
<span className="font-medium">{c.name}</span>
|
||||
{c.company && (
|
||||
<span className="ml-1 text-muted-foreground text-xs">
|
||||
{c.company}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{renderContactDropdown(
|
||||
recipientSuggestions,
|
||||
recipientFocused,
|
||||
"recipient",
|
||||
recipient,
|
||||
(c) => {
|
||||
setRecipient(c.company ? `${c.name} (${c.company})` : c.name);
|
||||
setRecipientContactId(c.id);
|
||||
setRecipientFocused(false);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Company + Status + Deadline */}
|
||||
{/* Assignee (Responsabil) */}
|
||||
<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>
|
||||
@@ -388,22 +608,47 @@ export function RegistryEntryForm({
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Status</Label>
|
||||
<Select
|
||||
value={status}
|
||||
onValueChange={(v) => setStatus(v as RegistryStatus)}
|
||||
>
|
||||
<SelectTrigger className="mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="deschis">Deschis</SelectItem>
|
||||
<SelectItem value="inchis">Închis</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Label className="flex items-center gap-1.5">
|
||||
Închis
|
||||
<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>Termen limită</Label>
|
||||
<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}
|
||||
@@ -413,6 +658,105 @@ export function RegistryEntryForm({
|
||||
</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>
|
||||
{threadParent && (
|
||||
<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">
|
||||
<Badge
|
||||
variant={
|
||||
threadParent.direction === "intrat" ? "default" : "secondary"
|
||||
}
|
||||
className="text-[10px]"
|
||||
>
|
||||
{threadParent.direction === "intrat" ? "↓" : "↑"}
|
||||
</Badge>
|
||||
<span className="font-mono text-xs">{threadParent.number}</span>
|
||||
<span className="truncate text-muted-foreground text-xs">
|
||||
{threadParent.subject}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="ml-auto h-6 w-6 p-0"
|
||||
onClick={() => setThreadParentId("")}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</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>
|
||||
@@ -582,6 +926,14 @@ export function RegistryEntryForm({
|
||||
</Button>
|
||||
<Button type="submit">{initial ? "Actualizează" : "Adaugă"}</Button>
|
||||
</div>
|
||||
|
||||
{/* Quick contact creation dialog */}
|
||||
<QuickContactDialog
|
||||
open={quickContactOpen}
|
||||
onOpenChange={setQuickContactOpen}
|
||||
initialName={quickContactName}
|
||||
onConfirm={handleQuickContactConfirm}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user