chore: initial Vector 2.0 monorepo
CI / Lint · Typecheck · Test · Build (push) Failing after 5m41s
CI / Playwright (smoke) (push) Has been skipped

Ground-up TypeScript rewrite of the Vector hardware parts inventory
system. Ships the full roadmap (Phases 0-8) in one initial commit:

- pnpm + Turbo monorepo: apps/{api,web,e2e}, packages/{db,shared,ui,config}
- Express 5 + Prisma 5 + zod validation + JWT w/ refresh-token rotation
- React 19 + Vite + shadcn/ui + TanStack Query/Table + nuqs URL state
- Repair/RMA, tags, bulk ops, saved views, CSV audit export
- Analytics dashboard on Recharts + EOL tracking
- Signed webhook subscriptions (HMAC-SHA256) with in-process emitter
- Vitest unit tests (shared schemas, api services/helpers) + Playwright skeleton
- Gitea Actions CI (lint, typecheck, test+coverage, build) + Renovate

Deferred follow-ups: Postgres cutover (data-migration script ready),
BullMQ worker for webhook delivery, @react-pdf PDF export, CSV import wizard.
This commit is contained in:
2026-04-16 20:52:32 -04:00
commit 7c0d422228
216 changed files with 19393 additions and 0 deletions
+345
View File
@@ -0,0 +1,345 @@
import { useMemo, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import type { ColumnDef } from '@tanstack/react-table';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { parseAsString } from 'nuqs';
import { toast } from 'sonner';
import { Edit, MoreHorizontal, Package, Plus, Trash2 } from 'lucide-react';
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@vector/ui';
import { PageHeader } from '../components/layout/PageHeader.js';
import { DataTable } from '../components/data-table/DataTable.js';
import { PartStateBadge, partStateOptions } from '../components/parts/PartStateBadge.js';
import { PartFormDialog } from '../components/parts/PartFormDialog.js';
import { PartBulkStateDialog } from '../components/parts/PartBulkStateDialog.js';
import { ConfirmDialog } from '../components/ConfirmDialog.js';
import { listParts, deletePart } from '../lib/api/parts.js';
import { listManufacturers } from '../lib/api/manufacturers.js';
import { listTags } from '../lib/api/tags.js';
import { ApiRequestError } from '../lib/api/client.js';
import type { Part } from '../lib/api/types.js';
import { queryKeys } from '../lib/queryKeys.js';
import { useAuth } from '../contexts/AuthContext.js';
type PartsFilters = {
state: string | null;
manufacturerId: string | null;
tagId: string | null;
};
const filterParsers = {
state: parseAsString,
manufacturerId: parseAsString,
tagId: parseAsString,
};
const ALL = '__all__';
export default function Parts() {
const { user } = useAuth();
const navigate = useNavigate();
const queryClient = useQueryClient();
const isAdmin = user?.role === 'ADMIN';
const [createOpen, setCreateOpen] = useState(false);
const [editing, setEditing] = useState<Part | null>(null);
const [deleting, setDeleting] = useState<Part | null>(null);
const [bulkIds, setBulkIds] = useState<string[] | null>(null);
const [clearSelection, setClearSelection] = useState<(() => void) | null>(null);
const manufacturers = useQuery({
queryKey: queryKeys.manufacturers.list({ pageSize: 100 }),
queryFn: () => listManufacturers({ pageSize: 100 }),
});
const tagsQuery = useQuery({
queryKey: queryKeys.tags.list({ pageSize: 100 }),
queryFn: () => listTags({ pageSize: 100 }),
});
const deleteMutation = useMutation({
mutationFn: (id: string) => deletePart(id),
onSuccess: () => {
toast.success('Part deleted');
queryClient.invalidateQueries({ queryKey: queryKeys.parts.all });
setDeleting(null);
},
onError: (err) => {
toast.error(err instanceof ApiRequestError ? err.body.message : 'Delete failed');
},
});
const columns = useMemo<ColumnDef<Part>[]>(
() => [
{
accessorKey: 'serialNumber',
header: 'Serial',
cell: ({ row }) => (
<Link
to={`/parts/${row.original.id}`}
className="font-mono text-xs font-medium text-foreground hover:underline"
>
{row.original.serialNumber}
</Link>
),
},
{
accessorKey: 'mpn',
header: 'MPN',
cell: ({ row }) => <span className="text-sm">{row.original.mpn}</span>,
},
{
id: 'manufacturer',
header: 'Manufacturer',
cell: ({ row }) => (
<span className="text-sm text-muted-foreground">{row.original.manufacturer.name}</span>
),
},
{
accessorKey: 'state',
header: 'State',
cell: ({ row }) => <PartStateBadge state={row.original.state} />,
},
{
id: 'location',
header: 'Location',
cell: ({ row }) => {
const path = row.original.bin?.fullPath;
return path ? (
<span className="text-xs font-mono text-muted-foreground">{path}</span>
) : (
<span className="text-xs text-muted-foreground italic">Unassigned</span>
);
},
},
{
accessorKey: 'price',
header: 'Price',
cell: ({ row }) =>
row.original.price != null ? (
<span className="text-sm tabular-nums">${row.original.price.toFixed(2)}</span>
) : (
<span className="text-xs text-muted-foreground"></span>
),
},
{
id: 'actions',
header: () => <span className="sr-only">Actions</span>,
size: 40,
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-7 w-7">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem onSelect={() => navigate(`/parts/${row.original.id}`)}>
View
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setEditing(row.original)}>
<Edit className="h-3.5 w-3.5" />
Edit
</DropdownMenuItem>
{isAdmin && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() => setDeleting(row.original)}
className="text-destructive focus:text-destructive"
>
<Trash2 className="h-3.5 w-3.5" />
Delete
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
),
},
],
[navigate, isAdmin],
);
return (
<div className="space-y-5">
<PageHeader
title="Parts"
description="Search, filter, and manage every tracked part."
actions={
isAdmin && (
<Button onClick={() => setCreateOpen(true)}>
<Plus className="h-4 w-4" />
New part
</Button>
)
}
/>
<DataTable<Part, PartsFilters>
columns={columns}
getRowId={(p) => p.id}
queryKey={(params) =>
queryKeys.parts.list({
page: params.page,
pageSize: params.pageSize,
q: params.q,
sort: params.sort,
state: params.filters.state,
manufacturerId: params.filters.manufacturerId,
tagId: params.filters.tagId,
})
}
queryFn={(params) =>
listParts({
page: params.page,
pageSize: params.pageSize,
q: params.q,
sort: params.sort,
state: params.filters.state ?? undefined,
manufacturerId: params.filters.manufacturerId ?? undefined,
tagId: params.filters.tagId ?? undefined,
})
}
filterParsers={filterParsers}
searchPlaceholder="Search serial, MPN, notes…"
enableSelection={isAdmin}
emptyState={
<div className="flex flex-col items-center gap-1 py-8 text-muted-foreground">
<Package className="h-6 w-6" />
<span className="text-sm">No parts match these filters.</span>
</div>
}
toolbar={({ filters, setFilter }) => (
<PartsFilters
manufacturers={manufacturers.data?.data ?? []}
tags={tagsQuery.data?.data ?? []}
state={filters.state ?? ALL}
manufacturerId={filters.manufacturerId ?? ALL}
tagId={filters.tagId ?? ALL}
onState={(v) => setFilter('state', v === ALL ? null : v)}
onManufacturer={(v) => setFilter('manufacturerId', v === ALL ? null : v)}
onTag={(v) => setFilter('tagId', v === ALL ? null : v)}
/>
)}
bulkActions={(ids, clear) => (
<Button
variant="outline"
size="sm"
onClick={() => {
setBulkIds(ids);
setClearSelection(() => clear);
}}
>
Bulk edit
</Button>
)}
/>
<PartFormDialog open={createOpen} onOpenChange={setCreateOpen} />
<PartFormDialog
open={Boolean(editing)}
onOpenChange={(o) => !o && setEditing(null)}
part={editing}
/>
<PartBulkStateDialog
open={Boolean(bulkIds)}
onOpenChange={(o) => {
if (!o) setBulkIds(null);
}}
partIds={bulkIds ?? []}
onDone={() => clearSelection?.()}
/>
<ConfirmDialog
open={Boolean(deleting)}
onOpenChange={(o) => !o && setDeleting(null)}
title="Delete part?"
description={
deleting
? `Permanently remove part ${deleting.serialNumber}. Its history will be removed too.`
: undefined
}
confirmLabel="Delete"
destructive
pending={deleteMutation.isPending}
onConfirm={() => deleting && deleteMutation.mutate(deleting.id)}
/>
</div>
);
}
interface PartsFiltersProps {
manufacturers: { id: string; name: string }[];
tags: { id: string; name: string }[];
state: string;
manufacturerId: string;
tagId: string;
onState: (v: string) => void;
onManufacturer: (v: string) => void;
onTag: (v: string) => void;
}
function PartsFilters({
manufacturers,
tags,
state,
manufacturerId,
tagId,
onState,
onManufacturer,
onTag,
}: PartsFiltersProps) {
return (
<div className="flex items-center gap-2">
<Select value={state} onValueChange={onState}>
<SelectTrigger className="h-8 w-36 text-xs">
<SelectValue placeholder="State" />
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL}>All states</SelectItem>
{partStateOptions.map((o) => (
<SelectItem key={o.value} value={o.value}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={manufacturerId} onValueChange={onManufacturer}>
<SelectTrigger className="h-8 w-48 text-xs">
<SelectValue placeholder="Manufacturer" />
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL}>All manufacturers</SelectItem>
{manufacturers.map((m) => (
<SelectItem key={m.id} value={m.id}>
{m.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={tagId} onValueChange={onTag}>
<SelectTrigger className="h-8 w-36 text-xs">
<SelectValue placeholder="Tag" />
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL}>All tags</SelectItem>
{tags.map((t) => (
<SelectItem key={t.id} value={t.id}>
{t.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
}