feat: rework EOL, repairs, and hosts for real workflow
Four domain-model changes driven by exercising the deployed 2.0 build: - EOL moves from manufacturer to MPN via new PartModel catalog table, so alerts fire on the thing that actually ages. - Repairs re-home to Host (required hostId + problem text) with an optional RepairJobPart join for affected parts; drop Part.replacementPartId. - New /repairs/:id detail page with editable problem, part list, and a RepairComment thread (REPAIR_COMMENTED events fan out to each problem part's timeline). - Host.assetId (required, unique) surfaces prominently on the repair page so techs can confirm they're touching the right box. Single destructive migration reshapes existing dev data. All 7 packages typecheck clean; 30 API tests pass (9 new covering host membership, upsertByMpn idempotency + race, assetId 409, comment userId stamping). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import type { CreateHostRequest, UpdateHostRequest } from '@vector/shared';
|
||||
import { api } from './client.js';
|
||||
import { getList } from './paginated.js';
|
||||
import type { Host } from './types.js';
|
||||
import type { Host, Part } from './types.js';
|
||||
|
||||
export type HostListFilters = {
|
||||
page?: number;
|
||||
@@ -18,6 +18,11 @@ export async function getHost(id: string): Promise<Host> {
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function listHostDeployedParts(id: string): Promise<Part[]> {
|
||||
const res = await api.get<Part[]>(`/hosts/${id}/deployed-parts`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function createHost(input: CreateHostRequest): Promise<Host> {
|
||||
const res = await api.post<Host>('/hosts', input);
|
||||
return res.data;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import type {
|
||||
CreatePartModelRequest,
|
||||
UpdatePartModelRequest,
|
||||
} from '@vector/shared';
|
||||
import { api } from './client.js';
|
||||
import { getList } from './paginated.js';
|
||||
import type { PartModel } from './types.js';
|
||||
|
||||
export type PartModelListFilters = {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
manufacturerId?: string;
|
||||
q?: string;
|
||||
eolBefore?: string;
|
||||
};
|
||||
|
||||
export function listPartModels(filters: PartModelListFilters = {}) {
|
||||
return getList<PartModel>('/part-models', filters);
|
||||
}
|
||||
|
||||
export async function getPartModel(id: string): Promise<PartModel> {
|
||||
const res = await api.get<PartModel>(`/part-models/${id}`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function createPartModel(input: CreatePartModelRequest): Promise<PartModel> {
|
||||
const res = await api.post<PartModel>('/part-models', input);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function updatePartModel(
|
||||
id: string,
|
||||
input: UpdatePartModelRequest,
|
||||
): Promise<PartModel> {
|
||||
const res = await api.patch<PartModel>(`/part-models/${id}`, input);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function deletePartModel(id: string): Promise<void> {
|
||||
await api.delete(`/part-models/${id}`);
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
import type {
|
||||
CreateRepairCommentRequest,
|
||||
CreateRepairJobRequest,
|
||||
RepairStatus,
|
||||
UpdateRepairJobRequest,
|
||||
} from '@vector/shared';
|
||||
import { api } from './client.js';
|
||||
import { getList } from './paginated.js';
|
||||
import type { RepairJob } from './types.js';
|
||||
import type { RepairComment, RepairJob } from './types.js';
|
||||
|
||||
export type RepairListFilters = {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
status?: RepairStatus;
|
||||
partId?: string;
|
||||
hostId?: string;
|
||||
problemPartId?: string;
|
||||
assigneeId?: string;
|
||||
openOnly?: boolean;
|
||||
};
|
||||
@@ -26,11 +27,6 @@ export async function getRepair(id: string): Promise<RepairJob> {
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function listRepairsForPart(partId: string): Promise<RepairJob[]> {
|
||||
const res = await api.get<RepairJob[]>(`/parts/${partId}/repairs`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function createRepair(input: CreateRepairJobRequest): Promise<RepairJob> {
|
||||
const res = await api.post<RepairJob>('/repairs', input);
|
||||
return res.data;
|
||||
@@ -47,3 +43,18 @@ export async function updateRepair(
|
||||
export async function deleteRepair(id: string): Promise<void> {
|
||||
await api.delete(`/repairs/${id}`);
|
||||
}
|
||||
|
||||
export function listRepairComments(
|
||||
id: string,
|
||||
filters: { page?: number; pageSize?: number } = {},
|
||||
) {
|
||||
return getList<RepairComment>(`/repairs/${id}/comments`, filters);
|
||||
}
|
||||
|
||||
export async function addRepairComment(
|
||||
id: string,
|
||||
input: CreateRepairCommentRequest,
|
||||
): Promise<RepairComment> {
|
||||
const res = await api.post<RepairComment>(`/repairs/${id}/comments`, input);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
@@ -6,11 +6,22 @@ import type { PartEventType, PartState, RepairStatus, Role } from '@vector/share
|
||||
export interface Manufacturer {
|
||||
id: string;
|
||||
name: string;
|
||||
eolDate: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface PartModel {
|
||||
id: string;
|
||||
manufacturerId: string;
|
||||
mpn: string;
|
||||
eolDate: string | null;
|
||||
notes: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
manufacturer?: Manufacturer;
|
||||
_count?: { parts: number };
|
||||
}
|
||||
|
||||
export interface Site {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -42,18 +53,20 @@ export interface BinWithPath extends Bin {
|
||||
export interface Part {
|
||||
id: string;
|
||||
serialNumber: string;
|
||||
mpn: string;
|
||||
partModelId: string;
|
||||
manufacturerId: string;
|
||||
price: number | null;
|
||||
state: PartState;
|
||||
binId: string | null;
|
||||
categoryId: string | null;
|
||||
replacementPartId: string | null;
|
||||
hostId: string | null;
|
||||
notes: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
manufacturer: Manufacturer;
|
||||
partModel: PartModel;
|
||||
bin: BinWithPath | null;
|
||||
host: Host | null;
|
||||
}
|
||||
|
||||
export interface PartEvent {
|
||||
@@ -79,6 +92,7 @@ export interface User {
|
||||
|
||||
export interface Host {
|
||||
id: string;
|
||||
assetId: string;
|
||||
name: string;
|
||||
location: string | null;
|
||||
notes: string | null;
|
||||
@@ -101,20 +115,36 @@ export interface Category {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface RepairJobProblemPart {
|
||||
repairJobId: string;
|
||||
partId: string;
|
||||
createdAt: string;
|
||||
part: Part;
|
||||
}
|
||||
|
||||
export interface RepairJob {
|
||||
id: string;
|
||||
partId: string;
|
||||
hostId: string | null;
|
||||
hostId: string;
|
||||
assigneeId: string | null;
|
||||
status: RepairStatus;
|
||||
problem: string;
|
||||
notes: string | null;
|
||||
openedAt: string;
|
||||
closedAt: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
part: Part;
|
||||
host: Host | null;
|
||||
host: Host;
|
||||
assignee: Pick<User, 'id' | 'username' | 'email' | 'role'> | null;
|
||||
problemParts: RepairJobProblemPart[];
|
||||
}
|
||||
|
||||
export interface RepairComment {
|
||||
id: string;
|
||||
repairJobId: string;
|
||||
userId: string | null;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
user: Pick<User, 'id' | 'username'> | null;
|
||||
}
|
||||
|
||||
export interface SavedView {
|
||||
|
||||
@@ -47,12 +47,20 @@ export const queryKeys = {
|
||||
list: (filters?: Record<string, unknown>) =>
|
||||
[...queryKeys.hosts.all, 'list', filters ?? {}] as const,
|
||||
detail: (id: string) => [...queryKeys.hosts.all, 'detail', id] as const,
|
||||
deployedParts: (id: string) => [...queryKeys.hosts.all, 'deployed-parts', id] as const,
|
||||
},
|
||||
repairs: {
|
||||
all: ['repairs'] as const,
|
||||
list: (filters?: Record<string, unknown>) =>
|
||||
[...queryKeys.repairs.all, 'list', filters ?? {}] as const,
|
||||
detail: (id: string) => [...queryKeys.repairs.all, 'detail', id] as const,
|
||||
comments: (id: string) => [...queryKeys.repairs.all, 'comments', id] as const,
|
||||
},
|
||||
partModels: {
|
||||
all: ['part-models'] as const,
|
||||
list: (filters?: Record<string, unknown>) =>
|
||||
[...queryKeys.partModels.all, 'list', filters ?? {}] as const,
|
||||
detail: (id: string) => [...queryKeys.partModels.all, 'detail', id] as const,
|
||||
},
|
||||
tags: {
|
||||
all: ['tags'] as const,
|
||||
|
||||
Reference in New Issue
Block a user