2 Коміти

Автор SHA1 Повідомлення Дата
  CANCERYS\kw093 4883ead941 Merge branch 'master' of https://git.2fi-solutions.com/derek/FPSMS-frontend 1 тиждень тому
  CANCERYS\kw093 62148f2765 pickordersearch 1 тиждень тому
15 змінених файлів з 4754 додано та 86 видалено
  1. +87
    -0
      src/app/api/pickOrder/actions.ts
  2. +179
    -0
      src/components/PickOrderSearch/EscalationComponent.tsx
  3. +795
    -0
      src/components/PickOrderSearch/PickExecution.tsx
  4. +172
    -86
      src/components/PickOrderSearch/PickOrderSearch.tsx
  5. +288
    -0
      src/components/PickOrderSearch/PickQcStockInModalVer2.tsx
  6. +335
    -0
      src/components/PickOrderSearch/PickQcStockInModalVer3.tsx
  7. +527
    -0
      src/components/PickOrderSearch/PutawayForm.tsx
  8. +395
    -0
      src/components/PickOrderSearch/QCDatagrid.tsx
  9. +460
    -0
      src/components/PickOrderSearch/QcFormVer2.tsx
  10. +78
    -0
      src/components/PickOrderSearch/QcSelect.tsx
  11. +321
    -0
      src/components/PickOrderSearch/StockInFormVer2.tsx
  12. +24
    -0
      src/components/PickOrderSearch/TwoLineCell.tsx
  13. +78
    -0
      src/components/PickOrderSearch/dummyQcTemplate.tsx
  14. +635
    -0
      src/components/PickOrderSearch/newcreatitem.tsx
  15. +380
    -0
      src/components/PickOrderSearch/pickorderModelVer2.tsx

+ 87
- 0
src/app/api/pickOrder/actions.ts Переглянути файл

@@ -81,6 +81,93 @@ export interface PickOrderApprovalInput {
rejectQty: number;
status: string;
}


export interface GetPickOrderInfoResponse {
pickOrders: GetPickOrderInfo[];
items: CurrentInventoryItemInfo[];
}

export interface GetPickOrderInfo {
id: number;
code: string;
targetDate: string;
type: string;
status: string;
pickOrderLines: GetPickOrderLineInfo[];
}

export interface GetPickOrderLineInfo {
id: number;
itemId: number;
itemCode: string;
itemName: string;
availableQty: number;
requiredQty: number;
uomCode: string;
uomDesc: string;
suggestedList: any[];
}

export interface CurrentInventoryItemInfo {
id: number;
code: string;
name: string;
uomDesc: string;
availableQty: number;
requiredQty: number;
}


export const fetchPickOrderDetails = cache(async (ids: string) => {
return serverFetchJson<GetPickOrderInfoResponse>(
`${BASE_API_URL}/pickOrder/detail/${ids}`,
{
method: "GET",
next: { tags: ["pickorder"] },
},
);
});
export interface PickOrderLotDetailResponse {
lotId: number;
lotNo: string;
expiryDate: string;
location: string;
stockUnit: string;
availableQty: number;
requiredQty: number;
actualPickQty: number;
suggestedPickLotId: number;
lotStatus: string;
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable';
}


export const fetchAllPickOrderDetails = cache(async () => {
return serverFetchJson<GetPickOrderInfoResponse>(
`${BASE_API_URL}/pickOrder/detail`,
{
method: "GET",
next: { tags: ["pickorder"] },
},
);
});

export const fetchPickOrderLineLotDetails = cache(async (pickOrderLineId: number) => {
return serverFetchJson<PickOrderLotDetailResponse[]>(
`${BASE_API_URL}/pickOrder/lot-details/${pickOrderLineId}`,
{
method: "GET",
next: { tags: ["pickorder"] },
},
);
});






