manufacturing and gantt

This commit is contained in:
2026-03-15 12:11:46 -05:00
parent a9d31730f8
commit 16582d3cea
26 changed files with 1614 additions and 75 deletions

View File

@@ -3,6 +3,7 @@ import type {
InventoryBomLineInput,
InventoryItemDetailDto,
InventoryItemInput,
InventoryItemOperationDto,
InventoryStockBalanceDto,
WarehouseDetailDto,
WarehouseInput,
@@ -34,6 +35,20 @@ type BomLineRecord = {
};
};
type OperationRecord = {
id: string;
setupMinutes: number;
runMinutesPerUnit: number;
moveMinutes: number;
notes: string;
position: number;
station: {
id: string;
code: string;
name: string;
};
};
type InventoryDetailRecord = {
id: string;
sku: string;
@@ -50,6 +65,7 @@ type InventoryDetailRecord = {
createdAt: Date;
updatedAt: Date;
bomLines: BomLineRecord[];
operations: OperationRecord[];
inventoryTransactions: InventoryTransactionRecord[];
};
@@ -106,6 +122,21 @@ function mapBomLine(record: BomLineRecord): InventoryBomLineDto {
};
}
function mapOperation(record: OperationRecord): InventoryItemOperationDto {
return {
id: record.id,
stationId: record.station.id,
stationCode: record.station.code,
stationName: record.station.name,
setupMinutes: record.setupMinutes,
runMinutesPerUnit: record.runMinutesPerUnit,
moveMinutes: record.moveMinutes,
estimatedMinutesPerUnit: record.setupMinutes + record.runMinutesPerUnit + record.moveMinutes,
position: record.position,
notes: record.notes,
};
}
function mapWarehouseLocation(record: WarehouseLocationRecord): WarehouseLocationDto {
return {
id: record.id,
@@ -225,6 +256,7 @@ function mapDetail(record: InventoryDetailRecord): InventoryItemDetailDto {
notes: record.notes,
createdAt: record.createdAt.toISOString(),
bomLines: record.bomLines.slice().sort((a, b) => a.position - b.position).map(mapBomLine),
operations: record.operations.slice().sort((a, b) => a.position - b.position).map(mapOperation),
onHandQuantity: stockBalances.reduce((sum, balance) => sum + balance.quantityOnHand, 0),
stockBalances,
recentTransactions,
@@ -298,6 +330,19 @@ function normalizeBomLines(bomLines: InventoryBomLineInput[]) {
.filter((line) => line.componentItemId.trim().length > 0);
}
function normalizeOperations(operations: InventoryItemInput["operations"]) {
return operations
.map((operation, index) => ({
stationId: operation.stationId,
setupMinutes: Number(operation.setupMinutes),
runMinutesPerUnit: Number(operation.runMinutesPerUnit),
moveMinutes: Number(operation.moveMinutes),
notes: operation.notes,
position: operation.position ?? (index + 1) * 10,
}))
.filter((operation) => operation.stationId.trim().length > 0);
}
function normalizeWarehouseLocations(locations: WarehouseLocationInput[]) {
return locations
.map((location) => ({
@@ -346,6 +391,49 @@ async function validateBomLines(parentItemId: string | null, bomLines: Inventory
return { ok: true as const, bomLines: normalized };
}
async function validateOperations(type: InventoryItemType, operations: InventoryItemInput["operations"]) {
const normalized = normalizeOperations(operations);
if (type === "ASSEMBLY" || type === "MANUFACTURED") {
if (normalized.length === 0) {
return { ok: false as const, reason: "Assembly and manufactured items require at least one station operation." };
}
} else if (normalized.length > 0) {
return { ok: false as const, reason: "Only assembly and manufactured items may define station operations." };
}
if (normalized.some((operation) => operation.setupMinutes < 0 || operation.runMinutesPerUnit < 0 || operation.moveMinutes < 0)) {
return { ok: false as const, reason: "Operation times must be zero or greater." };
}
if (normalized.some((operation) => operation.setupMinutes + operation.runMinutesPerUnit + operation.moveMinutes <= 0)) {
return { ok: false as const, reason: "Each operation must have at least some planned time." };
}
const stationIds = [...new Set(normalized.map((operation) => operation.stationId))];
if (stationIds.length === 0) {
return { ok: true as const, operations: normalized };
}
const existingStations = await prisma.manufacturingStation.findMany({
where: {
id: {
in: stationIds,
},
isActive: true,
},
select: {
id: true,
},
});
if (existingStations.length !== stationIds.length) {
return { ok: false as const, reason: "One or more selected stations do not exist or are inactive." };
}
return { ok: true as const, operations: normalized };
}
export async function listInventoryItems(filters: InventoryListFilters = {}) {
const items = await prisma.inventoryItem.findMany({
where: buildWhereClause(filters),
@@ -404,6 +492,18 @@ export async function getInventoryItemById(itemId: string) {
},
orderBy: [{ position: "asc" }, { createdAt: "asc" }],
},
operations: {
include: {
station: {
select: {
id: true,
code: true,
name: true,
},
},
},
orderBy: [{ position: "asc" }, { createdAt: "asc" }],
},
inventoryTransactions: {
include: {
warehouse: {
@@ -511,6 +611,10 @@ export async function createInventoryItem(payload: InventoryItemInput) {
if (!validatedBom.ok) {
return null;
}
const validatedOperations = await validateOperations(payload.type, payload.operations);
if (!validatedOperations.ok) {
return null;
}
const item = await prisma.inventoryItem.create({
data: {
@@ -530,6 +634,11 @@ export async function createInventoryItem(payload: InventoryItemInput) {
create: validatedBom.bomLines,
}
: undefined,
operations: validatedOperations.operations.length
? {
create: validatedOperations.operations,
}
: undefined,
},
select: {
id: true,
@@ -552,6 +661,10 @@ export async function updateInventoryItem(itemId: string, payload: InventoryItem
if (!validatedBom.ok) {
return null;
}
const validatedOperations = await validateOperations(payload.type, payload.operations);
if (!validatedOperations.ok) {
return null;
}
const item = await prisma.inventoryItem.update({
where: { id: itemId },
@@ -571,6 +684,10 @@ export async function updateInventoryItem(itemId: string, payload: InventoryItem
deleteMany: {},
create: validatedBom.bomLines,
},
operations: {
deleteMany: {},
create: validatedOperations.operations,
},
},
select: {
id: true,