feat(it-inventory): link assignedTo to Address Book contacts with autocomplete
This commit is contained in:
@@ -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>
|
||||||
|
<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">
|
||||||
|
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">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>
|
||||||
<th className="px-3 py-2 text-right font-medium">Acțiuni</th>
|
<th className="px-3 py-2 text-right font-medium">
|
||||||
</tr></thead>
|
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 că vrei să ștergi acest echipament? Acțiunea este ireversibilă.</p>
|
<DialogTitle>Confirmare ștergere</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<p className="text-sm">
|
||||||
|
Ești sigur că vrei să ș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
|
||||||
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onSubmit({
|
onSubmit({
|
||||||
name, type, serialNumber, assignedTo, company, location, purchaseDate, status,
|
name,
|
||||||
ipAddress, macAddress, warrantyExpiry, purchaseCost, rackLocation, vendor, model,
|
type,
|
||||||
notes, tags: initial?.tags ?? [], visibility: initial?.visibility ?? 'all',
|
serialNumber,
|
||||||
|
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">
|
}}
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user