export const createPickOrder = async (data: SavePickOrderRequest) => {
console.log(data);
const po = await serverFetchJson<PostPickOrderResponse>(


+ 179
- 0
src/components/PickOrderSearch/EscalationComponent.tsx Переглянути файл

@@ -0,0 +1,179 @@
import React, { useState, ChangeEvent, FormEvent, Dispatch } from 'react';
import {
Box,
Button,
Collapse,
FormControl,
InputLabel,
Select,
MenuItem,
TextField,
Checkbox,
FormControlLabel,
Paper,
Typography,
RadioGroup,
Radio,
Stack,
Autocomplete,
} from '@mui/material';
import { SelectChangeEvent } from '@mui/material/Select';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import { useTranslation } from 'react-i18next';

interface NameOption {
value: string;
label: string;
}

interface FormData {
name: string;
quantity: string;
message: string;
}

interface Props {
forSupervisor: boolean
isCollapsed: boolean
setIsCollapsed: Dispatch<React.SetStateAction<boolean>>
}
const EscalationComponent: React.FC<Props> = ({
forSupervisor,
isCollapsed,
setIsCollapsed
}) => {
const { t } = useTranslation("purchaseOrder");
const [formData, setFormData] = useState<FormData>({
name: '',
quantity: '',
message: '',
});

const nameOptions: NameOption[] = [
{ value: '', label: '請選擇姓名...' },
{ value: 'john', label: '張大明' },
{ value: 'jane', label: '李小美' },
{ value: 'mike', label: '王志強' },
{ value: 'sarah', label: '陳淑華' },
{ value: 'david', label: '林建國' },
];

const handleInputChange = (
event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | SelectChangeEvent<string>
): void => {
const { name, value } = event.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
};

const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault();
console.log('表單已提交:', formData);
// 處理表單提交
};

const handleCollapseToggle = (e: ChangeEvent<HTMLInputElement>): void => {
setIsCollapsed(e.target.checked);
};

return (
// <Paper elevation={3} sx={{ maxWidth: 400, mx: 'auto', p: 3 }}>
<>
<Paper>
{/* <Paper elevation={3} sx={{ mx: 'auto', p: 3 }}> */}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<FormControlLabel
control={
<Checkbox
checked={isCollapsed}
onChange={handleCollapseToggle}
color="primary"
/>
}
label={
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography variant="body1">上報結果</Typography>
{isCollapsed ? (
<ExpandLessIcon sx={{ ml: 1 }} />
) : (
<ExpandMoreIcon sx={{ ml: 1 }} />
)}
</Box>
}
/>
</Box>
<Collapse in={isCollapsed}>
<Box component="form" onSubmit={handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{forSupervisor ? (
<FormControl>
<RadioGroup
row
aria-labelledby="demo-radio-buttons-group-label"
defaultValue="pass"
name="radio-buttons-group"
>
<FormControlLabel value="pass" control={<Radio />} label="合格" />
<FormControlLabel value="fail" control={<Radio />} label="不合格" />
</RadioGroup>
</FormControl>
): undefined}
<FormControl fullWidth>
<select
id="name"
name="name"
value={formData.name}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
>
{nameOptions.map((option: NameOption) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</FormControl>
<TextField
fullWidth
id="quantity"
name="quantity"
label="數量"
type="number"
value={formData.quantity}
onChange={handleInputChange}
InputProps={{ inputProps: { min: 1 } }}
placeholder="請輸入數量"
/>

<TextField
fullWidth
id="message"
name="message"
label="備註"
multiline
rows={4}
value={formData.message}
onChange={handleInputChange}
placeholder="請輸入您的備註"
/>

<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
type="submit"
variant="contained"
color="primary"
>
{t("update qc info")}
</Button>
</Stack>
</Box>
</Collapse>
</Paper>
</>
);
}

export default EscalationComponent;

+ 795
- 0
src/components/PickOrderSearch/PickExecution.tsx Переглянути файл

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

import {
Autocomplete,
Box,
Button,
CircularProgress,
FormControl,
Grid,
Paper,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
Typography,
Checkbox,
FormControlLabel,
Select,
MenuItem,
InputLabel,
} from "@mui/material";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import {
ByItemsSummary,
ConsoPickOrderResult,
PickOrderLine,
PickOrderResult,
} from "@/app/api/pickOrder";
import { useRouter } from "next/navigation";
import { GridInputRowSelectionModel } from "@mui/x-data-grid";
import {
fetchConsoDetail,
fetchConsoPickOrderClient,
releasePickOrder,
ReleasePickOrderInputs,
fetchPickOrderDetails,
fetchAllPickOrderDetails,
GetPickOrderInfoResponse,
GetPickOrderLineInfo,
} from "@/app/api/pickOrder/actions";
import { EditNote } from "@mui/icons-material";
import { fetchNameList, NameList } from "@/app/api/user/actions";
import {
FormProvider,
SubmitErrorHandler,
SubmitHandler,
useForm,
} from "react-hook-form";
import { pickOrderStatusMap } from "@/app/utils/formatUtil";
import { QcItemWithChecks } from "@/app/api/qc";
import { fetchQcItemCheck, fetchPickOrderQcResult } from "@/app/api/qc/actions";

import { PurchaseQcResult } from "@/app/api/po/actions";
import PickQcStockInModalVer2 from "./PickQcStockInModalVer3";
import { fetchPickOrderLineLotDetails, PickOrderLotDetailResponse } from "@/app/api/pickOrder/actions";
import SearchResults, { Column } from "../SearchResults/SearchResults";
import { defaultPagingController } from "../SearchResults/SearchResults";
interface Props {
filterArgs: Record<string, any>;
}


interface LotPickData {
id: number;
lotId: number;
lotNo: string;
expiryDate: string;
location: string;
stockUnit: string;
availableQty: number;
requiredQty: number;
actualPickQty: number;
lotStatus: string;
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable';
}

interface PickQtyData {
[lineId: number]: {
[lotId: number]: number;
};
}

const PickExecution: React.FC<Props> = ({ filterArgs }) => {
const { t } = useTranslation("pickOrder");
const router = useRouter();
const [filteredPickOrders, setFilteredPickOrders] = useState(
[] as ConsoPickOrderResult[],
);
const [isLoading, setIsLoading] = useState(false);
const [selectedConsoCode, setSelectedConsoCode] = useState<string | undefined>();
const [revertIds, setRevertIds] = useState<GridInputRowSelectionModel>([]);
const [totalCount, setTotalCount] = useState<number>();
const [usernameList, setUsernameList] = useState<NameList[]>([]);

const [byPickOrderRows, setByPickOrderRows] = useState<
Omit<PickOrderResult, "items">[] | undefined
>(undefined);
const [byItemsRows, setByItemsRows] = useState<ByItemsSummary[] | undefined>(
undefined,
);
const [disableRelease, setDisableRelease] = useState<boolean>(true);
const [selectedRowId, setSelectedRowId] = useState<number | null>(null);

const [pickOrderDetails, setPickOrderDetails] = useState<GetPickOrderInfoResponse | null>(null);
const [detailLoading, setDetailLoading] = useState(false);
const [pickQtyData, setPickQtyData] = useState<PickQtyData>({});
const [lotData, setLotData] = useState<LotPickData[]>([]);

const [qcItems, setQcItems] = useState<QcItemWithChecks[]>([]);
const [qcModalOpen, setQcModalOpen] = useState(false);
const [selectedItemForQc, setSelectedItemForQc] = useState<GetPickOrderLineInfo & {
pickOrderCode: string;
qcResult?: PurchaseQcResult[];
} | null>(null);

const formProps = useForm<ReleasePickOrderInputs>();
const errors = formProps.formState.errors;

const onDetailClick = useCallback(
(pickOrder: any) => {
console.log(pickOrder);
const status = pickOrder.status;
if (pickOrderStatusMap[status] >= 3) {
router.push(`/pickOrder/detail?consoCode=${pickOrder.consoCode}`);
} else {
setSelectedConsoCode(pickOrder.consoCode);
}
},
[router],
);

const fetchNewPageConsoPickOrder = useCallback(
async (
pagingController: Record<string, number>,
filterArgs: Record<string, number>,
) => {
setIsLoading(true);
const params = {
...pagingController,
...filterArgs,
};
const res = await fetchConsoPickOrderClient(params);
if (res) {
console.log(res);
setFilteredPickOrders(res.records);
setTotalCount(res.total);
}
setIsLoading(false);
},
[],
);

useEffect(() => {
fetchNewPageConsoPickOrder({ limit: 10, offset: 0 }, filterArgs);
}, [fetchNewPageConsoPickOrder, filterArgs]);

const isReleasable = useCallback((itemList: ByItemsSummary[]): boolean => {
let isReleasable = true;
for (const item of itemList) {
isReleasable = item.requiredQty >= item.availableQty;
if (!isReleasable) return isReleasable;
}
return isReleasable;
}, []);

const fetchConso = useCallback(
async (consoCode: string) => {
const res = await fetchConsoDetail(consoCode);
const nameListRes = await fetchNameList();
if (res) {
console.log(res);
setByPickOrderRows(res.pickOrders);
setByItemsRows(res.items);
setDisableRelease(isReleasable(res.items));
} else {
console.log("error");
console.log(res);
}
if (nameListRes) {
console.log(nameListRes);
setUsernameList(nameListRes);
}
},
[isReleasable],
);

const handleFetchAllPickOrderDetails = useCallback(async () => {
setDetailLoading(true);
try {
const data = await fetchAllPickOrderDetails();
setPickOrderDetails(data);
console.log("All Pick Order Details:", data);
const initialPickQtyData: PickQtyData = {};
data.pickOrders.forEach((pickOrder: any) => {
pickOrder.pickOrderLines.forEach((line: any) => {
initialPickQtyData[line.id] = {};
});
});
setPickQtyData(initialPickQtyData);
} catch (error) {
console.error("Error fetching all pick order details:", error);
} finally {
setDetailLoading(false);
}
}, []);

useEffect(() => {
handleFetchAllPickOrderDetails();
}, [handleFetchAllPickOrderDetails]);

const onChange = useCallback(
(event: React.SyntheticEvent, newValue: NameList) => {
console.log(newValue);
formProps.setValue("assignTo", newValue.id);
},
[formProps],
);

const onSubmit = useCallback<SubmitHandler<ReleasePickOrderInputs>>(
async (data, event) => {
console.log(data);
try {
const res = await releasePickOrder(data);
console.log(res);
if (res.consoCode.length > 0) {
console.log(res);
router.push(`/pickOrder/detail?consoCode=${res.consoCode}`);
} else {
console.log(res);
}
} catch (error) {
console.log(error);
}
},
[router],
);
const onSubmitError = useCallback<SubmitErrorHandler<ReleasePickOrderInputs>>(
(errors) => {},
[],
);

const handleConsolidate_revert = useCallback(() => {
console.log(revertIds);
}, [revertIds]);

useEffect(() => {
if (selectedConsoCode) {
fetchConso(selectedConsoCode);
formProps.setValue("consoCode", selectedConsoCode);
}
}, [selectedConsoCode, fetchConso, formProps]);

const handlePickQtyChange = useCallback((lineId: number, lotId: number, value: number) => {
setPickQtyData(prev => ({
...prev,
[lineId]: {
...prev[lineId],
[lotId]: value
}
}));
}, []);

const handleSubmitPickQty = useCallback((lineId: number, lotId: number) => {
const qty = pickQtyData[lineId]?.[lotId] || 0;
console.log(`提交拣货数量: Line ${lineId}, Lot ${lotId}, Qty ${qty}`);
}, [pickQtyData]);

const getTotalPickedQty = useCallback((lineId: number) => {
const lineData = pickQtyData[lineId];
if (!lineData) return 0;
return Object.values(lineData).reduce((sum, qty) => sum + qty, 0);
}, [pickQtyData]);

const handleInsufficientStock = useCallback(() => {
console.log("Insufficient stock - need to pick another lot");
alert("Insufficient stock - need to pick another lot");
}, []);

const handleQcCheck = useCallback(async (line: GetPickOrderLineInfo, pickOrderCode: string) => {
console.log("QC Check clicked for:", line, pickOrderCode);
try {
const qcItemsData = await fetchQcItemCheck(line.itemId);
setQcItems(qcItemsData);
console.log("QC Items:", qcItemsData);
let qcResult: PurchaseQcResult[] = [];
try {
qcResult = await fetchPickOrderQcResult(line.id);
console.log("QC Result:", qcResult);
} catch (error) {
console.log("No existing QC result found");
}
setSelectedItemForQc({
...line,
pickOrderCode,
qcResult
});
setQcModalOpen(true);
console.log("QC Modal should open now");
} catch (error) {
console.error("Error fetching QC data:", error);
}
}, []);

const handleCloseQcModal = useCallback(() => {
console.log("Closing QC modal");
setQcModalOpen(false);
setSelectedItemForQc(null);
}, []);

const handleSetItemDetail = useCallback((item: any) => {
setSelectedItemForQc(item);
}, []);

const renderMainTableRow = useCallback((line: GetPickOrderLineInfo, pickOrderCode: string) => {
const handleRowSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
setSelectedRowId(line.id);
try {
const lotDetails = await fetchPickOrderLineLotDetails(line.id);
console.log("Lot details from API:", lotDetails);
const realLotData: LotPickData[] = lotDetails.map((lot: any) => ({
id: lot.lotId, // Add id here
lotId: lot.lotId,
lotNo: lot.lotNo,
expiryDate: lot.expiryDate ? new Date(lot.expiryDate).toLocaleDateString() : 'N/A',
location: lot.location,
stockUnit: lot.stockUnit,
availableQty: lot.availableQty,
requiredQty: lot.requiredQty,
actualPickQty: lot.actualPickQty || 0,
lotStatus: lot.lotStatus,
lotAvailability: lot.lotAvailability
}));
setLotData(realLotData);
} catch (error) {
console.error("Error fetching lot details:", error);
setLotData([]);
}
} else {
setSelectedRowId(null);
setLotData([]);
}
};
// Calculate Balance to Pick (availableQty - requiredQty)
const balanceToPick = line.availableQty - line.requiredQty;
const totalPickedQty = getTotalPickedQty(line.id);

return (
<TableRow
key={line.id}
sx={{
"& > *": { borderBottom: "unset" },
color: "black",
backgroundColor: selectedRowId === line.id ? "action.selected" : "inherit",
cursor: "pointer",
"&:hover": {
backgroundColor: "action.hover",
},
}}
>
<TableCell align="center" sx={{ width: "60px" }}>
<Checkbox
checked={selectedRowId === line.id}
onChange={handleRowSelect}
onClick={(e) => e.stopPropagation()}
/>
</TableCell>
<TableCell align="left">{pickOrderCode}</TableCell>
<TableCell align="left">{line.itemCode}</TableCell>
<TableCell align="left">{line.itemName}</TableCell>
<TableCell align="right">{line.requiredQty}</TableCell>
<TableCell align="right" sx={{
//color: balanceToPick >= 0 ? 'success.main' : 'error.main',
//fontWeight: 'bold'
}}>
{balanceToPick}
</TableCell>
<TableCell align="right">{totalPickedQty}</TableCell>
</TableRow>
);
}, [selectedRowId, getTotalPickedQty]);

const selectedRow = useMemo(() => {
if (!selectedRowId || !pickOrderDetails) return null;
// 在所有 pick order lines 中查找选中的行
for (const pickOrder of pickOrderDetails.pickOrders) {
const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId);
if (foundLine) {
return { ...foundLine, pickOrderCode: pickOrder.code };
}
}
return null;
}, [selectedRowId, pickOrderDetails]);

// Add these state variables after the existing useState declarations
const [area4PagingController, setArea4PagingController] = useState(defaultPagingController);
const [area5PagingController, setArea5PagingController] = useState(defaultPagingController);
const [selectedLotIds, setSelectedLotIds] = useState<number[]>([]);

// Add these helper functions
const getPaginatedData = useCallback((data: any[], pagingController: any) => {
const startIndex = pagingController.pageNum * pagingController.pageSize;
const endIndex = startIndex + pagingController.pageSize;
return data.slice(startIndex, endIndex);
}, []);

const getTotalPages = useCallback((totalCount: number, pageSize: number) => {
return Math.ceil(totalCount / pageSize);
}, []);

// Add this useEffect to reset pagination when data changes
useEffect(() => {
setArea4PagingController({ pageNum: 0, pageSize: 10 });
}, [pickOrderDetails]);

useEffect(() => {
setArea5PagingController({ pageNum: 0, pageSize: 10 });
}, [lotData]);

// Add this function to handle row selection
const handleRowSelect = useCallback(async (lineId: number) => {
setSelectedRowId(lineId);
// Get real lot data
try {
const lotDetails = await fetchPickOrderLineLotDetails(lineId);
console.log("Lot details from API:", lotDetails);
const realLotData: LotPickData[] = lotDetails.map((lot: any) => ({
id: lot.lotId, // Add this line
lotId: lot.lotId,
lotNo: lot.lotNo,
expiryDate: lot.expiryDate ? new Date(lot.expiryDate).toLocaleDateString() : 'N/A',
location: lot.location,
stockUnit: lot.stockUnit,
availableQty: lot.availableQty,
requiredQty: lot.requiredQty,
actualPickQty: lot.actualPickQty || 0,
lotStatus: lot.lotStatus,
lotAvailability: lot.lotAvailability
}));
setLotData(realLotData);
} catch (error) {
console.error("Error fetching lot details:", error);
setLotData([]);
}
}, []);

const prepareArea4Data = useMemo(() => {
if (!pickOrderDetails) return [];
return pickOrderDetails.pickOrders.flatMap((pickOrder) =>
pickOrder.pickOrderLines.map((line) => ({
...line,
pickOrderCode: pickOrder.code,
balanceToPick: line.availableQty - line.requiredQty,
}))
);
}, [pickOrderDetails]);

const prepareArea5Data = useMemo(() => {
return lotData.map((lot) => ({
...lot,
id: lot.lotId, // Add id field for SearchResults
}));
}, [lotData]);

const area4Columns = useMemo<Column<GetPickOrderLineInfo & { pickOrderCode: string }>[]>(
() => [
{
name: "select",
label: "",
type: "checkbox",
disabled: () => false, // Allow all rows to be selectable
},
{
name: "pickOrderCode",
label: t("Pick Order#"),
},
{
name: "itemCode",
label: t("Item Code"),
},
{
name: "itemName",
label: t("Item Description"),
},
{
name: "requiredQty",
label: t("Required Qty"),
},
{
name: "balanceToPick",
label: t("Balance to Pick"),
renderCell: (params) => {
const balanceToPick = params.availableQty - params.requiredQty;
return (
<Typography
sx={{
color: balanceToPick >= 0 ? 'success.main' : 'error.main',
//fontWeight: 'bold'
}}
>
{balanceToPick}
</Typography>
);
},
},
{
name: "qtyAlreadyPicked",
label: t("Qty Already Picked"),
renderCell: (params) => {
return getTotalPickedQty(params.id);
},
},
],
[t, getTotalPickedQty],
);

const area5Columns = useMemo<Column<LotPickData>[]>(
() => [
{
name: "id", // Use "id" instead of "select"
label: "",
type: "checkbox",
disabled: () => false,
},
{
name: "lotNo",
label: t("Lot#"),
renderCell: (params) => (
<Box>
<Typography>{params.lotNo}</Typography>
{params.lotAvailability !== 'available' && (
<Typography variant="caption" color="error" display="block">
({params.lotAvailability === 'expired' ? 'Expired' :
params.lotAvailability === 'insufficient_stock' ? 'Insufficient' :
'Unavailable'})
</Typography>
)}
</Box>
),
},
{
name: "expiryDate",
label: t("Lot ExpiryDate"),
},
{
name: "location",
label: t("Lot Location"),
},
{
name: "stockUnit",
label: t("Stock Unit"),
},
{
name: "availableQty",
label: t("Available Lot"),
align: "right",
},
{
name: "requiredQty",
label: t("Lot Required Pick Qty"),
align: "right",
},
{
name: "actualPickQty",
label: t("Lot Actual Pick Qty"),
align: "right",
renderCell: (params) => (
<TextField
type="number"
value={selectedRowId ? (pickQtyData[selectedRowId]?.[params.lotId] || 0) : 0}
onChange={(e) => {
if (selectedRowId) {
handlePickQtyChange(
selectedRowId,
params.lotId,
parseInt(e.target.value) || 0
);
}
}}
inputProps={{ min: 0, max: params.availableQty }}
disabled={params.lotAvailability !== 'available'}
sx={{ width: '80px' }}
/>
),
},
{
name: "lotId",
label: t("Submit"),
align: "center",
renderCell: (params) => (
<Button
variant="contained"
onClick={() => {
if (selectedRowId) {
handleSubmitPickQty(selectedRowId, params.lotId);
}
}}
disabled={params.lotAvailability !== 'available' || !pickQtyData[selectedRowId!]?.[params.lotId]}
sx={{
fontSize: '0.75rem',
py: 0.5,
minHeight: '28px'
}}
>
{t("Submit")}
</Button>
),
},
],
[t, selectedRowId, pickQtyData, handlePickQtyChange, handleSubmitPickQty],
);

return (
<FormProvider {...formProps}>
<Stack spacing={2}>
{/* Area 4 & 5: Main Table and Detail Side by Side */}
<Grid container spacing={2} sx={{ height: '100%', flex: 1 }}>
{/* Area 4: Main Table */}
<Grid item xs={6} sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Typography variant="h6" gutterBottom>
{t("Pick Order Details")}
</Typography>
{detailLoading ? (
<Box display="flex" justifyContent="center" alignItems="center" flex={1}>
<CircularProgress size={40} />
</Box>
) : pickOrderDetails ? (
<>
<SearchResults<GetPickOrderLineInfo & { pickOrderCode: string }>
items={prepareArea4Data}
columns={area4Columns}
pagingController={area4PagingController}
setPagingController={setArea4PagingController}
totalCount={prepareArea4Data.length}
checkboxIds={selectedRowId ? [selectedRowId] : []}
setCheckboxIds={(ids) => {
const newSelectedId = ids[0] as number || null;
setSelectedRowId(newSelectedId);
// Handle lot data fetching when selection changes
if (newSelectedId) {
handleRowSelect(newSelectedId);
} else {
setLotData([]);
}
}}
/>
</>
) : (
<Box display="flex" justifyContent="center" alignItems="center" flex={1}>
<Typography variant="body2" color="text.secondary">
正在載入數據...
</Typography>
</Box>
)}
</Grid>

{/* Area 5: Item lot to be Pick */}
<Grid item xs={6} sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
{selectedRow && lotData.length > 0 ? (
<>
<Typography variant="h6" gutterBottom>
Item lot to be Pick: {selectedRow.pickOrderCode} - {selectedRow.itemName}
</Typography>
<SearchResults<LotPickData>
items={prepareArea5Data}
columns={area5Columns}
pagingController={area5PagingController}
setPagingController={setArea5PagingController}
totalCount={prepareArea5Data.length}
checkboxIds={selectedLotIds}
setCheckboxIds={(ids) => {
setSelectedLotIds(Array.isArray(ids) ? ids as number[] : []);
}}
/>
{/* Action buttons below the table */}
<Box sx={{ mt: 2 }}>
<Stack direction="row" spacing={1}>
<Button
variant="contained"
onClick={() => {
if (selectedRowId && selectedRow) {
handleQcCheck(selectedRow, selectedRow.pickOrderCode);
}
}}
sx={{ whiteSpace: 'nowrap' }}
>
{t("Qc Check")}
</Button>
<Button
variant="contained"
onClick={() => handleInsufficientStock()}
sx={{ whiteSpace: 'nowrap' }}
>
{t("Insufficient Stock & Pick Another Lot")}
</Button>
</Stack>
</Box>
</>
) : (
<Box display="flex" justifyContent="center" alignItems="center" flex={1}>
<Typography variant="h6" color="text.secondary">
Please select an pick order lot to view lot information
</Typography>
</Box>
)}
</Grid>
</Grid>

{/* Area 6: Action Buttons */}
{selectedRow && (
<Grid container>
<Grid item xs={12} display="flex" justifyContent="end" alignItems="end">
<Button
disabled={(revertIds as number[]).length < 1}
variant="outlined"
onClick={handleConsolidate_revert}
sx={{ mr: 1 }}
>
{t("remove")}
</Button>
<Button
disabled={disableRelease}
variant="contained"
onClick={formProps.handleSubmit(onSubmit, onSubmitError)}
>
{t("release")}
</Button>
</Grid>
</Grid>
)}

{/* QC Modal */}
{selectedItemForQc && qcModalOpen && (
<PickQcStockInModalVer2
open={qcModalOpen}
onClose={handleCloseQcModal}
itemDetail={selectedItemForQc}
setItemDetail={handleSetItemDetail}
qc={qcItems}
warehouse={[]}
/>
)}
</Stack>
</FormProvider>
);
};

export default PickExecution;

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

@@ -18,7 +18,9 @@ import {
import { Button, Grid, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material";
import PickOrders from "./PickOrders";
import ConsolidatedPickOrders from "./ConsolidatedPickOrders";
import PickExecution from "./PickExecution";
import CreatePickOrderModal from "./CreatePickOrderModal";
import NewCreateItem from "./newcreatitem";
import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions";
import { fetchPickOrderClient } from "@/app/api/pickOrder/actions";

@@ -39,6 +41,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
const [items, setItems] = useState<ItemCombo[]>([])
const [filteredPickOrders, setFilteredPickOrders] = useState(pickOrders);
const [filterArgs, setFilterArgs] = useState<Record<string, any>>({});
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
const [tabIndex, setTabIndex] = useState(0);
const [totalCount, setTotalCount] = useState<number>();

@@ -48,6 +51,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
},
[],
);
const openCreateModal = useCallback(async () => {
console.log("testing")
const res = await fetchAllItemsInClient()
@@ -60,69 +64,123 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
setIsOpenCreateModal(false)
}, [])

useEffect(() => {
if (tabIndex === 3) {
const loadItems = async () => {
try {
const itemsData = await fetchAllItemsInClient();
console.log("PickOrderSearch loaded items:", itemsData.length);
setItems(itemsData);
} catch (error) {
console.error("Error loading items in PickOrderSearch:", error);
}
};
// 如果还没有数据,则加载
if (items.length === 0) {
loadItems();
}
}
}, [tabIndex, items.length]);

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{ label: t("Code"), paramName: "code", type: "text" },
{
label: t("Target Date From"),
label2: t("Target Date To"),
paramName: "targetDate",
type: "dateRange",
},
{
label: t("Type"),
paramName: "type",
type: "autocomplete",
options: sortBy(
uniqBy(
pickOrders.map((po) => ({
value: po.type,
label: t(upperCase(po.type)),
})),
"value",
),
"label",
),
},
{
label: t("Status"),
paramName: "status",
type: "autocomplete",
options: sortBy(
uniqBy(
pickOrders.map((po) => ({
value: po.status,
label: t(upperFirst(po.status)),
})),
"value",
),
"label",
),
},
{
label: t("Items"),
paramName: "items",
type: "autocomplete", // multiple: true,
options: uniqBy(
flatten(
sortBy(
pickOrders.map((po) =>
po.items
? po.items.map((item) => ({
value: item.name,
label: item.name,
// group: item.type
}))
: [],
() => {
const baseCriteria: Criterion<SearchParamNames>[] = [
{
label: tabIndex === 3 ? t("Item Code") : t("Code"),
paramName: "code",
type: "text"
},
{
label: t("Type"),
paramName: "type",
type: "autocomplete",
options: tabIndex === 3
?
[
{ value: "Consumable", label: t("Consumable") },
{ value: "Material", label: t("Material") },
{ value: "Product", label: t("Product") }
]
:
sortBy(
uniqBy(
pickOrders.map((po) => ({
value: po.type,
label: t(upperCase(po.type)),
})),
"value",
),
"label",
),
"label",
},
{
label: tabIndex === 3 ? t("Item Name") : t("Items"),
paramName: "items",
type: tabIndex === 3 ? "text" : "autocomplete",
options: tabIndex === 3
? []
:
uniqBy(
flatten(
sortBy(
pickOrders.map((po) =>
po.items
? po.items.map((item) => ({
value: item.name,
label: item.name,
}))
: [],
),
"label",
),
),
"value",
),
},
];

if (tabIndex === 3) {
baseCriteria.splice(1, 0, {
label: t("Target Date"),
paramName: "targetDate",
type: "date",
});
} else {
baseCriteria.splice(1, 0, {
label: t("Target Date From"),
label2: t("Target Date To"),
paramName: "targetDate",
type: "dateRange",
});
}

if (tabIndex !== 3) {
baseCriteria.splice(4, 0, {
label: t("Status"),
paramName: "status",
type: "autocomplete",
options: sortBy(
uniqBy(
pickOrders.map((po) => ({
value: po.status,
label: t(upperFirst(po.status)),
})),
"value",
),
"label",
),
"value",
),
},
],
[pickOrders, t],
});
}

return baseCriteria;
},
[pickOrders, t, tabIndex, items],
);

const fetchNewPagePickOrder = useCallback(
@@ -186,40 +244,66 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
setFilterArgs({ ...query }); // modify later
setFilteredPickOrders(
pickOrders.filter((po) => {
const poTargetDateStr = arrayToDayjs(po.targetDate);

// console.log(intersectionWith(po.items?.map(item => item.name), query.items))
return (
po.code.toLowerCase().includes(query.code.toLowerCase()) &&
(isEmpty(query.targetDate) ||
console.log("SearchBox search triggered with query:", query);
setSearchQuery({ ...query });
// when tabIndex === 3, do not execute any search logic, only store query conditions
if (tabIndex !== 3) {
// only execute search logic when other tabs
setFilterArgs({ ...query });
setFilteredPickOrders(
pickOrders.filter((po) => {
const poTargetDateStr = arrayToDayjs(po.targetDate);

// safely check search conditions
const codeMatch = !query.code ||
po.code.toLowerCase().includes((query.code || "").toLowerCase());
const dateMatch = !query.targetDate ||
poTargetDateStr.isSame(query.targetDate) ||
poTargetDateStr.isAfter(query.targetDate)) &&
(isEmpty(query.targetDateTo) ||
poTargetDateStr.isAfter(query.targetDate);
const dateToMatch = !query.targetDateTo ||
poTargetDateStr.isSame(query.targetDateTo) ||
poTargetDateStr.isBefore(query.targetDateTo)) &&
(intersectionWith(["All"], query.items).length > 0 ||
intersectionWith(
po.items?.map((item) => item.name),
query.items,
).length > 0) &&
(query.status.toLowerCase() == "all" ||
po.status
.toLowerCase()
.includes(query.status.toLowerCase())) &&
(query.type.toLowerCase() == "all" ||
po.type.toLowerCase().includes(query.type.toLowerCase()))
);
}),
);
poTargetDateStr.isBefore(query.targetDateTo);
const itemsMatch = !query.items ||
Array.isArray(query.items) && (
intersectionWith(["All"], query.items).length > 0 ||
intersectionWith(
po.items?.map((item) => item.name) || [],
query.items,
).length > 0
);
const statusMatch = !query.status ||
query.status.toLowerCase() === "all" ||
po.status.toLowerCase().includes((query.status || "").toLowerCase());
const typeMatch = !query.type ||
query.type.toLowerCase() === "all" ||
po.type.toLowerCase().includes((query.type || "").toLowerCase());

return codeMatch && dateMatch && dateToMatch && itemsMatch && statusMatch && typeMatch;
}),
);
}
// when tabIndex === 3, SearchBox's search will not trigger any filtering, only pass data to NewCreateItem
}}
onReset={() => {
console.log("SearchBox reset triggered");
setSearchQuery({});
if (tabIndex !== 3) {
onReset();
}
}}
onReset={onReset}
/>
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">

<Tab label={t("Pick Orders")} iconPosition="end" />
<Tab label={t("Consolidated Pick Orders")} iconPosition="end" />
<Tab label={t("Pick Execution")} iconPosition="end" />
<Tab label={t("Create Item")} iconPosition="end" />
</Tabs>
{tabIndex === 0 && (
<PickOrders
@@ -228,6 +312,8 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
/>
)}
{tabIndex === 1 && <ConsolidatedPickOrders filterArgs={filterArgs} />}
{tabIndex === 2 && <PickExecution filterArgs={filterArgs} />}
{tabIndex === 3 && <NewCreateItem filterArgs={filterArgs} searchQuery={searchQuery} />}
</>
);
};


