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:
@@ -9,6 +9,7 @@ import { Textarea } from '@/shared/components/ui/textarea';
|
|||||||
import { Badge } from '@/shared/components/ui/badge';
|
import { Badge } from '@/shared/components/ui/badge';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
|
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 { CompanyId } from '@/core/auth/types';
|
||||||
import type { InventoryItem, InventoryItemType, InventoryItemStatus } from '../types';
|
import type { InventoryItem, InventoryItemType, InventoryItemStatus } from '../types';
|
||||||
import { useInventory } from '../hooks/use-inventory';
|
import { useInventory } from '../hooks/use-inventory';
|
||||||
@@ -28,8 +29,9 @@ export function ItInventoryModule() {
|
|||||||
const { items, allItems, loading, filters, updateFilter, addItem, updateItem, removeItem } = useInventory();
|
const { items, allItems, loading, filters, updateFilter, addItem, updateItem, removeItem } = useInventory();
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||||
const [editingItem, setEditingItem] = useState<InventoryItem | null>(null);
|
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) {
|
if (viewMode === 'edit' && editingItem) {
|
||||||
await updateItem(editingItem.id, data);
|
await updateItem(editingItem.id, data);
|
||||||
} else {
|
} else {
|
||||||
@@ -39,6 +41,13 @@ export function ItInventoryModule() {
|
|||||||
setEditingItem(null);
|
setEditingItem(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteConfirm = async () => {
|
||||||
|
if (deletingId) {
|
||||||
|
await removeItem(deletingId);
|
||||||
|
setDeletingId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
@@ -51,7 +60,6 @@ export function ItInventoryModule() {
|
|||||||
|
|
||||||
{viewMode === 'list' && (
|
{viewMode === 'list' && (
|
||||||
<>
|
<>
|
||||||
{/* Filters */}
|
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<div className="relative min-w-[200px] flex-1">
|
<div className="relative min-w-[200px] flex-1">
|
||||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
@@ -80,7 +88,6 @@ export function ItInventoryModule() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className="py-8 text-center text-sm text-muted-foreground">Se încarcă...</p>
|
<p className="py-8 text-center text-sm text-muted-foreground">Se încarcă...</p>
|
||||||
) : items.length === 0 ? (
|
) : items.length === 0 ? (
|
||||||
@@ -91,7 +98,9 @@ export function ItInventoryModule() {
|
|||||||
<thead><tr className="border-b bg-muted/40">
|
<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">Nume</th>
|
||||||
<th className="px-3 py-2 text-left font-medium">Tip</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">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">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">Locație</th>
|
||||||
<th className="px-3 py-2 text-left font-medium">Status</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">
|
<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 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"><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.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.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"><Badge variant="secondary">{STATUS_LABELS[item.status]}</Badge></td>
|
||||||
<td className="px-3 py-2 text-right">
|
<td className="px-3 py-2 text-right">
|
||||||
<div className="flex justify-end gap-1">
|
<div className="flex justify-end gap-1">
|
||||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setEditingItem(item); setViewMode('edit'); }}>
|
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setEditingItem(item); setViewMode('edit'); }}>
|
||||||
<Pencil className="h-3.5 w-3.5" />
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</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" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,13 +152,25 @@ export function ItInventoryModule() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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 că vrei să ș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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InventoryForm({ initial, onSubmit, onCancel }: {
|
function InventoryForm({ initial, onSubmit, onCancel }: {
|
||||||
initial?: InventoryItem;
|
initial?: InventoryItem;
|
||||||
onSubmit: (data: Omit<InventoryItem, 'id' | 'createdAt'>) => void;
|
onSubmit: (data: Omit<InventoryItem, 'id' | 'createdAt' | 'updatedAt'>) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [name, setName] = useState(initial?.name ?? '');
|
const [name, setName] = useState(initial?.name ?? '');
|
||||||
@@ -154,12 +181,26 @@ function InventoryForm({ initial, onSubmit, onCancel }: {
|
|||||||
const [location, setLocation] = useState(initial?.location ?? '');
|
const [location, setLocation] = useState(initial?.location ?? '');
|
||||||
const [purchaseDate, setPurchaseDate] = useState(initial?.purchaseDate ?? '');
|
const [purchaseDate, setPurchaseDate] = useState(initial?.purchaseDate ?? '');
|
||||||
const [status, setStatus] = useState<InventoryItemStatus>(initial?.status ?? 'active');
|
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 ?? '');
|
const [notes, setNotes] = useState(initial?.notes ?? '');
|
||||||
|
|
||||||
return (
|
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 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>
|
<div><Label>Tip</Label>
|
||||||
<Select value={type} onValueChange={(v) => setType(v as InventoryItemType)}>
|
<Select value={type} onValueChange={(v) => setType(v as InventoryItemType)}>
|
||||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
||||||
@@ -167,11 +208,17 @@ function InventoryForm({ initial, onSubmit, onCancel }: {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</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>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>
|
||||||
<div className="grid gap-4 sm:grid-cols-3">
|
<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>
|
<div><Label>Companie</Label>
|
||||||
<Select value={company} onValueChange={(v) => setCompany(v as CompanyId)}>
|
<Select value={company} onValueChange={(v) => setCompany(v as CompanyId)}>
|
||||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
||||||
@@ -183,18 +230,21 @@ function InventoryForm({ initial, onSubmit, onCancel }: {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div><Label>Locație</Label><Input value={location} onChange={(e) => setLocation(e.target.value)} className="mt-1" /></div>
|
<div><Label>Locație / Cameră</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><Label>Rack / Poziție</Label><Input value={rackLocation} onChange={(e) => setRackLocation(e.target.value)} className="mt-1" /></div>
|
||||||
</div>
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
|
||||||
<div><Label>Status</Label>
|
<div><Label>Status</Label>
|
||||||
<Select value={status} onValueChange={(v) => setStatus(v as InventoryItemStatus)}>
|
<Select value={status} onValueChange={(v) => setStatus(v as InventoryItemStatus)}>
|
||||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
<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>
|
<SelectContent>{(Object.keys(STATUS_LABELS) as InventoryItemStatus[]).map((s) => (<SelectItem key={s} value={s}>{STATUS_LABELS[s]}</SelectItem>))}</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div><Label>Note</Label><Textarea value={notes} onChange={(e) => setNotes(e.target.value)} rows={2} className="mt-1" /></div>
|
|
||||||
</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">
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
<Button type="button" variant="outline" onClick={onCancel}>Anulează</Button>
|
<Button type="button" variant="outline" onClick={onCancel}>Anulează</Button>
|
||||||
<Button type="submit">{initial ? 'Actualizează' : 'Adaugă'}</Button>
|
<Button type="submit">{initial ? 'Actualizează' : 'Adaugă'}</Button>
|
||||||
|
|||||||
@@ -40,8 +40,9 @@ export function useInventory() {
|
|||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
useEffect(() => { refresh(); }, [refresh]);
|
useEffect(() => { refresh(); }, [refresh]);
|
||||||
|
|
||||||
const addItem = useCallback(async (data: Omit<InventoryItem, 'id' | 'createdAt'>) => {
|
const addItem = useCallback(async (data: Omit<InventoryItem, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||||
const item: InventoryItem = { ...data, id: uuid(), createdAt: new Date().toISOString() };
|
const now = new Date().toISOString();
|
||||||
|
const item: InventoryItem = { ...data, id: uuid(), createdAt: now, updatedAt: now };
|
||||||
await storage.set(`${PREFIX}${item.id}`, item);
|
await storage.set(`${PREFIX}${item.id}`, item);
|
||||||
await refresh();
|
await refresh();
|
||||||
return item;
|
return item;
|
||||||
@@ -50,7 +51,11 @@ export function useInventory() {
|
|||||||
const updateItem = useCallback(async (id: string, updates: Partial<InventoryItem>) => {
|
const updateItem = useCallback(async (id: string, updates: Partial<InventoryItem>) => {
|
||||||
const existing = items.find((i) => i.id === id);
|
const existing = items.find((i) => i.id === id);
|
||||||
if (!existing) return;
|
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 storage.set(`${PREFIX}${id}`, updated);
|
||||||
await refresh();
|
await refresh();
|
||||||
}, [storage, refresh, items]);
|
}, [storage, refresh, items]);
|
||||||
@@ -70,7 +75,14 @@ export function useInventory() {
|
|||||||
if (filters.company !== 'all' && item.company !== filters.company) return false;
|
if (filters.company !== 'all' && item.company !== filters.company) return false;
|
||||||
if (filters.search) {
|
if (filters.search) {
|
||||||
const q = filters.search.toLowerCase();
|
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;
|
return true;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,8 +28,23 @@ export interface InventoryItem {
|
|||||||
location: string;
|
location: string;
|
||||||
purchaseDate: string;
|
purchaseDate: string;
|
||||||
status: InventoryItemStatus;
|
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[];
|
tags: string[];
|
||||||
notes: string;
|
notes: string;
|
||||||
visibility: Visibility;
|
visibility: Visibility;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user