Просмотр исходного кода

Merge branch 'master' of https://git.2fi-solutions.com/derek/FPSMS-frontend

# Conflicts:
#	src/app/api/settings/item/actions.ts
master
CANCERYS\kw093 1 месяц назад
Родитель
Сommit
202d17e89d
84 измененных файлов: 4860 добавлений и 2371 удалений
  1. +3
    -3
      .env.production
  2. +5
    -5
      next.config.js
  3. +19
    -0
      src/app/(main)/jo/edit/not-found.tsx
  4. +49
    -0
      src/app/(main)/jo/edit/page.tsx
  5. +35
    -0
      src/app/(main)/jo/page.tsx
  6. +0
    -10
      src/app/(main)/pickOrder/page.tsx
  7. +0
    -57
      src/app/(main)/scheduling/detail/edit/page.tsx
  8. +0
    -0
      src/app/(main)/scheduling/detailed/edit/not-found.tsx
  9. +57
    -0
      src/app/(main)/scheduling/detailed/edit/page.tsx
  10. +4
    -4
      src/app/(main)/scheduling/detailed/page.tsx
  11. +2
    -2
      src/app/(main)/scheduling/rough/edit/page.tsx
  12. +58
    -1
      src/app/api/jo/actions.ts
  13. +35
    -0
      src/app/api/jo/index.ts
  14. +46
    -2
      src/app/api/pickOrder/actions.ts
  15. +2
    -1
      src/app/api/pickOrder/index.ts
  16. +57
    -3
      src/app/api/scheduling/actions.ts
  17. +114
    -67
      src/app/api/scheduling/index.ts
  18. +29
    -1
      src/app/api/settings/item/actions.ts
  19. +2
    -0
      src/app/api/settings/item/index.ts
  20. +2
    -2
      src/app/utils/commonUtil.ts
  21. +31
    -0
      src/app/utils/formatUtil.ts
  22. +2
    -0
      src/components/Breadcrumb/Breadcrumb.tsx
  23. +0
    -1
      src/components/DetailSchedule/index.ts
  24. +0
    -37
      src/components/DetailScheduleDetail/DetailScheduleDetailWrapper.tsx
  25. +0
    -226
      src/components/DetailScheduleDetail/DetailScheudleDetailView.tsx
  26. +0
    -1319
      src/components/DetailScheduleDetail/ViewByBomDetails.tsx
  27. +4
    -1
      src/components/DetailScheduleDetail/ViewByFGDetails.tsx
  28. +0
    -1
      src/components/DetailScheduleDetail/index.ts
  29. +2
    -2
      src/components/DetailedSchedule/DetailedScheduleLoading.tsx
  30. +1
    -9
      src/components/DetailedSchedule/DetailedScheduleSearchView.tsx
  31. +6
    -6
      src/components/DetailedSchedule/DetailedScheduleWrapper.tsx
  32. +1
    -0
      src/components/DetailedSchedule/index.ts
  33. +29
    -14
      src/components/DetailedScheduleDetail/DetailInfoCard.tsx
  34. +272
    -0
      src/components/DetailedScheduleDetail/DetailedScheduleDetailView.tsx
  35. +44
    -0
      src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx
  36. +50
    -0
      src/components/DetailedScheduleDetail/ProdTimeColumn.tsx
  37. +1878
    -0
      src/components/DetailedScheduleDetail/TempRecords.tsx
  38. +240
    -0
      src/components/DetailedScheduleDetail/ViewByFGDetails.tsx
  39. +1
    -0
      src/components/DetailedScheduleDetail/index.ts
  40. +1
    -1
      src/components/DoSave/index.ts
  41. +1
    -1
      src/components/InventorySearch/InventorySearch.tsx
  42. +9
    -4
      src/components/ItemsSearch/ItemsSearch.tsx
  43. +84
    -0
      src/components/JoSave/InfoCard.tsx
  44. +107
    -0
      src/components/JoSave/JoSave.tsx
  45. +26
    -0
      src/components/JoSave/JoSaveWrapper.tsx
  46. +91
    -0
      src/components/JoSave/PickTable.tsx
  47. +1
    -0
      src/components/JoSave/index.ts
  48. +145
    -0
      src/components/JoSearch/JoSearch.tsx
  49. +21
    -0
      src/components/JoSearch/JoSearchWrapper.tsx
  50. +1
    -0
      src/components/JoSearch/index.ts
  51. +4
    -3
      src/components/LoginPage/LoginForm.tsx
  52. +2
    -2
      src/components/MailSetting/MailSetting.tsx
  53. +4
    -4
      src/components/MailSetting/TimesheetMailDetails.tsx
  54. +19
    -7
      src/components/NavigationContent/NavigationContent.tsx
  55. +3
    -3
      src/components/PickOrderDetail/ApprovalForm.tsx
  56. +50
    -41
      src/components/PickOrderDetail/PickOrderDetail.tsx
  57. +1
    -1
      src/components/PickOrderDetail/QcContent.tsx
  58. +4
    -4
      src/components/PickOrderDetail/QcForm.tsx
  59. +1
    -1
      src/components/PickOrderSearch/ConsolidatePickOrderItemSum.tsx
  60. +1
    -1
      src/components/PickOrderSearch/ConsolidatePickOrderSum.tsx
  61. +11
    -11
      src/components/PickOrderSearch/ConsolidatedPickOrders.tsx
  62. +318
    -0
      src/components/PickOrderSearch/CreateForm.tsx
  63. +98
    -0
      src/components/PickOrderSearch/CreatePickOrderModal.tsx
  64. +79
    -0
      src/components/PickOrderSearch/ItemSelect.tsx
  65. +74
    -13
      src/components/PickOrderSearch/PickOrderSearch.tsx
  66. +1
    -1
      src/components/PickOrderSearch/PickOrderSearchWrapper.tsx
  67. +27
    -20
      src/components/PickOrderSearch/PickOrders.tsx
  68. +73
    -0
      src/components/PickOrderSearch/UomSelect.tsx
  69. +1
    -1
      src/components/PoDetail/PoQcStockInModal.tsx
  70. +2
    -2
      src/components/PoDetail/PutawayForm.tsx
  71. +3
    -3
      src/components/PoDetail/QrModal.tsx
  72. +4
    -4
      src/components/PoSearch/PoSearch.tsx
  73. +1
    -1
      src/components/ProductionProcess/DefectsSection.tsx
  74. +10
    -10
      src/components/QrCodeScannerProvider/QrCodeScannerProvider.tsx
  75. +1
    -1
      src/components/RoughSchedule/RoughSchedileSearchView.tsx
  76. +2
    -2
      src/components/RoughScheduleDetail/RoughScheduleDetailWrapper.tsx
  77. +1
    -7
      src/components/RoughScheduleDetail/RoughScheudleDetailView.tsx
  78. +80
    -110
      src/components/RoughScheduleSetting/RoughScheduleSetting.tsx
  79. +6
    -5
      src/components/ScheduleTable/BomMaterialTable.tsx
  80. +402
    -327
      src/components/ScheduleTable/ScheduleTable.tsx
  81. +3
    -2
      src/components/SearchBox/MultiSelect.tsx
  82. +3
    -2
      src/components/SearchBox/SearchBox.tsx
  83. +1
    -1
      src/components/UserSearch/UserSearch.tsx
  84. +3
    -1
      src/i18n/zh/schedule.json

