# Conflicts: # src/app/api/pickOrder/index.ts # src/components/PickOrderSearch/PickOrderSearch.tsxmaster
| @@ -55,8 +55,8 @@ export default async function MainLayout({ | |||
| <Stack spacing={2}> | |||
| <I18nProvider namespaces={["common"]}> | |||
| <Breadcrumb /> | |||
| {children} | |||
| </I18nProvider> | |||
| {children} | |||
| </Stack> | |||
| </Box> | |||
| </> | |||
| @@ -0,0 +1,30 @@ | |||
| import { PreloadPickOrder } from "@/app/api/pickorder"; | |||
| import { SearchParams } from "@/app/utils/fetchUtil"; | |||
| import PickOrderDetail from "@/components/PickOrderDetail"; | |||
| import { getServerI18n, I18nProvider } from "@/i18n"; | |||
| import { Stack, Typography } from "@mui/material"; | |||
| import { Metadata } from "next"; | |||
| import { Suspense } from "react"; | |||
| export const metadata: Metadata = { | |||
| title: "Consolidated Pick Order Flow", | |||
| }; | |||
| type Props = {} & SearchParams; | |||
| const PickOrder: React.FC<Props> = async ({ searchParams }) => { | |||
| const { t } = await getServerI18n("pickOrder"); | |||
| PreloadPickOrder(); | |||
| return ( | |||
| <> | |||
| <I18nProvider namespaces={["pickOrder"]}> | |||
| <Suspense fallback={<PickOrderDetail.Loading />}> | |||
| <PickOrderDetail consoCode={`${searchParams["consoCode"]}`}/> | |||
| </Suspense> | |||
| </I18nProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| export default PickOrder; | |||
| @@ -1,4 +1,4 @@ | |||
| import { PreloadPickOrder } from "@/app/api/pickOrder"; | |||
| import { PreloadPickOrder } from "@/app/api/pickorder"; | |||
| import PickOrderSearch from "@/components/PickOrderSearch"; | |||
| import { getServerI18n } from "@/i18n"; | |||
| import { I18nProvider } from "@/i18n"; | |||
| @@ -0,0 +1,23 @@ | |||
| "use server"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| // import { ServerFetchError, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||
| import { revalidateTag } from "next/cache"; | |||
| import { cache } from "react"; | |||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { QcItemResult } from "../settings/qcItem"; | |||
| import { RecordsRes } from "../utils"; | |||
| // import { BASE_API_URL } from "@/config/api"; | |||
| export interface LotLineInfo { | |||
| inventoryLotLineId: number, | |||
| lotNo: string, | |||
| remainingQty: number, | |||
| uom: string | |||
| } | |||
| export const fetchLotDetail = cache(async (stockInLineId: number) => { | |||
| return serverFetchJson<LotLineInfo>(`${BASE_API_URL}/inventoryLotLine/lot-detail/${stockInLineId}`, { | |||
| method: 'GET', | |||
| next: { tags: ["inventory"] }, | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,101 @@ | |||
| "use server"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| // import { ServerFetchError, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||
| import { revalidateTag } from "next/cache"; | |||
| import { cache } from "react"; | |||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { QcItemResult } from "../settings/qcItem"; | |||
| import { RecordsRes } from "../utils"; | |||
| import { ConsoPickOrderResult, PickOrderLineWithSuggestedLot, PickOrderResult, PreReleasePickOrderSummary } from "."; | |||
| // import { BASE_API_URL } from "@/config/api"; | |||
| export interface ReleasePickOrderInputs { | |||
| consoCode: string | |||
| assignTo: number, | |||
| } | |||
| export const consolidatePickOrder = async (ids: number[]) => { | |||
| const pickOrder = await serverFetchJson<any>(`${BASE_API_URL}/pickOrder/conso`, { | |||
| method: "POST", | |||
| body: JSON.stringify({ ids: ids }), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| // revalidateTag("po"); | |||
| return pickOrder | |||
| } | |||
| export const consolidatePickOrder_revert = async (ids: number[]) => { | |||
| const pickOrder = await serverFetchJson<any>(`${BASE_API_URL}/pickOrder/deconso`, { | |||
| method: "POST", | |||
| body: JSON.stringify({ ids: ids }), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| // revalidateTag("po"); | |||
| return pickOrder | |||
| } | |||
| export const fetchPickOrderClient = cache(async (queryParams?: Record<string, any>) => { | |||
| if (queryParams) { | |||
| const queryString = new URLSearchParams(queryParams).toString(); | |||
| return serverFetchJson<RecordsRes<PickOrderResult[]>>(`${BASE_API_URL}/pickOrder/getRecordByPage?${queryString}`, { | |||
| method: 'GET', | |||
| next: { tags: ["pickorder"] }, | |||
| }); | |||
| } else { | |||
| return serverFetchJson<RecordsRes<PickOrderResult[]>>(`${BASE_API_URL}/pickOrder/getRecordByPage`, { | |||
| method: 'GET', | |||
| next: { tags: ["pickorder"] }, | |||
| }); | |||
| } | |||
| }); | |||
| export const fetchConsoPickOrderClient = cache(async (queryParams?: Record<string, any>) => { | |||
| if (queryParams) { | |||
| const queryString = new URLSearchParams(queryParams).toString(); | |||
| return serverFetchJson<RecordsRes<ConsoPickOrderResult[]>>(`${BASE_API_URL}/pickOrder/getRecordByPage-conso?${queryString}`, { | |||
| method: 'GET', | |||
| next: { tags: ["pickorder"] }, | |||
| }); | |||
| } else { | |||
| return serverFetchJson<RecordsRes<ConsoPickOrderResult[]>>(`${BASE_API_URL}/pickOrder/getRecordByPage-conso`, { | |||
| method: 'GET', | |||
| next: { tags: ["pickorder"] }, | |||
| }); | |||
| } | |||
| }); | |||
| export const fetchPickOrderLineClient = cache(async (queryParams?: Record<string, any>) => { | |||
| if (queryParams) { | |||
| const queryString = new URLSearchParams(queryParams).toString(); | |||
| return serverFetchJson<RecordsRes<PickOrderLineWithSuggestedLot[]>>(`${BASE_API_URL}/pickOrder/get-pickorder-line-byPage?${queryString}`, { | |||
| method: 'GET', | |||
| next: { tags: ["pickorder"] }, | |||
| }); | |||
| } else { | |||
| return serverFetchJson<RecordsRes<PickOrderLineWithSuggestedLot[]>>(`${BASE_API_URL}/pickOrder/get-pickorder-line-byPage`, { | |||
| method: 'GET', | |||
| next: { tags: ["pickorder"] }, | |||
| }); | |||
| } | |||
| }); | |||
| export const fetchConsoDetail = cache(async (consoCode: string) => { | |||
| return serverFetchJson<PreReleasePickOrderSummary>(`${BASE_API_URL}/pickOrder/pre-release-info/${consoCode}`, { | |||
| method: 'GET', | |||
| next: { tags: ["pickorder"] }, | |||
| }); | |||
| }); | |||
| export const releasePickOrder = async (data: ReleasePickOrderInputs) => { | |||
| console.log(data) | |||
| console.log(JSON.stringify(data)) | |||
| const po = await serverFetchJson<any>(`${BASE_API_URL}/pickOrder/releaseConso`, { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| revalidateTag("pickorder"); | |||
| return po | |||
| } | |||
| @@ -1,8 +1,6 @@ | |||
| import "server-only"; | |||
| // import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| // import { BASE_API_URL } from "@/config/api"; | |||
| import { serverFetchJson } from "../../utils/fetchUtil"; | |||
| import { BASE_API_URL } from "../../../config/api"; | |||
| import { Pageable, serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| import { cache } from "react"; | |||
| interface PickOrderItemInfo { | |||
| @@ -20,14 +18,77 @@ export interface PickOrderResult{ | |||
| status: string, | |||
| releasedBy: string, | |||
| items?: PickOrderItemInfo[] | null, | |||
| pickOrderLine?: PickOrderLine[] | |||
| } | |||
| export interface PickOrderLine { | |||
| id: number, | |||
| itemId: number, | |||
| itemCode: string, | |||
| itemName: string, | |||
| availableQty: number, | |||
| requiredQty: number, | |||
| uomCode: string, | |||
| uomDesc: string | |||
| } | |||
| export interface ConsoPickOrderResult{ | |||
| id: number, | |||
| code: string, | |||
| consoCode?: string, | |||
| targetDate: number[], | |||
| completeDate?: number[], | |||
| type: string, | |||
| status: string, | |||
| releasedBy: string, | |||
| items?: PickOrderItemInfo[] | null, | |||
| } | |||
| export interface FetchPickOrders extends Pageable { | |||
| code: string | undefined | |||
| targetDateFrom: string | undefined | |||
| targetDateTo: string | undefined | |||
| type: string | undefined | |||
| status: string | undefined | |||
| itemName: string | undefined | |||
| } | |||
| export type ByItemsSummary = { | |||
| id: number, | |||
| code: string, | |||
| name: string, | |||
| uomDesc: string, | |||
| availableQty: number, | |||
| requiredQty: number, | |||
| } | |||
| export interface PreReleasePickOrderSummary { | |||
| consoCode: string | |||
| pickOrders: Omit<PickOrderResult, "items">[] | |||
| items: ByItemsSummary[] | |||
| } | |||
| export interface PickOrderLineWithSuggestedLot { | |||
| id: number, | |||
| itemName: string, | |||
| qty: number, | |||
| uom: string | |||
| status: string | |||
| warehouse: string | |||
| suggestedLotNo: string | |||
| } | |||
| export const PreloadPickOrder = () => { | |||
| fetchPickOrders() | |||
| fetchPickOrders({ | |||
| code: undefined, | |||
| targetDateFrom: undefined, | |||
| targetDateTo: undefined, | |||
| type: undefined, | |||
| status: undefined, | |||
| itemName: undefined, | |||
| }) | |||
| } | |||
| export const fetchPickOrders = cache(async () => { | |||
| return serverFetchJson<PickOrderResult[]>(`${BASE_API_URL}/pickOrder/list`, { | |||
| export const fetchPickOrders = cache(async (queryParams: FetchPickOrders) => { | |||
| const queryString = new URLSearchParams(queryParams as Record<string, any>).toString(); | |||
| return serverFetchJson<PickOrderResult[]>(`${BASE_API_URL}/pickOrder/list?${queryString}`, { | |||
| next: { | |||
| tags: ["pickOrders"] | |||
| } | |||
| @@ -6,8 +6,10 @@ import { serverFetchJson } from "../../utils/fetchUtil"; | |||
| import { BASE_API_URL } from "../../../config/api"; | |||
| export interface QrCodeInfo { | |||
| stockInLineId?: number; | |||
| itemId: number | |||
| warehouseId?: number | |||
| lotNo?: string | |||
| } | |||
| // warehouse qrcode | |||
| warehouseId?: number | |||
| // item qrcode | |||
| stockInLineId?: number; | |||
| itemId: number | |||
| lotNo?: string | |||
| } | |||
| @@ -22,12 +22,23 @@ export interface PasswordInputs { | |||
| newPasswordCheck: string; | |||
| } | |||
| export interface NameList { | |||
| id: number | |||
| name: string | |||
| } | |||
| export const fetchUserDetails = cache(async (id: number) => { | |||
| return serverFetchJson<UserDetail>(`${BASE_API_URL}/user/${id}`, { | |||
| next: { tags: ["user"] }, | |||
| }); | |||
| }); | |||
| export const fetchNameList = cache(async () => { | |||
| return serverFetchJson<NameList[]>(`${BASE_API_URL}/user/name-list`, { | |||
| next: { tags: ["user"] }, | |||
| }); | |||
| }); | |||
| export const editUser = async (id: number, data: UserInputs) => { | |||
| const newUser = serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, { | |||
| method: "PUT", | |||
| @@ -3,6 +3,11 @@ import { getServerSession } from "next-auth"; | |||
| import { headers } from "next/headers"; | |||
| import { redirect } from "next/navigation"; | |||
| export interface Pageable { | |||
| pageSize?: number | |||
| pageNum?: number | |||
| } | |||
| export type SearchParams = { | |||
| searchParams: { [key: string]: string | string[] | undefined }; | |||
| } | |||
| @@ -67,6 +67,13 @@ export const stockInLineStatusMap: { [status: string]: number } = { | |||
| "rejected": 9, | |||
| }; | |||
| export const pickOrderStatusMap: { [status: string]: number } = { | |||
| "pending": 1, | |||
| "consolidated": 2, | |||
| "released": 3, | |||
| "completed": 4, | |||
| }; | |||
| export const calculateWeight = (qty: number, uom: Uom) => { | |||
| return qty * (uom.unit2Qty || 1) * (uom.unit3Qty || 1) * (uom.unit4Qty || 1); | |||
| } | |||
| @@ -24,6 +24,7 @@ const pathToLabelMap: { [path: string]: string } = { | |||
| "/do": "Delivery Order", | |||
| "/pickOrder": "Pick Order", | |||
| "/po": "Purchase Order", | |||
| "/dashboard": "dashboard", | |||
| }; | |||
| const Breadcrumb = () => { | |||
| @@ -52,7 +52,7 @@ const NavigationContent: React.FC = () => { | |||
| { | |||
| icon: <RequestQuote />, | |||
| label: "Pick Order", | |||
| path: "/pickOrder", | |||
| path: "/pickorder", | |||
| }, | |||
| // { | |||
| // icon: <RequestQuote />, | |||
| @@ -0,0 +1,314 @@ | |||
| "use client"; | |||
| import { | |||
| Button, | |||
| ButtonProps, | |||
| Card, | |||
| CardContent, | |||
| CardHeader, | |||
| CircularProgress, | |||
| Grid, | |||
| Stack, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import StyledDataGrid from "../StyledDataGrid"; | |||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { GridColDef } 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 { fetchPickOrderLineClient } from "@/app/api/pickorder/actions"; | |||
| import { PickOrderLineWithSuggestedLot } from "@/app/api/pickorder"; | |||
| import { Pageable } from "@/app/utils/fetchUtil"; | |||
| import { QrCodeInfo } from "@/app/api/qrcode"; | |||
| import { QrCode } from "../QrCode"; | |||
| import { fetchLotDetail, LotLineInfo } from "@/app/api/inventory/actions"; | |||
| import { GridRowModesModel } from "@mui/x-data-grid"; | |||
| interface Props { | |||
| consoCode: string; | |||
| } | |||
| interface IsLoadingModel { | |||
| pickOrderLineTable: boolean; | |||
| stockOutLineTable: boolean; | |||
| } | |||
| const PickOrderDetail: React.FC<Props> = ({ consoCode }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| const [selectedRow, setSelectRow] = useState<GridRowSelectionModel>(); | |||
| const [isLoadingModel, setIsLoadingModel] = useState<IsLoadingModel>({ | |||
| pickOrderLineTable: false, | |||
| stockOutLineTable: false, | |||
| }); | |||
| const [polCriteriaArgs, setPolCriteriaArgs] = useState<Pageable>({ | |||
| pageNum: 1, | |||
| pageSize: 10, | |||
| }); | |||
| const [solCriteriaArgs, setSolCriteriaArgs] = useState<Pageable>({ | |||
| pageNum: 1, | |||
| pageSize: 10, | |||
| }); | |||
| const [polTotalCount, setPolTotalCount] = useState(0); | |||
| const [solTotalCount, setSolTotalCount] = useState(0); | |||
| const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | |||
| const [pickOrderLine, setPickOrderLine] = useState< | |||
| PickOrderLineWithSuggestedLot[] | |||
| >([]); | |||
| const pickOrderLineColumns = useMemo<GridColDef[]>( | |||
| () => [ | |||
| { | |||
| field: "id", | |||
| headerName: "pickOrderLineId", | |||
| flex: 1, | |||
| }, | |||
| { | |||
| field: "itemName", | |||
| headerName: "itemId", | |||
| flex: 1, | |||
| }, | |||
| { | |||
| field: "qty", | |||
| headerName: "qty", | |||
| flex: 1, | |||
| }, | |||
| { | |||
| field: "uom", | |||
| headerName: "uom", | |||
| flex: 1, | |||
| }, | |||
| { | |||
| field: "warehouse", | |||
| headerName: "location", | |||
| flex: 1, | |||
| }, | |||
| { | |||
| field: "suggestedLotNo", | |||
| headerName: "suggestedLotNo", | |||
| flex: 1.2, | |||
| }, | |||
| ], | |||
| [] | |||
| ); | |||
| const [stockOutLine, setStockOutLine] = useState([]); | |||
| const stockOutLineColumns = useMemo<GridColDef[]>( | |||
| () => [ | |||
| { | |||
| field: "code", | |||
| headerName: "actual lot (out line", | |||
| flex: 1, | |||
| }, | |||
| ], | |||
| [] | |||
| ); | |||
| const handleStartPickOrder = useCallback(async () => {}, []); | |||
| const handleCompletePickOrder = useCallback(async () => {}, []); | |||
| useEffect(() => { | |||
| console.log(selectedRow); | |||
| }, [selectedRow]); | |||
| 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 PO"), | |||
| buttonIcon: <DoneIcon />, | |||
| buttonColor: "info", | |||
| disabled: true, | |||
| }), | |||
| [] | |||
| ); | |||
| const [isOpenScanner, setOpenScanner] = useState(false); | |||
| const onOpenScanner = useCallback(() => { | |||
| setOpenScanner((prev) => !prev); | |||
| }, []); | |||
| const fetchPickOrderLine = useCallback( | |||
| async (params: Record<string, any>) => { | |||
| setIsLoadingModel((prev) => ({ | |||
| ...prev, | |||
| pickOrderLineTable: true, | |||
| })); | |||
| const res = await fetchPickOrderLineClient({ | |||
| ...params, | |||
| consoCode: consoCode, | |||
| }); | |||
| if (res) { | |||
| console.log(res); | |||
| setPickOrderLine(res.records); | |||
| setPolTotalCount(res.total); | |||
| } else { | |||
| console.log("error"); | |||
| console.log(res); | |||
| } | |||
| setIsLoadingModel((prev) => ({ | |||
| ...prev, | |||
| pickOrderLineTable: false, | |||
| })); | |||
| }, | |||
| [fetchPickOrderLineClient, consoCode] | |||
| ); | |||
| const fetchStockOutLine = useCallback( | |||
| async (params: Record<string, any>) => {}, | |||
| [] | |||
| ); | |||
| useEffect(() => { | |||
| fetchPickOrderLine(polCriteriaArgs); | |||
| }, [polCriteriaArgs]); | |||
| useEffect(() => { | |||
| fetchStockOutLine(solCriteriaArgs); | |||
| }, [solCriteriaArgs]); | |||
| const getLotDetail = useCallback( | |||
| async (stockInLineId: number): Promise<LotLineInfo> => { | |||
| const res = await fetchLotDetail(stockInLineId); | |||
| return res; | |||
| }, | |||
| [fetchLotDetail] | |||
| ); | |||
| const scanner = useQcCodeScanner(); | |||
| useEffect(() => { | |||
| if (isOpenScanner && !scanner.isScanning) { | |||
| scanner.startScan(); | |||
| } else if (!isOpenScanner && scanner.isScanning) { | |||
| scanner.stopScan(); | |||
| } | |||
| }, [isOpenScanner]); | |||
| useEffect(() => { | |||
| if (scanner.values.length > 0) { | |||
| console.log(scanner.values[0]); | |||
| const data: QrCodeInfo = JSON.parse(scanner.values[0]); | |||
| console.log(data); | |||
| if (data.stockInLineId) { | |||
| console.log("still got in"); | |||
| console.log(data.stockInLineId); | |||
| // fetch | |||
| getLotDetail(data.stockInLineId).then((value) => {}); | |||
| } | |||
| scanner.resetScan(); | |||
| } | |||
| }, [scanner.values]); | |||
| const homemade_Qrcode = { | |||
| stockInLineId: 156, | |||
| }; | |||
| return ( | |||
| <> | |||
| <Stack spacing={2}> | |||
| <Grid container xs={12} justifyContent="start"> | |||
| <Grid item xs={12}> | |||
| <Typography variant="h4" marginInlineEnd={2}> | |||
| {consoCode} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={8}> | |||
| <Button | |||
| // onClick={buttonData.onClick} | |||
| disabled={buttonData.disabled} | |||
| color={buttonData.buttonColor as ButtonProps["color"]} | |||
| startIcon={buttonData.buttonIcon} | |||
| > | |||
| {buttonData.buttonText} | |||
| </Button> | |||
| </Grid> | |||
| <Grid | |||
| item | |||
| xs={4} | |||
| display="flex" | |||
| justifyContent="end" | |||
| alignItems="end" | |||
| > | |||
| <Button onClick={onOpenScanner}> | |||
| {isOpenScanner ? t("binding") : t("bind")} | |||
| </Button> | |||
| </Grid> | |||
| {/* homemade qrcode for testing purpose */} | |||
| {/* <Grid | |||
| item | |||
| xs={12} | |||
| style={{ display: "flex", justifyContent: "center" }} | |||
| > | |||
| <QrCode | |||
| content={homemade_Qrcode} | |||
| sx={{ width: 200, height: 200 }} | |||
| /> | |||
| </Grid> */} | |||
| </Grid> | |||
| <Grid container xs={12} justifyContent="space-between"> | |||
| {/* <Grid item xs={12} sx={{ height: 400 }}> | |||
| <StyledDataGrid rows={pickOrderLine} columns={columns} /> | |||
| </Grid> */} | |||
| <Grid item xs={12} sx={{ height: 400 }}> | |||
| {isLoadingModel.pickOrderLineTable ? ( | |||
| <CircularProgress size={40} /> | |||
| ) : ( | |||
| <StyledDataGrid | |||
| rows={pickOrderLine} | |||
| columns={pickOrderLineColumns} | |||
| rowSelectionModel={selectedRow} | |||
| onRowSelectionModelChange={(newRowSelectionModel) => { | |||
| setSelectRow(newRowSelectionModel); | |||
| }} | |||
| initialState={{ | |||
| pagination: { | |||
| paginationModel: { pageSize: 10, page: 0 }, | |||
| }, | |||
| }} | |||
| pageSizeOptions={[10, 25, 50, 100]} | |||
| onPaginationModelChange={async (model, details) => { | |||
| setPolCriteriaArgs({ | |||
| pageNum: model.page + 1, | |||
| pageSize: model.pageSize, | |||
| }); | |||
| }} | |||
| rowCount={polTotalCount} | |||
| /> | |||
| )} | |||
| </Grid> | |||
| <Grid item xs={12} sx={{ height: 400 }}> | |||
| <StyledDataGrid | |||
| rows={stockOutLine} | |||
| columns={stockOutLineColumns} | |||
| rowModesModel={rowModesModel} | |||
| onRowModesModelChange={setRowModesModel} | |||
| disableColumnMenu | |||
| editMode="row" | |||
| // processRowUpdate={processRowUpdate} | |||
| // onProcessRowUpdateError={onProcessRowUpdateError} | |||
| initialState={{ | |||
| pagination: { | |||
| paginationModel: { pageSize: 10, page: 0 }, | |||
| }, | |||
| }} | |||
| pageSizeOptions={[10, 25, 50, 100]} | |||
| onPaginationModelChange={async (model, details) => { | |||
| setSolCriteriaArgs({ | |||
| pageNum: model.page + 1, | |||
| pageSize: model.pageSize, | |||
| }); | |||
| }} | |||
| rowCount={solTotalCount} | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| </Stack> | |||
| </> | |||
| ); | |||
| }; | |||
| export default PickOrderDetail; | |||
| @@ -0,0 +1,40 @@ | |||
| import Card from "@mui/material/Card"; | |||
| import CardContent from "@mui/material/CardContent"; | |||
| import Skeleton from "@mui/material/Skeleton"; | |||
| import Stack from "@mui/material/Stack"; | |||
| import React from "react"; | |||
| // Can make this nicer | |||
| export const PickOrderDetailLoading: React.FC = () => { | |||
| return ( | |||
| <> | |||
| <Card> | |||
| <CardContent> | |||
| <Stack spacing={2}> | |||
| <Skeleton variant="rounded" height={60} /> | |||
| <Skeleton variant="rounded" height={60} /> | |||
| <Skeleton variant="rounded" height={60} /> | |||
| <Skeleton | |||
| variant="rounded" | |||
| height={50} | |||
| width={100} | |||
| sx={{ alignSelf: "flex-end" }} | |||
| /> | |||
| </Stack> | |||
| </CardContent> | |||
| </Card> | |||
| <Card> | |||
| <CardContent> | |||
| <Stack spacing={2}> | |||
| <Skeleton variant="rounded" height={40} /> | |||
| <Skeleton variant="rounded" height={40} /> | |||
| <Skeleton variant="rounded" height={40} /> | |||
| <Skeleton variant="rounded" height={40} /> | |||
| </Stack> | |||
| </CardContent> | |||
| </Card> | |||
| </> | |||
| ); | |||
| }; | |||
| export default PickOrderDetailLoading; | |||
| @@ -0,0 +1,35 @@ | |||
| import { fetchAllItems } from "@/app/api/settings/item"; | |||
| // import ItemsSearch from "./ItemsSearch"; | |||
| // import ItemsSearchLoading from "./ItemsSearchLoading"; | |||
| import { SearchParams } from "@/app/utils/fetchUtil"; | |||
| import { TypeEnum } from "@/app/utils/typeEnum"; | |||
| import { notFound } from "next/navigation"; | |||
| import { fetchPoWithStockInLines, PoResult } from "@/app/api/po"; | |||
| import { QcItemWithChecks } from "@/app/api/qc"; | |||
| import { fetchWarehouseList } from "@/app/api/warehouse"; | |||
| import { fetchQcItemCheck } from "@/app/api/qc/actions"; | |||
| import PickOrderDetail from "./PickOrderDetail"; | |||
| import PickOrderDetailLoading from "./PickOrderDetailLoading"; | |||
| interface SubComponents { | |||
| Loading: typeof PickOrderDetailLoading; | |||
| } | |||
| type Props = { | |||
| consoCode: string; | |||
| }; | |||
| const PoDetailWrapper: React.FC<Props> & SubComponents = async ({ consoCode }) => { | |||
| // const [poWithStockInLine, warehouse, qc] = await Promise.all([ | |||
| // fetchPoWithStockInLines(id), | |||
| // fetchWarehouseList(), | |||
| // fetchQcItemCheck(), | |||
| // ]); | |||
| // const poWithStockInLine = await fetchPoWithStockInLines(id) | |||
| return <PickOrderDetail consoCode={consoCode}/>; | |||
| }; | |||
| PoDetailWrapper.Loading = PickOrderDetailLoading; | |||
| export default PoDetailWrapper; | |||
| @@ -0,0 +1 @@ | |||
| export { default } from "./PickOrderDetailWrapper" | |||
| @@ -0,0 +1,91 @@ | |||
| "use client"; | |||
| import dayjs from "dayjs"; | |||
| import arraySupport from "dayjs/plugin/arraySupport"; | |||
| import StyledDataGrid from "../StyledDataGrid"; | |||
| import { | |||
| Dispatch, | |||
| SetStateAction, | |||
| useCallback, | |||
| useEffect, | |||
| useMemo, | |||
| useState, | |||
| } from "react"; | |||
| import { GridColDef } from "@mui/x-data-grid"; | |||
| import { CircularProgress, Grid, Typography } from "@mui/material"; | |||
| import { ByItemsSummary } from "@/app/api/pickorder"; | |||
| import { useTranslation } from "react-i18next"; | |||
| dayjs.extend(arraySupport); | |||
| interface Props { | |||
| rows: ByItemsSummary[] | undefined; | |||
| setRows: Dispatch<SetStateAction<ByItemsSummary[] | undefined>>; | |||
| } | |||
| const ConsolidatePickOrderItemSum: React.FC<Props> = ({ rows, setRows }) => { | |||
| console.log(rows); | |||
| const { t } = useTranslation("pickOrder"); | |||
| const columns = useMemo<GridColDef[]>( | |||
| () => [ | |||
| { | |||
| field: "name", | |||
| headerName: "name", | |||
| flex: 1, | |||
| renderCell: (params) => { | |||
| console.log(params.row.name); | |||
| return params.row.name; | |||
| }, | |||
| }, | |||
| { | |||
| field: "requiredQty", | |||
| headerName: "requiredQty", | |||
| flex: 1, | |||
| renderCell: (params) => { | |||
| console.log(params.row.requiredQty); | |||
| const requiredQty = params.row.requiredQty ?? 0; | |||
| return `${requiredQty} ${params.row.uomDesc}`; | |||
| }, | |||
| }, | |||
| { | |||
| field: "availableQty", | |||
| headerName: "availableQty", | |||
| flex: 1, | |||
| renderCell: (params) => { | |||
| console.log(params.row.availableQty); | |||
| const availableQty = params.row.availableQty ?? 0; | |||
| return `${availableQty} ${params.row.uomDesc}`; | |||
| }, | |||
| }, | |||
| ], | |||
| [] | |||
| ); | |||
| return ( | |||
| <Grid | |||
| container | |||
| rowGap={1} | |||
| // direction="column" | |||
| alignItems="center" | |||
| justifyContent="center" | |||
| > | |||
| <Grid item xs={12}> | |||
| <Typography variant="h5" marginInlineEnd={2}> | |||
| {t("Items Included")} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| {!rows ? ( | |||
| <CircularProgress size={40} /> | |||
| ) : ( | |||
| <StyledDataGrid | |||
| sx={{ maxHeight: 450 }} | |||
| rows={rows} | |||
| columns={columns} | |||
| /> | |||
| )} | |||
| </Grid> | |||
| </Grid> | |||
| ); | |||
| }; | |||
| export default ConsolidatePickOrderItemSum; | |||
| @@ -0,0 +1,115 @@ | |||
| "use client"; | |||
| import dayjs from "dayjs"; | |||
| import arraySupport from "dayjs/plugin/arraySupport"; | |||
| import StyledDataGrid from "../StyledDataGrid"; | |||
| import { | |||
| Dispatch, | |||
| SetStateAction, | |||
| useCallback, | |||
| useEffect, | |||
| useMemo, | |||
| useState, | |||
| } 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 { useTranslation } from "react-i18next"; | |||
| dayjs.extend(arraySupport); | |||
| interface Props { | |||
| consoCode: string; | |||
| rows: Omit<PickOrderResult, "items">[] | undefined; | |||
| setRows: Dispatch< | |||
| SetStateAction<Omit<PickOrderResult, "items">[] | undefined> | |||
| >; | |||
| revertIds: GridInputRowSelectionModel; | |||
| setRevertIds: Dispatch<SetStateAction<GridInputRowSelectionModel>>; | |||
| } | |||
| const ConsolidatePickOrderSum: React.FC<Props> = ({ | |||
| consoCode, | |||
| rows, | |||
| setRows, | |||
| revertIds, | |||
| setRevertIds, | |||
| }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| const columns = useMemo<GridColDef[]>( | |||
| () => [ | |||
| { | |||
| field: "code", | |||
| headerName: "code", | |||
| flex: 0.6, | |||
| }, | |||
| { | |||
| field: "pickOrderLines", | |||
| headerName: "items", | |||
| flex: 1, | |||
| renderCell: (params) => { | |||
| console.log(params); | |||
| const pickOrderLine = params.row.pickOrderLines as any[]; | |||
| return ( | |||
| <Box | |||
| sx={{ | |||
| display: "flex", | |||
| flexDirection: "column", | |||
| maxHeight: 100, | |||
| overflowY: "scroll", | |||
| scrollbarWidth: "none", // For Firefox | |||
| "&::-webkit-scrollbar": { | |||
| display: "none", // For Chrome, Safari, and Opera | |||
| }, | |||
| }} | |||
| > | |||
| {pickOrderLine.map((item, index) => ( | |||
| <Grid sx={{mt:1}} | |||
| key={index} | |||
| >{`${item.itemName} x ${item.requiredQty} ${item.uomDesc}`}</Grid> // Render each name in a span | |||
| ))} | |||
| </Box> | |||
| ); | |||
| }, | |||
| }, | |||
| ], | |||
| [] | |||
| ); | |||
| return ( | |||
| <Grid | |||
| container | |||
| rowGap={1} | |||
| // direction="column" | |||
| alignItems="center" | |||
| justifyContent="center" | |||
| > | |||
| <Grid item xs={12}> | |||
| <Typography variant="h5" marginInlineEnd={2}> | |||
| {t("Pick Order Included")} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| {!rows ? ( | |||
| <CircularProgress size={40} /> | |||
| ) : ( | |||
| <StyledDataGrid | |||
| sx={{ maxHeight: 450 }} | |||
| checkboxSelection | |||
| rowSelectionModel={revertIds} | |||
| onRowSelectionModelChange={(newRowSelectionModel) => { | |||
| setRevertIds(newRowSelectionModel); | |||
| }} | |||
| getRowHeight={(params) => { | |||
| return 100 | |||
| }} | |||
| rows={rows} | |||
| columns={columns} | |||
| /> | |||
| )} | |||
| </Grid> | |||
| </Grid> | |||
| ); | |||
| }; | |||
| export default ConsolidatePickOrderSum; | |||
| @@ -1,12 +1,372 @@ | |||
| import { | |||
| Autocomplete, | |||
| Box, | |||
| Button, | |||
| CircularProgress, | |||
| FormControl, | |||
| Grid, | |||
| Modal, | |||
| ModalProps, | |||
| TextField, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import { GridToolbarContainer } from "@mui/x-data-grid"; | |||
| import { | |||
| FooterPropsOverrides, | |||
| GridColDef, | |||
| GridRowSelectionModel, | |||
| useGridApiRef, | |||
| } from "@mui/x-data-grid"; | |||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import StyledDataGrid from "../StyledDataGrid"; | |||
| import SearchResults, { | |||
| Column, | |||
| defaultPagingController, | |||
| } from "../SearchResults/SearchResults"; | |||
| import { | |||
| ByItemsSummary, | |||
| ConsoPickOrderResult, | |||
| PickOrderLine, | |||
| PickOrderResult, | |||
| } from "@/app/api/pickorder"; | |||
| import { useRouter, useSearchParams } from "next/navigation"; | |||
| import ConsolidatePickOrderItemSum from "./ConsolidatePickOrderItemSum"; | |||
| import ConsolidatePickOrderSum from "./ConsolidatePickOrderSum"; | |||
| import { GridInputRowSelectionModel } from "@mui/x-data-grid"; | |||
| import { | |||
| fetchConsoDetail, | |||
| fetchConsoPickOrderClient, | |||
| releasePickOrder, | |||
| ReleasePickOrderInputs, | |||
| } 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"; | |||
| import { | |||
| FormProvider, | |||
| SubmitErrorHandler, | |||
| SubmitHandler, | |||
| useForm, | |||
| } from "react-hook-form"; | |||
| import { pickOrderStatusMap } from "@/app/utils/formatUtil"; | |||
| interface Props { | |||
| filterArgs: Record<string, any>; | |||
| } | |||
| const style = { | |||
| position: "absolute", | |||
| top: "50%", | |||
| left: "50%", | |||
| transform: "translate(-50%, -50%)", | |||
| bgcolor: "background.paper", | |||
| pt: 5, | |||
| px: 5, | |||
| pb: 10, | |||
| width: 1500, | |||
| }; | |||
| interface DisableButton { | |||
| releaseBtn: boolean; | |||
| removeBtn: boolean; | |||
| } | |||
| const ConsolidatedPickOrders: React.FC<Props> = ({ | |||
| const ConsolidatedPickOrders: React.FC<Props> = ({ filterArgs }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| const router = useRouter(); | |||
| const apiRef = useGridApiRef(); | |||
| const [filteredPickOrders, setFilteredPickOrders] = useState( | |||
| [] as ConsoPickOrderResult[] | |||
| ); | |||
| const [isLoading, setIsLoading] = useState(false); | |||
| const [modalOpen, setModalOpen] = useState(false); //change back to false | |||
| const [consoCode, setConsoCode] = useState<string | undefined>(); ///change back to undefined | |||
| const [revertIds, setRevertIds] = useState<GridInputRowSelectionModel>([]); | |||
| const [totalCount, setTotalCount] = useState<number>(); | |||
| const [usernameList, setUsernameList] = useState<NameList[]>([]); | |||
| }) => { | |||
| return <></> | |||
| } | |||
| const [byPickOrderRows, setByPickOrderRows] = useState< | |||
| Omit<PickOrderResult, "items">[] | undefined | |||
| >(undefined); | |||
| const [byItemsRows, setByItemsRows] = useState<ByItemsSummary[] | undefined>( | |||
| undefined | |||
| ); | |||
| const [disableRelease, setDisableRelease] = useState<boolean>(true); | |||
| const formProps = useForm<ReleasePickOrderInputs>(); | |||
| const errors = formProps.formState.errors; | |||
| const openDetailModal = useCallback((consoCode: string) => { | |||
| setConsoCode(consoCode); | |||
| setModalOpen(true); | |||
| }, []); | |||
| const closeDetailModal = useCallback(() => { | |||
| setModalOpen(false); | |||
| setConsoCode(undefined); | |||
| }, []); | |||
| const onDetailClick = useCallback( | |||
| (pickOrder: any) => { | |||
| console.log(pickOrder); | |||
| const status = pickOrder.status | |||
| if (pickOrderStatusMap[status] >= 2) { | |||
| router.push(`/pickorder/detail?consoCode=${pickOrder.consoCode}`); | |||
| } else { | |||
| openDetailModal(pickOrder.consoCode); | |||
| } | |||
| }, | |||
| [router, openDetailModal] | |||
| ); | |||
| const columns = useMemo<Column<ConsoPickOrderResult>[]>( | |||
| () => [ | |||
| { | |||
| name: "id", | |||
| label: t("Detail"), | |||
| onClick: onDetailClick, | |||
| buttonIcon: <EditNote />, | |||
| }, | |||
| { | |||
| name: "consoCode", | |||
| label: t("consoCode"), | |||
| }, | |||
| { | |||
| name: "status", | |||
| label: t("status"), | |||
| }, | |||
| ], | |||
| [] | |||
| ); | |||
| const [pagingController, setPagingController] = useState( | |||
| defaultPagingController | |||
| ); | |||
| // pass conso code back to assign | |||
| // pass user back to assign | |||
| const fetchNewPageConsoPickOrder = useCallback( | |||
| async ( | |||
| pagingController: Record<string, number>, | |||
| filterArgs: Record<string, number> | |||
| ) => { | |||
| setIsLoading(true); | |||
| const params = { | |||
| ...pagingController, | |||
| ...filterArgs, | |||
| }; | |||
| const res = await fetchConsoPickOrderClient(params); | |||
| if (res) { | |||
| console.log(res); | |||
| setFilteredPickOrders(res.records); | |||
| setTotalCount(res.total); | |||
| } | |||
| setIsLoading(false); | |||
| }, | |||
| [] | |||
| ); | |||
| useEffect(() => { | |||
| fetchNewPageConsoPickOrder(pagingController, filterArgs); | |||
| }, [fetchNewPageConsoPickOrder, pagingController, filterArgs]); | |||
| const isReleasable = useCallback((itemList: ByItemsSummary[]): boolean => { | |||
| var isReleasable = true; | |||
| for (const item of itemList) { | |||
| isReleasable = item.requiredQty >= item.availableQty; | |||
| if (!isReleasable) return isReleasable; | |||
| } | |||
| return isReleasable; | |||
| }, []); | |||
| const fetchConso = useCallback( | |||
| async (consoCode: string) => { | |||
| const res = await fetchConsoDetail(consoCode); | |||
| const nameListRes = await fetchNameList(); | |||
| if (res) { | |||
| console.log(res); | |||
| setByPickOrderRows(res.pickOrders); | |||
| // for testing | |||
| // for (const item of res.items) { | |||
| // item.availableQty = 1000; | |||
| // } | |||
| setByItemsRows(res.items); | |||
| setDisableRelease(isReleasable(res.items)); | |||
| } else { | |||
| console.log("error"); | |||
| console.log(res); | |||
| } | |||
| if (nameListRes) { | |||
| console.log(nameListRes); | |||
| setUsernameList(nameListRes); | |||
| } | |||
| }, | |||
| [isReleasable] | |||
| ); | |||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
| (...args) => { | |||
| closeDetailModal(); | |||
| // reset(); | |||
| }, | |||
| [closeDetailModal] | |||
| ); | |||
| const onChange = useCallback( | |||
| ( | |||
| event: React.SyntheticEvent, | |||
| newValue: NameList | |||
| ) => { | |||
| console.log(newValue); | |||
| formProps.setValue("assignTo", newValue.id); | |||
| }, | |||
| [] | |||
| ); | |||
| const onSubmit = useCallback<SubmitHandler<ReleasePickOrderInputs & {}>>( | |||
| async (data, event) => { | |||
| console.log(data); | |||
| try { | |||
| const res = await releasePickOrder(data) | |||
| console.log(res) | |||
| if (res.status = 200) { | |||
| router.push(`/pickorder/detail?consoCode=${data.consoCode}`); | |||
| } else { | |||
| throw Error("hv error") | |||
| } | |||
| } catch (error) { | |||
| console.log(error) | |||
| } | |||
| }, | |||
| [releasePickOrder] | |||
| ); | |||
| const onSubmitError = useCallback<SubmitErrorHandler<ReleasePickOrderInputs>>( | |||
| (errors) => {}, | |||
| [] | |||
| ); | |||
| const handleConsolidate_revert = useCallback(() => { | |||
| console.log(revertIds); | |||
| }, [revertIds]); | |||
| useEffect(() => { | |||
| if (consoCode) { | |||
| fetchConso(consoCode); | |||
| formProps.setValue("consoCode", consoCode) | |||
| } | |||
| }, [consoCode]); | |||
| return ( | |||
| <> | |||
| <Grid | |||
| container | |||
| rowGap={1} | |||
| // direction="column" | |||
| alignItems="center" | |||
| justifyContent="center" | |||
| > | |||
| <Grid item xs={12}> | |||
| {isLoading ? ( | |||
| <CircularProgress size={40} /> | |||
| ) : ( | |||
| <SearchResults<ConsoPickOrderResult> | |||
| items={filteredPickOrders} | |||
| columns={columns} | |||
| pagingController={pagingController} | |||
| setPagingController={setPagingController} | |||
| totalCount={totalCount} | |||
| /> | |||
| )} | |||
| </Grid> | |||
| </Grid> | |||
| {consoCode != undefined ? ( | |||
| <Modal open={modalOpen} onClose={closeHandler}> | |||
| <FormProvider {...formProps}> | |||
| <Box | |||
| sx={{ ...style, maxHeight: 800 }} | |||
| component="form" | |||
| onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||
| > | |||
| <Grid container> | |||
| <Grid item xs={8}> | |||
| <Typography mb={2} variant="h4"> | |||
| {consoCode} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid | |||
| item | |||
| xs={4} | |||
| display="flex" | |||
| justifyContent="end" | |||
| alignItems="end" | |||
| > | |||
| <FormControl fullWidth> | |||
| <Autocomplete | |||
| disableClearable | |||
| fullWidth | |||
| getOptionLabel={(option) => option.name} | |||
| options={usernameList} | |||
| onChange={onChange} | |||
| renderInput={(params) => <TextField {...params} />} | |||
| /> | |||
| </FormControl> | |||
| </Grid> | |||
| </Grid> | |||
| <Box | |||
| sx={{ | |||
| height: 400, | |||
| overflowY: "auto", | |||
| }} | |||
| > | |||
| <Grid container> | |||
| <Grid item xs={12} sx={{ mt: 2 }}> | |||
| <ConsolidatePickOrderSum | |||
| rows={byPickOrderRows} | |||
| setRows={setByPickOrderRows} | |||
| consoCode={consoCode} | |||
| revertIds={revertIds} | |||
| setRevertIds={setRevertIds} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <ConsolidatePickOrderItemSum | |||
| rows={byItemsRows} | |||
| setRows={setByItemsRows} | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| <Grid container> | |||
| <Grid | |||
| item | |||
| xs={12} | |||
| display="flex" | |||
| justifyContent="end" | |||
| alignItems="end" | |||
| > | |||
| <Button | |||
| disabled={(revertIds as number[]).length < 1} | |||
| variant="outlined" | |||
| onClick={handleConsolidate_revert} | |||
| sx={{ mr: 1 }} | |||
| > | |||
| {t("remove")} | |||
| </Button> | |||
| <Button | |||
| disabled={disableRelease} | |||
| variant="outlined" | |||
| // onClick={handleRelease} | |||
| type="submit" | |||
| > | |||
| {t("release")} | |||
| </Button> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| </FormProvider> | |||
| </Modal> | |||
| ) : undefined} | |||
| </> | |||
| ); | |||
| }; | |||
| export default ConsolidatedPickOrders; | |||
| export default ConsolidatedPickOrders; | |||
| @@ -1,24 +1,40 @@ | |||
| "use client" | |||
| import { PickOrderResult } from "@/app/api/pickOrder"; | |||
| "use client"; | |||
| import { PickOrderResult } from "@/app/api/pickorder"; | |||
| import { SearchParams } from "@/app/utils/fetchUtil"; | |||
| import { useCallback, 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 { | |||
| 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 PickOrders from "./PickOrders"; | |||
| import ConsolidatedPickOrders from "./ConsolidatedPickOrders"; | |||
| import { getServerI18n } from "@/i18n"; | |||
| interface Props { | |||
| pickOrders: PickOrderResult[]; | |||
| pickOrders: PickOrderResult[]; | |||
| } | |||
| type SearchQuery = Partial<Omit<PickOrderResult, | |||
| | "id" | |||
| | "consoCode" | |||
| | "completeDate">> | |||
| type SearchQuery = Partial< | |||
| Omit<PickOrderResult, "id" | "consoCode" | "completeDate"> | |||
| >; | |||
| type SearchParamNames = keyof SearchQuery; | |||
| @@ -27,76 +43,134 @@ const PickOrderSearch: React.FC<Props> = ({ | |||
| }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| const [filteredPickOrders, setFilteredPickOrders] = useState(pickOrders) | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||
| (_e, newValue) => { | |||
| setTabIndex(newValue); | |||
| }, | |||
| [], | |||
| ); | |||
| const [filteredPickOrders, setFilteredPickOrders] = useState(pickOrders); | |||
| const [filterArgs, setFilterArgs] = useState<Record<string, any>>({}); | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||
| (_e, newValue) => { | |||
| setTabIndex(newValue); | |||
| }, | |||
| [] | |||
| ); | |||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => [ | |||
| { label: t("Code"), paramName: "code", type: "text" }, | |||
| { label: t("Target Date From"), label2: t("Target Date To"), paramName: "targetDate", type: "dateRange" }, | |||
| { | |||
| label: t("Type"), paramName: "type", type: "autocomplete", | |||
| options: sortBy( | |||
| uniqBy(pickOrders.map((po) => ({ value: po.type, label: t(upperCase(po.type)) })), "value"), | |||
| "label") | |||
| }, | |||
| { | |||
| label: t("Status"), paramName: "status", type: "autocomplete", | |||
| options: sortBy( | |||
| uniqBy(pickOrders.map((po) => ({ value: po.status, label: t(upperFirst(po.status)) })), "value"), | |||
| "label") | |||
| }, | |||
| { | |||
| label: t("Items"), paramName: "items", type: "autocomplete", // multiple: true, | |||
| options: uniqBy(flatten(sortBy( | |||
| pickOrders.map((po) => po.items ? po.items.map((item) => ({ | |||
| value: item.name, label: item.name, | |||
| // group: item.type | |||
| })) : []), | |||
| "label")), "value") | |||
| }, | |||
| ], [t]) | |||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
| () => [ | |||
| { label: t("Code"), paramName: "code", type: "text" }, | |||
| { | |||
| label: t("Target Date From"), | |||
| label2: t("Target Date To"), | |||
| paramName: "targetDate", | |||
| type: "dateRange", | |||
| }, | |||
| { | |||
| label: t("Type"), | |||
| paramName: "type", | |||
| type: "autocomplete", | |||
| options: sortBy( | |||
| uniqBy( | |||
| pickOrders.map((po) => ({ | |||
| value: po.type, | |||
| label: t(upperCase(po.type)), | |||
| })), | |||
| "value" | |||
| ), | |||
| "label" | |||
| ), | |||
| }, | |||
| { | |||
| label: t("Status"), | |||
| paramName: "status", | |||
| type: "autocomplete", | |||
| options: sortBy( | |||
| uniqBy( | |||
| pickOrders.map((po) => ({ | |||
| value: po.status, | |||
| label: t(upperFirst(po.status)), | |||
| })), | |||
| "value" | |||
| ), | |||
| "label" | |||
| ), | |||
| }, | |||
| { | |||
| label: t("Items"), | |||
| paramName: "items", | |||
| type: "autocomplete", // multiple: true, | |||
| options: uniqBy( | |||
| flatten( | |||
| sortBy( | |||
| pickOrders.map((po) => | |||
| po.items | |||
| ? po.items.map((item) => ({ | |||
| value: item.name, | |||
| label: item.name, | |||
| // group: item.type | |||
| })) | |||
| : [] | |||
| ), | |||
| "label" | |||
| ) | |||
| ), | |||
| "value" | |||
| ), | |||
| }, | |||
| ], | |||
| [t] | |||
| ); | |||
| const onReset = useCallback(() => { | |||
| setFilteredPickOrders(pickOrders) | |||
| }, [pickOrders]) | |||
| const onReset = useCallback(() => { | |||
| setFilteredPickOrders(pickOrders); | |||
| }, [pickOrders]); | |||
| return ( | |||
| <> | |||
| <SearchBox | |||
| criteria={searchCriteria} | |||
| onSearch={(query) => { | |||
| setFilteredPickOrders( | |||
| pickOrders.filter( | |||
| (po) => { | |||
| const poTargetDateStr = arrayToDayjs(po.targetDate) | |||
| return ( | |||
| <> | |||
| <SearchBox | |||
| criteria={searchCriteria} | |||
| onSearch={(query) => { | |||
| setFilterArgs({ ...query }); // modify later | |||
| setFilteredPickOrders( | |||
| pickOrders.filter((po) => { | |||
| const poTargetDateStr = arrayToDayjs(po.targetDate); | |||
| // console.log(intersectionWith(po.items?.map(item => item.name), query.items)) | |||
| return po.code.toLowerCase().includes(query.code.toLowerCase()) | |||
| && (isEmpty(query.targetDate) || poTargetDateStr.isSame(query.targetDate) || poTargetDateStr.isAfter(query.targetDate)) | |||
| && (isEmpty(query.targetDateTo) || poTargetDateStr.isSame(query.targetDateTo) || poTargetDateStr.isBefore(query.targetDateTo)) | |||
| && (intersectionWith(["All"], query.items).length > 0 || intersectionWith(po.items?.map(item => item.name), query.items).length > 0) | |||
| && (query.status.toLowerCase() == "all" || po.status.toLowerCase().includes(query.status.toLowerCase())) | |||
| && (query.type.toLowerCase() == "all" || po.type.toLowerCase().includes(query.type.toLowerCase())) | |||
| } | |||
| ) | |||
| ) | |||
| }} | |||
| onReset={onReset} | |||
| /> | |||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||
| <Tab label={t("Pick Orders")} iconPosition="end" /> | |||
| <Tab label={t("Consolidated Pick Orders")} iconPosition="end" /> | |||
| </Tabs> | |||
| {tabIndex === 0 && <PickOrders filteredPickOrders={filteredPickOrders}/>} | |||
| </> | |||
| ) | |||
| } | |||
| // console.log(intersectionWith(po.items?.map(item => item.name), query.items)) | |||
| return ( | |||
| po.code.toLowerCase().includes(query.code.toLowerCase()) && | |||
| (isEmpty(query.targetDate) || | |||
| poTargetDateStr.isSame(query.targetDate) || | |||
| poTargetDateStr.isAfter(query.targetDate)) && | |||
| (isEmpty(query.targetDateTo) || | |||
| poTargetDateStr.isSame(query.targetDateTo) || | |||
| poTargetDateStr.isBefore(query.targetDateTo)) && | |||
| (intersectionWith(["All"], query.items).length > 0 || | |||
| intersectionWith( | |||
| po.items?.map((item) => item.name), | |||
| query.items | |||
| ).length > 0) && | |||
| (query.status.toLowerCase() == "all" || | |||
| po.status | |||
| .toLowerCase() | |||
| .includes(query.status.toLowerCase())) && | |||
| (query.type.toLowerCase() == "all" || | |||
| po.type.toLowerCase().includes(query.type.toLowerCase())) | |||
| ); | |||
| }) | |||
| ); | |||
| }} | |||
| onReset={onReset} | |||
| /> | |||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||
| <Tab label={t("Pick Orders")} iconPosition="end" /> | |||
| <Tab label={t("Consolidated Pick Orders")} iconPosition="end" /> | |||
| </Tabs> | |||
| {tabIndex === 0 && ( | |||
| <PickOrders | |||
| filteredPickOrders={filteredPickOrders} | |||
| filterArgs={filterArgs} | |||
| /> | |||
| )} | |||
| {tabIndex === 1 && <ConsolidatedPickOrders filterArgs={filterArgs} />} | |||
| </> | |||
| ); | |||
| }; | |||
| export default PickOrderSearch; | |||
| export default PickOrderSearch; | |||
| @@ -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"; | |||
| @@ -10,7 +10,14 @@ const PickOrderSearchWrapper: React.FC & SubComponents = async () => { | |||
| const [ | |||
| pickOrders | |||
| ] = await Promise.all([ | |||
| fetchPickOrders() | |||
| fetchPickOrders({ | |||
| code: undefined, | |||
| targetDateFrom: undefined, | |||
| targetDateTo: undefined, | |||
| type: undefined, | |||
| status: undefined, | |||
| itemName: undefined, | |||
| }) | |||
| ]) | |||
| return <PickOrderSearch pickOrders={pickOrders}/> | |||
| @@ -1,100 +1,157 @@ | |||
| import { Button, Grid } from "@mui/material"; | |||
| 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, useMemo, useState } from "react"; | |||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { isEmpty, upperCase, upperFirst } from "lodash"; | |||
| import { arrayToDateString } from "@/app/utils/formatUtil"; | |||
| import { consolidatePickOrder, fetchPickOrderClient } from "@/app/api/pickorder/actions"; | |||
| import useUploadContext from "../UploadProvider/useUploadContext"; | |||
| interface Props { | |||
| filteredPickOrders: PickOrderResult[], | |||
| filteredPickOrders: PickOrderResult[]; | |||
| filterArgs: Record<string, any>; | |||
| } | |||
| const PickOrders: React.FC<Props> = ({ | |||
| filteredPickOrders | |||
| }) => { | |||
| const { t } = useTranslation("pickOrder") | |||
| const [selectedRows, setSelectedRows] = useState<(string | number)[]>([]); | |||
| const PickOrders: React.FC<Props> = ({ filteredPickOrders, filterArgs }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| const [selectedRows, setSelectedRows] = useState<(string | number)[]>([]); | |||
| const [filteredPickOrder, setFilteredPickOrder] = useState( | |||
| [] as PickOrderResult[] | |||
| ); | |||
| const { setIsUploading } = useUploadContext(); | |||
| const [isLoading, setIsLoading] = useState(false); | |||
| const [pagingController, setPagingController] = useState({ | |||
| pageNum: 0, | |||
| pageSize: 10, | |||
| }); | |||
| const [totalCount, setTotalCount] = useState<number>(); | |||
| const handleConsolidatedRows = useCallback(() => { | |||
| 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]); | |||
| }, [selectedRows]) | |||
| const fetchNewPagePickOrder = useCallback( | |||
| async ( | |||
| pagingController: Record<string, number>, | |||
| filterArgs: Record<string, number> | |||
| ) => { | |||
| setIsLoading(true); | |||
| const params = { | |||
| ...pagingController, | |||
| ...filterArgs, | |||
| }; | |||
| const res = await fetchPickOrderClient(params) | |||
| if (res) { | |||
| console.log(res); | |||
| setFilteredPickOrder(res.records); | |||
| setTotalCount(res.total); | |||
| } | |||
| setIsLoading(false); | |||
| }, | |||
| [] | |||
| ); | |||
| const columns = useMemo<Column<PickOrderResult>[]>(() => [ | |||
| { | |||
| name: "id", | |||
| label: "", | |||
| type: "checkbox", | |||
| disabled: (params) => { | |||
| return !isEmpty(params.consoCode); | |||
| } | |||
| }, | |||
| { | |||
| name: "code", | |||
| label: t("Code"), | |||
| }, | |||
| { | |||
| name: "consoCode", | |||
| label: t("Consolidated Code"), | |||
| renderCell: (params) => { | |||
| return params.consoCode ?? "N/A" | |||
| } | |||
| useEffect(() => { | |||
| fetchNewPagePickOrder(pagingController, filterArgs); | |||
| }, [fetchNewPagePickOrder, pagingController, filterArgs]); | |||
| const columns = useMemo<Column<PickOrderResult>[]>( | |||
| () => [ | |||
| { | |||
| name: "id", | |||
| label: "", | |||
| type: "checkbox", | |||
| disabled: (params) => { | |||
| return !isEmpty(params.consoCode); | |||
| }, | |||
| { | |||
| name: "type", | |||
| label: t("type"), | |||
| renderCell: (params) => { | |||
| return upperCase(params.type) | |||
| } | |||
| }, | |||
| { | |||
| name: "code", | |||
| label: t("Code"), | |||
| }, | |||
| { | |||
| name: "consoCode", | |||
| label: t("Consolidated Code"), | |||
| renderCell: (params) => { | |||
| return params.consoCode ?? ""; | |||
| }, | |||
| { | |||
| name: "items", | |||
| label: t("Items"), | |||
| renderCell: (params) => { | |||
| return params.items?.map((i) => i.name).join(", ") | |||
| } | |||
| }, | |||
| { | |||
| name: "type", | |||
| label: t("type"), | |||
| renderCell: (params) => { | |||
| return upperCase(params.type); | |||
| }, | |||
| { | |||
| name: "targetDate", | |||
| label: t("Target Date"), | |||
| renderCell: (params) => { | |||
| return arrayToDateString(params.targetDate) | |||
| } | |||
| }, | |||
| { | |||
| name: "items", | |||
| label: t("Items"), | |||
| renderCell: (params) => { | |||
| return params.items?.map((i) => i.name).join(", "); | |||
| }, | |||
| { | |||
| name: "releasedBy", | |||
| label: t("Released By"), | |||
| }, | |||
| { | |||
| name: "targetDate", | |||
| label: t("Target Date"), | |||
| renderCell: (params) => { | |||
| return arrayToDateString(params.targetDate); | |||
| }, | |||
| { | |||
| name: "status", | |||
| label: t("Status"), | |||
| renderCell: (params) => { | |||
| return upperFirst(params.status) | |||
| } | |||
| }, | |||
| { | |||
| name: "releasedBy", | |||
| label: t("Released By"), | |||
| }, | |||
| { | |||
| name: "status", | |||
| label: t("Status"), | |||
| renderCell: (params) => { | |||
| return upperFirst(params.status); | |||
| }, | |||
| ], [t]) | |||
| }, | |||
| ], | |||
| [t] | |||
| ); | |||
| return ( | |||
| <Grid container rowGap={1}> | |||
| <Grid item xs={3}> | |||
| <Button | |||
| disabled={selectedRows.length < 1} | |||
| variant="outlined" | |||
| > | |||
| {t("Consolidate")} | |||
| </Button> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <SearchResults<PickOrderResult> items={filteredPickOrders} columns={columns} pagingController={{ | |||
| pageNum: 0, | |||
| pageSize: 0 | |||
| }} | |||
| checkboxIds={selectedRows} | |||
| setCheckboxIds={setSelectedRows} | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| ) | |||
| } | |||
| return ( | |||
| <Grid container rowGap={1}> | |||
| <Grid item xs={3}> | |||
| <Button | |||
| disabled={selectedRows.length < 1} | |||
| variant="outlined" | |||
| onClick={handleConsolidatedRows} | |||
| > | |||
| {t("Consolidate")} | |||
| </Button> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| {isLoading ? ( | |||
| <CircularProgress size={40} /> | |||
| ) : ( | |||
| <SearchResults<PickOrderResult> | |||
| items={filteredPickOrder} | |||
| columns={columns} | |||
| pagingController={pagingController} | |||
| setPagingController={setPagingController} | |||
| totalCount={totalCount} | |||
| checkboxIds={selectedRows!!} | |||
| setCheckboxIds={setSelectedRows} | |||
| /> | |||
| )} | |||
| </Grid> | |||
| </Grid> | |||
| ); | |||
| }; | |||
| export default PickOrders; | |||
| export default PickOrders; | |||
| @@ -152,7 +152,7 @@ const PoSearch: React.FC<Props> = ({ | |||
| setTotalCount(res.total); | |||
| } | |||
| }, | |||
| [fetchPoListClient, pagingController] | |||
| [fetchPoListClient] | |||
| ); | |||
| useEffect(() => { | |||
| @@ -209,7 +209,24 @@ function SearchResults<T extends ResultWithId>({ | |||
| }; | |||
| // checkbox | |||
| const handleRowClick = useCallback((event: MouseEvent<unknown>, id: string | number) => { | |||
| const handleRowClick = useCallback((event: MouseEvent<unknown>, item: T, columns: Column<T>[]) => { | |||
| // check is disabled or not | |||
| var disabled = false | |||
| columns.forEach((col) => { | |||
| if (isCheckboxColumn(col) && col.disabled) { | |||
| disabled = col.disabled(item) | |||
| if (disabled) { | |||
| return; | |||
| } | |||
| } | |||
| }) | |||
| if (disabled) { | |||
| return; | |||
| } | |||
| // set id | |||
| const id = item.id | |||
| if (setCheckboxIds) { | |||
| const selectedIndex = checkboxIds.indexOf(id); | |||
| let newSelected: (string | number)[] = []; | |||
| @@ -257,7 +274,7 @@ function SearchResults<T extends ResultWithId>({ | |||
| hover | |||
| tabIndex={-1} | |||
| key={item.id} | |||
| onClick={setCheckboxIds ? (event) => handleRowClick(event, item.id) : undefined} | |||
| onClick={setCheckboxIds? (event) => handleRowClick(event, item, columns) : undefined} | |||
| role={setCheckboxIds ? "checkbox" : undefined} | |||
| > | |||
| {columns.map((column, idx) => { | |||
| @@ -1,4 +1,24 @@ | |||
| { | |||
| "Overview": "概述", | |||
| "Qc Item": "品質檢驗項目", | |||
| "Dashboard": "儀表板", | |||
| "dashboard": "儀表板", | |||
| "Raw Material": "原料", | |||
| "Purchase Order": "採購訂單", | |||
| "Pick Order": "提料單", | |||
| "View item In-out And inventory Ledger": "存貨", | |||
| "Inventory": "存貨", | |||
| "Delivery": "送貨", | |||
| "Delivery Order": "送貨單", | |||
| "Scheduling": "生產計劃", | |||
| "Demand Forecast Setting": "粗排設定", | |||
| "Demand Forecast": "粗排", | |||
| "FG & Material Demand Forecast Detail": "成品 & 原料粗排細節", | |||
| "Detail Scheduling": "細排", | |||
| "FG Production Schedule": "成品生產計劃", | |||
| "Settings": "設定", | |||
| "Edit": "編輯", | |||
| "Search Criteria": "搜尋條件", | |||
| "All": "全部", | |||
| "No options": "沒有選項", | |||