feat(it-inventory): link assignedTo to Address Book contacts with autocomplete

This commit is contained in:
AI Assistant
2026-02-19 06:43:42 +02:00
parent b96b004baf
commit a49dbb2ced
2 changed files with 516 additions and 136 deletions

View File

@@ -1,43 +1,86 @@
'use client'; "use client";
import { useState } from 'react'; import { useState, useMemo } from "react";
import { Plus, Pencil, Trash2, Search } from 'lucide-react'; import { Plus, Pencil, Trash2, Search } from "lucide-react";
import { Button } from '@/shared/components/ui/button'; import { Button } from "@/shared/components/ui/button";
import { Input } from '@/shared/components/ui/input'; import { Input } from "@/shared/components/ui/input";
import { Label } from '@/shared/components/ui/label'; import { Label } from "@/shared/components/ui/label";
import { Textarea } from '@/shared/components/ui/textarea'; 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 {
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select'; Card,
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/shared/components/ui/dialog'; CardContent,
import type { CompanyId } from '@/core/auth/types'; CardHeader,
import type { InventoryItem, InventoryItemType, InventoryItemStatus } from '../types'; CardTitle,
import { useInventory } from '../hooks/use-inventory'; } 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";
import { useContacts } from "@/modules/address-book/hooks/use-contacts";
const TYPE_LABELS: Record<InventoryItemType, string> = { const TYPE_LABELS: Record<InventoryItemType, string> = {
laptop: 'Laptop', desktop: 'Desktop', monitor: 'Monitor', printer: 'Imprimantă', laptop: "Laptop",
phone: 'Telefon', tablet: 'Tabletă', network: 'Rețea', peripheral: 'Periferic', other: 'Altele', desktop: "Desktop",
monitor: "Monitor",
printer: "Imprimantă",
phone: "Telefon",
tablet: "Tabletă",
network: "Rețea",
peripheral: "Periferic",
other: "Altele",
}; };
const STATUS_LABELS: Record<InventoryItemStatus, string> = { const STATUS_LABELS: Record<InventoryItemStatus, string> = {
active: 'Activ', 'in-repair': 'În reparație', storage: 'Depozitat', decommissioned: 'Dezafectat', active: "Activ",
"in-repair": "În reparație",
storage: "Depozitat",
decommissioned: "Dezafectat",
}; };
type ViewMode = 'list' | 'add' | 'edit'; type ViewMode = "list" | "add" | "edit";
export function ItInventoryModule() { export function ItInventoryModule() {
const { items, allItems, loading, filters, updateFilter, addItem, updateItem, removeItem } = useInventory(); const {
const [viewMode, setViewMode] = useState<ViewMode>('list'); items,
allItems,
loading,
filters,
updateFilter,
addItem,
updateItem,
removeItem,
} = useInventory();
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 [deletingId, setDeletingId] = useState<string | null>(null);
const handleSubmit = async (data: Omit<InventoryItem, 'id' | 'createdAt' | 'updatedAt'>) => { const handleSubmit = async (
if (viewMode === 'edit' && editingItem) { data: Omit<InventoryItem, "id" | "createdAt" | "updatedAt">,
) => {
if (viewMode === "edit" && editingItem) {
await updateItem(editingItem.id, data); await updateItem(editingItem.id, data);
} else { } else {
await addItem(data); await addItem(data);
} }
setViewMode('list'); setViewMode("list");
setEditingItem(null); setEditingItem(null);
}; };
@@ -52,81 +95,180 @@ export function ItInventoryModule() {
<div className="space-y-6"> <div className="space-y-6">
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4"> <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Total</p><p className="text-2xl font-bold">{allItems.length}</p></CardContent></Card> <Card>
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Active</p><p className="text-2xl font-bold">{allItems.filter((i) => i.status === 'active').length}</p></CardContent></Card> <CardContent className="p-4">
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">În reparație</p><p className="text-2xl font-bold">{allItems.filter((i) => i.status === 'in-repair').length}</p></CardContent></Card> <p className="text-xs text-muted-foreground">Total</p>
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Dezafectate</p><p className="text-2xl font-bold">{allItems.filter((i) => i.status === 'decommissioned').length}</p></CardContent></Card> <p className="text-2xl font-bold">{allItems.length}</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<p className="text-xs text-muted-foreground">Active</p>
<p className="text-2xl font-bold">
{allItems.filter((i) => i.status === "active").length}
</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<p className="text-xs text-muted-foreground">În reparație</p>
<p className="text-2xl font-bold">
{allItems.filter((i) => i.status === "in-repair").length}
</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<p className="text-xs text-muted-foreground">Dezafectate</p>
<p className="text-2xl font-bold">
{allItems.filter((i) => i.status === "decommissioned").length}
</p>
</CardContent>
</Card>
</div> </div>
{viewMode === 'list' && ( {viewMode === "list" && (
<> <>
<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" />
<Input placeholder="Caută..." value={filters.search} onChange={(e) => updateFilter('search', e.target.value)} className="pl-9" /> <Input
placeholder="Caută..."
value={filters.search}
onChange={(e) => updateFilter("search", e.target.value)}
className="pl-9"
/>
</div> </div>
<Select value={filters.type} onValueChange={(v) => updateFilter('type', v as InventoryItemType | 'all')}> <Select
<SelectTrigger className="w-[140px]"><SelectValue /></SelectTrigger> value={filters.type}
onValueChange={(v) =>
updateFilter("type", v as InventoryItemType | "all")
}
>
<SelectTrigger className="w-[140px]">
<SelectValue />
</SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">Toate tipurile</SelectItem> <SelectItem value="all">Toate tipurile</SelectItem>
{(Object.keys(TYPE_LABELS) as InventoryItemType[]).map((t) => ( {(Object.keys(TYPE_LABELS) as InventoryItemType[]).map((t) => (
<SelectItem key={t} value={t}>{TYPE_LABELS[t]}</SelectItem> <SelectItem key={t} value={t}>
{TYPE_LABELS[t]}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={filters.status} onValueChange={(v) => updateFilter('status', v as InventoryItemStatus | 'all')}> <Select
<SelectTrigger className="w-[140px]"><SelectValue /></SelectTrigger> value={filters.status}
onValueChange={(v) =>
updateFilter("status", v as InventoryItemStatus | "all")
}
>
<SelectTrigger className="w-[140px]">
<SelectValue />
</SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">Toate</SelectItem> <SelectItem value="all">Toate</SelectItem>
{(Object.keys(STATUS_LABELS) as InventoryItemStatus[]).map((s) => ( {(Object.keys(STATUS_LABELS) as InventoryItemStatus[]).map(
<SelectItem key={s} value={s}>{STATUS_LABELS[s]}</SelectItem> (s) => (
))} <SelectItem key={s} value={s}>
{STATUS_LABELS[s]}
</SelectItem>
),
)}
</SelectContent> </SelectContent>
</Select> </Select>
<Button onClick={() => setViewMode('add')} className="shrink-0"> <Button onClick={() => setViewMode("add")} className="shrink-0">
<Plus className="mr-1.5 h-4 w-4" /> Adaugă <Plus className="mr-1.5 h-4 w-4" /> Adaugă
</Button> </Button>
</div> </div>
{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 ? (
<p className="py-8 text-center text-sm text-muted-foreground">Niciun echipament găsit.</p> <p className="py-8 text-center text-sm text-muted-foreground">
Niciun echipament găsit.
</p>
) : ( ) : (
<div className="overflow-x-auto rounded-lg border"> <div className="overflow-x-auto rounded-lg border">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead><tr className="border-b bg-muted/40"> <thead>
<th className="px-3 py-2 text-left font-medium">Nume</th> <tr className="border-b bg-muted/40">
<th className="px-3 py-2 text-left font-medium">Tip</th> <th className="px-3 py-2 text-left font-medium">Nume</th>
<th className="px-3 py-2 text-left font-medium">Vendor/Model</th> <th className="px-3 py-2 text-left font-medium">Tip</th>
<th className="px-3 py-2 text-left font-medium">S/N</th> <th className="px-3 py-2 text-left font-medium">
<th className="px-3 py-2 text-left font-medium">IP</th> Vendor/Model
<th className="px-3 py-2 text-left font-medium">Atribuit</th> </th>
<th className="px-3 py-2 text-left font-medium">Locație</th> <th className="px-3 py-2 text-left font-medium">S/N</th>
<th className="px-3 py-2 text-left font-medium">Status</th> <th className="px-3 py-2 text-left font-medium">IP</th>
<th className="px-3 py-2 text-right font-medium">Acțiuni</th> <th className="px-3 py-2 text-left font-medium">
</tr></thead> 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>
<th className="px-3 py-2 text-right font-medium">
Acțiuni
</th>
</tr>
</thead>
<tbody> <tbody>
{items.map((item) => ( {items.map((item) => (
<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"> <td className="px-3 py-2 text-xs">
{item.vendor && <span>{item.vendor}</span>} {item.vendor && <span>{item.vendor}</span>}
{item.vendor && item.model && <span className="text-muted-foreground"> / </span>} {item.vendor && item.model && (
{item.model && <span className="text-muted-foreground">{item.model}</span>} <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>
<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 text-xs">{item.rackLocation || item.location}</td> <td className="px-3 py-2 text-xs">
<td className="px-3 py-2"><Badge variant="secondary">{STATUS_LABELS[item.status]}</Badge></td> {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"> <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={() => setDeletingId(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>
@@ -140,27 +282,48 @@ export function ItInventoryModule() {
</> </>
)} )}
{(viewMode === 'add' || viewMode === 'edit') && ( {(viewMode === "add" || viewMode === "edit") && (
<Card> <Card>
<CardHeader><CardTitle>{viewMode === 'edit' ? 'Editare echipament' : 'Echipament nou'}</CardTitle></CardHeader> <CardHeader>
<CardTitle>
{viewMode === "edit" ? "Editare echipament" : "Echipament nou"}
</CardTitle>
</CardHeader>
<CardContent> <CardContent>
<InventoryForm <InventoryForm
initial={editingItem ?? undefined} initial={editingItem ?? undefined}
onSubmit={handleSubmit} onSubmit={handleSubmit}
onCancel={() => { setViewMode('list'); setEditingItem(null); }} onCancel={() => {
setViewMode("list");
setEditingItem(null);
}}
/> />
</CardContent> </CardContent>
</Card> </Card>
)} )}
{/* Delete confirmation */} {/* Delete confirmation */}
<Dialog open={deletingId !== null} onOpenChange={(open) => { if (!open) setDeletingId(null); }}> <Dialog
open={deletingId !== null}
onOpenChange={(open) => {
if (!open) setDeletingId(null);
}}
>
<DialogContent> <DialogContent>
<DialogHeader><DialogTitle>Confirmare ștergere</DialogTitle></DialogHeader> <DialogHeader>
<p className="text-sm">Ești sigur vrei ștergi acest echipament? Acțiunea este ireversibilă.</p> <DialogTitle>Confirmare ștergere</DialogTitle>
</DialogHeader>
<p className="text-sm">
Ești sigur vrei ștergi acest echipament? Acțiunea este
ireversibilă.
</p>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setDeletingId(null)}>Anulează</Button> <Button variant="outline" onClick={() => setDeletingId(null)}>
<Button variant="destructive" onClick={handleDeleteConfirm}>Șterge</Button> Anulează
</Button>
<Button variant="destructive" onClick={handleDeleteConfirm}>
Șterge
</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -168,60 +331,214 @@ export function ItInventoryModule() {
); );
} }
function InventoryForm({ initial, onSubmit, onCancel }: { function InventoryForm({
initial,
onSubmit,
onCancel,
}: {
initial?: InventoryItem; initial?: InventoryItem;
onSubmit: (data: Omit<InventoryItem, 'id' | 'createdAt' | 'updatedAt'>) => void; onSubmit: (
data: Omit<InventoryItem, "id" | "createdAt" | "updatedAt">,
) => void;
onCancel: () => void; onCancel: () => void;
}) { }) {
const [name, setName] = useState(initial?.name ?? ''); const { allContacts } = useContacts();
const [type, setType] = useState<InventoryItemType>(initial?.type ?? 'laptop');
const [serialNumber, setSerialNumber] = useState(initial?.serialNumber ?? ''); const [name, setName] = useState(initial?.name ?? "");
const [assignedTo, setAssignedTo] = useState(initial?.assignedTo ?? ''); const [type, setType] = useState<InventoryItemType>(
const [company, setCompany] = useState<CompanyId>(initial?.company ?? 'beletage'); initial?.type ?? "laptop",
const [location, setLocation] = useState(initial?.location ?? ''); );
const [purchaseDate, setPurchaseDate] = useState(initial?.purchaseDate ?? ''); const [serialNumber, setSerialNumber] = useState(initial?.serialNumber ?? "");
const [status, setStatus] = useState<InventoryItemStatus>(initial?.status ?? 'active'); const [assignedTo, setAssignedTo] = useState(initial?.assignedTo ?? "");
const [ipAddress, setIpAddress] = useState(initial?.ipAddress ?? ''); const [assignedToContactId, setAssignedToContactId] = useState(
const [macAddress, setMacAddress] = useState(initial?.macAddress ?? ''); initial?.assignedToContactId ?? "",
const [warrantyExpiry, setWarrantyExpiry] = useState(initial?.warrantyExpiry ?? ''); );
const [purchaseCost, setPurchaseCost] = useState(initial?.purchaseCost ?? ''); const [assignedToFocused, setAssignedToFocused] = useState(false);
const [rackLocation, setRackLocation] = useState(initial?.rackLocation ?? ''); const [company, setCompany] = useState<CompanyId>(
const [vendor, setVendor] = useState(initial?.vendor ?? ''); initial?.company ?? "beletage",
const [model, setModel] = useState(initial?.model ?? ''); );
const [notes, setNotes] = useState(initial?.notes ?? ''); 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 ?? "");
// 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]);
return ( return (
<form onSubmit={(e) => { <form
e.preventDefault(); onSubmit={(e) => {
onSubmit({ e.preventDefault();
name, type, serialNumber, assignedTo, company, location, purchaseDate, status, onSubmit({
ipAddress, macAddress, warrantyExpiry, purchaseCost, rackLocation, vendor, model, name,
notes, tags: initial?.tags ?? [], visibility: initial?.visibility ?? 'all', type,
}); serialNumber,
}} className="space-y-4"> assignedTo,
assignedToContactId,
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>
<div><Label>Tip</Label> <Label>Nume echipament *</Label>
<Select value={type} onValueChange={(v) => setType(v as InventoryItemType)}> <Input
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger> value={name}
<SelectContent>{(Object.keys(TYPE_LABELS) as InventoryItemType[]).map((t) => (<SelectItem key={t} value={t}>{TYPE_LABELS[t]}</SelectItem>))}</SelectContent> 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>
<SelectContent>
{(Object.keys(TYPE_LABELS) as InventoryItemType[]).map((t) => (
<SelectItem key={t} value={t}>
{TYPE_LABELS[t]}
</SelectItem>
))}
</SelectContent>
</Select> </Select>
</div> </div>
</div> </div>
<div className="grid gap-4 sm:grid-cols-3"> <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>
<div><Label>Model</Label><Input value={model} onChange={(e) => setModel(e.target.value)} className="mt-1" /></div> <Label>Vendor</Label>
<div><Label>Număr serie</Label><Input value={serialNumber} onChange={(e) => setSerialNumber(e.target.value)} className="mt-1" /></div> <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> </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>
<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> <Label>Adresă IP</Label>
<div><Label>Atribuit</Label><Input value={assignedTo} onChange={(e) => setAssignedTo(e.target.value)} className="mt-1" /></div> <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 className="relative">
<Label>Atribuit</Label>
<Input
value={assignedTo}
onChange={(e) => {
setAssignedTo(e.target.value);
setAssignedToContactId("");
}}
onFocus={() => setAssignedToFocused(true)}
onBlur={() => setTimeout(() => setAssignedToFocused(false), 200)}
className="mt-1"
placeholder="Caută după nume..."
/>
{assignedToFocused && assignedToSuggestions.length > 0 && (
<div className="absolute z-10 mt-1 w-full rounded-md border bg-popover p-1 shadow-md">
{assignedToSuggestions.map((c) => (
<button
key={c.id}
type="button"
className="w-full rounded px-2 py-1.5 text-left text-sm hover:bg-accent"
onMouseDown={() => {
setAssignedTo(
c.company ? `${c.name} (${c.company})` : c.name,
);
setAssignedToContactId(c.id);
setAssignedToFocused(false);
}}
>
<span className="font-medium">{c.name}</span>
{c.company && (
<span className="ml-1 text-muted-foreground text-xs">
{c.company}
</span>
)}
</button>
))}
</div>
)}
</div>
</div> </div>
<div className="grid gap-4 sm:grid-cols-4"> <div className="grid gap-4 sm:grid-cols-4">
<div><Label>Companie</Label> <div>
<Select value={company} onValueChange={(v) => setCompany(v as CompanyId)}> <Label>Companie</Label>
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger> <Select
value={company}
onValueChange={(v) => setCompany(v as CompanyId)}
>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="beletage">Beletage</SelectItem> <SelectItem value="beletage">Beletage</SelectItem>
<SelectItem value="urban-switch">Urban Switch</SelectItem> <SelectItem value="urban-switch">Urban Switch</SelectItem>
@@ -230,24 +547,86 @@ function InventoryForm({ initial, onSubmit, onCancel }: {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div><Label>Locație / Cameră</Label><Input value={location} onChange={(e) => setLocation(e.target.value)} className="mt-1" /></div> <div>
<div><Label>Rack / Poziție</Label><Input value={rackLocation} onChange={(e) => setRackLocation(e.target.value)} className="mt-1" /></div> <Label>Locație / Cameră</Label>
<div><Label>Status</Label> <Input
<Select value={status} onValueChange={(v) => setStatus(v as InventoryItemStatus)}> value={location}
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger> onChange={(e) => setLocation(e.target.value)}
<SelectContent>{(Object.keys(STATUS_LABELS) as InventoryItemStatus[]).map((s) => (<SelectItem key={s} value={s}>{STATUS_LABELS[s]}</SelectItem>))}</SelectContent> 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> </Select>
</div> </div>
</div> </div>
<div className="grid gap-4 sm:grid-cols-3"> <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>
<div><Label>Cost achiziție (RON)</Label><Input type="number" value={purchaseCost} onChange={(e) => setPurchaseCost(e.target.value)} className="mt-1" /></div> <Label>Data achiziție</Label>
<div><Label>Expirare garanție</Label><Input type="date" value={warrantyExpiry} onChange={(e) => setWarrantyExpiry(e.target.value)} className="mt-1" /></div> <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>
<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}>
<Button type="submit">{initial ? 'Actualizează' : 'Adaugă'}</Button> Anulează
</Button>
<Button type="submit">{initial ? "Actualizează" : "Adaugă"}</Button>
</div> </div>
</form> </form>
); );

View File

@@ -1,22 +1,22 @@
import type { Visibility } from '@/core/module-registry/types'; import type { Visibility } from "@/core/module-registry/types";
import type { CompanyId } from '@/core/auth/types'; import type { CompanyId } from "@/core/auth/types";
export type InventoryItemType = export type InventoryItemType =
| 'laptop' | "laptop"
| 'desktop' | "desktop"
| 'monitor' | "monitor"
| 'printer' | "printer"
| 'phone' | "phone"
| 'tablet' | "tablet"
| 'network' | "network"
| 'peripheral' | "peripheral"
| 'other'; | "other";
export type InventoryItemStatus = export type InventoryItemStatus =
| 'active' | "active"
| 'in-repair' | "in-repair"
| 'storage' | "storage"
| 'decommissioned'; | "decommissioned";
export interface InventoryItem { export interface InventoryItem {
id: string; id: string;
@@ -24,6 +24,7 @@ export interface InventoryItem {
type: InventoryItemType; type: InventoryItemType;
serialNumber: string; serialNumber: string;
assignedTo: string; assignedTo: string;
assignedToContactId?: string;
company: CompanyId; company: CompanyId;
location: string; location: string;
purchaseDate: string; purchaseDate: string;