瀏覽代碼

update

master
CANCERYS\kw093 2 月之前
父節點
當前提交
83eaa0c399
共有 6 個文件被更改,包括 1102 次插入1123 次删除
  1. +122
    -0
      src/app/api/jo/actions.ts
  2. +3
    -3
      src/components/Jodetail/FInishedJobOrderRecord.tsx
  3. +521
    -255
      src/components/Jodetail/JobPickExecution.tsx
  4. +41
    -26
      src/components/Jodetail/JobPickExecutionForm.tsx
  5. +334
    -761
      src/components/Jodetail/JobPickExecutionsecondscan.tsx
  6. +81
    -78
      src/components/Jodetail/JodetailSearch.tsx

+ 122
- 0
src/app/api/jo/actions.ts 查看文件

@@ -76,7 +76,129 @@ export interface JobOrderDetail {
pickLines: any[];
status: string;
}
export interface UnassignedJobOrderPickOrder {
pickOrderId: number;
pickOrderCode: string;
pickOrderConsoCode: string;
pickOrderTargetDate: string;
pickOrderStatus: string;
jobOrderId: number;
jobOrderCode: string;
jobOrderName: string;
reqQty: number;
uom: string;
planStart: string;
planEnd: string;
}

export interface AssignJobOrderResponse {
id: number | null;
code: string | null;
name: string | null;
type: string | null;
message: string | null;
errorPosition: string | null;
}
export const recordSecondScanIssue = cache(async (
pickOrderId: number,
itemId: number,
data: {
qty: number;
isMissing: boolean;
isBad: boolean;
reason: string;
createdBy: number;
}
) => {
return serverFetchJson<any>(
`${BASE_API_URL}/jo/second-scan-issue/${pickOrderId}/${itemId}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
next: { tags: ["jo-second-scan"] },
},
);
});
export const updateSecondQrScanStatus = cache(async (pickOrderId: number, itemId: number) => {
return serverFetchJson<any>(
`${BASE_API_URL}/jo/second-scan-qr/${pickOrderId}/${itemId}`,
{
method: "POST",
next: { tags: ["jo-second-scan"] },
},
);
});

export const submitSecondScanQuantity = cache(async (
pickOrderId: number,
itemId: number,
data: { qty: number; isMissing?: boolean; isBad?: boolean; reason?: string }
) => {
return serverFetchJson<any>(
`${BASE_API_URL}/jo/second-scan-submit/${pickOrderId}/${itemId}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
next: { tags: ["jo-second-scan"] },
},
);
});
// 获取未分配的 Job Order pick orders
export const fetchUnassignedJobOrderPickOrders = cache(async () => {
return serverFetchJson<UnassignedJobOrderPickOrder[]>(
`${BASE_API_URL}/jo/unassigned-job-order-pick-orders`,
{
method: "GET",
next: { tags: ["jo-unassigned"] },
},
);
});

