feat: laundry-list polish pass
Seven bundled improvements: - PartModel combobox on Add Part + Log Repair (known MPN auto-fills; unknown reveals manufacturer picker for catalog upsert). - Host lifecycle: state (DEPLOYED/DEGRADED/TESTING) and stack (PRODUCTION/VETTING) fields, driven by external clients via the API. - Locations page redesigned as a 2-pane tree + bin grid with breadcrumb. - PENDING_REPAIR custody state: tech takes a SPARE into custody for a future swap; resolves to DEPLOYED via Repair or back to SPARE via a bin-required drop-off. - Move Category from Part to PartModel; seed common categories (GPU/RAM/SSD/HDD/NIC/CPU/PSU/MOBO). Parts table gets a Category column and filter sourced from the model. - Fix Deployed Value 100x bug on the Dashboard (price is stored as dollars, not cents). - PartModels table shows "No" instead of "--" when destroyOnFail=false. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -78,7 +78,7 @@ export async function log(
|
||||
): Promise<RepairWithRelations> {
|
||||
const host = await resolveHost(tx, input);
|
||||
|
||||
// 1. Resolve replacement — must exist, must be SPARE.
|
||||
// 1. Resolve replacement — must exist; must be SPARE, or a PENDING_REPAIR held by the actor.
|
||||
const replacement = await tx.part.findUnique({
|
||||
where: { serialNumber: input.replacementSerial },
|
||||
include: { partModel: true },
|
||||
@@ -86,9 +86,11 @@ export async function log(
|
||||
if (!replacement) {
|
||||
throw errors.badRequest(`Replacement part ${input.replacementSerial} not found`);
|
||||
}
|
||||
if (replacement.state !== 'SPARE') {
|
||||
const heldForRepairByActor =
|
||||
replacement.state === 'PENDING_REPAIR' && replacement.custodianId === actor.id;
|
||||
if (replacement.state !== 'SPARE' && !heldForRepairByActor) {
|
||||
throw errors.badRequest(
|
||||
`Replacement part ${input.replacementSerial} is ${replacement.state}, must be SPARE`,
|
||||
`Replacement part ${input.replacementSerial} is ${replacement.state}, must be SPARE or PENDING_REPAIR held by you`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,10 +106,22 @@ export async function log(
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const pm = await partModelsSvc.upsertByMpn(tx, {
|
||||
manufacturerId: input.brokenManufacturerId,
|
||||
mpn: input.brokenMpn,
|
||||
});
|
||||
let pm: { id: string; manufacturerId: string };
|
||||
if (input.brokenPartModelId) {
|
||||
const existing = await tx.partModel.findUnique({ where: { id: input.brokenPartModelId } });
|
||||
if (!existing) throw errors.badRequest('Broken part model does not exist');
|
||||
pm = { id: existing.id, manufacturerId: existing.manufacturerId };
|
||||
} else {
|
||||
if (!input.brokenMpn || !input.brokenManufacturerId) {
|
||||
throw errors.badRequest(
|
||||
'Provide brokenPartModelId or both brokenMpn and brokenManufacturerId',
|
||||
);
|
||||
}
|
||||
pm = await partModelsSvc.upsertByMpn(tx, {
|
||||
manufacturerId: input.brokenManufacturerId,
|
||||
mpn: input.brokenMpn,
|
||||
});
|
||||
}
|
||||
const created = await tx.part.create({
|
||||
data: {
|
||||
serialNumber: input.brokenSerial,
|
||||
|
||||
Reference in New Issue
Block a user