@@ -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" | |||
}, | |||
@@ -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<LotLineInfo>( | |||
`${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<InventoryResultByPage>(`${BASE_API_URL}/inventory/getRecordByPage?${queryStr}`, | |||
{ next: { tags: ["inventories"] } } | |||
) | |||
}) | |||
export const fetchInventoryLotLines = cache(async (data: SearchInventoryLotLine) => { | |||
const queryStr = convertObjToURLSearchParams(data) | |||
return serverFetchJson<InventoryLotLineResultByPage>(`${BASE_API_URL}/inventoryLotLine/getRecordByPage?${queryStr}`, { | |||
next: { tags: ["inventoryLotLines"] }, | |||
}); | |||
}); |
@@ -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(); | |||
}; | |||
@@ -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 | |||
} | |||
@@ -26,3 +26,10 @@ export const convertObjToURLSearchParams = <T extends object>( | |||
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 | |||
} |
@@ -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); | |||
@@ -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<T, E> 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<T, V, E>({ | |||
@@ -126,7 +128,11 @@ function InputDataGrid<T, V, E>({ | |||
const list: TableRow<V, E>[] = 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<GridRowSelectionModel>(() => { | |||
@@ -154,7 +160,7 @@ function InputDataGrid<T, V, E>({ | |||
console.log(errors); | |||
apiRef.current.updateRows([{ ...row, _error: errors }]); | |||
}, | |||
[apiRef, rowModesModel], | |||
[apiRef], | |||
); | |||
const processRowUpdate = useCallback( | |||
@@ -202,7 +208,7 @@ function InputDataGrid<T, V, E>({ | |||
const reset = useCallback(() => { | |||
setRowModesModel({}); | |||
setRows(originalRows); | |||
}, []); | |||
}, [originalRows]); | |||
const handleCancel = useCallback( | |||
(id: GridRowId) => () => { | |||
@@ -219,14 +225,14 @@ function InputDataGrid<T, V, E>({ | |||
); | |||
} | |||
}, | |||
[setRowModesModel, rows], | |||
[rows, getRowId], | |||
); | |||
const handleDelete = useCallback( | |||
(id: GridRowId) => () => { | |||
setRows((prevRows) => prevRows.filter((row) => getRowId(row) !== id)); | |||
}, | |||
[], | |||
[getRowId], | |||
); | |||
const _columns = useMemo<GridColDef[]>( | |||
@@ -281,7 +287,7 @@ function InputDataGrid<T, V, E>({ | |||
// console.log(formKey) | |||
// console.log(rows) | |||
setValue(formKey, rows); | |||
}, [formKey, rows]); | |||
}, [formKey, rows, setValue]); | |||
const footer = ( | |||
<Box display="flex" gap={2} alignItems="center"> | |||
@@ -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<Props> = ({ inventoryLotLines, pagingController, setPagingController, totalCount, item }) => { | |||
const { t } = useTranslation(["inventory"]); | |||
const columns = useMemo<Column<InventoryLotLineResult>[]>( | |||
() => [ | |||
// { | |||
// 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: <CheckCircleOutline fontSize="small"/>, | |||
// unavailable: <DoDisturb fontSize="small"/>, | |||
// }, | |||
// colors: { | |||
// available: "success", | |||
// unavailable: "error", | |||
// } | |||
// }, | |||
], | |||
[t], | |||
); | |||
return <> | |||
<Typography variant="h6">{item ? `${t("Item selected")}: ${item.itemCode} | ${item.itemName} (${t(item.itemType)})` : t("No items are selected yet.")}</Typography> | |||
<SearchResults<InventoryLotLineResult> | |||
items={inventoryLotLines ?? []} | |||
columns={columns} | |||
pagingController={pagingController} | |||
setPagingController={setPagingController} | |||
totalCount={totalCount} | |||
/> | |||
</> | |||
} | |||
export default InventoryLotLineTable; |
@@ -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<Props> = ({ inventories }) => { | |||
const { t } = useTranslation(["inventory", "common"]); | |||
const [filteredInventories, setFilteredInventories] = useState(inventories); | |||
// Inventory | |||
const [filteredInventories, setFilteredInventories] = useState<InventoryResult[]>([]); | |||
const [inventoriesPagingController, setInventoriesPagingController] = useState(defaultPagingController) | |||
const [inventoriesTotalCount, setInventoriesTotalCount] = useState(0) | |||
const [item, setItem] = useState<InventoryResult | null>(null) | |||
// Inventory Lot Line | |||
const [filteredInventoryLotLines, setFilteredInventoryLotLines] = useState<InventoryLotLineResult[]>([]); | |||
const [inventoryLotLinesPagingController, setInventoryLotLinesPagingController] = useState(defaultPagingController) | |||
const [inventoryLotLinesTotalCount, setInventoryLotLinesTotalCount] = useState(0) | |||
const [inputs, setInputs] = useState<Record<SearchParamNames, string>>({ | |||
itemId: "", | |||
itemCode: "", | |||
itemName: "", | |||
itemType: "", | |||
onHandQty: "", | |||
onHoldQty: "", | |||
unavailableQty: "", | |||
availableQty: "", | |||
currencyName: "", | |||
status: "", | |||
baseUom: "", | |||
}); | |||
const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
() => [ | |||
@@ -43,88 +70,102 @@ const InventorySearch: React.FC<Props> = ({ 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<SearchParamNames, string>, | |||
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<Column<InventoryResult>[]>( | |||
() => [ | |||
{ | |||
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: <CheckCircleOutline fontSize="small"/>, | |||
// unavailable: <DoDisturb fontSize="small"/>, | |||
// }, | |||
// 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<Props> = ({ 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} | |||
/> | |||
<SearchResults<InventoryResult> | |||
items={filteredInventories} | |||
columns={columns} | |||
pagingController={{ | |||
pageNum: 0, | |||
pageSize: 0, | |||
// totalCount: 0, | |||
}} | |||
<InventoryTable | |||
inventories={filteredInventories} | |||
pagingController={inventoriesPagingController} | |||
setPagingController={setInventoriesPagingController} | |||
totalCount={inventoriesTotalCount} | |||
onRowClick={onInventoryRowClick} | |||
/> | |||
<InventoryLotLineTable | |||
inventoryLotLines={filteredInventoryLotLines} | |||
pagingController={inventoryLotLinesPagingController} | |||
setPagingController={setInventoryLotLinesPagingController} | |||
totalCount={inventoryLotLinesTotalCount} | |||
item={item} | |||
/> | |||
</> | |||
); | |||
@@ -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<Props> = ({ inventories, pagingController, setPagingController, totalCount, onRowClick }) => { | |||
const { t } = useTranslation(["inventory"]); | |||
const columns = useMemo<Column<InventoryResult>[]>( | |||
() => [ | |||
{ | |||
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: <CheckCircleOutline fontSize="small"/>, | |||
// unavailable: <DoDisturb fontSize="small"/>, | |||
// }, | |||
// colors: { | |||
// available: "success", | |||
// unavailable: "error", | |||
// } | |||
// }, | |||
], | |||
[t], | |||
); | |||
return <SearchResults<InventoryResult> | |||
items={inventories} | |||
columns={columns} | |||
pagingController={pagingController} | |||
setPagingController={setPagingController} | |||
totalCount={totalCount} | |||
onRowClick={onRowClick} | |||
/> | |||
} | |||
export default InventoryTable; |
@@ -137,11 +137,11 @@ const PickOrderDetail: React.FC<Props> = ({ consoCode, qc }) => { | |||
const pickOrderLineColumns = useMemo<GridColDef[]>( | |||
() => [ | |||
{ | |||
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<Props> = ({ 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<Props> = ({ 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( | |||
( | |||
@@ -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; | |||
@@ -107,6 +107,7 @@ const CreateForm: React.FC<Props> = ({ items }) => { | |||
{ | |||
field: "itemId", | |||
headerName: t("Item"), | |||
// width: 100, | |||
flex: 1, | |||
editable: true, | |||
valueFormatter(params) { | |||
@@ -162,6 +163,7 @@ const CreateForm: React.FC<Props> = ({ items }) => { | |||
{ | |||
field: "qty", | |||
headerName: t("qty"), | |||
// width: 100, | |||
flex: 1, | |||
type: "number", | |||
editable: true, | |||
@@ -181,6 +183,7 @@ const CreateForm: React.FC<Props> = ({ items }) => { | |||
{ | |||
field: "uom", | |||
headerName: t("uom"), | |||
// width: 100, | |||
flex: 1, | |||
editable: true, | |||
// renderEditCell(params: GridRenderEditCellParams<PolRow>) { | |||
@@ -257,42 +260,42 @@ const CreateForm: React.FC<Props> = ({ items }) => { | |||
</FormControl> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<Controller | |||
control={control} | |||
name="targetDate" | |||
// rules={{ required: !Boolean(productionDate) }} | |||
render={({ field }) => { | |||
return ( | |||
<LocalizationProvider | |||
dateAdapter={AdapterDayjs} | |||
adapterLocale={`${language}-hk`} | |||
> | |||
<DatePicker | |||
{...field} | |||
sx={{ width: "100%" }} | |||
label={t("targetDate")} | |||
value={targetDate ? dayjs(targetDate) : undefined} | |||
onChange={(date) => { | |||
console.log(date); | |||
if (!date) return; | |||
console.log(date.format(INPUT_DATE_FORMAT)); | |||
setValue("targetDate", date.format(INPUT_DATE_FORMAT)); | |||
// field.onChange(date); | |||
}} | |||
inputRef={field.ref} | |||
slotProps={{ | |||
textField: { | |||
// required: true, | |||
error: Boolean(errors.targetDate?.message), | |||
helperText: errors.targetDate?.message, | |||
}, | |||
}} | |||
/> | |||
</LocalizationProvider> | |||
); | |||
}} | |||
/> | |||
</Grid> | |||
<Controller | |||
control={control} | |||
name="targetDate" | |||
// rules={{ required: !Boolean(productionDate) }} | |||
render={({ field }) => { | |||
return ( | |||
<LocalizationProvider | |||
dateAdapter={AdapterDayjs} | |||
adapterLocale={`${language}-hk`} | |||
> | |||
<DatePicker | |||
{...field} | |||
sx={{ width: "100%" }} | |||
label={t("targetDate")} | |||
value={targetDate ? dayjs(targetDate) : undefined} | |||
onChange={(date) => { | |||
console.log(date); | |||
if (!date) return; | |||
console.log(date.format(INPUT_DATE_FORMAT)); | |||
setValue("targetDate", date.format(INPUT_DATE_FORMAT)); | |||
// field.onChange(date); | |||
}} | |||
inputRef={field.ref} | |||
slotProps={{ | |||
textField: { | |||
// required: true, | |||
error: Boolean(errors.targetDate?.message), | |||
helperText: errors.targetDate?.message, | |||
}, | |||
}} | |||
/> | |||
</LocalizationProvider> | |||
); | |||
}} | |||
/> | |||
</Grid> | |||
</Grid> | |||
<Grid | |||
container | |||
@@ -21,7 +21,7 @@ const style = { | |||
px: 5, | |||
pb: 10, | |||
display: "block", | |||
width: { xs: "60%", sm: "60%", md: "60%" }, | |||
width: { xs: "100%", sm: "100%", md: "100%" }, | |||
}; | |||
interface Props extends Omit<ModalProps, "children"> { | |||
@@ -62,7 +62,7 @@ const CreatePickOrderModal: React.FC<Props> = ({ | |||
return ( | |||
<> | |||
<FormProvider {...formProps}> | |||
<Modal open={open} onClose={closeHandler} sx={{ overflowY: "scroll" }}> | |||
<Modal open={open} onClose={closeHandler}> | |||
<Box | |||
sx={style} | |||
component="form" | |||
@@ -87,7 +87,7 @@ const EscalationForm: React.FC<Props> = ({ | |||
useEffect(() => { | |||
console.log("triggered"); | |||
setValue("status", status); | |||
}, []); | |||
}, [setValue, status]); | |||
return ( | |||
<Grid container justifyContent="flex-start" alignItems="flex-start"> | |||
@@ -108,6 +108,18 @@ const EscalationForm: React.FC<Props> = ({ | |||
spacing={2} | |||
sx={{ mt: 0.5 }} | |||
> | |||
<Grid item xs={12}> | |||
<TextField | |||
label={t("productLotNo")} | |||
fullWidth | |||
{...register("productLotNo", { | |||
required: "productLotNo required!", | |||
})} | |||
disabled={disabled} | |||
error={Boolean(errors.productLotNo)} | |||
helperText={errors.productLotNo?.message} | |||
/> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField | |||
label={t("reportQty")} | |||
@@ -124,6 +136,24 @@ const EscalationForm: React.FC<Props> = ({ | |||
helperText={errors.acceptedQty?.message} | |||
/> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField | |||
label={t("uom")} | |||
fullWidth | |||
disabled={true} | |||
defaultValue={itemDetail.uom.code} | |||
/> | |||
</Grid> | |||
<Grid item xs={12}> | |||
<TextField | |||
label={t("remarks")} | |||
fullWidth | |||
{...register("remarks", { | |||
})} | |||
disabled={disabled} | |||
/> | |||
</Grid> | |||
</Grid> | |||
<Grid | |||
container | |||
@@ -41,41 +41,27 @@ import { | |||
fetchStockInLineInfo, | |||
PurchaseQcResult, | |||
startPo, | |||
testFetch, | |||
} from "@/app/api/po/actions"; | |||
import { | |||
use, | |||
useCallback, | |||
useContext, | |||
useEffect, | |||
useMemo, | |||
useState, | |||
} from "react"; | |||
import { FormProvider, useForm } from "react-hook-form"; | |||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; | |||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; | |||
import InputDataGrid, { | |||
TableRow as InputTableRow, | |||
} from "../InputDataGrid/InputDataGrid"; | |||
import PoInputGrid from "./PoInputGrid"; | |||
import { QcItemWithChecks } from "@/app/api/qc"; | |||
import { useSearchParams } from "next/navigation"; | |||
import { WarehouseResult } from "@/app/api/warehouse"; | |||
import { calculateWeight, returnWeightUnit } from "@/app/utils/formatUtil"; | |||
import QrCodeScanner from "../QrCodeScanner"; | |||
import { CameraDevice, Html5Qrcode } from "html5-qrcode"; | |||
import { CameraContext } from "../Cameras/CameraProvider"; | |||
import StyledDataGrid from "../StyledDataGrid"; | |||
import { QrCodeInfo } from "@/app/api/qrcode"; | |||
import { fetchQcResult } from "@/app/api/qc/actions"; | |||
import PoQcStockInModal from "./PoQcStockInModal"; | |||
import ReactQrCodeScannerModal, { | |||
ScannerConfig, | |||
} from "../ReactQrCodeScanner/ReactQrCodeScanner"; | |||
import QrModal from "./QrModal"; | |||
import { PlayArrow } from "@mui/icons-material"; | |||
import DoneIcon from "@mui/icons-material/Done"; | |||
import { QrCode } from "../QrCode"; | |||
import { getCustomWidth } from "@/app/utils/commonUtil"; | |||
type Props = { | |||
po: PoResult; | |||
@@ -106,14 +92,14 @@ const PoDetail: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ po, qc, warehouse }) => { | |||
} else { | |||
setCurrStatus("pending".toUpperCase()); | |||
} | |||
}, [processedQty]); | |||
}, [processedQty, row.qty]); | |||
return ( | |||
<> | |||
@@ -272,8 +258,9 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||
}; | |||
// break; | |||
} | |||
}, [purchaseOrder, handleStartPo, handleCompletePo]); | |||
}, [purchaseOrder.status, t, handleStartPo, handleCompletePo]); | |||
console.log(window.innerWidth) | |||
return ( | |||
<> | |||
<Stack | |||
@@ -301,20 +288,7 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||
{buttonData.buttonText} | |||
</Button> | |||
</Grid> | |||
{/* {purchaseOrder.status.toLowerCase() === "pending" && ( | |||
<Grid item> | |||
<Button onClick={handleStartPo}>Start</Button> | |||
</Grid> | |||
)} | |||
{purchaseOrder.status.toLowerCase() === "receiving" && ( | |||
<Grid item> | |||
<Button onClick={handleCompletePo}>Complete</Button> | |||
</Grid> | |||
)} */} | |||
</Grid> | |||
{/* <Grid container xs={12} justifyContent="space-between"> | |||
<Button onClick={handleCompletePo}>Complete</Button> | |||
</Grid> */} | |||
<Grid container xs={12} justifyContent="space-between"> | |||
<Grid item xs={8}> | |||
<Tabs | |||
@@ -346,7 +320,8 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||
</Grid> | |||
{/* tab 1 */} | |||
<Grid sx={{ display: tabIndex === 0 ? "block" : "none" }}> | |||
<TableContainer component={Paper}> | |||
<TableContainer component={Paper} sx={{ width: 'fit-content', overflow: 'auto' }}> | |||
{/* <TableContainer component={Paper} sx={{width: getCustomWidth(), overflow: 'auto' }}> */} | |||
<Table aria-label="collapsible table" stickyHeader> | |||
<TableHead> | |||
<TableRow> | |||
@@ -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<GridColDef[]>( | |||
() => [ | |||
{ | |||
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<StockInLineRow>, | |||
@@ -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( | |||
@@ -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<ModalProps, "children"> { | |||
@@ -153,9 +155,37 @@ const PoQcStockInModal: React.FC<Props> = ({ | |||
// } | |||
// 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<Props> = ({ | |||
} | |||
return hasErrors; | |||
}, | |||
[itemDetail, formProps], | |||
[accQty, itemDetail.acceptedQty, itemDetail.shelfLife, productLotNo, formProps, t], | |||
); | |||
const checkPutaway = useCallback( | |||
@@ -97,7 +97,7 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||
useEffect(() => { | |||
clearErrors(); | |||
validateForm(); | |||
}, [validateForm]); | |||
}, [clearErrors, validateForm]); | |||
const columns = useMemo<GridColDef[]>( | |||
() => [ | |||
@@ -30,7 +30,7 @@ const QcSelect: React.FC<Props> = ({ 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<Props> = ({ allQcs, value, error, onQcSelect }) => { | |||
group: "existing", | |||
})), | |||
]; | |||
}, [filteredQc]); | |||
}, [t, filteredQc]); | |||
const currentValue = options.find((o) => o.value === value) || options[0]; | |||
@@ -96,7 +96,8 @@ const StockInForm: React.FC<Props> = ({ | |||
console.log(expiryDate); | |||
if (expiryDate) clearErrors(); | |||
if (productionDate) clearErrors(); | |||
}, [productionDate, expiryDate]); | |||
}, [productionDate, expiryDate, clearErrors]); | |||
return ( | |||
<Grid container justifyContent="flex-start" alignItems="flex-start"> | |||
<Grid item xs={12}> | |||
@@ -111,6 +112,30 @@ const StockInForm: React.FC<Props> = ({ | |||
spacing={2} | |||
sx={{ mt: 0.5 }} | |||
> | |||
<Grid item xs={6}> | |||
<TextField | |||
label={t("dnNo")} | |||
fullWidth | |||
{...register("dnNo", { | |||
// required: "productLotNo required!", | |||
})} | |||
disabled={disabled} | |||
// error={Boolean(errors.productLotNo)} | |||
// helperText={errors.productLotNo?.message} | |||
/> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField | |||
label={t("invoiceNo")} | |||
fullWidth | |||
{...register("invoiceNo", { | |||
// required: "productLotNo required!", | |||
})} | |||
disabled={disabled} | |||
// error={Boolean(errors.productLotNo)} | |||
// helperText={errors.productLotNo?.message} | |||
/> | |||
</Grid> | |||
<Grid item xs={4}> | |||
<TextField | |||
label={t("productLotNo")} | |||
@@ -119,8 +144,8 @@ const StockInForm: React.FC<Props> = ({ | |||
// required: "productLotNo required!", | |||
})} | |||
disabled={disabled} | |||
// error={Boolean(errors.productLotNo)} | |||
// helperText={errors.productLotNo?.message} | |||
error={Boolean(errors.productLotNo)} | |||
helperText={errors.productLotNo?.message} | |||
/> | |||
</Grid> | |||
<Grid item xs={4}> | |||
@@ -171,7 +196,7 @@ const StockInForm: React.FC<Props> = ({ | |||
helperText={errors.acceptedQty?.message} | |||
/> | |||
</Grid> | |||
<Grid item xs={4}> | |||
{/* <Grid item xs={4}> | |||
<TextField | |||
label={t("acceptedWeight")} | |||
fullWidth | |||
@@ -182,7 +207,7 @@ const StockInForm: React.FC<Props> = ({ | |||
error={Boolean(errors.acceptedWeight)} | |||
helperText={errors.acceptedWeight?.message} | |||
/> | |||
</Grid> | |||
</Grid> */} | |||
<Grid item xs={4}> | |||
<Controller | |||
control={control} | |||
@@ -248,7 +248,7 @@ function SearchBox<T extends string>({ | |||
<MenuItem value={"All"}>{t("All")}</MenuItem> | |||
{c.options.map((option) => ( | |||
<MenuItem key={option} value={option}> | |||
{option} | |||
{t(option)} | |||
</MenuItem> | |||
))} | |||
</Select> | |||
@@ -100,6 +100,7 @@ interface Props<T extends ResultWithId> { | |||
isAutoPaging?: boolean; | |||
checkboxIds?: (string | number)[]; | |||
setCheckboxIds?: Dispatch<SetStateAction<(string | number)[]>>; | |||
onRowClick?: (item: T) => void; | |||
} | |||
function isActionColumn<T extends ResultWithId>( | |||
@@ -138,8 +139,8 @@ function convertObjectKeysToLowercase<T extends object>( | |||
): 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<T extends ResultWithId>({ | |||
items, | |||
columns, | |||
@@ -184,6 +193,7 @@ function SearchResults<T extends ResultWithId>({ | |||
totalCount, | |||
checkboxIds = [], | |||
setCheckboxIds = undefined, | |||
onRowClick = undefined, | |||
}: Props<T>) { | |||
const [page, setPage] = React.useState(0); | |||
const [rowsPerPage, setRowsPerPage] = React.useState(10); | |||
@@ -279,40 +289,25 @@ function SearchResults<T extends ResultWithId>({ | |||
<TableBody> | |||
{isAutoPaging | |||
? items | |||
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) | |||
.map((item) => { | |||
return ( | |||
<TableRow | |||
hover | |||
tabIndex={-1} | |||
key={item.id} | |||
onClick={ | |||
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) | |||
.map((item) => { | |||
return ( | |||
<TableRow | |||
hover | |||
tabIndex={-1} | |||
key={item.id} | |||
onClick={(event) => { | |||
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 ( | |||
<TabelCells | |||
key={`${columnName.toString()}-${idx}`} | |||
column={column} | |||
columnName={columnName} | |||
idx={idx} | |||
item={item} | |||
checkboxIds={checkboxIds} | |||
/> | |||
); | |||
})} | |||
</TableRow> | |||
); | |||
}) | |||
: items.map((item) => { | |||
return ( | |||
<TableRow hover tabIndex={-1} key={item.id}> | |||
} | |||
role={setCheckboxIds ? "checkbox" : undefined} | |||
> | |||
{columns.map((column, idx) => { | |||
const columnName = column.name; | |||
@@ -329,7 +324,27 @@ function SearchResults<T extends ResultWithId>({ | |||
})} | |||
</TableRow> | |||
); | |||
})} | |||
}) | |||
: items.map((item) => { | |||
return ( | |||
<TableRow hover tabIndex={-1} key={item.id}> | |||
{columns.map((column, idx) => { | |||
const columnName = column.name; | |||
return ( | |||
<TabelCells | |||
key={`${columnName.toString()}-${idx}`} | |||
column={column} | |||
columnName={columnName} | |||
idx={idx} | |||
item={item} | |||
checkboxIds={checkboxIds} | |||
/> | |||
); | |||
})} | |||
</TableRow> | |||
); | |||
})} | |||
</TableBody> | |||
</Table> | |||
</TableContainer> | |||
@@ -35,6 +35,7 @@ | |||
"Project":"專案", | |||
"Product":"產品", | |||
"Material":"材料", | |||
"mat":"原料", | |||
"FG":"成品", | |||
"FG & Material Demand Forecast Detail":"成品及材料需求預測詳情", | |||
"View item In-out And inventory Ledger":"查看物料出入庫及庫存日誌", | |||
@@ -7,5 +7,13 @@ | |||
"Qty": "數量", | |||
"UoM": "單位", | |||
"mat": "物料", | |||
"fg": "成品" | |||
} | |||
"fg": "成品", | |||
"Available Qty": "可用數量 (銷售單位)", | |||
"Sales UoM": "銷售單位", | |||
"Available Qty Per Smallest Unit": "可用數量 (基本單位)", | |||
"Base UoM": "基本單位", | |||
"Lot No": "批號", | |||
"Expiry Date": "到期日", | |||
"No items are selected yet.": "未選擇項目", | |||
"Item selected": "已選擇項目" | |||
} |
@@ -27,7 +27,7 @@ | |||
"total weight": "總重量", | |||
"weight unit": "重量單位", | |||
"price": "價格", | |||
"processed": "已入倉", | |||
"processed": "已處理", | |||
"expiryDate": "到期日", | |||
"acceptedQty": "接受數量", | |||
"weight": "重量", | |||