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
@@ -0,0 +1,325 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { z } from 'zod';
import { Loader2 } from 'lucide-react';
import { toast } from 'sonner';
import { PartState } from '@vector/shared';
import {
Button,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Textarea,
} from '@vector/ui';
import { listManufacturers } from '../../lib/api/manufacturers.js';
import { listBins } from '../../lib/api/bins.js';
import { createPart, updatePart } from '../../lib/api/parts.js';
import type { Part } from '../../lib/api/types.js';
import { ApiRequestError } from '../../lib/api/client.js';
import { queryKeys } from '../../lib/queryKeys.js';
import { partStateOptions } from './PartStateBadge.js';
// Schema reflects the server's CreatePartRequest but keeps strings for the form, letting the
// submit handler coerce to the network shape.
const PartFormSchema = z.object({
serialNumber: z.string().min(1, 'Required').max(128),
mpn: z.string().min(1, 'Required').max(128),
manufacturerId: z.string().uuid('Select a manufacturer'),
state: PartState,
binId: z.string().optional(), // '' = none
price: z.string().optional(), // empty string = null
notes: z.string().max(4096).optional(),
});
type PartFormValues = z.infer<typeof PartFormSchema>;
const UNASSIGNED = '__none__';
interface PartFormDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
part?: Part | null;
}
export function PartFormDialog({ open, onOpenChange, part }: PartFormDialogProps) {
const editing = Boolean(part);
const queryClient = useQueryClient();
const form = useForm<PartFormValues>({
resolver: zodResolver(PartFormSchema),
defaultValues: {
serialNumber: '',
mpn: '',
manufacturerId: '',
state: 'SPARE',
binId: '',
price: '',
notes: '',
},
});
useEffect(() => {
if (!open) return;
form.reset(
part
? {
serialNumber: part.serialNumber,
mpn: part.mpn,
manufacturerId: part.manufacturerId,
state: part.state,
binId: part.binId ?? '',
price: part.price != null ? String(part.price) : '',
notes: part.notes ?? '',
}
: {
serialNumber: '',
mpn: '',
manufacturerId: '',
state: 'SPARE',
binId: '',
price: '',
notes: '',
},
);
}, [open, part, form]);
const manufacturers = useQuery({
queryKey: queryKeys.manufacturers.list({ pageSize: 100 }),
queryFn: () => listManufacturers({ pageSize: 100 }),
enabled: open,
});
const bins = useQuery({
queryKey: queryKeys.bins.list({ pageSize: 100 }),
queryFn: () => listBins({ pageSize: 100 }),
enabled: open,
});
const mutation = useMutation({
mutationFn: async (values: PartFormValues) => {
const payload = {
serialNumber: values.serialNumber,
mpn: values.mpn,
manufacturerId: values.manufacturerId,
state: values.state,
binId: values.binId ? values.binId : null,
price: values.price === '' ? null : Number(values.price),
notes: values.notes ? values.notes : null,
};
return editing && part
? updatePart(part.id, payload)
: createPart(payload);
},
onSuccess: (saved) => {
toast.success(editing ? 'Part updated' : 'Part created');
queryClient.invalidateQueries({ queryKey: queryKeys.parts.all });
if (editing) {
queryClient.setQueryData(queryKeys.parts.detail(saved.id), saved);
}
onOpenChange(false);
},
onError: (err) => {
const msg =
err instanceof ApiRequestError ? err.body.message : 'Could not save part';
toast.error(msg);
},
});
const onSubmit = (values: PartFormValues) => {
if (values.price !== '' && values.price !== undefined) {
const n = Number(values.price);
if (!Number.isFinite(n) || n < 0) {
form.setError('price', { message: 'Must be a non-negative number' });
return;
}
}
mutation.mutate(values);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-xl">
<DialogHeader>
<DialogTitle>{editing ? 'Edit part' : 'New part'}</DialogTitle>
<DialogDescription>
{editing
? 'Update this part. Changes are logged to its history.'
: 'Add a part to inventory. Serial numbers must be unique.'}
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<FormField
control={form.control}
name="serialNumber"
render={({ field }) => (
<FormItem>
<FormLabel>Serial</FormLabel>
<FormControl>
<Input autoFocus {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="mpn"
render={({ field }) => (
<FormItem>
<FormLabel>MPN</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="manufacturerId"
render={({ field }) => (
<FormItem>
<FormLabel>Manufacturer</FormLabel>
<Select value={field.value} onValueChange={field.onChange}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select manufacturer" />
</SelectTrigger>
</FormControl>
<SelectContent>
{manufacturers.data?.data.map((m) => (
<SelectItem key={m.id} value={m.id}>
{m.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-3">
<FormField
control={form.control}
name="state"
render={({ field }) => (
<FormItem>
<FormLabel>State</FormLabel>
<Select value={field.value} onValueChange={field.onChange}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{partStateOptions.map((o) => (
<SelectItem key={o.value} value={o.value}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="price"
render={({ field }) => (
<FormItem>
<FormLabel>Price (USD)</FormLabel>
<FormControl>
<Input type="number" step="0.01" min="0" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="binId"
render={({ field }) => (
<FormItem>
<FormLabel>Location</FormLabel>
<Select
value={field.value ? field.value : UNASSIGNED}
onValueChange={(v) => field.onChange(v === UNASSIGNED ? '' : v)}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Unassigned" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value={UNASSIGNED}>Unassigned</SelectItem>
{bins.data?.data.map((b) => (
<SelectItem key={b.id} value={b.id}>
{b.fullPath ?? b.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="notes"
render={({ field }) => (
<FormItem>
<FormLabel>Notes</FormLabel>
<FormControl>
<Textarea rows={3} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={mutation.isPending}
>
Cancel
</Button>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending && <Loader2 className="h-4 w-4 animate-spin" />}
{editing ? 'Save changes' : 'Create part'}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}