+ 288
- 0
src/components/PickOrderSearch/PickQcStockInModalVer2.tsx Переглянути файл

@@ -0,0 +1,288 @@
"use client";
// 修改为 PickOrder 相关的导入
import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions";
import { QcItemWithChecks } from "@/app/api/qc";
import { PurchaseQcResult } from "@/app/api/po/actions";
import {
Box,
Button,
Grid,
Modal,
ModalProps,
Stack,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
} from "@mui/material";
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { dummyQCData, QcData } from "../PoDetail/dummyQcTemplate";
import { submitDialogWithWarning } from "../Swal/CustomAlerts";

const style = {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
bgcolor: "background.paper",
pt: 5,
px: 5,
pb: 10,
display: "block",
width: { xs: "60%", sm: "60%", md: "60%" },
};

// 修改接口定义
interface CommonProps extends Omit<ModalProps, "children"> {
itemDetail: GetPickOrderLineInfo & {
pickOrderCode: string;
qcResult?: PurchaseQcResult[]
};
setItemDetail: Dispatch<
SetStateAction<
| (GetPickOrderLineInfo & {
pickOrderCode: string;
warehouseId?: number;
})
| undefined
>
>;
qc?: QcItemWithChecks[];
warehouse?: any[];
}

interface Props extends CommonProps {
itemDetail: GetPickOrderLineInfo & {
pickOrderCode: string;
qcResult?: PurchaseQcResult[]
};
}

// 修改组件名称
const PickQcStockInModalVer2: React.FC<Props> = ({
open,
onClose,
itemDetail,
setItemDetail,
qc,
warehouse,
}) => {
console.log(warehouse);
// 修改翻译键
const {
t,
i18n: { language },
} = useTranslation("pickOrder");
const [qcItems, setQcItems] = useState(dummyQCData)
const formProps = useForm<any>({
defaultValues: {
...itemDetail,
},
});
const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>(
(...args) => {
onClose?.(...args);
},
[onClose],
);

// QC submission handler
const onSubmitQc = useCallback<SubmitHandler<any>>(
async (data, event) => {
console.log("QC Submission:", event!.nativeEvent);
// Get QC data from the shared form context
const qcAccept = data.qcAccept;
const acceptQty = data.acceptQty;
// Validate QC data
const validationErrors : string[] = [];
// Check if all QC items have results
const itemsWithoutResult = qcItems.filter(item => item.isPassed === undefined);
if (itemsWithoutResult.length > 0) {
validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.qcItem).join(', ')}`);
}

// Check if failed items have failed quantity
const failedItemsWithoutQty = qcItems.filter(item =>
item.isPassed === false && (!item.failedQty || item.failedQty <= 0)
);
if (failedItemsWithoutQty.length > 0) {
validationErrors.push(`${t("Failed items must have failed quantity")}: ${failedItemsWithoutQty.map(item => item.qcItem).join(', ')}`);
}

// Check if accept quantity is valid
if (acceptQty === undefined || acceptQty <= 0) {
validationErrors.push("Accept quantity must be greater than 0");
}

if (validationErrors.length > 0) {
console.error("QC Validation failed:", validationErrors);
alert(`未完成品檢: ${validationErrors}`);
return;
}

const qcData = {
qcAccept: qcAccept,
acceptQty: acceptQty,
qcItems: qcItems.map(item => ({
id: item.id,
qcItem: item.qcItem,
qcDescription: item.qcDescription,
isPassed: item.isPassed,
failedQty: (item.failedQty && !item.isPassed) || 0,
remarks: item.remarks || ''
}))
};

console.log("QC Data for submission:", qcData);
// await submitQcData(qcData);

if (!qcData.qcItems.every((qc) => qc.isPassed) && qcData.qcAccept) {
submitDialogWithWarning(() => {
console.log("QC accepted with failed items");
onClose();
}, t, {title:"有不合格檢查項目,確認接受收貨?", confirmButtonText: "Confirm", html: ""});
return;
}

if (qcData.qcAccept) {
console.log("QC accepted");
onClose();
} else {
console.log("QC rejected");
onClose();
}
},
[qcItems, onClose, t],
);

const handleQcItemChange = useCallback((index: number, field: keyof QcData, value: any) => {
setQcItems(prev => prev.map((item, i) =>
i === index ? { ...item, [field]: value } : item
));
}, []);

return (
<>
<FormProvider {...formProps}>
<Modal open={open} onClose={closeHandler}>
<Box
sx={{
...style,
padding: 2,
maxHeight: "90vh",
overflowY: "auto",
marginLeft: 3,
marginRight: 3,
}}
>
<Grid container justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={12}>
<Typography variant="h6" display="block" marginBlockEnd={1}>
GroupA - {itemDetail.pickOrderCode}
</Typography>
<Typography variant="body2" color="text.secondary" marginBlockEnd={2}>
記錄探測溫度的時間,請在1小時內完成出庫,以保障食品安全 監察方法、日闸檢查、嗅覺檢查和使用適當的食物温度計椒鱼食物溫度是否符合指標
</Typography>
</Grid>
{/* QC 表格 */}
<Grid item xs={12}>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>QC模板代號</TableCell>
<TableCell>檢查項目</TableCell>
<TableCell>QC Result</TableCell>
<TableCell>Failed Qty</TableCell>
<TableCell>Remarks</TableCell>
</TableRow>
</TableHead>
<TableBody>
{qcItems.map((item, index) => (
<TableRow key={item.id}>
<TableCell>{item.id}</TableCell>
<TableCell>{item.qcDescription}</TableCell>
<TableCell>
<select
value={item.isPassed === undefined ? '' : item.isPassed ? 'pass' : 'fail'}
onChange={(e) => handleQcItemChange(index, 'isPassed', e.target.value === 'pass')}
>
<option value="">Select</option>
<option value="pass">Pass</option>
<option value="fail">Fail</option>
</select>
</TableCell>
<TableCell>
<input
type="number"
value={item.failedQty || 0}
onChange={(e) => handleQcItemChange(index, 'failedQty', parseInt(e.target.value) || 0)}
disabled={item.isPassed !== false}
/>
</TableCell>
<TableCell>
<input
type="text"
value={item.remarks || ''}
onChange={(e) => handleQcItemChange(index, 'remarks', e.target.value)}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Grid>
{/* 按钮 */}
<Grid item xs={12} sx={{ mt: 2 }}>
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
variant="contained"
color="success"
onClick={formProps.handleSubmit(onSubmitQc)}
>
QC Accept
</Button>
<Button
variant="contained"
color="warning"
onClick={() => {
console.log("Sort to accept");
onClose();
}}
>
Sort to Accept
</Button>
<Button
variant="contained"
color="error"
onClick={() => {
console.log("Reject and pick another lot");
onClose();
}}
>
Reject and Pick Another Lot
</Button>
</Stack>
</Grid>
</Grid>
</Box>
</Modal>
</FormProvider>
</>
);
};

export default PickQcStockInModalVer2;

+ 335
- 0
src/components/PickOrderSearch/PickQcStockInModalVer3.tsx Переглянути файл

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

import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions";
import { QcItemWithChecks } from "@/app/api/qc";
import { PurchaseQcResult } from "@/app/api/po/actions";
import {
Box,
Button,
Grid,
Modal,
ModalProps,
Stack,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
TextField,
Radio,
RadioGroup,
FormControlLabel,
FormControl,
} from "@mui/material";
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { dummyQCData, QcData } from "../PoDetail/dummyQcTemplate";
import { submitDialogWithWarning } from "../Swal/CustomAlerts";

const style = {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
bgcolor: "background.paper",
pt: 5,
px: 5,
pb: 10,
display: "block",
width: { xs: "60%", sm: "60%", md: "60%" },
};


interface CommonProps extends Omit<ModalProps, "children"> {
itemDetail: GetPickOrderLineInfo & {
pickOrderCode: string;
qcResult?: PurchaseQcResult[]
};
setItemDetail: Dispatch<
SetStateAction<
| (GetPickOrderLineInfo & {
pickOrderCode: string;
warehouseId?: number;
})
| undefined
>
>;
qc?: QcItemWithChecks[];
warehouse?: any[];
}

interface Props extends CommonProps {
itemDetail: GetPickOrderLineInfo & {
pickOrderCode: string;
qcResult?: PurchaseQcResult[]
};
}


const PickQcStockInModalVer2: React.FC<Props> = ({
open,
onClose,
itemDetail,
setItemDetail,
qc,
warehouse,
}) => {
console.log(warehouse);
const {
t,
i18n: { language },
} = useTranslation("pickOrder");
const [qcItems, setQcItems] = useState(dummyQCData)
const formProps = useForm<any>({
defaultValues: {
...itemDetail,
},
});
const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>(
(...args) => {
onClose?.(...args);
},
[onClose],
);

// QC submission handler
const onSubmitQc = useCallback<SubmitHandler<any>>(
async (data, event) => {
console.log("QC Submission:", event!.nativeEvent);
// Get QC data from the shared form context
const qcAccept = data.qcAccept;
const acceptQty = data.acceptQty;
// Validate QC data
const validationErrors : string[] = [];
// Check if all QC items have results
const itemsWithoutResult = qcItems.filter(item => item.isPassed === undefined);
if (itemsWithoutResult.length > 0) {
validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.qcItem).join(', ')}`);
}

