# Conflicts: # src/app/api/pickOrder/index.ts # src/components/PickOrderSearch/PickOrderSearch.tsxmaster
@@ -55,8 +55,8 @@ export default async function MainLayout({ | |||||
<Stack spacing={2}> | <Stack spacing={2}> | ||||
<I18nProvider namespaces={["common"]}> | <I18nProvider namespaces={["common"]}> | ||||
<Breadcrumb /> | <Breadcrumb /> | ||||
{children} | |||||
</I18nProvider> | </I18nProvider> | ||||
{children} | |||||
</Stack> | </Stack> | ||||
</Box> | </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 PickOrderSearch from "@/components/PickOrderSearch"; | ||||
import { getServerI18n } from "@/i18n"; | import { getServerI18n } from "@/i18n"; | ||||
import { I18nProvider } 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 "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"; | import { cache } from "react"; | ||||
interface PickOrderItemInfo { | interface PickOrderItemInfo { | ||||
@@ -20,14 +18,77 @@ export interface PickOrderResult{ | |||||
status: string, | status: string, | ||||
releasedBy: string, | releasedBy: string, | ||||
items?: PickOrderItemInfo[] | null, | 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 = () => { | 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: { | next: { | ||||
tags: ["pickOrders"] | tags: ["pickOrders"] | ||||
} | } | ||||
@@ -6,8 +6,10 @@ import { serverFetchJson } from "../../utils/fetchUtil"; | |||||
import { BASE_API_URL } from "../../../config/api"; | import { BASE_API_URL } from "../../../config/api"; | ||||
export interface QrCodeInfo { | 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; | newPasswordCheck: string; | ||||
} | } | ||||
export interface NameList { | |||||
id: number | |||||
name: string | |||||
} | |||||
export const fetchUserDetails = cache(async (id: number) => { | export const fetchUserDetails = cache(async (id: number) => { | ||||
return serverFetchJson<UserDetail>(`${BASE_API_URL}/user/${id}`, { | return serverFetchJson<UserDetail>(`${BASE_API_URL}/user/${id}`, { | ||||
next: { tags: ["user"] }, | 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) => { | export const editUser = async (id: number, data: UserInputs) => { | ||||
const newUser = serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, { | const newUser = serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, { | ||||
method: "PUT", | method: "PUT", | ||||
@@ -3,6 +3,11 @@ import { getServerSession } from "next-auth"; | |||||
import { headers } from "next/headers"; | import { headers } from "next/headers"; | ||||
import { redirect } from "next/navigation"; | import { redirect } from "next/navigation"; | ||||
export interface Pageable { | |||||
pageSize?: number | |||||
pageNum?: number | |||||
} | |||||
export type SearchParams = { | export type SearchParams = { | ||||
searchParams: { [key: string]: string | string[] | undefined }; | searchParams: { [key: string]: string | string[] | undefined }; | ||||
} | } | ||||
@@ -67,6 +67,13 @@ export const stockInLineStatusMap: { [status: string]: number } = { | |||||
"rejected": 9, | "rejected": 9, | ||||
}; | }; | ||||
export const pickOrderStatusMap: { [status: string]: number } = { | |||||
"pending": 1, | |||||
"consolidated": 2, | |||||
"released": 3, | |||||
"completed": 4, | |||||
}; | |||||
export const calculateWeight = (qty: number, uom: Uom) => { | export const calculateWeight = (qty: number, uom: Uom) => { | ||||
return qty * (uom.unit2Qty || 1) * (uom.unit3Qty || 1) * (uom.unit4Qty || 1); | return qty * (uom.unit2Qty || 1) * (uom.unit3Qty || 1) * (uom.unit4Qty || 1); | ||||
} | } | ||||
@@ -24,6 +24,7 @@ const pathToLabelMap: { [path: string]: string } = { | |||||
"/do": "Delivery Order", | "/do": "Delivery Order", | ||||
"/pickOrder": "Pick Order", | "/pickOrder": "Pick Order", | ||||
"/po": "Purchase Order", | "/po": "Purchase Order", | ||||
"/dashboard": "dashboard", | |||||
}; | }; | ||||
const Breadcrumb = () => { | const Breadcrumb = () => { | ||||
@@ -52,7 +52,7 @@ const NavigationContent: React.FC = () => { | |||||
{ | { | ||||
icon: <RequestQuote />, | icon: <RequestQuote />, | ||||
label: "Pick Order", | label: "Pick Order", | ||||
path: "/pickOrder", | |||||
path: "/pickorder", | |||||
}, | }, | ||||
// { | // { | ||||
// icon: <RequestQuote />, | // 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 { | 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 { SearchParams } from "@/app/utils/fetchUtil"; | ||||
import { useCallback, useMemo, useState } from "react"; | import { useCallback, useMemo, useState } from "react"; | ||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
import SearchResults, { Column } from "../SearchResults"; | 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 dayjs from "dayjs"; | ||||
import { Button, Grid, Stack, Tab, Tabs, TabsProps } from "@mui/material"; | import { Button, Grid, Stack, Tab, Tabs, TabsProps } from "@mui/material"; | ||||
import PickOrders from "./PickOrders"; | import PickOrders from "./PickOrders"; | ||||
import ConsolidatedPickOrders from "./ConsolidatedPickOrders"; | |||||
import { getServerI18n } from "@/i18n"; | import { getServerI18n } from "@/i18n"; | ||||
interface Props { | 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; | type SearchParamNames = keyof SearchQuery; | ||||
@@ -27,76 +43,134 @@ const PickOrderSearch: React.FC<Props> = ({ | |||||
}) => { | }) => { | ||||
const { t } = useTranslation("pickOrder"); | 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 GeneralLoading from "../General/GeneralLoading"; | ||||
import PickOrderSearch from "./PickOrderSearch"; | import PickOrderSearch from "./PickOrderSearch"; | ||||
@@ -10,7 +10,14 @@ const PickOrderSearchWrapper: React.FC & SubComponents = async () => { | |||||
const [ | const [ | ||||
pickOrders | pickOrders | ||||
] = await Promise.all([ | ] = await Promise.all([ | ||||
fetchPickOrders() | |||||
fetchPickOrders({ | |||||
code: undefined, | |||||
targetDateFrom: undefined, | |||||
targetDateTo: undefined, | |||||
type: undefined, | |||||
status: undefined, | |||||
itemName: undefined, | |||||
}) | |||||
]) | ]) | ||||
return <PickOrderSearch pickOrders={pickOrders}/> | 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 SearchResults, { Column } from "../SearchResults/SearchResults"; | ||||
import { PickOrderResult } from "@/app/api/pickOrder"; | |||||
import { PickOrderResult } from "@/app/api/pickorder"; | |||||
import { useTranslation } from "react-i18next"; | 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 { isEmpty, upperCase, upperFirst } from "lodash"; | ||||
import { arrayToDateString } from "@/app/utils/formatUtil"; | import { arrayToDateString } from "@/app/utils/formatUtil"; | ||||
import { consolidatePickOrder, fetchPickOrderClient } from "@/app/api/pickorder/actions"; | |||||
import useUploadContext from "../UploadProvider/useUploadContext"; | |||||
interface Props { | 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); | setTotalCount(res.total); | ||||
} | } | ||||
}, | }, | ||||
[fetchPoListClient, pagingController] | |||||
[fetchPoListClient] | |||||
); | ); | ||||
useEffect(() => { | useEffect(() => { | ||||
@@ -209,7 +209,24 @@ function SearchResults<T extends ResultWithId>({ | |||||
}; | }; | ||||
// checkbox | // 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) { | if (setCheckboxIds) { | ||||
const selectedIndex = checkboxIds.indexOf(id); | const selectedIndex = checkboxIds.indexOf(id); | ||||
let newSelected: (string | number)[] = []; | let newSelected: (string | number)[] = []; | ||||
@@ -257,7 +274,7 @@ function SearchResults<T extends ResultWithId>({ | |||||
hover | hover | ||||
tabIndex={-1} | tabIndex={-1} | ||||
key={item.id} | key={item.id} | ||||
onClick={setCheckboxIds ? (event) => handleRowClick(event, item.id) : undefined} | |||||
onClick={setCheckboxIds? (event) => handleRowClick(event, item, columns) : undefined} | |||||
role={setCheckboxIds ? "checkbox" : undefined} | role={setCheckboxIds ? "checkbox" : undefined} | ||||
> | > | ||||
{columns.map((column, idx) => { | {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": "搜尋條件", | "Search Criteria": "搜尋條件", | ||||
"All": "全部", | "All": "全部", | ||||
"No options": "沒有選項", | "No options": "沒有選項", | ||||