Browse Source

Merge branch 'master' of http://svn.2fi-solutions.com:8300/derek/FPSMS-frontend

# Conflicts:
#	src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
master
CANCERYS\kw093 1 month ago
parent
commit
f633b05e57
13 changed files with 662 additions and 308 deletions
  1. +1
    -0
      src/app/api/pickOrder/actions.ts
  2. +91
    -0
      src/components/FinishedGoodSearch/FGPickOrderTicketReleaseTable.tsx
  3. +3
    -289
      src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
  4. +235
    -2
      src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx
  5. +307
    -0
      src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
  6. +4
    -2
      src/components/JoSearch/JoSearch.tsx
  7. +6
    -3
      src/components/JoSearch/JoSearchWrapper.tsx
  8. +4
    -4
      src/components/Logo/Logo.tsx
  9. +1
    -1
      src/components/MailField/MailField.css
  10. +1
    -1
      src/components/StockIn/FgStockInForm.tsx
  11. +1
    -1
      src/components/StockIn/StockInForm.tsx
  12. +6
    -4
      src/i18n/zh/pickOrder.json
  13. +2
    -1
      src/i18n/zh/purchaseOrder.json

+ 1
- 0
src/app/api/pickOrder/actions.ts View File

@@ -113,6 +113,7 @@ export interface GetPickOrderLineInfo {
requiredQty: number;
uomShortDesc: string;
uomDesc: string;

suggestedList: any[];
pickedQty: number;
}


+ 91
- 0
src/components/FinishedGoodSearch/FGPickOrderTicketReleaseTable.tsx View File

@@ -0,0 +1,91 @@
"use client";

