瀏覽代碼

update

master
CANCERYS\kw093 2 月之前
父節點
當前提交
15a9d175e1
共有 6 個文件被更改,包括 333 次插入125 次删除
  1. +49
    -0
      src/app/api/pickOrder/actions.ts
  2. +269
    -118
      src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
  3. +2
    -2
      src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx
  4. +10
    -3
      src/components/Jodetail/JobPickExecutionForm.tsx
  5. +2
    -2
      src/i18n/zh/inventory.json
  6. +1
    -0
      src/i18n/zh/jo.json

+ 49
- 0
src/app/api/pickOrder/actions.ts 查看文件

@@ -370,6 +370,21 @@ export interface UpdatePickExecutionIssueRequest {
handleRemark?: string;
}

export interface StoreLaneSummary {
storeId: string;
rows: LaneRow[];
}

export interface LaneRow {
truckDepartureTime: string;
lanes: LaneBtn[];
}

export interface LaneBtn {
truckLanceCode: string;
unassigned: number;
total: number;
}
export const updatePickExecutionIssueStatus = async (
data: UpdatePickExecutionIssueRequest
): Promise<PostPickOrderResponse> => {
@@ -384,6 +399,40 @@ export const updatePickExecutionIssueStatus = async (
revalidateTag("pickExecutionIssues");
return result;
};
export async function fetchStoreLaneSummary(storeId: string): Promise<StoreLaneSummary> {
const response = await serverFetchJson<StoreLaneSummary>(
`${BASE_API_URL}/doPickOrder/summary-by-store?storeId=${encodeURIComponent(storeId)}`,
{
method: "GET",
}
);
return response;
}

// 按车道分配订单
export async function assignByLane(
userId: number,
storeId: string,
truckLanceCode: string,
truckDepartureTime?: string
): Promise<any> {
const response = await serverFetchJson(
`${BASE_API_URL}/doPickOrder/assign-by-lane`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId,
storeId,
truckLanceCode,
truckDepartureTime,
}),
}
);
return response;
}
// ✅ 新增:获取已完成的 DO Pick Orders API
export const fetchCompletedDoPickOrders = async (
userId: number,


+ 269
- 118
src/components/FinishedGoodSearch/FinishedGoodSearch.tsx 查看文件

@@ -32,7 +32,7 @@ import PickExecutionDetail from "./GoodPickExecutiondetail";
import GoodPickExecutionRecord from "./GoodPickExecutionRecord";
import Swal from "sweetalert2";
import { printDN, printDNLabels } from "@/app/api/do/actions";
import { FGPickOrderResponse } from "@/app/api/pickOrder/actions";
import { FGPickOrderResponse, fetchStoreLaneSummary, assignByLane,type StoreLaneSummary } from "@/app/api/pickOrder/actions";
import FGPickOrderCard from "./FGPickOrderCard";

interface Props {
@@ -59,6 +59,9 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
const [tabIndex, setTabIndex] = useState(0);
const [totalCount, setTotalCount] = useState<number>();
const [isAssigning, setIsAssigning] = useState(false);
const [summary2F, setSummary2F] = useState<StoreLaneSummary | null>(null);
const [summary4F, setSummary4F] = useState<StoreLaneSummary | null>(null);
const [isLoadingSummary, setIsLoadingSummary] = useState(false);
const [hideCompletedUntilNext, setHideCompletedUntilNext] = useState<boolean>(
typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true'
);
@@ -76,7 +79,27 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
setReleasedOrderCount(0);
}
}, []);
const loadSummaries = useCallback(async () => {
setIsLoadingSummary(true);
try {
const [s2, s4] = await Promise.all([
fetchStoreLaneSummary("2/F"),
fetchStoreLaneSummary("4/F")
]);
setSummary2F(s2);
setSummary4F(s4);
} catch (error) {
console.error("Error loading summaries:", error);
} finally {
setIsLoadingSummary(false);
}
}, []);
useEffect(() => {
loadSummaries();
// 每30秒刷新一次

}, [loadSummaries]);
const handleDraft = useCallback(async () =>{
try{
if (fgPickOrdersData.length === 0) {
@@ -419,10 +442,11 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
const onAssigned = () => {
localStorage.removeItem('hideCompletedUntilNext');
setHideCompletedUntilNext(false);
loadSummaries();
};
window.addEventListener('pickOrderAssigned', onAssigned);
return () => window.removeEventListener('pickOrderAssigned', onAssigned);
}, []);
}, [loadSummaries]);
// ... existing code ...

