diff --git a/SESSION-LOG.md b/SESSION-LOG.md index 7242f44..ca85b97 100644 --- a/SESSION-LOG.md +++ b/SESSION-LOG.md @@ -4,6 +4,38 @@ --- +## Session — 2026-02-27 late night (GitHub Copilot - Claude Opus 4.6) + +### Context + +Continued Phase 3. Fixed critical Registratura bugs reported by user + started next task. + +### Completed + +- **Registratura Bug Fixes (6 issues):** + 1. **File upload loading feedback** — Added `uploadingCount` state tracking FileReader progress. Shows spinner + "Se încarcă X fișiere…" next to Atașamente label. Submit button shows "Se încarcă fișiere…" and is disabled while files load. + 2. **Duplicate registration numbers** — Root cause: `generateRegistryNumber` counted entries instead of parsing max existing number. Fix: parse actual number from regex `PREFIX-NNNN/YYYY`, find max, +1. Also: `addEntry` now fetches fresh entries from storage before generating number (eliminates race condition from stale state). + 3. **Form submission lock** — Added `isSubmitting` state. Submit button disabled + shows Loader2 spinner during save. Prevents double-click creating multiple entries. + 4. **Unified close/resolve flow** — Added `ClosureResolution` type (finalizat/aprobat-tacit/respins/retras/altele) to `ClosureInfo`. CloseGuardDialog now has resolution selector matching deadline resolve options. ClosureBanner shows resolution badge. + 5. **Backdating support** — Date field renamed "Data document" with tooltip explaining retroactive registration. Added `registrationDate` field on RegistryEntry (auto = today). Registry table shows "(înr. DATE)" when registrationDate differs from document date. Numbers remain sequential regardless of document date. + 6. **"Actualizează" button feedback** — Submit button now shows loading spinner when saving, disabled during upload. `onSubmit` prop accepts `Promise` for proper async tracking. + +### Files Modified + +- `src/modules/registratura/types.ts` — Added `ClosureResolution` type, `registrationDate` field on RegistryEntry, `resolution` field on ClosureInfo +- `src/modules/registratura/services/registry-service.ts` — Rewrote `generateRegistryNumber` to parse max existing number via regex +- `src/modules/registratura/hooks/use-registry.ts` — `addEntry` fetches fresh entries before generating number +- `src/modules/registratura/components/registry-entry-form.tsx` — Upload progress tracking, submission lock, date tooltip, async submit +- `src/modules/registratura/components/registry-table.tsx` — "Data doc." header, shows registrationDate when different +- `src/modules/registratura/components/close-guard-dialog.tsx` — Resolution selector added +- `src/modules/registratura/components/closure-banner.tsx` — Resolution badge display + +### Commits + +- `8042df4` fix(registratura): prevent duplicate numbers, add upload progress, submission lock, unified close/resolve, backdating support + +--- + ## Session — 2026-02-27 night (GitHub Copilot - Claude Opus 4.6) ### Context diff --git a/src/modules/it-inventory/components/it-inventory-module.tsx b/src/modules/it-inventory/components/it-inventory-module.tsx index 50ad9f5..7eea9db 100644 --- a/src/modules/it-inventory/components/it-inventory-module.tsx +++ b/src/modules/it-inventory/components/it-inventory-module.tsx @@ -27,36 +27,38 @@ import { DialogTitle, DialogFooter, } from "@/shared/components/ui/dialog"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/shared/components/ui/tabs"; import type { CompanyId } from "@/core/auth/types"; import type { InventoryItem, InventoryItemType, InventoryItemStatus, } from "../types"; +import { + DEFAULT_TYPE_LABELS, + STATUS_LABELS, + RACK_MOUNTABLE_TYPES, +} from "../types"; import { useInventory } from "../hooks/use-inventory"; -import { useContacts } from "@/modules/address-book/hooks/use-contacts"; - -const TYPE_LABELS: Record = { - laptop: "Laptop", - desktop: "Desktop", - monitor: "Monitor", - printer: "Imprimantă", - phone: "Telefon", - tablet: "Tabletă", - network: "Rețea", - peripheral: "Periferic", - other: "Altele", -}; - -const STATUS_LABELS: Record = { - active: "Activ", - "in-repair": "În reparație", - storage: "Depozitat", - decommissioned: "Dezafectat", -}; +import { ServerRack } from "./server-rack"; +import { cn } from "@/shared/lib/utils"; type ViewMode = "list" | "add" | "edit"; +/** Resolve type label from defaults or custom */ +function getTypeLabel(type: string, customTypes: Map): string { + return ( + DEFAULT_TYPE_LABELS[type] ?? + customTypes.get(type) ?? + type.replace(/-/g, " ").replace(/^\w/, (c) => c.toUpperCase()) + ); +} + export function ItInventoryModule() { const { items, @@ -72,6 +74,37 @@ export function ItInventoryModule() { const [editingItem, setEditingItem] = useState(null); const [deletingId, setDeletingId] = useState(null); + // Build dynamic type list from defaults + existing items + const allTypes = useMemo(() => { + const map = new Map(); + for (const [key, label] of Object.entries(DEFAULT_TYPE_LABELS)) { + map.set(key, label); + } + for (const item of allItems) { + if (!map.has(item.type)) { + map.set( + item.type, + item.type.replace(/-/g, " ").replace(/^\w/, (c) => c.toUpperCase()), + ); + } + } + return map; + }, [allItems]); + + // Custom types beyond defaults (for display) + const customTypes = useMemo(() => { + const map = new Map(); + for (const item of allItems) { + if (!DEFAULT_TYPE_LABELS[item.type]) { + map.set( + item.type, + item.type.replace(/-/g, " ").replace(/^\w/, (c) => c.toUpperCase()), + ); + } + } + return map; + }, [allItems]); + const handleSubmit = async ( data: Omit, ) => { @@ -91,319 +124,397 @@ export function ItInventoryModule() { } }; - return ( -
- {/* Stats */} -
- - -