// Check if failed items have failed quantity
const failedItemsWithoutQty = qcItems.filter(item =>
item.isPassed === false && (!item.failedQty || item.failedQty <= 0)
);
if (failedItemsWithoutQty.length > 0) {
validationErrors.push(`${t("Failed items must have failed quantity")}: ${failedItemsWithoutQty.map(item => item.qcItem).join(', ')}`);
}

// Check if accept quantity is valid
if (acceptQty === undefined || acceptQty <= 0) {
validationErrors.push("Accept quantity must be greater than 0");
}

if (validationErrors.length > 0) {
console.error("QC Validation failed:", validationErrors);
alert(`QC failed: ${validationErrors}`);
return;
}

const qcData = {
qcAccept: qcAccept,
acceptQty: acceptQty,
qcItems: qcItems.map(item => ({
id: item.id,
qcItem: item.qcItem,
qcDescription: item.qcDescription,
isPassed: item.isPassed,
failedQty: (item.failedQty && !item.isPassed) || 0,
remarks: item.remarks || ''
}))
};

console.log("QC Data for submission:", qcData);
// await submitQcData(qcData);

if (!qcData.qcItems.every((qc) => qc.isPassed) && qcData.qcAccept) {
submitDialogWithWarning(() => {
console.log("QC accepted with failed items");
onClose?.();
}, t, {title:"有不合格檢查項目,確認接受收貨?", confirmButtonText: "Confirm", html: ""});
return;
}

if (qcData.qcAccept) {
console.log("QC accepted");
onClose?.();
} else {
console.log("QC rejected");
onClose?.();
}
},
[qcItems, onClose, t],
);

const handleQcItemChange = useCallback((index: number, field: keyof QcData, value: any) => {
setQcItems(prev => prev.map((item, i) =>
i === index ? { ...item, [field]: value } : item
));
}, []);

return (
<>
<FormProvider {...formProps}>
<Modal open={open} onClose={closeHandler}>
<Box
sx={{
...style,
padding: 2,
maxHeight: "90vh",
overflowY: "auto",
marginLeft: 3,
marginRight: 3,
}}
>
<Grid container justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={12}>
<Typography variant="h6" display="block" marginBlockEnd={1}>
GroupA - {itemDetail.pickOrderCode}
</Typography>
<Typography variant="body2" color="text.secondary" marginBlockEnd={2}>
記錄探測溫度的時間,請在1小時內完成出庫,以保障食品安全 監察方法、日闸檢查、嗅覺檢查和使用適當的食物温度計椒鱼食物溫度是否符合指標
</Typography>
</Grid>
{/* QC table - same as QcFormVer2 */}
<Grid item xs={12}>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell sx={{ width: '80px' }}>QC模板代號</TableCell>
<TableCell sx={{ width: '300px' }}>檢查項目</TableCell>
<TableCell sx={{ width: '120px' }}>QC RESULT</TableCell>
<TableCell sx={{ width: '80px' }}>FAILED QTY</TableCell>
<TableCell sx={{ width: '300px' }}>REMARKS</TableCell>
</TableRow>
</TableHead>
<TableBody>
{qcItems.map((item, index) => (
<TableRow key={item.id}>
<TableCell>{item.id}</TableCell>
<TableCell sx={{
maxWidth: '300px',
wordWrap: 'break-word',
whiteSpace: 'normal'
}}>
{item.qcDescription}
</TableCell>
<TableCell>
{/* same as QcFormVer2 */}
<FormControl>
<RadioGroup
row
aria-labelledby="demo-radio-buttons-group-label"
value={item.isPassed === undefined ? "" : (item.isPassed ? "true" : "false")}
onChange={(e) => {
const value = e.target.value;
handleQcItemChange(index, 'isPassed', value === "true");
}}
name={`isPassed-${item.id}`}
>
<FormControlLabel
value="true"
control={<Radio size="small" />}
label="合格"
sx={{
color: item.isPassed === true ? "green" : "inherit",
"& .Mui-checked": {color: "green"}
}}
/>
<FormControlLabel
value="false"
control={<Radio size="small" />}
label="不合格"
sx={{
color: item.isPassed === false ? "red" : "inherit",
"& .Mui-checked": {color: "red"}
}}
/>
</RadioGroup>
</FormControl>
</TableCell>
<TableCell>
<TextField
type="number"
size="small"
value={!item.isPassed ? (item.failedQty ?? 0) : 0}
disabled={item.isPassed}
onChange={(e) => {
const v = e.target.value;
const next = v === '' ? undefined : Number(v);
if (Number.isNaN(next)) return;
handleQcItemChange(index, 'failedQty', next);
}}
inputProps={{ min: 0 }}
sx={{ width: '60px' }}
/>
</TableCell>
<TableCell>
<TextField
size="small"
value={item.remarks ?? ''}
onChange={(e) => {
const remarks = e.target.value;
handleQcItemChange(index, 'remarks', remarks);
}}
sx={{ width: '280px' }}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Grid>
{/* buttons */}
<Grid item xs={12} sx={{ mt: 2 }}>
<Stack direction="row" justifyContent="flex-start" gap={1}>
<Button
variant="contained"
onClick={formProps.handleSubmit(onSubmitQc)}
>
QC Accept
</Button>
<Button
variant="contained"
onClick={() => {
console.log("Sort to accept");
onClose?.();
}}
>
Sort to Accept
</Button>
<Button
variant="contained"
onClick={() => {
console.log("Reject and pick another lot");
onClose?.();
}}
>
Reject and Pick Another Lot
</Button>
</Stack>
</Grid>
</Grid>
</Box>
</Modal>
</FormProvider>
</>
);
};

export default PickQcStockInModalVer2;

+ 527
- 0
src/components/PickOrderSearch/PutawayForm.tsx Переглянути файл

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

import { PurchaseQcResult, PutawayInput, PutawayLine } from "@/app/api/po/actions";
import {
Autocomplete,
Box,
Button,
Card,
CardContent,
FormControl,
Grid,
Modal,
ModalProps,
Stack,
TextField,
Tooltip,
Typography,
} from "@mui/material";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import StyledDataGrid from "../StyledDataGrid";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
GridColDef,
GridRowIdGetter,
GridRowModel,
useGridApiContext,
GridRenderCellParams,
GridRenderEditCellParams,
useGridApiRef,
} from "@mui/x-data-grid";
import InputDataGrid from "../InputDataGrid";
import { TableRow } from "../InputDataGrid/InputDataGrid";
import TwoLineCell from "./TwoLineCell";
import QcSelect from "./QcSelect";
import { QcItemWithChecks } from "@/app/api/qc";
import { GridEditInputCell } from "@mui/x-data-grid";
import { StockInLine } from "@/app/api/po";
import { WarehouseResult } from "@/app/api/warehouse";
import {
OUTPUT_DATE_FORMAT,
stockInLineStatusMap,
} from "@/app/utils/formatUtil";
import { QRCodeSVG } from "qrcode.react";
import { QrCode } from "../QrCode";
import ReactQrCodeScanner, {
ScannerConfig,
} from "../ReactQrCodeScanner/ReactQrCodeScanner";
import { QrCodeInfo } from "@/app/api/qrcode";
import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider";
import dayjs from "dayjs";
import arraySupport from "dayjs/plugin/arraySupport";
import { dummyPutawayLine } from "./dummyQcTemplate";
dayjs.extend(arraySupport);

interface Props {
itemDetail: StockInLine;
warehouse: WarehouseResult[];
disabled: boolean;
// qc: QcItemWithChecks[];
}
type EntryError =
| {
[field in keyof PutawayLine]?: string;
}
| undefined;

type PutawayRow = TableRow<Partial<PutawayLine>, EntryError>;

const style = {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
bgcolor: "background.paper",
pt: 5,
px: 5,
pb: 10,
width: "auto",
};

const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse, disabled }) => {
const { t } = useTranslation("purchaseOrder");
const apiRef = useGridApiRef();
const {
register,
formState: { errors, defaultValues, touchedFields },
watch,
control,
setValue,
getValues,
reset,
resetField,
setError,
clearErrors,
} = useFormContext<PutawayInput>();
console.log(itemDetail);
// const [recordQty, setRecordQty] = useState(0);
const [warehouseId, setWarehouseId] = useState(itemDetail.defaultWarehouseId);
const filteredWarehouse = useMemo(() => {
// do filtering here if any
return warehouse;
}, []);

const defaultOption = {
value: 0, // think think sin
label: t("Select warehouse"),
group: "default",
};
const options = useMemo(() => {
return [
// {
// value: 0, // think think sin
// label: t("Select warehouse"),
// group: "default",
// },
...filteredWarehouse.map((w) => ({
value: w.id,
label: `${w.code} - ${w.name}`,
group: "existing",
})),
];
}, [filteredWarehouse]);
const currentValue =
warehouseId > 0
? options.find((o) => o.value === warehouseId)
: options.find((o) => o.value === getValues("warehouseId")) ||
defaultOption;

const onChange = useCallback(
(
event: React.SyntheticEvent,
newValue: { value: number; group: string } | { value: number }[],
) => {
const singleNewVal = newValue as {
value: number;
group: string;
};
console.log(singleNewVal);
console.log("onChange");
// setValue("warehouseId", singleNewVal.value);
setWarehouseId(singleNewVal.value);
},
[],
);
console.log(watch("putawayLine"))
// const accQty = watch("acceptedQty");
// const validateForm = useCallback(() => {
// console.log(accQty);
// if (accQty > itemDetail.acceptedQty) {
// setError("acceptedQty", {
// message: `acceptedQty must not greater than ${itemDetail.acceptedQty}`,
// type: "required",
// });
// }
// if (accQty < 1) {
// setError("acceptedQty", {
// message: `minimal value is 1`,
// type: "required",
// });
// }
// if (isNaN(accQty)) {
// setError("acceptedQty", {
// message: `value must be a number`,
// type: "required",
// });
// }
// }, [accQty]);

// useEffect(() => {
// clearErrors();
// validateForm();
// }, [validateForm]);

const qrContent = useMemo(
() => ({
stockInLineId: itemDetail.id,
itemId: itemDetail.itemId,
lotNo: itemDetail.lotNo,
// warehouseId: 2 // for testing
// expiryDate: itemDetail.expiryDate,
// productionDate: itemDetail.productionDate,
// supplier: itemDetail.supplier,
// poCode: itemDetail.poCode,
}),
[itemDetail],
);
const [isOpenScanner, setOpenScanner] = useState(false);

const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>(
(...args) => {
setOpenScanner(false);
},
[],
);

const onOpenScanner = useCallback(() => {
setOpenScanner(true);
}, []);

const onCloseScanner = useCallback(() => {
setOpenScanner(false);
}, []);
const scannerConfig = useMemo<ScannerConfig>(
() => ({
onUpdate: (err, result) => {
console.log(result);
console.log(Boolean(result));
if (result) {
const data: QrCodeInfo = JSON.parse(result.getText());
console.log(data);
if (data.warehouseId) {
console.log(data.warehouseId);
setWarehouseId(data.warehouseId);
onCloseScanner();
}
} else return;
},
}),
[onCloseScanner],
);

// QR Code Scanner
const scanner = useQrCodeScannerContext();
useEffect(() => {
if (isOpenScanner) {
scanner.startScan();
} else if (!isOpenScanner) {
scanner.stopScan();
}
}, [isOpenScanner]);

useEffect(() => {
if (scanner.values.length > 0) {
console.log(scanner.values[0]);
const data: QrCodeInfo = JSON.parse(scanner.values[0]);
console.log(data);
if (data.warehouseId) {
console.log(data.warehouseId);
setWarehouseId(data.warehouseId);
onCloseScanner();
}
scanner.resetScan();
}
}, [scanner.values]);

useEffect(() => {
setValue("status", "completed");
setValue("warehouseId", options[0].value);
}, []);

useEffect(() => {
if (warehouseId > 0) {
setValue("warehouseId", warehouseId);
clearErrors("warehouseId");
}
}, [warehouseId]);

const getWarningTextHardcode = useCallback((): string | undefined => {
console.log(options)
if (options.length === 0) return undefined
const defaultWarehouseId = options[0].value;
const currWarehouseId = watch("warehouseId");
if (defaultWarehouseId !== currWarehouseId) {
return t("not default warehosue");
}
return undefined;
}, [options]);

const columns = useMemo<GridColDef[]>(
() => [
{
field: "qty",
headerName: t("qty"),
flex: 1,
// renderCell(params) {
// return <>100</>
// },
},
{
field: "warehouse",
headerName: t("warehouse"),
flex: 1,
// renderCell(params) {
// return <>{filteredWarehouse[0].name}</>
// },
},
{
field: "printQty",
headerName: t("printQty"),
flex: 1,
// renderCell(params) {
// return <>100</>
// },
},
], [])

const validation = useCallback(
(newRow: GridRowModel<PutawayRow>): EntryError => {
const error: EntryError = {};
const { qty, warehouseId, printQty } = newRow;

return Object.keys(error).length > 0 ? error : undefined;
},
[],
);

return (
<Grid container justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={12}>
<Typography variant="h6" display="block" marginBlockEnd={1}>
{t("Putaway Detail")}
</Typography>
</Grid>
<Grid
container
justifyContent="flex-start"
alignItems="flex-start"
spacing={2}
sx={{ mt: 0.5 }}
>
<Grid item xs={12}>
<TextField
label={t("LotNo")}
fullWidth
value={itemDetail.lotNo}
disabled
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Supplier")}
fullWidth
value={itemDetail.supplier}
disabled
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Po Code")}
fullWidth
value={itemDetail.poCode}
disabled
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("itemName")}
fullWidth
value={itemDetail.itemName}
disabled
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("itemNo")}
fullWidth
value={itemDetail.itemNo}
disabled
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("qty")}
fullWidth
value={itemDetail.acceptedQty}
disabled
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("productionDate")}
fullWidth
value={
// dayjs(itemDetail.productionDate)
dayjs()
// .add(-1, "month")
.format(OUTPUT_DATE_FORMAT)}
disabled
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("expiryDate")}
fullWidth
value={
// dayjs(itemDetail.expiryDate)
dayjs()
.add(20, "day")
.format(OUTPUT_DATE_FORMAT)}
disabled
/>
</Grid>
<Grid item xs={6}>
<FormControl fullWidth>
<Autocomplete
noOptionsText={t("No Warehouse")}
disableClearable
disabled
fullWidth
defaultValue={options[0]} /// modify this later
// onChange={onChange}
getOptionLabel={(option) => option.label}
options={options}
renderInput={(params) => (
<TextField {...params} label={t("Default Warehouse")} />
)}
/>
</FormControl>
</Grid>
{/* <Grid item xs={5.5}>
<TextField
label={t("acceptedQty")}
fullWidth
{...register("acceptedQty", {
required: "acceptedQty required!",
min: 1,
max: itemDetail.acceptedQty,
valueAsNumber: true,
})}
// defaultValue={itemDetail.acceptedQty}
disabled={disabled}
error={Boolean(errors.acceptedQty)}
helperText={errors.acceptedQty?.message}
/>
</Grid>
<Grid item xs={1}>
<Button disabled={disabled} onClick={onOpenScanner}>
{t("bind")}
</Button>
</Grid> */}
{/* <Grid item xs={5.5}>
<Controller
control={control}
name="warehouseId"
render={({ field }) => {
console.log(field);
return (
<Autocomplete
noOptionsText={t("No Warehouse")}
disableClearable
fullWidth
value={options.find((o) => o.value == field.value)}
onChange={onChange}
getOptionLabel={(option) => option.label}
options={options}
renderInput={(params) => (
<TextField
{...params}
label={"Select warehouse"}
error={Boolean(errors.warehouseId?.message)}
helperText={warehouseHelperText}
// helperText={errors.warehouseId?.message}
/>
)}
/>
);
}}
/>
<FormControl fullWidth>
<Autocomplete
noOptionsText={t("No Warehouse")}
disableClearable
fullWidth
// value={warehouseId > 0
// ? options.find((o) => o.value === warehouseId)
// : undefined}
defaultValue={options[0]}
// defaultValue={options.find((o) => o.value === 1)}
value={currentValue}
onChange={onChange}
getOptionLabel={(option) => option.label}
options={options}
renderInput={(params) => (
<TextField
{...params}
// label={"Select warehouse"}
disabled={disabled}
error={Boolean(errors.warehouseId?.message)}
helperText={
errors.warehouseId?.message ?? getWarningTextHardcode()
}
// helperText={warehouseHelperText}
/>
)}
/>
</FormControl>
</Grid> */}
<Grid
item
xs={12}
style={{ display: "flex", justifyContent: "center" }}
>
{/* <QrCode content={qrContent} sx={{ width: 200, height: 200 }} /> */}
<InputDataGrid<PutawayInput, PutawayLine, EntryError>
apiRef={apiRef}
checkboxSelection={false}
_formKey={"putawayLine"}
columns={columns}
validateRow={validation}
needAdd={true}
showRemoveBtn={false}
/>
</Grid>
</Grid>
{/* <Grid
container
justifyContent="flex-start"
alignItems="flex-start"
spacing={2}
sx={{ mt: 0.5 }}
>
<Button onClick={onOpenScanner}>bind</Button>
</Grid> */}