useEffect(() => {
@@ -453,62 +477,57 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
}, [tabIndex]);

// ... existing code ...
const handleAssignByStore = async (storeId: "2/F" | "4/F") => {
if (!currentUserId) {
console.error("Missing user id in session");
return;
}
const handleAssignByLane = useCallback(async (
storeId: string,
truckDepartureTime: string,
truckLanceCode: string
) => {
if (!currentUserId) {
console.error("Missing user id in session");
return;
}
setIsAssigning(true);
try {
const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime);
setIsAssigning(true);
try {
const res = await autoAssignAndReleasePickOrderByStore(currentUserId, storeId);
console.log("Assign by store result:", res);
// ✅ Handle different response codes
if (res.code === "SUCCESS") {
console.log("✅ Successfully assigned pick order to store", storeId);
// ✅ Trigger refresh to show newly assigned data
window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
} else if (res.code === "USER_BUSY") {
console.warn("⚠️ User already has pick orders in progress:", res.message);
Swal.fire({
icon: "warning",
title: t("Warning"),
text: t("You already have a pick order in progess. Please complete it first before taking next pick order."),
confirmButtonText: t("Confirm"),
confirmButtonColor: "#8dba00"
});
// ✅ Show warning but still refresh to show existing orders
//alert(`Warning: ${res.message}`);
window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
} else if (res.code === "NO_ORDERS") {
console.log("ℹ️ No available pick orders for store", storeId);
Swal.fire({
icon: "info",
title: t("Info"),
text: t("No available pick order(s) for this floor."),
confirmButtonText: t("Confirm"),
confirmButtonColor: "#8dba00"
});
//alert(`Info: ${res.message}`);
} else {
console.log("ℹ️ Assignment result:", res.message);
alert(`Info: ${res.message}`);
}
} catch (error) {
console.error("❌ Error assigning by store:", error);
if (res.code === "SUCCESS") {
console.log("✅ Successfully assigned pick order from lane", truckLanceCode);
window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
loadSummaries(); // 刷新按钮状态
} else if (res.code === "USER_BUSY") {
Swal.fire({
icon: "error",
title: t("Error"),
text: t("Error occurred during assignment."),
confirmButtonText: t("Confirm"),
confirmButtonColor: "#8dba00"
});
//alert("Error occurred during assignment");
} finally {
setIsAssigning(false);
icon: "warning",
title: t("Warning"),
text: t("You already have a pick order in progess. Please complete it first before taking next pick order."),
confirmButtonText: t("Confirm"),
confirmButtonColor: "#8dba00"
});
window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
} else if (res.code === "NO_ORDERS") {
Swal.fire({
icon: "info",
title: t("Info"),
text: t("No available pick order(s) for this lane."),
confirmButtonText: t("Confirm"),
confirmButtonColor: "#8dba00"
});
} else {
console.log("ℹ️ Assignment result:", res.message);
}
};
} catch (error) {
console.error("❌ Error assigning by lane:", error);
Swal.fire({
icon: "error",
title: t("Error"),
text: t("Error occurred during assignment."),
confirmButtonText: t("Confirm"),
confirmButtonColor: "#8dba00"
});
} finally {
setIsAssigning(false);
}
}, [currentUserId, t, loadSummaries]);
// ✅ Manual assignment handler - uses the action function

const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
@@ -711,6 +730,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
height: '100vh', // Full viewport height
overflow: 'auto' // Single scrollbar for the whole page
}}>
{/* Header section */}
<Box sx={{ p: 2, borderBottom: '1px solid #e0e0e0' }}>
<Stack rowGap={2}>
@@ -724,74 +744,205 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
</Grid>

