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