<Modal open={isOpenScanner} onClose={closeHandler}>
<Box sx={style}>
<Typography variant="h4">
{t("Please scan warehouse qr code.")}
</Typography>
{/* <ReactQrCodeScanner scannerConfig={scannerConfig} /> */}
</Box>
</Modal>
</Grid>
);
};
export default PutawayForm;

+ 395
- 0
src/components/PickOrderSearch/QCDatagrid.tsx Переглянути файл

@@ -0,0 +1,395 @@
"use client";
import {
Dispatch,
MutableRefObject,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import StyledDataGrid from "../StyledDataGrid";
import {
FooterPropsOverrides,
GridActionsCellItem,
GridCellParams,
GridColDef,
GridEventListener,
GridRowEditStopReasons,
GridRowId,
GridRowIdGetter,
GridRowModel,
GridRowModes,
GridRowModesModel,
GridRowSelectionModel,
GridToolbarContainer,
GridValidRowModel,
useGridApiRef,
} from "@mui/x-data-grid";
import { set, useFormContext } from "react-hook-form";
import SaveIcon from "@mui/icons-material/Save";
import DeleteIcon from "@mui/icons-material/Delete";
import CancelIcon from "@mui/icons-material/Cancel";
import { Add } from "@mui/icons-material";
import { Box, Button, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
import {
GridApiCommunity,
GridSlotsComponentsProps,
} from "@mui/x-data-grid/internals";
import { dummyQCData } from "./dummyQcTemplate";
// T == CreatexxxInputs map of the form's fields
// V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc
// E == error
interface ResultWithId {
id: string | number;
}
// export type InputGridProps = {
// [key: string]: any
// }
interface DefaultResult<E> {
_isNew: boolean;
_error: E;
}

interface SelectionResult<E> {
active: boolean;
_isNew: boolean;
_error: E;
}
type Result<E> = DefaultResult<E> | SelectionResult<E>;

export type TableRow<V, E> = Partial<
V & {
isActive: boolean | undefined;
_isNew: boolean;
_error: E;
} & ResultWithId
>;

export interface InputDataGridProps<T, V, E> {
apiRef: MutableRefObject<GridApiCommunity>;
// checkboxSelection: false | undefined;
_formKey: keyof T;
columns: GridColDef[];
validateRow: (newRow: GridRowModel<TableRow<V, E>>) => E;
needAdd?: boolean;
}

export interface SelectionInputDataGridProps<T, V, E> {
// thinking how do
apiRef: MutableRefObject<GridApiCommunity>;
// checkboxSelection: true;
_formKey: keyof T;
columns: GridColDef[];
validateRow: (newRow: GridRowModel<TableRow<V, E>>) => E;
}

export type Props<T, V, E> =
| InputDataGridProps<T, V, E>
| SelectionInputDataGridProps<T, V, E>;
export class ProcessRowUpdateError<T, E> extends Error {
public readonly row: T;
public readonly errors: E | undefined;
constructor(row: T, message?: string, errors?: E) {
super(message);
this.row = row;
this.errors = errors;

Object.setPrototypeOf(this, ProcessRowUpdateError.prototype);
}
}
// T == CreatexxxInputs map of the form's fields
// V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc
// E == error
function InputDataGrid<T, V, E>({
apiRef,
// checkboxSelection = false,
_formKey,
columns,
validateRow,
}: Props<T, V, E>) {
const {
t,
// i18n: { language },
} = useTranslation("purchaseOrder");
const formKey = _formKey.toString();
const { setValue, getValues } = useFormContext();
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});
// const apiRef = useGridApiRef();
const getRowId = useCallback<GridRowIdGetter<TableRow<V, E>>>(
(row) => row.id! as number,
[],
);
const formValue = getValues(formKey)
const list: TableRow<V, E>[] = !formValue || formValue.length == 0 ? dummyQCData : getValues(formKey);
console.log(list)
const [rows, setRows] = useState<TableRow<V, E>[]>(() => {
// const list: TableRow<V, E>[] = getValues(formKey);
console.log(list)
return list && list.length > 0 ? list : [];
});
console.log(rows)
// const originalRows = list && list.length > 0 ? list : [];
const originalRows = useMemo(() => (
list && list.length > 0 ? list : []
), [list])
// const originalRowModel = originalRows.filter((li) => li.isActive).map(i => i.id) as GridRowSelectionModel
const [rowSelectionModel, setRowSelectionModel] =
useState<GridRowSelectionModel>(() => {
// const rowModel = list.filter((li) => li.isActive).map(i => i.id) as GridRowSelectionModel
const rowModel: GridRowSelectionModel = getValues(
`${formKey}_active`,
) as GridRowSelectionModel;
console.log(rowModel);
return rowModel;
});

useEffect(() => {
for (let i = 0; i < rows.length; i++) {
const currRow = rows[i]
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
[currRow.id as number]: { mode: GridRowModes.View },
}));
}
}, [rows])

const handleSave = useCallback(
(id: GridRowId) => () => {
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
[id]: { mode: GridRowModes.View },
}));
},
[],
);
const onProcessRowUpdateError = useCallback(
(updateError: ProcessRowUpdateError<T, E>) => {
const errors = updateError.errors;
const row = updateError.row;
console.log(errors);
apiRef.current.updateRows([{ ...row, _error: errors }]);
},
[apiRef],
);

const processRowUpdate = useCallback(
(
newRow: GridRowModel<TableRow<V, E>>,
originalRow: GridRowModel<TableRow<V, E>>,
) => {
/////////////////
// validation here
const errors = validateRow(newRow);
console.log(newRow);
if (errors) {
throw new ProcessRowUpdateError(
originalRow,
"validation error",
errors,
);
}
/////////////////
const { _isNew, _error, ...updatedRow } = newRow;
const rowToSave = {
...updatedRow,
} as TableRow<V, E>; /// test
console.log(rowToSave);
setRows((rw) =>
rw.map((r) => (getRowId(r) === getRowId(originalRow) ? rowToSave : r)),
);
return rowToSave;
},
[validateRow, getRowId],
);

const addRow = useCallback(() => {
const newEntry = { id: Date.now(), _isNew: true } as TableRow<V, E>;
setRows((prev) => [...prev, newEntry]);
setRowModesModel((model) => ({
...model,
[getRowId(newEntry)]: {
mode: GridRowModes.Edit,
// fieldToFocus: "team", /// test
},
}));
}, [getRowId]);

const reset = useCallback(() => {
setRowModesModel({});
setRows(originalRows);
}, [originalRows]);

const handleCancel = useCallback(
(id: GridRowId) => () => {
setRowModesModel((model) => ({
...model,
[id]: { mode: GridRowModes.View, ignoreModifications: true },
}));
const editedRow = rows.find((row) => getRowId(row) === id);
if (editedRow?._isNew) {
setRows((rw) => rw.filter((r) => getRowId(r) !== id));
} else {
setRows((rw) =>
rw.map((r) => (getRowId(r) === id ? { ...r, _error: undefined } : r)),
);
}
},
[rows, getRowId],
);

const handleDelete = useCallback(
(id: GridRowId) => () => {
setRows((prevRows) => prevRows.filter((row) => getRowId(row) !== id));
},
[getRowId],
);

const _columns = useMemo<GridColDef[]>(
() => [
...columns,
{
field: "actions",
type: "actions",
headerName: "",
flex: 0.5,
cellClassName: "actions",
getActions: ({ id }: { id: GridRowId }) => {
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;
if (isInEditMode) {
return [
<GridActionsCellItem
icon={<SaveIcon />}
label="Save"
key="edit"
sx={{
color: "primary.main",
}}
onClick={handleSave(id)}
/>,
<GridActionsCellItem
icon={<CancelIcon />}
label="Cancel"
key="edit"
onClick={handleCancel(id)}
/>,
];
}
return [
<GridActionsCellItem
icon={<DeleteIcon />}
label="Delete"
sx={{
color: "error.main",
}}
onClick={handleDelete(id)}
color="inherit"
key="edit"
/>,
];
},
},
],
[columns, rowModesModel, handleSave, handleCancel, handleDelete],
);
// sync useForm
useEffect(() => {
// console.log(formKey)
// console.log(rows)
setValue(formKey, rows);
}, [formKey, rows, setValue]);

const footer = (
<Box display="flex" gap={2} alignItems="center">
<Button
disableRipple
variant="outlined"
startIcon={<Add />}
onClick={addRow}
size="small"
>
新增
{/* {t("Add Record")} */}
</Button>
<Button
disableRipple
variant="outlined"
startIcon={<Add />}
onClick={reset}
size="small"
>
{/* {t("Clean Record")} */}
清除
</Button>
</Box>
);
// const handleRowEditStop: GridEventListener<'rowEditStop'> = (params, event) => {
// if (params.reason === GridRowEditStopReasons.rowFocusOut) {
// event.defaultMuiPrevented = true;
// }
// };

return (
<StyledDataGrid
// {...props}
// getRowId={getRowId as GridRowIdGetter<GridValidRowModel>}
rowSelectionModel={rowSelectionModel}
apiRef={apiRef}
rows={rows}
columns={columns}
editMode="row"
autoHeight
sx={{
"--DataGrid-overlayHeight": "100px",
".MuiDataGrid-row .MuiDataGrid-cell.hasError": {
border: "1px solid",
borderColor: "error.main",
},
".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": {
border: "1px solid",
borderColor: "warning.main",
},
}}
disableColumnMenu
processRowUpdate={processRowUpdate as any}
// onRowEditStop={handleRowEditStop}
rowModesModel={rowModesModel}
onRowModesModelChange={setRowModesModel}
onProcessRowUpdateError={onProcessRowUpdateError}
getCellClassName={(params: GridCellParams<TableRow<T, E>>) => {
let classname = "";
if (params.row._error) {
classname = "hasError";
}
return classname;
}}
slots={{
// footer: FooterToolbar,
noRowsOverlay: NoRowsOverlay,
}}
// slotProps={{
// footer: { child: footer },
// }
// }
/>
);
}
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
};
const NoRowsOverlay: React.FC = () => {
const { t } = useTranslation("home");
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
height="100%"
>
<Typography variant="caption">{t("Add some entries!")}</Typography>
</Box>
);
};
export default InputDataGrid;

+ 460
- 0
src/components/PickOrderSearch/QcFormVer2.tsx Переглянути файл

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

import { PurchaseQcResult, PurchaseQCInput } from "@/app/api/po/actions";
import {
Box,
Card,
CardContent,
Checkbox,
FormControl,
FormControlLabel,
Grid,
Radio,
RadioGroup,
Stack,
Tab,
Tabs,
TabsProps,
TextField,
Tooltip,
Typography,
} from "@mui/material";
import { useFormContext, Controller } from "react-hook-form";
import { useTranslation } from "react-i18next";
import StyledDataGrid from "../StyledDataGrid";
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
import {
GridColDef,
GridRowIdGetter,
GridRowModel,
useGridApiContext,
GridRenderCellParams,
GridRenderEditCellParams,
useGridApiRef,
GridRowSelectionModel,
} from "@mui/x-data-grid";
import InputDataGrid from "../InputDataGrid";
import { TableRow } from "../InputDataGrid/InputDataGrid";
import TwoLineCell from "./TwoLineCell";
import QcSelect from "./QcSelect";
import { GridEditInputCell } from "@mui/x-data-grid";
import { StockInLine } from "@/app/api/po";
import { stockInLineStatusMap } from "@/app/utils/formatUtil";
import { fetchQcItemCheck, fetchQcResult } from "@/app/api/qc/actions";
import { QcItemWithChecks } from "@/app/api/qc";
import axios from "@/app/(main)/axios/axiosInstance";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
import axiosInstance from "@/app/(main)/axios/axiosInstance";
import EscalationComponent from "./EscalationComponent";
import QcDataGrid from "./QCDatagrid";
import StockInFormVer2 from "./StockInFormVer2";
import { dummyEscalationHistory, dummyQCData, QcData } from "./dummyQcTemplate";
import { ModalFormInput } from "@/app/api/dashboard/actions";
import { escape } from "lodash";

interface Props {
itemDetail: StockInLine;
qc: QcItemWithChecks[];
disabled: boolean;
qcItems: QcData[]
setQcItems: Dispatch<SetStateAction<QcData[]>>
}

type EntryError =
| {
[field in keyof QcData]?: string;
}
| undefined;

