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:
@@ -1,31 +1,46 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useStorage } from '@/core/storage';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { RegistryEntry, RegistryDirection, RegistryStatus, DocumentType, TrackedDeadline, DeadlineResolution } from '../types';
|
||||
import { getAllEntries, saveEntry, deleteEntry, generateRegistryNumber } from '../services/registry-service';
|
||||
import { createTrackedDeadline, resolveDeadline as resolveDeadlineFn } from '../services/deadline-service';
|
||||
import { getDeadlineType } from '../services/deadline-catalog';
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useStorage } from "@/core/storage";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import type {
|
||||
RegistryEntry,
|
||||
RegistryDirection,
|
||||
RegistryStatus,
|
||||
DocumentType,
|
||||
TrackedDeadline,
|
||||
DeadlineResolution,
|
||||
} from "../types";
|
||||
import {
|
||||
getAllEntries,
|
||||
saveEntry,
|
||||
deleteEntry,
|
||||
generateRegistryNumber,
|
||||
} from "../services/registry-service";
|
||||
import {
|
||||
createTrackedDeadline,
|
||||
resolveDeadline as resolveDeadlineFn,
|
||||
} from "../services/deadline-service";
|
||||
import { getDeadlineType } from "../services/deadline-catalog";
|
||||
|
||||
export interface RegistryFilters {
|
||||
search: string;
|
||||
direction: RegistryDirection | 'all';
|
||||
status: RegistryStatus | 'all';
|
||||
documentType: DocumentType | 'all';
|
||||
direction: RegistryDirection | "all";
|
||||
status: RegistryStatus | "all";
|
||||
documentType: DocumentType | "all";
|
||||
company: string;
|
||||
}
|
||||
|
||||
export function useRegistry() {
|
||||
const storage = useStorage('registratura');
|
||||
const storage = useStorage("registratura");
|
||||
const [entries, setEntries] = useState<RegistryEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filters, setFilters] = useState<RegistryFilters>({
|
||||
search: '',
|
||||
direction: 'all',
|
||||
status: 'all',
|
||||
documentType: 'all',
|
||||
company: 'all',
|
||||
search: "",
|
||||
direction: "all",
|
||||
status: "all",
|
||||
documentType: "all",
|
||||
company: "all",
|
||||
});
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
@@ -36,139 +51,189 @@ export function useRegistry() {
|
||||
}, [storage]);
|
||||
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
useEffect(() => { refresh(); }, [refresh]);
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const addEntry = useCallback(async (data: Omit<RegistryEntry, 'id' | 'number' | 'createdAt' | 'updatedAt'>) => {
|
||||
const now = new Date().toISOString();
|
||||
const number = generateRegistryNumber(data.company, data.date, entries);
|
||||
const entry: RegistryEntry = {
|
||||
...data,
|
||||
id: uuid(),
|
||||
number,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
await saveEntry(storage, entry);
|
||||
await refresh();
|
||||
return entry;
|
||||
}, [storage, refresh, entries]);
|
||||
const addEntry = useCallback(
|
||||
async (
|
||||
data: Omit<RegistryEntry, "id" | "number" | "createdAt" | "updatedAt">,
|
||||
) => {
|
||||
// Fetch fresh entries to prevent duplicate numbers from stale state
|
||||
const freshEntries = await getAllEntries(storage);
|
||||
const now = new Date().toISOString();
|
||||
const number = generateRegistryNumber(
|
||||
data.company,
|
||||
data.date,
|
||||
freshEntries,
|
||||
);
|
||||
const entry: RegistryEntry = {
|
||||
...data,
|
||||
id: uuid(),
|
||||
number,
|
||||
registrationDate: new Date().toISOString().slice(0, 10),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
await saveEntry(storage, entry);
|
||||
await refresh();
|
||||
return entry;
|
||||
},
|
||||
[storage, refresh],
|
||||
);
|
||||
|
||||
const updateEntry = useCallback(async (id: string, updates: Partial<RegistryEntry>) => {
|
||||
const existing = entries.find((e) => e.id === id);
|
||||
if (!existing) return;
|
||||
const updated: RegistryEntry = {
|
||||
...existing,
|
||||
...updates,
|
||||
id: existing.id,
|
||||
number: existing.number,
|
||||
createdAt: existing.createdAt,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
await saveEntry(storage, updated);
|
||||
await refresh();
|
||||
}, [storage, refresh, entries]);
|
||||
const updateEntry = useCallback(
|
||||
async (id: string, updates: Partial<RegistryEntry>) => {
|
||||
const existing = entries.find((e) => e.id === id);
|
||||
if (!existing) return;
|
||||
const updated: RegistryEntry = {
|
||||
...existing,
|
||||
...updates,
|
||||
id: existing.id,
|
||||
number: existing.number,
|
||||
createdAt: existing.createdAt,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
await saveEntry(storage, updated);
|
||||
await refresh();
|
||||
},
|
||||
[storage, refresh, entries],
|
||||
);
|
||||
|
||||
const removeEntry = useCallback(async (id: string) => {
|
||||
await deleteEntry(storage, id);
|
||||
await refresh();
|
||||
}, [storage, refresh]);
|
||||
const removeEntry = useCallback(
|
||||
async (id: string) => {
|
||||
await deleteEntry(storage, id);
|
||||
await refresh();
|
||||
},
|
||||
[storage, refresh],
|
||||
);
|
||||
|
||||
/** Close an entry and optionally its linked entries */
|
||||
const closeEntry = useCallback(async (id: string, closeLinked: boolean) => {
|
||||
const entry = entries.find((e) => e.id === id);
|
||||
if (!entry) return;
|
||||
await updateEntry(id, { status: 'inchis' });
|
||||
const linked = entry.linkedEntryIds ?? [];
|
||||
if (closeLinked && linked.length > 0) {
|
||||
for (const linkedId of linked) {
|
||||
const linked = entries.find((e) => e.id === linkedId);
|
||||
if (linked && linked.status !== 'inchis') {
|
||||
const updatedLinked: RegistryEntry = {
|
||||
...linked,
|
||||
status: 'inchis',
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
await saveEntry(storage, updatedLinked);
|
||||
const closeEntry = useCallback(
|
||||
async (id: string, closeLinked: boolean) => {
|
||||
const entry = entries.find((e) => e.id === id);
|
||||
if (!entry) return;
|
||||
await updateEntry(id, { status: "inchis" });
|
||||
const linked = entry.linkedEntryIds ?? [];
|
||||
if (closeLinked && linked.length > 0) {
|
||||
for (const linkedId of linked) {
|
||||
const linked = entries.find((e) => e.id === linkedId);
|
||||
if (linked && linked.status !== "inchis") {
|
||||
const updatedLinked: RegistryEntry = {
|
||||
...linked,
|
||||
status: "inchis",
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
await saveEntry(storage, updatedLinked);
|
||||
}
|
||||
}
|
||||
await refresh();
|
||||
}
|
||||
await refresh();
|
||||
}
|
||||
}, [entries, updateEntry, storage, refresh]);
|
||||
},
|
||||
[entries, updateEntry, storage, refresh],
|
||||
);
|
||||
|
||||
const updateFilter = useCallback(<K extends keyof RegistryFilters>(key: K, value: RegistryFilters[K]) => {
|
||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||
}, []);
|
||||
const updateFilter = useCallback(
|
||||
<K extends keyof RegistryFilters>(key: K, value: RegistryFilters[K]) => {
|
||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// ── Deadline operations ──
|
||||
|
||||
const addDeadline = useCallback(async (entryId: string, typeId: string, startDate: string, chainParentId?: string) => {
|
||||
const entry = entries.find((e) => e.id === entryId);
|
||||
if (!entry) return null;
|
||||
const tracked = createTrackedDeadline(typeId, startDate, chainParentId);
|
||||
if (!tracked) return null;
|
||||
const existing = entry.trackedDeadlines ?? [];
|
||||
const updated: RegistryEntry = {
|
||||
...entry,
|
||||
trackedDeadlines: [...existing, tracked],
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
await saveEntry(storage, updated);
|
||||
await refresh();
|
||||
return tracked;
|
||||
}, [entries, storage, refresh]);
|
||||
const addDeadline = useCallback(
|
||||
async (
|
||||
entryId: string,
|
||||
typeId: string,
|
||||
startDate: string,
|
||||
chainParentId?: string,
|
||||
) => {
|
||||
const entry = entries.find((e) => e.id === entryId);
|
||||
if (!entry) return null;
|
||||
const tracked = createTrackedDeadline(typeId, startDate, chainParentId);
|
||||
if (!tracked) return null;
|
||||
const existing = entry.trackedDeadlines ?? [];
|
||||
const updated: RegistryEntry = {
|
||||
...entry,
|
||||
trackedDeadlines: [...existing, tracked],
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
await saveEntry(storage, updated);
|
||||
await refresh();
|
||||
return tracked;
|
||||
},
|
||||
[entries, storage, refresh],
|
||||
);
|
||||
|
||||
const resolveDeadline = useCallback(async (
|
||||
entryId: string,
|
||||
deadlineId: string,
|
||||
resolution: DeadlineResolution,
|
||||
note?: string,
|
||||
): Promise<TrackedDeadline | null> => {
|
||||
const entry = entries.find((e) => e.id === entryId);
|
||||
if (!entry) return null;
|
||||
const deadlines = entry.trackedDeadlines ?? [];
|
||||
const idx = deadlines.findIndex((d) => d.id === deadlineId);
|
||||
if (idx === -1) return null;
|
||||
const dl = deadlines[idx];
|
||||
if (!dl) return null;
|
||||
const resolved = resolveDeadlineFn(dl, resolution, note);
|
||||
const updatedDeadlines = [...deadlines];
|
||||
updatedDeadlines[idx] = resolved;
|
||||
const updated: RegistryEntry = {
|
||||
...entry,
|
||||
trackedDeadlines: updatedDeadlines,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
await saveEntry(storage, updated);
|
||||
const resolveDeadline = useCallback(
|
||||
async (
|
||||
entryId: string,
|
||||
deadlineId: string,
|
||||
resolution: DeadlineResolution,
|
||||
note?: string,
|
||||
): Promise<TrackedDeadline | null> => {
|
||||
const entry = entries.find((e) => e.id === entryId);
|
||||
if (!entry) return null;
|
||||
const deadlines = entry.trackedDeadlines ?? [];
|
||||
const idx = deadlines.findIndex((d) => d.id === deadlineId);
|
||||
if (idx === -1) return null;
|
||||
const dl = deadlines[idx];
|
||||
if (!dl) return null;
|
||||
const resolved = resolveDeadlineFn(dl, resolution, note);
|
||||
const updatedDeadlines = [...deadlines];
|
||||
updatedDeadlines[idx] = resolved;
|
||||
const updated: RegistryEntry = {
|
||||
...entry,
|
||||
trackedDeadlines: updatedDeadlines,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
await saveEntry(storage, updated);
|
||||
|
||||
// If the resolved deadline has a chain, automatically check for the next type
|
||||
const def = getDeadlineType(dl.typeId);
|
||||
await refresh();
|
||||
// If the resolved deadline has a chain, automatically check for the next type
|
||||
const def = getDeadlineType(dl.typeId);
|
||||
await refresh();
|
||||
|
||||
if (
|
||||
def?.chainNextTypeId &&
|
||||
(resolution === "completed" || resolution === "aprobat-tacit")
|
||||
) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
if (def?.chainNextTypeId && (resolution === 'completed' || resolution === 'aprobat-tacit')) {
|
||||
return resolved;
|
||||
}
|
||||
},
|
||||
[entries, storage, refresh],
|
||||
);
|
||||
|
||||
return resolved;
|
||||
}, [entries, storage, refresh]);
|
||||
|
||||
const removeDeadline = useCallback(async (entryId: string, deadlineId: string) => {
|
||||
const entry = entries.find((e) => e.id === entryId);
|
||||
if (!entry) return;
|
||||
const deadlines = entry.trackedDeadlines ?? [];
|
||||
const updated: RegistryEntry = {
|
||||
...entry,
|
||||
trackedDeadlines: deadlines.filter((d) => d.id !== deadlineId),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
await saveEntry(storage, updated);
|
||||
await refresh();
|
||||
}, [entries, storage, refresh]);
|
||||
const removeDeadline = useCallback(
|
||||
async (entryId: string, deadlineId: string) => {
|
||||
const entry = entries.find((e) => e.id === entryId);
|
||||
if (!entry) return;
|
||||
const deadlines = entry.trackedDeadlines ?? [];
|
||||
const updated: RegistryEntry = {
|
||||
...entry,
|
||||
trackedDeadlines: deadlines.filter((d) => d.id !== deadlineId),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
await saveEntry(storage, updated);
|
||||
await refresh();
|
||||
},
|
||||
[entries, storage, refresh],
|
||||
);
|
||||
|
||||
const filteredEntries = entries.filter((entry) => {
|
||||
if (filters.direction !== 'all' && entry.direction !== filters.direction) return false;
|
||||
if (filters.status !== 'all' && entry.status !== filters.status) return false;
|
||||
if (filters.documentType !== 'all' && entry.documentType !== filters.documentType) return false;
|
||||
if (filters.company !== 'all' && entry.company !== filters.company) return false;
|
||||
if (filters.direction !== "all" && entry.direction !== filters.direction)
|
||||
return false;
|
||||
if (filters.status !== "all" && entry.status !== filters.status)
|
||||
return false;
|
||||
if (
|
||||
filters.documentType !== "all" &&
|
||||
entry.documentType !== filters.documentType
|
||||
)
|
||||
return false;
|
||||
if (filters.company !== "all" && entry.company !== filters.company)
|
||||
return false;
|
||||
if (filters.search) {
|
||||
const q = filters.search.toLowerCase();
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user