Total

-

{allItems.length}

-
-
- - -

Active

-

- {allItems.filter((i) => i.status === "active").length} -

-
-
- - -

În reparație

-

- {allItems.filter((i) => i.status === "in-repair").length} -

-
-
- - -

Dezafectate

-

- {allItems.filter((i) => i.status === "decommissioned").length} -

-
-
-
+ const rentedCount = allItems.filter((i) => i.status === "rented").length; - {viewMode === "list" && ( - <> -
-
- - updateFilter("search", e.target.value)} - className="pl-9" - /> -
- - - + +

Închiriate

+

0 && "text-purple-600 dark:text-purple-400", + )} + > + {rentedCount} +

+
+ + + +

Dezafectate

+

+ {allItems.filter((i) => i.status === "decommissioned").length} +

+
+
- {loading ? ( -

- Se încarcă... -

- ) : items.length === 0 ? ( -

- Niciun echipament găsit. -

- ) : ( -
- - - - - - - - - - - - - - - - {items.map((item) => ( - - - - - - - - - - + + + + + + + + + ); + })} + +
NumeTip - Vendor/Model - S/NIP - Atribuit - LocațieStatus - Acțiuni -
{item.name} - - {TYPE_LABELS[item.type]} - - - {item.vendor && {item.vendor}} - {item.vendor && item.model && ( - / - )} - {item.model && ( - - {item.model} - - )} - - {item.serialNumber} - - {item.ipAddress} - {item.assignedTo} - {item.rackLocation || item.location} - - - {STATUS_LABELS[item.status]} - - -
- +
+ + {loading ? ( +

+ Se încarcă... +

+ ) : items.length === 0 ? ( +

+ Niciun echipament găsit. +

+ ) : ( +
+ + + + + + + + + + + + + + + {items.map((item) => { + const isRented = item.status === "rented"; + return ( + - - - - - - - ))} - -
+ Nume + Tip + Vendor/Model + S/NIP + Locație + + Status + + Acțiuni +
-
+
+ {item.name} + + + {getTypeLabel(item.type, customTypes)} + + + {item.vendor && {item.vendor}} + {item.vendor && item.model && ( + + {" "} + /{" "} + + )} + {item.model && ( + + {item.model} + + )} + + {item.serialNumber} + + {item.ipAddress} + + {item.rackPosition ? ( + + U{item.rackPosition} + {item.rackSize && item.rackSize > 1 + ? `–U${item.rackPosition + item.rackSize - 1}` + : ""} + {item.rackLocation + ? ` · ${item.rackLocation}` + : ""} + + ) : ( + item.rackLocation || item.location || "—" + )} + + + {STATUS_LABELS[item.status] ?? item.status} + + +
+ + +
+
+
+ )} + + {!loading && ( +

+ {items.length} din {allItems.length} echipamente afișate +

+ )} + )} - - )} - {(viewMode === "add" || viewMode === "edit") && ( - - - - {viewMode === "edit" ? "Editare echipament" : "Echipament nou"} - - - - { - setViewMode("list"); - setEditingItem(null); - }} - /> - - - )} + {(viewMode === "add" || viewMode === "edit") && ( + + + + {viewMode === "edit" + ? "Editare echipament" + : "Echipament nou"} + + + + { + setViewMode("list"); + setEditingItem(null); + }} + /> + + + )} - {/* Delete confirmation */} - { - if (!open) setDeletingId(null); - }} - > - - - Confirmare ștergere - -

- Ești sigur că vrei să ștergi acest echipament? Acțiunea este - ireversibilă. -

- - - - -
-
-
+ {/* Delete confirmation */} + { + if (!open) setDeletingId(null); + }} + > + + + Confirmare ștergere + +

+ Ești sigur că vrei să ștergi acest echipament? Acțiunea este + ireversibilă. +

