"use client"; import React from "react"; import { useTranslation } from "react-i18next"; import DeleteOutline from "@mui/icons-material/DeleteOutline"; import { Alert, Box, Button, Checkbox, FormControlLabel, IconButton, Stack, ToggleButton, ToggleButtonGroup, Typography, } from "@mui/material"; import type { WarehousePickRow } from "@/app/api/settings/deliveryOrderFloor/client"; import { EXCLUDE_ALL_AREAS, earlierOverlappingExcludeGroups, emptyExcludeWarehouseRule, excludeRuleHasTarget, groupExcludeRuleIndexes, type ExcludePrintMode, type ExcludeWarehouseRule, } from "@/app/api/settings/deliveryOrderFloor/constants"; const BUTTON_WIDTH = 80; function tokens(raw: string): string[] { return raw .split(",") .map((part) => part.trim()) .filter(Boolean); } function uniqueSorted(values: string[]): string[] { const seen = new Set(); const out: string[] = []; for (const value of values) { const key = value.toUpperCase(); if (!value || seen.has(key)) continue; seen.add(key); out.push(value); } return out.sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" })); } function slotCode(code: string): string { const parts = code.split("-"); return parts[parts.length - 1] || ""; } function slotKey(raw: string): string { return tokens(raw) .map((slot) => slot.toUpperCase()) .sort() .join(","); } function shownSlots(stored: string, areaSlots: string[]): string[] { const picked = tokens(stored); if (picked.includes("-")) return []; if (picked.length === 0) return areaSlots; return areaSlots.filter((slot) => picked.some((item) => item.toUpperCase() === slot.toUpperCase())); } function slotsToStore(picked: string[], areaSlots: string[]): string { if (areaSlots.length > 0 && picked.length === areaSlots.length) return ""; if (picked.length === 0) return "-"; return picked.join(","); } function areasOnRule(rule: ExcludeWarehouseRule, areaNames: string[]): string[] { const picked = tokens(rule.areas); if (picked.includes(EXCLUDE_ALL_AREAS)) return areaNames; return picked; } function replaceGroup(rules: ExcludeWarehouseRule[], indexes: number[], nextGroup: ExcludeWarehouseRule[]): ExcludeWarehouseRule[] { const indexSet = new Set(indexes); const first = indexes[0] ?? 0; const out: ExcludeWarehouseRule[] = []; rules.forEach((rule, index) => { if (index === first) out.push(...nextGroup); else if (!indexSet.has(index)) out.push(rule); }); return out; } function withoutArea(group: ExcludeWarehouseRule[], area: string, areaNames: string[]): ExcludeWarehouseRule[] { return group .map((rule) => ({ ...rule, areas: areasOnRule(rule, areaNames) .filter((item) => item.toUpperCase() !== area.toUpperCase()) .join(","), })) .filter((rule) => tokens(rule.areas).length > 0); } function withAreaSlots( group: ExcludeWarehouseRule[], area: string, slotCsv: string, areaNames: string[], template: ExcludeWarehouseRule, ): ExcludeWarehouseRule[] { const stripped = withoutArea(group, area, areaNames); const key = slotKey(slotCsv); const hitIndex = stripped.findIndex((rule) => slotKey(rule.slots) === key); if (hitIndex >= 0) { return stripped.map((rule, index) => index === hitIndex ? { ...rule, areas: [...tokens(rule.areas), area].join(",") } : rule, ); } const next = [...stripped, { ...template, areas: area, slots: tokens(slotCsv).join(",") }]; return next.length > 0 ? next : [{ ...template, areas: "", slots: "" }]; } function isChosen(value: string | string[] | null, option: string): boolean { if (Array.isArray(value)) return value.some((item) => item.toUpperCase() === option.toUpperCase()); return typeof value === "string" && value.toUpperCase() === option.toUpperCase(); } function ToggleRows({ label, options, value, exclusive, disabled, onChange, renderLabel, }: { label: string; options: string[]; value: string | string[] | null; exclusive: boolean; disabled?: boolean; onChange: (next: string | string[] | null) => void; renderLabel?: (option: string) => string; }) { const toggle = (option: string) => { if (disabled) return; if (exclusive) { onChange(isChosen(value, option) ? null : option); return; } const previous = Array.isArray(value) ? value : []; const next = isChosen(previous, option) ? previous.filter((item) => item.toUpperCase() !== option.toUpperCase()) : [...previous, option]; onChange(next); }; return ( {label} {options.length === 0 ? ( — ) : ( {options.map((option) => ( toggle(option)} sx={{ width: BUTTON_WIDTH, minWidth: BUTTON_WIDTH, maxWidth: BUTTON_WIDTH, px: 0, textTransform: "none", }} > {renderLabel ? renderLabel(option) : option} ))} )} ); } export function ExcludeWarehouseRuleList({ rules, warehouses, saving, onSave, }: { rules: ExcludeWarehouseRule[]; warehouses: WarehousePickRow[]; saving: boolean; onSave: (next: ExcludeWarehouseRule[]) => void; }) { const { t } = useTranslation("deliveryOrderFloor"); const [viewByGroup, setViewByGroup] = React.useState>({}); const saveGroup = (indexes: number[], nextGroup: ExcludeWarehouseRule[]) => { onSave(replaceGroup(rules, indexes, nextGroup)); }; const floors = uniqueSorted(warehouses.map((row) => row.storeId)); const groups = groupExcludeRuleIndexes(rules).map((indexes) => ({ key: indexes.join(","), indexes })); return ( {rules.length === 0 ? ( {t("Empty exclude rules")} ) : ( groups.map((group, groupIndex) => { const overlappedBy = earlierOverlappingExcludeGroups(rules, groupIndex); const groupRules = group.indexes.map((index) => rules[index]).filter((rule): rule is ExcludeWarehouseRule => rule != null); const rule = groupRules[0]; if (!rule) return null; const warehousesForFloor = warehouses.filter( (row) => row.storeId.toUpperCase() === rule.floor.trim().toUpperCase(), ); const warehouseOptions = uniqueSorted(warehousesForFloor.map((row) => row.warehouse)); const areasForWarehouse = warehousesForFloor.filter( (row) => row.warehouse.toUpperCase() === rule.warehouse.trim().toUpperCase(), ); const areaNames = uniqueSorted(areasForWarehouse.map((row) => row.area)); const slotsByArea = new Map(); areasForWarehouse.forEach((row) => { const slot = slotCode(row.code); const list = slotsByArea.get(row.area) ?? []; if (slot && !list.some((item) => item.toUpperCase() === slot.toUpperCase())) list.push(slot); slotsByArea.set(row.area, list); }); const selectedAreas = uniqueSorted(groupRules.flatMap((item) => areasOnRule(item, areaNames))); const viewArea = areaNames.find((area) => area.toUpperCase() === (viewByGroup[group.key] ?? "").toUpperCase()) ?? ""; const viewedRule = groupRules.find((item) => areasOnRule(item, areaNames).some((area) => area.toUpperCase() === viewArea.toUpperCase()), ); const viewSlots = uniqueSorted(slotsByArea.get(viewArea) ?? []); const pressedSlots = viewArea ? shownSlots(viewedRule?.slots ?? "", viewSlots) : []; const partialAreas = new Set( selectedAreas .filter((area) => { const areaSlots = uniqueSorted(slotsByArea.get(area) ?? []); const owner = groupRules.find((item) => areasOnRule(item, areaNames).some((name) => name.toUpperCase() === area.toUpperCase()), ); return shownSlots(owner?.slots ?? "", areaSlots).length < areaSlots.length; }) .map((area) => area.toUpperCase()), ); const selectableRules = groupRules.filter((item) => excludeRuleHasTarget(item)); const canEnable = selectableRules.length > 0; const applyGroup = (nextGroup: ExcludeWarehouseRule[]) => { const kept = nextGroup.length > 0 ? nextGroup : [{ ...rule, areas: "", slots: "" }]; saveGroup(group.indexes, kept); }; const setAreaIncluded = (area: string, included: boolean) => { if (!included) { const next = withoutArea(groupRules, area, areaNames); applyGroup(next); return; } applyGroup(withAreaSlots(groupRules, area, "", areaNames, rule)); }; return ( {t("Exclude rule n", { n: groupIndex + 1 })} item.enabled !== false) } disabled={saving || !canEnable || overlappedBy.length > 0} onChange={(event) => applyGroup( groupRules.map((item) => ({ ...item, enabled: event.target.checked && excludeRuleHasTarget(item), })), ) } /> } label={t("Rule enabled")} /> onSave(rules.filter((_, ruleIndex) => !group.indexes.includes(ruleIndex)))} > {overlappedBy.length > 0 ? ( {t("Putaway rule overlap", { rules: overlappedBy.map((index) => index + 1).join(", ") })} ) : null} applyGroup([ { ...rule, floor: typeof next === "string" ? next : "", warehouse: "", areas: "", slots: "", }, ]) } /> applyGroup([ { ...rule, warehouse: typeof next === "string" ? next : "", areas: "", slots: "", }, ]) } /> {t("Area")} {areaNames.length === 0 ? ( — ) : ( 0 && selectedAreas.length === areaNames.length} onChange={(_, checked) => { if (!checked) { applyGroup([{ ...rule, areas: "", slots: "" }]); return; } let next = groupRules; areaNames.forEach((area) => { if (!selectedAreas.some((item) => item.toUpperCase() === area.toUpperCase())) { next = withAreaSlots(next, area, "", areaNames, rule); } }); applyGroup(next); }} sx={{ p: 0.25 }} /> {t("All")} {areaNames.map((area) => { const selected = selectedAreas.some((item) => item.toUpperCase() === area.toUpperCase()); const viewing = viewArea.toUpperCase() === area.toUpperCase(); const partial = partialAreas.has(area.toUpperCase()); return ( { setViewByGroup((prev) => ({ ...prev, [group.key]: area })); setAreaIncluded(area, checked); }} sx={{ p: 0.25 }} /> setViewByGroup((prev) => ({ ...prev, [group.key]: area }))} sx={{ flex: 1, textAlign: "center", cursor: "pointer", py: 0.5, pr: 0.5 }} > {area} ); })} )} { if (!viewArea) return; const picked = Array.isArray(next) ? next : []; applyGroup(withAreaSlots(groupRules, viewArea, slotsToStore(picked, viewSlots), areaNames, rule)); }} /> {!viewArea ? ( {t("Click area to view slots")} ) : ( {t("Viewing area", { area: viewArea })} {!viewedRule ? ` ${t("Area not in rule")}` : ""} )} {t("Print list label")} { if (!next) return; applyGroup(groupRules.map((item) => ({ ...item, printMode: next }))); }} sx={{ flexWrap: "wrap" }} > {t("Print hide list")} {t("Print hide list hint")} {t("Print hide qr")} {t("Print hide qr hint")} ); }) )} ); }