작성자 | SHA1 | 메시지 | 날짜 |
---|---|---|---|
|
4883ead941 | Merge branch 'master' of https://git.2fi-solutions.com/derek/FPSMS-frontend | 1 주 전 |
|
62148f2765 | pickordersearch | 1 주 전 |
@@ -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>( | |||
@@ -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; |
@@ -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; |
@@ -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} />} | |||
</> | |||
); | |||
}; | |||
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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 | |||
} | |||
] |
@@ -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; |
@@ -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; |