type QcRow = TableRow<Partial<QcData>, EntryError>;
// fetchQcItemCheck
const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcItems }) => {
const { t } = useTranslation("purchaseOrder");
const apiRef = useGridApiRef();
const {
register,
formState: { errors, defaultValues, touchedFields },
watch,
control,
setValue,
getValues,
reset,
resetField,
setError,
clearErrors,
} = useFormContext<PurchaseQCInput>();
const [tabIndex, setTabIndex] = useState(0);
const [rowSelectionModel, setRowSelectionModel] = useState<GridRowSelectionModel>();
const [escalationHistory, setEscalationHistory] = useState(dummyEscalationHistory);
const [qcResult, setQcResult] = useState();
const qcAccept = watch("qcAccept");
// const [qcAccept, setQcAccept] = useState(true);
// const [qcItems, setQcItems] = useState(dummyQCData)

const column = useMemo<GridColDef[]>(
() => [
{
field: "escalation",
headerName: t("escalation"),
flex: 1,
},
{
field: "supervisor",
headerName: t("supervisor"),
flex: 1,
},
], []
)
const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[],
);

//// validate form
const accQty = watch("acceptQty");
const validateForm = useCallback(() => {
console.log(accQty);
if (accQty > itemDetail.acceptedQty) {
setError("acceptQty", {
message: `${t("acceptQty must not greater than")} ${
itemDetail.acceptedQty
}`,
type: "required",
});
}
if (accQty < 1) {
setError("acceptQty", {
message: t("minimal value is 1"),
type: "required",
});
}
if (isNaN(accQty)) {
setError("acceptQty", {
message: t("value must be a number"),
type: "required",
});
}
}, [accQty]);

useEffect(() => {
clearErrors();
validateForm();
}, [clearErrors, validateForm]);

const columns = useMemo<GridColDef[]>(
() => [
{
field: "escalation",
headerName: t("escalation"),
flex: 1,
},
{
field: "supervisor",
headerName: t("supervisor"),
flex: 1,
},
],
[],
);
/// validate datagrid
const validation = useCallback(
(newRow: GridRowModel<QcRow>): EntryError => {
const error: EntryError = {};
// const { qcItemId, failQty } = newRow;
return Object.keys(error).length > 0 ? error : undefined;
},
[],
);

function BooleanEditCell(params: GridRenderEditCellParams) {
const apiRef = useGridApiContext();
const { id, field, value } = params;

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
apiRef.current.setEditCellValue({ id, field, value: e.target.checked });
apiRef.current.stopCellEditMode({ id, field }); // commit immediately
};

return <Checkbox checked={!!value} onChange={handleChange} sx={{ p: 0 }} />;
}

const qcColumns: GridColDef[] = [
{
field: "qcItem",
headerName: t("qcItem"),
flex: 2,
renderCell: (params) => (
<Box>
<b>{params.value}</b><br/>
{params.row.qcDescription}<br/>
</Box>
),
},
{
field: 'isPassed',
headerName: t("qcResult"),
flex: 1.5,
renderCell: (params) => {
const currentValue = params.value;
return (
<FormControl>
<RadioGroup
row
aria-labelledby="demo-radio-buttons-group-label"
value={currentValue === undefined ? "" : (currentValue ? "true" : "false")}
onChange={(e) => {
const value = e.target.value;
setQcItems((prev) =>
prev.map((r): QcData => (r.id === params.id ? { ...r, isPassed: value === "true" } : r))
);
}}
name={`isPassed-${params.id}`}
>
<FormControlLabel
value="true"
control={<Radio />}
label="合格"
sx={{
color: currentValue === true ? "green" : "inherit",
"& .Mui-checked": {color: "green"}
}}
/>
<FormControlLabel
value="false"
control={<Radio />}
label="不合格"
sx={{
color: currentValue === false ? "red" : "inherit",
"& .Mui-checked": {color: "red"}
}}
/>
</RadioGroup>
</FormControl>
);
},
},
{
field: "failedQty",
headerName: t("failedQty"),
flex: 1,
// editable: true,
renderCell: (params) => (
<TextField
type="number"
size="small"
value={!params.row.isPassed? (params.value ?? '') : '0'}
disabled={params.row.isPassed}
onChange={(e) => {
const v = e.target.value;
const next = v === '' ? undefined : Number(v);
if (Number.isNaN(next)) return;
setQcItems((prev) =>
prev.map((r) => (r.id === params.id ? { ...r, failedQty: next } : r))
);
}}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
inputProps={{ min: 0 }}
sx={{ width: '100%' }}
/>
),
},
{
field: "remarks",
headerName: t("remarks"),
flex: 2,
renderCell: (params) => (
<TextField
size="small"
value={params.value ?? ''}
onChange={(e) => {
const remarks = e.target.value;
// const next = v === '' ? undefined : Number(v);
// if (Number.isNaN(next)) return;
setQcItems((prev) =>
prev.map((r) => (r.id === params.id ? { ...r, remarks: remarks } : r))
);
}}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
inputProps={{ min: 0 }}
sx={{ width: '100%' }}
/>
),
},
]

useEffect(() => {
console.log(itemDetail);
}, [itemDetail]);

// Set initial value for acceptQty
useEffect(() => {
if (itemDetail?.acceptedQty !== undefined) {
setValue("acceptQty", itemDetail.acceptedQty);
}
}, [itemDetail?.acceptedQty, setValue]);

// const [openCollapse, setOpenCollapse] = useState(false)
const [isCollapsed, setIsCollapsed] = useState<boolean>(false);

const onFailedOpenCollapse = useCallback((qcItems: QcData[]) => {
const isFailed = qcItems.some((qc) => !qc.isPassed)
console.log(isFailed)
if (isFailed) {
setIsCollapsed(true)
} else {
setIsCollapsed(false)
}
}, [])

// const handleRadioChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
// const value = event.target.value === 'true';
// setValue("qcAccept", value);
// }, [setValue]);


useEffect(() => {
console.log(itemDetail);
}, [itemDetail]);

useEffect(() => {
// onFailedOpenCollapse(qcItems) // This function is no longer needed
}, [qcItems]); // Removed onFailedOpenCollapse from dependency array

return (
<>
<Grid container justifyContent="flex-start" alignItems="flex-start">
<Grid
container
justifyContent="flex-start"
alignItems="flex-start"
spacing={2}
sx={{ mt: 0.5 }}
>
<Grid item xs={12}>
<Tabs
value={tabIndex}
onChange={handleTabChange}
variant="scrollable"
>
<Tab label={t("QC Info")} iconPosition="end" />
<Tab label={t("Escalation History")} iconPosition="end" />
</Tabs>
</Grid>
{tabIndex == 0 && (
<>
<Grid item xs={12}>
{/* <QcDataGrid<ModalFormInput, QcData, EntryError>
apiRef={apiRef}
columns={qcColumns}
_formKey="qcResult"
validateRow={validation}
/> */}
<StyledDataGrid
columns={qcColumns}
rows={qcItems}
autoHeight
/>
</Grid>
{/* <Grid item xs={12}>
<EscalationComponent
forSupervisor={false}
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
/>
</Grid> */}
</>
)}
{tabIndex == 1 && (
<>
{/* <Grid item xs={12}>
<StockInFormVer2
itemDetail={itemDetail}
disabled={false}
/>
</Grid> */}
<Grid item xs={12}>
<Typography variant="h6" display="block" marginBlockEnd={1}>
{t("Escalation Info")}
</Typography>
</Grid>
<Grid item xs={12}>
<StyledDataGrid
rows={escalationHistory}
columns={columns}
onRowSelectionModelChange={(newRowSelectionModel) => {
setRowSelectionModel(newRowSelectionModel);
}}
/>
</Grid>
</>
)}
<Grid item xs={12}>
<FormControl>
<Controller
name="qcAccept"
control={control}
defaultValue={true}
render={({ field }) => (
<RadioGroup
row
aria-labelledby="demo-radio-buttons-group-label"
{...field}
value={field.value?.toString() || "true"}
onChange={(e) => {
const value = e.target.value === 'true';
if (!value && Boolean(errors.acceptQty)) {
setValue("acceptQty", itemDetail.acceptedQty);
}
field.onChange(value);
}}
>
<FormControlLabel value="true" control={<Radio />} label="接受" />
<Box sx={{mr:2}}>
<TextField
type="number"
label={t("acceptQty")}
sx={{ width: '150px' }}
defaultValue={accQty}
disabled={!qcAccept}
{...register("acceptQty", {
required: "acceptQty required!",
})}
error={Boolean(errors.acceptQty)}
helperText={errors.acceptQty?.message}
/>
</Box>
<FormControlLabel value="false" control={<Radio />} label="不接受及上報" />
</RadioGroup>
)}
/>
</FormControl>
</Grid>
{/* <Grid item xs={12}>
<Typography variant="h6" display="block" marginBlockEnd={1}>
{t("Escalation Result")}
</Typography>
</Grid>
<Grid item xs={12}>
<EscalationComponent
forSupervisor={true}
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
/>
</Grid> */}
</Grid>
</Grid>
</>
);
};
export default QcFormVer2;

+ 78
- 0
src/components/PickOrderSearch/QcSelect.tsx Переглянути файл

@@ -0,0 +1,78 @@
import React, { useCallback, useMemo } from "react";
import {
Autocomplete,
Box,
Checkbox,
Chip,
ListSubheader,
MenuItem,
TextField,
Tooltip,
} from "@mui/material";
import { QcItemWithChecks } from "@/app/api/qc";
import { useTranslation } from "react-i18next";

interface CommonProps {
allQcs: QcItemWithChecks[];
error?: boolean;
}

interface SingleAutocompleteProps extends CommonProps {
value: number | string | undefined;
onQcSelect: (qcItemId: number) => void | Promise<void>;
// multiple: false;
}

type Props = SingleAutocompleteProps;

const QcSelect: React.FC<Props> = ({ allQcs, value, error, onQcSelect }) => {
const { t } = useTranslation("home");
const filteredQc = useMemo(() => {
// do filtering here if any
return allQcs;
}, [allQcs]);
const options = useMemo(() => {
return [
{
value: -1, // think think sin
label: t("None"),
group: "default",
},
...filteredQc.map((q) => ({
value: q.id,
label: `${q.code} - ${q.name}`,
group: "existing",
})),
];
}, [t, filteredQc]);

const currentValue = options.find((o) => o.value === value) || options[0];

const onChange = useCallback(
(
event: React.SyntheticEvent,
newValue: { value: number; group: string } | { value: number }[],
) => {
const singleNewVal = newValue as {
value: number;
group: string;
};
onQcSelect(singleNewVal.value);
},
[onQcSelect],
);

return (
<Autocomplete
noOptionsText={t("No Qc")}
disableClearable
fullWidth
value={currentValue}
onChange={onChange}
getOptionLabel={(option) => option.label}
options={options}
renderInput={(params) => <TextField {...params} error={error} />}
/>
);
};
export default QcSelect;

+ 321
- 0
src/components/PickOrderSearch/StockInFormVer2.tsx Переглянути файл

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

import {
PurchaseQcResult,
PurchaseQCInput,
StockInInput,
} from "@/app/api/po/actions";
import {
Box,
Card,
CardContent,
Grid,
Stack,
TextField,
Tooltip,
Typography,
} from "@mui/material";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import StyledDataGrid from "../StyledDataGrid";
import { useCallback, useEffect, useMemo } from "react";
import {
GridColDef,
GridRowIdGetter,
GridRowModel,
useGridApiContext,
GridRenderCellParams,
GridRenderEditCellParams,
useGridApiRef,
} from "@mui/x-data-grid";
import InputDataGrid from "../InputDataGrid";
import { TableRow } from "../InputDataGrid/InputDataGrid";
import TwoLineCell from "./TwoLineCell";
import QcSelect from "./QcSelect";
import { QcItemWithChecks } from "@/app/api/qc";
import { GridEditInputCell } from "@mui/x-data-grid";
import { StockInLine } from "@/app/api/po";
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import dayjs from "dayjs";
// 修改接口以支持 PickOrder 数据
import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions";

// change PurchaseQcResult to stock in entry props
interface Props {
itemDetail: StockInLine | (GetPickOrderLineInfo & { pickOrderCode: string });
// qc: QcItemWithChecks[];
disabled: boolean;
}
type EntryError =
| {
[field in keyof StockInInput]?: string;
}
| undefined;

// type PoQcRow = TableRow<Partial<PurchaseQcResult>, EntryError>;

const StockInFormVer2: React.FC<Props> = ({
// qc,
itemDetail,
disabled,
}) => {
const {
t,
i18n: { language },
} = useTranslation("purchaseOrder");
const apiRef = useGridApiRef();
const {
register,
formState: { errors, defaultValues, touchedFields },
watch,
control,
setValue,
getValues,
reset,
resetField,
setError,
clearErrors,
} = useFormContext<StockInInput>();
// console.log(itemDetail);

useEffect(() => {
console.log("triggered");
// receiptDate default tdy
setValue("receiptDate", dayjs().add(0, "month").format(INPUT_DATE_FORMAT));
setValue("status", "received");
}, [setValue]);

useEffect(() => {
console.log(errors);
}, [errors]);

const productionDate = watch("productionDate");
const expiryDate = watch("expiryDate");
const uom = watch("uom");

useEffect(() => {
console.log(uom);
console.log(productionDate);
console.log(expiryDate);
if (expiryDate) clearErrors();
if (productionDate) clearErrors();
}, [expiryDate, productionDate, clearErrors]);

// 检查是否为 PickOrder 数据
const isPickOrderData = 'pickOrderCode' in itemDetail;

// 获取 UOM 显示值
const getUomDisplayValue = () => {
if (isPickOrderData) {
// PickOrder 数据
const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string };
return pickOrderItem.uomDesc || pickOrderItem.uomCode || '';
} else {
// StockIn 数据
const stockInItem = itemDetail as StockInLine;
return uom?.code || stockInItem.uom?.code || '';
}
};

// 获取 Item 显示值
const getItemDisplayValue = () => {
if (isPickOrderData) {
// PickOrder 数据
const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string };
return pickOrderItem.itemCode || '';
} else {
// StockIn 数据
const stockInItem = itemDetail as StockInLine;
return stockInItem.itemNo || '';
}
};

// 获取 Item Name 显示值
const getItemNameDisplayValue = () => {
if (isPickOrderData) {
// PickOrder 数据
const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string };
return pickOrderItem.itemName || '';
} else {
// StockIn 数据
const stockInItem = itemDetail as StockInLine;
return stockInItem.itemName || '';
}
};

// 获取 Quantity 显示值
const getQuantityDisplayValue = () => {
if (isPickOrderData) {
// PickOrder 数据
const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string };
return pickOrderItem.requiredQty || 0;
} else {
// StockIn 数据
const stockInItem = itemDetail as StockInLine;
return stockInItem.acceptedQty || 0;
}
};

