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; | expiryDate: string | null; | ||||
| remainingQty: number; | remainingQty: number; | ||||
| uomDesc?: string | null; | 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; | canHandle?: boolean; | ||||
| /** When canHandle is false: FUTURE | STOCK_TAKE_OPEN */ | |||||
| canHandleBlockKey?: string | null; | |||||
| } | } | ||||
| export interface ExpiryItemFilter { | export interface ExpiryItemFilter { | ||||
| @@ -114,6 +114,13 @@ function canHandleExpiryItem(item: ExpiryItemResult): boolean { | |||||
| return d != null && !d.isAfter(dayjs(), "day"); | 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 */ | /** FP-MTMS Version Checklist | Functions Ref. No. 74 | v1.0.3 | 2026-09-08 */ | ||||
| const ExpiryHandleTab: React.FC = () => { | const ExpiryHandleTab: React.FC = () => { | ||||
| const BATCH_CHUNK_SIZE = 20; | const BATCH_CHUNK_SIZE = 20; | ||||
| @@ -285,7 +292,7 @@ const ExpiryHandleTab: React.FC = () => { | |||||
| ); | ); | ||||
| if (canHandle) return button; | if (canHandle) return button; | ||||
| return ( | return ( | ||||
| <Tooltip title={t("Not yet due; cannot dispose until the expiry date")}> | |||||
| <Tooltip title={t(cannotHandleTooltipKey(item))}> | |||||
| <span>{button}</span> | <span>{button}</span> | ||||
| </Tooltip> | </Tooltip> | ||||
| ); | ); | ||||
| @@ -62,6 +62,7 @@ import { stockTakeHiddenOnHand, stockTakeQtyGapWarnText } from "./stockTakeQtyGa | |||||
| import { useStockTakeQtyGapWarnPercent } from "./useStockTakeQtyGapWarnPercent"; | import { useStockTakeQtyGapWarnPercent } from "./useStockTakeQtyGapWarnPercent"; | ||||
| import { useSession } from "next-auth/react"; | import { useSession } from "next-auth/react"; | ||||
| import { SessionWithTokens } from "@/config/authConfig"; | import { SessionWithTokens } from "@/config/authConfig"; | ||||
| import { AUTH } from "@/authorities"; | |||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import { OUTPUT_DATE_FORMAT, roundDownPercent } from "@/app/utils/formatUtil"; | import { OUTPUT_DATE_FORMAT, roundDownPercent } from "@/app/utils/formatUtil"; | ||||
| import { DataGrid, GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; | import { DataGrid, GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; | ||||
| @@ -373,6 +374,9 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||||
| const { t } = useTranslation(["stockTake", "common"]); | const { t } = useTranslation(["stockTake", "common"]); | ||||
| const qtyGapWarnPercent = useStockTakeQtyGapWarnPercent(); | const qtyGapWarnPercent = useStockTakeQtyGapWarnPercent(); | ||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | 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 [inventoryLotDetails, setInventoryLotDetails] = useState<InventoryLotDetailResponse[]>([]); | ||||
| const [loadingDetails, setLoadingDetails] = useState(false); | const [loadingDetails, setLoadingDetails] = useState(false); | ||||
| @@ -1575,7 +1579,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||||
| size="small" | size="small" | ||||
| variant="contained" | variant="contained" | ||||
| onClick={() => handleSaveApproverStockTake(detail)} | onClick={() => handleSaveApproverStockTake(detail)} | ||||
| disabled={saving} | |||||
| disabled={!isAdmin || saving} | |||||
| > | > | ||||
| {t("Save")} | {t("Save")} | ||||
| </Button> | </Button> | ||||
| @@ -1586,7 +1590,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||||
| variant="outlined" | variant="outlined" | ||||
| color="warning" | color="warning" | ||||
| onClick={() => handleUpdateStatusToNotMatch(detail)} | onClick={() => handleUpdateStatusToNotMatch(detail)} | ||||
| disabled={updatingStatus||hasSecond} | |||||
| disabled={!isAdmin || updatingStatus || hasSecond} | |||||
| > | > | ||||
| {t("Not Match")} | {t("Not Match")} | ||||
| </Button> | </Button> | ||||
| @@ -1618,6 +1622,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||||
| approverBadQty, | approverBadQty, | ||||
| saving, | saving, | ||||
| updatingStatus, | updatingStatus, | ||||
| isAdmin, | |||||
| blockNonIntegerKeys, | blockNonIntegerKeys, | ||||
| sanitizeIntegerInput, | sanitizeIntegerInput, | ||||
| handleSaveApproverStockTake, | handleSaveApproverStockTake, | ||||
| @@ -1632,7 +1637,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||||
| variant="contained" | variant="contained" | ||||
| color="primary" | color="primary" | ||||
| onClick={handleOpenBatchSaveConfirm} | onClick={handleOpenBatchSaveConfirm} | ||||
| disabled={batchSaving} | |||||
| disabled={!isAdmin || batchSaving} | |||||
| > | > | ||||
| {t("Batch Save All")} | {t("Batch Save All")} | ||||
| </Button> | </Button> | ||||
| @@ -223,6 +223,8 @@ interface PickerCardListProps { | |||||
| locationPick: PickerLocationPick; | locationPick: PickerLocationPick; | ||||
| onLocationPickChange: (next: PickerLocationPick) => void; | onLocationPickChange: (next: PickerLocationPick) => void; | ||||
| stockTakeRoundId?: number | null; | stockTakeRoundId?: number | null; | ||||
| /** 父層遞增:從明細返回時重抓區域卡片狀態 */ | |||||
| refreshNonce?: number; | |||||
| hideSectionList?: boolean; | hideSectionList?: boolean; | ||||
| autoOpenCreate?: boolean; | autoOpenCreate?: boolean; | ||||
| onCreateDialogClose?: () => void; | onCreateDialogClose?: () => void; | ||||
| @@ -312,6 +314,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||||
| locationPick, | locationPick, | ||||
| onLocationPickChange, | onLocationPickChange, | ||||
| stockTakeRoundId = null, | stockTakeRoundId = null, | ||||
| refreshNonce = 0, | |||||
| hideSectionList = false, | hideSectionList = false, | ||||
| autoOpenCreate = false, | autoOpenCreate = false, | ||||
| onCreateDialogClose, | onCreateDialogClose, | ||||
| @@ -496,7 +499,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||||
| return () => { | return () => { | ||||
| cancelled = true; | cancelled = true; | ||||
| }; | }; | ||||
| }, [hideSectionList, listRefreshNonce, stockTakeRoundId]); | |||||
| }, [hideSectionList, listRefreshNonce, refreshNonce, stockTakeRoundId]); | |||||
| const roundSectionCodes = useMemo( | const roundSectionCodes = useMemo( | ||||
| () => | () => | ||||
| @@ -705,7 +708,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||||
| return () => { | return () => { | ||||
| cancelled = true; | cancelled = true; | ||||
| }; | }; | ||||
| }, [listRefreshNonce]); | |||||
| }, [listRefreshNonce, refreshNonce]); | |||||
| //const startIdx = page * PER_PAGE; | //const startIdx = page * PER_PAGE; | ||||
| //const paged = stockTakeSessions.slice(startIdx, startIdx + PER_PAGE); | //const paged = stockTakeSessions.slice(startIdx, startIdx + PER_PAGE); | ||||
| @@ -762,11 +765,21 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||||
| }; | }; | ||||
| }, [openConfirmDialog]); | }, [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(() => { | const createStockTakeSummarySections = useMemo(() => { | ||||
| return [...createDialogSelectedSections].sort((a, b) => a.localeCompare(b)); | return [...createDialogSelectedSections].sort((a, b) => a.localeCompare(b)); | ||||
| @@ -2485,6 +2498,14 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||||
| {row.order} | {row.order} | ||||
| </Typography> | </Typography> | ||||
| ) : null} | ) : null} | ||||
| <Button | |||||
| size="small" | |||||
| variant="contained" | |||||
| sx={{ mt: 1 }} | |||||
| onClick={() => handleGoWarehouseSettingsForIssue(row)} | |||||
| > | |||||
| {t("Warehouse missing stock take section go settings")} | |||||
| </Button> | |||||
| </Box> | </Box> | ||||
| ))} | ))} | ||||
| </Stack> | </Stack> | ||||
| @@ -2503,14 +2524,6 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||||
| <Button variant="outlined" onClick={() => setMissingSectionWarnDrawerOpen(false)}> | <Button variant="outlined" onClick={() => setMissingSectionWarnDrawerOpen(false)}> | ||||
| {t("Cancel")} | {t("Cancel")} | ||||
| </Button> | </Button> | ||||
| <Button | |||||
| variant="contained" | |||||
| color="primary" | |||||
| onClick={handleGoWarehouseSettings} | |||||
| disabled={missingSectionCount === 0} | |||||
| > | |||||
| {t("Warehouse missing stock take section go settings")} | |||||
| </Button> | |||||
| </Box> | </Box> | ||||
| </Drawer> | </Drawer> | ||||
| </Box> | </Box> | ||||
| @@ -16,7 +16,6 @@ import { | |||||
| DialogContent, | DialogContent, | ||||
| DialogTitle, | DialogTitle, | ||||
| Grid, | Grid, | ||||
| LinearProgress, | |||||
| Stack, | Stack, | ||||
| Typography, | Typography, | ||||
| } from "@mui/material"; | } 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 { | interface StockTakeRoundCardListProps { | ||||
| isAdmin: boolean; | isAdmin: boolean; | ||||
| onEnterRound: (card: StockTakeRoundCard, rounds: StockTakeRoundCard[]) => void; | onEnterRound: (card: StockTakeRoundCard, rounds: StockTakeRoundCard[]) => void; | ||||
| @@ -277,35 +271,38 @@ const StockTakeRoundCardList: React.FC<StockTakeRoundCardListProps> = ({ | |||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("Main floor")}: {card.mainFloor?.trim() || "-"} | {t("Main floor")}: {card.mainFloor?.trim() || "-"} | ||||
| </Typography> | </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")} | {isItemRound ? t("Stock taken lots") : t("Stock taken")} | ||||
| </Typography> | </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> | </Typography> | ||||
| </Stack> | </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")} | {isItemRound ? t("Approved lots") : t("Approved progress")} | ||||
| </Typography> | </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> | </Typography> | ||||
| </Stack> | </Stack> | ||||
| <LinearProgress | |||||
| variant="determinate" | |||||
| color="success" | |||||
| value={progressPercent(approved, total)} | |||||
| sx={{ height: 8, borderRadius: 1 }} | |||||
| /> | |||||
| </Box> | |||||
| </Stack> | |||||
| </CardContent> | </CardContent> | ||||
| <CardActions sx={{ pt: 0.5, justifyContent: "space-between", flexWrap: "wrap", gap: 1 }}> | <CardActions sx={{ pt: 0.5, justifyContent: "space-between", flexWrap: "wrap", gap: 1 }}> | ||||
| <Stack direction="row" spacing={1}> | <Stack direction="row" spacing={1}> | ||||
| @@ -74,6 +74,7 @@ const StockTakeTab: React.FC = () => { | |||||
| const [approverLoading, setApproverLoading] = useState(false); | const [approverLoading, setApproverLoading] = useState(false); | ||||
| const [pickerListPage, setPickerListPage] = useState(0); | const [pickerListPage, setPickerListPage] = useState(0); | ||||
| const [pickerListPageSize] = useState(6); | const [pickerListPageSize] = useState(6); | ||||
| const [pickerListRefreshNonce, setPickerListRefreshNonce] = useState(0); | |||||
| const [pickerSearchFilters, setPickerSearchFilters] = useState<PickerCardListFilters>(DEFAULT_PICKER_CARD_LIST_FILTERS); | const [pickerSearchFilters, setPickerSearchFilters] = useState<PickerCardListFilters>(DEFAULT_PICKER_CARD_LIST_FILTERS); | ||||
| const [pickerAppliedFilters, setPickerAppliedFilters] = 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); | const [pickerLocationPick, setPickerLocationPick] = useState<PickerLocationPick>(EMPTY_PICKER_LOCATION); | ||||
| @@ -110,6 +111,8 @@ const StockTakeTab: React.FC = () => { | |||||
| const handleBackToList = useCallback(() => { | const handleBackToList = useCallback(() => { | ||||
| setSelectedSession(null); | setSelectedSession(null); | ||||
| setViewMode("details"); | setViewMode("details"); | ||||
| // 明細/重新盤點可能已改單頭狀態;列表是 display:none 未卸載,需重抓 | |||||
| setPickerListRefreshNonce((n) => n + 1); | |||||
| }, []); | }, []); | ||||
| const handleSnackbar = useCallback((message: string, severity: "success" | "error" | "warning") => { | const handleSnackbar = useCallback((message: string, severity: "success" | "error" | "warning") => { | ||||
| @@ -385,6 +388,7 @@ const StockTakeTab: React.FC = () => { | |||||
| locationPick={pickerLocationPick} | locationPick={pickerLocationPick} | ||||
| onLocationPickChange={setPickerLocationPick} | onLocationPickChange={setPickerLocationPick} | ||||
| stockTakeRoundId={roundId} | stockTakeRoundId={roundId} | ||||
| refreshNonce={pickerListRefreshNonce} | |||||
| onCardClick={(session) => { | onCardClick={(session) => { | ||||
| setViewScope("picker"); | setViewScope("picker"); | ||||
| handleCardClick(session); | handleCardClick(session); | ||||
| @@ -1,11 +1,11 @@ | |||||
| "use client"; | "use client"; | ||||
| import { useCallback, useMemo, useState } from "react"; | |||||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import SearchResults, { Column } from "../SearchResults/SearchResults"; | import SearchResults, { Column } from "../SearchResults/SearchResults"; | ||||
| import DeleteIcon from "@mui/icons-material/Delete"; | import DeleteIcon from "@mui/icons-material/Delete"; | ||||
| import EditIcon from "@mui/icons-material/Edit"; | 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 { deleteDialog, successDialog } from "../Swal/CustomAlerts"; | ||||
| import { WarehouseResult } from "@/app/api/warehouse"; | import { WarehouseResult } from "@/app/api/warehouse"; | ||||
| import { deleteWarehouse, editWarehouse } from "@/app/api/warehouse/actions"; | import { deleteWarehouse, editWarehouse } from "@/app/api/warehouse/actions"; | ||||
| @@ -31,8 +31,99 @@ interface Props { | |||||
| type SearchQuery = Partial<Omit<WarehouseResult, "id">>; | type SearchQuery = Partial<Omit<WarehouseResult, "id">>; | ||||
| type SearchParamNames = keyof SearchQuery; | 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 WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | ||||
| const { t } = useTranslation(["warehouse", "common"]); | const { t } = useTranslation(["warehouse", "common"]); | ||||
| const searchParams = useSearchParams(); | |||||
| const [filteredWarehouse, setFilteredWarehouse] = useState(warehouses); | const [filteredWarehouse, setFilteredWarehouse] = useState(warehouses); | ||||
| const [pagingController, setPagingController] = useState({ | const [pagingController, setPagingController] = useState({ | ||||
| pageNum: 1, | pageNum: 1, | ||||
| @@ -51,14 +142,22 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| const [isSavingEdit, setIsSavingEdit] = useState(false); | const [isSavingEdit, setIsSavingEdit] = useState(false); | ||||
| const [editError, setEditError] = useState(""); | 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) => { | const onDeleteClick = useCallback((warehouse: WarehouseResult) => { | ||||
| deleteDialog(async () => { | deleteDialog(async () => { | ||||
| @@ -74,17 +173,13 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| }, [t, router]); | }, [t, router]); | ||||
| const handleReset = useCallback(() => { | const handleReset = useCallback(() => { | ||||
| setSearchInputs({ | |||||
| store_id: "", | |||||
| warehouse: "", | |||||
| area: "", | |||||
| slot: "", | |||||
| stockTakeSection: "", | |||||
| stockTakeSectionDescription: "", | |||||
| }); | |||||
| setSearchInputs(EMPTY_WAREHOUSE_SEARCH); | |||||
| setFilteredWarehouse(warehouses); | setFilteredWarehouse(warehouses); | ||||
| setPagingController({ pageNum: 1, pageSize: pagingController.pageSize }); | 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) => { | const onEditClick = useCallback((warehouse: WarehouseResult) => { | ||||
| setEditingWarehouse(warehouse); | setEditingWarehouse(warehouse); | ||||
| @@ -154,114 +249,11 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| const handleSearch = useCallback(() => { | const handleSearch = useCallback(() => { | ||||
| setIsSearching(true); | setIsSearching(true); | ||||
| try { | 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 }); | setPagingController({ pageNum: 1, pageSize: pagingController.pageSize }); | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Error searching warehouses:", 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 { | } finally { | ||||
| setIsSearching(false); | setIsSearching(false); | ||||
| } | } | ||||
| @@ -39,6 +39,7 @@ | |||||
| "Failed to submit": "Failed to submit", | "Failed to submit": "Failed to submit", | ||||
| "Failed to submit expiry item": "Failed to submit expiry item", | "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", | "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 Date": "Handled Date", | ||||
| "Handled Start Date": "Handled Start Date", | "Handled Start Date": "Handled Start Date", | ||||
| "Handled End Date": "Handled End Date", | "Handled End Date": "Handled End Date", | ||||
| @@ -181,9 +181,9 @@ | |||||
| "Warehouse": "Warehouse", | "Warehouse": "Warehouse", | ||||
| "Warehouse code": "Warehouse code", | "Warehouse code": "Warehouse code", | ||||
| "Warehouse Location": "Warehouse Location", | "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 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 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 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", | "Warehouse missing stock take section tooltip none": "All warehouses have a stock take section", | ||||
| @@ -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": "今日到期但盤點進行中,請待盤點結束或該批退出盤點後再處置", | |||||
| "Handled Date": "處理日期", | "Handled Date": "處理日期", | ||||
| "Handled Start Date": "處理日期(開始)", | "Handled Start Date": "處理日期(開始)", | ||||
| "Handled End Date": "處理日期(結束)", | "Handled End Date": "處理日期(結束)", | ||||
| @@ -162,10 +162,10 @@ | |||||
| "No sections match search": "沒有符合搜索條件的區域", | "No sections match search": "沒有符合搜索條件的區域", | ||||
| "Confirm create stock take": "確認建立盤點", | "Confirm create stock take": "確認建立盤點", | ||||
| "Not filled": "(未填寫)", | "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 showing": "顯示前 {{shown}} 筆,共 {{count}} 筆", | ||||
| "Warehouse missing stock take section empty": "沒有未設定盤點區域的倉庫", | "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": "可用數量不能為負數", | "Available QTY cannot be negative": "可用數量不能為負數", | ||||
| "Stock take record saved successfully": "盤點記錄保存成功", | "Stock take record saved successfully": "盤點記錄保存成功", | ||||
| "Batch save completed: {{success}} success, {{errors}} errors": "批量保存完成:{{success}} 成功,{{errors}} 失敗", | "Batch save completed: {{success}} success, {{errors}} errors": "批量保存完成:{{success}} 成功,{{errors}} 失敗", | ||||