import React, { useState } from 'react';
import {
Box,
Typography,
FormControl,
InputLabel,
Select,
MenuItem,
Card,
CardContent,
Stack,
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';

const FGPickOrderTicketReleaseTable: React.FC = () => {
const { t } = useTranslation("pickOrder");
const [selectedDate, setSelectedDate] = useState<string>("today");
const [selectedFloor, setSelectedFloor] = useState<string>("");

const getDateLabel = (offset: number) => {
return dayjs().add(offset, 'day').format('YYYY-MM-DD');
};

return (
<Card sx={{ mb: 2 }}>
<CardContent>
{/* Title */}
<Typography variant="h5" sx={{ mb: 3, fontWeight: 600 }}>
Ticket Release Table
</Typography>

{/* Dropdown Menus */}
<Stack direction="row" spacing={2} sx={{ mb: 3 }}>
{/* Date Selection Dropdown */}
<FormControl sx={{ minWidth: 250 }} size="small">
<InputLabel id="date-select-label">{t("Select Date")}</InputLabel>
<Select
labelId="date-select-label"
id="date-select"
value={selectedDate}
label={t("Select Date")}
onChange={(e) => setSelectedDate(e.target.value)}
>
<MenuItem value="today">
{t("Today")} ({getDateLabel(0)})
</MenuItem>
<MenuItem value="tomorrow">
{t("Tomorrow")} ({getDateLabel(1)})
</MenuItem>
<MenuItem value="dayAfterTomorrow">
{t("Day After Tomorrow")} ({getDateLabel(2)})
</MenuItem>
</Select>
</FormControl>

{/* Floor Selection Dropdown */}
<FormControl sx={{ minWidth: 150 }} size="small">
<InputLabel id="floor-select-label">{t("Floor")}</InputLabel>
<Select
labelId="floor-select-label"
id="floor-select"
value={selectedFloor}
label={t("Floor")}
onChange={(e) => setSelectedFloor(e.target.value)}
displayEmpty
>
<MenuItem value="">
<em>{t("All Floors")}</em>
</MenuItem>
<MenuItem value="2F">2/F</MenuItem>
<MenuItem value="4F">4/F</MenuItem>
</Select>
</FormControl>
</Stack>

{/* Table content will go here */}
<Box sx={{ mt: 2 }}>
<Typography variant="body2" color="text.secondary">
{/* Add your table component here */}
Table content goes here...
</Typography>
</Box>
</CardContent>
</Card>
);
};

export default FGPickOrderTicketReleaseTable;

+ 3
- 289
src/components/FinishedGoodSearch/FinishedGoodSearch.tsx View File

@@ -38,6 +38,7 @@ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import dayjs, { Dayjs } from 'dayjs';
import FGPickOrderTicketReleaseTable from "./FGPickOrderTicketReleaseTable";

interface Props {
pickOrders: PickOrderResult[];
@@ -217,228 +218,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {

},[t]);

const handleDN = useCallback(async () =>{
const askNumofCarton = await Swal.fire({
title: t("Enter the number of cartons: "),
icon: "info",
input: "number",
inputPlaceholder: t("Number of cartons"),
inputAttributes:{
min: "1",
step: "1"
},
inputValidator: (value) => {
if(!value){
return t("You need to enter a number")
}
if(parseInt(value) < 1){
return t("Number must be at least 1");
}
return null
},
showCancelButton: true,
confirmButtonText: t("Confirm"),
cancelButtonText: t("Cancel"),
confirmButtonColor: "#8dba00",
cancelButtonColor: "#F04438",
showLoaderOnConfirm: true,
allowOutsideClick: () => !Swal.isLoading()
});

if (askNumofCarton.isConfirmed) {
const numOfCartons = askNumofCarton.value;
try{
if (fgPickOrdersData.length === 0) {
console.error("No FG Pick order data available");
return;
}

const currentFgOrder = fgPickOrdersData[0];

const printRequest = {
printerId: 1,
printQty: 1,
isDraft: false,
numOfCarton: numOfCartons,
deliveryOrderId: currentFgOrder.deliveryOrderId,
pickOrderId: currentFgOrder.pickOrderId
};

console.log("Printing Delivery Note with request: ", printRequest);

const response = await printDN(printRequest);

console.log("Print Delivery Note response: ", response);

if(response.success){
Swal.fire({
position: "bottom-end",
icon: "success",
text: t("Printed Successfully."),
showConfirmButton: false,
timer: 1500
});
} else {
console.error("Print failed: ", response.message);
}
} catch(error){
console.error("error: ", error)
}
}
},[t, fgPickOrdersData]);

const handleDNandLabel = useCallback(async () =>{
const askNumofCarton = await Swal.fire({
title: t("Enter the number of cartons: "),
icon: "info",
input: "number",
inputPlaceholder: t("Number of cartons"),
inputAttributes:{
min: "1",
step: "1"
},
inputValidator: (value) => {
if(!value){
return t("You need to enter a number")
}
if(parseInt(value) < 1){
return t("Number must be at least 1");
}
return null
},
showCancelButton: true,
confirmButtonText: t("Confirm"),
cancelButtonText: t("Cancel"),
confirmButtonColor: "#8dba00",
cancelButtonColor: "#F04438",
showLoaderOnConfirm: true,
allowOutsideClick: () => !Swal.isLoading()
});

if (askNumofCarton.isConfirmed) {
const numOfCartons = askNumofCarton.value;
try{
if (fgPickOrdersData.length === 0) {
console.error("No FG Pick order data available");
return;
}

const currentFgOrder = fgPickOrdersData[0];

const printDNRequest = {
printerId: 1,
printQty: 1,
isDraft: false,
numOfCarton: numOfCartons,
deliveryOrderId: currentFgOrder.deliveryOrderId,
pickOrderId: currentFgOrder.pickOrderId
};

const printDNLabelsRequest = {
printerId: 1,
printQty: 1,
numOfCarton: numOfCartons,
deliveryOrderId: currentFgOrder.deliveryOrderId
};
console.log("Printing Labels with request: ", printDNLabelsRequest);
console.log("Printing DN with request: ", printDNRequest);

const LabelsResponse = await printDNLabels(printDNLabelsRequest);
const DNResponse = await printDN(printDNRequest);
console.log("Print Labels response: ", LabelsResponse);
console.log("Print DN response: ", DNResponse);

if(LabelsResponse.success && DNResponse.success){
Swal.fire({
position: "bottom-end",
icon: "success",
text: t("Printed Successfully."),
showConfirmButton: false,
timer: 1500
});
} else {
if(!LabelsResponse.success){
console.error("Print failed: ", LabelsResponse.message);
}
else{
console.error("Print failed: ", DNResponse.message);
}
}
} catch(error){
console.error("error: ", error)
}
}
},[t, fgPickOrdersData]);

const handleLabel = useCallback(async () =>{
const askNumofCarton = await Swal.fire({
title: t("Enter the number of cartons: "),
icon: "info",
input: "number",
inputPlaceholder: t("Number of cartons"),
inputAttributes:{
min: "1",
step: "1"
},
inputValidator: (value) => {
if(!value){
return t("You need to enter a number")
}
if(parseInt(value) < 1){
return t("Number must be at least 1");
}
return null
},
showCancelButton: true,
confirmButtonText: t("Confirm"),
cancelButtonText: t("Cancel"),
confirmButtonColor: "#8dba00",
cancelButtonColor: "#F04438",
showLoaderOnConfirm: true,
allowOutsideClick: () => !Swal.isLoading()
});

if (askNumofCarton.isConfirmed) {
const numOfCartons = askNumofCarton.value;
try{
if (fgPickOrdersData.length === 0) {
console.error("No FG Pick order data available");
return;
}
const currentFgOrder = fgPickOrdersData[0];

const printRequest = {
printerId: 1,
printQty: 1,
numOfCarton: numOfCartons,
deliveryOrderId: currentFgOrder.deliveryOrderId,
};

console.log("Printing Labels with request: ", printRequest);

const response = await printDNLabels(printRequest);

console.log("Print Labels response: ", response);

if(response.success){
Swal.fire({
position: "bottom-end",
icon: "success",
text: t("Printed Successfully."),
showConfirmButton: false,
timer: 1500
});
} else {
console.error("Print failed: ", response.message);
}
} catch(error){
console.error("error: ", error)
}
}
},[t, fgPickOrdersData]);

useEffect(() => {
fetchReleasedOrderCount();
@@ -776,7 +556,6 @@ const handleAssignByLane = useCallback(async (
>
<Button
variant="contained"
size="small"
sx={{
py: 0.5, // 增加垂直padding
px: 1.25, // 增加水平padding
@@ -796,7 +575,6 @@ const handleAssignByLane = useCallback(async (
</Button>
<Button
variant="contained"
size="small"
sx={{
py: 0.5,
px: 1.25,
@@ -815,72 +593,6 @@ const handleAssignByLane = useCallback(async (
>
{t("Print Draft")}
</Button>
<Button
variant="contained"
disabled={!printButtonsEnabled}
size="small"
sx={{
py: 0.5,
px: 1.25,
height: '40px',
fontSize: '0.75rem',
lineHeight: 1.2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
'&.Mui-disabled': {
height: '40px'
}
}}
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}
size="small"
sx={{
py: 0.5,
px: 1.25,
height: '40px',
fontSize: '0.75rem',
lineHeight: 1.2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
'&.Mui-disabled': {
height: '40px'
}
}}
title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""}
onClick={handleDN}
>
{t("Print Pick Order")}
</Button>
<Button
variant="contained"
disabled={!printButtonsEnabled}
size="small"
sx={{
py: 0.5,
px: 1.25,
height: '40px',
fontSize: '0.75rem',
lineHeight: 1.2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
'&.Mui-disabled': {
height: '40px'
}
}}
title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""}
onClick={handleLabel}
>
{t("Print DN Label")}
</Button>
</Stack>
</Box>
</Grid>
@@ -895,6 +607,7 @@ const handleAssignByLane = useCallback(async (
<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("Ticket Release Table")} iconPosition="end" />
</Tabs>
</Box>
@@ -906,6 +619,7 @@ const handleAssignByLane = useCallback(async (
{tabIndex === 0 && <PickExecution filterArgs={filterArgs} onFgPickOrdersChange={setFgPickOrdersData}/>}
{tabIndex === 1 && <PickExecutionDetail filterArgs={filterArgs} />}
{tabIndex === 2 && <GoodPickExecutionRecord filterArgs={filterArgs} />}
{tabIndex === 3 && <FGPickOrderTicketReleaseTable/>}
</Box>
</Box>
);


+ 235
- 2
src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx View File

@@ -41,10 +41,10 @@ import {
checkPickOrderCompletion,
PickOrderCompletionResponse,
checkAndCompletePickOrderByConsoCode,
fetchCompletedDoPickOrders, // ✅ 新增:使用新的 API
fetchCompletedDoPickOrders,
CompletedDoPickOrderResponse,
CompletedDoPickOrderSearchParams,
fetchLotDetailsByPickOrderId // ✅ 修复:导入类型
fetchLotDetailsByPickOrderId
} from "@/app/api/pickOrder/actions";
import { fetchNameList, NameList } from "@/app/api/user/actions";
import {
@@ -63,6 +63,9 @@ import GoodPickExecutionForm from "./GoodPickExecutionForm";
import FGPickOrderCard from "./FGPickOrderCard";
import dayjs from "dayjs";
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import { printDN, printDNLabels } from "@/app/api/do/actions";
import Swal from "sweetalert2";


interface Props {
filterArgs: Record<string, any>;
@@ -108,6 +111,205 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => {
const formProps = useForm();
const errors = formProps.formState.errors;

// ✅ Print handler functions
const handleDN = useCallback(async (deliveryOrderId: number, pickOrderId: number) => {
const askNumofCarton = await Swal.fire({
title: t("Enter the number of cartons: "),
icon: "info",
input: "number",
inputPlaceholder: t("Number of cartons"),
inputAttributes:{
min: "1",
step: "1"
},
inputValidator: (value) => {
if(!value){
return t("You need to enter a number")
}
if(parseInt(value) < 1){
return t("Number must be at least 1");
}
return null
},
showCancelButton: true,
confirmButtonText: t("Confirm"),
cancelButtonText: t("Cancel"),
confirmButtonColor: "#8dba00",
cancelButtonColor: "#F04438",
showLoaderOnConfirm: true,
allowOutsideClick: () => !Swal.isLoading()
});

if (askNumofCarton.isConfirmed) {
const numOfCartons = askNumofCarton.value;
try{
const printRequest = {
printerId: 1,
printQty: 1,
isDraft: false,
numOfCarton: numOfCartons,
deliveryOrderId: deliveryOrderId,
pickOrderId: pickOrderId
};

console.log("Printing Delivery Note with request: ", printRequest);
const response = await printDN(printRequest);
console.log("Print Delivery Note response: ", response);

if(response.success){
Swal.fire({
position: "bottom-end",
icon: "success",
text: t("Printed Successfully."),
showConfirmButton: false,
timer: 1500
});
} else {
console.error("Print failed: ", response.message);
}
} catch(error){
console.error("error: ", error)
}
}
}, [t]);

const handleDNandLabel = useCallback(async (deliveryOrderId: number, pickOrderId: number) => {
const askNumofCarton = await Swal.fire({
title: t("Enter the number of cartons: "),
icon: "info",
input: "number",
inputPlaceholder: t("Number of cartons"),
inputAttributes:{
min: "1",
step: "1"
},
inputValidator: (value) => {
if(!value){
return t("You need to enter a number")
}
if(parseInt(value) < 1){
return t("Number must be at least 1");
}
return null
},
showCancelButton: true,
confirmButtonText: t("Confirm"),
cancelButtonText: t("Cancel"),
confirmButtonColor: "#8dba00",
cancelButtonColor: "#F04438",
showLoaderOnConfirm: true,
allowOutsideClick: () => !Swal.isLoading()
});

if (askNumofCarton.isConfirmed) {
const numOfCartons = askNumofCarton.value;
try{
const printDNRequest = {
printerId: 1,
printQty: 1,
isDraft: false,
numOfCarton: numOfCartons,
deliveryOrderId: deliveryOrderId,
pickOrderId: pickOrderId
};

const printDNLabelsRequest = {
printerId: 1,
printQty: 1,
numOfCarton: numOfCartons,
deliveryOrderId: deliveryOrderId
};
console.log("Printing Labels with request: ", printDNLabelsRequest);
console.log("Printing DN with request: ", printDNRequest);

const LabelsResponse = await printDNLabels(printDNLabelsRequest);
const DNResponse = await printDN(printDNRequest);
console.log("Print Labels response: ", LabelsResponse);
console.log("Print DN response: ", DNResponse);

if(LabelsResponse.success && DNResponse.success){
Swal.fire({
position: "bottom-end",
icon: "success",
text: t("Printed Successfully."),
showConfirmButton: false,
timer: 1500
});
} else {
if(!LabelsResponse.success){
console.error("Print failed: ", LabelsResponse.message);
}
else{
console.error("Print failed: ", DNResponse.message);
}
}
} catch(error){
console.error("error: ", error)
}
}
}, [t]);

const handleLabel = useCallback(async (deliveryOrderId: number) => {
const askNumofCarton = await Swal.fire({
title: t("Enter the number of cartons: "),
icon: "info",
input: "number",
inputPlaceholder: t("Number of cartons"),
inputAttributes:{
min: "1",
step: "1"
},
inputValidator: (value) => {
if(!value){
return t("You need to enter a number")
}
if(parseInt(value) < 1){
return t("Number must be at least 1");
}
return null
},
showCancelButton: true,
confirmButtonText: t("Confirm"),
cancelButtonText: t("Cancel"),
confirmButtonColor: "#8dba00",
cancelButtonColor: "#F04438",
showLoaderOnConfirm: true,
allowOutsideClick: () => !Swal.isLoading()
});

if (askNumofCarton.isConfirmed) {
const numOfCartons = askNumofCarton.value;
try{
const printRequest = {
printerId: 1,
printQty: 1,
numOfCarton: numOfCartons,
deliveryOrderId: deliveryOrderId,
};

console.log("Printing Labels with request: ", printRequest);
const response = await printDNLabels(printRequest);
console.log("Print Labels response: ", response);

if(response.success){
Swal.fire({
position: "bottom-end",
icon: "success",
text: t("Printed Successfully."),
showConfirmButton: false,
timer: 1500
});
} else {
console.error("Print failed: ", response.message);
}
} catch(error){
console.error("error: ", error)
}
}
}, [t]);

// ✅ 修改:使用新的 API 获取已完成的 DO Pick Orders
const fetchCompletedDoPickOrdersData = useCallback(async (searchParams?: CompletedDoPickOrderSearchParams) => {
if (!currentUserId) return;
@@ -406,6 +608,37 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => {
>
{t("View Details")}
</Button>
{doPickOrder.fgPickOrders && doPickOrder.fgPickOrders.length > 0 && (
<>
<Button
variant="contained"
onClick={() => handleDN(
doPickOrder.fgPickOrders[0].deliveryOrderId,
doPickOrder.fgPickOrders[0].pickOrderId
)}
>
{t("Print Pick Order")}
</Button>
<Button
variant="contained"
onClick={() => handleDNandLabel(
doPickOrder.fgPickOrders[0].deliveryOrderId,
doPickOrder.fgPickOrders[0].pickOrderId
)}
>
{t("Print DN & Label")}
</Button>
<Button
variant="contained"
onClick={() => handleLabel(
doPickOrder.fgPickOrders[0].deliveryOrderId
)}
>
{t("Print Label")}
</Button>
</>
)}
</CardActions>
</Card>
))}


+ 307
- 0
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx View File

@@ -19,6 +19,7 @@ import {
TablePagination,
Modal,
Chip,
Chip,
} from "@mui/material";
import TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider';
import { fetchLotDetail } from "@/app/api/inventory/actions";
@@ -34,6 +35,7 @@ import {
fetchFGPickOrders, // ✅ Add this import
FGPickOrderResponse,


checkPickOrderCompletion,
fetchAllPickOrderLotsHierarchical,
PickOrderCompletionResponse,
@@ -45,6 +47,7 @@ import {
fetchFGPickOrdersByUserId
} from "@/app/api/pickOrder/actions";
import FGPickOrderInfoCard from "./FGPickOrderInfoCard";
import FGPickOrderInfoCard from "./FGPickOrderInfoCard";
import LotConfirmationModal from "./LotConfirmationModal";
//import { fetchItem } from "@/app/api/settings/item";
import { updateInventoryLotLineStatus, analyzeQrCode } from "@/app/api/inventory/actions";
@@ -441,11 +444,13 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
setAllLotsCompleted(allCompleted);
return allCompleted;
}, []);
const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdOverride?: number) => {
const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdOverride?: number) => {
setCombinedDataLoading(true);
try {
const userIdToUse = userId || currentUserId;
console.log("🔍 fetchAllCombinedLotData called with userId:", userIdToUse);
console.log("🔍 fetchAllCombinedLotData called with userId:", userIdToUse);
if (!userIdToUse) {
@@ -456,6 +461,7 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
return;
}
// ✅ 获取新结构的层级数据
// ✅ 获取新结构的层级数据
const hierarchicalData = await fetchAllPickOrderLotsHierarchical(userIdToUse);
console.log("✅ Hierarchical data (new structure):", hierarchicalData);
@@ -565,6 +571,99 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
// ✅ 将层级数据转换为平铺格式(用于表格显示)
const flatLotData: any[] = [];
targetPickOrder.pickOrderLines.forEach((line: any) => {
if (line.lots && line.lots.length > 0) {
// ✅ 有 lots 的情况
line.lots.forEach((lot: any) => {
flatLotData.push({
pickOrderId: targetPickOrder.pickOrderId,
pickOrderCode: targetPickOrder.pickOrderCode,
pickOrderConsoCode: targetPickOrder.consoCode,
pickOrderTargetDate: targetPickOrder.targetDate,
pickOrderStatus: targetPickOrder.status,
pickOrderLineId: line.id,
pickOrderLineRequiredQty: line.requiredQty,
pickOrderLineStatus: line.status,
itemId: line.item.id,
itemCode: line.item.code,
itemName: line.item.name,
//uomCode: line.item.uomCode,
uomDesc: line.item.uomDesc,
uomShortDesc: line.item.uomShortDesc,
lotId: lot.id,
lotNo: lot.lotNo,
expiryDate: lot.expiryDate,
location: lot.location,
stockUnit: lot.stockUnit,
availableQty: lot.availableQty,
requiredQty: lot.requiredQty,
actualPickQty: lot.actualPickQty,
inQty: lot.inQty,
outQty: lot.outQty,
holdQty: lot.holdQty,
lotStatus: lot.lotStatus,
lotAvailability: lot.lotAvailability,
processingStatus: lot.processingStatus,
suggestedPickLotId: lot.suggestedPickLotId,
stockOutLineId: lot.stockOutLineId,
stockOutLineStatus: lot.stockOutLineStatus,
stockOutLineQty: lot.stockOutLineQty,
routerId: lot.router?.id,
routerIndex: lot.router?.index,
routerRoute: lot.router?.route,
routerArea: lot.router?.area,
});
});
} else {
// ✅ 没有 lots 的情况(null stock)- 也要显示
flatLotData.push({
pickOrderId: targetPickOrder.pickOrderId,
pickOrderCode: targetPickOrder.pickOrderCode,
pickOrderConsoCode: targetPickOrder.consoCode,
pickOrderTargetDate: targetPickOrder.targetDate,
pickOrderStatus: targetPickOrder.status,
pickOrderLineId: line.id,
pickOrderLineRequiredQty: line.requiredQty,
pickOrderLineStatus: line.status,
itemId: line.item.id,
itemCode: line.item.code,
itemName: line.item.name,
//uomCode: line.item.uomCode,
uomDesc: line.item.uomDesc,
// ✅ Null stock 字段
lotId: null,
lotNo: null,
expiryDate: null,
location: null,
stockUnit: line.item.uomDesc,
availableQty: 0,
requiredQty: line.requiredQty,
actualPickQty: 0,
inQty: 0,
outQty: 0,
holdQty: 0,
lotStatus: 'unavailable',
lotAvailability: 'insufficient_stock',
processingStatus: 'pending',
suggestedPickLotId: null,
stockOutLineId: null,
stockOutLineStatus: null,
stockOutLineQty: 0,
routerId: null,
routerIndex: 999999, // ✅ 放到最后
routerRoute: null,
routerArea: null,
uomShortDesc: line.item.uomShortDesc
});
}
});
targetPickOrder.pickOrderLines.forEach((line: any) => {
if (line.lots && line.lots.length > 0) {
// ✅ 有 lots 的情况
@@ -662,10 +761,13 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
console.log("✅ Transformed flat lot data:", flatLotData);
console.log("🔍 Total items (including null stock):", flatLotData.length);
console.log("🔍 Total items (including null stock):", flatLotData.length);
setCombinedLotData(flatLotData);
setOriginalCombinedData(flatLotData);
checkAllLotsCompleted(flatLotData);
} catch (error) {
console.error("❌ Error fetching combined lot data:", error);
setCombinedLotData([]);
@@ -675,6 +777,7 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
setCombinedDataLoading(false);
}
}, [currentUserId, selectedPickOrderId, checkAllLotsCompleted]);
}, [currentUserId, selectedPickOrderId, checkAllLotsCompleted]);
// ✅ Add effect to check completion when lot data changes
useEffect(() => {
if (combinedLotData.length > 0) {
@@ -1508,6 +1611,22 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
setPickOrderSwitching(false);
}
}, [pickOrderSwitching, currentUserId, fetchAllCombinedLotData]);
const handlePickOrderSwitch = useCallback(async (pickOrderId: number) => {
if (pickOrderSwitching) return;
setPickOrderSwitching(true);
try {
console.log("🔍 Switching to pick order:", pickOrderId);
setSelectedPickOrderId(pickOrderId);
// ✅ 强制刷新数据,确保显示正确的 pick order 数据
await fetchAllCombinedLotData(currentUserId, pickOrderId);
} catch (error) {
console.error("Error switching pick order:", error);
} finally {
setPickOrderSwitching(false);
}
}, [pickOrderSwitching, currentUserId, fetchAllCombinedLotData]);
const handleStopScan = useCallback(() => {
console.log("⏹️ Stopping manual QR scan...");
setIsManualScanning(false);
@@ -1645,6 +1764,40 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
lot.stockOutLineStatus !== 'rejected' &&
lot.stockOutLineStatus !== 'completed'
)}
>
<FormProvider {...formProps}>
<Stack spacing={2}>
{/* DO Header */}
{fgPickOrdersLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 2 }}>
<CircularProgress size={20} />
</Box>
) : (
fgPickOrders.length > 0 && (
<Paper sx={{ p: 2 }}>
<Stack spacing={2}>
{/* 基本信息 */}
<Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
<Typography variant="subtitle1">
<strong>{t("Shop Name")}:</strong> {fgPickOrders[0].shopName || '-'}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Store ID")}:</strong> {fgPickOrders[0].storeId || '-'}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Ticket No.")}:</strong> {fgPickOrders[0].ticketNo || '-'}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Departure Time")}:</strong> {fgPickOrders[0].DepartureTime || '-'}
</Typography>
</Stack>
lotData={combinedLotData}
onScanLot={handleQrCodeSubmit}
filterActive={(lot) => (
lot.lotAvailability !== 'rejected' &&
lot.stockOutLineStatus !== 'rejected' &&
lot.stockOutLineStatus !== 'completed'
)}
>
<FormProvider {...formProps}>
<Stack spacing={2}>
@@ -1981,6 +2134,160 @@ paginatedData.map((lot, index) => {
/>
)}
{/* ✅ 保留:Good Pick Execution Form Modal */}
{pickExecutionFormOpen && selectedLotForExecutionForm && (
<GoodPickExecutionForm
open={pickExecutionFormOpen}
onClose={() => {
setPickExecutionFormOpen(false);
setSelectedLotForExecutionForm(null);
}}
onSubmit={handlePickExecutionFormSubmit}
selectedLot={selectedLotForExecutionForm}
selectedPickOrderLine={{
id: selectedLotForExecutionForm.pickOrderLineId,
itemId: selectedLotForExecutionForm.itemId,
itemCode: selectedLotForExecutionForm.itemCode,
itemName: selectedLotForExecutionForm.itemName,
pickOrderCode: selectedLotForExecutionForm.pickOrderCode,
availableQty: selectedLotForExecutionForm.availableQty || 0,
requiredQty: selectedLotForExecutionForm.requiredQty || 0,
// uomCode: selectedLotForExecutionForm.uomCode || '',
uomDesc: selectedLotForExecutionForm.uomDesc || '',
pickedQty: selectedLotForExecutionForm.actualPickQty || 0,
uomShortDesc: selectedLotForExecutionForm.uomShortDesc || '',
suggestedList: []
}}
pickOrderId={selectedLotForExecutionForm.pickOrderId}
pickOrderCreateDate={new Date()}
/>
)}
</FormProvider>
<TableCell align="center">
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
{isIssueLot ? (
// ✅ Issue lot 只显示 Issue 按钮
<Button
variant="outlined"
size="small"
onClick={() => handlePickExecutionForm(lot)}
disabled={true}
sx={{
fontSize: '0.7rem',
py: 0.5,
minHeight: '28px',
minWidth: '60px',
borderColor: 'warning.main',
color: 'warning.main'
}}
title="Rejected lot - Issue only"
>
{t("Issue")}
</Button>
) : (
// ✅ Normal lot 显示两个按钮
<Stack direction="row" spacing={1} alignItems="center">
<Button
variant="contained"
onClick={() => {
const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
handlePickQtyChange(lotKey, submitQty);
handleSubmitPickQtyWithQty(lot, submitQty);
}}
disabled={
lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected' ||
lot.stockOutLineStatus === 'completed' ||
lot.stockOutLineStatus === 'pending'
}
sx={{ fontSize: '0.75rem', py: 0.5, minHeight: '28px', minWidth: '70px' }}
>
{t("Submit")}
</Button>
<Button
variant="outlined"
size="small"
onClick={() => handlePickExecutionForm(lot)}
disabled={
lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected' ||
lot.stockOutLineStatus === 'completed' ||
lot.stockOutLineStatus === 'pending'
}
sx={{
fontSize: '0.7rem',
py: 0.5,
minHeight: '28px',
minWidth: '60px',
borderColor: 'warning.main',
color: 'warning.main'
}}
title="Report missing or bad items"
>
{t("Issue")}
</Button>
</Stack>
)}
</Box>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div"
count={combinedLotData.length}
page={paginationController.pageNum}
rowsPerPage={paginationController.pageSize}
onPageChange={handlePageChange}
onRowsPerPageChange={handlePageSizeChange}
rowsPerPageOptions={[10, 25, 50]}
labelRowsPerPage={t("Rows per page")}
labelDisplayedRows={({ from, to, count }) =>
`${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
}
/>
</Box>
</Stack>
{/* ✅ 保留:QR Code Modal */}
<QrCodeModal
open={qrModalOpen}
onClose={() => {
setQrModalOpen(false);
setSelectedLotForQr(null);
stopScan();
resetScan();
}}
lot={selectedLotForQr}
combinedLotData={combinedLotData}
onQrCodeSubmit={handleQrCodeSubmitFromModal}
/>
{/* ✅ 保留:Lot Confirmation Modal */}
{lotConfirmationOpen && expectedLotData && scannedLotData && (
<LotConfirmationModal
open={lotConfirmationOpen}
onClose={() => {
setLotConfirmationOpen(false);
setExpectedLotData(null);
setScannedLotData(null);
}}
onConfirm={handleLotConfirmation}
expectedLot={expectedLotData}
scannedLot={scannedLotData}
isLoading={isConfirmingLot}
/>
)}
{/* ✅ 保留:Good Pick Execution Form Modal */}
{pickExecutionFormOpen && selectedLotForExecutionForm && (
<GoodPickExecutionForm


+ 4
- 2
src/components/JoSearch/JoSearch.tsx View File

@@ -25,17 +25,19 @@ import dayjs from "dayjs";

import { fetchInventories } from "@/app/api/inventory/actions";
import { InventoryResult } from "@/app/api/inventory";
import { PrinterCombo } from "@/app/api/settings/printer";

interface Props {
defaultInputs: SearchJoResultRequest,
bomCombo: BomCombo[]
printerCombo: PrinterCombo[];
}

type SearchQuery = Partial<Omit<JobOrder, "id">>;

type SearchParamNames = keyof SearchQuery;

const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo }) => {
const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo }) => {
const { t } = useTranslation("jo");
const router = useRouter()
const [filteredJos, setFilteredJos] = useState<JobOrder[]>([]);
@@ -426,7 +428,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo }) => {
open={openModal}
onClose={closeNewModal}
inputDetail={modalInfo}
printerCombo={[]}
printerCombo={printerCombo}
// skipQc={true}
/>
</>


+ 6
- 3
src/components/JoSearch/JoSearchWrapper.tsx View File

@@ -3,6 +3,7 @@ import GeneralLoading from "../General/GeneralLoading";
import JoSearch from "./JoSearch";
import { SearchJoResultRequest } from "@/app/api/jo/actions";
import { fetchBomCombo } from "@/app/api/bom";
import { fetchPrinterCombo } from "@/app/api/settings/printer";

interface SubComponents {
Loading: typeof GeneralLoading;
@@ -15,12 +16,14 @@ const JoSearchWrapper: React.FC & SubComponents = async () => {
}

const [
bomCombo
bomCombo,
printerCombo
] = await Promise.all([
fetchBomCombo()
fetchBomCombo(),
fetchPrinterCombo()
])
return <JoSearch defaultInputs={defaultInputs} bomCombo={bomCombo}/>
return <JoSearch defaultInputs={defaultInputs} bomCombo={bomCombo} printerCombo={printerCombo}/>
}

JoSearchWrapper.Loading = GeneralLoading;


+ 4
- 4
src/components/Logo/Logo.tsx View File

@@ -14,16 +14,16 @@ const Logo: React.FC<Props> = ({ width, height }) => {
<g
id="svgGroup"
strokeLinecap="round"
fill-rule="evenodd"
font-size="9pt"
fillRule="evenodd"
fontSize="9pt"
stroke="#000"
stroke-width="0.25mm"
strokeWidth="0.25mm"
fill="#000"
// style="stroke:#000;stroke-width:0.25mm;fill:#000"
>
<path
d="M 72.768 0.48 L 75.744 0.48 L 88.224 29.568 L 88.56 29.568 L 101.04 0.48 L 103.92 0.48 L 103.92 34.416 L 101.136 34.416 L 101.136 7.344 L 100.896 7.344 L 89.136 34.416 L 87.504 34.416 L 75.744 7.344 L 75.552 7.344 L 75.552 34.416 L 72.768 34.416 L 72.768 0.48 Z M 137.808 0.48 L 140.784 0.48 L 153.264 29.568 L 153.6 29.568 L 166.08 0.48 L 168.96 0.48 L 168.96 34.416 L 166.176 34.416 L 166.176 7.344 L 165.936 7.344 L 154.176 34.416 L 152.544 34.416 L 140.784 7.344 L 140.592 7.344 L 140.592 34.416 L 137.808 34.416 L 137.808 0.48 Z M 198.72 7.824 L 195.84 7.824 Q 195.456 4.848 193.224 3.696 Q 190.992 2.544 187.344 2.544 Q 183.168 2.544 181.152 4.152 Q 179.136 5.76 179.136 8.88 Q 179.136 10.704 179.832 11.856 Q 180.528 13.008 181.632 13.704 Q 182.736 14.4 183.984 14.808 Q 185.232 15.216 186.288 15.504 L 189.984 16.512 Q 191.376 16.896 193.008 17.472 Q 194.64 18.048 196.104 19.056 Q 197.568 20.064 198.48 21.648 Q 199.392 23.232 199.392 25.584 Q 199.392 28.272 198.096 30.432 Q 196.8 32.592 194.112 33.816 Q 191.424 35.04 187.248 35.04 Q 181.68 35.04 178.704 32.784 Q 175.728 30.528 175.344 26.688 L 178.32 26.688 Q 178.608 28.992 179.808 30.24 Q 181.008 31.488 182.904 31.992 Q 184.8 32.496 187.248 32.496 Q 191.664 32.496 194.088 30.792 Q 196.512 29.088 196.512 25.488 Q 196.512 23.328 195.48 22.08 Q 194.448 20.832 192.744 20.112 Q 191.04 19.392 189.072 18.864 L 184.464 17.616 Q 180.528 16.512 178.392 14.544 Q 176.256 12.576 176.256 9.12 Q 176.256 6.288 177.624 4.248 Q 178.992 2.208 181.512 1.104 Q 184.032 0 187.488 0 Q 190.848 0 193.272 0.984 Q 195.696 1.968 197.112 3.72 Q 198.528 5.472 198.72 7.824 Z M 0 34.416 L 0 0.48 L 19.344 0.48 L 19.344 2.976 L 2.88 2.976 L 2.88 16.176 L 17.76 16.176 L 17.76 18.672 L 2.88 18.672 L 2.88 34.416 L 0 34.416 Z M 108.336 2.976 L 108.336 0.48 L 133.392 0.48 L 133.392 2.976 L 122.304 2.976 L 122.304 34.416 L 119.424 34.416 L 119.424 2.976 L 108.336 2.976 Z M 25.152 34.416 L 25.152 0.48 L 36.48 0.48 Q 40.56 0.48 43.056 1.752 Q 45.552 3.024 46.704 5.328 Q 47.856 7.632 47.856 10.8 Q 47.856 13.968 46.704 16.32 Q 45.552 18.672 43.08 19.968 Q 40.608 21.264 36.576 21.264 L 28.032 21.264 L 28.032 34.416 L 25.152 34.416 Z M 28.032 18.768 L 36.384 18.768 Q 39.744 18.768 41.616 17.784 Q 43.488 16.8 44.232 15 Q 44.976 13.2 44.976 10.8 Q 44.976 8.352 44.232 6.6 Q 43.488 4.848 41.616 3.912 Q 39.744 2.976 36.288 2.976 L 28.032 2.976 L 28.032 18.768 Z M 65.664 18 L 65.664 20.496 L 52.704 20.496 L 52.704 18 L 65.664 18 Z"
vector-effect="non-scaling-stroke"
vectorEffect="non-scaling-stroke"
/>
</g>
</svg>


+ 1
- 1
src/components/MailField/MailField.css View File

@@ -123,7 +123,7 @@

/* Input styles */
/* .tiptap-input {
font-size: 14px;
fontSize: 14px;
font-weight: 500;
line-height: 12px;
} */

+ 1
- 1
src/components/StockIn/FgStockInForm.tsx View File

@@ -67,7 +67,7 @@ const textfieldSx = {
transform: "translate(14px, 1.2rem) scale(1)",
"&.MuiInputLabel-shrink": {
fontSize: 24,
transform: "translate(14px, -0.5rem) scale(1)",
transform: "translate(14px, -9px) scale(1)",
},
// [theme.breakpoints.down("sm")]: {
// fontSize: "1rem",


+ 1
- 1
src/components/StockIn/StockInForm.tsx View File

@@ -60,7 +60,7 @@ const textfieldSx = {
transform: "translate(14px, 1.2rem) scale(1)",
"&.MuiInputLabel-shrink": {
fontSize: 24,
transform: "translate(14px, -0.5rem) scale(1)",
transform: "translate(14px, -9px) scale(1)",
},
// [theme.breakpoints.down("sm")]: {
// fontSize: "1rem",


+ 6
- 4
src/i18n/zh/pickOrder.json View File

@@ -278,9 +278,9 @@
"Confirm":"確認",
"Update your suggested lot to the this scanned lot":"更新您的建議批次為此掃描的批次",
"Print Draft":"列印草稿",
"Print Pick Order and DN Label":"列印提料單和送貨單標",
"Print Pick Order and DN Label":"列印提料單和送貨單標",
"Print Pick Order":"列印提料單",
"Print DN Label":"列印送貨單標",
"Print DN Label":"列印送貨單標",
"Print All Draft" : "列印全部草稿",
"If you confirm, the system will:":"如果您確認,系統將:",
"QR code verified.":"QR 碼驗證成功。",
@@ -292,7 +292,6 @@
"Completed DO pick orders: ":"已完成送貨單提料單:",
"No completed DO pick orders found":"沒有已完成送貨單提料單",

"Print DN Label":"列印送貨單標貼",
"Enter the number of cartons: ": "請輸入總箱數",
"Number of cartons": "箱數",
"Select an action for the assigned pick orders.": "選擇分配提料單的動作。",
@@ -390,5 +389,8 @@
"Today": "是日",
"Tomorrow": "翌日",
"Day After Tomorrow": "後日",
"Select Date": "請選擇日期"
"Select Date": "請選擇日期",
"Print DN & Label": "列印提料單和送貨單標籤",
"Print Label": "列印送貨單標籤",
"Ticket Release Table": "查看提貨情況"
}

+ 2
- 1
src/i18n/zh/purchaseOrder.json View File

@@ -164,5 +164,6 @@
"Expiry Date cannot be earlier than Production Date": "到期日不可早於生產日期",
"Production Date must be earlier than Expiry Date": "生產日期必須早於到期日",
"confirm expiry date": "確認到期日",
"Invalid Date": "無效日期"
"Invalid Date": "無效日期",
"Missing QC Template, please contact administrator": "找不到品檢模板,請聯絡管理員"
}

Loading…
Cancel
Save