Bladeren bron

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

# Conflicts:
#	src/app/api/do/actions.tsx
stable1
CANCERYS\kw093 2 weken geleden
bovenliggende
commit
89421afaf7
8 gewijzigde bestanden met toevoegingen van 632 en 112 verwijderingen
  1. +27
    -0
      src/app/api/do/actions.tsx
  2. +159
    -6
      src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx
  3. +9
    -1
      src/components/InventorySearch/InventoryLotLineTable.tsx
  4. +391
    -97
      src/components/PoDetail/PoDetail.tsx
  5. +7
    -1
      src/components/PoDetail/QcStockInModal.tsx
  6. +7
    -1
      src/components/Qc/QcStockInModal.tsx
  7. +22
    -6
      src/components/Swal/CustomAlerts.tsx
  8. +10
    -0
      src/i18n/zh/pickOrder.json

+ 27
- 0
src/app/api/do/actions.tsx Bestand weergeven

@@ -100,6 +100,15 @@ export interface PrintDNLabelsRequest{
numOfCarton: number;
}

export interface PrintDNLabelsReprintRequest{
doPickOrderId: number;
printerId: number;
printQty: number;
fromCarton: number;
toCarton: number;
totalCartonsOnShipment: number;
}

export interface PrintDNLabelsRespone{
success: boolean;
message?: string
@@ -424,6 +433,24 @@ export async function printDNLabels(request: PrintDNLabelsRequest){

return { success: true, message: "Print job sent successfully (labels)"} as PrintDeliveryNoteResponse
}

export async function printDNLabelsReprint(request: PrintDNLabelsReprintRequest){
const params = new URLSearchParams();
params.append('doPickOrderId', request.doPickOrderId.toString());
params.append('printerId', request.printerId.toString());
if (request.printQty !== null && request.printQty !== undefined) {
params.append('printQty', request.printQty.toString());
}
params.append('fromCarton', request.fromCarton.toString());
params.append('toCarton', request.toCarton.toString());
params.append('totalCartonsOnShipment', request.totalCartonsOnShipment.toString());

await serverFetchWithNoContent(`${BASE_API_URL}/do/print-DNLabels-reprint?${params.toString()}`,{
method: "GET"
});

return { success: true, message: "Print job sent successfully (reprint labels)"} as PrintDeliveryNoteResponse
}
/*
export interface PrintWorkbenchDeliveryNoteRequest{
deliveryOrderPickOrderId: number;


+ 159
- 6
src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx Bestand weergeven

@@ -66,7 +66,7 @@ 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 { printDN, printDNLabels, printDNLabelsReprint } from "@/app/api/do/actions";
import Swal from "sweetalert2";


@@ -173,7 +173,20 @@ const GoodPickExecutionRecord: React.FC<Props> = ({
confirmButtonColor: "#8dba00",
cancelButtonColor: "#F04438",
showLoaderOnConfirm: true,
allowOutsideClick: () => !Swal.isLoading()
allowOutsideClick: () => !Swal.isLoading(),
didOpen: () => {
const input = Swal.getInput() as HTMLInputElement | null;
if (input) {
input.style.outline = "none";
input.style.boxShadow = "none";
input.style.border = "1px solid #d9d9d9";
input.onfocus = () => {
input.style.outline = "none";
input.style.boxShadow = "none";
input.style.borderColor = "#d9d9d9";
};
}
}
});

if (askNumofCarton.isConfirmed) {
@@ -209,7 +222,7 @@ const GoodPickExecutionRecord: React.FC<Props> = ({
}, [t, a4Printer]);

const handleDNandLabel = useCallback(async (recordId: number) => {
if (!a4Printer || !labelPrinter) {
if (!a4Printer) {
Swal.fire({
position: "bottom-end",
icon: "warning",
@@ -253,7 +266,20 @@ const GoodPickExecutionRecord: React.FC<Props> = ({
confirmButtonColor: "#8dba00",
cancelButtonColor: "#F04438",
showLoaderOnConfirm: true,
allowOutsideClick: () => !Swal.isLoading()
allowOutsideClick: () => !Swal.isLoading(),
didOpen: () => {
const input = Swal.getInput() as HTMLInputElement | null;
if (input) {
input.style.outline = "none";
input.style.boxShadow = "none";
input.style.border = "1px solid #d9d9d9";
input.onfocus = () => {
input.style.outline = "none";
input.style.boxShadow = "none";
input.style.borderColor = "#d9d9d9";
};
}
}
});

if (askNumofCarton.isConfirmed) {
@@ -307,6 +333,17 @@ const GoodPickExecutionRecord: React.FC<Props> = ({

const handleLabel = useCallback(async (recordId: number) => {
console.log(" [Print Label] Button clicked for recordId:", recordId);
if (!labelPrinter) {
Swal.fire({
position: "bottom-end",
icon: "warning",
text: t("Please select a label printer first"),
showConfirmButton: false,
timer: 1500
});
return;
}

const askNumofCarton = await Swal.fire({
title: t("Enter the number of cartons: "),
icon: "info",
@@ -331,14 +368,27 @@ const GoodPickExecutionRecord: React.FC<Props> = ({
confirmButtonColor: "#8dba00",
cancelButtonColor: "#F04438",
showLoaderOnConfirm: true,
allowOutsideClick: () => !Swal.isLoading()
allowOutsideClick: () => !Swal.isLoading(),
didOpen: () => {
const input = Swal.getInput() as HTMLInputElement | null;
if (input) {
input.style.outline = "none";
input.style.boxShadow = "none";
input.style.border = "1px solid #d9d9d9";
input.onfocus = () => {
input.style.outline = "none";
input.style.boxShadow = "none";
input.style.borderColor = "#d9d9d9";
};
}
}
});

if (askNumofCarton.isConfirmed) {
const numOfCartons = askNumofCarton.value;
try{
const printRequest = {
printerId: labelPrinter?.id ?? 0,
printerId: labelPrinter.id,
printQty: 1,
numOfCarton: numOfCartons,
doPickOrderId: recordId
@@ -365,6 +415,103 @@ const GoodPickExecutionRecord: React.FC<Props> = ({
}
}, [t, labelPrinter]);

const handleLabelReprint = useCallback(async (doPickOrder: CompletedDoPickOrderResponse) => {
if (!labelPrinter) {
Swal.fire({
position: "bottom-end",
icon: "warning",
text: t("Please select a label printer first"),
showConfirmButton: false,
timer: 1500
});
return;
}

const defaultTotalCartons = Math.max(1, doPickOrder.numberOfCartons || 1);
const result = await Swal.fire({
title: t("Reprint DN Label"),
html: `
<div style="display:flex;flex-direction:column;gap:10px;text-align:left;">
<div style="display:flex;align-items:center;gap:12px;">
<label for="swal-from-carton" style="min-width:120px;">${t("From carton")}</label>
<input id="swal-from-carton" class="swal2-input" type="number" min="1" step="1" value="1" style="margin:0;flex:1;outline:none;box-shadow:none;border:1px solid #d9d9d9;" onfocus="this.style.outline='none';this.style.boxShadow='none';this.style.borderColor='#d9d9d9';" />
</div>
<div style="display:flex;align-items:center;gap:12px;">
<label for="swal-to-carton" style="min-width:120px;">${t("To carton")}</label>
<input id="swal-to-carton" class="swal2-input" type="number" min="1" step="1" value="1" style="margin:0;flex:1;outline:none;box-shadow:none;border:1px solid #d9d9d9;" onfocus="this.style.outline='none';this.style.boxShadow='none';this.style.borderColor='#d9d9d9';" />
</div>
<div style="display:flex;align-items:center;gap:12px;">
<label for="swal-total-carton" style="min-width:120px;">${t("Total cartons on shipment")}</label>
<input id="swal-total-carton" class="swal2-input" type="number" min="1" step="1" value="${defaultTotalCartons}" style="margin:0;flex:1;outline:none;box-shadow:none;border:1px solid #d9d9d9;" onfocus="this.style.outline='none';this.style.boxShadow='none';this.style.borderColor='#d9d9d9';" />
</div>
</div>
`,
showCancelButton: true,
confirmButtonText: t("Confirm"),
cancelButtonText: t("Cancel"),
confirmButtonColor: "#8dba00",
cancelButtonColor: "#F04438",
focusConfirm: false,
preConfirm: () => {
const fromCarton = Number((document.getElementById("swal-from-carton") as HTMLInputElement | null)?.value || "0");
const toCarton = Number((document.getElementById("swal-to-carton") as HTMLInputElement | null)?.value || "0");
const totalCartonsOnShipment = Number((document.getElementById("swal-total-carton") as HTMLInputElement | null)?.value || "0");

if (!Number.isInteger(fromCarton) || fromCarton < 1) {
Swal.showValidationMessage(t("From carton must be at least 1"));
return null;
}
if (!Number.isInteger(toCarton) || toCarton < fromCarton) {
Swal.showValidationMessage(t("To carton must be greater than or equal to from carton"));
return null;
}
if (!Number.isInteger(totalCartonsOnShipment) || totalCartonsOnShipment < 1) {
Swal.showValidationMessage(t("Total cartons on shipment must be at least 1"));
return null;
}
if (toCarton > totalCartonsOnShipment) {
Swal.showValidationMessage(t("To carton cannot be greater than total cartons on shipment"));
return null;
}

return {
fromCarton,
toCarton,
totalCartonsOnShipment,
};
}
});

if (!result.isConfirmed || !result.value) {
return;
}

try {
const response = await printDNLabelsReprint({
doPickOrderId: doPickOrder.doPickOrderRecordId,
printerId: labelPrinter.id,
printQty: 1,
fromCarton: result.value.fromCarton,
toCarton: result.value.toCarton,
totalCartonsOnShipment: result.value.totalCartonsOnShipment,
});

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

// 修改:使用新的 API 获取已完成的 DO Pick Orders
const fetchCompletedDoPickOrdersData = useCallback(async (searchParams?: CompletedDoPickOrderSearchParams) => {
if (listScope === "mine" && !currentUserId) return;
@@ -843,6 +990,12 @@ if (showDetailView && selectedDoPickOrder) {
>
{t("Print Label")}
</Button>
<Button
variant="contained"
onClick={() => handleLabelReprint(doPickOrder)}
>
{t("Reprint Label(s)")}
</Button>
</>
</CardActions>


+ 9
- 1
src/components/InventorySearch/InventoryLotLineTable.tsx Bestand weergeven

@@ -502,7 +502,15 @@ const prevAdjustmentModalOpenRef = useRef(false);
const response = await createStockTransfer(request);
if (response && response.type === "success") {
msg(t("Stock transfer successful"));
const successMsg =
response.code === "MERGED_EXISTING_LOT_AMBIGUOUS"
? t("Stock transfer merged ambiguous")
: response.code === "MERGED_EXISTING_LOT"
? t("Stock transfer merged existing lot")
: response.code === "CREATED_NEW_LOT"
? t("Stock transfer created new lot")
: response.message?.trim() || t("Stock transfer successful");
msg(successMsg);
handleCloseStockTransferModal();
await onStockTransferSuccess?.();
} else {


+ 391
- 97
src/components/PoDetail/PoDetail.tsx Bestand weergeven

@@ -31,6 +31,11 @@ import {
CardContent,
Radio,
alpha,
Autocomplete,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
} from "@mui/material";
import { useTranslation } from "react-i18next";
import { submitDialogWithWarning } from "../Swal/CustomAlerts";
@@ -69,8 +74,8 @@ import QrModal from "./QrModal";
import { PlayArrow } from "@mui/icons-material";
import DoneIcon from "@mui/icons-material/Done";
import { downloadFile, getCustomWidth } from "@/app/utils/commonUtil";
import PoInfoCard from "./PoInfoCard";
import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil";
import { arrayToDateString } from "@/app/utils/formatUtil";
import { List, ListItem, ListItemButton, ListItemText, Divider } from "@mui/material";
import { Controller, FormProvider, useForm } from "react-hook-form";
import dayjs, { Dayjs } from "dayjs";
@@ -81,6 +86,7 @@ import { getMailTemplatePdfForStockInLine } from "@/app/api/mailTemplate/actions
import { PrinterCombo } from "@/app/api/settings/printer";
import { EscalationCombo } from "@/app/api/user";
import { StockInLine } from "@/app/api/stockIn";
import { printQrCodeForSil } from "@/app/api/stockIn/actions";
import { useSession } from "next-auth/react";
import { AUTH } from "@/authorities";
//import { useRouter } from "next/navigation";
@@ -155,7 +161,16 @@ const PoSearchList: React.FC<{
}, [poList, searchTerm, t]);

return (
<Paper sx={{ p: 2, maxHeight: "480px", overflow: "auto", minWidth: "300px", height: "480px" }}>
<Paper
sx={{
p: 2,
minWidth: "300px",
height: "100%",
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}
>
<Typography variant="h6" gutterBottom>
{t("Purchase Order")}
</Typography>
@@ -175,51 +190,53 @@ const PoSearchList: React.FC<{
),
}}
/>
{loading ? (
<LoadingComponent />
) : filteredPoList.length > 0 ? (
<List dense sx={{ width: "100%" }}>
{filteredPoList.map((poItem, index) => (
<div key={poItem.id}>
<ListItem disablePadding sx={{ width: "100%" }}>
<ListItemButton
selected={selectedPoId === poItem.id}
onClick={() => onSelect(poItem)}
sx={{
width: "100%",
"&.Mui-selected": {
backgroundColor: "primary.light",
"&:hover": {
<Box sx={{ flex: 1, overflow: "auto" }}>
{loading ? (
<LoadingComponent />
) : filteredPoList.length > 0 ? (
<List dense sx={{ width: "100%" }}>
{filteredPoList.map((poItem, index) => (
<div key={poItem.id}>
<ListItem disablePadding sx={{ width: "100%" }}>
<ListItemButton
selected={selectedPoId === poItem.id}
onClick={() => onSelect(poItem)}
sx={{
width: "100%",
"&.Mui-selected": {
backgroundColor: "primary.light",
"&:hover": {
backgroundColor: "primary.light",
},
},
},
}}
>
<ListItemText
primary={
<Typography variant="body2" sx={{ wordBreak: "break-all" }}>
{poItem.code}
</Typography>
}
secondary={
<Typography variant="caption" color="text.secondary">
{t(`${poItem.status.toLowerCase()}`)}
</Typography>
}
/>
</ListItemButton>
</ListItem>
{index < filteredPoList.length - 1 && <Divider />}
</div>
))}
</List>
) : (
<Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
{searchTerm.trim()
? t("No purchase orders match your search", { defaultValue: "沒有符合搜尋的採購單" })
: t("No purchase orders to show", { defaultValue: "沒有可顯示的採購單" })}
</Typography>
)}
}}
>
<ListItemText
primary={
<Typography variant="body2" sx={{ wordBreak: "break-all" }}>
{poItem.code}
</Typography>
}
secondary={
<Typography variant="caption" color="text.secondary">
{t(`${poItem.status.toLowerCase()}`)}
</Typography>
}
/>
</ListItemButton>
</ListItem>
{index < filteredPoList.length - 1 && <Divider />}
</div>
))}
</List>
) : (
<Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
{searchTerm.trim()
? t("No purchase orders match your search", { defaultValue: "沒有符合搜尋的採購單" })
: t("No purchase orders to show", { defaultValue: "沒有可顯示的採購單" })}
</Typography>
)}
</Box>
{searchTerm && (
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: "block" }}>
{`${t("Found")} ${filteredPoList.length} ${t("Purchase Order")}`}
@@ -311,6 +328,86 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => {
receiptDate: dayjsToDateString(dayjs())
}
})

const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | undefined>(
printerCombo?.[0],
);
const [printQty, setPrintQty] = useState(1);
const [printDialogOpen, setPrintDialogOpen] = useState(false);
const [isBulkPrinting, setIsBulkPrinting] = useState(false);
const [printStatusFilter, setPrintStatusFilter] = useState({
received: true,
completed: false,
});
const [selectedPrintSilIds, setSelectedPrintSilIds] = useState<Set<number>>(
() => new Set(),
);

const eligiblePrintSils = useMemo(() => {
const statusSet = new Set<string>();
if (printStatusFilter.received) statusSet.add("received");
if (printStatusFilter.completed) statusSet.add("completed");
const pols = purchaseOrder.pol ?? [];
return pols
.flatMap((pol) => pol.stockInLine ?? [])
.filter((sil) => statusSet.has((sil.status ?? "").toLowerCase().trim()));
}, [purchaseOrder.pol, printStatusFilter.completed, printStatusFilter.received]);

const openPrintDialog = useCallback(() => {
setSelectedPrintSilIds(new Set());
setPrintDialogOpen(true);
}, []);

const closePrintDialog = useCallback(() => {
if (isBulkPrinting) return;
setPrintDialogOpen(false);
}, [isBulkPrinting]);

const togglePrintSilSelection = useCallback((id: number, checked: boolean) => {
setSelectedPrintSilIds((prev) => {
const next = new Set(prev);
if (checked) next.add(id);
else next.delete(id);
return next;
});
}, []);

const setAllVisiblePrintSilsSelected = useCallback((checked: boolean) => {
setSelectedPrintSilIds(() => {
if (!checked) return new Set();
return new Set(eligiblePrintSils.map((s) => s.id));
});
}, [eligiblePrintSils]);

const handleBulkPrint = useCallback(async () => {
if (!selectedPrinter) {
alert("請先選擇印表機");
return;
}
if (!Number.isFinite(printQty) || printQty <= 0) {
alert("列印數量必須大於 0");
return;
}
const ids = Array.from(selectedPrintSilIds.values());
if (ids.length <= 0) {
alert("請先選擇要列印的項目");
return;
}
setIsBulkPrinting(true);
try {
for (const id of ids) {
await printQrCodeForSil({
stockInLineId: id,
printerId: selectedPrinter.id,
printQty,
});
}
setPrintDialogOpen(false);
} finally {
setIsBulkPrinting(false);
}
}, [printQty, selectedPrinter, selectedPrintSilIds]);

/** Only loads sidebar list when `selectedIds` is in the URL; otherwise show current PO only (no /po/list fetch). */
const fetchPoList = useCallback(async () => {
if (!selectedIdsParam) return;
@@ -836,6 +933,21 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => {
}
}, [])

const fillTodayLotNo = useCallback(() => {
const today = dayjs().format("YYYYMMDD");
setPolInputList((prev) => {
const next: Record<number, PolInputResult> = { ...prev };
(rows ?? []).forEach((r) => {
const current = next[r.id] ?? { lotNo: "", dnQty: "" };
const lotNo = (current.lotNo ?? "").trim();
if (!lotNo) {
next[r.id] = { ...current, lotNo: today };
}
});
return next;
});
}, [rows]);

return (
<>
<Stack spacing={2}>
@@ -850,10 +962,10 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => {
</Grid>

{/* area2: dn info */}
<Grid container spacing={3} sx={{ maxWidth: 'fit-content' }}>
<Grid container spacing={3} sx={{ maxWidth: 'fit-content' }} alignItems="stretch">
{/* left side select po */}
<Grid item xs={4}>
<Stack spacing={1}>
<Grid item xs={4} sx={{ display: "flex" }}>
<Stack spacing={1} sx={{ flex: 1 }}>
<PoSearchList
poList={poList}
selectedPoId={selectedPoId}
@@ -867,71 +979,131 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => {
<Grid item xs={8}>
<Grid container spacing={3} sx={{ maxWidth: 'fit-content' }}>
<Grid item xs={12}>
<PoInfoCard po={purchaseOrder} />
</Grid>
<Grid item xs={12}>
{true ? (
<FormProvider {...dnFormProps}>
<Stack component={"form"} spacing={2}>
<Card sx={{ display: "block", height: "230px" }}>
<CardContent component={Stack} spacing={2}>
<Grid container spacing={2} sx={{ maxWidth: 'fit-content' }}>
<Grid item xs={12}>
<Card sx={{ display: "block" }}>
<CardContent component={Stack} spacing={2}>
<TextField
label={t("Supplier")}
fullWidth
disabled={true}
value={purchaseOrder.supplier ?? ""}
/>

<Grid container spacing={2}>
<Grid item xs={6}>
<Stack spacing={2}>
<TextField
label={t("Order Date")}
fullWidth
disabled={true}
value={arrayToDateString(purchaseOrder.orderDate as any)}
/>
<TextField
{...dnFormProps.register("dnNo")}
label={t("dnNo")}
type="text"
variant="outlined"
fullWidth
// InputProps={{
// inputProps: {
// min: 0,
// step: 1
// }
// }}
/>
</Grid>
<Grid item xs={12}>
<LocalizationProvider
dateAdapter={AdapterDayjs}
// TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
adapterLocale="zh-hk"
localeText={zhHK.components.MuiLocalizationProvider.defaultProps.localeText}
>
</Stack>
</Grid>

<Grid item xs={6}>
<Stack spacing={2}>
<TextField
label={t("ETA")}
fullWidth
disabled={true}
value={arrayToDateString(purchaseOrder.estimatedArrivalDate as any)}
/>
<LocalizationProvider
dateAdapter={AdapterDayjs}
adapterLocale="zh-hk"
localeText={zhHK.components.MuiLocalizationProvider.defaultProps.localeText}
>
<Controller
control={dnFormProps.control}
name="receiptDate"
render={({ field }) => (
<DatePicker
label={t("receiptDate")}
format={`${OUTPUT_DATE_FORMAT}`}
defaultValue={dateStringToDayjs(field.value)}
onChange={(newValue: Dayjs | null) => {
handleDatePickerChange(newValue, field.onChange)
}}
slotProps={{ textField: { fullWidth: true }}}
/>
<DatePicker
label={t("receiptDate")}
format={`${OUTPUT_DATE_FORMAT}`}
defaultValue={dateStringToDayjs(field.value)}
onChange={(newValue: Dayjs | null) => {
handleDatePickerChange(newValue, field.onChange);
}}
slotProps={{ textField: { fullWidth: true } }}
/>
)}
/>
</LocalizationProvider>
</Grid>
{/* <TextField
label={t("dnDate")}
type="text"
variant="outlined"
defaultValue={"11/08/2025"}
fullWidth
/> */}
/>
</LocalizationProvider>
</Stack>
</Grid>
{/* <Button variant="contained" onClick={onOpenScanner} fullWidth>
提交
</Button> */}
</Grid>
</CardContent>
</Card>
</FormProvider>
</Grid>
<Grid item xs={12}>
<Grid container spacing={2} alignItems="stretch">
<Grid item xs={6} sx={{ display: "flex" }}>
<Card sx={{ display: "block", flex: 1 }}>
<CardContent component={Stack} spacing={2}>
<Typography variant="h6">列印</Typography>
<Autocomplete
disableClearable
options={printerCombo}
value={selectedPrinter}
onChange={(_event, value) => setSelectedPrinter(value)}
renderInput={(params) => (
<TextField
{...params}
variant="outlined"
label={t("Printer")}
fullWidth
/>
)}
/>
<TextField
variant="outlined"
label={t("Print Qty")}
value={printQty}
onChange={(event) => {
const cleaned = String(event.target.value).replace(/[^0-9]/g, "");
setPrintQty(Number(cleaned || 0));
}}
fullWidth
/>
<Button
variant="contained"
onClick={openPrintDialog}
disabled={(printerCombo?.length ?? 0) <= 0}
>
選擇列印項目
</Button>
<Typography variant="caption" color="text.secondary">
只會顯示「待上架 / 已上架」的來貨記錄
</Typography>
</CardContent>
</Card>
</Stack>
</FormProvider>
) : undefined}
</Grid>
<Grid item xs={6} sx={{ display: "flex" }}>
<Card sx={{ display: "block", flex: 1 }}>
<CardContent component={Stack} spacing={2} sx={{ height: "100%" }}>
<Typography variant="h6" sx={{ visibility: "hidden" }}>
列印
</Typography>
<Button
variant="outlined"
onClick={fillTodayLotNo}
sx={{ flex: 1 }}
>
一鍵填入來貨編號(今日)
</Button>
</CardContent>
</Card>
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
@@ -1016,6 +1188,128 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => {
/> */}
</Grid>
</Stack>
<Dialog open={printDialogOpen} onClose={closePrintDialog} fullWidth maxWidth="md">
<DialogTitle>列印標籤</DialogTitle>
<DialogContent>
<Stack spacing={1.5} sx={{ mt: 1 }}>
<Stack direction="row" spacing={2} alignItems="center" flexWrap="wrap">
<FormControlLabel
control={
<Checkbox
checked={printStatusFilter.received}
onChange={(e) =>
setPrintStatusFilter((p) => ({ ...p, received: e.target.checked }))
}
/>
}
label="待上架"
/>
<FormControlLabel
control={
<Checkbox
checked={printStatusFilter.completed}
onChange={(e) =>
setPrintStatusFilter((p) => ({ ...p, completed: e.target.checked }))
}
/>
}
label="已上架"
/>
<FormControlLabel
control={
<Checkbox
checked={
eligiblePrintSils.length > 0 &&
selectedPrintSilIds.size === eligiblePrintSils.length
}
indeterminate={
selectedPrintSilIds.size > 0 &&
selectedPrintSilIds.size < eligiblePrintSils.length
}
onChange={(e) => setAllVisiblePrintSilsSelected(e.target.checked)}
/>
}
label="全選(目前篩選結果)"
/>
<Typography variant="caption" color="text.secondary">
已選擇 {selectedPrintSilIds.size} / {eligiblePrintSils.length}
</Typography>
</Stack>

<TableContainer component={Paper} variant="outlined">
<Table size="small" stickyHeader>
<TableHead>
<TableRow>
<TableCell padding="checkbox"></TableCell>
<TableCell>貨品編號</TableCell>
<TableCell>貨品名稱</TableCell>
<TableCell align="right">換算庫存數量</TableCell>
<TableCell>庫存單位</TableCell>
<TableCell>收貨日期</TableCell>
<TableCell>來貨批號</TableCell>
<TableCell>來貨狀態</TableCell>
</TableRow>
</TableHead>
<TableBody>
{eligiblePrintSils.map((sil) => {
const status = (sil.status ?? "").toLowerCase().trim();
const statusText =
status === "received" ? "待上架" : status === "completed" ? "已上架" : sil.status;
const receiptText = sil.receiptDate
? Array.isArray(sil.receiptDate)
? arrayToDateString(sil.receiptDate)
: String(sil.receiptDate)
: "-";
const stockQty = Number(sil.acceptedQty ?? 0);
const stockQtyText =
Number.isFinite(stockQty) && stockQty > 0
? decimalFormatter.format(stockQty)
: decimalFormatter.format(0);
return (
<TableRow key={sil.id} hover>
<TableCell padding="checkbox">
<Checkbox
checked={selectedPrintSilIds.has(sil.id)}
onChange={(e) => togglePrintSilSelection(sil.id, e.target.checked)}
/>
</TableCell>
<TableCell>{sil.itemNo}</TableCell>
<TableCell>{sil.itemName}</TableCell>
<TableCell align="right">{stockQtyText}</TableCell>
<TableCell>{sil.stockUomDesc || "-"}</TableCell>
<TableCell>{receiptText}</TableCell>
<TableCell>{sil.productLotNo || "-"}</TableCell>
<TableCell>{statusText}</TableCell>
</TableRow>
);
})}
{eligiblePrintSils.length === 0 && (
<TableRow>
<TableCell colSpan={8}>
<Typography variant="body2" color="text.secondary">
沒有符合條件的項目
</Typography>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={closePrintDialog} disabled={isBulkPrinting}>
取消
</Button>
<Button
variant="contained"
onClick={handleBulkPrint}
disabled={isBulkPrinting || selectedPrintSilIds.size <= 0 || !selectedPrinter}
>
{isBulkPrinting ? "列印中..." : "列印"}
</Button>
</DialogActions>
</Dialog>
{/* {itemInfo !== undefined && (
<>
<PoQcStockInModal


+ 7
- 1
src/components/PoDetail/QcStockInModal.tsx Bestand weergeven

@@ -423,7 +423,13 @@ const PoQcStockInModalVer2: React.FC<Props> = ({
} else {
closeHandler({}, "backdropClick");
}
msg("已更新來貨狀態");
msg("已更新來貨狀態", {
position:
typeof window !== "undefined" &&
window.location.pathname.startsWith("/po/edit")
? "top-end"
: "bottom-end",
});
return ;

},


+ 7
- 1
src/components/Qc/QcStockInModal.tsx Bestand weergeven

@@ -548,7 +548,13 @@ const QcStockInModal: React.FC<Props> = ({
closeWithResult(qcRes);
}
setIsSubmitting(false);
msg("已更新來貨狀態");
msg("已更新來貨狀態", {
position:
typeof window !== "undefined" &&
window.location.pathname.startsWith("/po/edit")
? "top-end"
: "bottom-end",
});
return ;

},


+ 22
- 6
src/components/Swal/CustomAlerts.tsx Bestand weergeven

@@ -8,12 +8,28 @@ export type SweetAlertConfirmButtonText = string | undefined;

type Transaction = TFunction<["translation", ...string[]], undefined>;

export const msg = (title: SweetAlertTitle) => {
type ToastPosition =
| "top"
| "top-start"
| "top-end"
| "center"
| "center-start"
| "center-end"
| "bottom"
| "bottom-start"
| "bottom-end";

type MsgOptions = {
position?: ToastPosition;
timer?: number;
};

export const msg = (title: SweetAlertTitle, options?: MsgOptions) => {
Swal.mixin({
toast: true,
position: "bottom-end",
position: options?.position ?? "bottom-end",
showConfirmButton: false,
timer: 3000,
timer: options?.timer ?? 3000,
timerProgressBar: true,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
@@ -28,12 +44,12 @@ export const msg = (title: SweetAlertTitle) => {
});
};

export const msgError = (title: SweetAlertTitle) => {
export const msgError = (title: SweetAlertTitle, options?: MsgOptions) => {
Swal.mixin({
toast: true,
position: "bottom-end",
position: options?.position ?? "bottom-end",
showConfirmButton: false,
timer: 3000,
timer: options?.timer ?? 3000,
timerProgressBar: true,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;


+ 10
- 0
src/i18n/zh/pickOrder.json Bestand weergeven

@@ -232,6 +232,7 @@
"A4 Printer": "A4 打印機",
"Label Printer": "標籤打印機",
"Please select a printer first": "請先選擇打印機",
"Please select a label printer first": "請先選擇標籤打印機",

"Lot No": "批號",
"Expiry Date": "到期日",
@@ -457,6 +458,15 @@
"Search by Truck": "搜尋貨車",
"Print DN & Label": "列印提料單和送貨單標籤",
"Print Label": "列印送貨單標籤",
"Reprint Label(s)": "補印標籤",
"Reprint DN Label": "補印送貨單標籤",
"From carton": "起始箱號",
"To carton": "結束箱號",
"Total cartons on shipment": "總箱數",
"From carton must be at least 1": "起始箱號必須至少為 1",
"To carton must be greater than or equal to from carton": "結束箱號必須大於或等於起始箱號",
"Total cartons on shipment must be at least 1": "總箱數必須至少為 1",
"To carton cannot be greater than total cartons on shipment": "結束箱號不能大於總箱數",
"Not Yet Finished Released Do Pick Orders": "未完成提料單",
"Not yet finished released do pick orders": "未完成提料單",
"Released orders not yet completed - click lane to select and assign": "未完成提料單- 點擊貨車班次選擇並分配",


Laden…
Annuleren
Opslaan