import { useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { AlertTriangle, ArrowLeft, Edit, Trash2 } from 'lucide-react';
import { toast } from 'sonner';
import {
Button,
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
Separator,
Skeleton,
} from '@vector/ui';
import { getPart, deletePart } from '../lib/api/parts.js';
import { ApiRequestError } from '../lib/api/client.js';
import { queryKeys } from '../lib/queryKeys.js';
import { useAuth } from '../contexts/AuthContext.js';
import { PartStateBadge } from '../components/parts/PartStateBadge.js';
import { PartEventTimeline } from '../components/parts/PartEventTimeline.js';
import { PartFormDialog } from '../components/parts/PartFormDialog.js';
import { TagPicker } from '../components/tags/TagPicker.js';
import { ConfirmDialog } from '../components/ConfirmDialog.js';
function DetailRow({ label, value }: { label: string; value: React.ReactNode }) {
return (
{label}
{value}
);
}
export default function PartDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { user } = useAuth();
const isAdmin = user?.role === 'ADMIN';
const [editOpen, setEditOpen] = useState(false);
const [confirmDelete, setConfirmDelete] = useState(false);
const { data: part, isPending, isError, error } = useQuery({
queryKey: queryKeys.parts.detail(id!),
queryFn: () => getPart(id!),
enabled: Boolean(id),
});
const deleteMutation = useMutation({
mutationFn: () => deletePart(id!),
onSuccess: () => {
toast.success('Part deleted');
queryClient.invalidateQueries({ queryKey: queryKeys.parts.all });
navigate('/parts', { replace: true });
},
onError: (err) => {
toast.error(err instanceof ApiRequestError ? err.body.message : 'Delete failed');
},
});
if (isPending) {
return (
);
}
if (isError || !part) {
const msg = error instanceof ApiRequestError ? error.body.message : 'Part not found.';
return (
Part unavailable
{msg}
);
}
const eolDate = part.partModel.eolDate ? new Date(part.partModel.eolDate) : null;
const pastEol = eolDate ? eolDate.getTime() < Date.now() : false;
return (
{part.serialNumber}
{part.manufacturer.name} · {part.partModel.mpn}
{isAdmin && (
)}
{pastEol && eolDate && (
{part.partModel.mpn} reached EOL {eolDate.toLocaleDateString()}.
{' '}
Plan a replacement for this part.
)}
Summary
{part.serialNumber}} />
{part.manufacturer.name}
}
/>
} />
{part.host.assetId} / {part.host.name}
) : part.custodian ? (
Custody: {part.custodian.username}
) : part.bin?.fullPath ? (
{part.bin.fullPath}
) : (
Unassigned
)
}
/>
${part.price.toFixed(2)}
) : (
—
)
}
/>
{eolDate.toLocaleDateString()}
) : (
—
)
}
/>
{part.notes && (
<>
>
)}
History
Every field change is logged here.
deleteMutation.mutate()}
/>
);
}