manufacturing and gantt
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user