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
+144
View File
@@ -0,0 +1,144 @@
import { describe, expect, it } from 'vitest';
import type { Tx } from './types.js';
import { dashboard } from './analytics.js';
// Minimal in-memory tx double exercising the dashboard() aggregator.
// We only stub the calls dashboard() actually makes; other Prisma methods remain unimplemented.
function makeTx(args: {
partCount: number;
stateRows: { state: string; count: number; totalPrice: number }[];
parts: {
id: string;
state: string;
binId: string | null;
createdAt: Date;
manufacturerId: string;
}[];
openRepairs: number;
eolManufacturers: { id: string; name: string; eolDate: Date | null }[];
bins: { id: string; name: string; room: { name: string; site: { name: string } } }[];
}): Tx {
const tx = {
part: {
count: async () => args.partCount,
groupBy: async () =>
args.stateRows.map((s) => ({
state: s.state,
_count: { _all: s.count },
_sum: { price: s.totalPrice },
})),
findMany: async () => args.parts,
},
repairJob: {
count: async () => args.openRepairs,
},
manufacturer: {
findMany: async () => args.eolManufacturers,
},
bin: {
findMany: async () => args.bins,
},
};
return tx as unknown as Tx;
}
const now = new Date('2026-04-16T00:00:00.000Z');
const daysAgo = (n: number) => new Date(now.getTime() - n * 24 * 60 * 60 * 1000);
describe('analytics.dashboard', () => {
it('aggregates totals, state counts and open repairs', async () => {
const tx = makeTx({
partCount: 5,
stateRows: [
{ state: 'SPARE', count: 3, totalPrice: 1500 },
{ state: 'DEPLOYED', count: 2, totalPrice: 8000 },
],
parts: [],
openRepairs: 4,
eolManufacturers: [],
bins: [],
});
const r = await dashboard(tx);
expect(r.totalParts).toBe(5);
expect(r.openRepairs).toBe(4);
expect(r.byState).toEqual([
{ state: 'SPARE', count: 3, totalPrice: 1500 },
{ state: 'DEPLOYED', count: 2, totalPrice: 8000 },
]);
});
it('buckets parts by age correctly', async () => {
const tx = makeTx({
partCount: 4,
stateRows: [],
parts: [
{ id: 'p1', state: 'SPARE', binId: null, createdAt: daysAgo(10), manufacturerId: 'm' },
{ id: 'p2', state: 'SPARE', binId: null, createdAt: daysAgo(60), manufacturerId: 'm' },
{ id: 'p3', state: 'SPARE', binId: null, createdAt: daysAgo(400), manufacturerId: 'm' },
{ id: 'p4', state: 'SPARE', binId: null, createdAt: daysAgo(900), manufacturerId: 'm' },
],
openRepairs: 0,
eolManufacturers: [],
bins: [],
});
const r = await dashboard(tx);
const byLabel = Object.fromEntries(r.ageBuckets.map((b) => [b.label, b.count]));
expect(byLabel['030d']).toBe(1);
expect(byLabel['3190d']).toBe(1);
expect(byLabel['12y']).toBe(1);
expect(byLabel['2y+']).toBe(1);
// totals should match
expect(r.ageBuckets.reduce((s, b) => s + b.count, 0)).toBe(4);
});
it('ranks top bins and labels them site/room/bin', async () => {
const tx = makeTx({
partCount: 4,
stateRows: [],
parts: [
{ id: '1', state: 'SPARE', binId: 'b1', createdAt: daysAgo(1), manufacturerId: 'm' },
{ id: '2', state: 'SPARE', binId: 'b1', createdAt: daysAgo(1), manufacturerId: 'm' },
{ id: '3', state: 'SPARE', binId: 'b2', createdAt: daysAgo(1), manufacturerId: 'm' },
{ id: '4', state: 'SPARE', binId: null, createdAt: daysAgo(1), manufacturerId: 'm' },
],
openRepairs: 0,
eolManufacturers: [],
bins: [
{ id: 'b1', name: 'A1', room: { name: 'Lab', site: { name: 'HQ' } } },
{ id: 'b2', name: 'B2', room: { name: 'Lab', site: { name: 'HQ' } } },
],
});
const r = await dashboard(tx);
expect(r.topBins).toEqual([
{ binId: 'b1', label: 'HQ / Lab / A1', count: 2 },
{ binId: 'b2', label: 'HQ / Lab / B2', count: 1 },
]);
});
it('flags manufacturers whose EOL has passed and have deployed parts', async () => {
const tx = makeTx({
partCount: 3,
stateRows: [],
parts: [
{ id: '1', state: 'DEPLOYED', binId: null, createdAt: daysAgo(1), manufacturerId: 'm1' },
{ id: '2', state: 'DEPLOYED', binId: null, createdAt: daysAgo(1), manufacturerId: 'm1' },
{ id: '3', state: 'DEPLOYED', binId: null, createdAt: daysAgo(1), manufacturerId: 'm2' },
],
openRepairs: 0,
eolManufacturers: [
{ id: 'm1', name: 'Acme', eolDate: daysAgo(30) },
{ id: 'm2', name: 'Beta', eolDate: daysAgo(10) },
{ id: 'm3', name: 'Gamma', eolDate: daysAgo(5) },
],
bins: [],
});
const r = await dashboard(tx);
expect(r.deployedPastEol.map((m) => m.name)).toEqual(['Acme', 'Beta']);
expect(r.deployedPastEol[0]).toMatchObject({ manufacturerId: 'm1', deployedCount: 2 });
expect(r.deployedPastEol[1]).toMatchObject({ manufacturerId: 'm2', deployedCount: 1 });
});
});