# Conflicts: # src/app/api/settings/item/actions.tsmaster
| @@ -1,4 +1,4 @@ | |||
| API_URL=http://localhost:8090/api | |||
| API_URL=http://10.100.0.81:8090/api | |||
| NEXTAUTH_SECRET=secret | |||
| NEXTAUTH_URL=https://fpsms-uat.2fi-solutions.com | |||
| NEXT_PUBLIC_API_URL=http://localhost:8090/api | |||
| NEXTAUTH_URL=http://10.100.0.81:3000 | |||
| NEXT_PUBLIC_API_URL=http://10.100.0.81:8090/api | |||
| @@ -9,11 +9,11 @@ const withPWA = require("next-pwa")({ | |||
| }); | |||
| const nextConfig = { | |||
| // eslint: { | |||
| // // Warning: This allows production builds to successfully complete even if | |||
| // // your project has ESLint errors. | |||
| // ignoreDuringBuilds: true, | |||
| // }, | |||
| eslint: { | |||
| // Warning: This allows production builds to successfully complete even if | |||
| // your project has ESLint errors. | |||
| ignoreDuringBuilds: true, | |||
| }, | |||
| }; | |||
| module.exports = withPWA(nextConfig); | |||
| @@ -0,0 +1,19 @@ | |||
| import { getServerI18n } from "@/i18n"; | |||
| import { Stack, Typography, Link } from "@mui/material"; | |||
| import NextLink from "next/link"; | |||
| export default async function NotFound() { | |||
| const { t } = await getServerI18n("schedule", "common"); | |||
| return ( | |||
| <Stack spacing={2}> | |||
| <Typography variant="h4">{t("Not Found")}</Typography> | |||
| <Typography variant="body1"> | |||
| {t("The job order page was not found!")} | |||
| </Typography> | |||
| <Link href="/settings/scheduling" component={NextLink} variant="body2"> | |||
| {t("Return to all job orders")} | |||
| </Link> | |||
| </Stack> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,49 @@ | |||
| import { fetchJoDetail } from "@/app/api/jo"; | |||
| import { SearchParams, ServerFetchError } from "@/app/utils/fetchUtil"; | |||
| import JoSave from "@/components/JoSave"; | |||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||
| import { Typography } from "@mui/material"; | |||
| import { isArray } from "lodash"; | |||
| import { Metadata } from "next"; | |||
| import { notFound } from "next/navigation"; | |||
| import { Suspense } from "react"; | |||
| export const metadata: Metadata = { | |||
| title: "Edit Job Order Detail" | |||
| } | |||
| type Props = SearchParams; | |||
| const JoEdit: React.FC<Props> = async ({ searchParams }) => { | |||
| const { t } = await getServerI18n("jo"); | |||
| const id = searchParams["id"]; | |||
| if (!id || isArray(id) || !isFinite(parseInt(id))) { | |||
| notFound(); | |||
| } | |||
| try { | |||
| await fetchJoDetail(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("Edit Job Order Detail")} | |||
| </Typography> | |||
| <I18nProvider namespaces={["jo", "common"]}> | |||
| <Suspense fallback={<JoSave.Loading />}> | |||
| <JoSave id={parseInt(id)} /> | |||
| </Suspense> | |||
| </I18nProvider> | |||
| </> | |||
| ); | |||
| } | |||
| export default JoEdit; | |||
| @@ -0,0 +1,35 @@ | |||
| import JoSearch from "@/components/JoSearch"; | |||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||
| import { Stack, Typography } from "@mui/material"; | |||
| import { Metadata } from "next"; | |||
| import React, { Suspense } from "react"; | |||
| export const metadata: Metadata = { | |||
| title: "Job Order" | |||
| } | |||
| const jo: React.FC = async () => { | |||
| const { t } = await getServerI18n("jo"); | |||
| return ( | |||
| <> | |||
| <Stack | |||
| direction="row" | |||
| justifyContent="space-between" | |||
| flexWrap="wrap" | |||
| rowGap={2} | |||
| > | |||
| <Typography variant="h4" marginInlineEnd={2}> | |||
| {t("Job Order")} | |||
| </Typography> | |||
| </Stack> | |||
| <I18nProvider namespaces={["jo", "common"]}> | |||
| <Suspense fallback={<JoSearch.Loading />}> | |||
| <JoSearch /> | |||
| </Suspense> | |||
| </I18nProvider> | |||
| </> | |||
| ) | |||
| } | |||
| export default jo; | |||
| @@ -17,16 +17,6 @@ const PickOrder: React.FC = async () => { | |||
| return ( | |||
| <> | |||
| <Stack | |||
| direction={"row"} | |||
| justifyContent={"space-between"} | |||
| flexWrap={"wrap"} | |||
| rowGap={2} | |||
| > | |||
| <Typography variant="h4" marginInlineEnd={2}> | |||
| {t("Pick Order")} | |||
| </Typography> | |||
| </Stack> | |||
| <I18nProvider namespaces={["pickOrder", "common"]}> | |||
| <Suspense fallback={<PickOrderSearch.Loading />}> | |||
| <PickOrderSearch /> | |||
| @@ -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 && | |||
| @@ -1,8 +1,39 @@ | |||
| "use server"; | |||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { cache } from 'react'; | |||
| import { Pageable, serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { Machine, Operator } from "."; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| import { revalidateTag } from "next/cache"; | |||
| import { convertObjToURLSearchParams } from "@/app/utils/commonUtil"; | |||
| export interface SearchJoResultRequest extends Pageable { | |||
| code: string; | |||
| name: string; | |||
| } | |||
| export interface SearchJoResultResponse { | |||
| records: SearchJoResult[]; | |||
| total: number; | |||
| } | |||
| export interface SearchJoResult { | |||
| id: number; | |||
| code: string; | |||
| name: string; | |||
| reqQty: number; | |||
| uom: string; | |||
| status: string; | |||
| } | |||
| export interface ReleaseJoRequest { | |||
| id: number; | |||
| } | |||
| export interface ReleaseJoResponse { | |||
| id: number; | |||
| entity: { status: string } | |||
| } | |||
| export interface IsOperatorExistResponse<T> { | |||
| id: number | null; | |||
| @@ -49,3 +80,29 @@ export const isCorrectMachineUsed = async (machineCode: string) => { | |||
| revalidateTag("po"); | |||
| return isExist; | |||
| }; | |||
| export const fetchJos = cache(async (data?: SearchJoResultRequest) => { | |||
| const queryStr = convertObjToURLSearchParams(data) | |||
| const response = serverFetchJson<SearchJoResultResponse>( | |||
| `${BASE_API_URL}/jo/getRecordByPage?${queryStr}`, | |||
| { | |||
| method: "GET", | |||
| headers: { "Content-Type": "application/json" }, | |||
| next: { | |||
| tags: ["jos"] | |||
| } | |||
| } | |||
| ) | |||
| return response | |||
| }) | |||
| export const releaseJo = cache(async (data: ReleaseJoRequest) => { | |||
| return serverFetchJson<ReleaseJoResponse>(`${BASE_API_URL}/jo/release`, | |||
| { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }) | |||
| }) | |||
| @@ -1,5 +1,9 @@ | |||
| "server=only"; | |||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| import { cache } from "react"; | |||
| export interface Operator { | |||
| id: number; | |||
| name: string; | |||
| @@ -12,3 +16,34 @@ export interface Machine { | |||
| code: string; | |||
| qrCode: string; | |||
| } | |||
| export interface JoDetail { | |||
| id: number; | |||
| code: string; | |||
| name: string; | |||
| reqQty: number; | |||
| outputQtyUom: string; | |||
| pickLines: JoDetailPickLine[]; | |||
| status: string; | |||
| } | |||
| export interface JoDetailPickLine { | |||
| id: number; | |||
| code: string; | |||
| name: string; | |||
| lotNo: string; | |||
| reqQty: number; | |||
| uom: string; | |||
| status: string; | |||
| } | |||
| export const fetchJoDetail = cache(async (id: number) => { | |||
| return serverFetchJson<JoDetail>(`${BASE_API_URL}/jo/detail/${id}`, | |||
| { | |||
| method: "GET", | |||
| headers: { "Content-Type": "application/json"}, | |||
| next: { | |||
| tags: ["jo"] | |||
| } | |||
| }) | |||
| }) | |||
| @@ -15,6 +15,27 @@ import { | |||
| } from "."; | |||
| import { PurchaseQcResult } from "../po/actions"; | |||
| // import { BASE_API_URL } from "@/config/api"; | |||
| export interface SavePickOrderLineRequest { | |||
| itemId: number | |||
| qty: number | |||
| uomId: number | |||
| } | |||
| export interface SavePickOrderRequest { | |||
| type: string | |||
| targetDate: string | |||
| pickOrderLine: SavePickOrderLineRequest[] | |||
| } | |||
| export interface PostPickOrderResponse<T = null> { | |||
| id: number | null; | |||
| name: string; | |||
| code: string; | |||
| type?: string; | |||
| message: string | null; | |||
| errorPosition: string | |||
| entity?: T | T[]; | |||
| } | |||
| export interface PostStockOutLiineResponse<T> { | |||
| id: number | null; | |||
| name: string; | |||
| @@ -22,7 +43,7 @@ export interface PostStockOutLiineResponse<T> { | |||
| type?: string; | |||
| message: string | null; | |||
| errorPosition: string | keyof T; | |||
| entity: T | T[]; | |||
| entity: T | T[] | null; | |||
| } | |||
| export interface ReleasePickOrderInputs { | |||
| @@ -60,6 +81,19 @@ export interface PickOrderApprovalInput { | |||
| rejectQty: number; | |||
| status: string; | |||
| } | |||
| export const createPickOrder = async (data: SavePickOrderRequest) => { | |||
| console.log(data); | |||
| const po = await serverFetchJson<PostPickOrderResponse>( | |||
| `${BASE_API_URL}/pickOrder/create`, | |||
| { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }, | |||
| ); | |||
| revalidateTag("pickorder"); | |||
| return po; | |||
| } | |||
| export const consolidatePickOrder = async (ids: number[]) => { | |||
| const pickOrder = await serverFetchJson<any>( | |||
| @@ -181,7 +215,7 @@ export const fetchConsoDetail = cache(async (consoCode: string) => { | |||
| export const releasePickOrder = async (data: ReleasePickOrderInputs) => { | |||
| console.log(data); | |||
| console.log(JSON.stringify(data)); | |||
| const po = await serverFetchJson<{ body: any; status: number }>( | |||
| const po = await serverFetchJson<{ consoCode: string }>( | |||
| `${BASE_API_URL}/pickOrder/releaseConso`, | |||
| { | |||
| method: "POST", | |||
| @@ -232,3 +266,13 @@ export const completeConsoPickOrder = async (consoCode: string) => { | |||
| revalidateTag("pickorder"); | |||
| return po; | |||
| }; | |||
| export const fetchConsoStatus = cache(async (consoCode: string) => { | |||
| return serverFetchJson<{ status: string }>( | |||
| `${BASE_API_URL}/stockOut/get-status/${consoCode}`, | |||
| { | |||
| method: "GET", | |||
| next: { tags: ["pickorder"] }, | |||
| }, | |||
| ); | |||
| }); | |||
| @@ -13,7 +13,7 @@ export interface PickOrderResult { | |||
| id: number; | |||
| code: string; | |||
| consoCode?: string; | |||
| targetDate: number[]; | |||
| targetDate: string; | |||
| completeDate?: number[]; | |||
| type: string; | |||
| status: string; | |||
| @@ -80,6 +80,7 @@ export interface PreReleasePickOrderSummary { | |||
| } | |||
| export interface PickOrderLineWithSuggestedLot { | |||
| poStatus: string; | |||
| id: number; | |||
| itemName: string; | |||
| qty: number; | |||
| @@ -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,6 +32,29 @@ export interface ProdScheduleResultByPage { | |||
| records: ProdScheduleResult[]; | |||
| } | |||
| 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); | |||
| @@ -61,9 +85,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 +97,33 @@ export const testDetailSchedule = cache(async () => { | |||
| }, | |||
| ); | |||
| }); | |||
| 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" }, | |||
| } | |||
| ); | |||
| revalidateTag("prodSchedules"); | |||
| return response; | |||
| }) | |||
| @@ -5,85 +5,132 @@ 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; | |||
| bomOutputQty: number; | |||
| prodTimeInMinute: DetailedProdScheduleLineProdTimeResult[]; | |||
| priority: number; | |||
| approved: boolean; | |||
| proportion: 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"] | |||
| } | |||
| }) | |||
| }) | |||
| @@ -7,8 +7,9 @@ import { | |||
| import { revalidateTag } from "next/cache"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| import { CreateItemResponse } from "../../utils"; | |||
| import { ItemQc } from "."; | |||
| import { ItemQc, ItemsResult } from "."; | |||
| import { QcChecksInputs } from "../qcCheck/actions"; | |||
| import { cache } from "react"; | |||
| // export type TypeInputs = { | |||
| // id: number; | |||
| @@ -37,6 +38,7 @@ export type CreateItemInputs = { | |||
| }; | |||
| export const saveItem = async (data: CreateItemInputs) => { | |||
| <<<<<<< HEAD | |||
| // try { | |||
| const item = await serverFetchJson<CreateItemResponse<CreateItemInputs>>(`${BASE_API_URL}/items/new`, { | |||
| method: "POST", | |||
| @@ -46,3 +48,29 @@ export const saveItem = async (data: CreateItemInputs) => { | |||
| revalidateTag("items"); | |||
| return item | |||
| }; | |||
| ======= | |||
| const item = await serverFetchJson<CreateItemResponse<CreateItemInputs>>( | |||
| `${BASE_API_URL}/items/new`, | |||
| { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }, | |||
| ); | |||
| revalidateTag("items"); | |||
| return item; | |||
| }; | |||
| export interface ItemCombo { | |||
| id: number, | |||
| label: string, | |||
| uomId: number, | |||
| uom: string, | |||
| } | |||
| export const fetchAllItemsInClient = cache(async () => { | |||
| return serverFetchJson<ItemCombo[]>(`${BASE_API_URL}/items/consumables`, { | |||
| next: { tags: ["items"] }, | |||
| }); | |||
| }); | |||
| >>>>>>> da1fb872bf3b92e66cff3af4e1b8d1057c47effa | |||
| @@ -35,6 +35,8 @@ export type ItemsResult = { | |||
| type: string; | |||
| qcChecks: ItemQc[]; | |||
| action?: any; | |||
| fgName?: string; | |||
| excludeDate?: string; | |||
| }; | |||
| export type Result = { | |||
| @@ -10,8 +10,8 @@ export const downloadFile = (blobData: Uint8Array, filename: string) => { | |||
| link.click(); | |||
| }; | |||
| export const convertObjToURLSearchParams = <T extends Object>( | |||
| data: T | null, | |||
| export const convertObjToURLSearchParams = <T extends object>( | |||
| data?: T | null, | |||
| ): string => { | |||
| if (isEmpty(data)) { | |||
| return ""; | |||
| @@ -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", {}); | |||
| @@ -66,6 +67,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}` | |||
| } | |||
| const colon = finalHrStr.length > 0 && finalMinStr.length > 0 ? ":" : "" | |||
| return `${finalHrStr} ${colon} ${finalMinStr}`.trim() | |||
| } | |||
| export const stockInLineStatusMap: { [status: string]: number } = { | |||
| draft: 0, | |||
| pending: 1, | |||
| @@ -25,6 +25,8 @@ const pathToLabelMap: { [path: string]: string } = { | |||
| "/pickOrder": "Pick Order", | |||
| "/po": "Purchase Order", | |||
| "/dashboard": "dashboard", | |||
| "/jo": "Job Order", | |||
| "/jo/edit": "Edit Job Order", | |||
| }; | |||
| const Breadcrumb = () => { | |||
| @@ -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,226 +0,0 @@ | |||
| "use client"; | |||
| 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, | |||
| Grid, | |||
| Link, | |||
| Stack, | |||
| Tab, | |||
| Tabs, | |||
| TabsProps, | |||
| 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 ViewByFGDetails, { | |||
| FGRecord, | |||
| } from "@/components/DetailScheduleDetail/ViewByFGDetails"; | |||
| import ViewByBomDetails from "@/components/DetailScheduleDetail/ViewByBomDetails"; | |||
| import EditableSearchResults, { | |||
| Column, | |||
| } from "@/components/SearchResults/EditableSearchResults"; | |||
| // temp interface input | |||
| export interface SaveDetailSchedule { | |||
| id: number; | |||
| productionDate: string; | |||
| totalJobOrders: number; | |||
| totalProductionQty: number; | |||
| } | |||
| type Props = { | |||
| isEditMode: boolean; | |||
| // type: TypeEnum; | |||
| defaultValues: Partial<SaveDetailSchedule> | undefined; | |||
| // qcChecks: ItemQc[] | |||
| }; | |||
| const DetailScheduleDetailView: React.FC<Props> = ({ | |||
| isEditMode, | |||
| // type, | |||
| defaultValues, | |||
| // qcChecks | |||
| }) => { | |||
| // console.log(type) | |||
| const apiRef = useGridApiRef(); | |||
| const params = useSearchParams(); | |||
| console.log(params.get("id")); | |||
| const [serverError, setServerError] = useState(""); | |||
| 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]); | |||
| // console.log(typeId) | |||
| const formProps = useForm<SaveDetailSchedule>({ | |||
| defaultValues: defaultValues ? defaultValues : { | |||
| id: 1, | |||
| productionDate: "2025-05-07", | |||
| totalJobOrders: 13, | |||
| totalProductionQty: 21000, | |||
| } as SaveDetailSchedule, | |||
| }); | |||
| const errors = formProps.formState.errors; | |||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||
| (_e, newValue) => { | |||
| setTabIndex(newValue); | |||
| }, | |||
| [], | |||
| ); | |||
| const [pagingController, setPagingController] = useState({ | |||
| pageNum: 1, | |||
| pageSize: 10, | |||
| totalCount: 0, | |||
| }); | |||
| const handleCancel = () => { | |||
| router.replace(`/scheduling/Detail`); | |||
| }; | |||
| const onSubmit = useCallback<SubmitHandler<SaveDetailSchedule>>( | |||
| async (data, event) => { | |||
| const hasErrors = false; | |||
| console.log(errors); | |||
| // console.log(apiRef.current.getCellValue(2, "lowerLimit")) | |||
| // apiRef.current. | |||
| try { | |||
| if (hasErrors) { | |||
| setServerError(t("An error has occurred. Please try again later.")); | |||
| return false; | |||
| } | |||
| } catch (e) { | |||
| // backend error | |||
| setServerError(t("An error has occurred. Please try again later.")); | |||
| console.log(e); | |||
| } | |||
| }, | |||
| [apiRef, router, t], | |||
| ); | |||
| // multiple tabs | |||
| const onSubmitError = useCallback<SubmitErrorHandler<SaveDetailSchedule>>( | |||
| (errors) => {}, | |||
| [], | |||
| ); | |||
| const onClickEdit = () => { | |||
| setIsEdit(!isEdit); | |||
| }; | |||
| return ( | |||
| <> | |||
| <FormProvider {...formProps}> | |||
| <Stack | |||
| spacing={2} | |||
| component="form" | |||
| onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||
| > | |||
| {/*<Grid>*/} | |||
| {/* <Typography mb={2} variant="h4">*/} | |||
| {/* {t(`${mode} ${title}`)}*/} | |||
| {/* </Typography>*/} | |||
| {/*</Grid>*/} | |||
| <DetailInfoCard | |||
| // recordDetails={formProps.formState.defaultValues} | |||
| isEditing={isEdit} | |||
| /> | |||
| <Stack | |||
| direction="row" | |||
| justifyContent="space-between" | |||
| flexWrap="wrap" | |||
| rowGap={2} | |||
| > | |||
| <Button | |||
| variant="contained" | |||
| onClick={onClickEdit} | |||
| // startIcon={<Add />} | |||
| //LinkComponent={Link} | |||
| //href="qcCategory/create" | |||
| > | |||
| {isEdit ? t("Save") : t("Edit")} | |||
| </Button> | |||
| </Stack> | |||
| {/* <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||
| <Tab label={t("View By FG") + (tabIndex === 0 ? " (Selected)" : "")} iconPosition="end" /> | |||
| <Tab label={t("View By Material") + (tabIndex === 1 ? " (Selected)" : "")} iconPosition="end" /> | |||
| </Tabs> */} | |||
| {serverError && ( | |||
| <Typography variant="body2" color="error" alignSelf="flex-end"> | |||
| {serverError} | |||
| </Typography> | |||
| )} | |||
| {/* {tabIndex === 0 && <ViewByFGDetails isEdit={isEdit} apiRef={apiRef} />} */} | |||
| <ViewByFGDetails isEdit={isEdit} apiRef={apiRef} /> | |||
| {/* {tabIndex === 1 && <ViewByBomDetails isEdit={isEdit} apiRef={apiRef} isHideButton={true} />} */} | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button | |||
| name="submit" | |||
| variant="contained" | |||
| startIcon={<Check />} | |||
| type="submit" | |||
| // disabled={submitDisabled} | |||
| > | |||
| {isEditMode ? t("Save") : t("Confirm")} | |||
| </Button> | |||
| <Button | |||
| variant="outlined" | |||
| startIcon={<Close />} | |||
| onClick={handleCancel} | |||
| > | |||
| {t("Cancel")} | |||
| </Button> | |||
| </Stack> | |||
| </Stack> | |||
| </FormProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| export default DetailScheduleDetailView; | |||
| @@ -40,6 +40,8 @@ export type FGRecord = { | |||
| inStockQty: number; | |||
| productionQty?: number; | |||
| purchaseQty?: number; | |||
| safetyStock?: number; | |||
| lastMonthAvgStock?: number | |||
| }; | |||
| const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit }) => { | |||
| @@ -1742,7 +1744,7 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit }) => { | |||
| }, | |||
| ]); | |||
| const updatePagingController = (updatedObj) => { | |||
| const updatePagingController = (updatedObj: any) => { | |||
| setPagingController((prevState) => { | |||
| return prevState.map((item, index) => { | |||
| if (index === updatedObj?.index) { | |||
| @@ -2176,6 +2178,7 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit }) => { | |||
| {`${t("FG Demand Date")}: ${date}`} | |||
| </Typography> */} | |||
| <EditableSearchResults<FGRecord> | |||
| index={1} | |||
| items={fakeRecords[index]} // Use the corresponding records for the day | |||
| columns={columns} | |||
| setPagingController={updatePagingController} | |||
| @@ -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} | |||
| @@ -0,0 +1,272 @@ | |||
| "use client"; | |||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { useRouter, useSearchParams } from "next/navigation"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { | |||
| FormProvider, | |||
| SubmitErrorHandler, | |||
| SubmitHandler, | |||
| useFieldArray, | |||
| useForm, | |||
| } from "react-hook-form"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| Grid, | |||
| Link, | |||
| Stack, | |||
| Tab, | |||
| Tabs, | |||
| TabsProps, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import { Add, Check, Close, EditNote } from "@mui/icons-material"; | |||
| import { useGridApiRef } from "@mui/x-data-grid"; | |||
| import DetailInfoCard from "@/components/DetailedScheduleDetail/DetailInfoCard"; | |||
| import ViewByFGDetails, { | |||
| // FGRecord, | |||
| } from "@/components/DetailedScheduleDetail/ViewByFGDetails"; | |||
| 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 { | |||
| // id: number; | |||
| // productionDate: string; | |||
| // totalJobOrders: number; | |||
| // totalProductionQty: number; | |||
| // } | |||
| type Props = { | |||
| isEditMode: boolean; | |||
| // type: TypeEnum; | |||
| defaultValues: Partial<DetailedProdScheduleResult> | undefined; | |||
| // qcChecks: ItemQc[] | |||
| type: ScheduleType; | |||
| }; | |||
| const DetailedScheduleDetailView: React.FC<Props> = ({ | |||
| isEditMode, | |||
| // type, | |||
| defaultValues, | |||
| // qcChecks, | |||
| type | |||
| }) => { | |||
| // console.log(type) | |||
| const apiRef = useGridApiRef(); | |||
| const params = useSearchParams(); | |||
| console.log(params.get("id")); | |||
| const [serverError, setServerError] = useState(""); | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| const { t } = useTranslation("schedule"); | |||
| const router = useRouter(); | |||
| 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); | |||
| }, | |||
| [], | |||
| ); | |||
| // const calNewProportion = useCallback((demandQty: number, bomOutputQty: number) => { | |||
| // return ((demandQty ?? 0) / (bomOutputQty ?? 1)).toFixed(2) | |||
| // }, []) | |||
| // const [pagingController, setPagingController] = useState({ | |||
| // pageNum: 1, | |||
| // pageSize: 10, | |||
| // totalCount: 0, | |||
| // }); | |||
| const handleCancel = () => { | |||
| router.replace(`/scheduling/detailed`); | |||
| }; | |||
| const onSubmit = useCallback<SubmitHandler<DetailedProdScheduleResult>>( | |||
| async (data, event) => { | |||
| const hasErrors = false; | |||
| console.log(errors); | |||
| // console.log(apiRef.current.getCellValue(2, "lowerLimit")) | |||
| // apiRef.current. | |||
| try { | |||
| if (hasErrors) { | |||
| setServerError(t("An error has occurred. Please try again later.")); | |||
| return false; | |||
| } | |||
| } catch (e) { | |||
| // backend error | |||
| setServerError(t("An error has occurred. Please try again later.")); | |||
| console.log(e); | |||
| } | |||
| }, | |||
| [apiRef, router, t], | |||
| ); | |||
| // multiple tabs | |||
| const onSubmitError = useCallback<SubmitErrorHandler<DetailedProdScheduleResult>>( | |||
| (errors) => { }, | |||
| [], | |||
| ); | |||
| const onClickEdit = () => { | |||
| setIsEdit(!isEdit); | |||
| }; | |||
| 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}> | |||
| <Stack | |||
| spacing={2} | |||
| component="form" | |||
| onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||
| > | |||
| {/*<Grid>*/} | |||
| {/* <Typography mb={2} variant="h4">*/} | |||
| {/* {t(`${mode} ${title}`)}*/} | |||
| {/* </Typography>*/} | |||
| {/*</Grid>*/} | |||
| <DetailInfoCard | |||
| // recordDetails={formProps.formState.defaultValues} | |||
| // isEditing={isEdit} | |||
| isEditing={false} | |||
| /> | |||
| {/* <Stack | |||
| direction="row" | |||
| justifyContent="space-between" | |||
| flexWrap="wrap" | |||
| rowGap={2} | |||
| > | |||
| <Button | |||
| variant="contained" | |||
| onClick={onClickEdit} | |||
| // startIcon={<Add />} | |||
| //LinkComponent={Link} | |||
| //href="qcCategory/create" | |||
| > | |||
| {isEdit ? t("Save") : t("Edit")} | |||
| </Button> | |||
| </Stack> */} | |||
| {/* <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||
| <Tab label={t("View By FG") + (tabIndex === 0 ? " (Selected)" : "")} iconPosition="end" /> | |||
| <Tab label={t("View By Material") + (tabIndex === 1 ? " (Selected)" : "")} iconPosition="end" /> | |||
| </Tabs> */} | |||
| {serverError && ( | |||
| <Typography variant="body2" color="error" alignSelf="flex-end"> | |||
| {serverError} | |||
| </Typography> | |||
| )} | |||
| {/* {tabIndex === 0 && <ViewByFGDetails isEdit={isEdit} apiRef={apiRef} />} */} | |||
| <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 | |||
| name="submit" | |||
| variant="contained" | |||
| startIcon={<Check />} | |||
| type="submit" | |||
| // disabled={submitDisabled} | |||
| > | |||
| {isEditMode ? t("Save") : t("Confirm")} | |||
| </Button> */} | |||
| <Button | |||
| variant="outlined" | |||
| startIcon={<ArrowBackIcon />} | |||
| onClick={handleCancel} | |||
| > | |||
| {t("Back")} | |||
| </Button> | |||
| </Stack> | |||
| </Stack> | |||
| </FormProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| export default DetailedScheduleDetailView; | |||
| @@ -0,0 +1,44 @@ | |||
| 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,240 @@ | |||
| "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: (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 = { | |||
| // id: string | number; | |||
| // code: string; | |||
| // name: string; | |||
| // inStockQty: number; | |||
| // productionQty?: number; | |||
| // purchaseQty?: number; | |||
| // }; | |||
| const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick, onEditClick, handleEditChange, onSaveClick, onCancelClick }) => { | |||
| const { | |||
| t, | |||
| i18n: { language }, | |||
| } = useTranslation("schedule"); | |||
| const { | |||
| getValues, | |||
| watch, | |||
| 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", | |||
| renderCell: (row) => { | |||
| return t(row.type); | |||
| }, | |||
| // 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-number", | |||
| style: { | |||
| textAlign: "right", | |||
| }, | |||
| renderCell: (row) => { | |||
| if (typeof row.demandQty == "number") { | |||
| return integerFormatter.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} | |||
| onReleaseClick={onReleaseClick} | |||
| onEditClick={onEditClick} | |||
| handleEditChange={handleEditChange} | |||
| onSaveClick={onSaveClick} | |||
| onCancelClick={onCancelClick} | |||
| /> | |||
| </Grid> | |||
| {/* ))} */} | |||
| </Grid> | |||
| ); | |||
| }; | |||
| export default ViewByFGDetails; | |||
| @@ -0,0 +1 @@ | |||
| export { default } from "./DetailedScheduleDetailWrapper"; | |||
| @@ -1 +1 @@ | |||
| export default from "./DoSaveWrapper" | |||
| // export default from "./DoSaveWrapper" | |||
| @@ -153,7 +153,7 @@ const InventorySearch: React.FC<Props> = ({ inventories }) => { | |||
| pagingController={{ | |||
| pageNum: 0, | |||
| pageSize: 0, | |||
| totalCount: 0, | |||
| // totalCount: 0, | |||
| }} | |||
| /> | |||
| </> | |||
| @@ -36,7 +36,7 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||
| { label: t("Name"), paramName: "name", type: "text" }, | |||
| ]; | |||
| return searchCriteria; | |||
| }, [t, items]); | |||
| }, [t]); | |||
| const onDetailClick = useCallback( | |||
| (item: ItemsResult) => { | |||
| @@ -70,7 +70,7 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||
| onClick: onDeleteClick, | |||
| }, | |||
| ], | |||
| [filteredItems], | |||
| [onDeleteClick, onDetailClick, t], | |||
| ); | |||
| const refetchData = useCallback( | |||
| @@ -102,12 +102,17 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||
| throw error; // Rethrow the error for further handling | |||
| } | |||
| }, | |||
| [axiosInstance, pagingController.pageNum, pagingController.pageSize], | |||
| [pagingController.pageNum, pagingController.pageSize], | |||
| ); | |||
| useEffect(() => { | |||
| refetchData(filterObj); | |||
| }, [filterObj, pagingController.pageNum, pagingController.pageSize]); | |||
| }, [ | |||
| filterObj, | |||
| pagingController.pageNum, | |||
| pagingController.pageSize, | |||
| refetchData, | |||
| ]); | |||
| const onReset = useCallback(() => { | |||
| setFilteredItems(items); | |||
| @@ -0,0 +1,84 @@ | |||
| import { JoDetail } from "@/app/api/jo"; | |||
| import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; | |||
| import { Box, Card, CardContent, Grid, Stack, TextField } from "@mui/material"; | |||
| import { upperFirst } from "lodash"; | |||
| import { useFormContext } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| type Props = { | |||
| }; | |||
| const InfoCard: React.FC<Props> = ({ | |||
| }) => { | |||
| const { t } = useTranslation(); | |||
| const { control, getValues, register, watch } = useFormContext<JoDetail>(); | |||
| return ( | |||
| <Card sx={{ display: "block" }}> | |||
| <CardContent component={Stack} spacing={4}> | |||
| <Box> | |||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| // { | |||
| // ...register("status") | |||
| // } | |||
| label={t("Status")} | |||
| fullWidth | |||
| disabled={true} | |||
| value={`${t(upperFirst(watch("status")))}`} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}/> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| { | |||
| ...register("code") | |||
| } | |||
| label={t("Code")} | |||
| fullWidth | |||
| disabled={true} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| { | |||
| ...register("name") | |||
| } | |||
| label={t("Name")} | |||
| fullWidth | |||
| disabled={true} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| // { | |||
| // ...register("name") | |||
| // } | |||
| label={t("Req. Qty")} | |||
| fullWidth | |||
| disabled={true} | |||
| defaultValue={`${integerFormatter.format(watch("reqQty"))}`} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| { | |||
| ...register("outputQtyUom") | |||
| } | |||
| label={t("UoM")} | |||
| fullWidth | |||
| disabled={true} | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| </CardContent> | |||
| </Card> | |||
| ) | |||
| } | |||
| export default InfoCard; | |||
| @@ -0,0 +1,107 @@ | |||
| "use client" | |||
| import { JoDetail } from "@/app/api/jo" | |||
| import { useRouter } from "next/navigation"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import useUploadContext from "../UploadProvider/useUploadContext"; | |||
| import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; | |||
| import { useCallback, useState } from "react"; | |||
| import { Button, Stack, Typography } from "@mui/material"; | |||
| import StartIcon from "@mui/icons-material/Start"; | |||
| import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | |||
| import { releaseJo } from "@/app/api/jo/actions"; | |||
| import InfoCard from "./InfoCard"; | |||
| import PickTable from "./PickTable"; | |||
| type Props = { | |||
| id: number; | |||
| defaultValues: Partial<JoDetail> | undefined; | |||
| } | |||
| const JoSave: React.FC<Props> = ({ | |||
| defaultValues, | |||
| id, | |||
| }) => { | |||
| const { t } = useTranslation("jo") | |||
| const router = useRouter(); | |||
| const { setIsUploading } = useUploadContext(); | |||
| const [serverError, setServerError] = useState(""); | |||
| const formProps = useForm<JoDetail>({ | |||
| defaultValues: defaultValues | |||
| }) | |||
| const handleBack = useCallback(() => { | |||
| router.replace(`/jo`) | |||
| }, []) | |||
| const handleRelease = useCallback(async () => { | |||
| try { | |||
| setIsUploading(true) | |||
| if (id) { | |||
| console.log(id) | |||
| const response = await releaseJo({ id: id }) | |||
| console.log(response.entity.status) | |||
| if (response) { | |||
| formProps.setValue("status", response.entity.status) | |||
| console.log(formProps.watch("status")) | |||
| } | |||
| } | |||
| } catch (e) { | |||
| // backend error | |||
| setServerError(t("An error has occurred. Please try again later.")); | |||
| console.log(e); | |||
| } finally { | |||
| setIsUploading(false) | |||
| } | |||
| }, []) | |||
| const onSubmit = useCallback<SubmitHandler<JoDetail>>(async (data, event) => { | |||
| console.log(data) | |||
| }, [t]) | |||
| const onSubmitError = useCallback<SubmitErrorHandler<JoDetail>>((errors) => { | |||
| console.log(errors) | |||
| }, [t]) | |||
| return <> | |||
| <FormProvider {...formProps}> | |||
| <Stack | |||
| spacing={2} | |||
| component="form" | |||
| onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||
| > | |||
| {serverError && ( | |||
| <Typography variant="body2" color="error" alignSelf="flex-end"> | |||
| {serverError} | |||
| </Typography> | |||
| )} | |||
| { | |||
| formProps.watch("status").toLowerCase() === "planning" && ( | |||
| <Stack direction="row" justifyContent="flex-start" gap={1}> | |||
| <Button | |||
| variant="outlined" | |||
| startIcon={<StartIcon />} | |||
| onClick={handleRelease} | |||
| > | |||
| {t("Release")} | |||
| </Button> | |||
| </Stack> | |||
| )} | |||
| <InfoCard /> | |||
| <PickTable /> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button | |||
| variant="outlined" | |||
| startIcon={<ArrowBackIcon />} | |||
| onClick={handleBack} | |||
| > | |||
| {t("Back")} | |||
| </Button> | |||
| </Stack> | |||
| </Stack> | |||
| </FormProvider> | |||
| </> | |||
| } | |||
| export default JoSave; | |||
| @@ -0,0 +1,26 @@ | |||
| import React from "react"; | |||
| import GeneralLoading from "../General/GeneralLoading"; | |||
| import { fetchJoDetail } from "@/app/api/jo"; | |||
| import JoSave from "./JoSave"; | |||
| interface SubComponents { | |||
| Loading: typeof GeneralLoading; | |||
| } | |||
| type JoSaveProps = { | |||
| id?: number; | |||
| } | |||
| type Props = JoSaveProps | |||
| const JoSaveWrapper: React.FC<Props> & SubComponents = async ({ | |||
| id, | |||
| }) => { | |||
| const jo = id ? await fetchJoDetail(id) : undefined | |||
| return <JoSave id={id} defaultValues={jo}/> | |||
| } | |||
| JoSaveWrapper.Loading = GeneralLoading; | |||
| export default JoSaveWrapper; | |||
| @@ -0,0 +1,91 @@ | |||
| import { JoDetail } from "@/app/api/jo"; | |||
| import { decimalFormatter } from "@/app/utils/formatUtil"; | |||
| import { GridColDef } from "@mui/x-data-grid"; | |||
| import { isEmpty, upperFirst } from "lodash"; | |||
| import { useMemo } from "react"; | |||
| import { useFormContext } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import StyledDataGrid from "../StyledDataGrid/StyledDataGrid"; | |||
| type Props = { | |||
| }; | |||
| const PickTable: React.FC<Props> = ({ | |||
| }) => { | |||
| const { t } = useTranslation("jo") | |||
| const { | |||
| watch | |||
| } = useFormContext<JoDetail>() | |||
| const columns = useMemo<GridColDef[]>(() => [ | |||
| { | |||
| field: "code", | |||
| headerName: t("Code"), | |||
| flex: 1, | |||
| }, | |||
| { | |||
| field: "name", | |||
| headerName: t("Name"), | |||
| flex: 1, | |||
| }, | |||
| { | |||
| field: "lotNo", | |||
| headerName: t("Lot No."), | |||
| flex: 1, | |||
| renderCell: (row) => { | |||
| return isEmpty(row.value) ? "N/A" : row.value | |||
| }, | |||
| }, | |||
| { | |||
| field: "reqQty", | |||
| headerName: t("Req. Qty"), | |||
| flex: 1, | |||
| align: "right", | |||
| headerAlign: "right", | |||
| renderCell: (row) => { | |||
| return decimalFormatter.format(row.value) | |||
| }, | |||
| }, | |||
| { | |||
| field: "uom", | |||
| headerName: t("UoM"), | |||
| flex: 1, | |||
| align: "left", | |||
| headerAlign: "left", | |||
| }, | |||
| { | |||
| field: "status", | |||
| headerName: t("Status"), | |||
| flex: 1, | |||
| renderCell: (row) => { | |||
| return upperFirst(row.value) | |||
| }, | |||
| }, | |||
| ], []) | |||
| return ( | |||
| <> | |||
| <StyledDataGrid | |||
| sx={{ | |||
| "--DataGrid-overlayHeight": "100px", | |||
| ".MuiDataGrid-row .MuiDataGrid-cell.hasError": { | |||
| border: "1px solid", | |||
| borderColor: "error.main", | |||
| }, | |||
| ".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": { | |||
| border: "1px solid", | |||
| borderColor: "warning.main", | |||
| }, | |||
| }} | |||
| disableColumnMenu | |||
| rows={watch("pickLines")} | |||
| columns={columns} | |||
| /> | |||
| </> | |||
| ) | |||
| } | |||
| export default PickTable; | |||
| @@ -0,0 +1 @@ | |||
| export { default } from "./JoSaveWrapper"; | |||
| @@ -0,0 +1,145 @@ | |||
| "use client" | |||
| import { SearchJoResult, SearchJoResultRequest, fetchJos } from "@/app/api/jo/actions"; | |||
| import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { Criterion } from "../SearchBox"; | |||
| import SearchResults, { Column, defaultPagingController } from "../SearchResults/SearchResults"; | |||
| import { EditNote } from "@mui/icons-material"; | |||
| import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; | |||
| import { uniqBy, upperFirst } from "lodash"; | |||
| import SearchBox from "../SearchBox/SearchBox"; | |||
| import { useRouter } from "next/navigation"; | |||
| interface Props { | |||
| defaultInputs: SearchJoResultRequest | |||
| } | |||
| type SearchQuery = Partial<Omit<SearchJoResult, "id">>; | |||
| type SearchParamNames = keyof SearchQuery; | |||
| const JoSearch: React.FC<Props> = ({ defaultInputs }) => { | |||
| const { t } = useTranslation("jo"); | |||
| const router = useRouter() | |||
| const [filteredJos, setFilteredJos] = useState<SearchJoResult[]>([]); | |||
| const [inputs, setInputs] = useState(defaultInputs); | |||
| const [pagingController, setPagingController] = useState( | |||
| defaultPagingController | |||
| ) | |||
| const [totalCount, setTotalCount] = useState(0) | |||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => [ | |||
| { label: t("Code"), paramName: "code", type: "text" }, | |||
| { label: t("Name"), paramName: "name", type: "text" }, | |||
| ], [t]) | |||
| const columns = useMemo<Column<SearchJoResult>[]>( | |||
| () => [ | |||
| { | |||
| name: "id", | |||
| label: t("Details"), | |||
| onClick: (record) => onDetailClick(record), | |||
| buttonIcon: <EditNote />, | |||
| }, | |||
| { | |||
| name: "code", | |||
| label: t("Code") | |||
| }, | |||
| { | |||
| name: "name", | |||
| label: t("Name"), | |||
| }, | |||
| { | |||
| name: "reqQty", | |||
| label: t("Req. Qty"), | |||
| align: "right", | |||
| headerAlign: "right", | |||
| renderCell: (row) => { | |||
| return integerFormatter.format(row.reqQty) | |||
| } | |||
| }, | |||
| { | |||
| name: "uom", | |||
| label: t("UoM"), | |||
| align: "left", | |||
| headerAlign: "left", | |||
| renderCell: (row) => { | |||
| return t(row.uom) | |||
| } | |||
| }, | |||
| { | |||
| name: "status", | |||
| label: t("Status"), | |||
| renderCell: (row) => { | |||
| return t(upperFirst(row.status)) | |||
| } | |||
| } | |||
| ], [] | |||
| ) | |||
| const refetchData = useCallback(async ( | |||
| query: Record<SearchParamNames, string> | SearchJoResultRequest, | |||
| actionType: "reset" | "search" | "paging", | |||
| ) => { | |||
| const params: SearchJoResultRequest = { | |||
| code: query.code, | |||
| name: query.name, | |||
| pageNum: pagingController.pageNum - 1, | |||
| pageSize: pagingController.pageSize, | |||
| } | |||
| const response = await fetchJos(params) | |||
| if (response) { | |||
| setTotalCount(response.total); | |||
| switch (actionType) { | |||
| case "reset": | |||
| case "search": | |||
| setFilteredJos(() => response.records); | |||
| break; | |||
| case "paging": | |||
| setFilteredJos((fs) => | |||
| uniqBy([...fs, ...response.records], "id"), | |||
| ); | |||
| break; | |||
| } | |||
| } | |||
| }, [pagingController, setPagingController]) | |||
| useEffect(() => { | |||
| refetchData(inputs, "paging"); | |||
| }, [pagingController]); | |||
| const onDetailClick = useCallback((record: SearchJoResult) => { | |||
| router.push(`/jo/edit?id=${record.id}`) | |||
| }, []) | |||
| const onSearch = useCallback((query: Record<SearchParamNames, string>) => { | |||
| setInputs(() => ({ | |||
| code: query.code, | |||
| name: query.name | |||
| })) | |||
| refetchData(query, "search"); | |||
| }, []) | |||
| const onReset = useCallback(() => { | |||
| refetchData(defaultInputs, "paging"); | |||
| }, []) | |||
| return <> | |||
| <SearchBox | |||
| criteria={searchCriteria} | |||
| onSearch={onSearch} | |||
| onReset={onReset} | |||
| /> | |||
| <SearchResults<SearchJoResult> | |||
| items={filteredJos} | |||
| columns={columns} | |||
| setPagingController={setPagingController} | |||
| pagingController={pagingController} | |||
| totalCount={totalCount} | |||
| // isAutoPaging={false} | |||
| /> | |||
| </> | |||
| } | |||
| export default JoSearch; | |||
| @@ -0,0 +1,21 @@ | |||
| import React from "react"; | |||
| import GeneralLoading from "../General/GeneralLoading"; | |||
| import JoSearch from "./JoSearch"; | |||
| import { SearchJoResultRequest } from "@/app/api/jo/actions"; | |||
| interface SubComponents { | |||
| Loading: typeof GeneralLoading; | |||
| } | |||
| const JoSearchWrapper: React.FC & SubComponents = async () => { | |||
| const defaultInputs: SearchJoResultRequest = { | |||
| code: "", | |||
| name: "", | |||
| } | |||
| return <JoSearch defaultInputs={defaultInputs}/> | |||
| } | |||
| JoSearchWrapper.Loading = GeneralLoading; | |||
| export default JoSearchWrapper; | |||
| @@ -0,0 +1 @@ | |||
| export { default } from "./JoSearchWrapper" | |||
| @@ -24,6 +24,7 @@ type LoginFields = { | |||
| type SessionWithAbilities = | |||
| | ({ | |||
| abilities: string[]; | |||
| accessToken?: string; | |||
| } & Session) | |||
| | null; | |||
| @@ -73,9 +74,9 @@ const LoginForm: React.FC = () => { | |||
| // set auth to local storage | |||
| const session = (await getSession()) as SessionWithAbilities; | |||
| // @ts-ignore | |||
| window.localStorage.setItem("accessToken", session?.accessToken); | |||
| setAccessToken(session?.accessToken); | |||
| SetupAxiosInterceptors(session?.accessToken); | |||
| window.localStorage.setItem("accessToken", session?.accessToken ?? ""); | |||
| setAccessToken(session?.accessToken ?? null); | |||
| SetupAxiosInterceptors(session?.accessToken ?? null); | |||
| // console.log(session) | |||
| window.localStorage.setItem( | |||
| "abilities", | |||
| @@ -31,8 +31,8 @@ import { MailTemplate } from "@/app/api/mail"; | |||
| import TemplateDetails from "./TemplateDetails"; | |||
| import QrCodeScanner from "../QrCodeScanner/QrCodeScanner"; | |||
| import { | |||
| QcCodeScannerContext, | |||
| useQcCodeScanner, | |||
| QrCodeScannerContext, | |||
| useQrCodeScannerContext, | |||
| } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | |||
| export interface Props { | |||
| @@ -34,7 +34,7 @@ const TimesheetMailDetails: React.FC<Props> = ({ isActive }) => { | |||
| {t("Timesheet Template")} | |||
| </Typography> | |||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
| <Grid item xs={8}> | |||
| {/* <Grid item xs={8}> | |||
| <TextField | |||
| label={t("Cc")} | |||
| fullWidth | |||
| @@ -57,7 +57,7 @@ const TimesheetMailDetails: React.FC<Props> = ({ isActive }) => { | |||
| })} | |||
| error={Boolean(errors.template?.subject)} | |||
| /> | |||
| </Grid> | |||
| </Grid> */} | |||
| <Grid item xs={8}> | |||
| <TextField | |||
| label={t("Required Params")} | |||
| @@ -67,7 +67,7 @@ const TimesheetMailDetails: React.FC<Props> = ({ isActive }) => { | |||
| // error={Boolean(errors.template?.template)} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| {/* <Grid item xs={12}> | |||
| <Controller | |||
| control={control} | |||
| name="template.template" | |||
| @@ -83,7 +83,7 @@ const TimesheetMailDetails: React.FC<Props> = ({ isActive }) => { | |||
| validate: (value) => value?.includes("${date}"), | |||
| }} | |||
| /> | |||
| </Grid> | |||
| </Grid> */} | |||
| </Grid> | |||
| </Box> | |||
| </CardContent> | |||
| @@ -52,7 +52,7 @@ const NavigationContent: React.FC = () => { | |||
| { | |||
| icon: <RequestQuote />, | |||
| label: "Pick Order", | |||
| path: "/pickorder", | |||
| path: "/pickOrder", | |||
| }, | |||
| // { | |||
| // icon: <RequestQuote />, | |||
| @@ -179,7 +179,7 @@ const NavigationContent: React.FC = () => { | |||
| { | |||
| icon: <RequestQuote />, | |||
| label: "Detail Scheduling", | |||
| path: "/scheduling/detail", | |||
| path: "/scheduling/detailed", | |||
| }, | |||
| { | |||
| icon: <RequestQuote />, | |||
| @@ -188,6 +188,18 @@ const NavigationContent: React.FC = () => { | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| icon: <RequestQuote />, | |||
| label: "Job Order", | |||
| path: "", | |||
| children: [ | |||
| { | |||
| icon: <RequestQuote />, | |||
| label: "Job Order", | |||
| path: "/jo", | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| icon: <RequestQuote />, | |||
| label: "Settings", | |||
| @@ -263,11 +275,11 @@ const NavigationContent: React.FC = () => { | |||
| label: "QC Check Template", | |||
| path: "/settings/user", | |||
| }, | |||
| { | |||
| icon: <RequestQuote />, | |||
| label: "Mail", | |||
| path: "/settings/mail", | |||
| }, | |||
| // { | |||
| // icon: <RequestQuote />, | |||
| // label: "Mail", | |||
| // path: "/settings/mail", | |||
| // }, | |||
| { | |||
| icon: <RequestQuote />, | |||
| label: "Import Testing", | |||
| @@ -5,14 +5,14 @@ import { | |||
| PickOrderQcInput, | |||
| updateStockOutLine, | |||
| UpdateStockOutLine, | |||
| } from "@/app/api/pickorder/actions"; | |||
| } from "@/app/api/pickOrder/actions"; | |||
| import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||
| import QcContent from "./QcContent"; | |||
| import { Box, Button, Modal, ModalProps, Stack } from "@mui/material"; | |||
| import { useCallback } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { Check } from "@mui/icons-material"; | |||
| import { StockOutLine } from "@/app/api/pickorder"; | |||
| import { StockOutLine } from "@/app/api/pickOrder"; | |||
| import dayjs from "dayjs"; | |||
| import { INPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT } from "@/app/utils/formatUtil"; | |||
| import ApprovalContent from "./ApprovalContent"; | |||
| @@ -63,7 +63,7 @@ const ApprovalForm: React.FC<Props> = ({ | |||
| [onClose], | |||
| ); | |||
| const onSubmit = useCallback<SubmitHandler<PickOrderApprovalInput & {}>>( | |||
| const onSubmit = useCallback<SubmitHandler<PickOrderApprovalInput>>( | |||
| async (data, event) => { | |||
| console.log(data); | |||
| // checking later | |||
| @@ -1,12 +1,7 @@ | |||
| "use client"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| ButtonProps, | |||
| Card, | |||
| CardContent, | |||
| CardHeader, | |||
| CircularProgress, | |||
| Grid, | |||
| Stack, | |||
| @@ -28,14 +23,14 @@ import { | |||
| GridEditInputCell, | |||
| GridRowParams, | |||
| } from "@mui/x-data-grid"; | |||
| import { PlayArrow } from "@mui/icons-material"; | |||
| import DoneIcon from "@mui/icons-material/Done"; | |||
| import { GridRowSelectionModel } from "@mui/x-data-grid"; | |||
| import { useQcCodeScanner } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | |||
| import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | |||
| import { | |||
| completeConsoPickOrder, | |||
| CreateStockOutLine, | |||
| createStockOutLine, | |||
| fetchConsoStatus, | |||
| fetchPickOrderLineClient, | |||
| fetchStockOutLineClient, | |||
| PickOrderApprovalInput, | |||
| @@ -63,6 +58,7 @@ import AutoFixNormalIcon from "@mui/icons-material/AutoFixNormal"; | |||
| import ApprovalForm from "./ApprovalForm"; | |||
| import InfoIcon from "@mui/icons-material/Info"; | |||
| import VerifiedIcon from "@mui/icons-material/Verified"; | |||
| import { isNullOrUndefined } from "html5-qrcode/esm/core"; | |||
| interface Props { | |||
| qc: QcItemWithChecks[]; | |||
| @@ -171,9 +167,16 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||
| headerName: "location", | |||
| flex: 1, | |||
| renderCell: (params) => { | |||
| if (!params.row.warehouse) return <></>; | |||
| const warehouseList = JSON.parse(params.row.warehouse) as string[]; | |||
| return FitAllCell(warehouseList); | |||
| // console.log(params.row.warehouse) | |||
| return params.row.warehouse; | |||
| if (isNullOrUndefined(params.row.warehouse)) { | |||
| return <></>; | |||
| } else { | |||
| const warehouseList = JSON.parse( | |||
| `{${params.row.warehouse}}`, | |||
| ) as string[]; | |||
| return FitAllCell(warehouseList); | |||
| } | |||
| }, | |||
| }, | |||
| { | |||
| @@ -181,7 +184,8 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||
| headerName: "suggestedLotNo", | |||
| flex: 1.2, | |||
| renderCell: (params) => { | |||
| if (!params.row.suggestedLotNo) return <></>; | |||
| return params.row.suggestedLotNo; | |||
| if (isNullOrUndefined(params.row.suggestedLotNo)) return <></>; | |||
| const suggestedLotNoList = JSON.parse( | |||
| params.row.suggestedLotNo, | |||
| ) as string[]; | |||
| @@ -200,7 +204,14 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||
| [], | |||
| ); | |||
| const [isCompletedOrder, setIsCompletedOrder] = useState(false); | |||
| const [isDisableComplete, setIsDisableComplete] = useState(true); | |||
| const [status, setStatus] = useState(""); | |||
| const getConsoStatus = useCallback(async () => { | |||
| const status = await fetchConsoStatus(consoCode); | |||
| console.log(status); | |||
| setStatus(status.status); | |||
| }, [fetchConsoStatus]); | |||
| const fetchPickOrderLine = useCallback( | |||
| async (params: Record<string, any>) => { | |||
| @@ -215,9 +226,10 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||
| if (res) { | |||
| console.log(res); | |||
| console.log(res.records.every((line) => line.status == "completed")); | |||
| setIsCompletedOrder(() => | |||
| res.records.every((line) => line.status == "completed"), | |||
| ); | |||
| setIsDisableComplete(res.records[0].poStatus === "completed"); | |||
| // setIsDisableComplete(() => | |||
| // res.records.every((line) => line.status !== "completed"), | |||
| // ); | |||
| setPickOrderLine(res.records); | |||
| setPolTotalCount(res.total); | |||
| @@ -233,21 +245,6 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||
| [fetchPickOrderLineClient, consoCode], | |||
| ); | |||
| const buttonData = useMemo( | |||
| () => ({ | |||
| buttonName: "complete", | |||
| title: t("Do you want to complete?"), | |||
| confirmButtonText: t("Complete"), | |||
| successTitle: t("Complete Success"), | |||
| errorTitle: t("Complete Fail"), | |||
| buttonText: t("Complete Pick Order"), | |||
| buttonIcon: <DoneIcon />, | |||
| buttonColor: "info", | |||
| disabled: true, | |||
| }), | |||
| [], | |||
| ); | |||
| const [stockOutLine, setStockOutLine] = useState<StockOutLine[]>([]); | |||
| const getRowId = useCallback<GridRowIdGetter<StockOutLineRow>>( | |||
| @@ -505,6 +502,7 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||
| key="edit" | |||
| />, | |||
| <GridActionsCellItem | |||
| key={1} | |||
| icon={<DoDisturbIcon />} | |||
| label="Delete" | |||
| sx={{ | |||
| @@ -593,7 +591,9 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||
| useEffect(() => { | |||
| if (!qcOpen || !approvalOpen) { | |||
| triggerRefetch(); | |||
| getConsoStatus(); | |||
| fetchPickOrderLine(polCriteriaArgs); | |||
| // getConsoStatus() | |||
| } | |||
| if (selectedRow.length > 0) fetchStockOutLine(solCriteriaArgs, selectedRow); | |||
| }, [ | |||
| @@ -603,6 +603,7 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||
| selectedRow, | |||
| triggerRefetch, | |||
| polCriteriaArgs, | |||
| getConsoStatus, | |||
| ]); | |||
| const getLotDetail = useCallback( | |||
| @@ -630,7 +631,7 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||
| setOpenScanner((prev) => !prev); | |||
| }, []); | |||
| const scanner = useQcCodeScanner(); | |||
| const scanner = useQrCodeScannerContext(); | |||
| useEffect(() => { | |||
| if (isOpenScanner && !scanner.isScanning) { | |||
| scanner.startScan(); | |||
| @@ -672,6 +673,8 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||
| changeRow, | |||
| addRow, | |||
| getLotDetail, | |||
| scanner, | |||
| setIsUploading, | |||
| ]); | |||
| const mannuallyAddRow = useCallback(() => { | |||
| @@ -679,7 +682,7 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||
| addRow(qrcode); | |||
| // scanner.resetScan(); | |||
| }); | |||
| }, [addRow, homemade_Qrcode]); | |||
| }, [addRow, getLotDetail, homemade_Qrcode.stockInLineId]); | |||
| const validation = useCallback( | |||
| ( | |||
| @@ -739,32 +742,35 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||
| const handleCompleteOrder = useCallback(async () => { | |||
| const res = await completeConsoPickOrder(consoCode); | |||
| if (res) { | |||
| if (res.message === "completed") { | |||
| console.log(res); | |||
| // completed | |||
| triggerRefetch(); | |||
| // setIsCompletedOrder(false) | |||
| } else { | |||
| // not completed | |||
| triggerRefetch(); | |||
| } | |||
| }, [consoCode, triggerRefetch]); | |||
| return ( | |||
| <> | |||
| <Stack spacing={2}> | |||
| <Grid container xs={12} justifyContent="start"> | |||
| <Grid item xs={12}> | |||
| <Typography variant="h4" marginInlineEnd={2}> | |||
| {consoCode} | |||
| {consoCode} - {status} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={8}> | |||
| <Button | |||
| // onClick={buttonData.onClick} | |||
| disabled={!isCompletedOrder} | |||
| color={buttonData.buttonColor as ButtonProps["color"]} | |||
| startIcon={buttonData.buttonIcon} | |||
| disabled={isDisableComplete} | |||
| color={"info"} | |||
| startIcon={<DoneIcon />} | |||
| onClick={handleCompleteOrder} | |||
| > | |||
| {buttonData.buttonText} | |||
| {t("Complete Pick Order")} | |||
| </Button> | |||
| </Grid> | |||
| <Grid | |||
| @@ -774,13 +780,16 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||
| justifyContent="end" | |||
| alignItems="end" | |||
| > | |||
| <Button | |||
| {/* <Button | |||
| onClick={mannuallyAddRow} | |||
| disabled={selectedRow.length === 0} | |||
| > | |||
| {isOpenScanner ? t("manual scanning") : t("manual scan")} | |||
| </Button> | |||
| <Button onClick={onOpenScanner} disabled={selectedRow.length === 0}> | |||
| </Button> */} | |||
| <Button | |||
| onClick={onOpenScanner} | |||
| disabled={isDisableComplete ?? selectedRow.length === 0} | |||
| > | |||
| {isOpenScanner ? t("binding") : t("bind")} | |||
| </Button> | |||
| </Grid> | |||
| @@ -36,7 +36,7 @@ import { NEXT_PUBLIC_API_URL } from "@/config/api"; | |||
| import axiosInstance from "@/app/(main)/axios/axiosInstance"; | |||
| import TwoLineCell from "../PoDetail/TwoLineCell"; | |||
| import QcSelect from "../PoDetail/QcSelect"; | |||
| import { PickOrderQcInput } from "@/app/api/pickorder/actions"; | |||
| import { PickOrderQcInput } from "@/app/api/pickOrder/actions"; | |||
| interface Props { | |||
| qcDefaultValues: PickOrderQcInput; | |||
| @@ -4,14 +4,14 @@ import { | |||
| PickOrderQcInput, | |||
| updateStockOutLine, | |||
| UpdateStockOutLine, | |||
| } from "@/app/api/pickorder/actions"; | |||
| } from "@/app/api/pickOrder/actions"; | |||
| import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||
| import QcContent from "./QcContent"; | |||
| import { Box, Button, Modal, ModalProps, Stack } from "@mui/material"; | |||
| import { useCallback } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { Check } from "@mui/icons-material"; | |||
| import { StockOutLine } from "@/app/api/pickorder"; | |||
| import { StockOutLine } from "@/app/api/pickOrder"; | |||
| import dayjs from "dayjs"; | |||
| import { INPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT } from "@/app/utils/formatUtil"; | |||
| @@ -60,7 +60,7 @@ const QcForm: React.FC<Props> = ({ | |||
| [onClose], | |||
| ); | |||
| const onSubmit = useCallback<SubmitHandler<PickOrderQcInput & {}>>( | |||
| const onSubmit = useCallback<SubmitHandler<PickOrderQcInput>>( | |||
| async (data, event) => { | |||
| console.log(data); | |||
| console.log(qcDefaultValues); | |||
| @@ -86,7 +86,7 @@ const QcForm: React.FC<Props> = ({ | |||
| console.log("bug la"); | |||
| } | |||
| }, | |||
| [updateStockOutLine], | |||
| [closeHandler, qcDefaultValues], | |||
| ); | |||
| return ( | |||
| <> | |||
| @@ -12,7 +12,7 @@ import { | |||
| } from "react"; | |||
| import { GridColDef } from "@mui/x-data-grid"; | |||
| import { CircularProgress, Grid, Typography } from "@mui/material"; | |||
| import { ByItemsSummary } from "@/app/api/pickorder"; | |||
| import { ByItemsSummary } from "@/app/api/pickOrder"; | |||
| import { useTranslation } from "react-i18next"; | |||
| dayjs.extend(arraySupport); | |||
| @@ -12,7 +12,7 @@ import { | |||
| } from "react"; | |||
| import { GridColDef, GridInputRowSelectionModel } from "@mui/x-data-grid"; | |||
| import { Box, CircularProgress, Grid, Typography } from "@mui/material"; | |||
| import { PickOrderResult } from "@/app/api/pickorder"; | |||
| import { PickOrderResult } from "@/app/api/pickOrder"; | |||
| import { useTranslation } from "react-i18next"; | |||
| dayjs.extend(arraySupport); | |||
| @@ -29,7 +29,7 @@ import { | |||
| ConsoPickOrderResult, | |||
| PickOrderLine, | |||
| PickOrderResult, | |||
| } from "@/app/api/pickorder"; | |||
| } from "@/app/api/pickOrder"; | |||
| import { useRouter, useSearchParams } from "next/navigation"; | |||
| import ConsolidatePickOrderItemSum from "./ConsolidatePickOrderItemSum"; | |||
| import ConsolidatePickOrderSum from "./ConsolidatePickOrderSum"; | |||
| @@ -39,7 +39,7 @@ import { | |||
| fetchConsoPickOrderClient, | |||
| releasePickOrder, | |||
| ReleasePickOrderInputs, | |||
| } from "@/app/api/pickorder/actions"; | |||
| } from "@/app/api/pickOrder/actions"; | |||
| import { EditNote } from "@mui/icons-material"; | |||
| import { fetchNameList, NameList } from "@/app/api/user/actions"; | |||
| import { useField } from "@mui/x-date-pickers/internals"; | |||
| @@ -111,7 +111,7 @@ const ConsolidatedPickOrders: React.FC<Props> = ({ filterArgs }) => { | |||
| console.log(pickOrder); | |||
| const status = pickOrder.status; | |||
| if (pickOrderStatusMap[status] >= 3) { | |||
| router.push(`/pickorder/detail?consoCode=${pickOrder.consoCode}`); | |||
| router.push(`/pickOrder/detail?consoCode=${pickOrder.consoCode}`); | |||
| } else { | |||
| openDetailModal(pickOrder.consoCode); | |||
| } | |||
| @@ -135,7 +135,7 @@ const ConsolidatedPickOrders: React.FC<Props> = ({ filterArgs }) => { | |||
| label: t("status"), | |||
| }, | |||
| ], | |||
| [], | |||
| [onDetailClick, t], | |||
| ); | |||
| const [pagingController, setPagingController] = useState( | |||
| defaultPagingController, | |||
| @@ -215,18 +215,18 @@ const ConsolidatedPickOrders: React.FC<Props> = ({ filterArgs }) => { | |||
| console.log(newValue); | |||
| formProps.setValue("assignTo", newValue.id); | |||
| }, | |||
| [], | |||
| [formProps], | |||
| ); | |||
| const onSubmit = useCallback<SubmitHandler<ReleasePickOrderInputs & {}>>( | |||
| const onSubmit = useCallback<SubmitHandler<ReleasePickOrderInputs>>( | |||
| async (data, event) => { | |||
| console.log(data); | |||
| try { | |||
| const res = await releasePickOrder(data); | |||
| console.log(res); | |||
| console.log(res.status); | |||
| if (res.status === 200) { | |||
| router.push(`/pickorder/detail?consoCode=${data.consoCode}`); | |||
| if (res.consoCode.length > 0) { | |||
| console.log(res); | |||
| router.push(`/pickOrder/detail?consoCode=${res.consoCode}`); | |||
| } else { | |||
| console.log(res); | |||
| } | |||
| @@ -234,7 +234,7 @@ const ConsolidatedPickOrders: React.FC<Props> = ({ filterArgs }) => { | |||
| console.log(error); | |||
| } | |||
| }, | |||
| [releasePickOrder], | |||
| [router], | |||
| ); | |||
| const onSubmitError = useCallback<SubmitErrorHandler<ReleasePickOrderInputs>>( | |||
| (errors) => {}, | |||
| @@ -250,7 +250,7 @@ const ConsolidatedPickOrders: React.FC<Props> = ({ filterArgs }) => { | |||
| fetchConso(consoCode); | |||
| formProps.setValue("consoCode", consoCode); | |||
| } | |||
| }, [consoCode]); | |||
| }, [consoCode, fetchConso, formProps]); | |||
| return ( | |||
| <> | |||
| @@ -0,0 +1,318 @@ | |||
| "use client"; | |||
| import { PurchaseQcResult, PurchaseQCInput } from "@/app/api/po/actions"; | |||
| import { | |||
| Autocomplete, | |||
| Box, | |||
| Card, | |||
| CardContent, | |||
| FormControl, | |||
| Grid, | |||
| Stack, | |||
| TextField, | |||
| Tooltip, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import { Controller, useFormContext } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import StyledDataGrid from "../StyledDataGrid"; | |||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { | |||
| GridColDef, | |||
| GridRowIdGetter, | |||
| GridRowModel, | |||
| useGridApiContext, | |||
| GridRenderCellParams, | |||
| GridRenderEditCellParams, | |||
| useGridApiRef, | |||
| } from "@mui/x-data-grid"; | |||
| import InputDataGrid from "../InputDataGrid"; | |||
| import { TableRow } from "../InputDataGrid/InputDataGrid"; | |||
| import { GridEditInputCell } from "@mui/x-data-grid"; | |||
| import { StockInLine } from "@/app/api/po"; | |||
| import { INPUT_DATE_FORMAT, stockInLineStatusMap } from "@/app/utils/formatUtil"; | |||
| import { fetchQcItemCheck, fetchQcResult } from "@/app/api/qc/actions"; | |||
| import { QcItemWithChecks } from "@/app/api/qc"; | |||
| import axios from "@/app/(main)/axios/axiosInstance"; | |||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | |||
| import axiosInstance from "@/app/(main)/axios/axiosInstance"; | |||
| import { SavePickOrderLineRequest, SavePickOrderRequest } from "@/app/api/pickOrder/actions"; | |||
| import TwoLineCell from "../PoDetail/TwoLineCell"; | |||
| import ItemSelect from "./ItemSelect"; | |||
| import { ItemCombo } from "@/app/api/settings/item/actions"; | |||
| import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | |||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
| import dayjs from "dayjs"; | |||
| interface Props { | |||
| items: ItemCombo[]; | |||
| // disabled: boolean; | |||
| } | |||
| type EntryError = | |||
| | { | |||
| [field in keyof SavePickOrderLineRequest]?: string; | |||
| } | |||
| | undefined; | |||
| type PolRow = TableRow<Partial<SavePickOrderLineRequest>, EntryError>; | |||
| // fetchQcItemCheck | |||
| const CreateForm: React.FC<Props> = ({ items }) => { | |||
| const { | |||
| t, | |||
| i18n: { language }, | |||
| } = useTranslation("purchaseOrder"); | |||
| const apiRef = useGridApiRef(); | |||
| const { | |||
| formState: { errors, defaultValues, touchedFields }, | |||
| watch, | |||
| control, | |||
| setValue, | |||
| } = useFormContext<SavePickOrderRequest>(); | |||
| console.log(defaultValues); | |||
| const targetDate = watch("targetDate"); | |||
| //// validate form | |||
| // const accQty = watch("acceptedQty"); | |||
| // const validateForm = useCallback(() => { | |||
| // console.log(accQty); | |||
| // if (accQty > itemDetail.acceptedQty) { | |||
| // setError("acceptedQty", { | |||
| // message: `${t("acceptedQty must not greater than")} ${ | |||
| // itemDetail.acceptedQty | |||
| // }`, | |||
| // type: "required", | |||
| // }); | |||
| // } | |||
| // if (accQty < 1) { | |||
| // setError("acceptedQty", { | |||
| // message: t("minimal value is 1"), | |||
| // type: "required", | |||
| // }); | |||
| // } | |||
| // if (isNaN(accQty)) { | |||
| // setError("acceptedQty", { | |||
| // message: t("value must be a number"), | |||
| // type: "required", | |||
| // }); | |||
| // } | |||
| // }, [accQty]); | |||
| // useEffect(() => { | |||
| // clearErrors(); | |||
| // validateForm(); | |||
| // }, [clearErrors, validateForm]); | |||
| const columns = useMemo<GridColDef[]>( | |||
| () => [ | |||
| { | |||
| field: "itemId", | |||
| headerName: t("Item"), | |||
| flex: 1, | |||
| editable: true, | |||
| valueFormatter(params) { | |||
| const row = params.id ? params.api.getRow<PolRow>(params.id) : null; | |||
| if (!row) { | |||
| return null; | |||
| } | |||
| const Item = items.find((q) => q.id === row.itemId); | |||
| return Item ? Item.label : t("Please select item"); | |||
| }, | |||
| renderCell(params: GridRenderCellParams<PolRow, number>) { | |||
| console.log(params.value); | |||
| return <TwoLineCell>{params.formattedValue}</TwoLineCell>; | |||
| }, | |||
| renderEditCell(params: GridRenderEditCellParams<PolRow, number>) { | |||
| const errorMessage = | |||
| params.row._error?.[params.field as keyof SavePickOrderLineRequest]; | |||
| console.log(errorMessage); | |||
| const content = ( | |||
| // <></> | |||
| <ItemSelect | |||
| allItems={items} | |||
| value={params.row.itemId} | |||
| onItemSelect={async (itemId, uom, uomId) => { | |||
| console.log(uom) | |||
| await params.api.setEditCellValue({ | |||
| id: params.id, | |||
| field: "itemId", | |||
| value: itemId, | |||
| }); | |||
| await params.api.setEditCellValue({ | |||
| id: params.id, | |||
| field: "uom", | |||
| value: uom | |||
| }) | |||
| await params.api.setEditCellValue({ | |||
| id: params.id, | |||
| field: "uomId", | |||
| value: uomId | |||
| }) | |||
| }} | |||
| /> | |||
| ); | |||
| return errorMessage ? ( | |||
| <Tooltip title={errorMessage}> | |||
| <Box width="100%">{content}</Box> | |||
| </Tooltip> | |||
| ) : ( | |||
| content | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| field: "qty", | |||
| headerName: t("qty"), | |||
| flex: 1, | |||
| type: "number", | |||
| editable: true, | |||
| renderEditCell(params: GridRenderEditCellParams<PolRow>) { | |||
| const errorMessage = | |||
| params.row._error?.[params.field as keyof SavePickOrderLineRequest]; | |||
| const content = <GridEditInputCell {...params} />; | |||
| return errorMessage ? ( | |||
| <Tooltip title={t(errorMessage)}> | |||
| <Box width="100%">{content}</Box> | |||
| </Tooltip> | |||
| ) : ( | |||
| content | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| field: "uom", | |||
| headerName: t("uom"), | |||
| flex: 1, | |||
| editable: true, | |||
| // renderEditCell(params: GridRenderEditCellParams<PolRow>) { | |||
| // console.log(params.row) | |||
| // const errorMessage = | |||
| // params.row._error?.[params.field as keyof SavePickOrderLineRequest]; | |||
| // const content = <GridEditInputCell {...params} />; | |||
| // return errorMessage ? ( | |||
| // <Tooltip title={t(errorMessage)}> | |||
| // <Box width="100%">{content}</Box> | |||
| // </Tooltip> | |||
| // ) : ( | |||
| // content | |||
| // ); | |||
| // } | |||
| } | |||
| ], | |||
| [items, t], | |||
| ); | |||
| /// validate datagrid | |||
| const validation = useCallback( | |||
| (newRow: GridRowModel<PolRow>): EntryError => { | |||
| const error: EntryError = {}; | |||
| const { itemId, qty } = newRow; | |||
| if (!itemId || itemId <= 0) { | |||
| error["itemId"] = t("select qc"); | |||
| } | |||
| if (!qty || qty <= 0) { | |||
| error["qty"] = t("enter a qty"); | |||
| } | |||
| return Object.keys(error).length > 0 ? error : undefined; | |||
| }, | |||
| [], | |||
| ); | |||
| const typeList = [ | |||
| { | |||
| type: "Consumable" | |||
| } | |||
| ] | |||
| const onChange = useCallback( | |||
| (event: React.SyntheticEvent, newValue: {type: string}) => { | |||
| console.log(newValue); | |||
| setValue("type", newValue.type); | |||
| }, | |||
| [setValue], | |||
| ); | |||
| return ( | |||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||
| <Grid item xs={12}> | |||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||
| {t("Pick Order Detail")} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid | |||
| container | |||
| justifyContent="flex-start" | |||
| alignItems="flex-start" | |||
| spacing={2} | |||
| sx={{ mt: 0.5 }} | |||
| > | |||
| <Grid item xs={6} lg={6}> | |||
| <FormControl fullWidth> | |||
| <Autocomplete | |||
| disableClearable | |||
| fullWidth | |||
| getOptionLabel={(option) => option.type} | |||
| options={typeList} | |||
| onChange={onChange} | |||
| renderInput={(params) => <TextField {...params} label={t("type")}/>} | |||
| /> | |||
| </FormControl> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <Controller | |||
| control={control} | |||
| name="targetDate" | |||
| // rules={{ required: !Boolean(productionDate) }} | |||
| render={({ field }) => { | |||
| return ( | |||
| <LocalizationProvider | |||
| dateAdapter={AdapterDayjs} | |||
| adapterLocale={`${language}-hk`} | |||
| > | |||
| <DatePicker | |||
| {...field} | |||
| sx={{ width: "100%" }} | |||
| label={t("targetDate")} | |||
| value={targetDate ? dayjs(targetDate) : undefined} | |||
| onChange={(date) => { | |||
| console.log(date); | |||
| if (!date) return; | |||
| console.log(date.format(INPUT_DATE_FORMAT)); | |||
| setValue("targetDate", date.format(INPUT_DATE_FORMAT)); | |||
| // field.onChange(date); | |||
| }} | |||
| inputRef={field.ref} | |||
| slotProps={{ | |||
| textField: { | |||
| // required: true, | |||
| error: Boolean(errors.targetDate?.message), | |||
| helperText: errors.targetDate?.message, | |||
| }, | |||
| }} | |||
| /> | |||
| </LocalizationProvider> | |||
| ); | |||
| }} | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| <Grid | |||
| container | |||
| justifyContent="flex-start" | |||
| alignItems="flex-start" | |||
| spacing={2} | |||
| sx={{ mt: 0.5 }} | |||
| > | |||
| <Grid item xs={12}> | |||
| <InputDataGrid<SavePickOrderRequest, SavePickOrderLineRequest, EntryError> | |||
| apiRef={apiRef} | |||
| checkboxSelection={false} | |||
| _formKey={"pickOrderLine"} | |||
| columns={columns} | |||
| validateRow={validation} | |||
| needAdd={true} | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| </Grid> | |||
| ); | |||
| }; | |||
| export default CreateForm; | |||
| @@ -0,0 +1,98 @@ | |||
| import { createPickOrder, SavePickOrderRequest } from "@/app/api/pickOrder/actions"; | |||
| import { Box, Button, Modal, ModalProps, Stack } from "@mui/material"; | |||
| import dayjs from "dayjs"; | |||
| import arraySupport from "dayjs/plugin/arraySupport"; | |||
| import { useCallback } from "react"; | |||
| import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import CreateForm from "./CreateForm"; | |||
| import { ItemCombo } from "@/app/api/settings/item/actions"; | |||
| import { Check } from "@mui/icons-material"; | |||
| dayjs.extend(arraySupport); | |||
| const style = { | |||
| position: "absolute", | |||
| top: "50%", | |||
| left: "50%", | |||
| transform: "translate(-50%, -50%)", | |||
| overflow: "scroll", | |||
| bgcolor: "background.paper", | |||
| pt: 5, | |||
| px: 5, | |||
| pb: 10, | |||
| display: "block", | |||
| width: { xs: "60%", sm: "60%", md: "60%" }, | |||
| }; | |||
| interface Props extends Omit<ModalProps, "children"> { | |||
| items: ItemCombo[] | |||
| } | |||
| const CreatePickOrderModal: React.FC<Props> = ({ | |||
| open, | |||
| onClose, | |||
| items | |||
| }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| const formProps = useForm<SavePickOrderRequest>(); | |||
| const errors = formProps.formState.errors; | |||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
| (...args) => { | |||
| onClose?.(...args); | |||
| // reset(); | |||
| }, | |||
| [onClose] | |||
| ); | |||
| const onSubmit = useCallback<SubmitHandler<SavePickOrderRequest>>( | |||
| async (data, event) => { | |||
| console.log(data) | |||
| try { | |||
| const res = await createPickOrder(data) | |||
| if (res.id) { | |||
| closeHandler({}, "backdropClick"); | |||
| } | |||
| } catch (error) { | |||
| console.log(error) | |||
| throw error | |||
| } | |||
| // formProps.reset() | |||
| }, | |||
| [closeHandler] | |||
| ); | |||
| return ( | |||
| <> | |||
| <FormProvider {...formProps}> | |||
| <Modal open={open} onClose={closeHandler} sx={{ overflowY: "scroll" }}> | |||
| <Box | |||
| sx={style} | |||
| component="form" | |||
| onSubmit={formProps.handleSubmit(onSubmit)} | |||
| > | |||
| <CreateForm | |||
| items={items} | |||
| /> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button | |||
| name="submit" | |||
| variant="contained" | |||
| startIcon={<Check />} | |||
| type="submit" | |||
| > | |||
| {t("submit")} | |||
| </Button> | |||
| <Button | |||
| name="reset" | |||
| variant="contained" | |||
| startIcon={<Check />} | |||
| onClick={() => formProps.reset()} | |||
| > | |||
| {t("reset")} | |||
| </Button> | |||
| </Stack> | |||
| </Box> | |||
| </Modal> | |||
| </FormProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| export default CreatePickOrderModal; | |||
| @@ -0,0 +1,79 @@ | |||
| import { ItemCombo } from "@/app/api/settings/item/actions"; | |||
| import { Autocomplete, TextField } from "@mui/material"; | |||
| import { useCallback, useMemo } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| interface CommonProps { | |||
| allItems: ItemCombo[]; | |||
| error?: boolean; | |||
| } | |||
| interface SingleAutocompleteProps extends CommonProps { | |||
| value: number | string | undefined; | |||
| onItemSelect: (itemId: number, uom: string, uomId: number) => void | Promise<void>; | |||
| // multiple: false; | |||
| } | |||
| type Props = SingleAutocompleteProps; | |||
| const ItemSelect: React.FC<Props> = ({ | |||
| allItems, | |||
| value, | |||
| error, | |||
| onItemSelect | |||
| }) => { | |||
| const { t } = useTranslation("item"); | |||
| const filteredItems = useMemo(() => { | |||
| return allItems | |||
| }, [allItems]) | |||
| const options = useMemo(() => { | |||
| return [ | |||
| { | |||
| value: -1, // think think sin | |||
| label: t("None"), | |||
| uom: "", | |||
| uomId: -1, | |||
| group: "default", | |||
| }, | |||
| ...filteredItems.map((i) => ({ | |||
| value: i.id as number, | |||
| label: i.label, | |||
| uom: i.uom, | |||
| uomId: i.uomId, | |||
| group: "existing", | |||
| })), | |||
| ]; | |||
| }, [t, filteredItems]); | |||
| const currentValue = options.find((o) => o.value === value) || options[0]; | |||
| const onChange = useCallback( | |||
| ( | |||
| event: React.SyntheticEvent, | |||
| newValue: { value: number; uom: string; uomId: number; group: string } | { uom: string; uomId: number; value: number }[], | |||
| ) => { | |||
| const singleNewVal = newValue as { | |||
| value: number; | |||
| uom: string; | |||
| uomId: number; | |||
| group: string; | |||
| }; | |||
| onItemSelect(singleNewVal.value, singleNewVal.uom, singleNewVal.uomId) | |||
| } | |||
| , [onItemSelect]) | |||
| return ( | |||
| <Autocomplete | |||
| noOptionsText={t("No Item")} | |||
| disableClearable | |||
| fullWidth | |||
| value={currentValue} | |||
| onChange={onChange} | |||
| getOptionLabel={(option) => option.label} | |||
| options={options} | |||
| renderInput={(params) => <TextField {...params} error={error} />} | |||
| /> | |||
| ); | |||
| } | |||
| export default ItemSelect | |||
| @@ -1,33 +1,27 @@ | |||
| "use client"; | |||
| import { PickOrderResult } from "@/app/api/pickorder"; | |||
| import { SearchParams } from "@/app/utils/fetchUtil"; | |||
| import { useCallback, useMemo, useState } from "react"; | |||
| import { PickOrderResult } from "@/app/api/pickOrder"; | |||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import SearchBox, { Criterion } from "../SearchBox"; | |||
| import SearchResults, { Column } from "../SearchResults"; | |||
| import { | |||
| flatten, | |||
| groupBy, | |||
| intersectionWith, | |||
| isEmpty, | |||
| map, | |||
| sortBy, | |||
| sortedUniq, | |||
| uniqBy, | |||
| upperCase, | |||
| upperFirst, | |||
| } from "lodash"; | |||
| import { | |||
| arrayToDateString, | |||
| arrayToDayjs, | |||
| dateStringToDayjs, | |||
| } from "@/app/utils/formatUtil"; | |||
| import dayjs from "dayjs"; | |||
| import { Button, Grid, Stack, Tab, Tabs, TabsProps } from "@mui/material"; | |||
| import { Button, Grid, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material"; | |||
| import PickOrders from "./PickOrders"; | |||
| import ConsolidatedPickOrders from "./ConsolidatedPickOrders"; | |||
| import CreatePickOrderModal from "./CreatePickOrderModal"; | |||
| import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions"; | |||
| import { fetchPickOrderClient } from "@/app/api/pickOrder/actions"; | |||
| import { getServerI18n } from "@/i18n"; | |||
| interface Props { | |||
| pickOrders: PickOrderResult[]; | |||
| } | |||
| @@ -41,15 +35,30 @@ type SearchParamNames = keyof SearchQuery; | |||
| const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| const [isOpenCreateModal, setIsOpenCreateModal] = useState(false) | |||
| const [items, setItems] = useState<ItemCombo[]>([]) | |||
| const [filteredPickOrders, setFilteredPickOrders] = useState(pickOrders); | |||
| const [filterArgs, setFilterArgs] = useState<Record<string, any>>({}); | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| const [totalCount, setTotalCount] = useState<number>(); | |||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||
| (_e, newValue) => { | |||
| setTabIndex(newValue); | |||
| }, | |||
| [], | |||
| ); | |||
| const openCreateModal = useCallback(async () => { | |||
| console.log("testing") | |||
| const res = await fetchAllItemsInClient() | |||
| console.log(res) | |||
| setItems(res) | |||
| setIsOpenCreateModal(true) | |||
| }, []) | |||
| const closeCreateModal = useCallback(() => { | |||
| setIsOpenCreateModal(false) | |||
| }, []) | |||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
| () => [ | |||
| @@ -113,15 +122,67 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| ), | |||
| }, | |||
| ], | |||
| [t], | |||
| [pickOrders, t], | |||
| ); | |||
| const fetchNewPagePickOrder = useCallback( | |||
| async ( | |||
| pagingController: Record<string, number>, | |||
| filterArgs: Record<string, number>, | |||
| ) => { | |||
| const params = { | |||
| ...pagingController, | |||
| ...filterArgs, | |||
| }; | |||
| const res = await fetchPickOrderClient(params); | |||
| if (res) { | |||
| console.log(res); | |||
| setFilteredPickOrders(res.records); | |||
| setTotalCount(res.total); | |||
| } | |||
| }, | |||
| [], | |||
| ); | |||
| const onReset = useCallback(() => { | |||
| setFilteredPickOrders(pickOrders); | |||
| }, [pickOrders]); | |||
| useEffect(() => { | |||
| if (!isOpenCreateModal) { | |||
| setTabIndex(1) | |||
| setTimeout(async () => { | |||
| setTabIndex(0) | |||
| }, 200) | |||
| } | |||
| }, [isOpenCreateModal]) | |||
| return ( | |||
| <> | |||
| <Stack | |||
| rowGap={2} | |||
| > | |||
| <Grid container> | |||
| <Grid item xs={8}> | |||
| <Typography variant="h4" marginInlineEnd={2}> | |||
| {t("Pick Order")} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={4} display="flex" justifyContent="end" alignItems="end"> | |||
| <Button | |||
| onClick={openCreateModal} | |||
| > | |||
| {t("create")} | |||
| </Button> | |||
| {isOpenCreateModal && | |||
| <CreatePickOrderModal | |||
| open={isOpenCreateModal} | |||
| onClose={closeCreateModal} | |||
| items={items} | |||
| />} | |||
| </Grid> | |||
| </Grid> | |||
| </Stack> | |||
| <SearchBox | |||
| criteria={searchCriteria} | |||
| onSearch={(query) => { | |||
| @@ -1,4 +1,4 @@ | |||
| import { fetchPickOrders } from "@/app/api/pickorder"; | |||
| import { fetchPickOrders } from "@/app/api/pickOrder"; | |||
| import GeneralLoading from "../General/GeneralLoading"; | |||
| import PickOrderSearch from "./PickOrderSearch"; | |||
| @@ -1,16 +1,18 @@ | |||
| import { Button, CircularProgress, Grid } from "@mui/material"; | |||
| import SearchResults, { Column } from "../SearchResults/SearchResults"; | |||
| import { PickOrderResult } from "@/app/api/pickorder"; | |||
| import { PickOrderResult } from "@/app/api/pickOrder"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { isEmpty, upperCase, upperFirst } from "lodash"; | |||
| import { arrayToDateString } from "@/app/utils/formatUtil"; | |||
| import { arrayToDateString, OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| import { | |||
| consolidatePickOrder, | |||
| fetchPickOrderClient, | |||
| } from "@/app/api/pickorder/actions"; | |||
| } from "@/app/api/pickOrder/actions"; | |||
| import useUploadContext from "../UploadProvider/useUploadContext"; | |||
| import dayjs from "dayjs"; | |||
| import arraySupport from "dayjs/plugin/arraySupport"; | |||
| dayjs.extend(arraySupport); | |||
| interface Props { | |||
| filteredPickOrders: PickOrderResult[]; | |||
| filterArgs: Record<string, any>; | |||
| @@ -30,21 +32,6 @@ const PickOrders: React.FC<Props> = ({ filteredPickOrders, filterArgs }) => { | |||
| }); | |||
| const [totalCount, setTotalCount] = useState<number>(); | |||
| const handleConsolidatedRows = useCallback(async () => { | |||
| console.log(selectedRows); | |||
| setIsUploading(true); | |||
| try { | |||
| const res = await consolidatePickOrder(selectedRows as number[]); | |||
| if (res) { | |||
| console.log(res); | |||
| } | |||
| } catch { | |||
| setIsUploading(false); | |||
| } | |||
| fetchNewPagePickOrder(pagingController, filterArgs); | |||
| setIsUploading(false); | |||
| }, [selectedRows, pagingController]); | |||
| const fetchNewPagePickOrder = useCallback( | |||
| async ( | |||
| pagingController: Record<string, number>, | |||
| @@ -66,6 +53,22 @@ const PickOrders: React.FC<Props> = ({ filteredPickOrders, filterArgs }) => { | |||
| [], | |||
| ); | |||
| const handleConsolidatedRows = useCallback(async () => { | |||
| console.log(selectedRows); | |||
| setIsUploading(true); | |||
| try { | |||
| const res = await consolidatePickOrder(selectedRows as number[]); | |||
| if (res) { | |||
| console.log(res); | |||
| } | |||
| } catch { | |||
| setIsUploading(false); | |||
| } | |||
| fetchNewPagePickOrder(pagingController, filterArgs); | |||
| setIsUploading(false); | |||
| }, [selectedRows, setIsUploading, fetchNewPagePickOrder, pagingController, filterArgs]); | |||
| useEffect(() => { | |||
| fetchNewPagePickOrder(pagingController, filterArgs); | |||
| }, [fetchNewPagePickOrder, pagingController, filterArgs]); | |||
| @@ -109,7 +112,11 @@ const PickOrders: React.FC<Props> = ({ filteredPickOrders, filterArgs }) => { | |||
| name: "targetDate", | |||
| label: t("Target Date"), | |||
| renderCell: (params) => { | |||
| return arrayToDateString(params.targetDate); | |||
| return ( | |||
| dayjs(params.targetDate) | |||
| .add(-1, "month") | |||
| .format(OUTPUT_DATE_FORMAT) | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| @@ -0,0 +1,73 @@ | |||
| import { ItemCombo } from "@/app/api/settings/item/actions"; | |||
| import { Autocomplete, TextField } from "@mui/material"; | |||
| import { useCallback, useMemo } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| interface CommonProps { | |||
| allUom: ItemCombo[]; | |||
| error?: boolean; | |||
| } | |||
| interface SingleAutocompleteProps extends CommonProps { | |||
| value: number | string | undefined; | |||
| onUomSelect: (itemId: number) => void | Promise<void>; | |||
| // multiple: false; | |||
| } | |||
| type Props = SingleAutocompleteProps; | |||
| const UomSelect: React.FC<Props> = ({ | |||
| allUom, | |||
| value, | |||
| error, | |||
| onUomSelect | |||
| }) => { | |||
| const { t } = useTranslation("item"); | |||
| const filteredUom = useMemo(() => { | |||
| return allUom | |||
| }, [allUom]) | |||
| const options = useMemo(() => { | |||
| return [ | |||
| { | |||
| value: -1, // think think sin | |||
| label: t("None"), | |||
| group: "default", | |||
| }, | |||
| ...filteredUom.map((i) => ({ | |||
| value: i.id as number, | |||
| label: i.label, | |||
| group: "existing", | |||
| })), | |||
| ]; | |||
| }, [t, filteredUom]); | |||
| const currentValue = options.find((o) => o.value === value) || options[0]; | |||
| const onChange = useCallback( | |||
| ( | |||
| event: React.SyntheticEvent, | |||
| newValue: { value: number; group: string } | { value: number }[], | |||
| ) => { | |||
| const singleNewVal = newValue as { | |||
| value: number; | |||
| group: string; | |||
| }; | |||
| onUomSelect(singleNewVal.value) | |||
| } | |||
| , [onUomSelect]) | |||
| return ( | |||
| <Autocomplete | |||
| noOptionsText={t("No Uom")} | |||
| disableClearable | |||
| fullWidth | |||
| value={currentValue} | |||
| onChange={onChange} | |||
| getOptionLabel={(option) => option.label} | |||
| options={options} | |||
| renderInput={(params) => <TextField {...params} error={error} />} | |||
| /> | |||
| ); | |||
| } | |||
| export default UomSelect | |||
| @@ -203,7 +203,7 @@ const PoQcStockInModal: React.FC<Props> = ({ | |||
| [itemDetail, formProps], | |||
| ); | |||
| const onSubmit = useCallback<SubmitHandler<ModalFormInput & {}>>( | |||
| const onSubmit = useCallback<SubmitHandler<ModalFormInput>>( | |||
| async (data, event) => { | |||
| setBtnIsLoading(true); | |||
| setIsUploading(true); | |||
| @@ -47,7 +47,7 @@ import ReactQrCodeScanner, { | |||
| ScannerConfig, | |||
| } from "../ReactQrCodeScanner/ReactQrCodeScanner"; | |||
| import { QrCodeInfo } from "@/app/api/qrcode"; | |||
| import { useQcCodeScanner } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | |||
| import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | |||
| import dayjs from "dayjs"; | |||
| import arraySupport from "dayjs/plugin/arraySupport"; | |||
| dayjs.extend(arraySupport); | |||
| @@ -219,7 +219,7 @@ const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse, disabled }) => { | |||
| ); | |||
| // QR Code Scanner | |||
| const scanner = useQcCodeScanner(); | |||
| const scanner = useQrCodeScannerContext(); | |||
| useEffect(() => { | |||
| if (isOpenScanner) { | |||
| scanner.startScan(); | |||
| @@ -27,7 +27,7 @@ import { QrCodeInfo } from "@/app/api/qrcode"; | |||
| import { Check } from "@mui/icons-material"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { useSearchParams } from "next/navigation"; | |||
| import { useQcCodeScanner } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | |||
| import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | |||
| interface Props extends Omit<ModalProps, "children"> { | |||
| warehouse: WarehouseResult[]; | |||
| @@ -83,7 +83,7 @@ const QrModal: React.FC<Props> = ({ open, onClose, warehouse }) => { | |||
| ); | |||
| // QR Code Scanner | |||
| const scanner = useQcCodeScanner(); | |||
| const scanner = useQrCodeScannerContext(); | |||
| useEffect(() => { | |||
| if (open && !scanner.isScanning) { | |||
| scanner.startScan(); | |||
| @@ -136,7 +136,7 @@ const QrModal: React.FC<Props> = ({ open, onClose, warehouse }) => { | |||
| if (stockInLineId) fetchStockInLine(stockInLineId); | |||
| }, [stockInLineId]); | |||
| const onSubmit = useCallback<SubmitHandler<ModalFormInput & {}>>( | |||
| const onSubmit = useCallback<SubmitHandler<ModalFormInput>>( | |||
| async (data, event) => { | |||
| const hasErrors = false; | |||
| console.log("errors"); | |||
| @@ -15,7 +15,7 @@ import { useSession } from "next-auth/react"; | |||
| import { defaultPagingController } from "../SearchResults/SearchResults"; | |||
| import { fetchPoListClient, testing } from "@/app/api/po/actions"; | |||
| import dayjs from "dayjs"; | |||
| import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| import { arrayToDateString, OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| import arraySupport from "dayjs/plugin/arraySupport"; | |||
| dayjs.extend(arraySupport); | |||
| @@ -72,7 +72,7 @@ const PoSearch: React.FC<Props> = ({ | |||
| [router], | |||
| ); | |||
| const onDeleteClick = useCallback((po: PoResult) => {}, [router]); | |||
| const onDeleteClick = useCallback((po: PoResult) => {}, []); | |||
| const columns = useMemo<Column<PoResult>[]>( | |||
| () => [ | |||
| @@ -92,8 +92,8 @@ const PoSearch: React.FC<Props> = ({ | |||
| renderCell: (params) => { | |||
| return ( | |||
| dayjs(params.orderDate) | |||
| // .add(-1, "month") | |||
| .format(OUTPUT_DATE_FORMAT) | |||
| .add(-1, "month") | |||
| .format(OUTPUT_DATE_FORMAT) | |||
| ); | |||
| }, | |||
| }, | |||
| @@ -37,7 +37,7 @@ const DefectsSection: React.FC<DefectsSectionProps> = ({ | |||
| if (defectToAdd) { | |||
| // Check for duplicate code (skip if code is empty) | |||
| const isDuplicate = | |||
| defectToAdd.code && defects.some((d) => d.code === defectToAdd.code); | |||
| defectToAdd.code && defects.some((d) => d.code === defectToAdd!.code); | |||
| if (!isDuplicate) { | |||
| const updatedDefects = [...defects, defectToAdd]; | |||
| onDefectsChange(updatedDefects); | |||
| @@ -8,7 +8,7 @@ import { | |||
| useState, | |||
| } from "react"; | |||
| interface QcCodeScanner { | |||
| interface QrCodeScanner { | |||
| values: string[]; | |||
| isScanning: boolean; | |||
| startScan: () => void; | |||
| @@ -20,14 +20,14 @@ interface QrCodeScannerProviderProps { | |||
| children: ReactNode; | |||
| } | |||
| export const QcCodeScannerContext = createContext<QcCodeScanner | undefined>( | |||
| export const QrCodeScannerContext = createContext<QrCodeScanner | undefined>( | |||
| undefined, | |||
| ); | |||
| const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||
| children, | |||
| }) => { | |||
| const [qcCodeScannerValues, setQrCodeScannerValues] = useState<string[]>([]); | |||
| const [qrCodeScannerValues, setQrCodeScannerValues] = useState<string[]>([]); | |||
| const [isScanning, setIsScanning] = useState<boolean>(false); | |||
| const [keys, setKeys] = useState<string[]>([]); | |||
| const [leftCurlyBraceCount, setLeftCurlyBraceCount] = useState<number>(0); | |||
| @@ -82,7 +82,7 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||
| keys.join("").substring(startBrace, endBrace + 1), | |||
| ]); | |||
| console.log(keys); | |||
| console.log(qcCodeScannerValues); | |||
| console.log(qrCodeScannerValues); | |||
| // reset | |||
| setKeys(() => []); | |||
| @@ -92,9 +92,9 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||
| }, [keys, leftCurlyBraceCount, rightCurlyBraceCount]); | |||
| return ( | |||
| <QcCodeScannerContext.Provider | |||
| <QrCodeScannerContext.Provider | |||
| value={{ | |||
| values: qcCodeScannerValues, | |||
| values: qrCodeScannerValues, | |||
| isScanning: isScanning, | |||
| startScan: startQrCodeScanner, | |||
| stopScan: endQrCodeScanner, | |||
| @@ -102,15 +102,15 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||
| }} | |||
| > | |||
| {children} | |||
| </QcCodeScannerContext.Provider> | |||
| </QrCodeScannerContext.Provider> | |||
| ); | |||
| }; | |||
| export const useQcCodeScanner = (): QcCodeScanner => { | |||
| const context = useContext(QcCodeScannerContext); | |||
| export const useQrCodeScannerContext = (): QrCodeScanner => { | |||
| const context = useContext(QrCodeScannerContext); | |||
| if (!context) { | |||
| throw new Error( | |||
| "useQcCodeScanner must be used within a QcCodeScannerProvider", | |||
| "useQrCodeScanner must be used within a QrCodeScannerProvider", | |||
| ); | |||
| } | |||
| return context; | |||
| @@ -245,7 +245,7 @@ const RSOverview: React.FC<Props> = ({ type, defaultInputs }) => { | |||
| // setFilteredSchedules(items ?? []); | |||
| // setFilterObj({}); | |||
| // setTempSelectedValue({}); | |||
| refetchData(inputs, "reset"); | |||
| refetchData(defaultInputs, "reset"); | |||
| }, []); | |||
| return ( | |||
| @@ -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; | |||
| @@ -3,7 +3,6 @@ | |||
| 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"; | |||
| @@ -13,7 +12,7 @@ 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 EditableSearchResults, { Column } from "@/components/SearchResults/EditableSearchResults"; // Make sure to import Qs | |||
| import { ItemsResultResponse } from "@/app/api/settings/item"; | |||
| type Props = { | |||
| items: ItemsResult[]; | |||
| @@ -64,35 +63,62 @@ const RSSOverview: React.FC<Props> = ({ items }) => { | |||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => { | |||
| const searchCriteria: Criterion<SearchParamNames>[] = [ | |||
| { label: t("Finished Goods Name"), paramName: "fgName", type: "text" }, | |||
| { label: t("Finished Goods Name"), paramName: "name", type: "text" }, | |||
| { | |||
| label: t("Exclude Date"), | |||
| paramName: "excludeDate", | |||
| type: "multi-select", | |||
| options: dayOptions, | |||
| selectedValues: filterObj, | |||
| paramName: "shelfLife", | |||
| type: "select", | |||
| options: ["qcChecks"], | |||
| // selectedValues: filterObj, | |||
| handleSelectionChange: handleSelectionChange, | |||
| }, | |||
| ]; | |||
| return searchCriteria; | |||
| }, [t, items]); | |||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
| () => { | |||
| var searchCriteria: Criterion<SearchParamNames>[] = [ | |||
| { label: t("Finished Goods Name"), paramName: "name", type: "text" }, | |||
| { | |||
| label: t("Exclude Date"), | |||
| paramName: "excludeDate", | |||
| type: "multi-select", | |||
| options: dayOptions, | |||
| selectedValues: filterObj, | |||
| handleSelectionChange: handleSelectionChange, | |||
| }, | |||
| ] | |||
| return searchCriteria | |||
| }, | |||
| }, | |||
| // const onDetailClick = useCallback( | |||
| // (item: ItemsResult) => { | |||
| // router.push(`/settings/items/edit?id=${item.id}`); | |||
| // }, | |||
| // [router] | |||
| // ); | |||
| const handleEditClick = useCallback((item: ItemsResult) => {}, [router]); | |||
| const onDeleteClick = useCallback((item: ItemsResult) => {}, [router]); | |||
| const columns = useMemo<Column<ItemsResult>[]>( | |||
| () => [ | |||
| // { | |||
| // name: "id", | |||
| // label: t("Details"), | |||
| // onClick: ()=>{}, | |||
| // buttonIcon: <EditNote />, | |||
| // }, | |||
| // { | |||
| // name: "name", | |||
| // label: "Finished Goods Name", | |||
| // // type: "input", | |||
| // }, | |||
| // { | |||
| // name: "excludeDate", | |||
| // label: t("Exclude Date"), | |||
| // type: "multi-select", | |||
| // options: dayOptions, | |||
| // renderCell: (item: ItemsResult) => { | |||
| // console.log("[debug] render item", item); | |||
| // const selectedValues = (item.excludeDate as number[]) ?? []; // Assuming excludeDate is an array of numbers | |||
| // const selectedLabels = dayOptions | |||
| // .filter((option) => selectedValues.includes(option.value)) | |||
| // .map((option) => option.label) | |||
| // .join(", "); // Join labels into a string | |||
| // return ( | |||
| // <span onDoubleClick={() => handleEditClick(item.id as number)}> | |||
| // {selectedLabels || "None"}{" "} | |||
| // {/* Display "None" if no days are selected */} | |||
| // </span> | |||
| // ); | |||
| // }, | |||
| // { | |||
| // name: "action", | |||
| // label: t(""), | |||
| @@ -113,100 +139,41 @@ const RSSOverview: React.FC<Props> = ({ items }) => { | |||
| return; // Exit the function if the token is not set | |||
| } | |||
| const columns = useMemo<Column<ItemsResult>[]>( | |||
| () => [ | |||
| // { | |||
| // name: "id", | |||
| // label: t("Details"), | |||
| // onClick: ()=>{}, | |||
| // buttonIcon: <EditNote />, | |||
| // }, | |||
| { | |||
| field: "name", | |||
| label: t("Finished Goods Name"), | |||
| type: 'input', | |||
| }, | |||
| { | |||
| field: "excludeDate", | |||
| label: t("Exclude Date"), | |||
| type: 'multi-select', | |||
| options: dayOptions, | |||
| renderCell: (item: ItemsResult) => { | |||
| console.log("[debug] render item", item) | |||
| const selectedValues = item.excludeDate as number[] ?? []; // Assuming excludeDate is an array of numbers | |||
| const selectedLabels = dayOptions | |||
| .filter(option => selectedValues.includes(option.value)) | |||
| .map(option => option.label) | |||
| .join(", "); // Join labels into a string | |||
| return ( | |||
| <span onDoubleClick={() => handleEditClick(item.id as number)}> | |||
| {selectedLabels || "None"} {/* Display "None" if no days are selected */} | |||
| </span> | |||
| ); | |||
| }, | |||
| }, | |||
| // { | |||
| // name: "action", | |||
| // label: t(""), | |||
| // buttonIcon: <GridDeleteIcon />, | |||
| // onClick: onDeleteClick, | |||
| // }, | |||
| ], | |||
| [filteredItems] | |||
| ); | |||
| useEffect(() => { | |||
| refetchData(filterObj); | |||
| }, [filterObj, pagingController.pageNum, pagingController.pageSize]); | |||
| const refetchData = async (filterObj: SearchQuery) => { | |||
| const authHeader = axiosInstance.defaults.headers['Authorization']; | |||
| if (!authHeader) { | |||
| return; // Exit the function if the token is not set | |||
| } | |||
| const params ={ | |||
| pageNum: pagingController.pageNum, | |||
| pageSize: pagingController.pageSize, | |||
| ...filterObj, | |||
| ...tempSelectedValue, | |||
| } | |||
| try { | |||
| const response = await axiosInstance.get<ItemsResultResponse>(`${NEXT_PUBLIC_API_URL}/items/getRecordByPage`, { | |||
| params, | |||
| paramsSerializer: (params) => { | |||
| return Qs.stringify(params, { arrayFormat: 'repeat' }); | |||
| }, | |||
| }); | |||
| setFilteredItems(response.data.records); | |||
| setPagingController({ | |||
| ...pagingController, | |||
| totalCount: response.data.total | |||
| }) | |||
| return response; // Return the data from the response | |||
| } catch (error) { | |||
| console.error('Error fetching items:', error); | |||
| throw error; // Rethrow the error for further handling | |||
| } | |||
| const params = { | |||
| pageNum: pagingController.pageNum, | |||
| pageSize: pagingController.pageSize, | |||
| ...filterObj, | |||
| ...tempSelectedValue, | |||
| }; | |||
| const onReset = useCallback(() => { | |||
| //setFilteredItems(items ?? []); | |||
| setFilterObj({}); | |||
| setTempSelectedValue({}); | |||
| refetchData(filterObj); | |||
| }, [items]); | |||
| try { | |||
| const response = await axiosInstance.get<ItemsResultResponse>( | |||
| `${NEXT_PUBLIC_API_URL}/items/getRecordByPage`, | |||
| { | |||
| params, | |||
| paramsSerializer: (params) => { | |||
| return Qs.stringify(params, { arrayFormat: "repeat" }); | |||
| }, | |||
| }, | |||
| ); | |||
| setFilteredItems(response.data.records); | |||
| setPagingController({ | |||
| ...pagingController, | |||
| totalCount: response.data.total, | |||
| }); | |||
| return response; // Return the data from the response | |||
| } catch (error) { | |||
| console.error("Error fetching items:", error); | |||
| throw error; // Rethrow the error for further handling | |||
| } | |||
| }; | |||
| const onReset = useCallback(() => { | |||
| //setFilteredItems(items ?? []); | |||
| setFilterObj({}); | |||
| setTempSelectedValue({}); | |||
| refetchData(); | |||
| }, [items]); | |||
| refetchData(filterObj); | |||
| }, [items, filterObj]); | |||
| return ( | |||
| <> | |||
| @@ -220,6 +187,9 @@ const RSSOverview: React.FC<Props> = ({ items }) => { | |||
| onReset={onReset} | |||
| /> | |||
| <EditableSearchResults<ItemsResult> | |||
| index={1} // bug | |||
| isEdit | |||
| isEditable | |||
| items={filteredItems} | |||
| columns={columns} | |||
| setPagingController={setPagingController} | |||
| @@ -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; | |||
| } | |||
| @@ -113,7 +114,7 @@ function BomMaterialTable({ bomMaterial }: Props) { | |||
| headerName: t("Available Qty"), | |||
| flex: 0.5, | |||
| type: "number", | |||
| editable: true, | |||
| // editable: true, | |||
| align: "right", | |||
| headerAlign: "right", | |||
| renderCell: (row) => { | |||
| @@ -125,7 +126,7 @@ function BomMaterialTable({ bomMaterial }: Props) { | |||
| field: "demandQty", | |||
| headerName: t("Demand Qty"), | |||
| flex: 0.5, | |||
| editable: true, | |||
| // editable: true, | |||
| align: "right", | |||
| headerAlign: "right", | |||
| renderCell: (row) => { | |||
| @@ -136,7 +137,7 @@ function BomMaterialTable({ bomMaterial }: Props) { | |||
| field: "status", | |||
| headerName: t("status"), | |||
| flex: 0.5, | |||
| editable: true, | |||
| // editable: true, | |||
| align: "center", | |||
| headerAlign: "center", | |||
| renderCell: (params) => { | |||
| @@ -219,7 +220,7 @@ function BomMaterialTable({ bomMaterial }: Props) { | |||
| }, | |||
| }} | |||
| disableColumnMenu | |||
| editMode="row" | |||
| // editMode="row" | |||
| rows={entries} | |||
| rowModesModel={rowModesModel} | |||
| onRowModesModelChange={setRowModesModel} | |||
| @@ -1,11 +1,12 @@ | |||
| "use client"; | |||
| import React, { | |||
| CSSProperties, | |||
| DetailedHTMLProps, | |||
| HTMLAttributes, | |||
| useEffect, | |||
| useState, | |||
| CSSProperties, | |||
| DetailedHTMLProps, | |||
| HTMLAttributes, | |||
| useEffect, | |||
| useRef, | |||
| useState, | |||
| } from "react"; | |||
| import Paper from "@mui/material/Paper"; | |||
| import Table from "@mui/material/Table"; | |||
| @@ -30,363 +31,437 @@ 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"; | |||
| import { useFormContext } from "react-hook-form"; | |||
| 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; | |||
| 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>({ | |||
| 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, | |||
| 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"); | |||
| 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"); | |||
| console.log(items) | |||
| const handleChangeRowsPerPage = ( | |||
| event: React.ChangeEvent<HTMLInputElement>, | |||
| ) => { | |||
| setRowsPerPage(+event.target.value); | |||
| setPage(0); | |||
| if (setPagingController) { | |||
| setPagingController({ | |||
| ...pagingController, | |||
| pageSize: +event.target.value, | |||
| pageNum: 1, | |||
| index: index, | |||
| }); | |||
| } | |||
| }; | |||
| useEffect(() => { | |||
| setEditedItems(items); | |||
| }, [items]); | |||
| const handleEditClick = (id: number) => { | |||
| setEditingRowId(id); | |||
| }; | |||
| const handleChangePage = (_event: unknown, newPage: number) => { | |||
| setPage(newPage); | |||
| if (setPagingController && pagingController) { | |||
| setPagingController({ | |||
| ...pagingController, | |||
| pageNum: newPage + 1, | |||
| index: index ?? -1, | |||
| }); | |||
| } | |||
| }; | |||
| 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 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 handleInputChange = ( | |||
| id: number, | |||
| field: keyof T, | |||
| value: string | number[], | |||
| ) => { | |||
| setEditedItems((prev) => | |||
| prev.map((item) => (item.id === id ? { ...item, [field]: value } : item)), | |||
| ); | |||
| }; | |||
| const handleEditClick = (id: number) => { | |||
| setEditingRowId(id); | |||
| if (onEditClick) { | |||
| onEditClick(id) | |||
| } | |||
| }; | |||
| const handleSaveClick = (item: T) => { | |||
| setEditingRowId(null); | |||
| // Call API or any save logic here | |||
| 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, | |||
| ) => { | |||
| if (handleEditChange) { | |||
| handleEditChange(id, field, value) | |||
| } else { | |||
| 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 | |||
| }); | |||
| const handleCancelClick = (id: number) => { | |||
| if (onCancelClick) { | |||
| onCancelClick(id) | |||
| } | |||
| setEditingRowId(null); | |||
| setEditingRowId(null) | |||
| } | |||
| }, [isEdit]); | |||
| function isRoughType(type: ScheduleType): type is "rough" { | |||
| return type === "rough"; | |||
| } | |||
| 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]); | |||
| function isDetailedType(type: ScheduleType): type is "detailed" { | |||
| return type === "detailed"; | |||
| } | |||
| function isRoughType(type: ScheduleType): type is "rough" { | |||
| return type === "rough"; | |||
| } | |||
| 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 | |||
| 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> | |||
| )} | |||
| {(isEditable || hasCollapse) && ( | |||
| <TableCell> | |||
| {editingRowId === row.id ? ( | |||
| <> | |||
| {isDetailedType(type) && isEditable && ( | |||
| <IconButton | |||
| disabled={!isEdit} | |||
| onClick={() => handleSaveClick(row)} | |||
| > | |||
| <SaveIcon /> | |||
| </IconButton> | |||
| )} | |||
| {isDetailedType(type) && isEditable && ( | |||
| <IconButton | |||
| disabled={!isEdit} | |||
| onClick={() => handleCancelClick(row.id as number)} | |||
| > | |||
| <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 || (row as unknown as DetailedProdScheduleLineResult).approved} | |||
| 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 "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 ( | |||
| // <MultiSelect | |||
| // //label={column.label} | |||
| // options={column.options ?? []} | |||
| // selectedValues={[]} | |||
| // onChange={(selectedValues) => handleInputChange(row.id as number, columnName, selectedValues)} | |||
| // /> | |||
| // ); | |||
| case "read-only": | |||
| 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 | |||
| } | |||
| })() | |||
| ) : 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; | |||
| @@ -18,6 +18,7 @@ interface MultiSelectProps { | |||
| options: Option[]; | |||
| selectedValues: number[]; | |||
| onChange: (values: number[]) => void; | |||
| isReset?: boolean; | |||
| } | |||
| const MultiSelect: React.FC<MultiSelectProps> = ({ | |||
| @@ -49,13 +50,13 @@ const MultiSelect: React.FC<MultiSelectProps> = ({ | |||
| <Select | |||
| multiple | |||
| value={displayValues} | |||
| onChange={handleChange} | |||
| onChange={handleChange as any} | |||
| renderValue={(selected) => ( | |||
| <Box sx={{ display: "flex", flexWrap: "wrap" }}> | |||
| {(selected as number[]).map((value) => ( | |||
| <Chip | |||
| key={value} | |||
| label={options.find((item) => item.value == value).label} | |||
| label={options.find((item) => item.value == value)?.label ?? ""} | |||
| sx={{ margin: 0.5 }} | |||
| /> | |||
| ))} | |||
| @@ -227,7 +227,8 @@ function SearchBox<T extends string>({ | |||
| value={inputs[c.paramName]} | |||
| /> | |||
| )} | |||
| {c.type === "multi-select" && ( | |||
| {/* eslint-disable-next-line @typescript-eslint/no-unused-vars */} | |||
| {/* {c.type === "multi-select" && ( | |||
| <MultiSelect | |||
| label={t(c.label)} | |||
| options={c?.options} | |||
| @@ -235,7 +236,7 @@ function SearchBox<T extends string>({ | |||
| onChange={c.handleSelectionChange} | |||
| isReset={isReset} | |||
| /> | |||
| )} | |||
| )} */} | |||
| {c.type === "select" && ( | |||
| <FormControl fullWidth> | |||
| <InputLabel>{t(c.label)}</InputLabel> | |||
| @@ -88,7 +88,7 @@ const UserSearch: React.FC<Props> = ({ users }) => { | |||
| <SearchResults<UserResult> | |||
| items={filteredUser} | |||
| columns={columns} | |||
| pagingController={{ pageNum: 1, pageSize: 10, totalCount: 100 }} | |||
| pagingController={{ pageNum: 1, pageSize: 10 }} | |||
| /> | |||
| </> | |||
| ); | |||
| @@ -81,5 +81,7 @@ | |||
| "Job Qty": "工單數量", | |||
| "mat": "物料", | |||
| "Product Count(s)": "產品數量", | |||
| "Schedule Period To": "排程期間至" | |||
| "Schedule Period To": "排程期間至", | |||
| "Overall": "總計", | |||
| "Back": "返回" | |||
| } | |||