WorkbenchTicketReleaseTable 加 user filter DO release / FloorLanePanel:Truck X 票 + 搜尋依樓層production
| @@ -242,13 +242,16 @@ export async function fetchWorkbenchReleasedDoPickOrdersForSelection( | |||
| shopName?: string, | |||
| storeId?: string, | |||
| truck?: string, | |||
| releaseType?: string | |||
| releaseType?: string, | |||
| /** Optional `2F`/`4F`: Truck X split by DO supplier preferred floor (storeId stays null). */ | |||
| floor?: string | |||
| ): Promise<ReleasedDoPickOrderListItem[]> { | |||
| const params = new URLSearchParams(); | |||
| if (shopName?.trim()) params.append("shopName", shopName.trim()); | |||
| if (storeId?.trim()) params.append("storeId", storeId.trim()); | |||
| if (truck?.trim()) params.append("truck", truck.trim()); | |||
| if (releaseType?.trim()) params.append("releaseType", releaseType.trim()); | |||
| if (floor?.trim()) params.append("floor", floor.trim().replace("/", "").toUpperCase()); | |||
| const query = params.toString(); | |||
| const url = `${BASE_API_URL}/doPickOrder/workbench/released${query ? `?${query}` : ""}`; | |||
| const response = await serverFetchJson<ReleasedDoPickOrderListItem[]>(url, { method: "GET" }); | |||
| @@ -261,7 +264,9 @@ export async function fetchWorkbenchReleasedDoPickOrdersForSelectionToday( | |||
| storeId?: string, | |||
| truck?: string, | |||
| requiredDeliveryDate?: string, | |||
| releaseType?: string | |||
| releaseType?: string, | |||
| /** Optional `2F`/`4F`: Truck X split by DO supplier preferred floor (storeId stays null). */ | |||
| floor?: string | |||
| ): Promise<ReleasedDoPickOrderListItem[]> { | |||
| const params = new URLSearchParams(); | |||
| if (shopName?.trim()) params.append("shopName", shopName.trim()); | |||
| @@ -269,6 +274,7 @@ export async function fetchWorkbenchReleasedDoPickOrdersForSelectionToday( | |||
| if (truck?.trim()) params.append("truck", truck.trim()); | |||
| if (requiredDeliveryDate?.trim()) params.append("requiredDate", requiredDeliveryDate.trim()); | |||
| if (releaseType?.trim()) params.append("releaseType", releaseType.trim()); | |||
| if (floor?.trim()) params.append("floor", floor.trim().replace("/", "").toUpperCase()); | |||
| const query = params.toString(); | |||
| const url = `${BASE_API_URL}/doPickOrder/workbench/released-today${query ? `?${query}` : ""}`; | |||
| const response = await serverFetchJson<ReleasedDoPickOrderListItem[]>(url, { method: "GET" }); | |||
| @@ -85,7 +85,13 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ | |||
| const [modalReleaseTypeFilter, setModalReleaseTypeFilter] = useState<string | undefined>(undefined); | |||
| const [modalFilterRequiredDeliveryDate, setModalFilterRequiredDeliveryDate] = useState<string | undefined>(undefined); | |||
| const [modalInitialShopSearch, setModalInitialShopSearch] = useState<string | undefined>(undefined); | |||
| const defaultTruckCount = summary4F?.defaultTruckCount ?? 0; | |||
| const [modalTruckXFloor, setModalTruckXFloor] = useState<string | undefined>(undefined); | |||
| const ticketFloorApiKey = useMemo( | |||
| () => ticketFloor.replace("/", "").trim().toUpperCase(), | |||
| [ticketFloor], | |||
| ); | |||
| const defaultTruckCount = | |||
| (ticketFloor === "2/F" ? summary2F?.defaultTruckCount : summary4F?.defaultTruckCount) ?? 0; | |||
| const etraEnterInFlightRef = useRef(false); | |||
| const [etraMergeDialogOpen, setEtraMergeDialogOpen] = useState(false); | |||
| @@ -108,16 +114,25 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ | |||
| shopName?: string, | |||
| storeId?: string, | |||
| truck?: string, | |||
| releaseType?: string | |||
| ) => fetchWorkbenchReleasedDoPickOrdersForSelection(shopName, storeId, truck, releaseType), | |||
| releaseType?: string, | |||
| floor?: string | |||
| ) => fetchWorkbenchReleasedDoPickOrdersForSelection(shopName, storeId, truck, releaseType, floor), | |||
| loadToday: ( | |||
| shopName?: string, | |||
| storeId?: string, | |||
| truck?: string, | |||
| requiredDeliveryDate?: string, | |||
| releaseType?: string | |||
| releaseType?: string, | |||
| floor?: string | |||
| ) => | |||
| fetchWorkbenchReleasedDoPickOrdersForSelectionToday(shopName, storeId, truck, requiredDeliveryDate, releaseType), | |||
| fetchWorkbenchReleasedDoPickOrdersForSelectionToday( | |||
| shopName, | |||
| storeId, | |||
| truck, | |||
| requiredDeliveryDate, | |||
| releaseType, | |||
| floor | |||
| ), | |||
| assignByListItemId: assignByDeliveryOrderPickOrderId, | |||
| }), | |||
| [], | |||
| @@ -234,7 +249,14 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ | |||
| pendingRef.current += 1; | |||
| startFullTimer(); | |||
| try { | |||
| const list = await fetchWorkbenchReleasedDoPickOrdersForSelection(undefined, undefined, "車線-X"); | |||
| // storeId stays undefined (Truck X); floor splits by DO supplier preferred floor. | |||
| const list = await fetchWorkbenchReleasedDoPickOrdersForSelection( | |||
| undefined, | |||
| undefined, | |||
| "車線-X", | |||
| undefined, | |||
| ticketFloorApiKey | |||
| ); | |||
| setBeforeTodayTruckXCount(list.length); | |||
| } catch { | |||
| setBeforeTodayTruckXCount(0); | |||
| @@ -244,12 +266,13 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ | |||
| } | |||
| }; | |||
| void loadBeforeTodayTruckX(); | |||
| }, [inEtraUi]); | |||
| }, [inEtraUi, ticketFloorApiKey]); | |||
| const clearModalEtraContext = useCallback(() => { | |||
| setModalReleaseTypeFilter(undefined); | |||
| setModalFilterRequiredDeliveryDate(undefined); | |||
| setModalInitialShopSearch(undefined); | |||
| setModalTruckXFloor(undefined); | |||
| }, []); | |||
| const openEnterEtraView = useCallback(async () => { | |||
| @@ -538,6 +561,7 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ | |||
| setSelectedTruck("車線-X"); | |||
| setIsDefaultTruck(true); | |||
| setDefaultDateScope("today"); | |||
| setModalTruckXFloor(ticketFloorApiKey); | |||
| setModalOpen(true); | |||
| }} | |||
| > | |||
| @@ -638,10 +662,11 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ | |||
| variant="outlined" | |||
| onClick={() => { | |||
| clearModalEtraContext(); | |||
| setSelectedStore("4/F"); | |||
| setSelectedStore(""); | |||
| setSelectedTruck("車線-X"); | |||
| setIsDefaultTruck(true); | |||
| setDefaultDateScope("before"); | |||
| setModalTruckXFloor(ticketFloorApiKey); | |||
| setModalOpen(true); | |||
| }} | |||
| > | |||
| @@ -746,6 +771,7 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ | |||
| releaseTypeFilter={modalReleaseTypeFilter} | |||
| filterRequiredDeliveryDate={modalFilterRequiredDeliveryDate} | |||
| initialShopSearch={modalInitialShopSearch} | |||
| truckXFloor={modalTruckXFloor} | |||
| onClose={() => { | |||
| setModalOpen(false); | |||
| clearModalEtraContext(); | |||
| @@ -1223,13 +1223,21 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| }, [fetchAllCombinedLotData]); | |||
| const openWorkbenchLotLabelModalForLot = useCallback( | |||
| (lot: any, reminderText?: string | null) => { | |||
| ( | |||
| lot: any, | |||
| reminderText?: string | null, | |||
| options?: { markAsScanIssue?: boolean }, | |||
| ) => { | |||
| const itemId = Number(lot?.itemId); | |||
| const stockInLineId = Number(lot?.stockInLineId); | |||
| const solId = Number(lot?.stockOutLineId); | |||
| if (!Number.isFinite(itemId) || itemId <= 0 || !Number.isFinite(solId) || solId <= 0) { | |||
| return; | |||
| } | |||
| // 掃碼 issue 才標記:手動「批號二維碼」不傳 markAsScanIssue,批號不變紅 | |||
| if (options?.markAsScanIssue) { | |||
| rememberWorkbenchScanReject(solId, reminderText); | |||
| } | |||
| setWorkbenchLotLabelContextLot(lot); | |||
| if (Number.isFinite(stockInLineId) && stockInLineId > 0) { | |||
| setWorkbenchLotLabelInitialPayload({ itemId, stockInLineId }); | |||
| @@ -1241,7 +1249,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| setQrScanSuccess(false); | |||
| setWorkbenchLotLabelModalOpen(true); | |||
| }, | |||
| [], | |||
| [rememberWorkbenchScanReject], | |||
| ); | |||
| const shouldOpenWorkbenchLotLabelModalForFailure = useCallback( | |||
| @@ -1826,6 +1834,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| openWorkbenchLotLabelModalForLot( | |||
| scannedLot, | |||
| t("This lot is not available, please scan another lot."), | |||
| { markAsScanIssue: true }, | |||
| ); | |||
| return; | |||
| } | |||
| @@ -1841,6 +1850,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| openWorkbenchLotLabelModalForLot( | |||
| scannedLot, | |||
| `Lot is expired (expiry=${scannedLot.expiryDate || "-"})`, | |||
| { markAsScanIssue: true }, | |||
| ); | |||
| return; | |||
| } | |||
| @@ -1963,7 +1973,9 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) && | |||
| expectedLot | |||
| ) { | |||
| openWorkbenchLotLabelModalForLot(expectedLot, failMsg); | |||
| openWorkbenchLotLabelModalForLot(expectedLot, failMsg, { | |||
| markAsScanIssue: true, | |||
| }); | |||
| return; | |||
| } | |||
| if (workbenchMode && expectedLot.stockOutLineId != null) { | |||
| @@ -2135,7 +2147,9 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) && | |||
| expectedLot | |||
| ) { | |||
| openWorkbenchLotLabelModalForLot(expectedLot, failMsg); | |||
| openWorkbenchLotLabelModalForLot(expectedLot, failMsg, { | |||
| markAsScanIssue: true, | |||
| }); | |||
| return; | |||
| } | |||
| if (workbenchMode && expectedLot.stockOutLineId != null) { | |||
| @@ -2330,7 +2344,9 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) && | |||
| exactMatch | |||
| ) { | |||
| openWorkbenchLotLabelModalForLot(exactMatch, failMsg); | |||
| openWorkbenchLotLabelModalForLot(exactMatch, failMsg, { | |||
| markAsScanIssue: true, | |||
| }); | |||
| return; | |||
| } | |||
| if (workbenchMode && exactMatch.stockOutLineId != null) { | |||
| @@ -2474,7 +2490,9 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) && | |||
| expectedLot | |||
| ) { | |||
| openWorkbenchLotLabelModalForLot(expectedLot, failMsg); | |||
| openWorkbenchLotLabelModalForLot(expectedLot, failMsg, { | |||
| markAsScanIssue: true, | |||
| }); | |||
| return; | |||
| } | |||
| if (workbenchMode && expectedLot.stockOutLineId != null) { | |||
| @@ -4154,10 +4172,21 @@ const handleSubmitAllScanned = useCallback(async () => { | |||
| <TableCell>{t("Item Code")}</TableCell> | |||
| <TableCell>{t("Item Name")}</TableCell> | |||
| <TableCell>{t("Route")}</TableCell> | |||
| <TableCell>{t("Suggest Lot No.")}</TableCell> | |||
| <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell> | |||
| <TableCell align="center">{t("Scan Result")}</TableCell> | |||
| <TableCell | |||
| align="center" | |||
| sx={{ minWidth: 220, whiteSpace: "nowrap" }} | |||
| > | |||
| {`${t("Route")} / ${t("Suggest Lot No.")}`} | |||
| </TableCell> | |||
| <TableCell align="right" sx={{ whiteSpace: "nowrap", width: 88, px: 1 }}> | |||
| {t("Lot Required Pick Qty")} | |||
| </TableCell> | |||
| <TableCell | |||
| align="center" | |||
| sx={{ width: 88, minWidth: 88, px: 0.5, whiteSpace: "nowrap" }} | |||
| > | |||
| {t("Scan Result")} | |||
| </TableCell> | |||
| {/*<TableCell align="center">{t("Qty will submit")}</TableCell>*/} | |||
| <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell> | |||
| </TableRow> | |||
| @@ -4165,7 +4194,7 @@ const handleSubmitAllScanned = useCallback(async () => { | |||
| <TableBody> | |||
| {paginatedData.length === 0 ? ( | |||
| <TableRow> | |||
| <TableCell colSpan={11} align="center"> | |||
| <TableCell colSpan={7} align="center"> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("No data available")} | |||
| </Typography> | |||
| @@ -4226,36 +4255,49 @@ paginatedData.map((row, index) => { | |||
| {row.isGroupFirst ? lot.itemName + '(' + lot.stockUnit + ')' : ""} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| <Typography variant="body2"> | |||
| {lot.routerRoute || '-'} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| <Stack direction="row" spacing={1} alignItems="center"> | |||
| <Box sx={{ flex: 1, minWidth: 0 }}> | |||
| <TableCell align="center" sx={{ minWidth: 220 }}> | |||
| <Stack | |||
| direction="row" | |||
| spacing={1} | |||
| alignItems="center" | |||
| justifyContent="center" | |||
| sx={{ width: "100%" }} | |||
| > | |||
| <Box sx={{ textAlign: "center" }}> | |||
| <Typography variant="body2" sx={{ whiteSpace: "nowrap", textAlign: "center" }}> | |||
| {lot.routerRoute || "-"} | |||
| </Typography> | |||
| {(() => { | |||
| const hasLotNo = Boolean(lot.lotNo); | |||
| const isExpired = lot.lotAvailability === 'expired'; | |||
| const isUnavailable = isInventoryLotLineUnavailable(lot); | |||
| const isNoLotHint = !hasLotNo && !rejectDisplay; // 顯示「請檢查周圍…」那種 | |||
| const isIssueText = | |||
| Boolean(rejectDisplay) || !hasLotNo || isExpired || isUnavailable; | |||
| const textColor = | |||
| isNoLotHint | |||
| ? 'error.main' // 或 'text.primary':固定黑,不受 handled / unavailable 影響 | |||
| : rejectDisplay || isSolRejected || isUnavailable | |||
| ? 'error.main' | |||
| : isExpired | |||
| ? 'warning.main' | |||
| : 'inherit'; | |||
| const solStatus = String(lot.stockOutLineStatus ?? "").toLowerCase(); | |||
| const isComplete = | |||
| solStatus === "completed" || | |||
| solStatus === "checked" || | |||
| solStatus === "partially_completed" || | |||
| solStatus === "partially_complete"; | |||
| const textColor = | |||
| isNoLotHint | |||
| ? "error.main" | |||
| : rejectDisplay || isSolRejected || isUnavailable | |||
| ? "error.main" | |||
| : isExpired | |||
| ? "warning.main" | |||
| : isComplete | |||
| ? "success.main" | |||
| : "inherit"; | |||
| const lotOnly = | |||
| hasLotNo && !rejectDisplay && !isExpired && !isUnavailable; | |||
| return ( | |||
| <Typography | |||
| variant={isIssueText ? "body2" : "body2"} | |||
| variant="body2" | |||
| sx={{ | |||
| color: textColor, | |||
| whiteSpace: 'pre-wrap', | |||
| wordBreak: 'break-word', | |||
| whiteSpace: lotOnly ? "nowrap" : "pre-wrap", | |||
| wordBreak: lotOnly ? "normal" : "break-word", | |||
| textAlign: "center", | |||
| }} | |||
| > | |||
| {hasLotNo ? ( | |||
| @@ -4299,7 +4341,6 @@ paginatedData.map((row, index) => { | |||
| sx={{ | |||
| flexShrink: 0, | |||
| fontSize: "0.75rem", | |||
| //py: 0.25, | |||
| minWidth: "auto", | |||
| px: 1, | |||
| whiteSpace: "nowrap", | |||
| @@ -4310,23 +4351,34 @@ paginatedData.map((row, index) => { | |||
| )} | |||
| </Stack> | |||
| </TableCell> | |||
| <TableCell align="right"> | |||
| {(() => { | |||
| const requiredQty = lot.requiredQty || 0; | |||
| return requiredQty.toLocaleString() + '(' + lot.uomShortDesc + ')'; | |||
| })()} | |||
| <TableCell align="right" sx={{ whiteSpace: "nowrap", width: 88, px: 1 }}> | |||
| <Typography variant="body1"> | |||
| {(() => { | |||
| const requiredQty = lot.requiredQty || 0; | |||
| return requiredQty.toLocaleString() + '(' + lot.uomShortDesc + ')'; | |||
| })()} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| <TableCell align="center" sx={{ width: 88, minWidth: 88, px: 0.5 }}> | |||
| {(() => { | |||
| const status = lot.stockOutLineStatus?.toLowerCase(); | |||
| const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected'; | |||
| const isNoLot = !lot.lotNo; | |||
| const scanResultSlotSx = { | |||
| width: 42, | |||
| height: 42, | |||
| display: 'flex', | |||
| justifyContent: 'center', | |||
| alignItems: 'center', | |||
| mx: 'auto', | |||
| } as const; | |||
| // rejected lot:显示红色勾选(已扫描但被拒绝) | |||
| if (isRejected && !isNoLot) { | |||
| return ( | |||
| <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}> | |||
| <Box sx={scanResultSlotSx}> | |||
| <Checkbox | |||
| checked={true} | |||
| disabled={true} | |||
| @@ -4350,13 +4402,13 @@ paginatedData.map((row, index) => { | |||
| status !== "partially_completed" && | |||
| status !== "partially_complete" | |||
| ) { | |||
| return null; | |||
| return <Box sx={scanResultSlotSx} />; | |||
| } | |||
| // 正常 lot:已扫描(checked/partially_completed/completed) | |||
| if (!isNoLot && status !== 'pending' && status !== 'rejected') { | |||
| return ( | |||
| <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}> | |||
| <Box sx={scanResultSlotSx}> | |||
| <Checkbox | |||
| checked={true} | |||
| disabled={true} | |||
| @@ -4375,7 +4427,7 @@ paginatedData.map((row, index) => { | |||
| // noLot 且已完成/部分完成:显示红色勾选 | |||
| if (isNoLot && (status === 'partially_completed' || status === 'completed')) { | |||
| return ( | |||
| <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}> | |||
| <Box sx={scanResultSlotSx}> | |||
| <Checkbox | |||
| checked={true} | |||
| disabled={true} | |||
| @@ -4391,7 +4443,7 @@ paginatedData.map((row, index) => { | |||
| ); | |||
| } | |||
| return null; | |||
| return <Box sx={scanResultSlotSx} />; | |||
| })()} | |||
| </TableCell> | |||
| {/* | |||
| @@ -4477,11 +4529,10 @@ paginatedData.map((row, index) => { | |||
| <Stack direction="row" spacing={1} alignItems="center" justifyContent="center"> | |||
| {isRowPicked ? ( | |||
| <Typography | |||
| variant="body2" | |||
| variant="body1" | |||
| sx={{ | |||
| width: 96, | |||
| textAlign: "center", | |||
| fontSize: "1rem", | |||
| fontWeight: 500, | |||
| }} | |||
| > | |||
| @@ -4512,7 +4563,16 @@ paginatedData.map((row, index) => { | |||
| inputProps={{ min: 0, step: 1 }} | |||
| sx={{ | |||
| width: 96, | |||
| "& .MuiInputBase-input": { fontSize: "0.75rem", py: 0.5, textAlign: "center" }, | |||
| "& .MuiInputBase-input": { | |||
| typography: "body1", | |||
| py: 0.5, | |||
| textAlign: "center", | |||
| "&::-webkit-outer-spin-button, &::-webkit-inner-spin-button": { | |||
| WebkitAppearance: "none", | |||
| margin: 0, | |||
| }, | |||
| MozAppearance: "textfield", | |||
| }, | |||
| }} | |||
| /> | |||
| )} | |||
| @@ -4557,7 +4617,14 @@ paginatedData.map((row, index) => { | |||
| } | |||
| sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '60px' }} | |||
| sx={{ | |||
| fontSize: "0.7rem", | |||
| py: 0.5, | |||
| minHeight: "28px", | |||
| minWidth: "72px", | |||
| whiteSpace: "nowrap", | |||
| flexShrink: 0, | |||
| }} | |||
| > | |||
| {t("Just Completed")} | |||
| </Button> | |||
| @@ -5,13 +5,16 @@ import { | |||
| Box, | |||
| Card, | |||
| CardContent, | |||
| Checkbox, | |||
| Chip, | |||
| CircularProgress, | |||
| FormControl, | |||
| InputLabel, | |||
| ListItemText, | |||
| MenuItem, | |||
| Paper, | |||
| Select, | |||
| SelectChangeEvent, | |||
| Stack, | |||
| Table, | |||
| TableBody, | |||
| @@ -65,6 +68,14 @@ function showDoPickOpsButtons(row: WorkbenchTicketReleaseTable): boolean { | |||
| ); | |||
| } | |||
| const HANDLER_UNASSIGNED = "__UNASSIGNED__"; | |||
| const HANDLER_SELECT_ALL = "__ALL__"; | |||
| function handlerFilterKey(handlerName: string | null | undefined): string { | |||
| const name = handlerName?.trim() ?? ""; | |||
| return name || HANDLER_UNASSIGNED; | |||
| } | |||
| const WorkbenchTicketReleaseTableTab: React.FC = () => { | |||
| const { t } = useTranslation("ticketReleaseTable"); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| @@ -73,6 +84,7 @@ const WorkbenchTicketReleaseTableTab: React.FC = () => { | |||
| const [queryDate, setQueryDate] = useState<Dayjs>(() => dayjs()); | |||
| const [selectedFloor, setSelectedFloor] = useState<string>(""); | |||
| const [selectedStatus, setSelectedStatus] = useState<string>("released"); | |||
| const [selectedHandlers, setSelectedHandlers] = useState<string[]>([]); | |||
| const [data, setData] = useState<WorkbenchTicketReleaseTable[]>([]); | |||
| const [loading, setLoading] = useState<boolean>(true); | |||
| const [now, setNow] = useState(dayjs()); | |||
| @@ -111,6 +123,28 @@ const WorkbenchTicketReleaseTableTab: React.FC = () => { | |||
| }, []); | |||
| const dayStr = queryDate.format("YYYY-MM-DD"); | |||
| const handlerOptions = useMemo(() => { | |||
| const names = new Set<string>(); | |||
| let hasUnassigned = false; | |||
| for (const item of data) { | |||
| if (selectedFloor && item.storeId !== selectedFloor) continue; | |||
| const name = item.handlerName?.trim(); | |||
| if (name) names.add(name); | |||
| else hasUnassigned = true; | |||
| } | |||
| const sorted = Array.from(names).sort((a, b) => a.localeCompare(b, "zh-Hant")); | |||
| return hasUnassigned ? [HANDLER_UNASSIGNED, ...sorted] : sorted; | |||
| }, [data, selectedFloor]); | |||
| useEffect(() => { | |||
| setSelectedHandlers((prev) => { | |||
| if (prev.length === 0) return prev; | |||
| const next = prev.filter((name) => handlerOptions.includes(name)); | |||
| return next.length === prev.length ? prev : next; | |||
| }); | |||
| }, [handlerOptions]); | |||
| const filteredData = useMemo(() => { | |||
| return data.filter((item) => { | |||
| if (selectedFloor && item.storeId !== selectedFloor) return false; | |||
| @@ -119,9 +153,27 @@ const WorkbenchTicketReleaseTableTab: React.FC = () => { | |||
| if (itemDate !== dayStr) return false; | |||
| } | |||
| if (selectedStatus && item.ticketStatus?.toLowerCase() !== selectedStatus.toLowerCase()) return false; | |||
| if (selectedHandlers.length > 0 && !selectedHandlers.includes(handlerFilterKey(item.handlerName))) { | |||
| return false; | |||
| } | |||
| return true; | |||
| }); | |||
| }, [data, dayStr, selectedFloor, selectedStatus]); | |||
| }, [data, dayStr, selectedFloor, selectedStatus, selectedHandlers]); | |||
| const allHandlersSelected = | |||
| handlerOptions.length > 0 && selectedHandlers.length === handlerOptions.length; | |||
| const handleHandlersChange = (event: SelectChangeEvent<string[]>) => { | |||
| const value = event.target.value; | |||
| const next = typeof value === "string" ? value.split(",") : value; | |||
| if (next.includes(HANDLER_SELECT_ALL)) { | |||
| setSelectedHandlers(allHandlersSelected ? [] : handlerOptions); | |||
| setPaginationController((prev) => ({ ...prev, pageNum: 0 })); | |||
| return; | |||
| } | |||
| setSelectedHandlers(next.filter((v) => v !== HANDLER_SELECT_ALL)); | |||
| setPaginationController((prev) => ({ ...prev, pageNum: 0 })); | |||
| }; | |||
| const paginatedData = useMemo(() => { | |||
| const startIndex = paginationController.pageNum * paginationController.pageSize; | |||
| @@ -247,6 +299,39 @@ const WorkbenchTicketReleaseTableTab: React.FC = () => { | |||
| <MenuItem value="completed">{t("completed")}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| <FormControl sx={{ minWidth: 200 }} size="small"> | |||
| <InputLabel id="workbench-handler-select-label" shrink> | |||
| {t("Handler Name")} | |||
| </InputLabel> | |||
| <Select | |||
| labelId="workbench-handler-select-label" | |||
| multiple | |||
| value={selectedHandlers} | |||
| label={t("Handler Name")} | |||
| onChange={handleHandlersChange} | |||
| displayEmpty | |||
| renderValue={(selected) => { | |||
| if (selected.length === 0) return t("All Handlers"); | |||
| return selected | |||
| .map((v) => (v === HANDLER_UNASSIGNED ? t("Unassigned") : v)) | |||
| .join(", "); | |||
| }} | |||
| > | |||
| <MenuItem value={HANDLER_SELECT_ALL}> | |||
| <Checkbox | |||
| checked={allHandlersSelected} | |||
| indeterminate={selectedHandlers.length > 0 && !allHandlersSelected} | |||
| /> | |||
| <ListItemText primary={t("Select All")} /> | |||
| </MenuItem> | |||
| {handlerOptions.map((name) => ( | |||
| <MenuItem key={name} value={name}> | |||
| <Checkbox checked={selectedHandlers.includes(name)} /> | |||
| <ListItemText primary={name === HANDLER_UNASSIGNED ? t("Unassigned") : name} /> | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| </FormControl> | |||
| <Box sx={{ flexGrow: 1 }} /> | |||
| <Stack direction="row" spacing={2} sx={{ flexShrink: 0, alignSelf: "center" }}> | |||
| <Typography variant="body2" sx={{ color: "text.secondary" }} suppressHydrationWarning> | |||
| @@ -36,7 +36,9 @@ export type ReleasedDoPickListBridge = { | |||
| shopName?: string, | |||
| storeId?: string, | |||
| truck?: string, | |||
| releaseType?: string | |||
| releaseType?: string, | |||
| /** Optional `2F`/`4F` for Truck X supplier-floor split */ | |||
| floor?: string | |||
| ) => Promise<ReleasedDoPickOrderListItem[]>; | |||
| /** Optional 4th arg: workbench `requiredDeliveryDate` (YYYY-MM-DD) for default truck list; omit = calendar today. */ | |||
| loadToday: ( | |||
| @@ -44,7 +46,9 @@ export type ReleasedDoPickListBridge = { | |||
| storeId?: string, | |||
| truck?: string, | |||
| requiredDeliveryDate?: string, | |||
| releaseType?: string | |||
| releaseType?: string, | |||
| /** Optional `2F`/`4F` for Truck X supplier-floor split */ | |||
| floor?: string | |||
| ) => Promise<ReleasedDoPickOrderListItem[]>; | |||
| assignByListItemId: (userId: number, id: number) => Promise<PostPickOrderResponse>; | |||
| }; | |||
| @@ -70,6 +74,10 @@ interface Props { | |||
| * requiredDate instead of historical released (delivery date before calendar today). | |||
| */ | |||
| filterRequiredDeliveryDate?: string; | |||
| /** | |||
| * Truck X only: `2F`/`4F` (or `2/F`/`4/F`) — filter by DO supplier preferred floor; storeId stays null. | |||
| */ | |||
| truckXFloor?: string; | |||
| } | |||
| const ReleasedDoPickOrderSelectModal: React.FC<Props> = ({ | |||
| @@ -85,6 +93,7 @@ const ReleasedDoPickOrderSelectModal: React.FC<Props> = ({ | |||
| releaseTypeFilter, | |||
| initialShopSearch, | |||
| filterRequiredDeliveryDate, | |||
| truckXFloor, | |||
| }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| @@ -102,6 +111,7 @@ const ReleasedDoPickOrderSelectModal: React.FC<Props> = ({ | |||
| const loadReleased = listBridge?.loadBeforeToday ?? fetchReleasedDoPickOrdersForSelection; | |||
| const loadTodayFn = listBridge?.loadToday ?? fetchReleasedDoPickOrdersForSelectionToday; | |||
| const floorArg = truckXFloor?.trim() || undefined; | |||
| if (isDefaultTruck) { | |||
| if (defaultDateScopeProp === "today") { | |||
| data = await loadTodayFn( | |||
| @@ -109,14 +119,16 @@ const ReleasedDoPickOrderSelectModal: React.FC<Props> = ({ | |||
| undefined, | |||
| "車線-X", | |||
| defaultTruckRequiredDeliveryDate?.trim() || undefined, | |||
| releaseTypeFilter?.trim() || undefined | |||
| releaseTypeFilter?.trim() || undefined, | |||
| floorArg | |||
| ); | |||
| } else { | |||
| data = await loadReleased( | |||
| undefined, | |||
| undefined, | |||
| "車線-X", | |||
| releaseTypeFilter?.trim() || undefined | |||
| releaseTypeFilter?.trim() || undefined, | |||
| floorArg | |||
| ); | |||
| } | |||
| } else if (filterRequiredDeliveryDate?.trim() && listBridge?.loadToday) { | |||
| @@ -143,7 +155,7 @@ const ReleasedDoPickOrderSelectModal: React.FC<Props> = ({ | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| }, [open, shopSearch, storeId, truck, isDefaultTruck, defaultDateScopeProp, listBridge, defaultTruckRequiredDeliveryDate, releaseTypeFilter, filterRequiredDeliveryDate]); | |||
| }, [open, shopSearch, storeId, truck, isDefaultTruck, defaultDateScopeProp, listBridge, defaultTruckRequiredDeliveryDate, releaseTypeFilter, filterRequiredDeliveryDate, truckXFloor]); | |||
| useEffect(() => { | |||
| loadList(); | |||
| @@ -1655,6 +1655,11 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| const isRowExpired = | |||
| isWorkbenchSourceLotExpired(r) && !isRowRejected; | |||
| const isRowUnavailable = isInventoryLotLineUnavailable(r); | |||
| const isRowComplete = | |||
| rowStatus === "completed" || | |||
| rowStatus === "checked" || | |||
| rowStatus === "partially_completed" || | |||
| rowStatus === "partially_complete"; | |||
| return ( | |||
| <TableRow key={r.key}> | |||
| @@ -1678,7 +1683,9 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| ? "error.main" | |||
| : isRowExpired || isLotAvailabilityExpired(r) | |||
| ? "warning.main" | |||
| : "inherit", | |||
| : isRowComplete | |||
| ? "success.main" | |||
| : "inherit", | |||
| }} | |||
| > | |||
| {r.lotNo ? ( | |||
| @@ -1833,7 +1840,19 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| return { ...prev, [r.stockOutLineId]: n }; | |||
| }); | |||
| }} | |||
| sx={{ width: 96 }} | |||
| sx={{ | |||
| width: 96, | |||
| "& .MuiInputBase-input": { | |||
| typography: "body1", | |||
| py: 0.5, | |||
| textAlign: "center", | |||
| "&::-webkit-outer-spin-button, &::-webkit-inner-spin-button": { | |||
| WebkitAppearance: "none", | |||
| margin: 0, | |||
| }, | |||
| MozAppearance: "textfield", | |||
| }, | |||
| }} | |||
| disabled={!r.stockOutLineId || qtyEditableBySolId[r.stockOutLineId] !== true} | |||
| inputProps={{ min: 0, step: 1 }} | |||
| /> | |||
| @@ -1,6 +1,7 @@ | |||
| { | |||
| "Actions": "Actions", | |||
| "All Floors": "All Floors", | |||
| "All Handlers": "All Handlers", | |||
| "All Statuses": "All Statuses", | |||
| "Auto-refresh every 1 minute": "Auto-refresh every 1 minute", | |||
| "Auto-refresh every 10 minutes": "Auto-refresh every 10 minutes", | |||
| @@ -30,9 +31,11 @@ | |||
| "Revert assignment": "Revert assignment", | |||
| "Revert assignment hint": "Revert assignment hint", | |||
| "Rows per page": "Rows per page", | |||
| "Select All": "Select All", | |||
| "Select Date": "Select Date", | |||
| "Shop Name": "Shop Name", | |||
| "Status": "Status", | |||
| "Unassigned": "Unassigned", | |||
| "Store ID": "Store ID", | |||
| "Target Date": "Target Date", | |||
| "Ticket Information": "Ticket Information", | |||
| @@ -12,7 +12,7 @@ | |||
| "N/A": "不適用", | |||
| "Release Pick Orders": "放單", | |||
| "released": "已放單", | |||
| "is unavailable. Please check around have available QR code or not.": "不可用。請檢查周圍是否有可用的 QR 碼。", | |||
| "is unavailable. Please check around have available QR code or not.": "不可用。請檢查周圍是否有可用的二維碼。", | |||
| "No lot rows. Select a pick order above.": "沒有批次行。請選擇一個提料單。", | |||
| "Loading...": "載入中...", | |||
| "Suggestion success": "建議成功", | |||
| @@ -65,7 +65,7 @@ | |||
| "items": "項目", | |||
| "Select Pick Order:": "選擇提料單:", | |||
| "No Stock Available": "沒有庫存", | |||
| "is expired. Please check around have available QR code or not.": "已過期。請檢查周圍是否有可用的 QR 碼。", | |||
| "is expired. Please check around have available QR code or not.": "已過期。請檢查周圍是否有可用的二維碼。", | |||
| "Start Fail": "開始失敗", | |||
| "Start PO": "開始採購訂單", | |||
| "Do you want to complete?": "確定完成嗎?", | |||
| @@ -136,7 +136,7 @@ | |||
| "LotNo": "批號", | |||
| "Po Code": "採購訂單編號", | |||
| "No Warehouse": "沒有倉庫", | |||
| "Please scan warehouse qr code.": "請掃描倉庫 QR 碼。", | |||
| "Please scan warehouse qr code.": "請掃描倉庫二維碼。", | |||
| "Reject": "拒絕", | |||
| "submit": "確認提交", | |||
| @@ -270,7 +270,7 @@ | |||
| "Pick order completed successfully!": "提料單完成成功!", | |||
| "Lot has been rejected and marked as unavailable.": "批號已拒絕並標記為不可用。", | |||
| "This order is insufficient, please pick another lot.": "此訂單不足,請選擇其他批號。", | |||
| "Please finish QR code scan, QC check and pick order.": "請完成 QR 碼掃描、QC 檢查和提料。", | |||
| "Please finish QR code scan, QC check and pick order.": "請完成二維碼掃描、QC 檢查和提料。", | |||
| "No data available": "沒有資料", | |||
| "Please submit the pick order.": "請提交提料單。", | |||
| "Item lot to be Pick:": "批次貨品提料:", | |||
| @@ -293,13 +293,13 @@ | |||
| "Original Available Qty": "原可用數", | |||
| "Remaining Available Qty": "剩餘可用數", | |||
| "Please submit pick order.": "請提交提料單。", | |||
| "Please finish QR code scan and pick order.": "請完成 QR 碼掃描和提料。", | |||
| "Please finish QR code scanand pick order.": "請完成 QR 碼掃描和提料。", | |||
| "Please finish QR code scan and pick order.": "請完成二維碼掃描和提料。", | |||
| "Please finish QR code scanand pick order.": "請完成二維碼掃描和提料。", | |||
| "First created group": "首次建立分組", | |||
| "Latest created group": "最新建立分組", | |||
| "Manual Input": "手動輸入", | |||
| "QR Code Scan for Lot": " QR 碼掃描批次", | |||
| "Processing QR code...": "處理 QR 碼...", | |||
| "QR Code Scan for Lot": "二維碼掃描批次", | |||
| "Processing QR code...": "處理二維碼...", | |||
| "The input is not the same as the expected lot number.": "輸入的批次號碼與預期的不符。", | |||
| "Verified successfully!": "驗證成功!", | |||
| "Cancel": "取消", | |||
| @@ -370,7 +370,7 @@ | |||
| "Scanning...":"掃描中...", | |||
| "Print DN/Label":"列印送貨單/標籤", | |||
| "Store ID":"儲存編號", | |||
| "QR code does not match any item in current orders.":"QR 碼不符合當前訂單中的任何貨品。", | |||
| "QR code does not match any item in current orders.":"二維碼不符合當前訂單中的任何貨品。", | |||
| "Lot Number Mismatch":"批次號碼不符", | |||
| "The scanned item matches the expected item, but the lot number is different. Do you want to proceed with this different lot?":"掃描的貨品與預期的貨品相同,但批次號碼不同。您是否要繼續使用不同的批次?", | |||
| "The scanned item matches the expected item, but the lot number is different. Scan again to confirm: scan the expected lot QR to keep the suggested lot, or scan the other lot QR again to switch.":"掃描貨品相同但批次不同。請再掃描一次以確認:掃描「建議批次」的 QR 可沿用該批次;再掃描「另一批次」的 QR 則切換為該批次。", | |||
| @@ -389,7 +389,7 @@ | |||
| "Lot switch failed":"批次切換失敗", | |||
| "The system could not switch to the scanned lot. Review the lots below, then tap Confirm to retry.":"系統無法切換至掃描的批次。請核對下方批次後按「確認」重試。", | |||
| "You can also scan again: expected lot QR keeps the suggested line; scanned lot QR retries the switch.":"您也可以再掃描:掃描建議批次 QR 可保留該行;掃描欲切換批次 QR 可再次嘗試切換。", | |||
| "QR code verified.":"QR 碼驗證成功。", | |||
| "QR code verified.":"二維碼驗證成功。", | |||
| "Order Finished":"訂單完成", | |||
| "Submitted Status":"提交狀態", | |||
| "Pick Execution Record":"提料執行記錄", | |||
| @@ -507,16 +507,17 @@ | |||
| "packaging": "提料中", | |||
| "No Stock Available": "沒有庫存可用", | |||
| "This lot is not available, please scan another lot.": "此批號不可用,請掃描其他批號。", | |||
| "This lot UOM does not match the pick line. Please scan another lot.": "此批號單位與提貨行不符,請掃描其他批號。", | |||
| "Scanned lot UOM does not match pick line UOM": "此批號單位與提貨行不符,請掃描其他批號。", | |||
| "This lot UOM does not match the pick line. Please scan another lot.": "此批號單位不符,請掃描其他批號。", | |||
| "Scanned lot UOM does not match pick line UOM": "此批號單位不符,請掃描其他批號。", | |||
| "This lot has already been picked. Please scan another lot.": "此批號已提貨,請掃描其他批號。", | |||
| "Scanned lot is already completed or checked": "此物料已提貨,請掃描其他批號。", | |||
| "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.": "請檢查周圍是否有其他可用 QR 碼。", | |||
| "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.": "請檢查周圍是否有其他可用二維碼。", | |||
| "Lot is expired (expiry={{expiry}})": "掃描批號已過期(到期日={{expiry}})", | |||
| "Day After Tomorrow": "後日", | |||
| "Lot line is unavailable": "掃描批次不可用", | |||
| "Select Date": "請選擇日期", | |||
| "Suggest Lot No.": "推薦批號", | |||
| "Suggest Lot No.": "推薦批號/實際批號", | |||
| "Search by Shop": "搜索商店", | |||
| "Search by Truck": "搜索貨車", | |||
| "Print DN & Label": "列印提料單和送貨單標籤", | |||
| @@ -545,13 +546,13 @@ | |||
| "4F ticket": "4/F 票", | |||
| "4F lane panel legend": "貨車班次 — 裝載序(未撳數/總單數)", | |||
| "Loading sequence n": "板{{n}}", | |||
| "lot QR code": "批號 QR 碼", | |||
| "lot QR code": "批號二維碼", | |||
| "label Printer" : "標籤打印機", | |||
| "A4 Printer" : "A4 打印機", | |||
| "Loading Sequence": "裝載序", | |||
| "Ticket No": "提票號碼", | |||
| "The scanned lot inventory line is unavailable. Cannot switch or bind; pick line was not updated.": "掃描的庫存批行為「不可用」,無法換批或綁定;揀貨行未更新。", | |||
| "is unavable. Please check around have available QR code or not.": "此批號不可用,請檢查周圍是否有可用的 QR 碼。", | |||
| "is unavable. Please check around have available QR code or not.": "此批號不可用,請檢查周圍是否有可用的二維碼。", | |||
| "Lot switch failed; pick line was not marked as checked.": "換批失敗;揀貨行未標為已核對。", | |||
| "Lot confirmation failed. Please try again.": "確認批號失敗,請重試。", | |||
| "Powder Mixture": "箱料粉", | |||
| @@ -25,6 +25,9 @@ | |||
| "Released Time": "開始時間", | |||
| "Completed Time": "完成時間", | |||
| "Handler Name": "負責員工", | |||
| "All Handlers": "所有員工", | |||
| "Select All": "全選", | |||
| "Unassigned": "未分配", | |||
| "Number of FG Items (Order Item(s) Count)": "訂單項目數量", | |||
| "No data available": "沒有資料", | |||
| "Rows per page": "每頁行數", | |||