{/* Last 2 buttons aligned right */}
<Grid item xs={6} >
<Stack direction="row" spacing={1}>
<Button
variant="contained"
onClick={() => handleAssignByStore("2/F")}
disabled={isAssigning}
>
{isAssigning ? t("Assigning pick order...") : t("Pick Execution 2/F")}
</Button>
<Button
variant="contained"
onClick={() => handleAssignByStore("4/F")}
disabled={isAssigning}
>
{isAssigning ? t("Assigning pick order...") : t("Pick Execution 4/F")}
</Button>
</Stack>
</Grid>
<Grid item xs={12}>
<Grid container alignItems="flex-start" spacing={1}>
{/* 2/F 楼层面板 */}
<Grid item>
<Box
sx={{
border: '1px solid #e0e0e0',
borderRadius: 1,
p: 1,
minWidth: 320,
mr: 1,
backgroundColor: '#fafafa'
}}
>
<Typography variant="subtitle2" sx={{ mb: 0.5, fontWeight: 600, textAlign: 'center' }}>
2/F
</Typography>
{isLoadingSummary ? (
<Typography variant="caption">Loading...</Typography>
) : (
<Stack spacing={0.5}>
{summary2F?.rows.map((row, rowIdx) => (
<Box
key={rowIdx}
sx={{
border: '1px solid #e0e0e0',
borderRadius: 0.5,
p: 0.5,
backgroundColor: '#fff'
}}
>
<Typography
variant="caption"
sx={{
display: 'block',
mb: 0.5,
fontWeight: 500,
textAlign: 'center',
fontSize: '0.7rem'
}}
>
{row.truckDepartureTime}
</Typography>
<Stack direction="row" spacing={0.25} alignItems="center" justifyContent="center">
{row.lanes.map((lane, laneIdx) => (
<Button
key={laneIdx}
variant="outlined"
size="small"
disabled={lane.unassigned === 0 || isAssigning}
onClick={() => handleAssignByLane("2/F", row.truckDepartureTime, lane.truckLanceCode)}
sx={{
minWidth: 80,
fontSize: '0.7rem',
py: 0.25,
px: 0.5,
borderWidth: 1,
borderColor: '#ccc',
'&:hover': {
borderColor: '#999',
backgroundColor: '#f5f5f5'
}
}}
>
{`${lane.truckLanceCode} (${lane.unassigned}/${lane.total})`}
</Button>
))}
</Stack>
</Box>
))}
</Stack>
)}
</Box>
</Grid>

{/* 4/F 楼层面板 */}
<Grid item>
<Box
sx={{
border: '1px solid #e0e0e0',
borderRadius: 1,
p: 1,
minWidth: 320,
backgroundColor: '#fafafa'
}}
>
<Typography variant="subtitle2" sx={{ mb: 0.5, fontWeight: 600, textAlign: 'center' }}>
4/F
</Typography>
{isLoadingSummary ? (
<Typography variant="caption">Loading...</Typography>
) : (
<Stack spacing={0.5}>
{summary4F?.rows.map((row, rowIdx) => (
<Box
key={rowIdx}
sx={{
border: '1px solid #e0e0e0',
borderRadius: 0.5,
p: 0.5,
backgroundColor: '#fff'
}}
>
<Typography
variant="caption"
sx={{
display: 'block',
mb: 0.5,
fontWeight: 500,
textAlign: 'center',
fontSize: '0.7rem'
}}
>
{row.truckDepartureTime}
</Typography>
<Stack direction="row" spacing={0.25} alignItems="center" justifyContent="center">
{row.lanes.map((lane, laneIdx) => (
<Button
key={laneIdx}
variant="outlined"
size="small"
disabled={lane.unassigned === 0 || isAssigning}
onClick={() => handleAssignByLane("4/F", row.truckDepartureTime, lane.truckLanceCode)}
sx={{
minWidth: 80,
fontSize: '0.7rem',
py: 0.25,
px: 0.5,
borderWidth: 1,
borderColor: '#ccc',
'&:hover': {
borderColor: '#999',
backgroundColor: '#f5f5f5'
}
}}
>
{`${lane.truckLanceCode} (${lane.unassigned}/${lane.total})`}
</Button>
))}
</Stack>
</Box>
))}
</Stack>
)}
</Box>
</Grid>
{/* ✅ Updated print buttons with completion status */}
<Grid item xs={6} display="flex" justifyContent="flex-end">
<Stack direction="row" spacing={1}>
<Button
variant="contained"
onClick={handleAllDraft}
>
{t("Print All Draft")} ({releasedOrderCount})
</Button>

