inventory
This commit is contained in:
@@ -230,4 +230,33 @@ export async function bootstrapAppData() {
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if ((await prisma.warehouse.count()) === 0) {
|
||||
await prisma.warehouse.create({
|
||||
data: {
|
||||
code: "MAIN",
|
||||
name: "Main Warehouse",
|
||||
notes: "Primary stocking location for finished goods and purchased materials.",
|
||||
locations: {
|
||||
create: [
|
||||
{
|
||||
code: "RECV",
|
||||
name: "Receiving",
|
||||
notes: "Initial inbound inspection and receipt staging.",
|
||||
},
|
||||
{
|
||||
code: "STOCK-A1",
|
||||
name: "Aisle A1",
|
||||
notes: "General rack storage for standard material.",
|
||||
},
|
||||
{
|
||||
code: "FG-STAGE",
|
||||
name: "Finished Goods Staging",
|
||||
notes: "Outbound-ready finished assemblies.",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,14 @@ import { fail, ok } from "../../lib/http.js";
|
||||
import { requirePermissions } from "../../lib/rbac.js";
|
||||
import {
|
||||
createInventoryItem,
|
||||
createWarehouse,
|
||||
getInventoryItemById,
|
||||
getWarehouseById,
|
||||
listInventoryItemOptions,
|
||||
listInventoryItems,
|
||||
listWarehouses,
|
||||
updateInventoryItem,
|
||||
updateWarehouse,
|
||||
} from "./service.js";
|
||||
|
||||
const bomLineSchema = z.object({
|
||||
@@ -41,6 +45,19 @@ const inventoryListQuerySchema = z.object({
|
||||
type: z.enum(inventoryItemTypes).optional(),
|
||||
});
|
||||
|
||||
const warehouseLocationSchema = z.object({
|
||||
code: z.string().trim().min(1).max(64),
|
||||
name: z.string().trim().min(1).max(160),
|
||||
notes: z.string(),
|
||||
});
|
||||
|
||||
const warehouseSchema = z.object({
|
||||
code: z.string().trim().min(1).max(64),
|
||||
name: z.string().trim().min(1).max(160),
|
||||
notes: z.string(),
|
||||
locations: z.array(warehouseLocationSchema),
|
||||
});
|
||||
|
||||
function getRouteParam(value: unknown) {
|
||||
return typeof value === "string" ? value : null;
|
||||
}
|
||||
@@ -113,3 +130,49 @@ inventoryRouter.put("/items/:itemId", requirePermissions([permissions.inventoryW
|
||||
|
||||
return ok(response, item);
|
||||
});
|
||||
|
||||
inventoryRouter.get("/warehouses", requirePermissions([permissions.inventoryRead]), async (_request, response) => {
|
||||
return ok(response, await listWarehouses());
|
||||
});
|
||||
|
||||
inventoryRouter.get("/warehouses/:warehouseId", requirePermissions([permissions.inventoryRead]), async (request, response) => {
|
||||
const warehouseId = getRouteParam(request.params.warehouseId);
|
||||
if (!warehouseId) {
|
||||
return fail(response, 400, "INVALID_INPUT", "Warehouse id is invalid.");
|
||||
}
|
||||
|
||||
const warehouse = await getWarehouseById(warehouseId);
|
||||
if (!warehouse) {
|
||||
return fail(response, 404, "WAREHOUSE_NOT_FOUND", "Warehouse was not found.");
|
||||
}
|
||||
|
||||
return ok(response, warehouse);
|
||||
});
|
||||
|
||||
inventoryRouter.post("/warehouses", requirePermissions([permissions.inventoryWrite]), async (request, response) => {
|
||||
const parsed = warehouseSchema.safeParse(request.body);
|
||||
if (!parsed.success) {
|
||||
return fail(response, 400, "INVALID_INPUT", "Warehouse payload is invalid.");
|
||||
}
|
||||
|
||||
return ok(response, await createWarehouse(parsed.data), 201);
|
||||
});
|
||||
|
||||
inventoryRouter.put("/warehouses/:warehouseId", requirePermissions([permissions.inventoryWrite]), async (request, response) => {
|
||||
const warehouseId = getRouteParam(request.params.warehouseId);
|
||||
if (!warehouseId) {
|
||||
return fail(response, 400, "INVALID_INPUT", "Warehouse id is invalid.");
|
||||
}
|
||||
|
||||
const parsed = warehouseSchema.safeParse(request.body);
|
||||
if (!parsed.success) {
|
||||
return fail(response, 400, "INVALID_INPUT", "Warehouse payload is invalid.");
|
||||
}
|
||||
|
||||
const warehouse = await updateWarehouse(warehouseId, parsed.data);
|
||||
if (!warehouse) {
|
||||
return fail(response, 404, "WAREHOUSE_NOT_FOUND", "Warehouse was not found.");
|
||||
}
|
||||
|
||||
return ok(response, warehouse);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,11 @@ import type {
|
||||
InventoryBomLineInput,
|
||||
InventoryItemDetailDto,
|
||||
InventoryItemInput,
|
||||
WarehouseDetailDto,
|
||||
WarehouseInput,
|
||||
WarehouseLocationDto,
|
||||
WarehouseLocationInput,
|
||||
WarehouseSummaryDto,
|
||||
InventoryItemStatus,
|
||||
InventoryItemSummaryDto,
|
||||
InventoryItemType,
|
||||
@@ -41,6 +46,23 @@ type InventoryDetailRecord = {
|
||||
bomLines: BomLineRecord[];
|
||||
};
|
||||
|
||||
type WarehouseLocationRecord = {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
notes: string;
|
||||
};
|
||||
|
||||
type WarehouseDetailRecord = {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
notes: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
locations: WarehouseLocationRecord[];
|
||||
};
|
||||
|
||||
function mapBomLine(record: BomLineRecord): InventoryBomLineDto {
|
||||
return {
|
||||
id: record.id,
|
||||
@@ -54,6 +76,15 @@ function mapBomLine(record: BomLineRecord): InventoryBomLineDto {
|
||||
};
|
||||
}
|
||||
|
||||
function mapWarehouseLocation(record: WarehouseLocationRecord): WarehouseLocationDto {
|
||||
return {
|
||||
id: record.id,
|
||||
code: record.code,
|
||||
name: record.name,
|
||||
notes: record.notes,
|
||||
};
|
||||
}
|
||||
|
||||
function mapSummary(record: {
|
||||
id: string;
|
||||
sku: string;
|
||||
@@ -102,6 +133,37 @@ function mapDetail(record: InventoryDetailRecord): InventoryItemDetailDto {
|
||||
};
|
||||
}
|
||||
|
||||
function mapWarehouseSummary(record: {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
updatedAt: Date;
|
||||
_count: { locations: number };
|
||||
}): WarehouseSummaryDto {
|
||||
return {
|
||||
id: record.id,
|
||||
code: record.code,
|
||||
name: record.name,
|
||||
locationCount: record._count.locations,
|
||||
updatedAt: record.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function mapWarehouseDetail(record: WarehouseDetailRecord): WarehouseDetailDto {
|
||||
return {
|
||||
...mapWarehouseSummary({
|
||||
id: record.id,
|
||||
code: record.code,
|
||||
name: record.name,
|
||||
updatedAt: record.updatedAt,
|
||||
_count: { locations: record.locations.length },
|
||||
}),
|
||||
notes: record.notes,
|
||||
createdAt: record.createdAt.toISOString(),
|
||||
locations: record.locations.map(mapWarehouseLocation),
|
||||
};
|
||||
}
|
||||
|
||||
interface InventoryListFilters {
|
||||
query?: string;
|
||||
status?: InventoryItemStatus;
|
||||
@@ -138,6 +200,16 @@ function normalizeBomLines(bomLines: InventoryBomLineInput[]) {
|
||||
.filter((line) => line.componentItemId.trim().length > 0);
|
||||
}
|
||||
|
||||
function normalizeWarehouseLocations(locations: WarehouseLocationInput[]) {
|
||||
return locations
|
||||
.map((location) => ({
|
||||
code: location.code.trim(),
|
||||
name: location.name.trim(),
|
||||
notes: location.notes,
|
||||
}))
|
||||
.filter((location) => location.code.length > 0 && location.name.length > 0);
|
||||
}
|
||||
|
||||
async function validateBomLines(parentItemId: string | null, bomLines: InventoryBomLineInput[]) {
|
||||
const normalized = normalizeBomLines(bomLines);
|
||||
|
||||
@@ -325,3 +397,87 @@ export async function updateInventoryItem(itemId: string, payload: InventoryItem
|
||||
|
||||
return mapDetail(item);
|
||||
}
|
||||
|
||||
export async function listWarehouses() {
|
||||
const warehouses = await prisma.warehouse.findMany({
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
locations: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [{ code: "asc" }],
|
||||
});
|
||||
|
||||
return warehouses.map(mapWarehouseSummary);
|
||||
}
|
||||
|
||||
export async function getWarehouseById(warehouseId: string) {
|
||||
const warehouse = await prisma.warehouse.findUnique({
|
||||
where: { id: warehouseId },
|
||||
include: {
|
||||
locations: {
|
||||
orderBy: [{ code: "asc" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return warehouse ? mapWarehouseDetail(warehouse) : null;
|
||||
}
|
||||
|
||||
export async function createWarehouse(payload: WarehouseInput) {
|
||||
const locations = normalizeWarehouseLocations(payload.locations);
|
||||
|
||||
const warehouse = await prisma.warehouse.create({
|
||||
data: {
|
||||
code: payload.code.trim(),
|
||||
name: payload.name.trim(),
|
||||
notes: payload.notes,
|
||||
locations: locations.length
|
||||
? {
|
||||
create: locations,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
include: {
|
||||
locations: {
|
||||
orderBy: [{ code: "asc" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return mapWarehouseDetail(warehouse);
|
||||
}
|
||||
|
||||
export async function updateWarehouse(warehouseId: string, payload: WarehouseInput) {
|
||||
const existingWarehouse = await prisma.warehouse.findUnique({
|
||||
where: { id: warehouseId },
|
||||
});
|
||||
|
||||
if (!existingWarehouse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const locations = normalizeWarehouseLocations(payload.locations);
|
||||
|
||||
const warehouse = await prisma.warehouse.update({
|
||||
where: { id: warehouseId },
|
||||
data: {
|
||||
code: payload.code.trim(),
|
||||
name: payload.name.trim(),
|
||||
notes: payload.notes,
|
||||
locations: {
|
||||
deleteMany: {},
|
||||
create: locations,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
locations: {
|
||||
orderBy: [{ code: "asc" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return mapWarehouseDetail(warehouse);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user