lot expiry after create round before appove tonigh lot will expiry can not do expiry handle ,block while stock taking relatestockTakeUpdate
| @@ -18,8 +18,10 @@ export interface ExpiryItemResult { | |||
| expiryDate: string | null; | |||
| remainingQty: number; | |||
| uomDesc?: string | null; | |||
| /** True when expiryDate is today or earlier. */ | |||
| /** True when expiryDate is today or earlier (and not blocked by open stock take for today-expiry). */ | |||
| canHandle?: boolean; | |||
| /** When canHandle is false: FUTURE | STOCK_TAKE_OPEN */ | |||
| canHandleBlockKey?: string | null; | |||
| } | |||
| export interface ExpiryItemFilter { | |||
| @@ -114,6 +114,13 @@ function canHandleExpiryItem(item: ExpiryItemResult): boolean { | |||
| return d != null && !d.isAfter(dayjs(), "day"); | |||
| } | |||
| function cannotHandleTooltipKey(item: ExpiryItemResult): string { | |||
| if (item.canHandleBlockKey === "STOCK_TAKE_OPEN") { | |||
| return "Expires today but in open stock take; cannot dispose"; | |||
| } | |||
| return "Not yet due; cannot dispose until the expiry date"; | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 74 | v1.0.3 | 2026-09-08 */ | |||
| const ExpiryHandleTab: React.FC = () => { | |||
| const BATCH_CHUNK_SIZE = 20; | |||
| @@ -285,7 +292,7 @@ const ExpiryHandleTab: React.FC = () => { | |||
| ); | |||
| if (canHandle) return button; | |||
| return ( | |||
| <Tooltip title={t("Not yet due; cannot dispose until the expiry date")}> | |||
| <Tooltip title={t(cannotHandleTooltipKey(item))}> | |||
| <span>{button}</span> | |||
| </Tooltip> | |||
| ); | |||
| @@ -62,6 +62,7 @@ import { stockTakeHiddenOnHand, stockTakeQtyGapWarnText } from "./stockTakeQtyGa | |||
| import { useStockTakeQtyGapWarnPercent } from "./useStockTakeQtyGapWarnPercent"; | |||
| import { useSession } from "next-auth/react"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; | |||
| import { AUTH } from "@/authorities"; | |||
| import dayjs from "dayjs"; | |||
| import { OUTPUT_DATE_FORMAT, roundDownPercent } from "@/app/utils/formatUtil"; | |||
| import { DataGrid, GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; | |||
| @@ -373,6 +374,9 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| const { t } = useTranslation(["stockTake", "common"]); | |||
| const qtyGapWarnPercent = useStockTakeQtyGapWarnPercent(); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const isAdmin = (session?.abilities ?? session?.user?.abilities ?? []).some( | |||
| (ability) => String(ability).trim() === AUTH.ADMIN, | |||
| ); | |||
| const [inventoryLotDetails, setInventoryLotDetails] = useState<InventoryLotDetailResponse[]>([]); | |||
| const [loadingDetails, setLoadingDetails] = useState(false); | |||
| @@ -1575,7 +1579,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| size="small" | |||
| variant="contained" | |||
| onClick={() => handleSaveApproverStockTake(detail)} | |||
| disabled={saving} | |||
| disabled={!isAdmin || saving} | |||
| > | |||
| {t("Save")} | |||
| </Button> | |||
| @@ -1586,7 +1590,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| variant="outlined" | |||
| color="warning" | |||
| onClick={() => handleUpdateStatusToNotMatch(detail)} | |||
| disabled={updatingStatus||hasSecond} | |||
| disabled={!isAdmin || updatingStatus || hasSecond} | |||
| > | |||
| {t("Not Match")} | |||
| </Button> | |||
| @@ -1618,6 +1622,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| approverBadQty, | |||
| saving, | |||
| updatingStatus, | |||
| isAdmin, | |||
| blockNonIntegerKeys, | |||
| sanitizeIntegerInput, | |||
| handleSaveApproverStockTake, | |||
| @@ -1632,7 +1637,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| variant="contained" | |||
| color="primary" | |||
| onClick={handleOpenBatchSaveConfirm} | |||
| disabled={batchSaving} | |||
| disabled={!isAdmin || batchSaving} | |||
| > | |||
| {t("Batch Save All")} | |||
| </Button> | |||
| @@ -223,6 +223,8 @@ interface PickerCardListProps { | |||
| locationPick: PickerLocationPick; | |||
| onLocationPickChange: (next: PickerLocationPick) => void; | |||
| stockTakeRoundId?: number | null; | |||
| /** 父層遞增:從明細返回時重抓區域卡片狀態 */ | |||
| refreshNonce?: number; | |||
| hideSectionList?: boolean; | |||
| autoOpenCreate?: boolean; | |||
| onCreateDialogClose?: () => void; | |||
| @@ -312,6 +314,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||
| locationPick, | |||
| onLocationPickChange, | |||
| stockTakeRoundId = null, | |||
| refreshNonce = 0, | |||
| hideSectionList = false, | |||
| autoOpenCreate = false, | |||
| onCreateDialogClose, | |||
| @@ -496,7 +499,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||
| return () => { | |||
| cancelled = true; | |||
| }; | |||
| }, [hideSectionList, listRefreshNonce, stockTakeRoundId]); | |||
| }, [hideSectionList, listRefreshNonce, refreshNonce, stockTakeRoundId]); | |||
| const roundSectionCodes = useMemo( | |||
| () => | |||
| @@ -705,7 +708,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||
| return () => { | |||
| cancelled = true; | |||
| }; | |||
| }, [listRefreshNonce]); | |||
| }, [listRefreshNonce, refreshNonce]); | |||
| //const startIdx = page * PER_PAGE; | |||
| //const paged = stockTakeSessions.slice(startIdx, startIdx + PER_PAGE); | |||
| @@ -762,11 +765,21 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||
| }; | |||
| }, [openConfirmDialog]); | |||
| const handleGoWarehouseSettings = useCallback(() => { | |||
| setMissingSectionWarnDrawerOpen(false); | |||
| setOpenConfirmDialog(false); | |||
| router.push("/settings/warehouse"); | |||
| }, [router]); | |||
| const handleGoWarehouseSettingsForIssue = useCallback( | |||
| (row: MissingStockTakeSectionIssueItem) => { | |||
| const params = new URLSearchParams(); | |||
| if (row.storeId?.trim()) params.set("store_id", row.storeId.trim()); | |||
| if (row.warehouse?.trim()) params.set("warehouse", row.warehouse.trim()); | |||
| if (row.area?.trim()) params.set("area", row.area.trim()); | |||
| if (row.slot?.trim()) params.set("slot", row.slot.trim()); | |||
| if (row.code?.trim()) params.set("code", row.code.trim()); | |||
| setMissingSectionWarnDrawerOpen(false); | |||
| setOpenConfirmDialog(false); | |||
| const qs = params.toString(); | |||
| router.push(qs ? `/settings/warehouse?${qs}` : "/settings/warehouse"); | |||
| }, | |||
| [router], | |||
| ); | |||
| const createStockTakeSummarySections = useMemo(() => { | |||
| return [...createDialogSelectedSections].sort((a, b) => a.localeCompare(b)); | |||
| @@ -2485,6 +2498,14 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||
| {row.order} | |||
| </Typography> | |||
| ) : null} | |||
| <Button | |||
| size="small" | |||
| variant="contained" | |||
| sx={{ mt: 1 }} | |||
| onClick={() => handleGoWarehouseSettingsForIssue(row)} | |||
| > | |||
| {t("Warehouse missing stock take section go settings")} | |||
| </Button> | |||
| </Box> | |||
| ))} | |||
| </Stack> | |||
| @@ -2503,14 +2524,6 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||
| <Button variant="outlined" onClick={() => setMissingSectionWarnDrawerOpen(false)}> | |||
| {t("Cancel")} | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| color="primary" | |||
| onClick={handleGoWarehouseSettings} | |||
| disabled={missingSectionCount === 0} | |||
| > | |||
| {t("Warehouse missing stock take section go settings")} | |||
| </Button> | |||
| </Box> | |||
| </Drawer> | |||
| </Box> | |||
| @@ -16,7 +16,6 @@ import { | |||
| DialogContent, | |||
| DialogTitle, | |||
| Grid, | |||
| LinearProgress, | |||
| Stack, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| @@ -55,11 +54,6 @@ export function roundCardToSession(card: StockTakeRoundCard): AllPickedStockTake | |||
| }; | |||
| } | |||
| function progressPercent(done: number, total: number): number { | |||
| if (total <= 0) return 0; | |||
| return Math.min(100, Math.round((done / total) * 100)); | |||
| } | |||
| interface StockTakeRoundCardListProps { | |||
| isAdmin: boolean; | |||
| onEnterRound: (card: StockTakeRoundCard, rounds: StockTakeRoundCard[]) => void; | |||
| @@ -277,35 +271,38 @@ const StockTakeRoundCardList: React.FC<StockTakeRoundCardListProps> = ({ | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Main floor")}: {card.mainFloor?.trim() || "-"} | |||
| </Typography> | |||
| <Box sx={{ mt: 1.5 }}> | |||
| <Stack direction="row" justifyContent="space-between" sx={{ mb: 0.25 }}> | |||
| <Typography variant="caption" color="text.secondary"> | |||
| <Stack | |||
| direction="row" | |||
| spacing={2} | |||
| sx={{ mt: 1.5 }} | |||
| alignItems="flex-start" | |||
| justifyContent="space-between" | |||
| > | |||
| <Stack spacing={0.25} sx={{ flex: 1, minWidth: 0 }}> | |||
| <Typography variant="caption" color="text.secondary" noWrap> | |||
| {isItemRound ? t("Stock taken lots") : t("Stock taken")} | |||
| </Typography> | |||
| <Typography variant="caption" color="text.secondary"> | |||
| {taken}/{total} | |||
| <Typography | |||
| fontWeight={700} | |||
| color="primary.main" | |||
| sx={{ fontSize: "1.5rem", lineHeight: 1.2, fontVariantNumeric: "tabular-nums" }} | |||
| > | |||
| {taken} / {total} | |||
| </Typography> | |||
| </Stack> | |||
| <LinearProgress | |||
| variant="determinate" | |||
| value={progressPercent(taken, total)} | |||
| sx={{ height: 8, borderRadius: 1, mb: 1 }} | |||
| /> | |||
| <Stack direction="row" justifyContent="space-between" sx={{ mb: 0.25 }}> | |||
| <Typography variant="caption" color="text.secondary"> | |||
| <Stack spacing={0.25} sx={{ flex: 1, minWidth: 0 }}> | |||
| <Typography variant="caption" color="text.secondary" noWrap> | |||
| {isItemRound ? t("Approved lots") : t("Approved progress")} | |||
| </Typography> | |||
| <Typography variant="caption" color="text.secondary"> | |||
| {approved}/{total} | |||
| <Typography | |||
| fontWeight={700} | |||
| color="success.main" | |||
| sx={{ fontSize: "1.5rem", lineHeight: 1.2, fontVariantNumeric: "tabular-nums" }} | |||
| > | |||
| {approved} / {total} | |||
| </Typography> | |||
| </Stack> | |||
| <LinearProgress | |||
| variant="determinate" | |||
| color="success" | |||
| value={progressPercent(approved, total)} | |||
| sx={{ height: 8, borderRadius: 1 }} | |||
| /> | |||
| </Box> | |||
| </Stack> | |||
| </CardContent> | |||
| <CardActions sx={{ pt: 0.5, justifyContent: "space-between", flexWrap: "wrap", gap: 1 }}> | |||
| <Stack direction="row" spacing={1}> | |||
| @@ -74,6 +74,7 @@ const StockTakeTab: React.FC = () => { | |||
| const [approverLoading, setApproverLoading] = useState(false); | |||
| const [pickerListPage, setPickerListPage] = useState(0); | |||
| const [pickerListPageSize] = useState(6); | |||
| const [pickerListRefreshNonce, setPickerListRefreshNonce] = useState(0); | |||
| const [pickerSearchFilters, setPickerSearchFilters] = useState<PickerCardListFilters>(DEFAULT_PICKER_CARD_LIST_FILTERS); | |||
| const [pickerAppliedFilters, setPickerAppliedFilters] = useState<PickerCardListFilters>(DEFAULT_PICKER_CARD_LIST_FILTERS); | |||
| const [pickerLocationPick, setPickerLocationPick] = useState<PickerLocationPick>(EMPTY_PICKER_LOCATION); | |||
| @@ -110,6 +111,8 @@ const StockTakeTab: React.FC = () => { | |||
| const handleBackToList = useCallback(() => { | |||
| setSelectedSession(null); | |||
| setViewMode("details"); | |||
| // 明細/重新盤點可能已改單頭狀態;列表是 display:none 未卸載,需重抓 | |||
| setPickerListRefreshNonce((n) => n + 1); | |||
| }, []); | |||
| const handleSnackbar = useCallback((message: string, severity: "success" | "error" | "warning") => { | |||
| @@ -385,6 +388,7 @@ const StockTakeTab: React.FC = () => { | |||
| locationPick={pickerLocationPick} | |||
| onLocationPickChange={setPickerLocationPick} | |||
| stockTakeRoundId={roundId} | |||
| refreshNonce={pickerListRefreshNonce} | |||
| onCardClick={(session) => { | |||
| setViewScope("picker"); | |||
| handleCardClick(session); | |||
| @@ -1,11 +1,11 @@ | |||
| "use client"; | |||
| import { useCallback, useMemo, useState } from "react"; | |||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import SearchResults, { Column } from "../SearchResults/SearchResults"; | |||
| import DeleteIcon from "@mui/icons-material/Delete"; | |||
| import EditIcon from "@mui/icons-material/Edit"; | |||
| import { useRouter } from "next/navigation"; | |||
| import { useRouter, useSearchParams } from "next/navigation"; | |||
| import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; | |||
| import { WarehouseResult } from "@/app/api/warehouse"; | |||
| import { deleteWarehouse, editWarehouse } from "@/app/api/warehouse/actions"; | |||
| @@ -31,8 +31,99 @@ interface Props { | |||
| type SearchQuery = Partial<Omit<WarehouseResult, "id">>; | |||
| type SearchParamNames = keyof SearchQuery; | |||
| type WarehouseSearchInputs = { | |||
| store_id: string; | |||
| warehouse: string; | |||
| area: string; | |||
| slot: string; | |||
| stockTakeSection: string; | |||
| stockTakeSectionDescription: string; | |||
| }; | |||
| const EMPTY_WAREHOUSE_SEARCH: WarehouseSearchInputs = { | |||
| store_id: "", | |||
| warehouse: "", | |||
| area: "", | |||
| slot: "", | |||
| stockTakeSection: "", | |||
| stockTakeSectionDescription: "", | |||
| }; | |||
| function parseWarehouseSearchFromParams(params: URLSearchParams): WarehouseSearchInputs { | |||
| const next: WarehouseSearchInputs = { | |||
| store_id: (params.get("store_id") || "").trim(), | |||
| warehouse: (params.get("warehouse") || "").trim(), | |||
| area: (params.get("area") || "").trim(), | |||
| slot: (params.get("slot") || "").trim(), | |||
| stockTakeSection: (params.get("stockTakeSection") || "").trim(), | |||
| stockTakeSectionDescription: (params.get("stockTakeSectionDescription") || "").trim(), | |||
| }; | |||
| const code = (params.get("code") || "").trim(); | |||
| if (code && !next.store_id && !next.warehouse && !next.area && !next.slot) { | |||
| const parts = code.split("-"); | |||
| if (parts.length >= 4) { | |||
| next.store_id = parts[0] || ""; | |||
| next.warehouse = parts[1] || ""; | |||
| next.area = parts[2] || ""; | |||
| next.slot = parts.slice(3).join("-") || ""; | |||
| } | |||
| } | |||
| return next; | |||
| } | |||
| function filterWarehousesBySearch( | |||
| list: WarehouseResult[], | |||
| inputs: WarehouseSearchInputs, | |||
| ): WarehouseResult[] { | |||
| const storeId = inputs.store_id?.trim() || ""; | |||
| const warehouse = inputs.warehouse?.trim() || ""; | |||
| const area = inputs.area?.trim() || ""; | |||
| const slot = inputs.slot?.trim() || ""; | |||
| const stockTakeSection = inputs.stockTakeSection?.trim() || ""; | |||
| const stockTakeSectionDescription = inputs.stockTakeSectionDescription?.trim() || ""; | |||
| if (!storeId && !warehouse && !area && !slot && !stockTakeSection && !stockTakeSectionDescription) { | |||
| return list; | |||
| } | |||
| return list.filter((warehouseItem) => { | |||
| if (stockTakeSection) { | |||
| const itemStockTakeSection = String(warehouseItem.stockTakeSection || "").toLowerCase(); | |||
| if (!itemStockTakeSection.includes(stockTakeSection.toLowerCase())) return false; | |||
| } | |||
| if (stockTakeSectionDescription) { | |||
| const itemStockTakeSectionDescription = String( | |||
| warehouseItem.stockTakeSectionDescription || "", | |||
| ).toLowerCase(); | |||
| if (!itemStockTakeSectionDescription.includes(stockTakeSectionDescription.toLowerCase())) { | |||
| return false; | |||
| } | |||
| } | |||
| if (storeId || warehouse || area || slot) { | |||
| if (!warehouseItem.code) return false; | |||
| const codeValue = String(warehouseItem.code).toLowerCase(); | |||
| const codeParts = codeValue.split("-"); | |||
| if (codeParts.length >= 4) { | |||
| const slotPart = codeParts.slice(3).join("-"); | |||
| return ( | |||
| (!storeId || codeParts[0].includes(storeId.toLowerCase())) && | |||
| (!warehouse || codeParts[1].includes(warehouse.toLowerCase())) && | |||
| (!area || codeParts[2].includes(area.toLowerCase())) && | |||
| (!slot || slotPart.includes(slot.toLowerCase())) | |||
| ); | |||
| } | |||
| return ( | |||
| (!storeId || codeValue.includes(storeId.toLowerCase())) && | |||
| (!warehouse || codeValue.includes(warehouse.toLowerCase())) && | |||
| (!area || codeValue.includes(area.toLowerCase())) && | |||
| (!slot || codeValue.includes(slot.toLowerCase())) | |||
| ); | |||
| } | |||
| return true; | |||
| }); | |||
| } | |||
| const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||
| const { t } = useTranslation(["warehouse", "common"]); | |||
| const searchParams = useSearchParams(); | |||
| const [filteredWarehouse, setFilteredWarehouse] = useState(warehouses); | |||
| const [pagingController, setPagingController] = useState({ | |||
| pageNum: 1, | |||
| @@ -51,14 +142,22 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||
| const [isSavingEdit, setIsSavingEdit] = useState(false); | |||
| const [editError, setEditError] = useState(""); | |||
| const [searchInputs, setSearchInputs] = useState({ | |||
| store_id: "", | |||
| warehouse: "", | |||
| area: "", | |||
| slot: "", | |||
| stockTakeSection: "", | |||
| stockTakeSectionDescription: "", | |||
| }); | |||
| const [searchInputs, setSearchInputs] = useState<WarehouseSearchInputs>(EMPTY_WAREHOUSE_SEARCH); | |||
| useEffect(() => { | |||
| const fromUrl = parseWarehouseSearchFromParams(searchParams); | |||
| const hasFilter = | |||
| Boolean(fromUrl.store_id) || | |||
| Boolean(fromUrl.warehouse) || | |||
| Boolean(fromUrl.area) || | |||
| Boolean(fromUrl.slot) || | |||
| Boolean(fromUrl.stockTakeSection) || | |||
| Boolean(fromUrl.stockTakeSectionDescription); | |||
| if (!hasFilter) return; | |||
| setSearchInputs(fromUrl); | |||
| setFilteredWarehouse(filterWarehousesBySearch(warehouses, fromUrl)); | |||
| setPagingController((prev) => ({ pageNum: 1, pageSize: prev.pageSize })); | |||
| }, [searchParams, warehouses]); | |||
| const onDeleteClick = useCallback((warehouse: WarehouseResult) => { | |||
| deleteDialog(async () => { | |||
| @@ -74,17 +173,13 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||
| }, [t, router]); | |||
| const handleReset = useCallback(() => { | |||
| setSearchInputs({ | |||
| store_id: "", | |||
| warehouse: "", | |||
| area: "", | |||
| slot: "", | |||
| stockTakeSection: "", | |||
| stockTakeSectionDescription: "", | |||
| }); | |||
| setSearchInputs(EMPTY_WAREHOUSE_SEARCH); | |||
| setFilteredWarehouse(warehouses); | |||
| setPagingController({ pageNum: 1, pageSize: pagingController.pageSize }); | |||
| }, [warehouses, pagingController.pageSize]); | |||
| if (searchParams.toString()) { | |||
| router.replace("/settings/warehouse"); | |||
| } | |||
| }, [warehouses, pagingController.pageSize, searchParams, router]); | |||
| const onEditClick = useCallback((warehouse: WarehouseResult) => { | |||
| setEditingWarehouse(warehouse); | |||
| @@ -154,114 +249,11 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||
| const handleSearch = useCallback(() => { | |||
| setIsSearching(true); | |||
| try { | |||
| let results: WarehouseResult[] = warehouses; | |||
| const storeId = searchInputs.store_id?.trim() || ""; | |||
| const warehouse = searchInputs.warehouse?.trim() || ""; | |||
| const area = searchInputs.area?.trim() || ""; | |||
| const slot = searchInputs.slot?.trim() || ""; | |||
| const stockTakeSection = searchInputs.stockTakeSection?.trim() || ""; | |||
| const stockTakeSectionDescription = searchInputs.stockTakeSectionDescription?.trim() || ""; | |||
| if (storeId || warehouse || area || slot || stockTakeSection || stockTakeSectionDescription) { | |||
| results = warehouses.filter((warehouseItem) => { | |||
| if (stockTakeSection) { | |||
| const itemStockTakeSection = String(warehouseItem.stockTakeSection || "").toLowerCase(); | |||
| if (!itemStockTakeSection.includes(stockTakeSection.toLowerCase())) { | |||
| return false; | |||
| } | |||
| } | |||
| if (stockTakeSectionDescription) { | |||
| const itemStockTakeSectionDescription = String(warehouseItem.stockTakeSectionDescription || "").toLowerCase(); | |||
| if (!itemStockTakeSectionDescription.includes(stockTakeSectionDescription.toLowerCase())) { | |||
| return false; | |||
| } | |||
| } | |||
| if (storeId || warehouse || area || slot) { | |||
| if (!warehouseItem.code) { | |||
| return false; | |||
| } | |||
| const codeValue = String(warehouseItem.code).toLowerCase(); | |||
| const codeParts = codeValue.split("-"); | |||
| if (codeParts.length >= 4) { | |||
| const codeStoreId = codeParts[0] || ""; | |||
| const codeWarehouse = codeParts[1] || ""; | |||
| const codeArea = codeParts[2] || ""; | |||
| const codeSlot = codeParts[3] || ""; | |||
| const storeIdMatch = !storeId || codeStoreId.includes(storeId.toLowerCase()); | |||
| const warehouseMatch = !warehouse || codeWarehouse.includes(warehouse.toLowerCase()); | |||
| const areaMatch = !area || codeArea.includes(area.toLowerCase()); | |||
| const slotMatch = !slot || codeSlot.includes(slot.toLowerCase()); | |||
| return storeIdMatch && warehouseMatch && areaMatch && slotMatch; | |||
| } | |||
| const storeIdMatch = !storeId || codeValue.includes(storeId.toLowerCase()); | |||
| const warehouseMatch = !warehouse || codeValue.includes(warehouse.toLowerCase()); | |||
| const areaMatch = !area || codeValue.includes(area.toLowerCase()); | |||
| const slotMatch = !slot || codeValue.includes(slot.toLowerCase()); | |||
| return storeIdMatch && warehouseMatch && areaMatch && slotMatch; | |||
| } | |||
| return true; | |||
| }); | |||
| } else { | |||
| results = warehouses; | |||
| } | |||
| setFilteredWarehouse(results); | |||
| setFilteredWarehouse(filterWarehousesBySearch(warehouses, searchInputs)); | |||
| setPagingController({ pageNum: 1, pageSize: pagingController.pageSize }); | |||
| } catch (error) { | |||
| console.error("Error searching warehouses:", error); | |||
| const storeId = searchInputs.store_id?.trim().toLowerCase() || ""; | |||
| const warehouse = searchInputs.warehouse?.trim().toLowerCase() || ""; | |||
| const area = searchInputs.area?.trim().toLowerCase() || ""; | |||
| const slot = searchInputs.slot?.trim().toLowerCase() || ""; | |||
| const stockTakeSection = searchInputs.stockTakeSection?.trim().toLowerCase() || ""; | |||
| const stockTakeSectionDescription = searchInputs.stockTakeSectionDescription?.trim().toLowerCase() || ""; | |||
| setFilteredWarehouse( | |||
| warehouses.filter((warehouseItem) => { | |||
| if (stockTakeSection) { | |||
| const itemStockTakeSection = String(warehouseItem.stockTakeSection || "").toLowerCase(); | |||
| if (!itemStockTakeSection.includes(stockTakeSection)) { | |||
| return false; | |||
| } | |||
| } | |||
| if (stockTakeSectionDescription) { | |||
| const itemStockTakeSectionDescription = String(warehouseItem.stockTakeSectionDescription || "").toLowerCase(); | |||
| if (!itemStockTakeSectionDescription.includes(stockTakeSectionDescription)) { | |||
| return false; | |||
| } | |||
| } | |||
| if (storeId || warehouse || area || slot) { | |||
| if (!warehouseItem.code) { | |||
| return false; | |||
| } | |||
| const codeValue = String(warehouseItem.code).toLowerCase(); | |||
| const codeParts = codeValue.split("-"); | |||
| if (codeParts.length >= 4) { | |||
| const storeIdMatch = !storeId || codeParts[0].includes(storeId); | |||
| const warehouseMatch = !warehouse || codeParts[1].includes(warehouse); | |||
| const areaMatch = !area || codeParts[2].includes(area); | |||
| const slotMatch = !slot || codeParts[3].includes(slot); | |||
| return storeIdMatch && warehouseMatch && areaMatch && slotMatch; | |||
| } | |||
| return (!storeId || codeValue.includes(storeId)) && | |||
| (!warehouse || codeValue.includes(warehouse)) && | |||
| (!area || codeValue.includes(area)) && | |||
| (!slot || codeValue.includes(slot)); | |||
| } | |||
| return true; | |||
| }) | |||
| ); | |||
| setFilteredWarehouse(filterWarehousesBySearch(warehouses, searchInputs)); | |||
| } finally { | |||
| setIsSearching(false); | |||
| } | |||
| @@ -39,6 +39,7 @@ | |||
| "Failed to submit": "Failed to submit", | |||
| "Failed to submit expiry item": "Failed to submit expiry item", | |||
| "Not yet due; cannot dispose until the expiry date": "Not yet due; cannot dispose until the expiry date", | |||
| "Expires today but in open stock take; cannot dispose": "Expires today but this lot is in an open stock take; dispose after the round finishes or the lot leaves the round", | |||
| "Handled Date": "Handled Date", | |||
| "Handled Start Date": "Handled Start Date", | |||
| "Handled End Date": "Handled End Date", | |||
| @@ -181,9 +181,9 @@ | |||
| "Warehouse": "Warehouse", | |||
| "Warehouse code": "Warehouse code", | |||
| "Warehouse Location": "Warehouse Location", | |||
| "Warehouse missing stock take section drawer hint": "These locations have no stock take section (ST-xxx) and cannot be included in stock take. Fix in warehouse settings.", | |||
| "Warehouse missing stock take section drawer hint": "These locations have no stock take section (ST-xxx) and cannot be included in stock take. Use “Go to this warehouse” on each row to open settings with that location searched.", | |||
| "Warehouse missing stock take section empty": "No warehouses missing stock take section", | |||
| "Warehouse missing stock take section go settings": "Go to warehouse settings", | |||
| "Warehouse missing stock take section go settings": "Go to this warehouse", | |||
| "Warehouse missing stock take section showing": "Showing {{shown}} of {{count}}", | |||
| "Warehouse missing stock take section tooltip has": "{{count}} warehouse location(s) missing stock take section — click to view", | |||
| "Warehouse missing stock take section tooltip none": "All warehouses have a stock take section", | |||
| @@ -39,6 +39,7 @@ | |||
| "Failed to submit": "提交失敗", | |||
| "Failed to submit expiry item": "提交過期品失敗", | |||
| "Not yet due; cannot dispose until the expiry date": "尚未到期,到期日當日才可處置", | |||
| "Expires today but in open stock take; cannot dispose": "今日到期但盤點進行中,請待盤點結束或該批退出盤點後再處置", | |||
| "Handled Date": "處理日期", | |||
| "Handled Start Date": "處理日期(開始)", | |||
| "Handled End Date": "處理日期(結束)", | |||
| @@ -162,10 +162,10 @@ | |||
| "No sections match search": "沒有符合搜索條件的區域", | |||
| "Confirm create stock take": "確認建立盤點", | |||
| "Not filled": "(未填寫)", | |||
| "Warehouse missing stock take section drawer hint": "以下倉庫位置尚未設定盤點區域(ST-xxx),無法納入盤點區域選擇。請至倉庫設定補上。", | |||
| "Warehouse missing stock take section drawer hint": "以下倉庫位置尚未設定盤點區域(ST-xxx),無法納入盤點區域選擇。請點各筆「前往此倉庫設定」補上。", | |||
| "Warehouse missing stock take section showing": "顯示前 {{shown}} 筆,共 {{count}} 筆", | |||
| "Warehouse missing stock take section empty": "沒有未設定盤點區域的倉庫", | |||
| "Warehouse missing stock take section go settings": "前往倉庫設定", | |||
| "Warehouse missing stock take section go settings": "前往此倉庫設定", | |||
| "Available QTY cannot be negative": "可用數量不能為負數", | |||
| "Stock take record saved successfully": "盤點記錄保存成功", | |||
| "Batch save completed: {{success}} success, {{errors}} errors": "批量保存完成:{{success}} 成功,{{errors}} 失敗", | |||