<Button
variant="contained"
// disabled={!printButtonsEnabled}
title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""}
onClick={handleDraft}
>
{t("Print Draft")}
</Button>
<Button
variant="contained"
disabled={!printButtonsEnabled}
title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""}
onClick={handleDNandLabel}
>
{t("Print Pick Order and DN Label")}
</Button>
<Button
variant="contained"
disabled={!printButtonsEnabled}
title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""}
onClick={handleDN}
>
{t("Print Pick Order")}
</Button>
<Button
variant="contained"
disabled={!printButtonsEnabled}
title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""}
onClick={handleLabel}
>
{t("Print DN Label")}
</Button>
</Stack>
</Grid>


<Grid item xs>
<Box sx={{ width: '100%', display: 'flex', justifyContent: 'flex-end' }}>
<Stack direction="row" spacing={1}>
<Button variant="contained" onClick={handleAllDraft}>
{t("Print All Draft")} ({releasedOrderCount})
</Button>
<Button
variant="contained"
// disabled={!printButtonsEnabled}
title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""}
onClick={handleDraft}
>
{t("Print Draft")}
</Button>
<Button
variant="contained"
disabled={!printButtonsEnabled}
title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""}
onClick={handleDNandLabel}
>
{t("Print Pick Order and DN Label")}
</Button>
<Button
variant="contained"
disabled={!printButtonsEnabled}
title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""}
onClick={handleDN}
>
{t("Print Pick Order")}
</Button>
<Button
variant="contained"
disabled={!printButtonsEnabled}
title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""}
onClick={handleLabel}
>
{t("Print DN Label")}
</Button>
</Stack>
</Box>
</Grid>
</Grid>
</Grid>
</Grid>
</Stack>
</Box>






{/* Tabs section - ✅ Move the click handler here */}
<Box sx={{
borderBottom: '1px solid #e0e0e0'


+ 2
- 2
src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx 查看文件

@@ -86,8 +86,8 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]);
// 计算剩余可用数量
const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
const remainingQty = lot.inQty - lot.outQty;
return Math.max(0, remainingQty);
// ✅ 直接使用 availableQty,因为 API 没有返回 inQty 和 outQty
return lot.availableQty || 0;
}, []);
const calculateRequiredQty = useCallback((lot: LotPickData) => {
// ✅ Use the original required quantity, not subtracting actualPickQty


+ 10
- 3
src/components/Jodetail/JobPickExecutionForm.tsx 查看文件

@@ -88,8 +88,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
const [verifiedQty, setVerifiedQty] = useState<number>(0);
const { data: session } = useSession() as { data: SessionWithTokens | null };
const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
const remainingQty = lot.inQty - lot.outQty;
return Math.max(0, remainingQty);
return lot.availableQty || 0;
}, []);
const calculateRequiredQty = useCallback((lot: LotPickData) => {
// ✅ Use the original required quantity, not subtracting actualPickQty
@@ -275,7 +274,15 @@ useEffect(() => {
// helperText={t('Still need to pick')}
/>
</Grid>
<Grid item xs={6}>
<TextField
fullWidth
label={t('Remaining Available Qty')}
value={remainingAvailableQty}
disabled
variant="outlined"
/>
</Grid>


<Grid item xs={12}>


+ 2
- 2
src/i18n/zh/inventory.json 查看文件

@@ -55,9 +55,9 @@
"Material": "物料",
"Miss Qty": "缺貨數量",
"No issues found": "未找到問題",
"Pick Order": "貨單",
"Pick Order": "貨單",
"Pick Order, Issue No, Item, Lot...": "揀貨單, 問題編號, 貨品, 批號...",
"Picker": "貨員",
"Picker": "貨員",
"Refresh": "刷新",
"Required Qty": "所需數量",
"Resolved": "已解決",


+ 1
- 0
src/i18n/zh/jo.json 查看文件

@@ -200,6 +200,7 @@
"Verified successfully!": "驗證成功!",
"At least one issue must be reported": "至少有一個問題必須報告",
"Available in warehouse": "在倉庫中可用",
"Remaining Available Qty": "剩餘可用數量",
"Describe the issue with bad items": "描述不良物料的問題",
"Enter bad item quantity (required if no missing items)": "請輸入不良物料數量(如果沒有缺失物料)",
"Enter missing quantity (required if no bad items)": "請輸入缺失物料數量(如果沒有不良物料)",


Loading…
取消
儲存