+ 3
- 3
.env.production Просмотреть файл

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

+ 5
- 5
next.config.js Просмотреть файл

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

+ 19
- 0
src/app/(main)/jo/edit/not-found.tsx Просмотреть файл

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

+ 49
- 0
src/app/(main)/jo/edit/page.tsx Просмотреть файл

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

+ 35
- 0
src/app/(main)/jo/page.tsx Просмотреть файл

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

+ 0
- 10
src/app/(main)/pickOrder/page.tsx Просмотреть файл

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


+ 0
- 57
src/app/(main)/scheduling/detail/edit/page.tsx Просмотреть файл

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

src/app/(main)/scheduling/detail/edit/not-found.tsx → src/app/(main)/scheduling/detailed/edit/not-found.tsx Просмотреть файл


+ 57
- 0
src/app/(main)/scheduling/detailed/edit/page.tsx Просмотреть файл

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

src/app/(main)/scheduling/detail/page.tsx → src/app/(main)/scheduling/detailed/page.tsx Просмотреть файл

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

+ 2
- 2
src/app/(main)/scheduling/rough/edit/page.tsx Просмотреть файл

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


+ 58
- 1
src/app/api/jo/actions.ts Просмотреть файл

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

+ 35
- 0
src/app/api/jo/index.ts Просмотреть файл

@@ -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"]
}
})
})

+ 46
- 2
src/app/api/pickOrder/actions.ts Просмотреть файл

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

+ 2
- 1
src/app/api/pickOrder/index.ts Просмотреть файл

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


