feat(it-inventory): add IP/MAC/warranty/cost/rack/vendor/model fields and delete confirmation

- New fields: ipAddress, macAddress, warrantyExpiry, purchaseCost, rackLocation, vendor, model
- Delete confirmation dialog
- Expanded table columns for vendor/model and IP
- Search includes IP, vendor, and model
- Form layout with organized field groups

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Marius Tarau
2026-02-18 06:35:33 +02:00
parent 93cf6feae2
commit f7e6cbbc65
3 changed files with 96 additions and 19 deletions

View File

@@ -9,6 +9,7 @@ import { Textarea } from '@/shared/components/ui/textarea';
import { Badge } from '@/shared/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/shared/components/ui/dialog';
import type { CompanyId } from '@/core/auth/types';
import type { InventoryItem, InventoryItemType, InventoryItemStatus } from '../types';
import { useInventory } from '../hooks/use-inventory';
@@ -28,8 +29,9 @@ export function ItInventoryModule() {
const { items, allItems, loading, filters, updateFilter, addItem, updateItem, removeItem } = useInventory();
const [viewMode, setViewMode] = useState<ViewMode>('list');
const [editingItem, setEditingItem] = useState<InventoryItem | null>(null);
const [deletingId, setDeletingId] = useState<string | null>(null);
const handleSubmit = async (data: Omit<InventoryItem, 'id' | 'createdAt'>) => {
const handleSubmit = async (data: Omit<InventoryItem, 'id' | 'createdAt' | 'updatedAt'>) => {
if (viewMode === 'edit' && editingItem) {
await updateItem(editingItem.id, data);
} else {
@@ -39,6 +41,13 @@ export function ItInventoryModule() {
setEditingItem(null);
};
const handleDeleteConfirm = async () => {
if (deletingId) {
await removeItem(deletingId);
setDeletingId(null);
}
};
return (
<div className="space-y-6">
{/* Stats */}
@@ -51,7 +60,6 @@ export function ItInventoryModule() {
{viewMode === 'list' && (
<>
{/* Filters */}
<div className="flex flex-wrap items-center gap-3">
<div className="relative min-w-[200px] flex-1">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
@@ -80,7 +88,6 @@ export function ItInventoryModule() {
</Button>
</div>
{/* Table */}
{loading ? (
<p className="py-8 text-center text-sm text-muted-foreground">Se încarcă...</p>
) : items.length === 0 ? (
@@ -91,7 +98,9 @@ export function ItInventoryModule() {
<thead><tr className="border-b bg-muted/40">
<th className="px-3 py-2 text-left font-medium">Nume</th>
<th className="px-3 py-2 text-left font-medium">Tip</th>
<th className="px-3 py-2 text-left font-medium">Vendor/Model</th>
<th className="px-3 py-2 text-left font-medium">S/N</th>
<th className="px-3 py-2 text-left font-medium">IP</th>
<th className="px-3 py-2 text-left font-medium">Atribuit</th>
<th className="px-3 py-2 text-left font-medium">Locație</th>
<th className="px-3 py-2 text-left font-medium">Status</th>
@@ -102,16 +111,22 @@ export function ItInventoryModule() {
<tr key={item.id} className="border-b hover:bg-muted/20 transition-colors">
<td className="px-3 py-2 font-medium">{item.name}</td>
<td className="px-3 py-2"><Badge variant="outline">{TYPE_LABELS[item.type]}</Badge></td>
<td className="px-3 py-2 text-xs">
{item.vendor && <span>{item.vendor}</span>}
{item.vendor && item.model && <span className="text-muted-foreground"> / </span>}
{item.model && <span className="text-muted-foreground">{item.model}</span>}
</td>
<td className="px-3 py-2 font-mono text-xs">{item.serialNumber}</td>
<td className="px-3 py-2 font-mono text-xs">{item.ipAddress}</td>
<td className="px-3 py-2">{item.assignedTo}</td>
<td className="px-3 py-2">{item.location}</td>
<td className="px-3 py-2 text-xs">{item.rackLocation || item.location}</td>
<td className="px-3 py-2"><Badge variant="secondary">{STATUS_LABELS[item.status]}</Badge></td>
<td className="px-3 py-2 text-right">
<div className="flex justify-end gap-1">
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setEditingItem(item); setViewMode('edit'); }}>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => removeItem(item.id)}>
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => setDeletingId(item.id)}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
@@ -137,13 +152,25 @@ export function ItInventoryModule() {
</CardContent>
</Card>
)}
{/* Delete confirmation */}
<Dialog open={deletingId !== null} onOpenChange={(open) => { if (!open) setDeletingId(null); }}>
<DialogContent>
<DialogHeader><DialogTitle>Confirmare ștergere</DialogTitle></DialogHeader>
<p className="text-sm">Ești sigur vrei ștergi acest echipament? Acțiunea este ireversibilă.</p>
<DialogFooter>
<Button variant="outline" onClick={() => setDeletingId(null)}>Anulează</Button>
<Button variant="destructive" onClick={handleDeleteConfirm}>Șterge</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
function InventoryForm({ initial, onSubmit, onCancel }: {
initial?: InventoryItem;
onSubmit: (data: Omit<InventoryItem, 'id' | 'createdAt'>) => void;
onSubmit: (data: Omit<InventoryItem, 'id' | 'createdAt' | 'updatedAt'>) => void;
onCancel: () => void;
}) {
const [name, setName] = useState(initial?.name ?? '');
@@ -154,12 +181,26 @@ function InventoryForm({ initial, onSubmit, onCancel }: {
const [location, setLocation] = useState(initial?.location ?? '');
const [purchaseDate, setPurchaseDate] = useState(initial?.purchaseDate ?? '');
const [status, setStatus] = useState<InventoryItemStatus>(initial?.status ?? 'active');
const [ipAddress, setIpAddress] = useState(initial?.ipAddress ?? '');
const [macAddress, setMacAddress] = useState(initial?.macAddress ?? '');
const [warrantyExpiry, setWarrantyExpiry] = useState(initial?.warrantyExpiry ?? '');
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 ?? '');
return (
<form onSubmit={(e) => { e.preventDefault(); onSubmit({ name, type, serialNumber, assignedTo, company, location, purchaseDate, status, notes, tags: initial?.tags ?? [], visibility: initial?.visibility ?? 'all' }); }} className="space-y-4">
<form onSubmit={(e) => {
e.preventDefault();
onSubmit({
name, type, serialNumber, assignedTo, company, location, purchaseDate, status,
ipAddress, macAddress, warrantyExpiry, purchaseCost, rackLocation, vendor, model,
notes, tags: initial?.tags ?? [], visibility: initial?.visibility ?? 'all',
});
}} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div><Label>Nume echipament</Label><Input value={name} onChange={(e) => setName(e.target.value)} className="mt-1" required /></div>
<div><Label>Nume echipament *</Label><Input value={name} onChange={(e) => setName(e.target.value)} className="mt-1" required /></div>
<div><Label>Tip</Label>
<Select value={type} onValueChange={(v) => setType(v as InventoryItemType)}>
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
@@ -167,11 +208,17 @@ function InventoryForm({ initial, onSubmit, onCancel }: {
</Select>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 sm:grid-cols-3">
<div><Label>Vendor</Label><Input value={vendor} onChange={(e) => setVendor(e.target.value)} className="mt-1" placeholder="Dell, HP, Lenovo..." /></div>
<div><Label>Model</Label><Input value={model} onChange={(e) => setModel(e.target.value)} className="mt-1" /></div>
<div><Label>Număr serie</Label><Input value={serialNumber} onChange={(e) => setSerialNumber(e.target.value)} className="mt-1" /></div>
<div><Label>Atribuit</Label><Input value={assignedTo} onChange={(e) => setAssignedTo(e.target.value)} className="mt-1" /></div>
</div>
<div className="grid gap-4 sm:grid-cols-3">
<div><Label>Adresă IP</Label><Input value={ipAddress} onChange={(e) => setIpAddress(e.target.value)} className="mt-1" placeholder="192.168.1.x" /></div>
<div><Label>Adresă MAC</Label><Input value={macAddress} onChange={(e) => setMacAddress(e.target.value)} className="mt-1" placeholder="AA:BB:CC:DD:EE:FF" /></div>
<div><Label>Atribuit</Label><Input value={assignedTo} onChange={(e) => setAssignedTo(e.target.value)} className="mt-1" /></div>
</div>
<div className="grid gap-4 sm:grid-cols-4">
<div><Label>Companie</Label>
<Select value={company} onValueChange={(v) => setCompany(v as CompanyId)}>
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
@@ -183,18 +230,21 @@ function InventoryForm({ initial, onSubmit, onCancel }: {
</SelectContent>
</Select>
</div>
<div><Label>Locație</Label><Input value={location} onChange={(e) => setLocation(e.target.value)} className="mt-1" /></div>
<div><Label>Data achiziție</Label><Input type="date" value={purchaseDate} onChange={(e) => setPurchaseDate(e.target.value)} className="mt-1" /></div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div><Label>Locație / Cameră</Label><Input value={location} onChange={(e) => setLocation(e.target.value)} className="mt-1" /></div>
<div><Label>Rack / Poziție</Label><Input value={rackLocation} onChange={(e) => setRackLocation(e.target.value)} className="mt-1" /></div>
<div><Label>Status</Label>
<Select value={status} onValueChange={(v) => setStatus(v as InventoryItemStatus)}>
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
<SelectContent>{(Object.keys(STATUS_LABELS) as InventoryItemStatus[]).map((s) => (<SelectItem key={s} value={s}>{STATUS_LABELS[s]}</SelectItem>))}</SelectContent>
</Select>
</div>
<div><Label>Note</Label><Textarea value={notes} onChange={(e) => setNotes(e.target.value)} rows={2} className="mt-1" /></div>
</div>
<div className="grid gap-4 sm:grid-cols-3">
<div><Label>Data achiziție</Label><Input type="date" value={purchaseDate} onChange={(e) => setPurchaseDate(e.target.value)} className="mt-1" /></div>
<div><Label>Cost achiziție (RON)</Label><Input type="number" value={purchaseCost} onChange={(e) => setPurchaseCost(e.target.value)} className="mt-1" /></div>
<div><Label>Expirare garanție</Label><Input type="date" value={warrantyExpiry} onChange={(e) => setWarrantyExpiry(e.target.value)} className="mt-1" /></div>
</div>
<div><Label>Note</Label><Textarea value={notes} onChange={(e) => setNotes(e.target.value)} rows={2} className="mt-1" /></div>
<div className="flex justify-end gap-2 pt-2">
<Button type="button" variant="outline" onClick={onCancel}>Anulează</Button>
<Button type="submit">{initial ? 'Actualizează' : 'Adaugă'}</Button>

View File

@@ -40,8 +40,9 @@ export function useInventory() {
// eslint-disable-next-line react-hooks/set-state-in-effect
useEffect(() => { refresh(); }, [refresh]);
const addItem = useCallback(async (data: Omit<InventoryItem, 'id' | 'createdAt'>) => {
const item: InventoryItem = { ...data, id: uuid(), createdAt: new Date().toISOString() };
const addItem = useCallback(async (data: Omit<InventoryItem, 'id' | 'createdAt' | 'updatedAt'>) => {
const now = new Date().toISOString();
const item: InventoryItem = { ...data, id: uuid(), createdAt: now, updatedAt: now };
await storage.set(`${PREFIX}${item.id}`, item);
await refresh();
return item;
@@ -50,7 +51,11 @@ export function useInventory() {
const updateItem = useCallback(async (id: string, updates: Partial<InventoryItem>) => {
const existing = items.find((i) => i.id === id);
if (!existing) return;
const updated = { ...existing, ...updates, id: existing.id, createdAt: existing.createdAt };
const updated: InventoryItem = {
...existing, ...updates,
id: existing.id, createdAt: existing.createdAt,
updatedAt: new Date().toISOString(),
};
await storage.set(`${PREFIX}${id}`, updated);
await refresh();
}, [storage, refresh, items]);
@@ -70,7 +75,14 @@ export function useInventory() {
if (filters.company !== 'all' && item.company !== filters.company) return false;
if (filters.search) {
const q = filters.search.toLowerCase();
return item.name.toLowerCase().includes(q) || item.serialNumber.toLowerCase().includes(q) || item.assignedTo.toLowerCase().includes(q);
return (
item.name.toLowerCase().includes(q) ||
item.serialNumber.toLowerCase().includes(q) ||
item.assignedTo.toLowerCase().includes(q) ||
item.ipAddress.toLowerCase().includes(q) ||
item.vendor.toLowerCase().includes(q) ||
item.model.toLowerCase().includes(q)
);
}
return true;
});

View File

@@ -28,8 +28,23 @@ export interface InventoryItem {
location: string;
purchaseDate: string;
status: InventoryItemStatus;
/** IP address */
ipAddress: string;
/** MAC address */
macAddress: string;
/** Warranty expiry date (YYYY-MM-DD) */
warrantyExpiry: string;
/** Purchase cost (RON) */
purchaseCost: string;
/** Room / rack position */
rackLocation: string;
/** Vendor / manufacturer */
vendor: string;
/** Model name/number */
model: string;
tags: string[];
notes: string;
visibility: Visibility;
createdAt: string;
updatedAt: string;
}