|
|
@@ -12,7 +12,11 @@ import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined"; |
|
|
import ShoppingCartOutlined from "@mui/icons-material/ShoppingCartOutlined"; |
|
|
import ShoppingCartOutlined from "@mui/icons-material/ShoppingCartOutlined"; |
|
|
import WarningAmberOutlined from "@mui/icons-material/WarningAmberOutlined"; |
|
|
import WarningAmberOutlined from "@mui/icons-material/WarningAmberOutlined"; |
|
|
import Search from "@mui/icons-material/Search"; |
|
|
import Search from "@mui/icons-material/Search"; |
|
|
import { ExcludeWarehouseRuleList } from "@/components/DeliveryOrderFloorSettings/ExcludeWarehouseRuleList"; |
|
|
|
|
|
|
|
|
import { |
|
|
|
|
|
ExcludeWarehouseRuleList, |
|
|
|
|
|
incompleteExcludeGroups, |
|
|
|
|
|
type ExcludeMissingField, |
|
|
|
|
|
} from "@/components/DeliveryOrderFloorSettings/ExcludeWarehouseRuleList"; |
|
|
import { |
|
|
import { |
|
|
Alert, |
|
|
Alert, |
|
|
Box, |
|
|
Box, |
|
|
@@ -46,6 +50,7 @@ import { |
|
|
fetchSupplierComboClient, |
|
|
fetchSupplierComboClient, |
|
|
fetchWarehouseCodeRowsClient, |
|
|
fetchWarehouseCodeRowsClient, |
|
|
postSettingClient, |
|
|
postSettingClient, |
|
|
|
|
|
savePickExcludeRulesClient, |
|
|
type ShopComboRow, |
|
|
type ShopComboRow, |
|
|
type WarehousePickRow, |
|
|
type WarehousePickRow, |
|
|
} from "@/app/api/settings/deliveryOrderFloor/client"; |
|
|
} from "@/app/api/settings/deliveryOrderFloor/client"; |
|
|
@@ -59,6 +64,7 @@ import { |
|
|
SETTING_DO_PICK_EXCLUDE_WAREHOUSES, |
|
|
SETTING_DO_PICK_EXCLUDE_WAREHOUSES, |
|
|
codesToExcludeRules, |
|
|
codesToExcludeRules, |
|
|
compactExcludeRules, |
|
|
compactExcludeRules, |
|
|
|
|
|
ensureExcludeCardIds, |
|
|
disableOverlappingExcludeRules, |
|
|
disableOverlappingExcludeRules, |
|
|
partitionExcludeCodes, |
|
|
partitionExcludeCodes, |
|
|
DEFAULT_AUTO_PUTAWAY_WAREHOUSE, |
|
|
DEFAULT_AUTO_PUTAWAY_WAREHOUSE, |
|
|
@@ -86,6 +92,7 @@ function normalizeCodesCsv(raw: string): string { |
|
|
type EditFloor = "2F" | "4F"; |
|
|
type EditFloor = "2F" | "4F"; |
|
|
type ExcludeKind = "do" | "jo"; |
|
|
type ExcludeKind = "do" | "jo"; |
|
|
type PageTab = "do" | "jo" | "po"; |
|
|
type PageTab = "do" | "jo" | "po"; |
|
|
|
|
|
type SettingsSaveSection = "suppliers" | "exclude-do" | "exclude-jo" | "putaway-po" | "putaway-jo"; |
|
|
type WarehousePickTarget = "exclude" | "putaway"; |
|
|
type WarehousePickTarget = "exclude" | "putaway"; |
|
|
type PutAwayRowField = "keywords" | "bom" | "location" | "exceptionKeywords"; |
|
|
type PutAwayRowField = "keywords" | "bom" | "location" | "exceptionKeywords"; |
|
|
type PutAwayEditCell = { kind: AutoPutAwayScope; index: number; field: PutAwayRowField; exceptionIndex?: number }; |
|
|
type PutAwayEditCell = { kind: AutoPutAwayScope; index: number; field: PutAwayRowField; exceptionIndex?: number }; |
|
|
@@ -200,6 +207,10 @@ function floorRowsToCsv(rows: FloorRow[]): string { |
|
|
return rows.map((r) => r.code.trim()).filter(Boolean).join(","); |
|
|
return rows.map((r) => r.code.trim()).filter(Boolean).join(","); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function sameJson(left: unknown, right: unknown): boolean { |
|
|
|
|
|
return JSON.stringify(left) === JSON.stringify(right); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const LOCATION_ALL = "ALL"; |
|
|
const LOCATION_ALL = "ALL"; |
|
|
const BUTTONS_PER_ROW = 15; |
|
|
const BUTTONS_PER_ROW = 15; |
|
|
const BUTTON_WIDTH = 80; |
|
|
const BUTTON_WIDTH = 80; |
|
|
@@ -230,12 +241,20 @@ const DeliveryOrderFloorSettings: React.FC = () => { |
|
|
|
|
|
|
|
|
const [codes2F, setCodes2F] = useState(""); |
|
|
const [codes2F, setCodes2F] = useState(""); |
|
|
const [codes4F, setCodes4F] = useState(""); |
|
|
const [codes4F, setCodes4F] = useState(""); |
|
|
|
|
|
const [savedCodes2F, setSavedCodes2F] = useState(""); |
|
|
|
|
|
const [savedCodes4F, setSavedCodes4F] = useState(""); |
|
|
const [doExclude, setDoExclude] = useState(""); |
|
|
const [doExclude, setDoExclude] = useState(""); |
|
|
const [joExclude, setJoExclude] = useState(""); |
|
|
const [joExclude, setJoExclude] = useState(""); |
|
|
const [doExcludeRules, setDoExcludeRules] = useState<ExcludeWarehouseRule[]>([]); |
|
|
const [doExcludeRules, setDoExcludeRules] = useState<ExcludeWarehouseRule[]>([]); |
|
|
const [joExcludeRules, setJoExcludeRules] = useState<ExcludeWarehouseRule[]>([]); |
|
|
const [joExcludeRules, setJoExcludeRules] = useState<ExcludeWarehouseRule[]>([]); |
|
|
|
|
|
const [savedDoExcludeRules, setSavedDoExcludeRules] = useState<ExcludeWarehouseRule[]>([]); |
|
|
|
|
|
const [savedJoExcludeRules, setSavedJoExcludeRules] = useState<ExcludeWarehouseRule[]>([]); |
|
|
const [poPutAway, setPoPutAway] = useState<AutoPutAwayRule[]>([]); |
|
|
const [poPutAway, setPoPutAway] = useState<AutoPutAwayRule[]>([]); |
|
|
const [joPutAway, setJoPutAway] = useState<AutoPutAwayRule[]>([]); |
|
|
const [joPutAway, setJoPutAway] = useState<AutoPutAwayRule[]>([]); |
|
|
|
|
|
const [savedPoPutAway, setSavedPoPutAway] = useState<AutoPutAwayRule[]>([]); |
|
|
|
|
|
const [savedJoPutAway, setSavedJoPutAway] = useState<AutoPutAwayRule[]>([]); |
|
|
|
|
|
const [savingSection, setSavingSection] = useState<SettingsSaveSection | null>(null); |
|
|
|
|
|
const [excludeMissingKind, setExcludeMissingKind] = useState<ExcludeKind | null>(null); |
|
|
|
|
|
|
|
|
const [dialogSaving, setDialogSaving] = useState(false); |
|
|
const [dialogSaving, setDialogSaving] = useState(false); |
|
|
const [comboLoading, setComboLoading] = useState(false); |
|
|
const [comboLoading, setComboLoading] = useState(false); |
|
|
@@ -257,7 +276,6 @@ const DeliveryOrderFloorSettings: React.FC = () => { |
|
|
const [editLocationMode, setEditLocationMode] = useState<PutAwayLocationMode>("warehouse"); |
|
|
const [editLocationMode, setEditLocationMode] = useState<PutAwayLocationMode>("warehouse"); |
|
|
const [editWarehouseCode, setEditWarehouseCode] = useState(""); |
|
|
const [editWarehouseCode, setEditWarehouseCode] = useState(""); |
|
|
const [warehousePickTarget, setWarehousePickTarget] = useState<WarehousePickTarget>("exclude"); |
|
|
const [warehousePickTarget, setWarehousePickTarget] = useState<WarehousePickTarget>("exclude"); |
|
|
const excludeSavePendingRef = useRef<{ kind: ExcludeKind; next: ExcludeWarehouseRule[] } | null>(null); |
|
|
|
|
|
const [excludeQuery, setExcludeQuery] = useState(""); |
|
|
const [excludeQuery, setExcludeQuery] = useState(""); |
|
|
const [excludePick, setExcludePick] = useState<ExcludePick>(emptyExcludePick); |
|
|
const [excludePick, setExcludePick] = useState<ExcludePick>(emptyExcludePick); |
|
|
const [excludeAddError, setExcludeAddError] = useState<string | null>(null); |
|
|
const [excludeAddError, setExcludeAddError] = useState<string | null>(null); |
|
|
@@ -270,12 +288,18 @@ const DeliveryOrderFloorSettings: React.FC = () => { |
|
|
const floor = await fetchDoFloorSettingsClient(); |
|
|
const floor = await fetchDoFloorSettingsClient(); |
|
|
setCodes2F(floor.suppliers2F); |
|
|
setCodes2F(floor.suppliers2F); |
|
|
setCodes4F(floor.suppliers4F); |
|
|
setCodes4F(floor.suppliers4F); |
|
|
|
|
|
setSavedCodes2F(floor.suppliers2F); |
|
|
|
|
|
setSavedCodes4F(floor.suppliers4F); |
|
|
setDoExclude(floor.doExcludeWarehouses); |
|
|
setDoExclude(floor.doExcludeWarehouses); |
|
|
setJoExclude(floor.joExcludeWarehouses); |
|
|
setJoExclude(floor.joExcludeWarehouses); |
|
|
setDoExcludeRules(floor.doExcludeRules); |
|
|
setDoExcludeRules(floor.doExcludeRules); |
|
|
setJoExcludeRules(floor.joExcludeRules); |
|
|
setJoExcludeRules(floor.joExcludeRules); |
|
|
|
|
|
setSavedDoExcludeRules(floor.doExcludeRules); |
|
|
|
|
|
setSavedJoExcludeRules(floor.joExcludeRules); |
|
|
setPoPutAway(floor.poAutoPutAway); |
|
|
setPoPutAway(floor.poAutoPutAway); |
|
|
setJoPutAway(floor.joAutoPutAway); |
|
|
setJoPutAway(floor.joAutoPutAway); |
|
|
|
|
|
setSavedPoPutAway(floor.poAutoPutAway); |
|
|
|
|
|
setSavedJoPutAway(floor.joAutoPutAway); |
|
|
} catch (e: unknown) { |
|
|
} catch (e: unknown) { |
|
|
setError(e instanceof Error ? e.message : String(e)); |
|
|
setError(e instanceof Error ? e.message : String(e)); |
|
|
} finally { |
|
|
} finally { |
|
|
@@ -324,24 +348,64 @@ const DeliveryOrderFloorSettings: React.FC = () => { |
|
|
const codes2FList = useMemo(() => supplierCodes(codes2F), [codes2F]); |
|
|
const codes2FList = useMemo(() => supplierCodes(codes2F), [codes2F]); |
|
|
const codes4FList = useMemo(() => supplierCodes(codes4F), [codes4F]); |
|
|
const codes4FList = useMemo(() => supplierCodes(codes4F), [codes4F]); |
|
|
const supplierName = (code: string) => findSupplierRow(supplierCombo ?? [], code)?.name?.trim() || ""; |
|
|
const supplierName = (code: string) => findSupplierRow(supplierCombo ?? [], code)?.name?.trim() || ""; |
|
|
|
|
|
const suppliersDirty = useMemo(() => { |
|
|
|
|
|
const current2 = normalizeCodesCsv(codes2FList.join(",")); |
|
|
|
|
|
const current4 = normalizeCodesCsv(codes4FList.join(",")); |
|
|
|
|
|
const saved2 = normalizeCodesCsv(supplierCodes(savedCodes2F).join(",")); |
|
|
|
|
|
const saved4 = normalizeCodesCsv(supplierCodes(savedCodes4F).join(",")); |
|
|
|
|
|
return current2 !== saved2 || current4 !== saved4; |
|
|
|
|
|
}, [codes2FList, codes4FList, savedCodes2F, savedCodes4F]); |
|
|
|
|
|
const excludeDirty = (kind: ExcludeKind) => |
|
|
|
|
|
!sameJson( |
|
|
|
|
|
kind === "do" ? doExcludeRules : joExcludeRules, |
|
|
|
|
|
kind === "do" ? savedDoExcludeRules : savedJoExcludeRules, |
|
|
|
|
|
); |
|
|
|
|
|
const putAwayDirty = (kind: AutoPutAwayScope) => |
|
|
|
|
|
!sameJson(kind === "po" ? poPutAway : joPutAway, kind === "po" ? savedPoPutAway : savedJoPutAway); |
|
|
|
|
|
|
|
|
|
|
|
const renderSectionSave = (dirty: boolean, section: SettingsSaveSection, onClick: () => void) => ( |
|
|
|
|
|
<Box sx={{ display: "flex", justifyContent: "flex-end", mt: 2 }}> |
|
|
|
|
|
<Button variant="contained" disabled={!dirty || dialogSaving} onClick={onClick}> |
|
|
|
|
|
{dialogSaving && savingSection === section ? <CircularProgress size={22} color="inherit" /> : t("Save changes")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
</Box> |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
const persistFloorSuppliers = async (floor: EditFloor, codes: string[]) => { |
|
|
|
|
|
|
|
|
const updateFloorCodes = (floor: EditFloor, codes: string[]) => { |
|
|
|
|
|
const normalized = normalizeCodesCsv(codes.filter((code) => code !== EMPTY_CODE_LIST_MARKER).join(",")); |
|
|
|
|
|
if (floor === "2F") setCodes2F(normalized); |
|
|
|
|
|
else setCodes4F(normalized); |
|
|
|
|
|
setSuccess(null); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const saveFloorSuppliers = async () => { |
|
|
if (saveInFlightRef.current) return; |
|
|
if (saveInFlightRef.current) return; |
|
|
saveInFlightRef.current = true; |
|
|
saveInFlightRef.current = true; |
|
|
setDialogSaving(true); |
|
|
setDialogSaving(true); |
|
|
|
|
|
setSavingSection("suppliers"); |
|
|
setError(null); |
|
|
setError(null); |
|
|
setSuccess(null); |
|
|
setSuccess(null); |
|
|
try { |
|
|
try { |
|
|
const normalized = normalizeCodesCsv(codes.filter((code) => code !== EMPTY_CODE_LIST_MARKER).join(",")); |
|
|
|
|
|
const key = floor === "2F" ? SETTING_DO_FLOOR_SUPPLIERS_2F : SETTING_DO_FLOOR_SUPPLIERS_4F; |
|
|
|
|
|
await postSettingClient(key, normalized || EMPTY_CODE_LIST_MARKER); |
|
|
|
|
|
if (floor === "2F") setCodes2F(normalized); |
|
|
|
|
|
else setCodes4F(normalized); |
|
|
|
|
|
|
|
|
const next2 = normalizeCodesCsv(codes2FList.join(",")); |
|
|
|
|
|
const next4 = normalizeCodesCsv(codes4FList.join(",")); |
|
|
|
|
|
const prev2 = normalizeCodesCsv(supplierCodes(savedCodes2F).join(",")); |
|
|
|
|
|
const prev4 = normalizeCodesCsv(supplierCodes(savedCodes4F).join(",")); |
|
|
|
|
|
if (next2 !== prev2) { |
|
|
|
|
|
await postSettingClient(SETTING_DO_FLOOR_SUPPLIERS_2F, next2 || EMPTY_CODE_LIST_MARKER); |
|
|
|
|
|
} |
|
|
|
|
|
if (next4 !== prev4) { |
|
|
|
|
|
await postSettingClient(SETTING_DO_FLOOR_SUPPLIERS_4F, next4 || EMPTY_CODE_LIST_MARKER); |
|
|
|
|
|
} |
|
|
|
|
|
setCodes2F(next2); |
|
|
|
|
|
setCodes4F(next4); |
|
|
|
|
|
setSavedCodes2F(next2); |
|
|
|
|
|
setSavedCodes4F(next4); |
|
|
setSuccess(t("Saved")); |
|
|
setSuccess(t("Saved")); |
|
|
} catch (e: unknown) { |
|
|
} catch (e: unknown) { |
|
|
setError(e instanceof Error ? e.message : String(e)); |
|
|
setError(e instanceof Error ? e.message : String(e)); |
|
|
} finally { |
|
|
} finally { |
|
|
setDialogSaving(false); |
|
|
setDialogSaving(false); |
|
|
|
|
|
setSavingSection(null); |
|
|
saveInFlightRef.current = false; |
|
|
saveInFlightRef.current = false; |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
@@ -375,7 +439,7 @@ const DeliveryOrderFloorSettings: React.FC = () => { |
|
|
} |
|
|
} |
|
|
setSupplierRowError((prev) => ({ ...prev, [floor]: undefined })); |
|
|
setSupplierRowError((prev) => ({ ...prev, [floor]: undefined })); |
|
|
setSupplierInput((prev) => ({ ...prev, [floor]: "" })); |
|
|
setSupplierInput((prev) => ({ ...prev, [floor]: "" })); |
|
|
void persistFloorSuppliers(floor, [...current, canonical]); |
|
|
|
|
|
|
|
|
updateFloorCodes(floor, [...current, canonical]); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const openExclude = async (kind: ExcludeKind) => { |
|
|
const openExclude = async (kind: ExcludeKind) => { |
|
|
@@ -520,50 +584,79 @@ const DeliveryOrderFloorSettings: React.FC = () => { |
|
|
}, [warehouses, excludePick.storeId, excludePick.warehouse, excludePick.area]); |
|
|
}, [warehouses, excludePick.storeId, excludePick.warehouse, excludePick.area]); |
|
|
const slotRows = useMemo(() => chunk(putAwaySlotChoices, BUTTONS_PER_ROW), [putAwaySlotChoices]); |
|
|
const slotRows = useMemo(() => chunk(putAwaySlotChoices, BUTTONS_PER_ROW), [putAwaySlotChoices]); |
|
|
|
|
|
|
|
|
const persistExcludeRules = async (kind: ExcludeKind, incoming: ExcludeWarehouseRule[], silent = false) => { |
|
|
|
|
|
|
|
|
const excludeFieldName = (field: ExcludeMissingField) => { |
|
|
|
|
|
if (field === "floor") return t("Floor"); |
|
|
|
|
|
if (field === "warehouse") return t("Warehouse"); |
|
|
|
|
|
if (field === "area") return t("Area"); |
|
|
|
|
|
return t("Putaway slot"); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const excludeIncompleteMessage = (rules: ExcludeWarehouseRule[]) => { |
|
|
|
|
|
const incomplete = incompleteExcludeGroups(rules); |
|
|
|
|
|
if (incomplete.length === 0) return null; |
|
|
|
|
|
return incomplete |
|
|
|
|
|
.map((issue) => |
|
|
|
|
|
t("Exclude rule incomplete", { |
|
|
|
|
|
n: issue.n, |
|
|
|
|
|
fields: issue.fields.map(excludeFieldName).join("、"), |
|
|
|
|
|
}), |
|
|
|
|
|
) |
|
|
|
|
|
.join(" "); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const stageExcludeRules = (kind: ExcludeKind, incoming: ExcludeWarehouseRule[]) => { |
|
|
const next = disableOverlappingExcludeRules(incoming); |
|
|
const next = disableOverlappingExcludeRules(incoming); |
|
|
if (kind === "do") setDoExcludeRules(next); |
|
|
if (kind === "do") setDoExcludeRules(next); |
|
|
else setJoExcludeRules(next); |
|
|
else setJoExcludeRules(next); |
|
|
if (saveInFlightRef.current) { |
|
|
|
|
|
excludeSavePendingRef.current = { kind, next }; |
|
|
|
|
|
|
|
|
setSuccess(null); |
|
|
|
|
|
if (excludeMissingKind === kind) { |
|
|
|
|
|
const message = excludeIncompleteMessage(next); |
|
|
|
|
|
setError(message); |
|
|
|
|
|
if (!message) setExcludeMissingKind(null); |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const saveExcludeRules = async (kind: ExcludeKind) => { |
|
|
|
|
|
if (saveInFlightRef.current) return; |
|
|
|
|
|
const current = kind === "do" ? doExcludeRules : joExcludeRules; |
|
|
|
|
|
const next = disableOverlappingExcludeRules(current); |
|
|
|
|
|
const incompleteMessage = excludeIncompleteMessage(next); |
|
|
|
|
|
if (incompleteMessage) { |
|
|
|
|
|
setExcludeMissingKind(kind); |
|
|
|
|
|
setError(incompleteMessage); |
|
|
|
|
|
setSuccess(null); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
setExcludeMissingKind(null); |
|
|
saveInFlightRef.current = true; |
|
|
saveInFlightRef.current = true; |
|
|
setDialogSaving(true); |
|
|
setDialogSaving(true); |
|
|
|
|
|
setSavingSection(kind === "do" ? "exclude-do" : "exclude-jo"); |
|
|
setError(null); |
|
|
setError(null); |
|
|
setSuccess(null); |
|
|
setSuccess(null); |
|
|
try { |
|
|
try { |
|
|
const rows = warehouseCombo ?? (await fetchWarehouseCodeRowsClient()); |
|
|
|
|
|
if (!warehouseCombo) setWarehouseCombo(rows); |
|
|
|
|
|
const partitioned = partitionExcludeCodes(next, rows); |
|
|
|
|
|
const csv = partitioned.all.length > 0 ? partitioned.all.join(",") : EMPTY_CODE_LIST_MARKER; |
|
|
|
|
|
const hideQrCsv = partitioned.hideQr.length > 0 ? partitioned.hideQr.join(",") : EMPTY_CODE_LIST_MARKER; |
|
|
|
|
|
const rulesJson = JSON.stringify(compactExcludeRules(next)); |
|
|
|
|
|
if (csv.length > 1000 || hideQrCsv.length > 1000 || rulesJson.length > 1000) { |
|
|
|
|
|
setError(t("List too long")); |
|
|
|
|
|
return; |
|
|
|
|
|
|
|
|
const saved = await savePickExcludeRulesClient( |
|
|
|
|
|
kind === "do" ? "DO" : "JO", |
|
|
|
|
|
ensureExcludeCardIds(compactExcludeRules(next)), |
|
|
|
|
|
); |
|
|
|
|
|
const kept = saved.rules; |
|
|
|
|
|
if (kind === "do") { |
|
|
|
|
|
setDoExcludeRules(kept); |
|
|
|
|
|
setSavedDoExcludeRules(kept); |
|
|
|
|
|
} else { |
|
|
|
|
|
setJoExcludeRules(kept); |
|
|
|
|
|
setSavedJoExcludeRules(kept); |
|
|
} |
|
|
} |
|
|
const rulesKey = kind === "do" ? SETTING_DO_PICK_EXCLUDE_RULES : SETTING_JO_PICK_EXCLUDE_RULES; |
|
|
|
|
|
const csvKey = kind === "do" ? SETTING_DO_PICK_EXCLUDE_WAREHOUSES : SETTING_JO_PICK_EXCLUDE_WAREHOUSES; |
|
|
|
|
|
const hideQrKey = kind === "do" ? SETTING_DO_PICK_EXCLUDE_HIDE_QR : SETTING_JO_PICK_EXCLUDE_HIDE_QR; |
|
|
|
|
|
const printModeKey = kind === "do" ? SETTING_DO_PICK_EXCLUDE_PRINT_MODE : SETTING_JO_PICK_EXCLUDE_PRINT_MODE; |
|
|
|
|
|
const legacyMode = |
|
|
|
|
|
partitioned.all.length > 0 && partitioned.hideQr.length === partitioned.all.length ? "hideQr" : "hideList"; |
|
|
|
|
|
await postSettingClient(rulesKey, rulesJson); |
|
|
|
|
|
await postSettingClient(csvKey, csv); |
|
|
|
|
|
await postSettingClient(hideQrKey, hideQrCsv); |
|
|
|
|
|
await postSettingClient(printModeKey, legacyMode); |
|
|
|
|
|
if (kind === "do") setDoExclude(csv === EMPTY_CODE_LIST_MARKER ? "" : csv); |
|
|
|
|
|
else setJoExclude(csv === EMPTY_CODE_LIST_MARKER ? "" : csv); |
|
|
|
|
|
if (!silent) setSuccess(t("Saved")); |
|
|
|
|
|
|
|
|
setSuccess( |
|
|
|
|
|
saved.droppedUnusedOrDeleted > 0 |
|
|
|
|
|
? t("Unused rules trimmed", { count: saved.droppedUnusedOrDeleted }) |
|
|
|
|
|
: t("Saved"), |
|
|
|
|
|
); |
|
|
} catch (e: unknown) { |
|
|
} catch (e: unknown) { |
|
|
setError(e instanceof Error ? e.message : String(e)); |
|
|
setError(e instanceof Error ? e.message : String(e)); |
|
|
} finally { |
|
|
} finally { |
|
|
setDialogSaving(false); |
|
|
setDialogSaving(false); |
|
|
|
|
|
setSavingSection(null); |
|
|
saveInFlightRef.current = false; |
|
|
saveInFlightRef.current = false; |
|
|
const pending = excludeSavePendingRef.current; |
|
|
|
|
|
excludeSavePendingRef.current = null; |
|
|
|
|
|
if (pending) void persistExcludeRules(pending.kind, pending.next); |
|
|
|
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
@@ -640,87 +733,69 @@ const DeliveryOrderFloorSettings: React.FC = () => { |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const persistPutAwayRules = async (kind: AutoPutAwayScope, rules: AutoPutAwayRule[], silent = false) => { |
|
|
|
|
|
|
|
|
const preparePutAwayRules = (kind: AutoPutAwayScope, rules: AutoPutAwayRule[]): AutoPutAwayRule[] => |
|
|
|
|
|
disableOverlappingRules( |
|
|
|
|
|
kind, |
|
|
|
|
|
rules.map((rule) => ({ |
|
|
|
|
|
itemKeywords: normalizeCodesCsv(rule.itemKeywords), |
|
|
|
|
|
bomTypes: kind === "jo" ? normalizeCodesCsv(rule.bomTypes) : "", |
|
|
|
|
|
locationMode: rule.locationMode, |
|
|
|
|
|
warehouseCode: rule.warehouseCode.trim() || DEFAULT_AUTO_PUTAWAY_WAREHOUSE, |
|
|
|
|
|
enabled: rule.enabled !== false, |
|
|
|
|
|
exceptions: (rule.exceptions ?? []).map((row) => ({ |
|
|
|
|
|
itemKeywords: normalizeCodesCsv(row.itemKeywords), |
|
|
|
|
|
})), |
|
|
|
|
|
})), |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const stagePutAwayRules = (kind: AutoPutAwayScope, rules: AutoPutAwayRule[]) => { |
|
|
|
|
|
const next = preparePutAwayRules(kind, rules); |
|
|
|
|
|
if (kind === "po") setPoPutAway(next); |
|
|
|
|
|
else setJoPutAway(next); |
|
|
|
|
|
setSuccess(null); |
|
|
|
|
|
return next; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const savePutAwayRules = async (kind: AutoPutAwayScope) => { |
|
|
if (saveInFlightRef.current) return; |
|
|
if (saveInFlightRef.current) return; |
|
|
|
|
|
const next = preparePutAwayRules(kind, kind === "po" ? poPutAway : joPutAway); |
|
|
|
|
|
if (next.some((rule) => rule.locationMode === "warehouse" && !rule.warehouseCode)) { |
|
|
|
|
|
setError(t("Select one warehouse")); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
const payload = JSON.stringify(next); |
|
|
|
|
|
if (payload.length > 1000) { |
|
|
|
|
|
setError(t("List too long")); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
saveInFlightRef.current = true; |
|
|
saveInFlightRef.current = true; |
|
|
setDialogSaving(true); |
|
|
setDialogSaving(true); |
|
|
|
|
|
setSavingSection(kind === "po" ? "putaway-po" : "putaway-jo"); |
|
|
setError(null); |
|
|
setError(null); |
|
|
setSuccess(null); |
|
|
setSuccess(null); |
|
|
try { |
|
|
try { |
|
|
const next = disableOverlappingRules( |
|
|
|
|
|
kind, |
|
|
|
|
|
rules.map((rule) => ({ |
|
|
|
|
|
itemKeywords: normalizeCodesCsv(rule.itemKeywords), |
|
|
|
|
|
bomTypes: kind === "jo" ? normalizeCodesCsv(rule.bomTypes) : "", |
|
|
|
|
|
locationMode: rule.locationMode, |
|
|
|
|
|
warehouseCode: rule.warehouseCode.trim() || DEFAULT_AUTO_PUTAWAY_WAREHOUSE, |
|
|
|
|
|
enabled: rule.enabled !== false, |
|
|
|
|
|
exceptions: (rule.exceptions ?? []).map((row) => ({ |
|
|
|
|
|
itemKeywords: normalizeCodesCsv(row.itemKeywords), |
|
|
|
|
|
})), |
|
|
|
|
|
})), |
|
|
|
|
|
); |
|
|
|
|
|
if (next.some((rule) => rule.locationMode === "warehouse" && !rule.warehouseCode)) { |
|
|
|
|
|
setError(t("Select one warehouse")); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
const payload = JSON.stringify(next); |
|
|
|
|
|
if (payload.length > 1000) { |
|
|
|
|
|
setError(t("List too long")); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
const key = kind === "po" ? SETTING_PO_AUTO_PUTAWAY_RULES : SETTING_JO_AUTO_PUTAWAY_RULES; |
|
|
const key = kind === "po" ? SETTING_PO_AUTO_PUTAWAY_RULES : SETTING_JO_AUTO_PUTAWAY_RULES; |
|
|
await postSettingClient(key, next.length === 0 ? EMPTY_CODE_LIST_MARKER : payload); |
|
|
await postSettingClient(key, next.length === 0 ? EMPTY_CODE_LIST_MARKER : payload); |
|
|
if (kind === "po") setPoPutAway(next); |
|
|
|
|
|
else setJoPutAway(next); |
|
|
|
|
|
if (!silent) { |
|
|
|
|
|
setSuccess(t("Saved")); |
|
|
|
|
|
setPutAwayEdit(null); |
|
|
|
|
|
|
|
|
if (kind === "po") { |
|
|
|
|
|
setPoPutAway(next); |
|
|
|
|
|
setSavedPoPutAway(next); |
|
|
|
|
|
} else { |
|
|
|
|
|
setJoPutAway(next); |
|
|
|
|
|
setSavedJoPutAway(next); |
|
|
} |
|
|
} |
|
|
|
|
|
setPutAwayEdit(null); |
|
|
|
|
|
setSuccess(t("Saved")); |
|
|
} catch (e: unknown) { |
|
|
} catch (e: unknown) { |
|
|
setError(e instanceof Error ? e.message : String(e)); |
|
|
setError(e instanceof Error ? e.message : String(e)); |
|
|
} finally { |
|
|
} finally { |
|
|
setDialogSaving(false); |
|
|
setDialogSaving(false); |
|
|
|
|
|
setSavingSection(null); |
|
|
saveInFlightRef.current = false; |
|
|
saveInFlightRef.current = false; |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const rulesOf = (kind: AutoPutAwayScope) => (kind === "po" ? poPutAway : joPutAway); |
|
|
const rulesOf = (kind: AutoPutAwayScope) => (kind === "po" ? poPutAway : joPutAway); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
if (loading) return; |
|
|
|
|
|
const jobs: Array<[AutoPutAwayScope, AutoPutAwayRule[]]> = []; |
|
|
|
|
|
(["po", "jo"] as const).forEach((kind) => { |
|
|
|
|
|
const current = kind === "po" ? poPutAway : joPutAway; |
|
|
|
|
|
const fixed = disableOverlappingRules(kind, current); |
|
|
|
|
|
const changed = fixed.some((rule, index) => rule.enabled !== (current[index]?.enabled !== false)); |
|
|
|
|
|
if (changed) jobs.push([kind, fixed]); |
|
|
|
|
|
}); |
|
|
|
|
|
if (jobs.length === 0) return; |
|
|
|
|
|
void (async () => { |
|
|
|
|
|
for (const [kind, rules] of jobs) { |
|
|
|
|
|
await persistPutAwayRules(kind, rules, true); |
|
|
|
|
|
} |
|
|
|
|
|
})(); |
|
|
|
|
|
}, [poPutAway, joPutAway, loading]); |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
if (loading) return; |
|
|
|
|
|
const jobs: Array<[ExcludeKind, ExcludeWarehouseRule[]]> = []; |
|
|
|
|
|
(["do", "jo"] as const).forEach((kind) => { |
|
|
|
|
|
const current = kind === "do" ? doExcludeRules : joExcludeRules; |
|
|
|
|
|
const fixed = disableOverlappingExcludeRules(current); |
|
|
|
|
|
const changed = fixed.some((rule, index) => rule.enabled !== (current[index]?.enabled !== false)); |
|
|
|
|
|
if (changed) jobs.push([kind, fixed]); |
|
|
|
|
|
}); |
|
|
|
|
|
if (jobs.length === 0) return; |
|
|
|
|
|
void (async () => { |
|
|
|
|
|
for (const [kind, rules] of jobs) { |
|
|
|
|
|
await persistExcludeRules(kind, rules, true); |
|
|
|
|
|
} |
|
|
|
|
|
})(); |
|
|
|
|
|
}, [doExcludeRules, joExcludeRules, loading]); |
|
|
|
|
|
|
|
|
|
|
|
const startRowEdit = async ( |
|
|
const startRowEdit = async ( |
|
|
kind: AutoPutAwayScope, |
|
|
kind: AutoPutAwayScope, |
|
|
index: number, |
|
|
index: number, |
|
|
@@ -759,7 +834,8 @@ const DeliveryOrderFloorSettings: React.FC = () => { |
|
|
if (field === "exceptionKeywords") { |
|
|
if (field === "exceptionKeywords") { |
|
|
rule.exceptions = csvTokens(csv).length === 0 ? [] : [{ itemKeywords: csv }]; |
|
|
rule.exceptions = csvTokens(csv).length === 0 ? [] : [{ itemKeywords: csv }]; |
|
|
} |
|
|
} |
|
|
await persistPutAwayRules(kind, next); |
|
|
|
|
|
|
|
|
stagePutAwayRules(kind, next); |
|
|
|
|
|
setPutAwayEdit(null); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const putAwayTokenKey = (kind: AutoPutAwayScope, index: number, field: "keywords" | "exceptionKeywords") => |
|
|
const putAwayTokenKey = (kind: AutoPutAwayScope, index: number, field: "keywords" | "exceptionKeywords") => |
|
|
@@ -780,7 +856,7 @@ const DeliveryOrderFloorSettings: React.FC = () => { |
|
|
if (!rule) return; |
|
|
if (!rule) return; |
|
|
if (field === "keywords") rule.itemKeywords = csv; |
|
|
if (field === "keywords") rule.itemKeywords = csv; |
|
|
else rule.exceptions = csvTokens(csv).length === 0 ? [] : [{ itemKeywords: csv }]; |
|
|
else rule.exceptions = csvTokens(csv).length === 0 ? [] : [{ itemKeywords: csv }]; |
|
|
void persistPutAwayRules(kind, next); |
|
|
|
|
|
|
|
|
stagePutAwayRules(kind, next); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const addPutAwayToken = ( |
|
|
const addPutAwayToken = ( |
|
|
@@ -848,8 +924,10 @@ const DeliveryOrderFloorSettings: React.FC = () => { |
|
|
rules={kind === "do" ? doExcludeRules : joExcludeRules} |
|
|
rules={kind === "do" ? doExcludeRules : joExcludeRules} |
|
|
warehouses={warehouseCombo ?? []} |
|
|
warehouses={warehouseCombo ?? []} |
|
|
saving={dialogSaving} |
|
|
saving={dialogSaving} |
|
|
onSave={(next) => void persistExcludeRules(kind, next)} |
|
|
|
|
|
|
|
|
showMissing={excludeMissingKind === kind} |
|
|
|
|
|
onSave={(next) => stageExcludeRules(kind, next)} |
|
|
/> |
|
|
/> |
|
|
|
|
|
{renderSectionSave(excludeDirty(kind), kind === "do" ? "exclude-do" : "exclude-jo", () => void saveExcludeRules(kind))} |
|
|
</Box> |
|
|
</Box> |
|
|
); |
|
|
); |
|
|
}; |
|
|
}; |
|
|
@@ -1010,7 +1088,7 @@ const DeliveryOrderFloorSettings: React.FC = () => { |
|
|
const next = rules.map((row, rowIndex) => |
|
|
const next = rules.map((row, rowIndex) => |
|
|
rowIndex === index ? { ...row, enabled: event.target.checked } : row, |
|
|
rowIndex === index ? { ...row, enabled: event.target.checked } : row, |
|
|
); |
|
|
); |
|
|
void persistPutAwayRules(kind, next); |
|
|
|
|
|
|
|
|
stagePutAwayRules(kind, next); |
|
|
}} |
|
|
}} |
|
|
/> |
|
|
/> |
|
|
} |
|
|
} |
|
|
@@ -1022,7 +1100,7 @@ const DeliveryOrderFloorSettings: React.FC = () => { |
|
|
size="small" |
|
|
size="small" |
|
|
color="error" |
|
|
color="error" |
|
|
disabled={dialogSaving} |
|
|
disabled={dialogSaving} |
|
|
onClick={() => void persistPutAwayRules(kind, rules.filter((_, i) => i !== index))} |
|
|
|
|
|
|
|
|
onClick={() => stagePutAwayRules(kind, rules.filter((_, i) => i !== index))} |
|
|
> |
|
|
> |
|
|
<DeleteOutline fontSize="small" /> |
|
|
<DeleteOutline fontSize="small" /> |
|
|
</IconButton> |
|
|
</IconButton> |
|
|
@@ -1156,11 +1234,16 @@ const DeliveryOrderFloorSettings: React.FC = () => { |
|
|
variant="outlined" |
|
|
variant="outlined" |
|
|
startIcon={<Add />} |
|
|
startIcon={<Add />} |
|
|
disabled={dialogSaving} |
|
|
disabled={dialogSaving} |
|
|
onClick={() => void persistPutAwayRules(kind, [...rules, emptyAutoPutAwayRule(kind)])} |
|
|
|
|
|
|
|
|
onClick={() => stagePutAwayRules(kind, [...rules, emptyAutoPutAwayRule(kind)])} |
|
|
sx={{ mt: 1.5 }} |
|
|
sx={{ mt: 1.5 }} |
|
|
> |
|
|
> |
|
|
{t("Add putaway rule")} |
|
|
{t("Add putaway rule")} |
|
|
</Button> |
|
|
</Button> |
|
|
|
|
|
{renderSectionSave( |
|
|
|
|
|
putAwayDirty(kind) && putAwayEdit?.kind !== kind, |
|
|
|
|
|
kind === "po" ? "putaway-po" : "putaway-jo", |
|
|
|
|
|
() => void savePutAwayRules(kind), |
|
|
|
|
|
)} |
|
|
</Box> |
|
|
</Box> |
|
|
); |
|
|
); |
|
|
}; |
|
|
}; |
|
|
@@ -1189,7 +1272,7 @@ const DeliveryOrderFloorSettings: React.FC = () => { |
|
|
<span> |
|
|
<span> |
|
|
<ValueChip |
|
|
<ValueChip |
|
|
label={label} |
|
|
label={label} |
|
|
onDelete={dialogSaving ? undefined : () => void persistFloorSuppliers(floor, codes.filter((item) => item !== code))} |
|
|
|
|
|
|
|
|
onDelete={dialogSaving ? undefined : () => updateFloorCodes(floor, codes.filter((item) => item !== code))} |
|
|
/> |
|
|
/> |
|
|
</span> |
|
|
</span> |
|
|
</Tooltip> |
|
|
</Tooltip> |
|
|
@@ -1318,6 +1401,7 @@ const DeliveryOrderFloorSettings: React.FC = () => { |
|
|
{renderSupplierRow("2F", codes2FList)} |
|
|
{renderSupplierRow("2F", codes2FList)} |
|
|
{renderSupplierRow("4F", codes4FList)} |
|
|
{renderSupplierRow("4F", codes4FList)} |
|
|
</Stack> |
|
|
</Stack> |
|
|
|
|
|
{renderSectionSave(suppliersDirty, "suppliers", () => void saveFloorSuppliers())} |
|
|
</Box> |
|
|
</Box> |
|
|
{renderExcludeCard("do")} |
|
|
{renderExcludeCard("do")} |
|
|
</Stack> |
|
|
</Stack> |
|
|
|