Old entries in localStorage lack new fields (department, role, contactPersons, linkedEntryIds, attachments, versions, placeholders, ipAddress, vendor, model). Add null-coalescing guards to prevent client-side crashes on property access. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
192 lines
6.3 KiB
TypeScript
192 lines
6.3 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { Plus } from 'lucide-react';
|
|
import { Button } from '@/shared/components/ui/button';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card';
|
|
import { Badge } from '@/shared/components/ui/badge';
|
|
import {
|
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
|
} from '@/shared/components/ui/dialog';
|
|
import { useRegistry } from '../hooks/use-registry';
|
|
import { RegistryFilters } from './registry-filters';
|
|
import { RegistryTable } from './registry-table';
|
|
import { RegistryEntryForm } from './registry-entry-form';
|
|
import { getOverdueDays } from '../services/registry-service';
|
|
import type { RegistryEntry } from '../types';
|
|
|
|
type ViewMode = 'list' | 'add' | 'edit';
|
|
|
|
export function RegistraturaModule() {
|
|
const {
|
|
entries, allEntries, loading, filters, updateFilter,
|
|
addEntry, updateEntry, removeEntry, closeEntry,
|
|
} = useRegistry();
|
|
|
|
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
|
const [editingEntry, setEditingEntry] = useState<RegistryEntry | null>(null);
|
|
const [closingId, setClosingId] = useState<string | null>(null);
|
|
|
|
const handleAdd = async (data: Omit<RegistryEntry, 'id' | 'number' | 'createdAt' | 'updatedAt'>) => {
|
|
await addEntry(data);
|
|
setViewMode('list');
|
|
};
|
|
|
|
const handleEdit = (entry: RegistryEntry) => {
|
|
setEditingEntry(entry);
|
|
setViewMode('edit');
|
|
};
|
|
|
|
const handleUpdate = async (data: Omit<RegistryEntry, 'id' | 'number' | 'createdAt' | 'updatedAt'>) => {
|
|
if (!editingEntry) return;
|
|
await updateEntry(editingEntry.id, data);
|
|
setEditingEntry(null);
|
|
setViewMode('list');
|
|
};
|
|
|
|
const handleDelete = async (id: string) => {
|
|
await removeEntry(id);
|
|
};
|
|
|
|
const handleCloseRequest = (id: string) => {
|
|
const entry = allEntries.find((e) => e.id === id);
|
|
if (entry && (entry.linkedEntryIds ?? []).length > 0) {
|
|
setClosingId(id);
|
|
} else {
|
|
closeEntry(id, false);
|
|
}
|
|
};
|
|
|
|
const handleCloseConfirm = (closeLinked: boolean) => {
|
|
if (closingId) {
|
|
closeEntry(closingId, closeLinked);
|
|
setClosingId(null);
|
|
}
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
setViewMode('list');
|
|
setEditingEntry(null);
|
|
};
|
|
|
|
// Stats
|
|
const total = allEntries.length;
|
|
const open = allEntries.filter((e) => e.status === 'deschis').length;
|
|
const overdue = allEntries.filter((e) => {
|
|
if (e.status !== 'deschis') return false;
|
|
const days = getOverdueDays(e.deadline);
|
|
return days !== null && days > 0;
|
|
}).length;
|
|
const intrat = allEntries.filter((e) => e.direction === 'intrat').length;
|
|
|
|
const closingEntry = closingId ? allEntries.find((e) => e.id === closingId) : null;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
<StatCard label="Total" value={total} />
|
|
<StatCard label="Deschise" value={open} />
|
|
<StatCard label="Depășite" value={overdue} variant={overdue > 0 ? 'destructive' : undefined} />
|
|
<StatCard label="Intrate" value={intrat} />
|
|
</div>
|
|
|
|
{viewMode === 'list' && (
|
|
<>
|
|
<div className="flex items-center justify-between gap-4">
|
|
<RegistryFilters filters={filters} onUpdate={updateFilter} />
|
|
<Button onClick={() => setViewMode('add')} className="shrink-0">
|
|
<Plus className="mr-1.5 h-4 w-4" /> Adaugă
|
|
</Button>
|
|
</div>
|
|
|
|
<RegistryTable
|
|
entries={entries}
|
|
loading={loading}
|
|
onEdit={handleEdit}
|
|
onDelete={handleDelete}
|
|
onClose={handleCloseRequest}
|
|
/>
|
|
|
|
{!loading && (
|
|
<p className="text-xs text-muted-foreground">
|
|
{entries.length} din {total} înregistrări afișate
|
|
</p>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{viewMode === 'add' && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
Înregistrare nouă
|
|
<Badge variant="outline" className="text-xs">Nr. auto</Badge>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<RegistryEntryForm
|
|
allEntries={allEntries}
|
|
onSubmit={handleAdd}
|
|
onCancel={handleCancel}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{viewMode === 'edit' && editingEntry && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Editare — {editingEntry.number}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<RegistryEntryForm
|
|
initial={editingEntry}
|
|
allEntries={allEntries}
|
|
onSubmit={handleUpdate}
|
|
onCancel={handleCancel}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Close confirmation dialog */}
|
|
<Dialog open={closingId !== null} onOpenChange={(open) => { if (!open) setClosingId(null); }}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Închide înregistrarea</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="py-2">
|
|
<p className="text-sm">
|
|
Această înregistrare are {closingEntry?.linkedEntryIds?.length ?? 0} înregistrări legate.
|
|
Vrei să le închizi și pe acestea?
|
|
</p>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setClosingId(null)}>Anulează</Button>
|
|
<Button variant="secondary" onClick={() => handleCloseConfirm(false)}>
|
|
Doar aceasta
|
|
</Button>
|
|
<Button onClick={() => handleCloseConfirm(true)}>
|
|
Închide toate legate
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StatCard({ label, value, variant }: { label: string; value: number; variant?: 'destructive' }) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<p className="text-xs text-muted-foreground">{label}</p>
|
|
<p className={`text-2xl font-bold ${variant === 'destructive' && value > 0 ? 'text-destructive' : ''}`}>
|
|
{value}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|