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:
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user