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