Parcourir la source

[Prod Schedule] Update Detailed Prod Schedule

master
cyril.tsui il y a 1 mois
Parent
révision
cf4155b612
7 fichiers modifiés avec 271 ajouts et 61 suppressions
  1. +44
    -9
      src/app/api/scheduling/actions.ts
  2. +3
    -0
      src/app/api/scheduling/index.ts
  3. +1
    -0
      src/app/utils/formatUtil.ts
  4. +100
    -18
      src/components/DetailedScheduleDetail/DetailedScheduleDetailView.tsx
  5. +32
    -18
      src/components/DetailedScheduleDetail/ViewByFGDetails.tsx
  6. +89
    -15
      src/components/ScheduleTable/ScheduleTable.tsx
  7. +2
    -1
      src/i18n/zh/schedule.json

+ 44
- 9
src/app/api/scheduling/actions.ts Voir le fichier

@@ -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;
})

+ 3
- 0
src/app/api/scheduling/index.ts Voir le fichier

@@ -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 {


+ 1
- 0
src/app/utils/formatUtil.ts Voir le fichier

@@ -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", {});


+ 100
- 18
src/components/DetailedScheduleDetail/DetailedScheduleDetailView.tsx Voir le fichier

@@ -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>


+ 32
- 18
src/components/DetailedScheduleDetail/ViewByFGDetails.tsx Voir le fichier

@@ -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>
);


+ 89
- 15
src/components/ScheduleTable/ScheduleTable.tsx Voir le fichier

@@ -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>


+ 2
- 1
src/i18n/zh/schedule.json Voir le fichier

@@ -82,5 +82,6 @@
"mat": "物料",
"Product Count(s)": "產品數量",
"Schedule Period To": "排程期間至",
"Overall": "總計"
"Overall": "總計",
"Back": "返回"
}

Chargement…
Annuler
Enregistrer