fix(registratura): prevent duplicate numbers, add upload progress, submission lock, unified close/resolve, backdating support

- generateRegistryNumber: parse max existing number instead of counting entries
- addEntry: fetch fresh entries before generating number (race condition fix)
- Form: isSubmitting lock prevents double-click submission
- Form: uploadingCount tracks FileReader progress, blocks submit while uploading
- Form: submit button shows Loader2 spinner during save/upload
- CloseGuardDialog: added ClosureResolution selector (finalizat/aprobat-tacit/respins/retras/altele)
- ClosureBanner: displays resolution badge
- Types: ClosureResolution type, registrationDate field on RegistryEntry
- Date field renamed 'Data document' with tooltip explaining backdating
- Registry table shows '(înr. DATE)' when registrationDate differs from document date
This commit is contained in:
AI Assistant
2026-02-27 21:56:47 +02:00
parent db6662be39
commit 8042df481f
7 changed files with 403 additions and 185 deletions
@@ -9,6 +9,7 @@ import {
UserPlus,
Info,
GitBranch,
Loader2,
} from "lucide-react";
import type { CompanyId } from "@/core/auth/types";
import type {
@@ -60,7 +61,7 @@ interface RegistryEntryFormProps {
allEntries?: RegistryEntry[];
onSubmit: (
data: Omit<RegistryEntry, "id" | "number" | "createdAt" | "updatedAt">,
) => void;
) => void | Promise<void>;
onCancel: () => void;
/** Callback to create a new Address Book contact */
onCreateContact?: (data: {
@@ -148,6 +149,11 @@ export function RegistryEntryForm({
const [linkedSearch, setLinkedSearch] = useState("");
const [threadSearch, setThreadSearch] = useState("");
// ── 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] =
@@ -283,7 +289,9 @@ export function RegistryEntryForm({
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files) return;
for (const file of Array.from(files)) {
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;
@@ -298,6 +306,10 @@ export function RegistryEntryForm({
addedAt: new Date().toISOString(),
},
]);
setUploadingCount((prev) => Math.max(0, prev - 1));
};
reader.onerror = () => {
setUploadingCount((prev) => Math.max(0, prev - 1));
};
reader.readAsDataURL(file);
}
@@ -308,31 +320,37 @@ export function RegistryEntryForm({
setAttachments((prev) => prev.filter((a) => a.id !== id));
};
const handleSubmit = (e: React.FormEvent) => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
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 || undefined,
linkedEntryIds,
attachments,
trackedDeadlines:
trackedDeadlines.length > 0 ? trackedDeadlines : undefined,
notes,
tags: initial?.tags ?? [],
visibility: initial?.visibility ?? "all",
});
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 || undefined,
linkedEntryIds,
attachments,
trackedDeadlines:
trackedDeadlines.length > 0 ? trackedDeadlines : undefined,
notes,
tags: initial?.tags ?? [],
visibility: initial?.visibility ?? "all",
});
} finally {
setIsSubmitting(false);
}
};
// ── Contact autocomplete dropdown renderer ──
@@ -479,13 +497,34 @@ export function RegistryEntryForm({
</div>
</div>
<div>
<Label>Data</Label>
<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>
)}
</div>
</div>
@@ -876,12 +915,22 @@ export function RegistryEntryForm({
{/* Attachments */}
<div>
<div className="flex items-center justify-between">
<Label>Atașamente</Label>
<Label className="flex items-center gap-1.5">
Atașamente
{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>
<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>
@@ -931,10 +980,31 @@ export function RegistryEntryForm({
</div>
<div className="flex justify-end gap-2 pt-2">
<Button type="button" variant="outline" onClick={onCancel}>
<Button
type="button"
variant="outline"
onClick={onCancel}
disabled={isSubmitting}
>
Anulează
</Button>
<Button type="submit">{initial ? "Actualizează" : "Adaugă"}</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 */}