return (
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h6" display="block" marginBlockEnd={1}>
{t("stock in information")}
</Typography>
</Grid>
<Grid item xs={6}>
<TextField
label={t("itemNo")}
fullWidth
{...register("itemNo", {
required: "itemNo required!",
})}
value={getItemDisplayValue()}
disabled={true}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("itemName")}
fullWidth
{...register("itemName", {
required: "itemName required!",
})}
value={getItemNameDisplayValue()}
disabled={true}
/>
</Grid>
<Grid item xs={6}>
<Controller
name="productionDate"
control={control}
rules={{
required: "productionDate required!",
}}
render={({ field }) => {
return (
<LocalizationProvider
dateAdapter={AdapterDayjs}
adapterLocale={`${language}-hk`}
>
<DatePicker
{...field}
sx={{ width: "100%" }}
label={t("productionDate")}
value={productionDate ? dayjs(productionDate) : undefined}
disabled={disabled}
onChange={(date) => {
console.log(date);
if (!date) return;
console.log(date.format(INPUT_DATE_FORMAT));
setValue("productionDate", date.format(INPUT_DATE_FORMAT));
// field.onChange(date);
}}
inputRef={field.ref}
slotProps={{
textField: {
// required: true,
error: Boolean(errors.productionDate?.message),
helperText: errors.productionDate?.message,
},
}}
/>
</LocalizationProvider>
);
}}
/>
</Grid>
<Grid item xs={6}>
<Controller
name="expiryDate"
control={control}
rules={{
required: "expiryDate required!",
}}
render={({ field }) => {
return (
<LocalizationProvider
dateAdapter={AdapterDayjs}
adapterLocale={`${language}-hk`}
>
<DatePicker
{...field}
sx={{ width: "100%" }}
label={t("expiryDate")}
value={expiryDate ? dayjs(expiryDate) : undefined}
disabled={disabled}
onChange={(date) => {
console.log(date);
if (!date) return;
console.log(date.format(INPUT_DATE_FORMAT));
setValue("expiryDate", date.format(INPUT_DATE_FORMAT));
// field.onChange(date);
}}
inputRef={field.ref}
slotProps={{
textField: {
// required: true,
error: Boolean(errors.expiryDate?.message),
helperText: errors.expiryDate?.message,
},
}}
/>
</LocalizationProvider>
);
}}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("receivedQty")}
fullWidth
{...register("receivedQty", {
required: "receivedQty required!",
})}
value={getQuantityDisplayValue()}
disabled={true}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("uom")}
fullWidth
{...register("uom", {
required: "uom required!",
})}
value={getUomDisplayValue()}
disabled={true}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("acceptedQty")}
fullWidth
{...register("acceptedQty", {
required: "acceptedQty required!",
})}
value={getQuantityDisplayValue()}
disabled={true}
// disabled={disabled}
// error={Boolean(errors.acceptedQty)}
// helperText={errors.acceptedQty?.message}
/>
</Grid>
{/* <Grid item xs={4}>
<TextField
label={t("acceptedWeight")}
fullWidth
// {...register("acceptedWeight", {
// required: "acceptedWeight required!",
// })}
disabled={disabled}
error={Boolean(errors.acceptedWeight)}
helperText={errors.acceptedWeight?.message}
/>
</Grid> */}
</Grid>
);
};
export default StockInFormVer2;

+ 24
- 0
src/components/PickOrderSearch/TwoLineCell.tsx Переглянути файл

@@ -0,0 +1,24 @@
import { Box, Tooltip } from "@mui/material";
import React from "react";

const TwoLineCell: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<Tooltip title={children}>
<Box
sx={{
whiteSpace: "normal",
overflow: "hidden",
textOverflow: "ellipsis",
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
lineHeight: "22px",
}}
>
{children}
</Box>
</Tooltip>
);
};

export default TwoLineCell;

+ 78
- 0
src/components/PickOrderSearch/dummyQcTemplate.tsx Переглянути файл

@@ -0,0 +1,78 @@
import { PutawayLine } from "@/app/api/po/actions"

export interface QcData {
id: number,
qcItem: string,
qcDescription: string,
isPassed: boolean | undefined
failedQty: number | undefined
remarks: string | undefined
}

export const dummyQCData: QcData[] = [
{
id: 1,
qcItem: "包裝",
qcDescription: "有破爛、污糟、脹袋、積水、與實物不符等任何一種情況,則不合格",
isPassed: undefined,
failedQty: undefined,
remarks: undefined,
},
{
id: 2,
qcItem: "肉質",
qcDescription: "肉質鬆散,則不合格",
isPassed: undefined,
failedQty: undefined,
remarks: undefined,
},
{
id: 3,
qcItem: "顔色",
qcDescription: "不是食材應有的顔色、顔色不均匀、出現其他顔色、腌料/醬顔色不均匀,油脂部分變綠色、黃色,",
isPassed: undefined,
failedQty: undefined,
remarks: undefined,
},
{
id: 4,
qcItem: "狀態",
qcDescription: "有結晶、結霜、解凍跡象、發霉、散發異味等任何一種情況,則不合格",
isPassed: undefined,
failedQty: undefined,
remarks: undefined,
},
{
id: 5,
qcItem: "異物",
qcDescription: "有不屬於本食材的雜質,則不合格",
isPassed: undefined,
failedQty: undefined,
remarks: undefined,
},
]

export interface EscalationData {
id: number,
escalation: string,
supervisor: string,
}


export const dummyEscalationHistory: EscalationData[] = [
{
id: 1,
escalation: "上報1",
supervisor: "陳大文"
},
]

export const dummyPutawayLine: PutawayLine[] = [
{
id: 1,
qty: 100,
warehouseId: 1,
warehouse: "W001 - 憶兆 3樓A倉",
printQty: 100
}
]

+ 635
- 0
src/components/PickOrderSearch/newcreatitem.tsx Переглянути файл

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

import { createPickOrder, SavePickOrderRequest, SavePickOrderLineRequest } from "@/app/api/pickOrder/actions";
import {
Autocomplete,
Box,
Button,
FormControl,
Grid,
Stack,
TextField,
Typography,
Checkbox,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
} from "@mui/material";
import { Controller, FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useCallback, useEffect, useMemo, useState } from "react";
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs from "dayjs";
import { Check, Search } from "@mui/icons-material";
import { ItemCombo, fetchAllItemsInClient } from "@/app/api/settings/item/actions";
import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import SearchResults, { Column } from "../SearchResults/SearchResults";

type Props = {
filterArgs?: Record<string, any>;
searchQuery?: Record<string, any>;
};

// 扩展表单类型以包含搜索字段
interface SearchFormData extends SavePickOrderRequest {
searchCode?: string;
searchName?: string;
}

interface CreatedItem {
itemId: number;
itemName: string;
itemCode: string;
qty: number;
uom: string;
uomId: number;
isSelected: boolean;
}

const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery }) => {
const { t } = useTranslation("pickOrder");
const [items, setItems] = useState<ItemCombo[]>([]);
const [filteredItems, setFilteredItems] = useState<ItemCombo[]>([]);
const [createdItems, setCreatedItems] = useState<CreatedItem[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [hasSearched, setHasSearched] = useState(false);
// Add state for selected item IDs in search results
const [selectedSearchItemIds, setSelectedSearchItemIds] = useState<(string | number)[]>([]);

const formProps = useForm<SearchFormData>();
const errors = formProps.formState.errors;
const targetDate = formProps.watch("targetDate");
const type = formProps.watch("type");
const searchCode = formProps.watch("searchCode");
const searchName = formProps.watch("searchName");

// 加载项目数据
useEffect(() => {
const loadItems = async () => {
try {
const itemsData = await fetchAllItemsInClient();
console.log("Loaded items:", itemsData);
setItems(itemsData);
setFilteredItems([]);
} catch (error) {
console.error("Error loading items:", error);
}
};
loadItems();
}, []);

// 根据搜索查询过滤项目
useEffect(() => {
if (searchQuery && items.length > 0) {
// 检查是否有有效的搜索条件
const hasValidSearch = (
(searchQuery.items && searchQuery.items.trim && searchQuery.items.trim() !== "") ||
(searchQuery.code && searchQuery.code.trim && searchQuery.code.trim() !== "") ||
(searchQuery.type && searchQuery.type !== "All")
);

if (hasValidSearch) {
let filtered = items;
// 处理项目名称搜索 - 确保 searchQuery.items 是数组
if (searchQuery.items) {
const itemsToSearch = Array.isArray(searchQuery.items)
? searchQuery.items
: [searchQuery.items];
if (itemsToSearch.length > 0 && !itemsToSearch.includes("All")) {
filtered = filtered.filter(item =>
itemsToSearch.some((searchItem: string) =>
item.label.toLowerCase().includes(searchItem.toLowerCase())
)
);
}
}
// 处理项目代码搜索
if (searchQuery.code) {
filtered = filtered.filter(item =>
item.label.toLowerCase().includes(searchQuery.code.toLowerCase())
);
}
// 处理类型搜索
if (searchQuery.type && searchQuery.type !== "All") {
// 这里可以根据实际需求调整类型过滤逻辑
// 目前先注释掉,因为项目数据可能没有类型字段
// filtered = filtered.filter(item => item.type === searchQuery.type);
}
filtered = filtered.slice(0, 10);
setFilteredItems(filtered);
setHasSearched(true);
} else {
// 如果没有有效的搜索条件,清空结果
setFilteredItems([]);
setHasSearched(false);
}
} else {
// 如果没有搜索查询,清空结果并重置搜索状态
setFilteredItems([]);
setHasSearched(false);
}
}, [searchQuery, items]);

// 新增:同步 SearchBox 的数据到表单
useEffect(() => {
if (searchQuery) {
// 同步类型
if (searchQuery.type) {
formProps.setValue("type", searchQuery.type);
}
// 同步目标日期
if (searchQuery.targetDate) {
formProps.setValue("targetDate", searchQuery.targetDate);
}
// 同步项目代码
if (searchQuery.code) {
formProps.setValue("searchCode", searchQuery.code);
}
// 同步项目名称
if (searchQuery.items) {
formProps.setValue("searchName", searchQuery.items);
}
}
}, [searchQuery, formProps]);

// 初始化时确保不显示任何结果
useEffect(() => {
setFilteredItems([]);
setHasSearched(false);
}, []);

const typeList = [
{ type: "Consumable" },
{ type: "Material" },
{ type: "Product" }
];

const handleTypeChange = useCallback(
(event: React.SyntheticEvent, newValue: {type: string} | null) => {
formProps.setValue("type", newValue?.type || "");
},
[formProps],
);

const handleSearch = useCallback(() => {
if (!type) {
alert(t("Please select type"));
return;
}
if (!searchCode && !searchName) {
alert(t("Please enter at least code or name"));
return;
}
setIsLoading(true);
setHasSearched(true);
console.log("Searching with:", { type, searchCode, searchName, itemsCount: items.length });
setTimeout(() => {
let filtered = items;
if (searchCode && searchCode.trim()) {
filtered = filtered.filter(item =>
item.label.toLowerCase().includes(searchCode.toLowerCase())
);
console.log("After code filter:", filtered.length);
}
if (searchName && searchName.trim()) {
filtered = filtered.filter(item =>
item.label.toLowerCase().includes(searchName.toLowerCase())
);
console.log("After name filter:", filtered.length);
}
filtered = filtered.slice(0, 100);
console.log("Final filtered results:", filtered.length);
setFilteredItems(filtered);
setIsLoading(false);
}, 500);
}, [type, searchCode, searchName, items, t]);

// Modified handler for search item selection
const handleSearchItemSelect = useCallback((itemId: number, isSelected: boolean) => {
if (isSelected) {
const item = filteredItems.find(i => i.id === itemId);
if (!item) return;
const existingItem = createdItems.find(created => created.itemId === item.id);
if (existingItem) {
alert(t("Item already exists in created items"));
return;
}
const newCreatedItem: CreatedItem = {
itemId: item.id,
itemName: item.label,
itemCode: item.label,
qty: 1,
uom: item.uom || "",
uomId: item.uomId || 0,
isSelected: true
};
setCreatedItems(prev => [...prev, newCreatedItem]);
}
}, [filteredItems, createdItems, t]);

// Handler for created item selection
const handleCreatedItemSelect = useCallback((itemId: number, isSelected: boolean) => {
setCreatedItems(prev =>
prev.map(item =>
item.itemId === itemId ? { ...item, isSelected } : item
)
);
}, []);

const handleQtyChange = useCallback((itemId: number, newQty: number) => {
setCreatedItems(prev =>
prev.map(item =>
item.itemId === itemId ? { ...item, qty: newQty } : item
)
);
}, []);

// Check if item is already in created items
const isItemInCreated = useCallback((itemId: number) => {
return createdItems.some(item => item.itemId === itemId);
}, [createdItems]);

const onSubmit = useCallback<SubmitHandler<SearchFormData>>(
async (data, event) => {
const selectedCreatedItems = createdItems.filter(item => item.isSelected);
if (selectedCreatedItems.length === 0) {
alert(t("Please select at least one item to submit"));
return;
}

let formattedTargetDate = data.targetDate;
if (data.targetDate && typeof data.targetDate === 'string') {
try {
const date = dayjs(data.targetDate);
formattedTargetDate = date.format('YYYY-MM-DD');
} catch (error) {
console.error("Invalid date format:", data.targetDate);
alert(t("Invalid date format"));
return;
}
}

const pickOrderData: SavePickOrderRequest = {
type: data.type || "Consumable",
targetDate: formattedTargetDate,
pickOrderLine: selectedCreatedItems.map(item => ({
itemId: item.itemId,
qty: item.qty,
uomId: item.uomId
} as SavePickOrderLineRequest))
};

console.log("Submitting pick order:", pickOrderData);
try {
const res = await createPickOrder(pickOrderData);
if (res.id) {
console.log("Pick order created successfully:", res);
setCreatedItems(prev => prev.filter(item => !item.isSelected));
formProps.reset();
setHasSearched(false);
setFilteredItems([]);
alert(t("Pick order created successfully"));
}
} catch (error) {
console.error("Error creating pick order:", error);
alert(t("Failed to create pick order"));
}
},
[createdItems, t, formProps]
);

const handleReset = useCallback(() => {
formProps.reset();
setCreatedItems([]);
setHasSearched(false);
setFilteredItems([]);
}, [formProps]);

// Pagination state
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);

// Handle page change
const handleChangePage = (
_event: React.MouseEvent | React.KeyboardEvent,
newPage: number,
) => {
console.log(_event);
setPage(newPage);
// The original code had setPagingController and defaultPagingController,
// but these are not defined in the provided context.
// Assuming they are meant to be part of a larger context or will be added.
// For now, commenting out the setPagingController part as it's not defined.
// if (setPagingController) {
// setPagingController({
// ...(pagingController ?? defaultPagingController),
// pageNum: newPage + 1,
// });
// }
};

// Handle rows per page change
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
console.log(event);
setRowsPerPage(+event.target.value);
setPage(0);
// The original code had setPagingController and defaultPagingController,
// but these are not defined in the provided context.
// Assuming they are meant to be part of a larger context or will be added.
// For now, commenting out the setPagingController part as it's not defined.
// if (setPagingController) {
// setPagingController({
// ...(pagingController ?? defaultPagingController),
// pageNum: 1,
// });
// }
};

// Define columns for SearchResults
const searchItemColumns: Column<ItemCombo>[] = useMemo(() => [
{
name: "id",
label: "",
type: "checkbox",
disabled: (item) => isItemInCreated(item.id), // Disable if already in created items
},
{
name: "label",
label: t("Item"),
renderCell: (item) => (
<Box>
<Typography variant="body2">{item.label}</Typography>
<Typography variant="caption" color="textSecondary">
ID: {item.id}
</Typography>
</Box>
),
},
{
name: "id", // Use id as placeholder for quantity
label: t("Order Quantity"),
renderCell: () => "-",
},
{
name: "uom",
label: t("Unit"),
renderCell: (item) => item.uom || "-",
},
], [t, isItemInCreated]);

