///
import {
type Cell,
cell,
Default,
handler,
lift,
recipe,
str,
} from "commontools";
interface BinDefinition {
id: string;
capacity: number;
}
interface ItemPlacement {
id: string;
bin: string;
}
interface WarehouseBinMapArgs {
bins: Default;
items: Default;
}
interface RelocateEvent {
itemId?: string;
targetBin?: string;
}
interface OccupancyEntry {
capacity: number;
used: number;
available: number;
}
const defaultBins: BinDefinition[] = [
{ id: "A1", capacity: 2 },
{ id: "B2", capacity: 3 },
{ id: "C3", capacity: 1 },
];
const defaultItems: ItemPlacement[] = [
{ id: "widget-100", bin: "A1" },
{ id: "widget-200", bin: "A1" },
{ id: "widget-300", bin: "B2" },
];
const sanitizeId = (value: unknown): string | null => {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed.toUpperCase() : null;
};
const sanitizeBins = (value: unknown): BinDefinition[] => {
if (!Array.isArray(value)) {
return structuredClone(defaultBins);
}
const seen = new Set();
const bins: BinDefinition[] = [];
for (const entry of value) {
const id = sanitizeId((entry as BinDefinition | undefined)?.id);
if (!id || seen.has(id)) continue;
const capacityValue = (entry as BinDefinition | undefined)?.capacity;
const capacity = typeof capacityValue === "number" && capacityValue > 0
? Math.floor(capacityValue)
: 1;
seen.add(id);
bins.push({ id, capacity });
}
if (bins.length === 0) {
return structuredClone(defaultBins);
}
return bins;
};
const sanitizePlacementList = (
entries: readonly ItemPlacement[] | undefined,
bins: readonly BinDefinition[],
): ItemPlacement[] => {
if (!Array.isArray(entries) || entries.length === 0) {
return [];
}
const capacities = new Map();
for (const bin of bins) {
capacities.set(bin.id, bin.capacity);
}
const usage = new Map();
const seen = new Set();
const placements: ItemPlacement[] = [];
for (const entry of entries) {
const id = sanitizeId(entry?.id);
const bin = sanitizeId(entry?.bin);
if (!id || !bin || seen.has(id) || !capacities.has(bin)) continue;
const used = usage.get(bin) ?? 0;
const limit = capacities.get(bin) ?? 0;
if (used >= limit) continue;
placements.push({ id, bin });
usage.set(bin, used + 1);
seen.add(id);
}
return placements;
};
const sanitizePlacements = (
value: unknown,
bins: readonly BinDefinition[],
): ItemPlacement[] => {
const initial = sanitizePlacementList(
Array.isArray(value) ? value as ItemPlacement[] : undefined,
bins,
);
if (initial.length > 0) return initial;
return sanitizePlacementList(defaultItems, bins);
};
const buildOccupancy = (
bins: readonly BinDefinition[],
placements: readonly ItemPlacement[],
): Record => {
const counts = new Map();
for (const placement of placements) {
const next = (counts.get(placement.bin) ?? 0) + 1;
counts.set(placement.bin, next);
}
const occupancy: Record = {};
for (const bin of bins) {
const used = counts.get(bin.id) ?? 0;
const capacity = bin.capacity;
const available = capacity - used;
occupancy[bin.id] = {
capacity,
used,
available: available > 0 ? available : 0,
};
}
return occupancy;
};
const relocateInventory = handler(
(
event: RelocateEvent | undefined,
context: {
items: Cell;
bins: Cell;
placements: Cell;
history: Cell;
},
) => {
const itemId = sanitizeId(event?.itemId);
const targetBin = sanitizeId(event?.targetBin);
if (!itemId || !targetBin) return;
const bins = context.bins.get() ?? [];
if (!bins.some((entry) => entry.id === targetBin)) return;
const placements = context.placements.get() ?? [];
const index = placements.findIndex((entry) => entry.id === itemId);
if (index === -1) return;
const currentPlacement = placements[index];
if (currentPlacement.bin === targetBin) return;
const capacities = new Map();
for (const bin of bins) {
capacities.set(bin.id, bin.capacity);
}
const usage = new Map();
for (let i = 0; i < placements.length; i++) {
if (i === index) continue;
const entry = placements[i];
usage.set(entry.bin, (usage.get(entry.bin) ?? 0) + 1);
}
const limit = capacities.get(targetBin) ?? 0;
const used = usage.get(targetBin) ?? 0;
if (used >= limit) return;
const updated = placements.map((entry, position) => ({
id: entry.id,
bin: position === index ? targetBin : entry.bin,
}));
context.items.set(updated);
const history = context.history.get();
const log = Array.isArray(history) ? history : [];
const message =
`Moved ${itemId} from ${currentPlacement.bin} to ${targetBin}`;
context.history.set([...log, message]);
buildOccupancy(bins, updated);
},
);
export const warehouseBinMap = recipe(
"Warehouse Bin Map",
({ bins, items }) => {
const binsList = lift(sanitizeBins)(bins);
const rawItems = lift((value: ItemPlacement[] | undefined) =>
Array.isArray(value) ? value : []
)(items);
const placements = lift((input: {
entries: ItemPlacement[];
binList: BinDefinition[];
}) => sanitizePlacements(input.entries, input.binList))({
entries: rawItems,
binList: binsList,
});
const occupancy = lift((input: {
binList: BinDefinition[];
placements: ItemPlacement[];
}) => buildOccupancy(input.binList, input.placements))({
binList: binsList,
placements,
});
const availableBins = lift((summary: Record) =>
Object.keys(summary).filter((id) => summary[id].available > 0)
)(occupancy);
const totalItems = lift((list: ItemPlacement[]) => list.length)(
placements,
);
const binCount = lift((list: BinDefinition[]) => list.length)(binsList);
const status = str`${totalItems} items across ${binCount} bins`;
const history = cell([]);
const lastAction = lift((entries: string[]) => {
if (!Array.isArray(entries) || entries.length === 0) {
return "initialized";
}
return entries[entries.length - 1];
})(history);
const relocate = relocateInventory({
items,
bins: binsList,
placements,
history,
});
return {
bins: binsList,
items,
placements,
occupancy,
availableBins,
status,
history,
lastAction,
relocate,
};
},
);