@@ -4,7 +4,8 @@ import { convertObjToURLSearchParams } from "@/app/utils/commonUtil"; | |||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
import { BASE_API_URL } from "@/config/api"; | |||
import { cache } from "react"; | |||
import { ScheduleType } from "."; | |||
import { DetailedProdScheduleLineBomMaterialResult, DetailedProdScheduleLineResult, ScheduleType } from "."; | |||
import { revalidateTag } from "next/cache"; | |||
export interface SearchProdSchedule { | |||
scheduleAt?: string; | |||
@@ -31,11 +32,29 @@ export interface ProdScheduleResultByPage { | |||
records: ProdScheduleResult[]; | |||
} | |||
export interface ReleaseDetailProdScheduleInputs { | |||
export interface ReleaseProdScheduleInputs { | |||
id: number; | |||
demandQty: number; | |||
} | |||
export interface ReleaseProdScheduleResponse { | |||
id: number; | |||
code: string; | |||
entity: { | |||
prodScheduleLines: DetailedProdScheduleLineResult[]; | |||
}; | |||
message: string; | |||
} | |||
export interface SaveProdScheduleResponse { | |||
id: number; | |||
code: string; | |||
entity: { | |||
bomMaterials: DetailedProdScheduleLineBomMaterialResult[] | |||
}; | |||
message: string; | |||
} | |||
export const fetchProdSchedules = cache( | |||
async (data: SearchProdSchedule | null) => { | |||
const params = convertObjToURLSearchParams<SearchProdSchedule>(data); | |||
@@ -79,16 +98,32 @@ export const testDetailedSchedule = cache(async () => { | |||
); | |||
}); | |||
export const releaseProdScheduleLine = cache(async (data: ReleaseDetailProdScheduleInputs) => { | |||
return serverFetchJson( | |||
`${BASE_API_URL}/productionSchedule/releaseLine`, | |||
export const releaseProdScheduleLine = cache(async (data: ReleaseProdScheduleInputs) => { | |||
const response = serverFetchJson<ReleaseProdScheduleResponse>( | |||
`${BASE_API_URL}/productionSchedule/detail/detailed/releaseLine`, | |||
{ | |||
method: "POST", | |||
body: JSON.stringify(data), | |||
headers: { "Content-Type": "application/json" }, | |||
} | |||
); | |||
revalidateTag("prodSchedules"); | |||
return response; | |||
}) | |||
export const saveProdScheduleLine = cache(async (data: ReleaseProdScheduleInputs) => { | |||
const response = serverFetchJson<SaveProdScheduleResponse>( | |||
`${BASE_API_URL}/productionSchedule/detail/detailed/save`, | |||
{ | |||
method: "POST", | |||
body: JSON.stringify(data), | |||
headers: { "Content-Type": "application/json" }, | |||
next: { | |||
tags: ["prodSchedules"], | |||
}, | |||
} | |||
) | |||
); | |||
revalidateTag("prodSchedules"); | |||
return response; | |||
}) |
@@ -93,8 +93,11 @@ export interface DetailedProdScheduleLineResult { | |||
name: string; | |||
type: string; | |||
demandQty: number; | |||
bomOutputQty: number; | |||
prodTimeInMinute: DetailedProdScheduleLineProdTimeResult[]; | |||
priority: number; | |||
approved: boolean; | |||
proportion: number; | |||
} | |||
export interface DetailedProdScheduleLineBomMaterialResult { | |||
@@ -23,6 +23,7 @@ export const moneyFormatter = new Intl.NumberFormat("en-HK", { | |||
export const decimalFormatter = new Intl.NumberFormat("en-HK", { | |||
minimumFractionDigits: 2, | |||
maximumFractionDigits: 2, | |||
}); | |||
export const integerFormatter = new Intl.NumberFormat("en-HK", {}); | |||
@@ -7,6 +7,7 @@ import { | |||
FormProvider, | |||
SubmitErrorHandler, | |||
SubmitHandler, | |||
useFieldArray, | |||
useForm, | |||
} from "react-hook-form"; | |||
import { | |||
@@ -26,7 +27,10 @@ import DetailInfoCard from "@/components/DetailedScheduleDetail/DetailInfoCard"; | |||
import ViewByFGDetails, { | |||
// FGRecord, | |||
} from "@/components/DetailedScheduleDetail/ViewByFGDetails"; | |||
import { DetailedProdScheduleResult, ScheduleType } from "@/app/api/scheduling"; | |||
import { DetailedProdScheduleLineResult, DetailedProdScheduleResult, ScheduleType } from "@/app/api/scheduling"; | |||
import { releaseProdScheduleLine, saveProdScheduleLine } from "@/app/api/scheduling/actions"; | |||
import useUploadContext from "../UploadProvider/useUploadContext"; | |||
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | |||
// temp interface input | |||
// export interface SaveDetailedSchedule { | |||
@@ -59,14 +63,20 @@ const DetailedScheduleDetailView: React.FC<Props> = ({ | |||
const [tabIndex, setTabIndex] = useState(0); | |||
const { t } = useTranslation("schedule"); | |||
const router = useRouter(); | |||
const [isEdit, setIsEdit] = useState(false); | |||
const [isEdit, setIsEdit] = useState(false); | |||
const { setIsUploading } = useUploadContext() | |||
// console.log(typeId) | |||
const formProps = useForm<DetailedProdScheduleResult>({ | |||
defaultValues: defaultValues | |||
}); | |||
const errors = formProps.formState.errors; | |||
const lineFormProps = useFieldArray<DetailedProdScheduleResult>({ | |||
control: formProps.control, | |||
name: "prodScheduleLines" | |||
}) | |||
const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||
(_e, newValue) => { | |||
setTabIndex(newValue); | |||
@@ -74,6 +84,10 @@ const [isEdit, setIsEdit] = useState(false); | |||
[], | |||
); | |||
// const calNewProportion = useCallback((demandQty: number, bomOutputQty: number) => { | |||
// return ((demandQty ?? 0) / (bomOutputQty ?? 1)).toFixed(2) | |||
// }, []) | |||
// const [pagingController, setPagingController] = useState({ | |||
// pageNum: 1, | |||
// pageSize: 10, | |||
@@ -81,7 +95,7 @@ const [isEdit, setIsEdit] = useState(false); | |||
// }); | |||
const handleCancel = () => { | |||
router.replace(`/scheduling/Detail`); | |||
router.replace(`/scheduling/detailed`); | |||
}; | |||
const onSubmit = useCallback<SubmitHandler<DetailedProdScheduleResult>>( | |||
@@ -106,7 +120,7 @@ const [isEdit, setIsEdit] = useState(false); | |||
// multiple tabs | |||
const onSubmitError = useCallback<SubmitErrorHandler<DetailedProdScheduleResult>>( | |||
(errors) => {}, | |||
(errors) => { }, | |||
[], | |||
); | |||
@@ -114,10 +128,69 @@ const [isEdit, setIsEdit] = useState(false); | |||
setIsEdit(!isEdit); | |||
}; | |||
const onReleaseClick = useCallback(() => { | |||
const onReleaseClick = useCallback(async (row: DetailedProdScheduleLineResult) => { | |||
setIsUploading(true) | |||
try { | |||
const response = await releaseProdScheduleLine({ | |||
id: row.id, | |||
demandQty: row.demandQty | |||
}) | |||
if (response) { | |||
const index = formProps.getValues("prodScheduleLines").findIndex(ele => ele.id == row.id) | |||
// console.log(index, formProps.getValues(`prodScheduleLines.${index}.approved`)) | |||
// formProps.setValue(`prodScheduleLines.${index}.approved`, true) | |||
// formProps.setValue(`prodScheduleLines.${index}.jobNo`, response.code) | |||
formProps.setValue(`prodScheduleLines`, response.entity.prodScheduleLines.sort((a, b) => b.priority - a.priority)) | |||
// console.log(index, formProps.getValues(`prodScheduleLines.${index}.approved`)) | |||
} | |||
setIsUploading(false) | |||
} catch (e) { | |||
console.log(e) | |||
setIsUploading(false) | |||
} | |||
}, []) | |||
const [tempValue, setTempValue] = useState<string | number | null>(null) | |||
const onEditClick = useCallback((rowId: number) => { | |||
const row = formProps.getValues("prodScheduleLines").find(ele => ele.id == rowId) | |||
if (row) { | |||
setTempValue(row.demandQty) | |||
} | |||
}, []) | |||
const handleEditChange = useCallback((rowId: number, fieldName: keyof DetailedProdScheduleLineResult, newValue: number | string) => { | |||
const index = formProps.getValues("prodScheduleLines").findIndex(ele => ele.id == rowId) | |||
formProps.setValue(`prodScheduleLines.${index}.demandQty`, Number(newValue)) | |||
}, []) | |||
const onSaveClick = useCallback(async (row: DetailedProdScheduleLineResult) => { | |||
setIsUploading(true) | |||
try { | |||
const response = await saveProdScheduleLine({ | |||
id: row.id, | |||
demandQty: row.demandQty | |||
}) | |||
if (response) { | |||
const index = formProps.getValues("prodScheduleLines").findIndex(ele => ele.id == row.id) | |||
formProps.setValue(`prodScheduleLines.${index}.bomMaterials`, response.entity.bomMaterials) | |||
} | |||
setIsUploading(false) | |||
} catch (e) { | |||
console.log(e) | |||
setIsUploading(false) | |||
} | |||
}, []) | |||
const onCancelClick = useCallback(async (rowId: number) => { | |||
// if (tempValue) { | |||
const index = formProps.getValues("prodScheduleLines").findIndex(ele => ele.id == rowId) | |||
formProps.setValue(`prodScheduleLines.${index}.demandQty`, Number(tempValue)) | |||
// } | |||
}, [tempValue]) | |||
return ( | |||
<> | |||
<FormProvider {...formProps}> | |||
@@ -133,9 +206,10 @@ const [isEdit, setIsEdit] = useState(false); | |||
{/*</Grid>*/} | |||
<DetailInfoCard | |||
// recordDetails={formProps.formState.defaultValues} | |||
isEditing={isEdit} | |||
// isEditing={isEdit} | |||
isEditing={false} | |||
/> | |||
<Stack | |||
{/* <Stack | |||
direction="row" | |||
justifyContent="space-between" | |||
flexWrap="wrap" | |||
@@ -144,13 +218,13 @@ const [isEdit, setIsEdit] = useState(false); | |||
<Button | |||
variant="contained" | |||
onClick={onClickEdit} | |||
// startIcon={<Add />} | |||
//LinkComponent={Link} | |||
//href="qcCategory/create" | |||
// startIcon={<Add />} | |||
//LinkComponent={Link} | |||
//href="qcCategory/create" | |||
> | |||
{isEdit ? t("Save") : t("Edit")} | |||
</Button> | |||
</Stack> | |||
</Stack> */} | |||
{/* <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||
<Tab label={t("View By FG") + (tabIndex === 0 ? " (Selected)" : "")} iconPosition="end" /> | |||
@@ -162,24 +236,32 @@ const [isEdit, setIsEdit] = useState(false); | |||
</Typography> | |||
)} | |||
{/* {tabIndex === 0 && <ViewByFGDetails isEdit={isEdit} apiRef={apiRef} />} */} | |||
<ViewByFGDetails isEdit={isEdit} apiRef={apiRef} onReleaseClick={onReleaseClick} type={type}/> | |||
<ViewByFGDetails | |||
isEdit={true} | |||
apiRef={apiRef} | |||
onReleaseClick={onReleaseClick} | |||
onEditClick={onEditClick} | |||
handleEditChange={handleEditChange} | |||
onSaveClick={onSaveClick} | |||
onCancelClick={onCancelClick} | |||
type={type} /> | |||
{/* {tabIndex === 1 && <ViewByBomDetails isEdit={isEdit} apiRef={apiRef} isHideButton={true} />} */} | |||
<Stack direction="row" justifyContent="flex-end" gap={1}> | |||
<Button | |||
{/* <Button | |||
name="submit" | |||
variant="contained" | |||
startIcon={<Check />} | |||
type="submit" | |||
// disabled={submitDisabled} | |||
// disabled={submitDisabled} | |||
> | |||
{isEditMode ? t("Save") : t("Confirm")} | |||
</Button> | |||
</Button> */} | |||
<Button | |||
variant="outlined" | |||
startIcon={<Close />} | |||
startIcon={<ArrowBackIcon />} | |||
onClick={handleCancel} | |||
> | |||
{t("Cancel")} | |||
{t("Back")} | |||
</Button> | |||
</Stack> | |||
</Stack> | |||
@@ -23,7 +23,11 @@ type Props = { | |||
apiRef: MutableRefObject<GridApiCommunity>; | |||
isEdit: boolean; | |||
type: ScheduleType; | |||
onReleaseClick: () => void; | |||
onReleaseClick: (item: DetailedProdScheduleLineResult) => void; | |||
onEditClick: (rowId: number) => void; | |||
handleEditChange: (rowId: number, fieldName: keyof DetailedProdScheduleLineResult, newValue: number | string) => void; | |||
onSaveClick: (item: DetailedProdScheduleLineResult) => void; | |||
onCancelClick: (rowId: number) => void; | |||
}; | |||
// export type FGRecord = { | |||
@@ -35,7 +39,7 @@ type Props = { | |||
// purchaseQty?: number; | |||
// }; | |||
const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick }) => { | |||
const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick, onEditClick, handleEditChange, onSaveClick, onCancelClick }) => { | |||
const { | |||
t, | |||
i18n: { language }, | |||
@@ -43,8 +47,10 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick | |||
const { | |||
getValues, | |||
watch, | |||
formState: { errors, defaultValues, touchedFields }, | |||
} = useFormContext<DetailedProdScheduleResult>(); | |||
// const apiRef = useGridApiRef(); | |||
// const [pagingController, setPagingController] = useState([ | |||
@@ -128,6 +134,9 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick | |||
field: "type", | |||
label: t("type"), | |||
type: "read-only", | |||
renderCell: (row) => { | |||
return t(row.type); | |||
}, | |||
// editable: true, | |||
}, | |||
// { | |||
@@ -148,7 +157,7 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick | |||
{ | |||
field: "demandQty", | |||
label: t("Demand Qty"), | |||
type: "input", | |||
type: "input-number", | |||
style: { | |||
textAlign: "right", | |||
}, | |||
@@ -202,23 +211,28 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick | |||
/> | |||
</Grid> */} | |||
{/* {dayPeriod.map((date, index) => ( */} | |||
<Grid item xs={12}> | |||
{/* <Typography variant="overline" display="block" marginBlockEnd={1}> | |||
<Grid item xs={12}> | |||
{/* <Typography variant="overline" display="block" marginBlockEnd={1}> | |||
{`${t("FG Demand Date")}: ${date}`} | |||
</Typography> */} | |||
<ScheduleTable<DetailedProdScheduleLineResult> | |||
type={type} | |||
// items={fakeRecords[index]} // Use the corresponding records for the day | |||
items={getValues("prodScheduleLines")} // Use the corresponding records for the day | |||
columns={columns} | |||
// setPagingController={updatePagingController} | |||
// pagingController={pagingController[index]} | |||
isAutoPaging={false} | |||
isEditable={true} | |||
isEdit={isEdit} | |||
hasCollapse={true} | |||
/> | |||
</Grid> | |||
<ScheduleTable<DetailedProdScheduleLineResult> | |||
type={type} | |||
// items={fakeRecords[index]} // Use the corresponding records for the day | |||
items={getValues("prodScheduleLines")} // Use the corresponding records for the day | |||
columns={columns} | |||
// setPagingController={updatePagingController} | |||
// pagingController={pagingController[index]} | |||
isAutoPaging={false} | |||
isEditable={true} | |||
isEdit={isEdit} | |||
hasCollapse={true} | |||
onReleaseClick={onReleaseClick} | |||
onEditClick={onEditClick} | |||
handleEditChange={handleEditChange} | |||
onSaveClick={onSaveClick} | |||
onCancelClick={onCancelClick} | |||
/> | |||
</Grid> | |||
{/* ))} */} | |||
</Grid> | |||
); | |||
@@ -5,6 +5,7 @@ import React, { | |||
DetailedHTMLProps, | |||
HTMLAttributes, | |||
useEffect, | |||
useRef, | |||
useState, | |||
} from "react"; | |||
import Paper from "@mui/material/Paper"; | |||
@@ -37,6 +38,7 @@ import { | |||
ScheduleType, | |||
} from "@/app/api/scheduling"; | |||
import { defaultPagingController } from "../SearchResults/SearchResults"; | |||
import { useFormContext } from "react-hook-form"; | |||
export interface ResultWithId { | |||
id: string | number; | |||
@@ -81,6 +83,11 @@ interface Props<T extends ResultWithId> { | |||
isEditable: boolean; | |||
hasCollapse: boolean; | |||
type: ScheduleType; | |||
onReleaseClick?: (item: T) => void; | |||
onEditClick?: (rowId: number) => void; | |||
handleEditChange?: (rowId: number, fieldName: keyof T, newValue: number | string) => void; | |||
onSaveClick?: (item: T) => void; | |||
onCancelClick?: (rowId: number) => void; | |||
} | |||
function ScheduleTable<T extends ResultWithId>({ | |||
@@ -95,15 +102,23 @@ function ScheduleTable<T extends ResultWithId>({ | |||
isEdit = false, | |||
isEditable = true, | |||
hasCollapse = false, | |||
onReleaseClick = undefined, | |||
onEditClick = undefined, | |||
handleEditChange = undefined, | |||
onSaveClick = undefined, | |||
onCancelClick = undefined, | |||
}: Props<T>) { | |||
const [page, setPage] = useState(0); | |||
const [rowsPerPage, setRowsPerPage] = useState(10); | |||
const [editingRowId, setEditingRowId] = useState<number | null>(null); | |||
const [editedItems, setEditedItems] = useState<T[]>(items); | |||
const { t } = useTranslation("schedule"); | |||
console.log(items) | |||
useEffect(() => { | |||
setEditedItems(items); | |||
}, [items]); | |||
const handleChangePage = (_event: unknown, newPage: number) => { | |||
setPage(newPage); | |||
if (setPagingController && pagingController) { | |||
@@ -132,24 +147,42 @@ function ScheduleTable<T extends ResultWithId>({ | |||
const handleEditClick = (id: number) => { | |||
setEditingRowId(id); | |||
if (onEditClick) { | |||
onEditClick(id) | |||
} | |||
}; | |||
const handleSaveClick = (item: T) => { | |||
setEditingRowId(null); | |||
// Call API or any save logic here | |||
setEditedItems((prev) => | |||
prev.map((row) => (row.id === item.id ? { ...row } : row)), | |||
); | |||
if (onSaveClick) { | |||
onSaveClick(item) | |||
} else { | |||
setEditedItems((prev) => | |||
prev.map((row) => (row.id === item.id ? { ...row } : row)), | |||
); | |||
} | |||
}; | |||
const handleReleaseClick = (item: T) => { | |||
if (onReleaseClick) { | |||
onReleaseClick(item) | |||
} | |||
} | |||
const handleInputChange = ( | |||
id: number, | |||
field: keyof T, | |||
value: string | number[], | |||
// value: string | number[], | |||
value: string | number, | |||
) => { | |||
setEditedItems((prev) => | |||
prev.map((item) => (item.id === id ? { ...item, [field]: value } : item)), | |||
); | |||
if (handleEditChange) { | |||
handleEditChange(id, field, value) | |||
} else { | |||
setEditedItems((prev) => | |||
prev.map((item) => (item.id === id ? { ...item, [field]: value } : item)), | |||
); | |||
} | |||
}; | |||
const handleDeleteClick = (id: number) => { | |||
@@ -157,6 +190,14 @@ function ScheduleTable<T extends ResultWithId>({ | |||
setEditedItems((prev) => prev.filter((item) => item.id !== id)); | |||
}; | |||
const handleCancelClick = (id: number) => { | |||
if (onCancelClick) { | |||
onCancelClick(id) | |||
} | |||
setEditingRowId(null) | |||
} | |||
useEffect(() => { | |||
console.log("[debug] isEdit in table", isEdit); | |||
//TODO: switch all record to not in edit mode and save the changes | |||
@@ -188,7 +229,15 @@ function ScheduleTable<T extends ResultWithId>({ | |||
<TableRow hover tabIndex={-1} key={row.id}> | |||
{isDetailedType(type) && ( | |||
<TableCell> | |||
<IconButton disabled={!isEdit}> | |||
<IconButton | |||
color="primary" | |||
disabled={ | |||
// !(row as unknown as DetailedProdScheduleLineResult).bomMaterials.every(ele => (ele.availableQty ?? 0) >= (ele.demandQty ?? 0)) | |||
// || | |||
editingRowId === row.id | |||
|| (row as unknown as DetailedProdScheduleLineResult).approved} | |||
onClick={() => handleReleaseClick(row)} | |||
> | |||
<PlayCircleOutlineIcon /> | |||
</IconButton> | |||
</TableCell> | |||
@@ -208,7 +257,7 @@ function ScheduleTable<T extends ResultWithId>({ | |||
{isDetailedType(type) && isEditable && ( | |||
<IconButton | |||
disabled={!isEdit} | |||
onClick={() => setEditingRowId(null)} | |||
onClick={() => handleCancelClick(row.id as number)} | |||
> | |||
<CancelIcon /> | |||
</IconButton> | |||
@@ -232,20 +281,20 @@ function ScheduleTable<T extends ResultWithId>({ | |||
<> | |||
{isDetailedType(type) && isEditable && ( | |||
<IconButton | |||
disabled={!isEdit} | |||
disabled={!isEdit || (row as unknown as DetailedProdScheduleLineResult).approved} | |||
onClick={() => handleEditClick(row.id as number)} | |||
> | |||
<EditIcon /> | |||
</IconButton> | |||
)} | |||
{isDetailedType(type) && isEditable && ( | |||
{/* {isDetailedType(type) && isEditable && ( | |||
<IconButton | |||
disabled={!isEdit} | |||
onClick={() => handleDeleteClick(row.id as number)} | |||
> | |||
<DeleteIcon /> | |||
</IconButton> | |||
)} | |||
)} */} | |||
{hasCollapse && ( | |||
<IconButton | |||
aria-label="expand row" | |||
@@ -286,6 +335,29 @@ function ScheduleTable<T extends ResultWithId>({ | |||
} | |||
/> | |||
); | |||
case "input-number": | |||
return ( | |||
<TextField | |||
type="number" | |||
hiddenLabel={true} | |||
fullWidth | |||
defaultValue={row[columnName] as string} | |||
onChange={(e) => { | |||
handleInputChange( | |||
row.id as number, | |||
columnName, | |||
e.target.value, | |||
) | |||
}} | |||
// onChange={(e) => | |||
// handleInputChange( | |||
// row.id as number, | |||
// columnName, | |||
// e.target.value, | |||
// ) | |||
// } | |||
/> | |||
); | |||
// case 'multi-select': | |||
// //TODO: May need update if use | |||
// return ( | |||
@@ -297,7 +369,9 @@ function ScheduleTable<T extends ResultWithId>({ | |||
// /> | |||
// ); | |||
case "read-only": | |||
return <span>{row[columnName] as string}</span>; | |||
return column.renderCell ? ( | |||
<div style={column.style}>{column.renderCell(row)}</div> | |||
) : <span>{row[columnName] as string}</span>; | |||
default: | |||
return null; // Handle any default case if needed | |||
} | |||
@@ -330,8 +404,8 @@ function ScheduleTable<T extends ResultWithId>({ | |||
<BomMaterialTable | |||
type={type} | |||
bomMaterial={ | |||
isDetailedType(type) ? (row as unknown as RoughProdScheduleLineResultByFg).bomMaterials | |||
: (row as unknown as DetailedProdScheduleLineResult).bomMaterials | |||
isDetailedType(type) ? (row as unknown as RoughProdScheduleLineResultByFg).bomMaterials | |||
: (row as unknown as DetailedProdScheduleLineResult).bomMaterials | |||
} | |||
/> | |||
</TableCell> | |||
@@ -82,5 +82,6 @@ | |||
"mat": "物料", | |||
"Product Count(s)": "產品數量", | |||
"Schedule Period To": "排程期間至", | |||
"Overall": "總計" | |||
"Overall": "總計", | |||
"Back": "返回" | |||
} |