Переглянути джерело

WorkbenchGoodPickExecutionDetail UI 大改

WorkbenchTicketReleaseTable 加 user filter

DO release / FloorLanePanel:Truck X 票 + 搜尋依樓層
production
CANCERYS\kw093 8 години тому
джерело
коміт
475857092d
9 змінених файлів з 304 додано та 82 видалено
  1. +8
    -2
      src/app/api/doworkbench/actions.ts
  2. +34
    -8
      src/components/DoWorkbench/WorkbenchFloorLanePanel.tsx
  3. +115
    -48
      src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx
  4. +86
    -1
      src/components/DoWorkbench/WorkbenchTicketReleaseTable.tsx
  5. +17
    -5
      src/components/FinishedGoodSearch/ReleasedDoPickOrderSelectModal.tsx
  6. +21
    -2
      src/components/PickOrderSearch/WorkbenchPickExecution.tsx
  7. +3
    -0
      src/i18n/en/ticketReleaseTable.json
  8. +17
    -16
      src/i18n/zh/pickOrder.json
  9. +3
    -0
      src/i18n/zh/ticketReleaseTable.json

+ 8
- 2
src/app/api/doworkbench/actions.ts Переглянути файл

@@ -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" });


+ 34
- 8
src/components/DoWorkbench/WorkbenchFloorLanePanel.tsx Переглянути файл

@@ -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();


+ 115
- 48
src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx Переглянути файл

@@ -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>


+ 86
- 1
src/components/DoWorkbench/WorkbenchTicketReleaseTable.tsx Переглянути файл

@@ -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>


+ 17
- 5
src/components/FinishedGoodSearch/ReleasedDoPickOrderSelectModal.tsx Переглянути файл

@@ -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();


+ 21
- 2
src/components/PickOrderSearch/WorkbenchPickExecution.tsx Переглянути файл

@@ -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 }}
/>


+ 3
- 0
src/i18n/en/ticketReleaseTable.json Переглянути файл

@@ -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",


+ 17
- 16
src/i18n/zh/pickOrder.json Переглянути файл

@@ -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": "箱料粉",


+ 3
- 0
src/i18n/zh/ticketReleaseTable.json Переглянути файл

@@ -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": "每頁行數",


Завантаження…
Відмінити
Зберегти