+ 57
- 3
src/app/api/scheduling/actions.ts Просмотреть файл

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

+ 114
- 67
src/app/api/scheduling/index.ts Просмотреть файл

@@ -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"]
}
})
})

+ 29
- 1
src/app/api/settings/item/actions.ts Просмотреть файл

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

+ 2
- 0
src/app/api/settings/item/index.ts Просмотреть файл

@@ -35,6 +35,8 @@ export type ItemsResult = {
type: string;
qcChecks: ItemQc[];
action?: any;
fgName?: string;
excludeDate?: string;
};

export type Result = {


+ 2
- 2
src/app/utils/commonUtil.ts Просмотреть файл

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


+ 31
- 0
src/app/utils/formatUtil.ts Просмотреть файл

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


+ 2
- 0
src/components/Breadcrumb/Breadcrumb.tsx Просмотреть файл

@@ -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 = () => {


+ 0
- 1
src/components/DetailSchedule/index.ts Просмотреть файл

@@ -1 +0,0 @@
export { default } from "./DetailScheduleWrapper";

+ 0
- 37
src/components/DetailScheduleDetail/DetailScheduleDetailWrapper.tsx Просмотреть файл

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

+ 0
- 226
src/components/DetailScheduleDetail/DetailScheudleDetailView.tsx Просмотреть файл

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

+ 0
- 1319
src/components/DetailScheduleDetail/ViewByBomDetails.tsx
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 4
- 1
src/components/DetailScheduleDetail/ViewByFGDetails.tsx Просмотреть файл

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


+ 0
- 1
src/components/DetailScheduleDetail/index.ts Просмотреть файл

@@ -1 +0,0 @@
export { default } from "./DetailScheduleDetailWrapper";

src/components/DetailSchedule/DetailScheduleLoading.tsx → src/components/DetailedSchedule/DetailedScheduleLoading.tsx Просмотреть файл

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

src/components/DetailSchedule/DetailScheduleSearchView.tsx → src/components/DetailedSchedule/DetailedScheduleSearchView.tsx Просмотреть файл

@@ -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>[]>(

src/components/DetailSchedule/DetailScheduleWrapper.tsx → src/components/DetailedSchedule/DetailedScheduleWrapper.tsx Просмотреть файл

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

+ 1
- 0
src/components/DetailedSchedule/index.ts Просмотреть файл

@@ -0,0 +1 @@
export { default } from "./DetailedScheduleWrapper";

src/components/DetailScheduleDetail/DetailInfoCard.tsx → src/components/DetailedScheduleDetail/DetailInfoCard.tsx Просмотреть файл

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

+ 272
- 0
src/components/DetailedScheduleDetail/DetailedScheduleDetailView.tsx Просмотреть файл

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

+ 44
- 0
src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx Просмотреть файл

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

+ 50
- 0
src/components/DetailedScheduleDetail/ProdTimeColumn.tsx Просмотреть файл

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

+ 1878
- 0
src/components/DetailedScheduleDetail/TempRecords.tsx
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 240
- 0
src/components/DetailedScheduleDetail/ViewByFGDetails.tsx Просмотреть файл

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

+ 1
- 0
src/components/DetailedScheduleDetail/index.ts Просмотреть файл

@@ -0,0 +1 @@
export { default } from "./DetailedScheduleDetailWrapper";

+ 1
- 1
src/components/DoSave/index.ts Просмотреть файл

@@ -1 +1 @@
export default from "./DoSaveWrapper"
// export default from "./DoSaveWrapper"

+ 1
- 1
src/components/InventorySearch/InventorySearch.tsx Просмотреть файл

@@ -153,7 +153,7 @@ const InventorySearch: React.FC<Props> = ({ inventories }) => {
pagingController={{
pageNum: 0,
pageSize: 0,
totalCount: 0,
// totalCount: 0,
}}
/>
</>


+ 9
- 4
src/components/ItemsSearch/ItemsSearch.tsx Просмотреть файл

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


+ 84
- 0
src/components/JoSave/InfoCard.tsx Просмотреть файл

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

+ 107
- 0
src/components/JoSave/JoSave.tsx Просмотреть файл

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

+ 26
- 0
src/components/JoSave/JoSaveWrapper.tsx Просмотреть файл

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

+ 91
- 0
src/components/JoSave/PickTable.tsx Просмотреть файл

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

+ 1
- 0
src/components/JoSave/index.ts Просмотреть файл

@@ -0,0 +1 @@
export { default } from "./JoSaveWrapper";

+ 145
- 0
src/components/JoSearch/JoSearch.tsx Просмотреть файл

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

+ 21
- 0
src/components/JoSearch/JoSearchWrapper.tsx Просмотреть файл

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

+ 1
- 0
src/components/JoSearch/index.ts Просмотреть файл

@@ -0,0 +1 @@
export { default } from "./JoSearchWrapper"

+ 4
- 3
src/components/LoginPage/LoginForm.tsx Просмотреть файл

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


+ 2
- 2
src/components/MailSetting/MailSetting.tsx Просмотреть файл

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


+ 4
- 4
src/components/MailSetting/TimesheetMailDetails.tsx Просмотреть файл

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


+ 19
- 7
src/components/NavigationContent/NavigationContent.tsx Просмотреть файл

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


+ 3
- 3
src/components/PickOrderDetail/ApprovalForm.tsx Просмотреть файл

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


+ 50
- 41
src/components/PickOrderDetail/PickOrderDetail.tsx Просмотреть файл

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


+ 1
- 1
src/components/PickOrderDetail/QcContent.tsx Просмотреть файл

@@ -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
- 4
src/components/PickOrderDetail/QcForm.tsx Просмотреть файл

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


+ 1
- 1
src/components/PickOrderSearch/ConsolidatePickOrderItemSum.tsx Просмотреть файл

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


+ 1
- 1
src/components/PickOrderSearch/ConsolidatePickOrderSum.tsx Просмотреть файл

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


+ 11
- 11
src/components/PickOrderSearch/ConsolidatedPickOrders.tsx Просмотреть файл

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


+ 318
- 0
src/components/PickOrderSearch/CreateForm.tsx Просмотреть файл

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

+ 98
- 0
src/components/PickOrderSearch/CreatePickOrderModal.tsx Просмотреть файл

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

+ 79
- 0
src/components/PickOrderSearch/ItemSelect.tsx Просмотреть файл

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

+ 74
- 13
src/components/PickOrderSearch/PickOrderSearch.tsx Просмотреть файл

@@ -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
- 1
src/components/PickOrderSearch/PickOrderSearchWrapper.tsx Просмотреть файл

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



+ 27
- 20
src/components/PickOrderSearch/PickOrders.tsx Просмотреть файл

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


+ 73
- 0
src/components/PickOrderSearch/UomSelect.tsx Просмотреть файл

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

+ 1
- 1
src/components/PoDetail/PoQcStockInModal.tsx Просмотреть файл

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


+ 2
- 2
src/components/PoDetail/PutawayForm.tsx Просмотреть файл

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


+ 3
- 3
src/components/PoDetail/QrModal.tsx Просмотреть файл

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


+ 4
- 4
src/components/PoSearch/PoSearch.tsx Просмотреть файл

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


+ 1
- 1
src/components/ProductionProcess/DefectsSection.tsx Просмотреть файл

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


+ 10
- 10
src/components/QrCodeScannerProvider/QrCodeScannerProvider.tsx Просмотреть файл

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


+ 1
- 1
src/components/RoughSchedule/RoughSchedileSearchView.tsx Просмотреть файл

@@ -245,7 +245,7 @@ const RSOverview: React.FC<Props> = ({ type, defaultInputs }) => {
// setFilteredSchedules(items ?? []);
// setFilterObj({});
// setTempSelectedValue({});
refetchData(inputs, "reset");
refetchData(defaultInputs, "reset");
}, []);

return (


+ 2
- 2
src/components/RoughScheduleDetail/RoughScheduleDetailWrapper.tsx Просмотреть файл

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


+ 1
- 7
src/components/RoughScheduleDetail/RoughScheudleDetailView.tsx Просмотреть файл

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


+ 80
- 110
src/components/RoughScheduleSetting/RoughScheduleSetting.tsx Просмотреть файл

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


+ 6
- 5
src/components/ScheduleTable/BomMaterialTable.tsx Просмотреть файл

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


+ 402
- 327
src/components/ScheduleTable/ScheduleTable.tsx Просмотреть файл

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

+ 3
- 2
src/components/SearchBox/MultiSelect.tsx Просмотреть файл

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


+ 3
- 2
src/components/SearchBox/SearchBox.tsx Просмотреть файл

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


+ 1
- 1
src/components/UserSearch/UserSearch.tsx Просмотреть файл

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


+ 3
- 1
src/i18n/zh/schedule.json Просмотреть файл

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

Загрузка…
Отмена
Сохранить