# 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_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 = { | 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); | 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 ( | 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"]}> | <I18nProvider namespaces={["pickOrder", "common"]}> | ||||
<Suspense fallback={<PickOrderSearch.Loading />}> | <Suspense fallback={<PickOrderSearch.Loading />}> | ||||
<PickOrderSearch /> | <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 { TypeEnum } from "@/app/utils/typeEnum"; | ||||
// import DetailSchedule from "@/components/DetailSchedule"; | |||||
// import DetailedSchedule from "@/components/DetailedSchedule"; | |||||
// import { getServerI18n } from "@/i18n"; | // import { getServerI18n } from "@/i18n"; | ||||
import DetailSchedule from "../../../../components/DetailSchedule"; | |||||
import DetailedSchedule from "../../../../components/DetailedSchedule"; | |||||
import { getServerI18n } from "../../../../i18n"; | import { getServerI18n } from "../../../../i18n"; | ||||
import { I18nProvider } from "@/i18n"; | import { I18nProvider } from "@/i18n"; | ||||
import Stack from "@mui/material/Stack"; | import Stack from "@mui/material/Stack"; | ||||
@@ -32,8 +32,8 @@ const DetailScheduling: React.FC = async () => { | |||||
</Typography> | </Typography> | ||||
</Stack> | </Stack> | ||||
<I18nProvider namespaces={["schedule", "common"]}> | <I18nProvider namespaces={["schedule", "common"]}> | ||||
<Suspense fallback={<DetailSchedule.Loading />}> | |||||
<DetailSchedule type={type} /> | |||||
<Suspense fallback={<DetailedSchedule.Loading />}> | |||||
<DetailedSchedule type={type} /> | |||||
</Suspense> | </Suspense> | ||||
</I18nProvider> | </I18nProvider> | ||||
</> | </> |
@@ -12,7 +12,7 @@ import RoughScheduleDetailView from "@/components/RoughScheduleDetail"; | |||||
import { SearchParams, ServerFetchError } from "@/app/utils/fetchUtil"; | import { SearchParams, ServerFetchError } from "@/app/utils/fetchUtil"; | ||||
import { isArray, parseInt } from "lodash"; | import { isArray, parseInt } from "lodash"; | ||||
import { notFound } from "next/navigation"; | import { notFound } from "next/navigation"; | ||||
import { fetchProdScheduleDetail } from "@/app/api/scheduling"; | |||||
import { fetchRoughProdScheduleDetail } from "@/app/api/scheduling"; | |||||
export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
title: "Demand Forecast Detail", | title: "Demand Forecast Detail", | ||||
@@ -30,7 +30,7 @@ const roughSchedulingDetail: React.FC<Props> = async ({ searchParams }) => { | |||||
} | } | ||||
try { | try { | ||||
await fetchProdScheduleDetail(parseInt(id)); | |||||
await fetchRoughProdScheduleDetail(parseInt(id)); | |||||
} catch (e) { | } catch (e) { | ||||
if ( | if ( | ||||
e instanceof ServerFetchError && | e instanceof ServerFetchError && | ||||
@@ -1,8 +1,39 @@ | |||||
"use server"; | "use server"; | ||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { cache } from 'react'; | |||||
import { Pageable, serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { Machine, Operator } from "."; | import { Machine, Operator } from "."; | ||||
import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
import { revalidateTag } from "next/cache"; | 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> { | export interface IsOperatorExistResponse<T> { | ||||
id: number | null; | id: number | null; | ||||
@@ -49,3 +80,29 @@ export const isCorrectMachineUsed = async (machineCode: string) => { | |||||
revalidateTag("po"); | revalidateTag("po"); | ||||
return isExist; | 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"; | "server=only"; | ||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { BASE_API_URL } from "@/config/api"; | |||||
import { cache } from "react"; | |||||
export interface Operator { | export interface Operator { | ||||
id: number; | id: number; | ||||
name: string; | name: string; | ||||
@@ -12,3 +16,34 @@ export interface Machine { | |||||
code: string; | code: string; | ||||
qrCode: 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 "."; | } from "."; | ||||
import { PurchaseQcResult } from "../po/actions"; | import { PurchaseQcResult } from "../po/actions"; | ||||
// import { BASE_API_URL } from "@/config/api"; | // 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> { | export interface PostStockOutLiineResponse<T> { | ||||
id: number | null; | id: number | null; | ||||
name: string; | name: string; | ||||
@@ -22,7 +43,7 @@ export interface PostStockOutLiineResponse<T> { | |||||
type?: string; | type?: string; | ||||
message: string | null; | message: string | null; | ||||
errorPosition: string | keyof T; | errorPosition: string | keyof T; | ||||
entity: T | T[]; | |||||
entity: T | T[] | null; | |||||
} | } | ||||
export interface ReleasePickOrderInputs { | export interface ReleasePickOrderInputs { | ||||
@@ -60,6 +81,19 @@ export interface PickOrderApprovalInput { | |||||
rejectQty: number; | rejectQty: number; | ||||
status: string; | 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[]) => { | export const consolidatePickOrder = async (ids: number[]) => { | ||||
const pickOrder = await serverFetchJson<any>( | const pickOrder = await serverFetchJson<any>( | ||||
@@ -181,7 +215,7 @@ export const fetchConsoDetail = cache(async (consoCode: string) => { | |||||
export const releasePickOrder = async (data: ReleasePickOrderInputs) => { | export const releasePickOrder = async (data: ReleasePickOrderInputs) => { | ||||
console.log(data); | console.log(data); | ||||
console.log(JSON.stringify(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`, | `${BASE_API_URL}/pickOrder/releaseConso`, | ||||
{ | { | ||||
method: "POST", | method: "POST", | ||||
@@ -232,3 +266,13 @@ export const completeConsoPickOrder = async (consoCode: string) => { | |||||
revalidateTag("pickorder"); | revalidateTag("pickorder"); | ||||
return po; | 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; | id: number; | ||||
code: string; | code: string; | ||||
consoCode?: string; | consoCode?: string; | ||||
targetDate: number[]; | |||||
targetDate: string; | |||||
completeDate?: number[]; | completeDate?: number[]; | ||||
type: string; | type: string; | ||||
status: string; | status: string; | ||||
@@ -80,6 +80,7 @@ export interface PreReleasePickOrderSummary { | |||||
} | } | ||||
export interface PickOrderLineWithSuggestedLot { | export interface PickOrderLineWithSuggestedLot { | ||||
poStatus: string; | |||||
id: number; | id: number; | ||||
itemName: string; | itemName: string; | ||||
qty: number; | qty: number; | ||||
@@ -4,7 +4,8 @@ import { convertObjToURLSearchParams } from "@/app/utils/commonUtil"; | |||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | import { serverFetchJson } from "@/app/utils/fetchUtil"; | ||||
import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
import { cache } from "react"; | import { cache } from "react"; | ||||
import { ScheduleType } from "."; | |||||
import { DetailedProdScheduleLineBomMaterialResult, DetailedProdScheduleLineResult, ScheduleType } from "."; | |||||
import { revalidateTag } from "next/cache"; | |||||
export interface SearchProdSchedule { | export interface SearchProdSchedule { | ||||
scheduleAt?: string; | scheduleAt?: string; | ||||
@@ -31,6 +32,29 @@ export interface ProdScheduleResultByPage { | |||||
records: ProdScheduleResult[]; | 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( | export const fetchProdSchedules = cache( | ||||
async (data: SearchProdSchedule | null) => { | async (data: SearchProdSchedule | null) => { | ||||
const params = convertObjToURLSearchParams<SearchProdSchedule>(data); | 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( | return serverFetchJson( | ||||
`${BASE_API_URL}/productionSchedule/testDetailSchedule`, | |||||
`${BASE_API_URL}/productionSchedule/testDetailedSchedule`, | |||||
{ | { | ||||
method: "GET", | method: "GET", | ||||
headers: { "Content-Type": "application/json" }, | 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"; | export type ScheduleType = "all" | "rough" | "detailed" | "manual"; | ||||
// Rough | |||||
export interface RoughProdScheduleResult { | 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 { | 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 { | 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 { | 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 { | 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 { revalidateTag } from "next/cache"; | ||||
import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
import { CreateItemResponse } from "../../utils"; | import { CreateItemResponse } from "../../utils"; | ||||
import { ItemQc } from "."; | |||||
import { ItemQc, ItemsResult } from "."; | |||||
import { QcChecksInputs } from "../qcCheck/actions"; | import { QcChecksInputs } from "../qcCheck/actions"; | ||||
import { cache } from "react"; | |||||
// export type TypeInputs = { | // export type TypeInputs = { | ||||
// id: number; | // id: number; | ||||
@@ -37,6 +38,7 @@ export type CreateItemInputs = { | |||||
}; | }; | ||||
export const saveItem = async (data: CreateItemInputs) => { | export const saveItem = async (data: CreateItemInputs) => { | ||||
<<<<<<< HEAD | |||||
// try { | // try { | ||||
const item = await serverFetchJson<CreateItemResponse<CreateItemInputs>>(`${BASE_API_URL}/items/new`, { | const item = await serverFetchJson<CreateItemResponse<CreateItemInputs>>(`${BASE_API_URL}/items/new`, { | ||||
method: "POST", | method: "POST", | ||||
@@ -46,3 +48,29 @@ export const saveItem = async (data: CreateItemInputs) => { | |||||
revalidateTag("items"); | revalidateTag("items"); | ||||
return item | 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; | type: string; | ||||
qcChecks: ItemQc[]; | qcChecks: ItemQc[]; | ||||
action?: any; | action?: any; | ||||
fgName?: string; | |||||
excludeDate?: string; | |||||
}; | }; | ||||
export type Result = { | export type Result = { | ||||
@@ -10,8 +10,8 @@ export const downloadFile = (blobData: Uint8Array, filename: string) => { | |||||
link.click(); | link.click(); | ||||
}; | }; | ||||
export const convertObjToURLSearchParams = <T extends Object>( | |||||
data: T | null, | |||||
export const convertObjToURLSearchParams = <T extends object>( | |||||
data?: T | null, | |||||
): string => { | ): string => { | ||||
if (isEmpty(data)) { | if (isEmpty(data)) { | ||||
return ""; | return ""; | ||||
@@ -23,6 +23,7 @@ export const moneyFormatter = new Intl.NumberFormat("en-HK", { | |||||
export const decimalFormatter = new Intl.NumberFormat("en-HK", { | export const decimalFormatter = new Intl.NumberFormat("en-HK", { | ||||
minimumFractionDigits: 2, | minimumFractionDigits: 2, | ||||
maximumFractionDigits: 2, | |||||
}); | }); | ||||
export const integerFormatter = new Intl.NumberFormat("en-HK", {}); | export const integerFormatter = new Intl.NumberFormat("en-HK", {}); | ||||
@@ -66,6 +67,36 @@ export const dayjsToDateString = (date: Dayjs) => { | |||||
return date.format(OUTPUT_DATE_FORMAT); | 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 } = { | export const stockInLineStatusMap: { [status: string]: number } = { | ||||
draft: 0, | draft: 0, | ||||
pending: 1, | pending: 1, | ||||
@@ -25,6 +25,8 @@ const pathToLabelMap: { [path: string]: string } = { | |||||
"/pickOrder": "Pick Order", | "/pickOrder": "Pick Order", | ||||
"/po": "Purchase Order", | "/po": "Purchase Order", | ||||
"/dashboard": "dashboard", | "/dashboard": "dashboard", | ||||
"/jo": "Job Order", | |||||
"/jo/edit": "Edit Job Order", | |||||
}; | }; | ||||
const Breadcrumb = () => { | 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; | inStockQty: number; | ||||
productionQty?: number; | productionQty?: number; | ||||
purchaseQty?: number; | purchaseQty?: number; | ||||
safetyStock?: number; | |||||
lastMonthAvgStock?: number | |||||
}; | }; | ||||
const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit }) => { | 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) => { | setPagingController((prevState) => { | ||||
return prevState.map((item, index) => { | return prevState.map((item, index) => { | ||||
if (index === updatedObj?.index) { | if (index === updatedObj?.index) { | ||||
@@ -2176,6 +2178,7 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit }) => { | |||||
{`${t("FG Demand Date")}: ${date}`} | {`${t("FG Demand Date")}: ${date}`} | ||||
</Typography> */} | </Typography> */} | ||||
<EditableSearchResults<FGRecord> | <EditableSearchResults<FGRecord> | ||||
index={1} | |||||
items={fakeRecords[index]} // Use the corresponding records for the day | items={fakeRecords[index]} // Use the corresponding records for the day | ||||
columns={columns} | columns={columns} | ||||
setPagingController={updatePagingController} | setPagingController={updatePagingController} | ||||
@@ -1 +0,0 @@ | |||||
export { default } from "./DetailScheduleDetailWrapper"; |
@@ -5,7 +5,7 @@ import Stack from "@mui/material/Stack"; | |||||
import React from "react"; | import React from "react"; | ||||
// Can make this nicer | // Can make this nicer | ||||
export const DetailScheduleLoading: React.FC = () => { | |||||
export const DetailedScheduleLoading: React.FC = () => { | |||||
return ( | return ( | ||||
<> | <> | ||||
<Card> | <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 React, { useCallback, useEffect, useMemo, useState } from "react"; | ||||
import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
import { ItemsResult } from "@/app/api/settings/item"; | |||||
import SearchResults, { Column } from "../SearchResults"; | import SearchResults, { Column } from "../SearchResults"; | ||||
import { EditNote } from "@mui/icons-material"; | import { EditNote } from "@mui/icons-material"; | ||||
import { useRouter, useSearchParams } from "next/navigation"; | 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 { 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 { ScheduleType } from "@/app/api/scheduling"; | ||||
import { | import { | ||||
ProdScheduleResult, | ProdScheduleResult, | ||||
@@ -107,7 +99,7 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => { | |||||
const onDetailClick = (record: ProdScheduleResult) => { | const onDetailClick = (record: ProdScheduleResult) => { | ||||
console.log("[debug] record", record); | 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>[]>( | const columns = useMemo<Column<ProdScheduleResult>[]>( |
@@ -1,18 +1,18 @@ | |||||
import React from "react"; | 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 { ScheduleType } from "@/app/api/scheduling"; | ||||
import { SearchProdSchedule } from "@/app/api/scheduling/actions"; | import { SearchProdSchedule } from "@/app/api/scheduling/actions"; | ||||
interface SubComponents { | interface SubComponents { | ||||
Loading: typeof DetailScheduleLoading; | |||||
Loading: typeof DetailedScheduleLoading; | |||||
} | } | ||||
type Props = { | type Props = { | ||||
type: ScheduleType; | type: ScheduleType; | ||||
}; | }; | ||||
const DetailScheduleWrapper: React.FC<Props> & SubComponents = async ({ | |||||
const DetailedScheduleWrapper: React.FC<Props> & SubComponents = async ({ | |||||
type, | type, | ||||
}) => { | }) => { | ||||
const defaultInputs: SearchProdSchedule = { | const defaultInputs: SearchProdSchedule = { | ||||
@@ -22,6 +22,6 @@ const DetailScheduleWrapper: React.FC<Props> & SubComponents = async ({ | |||||
return <DSOverview type={type} defaultInputs={defaultInputs} />; | 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 { TypeEnum } from "@/app/utils/typeEnum"; | ||||
import { CreateItemInputs } from "@/app/api/settings/item/actions"; | import { CreateItemInputs } from "@/app/api/settings/item/actions"; | ||||
import { NumberInputProps } from "@/components/CreateItem/NumberInputProps"; | 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 | // temp interface input | ||||
type Props = { | type Props = { | ||||
// recordDetails: SaveDetailSchedule; | |||||
// recordDetails: SaveDetailedSchedule; | |||||
isEditing: boolean; | isEditing: boolean; | ||||
}; | }; | ||||
@@ -39,14 +40,15 @@ const DetailInfoCard: React.FC<Props> = ({ | |||||
const { | const { | ||||
control, | control, | ||||
register, | register, | ||||
getValues, | |||||
formState: { errors, defaultValues, touchedFields }, | formState: { errors, defaultValues, touchedFields }, | ||||
} = useFormContext<SaveDetailSchedule>(); | |||||
} = useFormContext<DetailedProdScheduleResult>(); | |||||
const [details, setDetails] = useState<SaveDetailSchedule | undefined>(undefined); | |||||
// const [details, setDetails] = useState<DetailedProdScheduleResult | undefined>(undefined); | |||||
useEffect(() => { | useEffect(() => { | ||||
console.log("[debug] record details", defaultValues) | console.log("[debug] record details", defaultValues) | ||||
setDetails(defaultValues as SaveDetailSchedule); | |||||
// setDetails(defaultValues as DetailedProdScheduleResult); | |||||
}, [defaultValues]) | }, [defaultValues]) | ||||
useEffect(() => { | useEffect(() => { | ||||
@@ -65,9 +67,10 @@ const DetailInfoCard: React.FC<Props> = ({ | |||||
<TextField | <TextField | ||||
label={t("Production Date")} | label={t("Production Date")} | ||||
fullWidth | fullWidth | ||||
{...register("productionDate", { | |||||
required: "name required!", | |||||
})} | |||||
// {...register("scheduleAt", { | |||||
// required: "Schedule At required!", | |||||
// })} | |||||
defaultValue={`${arrayToDateString(getValues("scheduleAt"))}`} | |||||
// defaultValue={details?.scheduledPeriod} | // defaultValue={details?.scheduledPeriod} | ||||
disabled={!isEditing} | disabled={!isEditing} | ||||
// error={Boolean(errors.name)} | // error={Boolean(errors.name)} | ||||
@@ -78,9 +81,15 @@ const DetailInfoCard: React.FC<Props> = ({ | |||||
<TextField | <TextField | ||||
label={t("Total Job Orders")} | label={t("Total Job Orders")} | ||||
fullWidth | 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} | // defaultValue={details?.productCount} | ||||
disabled={!isEditing} | disabled={!isEditing} | ||||
// error={Boolean(errors.code)} | // error={Boolean(errors.code)} | ||||
@@ -89,7 +98,7 @@ const DetailInfoCard: React.FC<Props> = ({ | |||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<Controller | <Controller | ||||
name="totalProductionQty" | |||||
name="totalEstProdCount" | |||||
control={control} | control={control} | ||||
render={({ field }) => ( | render={({ field }) => ( | ||||
<TextField | <TextField | ||||
@@ -97,11 +106,17 @@ const DetailInfoCard: React.FC<Props> = ({ | |||||
label={t("Total Production Qty")} | label={t("Total Production Qty")} | ||||
fullWidth | fullWidth | ||||
disabled={!isEditing} | disabled={!isEditing} | ||||
value={ | |||||
// TODO: May update by table demand qty | |||||
defaultValue={ | |||||
typeof field.value == "number" | typeof field.value == "number" | ||||
? integerFormatter.format(field.value) | ? integerFormatter.format(field.value) | ||||
: 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} | // defaultValue={typeof (details?.productionCount) == "number" ? integerFormatter.format(details?.productionCount) : details?.productionCount} | ||||
// error={Boolean(errors.type)} | // error={Boolean(errors.type)} | ||||
// helperText={errors.type?.message} | // 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={{ | pagingController={{ | ||||
pageNum: 0, | pageNum: 0, | ||||
pageSize: 0, | pageSize: 0, | ||||
totalCount: 0, | |||||
// totalCount: 0, | |||||
}} | }} | ||||
/> | /> | ||||
</> | </> | ||||
@@ -36,7 +36,7 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
{ label: t("Name"), paramName: "name", type: "text" }, | { label: t("Name"), paramName: "name", type: "text" }, | ||||
]; | ]; | ||||
return searchCriteria; | return searchCriteria; | ||||
}, [t, items]); | |||||
}, [t]); | |||||
const onDetailClick = useCallback( | const onDetailClick = useCallback( | ||||
(item: ItemsResult) => { | (item: ItemsResult) => { | ||||
@@ -70,7 +70,7 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
onClick: onDeleteClick, | onClick: onDeleteClick, | ||||
}, | }, | ||||
], | ], | ||||
[filteredItems], | |||||
[onDeleteClick, onDetailClick, t], | |||||
); | ); | ||||
const refetchData = useCallback( | const refetchData = useCallback( | ||||
@@ -102,12 +102,17 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
throw error; // Rethrow the error for further handling | throw error; // Rethrow the error for further handling | ||||
} | } | ||||
}, | }, | ||||
[axiosInstance, pagingController.pageNum, pagingController.pageSize], | |||||
[pagingController.pageNum, pagingController.pageSize], | |||||
); | ); | ||||
useEffect(() => { | useEffect(() => { | ||||
refetchData(filterObj); | refetchData(filterObj); | ||||
}, [filterObj, pagingController.pageNum, pagingController.pageSize]); | |||||
}, [ | |||||
filterObj, | |||||
pagingController.pageNum, | |||||
pagingController.pageSize, | |||||
refetchData, | |||||
]); | |||||
const onReset = useCallback(() => { | const onReset = useCallback(() => { | ||||
setFilteredItems(items); | 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 = | type SessionWithAbilities = | ||||
| ({ | | ({ | ||||
abilities: string[]; | abilities: string[]; | ||||
accessToken?: string; | |||||
} & Session) | } & Session) | ||||
| null; | | null; | ||||
@@ -73,9 +74,9 @@ const LoginForm: React.FC = () => { | |||||
// set auth to local storage | // set auth to local storage | ||||
const session = (await getSession()) as SessionWithAbilities; | const session = (await getSession()) as SessionWithAbilities; | ||||
// @ts-ignore | // @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) | // console.log(session) | ||||
window.localStorage.setItem( | window.localStorage.setItem( | ||||
"abilities", | "abilities", | ||||
@@ -31,8 +31,8 @@ import { MailTemplate } from "@/app/api/mail"; | |||||
import TemplateDetails from "./TemplateDetails"; | import TemplateDetails from "./TemplateDetails"; | ||||
import QrCodeScanner from "../QrCodeScanner/QrCodeScanner"; | import QrCodeScanner from "../QrCodeScanner/QrCodeScanner"; | ||||
import { | import { | ||||
QcCodeScannerContext, | |||||
useQcCodeScanner, | |||||
QrCodeScannerContext, | |||||
useQrCodeScannerContext, | |||||
} from "../QrCodeScannerProvider/QrCodeScannerProvider"; | } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | ||||
export interface Props { | export interface Props { | ||||
@@ -34,7 +34,7 @@ const TimesheetMailDetails: React.FC<Props> = ({ isActive }) => { | |||||
{t("Timesheet Template")} | {t("Timesheet Template")} | ||||
</Typography> | </Typography> | ||||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | ||||
<Grid item xs={8}> | |||||
{/* <Grid item xs={8}> | |||||
<TextField | <TextField | ||||
label={t("Cc")} | label={t("Cc")} | ||||
fullWidth | fullWidth | ||||
@@ -57,7 +57,7 @@ const TimesheetMailDetails: React.FC<Props> = ({ isActive }) => { | |||||
})} | })} | ||||
error={Boolean(errors.template?.subject)} | error={Boolean(errors.template?.subject)} | ||||
/> | /> | ||||
</Grid> | |||||
</Grid> */} | |||||
<Grid item xs={8}> | <Grid item xs={8}> | ||||
<TextField | <TextField | ||||
label={t("Required Params")} | label={t("Required Params")} | ||||
@@ -67,7 +67,7 @@ const TimesheetMailDetails: React.FC<Props> = ({ isActive }) => { | |||||
// error={Boolean(errors.template?.template)} | // error={Boolean(errors.template?.template)} | ||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={12}> | |||||
{/* <Grid item xs={12}> | |||||
<Controller | <Controller | ||||
control={control} | control={control} | ||||
name="template.template" | name="template.template" | ||||
@@ -83,7 +83,7 @@ const TimesheetMailDetails: React.FC<Props> = ({ isActive }) => { | |||||
validate: (value) => value?.includes("${date}"), | validate: (value) => value?.includes("${date}"), | ||||
}} | }} | ||||
/> | /> | ||||
</Grid> | |||||
</Grid> */} | |||||
</Grid> | </Grid> | ||||
</Box> | </Box> | ||||
</CardContent> | </CardContent> | ||||
@@ -52,7 +52,7 @@ const NavigationContent: React.FC = () => { | |||||
{ | { | ||||
icon: <RequestQuote />, | icon: <RequestQuote />, | ||||
label: "Pick Order", | label: "Pick Order", | ||||
path: "/pickorder", | |||||
path: "/pickOrder", | |||||
}, | }, | ||||
// { | // { | ||||
// icon: <RequestQuote />, | // icon: <RequestQuote />, | ||||
@@ -179,7 +179,7 @@ const NavigationContent: React.FC = () => { | |||||
{ | { | ||||
icon: <RequestQuote />, | icon: <RequestQuote />, | ||||
label: "Detail Scheduling", | label: "Detail Scheduling", | ||||
path: "/scheduling/detail", | |||||
path: "/scheduling/detailed", | |||||
}, | }, | ||||
{ | { | ||||
icon: <RequestQuote />, | 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 />, | icon: <RequestQuote />, | ||||
label: "Settings", | label: "Settings", | ||||
@@ -263,11 +275,11 @@ const NavigationContent: React.FC = () => { | |||||
label: "QC Check Template", | label: "QC Check Template", | ||||
path: "/settings/user", | path: "/settings/user", | ||||
}, | }, | ||||
{ | |||||
icon: <RequestQuote />, | |||||
label: "Mail", | |||||
path: "/settings/mail", | |||||
}, | |||||
// { | |||||
// icon: <RequestQuote />, | |||||
// label: "Mail", | |||||
// path: "/settings/mail", | |||||
// }, | |||||
{ | { | ||||
icon: <RequestQuote />, | icon: <RequestQuote />, | ||||
label: "Import Testing", | label: "Import Testing", | ||||
@@ -5,14 +5,14 @@ import { | |||||
PickOrderQcInput, | PickOrderQcInput, | ||||
updateStockOutLine, | updateStockOutLine, | ||||
UpdateStockOutLine, | UpdateStockOutLine, | ||||
} from "@/app/api/pickorder/actions"; | |||||
} from "@/app/api/pickOrder/actions"; | |||||
import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | ||||
import QcContent from "./QcContent"; | import QcContent from "./QcContent"; | ||||
import { Box, Button, Modal, ModalProps, Stack } from "@mui/material"; | import { Box, Button, Modal, ModalProps, Stack } from "@mui/material"; | ||||
import { useCallback } from "react"; | import { useCallback } from "react"; | ||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import { Check } from "@mui/icons-material"; | import { Check } from "@mui/icons-material"; | ||||
import { StockOutLine } from "@/app/api/pickorder"; | |||||
import { StockOutLine } from "@/app/api/pickOrder"; | |||||
import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
import { INPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT } from "@/app/utils/formatUtil"; | import { INPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT } from "@/app/utils/formatUtil"; | ||||
import ApprovalContent from "./ApprovalContent"; | import ApprovalContent from "./ApprovalContent"; | ||||
@@ -63,7 +63,7 @@ const ApprovalForm: React.FC<Props> = ({ | |||||
[onClose], | [onClose], | ||||
); | ); | ||||
const onSubmit = useCallback<SubmitHandler<PickOrderApprovalInput & {}>>( | |||||
const onSubmit = useCallback<SubmitHandler<PickOrderApprovalInput>>( | |||||
async (data, event) => { | async (data, event) => { | ||||
console.log(data); | console.log(data); | ||||
// checking later | // checking later | ||||
@@ -1,12 +1,7 @@ | |||||
"use client"; | "use client"; | ||||
import { | import { | ||||
Box, | |||||
Button, | Button, | ||||
ButtonProps, | |||||
Card, | |||||
CardContent, | |||||
CardHeader, | |||||
CircularProgress, | CircularProgress, | ||||
Grid, | Grid, | ||||
Stack, | Stack, | ||||
@@ -28,14 +23,14 @@ import { | |||||
GridEditInputCell, | GridEditInputCell, | ||||
GridRowParams, | GridRowParams, | ||||
} from "@mui/x-data-grid"; | } from "@mui/x-data-grid"; | ||||
import { PlayArrow } from "@mui/icons-material"; | |||||
import DoneIcon from "@mui/icons-material/Done"; | import DoneIcon from "@mui/icons-material/Done"; | ||||
import { GridRowSelectionModel } from "@mui/x-data-grid"; | import { GridRowSelectionModel } from "@mui/x-data-grid"; | ||||
import { useQcCodeScanner } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | |||||
import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | |||||
import { | import { | ||||
completeConsoPickOrder, | completeConsoPickOrder, | ||||
CreateStockOutLine, | CreateStockOutLine, | ||||
createStockOutLine, | createStockOutLine, | ||||
fetchConsoStatus, | |||||
fetchPickOrderLineClient, | fetchPickOrderLineClient, | ||||
fetchStockOutLineClient, | fetchStockOutLineClient, | ||||
PickOrderApprovalInput, | PickOrderApprovalInput, | ||||
@@ -63,6 +58,7 @@ import AutoFixNormalIcon from "@mui/icons-material/AutoFixNormal"; | |||||
import ApprovalForm from "./ApprovalForm"; | import ApprovalForm from "./ApprovalForm"; | ||||
import InfoIcon from "@mui/icons-material/Info"; | import InfoIcon from "@mui/icons-material/Info"; | ||||
import VerifiedIcon from "@mui/icons-material/Verified"; | import VerifiedIcon from "@mui/icons-material/Verified"; | ||||
import { isNullOrUndefined } from "html5-qrcode/esm/core"; | |||||
interface Props { | interface Props { | ||||
qc: QcItemWithChecks[]; | qc: QcItemWithChecks[]; | ||||
@@ -171,9 +167,16 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||||
headerName: "location", | headerName: "location", | ||||
flex: 1, | flex: 1, | ||||
renderCell: (params) => { | 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", | headerName: "suggestedLotNo", | ||||
flex: 1.2, | flex: 1.2, | ||||
renderCell: (params) => { | renderCell: (params) => { | ||||
if (!params.row.suggestedLotNo) return <></>; | |||||
return params.row.suggestedLotNo; | |||||
if (isNullOrUndefined(params.row.suggestedLotNo)) return <></>; | |||||
const suggestedLotNoList = JSON.parse( | const suggestedLotNoList = JSON.parse( | ||||
params.row.suggestedLotNo, | params.row.suggestedLotNo, | ||||
) as string[]; | ) 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( | const fetchPickOrderLine = useCallback( | ||||
async (params: Record<string, any>) => { | async (params: Record<string, any>) => { | ||||
@@ -215,9 +226,10 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||||
if (res) { | if (res) { | ||||
console.log(res); | console.log(res); | ||||
console.log(res.records.every((line) => line.status == "completed")); | 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); | setPickOrderLine(res.records); | ||||
setPolTotalCount(res.total); | setPolTotalCount(res.total); | ||||
@@ -233,21 +245,6 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||||
[fetchPickOrderLineClient, consoCode], | [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 [stockOutLine, setStockOutLine] = useState<StockOutLine[]>([]); | ||||
const getRowId = useCallback<GridRowIdGetter<StockOutLineRow>>( | const getRowId = useCallback<GridRowIdGetter<StockOutLineRow>>( | ||||
@@ -505,6 +502,7 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||||
key="edit" | key="edit" | ||||
/>, | />, | ||||
<GridActionsCellItem | <GridActionsCellItem | ||||
key={1} | |||||
icon={<DoDisturbIcon />} | icon={<DoDisturbIcon />} | ||||
label="Delete" | label="Delete" | ||||
sx={{ | sx={{ | ||||
@@ -593,7 +591,9 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||||
useEffect(() => { | useEffect(() => { | ||||
if (!qcOpen || !approvalOpen) { | if (!qcOpen || !approvalOpen) { | ||||
triggerRefetch(); | triggerRefetch(); | ||||
getConsoStatus(); | |||||
fetchPickOrderLine(polCriteriaArgs); | fetchPickOrderLine(polCriteriaArgs); | ||||
// getConsoStatus() | |||||
} | } | ||||
if (selectedRow.length > 0) fetchStockOutLine(solCriteriaArgs, selectedRow); | if (selectedRow.length > 0) fetchStockOutLine(solCriteriaArgs, selectedRow); | ||||
}, [ | }, [ | ||||
@@ -603,6 +603,7 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||||
selectedRow, | selectedRow, | ||||
triggerRefetch, | triggerRefetch, | ||||
polCriteriaArgs, | polCriteriaArgs, | ||||
getConsoStatus, | |||||
]); | ]); | ||||
const getLotDetail = useCallback( | const getLotDetail = useCallback( | ||||
@@ -630,7 +631,7 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||||
setOpenScanner((prev) => !prev); | setOpenScanner((prev) => !prev); | ||||
}, []); | }, []); | ||||
const scanner = useQcCodeScanner(); | |||||
const scanner = useQrCodeScannerContext(); | |||||
useEffect(() => { | useEffect(() => { | ||||
if (isOpenScanner && !scanner.isScanning) { | if (isOpenScanner && !scanner.isScanning) { | ||||
scanner.startScan(); | scanner.startScan(); | ||||
@@ -672,6 +673,8 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||||
changeRow, | changeRow, | ||||
addRow, | addRow, | ||||
getLotDetail, | getLotDetail, | ||||
scanner, | |||||
setIsUploading, | |||||
]); | ]); | ||||
const mannuallyAddRow = useCallback(() => { | const mannuallyAddRow = useCallback(() => { | ||||
@@ -679,7 +682,7 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||||
addRow(qrcode); | addRow(qrcode); | ||||
// scanner.resetScan(); | // scanner.resetScan(); | ||||
}); | }); | ||||
}, [addRow, homemade_Qrcode]); | |||||
}, [addRow, getLotDetail, homemade_Qrcode.stockInLineId]); | |||||
const validation = useCallback( | const validation = useCallback( | ||||
( | ( | ||||
@@ -739,32 +742,35 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||||
const handleCompleteOrder = useCallback(async () => { | const handleCompleteOrder = useCallback(async () => { | ||||
const res = await completeConsoPickOrder(consoCode); | const res = await completeConsoPickOrder(consoCode); | ||||
if (res) { | |||||
if (res.message === "completed") { | |||||
console.log(res); | |||||
// completed | // completed | ||||
triggerRefetch(); | triggerRefetch(); | ||||
// setIsCompletedOrder(false) | |||||
} else { | } else { | ||||
// not completed | // not completed | ||||
triggerRefetch(); | triggerRefetch(); | ||||
} | } | ||||
}, [consoCode, triggerRefetch]); | }, [consoCode, triggerRefetch]); | ||||
return ( | return ( | ||||
<> | <> | ||||
<Stack spacing={2}> | <Stack spacing={2}> | ||||
<Grid container xs={12} justifyContent="start"> | <Grid container xs={12} justifyContent="start"> | ||||
<Grid item xs={12}> | <Grid item xs={12}> | ||||
<Typography variant="h4" marginInlineEnd={2}> | <Typography variant="h4" marginInlineEnd={2}> | ||||
{consoCode} | |||||
{consoCode} - {status} | |||||
</Typography> | </Typography> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={8}> | <Grid item xs={8}> | ||||
<Button | <Button | ||||
// onClick={buttonData.onClick} | // onClick={buttonData.onClick} | ||||
disabled={!isCompletedOrder} | |||||
color={buttonData.buttonColor as ButtonProps["color"]} | |||||
startIcon={buttonData.buttonIcon} | |||||
disabled={isDisableComplete} | |||||
color={"info"} | |||||
startIcon={<DoneIcon />} | |||||
onClick={handleCompleteOrder} | onClick={handleCompleteOrder} | ||||
> | > | ||||
{buttonData.buttonText} | |||||
{t("Complete Pick Order")} | |||||
</Button> | </Button> | ||||
</Grid> | </Grid> | ||||
<Grid | <Grid | ||||
@@ -774,13 +780,16 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||||
justifyContent="end" | justifyContent="end" | ||||
alignItems="end" | alignItems="end" | ||||
> | > | ||||
<Button | |||||
{/* <Button | |||||
onClick={mannuallyAddRow} | onClick={mannuallyAddRow} | ||||
disabled={selectedRow.length === 0} | disabled={selectedRow.length === 0} | ||||
> | > | ||||
{isOpenScanner ? t("manual scanning") : t("manual scan")} | {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")} | {isOpenScanner ? t("binding") : t("bind")} | ||||
</Button> | </Button> | ||||
</Grid> | </Grid> | ||||
@@ -36,7 +36,7 @@ import { NEXT_PUBLIC_API_URL } from "@/config/api"; | |||||
import axiosInstance from "@/app/(main)/axios/axiosInstance"; | import axiosInstance from "@/app/(main)/axios/axiosInstance"; | ||||
import TwoLineCell from "../PoDetail/TwoLineCell"; | import TwoLineCell from "../PoDetail/TwoLineCell"; | ||||
import QcSelect from "../PoDetail/QcSelect"; | import QcSelect from "../PoDetail/QcSelect"; | ||||
import { PickOrderQcInput } from "@/app/api/pickorder/actions"; | |||||
import { PickOrderQcInput } from "@/app/api/pickOrder/actions"; | |||||
interface Props { | interface Props { | ||||
qcDefaultValues: PickOrderQcInput; | qcDefaultValues: PickOrderQcInput; | ||||
@@ -4,14 +4,14 @@ import { | |||||
PickOrderQcInput, | PickOrderQcInput, | ||||
updateStockOutLine, | updateStockOutLine, | ||||
UpdateStockOutLine, | UpdateStockOutLine, | ||||
} from "@/app/api/pickorder/actions"; | |||||
} from "@/app/api/pickOrder/actions"; | |||||
import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | ||||
import QcContent from "./QcContent"; | import QcContent from "./QcContent"; | ||||
import { Box, Button, Modal, ModalProps, Stack } from "@mui/material"; | import { Box, Button, Modal, ModalProps, Stack } from "@mui/material"; | ||||
import { useCallback } from "react"; | import { useCallback } from "react"; | ||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import { Check } from "@mui/icons-material"; | import { Check } from "@mui/icons-material"; | ||||
import { StockOutLine } from "@/app/api/pickorder"; | |||||
import { StockOutLine } from "@/app/api/pickOrder"; | |||||
import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
import { INPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT } from "@/app/utils/formatUtil"; | import { INPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT } from "@/app/utils/formatUtil"; | ||||
@@ -60,7 +60,7 @@ const QcForm: React.FC<Props> = ({ | |||||
[onClose], | [onClose], | ||||
); | ); | ||||
const onSubmit = useCallback<SubmitHandler<PickOrderQcInput & {}>>( | |||||
const onSubmit = useCallback<SubmitHandler<PickOrderQcInput>>( | |||||
async (data, event) => { | async (data, event) => { | ||||
console.log(data); | console.log(data); | ||||
console.log(qcDefaultValues); | console.log(qcDefaultValues); | ||||
@@ -86,7 +86,7 @@ const QcForm: React.FC<Props> = ({ | |||||
console.log("bug la"); | console.log("bug la"); | ||||
} | } | ||||
}, | }, | ||||
[updateStockOutLine], | |||||
[closeHandler, qcDefaultValues], | |||||
); | ); | ||||
return ( | return ( | ||||
<> | <> | ||||
@@ -12,7 +12,7 @@ import { | |||||
} from "react"; | } from "react"; | ||||
import { GridColDef } from "@mui/x-data-grid"; | import { GridColDef } from "@mui/x-data-grid"; | ||||
import { CircularProgress, Grid, Typography } from "@mui/material"; | import { CircularProgress, Grid, Typography } from "@mui/material"; | ||||
import { ByItemsSummary } from "@/app/api/pickorder"; | |||||
import { ByItemsSummary } from "@/app/api/pickOrder"; | |||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
dayjs.extend(arraySupport); | dayjs.extend(arraySupport); | ||||
@@ -12,7 +12,7 @@ import { | |||||
} from "react"; | } from "react"; | ||||
import { GridColDef, GridInputRowSelectionModel } from "@mui/x-data-grid"; | import { GridColDef, GridInputRowSelectionModel } from "@mui/x-data-grid"; | ||||
import { Box, CircularProgress, Grid, Typography } from "@mui/material"; | 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"; | import { useTranslation } from "react-i18next"; | ||||
dayjs.extend(arraySupport); | dayjs.extend(arraySupport); | ||||
@@ -29,7 +29,7 @@ import { | |||||
ConsoPickOrderResult, | ConsoPickOrderResult, | ||||
PickOrderLine, | PickOrderLine, | ||||
PickOrderResult, | PickOrderResult, | ||||
} from "@/app/api/pickorder"; | |||||
} from "@/app/api/pickOrder"; | |||||
import { useRouter, useSearchParams } from "next/navigation"; | import { useRouter, useSearchParams } from "next/navigation"; | ||||
import ConsolidatePickOrderItemSum from "./ConsolidatePickOrderItemSum"; | import ConsolidatePickOrderItemSum from "./ConsolidatePickOrderItemSum"; | ||||
import ConsolidatePickOrderSum from "./ConsolidatePickOrderSum"; | import ConsolidatePickOrderSum from "./ConsolidatePickOrderSum"; | ||||
@@ -39,7 +39,7 @@ import { | |||||
fetchConsoPickOrderClient, | fetchConsoPickOrderClient, | ||||
releasePickOrder, | releasePickOrder, | ||||
ReleasePickOrderInputs, | ReleasePickOrderInputs, | ||||
} from "@/app/api/pickorder/actions"; | |||||
} from "@/app/api/pickOrder/actions"; | |||||
import { EditNote } from "@mui/icons-material"; | import { EditNote } from "@mui/icons-material"; | ||||
import { fetchNameList, NameList } from "@/app/api/user/actions"; | import { fetchNameList, NameList } from "@/app/api/user/actions"; | ||||
import { useField } from "@mui/x-date-pickers/internals"; | import { useField } from "@mui/x-date-pickers/internals"; | ||||
@@ -111,7 +111,7 @@ const ConsolidatedPickOrders: React.FC<Props> = ({ filterArgs }) => { | |||||
console.log(pickOrder); | console.log(pickOrder); | ||||
const status = pickOrder.status; | const status = pickOrder.status; | ||||
if (pickOrderStatusMap[status] >= 3) { | if (pickOrderStatusMap[status] >= 3) { | ||||
router.push(`/pickorder/detail?consoCode=${pickOrder.consoCode}`); | |||||
router.push(`/pickOrder/detail?consoCode=${pickOrder.consoCode}`); | |||||
} else { | } else { | ||||
openDetailModal(pickOrder.consoCode); | openDetailModal(pickOrder.consoCode); | ||||
} | } | ||||
@@ -135,7 +135,7 @@ const ConsolidatedPickOrders: React.FC<Props> = ({ filterArgs }) => { | |||||
label: t("status"), | label: t("status"), | ||||
}, | }, | ||||
], | ], | ||||
[], | |||||
[onDetailClick, t], | |||||
); | ); | ||||
const [pagingController, setPagingController] = useState( | const [pagingController, setPagingController] = useState( | ||||
defaultPagingController, | defaultPagingController, | ||||
@@ -215,18 +215,18 @@ const ConsolidatedPickOrders: React.FC<Props> = ({ filterArgs }) => { | |||||
console.log(newValue); | console.log(newValue); | ||||
formProps.setValue("assignTo", newValue.id); | formProps.setValue("assignTo", newValue.id); | ||||
}, | }, | ||||
[], | |||||
[formProps], | |||||
); | ); | ||||
const onSubmit = useCallback<SubmitHandler<ReleasePickOrderInputs & {}>>( | |||||
const onSubmit = useCallback<SubmitHandler<ReleasePickOrderInputs>>( | |||||
async (data, event) => { | async (data, event) => { | ||||
console.log(data); | console.log(data); | ||||
try { | try { | ||||
const res = await releasePickOrder(data); | const res = await releasePickOrder(data); | ||||
console.log(res); | 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 { | } else { | ||||
console.log(res); | console.log(res); | ||||
} | } | ||||
@@ -234,7 +234,7 @@ const ConsolidatedPickOrders: React.FC<Props> = ({ filterArgs }) => { | |||||
console.log(error); | console.log(error); | ||||
} | } | ||||
}, | }, | ||||
[releasePickOrder], | |||||
[router], | |||||
); | ); | ||||
const onSubmitError = useCallback<SubmitErrorHandler<ReleasePickOrderInputs>>( | const onSubmitError = useCallback<SubmitErrorHandler<ReleasePickOrderInputs>>( | ||||
(errors) => {}, | (errors) => {}, | ||||
@@ -250,7 +250,7 @@ const ConsolidatedPickOrders: React.FC<Props> = ({ filterArgs }) => { | |||||
fetchConso(consoCode); | fetchConso(consoCode); | ||||
formProps.setValue("consoCode", consoCode); | formProps.setValue("consoCode", consoCode); | ||||
} | } | ||||
}, [consoCode]); | |||||
}, [consoCode, fetchConso, formProps]); | |||||
return ( | 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"; | "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 { useTranslation } from "react-i18next"; | ||||
import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
import SearchResults, { Column } from "../SearchResults"; | |||||
import { | import { | ||||
flatten, | flatten, | ||||
groupBy, | |||||
intersectionWith, | intersectionWith, | ||||
isEmpty, | isEmpty, | ||||
map, | |||||
sortBy, | sortBy, | ||||
sortedUniq, | |||||
uniqBy, | uniqBy, | ||||
upperCase, | upperCase, | ||||
upperFirst, | upperFirst, | ||||
} from "lodash"; | } from "lodash"; | ||||
import { | import { | ||||
arrayToDateString, | |||||
arrayToDayjs, | arrayToDayjs, | ||||
dateStringToDayjs, | |||||
} from "@/app/utils/formatUtil"; | } 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 PickOrders from "./PickOrders"; | ||||
import ConsolidatedPickOrders from "./ConsolidatedPickOrders"; | 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 { | interface Props { | ||||
pickOrders: PickOrderResult[]; | pickOrders: PickOrderResult[]; | ||||
} | } | ||||
@@ -41,15 +35,30 @@ type SearchParamNames = keyof SearchQuery; | |||||
const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | ||||
const { t } = useTranslation("pickOrder"); | const { t } = useTranslation("pickOrder"); | ||||
const [isOpenCreateModal, setIsOpenCreateModal] = useState(false) | |||||
const [items, setItems] = useState<ItemCombo[]>([]) | |||||
const [filteredPickOrders, setFilteredPickOrders] = useState(pickOrders); | const [filteredPickOrders, setFilteredPickOrders] = useState(pickOrders); | ||||
const [filterArgs, setFilterArgs] = useState<Record<string, any>>({}); | const [filterArgs, setFilterArgs] = useState<Record<string, any>>({}); | ||||
const [tabIndex, setTabIndex] = useState(0); | const [tabIndex, setTabIndex] = useState(0); | ||||
const [totalCount, setTotalCount] = useState<number>(); | |||||
const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | ||||
(_e, newValue) => { | (_e, newValue) => { | ||||
setTabIndex(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( | 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(() => { | const onReset = useCallback(() => { | ||||
setFilteredPickOrders(pickOrders); | setFilteredPickOrders(pickOrders); | ||||
}, [pickOrders]); | }, [pickOrders]); | ||||
useEffect(() => { | |||||
if (!isOpenCreateModal) { | |||||
setTabIndex(1) | |||||
setTimeout(async () => { | |||||
setTabIndex(0) | |||||
}, 200) | |||||
} | |||||
}, [isOpenCreateModal]) | |||||
return ( | 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 | <SearchBox | ||||
criteria={searchCriteria} | criteria={searchCriteria} | ||||
onSearch={(query) => { | onSearch={(query) => { | ||||
@@ -1,4 +1,4 @@ | |||||
import { fetchPickOrders } from "@/app/api/pickorder"; | |||||
import { fetchPickOrders } from "@/app/api/pickOrder"; | |||||
import GeneralLoading from "../General/GeneralLoading"; | import GeneralLoading from "../General/GeneralLoading"; | ||||
import PickOrderSearch from "./PickOrderSearch"; | import PickOrderSearch from "./PickOrderSearch"; | ||||
@@ -1,16 +1,18 @@ | |||||
import { Button, CircularProgress, Grid } from "@mui/material"; | import { Button, CircularProgress, Grid } from "@mui/material"; | ||||
import SearchResults, { Column } from "../SearchResults/SearchResults"; | import SearchResults, { Column } from "../SearchResults/SearchResults"; | ||||
import { PickOrderResult } from "@/app/api/pickorder"; | |||||
import { PickOrderResult } from "@/app/api/pickOrder"; | |||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import { useCallback, useEffect, useMemo, useState } from "react"; | import { useCallback, useEffect, useMemo, useState } from "react"; | ||||
import { isEmpty, upperCase, upperFirst } from "lodash"; | import { isEmpty, upperCase, upperFirst } from "lodash"; | ||||
import { arrayToDateString } from "@/app/utils/formatUtil"; | |||||
import { arrayToDateString, OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||||
import { | import { | ||||
consolidatePickOrder, | consolidatePickOrder, | ||||
fetchPickOrderClient, | fetchPickOrderClient, | ||||
} from "@/app/api/pickorder/actions"; | |||||
} from "@/app/api/pickOrder/actions"; | |||||
import useUploadContext from "../UploadProvider/useUploadContext"; | import useUploadContext from "../UploadProvider/useUploadContext"; | ||||
import dayjs from "dayjs"; | |||||
import arraySupport from "dayjs/plugin/arraySupport"; | |||||
dayjs.extend(arraySupport); | |||||
interface Props { | interface Props { | ||||
filteredPickOrders: PickOrderResult[]; | filteredPickOrders: PickOrderResult[]; | ||||
filterArgs: Record<string, any>; | filterArgs: Record<string, any>; | ||||
@@ -30,21 +32,6 @@ const PickOrders: React.FC<Props> = ({ filteredPickOrders, filterArgs }) => { | |||||
}); | }); | ||||
const [totalCount, setTotalCount] = useState<number>(); | 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( | const fetchNewPagePickOrder = useCallback( | ||||
async ( | async ( | ||||
pagingController: Record<string, number>, | 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(() => { | useEffect(() => { | ||||
fetchNewPagePickOrder(pagingController, filterArgs); | fetchNewPagePickOrder(pagingController, filterArgs); | ||||
}, [fetchNewPagePickOrder, pagingController, filterArgs]); | }, [fetchNewPagePickOrder, pagingController, filterArgs]); | ||||
@@ -109,7 +112,11 @@ const PickOrders: React.FC<Props> = ({ filteredPickOrders, filterArgs }) => { | |||||
name: "targetDate", | name: "targetDate", | ||||
label: t("Target Date"), | label: t("Target Date"), | ||||
renderCell: (params) => { | 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], | [itemDetail, formProps], | ||||
); | ); | ||||
const onSubmit = useCallback<SubmitHandler<ModalFormInput & {}>>( | |||||
const onSubmit = useCallback<SubmitHandler<ModalFormInput>>( | |||||
async (data, event) => { | async (data, event) => { | ||||
setBtnIsLoading(true); | setBtnIsLoading(true); | ||||
setIsUploading(true); | setIsUploading(true); | ||||
@@ -47,7 +47,7 @@ import ReactQrCodeScanner, { | |||||
ScannerConfig, | ScannerConfig, | ||||
} from "../ReactQrCodeScanner/ReactQrCodeScanner"; | } from "../ReactQrCodeScanner/ReactQrCodeScanner"; | ||||
import { QrCodeInfo } from "@/app/api/qrcode"; | import { QrCodeInfo } from "@/app/api/qrcode"; | ||||
import { useQcCodeScanner } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | |||||
import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | |||||
import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
import arraySupport from "dayjs/plugin/arraySupport"; | import arraySupport from "dayjs/plugin/arraySupport"; | ||||
dayjs.extend(arraySupport); | dayjs.extend(arraySupport); | ||||
@@ -219,7 +219,7 @@ const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse, disabled }) => { | |||||
); | ); | ||||
// QR Code Scanner | // QR Code Scanner | ||||
const scanner = useQcCodeScanner(); | |||||
const scanner = useQrCodeScannerContext(); | |||||
useEffect(() => { | useEffect(() => { | ||||
if (isOpenScanner) { | if (isOpenScanner) { | ||||
scanner.startScan(); | scanner.startScan(); | ||||
@@ -27,7 +27,7 @@ import { QrCodeInfo } from "@/app/api/qrcode"; | |||||
import { Check } from "@mui/icons-material"; | import { Check } from "@mui/icons-material"; | ||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import { useSearchParams } from "next/navigation"; | import { useSearchParams } from "next/navigation"; | ||||
import { useQcCodeScanner } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | |||||
import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | |||||
interface Props extends Omit<ModalProps, "children"> { | interface Props extends Omit<ModalProps, "children"> { | ||||
warehouse: WarehouseResult[]; | warehouse: WarehouseResult[]; | ||||
@@ -83,7 +83,7 @@ const QrModal: React.FC<Props> = ({ open, onClose, warehouse }) => { | |||||
); | ); | ||||
// QR Code Scanner | // QR Code Scanner | ||||
const scanner = useQcCodeScanner(); | |||||
const scanner = useQrCodeScannerContext(); | |||||
useEffect(() => { | useEffect(() => { | ||||
if (open && !scanner.isScanning) { | if (open && !scanner.isScanning) { | ||||
scanner.startScan(); | scanner.startScan(); | ||||
@@ -136,7 +136,7 @@ const QrModal: React.FC<Props> = ({ open, onClose, warehouse }) => { | |||||
if (stockInLineId) fetchStockInLine(stockInLineId); | if (stockInLineId) fetchStockInLine(stockInLineId); | ||||
}, [stockInLineId]); | }, [stockInLineId]); | ||||
const onSubmit = useCallback<SubmitHandler<ModalFormInput & {}>>( | |||||
const onSubmit = useCallback<SubmitHandler<ModalFormInput>>( | |||||
async (data, event) => { | async (data, event) => { | ||||
const hasErrors = false; | const hasErrors = false; | ||||
console.log("errors"); | console.log("errors"); | ||||
@@ -15,7 +15,7 @@ import { useSession } from "next-auth/react"; | |||||
import { defaultPagingController } from "../SearchResults/SearchResults"; | import { defaultPagingController } from "../SearchResults/SearchResults"; | ||||
import { fetchPoListClient, testing } from "@/app/api/po/actions"; | import { fetchPoListClient, testing } from "@/app/api/po/actions"; | ||||
import dayjs from "dayjs"; | 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"; | import arraySupport from "dayjs/plugin/arraySupport"; | ||||
dayjs.extend(arraySupport); | dayjs.extend(arraySupport); | ||||
@@ -72,7 +72,7 @@ const PoSearch: React.FC<Props> = ({ | |||||
[router], | [router], | ||||
); | ); | ||||
const onDeleteClick = useCallback((po: PoResult) => {}, [router]); | |||||
const onDeleteClick = useCallback((po: PoResult) => {}, []); | |||||
const columns = useMemo<Column<PoResult>[]>( | const columns = useMemo<Column<PoResult>[]>( | ||||
() => [ | () => [ | ||||
@@ -92,8 +92,8 @@ const PoSearch: React.FC<Props> = ({ | |||||
renderCell: (params) => { | renderCell: (params) => { | ||||
return ( | return ( | ||||
dayjs(params.orderDate) | 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) { | if (defectToAdd) { | ||||
// Check for duplicate code (skip if code is empty) | // Check for duplicate code (skip if code is empty) | ||||
const isDuplicate = | const isDuplicate = | ||||
defectToAdd.code && defects.some((d) => d.code === defectToAdd.code); | |||||
defectToAdd.code && defects.some((d) => d.code === defectToAdd!.code); | |||||
if (!isDuplicate) { | if (!isDuplicate) { | ||||
const updatedDefects = [...defects, defectToAdd]; | const updatedDefects = [...defects, defectToAdd]; | ||||
onDefectsChange(updatedDefects); | onDefectsChange(updatedDefects); | ||||
@@ -8,7 +8,7 @@ import { | |||||
useState, | useState, | ||||
} from "react"; | } from "react"; | ||||
interface QcCodeScanner { | |||||
interface QrCodeScanner { | |||||
values: string[]; | values: string[]; | ||||
isScanning: boolean; | isScanning: boolean; | ||||
startScan: () => void; | startScan: () => void; | ||||
@@ -20,14 +20,14 @@ interface QrCodeScannerProviderProps { | |||||
children: ReactNode; | children: ReactNode; | ||||
} | } | ||||
export const QcCodeScannerContext = createContext<QcCodeScanner | undefined>( | |||||
export const QrCodeScannerContext = createContext<QrCodeScanner | undefined>( | |||||
undefined, | undefined, | ||||
); | ); | ||||
const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | ||||
children, | children, | ||||
}) => { | }) => { | ||||
const [qcCodeScannerValues, setQrCodeScannerValues] = useState<string[]>([]); | |||||
const [qrCodeScannerValues, setQrCodeScannerValues] = useState<string[]>([]); | |||||
const [isScanning, setIsScanning] = useState<boolean>(false); | const [isScanning, setIsScanning] = useState<boolean>(false); | ||||
const [keys, setKeys] = useState<string[]>([]); | const [keys, setKeys] = useState<string[]>([]); | ||||
const [leftCurlyBraceCount, setLeftCurlyBraceCount] = useState<number>(0); | const [leftCurlyBraceCount, setLeftCurlyBraceCount] = useState<number>(0); | ||||
@@ -82,7 +82,7 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||||
keys.join("").substring(startBrace, endBrace + 1), | keys.join("").substring(startBrace, endBrace + 1), | ||||
]); | ]); | ||||
console.log(keys); | console.log(keys); | ||||
console.log(qcCodeScannerValues); | |||||
console.log(qrCodeScannerValues); | |||||
// reset | // reset | ||||
setKeys(() => []); | setKeys(() => []); | ||||
@@ -92,9 +92,9 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||||
}, [keys, leftCurlyBraceCount, rightCurlyBraceCount]); | }, [keys, leftCurlyBraceCount, rightCurlyBraceCount]); | ||||
return ( | return ( | ||||
<QcCodeScannerContext.Provider | |||||
<QrCodeScannerContext.Provider | |||||
value={{ | value={{ | ||||
values: qcCodeScannerValues, | |||||
values: qrCodeScannerValues, | |||||
isScanning: isScanning, | isScanning: isScanning, | ||||
startScan: startQrCodeScanner, | startScan: startQrCodeScanner, | ||||
stopScan: endQrCodeScanner, | stopScan: endQrCodeScanner, | ||||
@@ -102,15 +102,15 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||||
}} | }} | ||||
> | > | ||||
{children} | {children} | ||||
</QcCodeScannerContext.Provider> | |||||
</QrCodeScannerContext.Provider> | |||||
); | ); | ||||
}; | }; | ||||
export const useQcCodeScanner = (): QcCodeScanner => { | |||||
const context = useContext(QcCodeScannerContext); | |||||
export const useQrCodeScannerContext = (): QrCodeScanner => { | |||||
const context = useContext(QrCodeScannerContext); | |||||
if (!context) { | if (!context) { | ||||
throw new Error( | throw new Error( | ||||
"useQcCodeScanner must be used within a QcCodeScannerProvider", | |||||
"useQrCodeScanner must be used within a QrCodeScannerProvider", | |||||
); | ); | ||||
} | } | ||||
return context; | return context; | ||||
@@ -245,7 +245,7 @@ const RSOverview: React.FC<Props> = ({ type, defaultInputs }) => { | |||||
// setFilteredSchedules(items ?? []); | // setFilteredSchedules(items ?? []); | ||||
// setFilterObj({}); | // setFilterObj({}); | ||||
// setTempSelectedValue({}); | // setTempSelectedValue({}); | ||||
refetchData(inputs, "reset"); | |||||
refetchData(defaultInputs, "reset"); | |||||
}, []); | }, []); | ||||
return ( | return ( | ||||
@@ -3,7 +3,7 @@ import { fetchItem } from "@/app/api/settings/item"; | |||||
import GeneralLoading from "@/components/General/GeneralLoading"; | import GeneralLoading from "@/components/General/GeneralLoading"; | ||||
import RoughScheduleDetailView from "@/components/RoughScheduleDetail/RoughScheudleDetailView"; | import RoughScheduleDetailView from "@/components/RoughScheduleDetail/RoughScheudleDetailView"; | ||||
import React from "react"; | import React from "react"; | ||||
import { ScheduleType, fetchProdScheduleDetail } from "@/app/api/scheduling"; | |||||
import { ScheduleType, fetchRoughProdScheduleDetail } from "@/app/api/scheduling"; | |||||
interface SubComponents { | interface SubComponents { | ||||
Loading: typeof GeneralLoading; | Loading: typeof GeneralLoading; | ||||
} | } | ||||
@@ -17,7 +17,7 @@ const RoughScheduleDetailWrapper: React.FC<Props> & SubComponents = async ({ | |||||
id, | id, | ||||
type, | type, | ||||
}) => { | }) => { | ||||
const prodSchedule = id ? await fetchProdScheduleDetail(id) : undefined; | |||||
const prodSchedule = id ? await fetchRoughProdScheduleDetail(id) : undefined; | |||||
return ( | return ( | ||||
<RoughScheduleDetailView | <RoughScheduleDetailView | ||||
@@ -3,14 +3,12 @@ | |||||
import { useCallback, useEffect, useMemo, useState } from "react"; | import { useCallback, useEffect, useMemo, useState } from "react"; | ||||
import { useRouter, useSearchParams } from "next/navigation"; | import { useRouter, useSearchParams } from "next/navigation"; | ||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import { CreateItemInputs, saveItem } from "@/app/api/settings/item/actions"; | |||||
import { | import { | ||||
FormProvider, | FormProvider, | ||||
SubmitErrorHandler, | SubmitErrorHandler, | ||||
SubmitHandler, | SubmitHandler, | ||||
useForm, | useForm, | ||||
} from "react-hook-form"; | } from "react-hook-form"; | ||||
import { deleteDialog } from "../Swal/CustomAlerts"; | |||||
import { | import { | ||||
Box, | Box, | ||||
Button, | Button, | ||||
@@ -23,16 +21,12 @@ import { | |||||
Typography, | Typography, | ||||
} from "@mui/material"; | } from "@mui/material"; | ||||
import { Add, Check, Close, EditNote } from "@mui/icons-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 DetailInfoCard from "@/components/RoughScheduleDetail/DetailInfoCard"; | ||||
import ViewByFGDetails from "@/components/RoughScheduleDetail/ViewByFGDetails"; | import ViewByFGDetails from "@/components/RoughScheduleDetail/ViewByFGDetails"; | ||||
import ViewByBomDetails from "@/components/RoughScheduleDetail/ViewByBomDetails"; | 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 { RoughProdScheduleResult, ScheduleType } from "@/app/api/scheduling"; | ||||
import { arrayToDayjs, dayjsToDateString } from "@/app/utils/formatUtil"; | import { arrayToDayjs, dayjsToDateString } from "@/app/utils/formatUtil"; | ||||
import { useGridApiRef } from "@mui/x-data-grid"; | |||||
type Props = { | type Props = { | ||||
isEditMode: boolean; | isEditMode: boolean; | ||||
@@ -3,7 +3,6 @@ | |||||
import React, { useCallback, useEffect, useMemo, useState } from "react"; | import React, { useCallback, useEffect, useMemo, useState } from "react"; | ||||
import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
import { ItemsResult } from "@/app/api/settings/item"; | import { ItemsResult } from "@/app/api/settings/item"; | ||||
import SearchResults, { Column } from "../SearchResults"; | |||||
import { EditNote } from "@mui/icons-material"; | import { EditNote } from "@mui/icons-material"; | ||||
import { useRouter, useSearchParams } from "next/navigation"; | import { useRouter, useSearchParams } from "next/navigation"; | ||||
import { GridDeleteIcon } from "@mui/x-data-grid"; | 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 { useTranslation } from "react-i18next"; | ||||
import axiosInstance from "@/app/(main)/axios/axiosInstance"; | import axiosInstance from "@/app/(main)/axios/axiosInstance"; | ||||
import Qs from "qs"; | 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"; | import { ItemsResultResponse } from "@/app/api/settings/item"; | ||||
type Props = { | type Props = { | ||||
items: ItemsResult[]; | items: ItemsResult[]; | ||||
@@ -64,35 +63,62 @@ const RSSOverview: React.FC<Props> = ({ items }) => { | |||||
const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => { | const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => { | ||||
const searchCriteria: Criterion<SearchParamNames>[] = [ | 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"), | label: t("Exclude Date"), | ||||
paramName: "excludeDate", | |||||
type: "multi-select", | |||||
options: dayOptions, | |||||
selectedValues: filterObj, | |||||
paramName: "shelfLife", | |||||
type: "select", | |||||
options: ["qcChecks"], | |||||
// selectedValues: filterObj, | |||||
handleSelectionChange: handleSelectionChange, | handleSelectionChange: handleSelectionChange, | ||||
}, | }, | ||||
]; | ]; | ||||
return searchCriteria; | return searchCriteria; | ||||
}, [t, items]); | }, [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", | // name: "action", | ||||
// label: t(""), | // label: t(""), | ||||
@@ -113,100 +139,41 @@ const RSSOverview: React.FC<Props> = ({ items }) => { | |||||
return; // Exit the function if the token is not set | 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(() => { | const onReset = useCallback(() => { | ||||
//setFilteredItems(items ?? []); | //setFilteredItems(items ?? []); | ||||
setFilterObj({}); | setFilterObj({}); | ||||
setTempSelectedValue({}); | setTempSelectedValue({}); | ||||
refetchData(); | |||||
}, [items]); | |||||
refetchData(filterObj); | |||||
}, [items, filterObj]); | |||||
return ( | return ( | ||||
<> | <> | ||||
@@ -220,6 +187,9 @@ const RSSOverview: React.FC<Props> = ({ items }) => { | |||||
onReset={onReset} | onReset={onReset} | ||||
/> | /> | ||||
<EditableSearchResults<ItemsResult> | <EditableSearchResults<ItemsResult> | ||||
index={1} // bug | |||||
isEdit | |||||
isEditable | |||||
items={filteredItems} | items={filteredItems} | ||||
columns={columns} | columns={columns} | ||||
setPagingController={setPagingController} | setPagingController={setPagingController} | ||||
@@ -37,6 +37,7 @@ import { decimalFormatter } from "@/app/utils/formatUtil"; | |||||
import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; | import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; | ||||
import HighlightOffIcon from "@mui/icons-material/HighlightOff"; | import HighlightOffIcon from "@mui/icons-material/HighlightOff"; | ||||
import { | import { | ||||
DetailedProdScheduleLineBomMaterialResult, | |||||
RoughProdScheduleLineBomMaterialResult, | RoughProdScheduleLineBomMaterialResult, | ||||
ScheduleType, | ScheduleType, | ||||
} from "@/app/api/scheduling"; | } from "@/app/api/scheduling"; | ||||
@@ -46,7 +47,7 @@ interface ResultWithId { | |||||
} | } | ||||
interface Props { | interface Props { | ||||
bomMaterial: RoughProdScheduleLineBomMaterialResult[]; | |||||
bomMaterial: RoughProdScheduleLineBomMaterialResult[] | DetailedProdScheduleLineBomMaterialResult[]; | |||||
type: ScheduleType; | type: ScheduleType; | ||||
} | } | ||||
@@ -113,7 +114,7 @@ function BomMaterialTable({ bomMaterial }: Props) { | |||||
headerName: t("Available Qty"), | headerName: t("Available Qty"), | ||||
flex: 0.5, | flex: 0.5, | ||||
type: "number", | type: "number", | ||||
editable: true, | |||||
// editable: true, | |||||
align: "right", | align: "right", | ||||
headerAlign: "right", | headerAlign: "right", | ||||
renderCell: (row) => { | renderCell: (row) => { | ||||
@@ -125,7 +126,7 @@ function BomMaterialTable({ bomMaterial }: Props) { | |||||
field: "demandQty", | field: "demandQty", | ||||
headerName: t("Demand Qty"), | headerName: t("Demand Qty"), | ||||
flex: 0.5, | flex: 0.5, | ||||
editable: true, | |||||
// editable: true, | |||||
align: "right", | align: "right", | ||||
headerAlign: "right", | headerAlign: "right", | ||||
renderCell: (row) => { | renderCell: (row) => { | ||||
@@ -136,7 +137,7 @@ function BomMaterialTable({ bomMaterial }: Props) { | |||||
field: "status", | field: "status", | ||||
headerName: t("status"), | headerName: t("status"), | ||||
flex: 0.5, | flex: 0.5, | ||||
editable: true, | |||||
// editable: true, | |||||
align: "center", | align: "center", | ||||
headerAlign: "center", | headerAlign: "center", | ||||
renderCell: (params) => { | renderCell: (params) => { | ||||
@@ -219,7 +220,7 @@ function BomMaterialTable({ bomMaterial }: Props) { | |||||
}, | }, | ||||
}} | }} | ||||
disableColumnMenu | disableColumnMenu | ||||
editMode="row" | |||||
// editMode="row" | |||||
rows={entries} | rows={entries} | ||||
rowModesModel={rowModesModel} | rowModesModel={rowModesModel} | ||||
onRowModesModelChange={setRowModesModel} | onRowModesModelChange={setRowModesModel} | ||||
@@ -1,11 +1,12 @@ | |||||
"use client"; | "use client"; | ||||
import React, { | import React, { | ||||
CSSProperties, | |||||
DetailedHTMLProps, | |||||
HTMLAttributes, | |||||
useEffect, | |||||
useState, | |||||
CSSProperties, | |||||
DetailedHTMLProps, | |||||
HTMLAttributes, | |||||
useEffect, | |||||
useRef, | |||||
useState, | |||||
} from "react"; | } from "react"; | ||||
import Paper from "@mui/material/Paper"; | import Paper from "@mui/material/Paper"; | ||||
import Table from "@mui/material/Table"; | 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 PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline"; | ||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import { | import { | ||||
RoughProdScheduleLineBomMaterialResult, | |||||
RoughProdScheduleLineResultByFg, | |||||
RoughProdScheduleResult, | |||||
ScheduleType, | |||||
DetailedProdScheduleLineResult, | |||||
RoughProdScheduleLineBomMaterialResult, | |||||
RoughProdScheduleLineResultByFg, | |||||
RoughProdScheduleResult, | |||||
ScheduleType, | |||||
} from "@/app/api/scheduling"; | } from "@/app/api/scheduling"; | ||||
import { defaultPagingController } from "../SearchResults/SearchResults"; | |||||
import { useFormContext } from "react-hook-form"; | |||||
export interface ResultWithId { | export interface ResultWithId { | ||||
id: string | number; | |||||
// id: number; | |||||
id: string | number; | |||||
// id: number; | |||||
} | } | ||||
interface BaseColumn<T extends ResultWithId> { | 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> { | 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> = | export type Column<T extends ResultWithId> = | ||||
| BaseColumn<T> | |||||
| ColumnWithAction<T>; | |||||
| BaseColumn<T> | |||||
| ColumnWithAction<T>; | |||||
interface Props<T extends ResultWithId> { | interface Props<T extends ResultWithId> { | ||||
index?: number; | |||||
items: T[]; | |||||
columns: Column<T>[]; | |||||
noWrapper?: boolean; | |||||
setPagingController: (value: { | |||||
pageNum: number; | |||||
pageSize: number; | |||||
totalCount: number; | |||||
index?: 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>({ | 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>) { | }: 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 ( | 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> | </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; | export default ScheduleTable; |
@@ -18,6 +18,7 @@ interface MultiSelectProps { | |||||
options: Option[]; | options: Option[]; | ||||
selectedValues: number[]; | selectedValues: number[]; | ||||
onChange: (values: number[]) => void; | onChange: (values: number[]) => void; | ||||
isReset?: boolean; | |||||
} | } | ||||
const MultiSelect: React.FC<MultiSelectProps> = ({ | const MultiSelect: React.FC<MultiSelectProps> = ({ | ||||
@@ -49,13 +50,13 @@ const MultiSelect: React.FC<MultiSelectProps> = ({ | |||||
<Select | <Select | ||||
multiple | multiple | ||||
value={displayValues} | value={displayValues} | ||||
onChange={handleChange} | |||||
onChange={handleChange as any} | |||||
renderValue={(selected) => ( | renderValue={(selected) => ( | ||||
<Box sx={{ display: "flex", flexWrap: "wrap" }}> | <Box sx={{ display: "flex", flexWrap: "wrap" }}> | ||||
{(selected as number[]).map((value) => ( | {(selected as number[]).map((value) => ( | ||||
<Chip | <Chip | ||||
key={value} | key={value} | ||||
label={options.find((item) => item.value == value).label} | |||||
label={options.find((item) => item.value == value)?.label ?? ""} | |||||
sx={{ margin: 0.5 }} | sx={{ margin: 0.5 }} | ||||
/> | /> | ||||
))} | ))} | ||||
@@ -227,7 +227,8 @@ function SearchBox<T extends string>({ | |||||
value={inputs[c.paramName]} | value={inputs[c.paramName]} | ||||
/> | /> | ||||
)} | )} | ||||
{c.type === "multi-select" && ( | |||||
{/* eslint-disable-next-line @typescript-eslint/no-unused-vars */} | |||||
{/* {c.type === "multi-select" && ( | |||||
<MultiSelect | <MultiSelect | ||||
label={t(c.label)} | label={t(c.label)} | ||||
options={c?.options} | options={c?.options} | ||||
@@ -235,7 +236,7 @@ function SearchBox<T extends string>({ | |||||
onChange={c.handleSelectionChange} | onChange={c.handleSelectionChange} | ||||
isReset={isReset} | isReset={isReset} | ||||
/> | /> | ||||
)} | |||||
)} */} | |||||
{c.type === "select" && ( | {c.type === "select" && ( | ||||
<FormControl fullWidth> | <FormControl fullWidth> | ||||
<InputLabel>{t(c.label)}</InputLabel> | <InputLabel>{t(c.label)}</InputLabel> | ||||
@@ -88,7 +88,7 @@ const UserSearch: React.FC<Props> = ({ users }) => { | |||||
<SearchResults<UserResult> | <SearchResults<UserResult> | ||||
items={filteredUser} | items={filteredUser} | ||||
columns={columns} | columns={columns} | ||||
pagingController={{ pageNum: 1, pageSize: 10, totalCount: 100 }} | |||||
pagingController={{ pageNum: 1, pageSize: 10 }} | |||||
/> | /> | ||||
</> | </> | ||||
); | ); | ||||
@@ -81,5 +81,7 @@ | |||||
"Job Qty": "工單數量", | "Job Qty": "工單數量", | ||||
"mat": "物料", | "mat": "物料", | ||||
"Product Count(s)": "產品數量", | "Product Count(s)": "產品數量", | ||||
"Schedule Period To": "排程期間至" | |||||
"Schedule Period To": "排程期間至", | |||||
"Overall": "總計", | |||||
"Back": "返回" | |||||
} | } |