// Handle checkbox selection from SearchResults
const handleSearchCheckboxChange = useCallback((ids: (string | number)[]) => {
setSelectedSearchItemIds(ids);
// Process newly selected items
ids.forEach(id => {
if (!isItemInCreated(id as number)) {
handleSearchItemSelect(id as number, true);
}
});
}, [isItemInCreated, handleSearchItemSelect]);

return (
<FormProvider {...formProps}>
<Box
component="form"
onSubmit={formProps.handleSubmit(onSubmit)}
>
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h6" display="block" marginBlockEnd={1}>
{t("Pick Order Detail")}
</Typography>
</Grid>
{/* 隐藏搜索条件区域 */}
{/*
<Grid item xs={2}>
<FormControl fullWidth>
<Autocomplete
disableClearable
fullWidth
getOptionLabel={(option) => option.type}
options={typeList}
onChange={handleTypeChange}
renderInput={(params) => <TextField {...params} label={t("type")} required/>}
/>
</FormControl>
</Grid>
<Grid item xs={2}>
<Controller
control={formProps.control}
name="searchCode"
render={({ field }) => (
<TextField
{...field}
fullWidth
label={t("code")}
placeholder={t("Enter item code")}
/>
)}
/>
</Grid>
<Grid item xs={2}>
<Controller
control={formProps.control}
name="searchName"
render={({ field }) => (
<TextField
{...field}
fullWidth
label={t("name")}
placeholder={t("Enter item name")}
/>
)}
/>
</Grid>
<Grid item xs={3}>
<Controller
control={formProps.control}
name="targetDate"
render={({ field }) => (
<LocalizationProvider
dateAdapter={AdapterDayjs}
adapterLocale="zh-hk"
>
<DatePicker
{...field}
sx={{ width: "100%" }}
label={t("targetDate")}
value={targetDate ? dayjs(targetDate) : undefined}
onChange={(date) => {
if (!date) return;
formProps.setValue("targetDate", date.format(INPUT_DATE_FORMAT));
}}
inputRef={field.ref}
slotProps={{
textField: {
error: Boolean(errors.targetDate?.message),
helperText: errors.targetDate?.message,
},
}}
/>
</LocalizationProvider>
)}
/>
</Grid>
<Grid item xs={3}>
<Button
variant="contained"
startIcon={<Search />}
onClick={handleSearch}
disabled={!type || (!searchCode && !searchName) || isLoading}
fullWidth
sx={{ height: '56px' }}
>
{isLoading ? t("Searching...") : t("Search")}
</Button>
</Grid>
*/}
</Grid>
{/* 创建项目区域 */}
{createdItems.length > 0 && (
<Box sx={{ mt: 3 }}>
<Typography variant="h6" marginBlockEnd={2}>
{t("Created Items")} ({createdItems.length})
</Typography>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell padding="checkbox">
<Typography variant="subtitle2">{t("Select")}</Typography>
</TableCell>
<TableCell>
<Typography variant="subtitle2">{t("Item")}</Typography>
</TableCell>
<TableCell>
<Typography variant="subtitle2">{t("Order Quantity")}</Typography>
</TableCell>
<TableCell>
<Typography variant="subtitle2">{t("Unit")}</Typography>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{createdItems.map((item) => (
<TableRow key={item.itemId}>
<TableCell padding="checkbox">
<Checkbox
checked={item.isSelected}
onChange={(e) => handleCreatedItemSelect(item.itemId, e.target.checked)}
/>
</TableCell>
<TableCell>
<Typography variant="body2">{item.itemName}</Typography>
<Typography variant="caption" color="textSecondary">
{item.itemCode}
</Typography>
</TableCell>
<TableCell>
<TextField
type="number"
size="small"
value={item.qty}
onChange={(e) => handleQtyChange(item.itemId, Number(e.target.value))}
inputProps={{ min: 1 }}
sx={{ width: '80px' }}
/>
</TableCell>
<TableCell>
<Typography variant="body2">{item.uom}</Typography>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
)}
{/* Search Results with SearchResults component */}
{hasSearched && filteredItems.length > 0 && (
<Box sx={{ mt: 3 }}>
<Typography variant="h6" marginBlockEnd={2}>
{t("Search Results")} ({filteredItems.length})
{filteredItems.length >= 100 && (
<Typography variant="caption" color="textSecondary" sx={{ ml: 2 }}>
{t("Showing first 100 results")}
</Typography>
)}
</Typography>
<SearchResults<ItemCombo>
items={filteredItems}
columns={searchItemColumns}
totalCount={filteredItems.length}
checkboxIds={selectedSearchItemIds}
setCheckboxIds={handleSearchCheckboxChange}
/>
</Box>
)}


{/* 操作按钮 */}
<Stack direction="row" justifyContent="flex-end" gap={1} sx={{ mt: 3 }}>
<Button
name="submit"
variant="contained"
startIcon={<Check />}
type="submit"
disabled={createdItems.filter(item => item.isSelected).length === 0}
>
{t("submit")}
</Button>
<Button
name="reset"
variant="outlined"
onClick={handleReset}
>
{t("reset")}
</Button>
</Stack>
</Box>
</FormProvider>
);
};

export default NewCreateItem;

+ 380
- 0
src/components/PickOrderSearch/pickorderModelVer2.tsx Переглянути файл

@@ -0,0 +1,380 @@
"use client";
// 修改为 PickOrder 相关的导入
import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions";
import { QcItemWithChecks } from "@/app/api/qc";
import { PurchaseQcResult } from "@/app/api/po/actions";
import {
Box,
Button,
Grid,
Modal,
ModalProps,
Stack,
Typography,
} from "@mui/material";
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import StockInFormVer2 from "./StockInFormVer2";
import QcFormVer2 from "./QcFormVer2";
import PutawayForm from "./PutawayForm";
import { dummyPutawayLine, dummyQCData, QcData } from "./dummyQcTemplate";
import { useGridApiRef } from "@mui/x-data-grid";
import {submitDialogWithWarning} from "../Swal/CustomAlerts";

const style = {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
bgcolor: "background.paper",
pt: 5,
px: 5,
pb: 10,
display: "block",
width: { xs: "60%", sm: "60%", md: "60%" },
// height: { xs: "60%", sm: "60%", md: "60%" },
};
// 修改接口定义
interface CommonProps extends Omit<ModalProps, "children"> {
itemDetail: GetPickOrderLineInfo & {
pickOrderCode: string;
qcResult?: PurchaseQcResult[]
};
setItemDetail: Dispatch<
SetStateAction<
| (GetPickOrderLineInfo & {
pickOrderCode: string;
warehouseId?: number;
})
| undefined
>
>;
qc?: QcItemWithChecks[];
warehouse?: any[];
}

interface Props extends CommonProps {
itemDetail: GetPickOrderLineInfo & {
pickOrderCode: string;
qcResult?: PurchaseQcResult[]
};
}

// 修改组件名称
const PickQcStockInModalVer2: React.FC<Props> = ({
open,
onClose,
itemDetail,
setItemDetail,
qc,
warehouse,
}) => {
console.log(warehouse);
// 修改翻译键
const {
t,
i18n: { language },
} = useTranslation("pickOrder");
const [qcItems, setQcItems] = useState(dummyQCData)
const formProps = useForm<any>({
defaultValues: {
...itemDetail,
putawayLine: dummyPutawayLine,
// receiptDate: itemDetail.receiptDate || dayjs().add(-1, "month").format(INPUT_DATE_FORMAT),
// warehouseId: itemDetail.defaultWarehouseId || 0
},
});
const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>(
(...args) => {
onClose?.(...args);
// reset();
},
[onClose],
);
const [openPutaway, setOpenPutaway] = useState(false);
const onOpenPutaway = useCallback(() => {
setOpenPutaway(true);
}, []);
const onClosePutaway = useCallback(() => {
setOpenPutaway(false);
}, []);
// Stock In submission handler
const onSubmitStockIn = useCallback<SubmitHandler<any>>(
async (data, event) => {
console.log("Stock In Submission:", event!.nativeEvent);
// Extract only stock-in related fields
const stockInData = {
// quantity: data.quantity,
// receiptDate: data.receiptDate,
// batchNumber: data.batchNumber,
// expiryDate: data.expiryDate,
// warehouseId: data.warehouseId,
// location: data.location,
// unitCost: data.unitCost,
data: data,
// Add other stock-in specific fields from your form
};
console.log("Stock In Data:", stockInData);
// Handle stock-in submission logic here
// e.g., call API, update state, etc.
},
[],
);
// QC submission handler
const onSubmitQc = useCallback<SubmitHandler<any>>(
async (data, event) => {
console.log("QC Submission:", event!.nativeEvent);
// Get QC data from the shared form context
const qcAccept = data.qcAccept;
const acceptQty = data.acceptQty;
// Validate QC data
const validationErrors : string[] = [];
// Check if all QC items have results
const itemsWithoutResult = qcItems.filter(item => item.isPassed === undefined);
if (itemsWithoutResult.length > 0) {
validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.qcItem).join(', ')}`);
}

// Check if failed items have failed quantity
const failedItemsWithoutQty = qcItems.filter(item =>
item.isPassed === false && (!item.failedQty || item.failedQty <= 0)
);
if (failedItemsWithoutQty.length > 0) {
validationErrors.push(`${t("Failed items must have failed quantity")}: ${failedItemsWithoutQty.map(item => item.qcItem).join(', ')}`);
}

// Check if QC accept decision is made
// if (qcAccept === undefined) {
// validationErrors.push("QC accept/reject decision is required");
// }

// Check if accept quantity is valid
if (acceptQty === undefined || acceptQty <= 0) {
validationErrors.push("Accept quantity must be greater than 0");
}

if (validationErrors.length > 0) {
console.error("QC Validation failed:", validationErrors);
alert(`未完成品檢: ${validationErrors}`);
return;
}

const qcData = {
qcAccept: qcAccept,
acceptQty: acceptQty,
qcItems: qcItems.map(item => ({
id: item.id,
qcItem: item.qcItem,
qcDescription: item.qcDescription,
isPassed: item.isPassed,
failedQty: (item.failedQty && !item.isPassed) || 0,
remarks: item.remarks || ''
}))
};
// const qcData = data;

console.log("QC Data for submission:", qcData);
// await submitQcData(qcData);

if (!qcData.qcItems.every((qc) => qc.isPassed) && qcData.qcAccept) {
submitDialogWithWarning(onOpenPutaway, t, {title:"有不合格檢查項目,確認接受收貨?", confirmButtonText: "Confirm", html: ""});
return;
}

if (qcData.qcAccept) {
onOpenPutaway();
} else {
onClose();
}
},
[onOpenPutaway, qcItems],
);
// Email supplier handler
const onSubmitEmailSupplier = useCallback<SubmitHandler<any>>(
async (data, event) => {
console.log("Email Supplier Submission:", event!.nativeEvent);
// Extract only email supplier related fields
const emailData = {
// supplierEmail: data.supplierEmail,
// issueDescription: data.issueDescription,
// qcComments: data.qcComments,
// defectNotes: data.defectNotes,
// attachments: data.attachments,
// escalationReason: data.escalationReason,
data: data,

// Add other email-specific fields
};
console.log("Email Supplier Data:", emailData);
// Handle email supplier logic here
// e.g., send email to supplier, log escalation, etc.
},
[],
);
// Putaway submission handler
const onSubmitPutaway = useCallback<SubmitHandler<any>>(
async (data, event) => {
console.log("Putaway Submission:", event!.nativeEvent);
// Extract only putaway related fields
const putawayData = {
// putawayLine: data.putawayLine,
// putawayLocation: data.putawayLocation,
// binLocation: data.binLocation,
// putawayQuantity: data.putawayQuantity,
// putawayNotes: data.putawayNotes,
data: data,

// Add other putaway specific fields
};
console.log("Putaway Data:", putawayData);
// Handle putaway submission logic here
// Close modal after successful putaway
closeHandler({}, "backdropClick");
},
[closeHandler],
);
// Print handler
const onPrint = useCallback(() => {
console.log("Print putaway documents");
// Handle print logic here
window.print();
}, []);
const acceptQty = formProps.watch("acceptedQty")

const checkQcIsPassed = useCallback((qcItems: QcData[]) => {
const isPassed = qcItems.every((qc) => qc.isPassed);
console.log(isPassed)
if (isPassed) {
formProps.setValue("passingQty", acceptQty)
} else {
formProps.setValue("passingQty", 0)
}
return isPassed
}, [acceptQty, formProps])

useEffect(() => {
// maybe check if submitted before
console.log(qcItems)
checkQcIsPassed(qcItems)
}, [qcItems, checkQcIsPassed])

return (
<>
<FormProvider {...formProps}>
<Modal open={open} onClose={closeHandler}>
<Box
sx={{
...style,
padding: 2,
maxHeight: "90vh",
overflowY: "auto",
marginLeft: 3,
marginRight: 3,
}}
>
{openPutaway ? (
<Box
component="form"
onSubmit={formProps.handleSubmit(onSubmitPutaway)}
>
<PutawayForm
itemDetail={itemDetail}
warehouse={warehouse!}
disabled={false}
/>
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
id="printButton"
type="button"
variant="contained"
color="primary"
sx={{ mt: 1 }}
onClick={onPrint}
>
{t("print")}
</Button>
<Button
id="putawaySubmit"
type="submit"
variant="contained"
color="primary"
sx={{ mt: 1 }}
>
{t("confirm putaway")}
</Button>
</Stack>
</Box>
) : (
<>
<Grid
container
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12}>
<Typography variant="h6" display="block" marginBlockEnd={1}>
{t("qc processing")}
</Typography>
</Grid>
<Grid item xs={12}>
<StockInFormVer2 itemDetail={itemDetail} disabled={false} />
</Grid>
</Grid>
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
id="stockInSubmit"
type="button"
variant="contained"
color="primary"
onClick={formProps.handleSubmit(onSubmitStockIn)}
>
{t("submitStockIn")}
</Button>
</Stack>
<Grid
container
justifyContent="flex-start"
alignItems="flex-start"
>
<QcFormVer2
qc={qc!}
itemDetail={itemDetail}
disabled={false}
qcItems={qcItems}
setQcItems={setQcItems}
/>
</Grid>
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
id="emailSupplier"
type="button"
variant="contained"
color="primary"
sx={{ mt: 1 }}
onClick={formProps.handleSubmit(onSubmitEmailSupplier)}
>
{t("email supplier")}
</Button>
<Button
id="qcSubmit"
type="button"
variant="contained"
color="primary"
sx={{ mt: 1 }}
onClick={formProps.handleSubmit(onSubmitQc)}
>
{t("confirm putaway")}
</Button>
</Stack>
</>
)}
</Box>
</Modal>
</FormProvider>
</>
);
};
export default PickQcStockInModalVer2;

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