// 分配 Job Order pick order 给用户
export const assignJobOrderPickOrder = async (pickOrderId: number, userId: number) => {
return serverFetchJson<AssignJobOrderResponse>(
`${BASE_API_URL}/jo/assign-job-order-pick-order/${pickOrderId}/${userId}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
}
);
};

// 获取 Job Order 分层数据
export const fetchJobOrderLotsHierarchical = cache(async (userId: number) => {
return serverFetchJson<any>(
`${BASE_API_URL}/jo/all-lots-hierarchical/${userId}`,
{
method: "GET",
next: { tags: ["jo-hierarchical"] },
},
);
});

// 获取已完成的 Job Order pick orders
export const fetchCompletedJobOrderPickOrders = cache(async (userId: number) => {
return serverFetchJson<any>(
`${BASE_API_URL}/jo/completed-job-order-pick-orders/${userId}`,
{
method: "GET",
next: { tags: ["jo-completed"] },
},
);
});

// 获取已完成的 Job Order pick order records
export const fetchCompletedJobOrderPickOrderRecords = cache(async (userId: number) => {
return serverFetchJson<any[]>(
`${BASE_API_URL}/jo/completed-job-order-pick-order-records/${userId}`,
{
method: "GET",
next: { tags: ["jo-records"] },
},
);
});
export const fetchJobOrderDetailByCode = cache(async (code: string) => {
return serverFetchJson<JobOrderDetail>(
`${BASE_API_URL}/jo/detailByCode/${code}`,


src/components/Jodetail/GoodPickExecutionRecord.tsx → src/components/Jodetail/FInishedJobOrderRecord.tsx 查看文件

@@ -59,7 +59,7 @@ import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerP
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig";
import { fetchStockInLineInfo } from "@/app/api/po/actions";
import GoodPickExecutionForm from "./GoodPickExecutionForm";
import GoodPickExecutionForm from "./JobPickExecutionForm";
import FGPickOrderCard from "./FGPickOrderCard";

interface Props {
@@ -99,7 +99,7 @@ interface PickOrderData {
lots: any[];
}

const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => {
const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
const { t } = useTranslation("pickOrder");
const router = useRouter();
const { data: session } = useSession() as { data: SessionWithTokens | null };
@@ -437,4 +437,4 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => {
);
};

export default GoodPickExecutionRecord;
export default FInishedJobOrderRecord;

src/components/Jodetail/JobPickExecution.tsx
文件差異過大導致無法顯示
查看文件


src/components/Jodetail/GoodPickExecutionForm.tsx → src/components/Jodetail/JobPickExecutionForm.tsx 查看文件

@@ -81,7 +81,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
const [errors, setErrors] = useState<FormErrors>({});
const [loading, setLoading] = useState(false);
const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]);
const [verifiedQty, setVerifiedQty] = useState<number>(0);
// 计算剩余可用数量
const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
const remainingQty = lot.inQty - lot.outQty;
@@ -123,17 +123,15 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
}
};

// 计算剩余可用数量
const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot);
const requiredQty = calculateRequiredQty(selectedLot);
// ✅ Initialize verified quantity to the received quantity (actualPickQty)
const initialVerifiedQty = selectedLot.actualPickQty || 0;
setVerifiedQty(initialVerifiedQty);
console.log("=== PickExecutionForm Debug ===");
console.log("selectedLot:", selectedLot);
console.log("inQty:", selectedLot.inQty);
console.log("outQty:", selectedLot.outQty);
console.log("holdQty:", selectedLot.holdQty);
console.log("availableQty:", selectedLot.availableQty);
console.log("calculated remainingAvailableQty:", remainingAvailableQty);
console.log("initialVerifiedQty:", initialVerifiedQty);
console.log("=== End Debug ===");
setFormData({
pickOrderId: pickOrderId,
pickOrderCode: selectedPickOrderLine.pickOrderCode,
@@ -147,18 +145,24 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
lotNo: selectedLot.lotNo,
storeLocation: selectedLot.location,
requiredQty: selectedLot.requiredQty,
actualPickQty: selectedLot.actualPickQty || 0,
actualPickQty: initialVerifiedQty, // ✅ Use the initial value
missQty: 0,
badItemQty: 0, // 初始化为 0,用户需要手动输入
badItemQty: 0,
issueRemark: '',
pickerName: '',
handledBy: undefined,
});
}
}, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate, calculateRemainingAvailableQty]);
}, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate]);

const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
// ✅ Update verified quantity state when actualPickQty changes
if (field === 'actualPickQty') {
setVerifiedQty(value);
}
// 清除错误
if (errors[field as keyof FormErrors]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
@@ -169,21 +173,21 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
const validateForm = (): boolean => {
const newErrors: FormErrors = {};
if (formData.actualPickQty === undefined || formData.actualPickQty < 0) {
if (verifiedQty === undefined || verifiedQty < 0) {
newErrors.actualPickQty = t('Qty is required');
}
// ✅ FIXED: Check if actual pick qty exceeds remaining available qty
if (formData.actualPickQty && formData.actualPickQty > remainingAvailableQty) {
newErrors.actualPickQty = t('Qty is not allowed to be greater than remaining available qty');
// ✅ Check if verified qty exceeds received qty
if (verifiedQty > (selectedLot?.actualPickQty || 0)) {
newErrors.actualPickQty = t('Verified quantity cannot exceed received quantity');
}
// ✅ FIXED: Check if actual pick qty exceeds required qty (use original required qty)
if (formData.actualPickQty && formData.actualPickQty > (selectedLot?.requiredQty || 0)) {
// ✅ Check if verified qty exceeds required qty
if (verifiedQty > (selectedLot?.requiredQty || 0)) {
newErrors.actualPickQty = t('Qty is not allowed to be greater than required qty');
}
// ✅ NEW: Require either missQty > 0 OR badItemQty > 0 (at least one issue must be reported)
// ✅ Require either missQty > 0 OR badItemQty > 0
const hasMissQty = formData.missQty && formData.missQty > 0;
const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0;
@@ -203,7 +207,13 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({

setLoading(true);
try {
await onSubmit(formData as PickExecutionIssueData);
// ✅ Use the verified quantity in the submission
const submissionData = {
...formData,
actualPickQty: verifiedQty
} as PickExecutionIssueData;
await onSubmit(submissionData);
onClose();
} catch (error) {
console.error('Error submitting pick execution issue:', error);
@@ -215,6 +225,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
const handleClose = () => {
setFormData({});
setErrors({});
setVerifiedQty(0);
onClose();
};

@@ -257,8 +268,8 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
<Grid item xs={6}>
<TextField
fullWidth
label={t('Remaining Available Qty')}
value={remainingAvailableQty}
label={t('Received Qty')}
value={formData.actualPickQty || 0}
disabled
variant="outlined"
// helperText={t('Available in warehouse')}
@@ -268,12 +279,16 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
<Grid item xs={12}>
<TextField
fullWidth
label={t('Actual Pick Qty')}
label={t('Verified Qty')}
type="number"
value={formData.actualPickQty || 0}
onChange={(e) => handleInputChange('actualPickQty', parseFloat(e.target.value) || 0)}
value={verifiedQty} // ✅ Use the separate state
onChange={(e) => {
const newValue = parseFloat(e.target.value) || 0;
setVerifiedQty(newValue);
handleInputChange('actualPickQty', newValue);
}}
error={!!errors.actualPickQty}
helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(remainingAvailableQty, selectedLot?.requiredQty || 0)}`}
helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(selectedLot?.actualPickQty || 0, selectedLot?.requiredQty || 0)}`}
variant="outlined"
/>
</Grid>

src/components/Jodetail/JobPickExecutionsecondscan.tsx
文件差異過大導致無法顯示
查看文件


+ 81
- 78
src/components/Jodetail/JodetailSearch.tsx 查看文件

@@ -17,13 +17,21 @@ import {
} from "@/app/utils/formatUtil";
import { Button, Grid, Stack, Tab, Tabs, TabsProps, Typography, Box } from "@mui/material";
import Jodetail from "./Jodetail"
import PickExecution from "./GoodPickExecution";
import PickExecution from "./JobPickExecution";
import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions";
import { fetchPickOrderClient, autoAssignAndReleasePickOrder, autoAssignAndReleasePickOrderByStore } from "@/app/api/pickOrder/actions";
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig";
import PickExecutionDetail from "./GoodPickExecutiondetail";
import GoodPickExecutionRecord from "./GoodPickExecutionRecord";
import JobPickExecutionsecondscan from "./JobPickExecutionsecondscan";
import FInishedJobOrderRecord from "./FInishedJobOrderRecord";
import JobPickExecution from "./JobPickExecution";
import {
fetchUnassignedJobOrderPickOrders,
assignJobOrderPickOrder,
fetchJobOrderLotsHierarchical,
fetchCompletedJobOrderPickOrders,
fetchCompletedJobOrderPickOrderRecords
} from "@/app/api/jo/actions";
interface Props {
pickOrders: PickOrderResult[];
}
@@ -48,6 +56,8 @@ const JodetailSearch: React.FC<Props> = ({ pickOrders }) => {
const [tabIndex, setTabIndex] = useState(0);
const [totalCount, setTotalCount] = useState<number>();
const [isAssigning, setIsAssigning] = useState(false);
const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]);
const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
const [hideCompletedUntilNext, setHideCompletedUntilNext] = useState<boolean>(
typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true'
);
@@ -125,7 +135,47 @@ const JodetailSearch: React.FC<Props> = ({ pickOrders }) => {
}
};
// ✅ Manual assignment handler - uses the action function

const loadUnassignedOrders = useCallback(async () => {
setIsLoadingUnassigned(true);
try {
const orders = await fetchUnassignedJobOrderPickOrders();
setUnassignedOrders(orders);
} catch (error) {
console.error("Error loading unassigned orders:", error);
} finally {
setIsLoadingUnassigned(false);
}
}, []);
// 分配订单给当前用户
const handleAssignOrder = useCallback(async (pickOrderId: number) => {
if (!currentUserId) {
console.error("Missing user id in session");
return;
}
try {
const result = await assignJobOrderPickOrder(pickOrderId, currentUserId);
if (result.message === "Successfully assigned") {
console.log("✅ Successfully assigned pick order");
// 刷新数据
window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
// 重新加载未分配订单列表
loadUnassignedOrders();
} else {
console.warn("⚠️ Assignment failed:", result.message);
alert(`Assignment failed: ${result.message}`);
}
} catch (error) {
console.error("❌ Error assigning order:", error);
alert("Error occurred during assignment");
}
}, [currentUserId, loadUnassignedOrders]);
// 在组件加载时获取未分配订单
useEffect(() => {
loadUnassignedOrders();
}, [loadUnassignedOrders]);

const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
@@ -333,80 +383,33 @@ const JodetailSearch: React.FC<Props> = ({ pickOrders }) => {
<Stack rowGap={2}>
<Grid container alignItems="center">
<Grid item xs={8}>
<Box mb={2}>
<Typography variant="h4" marginInlineEnd={2}>
{t("Finished Good Order")}
</Typography>
</Box>

</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>
{/* Unassigned Job Orders */}
{unassignedOrders.length > 0 && (
<Box sx={{ mt: 2, p: 2, border: '1px solid #e0e0e0', borderRadius: 1 }}>
<Typography variant="h6" gutterBottom>
{t("Unassigned Job Orders")} ({unassignedOrders.length})
</Typography>
<Stack direction="row" spacing={1} flexWrap="wrap">
{unassignedOrders.map((order) => (
<Button
key={order.pickOrderId}
variant="outlined"
size="small"
onClick={() => handleAssignOrder(order.pickOrderId)}
disabled={isLoadingUnassigned}
>
{order.pickOrderCode} - {order.jobOrderName}
</Button>
))}
</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={hideCompletedUntilNext ? "contained" : "outlined"}
color={hideCompletedUntilNext ? "warning" : "inherit"}
onClick={() => {
const next = !hideCompletedUntilNext;
setHideCompletedUntilNext(next);
if (next) localStorage.setItem('hideCompletedUntilNext', 'true');
else localStorage.removeItem('hideCompletedUntilNext');
window.dispatchEvent(new Event('pickOrderAssigned')); // ask detail to re-fetch
}}
>
{hideCompletedUntilNext ? t("Hide Completed: ON") : t("Hide Completed: OFF")}
</Button>
*/}
<Button
variant="contained"
disabled={!printButtonsEnabled}
title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""}
>
{t("Print Draft")}
</Button>
<Button
variant="contained"
disabled={!printButtonsEnabled}
title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""}
>
{t("Print Pick Order and DN Label")}
</Button>
<Button
variant="contained"
disabled={!printButtonsEnabled}
title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""}
>
{t("Print Pick Order")}
</Button>
<Button
variant="contained"
disabled={!printButtonsEnabled}
title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""}
>
{t("Print DN Label")}
</Button>
</Stack>
</Grid>


</Grid>
@@ -419,8 +422,8 @@ const JodetailSearch: React.FC<Props> = ({ pickOrders }) => {
}}>
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
<Tab label={t("Pick Order Detail")} iconPosition="end" />
<Tab label={t("Finished Good Detail")} iconPosition="end" />
<Tab label={t("Finished Good Record")} iconPosition="end" />
<Tab label={t("Job order match")} iconPosition="end" />
<Tab label={t("Finished Job Order Record")} iconPosition="end" />
</Tabs>
</Box>
@@ -429,9 +432,9 @@ const JodetailSearch: React.FC<Props> = ({ pickOrders }) => {
<Box sx={{
p: 2
}}>
{tabIndex === 0 && <PickExecution filterArgs={filterArgs} />}
{tabIndex === 1 && <PickExecutionDetail filterArgs={filterArgs} />}
{tabIndex === 2 && <GoodPickExecutionRecord filterArgs={filterArgs} />}
{tabIndex === 0 && <JobPickExecution filterArgs={filterArgs} />}
{tabIndex === 1 && <JobPickExecutionsecondscan filterArgs={filterArgs} />}
{tabIndex === 2 && <FInishedJobOrderRecord filterArgs={filterArgs} />}
</Box>
</Box>
);


Loading…
取消
儲存