diff --git a/package.json b/package.json index 7f04ff1..7e2c0bb 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "next dev", "build": "next build", - "start": "NODE_OPTIONS='--inspect' next start", + "start": "set NODE_OPTIONS=--inspect&& next start", "lint": "next lint", "type-check": "tsc --noEmit" }, diff --git a/src/app/api/inventory/actions.ts b/src/app/api/inventory/actions.ts index 216a3c2..e2bab86 100644 --- a/src/app/api/inventory/actions.ts +++ b/src/app/api/inventory/actions.ts @@ -3,9 +3,11 @@ 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 { Pageable, serverFetchJson } from "@/app/utils/fetchUtil"; import { QcItemResult } from "../settings/qcItem"; import { RecordsRes } from "../utils"; +import { convertObjToURLSearchParams } from "@/app/utils/commonUtil"; +import { InventoryLotLineResult, InventoryResult } from "."; // import { BASE_API_URL } from "@/config/api"; export interface LotLineInfo { @@ -18,6 +20,26 @@ export interface LotLineInfo { uom: string; } +export interface SearchInventoryLotLine extends Pageable { + itemId: number; +} + +export interface SearchInventory extends Pageable { + code: string; + name: string; + type: string; +} + +export interface InventoryResultByPage { + total: number; + records: InventoryResult[]; +} + +export interface InventoryLotLineResultByPage { + total: number; + records: InventoryLotLineResult[]; +} + export const fetchLotDetail = cache(async (stockInLineId: number) => { return serverFetchJson( `${BASE_API_URL}/inventoryLotLine/lot-detail/${stockInLineId}`, @@ -27,3 +49,19 @@ export const fetchLotDetail = cache(async (stockInLineId: number) => { }, ); }); + +export const fetchInventories = cache(async (data: SearchInventory) => { + const queryStr = convertObjToURLSearchParams(data) + + return serverFetchJson(`${BASE_API_URL}/inventory/getRecordByPage?${queryStr}`, + { next: { tags: ["inventories"] } } + ) +}) + + +export const fetchInventoryLotLines = cache(async (data: SearchInventoryLotLine) => { + const queryStr = convertObjToURLSearchParams(data) + return serverFetchJson(`${BASE_API_URL}/inventoryLotLine/getRecordByPage?${queryStr}`, { + next: { tags: ["inventoryLotLines"] }, + }); +}); diff --git a/src/app/api/inventory/index.ts b/src/app/api/inventory/index.ts index b4bf7c9..cdb9294 100644 --- a/src/app/api/inventory/index.ts +++ b/src/app/api/inventory/index.ts @@ -1,3 +1,4 @@ +import { convertObjToURLSearchParams } from "@/app/utils/commonUtil"; import { serverFetchJson } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; import { cache } from "react"; @@ -5,20 +6,54 @@ import "server-only"; export interface InventoryResult { id: number; + itemId: number; itemCode: string; itemName: string; itemType: string; + onHandQty: number; + onHoldQty: number; + unavailableQty: number; availableQty: number; uomCode: string; uomUdfudesc: string; // germPerSmallestUnit: number; - // qtyPerSmallestUnit: number; + qtyPerSmallestUnit: number; + baseUom: string; // smallestUnit: string; price: number; currencyName: string; status: string; } +export interface InventoryLotLineResult { + id: number; + lotNo: string; + item: InventoryLotLineItem; + warehouse: InventoryLotLineWarehouse; + inQty: number; + outQty: number; + holdQty: number; + expiryDate: number[]; + status: string; + availableQty: number; + uom: string; + qtyPerSmallestUnit: number; + baseUom: string; +} + +export interface InventoryLotLineItem { + id: number; + code: string; + name: string; + type: string; +} + +export interface InventoryLotLineWarehouse { + id: number; + code: string; + name: string; +} + export const preloadInventory = () => { fetchInventories(); }; diff --git a/src/app/api/po/actions.ts b/src/app/api/po/actions.ts index f3c4e6a..779d9f1 100644 --- a/src/app/api/po/actions.ts +++ b/src/app/api/po/actions.ts @@ -39,6 +39,8 @@ export interface PurchaseQcResult { export interface StockInInput { status: string; productLotNo?: string; + dnNo?: string; + invoiceNo?: string; receiptDate: string; acceptedQty: number; acceptedWeight?: number; @@ -55,7 +57,9 @@ export interface PurchaseQCInput { } export interface EscalationInput { status: string; + remarks?: string; handler: string; + productLotNo: string; acceptedQty: number; // this is the qty to be escalated // escalationQty: number } diff --git a/src/app/utils/commonUtil.ts b/src/app/utils/commonUtil.ts index 925ba83..2050a3d 100644 --- a/src/app/utils/commonUtil.ts +++ b/src/app/utils/commonUtil.ts @@ -26,3 +26,10 @@ export const convertObjToURLSearchParams = ( return params.toString(); }; + +export const getCustomWidth = (): number => { + const LIMIT_WIDTH = 1000 + const CUSTOM_WIDTH = 1100 + if (window.innerWidth < LIMIT_WIDTH) return CUSTOM_WIDTH + return window.innerWidth +} \ No newline at end of file diff --git a/src/app/utils/formatUtil.ts b/src/app/utils/formatUtil.ts index 4c797f0..174fc88 100644 --- a/src/app/utils/formatUtil.ts +++ b/src/app/utils/formatUtil.ts @@ -43,7 +43,8 @@ export const arrayToDayjs = (arr: ConfigType | (number | undefined)[]) => { if (isArray(arr) && every(arr, isValidNumber) && arr.length >= 3) { // [year, month, day] - tempArr = take(arr, 3); + // tempArr = take(arr, 3); + tempArr = `${arr[0]?.toString().padStart(4, "0")}-${arr[1]?.toString().padStart(2, "0")}-${arr[2]?.toString().padStart(2, "0")}`; } return dayjs(tempArr as ConfigType); diff --git a/src/components/InputDataGrid/InputDataGrid.tsx b/src/components/InputDataGrid/InputDataGrid.tsx index c8a3c83..b961c62 100644 --- a/src/components/InputDataGrid/InputDataGrid.tsx +++ b/src/components/InputDataGrid/InputDataGrid.tsx @@ -37,7 +37,9 @@ import { GridApiCommunity, GridSlotsComponentsProps, } from "@mui/x-data-grid/internals"; - +// T == CreatexxxInputs map of the form's fields +// V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc +// E == error interface ResultWithId { id: string | number; } @@ -97,7 +99,7 @@ export class ProcessRowUpdateError extends Error { Object.setPrototypeOf(this, ProcessRowUpdateError.prototype); } } -// T == CreatexxxInputs +// T == CreatexxxInputs map of the form's fields // V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc // E == error function InputDataGrid({ @@ -126,7 +128,11 @@ function InputDataGrid({ const list: TableRow[] = getValues(formKey); return list && list.length > 0 ? list : []; }); - const originalRows = list && list.length > 0 ? list : []; + // const originalRows = list && list.length > 0 ? list : []; + const originalRows = useMemo(() => ( + list && list.length > 0 ? list : [] + ), [list]) + // const originalRowModel = originalRows.filter((li) => li.isActive).map(i => i.id) as GridRowSelectionModel const [rowSelectionModel, setRowSelectionModel] = useState(() => { @@ -154,7 +160,7 @@ function InputDataGrid({ console.log(errors); apiRef.current.updateRows([{ ...row, _error: errors }]); }, - [apiRef, rowModesModel], + [apiRef], ); const processRowUpdate = useCallback( @@ -202,7 +208,7 @@ function InputDataGrid({ const reset = useCallback(() => { setRowModesModel({}); setRows(originalRows); - }, []); + }, [originalRows]); const handleCancel = useCallback( (id: GridRowId) => () => { @@ -219,14 +225,14 @@ function InputDataGrid({ ); } }, - [setRowModesModel, rows], + [rows, getRowId], ); const handleDelete = useCallback( (id: GridRowId) => () => { setRows((prevRows) => prevRows.filter((row) => getRowId(row) !== id)); }, - [], + [getRowId], ); const _columns = useMemo( @@ -281,7 +287,7 @@ function InputDataGrid({ // console.log(formKey) // console.log(rows) setValue(formKey, rows); - }, [formKey, rows]); + }, [formKey, rows, setValue]); const footer = ( diff --git a/src/components/InventorySearch/InventoryLotLineTable.tsx b/src/components/InventorySearch/InventoryLotLineTable.tsx new file mode 100644 index 0000000..ab19fff --- /dev/null +++ b/src/components/InventorySearch/InventoryLotLineTable.tsx @@ -0,0 +1,110 @@ +import { InventoryLotLineResult, InventoryResult } from "@/app/api/inventory"; +import { Dispatch, SetStateAction, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { Column } from "../SearchResults"; +import SearchResults, { defaultPagingController, defaultSetPagingController } from "../SearchResults/SearchResults"; +import { CheckCircleOutline, DoDisturb } from "@mui/icons-material"; +import { arrayToDateString } from "@/app/utils/formatUtil"; +import { Typography } from "@mui/material"; +import { isFinite } from "lodash"; + +interface Props { + inventoryLotLines: InventoryLotLineResult[] | null; + setPagingController: defaultSetPagingController; + pagingController: typeof defaultPagingController; + totalCount: number; + item: InventoryResult | null; +} + +const InventoryLotLineTable: React.FC = ({ inventoryLotLines, pagingController, setPagingController, totalCount, item }) => { + const { t } = useTranslation(["inventory"]); + + const columns = useMemo[]>( + () => [ + // { + // name: "item", + // label: t("Code"), + // renderCell: (params) => { + // return params.item.code; + // }, + // }, + // { + // name: "item", + // label: t("Name"), + // renderCell: (params) => { + // return params.item.name; + // }, + // }, + { + name: "lotNo", + label: t("Lot No"), + }, + // { + // name: "item", + // label: t("Type"), + // renderCell: (params) => { + // return t(params.item.type); + // }, + // }, + { + name: "availableQty", + label: t("Available Qty"), + align: "right", + headerAlign: "right", + type: "integer", + }, + { + name: "uom", + label: t("Sales UoM"), + align: "left", + headerAlign: "left", + }, + { + name: "qtyPerSmallestUnit", + label: t("Available Qty Per Smallest Unit"), + align: "right", + headerAlign: "right", + type: "integer", + }, + { + name: "baseUom", + label: t("Base UoM"), + align: "left", + headerAlign: "left", + }, + { + name: "expiryDate", + label: t("Expiry Date"), + renderCell: (params) => { + return arrayToDateString(params.expiryDate) + }, + }, + // { + // name: "status", + // label: t("Status"), + // type: "icon", + // icons: { + // available: , + // unavailable: , + // }, + // colors: { + // available: "success", + // unavailable: "error", + // } + // }, + ], + [t], + ); + return <> + {item ? `${t("Item selected")}: ${item.itemCode} | ${item.itemName} (${t(item.itemType)})` : t("No items are selected yet.")} + + items={inventoryLotLines ?? []} + columns={columns} + pagingController={pagingController} + setPagingController={setPagingController} + totalCount={totalCount} + /> + +} + +export default InventoryLotLineTable; \ No newline at end of file diff --git a/src/components/InventorySearch/InventorySearch.tsx b/src/components/InventorySearch/InventorySearch.tsx index b0440e2..d3866e6 100644 --- a/src/components/InventorySearch/InventorySearch.tsx +++ b/src/components/InventorySearch/InventorySearch.tsx @@ -1,11 +1,15 @@ "use client"; -import { InventoryResult } from "@/app/api/inventory"; +import { InventoryLotLineResult, InventoryResult } from "@/app/api/inventory"; import { useTranslation } from "react-i18next"; import SearchBox, { Criterion } from "../SearchBox"; -import { useCallback, useMemo, useState } from "react"; -import { uniq } from "lodash"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { orderBy, uniq, uniqBy } from "lodash"; import SearchResults, { Column } from "../SearchResults"; import { CheckCircleOutline, DoDisturb } from "@mui/icons-material"; +import InventoryTable from "./InventoryTable"; +import { defaultPagingController } from "../SearchResults/SearchResults"; +import InventoryLotLineTable from "./InventoryLotLineTable"; +import { SearchInventory, SearchInventoryLotLine, fetchInventories, fetchInventoryLotLines } from "@/app/api/inventory/actions"; interface Props { inventories: InventoryResult[]; @@ -31,7 +35,30 @@ type SearchParamNames = keyof SearchQuery; const InventorySearch: React.FC = ({ inventories }) => { const { t } = useTranslation(["inventory", "common"]); - const [filteredInventories, setFilteredInventories] = useState(inventories); + // Inventory + const [filteredInventories, setFilteredInventories] = useState([]); + const [inventoriesPagingController, setInventoriesPagingController] = useState(defaultPagingController) + const [inventoriesTotalCount, setInventoriesTotalCount] = useState(0) + const [item, setItem] = useState(null) + + // Inventory Lot Line + const [filteredInventoryLotLines, setFilteredInventoryLotLines] = useState([]); + const [inventoryLotLinesPagingController, setInventoryLotLinesPagingController] = useState(defaultPagingController) + const [inventoryLotLinesTotalCount, setInventoryLotLinesTotalCount] = useState(0) + + const [inputs, setInputs] = useState>({ + itemId: "", + itemCode: "", + itemName: "", + itemType: "", + onHandQty: "", + onHoldQty: "", + unavailableQty: "", + availableQty: "", + currencyName: "", + status: "", + baseUom: "", + }); const searchCriteria: Criterion[] = useMemo( () => [ @@ -43,88 +70,102 @@ const InventorySearch: React.FC = ({ inventories }) => { type: "select", options: uniq(inventories.map((i) => i.itemType)), }, - { - label: t("Status"), - paramName: "status", - type: "select", - options: uniq(inventories.map((i) => i.status)), - }, + // { + // label: t("Status"), + // paramName: "status", + // type: "select", + // options: uniq(inventories.map((i) => i.status)), + // }, ], [t], ); + const refetchInventoryData = useCallback(async ( + query: Record, + actionType: "reset" | "search" | "paging" + ) => { + const params: SearchInventory = { + code: query?.itemCode ?? '', + name: query?.itemName ?? '', + type: query?.itemType.toLowerCase() === "all" ? '' : query?.itemType ?? '', + pageNum: inventoriesPagingController.pageNum - 1, + pageSize: inventoriesPagingController.pageSize + } + + const response = await fetchInventories(params) + + if (response) { + console.log(response) + setInventoriesTotalCount(response.total); + switch (actionType) { + case "reset": + case "search": + setFilteredInventories(() => response.records); + break; + case "paging": + setFilteredInventories((fi) => + orderBy( + uniqBy([...fi, ...response.records], "id"), + ["id"], ["desc"] + )); + } + } + }, [inventoriesPagingController, setInventoriesPagingController]) + + useEffect(() => { + refetchInventoryData(inputs, "paging") + }, [inventoriesPagingController]) + + const refetchInventoryLotLineData = useCallback(async ( + itemId: number | null, + actionType: "reset" | "search" | "paging" + ) => { + if (!itemId) { + setItem(null) + setInventoryLotLinesTotalCount(0); + setFilteredInventoryLotLines([]) + return + } + + const params: SearchInventoryLotLine = { + itemId: itemId, + pageNum: inventoriesPagingController.pageNum - 1, + pageSize: inventoriesPagingController.pageSize + } + + const response = await fetchInventoryLotLines(params) + + if (response) { + setInventoryLotLinesTotalCount(response.total); + switch (actionType) { + case "reset": + case "search": + setFilteredInventoryLotLines(() => response.records); + break; + case "paging": + setFilteredInventoryLotLines((fi) => + orderBy( + uniqBy([...fi, ...response.records], "id"), + ["id"], ["desc"] + )); + } + } + }, [inventoriesPagingController, setInventoriesPagingController]) + + useEffect(() => { + refetchInventoryData(inputs, "paging") + }, [inventoriesPagingController]) + const onReset = useCallback(() => { - setFilteredInventories(inventories); - }, [inventories]); + refetchInventoryData(inputs, "reset"); + refetchInventoryLotLineData(null, "reset"); + // setFilteredInventories(inventories); + }, []); - const columns = useMemo[]>( - () => [ - { - name: "itemCode", - label: t("Code"), - }, - { - name: "itemName", - label: t("Name"), - }, - { - name: "itemType", - label: t("Type"), - renderCell: (params) => { - return t(params.itemType); - }, - }, - { - name: "availableQty", - label: t("Qty"), - align: "right", - headerAlign: "right", - type: "integer", - }, - { - name: "uomUdfudesc", - label: t("UoM"), - }, - // { - // name: "qtyPerSmallestUnit", - // label: t("Qty Per Smallest Unit"), - // align: "right", - // headerAlign: "right", - // type: "decimal" - // }, - // { - // name: "smallestUnit", - // label: t("Smallest Unit"), - // }, - // { - // name: "price", - // label: t("Price"), - // align: "right", - // sx: { - // alignItems: "right", - // justifyContent: "end", - // } - // }, - // { - // name: "currencyName", - // label: t("Currency"), - // }, - // { - // name: "status", - // label: t("Status"), - // type: "icon", - // icons: { - // available: , - // unavailable: , - // }, - // colors: { - // available: "success", - // unavailable: "error", - // } - // }, - ], - [t], - ); + const onInventoryRowClick = useCallback((item: InventoryResult) => { + setItem(item) + refetchInventoryLotLineData(item.itemId, "search") + }, []) return ( <> @@ -133,28 +174,35 @@ const InventorySearch: React.FC = ({ inventories }) => { onSearch={(query) => { // console.log(query) // console.log(inventories) - setFilteredInventories( - inventories.filter( - (i) => - i.itemCode.toLowerCase().includes(query.itemCode.toLowerCase()) && - i.itemName.toLowerCase().includes(query.itemName.toLowerCase()) && - (query.itemType == "All" || - i.itemType.toLowerCase().includes(query.itemType.toLowerCase())) && - (query.status == "All" || - i.status.toLowerCase().includes(query.status.toLowerCase())), - ), - ); + setInputs(() => query) + refetchInventoryData(query, "search") + // setFilteredInventories( + // inventories.filter( + // (i) => + // i.itemCode.toLowerCase().includes(query.itemCode.toLowerCase()) && + // i.itemName.toLowerCase().includes(query.itemName.toLowerCase()) && + // (query.itemType == "All" || + // i.itemType.toLowerCase().includes(query.itemType.toLowerCase())) && + // (query.status == "All" || + // i.status.toLowerCase().includes(query.status.toLowerCase())), + // ), + // ); }} onReset={onReset} /> - - items={filteredInventories} - columns={columns} - pagingController={{ - pageNum: 0, - pageSize: 0, - // totalCount: 0, - }} + + ); diff --git a/src/components/InventorySearch/InventoryTable.tsx b/src/components/InventorySearch/InventoryTable.tsx new file mode 100644 index 0000000..8bdc452 --- /dev/null +++ b/src/components/InventorySearch/InventoryTable.tsx @@ -0,0 +1,111 @@ +import { InventoryResult } from "@/app/api/inventory"; +import { Dispatch, SetStateAction, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { Column } from "../SearchResults"; +import SearchResults, { defaultPagingController, defaultSetPagingController } from "../SearchResults/SearchResults"; + +interface Props { + inventories: InventoryResult[]; + setPagingController: defaultSetPagingController; + pagingController: typeof defaultPagingController; + totalCount: number; + onRowClick: (item: InventoryResult) => void; +} + +const InventoryTable: React.FC = ({ inventories, pagingController, setPagingController, totalCount, onRowClick }) => { + const { t } = useTranslation(["inventory"]); + + const columns = useMemo[]>( + () => [ + { + name: "itemCode", + label: t("Code"), + }, + { + name: "itemName", + label: t("Name"), + }, + { + name: "itemType", + label: t("Type"), + renderCell: (params) => { + return t(params.itemType); + }, + }, + { + name: "availableQty", + label: t("Available Qty"), + align: "right", + headerAlign: "right", + type: "integer", + }, + { + name: "uomUdfudesc", + label: t("Sales UoM"), + align: "left", + headerAlign: "left", + }, + { + name: "qtyPerSmallestUnit", + label: t("Available Qty Per Smallest Unit"), + align: "right", + headerAlign: "right", + type: "integer", + }, + { + name: "baseUom", + label: t("Base UoM"), + align: "left", + headerAlign: "left", + }, + // { + // name: "qtyPerSmallestUnit", + // label: t("Qty Per Smallest Unit"), + // align: "right", + // headerAlign: "right", + // type: "decimal" + // }, + // { + // name: "smallestUnit", + // label: t("Smallest Unit"), + // }, + // { + // name: "price", + // label: t("Price"), + // align: "right", + // sx: { + // alignItems: "right", + // justifyContent: "end", + // } + // }, + // { + // name: "currencyName", + // label: t("Currency"), + // }, + // { + // name: "status", + // label: t("Status"), + // type: "icon", + // icons: { + // available: , + // unavailable: , + // }, + // colors: { + // available: "success", + // unavailable: "error", + // } + // }, + ], + [t], + ); + return + items={inventories} + columns={columns} + pagingController={pagingController} + setPagingController={setPagingController} + totalCount={totalCount} + onRowClick={onRowClick} + /> +} + +export default InventoryTable; \ No newline at end of file diff --git a/src/components/PickOrderDetail/PickOrderDetail.tsx b/src/components/PickOrderDetail/PickOrderDetail.tsx index 3697dce..64b7b80 100644 --- a/src/components/PickOrderDetail/PickOrderDetail.tsx +++ b/src/components/PickOrderDetail/PickOrderDetail.tsx @@ -137,11 +137,11 @@ const PickOrderDetail: React.FC = ({ consoCode, qc }) => { const pickOrderLineColumns = useMemo( () => [ - { - field: "id", - headerName: "pickOrderLineId", - flex: 1, - }, + // { + // field: "id", + // headerName: "pickOrderLineId", + // flex: 1, + // }, { field: "itemName", headerName: t("item"), @@ -643,7 +643,8 @@ const PickOrderDetail: React.FC = ({ consoCode, qc }) => { const homemade_Qrcode = { // stockInLineId: 156, // eggs // stockInLineId: 162, // chicken wings - stockInLineId: 168, // sesame + // stockInLineId: 168, // sesame + warehouseId: 2 }; useEffect(() => { @@ -677,12 +678,12 @@ const PickOrderDetail: React.FC = ({ consoCode, qc }) => { setIsUploading, ]); - const mannuallyAddRow = useCallback(() => { - getLotDetail(homemade_Qrcode.stockInLineId).then((qrcode) => { - addRow(qrcode); - // scanner.resetScan(); - }); - }, [addRow, getLotDetail, homemade_Qrcode.stockInLineId]); + // const mannuallyAddRow = useCallback(() => { + // getLotDetail(homemade_Qrcode.stockInLineId).then((qrcode) => { + // addRow(qrcode); + // // scanner.resetScan(); + // }); + // }, [addRow, getLotDetail, homemade_Qrcode.stockInLineId]); const validation = useCallback( ( diff --git a/src/components/PickOrderSearch/ConsolidatedPickOrders.tsx b/src/components/PickOrderSearch/ConsolidatedPickOrders.tsx index b5efbd3..b2c2c29 100644 --- a/src/components/PickOrderSearch/ConsolidatedPickOrders.tsx +++ b/src/components/PickOrderSearch/ConsolidatedPickOrders.tsx @@ -64,7 +64,8 @@ const style = { pt: 5, px: 5, pb: 10, - width: 1500, + // width: 1500, + width: { xs: "100%", sm: "100%", md: "100%" }, }; interface DisableButton { releaseBtn: boolean; diff --git a/src/components/PickOrderSearch/CreateForm.tsx b/src/components/PickOrderSearch/CreateForm.tsx index af46e8c..45e7514 100644 --- a/src/components/PickOrderSearch/CreateForm.tsx +++ b/src/components/PickOrderSearch/CreateForm.tsx @@ -107,6 +107,7 @@ const CreateForm: React.FC = ({ items }) => { { field: "itemId", headerName: t("Item"), + // width: 100, flex: 1, editable: true, valueFormatter(params) { @@ -162,6 +163,7 @@ const CreateForm: React.FC = ({ items }) => { { field: "qty", headerName: t("qty"), + // width: 100, flex: 1, type: "number", editable: true, @@ -181,6 +183,7 @@ const CreateForm: React.FC = ({ items }) => { { field: "uom", headerName: t("uom"), + // width: 100, flex: 1, editable: true, // renderEditCell(params: GridRenderEditCellParams) { @@ -257,42 +260,42 @@ const CreateForm: React.FC = ({ items }) => { - { - return ( - - { - 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, - }, - }} - /> - - ); - }} - /> - + { + return ( + + { + 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, + }, + }} + /> + + ); + }} + /> + { @@ -62,7 +62,7 @@ const CreatePickOrderModal: React.FC = ({ return ( <> - + = ({ useEffect(() => { console.log("triggered"); setValue("status", status); - }, []); + }, [setValue, status]); return ( @@ -108,6 +108,18 @@ const EscalationForm: React.FC = ({ spacing={2} sx={{ mt: 0.5 }} > + + + = ({ helperText={errors.acceptedQty?.message} /> + + + + + + = ({ po, qc, warehouse }) => { console.log(checkRes); const newPo = await fetchPoInClient(purchaseOrder.id); setPurchaseOrder(newPo); - }, [checkPolAndCompletePo, fetchPoInClient]); + }, [purchaseOrder.id]); const handleStartPo = useCallback(async () => { const startRes = await startPo(purchaseOrder.id); console.log(startRes); const newPo = await fetchPoInClient(purchaseOrder.id); setPurchaseOrder(newPo); - }, [startPo, fetchPoInClient]); + }, [purchaseOrder.id]); useEffect(() => { setRows(purchaseOrder.pol || []); @@ -127,11 +113,11 @@ const PoDetail: React.FC = ({ po, qc, warehouse }) => { const [stockInLine, setStockInLine] = useState(row.stockInLine); const totalWeight = useMemo( () => calculateWeight(row.qty, row.uom), - [calculateWeight], + [row.qty, row.uom], ); const weightUnit = useMemo( () => returnWeightUnit(row.uom), - [returnWeightUnit], + [row.uom], ); useEffect(() => { @@ -142,7 +128,7 @@ const PoDetail: React.FC = ({ po, qc, warehouse }) => { } else { setCurrStatus("pending".toUpperCase()); } - }, [processedQty]); + }, [processedQty, row.qty]); return ( <> @@ -272,8 +258,9 @@ const PoDetail: React.FC = ({ po, qc, warehouse }) => { }; // break; } - }, [purchaseOrder, handleStartPo, handleCompletePo]); + }, [purchaseOrder.status, t, handleStartPo, handleCompletePo]); + console.log(window.innerWidth) return ( <> = ({ po, qc, warehouse }) => { {buttonData.buttonText} - {/* {purchaseOrder.status.toLowerCase() === "pending" && ( - - - - )} - {purchaseOrder.status.toLowerCase() === "receiving" && ( - - - - )} */} - {/* - - */} = ({ po, qc, warehouse }) => { {/* tab 1 */} - + + {/* */} diff --git a/src/components/PoDetail/PoInputGrid.tsx b/src/components/PoDetail/PoInputGrid.tsx index 4c3c282..a290bb8 100644 --- a/src/components/PoDetail/PoInputGrid.tsx +++ b/src/components/PoDetail/PoInputGrid.tsx @@ -147,7 +147,7 @@ function PoInputGrid({ 0, ); setProcessedQty(processedQty); - }, [entries]); + }, [entries, setProcessedQty]); const handleDelete = useCallback( (id: GridRowId) => () => { @@ -155,6 +155,42 @@ function PoInputGrid({ }, [getRowId], ); + + const closeQcModal = useCallback(() => { + setQcOpen(false); + }, []); + const openQcModal = useCallback(() => { + setQcOpen(true); + }, []); + + const closeStockInModal = useCallback(() => { + setStockInOpen(false); + }, []); + const openStockInModal = useCallback(() => { + setStockInOpen(true); + }, []); + + const closePutAwayModal = useCallback(() => { + setPutAwayOpen(false); + }, []); + const openPutAwayModal = useCallback(() => { + setPutAwayOpen(true); + }, []); + + const closeEscalationModal = useCallback(() => { + setEscalOpen(false); + }, []); + const openEscalationModal = useCallback(() => { + setEscalOpen(true); + }, []); + + const closeRejectModal = useCallback(() => { + setRejectOpen(false); + }, []); + const openRejectModal = useCallback(() => { + setRejectOpen(true); + }, []); + const handleStart = useCallback( (id: GridRowId, params: any) => () => { setBtnIsLoading(true); @@ -189,7 +225,7 @@ function PoInputGrid({ // openStartModal(); }, 200); }, - [createStockInLine], + [setStockInLine], ); const fetchQcDefaultValue = useCallback(async (stockInLineId: GridRowId) => { return await fetchQcResult(stockInLineId as number); @@ -217,7 +253,7 @@ function PoInputGrid({ setBtnIsLoading(false); }, 200); }, - [fetchQcDefaultValue], + [fetchQcDefaultValue, openQcModal], ); const handleEscalation = useCallback( (id: GridRowId, params: any) => () => { @@ -234,7 +270,7 @@ function PoInputGrid({ // setBtnIsLoading(false); }, 200); }, - [], + [openEscalationModal], ); const handleReject = useCallback( @@ -254,7 +290,7 @@ function PoInputGrid({ // printQrcode(params.row); }, 200); }, - [], + [openRejectModal], ); const handleStockIn = useCallback( @@ -274,7 +310,7 @@ function PoInputGrid({ // setBtnIsLoading(false); }, 200); }, - [], + [openStockInModal], ); const handlePutAway = useCallback( @@ -294,7 +330,7 @@ function PoInputGrid({ // setBtnIsLoading(false); }, 200); }, - [], + [openPutAwayModal], ); const printQrcode = useCallback( @@ -310,79 +346,47 @@ function PoInputGrid({ } setBtnIsLoading(false); }, - [fetchPoQrcode, downloadFile], - ); - - const handleQrCode = useCallback( - (id: GridRowId, params: any) => () => { - setRowModesModel((prev) => ({ - ...prev, - [id]: { mode: GridRowModes.View }, - })); - setModalInfo(params.row); - setTimeout(() => { - // open stock in modal - // openPutAwayModal(); - // return the record with its status as pending - // update layout - console.log("delayed"); - printQrcode(params.row); - }, 200); - }, [], ); - const closeQcModal = useCallback(() => { - setQcOpen(false); - }, []); - const openQcModal = useCallback(() => { - setQcOpen(true); - }, []); - - const closeStockInModal = useCallback(() => { - setStockInOpen(false); - }, []); - const openStockInModal = useCallback(() => { - setStockInOpen(true); - }, []); - - const closePutAwayModal = useCallback(() => { - setPutAwayOpen(false); - }, []); - const openPutAwayModal = useCallback(() => { - setPutAwayOpen(true); - }, []); - - const closeEscalationModal = useCallback(() => { - setEscalOpen(false); - }, []); - const openEscalationModal = useCallback(() => { - setEscalOpen(true); - }, []); - - const closeRejectModal = useCallback(() => { - setRejectOpen(false); - }, []); - const openRejectModal = useCallback(() => { - setRejectOpen(true); - }, []); + // const handleQrCode = useCallback( + // (id: GridRowId, params: any) => () => { + // setRowModesModel((prev) => ({ + // ...prev, + // [id]: { mode: GridRowModes.View }, + // })); + // setModalInfo(params.row); + // setTimeout(() => { + // // open stock in modal + // // openPutAwayModal(); + // // return the record with its status as pending + // // update layout + // console.log("delayed"); + // printQrcode(params.row); + // }, 200); + // }, + // [printQrcode], + // ); const columns = useMemo( () => [ { field: "itemNo", headerName: t("itemNo"), - flex: 0.4, + width: 120, + // flex: 0.4, }, { field: "itemName", headerName: t("itemName"), - flex: 0.6, + width: 120, + // flex: 0.6, }, { field: "acceptedQty", headerName: t("acceptedQty"), - flex: 0.5, + // flex: 0.5, + width: 120, type: "number", // editable: true, // replace with tooltip + content @@ -390,7 +394,8 @@ function PoInputGrid({ { field: "uom", headerName: t("uom"), - flex: 0.5, + width: 120, + // flex: 0.5, renderCell: (params) => { return params.row.uom.code; }, @@ -398,7 +403,8 @@ function PoInputGrid({ { field: "weight", headerName: t("weight"), - flex: 0.5, + width: 120, + // flex: 0.5, renderCell: (params) => { const weight = calculateWeight( params.row.acceptedQty, @@ -411,7 +417,8 @@ function PoInputGrid({ { field: "status", headerName: t("status"), - flex: 0.5, + width: 120, + // flex: 0.5, renderCell: (params) => { return t(`${params.row.status}`); }, @@ -423,7 +430,8 @@ function PoInputGrid({ "stock in", )} | ${t("putaway")} | ${t("delete")}`, // headerName: "start | qc | escalation | stock in | putaway | delete", - flex: 1.5, + width: 300, + // flex: 1.5, cellClassName: "actions", getActions: (params) => { // console.log(params.row.status); @@ -494,7 +502,7 @@ function PoInputGrid({ (stockInLineStatusMap[status] >= 3 && stockInLineStatusMap[status] <= 5 && !session?.user?.abilities?.includes("APPROVAL")) - } + } // set _isNew to false after posting // or check status onClick={handleStockIn(params.row.id, params)} @@ -560,7 +568,7 @@ function PoInputGrid({ }, }, ], - [stockInLineStatusMap, btnIsLoading, handleQrCode, handleReject], + [t, handleStart, handleQC, handleEscalation, session?.user?.abilities, handleStockIn, handlePutAway, handleDelete, handleReject], ); const addRow = useCallback(() => { @@ -585,7 +593,7 @@ function PoInputGrid({ // fieldToFocus: "projectId", }, })); - }, [currQty, getRowId]); + }, [currQty, getRowId, itemDetail]); const validation = useCallback( ( newRow: GridRowModel, @@ -599,7 +607,7 @@ function PoInputGrid({ } return Object.keys(error).length > 0 ? error : undefined; }, - [currQty], + [currQty, itemDetail.qty, t], ); const processRowUpdate = useCallback( ( @@ -632,7 +640,7 @@ function PoInputGrid({ setCurrQty(total); return rowToSave; }, - [getRowId, entries], + [validation, entries, setStockInLine, getRowId], ); const onProcessRowUpdateError = useCallback( diff --git a/src/components/PoDetail/PoQcStockInModal.tsx b/src/components/PoDetail/PoQcStockInModal.tsx index 25d846b..a867f79 100644 --- a/src/components/PoDetail/PoQcStockInModal.tsx +++ b/src/components/PoDetail/PoQcStockInModal.tsx @@ -40,6 +40,8 @@ import { fetchPoQrcode } from "@/app/api/pdf/actions"; import UploadContext from "../UploadProvider/UploadProvider"; import useUploadContext from "../UploadProvider/useUploadContext"; import RejectForm from "./RejectForm"; +import { isNullOrUndefined } from "html5-qrcode/esm/core"; +import { isEmpty, isFinite } from "lodash"; dayjs.extend(arraySupport); interface CommonProps extends Omit { @@ -153,9 +155,37 @@ const PoQcStockInModal: React.FC = ({ // } // return date; // }, []); + const accQty = formProps.watch("acceptedQty"); + const productLotNo = formProps.watch("productLotNo"); const checkStockIn = useCallback( (data: ModalFormInput): boolean => { let hasErrors = false; + if (!isFinite(accQty) || accQty! <= 0 ) { + formProps.setError("acceptedQty", { + message: `${t("Accepted qty must greater than")} ${ + 0 + }`, + type: "required", + }); + hasErrors = true; + } else if (accQty! > itemDetail.acceptedQty) { + formProps.setError("acceptedQty", { + message: `${t("Accepted qty must not greater than")} ${ + itemDetail.acceptedQty + }`, + type: "required", + }); + hasErrors = true; + } + + if (isEmpty(productLotNo)) { + formProps.setError("productLotNo", { + message: `${t("Product Lot No must not be empty")}`, + type: "required", + }); + hasErrors = true; + } + if (itemDetail.shelfLife && !data.productionDate && !data.expiryDate) { formProps.setError("productionDate", { message: "Please provide at least one", @@ -184,7 +214,7 @@ const PoQcStockInModal: React.FC = ({ } return hasErrors; }, - [itemDetail, formProps], + [accQty, itemDetail.acceptedQty, itemDetail.shelfLife, productLotNo, formProps, t], ); const checkPutaway = useCallback( diff --git a/src/components/PoDetail/QcForm.tsx b/src/components/PoDetail/QcForm.tsx index 9b19e53..cfc4b77 100644 --- a/src/components/PoDetail/QcForm.tsx +++ b/src/components/PoDetail/QcForm.tsx @@ -97,7 +97,7 @@ const QcForm: React.FC = ({ qc, itemDetail, disabled }) => { useEffect(() => { clearErrors(); validateForm(); - }, [validateForm]); + }, [clearErrors, validateForm]); const columns = useMemo( () => [ diff --git a/src/components/PoDetail/QcSelect.tsx b/src/components/PoDetail/QcSelect.tsx index 4ceebea..b42732b 100644 --- a/src/components/PoDetail/QcSelect.tsx +++ b/src/components/PoDetail/QcSelect.tsx @@ -30,7 +30,7 @@ const QcSelect: React.FC = ({ allQcs, value, error, onQcSelect }) => { const filteredQc = useMemo(() => { // do filtering here if any return allQcs; - }, []); + }, [allQcs]); const options = useMemo(() => { return [ { @@ -44,7 +44,7 @@ const QcSelect: React.FC = ({ allQcs, value, error, onQcSelect }) => { group: "existing", })), ]; - }, [filteredQc]); + }, [t, filteredQc]); const currentValue = options.find((o) => o.value === value) || options[0]; diff --git a/src/components/PoDetail/StockInForm.tsx b/src/components/PoDetail/StockInForm.tsx index e8fc3e1..f846388 100644 --- a/src/components/PoDetail/StockInForm.tsx +++ b/src/components/PoDetail/StockInForm.tsx @@ -96,7 +96,8 @@ const StockInForm: React.FC = ({ console.log(expiryDate); if (expiryDate) clearErrors(); if (productionDate) clearErrors(); - }, [productionDate, expiryDate]); + }, [productionDate, expiryDate, clearErrors]); + return ( @@ -111,6 +112,30 @@ const StockInForm: React.FC = ({ spacing={2} sx={{ mt: 0.5 }} > + + + + + + = ({ // required: "productLotNo required!", })} disabled={disabled} - // error={Boolean(errors.productLotNo)} - // helperText={errors.productLotNo?.message} + error={Boolean(errors.productLotNo)} + helperText={errors.productLotNo?.message} /> @@ -171,7 +196,7 @@ const StockInForm: React.FC = ({ helperText={errors.acceptedQty?.message} /> - + {/* = ({ error={Boolean(errors.acceptedWeight)} helperText={errors.acceptedWeight?.message} /> - + */} ({ {t("All")} {c.options.map((option) => ( - {option} + {t(option)} ))} diff --git a/src/components/SearchResults/SearchResults.tsx b/src/components/SearchResults/SearchResults.tsx index 1130e0b..c4b42fb 100644 --- a/src/components/SearchResults/SearchResults.tsx +++ b/src/components/SearchResults/SearchResults.tsx @@ -100,6 +100,7 @@ interface Props { isAutoPaging?: boolean; checkboxIds?: (string | number)[]; setCheckboxIds?: Dispatch>; + onRowClick?: (item: T) => void; } function isActionColumn( @@ -138,8 +139,8 @@ function convertObjectKeysToLowercase( ): object | undefined { return obj ? Object.fromEntries( - Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value]), - ) + Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value]), + ) : undefined; } @@ -174,6 +175,14 @@ export const defaultPagingController: { pageNum: number; pageSize: number } = { pageNum: 1, pageSize: 10, }; + +export type defaultSetPagingController = Dispatch< + SetStateAction<{ + pageNum: number; + pageSize: number; + }> +> + function SearchResults({ items, columns, @@ -184,6 +193,7 @@ function SearchResults({ totalCount, checkboxIds = [], setCheckboxIds = undefined, + onRowClick = undefined, }: Props) { const [page, setPage] = React.useState(0); const [rowsPerPage, setRowsPerPage] = React.useState(10); @@ -279,40 +289,25 @@ function SearchResults({ {isAutoPaging ? items - .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - .map((item) => { - return ( - { + return ( + { setCheckboxIds - ? (event) => handleRowClick(event, item, columns) + ? handleRowClick(event, item, columns) : undefined + + if (onRowClick) { + onRowClick(item) + } } - role={setCheckboxIds ? "checkbox" : undefined} - > - {columns.map((column, idx) => { - const columnName = column.name; - - return ( - - ); - })} - - ); - }) - : items.map((item) => { - return ( - + } + role={setCheckboxIds ? "checkbox" : undefined} + > {columns.map((column, idx) => { const columnName = column.name; @@ -329,7 +324,27 @@ function SearchResults({ })} ); - })} + }) + : items.map((item) => { + return ( + + {columns.map((column, idx) => { + const columnName = column.name; + + return ( + + ); + })} + + ); + })}
diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index b18f858..eeb5909 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -35,6 +35,7 @@ "Project":"專案", "Product":"產品", "Material":"材料", + "mat":"原料", "FG":"成品", "FG & Material Demand Forecast Detail":"成品及材料需求預測詳情", "View item In-out And inventory Ledger":"查看物料出入庫及庫存日誌", diff --git a/src/i18n/zh/inventory.json b/src/i18n/zh/inventory.json index 1e5a6ce..3928aee 100644 --- a/src/i18n/zh/inventory.json +++ b/src/i18n/zh/inventory.json @@ -7,5 +7,13 @@ "Qty": "數量", "UoM": "單位", "mat": "物料", - "fg": "成品" -} \ No newline at end of file + "fg": "成品", + "Available Qty": "可用數量 (銷售單位)", + "Sales UoM": "銷售單位", + "Available Qty Per Smallest Unit": "可用數量 (基本單位)", + "Base UoM": "基本單位", + "Lot No": "批號", + "Expiry Date": "到期日", + "No items are selected yet.": "未選擇項目", + "Item selected": "已選擇項目" +} diff --git a/src/i18n/zh/purchaseOrder.json b/src/i18n/zh/purchaseOrder.json index 903132c..10ecb8e 100644 --- a/src/i18n/zh/purchaseOrder.json +++ b/src/i18n/zh/purchaseOrder.json @@ -27,7 +27,7 @@ "total weight": "總重量", "weight unit": "重量單位", "price": "價格", - "processed": "已入倉", + "processed": "已處理", "expiryDate": "到期日", "acceptedQty": "接受數量", "weight": "重量",