+ + + + +
+
+ + + + + + + ); } +// ── Inventory Form ── + function InventoryForm({ initial, + allTypes, onSubmit, onCancel, }: { initial?: InventoryItem; + allTypes: Map; onSubmit: ( data: Omit, ) => void; onCancel: () => void; }) { - const { allContacts } = useContacts(); - const [name, setName] = useState(initial?.name ?? ""); const [type, setType] = useState( initial?.type ?? "laptop", ); + const [customType, setCustomType] = useState(""); const [serialNumber, setSerialNumber] = useState(initial?.serialNumber ?? ""); - const [assignedTo, setAssignedTo] = useState(initial?.assignedTo ?? ""); - const [assignedToContactId, setAssignedToContactId] = useState( - initial?.assignedToContactId ?? "", - ); - const [assignedToFocused, setAssignedToFocused] = useState(false); const [company, setCompany] = useState( initial?.company ?? "beletage", ); const [location, setLocation] = useState(initial?.location ?? ""); - const [purchaseDate, setPurchaseDate] = useState(initial?.purchaseDate ?? ""); const [status, setStatus] = useState( initial?.status ?? "active", ); const [ipAddress, setIpAddress] = useState(initial?.ipAddress ?? ""); const [macAddress, setMacAddress] = useState(initial?.macAddress ?? ""); - const [warrantyExpiry, setWarrantyExpiry] = useState( - initial?.warrantyExpiry ?? "", + const [rackPosition, setRackPosition] = useState( + initial?.rackPosition?.toString() ?? "", + ); + const [rackSize, setRackSize] = useState( + initial?.rackSize?.toString() ?? "1", ); - const [purchaseCost, setPurchaseCost] = useState(initial?.purchaseCost ?? ""); const [rackLocation, setRackLocation] = useState(initial?.rackLocation ?? ""); const [vendor, setVendor] = useState(initial?.vendor ?? ""); const [model, setModel] = useState(initial?.model ?? ""); const [notes, setNotes] = useState(initial?.notes ?? ""); - // Contact suggestions for assignedTo autocomplete - const assignedToSuggestions = useMemo(() => { - if (!assignedTo || assignedTo.length < 2) return []; - const q = assignedTo.toLowerCase(); - return allContacts - .filter( - (c) => - c.name.toLowerCase().includes(q) || - c.company.toLowerCase().includes(q), - ) - .slice(0, 5); - }, [allContacts, assignedTo]); + const showRackFields = RACK_MOUNTABLE_TYPES.has(type); + + const handleAddCustomType = () => { + const label = customType.trim(); + if (!label) return; + const key = label.toLowerCase().replace(/\s+/g, "-"); + setType(key); + setCustomType(""); + }; return (
{ e.preventDefault(); + const rackPosNum = parseInt(rackPosition, 10); + const rackSizeNum = parseInt(rackSize, 10); onSubmit({ name, type, serialNumber, - assignedTo, - assignedToContactId, company, location, - purchaseDate, status, ipAddress, macAddress, - warrantyExpiry, - purchaseCost, + rackPosition: + showRackFields && rackPosNum >= 1 ? rackPosNum : undefined, + rackSize: + showRackFields && rackSizeNum >= 1 ? rackSizeNum : undefined, rackLocation, vendor, model, @@ -427,22 +538,48 @@ function InventoryForm({
+ {/* Add custom type inline */} +
+ setCustomType(e.target.value)} + placeholder="Tip nou..." + className="text-xs h-7" + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + handleAddCustomType(); + } + }} + /> + +
+
@@ -470,6 +607,7 @@ function InventoryForm({ />
+
@@ -489,47 +627,6 @@ function InventoryForm({ placeholder="AA:BB:CC:DD:EE:FF" />
-
- - { - setAssignedTo(e.target.value); - setAssignedToContactId(""); - }} - onFocus={() => setAssignedToFocused(true)} - onBlur={() => setTimeout(() => setAssignedToFocused(false), 200)} - className="mt-1" - placeholder="Caută după nume..." - /> - {assignedToFocused && assignedToSuggestions.length > 0 && ( -
- {assignedToSuggestions.map((c) => ( - - ))} -
- )} -
-
-
+
+ +
setRackLocation(e.target.value)} className="mt-1" + placeholder="Rack 1, Birou 2..." />
@@ -584,35 +685,45 @@ function InventoryForm({
-
-
- - setPurchaseDate(e.target.value)} - className="mt-1" - /> + + {/* Rack position fields — shown only for rack-mountable types */} + {showRackFields && ( +
+
+ + setRackPosition(e.target.value)} + className="mt-1" + placeholder="Ex: 12" + /> +
+
+ + +
-
- - setPurchaseCost(e.target.value)} - className="mt-1" - /> -
-
- - setWarrantyExpiry(e.target.value)} - className="mt-1" - /> -
-
+ )} +