@@ -1,57 +0,0 @@ | |||
import { Metadata } from "next"; | |||
// import { getServerI18n, I18nProvider } from "@/i18n"; | |||
import { getServerI18n, I18nProvider } from "../../../../../i18n"; | |||
import Typography from "@mui/material/Typography"; | |||
// import { fetchQcItemDetails, preloadQcItem } from "@/app/api/settings/qcItem"; | |||
// import QcItemSave from "@/components/QcItemSave"; | |||
import { | |||
fetchQcItemDetails, | |||
preloadQcItem, | |||
} from "../../../../../app/api/settings/qcItem"; | |||
import QcItemSave from "../../../../../components/QcItemSave"; | |||
import { isArray } from "lodash"; | |||
import { notFound } from "next/navigation"; | |||
// import { ServerFetchError } from "@/app/utils/fetchUtil"; | |||
// import DetailScheduleDetail from "@/components/DetailScheduleDetail"; | |||
import { ServerFetchError } from "../../../../../app/utils/fetchUtil"; | |||
import DetailScheduleDetail from "../../../../../components/DetailScheduleDetail"; | |||
export const metadata: Metadata = { | |||
title: "Qc Item", | |||
}; | |||
interface Props { | |||
searchParams: { [key: string]: string | string[] | undefined }; | |||
} | |||
const DetailScheduling: React.FC<Props> = async ({ searchParams }) => { | |||
const { t } = await getServerI18n("schedule"); | |||
const id = searchParams["id"]; | |||
if (!id || isArray(id)) { | |||
notFound(); | |||
} | |||
// try { | |||
// await fetchQcItemDetails(id) | |||
// } catch (e) { | |||
// if (e instanceof ServerFetchError && (e.response?.status === 404 || e.response?.status === 400)) { | |||
// console.log(e) | |||
// notFound(); | |||
// } | |||
// } | |||
return ( | |||
<> | |||
<Typography variant="h4" marginInlineEnd={2}> | |||
{t("FG Production Schedule")} | |||
</Typography> | |||
<I18nProvider namespaces={["schedule", "common", "project"]}> | |||
<DetailScheduleDetail id={id} /> | |||
</I18nProvider> | |||
</> | |||
); | |||
}; | |||
export default DetailScheduling; |
@@ -0,0 +1,57 @@ | |||
import { Metadata } from "next"; | |||
import { getServerI18n, I18nProvider } from "@/i18n"; | |||
// import { getServerI18n, I18nProvider } from "../../../../../i18n"; | |||
import Typography from "@mui/material/Typography"; | |||
import { isArray, parseInt } from "lodash"; | |||
import { notFound } from "next/navigation"; | |||
import { SearchParams, ServerFetchError } from "@/app/utils/fetchUtil"; | |||
import DetailedScheduleDetail from "@/components/DetailedScheduleDetail"; | |||
import { type } from "os"; | |||
import { fetchDetailedProdScheduleDetail } from "@/app/api/scheduling"; | |||
import { Suspense } from "react"; | |||
// import { ServerFetchError } from "../../../../../app/utils/fetchUtil"; | |||
// import DetailedScheduleDetail from "../../../../../components/DetailedScheduleDetail"; | |||
export const metadata: Metadata = { | |||
title: "FG Production Schedule", | |||
}; | |||
// interface Props { | |||
// searchParams: { [key: string]: string | string[] | undefined }; | |||
// } | |||
type Props = SearchParams; | |||
const DetailScheduling: React.FC<Props> = async ({ searchParams }) => { | |||
const { t } = await getServerI18n("schedule"); | |||
const id = searchParams["id"]; | |||
const type = "detailed" | |||
if (!id || isArray(id) || !isFinite(parseInt(id))) { | |||
notFound(); | |||
} | |||
try { | |||
await fetchDetailedProdScheduleDetail(parseInt(id)) | |||
} catch (e) { | |||
if (e instanceof ServerFetchError && (e.response?.status === 404 || e.response?.status === 400)) { | |||
console.log(e) | |||
notFound(); | |||
} | |||
} | |||
return ( | |||
<> | |||
<Typography variant="h4" marginInlineEnd={2}> | |||
{t("FG Production Schedule")} | |||
</Typography> | |||
<I18nProvider namespaces={["schedule", "common"]}> | |||
<Suspense fallback={<DetailedScheduleDetail.Loading />}> | |||
<DetailedScheduleDetail type={type} id={parseInt(id)} /> | |||
</Suspense> | |||
</I18nProvider> | |||
</> | |||
); | |||
}; | |||
export default DetailScheduling; |
@@ -1,8 +1,8 @@ | |||
// import { TypeEnum } from "@/app/utils/typeEnum"; | |||
// import DetailSchedule from "@/components/DetailSchedule"; | |||
// import DetailedSchedule from "@/components/DetailedSchedule"; | |||
// import { getServerI18n } from "@/i18n"; | |||
import DetailSchedule from "../../../../components/DetailSchedule"; | |||
import DetailedSchedule from "../../../../components/DetailedSchedule"; | |||
import { getServerI18n } from "../../../../i18n"; | |||
import { I18nProvider } from "@/i18n"; | |||
import Stack from "@mui/material/Stack"; | |||
@@ -32,8 +32,8 @@ const DetailScheduling: React.FC = async () => { | |||
</Typography> | |||
</Stack> | |||
<I18nProvider namespaces={["schedule", "common"]}> | |||
<Suspense fallback={<DetailSchedule.Loading />}> | |||
<DetailSchedule type={type} /> | |||
<Suspense fallback={<DetailedSchedule.Loading />}> | |||
<DetailedSchedule type={type} /> | |||
</Suspense> | |||
</I18nProvider> | |||
</> |
@@ -12,7 +12,7 @@ import RoughScheduleDetailView from "@/components/RoughScheduleDetail"; | |||
import { SearchParams, ServerFetchError } from "@/app/utils/fetchUtil"; | |||
import { isArray, parseInt } from "lodash"; | |||
import { notFound } from "next/navigation"; | |||
import { fetchProdScheduleDetail } from "@/app/api/scheduling"; | |||
import { fetchRoughProdScheduleDetail } from "@/app/api/scheduling"; | |||
export const metadata: Metadata = { | |||
title: "Demand Forecast Detail", | |||
@@ -30,7 +30,7 @@ const roughSchedulingDetail: React.FC<Props> = async ({ searchParams }) => { | |||
} | |||
try { | |||
await fetchProdScheduleDetail(parseInt(id)); | |||
await fetchRoughProdScheduleDetail(parseInt(id)); | |||
} catch (e) { | |||
if ( | |||
e instanceof ServerFetchError && | |||
@@ -31,6 +31,11 @@ export interface ProdScheduleResultByPage { | |||
records: ProdScheduleResult[]; | |||
} | |||
export interface ReleaseDetailProdScheduleInputs { | |||
id: number; | |||
demandQty: number; | |||
} | |||
export const fetchProdSchedules = cache( | |||
async (data: SearchProdSchedule | null) => { | |||
const params = convertObjToURLSearchParams<SearchProdSchedule>(data); | |||
@@ -61,9 +66,9 @@ export const testRoughSchedule = cache(async () => { | |||
); | |||
}); | |||
export const testDetailSchedule = cache(async () => { | |||
export const testDetailedSchedule = cache(async () => { | |||
return serverFetchJson( | |||
`${BASE_API_URL}/productionSchedule/testDetailSchedule`, | |||
`${BASE_API_URL}/productionSchedule/testDetailedSchedule`, | |||
{ | |||
method: "GET", | |||
headers: { "Content-Type": "application/json" }, | |||
@@ -73,3 +78,17 @@ export const testDetailSchedule = cache(async () => { | |||
}, | |||
); | |||
}); | |||
export const releaseProdScheduleLine = cache(async (data: ReleaseDetailProdScheduleInputs) => { | |||
return serverFetchJson( | |||
`${BASE_API_URL}/productionSchedule/releaseLine`, | |||
{ | |||
method: "POST", | |||
body: JSON.stringify(data), | |||
headers: { "Content-Type": "application/json" }, | |||
next: { | |||
tags: ["prodSchedules"], | |||
}, | |||
} | |||
) | |||
}) |
@@ -5,85 +5,129 @@ import "server-only"; | |||
export type ScheduleType = "all" | "rough" | "detailed" | "manual"; | |||
// Rough | |||
export interface RoughProdScheduleResult { | |||
id: number; | |||
scheduleAt: number[]; | |||
schedulePeriod: number[]; | |||
schedulePeriodTo: number[]; | |||
totalEstProdCount: number; | |||
totalFGType: number; | |||
type: string; | |||
prodScheduleLinesByFg: RoughProdScheduleLineResultByFg[]; | |||
prodScheduleLinesByFgByDate: { | |||
[assignDate: number]: RoughProdScheduleLineResultByFg[]; | |||
}; | |||
prodScheduleLinesByBom: RoughProdScheduleLineResultByBom[]; | |||
prodScheduleLinesByBomByDate: { | |||
[assignDate: number]: RoughProdScheduleLineResultByBomByDate[]; | |||
}; | |||
id: number; | |||
scheduleAt: number[]; | |||
schedulePeriod: number[]; | |||
schedulePeriodTo: number[]; | |||
totalEstProdCount: number; | |||
totalFGType: number; | |||
type: string; | |||
prodScheduleLinesByFg: RoughProdScheduleLineResultByFg[]; | |||
prodScheduleLinesByFgByDate: { | |||
[assignDate: number]: RoughProdScheduleLineResultByFg[]; | |||
}; | |||
prodScheduleLinesByBom: RoughProdScheduleLineResultByBom[]; | |||
prodScheduleLinesByBomByDate: { | |||
[assignDate: number]: RoughProdScheduleLineResultByBomByDate[]; | |||
}; | |||
} | |||
export interface RoughProdScheduleLineResultByFg { | |||
id: number; | |||
code: string; | |||
name: string; | |||
type: string; | |||
availableQty: number; | |||
prodQty: number; | |||
lastMonthAvgSales: number; | |||
estCloseBal: number; | |||
priority: number; | |||
assignDate: number; | |||
bomMaterials: RoughProdScheduleLineBomMaterialResult[]; | |||
id: number; | |||
code: string; | |||
name: string; | |||
type: string; | |||
availableQty: number; | |||
prodQty: number; | |||
lastMonthAvgSales: number; | |||
estCloseBal: number; | |||
priority: number; | |||
assignDate: number; | |||
bomMaterials: RoughProdScheduleLineBomMaterialResult[]; | |||
} | |||
export interface RoughProdScheduleLineBomMaterialResult { | |||
id: number; | |||
code: string; | |||
name: string; | |||
type: string; | |||
availableQty: number; | |||
demandQty: number; | |||
uomName: string; | |||
id: number; | |||
code: string; | |||
name: string; | |||
type: string; | |||
availableQty: number; | |||
demandQty: number; | |||
uomName: string; | |||
} | |||
export interface RoughProdScheduleLineResultByBom { | |||
id: number; | |||
code: string; | |||
name: string; | |||
type: string; | |||
availableQty: number; | |||
totalDemandQty: number; | |||
demandQty1: number; | |||
demandQty2: number; | |||
demandQty3: number; | |||
demandQty4: number; | |||
demandQty5: number; | |||
demandQty6: number; | |||
demandQty7: number; | |||
uomName: string; | |||
id: number; | |||
code: string; | |||
name: string; | |||
type: string; | |||
availableQty: number; | |||
totalDemandQty: number; | |||
demandQty1: number; | |||
demandQty2: number; | |||
demandQty3: number; | |||
demandQty4: number; | |||
demandQty5: number; | |||
demandQty6: number; | |||
demandQty7: number; | |||
uomName: string; | |||
} | |||
export interface RoughProdScheduleLineResultByBomByDate { | |||
id: number; | |||
code: string; | |||
name: string; | |||
type: string; | |||
availableQty: number; | |||
demandQty: number; | |||
assignDate: number; | |||
uomName: string; | |||
id: number; | |||
code: string; | |||
name: string; | |||
type: string; | |||
availableQty: number; | |||
demandQty: number; | |||
assignDate: number; | |||
uomName: string; | |||
} | |||
export const fetchProdScheduleDetail = cache(async (id: number) => { | |||
return serverFetchJson<RoughProdScheduleResult>( | |||
`${BASE_API_URL}/productionSchedule/detail/${id}`, | |||
{ | |||
method: "GET", | |||
headers: { "Content-Type": "application/json" }, | |||
next: { | |||
tags: ["prodSchedule"], | |||
}, | |||
}, | |||
); | |||
}); | |||
// Detailed | |||
export interface DetailedProdScheduleResult { | |||
id: number; | |||
scheduleAt: number[]; | |||
totalEstProdCount: number; | |||
totalFGType: number; | |||
prodScheduleLines: DetailedProdScheduleLineResult[]; | |||
} | |||
export interface DetailedProdScheduleLineResult { | |||
id: number; | |||
bomMaterials: DetailedProdScheduleLineBomMaterialResult[]; | |||
jobNo: string; | |||
code: string; | |||
name: string; | |||
type: string; | |||
demandQty: number; | |||
prodTimeInMinute: DetailedProdScheduleLineProdTimeResult[]; | |||
priority: number; | |||
} | |||
export interface DetailedProdScheduleLineBomMaterialResult { | |||
id: number; | |||
code: string; | |||
name: string; | |||
type: string; | |||
availableQty: number; | |||
demandQty: number; | |||
} | |||
export interface DetailedProdScheduleLineProdTimeResult { | |||
equipName: string; | |||
totalMinutes: number; | |||
} | |||
// API | |||
export const fetchRoughProdScheduleDetail = cache(async (id: number) => { | |||
return serverFetchJson<RoughProdScheduleResult>(`${BASE_API_URL}/productionSchedule/detail/rough/${id}`, { | |||
method: "GET", | |||
headers: { "Content-Type": "application/json" }, | |||
next: { | |||
tags: ["prodSchedule"] | |||
} | |||
}) | |||
}) | |||
export const fetchDetailedProdScheduleDetail = cache(async (id: number) => { | |||
return serverFetchJson<DetailedProdScheduleResult>(`${BASE_API_URL}/productionSchedule/detail/detailed/${id}`, { | |||
method: "GET", | |||
headers: { "Content-Type": "application/json" }, | |||
next: { | |||
tags: ["prodSchedule"] | |||
} | |||
}) | |||
}) |
@@ -66,6 +66,36 @@ export const dayjsToDateString = (date: Dayjs) => { | |||
return date.format(OUTPUT_DATE_FORMAT); | |||
}; | |||
export const minutesToHoursMinutes = (minutes: number): string => { | |||
const defaultHrStr = "hr" | |||
const defaultMinStr = "min" | |||
if (minutes == 0) { | |||
return `0 ${defaultMinStr}` | |||
} | |||
const hrs = Math.floor(minutes / 60) | |||
const mins = minutes % 60 | |||
let finalHrStr: string = "" | |||
if (hrs > 1) { | |||
finalHrStr = `${hrs} ${defaultHrStr}s` | |||
} else if (hrs == 1) { | |||
finalHrStr = `1 ${defaultHrStr}` | |||
} | |||
let finalMinStr: string = "" | |||
if (mins > 1) { | |||
finalMinStr = `${mins} ${defaultMinStr}s` | |||
} else if (mins == 1) { | |||
finalMinStr = `1 ${defaultMinStr}` | |||
} | |||
let colon = finalHrStr.length > 0 && finalMinStr.length > 0 ? ":" : "" | |||
return `${finalHrStr} ${colon} ${finalMinStr}`.trim() | |||
} | |||
export const stockInLineStatusMap: { [status: string]: number } = { | |||
draft: 0, | |||
pending: 1, | |||
@@ -1 +0,0 @@ | |||
export { default } from "./DetailScheduleWrapper"; |
@@ -1,37 +0,0 @@ | |||
import { CreateItemInputs } from "@/app/api/settings/item/actions"; | |||
import { fetchItem } from "@/app/api/settings/item"; | |||
import GeneralLoading from "@/components/General/GeneralLoading"; | |||
import DetailScheduleDetailView from "@/components/DetailScheduleDetail/DetailScheudleDetailView"; | |||
interface SubComponents { | |||
Loading: typeof GeneralLoading; | |||
} | |||
type EditDetailScheduleDetailProps = { | |||
id?: string | number; | |||
}; | |||
type Props = EditDetailScheduleDetailProps; | |||
const DetailScheduleDetailWrapper: React.FC<Props> & SubComponents = async ({ | |||
id, | |||
}) => { | |||
const defaultValues = { | |||
id: 1, | |||
productionDate: "2025-05-07", | |||
totalJobOrders: 13, | |||
totalProductionQty: 21000, | |||
}; | |||
return ( | |||
<DetailScheduleDetailView | |||
isEditMode={Boolean(id)} | |||
defaultValues={defaultValues} | |||
// qcChecks={qcChecks || []} | |||
/> | |||
); | |||
}; | |||
DetailScheduleDetailWrapper.Loading = GeneralLoading; | |||
export default DetailScheduleDetailWrapper; |
@@ -1 +0,0 @@ | |||
export { default } from "./DetailScheduleDetailWrapper"; |
@@ -5,7 +5,7 @@ import Stack from "@mui/material/Stack"; | |||
import React from "react"; | |||
// Can make this nicer | |||
export const DetailScheduleLoading: React.FC = () => { | |||
export const DetailedScheduleLoading: React.FC = () => { | |||
return ( | |||
<> | |||
<Card> | |||
@@ -37,4 +37,4 @@ export const DetailScheduleLoading: React.FC = () => { | |||
); | |||
}; | |||
export default DetailScheduleLoading; | |||
export default DetailedScheduleLoading; |
@@ -2,18 +2,10 @@ | |||
import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||
import SearchBox, { Criterion } from "../SearchBox"; | |||
import { ItemsResult } from "@/app/api/settings/item"; | |||
import SearchResults, { Column } from "../SearchResults"; | |||
import { EditNote } from "@mui/icons-material"; | |||
import { useRouter, useSearchParams } from "next/navigation"; | |||
import { GridDeleteIcon } from "@mui/x-data-grid"; | |||
import { TypeEnum } from "@/app/utils/typeEnum"; | |||
import axios from "axios"; | |||
import { BASE_API_URL, NEXT_PUBLIC_API_URL } from "@/config/api"; | |||
import { useTranslation } from "react-i18next"; | |||
import axiosInstance from "@/app/(main)/axios/axiosInstance"; | |||
import Qs from "qs"; | |||
import EditableSearchResults from "@/components/SearchResults/EditableSearchResults"; // Make sure to import Qs | |||
import { ScheduleType } from "@/app/api/scheduling"; | |||
import { | |||
ProdScheduleResult, | |||
@@ -107,7 +99,7 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => { | |||
const onDetailClick = (record: ProdScheduleResult) => { | |||
console.log("[debug] record", record); | |||
router.push(`/scheduling/detail/edit?id=${record.id}`); | |||
router.push(`/scheduling/detailed/edit?id=${record.id}`); | |||
}; | |||
const columns = useMemo<Column<ProdScheduleResult>[]>( |
@@ -1,18 +1,18 @@ | |||
import React from "react"; | |||
import { DetailScheduleLoading } from "./DetailScheduleLoading"; | |||
import DSOverview from "./DetailScheduleSearchView"; | |||
import { DetailedScheduleLoading } from "./DetailedScheduleLoading"; | |||
import DSOverview from "./DetailedScheduleSearchView"; | |||
import { ScheduleType } from "@/app/api/scheduling"; | |||
import { SearchProdSchedule } from "@/app/api/scheduling/actions"; | |||
interface SubComponents { | |||
Loading: typeof DetailScheduleLoading; | |||
Loading: typeof DetailedScheduleLoading; | |||
} | |||
type Props = { | |||
type: ScheduleType; | |||
}; | |||
const DetailScheduleWrapper: React.FC<Props> & SubComponents = async ({ | |||
const DetailedScheduleWrapper: React.FC<Props> & SubComponents = async ({ | |||
type, | |||
}) => { | |||
const defaultInputs: SearchProdSchedule = { | |||
@@ -22,6 +22,6 @@ const DetailScheduleWrapper: React.FC<Props> & SubComponents = async ({ | |||
return <DSOverview type={type} defaultInputs={defaultInputs} />; | |||
}; | |||
DetailScheduleWrapper.Loading = DetailScheduleLoading; | |||
DetailedScheduleWrapper.Loading = DetailedScheduleLoading; | |||
export default DetailScheduleWrapper; | |||
export default DetailedScheduleWrapper; |
@@ -0,0 +1 @@ | |||
export { default } from "./DetailedScheduleWrapper"; |
@@ -17,13 +17,14 @@ import { InputDataGridProps, TableRow } from "../InputDataGrid/InputDataGrid"; | |||
import { TypeEnum } from "@/app/utils/typeEnum"; | |||
import { CreateItemInputs } from "@/app/api/settings/item/actions"; | |||
import { NumberInputProps } from "@/components/CreateItem/NumberInputProps"; | |||
import { integerFormatter } from "@/app/utils/formatUtil"; | |||
import { SaveDetailSchedule } from "./DetailScheudleDetailView"; | |||
import { arrayToDateString, integerFormatter } from "@/app/utils/formatUtil"; | |||
import { DetailedProdScheduleResult } from "@/app/api/scheduling"; | |||
// import { SaveDetailedSchedule } from "./DetailedScheduleDetailView"; | |||
// temp interface input | |||
type Props = { | |||
// recordDetails: SaveDetailSchedule; | |||
// recordDetails: SaveDetailedSchedule; | |||
isEditing: boolean; | |||
}; | |||
@@ -39,14 +40,15 @@ const DetailInfoCard: React.FC<Props> = ({ | |||
const { | |||
control, | |||
register, | |||
getValues, | |||
formState: { errors, defaultValues, touchedFields }, | |||
} = useFormContext<SaveDetailSchedule>(); | |||
} = useFormContext<DetailedProdScheduleResult>(); | |||
const [details, setDetails] = useState<SaveDetailSchedule | undefined>(undefined); | |||
// const [details, setDetails] = useState<DetailedProdScheduleResult | undefined>(undefined); | |||
useEffect(() => { | |||
console.log("[debug] record details", defaultValues) | |||
setDetails(defaultValues as SaveDetailSchedule); | |||
// setDetails(defaultValues as DetailedProdScheduleResult); | |||
}, [defaultValues]) | |||
useEffect(() => { | |||
@@ -65,9 +67,10 @@ const DetailInfoCard: React.FC<Props> = ({ | |||
<TextField | |||
label={t("Production Date")} | |||
fullWidth | |||
{...register("productionDate", { | |||
required: "name required!", | |||
})} | |||
// {...register("scheduleAt", { | |||
// required: "Schedule At required!", | |||
// })} | |||
defaultValue={`${arrayToDateString(getValues("scheduleAt"))}`} | |||
// defaultValue={details?.scheduledPeriod} | |||
disabled={!isEditing} | |||
// error={Boolean(errors.name)} | |||
@@ -78,9 +81,15 @@ const DetailInfoCard: React.FC<Props> = ({ | |||
<TextField | |||
label={t("Total Job Orders")} | |||
fullWidth | |||
{...register("totalJobOrders", { | |||
required: "code required!", | |||
})} | |||
// {...register("totalFGType", { | |||
// required: "Total FG Type required!", | |||
// })} | |||
// TODO: May update by table row qty | |||
defaultValue={ | |||
typeof getValues("totalFGType") == "number" | |||
? integerFormatter.format(getValues("totalFGType")) | |||
: getValues("totalFGType") | |||
} | |||
// defaultValue={details?.productCount} | |||
disabled={!isEditing} | |||
// error={Boolean(errors.code)} | |||
@@ -89,7 +98,7 @@ const DetailInfoCard: React.FC<Props> = ({ | |||
</Grid> | |||
<Grid item xs={6}> | |||
<Controller | |||
name="totalProductionQty" | |||
name="totalEstProdCount" | |||
control={control} | |||
render={({ field }) => ( | |||
<TextField | |||
@@ -97,11 +106,17 @@ const DetailInfoCard: React.FC<Props> = ({ | |||
label={t("Total Production Qty")} | |||
fullWidth | |||
disabled={!isEditing} | |||
value={ | |||
// TODO: May update by table demand qty | |||
defaultValue={ | |||
typeof field.value == "number" | |||
? integerFormatter.format(field.value) | |||
: field.value | |||
} | |||
// value={ | |||
// typeof field.value == "number" | |||
// ? integerFormatter.format(field.value) | |||
// : field.value | |||
// } | |||
// defaultValue={typeof (details?.productionCount) == "number" ? integerFormatter.format(details?.productionCount) : details?.productionCount} | |||
// error={Boolean(errors.type)} | |||
// helperText={errors.type?.message} |
@@ -3,17 +3,12 @@ | |||
import { useCallback, useEffect, useMemo, useState } from "react"; | |||
import { useRouter, useSearchParams } from "next/navigation"; | |||
import { useTranslation } from "react-i18next"; | |||
import { | |||
//SaveDetailSchedule, | |||
saveItem, | |||
} from "@/app/api/settings/item/actions"; | |||
import { | |||
FormProvider, | |||
SubmitErrorHandler, | |||
SubmitHandler, | |||
useForm, | |||
} from "react-hook-form"; | |||
import { deleteDialog } from "../Swal/CustomAlerts"; | |||
import { | |||
Box, | |||
Button, | |||
@@ -26,38 +21,35 @@ import { | |||
Typography, | |||
} from "@mui/material"; | |||
import { Add, Check, Close, EditNote } from "@mui/icons-material"; | |||
import { ItemQc, ItemsResult } from "@/app/api/settings/item"; | |||
import { useGridApiRef } from "@mui/x-data-grid"; | |||
import ProductDetails from "@/components/CreateItem/ProductDetails"; | |||
import DetailInfoCard from "@/components/DetailScheduleDetail/DetailInfoCard"; | |||
import DetailInfoCard from "@/components/DetailedScheduleDetail/DetailInfoCard"; | |||
import ViewByFGDetails, { | |||
FGRecord, | |||
} from "@/components/DetailScheduleDetail/ViewByFGDetails"; | |||
import ViewByBomDetails from "@/components/DetailScheduleDetail/ViewByBomDetails"; | |||
import EditableSearchResults, { | |||
Column, | |||
} from "@/components/SearchResults/EditableSearchResults"; | |||
// FGRecord, | |||
} from "@/components/DetailedScheduleDetail/ViewByFGDetails"; | |||
import { DetailedProdScheduleResult, ScheduleType } from "@/app/api/scheduling"; | |||
// temp interface input | |||
export interface SaveDetailSchedule { | |||
id: number; | |||
productionDate: string; | |||
totalJobOrders: number; | |||
totalProductionQty: number; | |||
} | |||
// export interface SaveDetailedSchedule { | |||
// id: number; | |||
// productionDate: string; | |||
// totalJobOrders: number; | |||
// totalProductionQty: number; | |||
// } | |||
type Props = { | |||
isEditMode: boolean; | |||
// type: TypeEnum; | |||
defaultValues: Partial<SaveDetailSchedule> | undefined; | |||
defaultValues: Partial<DetailedProdScheduleResult> | undefined; | |||
// qcChecks: ItemQc[] | |||
type: ScheduleType; | |||
}; | |||
const DetailScheduleDetailView: React.FC<Props> = ({ | |||
const DetailedScheduleDetailView: React.FC<Props> = ({ | |||
isEditMode, | |||
// type, | |||
defaultValues, | |||
// qcChecks | |||
// qcChecks, | |||
type | |||
}) => { | |||
// console.log(type) | |||
const apiRef = useGridApiRef(); | |||
@@ -67,43 +59,11 @@ const DetailScheduleDetailView: React.FC<Props> = ({ | |||
const [tabIndex, setTabIndex] = useState(0); | |||
const { t } = useTranslation("schedule"); | |||
const router = useRouter(); | |||
const [isEdit, setIsEdit] = useState(false); | |||
//const title = "Demand Forecast Detail" | |||
const [mode, redirPath] = useMemo(() => { | |||
// var typeId = TypeEnum.CONSUMABLE_ID | |||
let title = ""; | |||
let mode = ""; | |||
let redirPath = ""; | |||
// if (type === TypeEnum.MATERIAL) { | |||
// typeId = TypeEnum.MATERIAL_ID | |||
// title = "Material"; | |||
// redirPath = "/settings/material"; | |||
// } | |||
// if (type === TypeEnum.PRODUCT) { | |||
// typeId = TypeEnum.PRODUCT_ID | |||
title = "Product"; | |||
redirPath = "scheduling/detail/edit"; | |||
// } | |||
// if (type === TypeEnum.BYPRODUCT) { | |||
// typeId = TypeEnum.BYPRODUCT_ID | |||
// title = "By-Product"; | |||
// redirPath = "/settings/byProduct"; | |||
// } | |||
if (isEditMode) { | |||
mode = "Edit"; | |||
} else { | |||
mode = "Create"; | |||
} | |||
return [mode, redirPath]; | |||
}, [isEditMode]); | |||
const [isEdit, setIsEdit] = useState(false); | |||
// console.log(typeId) | |||
const formProps = useForm<SaveDetailSchedule>({ | |||
defaultValues: defaultValues ? defaultValues : { | |||
id: 1, | |||
productionDate: "2025-05-07", | |||
totalJobOrders: 13, | |||
totalProductionQty: 21000, | |||
} as SaveDetailSchedule, | |||
const formProps = useForm<DetailedProdScheduleResult>({ | |||
defaultValues: defaultValues | |||
}); | |||
const errors = formProps.formState.errors; | |||
@@ -114,17 +74,17 @@ const DetailScheduleDetailView: React.FC<Props> = ({ | |||
[], | |||
); | |||
const [pagingController, setPagingController] = useState({ | |||
pageNum: 1, | |||
pageSize: 10, | |||
totalCount: 0, | |||
}); | |||
// const [pagingController, setPagingController] = useState({ | |||
// pageNum: 1, | |||
// pageSize: 10, | |||
// totalCount: 0, | |||
// }); | |||
const handleCancel = () => { | |||
router.replace(`/scheduling/Detail`); | |||
}; | |||
const onSubmit = useCallback<SubmitHandler<SaveDetailSchedule>>( | |||
const onSubmit = useCallback<SubmitHandler<DetailedProdScheduleResult>>( | |||
async (data, event) => { | |||
const hasErrors = false; | |||
console.log(errors); | |||
@@ -145,7 +105,7 @@ const DetailScheduleDetailView: React.FC<Props> = ({ | |||
); | |||
// multiple tabs | |||
const onSubmitError = useCallback<SubmitErrorHandler<SaveDetailSchedule>>( | |||
const onSubmitError = useCallback<SubmitErrorHandler<DetailedProdScheduleResult>>( | |||
(errors) => {}, | |||
[], | |||
); | |||
@@ -154,6 +114,10 @@ const DetailScheduleDetailView: React.FC<Props> = ({ | |||
setIsEdit(!isEdit); | |||
}; | |||
const onReleaseClick = useCallback(() => { | |||
}, []) | |||
return ( | |||
<> | |||
<FormProvider {...formProps}> | |||
@@ -198,7 +162,7 @@ const DetailScheduleDetailView: React.FC<Props> = ({ | |||
</Typography> | |||
)} | |||
{/* {tabIndex === 0 && <ViewByFGDetails isEdit={isEdit} apiRef={apiRef} />} */} | |||
<ViewByFGDetails isEdit={isEdit} apiRef={apiRef} /> | |||
<ViewByFGDetails isEdit={isEdit} apiRef={apiRef} onReleaseClick={onReleaseClick} type={type}/> | |||
{/* {tabIndex === 1 && <ViewByBomDetails isEdit={isEdit} apiRef={apiRef} isHideButton={true} />} */} | |||
<Stack direction="row" justifyContent="flex-end" gap={1}> | |||
<Button | |||
@@ -223,4 +187,4 @@ const DetailScheduleDetailView: React.FC<Props> = ({ | |||
</> | |||
); | |||
}; | |||
export default DetailScheduleDetailView; | |||
export default DetailedScheduleDetailView; |
@@ -0,0 +1,46 @@ | |||
import { CreateItemInputs } from "@/app/api/settings/item/actions"; | |||
import { fetchItem } from "@/app/api/settings/item"; | |||
import GeneralLoading from "@/components/General/GeneralLoading"; | |||
import DetailedScheduleDetailView from "@/components/DetailedScheduleDetail/DetailedScheduleDetailView"; | |||
import { ScheduleType, fetchDetailedProdScheduleDetail } from "@/app/api/scheduling"; | |||
interface SubComponents { | |||
Loading: typeof GeneralLoading; | |||
} | |||
type EditDetailedScheduleDetailProps = { | |||
id?: number; | |||
type: ScheduleType; | |||
}; | |||
type Props = EditDetailedScheduleDetailProps; | |||
const DetailedScheduleDetailWrapper: React.FC<Props> & SubComponents = async ({ | |||
id, | |||
type, | |||
}) => { | |||
// const defaultValues = { | |||
// id: 1, | |||
// productionDate: "2025-05-07", | |||
// totalJobOrders: 13, | |||
// totalProductionQty: 21000, | |||
// }; | |||
const prodSchedule = id ? await fetchDetailedProdScheduleDetail(id) : undefined | |||
if (prodSchedule) { | |||
prodSchedule.prodScheduleLines = prodSchedule.prodScheduleLines.sort((a, b) => b.priority - a.priority) | |||
} | |||
return ( | |||
<DetailedScheduleDetailView | |||
isEditMode={Boolean(id)} | |||
defaultValues={prodSchedule} | |||
type={type} | |||
// qcChecks={qcChecks || []} | |||
/> | |||
); | |||
}; | |||
DetailedScheduleDetailWrapper.Loading = GeneralLoading; | |||
export default DetailedScheduleDetailWrapper; |
@@ -0,0 +1,50 @@ | |||
import { DetailedProdScheduleLineProdTimeResult } from "@/app/api/scheduling" | |||
import { minutesToHoursMinutes } from "@/app/utils/formatUtil"; | |||
import { Box, Divider, Grid, Typography } from "@mui/material"; | |||
import React, { useMemo } from "react" | |||
import { useTranslation } from "react-i18next"; | |||
interface Props { | |||
prodTimeInMinute: DetailedProdScheduleLineProdTimeResult[]; | |||
} | |||
const ProdTimeColumn: React.FC<Props> = ({ prodTimeInMinute }) => { | |||
const { t } = useTranslation("schedule") | |||
const overallMinutes = useMemo(() => | |||
prodTimeInMinute | |||
.map((ele) => ele.totalMinutes) | |||
.reduce((acc, cur) => acc + cur, 0) | |||
, []) | |||
return ( | |||
<Box> | |||
{ | |||
prodTimeInMinute.map(({ equipName, totalMinutes }, index) => { | |||
return ( | |||
<Grid container key={`${equipName}-${index}`}> | |||
<Grid item key={`${equipName}-${index}-1`} xs={4} sx={{ display: 'flex', justifyContent: 'flex-end' }}> | |||
<Typography>{equipName}:</Typography> | |||
</Grid> | |||
<Grid item key={`${equipName}-${index}-2`} xs={8} sx={{ display: 'flex', justifyContent: 'flex-end' }}> | |||
<Typography>{minutesToHoursMinutes(totalMinutes)}</Typography> | |||
</Grid> | |||
</Grid> | |||
) | |||
}) | |||
} | |||
<Divider sx={{ border: 1, borderColor: "darkgray" }} /> | |||
<Grid container> | |||
<Grid item xs={4} sx={{ display: 'flex', justifyContent: 'flex-end' }}> | |||
<Typography>{t("Overall")}:</Typography> | |||
</Grid> | |||
<Grid item xs={8} sx={{ display: 'flex', justifyContent: 'flex-end' }}> | |||
<Typography>{minutesToHoursMinutes(overallMinutes)}</Typography> | |||
</Grid> | |||
</Grid> | |||
</Box> | |||
) | |||
} | |||
export default ProdTimeColumn |
@@ -0,0 +1,226 @@ | |||
"use client"; | |||
import { CreateItemInputs } from "@/app/api/settings/item/actions"; | |||
import { | |||
GridColDef, | |||
GridRowModel, | |||
GridRenderEditCellParams, | |||
GridEditInputCell, | |||
GridRowSelectionModel, | |||
useGridApiRef, | |||
} from "@mui/x-data-grid"; | |||
import { MutableRefObject, useCallback, useMemo, useState } from "react"; | |||
import { useFormContext } from "react-hook-form"; | |||
import { useTranslation } from "react-i18next"; | |||
import { Box, Grid, Tooltip, Typography } from "@mui/material"; | |||
import { GridApiCommunity } from "@mui/x-data-grid/internals"; | |||
import { decimalFormatter, integerFormatter, minutesToHoursMinutes } from "@/app/utils/formatUtil"; | |||
import { DetailedProdScheduleLineResult, DetailedProdScheduleResult, ScheduleType } from "@/app/api/scheduling"; | |||
import ProdTimeColumn from "./ProdTimeColumn"; | |||
import ScheduleTable, { Column } from "../ScheduleTable/ScheduleTable"; | |||
type Props = { | |||
apiRef: MutableRefObject<GridApiCommunity>; | |||
isEdit: boolean; | |||
type: ScheduleType; | |||
onReleaseClick: () => void; | |||
}; | |||
// export type FGRecord = { | |||
// id: string | number; | |||
// code: string; | |||
// name: string; | |||
// inStockQty: number; | |||
// productionQty?: number; | |||
// purchaseQty?: number; | |||
// }; | |||
const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick }) => { | |||
const { | |||
t, | |||
i18n: { language }, | |||
} = useTranslation("schedule"); | |||
const { | |||
getValues, | |||
formState: { errors, defaultValues, touchedFields }, | |||
} = useFormContext<DetailedProdScheduleResult>(); | |||
// const apiRef = useGridApiRef(); | |||
// const [pagingController, setPagingController] = useState([ | |||
// { | |||
// pageNum: 1, | |||
// pageSize: 10, | |||
// totalCount: 0, | |||
// }, | |||
// { | |||
// pageNum: 1, | |||
// pageSize: 10, | |||
// totalCount: 0, | |||
// }, | |||
// { | |||
// pageNum: 1, | |||
// pageSize: 10, | |||
// totalCount: 0, | |||
// }, | |||
// { | |||
// pageNum: 1, | |||
// pageSize: 10, | |||
// totalCount: 0, | |||
// }, | |||
// { | |||
// pageNum: 1, | |||
// pageSize: 10, | |||
// totalCount: 0, | |||
// }, | |||
// { | |||
// pageNum: 1, | |||
// pageSize: 10, | |||
// totalCount: 0, | |||
// }, | |||
// { | |||
// pageNum: 1, | |||
// pageSize: 10, | |||
// totalCount: 0, | |||
// }, | |||
// { | |||
// pageNum: 1, | |||
// pageSize: 10, | |||
// totalCount: 0, | |||
// }, | |||
// ]); | |||
// const updatePagingController = (updatedObj) => { | |||
// setPagingController((prevState) => { | |||
// return prevState.map((item, index) => { | |||
// if (index === updatedObj?.index) { | |||
// return { | |||
// ...item, | |||
// pageNum: item.pageNum, | |||
// pageSize: item.pageSize, | |||
// totalCount: item.totalCount, | |||
// }; | |||
// } else return item; | |||
// }); | |||
// }); | |||
// }; | |||
const columns = useMemo<Column<DetailedProdScheduleLineResult>[]>( | |||
() => [ | |||
{ | |||
field: "jobNo", | |||
label: t("Job No."), | |||
type: "read-only", | |||
// editable: true, | |||
}, | |||
{ | |||
field: "code", | |||
label: t("code"), | |||
type: "read-only", | |||
// editable: true, | |||
}, | |||
{ | |||
field: "name", | |||
label: t("name"), | |||
type: "read-only", | |||
}, | |||
{ | |||
field: "type", | |||
label: t("type"), | |||
type: "read-only", | |||
// editable: true, | |||
}, | |||
// { | |||
// field: "inStockQty", | |||
// label: "Available Qty", | |||
// type: 'read-only', | |||
// style: { | |||
// textAlign: "right", | |||
// }, | |||
// // editable: true, | |||
// renderCell: (row: FGRecord) => { | |||
// if (typeof (row.inStockQty) == "number") { | |||
// return decimalFormatter.format(row.inStockQty) | |||
// } | |||
// return row.inStockQty | |||
// } | |||
// }, | |||
{ | |||
field: "demandQty", | |||
label: t("Demand Qty"), | |||
type: "input", | |||
style: { | |||
textAlign: "right", | |||
}, | |||
renderCell: (row) => { | |||
if (typeof row.demandQty == "number") { | |||
return decimalFormatter.format(row.demandQty ?? 0); | |||
} | |||
return row.demandQty; | |||
}, | |||
}, | |||
{ | |||
field: "prodTimeInMinute", | |||
label: t("Estimated Production Time"), | |||
type: "read-only", | |||
style: { | |||
textAlign: "right", | |||
}, | |||
renderCell: (row) => { | |||
return <ProdTimeColumn prodTimeInMinute={row.prodTimeInMinute} /> | |||
} | |||
}, | |||
{ | |||
field: "priority", | |||
label: t("Production Priority"), | |||
type: "read-only", | |||
style: { | |||
textAlign: "right", | |||
}, | |||
// editable: true, | |||
}, | |||
], | |||
[], | |||
); | |||
return ( | |||
<Grid container spacing={2}> | |||
{/* <Grid item xs={12} key={"all"}> | |||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||
{t("FG Demand List (7 Days)")} | |||
</Typography> | |||
<EditableSearchResults<FGRecord> | |||
index={7} | |||
items={fakeOverallRecords} | |||
columns={overallColumns} | |||
setPagingController={updatePagingController} | |||
pagingController={pagingController[7]} | |||
isAutoPaging={false} | |||
isEditable={false} | |||
isEdit={isEdit} | |||
hasCollapse={true} | |||
/> | |||
</Grid> */} | |||
{/* {dayPeriod.map((date, index) => ( */} | |||
<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> | |||
{/* ))} */} | |||
</Grid> | |||
); | |||
}; | |||
export default ViewByFGDetails; |
@@ -0,0 +1 @@ | |||
export { default } from "./DetailedScheduleDetailWrapper"; |
@@ -179,7 +179,7 @@ const NavigationContent: React.FC = () => { | |||
{ | |||
icon: <RequestQuote />, | |||
label: "Detail Scheduling", | |||
path: "/scheduling/detail", | |||
path: "/scheduling/detailed", | |||
}, | |||
{ | |||
icon: <RequestQuote />, | |||
@@ -3,7 +3,7 @@ import { fetchItem } from "@/app/api/settings/item"; | |||
import GeneralLoading from "@/components/General/GeneralLoading"; | |||
import RoughScheduleDetailView from "@/components/RoughScheduleDetail/RoughScheudleDetailView"; | |||
import React from "react"; | |||
import { ScheduleType, fetchProdScheduleDetail } from "@/app/api/scheduling"; | |||
import { ScheduleType, fetchRoughProdScheduleDetail } from "@/app/api/scheduling"; | |||
interface SubComponents { | |||
Loading: typeof GeneralLoading; | |||
} | |||
@@ -17,7 +17,7 @@ const RoughScheduleDetailWrapper: React.FC<Props> & SubComponents = async ({ | |||
id, | |||
type, | |||
}) => { | |||
const prodSchedule = id ? await fetchProdScheduleDetail(id) : undefined; | |||
const prodSchedule = id ? await fetchRoughProdScheduleDetail(id) : undefined; | |||
return ( | |||
<RoughScheduleDetailView | |||
@@ -3,14 +3,12 @@ | |||
import { useCallback, useEffect, useMemo, useState } from "react"; | |||
import { useRouter, useSearchParams } from "next/navigation"; | |||
import { useTranslation } from "react-i18next"; | |||
import { CreateItemInputs, saveItem } from "@/app/api/settings/item/actions"; | |||
import { | |||
FormProvider, | |||
SubmitErrorHandler, | |||
SubmitHandler, | |||
useForm, | |||
} from "react-hook-form"; | |||
import { deleteDialog } from "../Swal/CustomAlerts"; | |||
import { | |||
Box, | |||
Button, | |||
@@ -23,16 +21,12 @@ import { | |||
Typography, | |||
} from "@mui/material"; | |||
import { Add, Check, Close, EditNote } from "@mui/icons-material"; | |||
import { ItemQc, ItemsResult } from "@/app/api/settings/item"; | |||
import { useGridApiRef } from "@mui/x-data-grid"; | |||
import ProductDetails from "@/components/CreateItem/ProductDetails"; | |||
import DetailInfoCard from "@/components/RoughScheduleDetail/DetailInfoCard"; | |||
import ViewByFGDetails from "@/components/RoughScheduleDetail/ViewByFGDetails"; | |||
import ViewByBomDetails from "@/components/RoughScheduleDetail/ViewByBomDetails"; | |||
import ScheduleTable from "@/components/ScheduleTable"; | |||
import { Column } from "@/components/ScheduleTable/ScheduleTable"; | |||
import { RoughProdScheduleResult, ScheduleType } from "@/app/api/scheduling"; | |||
import { arrayToDayjs, dayjsToDateString } from "@/app/utils/formatUtil"; | |||
import { useGridApiRef } from "@mui/x-data-grid"; | |||
type Props = { | |||
isEditMode: boolean; | |||
@@ -37,6 +37,7 @@ import { decimalFormatter } from "@/app/utils/formatUtil"; | |||
import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; | |||
import HighlightOffIcon from "@mui/icons-material/HighlightOff"; | |||
import { | |||
DetailedProdScheduleLineBomMaterialResult, | |||
RoughProdScheduleLineBomMaterialResult, | |||
ScheduleType, | |||
} from "@/app/api/scheduling"; | |||
@@ -46,7 +47,7 @@ interface ResultWithId { | |||
} | |||
interface Props { | |||
bomMaterial: RoughProdScheduleLineBomMaterialResult[]; | |||
bomMaterial: RoughProdScheduleLineBomMaterialResult[] | DetailedProdScheduleLineBomMaterialResult[]; | |||
type: ScheduleType; | |||
} | |||
@@ -1,11 +1,11 @@ | |||
"use client"; | |||
import React, { | |||
CSSProperties, | |||
DetailedHTMLProps, | |||
HTMLAttributes, | |||
useEffect, | |||
useState, | |||
CSSProperties, | |||
DetailedHTMLProps, | |||
HTMLAttributes, | |||
useEffect, | |||
useState, | |||
} from "react"; | |||
import Paper from "@mui/material/Paper"; | |||
import Table from "@mui/material/Table"; | |||
@@ -30,363 +30,365 @@ import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; | |||
import PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline"; | |||
import { useTranslation } from "react-i18next"; | |||
import { | |||
RoughProdScheduleLineBomMaterialResult, | |||
RoughProdScheduleLineResultByFg, | |||
RoughProdScheduleResult, | |||
ScheduleType, | |||
DetailedProdScheduleLineResult, | |||
RoughProdScheduleLineBomMaterialResult, | |||
RoughProdScheduleLineResultByFg, | |||
RoughProdScheduleResult, | |||
ScheduleType, | |||
} from "@/app/api/scheduling"; | |||
import { defaultPagingController } from "../SearchResults/SearchResults"; | |||
export interface ResultWithId { | |||
id: string | number; | |||
// id: number; | |||
id: string | number; | |||
// id: number; | |||
} | |||
interface BaseColumn<T extends ResultWithId> { | |||
field: keyof T; | |||
label: string; | |||
type: string; | |||
options?: T[]; | |||
renderCell?: (params: T) => React.ReactNode; | |||
style?: Partial<HTMLElement["style"]> & { | |||
[propName: string]: string; | |||
} & CSSProperties; | |||
field: keyof T; | |||
label: string; | |||
type: string; | |||
options?: T[]; | |||
renderCell?: (params: T) => React.ReactNode; | |||
style?: Partial<HTMLElement["style"]> & { | |||
[propName: string]: string; | |||
} & CSSProperties; | |||
} | |||
interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { | |||
onClick: (item: T) => void; | |||
buttonIcon: React.ReactNode; | |||
buttonColor?: "inherit" | "default" | "primary" | "secondary"; | |||
onClick: (item: T) => void; | |||
buttonIcon: React.ReactNode; | |||
buttonColor?: "inherit" | "default" | "primary" | "secondary"; | |||
} | |||
export type Column<T extends ResultWithId> = | |||
| BaseColumn<T> | |||
| ColumnWithAction<T>; | |||
| BaseColumn<T> | |||
| ColumnWithAction<T>; | |||
interface Props<T extends ResultWithId> { | |||
index?: number; | |||
items: T[]; | |||
columns: Column<T>[]; | |||
noWrapper?: boolean; | |||
setPagingController: (value: { | |||
pageNum: number; | |||
pageSize: number; | |||
totalCount: number; | |||
index?: number; | |||
}) => void; | |||
pagingController: { pageNum: number; pageSize: number; totalCount: number }; | |||
isAutoPaging: boolean; | |||
isEdit: boolean; | |||
isEditable: boolean; | |||
hasCollapse: boolean; | |||
type: ScheduleType; | |||
items: T[]; | |||
columns: Column<T>[]; | |||
noWrapper?: boolean; | |||
setPagingController?: (value: { | |||
pageNum: number; | |||
pageSize: number; | |||
totalCount: number; | |||
index?: number; | |||
}) => void; | |||
pagingController?: { pageNum: number; pageSize: number; totalCount: number }; | |||
isAutoPaging: boolean; | |||
isEdit: boolean; | |||
isEditable: boolean; | |||
hasCollapse: boolean; | |||
type: ScheduleType; | |||
} | |||
function ScheduleTable<T extends ResultWithId>({ | |||
type, | |||
index = 7, | |||
items, | |||
columns, | |||
noWrapper, | |||
pagingController, | |||
setPagingController, | |||
isAutoPaging = true, | |||
isEdit = false, | |||
isEditable = true, | |||
hasCollapse = false, | |||
type, | |||
index = 7, | |||
items, | |||
columns, | |||
noWrapper, | |||
pagingController = undefined, | |||
setPagingController = undefined, | |||
isAutoPaging = true, | |||
isEdit = false, | |||
isEditable = true, | |||
hasCollapse = false, | |||
}: 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"); | |||
useEffect(() => { | |||
setEditedItems(items); | |||
}, [items]); | |||
const handleChangePage = (_event: unknown, newPage: number) => { | |||
setPage(newPage); | |||
if (setPagingController) { | |||
setPagingController({ | |||
...pagingController, | |||
pageNum: newPage + 1, | |||
index: index ?? -1, | |||
}); | |||
} | |||
}; | |||
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"); | |||
useEffect(() => { | |||
setEditedItems(items); | |||
}, [items]); | |||
const handleChangePage = (_event: unknown, newPage: number) => { | |||
setPage(newPage); | |||
if (setPagingController && pagingController) { | |||
setPagingController({ | |||
...pagingController, | |||
pageNum: newPage + 1, | |||
index: index ?? -1, | |||
}); | |||
} | |||
}; | |||
const handleChangeRowsPerPage = ( | |||
event: React.ChangeEvent<HTMLInputElement>, | |||
) => { | |||
setRowsPerPage(+event.target.value); | |||
setPage(0); | |||
if (setPagingController) { | |||
setPagingController({ | |||
...pagingController, | |||
pageSize: +event.target.value, | |||
pageNum: 1, | |||
index: index, | |||
}); | |||
} | |||
}; | |||
const handleChangeRowsPerPage = ( | |||
event: React.ChangeEvent<HTMLInputElement>, | |||
) => { | |||
setRowsPerPage(+event.target.value); | |||
setPage(0); | |||
if (setPagingController && pagingController) { | |||
setPagingController({ | |||
...pagingController, | |||
pageSize: +event.target.value, | |||
pageNum: 1, | |||
index: index, | |||
}); | |||
} | |||
}; | |||
const handleEditClick = (id: number) => { | |||
setEditingRowId(id); | |||
}; | |||
const handleEditClick = (id: number) => { | |||
setEditingRowId(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)), | |||
); | |||
}; | |||
const handleSaveClick = (item: T) => { | |||
setEditingRowId(null); | |||
// Call API or any save logic here | |||
setEditedItems((prev) => | |||
prev.map((row) => (row.id === item.id ? { ...row } : row)), | |||
); | |||
}; | |||
const handleInputChange = ( | |||
id: number, | |||
field: keyof T, | |||
value: string | number[], | |||
) => { | |||
setEditedItems((prev) => | |||
prev.map((item) => (item.id === id ? { ...item, [field]: value } : item)), | |||
); | |||
}; | |||
const handleInputChange = ( | |||
id: number, | |||
field: keyof T, | |||
value: string | number[], | |||
) => { | |||
setEditedItems((prev) => | |||
prev.map((item) => (item.id === id ? { ...item, [field]: value } : item)), | |||
); | |||
}; | |||
const handleDeleteClick = (id: number) => { | |||
// Implement delete logic here | |||
setEditedItems((prev) => prev.filter((item) => item.id !== id)); | |||
}; | |||
const handleDeleteClick = (id: number) => { | |||
// Implement delete logic here | |||
setEditedItems((prev) => prev.filter((item) => item.id !== id)); | |||
}; | |||
useEffect(() => { | |||
console.log("[debug] isEdit in table", isEdit); | |||
//TODO: switch all record to not in edit mode and save the changes | |||
if (!isEdit) { | |||
editedItems?.forEach((item) => { | |||
// Call save logic here | |||
// console.log("Saving item:", item); | |||
// Reset editing state if needed | |||
}); | |||
useEffect(() => { | |||
console.log("[debug] isEdit in table", isEdit); | |||
//TODO: switch all record to not in edit mode and save the changes | |||
if (!isEdit) { | |||
editedItems?.forEach((item) => { | |||
// Call save logic here | |||
// console.log("Saving item:", item); | |||
// Reset editing state if needed | |||
}); | |||
setEditingRowId(null); | |||
} | |||
}, [isEdit]); | |||
setEditingRowId(null); | |||
} | |||
}, [isEdit]); | |||
function isRoughType(type: ScheduleType): type is "rough" { | |||
return type === "rough"; | |||
} | |||
function isRoughType(type: ScheduleType): type is "rough" { | |||
return type === "rough"; | |||
} | |||
function isDetailedType(type: ScheduleType): type is "detailed" { | |||
return type === "detailed"; | |||
} | |||
function isDetailedType(type: ScheduleType): type is "detailed" { | |||
return type === "detailed"; | |||
} | |||
function Row(props: { row: T }) { | |||
const { row } = props; | |||
const [open, setOpen] = useState(false); | |||
// console.log(row) | |||
return ( | |||
<> | |||
<TableRow hover tabIndex={-1} key={row.id}> | |||
{isDetailedType(type) && ( | |||
<TableCell> | |||
<IconButton disabled={!isEdit}> | |||
<PlayCircleOutlineIcon /> | |||
</IconButton> | |||
</TableCell> | |||
)} | |||
{(isEditable || hasCollapse) && ( | |||
<TableCell> | |||
{editingRowId === row.id ? ( | |||
<> | |||
{isDetailedType(type) && isEditable && ( | |||
<IconButton | |||
disabled={!isEdit} | |||
onClick={() => handleSaveClick(row)} | |||
> | |||
<SaveIcon /> | |||
</IconButton> | |||
)} | |||
{isDetailedType(type) && isEditable && ( | |||
<IconButton | |||
disabled={!isEdit} | |||
onClick={() => setEditingRowId(null)} | |||
> | |||
<CancelIcon /> | |||
</IconButton> | |||
)} | |||
{hasCollapse && ( | |||
<IconButton | |||
aria-label="expand row" | |||
size="small" | |||
onClick={() => setOpen(!open)} | |||
> | |||
{open ? ( | |||
<KeyboardArrowUpIcon /> | |||
) : ( | |||
<KeyboardArrowDownIcon /> | |||
)} | |||
<Typography>{t("View BoM")}</Typography> | |||
</IconButton> | |||
)} | |||
</> | |||
) : ( | |||
<> | |||
{isDetailedType(type) && isEditable && ( | |||
<IconButton | |||
disabled={!isEdit} | |||
onClick={() => handleEditClick(row.id as number)} | |||
> | |||
<EditIcon /> | |||
</IconButton> | |||
)} | |||
{isDetailedType(type) && isEditable && ( | |||
<IconButton | |||
disabled={!isEdit} | |||
onClick={() => handleDeleteClick(row.id as number)} | |||
> | |||
<DeleteIcon /> | |||
</IconButton> | |||
)} | |||
{hasCollapse && ( | |||
<IconButton | |||
aria-label="expand row" | |||
size="small" | |||
onClick={() => setOpen(!open)} | |||
> | |||
{open ? ( | |||
<KeyboardArrowUpIcon /> | |||
) : ( | |||
<KeyboardArrowDownIcon /> | |||
)} | |||
<Typography>{t("View BoM")}</Typography> | |||
</IconButton> | |||
)} | |||
</> | |||
)} | |||
</TableCell> | |||
)} | |||
{columns.map((column, idx) => { | |||
const columnName = column.field; | |||
return ( | |||
<TableCell key={`${columnName.toString()}-${idx}`}> | |||
{editingRowId === row.id ? ( | |||
(() => { | |||
switch (column.type) { | |||
case "input": | |||
function Row(props: { row: T }) { | |||
const { row } = props; | |||
const [open, setOpen] = useState(false); | |||
// console.log(row) | |||
return ( | |||
<> | |||
<TableRow hover tabIndex={-1} key={row.id}> | |||
{isDetailedType(type) && ( | |||
<TableCell> | |||
<IconButton disabled={!isEdit}> | |||
<PlayCircleOutlineIcon /> | |||
</IconButton> | |||
</TableCell> | |||
)} | |||
{(isEditable || hasCollapse) && ( | |||
<TableCell> | |||
{editingRowId === row.id ? ( | |||
<> | |||
{isDetailedType(type) && isEditable && ( | |||
<IconButton | |||
disabled={!isEdit} | |||
onClick={() => handleSaveClick(row)} | |||
> | |||
<SaveIcon /> | |||
</IconButton> | |||
)} | |||
{isDetailedType(type) && isEditable && ( | |||
<IconButton | |||
disabled={!isEdit} | |||
onClick={() => setEditingRowId(null)} | |||
> | |||
<CancelIcon /> | |||
</IconButton> | |||
)} | |||
{hasCollapse && ( | |||
<IconButton | |||
aria-label="expand row" | |||
size="small" | |||
onClick={() => setOpen(!open)} | |||
> | |||
{open ? ( | |||
<KeyboardArrowUpIcon /> | |||
) : ( | |||
<KeyboardArrowDownIcon /> | |||
)} | |||
<Typography>{t("View BoM")}</Typography> | |||
</IconButton> | |||
)} | |||
</> | |||
) : ( | |||
<> | |||
{isDetailedType(type) && isEditable && ( | |||
<IconButton | |||
disabled={!isEdit} | |||
onClick={() => handleEditClick(row.id as number)} | |||
> | |||
<EditIcon /> | |||
</IconButton> | |||
)} | |||
{isDetailedType(type) && isEditable && ( | |||
<IconButton | |||
disabled={!isEdit} | |||
onClick={() => handleDeleteClick(row.id as number)} | |||
> | |||
<DeleteIcon /> | |||
</IconButton> | |||
)} | |||
{hasCollapse && ( | |||
<IconButton | |||
aria-label="expand row" | |||
size="small" | |||
onClick={() => setOpen(!open)} | |||
> | |||
{open ? ( | |||
<KeyboardArrowUpIcon /> | |||
) : ( | |||
<KeyboardArrowDownIcon /> | |||
)} | |||
<Typography>{t("View BoM")}</Typography> | |||
</IconButton> | |||
)} | |||
</> | |||
)} | |||
</TableCell> | |||
)} | |||
{columns.map((column, idx) => { | |||
const columnName = column.field; | |||
return ( | |||
<TextField | |||
hiddenLabel={true} | |||
fullWidth | |||
defaultValue={row[columnName] as string} | |||
onChange={(e) => | |||
handleInputChange( | |||
row.id as number, | |||
columnName, | |||
e.target.value, | |||
) | |||
} | |||
/> | |||
<TableCell key={`${columnName.toString()}-${idx}`}> | |||
{editingRowId === row.id ? ( | |||
(() => { | |||
switch (column.type) { | |||
case "input": | |||
return ( | |||
<TextField | |||
hiddenLabel={true} | |||
fullWidth | |||
defaultValue={row[columnName] as string} | |||
onChange={(e) => | |||
handleInputChange( | |||
row.id as number, | |||
columnName, | |||
e.target.value, | |||
) | |||
} | |||
/> | |||
); | |||
// case 'multi-select': | |||
// //TODO: May need update if use | |||
// return ( | |||
// <MultiSelect | |||
// //label={column.label} | |||
// options={column.options ?? []} | |||
// selectedValues={[]} | |||
// onChange={(selectedValues) => handleInputChange(row.id as number, columnName, selectedValues)} | |||
// /> | |||
// ); | |||
case "read-only": | |||
return <span>{row[columnName] as string}</span>; | |||
default: | |||
return null; // Handle any default case if needed | |||
} | |||
})() | |||
) : column.renderCell ? ( | |||
<div style={column.style}>{column.renderCell(row)}</div> | |||
) : ( | |||
<div style={column.style}> | |||
<span | |||
onDoubleClick={() => | |||
isEdit && handleEditClick(row.id as number) | |||
} | |||
> | |||
{row[columnName] as string} | |||
</span> | |||
</div> | |||
)} | |||
</TableCell> | |||
); | |||
// case 'multi-select': | |||
// //TODO: May need update if use | |||
// return ( | |||
// <MultiSelect | |||
// //label={column.label} | |||
// options={column.options ?? []} | |||
// selectedValues={[]} | |||
// onChange={(selectedValues) => handleInputChange(row.id as number, columnName, selectedValues)} | |||
// /> | |||
// ); | |||
case "read-only": | |||
return <span>{row[columnName] as string}</span>; | |||
default: | |||
return null; // Handle any default case if needed | |||
} | |||
})() | |||
) : column.renderCell ? ( | |||
<div style={column.style}>{column.renderCell(row)}</div> | |||
) : ( | |||
<div style={column.style}> | |||
<span | |||
onDoubleClick={() => | |||
isEdit && handleEditClick(row.id as number) | |||
} | |||
> | |||
{row[columnName] as string} | |||
</span> | |||
</div> | |||
)} | |||
</TableCell> | |||
); | |||
})} | |||
</TableRow> | |||
<TableRow> | |||
{hasCollapse && ( | |||
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}> | |||
<Collapse in={open} timeout="auto" unmountOnExit> | |||
<Table> | |||
<TableBody> | |||
<TableRow> | |||
<TableCell> | |||
<BomMaterialTable | |||
type={type} | |||
bomMaterial={ | |||
(row as unknown as RoughProdScheduleLineResultByFg) | |||
.bomMaterials | |||
} | |||
/> | |||
</TableCell> | |||
</TableRow> | |||
</TableBody> | |||
})} | |||
</TableRow> | |||
<TableRow> | |||
{hasCollapse && ( | |||
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}> | |||
<Collapse in={open} timeout="auto" unmountOnExit> | |||
<Table> | |||
<TableBody> | |||
<TableRow> | |||
<TableCell> | |||
<BomMaterialTable | |||
type={type} | |||
bomMaterial={ | |||
isDetailedType(type) ? (row as unknown as RoughProdScheduleLineResultByFg).bomMaterials | |||
: (row as unknown as DetailedProdScheduleLineResult).bomMaterials | |||
} | |||
/> | |||
</TableCell> | |||
</TableRow> | |||
</TableBody> | |||
</Table> | |||
</Collapse> | |||
</TableCell> | |||
)} | |||
</TableRow> | |||
</> | |||
); | |||
} | |||
const table = ( | |||
<> | |||
<TableContainer sx={{ maxHeight: 440 }}> | |||
<Table stickyHeader> | |||
<TableHead> | |||
<TableRow> | |||
{isDetailedType(type) && <TableCell>{t("Release")}</TableCell>} | |||
{(isEditable || hasCollapse) && ( | |||
<TableCell>{t("Actions")}</TableCell> | |||
)}{" "} | |||
{/* Action Column Header */} | |||
{columns.map((column, idx) => ( | |||
<TableCell | |||
style={column.style} | |||
key={`${column.field.toString()}${idx}`} | |||
> | |||
{column.label} | |||
</TableCell> | |||
))} | |||
</TableRow> | |||
</TableHead> | |||
<TableBody> | |||
{/* {(isAutoPaging ? editedItems.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) : editedItems).map((item) => ( */} | |||
{editedItems | |||
?.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) | |||
?.map((item) => <Row key={item.id} row={item} />)} | |||
</TableBody> | |||
</Table> | |||
</Collapse> | |||
</TableCell> | |||
)} | |||
</TableRow> | |||
</> | |||
</TableContainer> | |||
<TablePagination | |||
rowsPerPageOptions={[10, 25, 100]} | |||
component="div" | |||
// count={pagingController.totalCount === 0 ? editedItems.length : pagingController.totalCount} | |||
count={editedItems?.length ?? 0} | |||
rowsPerPage={rowsPerPage} | |||
page={page} | |||
onPageChange={handleChangePage} | |||
onRowsPerPageChange={handleChangeRowsPerPage} | |||
/> | |||
</> | |||
); | |||
} | |||
const table = ( | |||
<> | |||
<TableContainer sx={{ maxHeight: 440 }}> | |||
<Table stickyHeader> | |||
<TableHead> | |||
<TableRow> | |||
{isDetailedType(type) && <TableCell>{t("Release")}</TableCell>} | |||
{(isEditable || hasCollapse) && ( | |||
<TableCell>{t("Actions")}</TableCell> | |||
)}{" "} | |||
{/* Action Column Header */} | |||
{columns.map((column, idx) => ( | |||
<TableCell | |||
style={column.style} | |||
key={`${column.field.toString()}${idx}`} | |||
> | |||
{column.label} | |||
</TableCell> | |||
))} | |||
</TableRow> | |||
</TableHead> | |||
<TableBody> | |||
{/* {(isAutoPaging ? editedItems.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) : editedItems).map((item) => ( */} | |||
{editedItems | |||
?.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) | |||
?.map((item) => <Row key={item.id} row={item} />)} | |||
</TableBody> | |||
</Table> | |||
</TableContainer> | |||
<TablePagination | |||
rowsPerPageOptions={[10, 25, 100]} | |||
component="div" | |||
// count={pagingController.totalCount === 0 ? editedItems.length : pagingController.totalCount} | |||
count={editedItems?.length ?? 0} | |||
rowsPerPage={rowsPerPage} | |||
page={page} | |||
onPageChange={handleChangePage} | |||
onRowsPerPageChange={handleChangeRowsPerPage} | |||
/> | |||
</> | |||
); | |||
return noWrapper ? table : <Paper sx={{ overflow: "hidden" }}>{table}</Paper>; | |||
return noWrapper ? table : <Paper sx={{ overflow: "hidden" }}>{table}</Paper>; | |||
} | |||
export default ScheduleTable; |
@@ -81,5 +81,6 @@ | |||
"Job Qty": "工單數量", | |||
"mat": "物料", | |||
"Product Count(s)": "產品數量", | |||
"Schedule Period To": "排程期間至" | |||
"Schedule Period To": "排程期間至", | |||
"Overall": "總計" | |||
} |