@@ -67,6 +67,25 @@ export interface isCorrectMachineUsedResponse<T> { | |||||
entity: T; | entity: T; | ||||
} | } | ||||
export interface JobOrderDetail { | |||||
id: number; | |||||
code: string; | |||||
name: string; | |||||
reqQty: number; | |||||
uom: string; | |||||
pickLines: any[]; | |||||
status: string; | |||||
} | |||||
export const fetchJobOrderDetailByCode = cache(async (code: string) => { | |||||
return serverFetchJson<JobOrderDetail>( | |||||
`${BASE_API_URL}/jo/detailByCode/${code}`, | |||||
{ | |||||
method: "GET", | |||||
next: { tags: ["jo"] }, | |||||
}, | |||||
); | |||||
}); | |||||
export const isOperatorExist = async (username: string) => { | export const isOperatorExist = async (username: string) => { | ||||
const isExist = await serverFetchJson<IsOperatorExistResponse<Operator>>( | const isExist = await serverFetchJson<IsOperatorExistResponse<Operator>>( | ||||
`${BASE_API_URL}/jop/isOperatorExist`, | `${BASE_API_URL}/jop/isOperatorExist`, | ||||
@@ -102,7 +102,7 @@ export interface GetPickOrderLineInfo { | |||||
itemId: number; | itemId: number; | ||||
itemCode: string; | itemCode: string; | ||||
itemName: string; | itemName: string; | ||||
availableQty: number; | |||||
availableQty: number| null; | |||||
requiredQty: number; | requiredQty: number; | ||||
uomCode: string; | uomCode: string; | ||||
uomDesc: string; | uomDesc: string; | ||||
@@ -142,7 +142,17 @@ export interface PickOrderLotDetailResponse { | |||||
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'; | lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'; | ||||
} | } | ||||
export interface GetPickOrderLineInfo { | |||||
id: number; | |||||
itemId: number; | |||||
itemCode: string; | |||||
itemName: string; | |||||
availableQty: number; | |||||
requiredQty: number; | |||||
uomCode: string; | |||||
uomDesc: string; | |||||
suggestedList: any[]; | |||||
} | |||||
export const fetchAllPickOrderDetails = cache(async () => { | export const fetchAllPickOrderDetails = cache(async () => { | ||||
return serverFetchJson<GetPickOrderInfoResponse>( | return serverFetchJson<GetPickOrderInfoResponse>( | ||||
`${BASE_API_URL}/pickOrder/detail`, | `${BASE_API_URL}/pickOrder/detail`, | ||||
@@ -182,7 +192,7 @@ export const createPickOrder = async (data: SavePickOrderRequest) => { | |||||
return po; | return po; | ||||
} | } | ||||
export const consolidatePickOrder = async (ids: number[]) => { | |||||
export const assignPickOrder = async (ids: number[]) => { | |||||
const pickOrder = await serverFetchJson<any>( | const pickOrder = await serverFetchJson<any>( | ||||
`${BASE_API_URL}/pickOrder/conso`, | `${BASE_API_URL}/pickOrder/conso`, | ||||
{ | { | ||||
@@ -231,6 +241,30 @@ export const fetchPickOrderClient = cache( | |||||
}, | }, | ||||
); | ); | ||||
export const fetchPickOrderWithStockClient = cache( | |||||
async (queryParams?: Record<string, any>) => { | |||||
if (queryParams) { | |||||
const queryString = new URLSearchParams(queryParams).toString(); | |||||
return serverFetchJson<RecordsRes<GetPickOrderInfo[]>>( | |||||
`${BASE_API_URL}/pickOrder/getRecordByPageWithStock?${queryString}`, | |||||
{ | |||||
method: "GET", | |||||
next: { tags: ["pickorder"] }, | |||||
}, | |||||
); | |||||
} else { | |||||
return serverFetchJson<RecordsRes<GetPickOrderInfo[]>>( | |||||
`${BASE_API_URL}/pickOrder/getRecordByPageWithStock`, | |||||
{ | |||||
method: "GET", | |||||
next: { tags: ["pickorder"] }, | |||||
}, | |||||
); | |||||
} | |||||
}, | |||||
); | |||||
export const fetchConsoPickOrderClient = cache( | export const fetchConsoPickOrderClient = cache( | ||||
async (queryParams?: Record<string, any>) => { | async (queryParams?: Record<string, any>) => { | ||||
if (queryParams) { | if (queryParams) { | ||||
@@ -57,6 +57,7 @@ export interface ItemCombo { | |||||
uomId: number, | uomId: number, | ||||
uom: string, | uom: string, | ||||
group?: string, | group?: string, | ||||
currentStockBalance?: number, | |||||
} | } | ||||
export const fetchAllItemsInClient = cache(async () => { | export const fetchAllItemsInClient = cache(async () => { | ||||
@@ -0,0 +1,490 @@ | |||||
"use client"; | |||||
import { | |||||
Autocomplete, | |||||
Box, | |||||
Button, | |||||
CircularProgress, | |||||
FormControl, | |||||
Grid, | |||||
Modal, | |||||
TextField, | |||||
Typography, | |||||
Accordion, | |||||
AccordionSummary, | |||||
AccordionDetails, | |||||
Table, | |||||
TableBody, | |||||
TableCell, | |||||
TableContainer, | |||||
TableHead, | |||||
TableRow, | |||||
Paper, | |||||
} from "@mui/material"; | |||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; | |||||
import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import SearchResults, { Column } from "../SearchResults/SearchResults"; | |||||
import { | |||||
PickOrderResult, | |||||
} from "@/app/api/pickOrder"; | |||||
import { | |||||
assignPickOrder, | |||||
fetchPickOrderClient, | |||||
fetchPickOrderWithStockClient, | |||||
releasePickOrder, | |||||
ReleasePickOrderInputs, | |||||
GetPickOrderInfo, | |||||
GetPickOrderLineInfo, | |||||
} from "@/app/api/pickOrder/actions"; | |||||
import { fetchNameList, NameList } from "@/app/api/user/actions"; | |||||
import { | |||||
FormProvider, | |||||
useForm, | |||||
} from "react-hook-form"; | |||||
import { isEmpty, upperCase, upperFirst } from "lodash"; | |||||
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||||
import useUploadContext from "../UploadProvider/useUploadContext"; | |||||
import dayjs from "dayjs"; | |||||
import arraySupport from "dayjs/plugin/arraySupport"; | |||||
import SearchBox, { Criterion } from "../SearchBox"; | |||||
import { flatten, intersectionWith, sortBy, uniqBy } from "lodash"; | |||||
import { arrayToDayjs } from "@/app/utils/formatUtil"; | |||||
dayjs.extend(arraySupport); | |||||
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: { xs: "100%", sm: "100%", md: "100%" }, | |||||
}; | |||||
const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||||
const { t } = useTranslation("pickOrder"); | |||||
const { setIsUploading } = useUploadContext(); | |||||
// State for Pick Orders | |||||
const [selectedRows, setSelectedRows] = useState<(string | number)[]>([]); | |||||
const [filteredPickOrder, setFilteredPickOrder] = useState([] as GetPickOrderInfo[]); | |||||
const [isLoadingPickOrders, setIsLoadingPickOrders] = useState(false); | |||||
const [pagingController, setPagingController] = useState({ | |||||
pageNum: 0, | |||||
pageSize: 10, | |||||
}); | |||||
const [totalCountPickOrders, setTotalCountPickOrders] = useState<number>(); | |||||
// State for Assign & Release Modal | |||||
const [modalOpen, setModalOpen] = useState(false); | |||||
const [usernameList, setUsernameList] = useState<NameList[]>([]); | |||||
// Add search state | |||||
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | |||||
const [originalPickOrderData, setOriginalPickOrderData] = useState([] as GetPickOrderInfo[]); | |||||
const formProps = useForm<ReleasePickOrderInputs>(); | |||||
const errors = formProps.formState.errors; | |||||
// Fetch Pick Orders with Stock Information | |||||
const fetchNewPagePickOrder = useCallback( | |||||
async ( | |||||
pagingController: Record<string, number>, | |||||
filterArgs: Record<string, number>, | |||||
) => { | |||||
setIsLoadingPickOrders(true); | |||||
const params = { | |||||
...pagingController, | |||||
...filterArgs, | |||||
}; | |||||
const res = await fetchPickOrderWithStockClient(params); | |||||
if (res) { | |||||
console.log(res); | |||||
setFilteredPickOrder(res.records); | |||||
setOriginalPickOrderData(res.records); // Store original data | |||||
setTotalCountPickOrders(res.total); | |||||
} | |||||
setIsLoadingPickOrders(false); | |||||
}, | |||||
[], | |||||
); | |||||
// Add search criteria | |||||
const searchCriteria: Criterion<any>[] = useMemo( | |||||
() => [ | |||||
{ | |||||
label: t("Pick Order Code"), | |||||
paramName: "code", | |||||
type: "text" | |||||
}, | |||||
{ | |||||
label: t("Type"), | |||||
paramName: "type", | |||||
type: "autocomplete", | |||||
options: sortBy( | |||||
uniqBy( | |||||
originalPickOrderData.map((po) => ({ | |||||
value: po.type, | |||||
label: t(upperCase(po.type)), | |||||
})), | |||||
"value", | |||||
), | |||||
"label", | |||||
), | |||||
}, | |||||
{ | |||||
label: t("Target Date From"), | |||||
label2: t("Target Date To"), | |||||
paramName: "targetDate", | |||||
type: "dateRange", | |||||
}, | |||||
{ | |||||
label: t("Status"), | |||||
paramName: "status", | |||||
type: "autocomplete", | |||||
options: sortBy( | |||||
uniqBy( | |||||
originalPickOrderData.map((po) => ({ | |||||
value: po.status, | |||||
label: t(upperFirst(po.status)), | |||||
})), | |||||
"value", | |||||
), | |||||
"label", | |||||
), | |||||
}, | |||||
], | |||||
[originalPickOrderData, t], | |||||
); | |||||
// Add search handler | |||||
const handleSearch = useCallback((query: Record<string, any>) => { | |||||
console.log("AssignAndRelease search triggered with query:", query); | |||||
setSearchQuery({ ...query }); | |||||
// Apply search filters to the data | |||||
const filtered = originalPickOrderData.filter((po) => { | |||||
const poTargetDateStr = arrayToDayjs(po.targetDate); | |||||
const codeMatch = !query.code || | |||||
po.code?.toLowerCase().includes((query.code || "").toLowerCase()); | |||||
const dateMatch = !query.targetDate || | |||||
poTargetDateStr.isSame(query.targetDate) || | |||||
poTargetDateStr.isAfter(query.targetDate); | |||||
const dateToMatch = !query.targetDateTo || | |||||
poTargetDateStr.isSame(query.targetDateTo) || | |||||
poTargetDateStr.isBefore(query.targetDateTo); | |||||
const statusMatch = !query.status || | |||||
query.status.toLowerCase() === "all" || | |||||
po.status?.toLowerCase().includes((query.status || "").toLowerCase()); | |||||
const typeMatch = !query.type || | |||||
query.type.toLowerCase() === "all" || | |||||
po.type?.toLowerCase().includes((query.type || "").toLowerCase()); | |||||
return codeMatch && dateMatch && dateToMatch && statusMatch && typeMatch; | |||||
}); | |||||
setFilteredPickOrder(filtered); | |||||
}, [originalPickOrderData]); | |||||
// Add reset handler | |||||
const handleReset = useCallback(() => { | |||||
setSearchQuery({}); | |||||
// Reset to original data | |||||
setFilteredPickOrder(originalPickOrderData); | |||||
}, [originalPickOrderData]); | |||||
// Handle Assign & Release | |||||
const handleAssignAndRelease = useCallback(async (data: ReleasePickOrderInputs) => { | |||||
if (selectedRows.length === 0) return; | |||||
setIsUploading(true); | |||||
try { | |||||
// First, assign the pick orders | |||||
const assignRes = await assignPickOrder(selectedRows as number[]); | |||||
if (assignRes) { | |||||
console.log("Assign successful:", assignRes); | |||||
// Get the assign code from the response | |||||
const consoCode = assignRes.consoCode || assignRes.code; | |||||
if (consoCode) { | |||||
// Then, release the assign pick order | |||||
const releaseData = { | |||||
consoCode: consoCode, | |||||
assignTo: data.assignTo | |||||
}; | |||||
const releaseRes = await releasePickOrder(releaseData); | |||||
if (releaseRes) { | |||||
console.log("Release successful:", releaseRes); | |||||
setModalOpen(false); | |||||
// Clear selected rows | |||||
setSelectedRows([]); | |||||
// Refresh the pick orders list | |||||
fetchNewPagePickOrder(pagingController, filterArgs); | |||||
} | |||||
} | |||||
} | |||||
} catch (error) { | |||||
console.error("Error in assign and release:", error); | |||||
} finally { | |||||
setIsUploading(false); | |||||
} | |||||
}, [selectedRows, setIsUploading, fetchNewPagePickOrder, pagingController, filterArgs]); | |||||
// Open assign & release modal | |||||
const openAssignModal = useCallback(() => { | |||||
setModalOpen(true); | |||||
// Reset form | |||||
formProps.reset(); | |||||
}, [formProps]); | |||||
// Load data | |||||
useEffect(() => { | |||||
fetchNewPagePickOrder(pagingController, filterArgs); | |||||
}, [fetchNewPagePickOrder, pagingController, filterArgs]); | |||||
// Load username list | |||||
useEffect(() => { | |||||
const loadUsernameList = async () => { | |||||
try { | |||||
const res = await fetchNameList(); | |||||
if (res) { | |||||
setUsernameList(res); | |||||
} | |||||
} catch (error) { | |||||
console.error("Error loading username list:", error); | |||||
} | |||||
}; | |||||
loadUsernameList(); | |||||
}, []); | |||||
// Pick Orders columns with detailed item information | |||||
const pickOrderColumns = useMemo<Column<GetPickOrderInfo>[]>( | |||||
() => [ | |||||
{ | |||||
name: "id", | |||||
label: "", | |||||
type: "checkbox", | |||||
disabled: (params) => { | |||||
return !isEmpty(params.consoCode); | |||||
}, | |||||
}, | |||||
{ | |||||
name: "code", | |||||
label: t("Pick Order Code"), | |||||
}, | |||||
{ | |||||
name: "pickOrderLines", | |||||
label: t("Items"), | |||||
renderCell: (params) => { | |||||
if (!params.pickOrderLines || params.pickOrderLines.length === 0) return ""; | |||||
return ( | |||||
<Accordion sx={{ boxShadow: 'none', '&:before': { display: 'none' } }}> | |||||
<AccordionSummary | |||||
expandIcon={<ExpandMoreIcon />} | |||||
sx={{ minHeight: 'auto', padding: 0 }} | |||||
> | |||||
<Typography variant="body2"> | |||||
{params.pickOrderLines.length} items | |||||
</Typography> | |||||
</AccordionSummary> | |||||
<AccordionDetails sx={{ padding: 1 }}> | |||||
<TableContainer component={Paper} sx={{ maxHeight: 200 }}> | |||||
<Table size="small"> | |||||
<TableHead> | |||||
<TableRow> | |||||
<TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}>{t("Item Name")}</TableCell> | |||||
<TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}>{t("Required Qty")}</TableCell> | |||||
<TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}>{t("Available Qty")}</TableCell> | |||||
<TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}>{t("Unit")}</TableCell> | |||||
</TableRow> | |||||
</TableHead> | |||||
<TableBody> | |||||
{params.pickOrderLines.map((line: GetPickOrderLineInfo, index: number) => ( | |||||
<TableRow key={index}> | |||||
<TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}> | |||||
{line.itemName} | |||||
</TableCell> | |||||
<TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}> | |||||
{line.requiredQty} | |||||
</TableCell> | |||||
<TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}> | |||||
<Typography | |||||
variant="caption" | |||||
//color={line.availableQty && line.availableQty >= line.requiredQty ? 'success.main' : 'error.main'} | |||||
> | |||||
{line.availableQty ?? 0} | |||||
</Typography> | |||||
</TableCell> | |||||
<TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}> | |||||
{line.uomDesc} | |||||
</TableCell> | |||||
</TableRow> | |||||
))} | |||||
</TableBody> | |||||
</Table> | |||||
</TableContainer> | |||||
</AccordionDetails> | |||||
</Accordion> | |||||
); | |||||
}, | |||||
}, | |||||
{ | |||||
name: "targetDate", | |||||
label: t("Target Date"), | |||||
renderCell: (params) => { | |||||
return ( | |||||
dayjs(params.targetDate) | |||||
.add(-1, "month") | |||||
.format(OUTPUT_DATE_FORMAT) | |||||
); | |||||
}, | |||||
}, | |||||
{ | |||||
name: "status", | |||||
label: t("Status"), | |||||
renderCell: (params) => { | |||||
return upperFirst(params.status); | |||||
}, | |||||
}, | |||||
], | |||||
[t], | |||||
); | |||||
return ( | |||||
<> | |||||
{/* Search Box */} | |||||
<SearchBox | |||||
criteria={searchCriteria} | |||||
onSearch={handleSearch} | |||||
onReset={handleReset} | |||||
/> | |||||
{/* Pick Orders View */} | |||||
<Grid container rowGap={1}> | |||||
{/* Remove the button from here */} | |||||
<Grid item xs={12}> | |||||
{isLoadingPickOrders ? ( | |||||
<CircularProgress size={40} /> | |||||
) : ( | |||||
<SearchResults<GetPickOrderInfo> | |||||
items={filteredPickOrder} | |||||
columns={pickOrderColumns} | |||||
pagingController={pagingController} | |||||
setPagingController={setPagingController} | |||||
totalCount={totalCountPickOrders} | |||||
checkboxIds={selectedRows!} | |||||
setCheckboxIds={setSelectedRows} | |||||
/> | |||||
)} | |||||
</Grid> | |||||
{/* Add the button below the table */} | |||||
<Grid item xs={12}> | |||||
<Box sx={{ display: 'flex', justifyContent: 'flex-start', mt: 2 }}> | |||||
<Button | |||||
disabled={selectedRows.length < 1} | |||||
variant="outlined" | |||||
onClick={openAssignModal} | |||||
> | |||||
{t("Assign & Release")} | |||||
</Button> | |||||
</Box> | |||||
</Grid> | |||||
</Grid> | |||||
{/* Assign & Release Modal */} | |||||
{modalOpen ? ( | |||||
<Modal | |||||
open={modalOpen} | |||||
onClose={() => setModalOpen(false)} | |||||
aria-labelledby="modal-modal-title" | |||||
aria-describedby="modal-modal-description" | |||||
> | |||||
<Box sx={style}> | |||||
<Grid container rowGap={2}> | |||||
<Grid item xs={12}> | |||||
<Typography variant="h6" component="h2"> | |||||
{t("assign & Release Pick Orders")} | |||||
</Typography> | |||||
</Grid> | |||||
<Grid item xs={12}> | |||||
<Typography variant="body1" color="text.secondary"> | |||||
{t("Selected Pick Orders")}: {selectedRows.length} | |||||
</Typography> | |||||
</Grid> | |||||
<Grid item xs={12}> | |||||
<FormProvider {...formProps}> | |||||
<form onSubmit={formProps.handleSubmit(handleAssignAndRelease)}> | |||||
<Grid container spacing={2}> | |||||
<Grid item xs={12}> | |||||
<FormControl fullWidth> | |||||
<Autocomplete | |||||
options={usernameList} | |||||
getOptionLabel={(option) => option.name} | |||||
onChange={(_, value) => { | |||||
formProps.setValue("assignTo", value?.id || 0); | |||||
}} | |||||
renderInput={(params) => ( | |||||
<TextField | |||||
{...params} | |||||
label={t("Assign To")} | |||||
error={!!errors.assignTo} | |||||
helperText={errors.assignTo?.message} | |||||
required | |||||
/> | |||||
)} | |||||
/> | |||||
</FormControl> | |||||
</Grid> | |||||
<Grid item xs={12}> | |||||
<Typography variant="body2" color="warning.main"> | |||||
{t("This action will assign the selected pick orders and release them immediately.")} | |||||
</Typography> | |||||
</Grid> | |||||
<Grid item xs={12}> | |||||
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'flex-end' }}> | |||||
<Button | |||||
variant="outlined" | |||||
onClick={() => setModalOpen(false)} | |||||
> | |||||
{t("Cancel")} | |||||
</Button> | |||||
<Button | |||||
type="submit" | |||||
variant="contained" | |||||
color="primary" | |||||
> | |||||
{t("Assign & Release")} | |||||
</Button> | |||||
</Box> | |||||
</Grid> | |||||
</Grid> | |||||
</form> | |||||
</FormProvider> | |||||
</Grid> | |||||
</Grid> | |||||
</Box> | |||||
</Modal> | |||||
) : undefined} | |||||
</> | |||||
); | |||||
}; | |||||
export default AssignAndRelease; |
@@ -21,6 +21,8 @@ import ConsolidatedPickOrders from "./ConsolidatedPickOrders"; | |||||
import PickExecution from "./PickExecution"; | import PickExecution from "./PickExecution"; | ||||
import CreatePickOrderModal from "./CreatePickOrderModal"; | import CreatePickOrderModal from "./CreatePickOrderModal"; | ||||
import NewCreateItem from "./newcreatitem"; | import NewCreateItem from "./newcreatitem"; | ||||
import AssignAndRelease from "./AssignAndRelease"; | |||||
import AssignTo from "./assignTo"; | |||||
import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions"; | import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions"; | ||||
import { fetchPickOrderClient } from "@/app/api/pickOrder/actions"; | import { fetchPickOrderClient } from "@/app/api/pickOrder/actions"; | ||||
@@ -116,42 +118,22 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
"label", | "label", | ||||
), | ), | ||||
}, | }, | ||||
{ | |||||
label: tabIndex === 3 ? t("Item Name") : t("Items"), | |||||
paramName: "items", | |||||
type: tabIndex === 3 ? "text" : "autocomplete", | |||||
options: tabIndex === 3 | |||||
? [] | |||||
: | |||||
uniqBy( | |||||
flatten( | |||||
sortBy( | |||||
pickOrders.map((po) => | |||||
po.items | |||||
? po.items.map((item) => ({ | |||||
value: item.name, | |||||
label: item.name, | |||||
})) | |||||
: [], | |||||
), | |||||
"label", | |||||
), | |||||
), | |||||
"value", | |||||
), | |||||
}, | |||||
]; | ]; | ||||
// Add Job Order search for Create Item tab (tabIndex === 3) | |||||
if (tabIndex === 3) { | if (tabIndex === 3) { | ||||
baseCriteria.splice(1, 0, { | baseCriteria.splice(1, 0, { | ||||
label: t("Job Order"), | |||||
paramName: "jobOrderCode" as any, // Type assertion for now | |||||
type: "text", | |||||
}); | |||||
baseCriteria.splice(2, 0, { | |||||
label: t("Target Date"), | label: t("Target Date"), | ||||
paramName: "targetDate", | paramName: "targetDate", | ||||
type: "date", | type: "date", | ||||
}); | }); | ||||
} else { | } else { | ||||
baseCriteria.splice(1, 0, { | baseCriteria.splice(1, 0, { | ||||
label: t("Target Date From"), | label: t("Target Date From"), | ||||
label2: t("Target Date To"), | label2: t("Target Date To"), | ||||
@@ -159,9 +141,36 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
type: "dateRange", | type: "dateRange", | ||||
}); | }); | ||||
} | } | ||||
// Add Items/Item Name criteria | |||||
baseCriteria.push({ | |||||
label: tabIndex === 3 ? t("Item Name") : t("Items"), | |||||
paramName: "items", | |||||
type: tabIndex === 3 ? "text" : "autocomplete", | |||||
options: tabIndex === 3 | |||||
? [] | |||||
: | |||||
uniqBy( | |||||
flatten( | |||||
sortBy( | |||||
pickOrders.map((po) => | |||||
po.items | |||||
? po.items.map((item) => ({ | |||||
value: item.name, | |||||
label: item.name, | |||||
})) | |||||
: [], | |||||
), | |||||
"label", | |||||
), | |||||
), | |||||
"value", | |||||
), | |||||
}); | |||||
// Add Status criteria for non-Create Item tabs | |||||
if (tabIndex !== 3) { | if (tabIndex !== 3) { | ||||
baseCriteria.splice(4, 0, { | |||||
baseCriteria.push({ | |||||
label: t("Status"), | label: t("Status"), | ||||
paramName: "status", | paramName: "status", | ||||
type: "autocomplete", | type: "autocomplete", | ||||
@@ -177,7 +186,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
), | ), | ||||
}); | }); | ||||
} | } | ||||
return baseCriteria; | return baseCriteria; | ||||
}, | }, | ||||
[pickOrders, t, tabIndex, items], | [pickOrders, t, tabIndex, items], | ||||
@@ -241,6 +250,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
</Grid> | </Grid> | ||||
</Grid> | </Grid> | ||||
</Stack> | </Stack> | ||||
{/* | |||||
<SearchBox | <SearchBox | ||||
criteria={searchCriteria} | criteria={searchCriteria} | ||||
onSearch={(query) => { | onSearch={(query) => { | ||||
@@ -298,22 +308,29 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
} | } | ||||
}} | }} | ||||
/> | /> | ||||
*/} | |||||
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | ||||
<Tab label={t("Select Items")} iconPosition="end" /> | |||||
<Tab label={t("Assign")} iconPosition="end" /> | |||||
<Tab label={t("Release")} iconPosition="end" /> | |||||
<Tab label={t("Pick Execution")} iconPosition="end" /> | |||||
<Tab label={t("Pick Orders")} iconPosition="end" /> | <Tab label={t("Pick Orders")} iconPosition="end" /> | ||||
<Tab label={t("Consolidated Pick Orders")} iconPosition="end" /> | <Tab label={t("Consolidated Pick Orders")} iconPosition="end" /> | ||||
<Tab label={t("Pick Execution")} iconPosition="end" /> | |||||
<Tab label={t("Create Item")} iconPosition="end" /> | |||||
</Tabs> | </Tabs> | ||||
{tabIndex === 0 && ( | |||||
{tabIndex === 4 && ( | |||||
<PickOrders | <PickOrders | ||||
filteredPickOrders={filteredPickOrders} | filteredPickOrders={filteredPickOrders} | ||||
filterArgs={filterArgs} | filterArgs={filterArgs} | ||||
/> | /> | ||||
)} | )} | ||||
{tabIndex === 1 && <ConsolidatedPickOrders filterArgs={filterArgs} />} | |||||
{tabIndex === 2 && <PickExecution filterArgs={filterArgs} />} | |||||
{tabIndex === 3 && <NewCreateItem filterArgs={filterArgs} searchQuery={searchQuery} />} | |||||
{tabIndex === 5 && <ConsolidatedPickOrders filterArgs={filterArgs} />} | |||||
{tabIndex === 3 && <PickExecution filterArgs={filterArgs} />} | |||||
{tabIndex === 0 && <NewCreateItem filterArgs={filterArgs} searchQuery={searchQuery} />} | |||||
{tabIndex === 1 && <AssignAndRelease filterArgs={filterArgs} />} | |||||
{tabIndex === 2 && <AssignTo filterArgs={filterArgs} />} | |||||
</> | </> | ||||
); | ); | ||||
}; | }; | ||||
@@ -0,0 +1,233 @@ | |||||
"use client"; | |||||
import { | |||||
Autocomplete, | |||||
Box, | |||||
Button, | |||||
CircularProgress, | |||||
Grid, | |||||
Stack, | |||||
TextField, | |||||
Typography, | |||||
} from "@mui/material"; | |||||
import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import SearchResults, { Column } from "../SearchResults/SearchResults"; | |||||
import { fetchConsoPickOrderClient } from "@/app/api/pickOrder/actions"; | |||||
import { fetchNameList, NameList } from "@/app/api/user/actions"; | |||||
import { isEmpty, upperFirst } from "lodash"; | |||||
import { arrayToDateString } from "@/app/utils/formatUtil"; | |||||
interface Props { | |||||
filterArgs: Record<string, any>; | |||||
} | |||||
interface AssignmentData { | |||||
id: string; | |||||
consoCode: string; | |||||
releasedDate: string | null; | |||||
status: string; | |||||
assignTo: number | null; | |||||
assignedUserName?: string; | |||||
} | |||||
const AssignTo: React.FC<Props> = ({ filterArgs }) => { | |||||
const { t } = useTranslation("pickOrder"); | |||||
// State | |||||
const [assignmentData, setAssignmentData] = useState<AssignmentData[]>([]); | |||||
const [isLoading, setIsLoading] = useState(false); | |||||
const [pagingController, setPagingController] = useState({ | |||||
pageNum: 1, | |||||
pageSize: 50, | |||||
}); | |||||
const [totalCount, setTotalCount] = useState<number>(); | |||||
const [usernameList, setUsernameList] = useState<NameList[]>([]); | |||||
const [selectedUser, setSelectedUser] = useState<NameList | null>(null); | |||||
// Fetch assignment data | |||||
const fetchAssignmentData = useCallback(async () => { | |||||
setIsLoading(true); | |||||
try { | |||||
const params = { | |||||
...pagingController, | |||||
...filterArgs, | |||||
// Add user filter if selected | |||||
...(selectedUser && { assignTo: selectedUser.id }), | |||||
}; | |||||
console.log("Fetching with params:", params); | |||||
const res = await fetchConsoPickOrderClient(params); | |||||
if (res) { | |||||
console.log("API response:", res); | |||||
// Enhance data with user names and add id | |||||
const enhancedData = res.records.map((record: any, index: number) => { | |||||
const userName = record.assignTo | |||||
? usernameList.find(user => user.id === record.assignTo)?.name | |||||
: null; | |||||
return { | |||||
...record, | |||||
id: record.consoCode || `temp-${index}`, | |||||
assignedUserName: userName || 'Unassigned', | |||||
}; | |||||
}); | |||||
setAssignmentData(enhancedData); | |||||
setTotalCount(res.total); | |||||
} | |||||
} catch (error) { | |||||
console.error("Error fetching assignment data:", error); | |||||
} finally { | |||||
setIsLoading(false); | |||||
} | |||||
}, [pagingController, filterArgs, selectedUser, usernameList]); | |||||
// Load username list | |||||
useEffect(() => { | |||||
const loadUsernameList = async () => { | |||||
try { | |||||
const res = await fetchNameList(); | |||||
if (res) { | |||||
console.log("Loaded username list:", res); | |||||
setUsernameList(res); | |||||
} | |||||
} catch (error) { | |||||
console.error("Error loading username list:", error); | |||||
} | |||||
}; | |||||
loadUsernameList(); | |||||
}, []); | |||||
// Fetch data when dependencies change | |||||
useEffect(() => { | |||||
fetchAssignmentData(); | |||||
}, [fetchAssignmentData]); | |||||
// Handle user selection | |||||
const handleUserChange = useCallback((event: any, newValue: NameList | null) => { | |||||
setSelectedUser(newValue); | |||||
// Reset to first page when filtering | |||||
setPagingController(prev => ({ ...prev, pageNum: 1 })); | |||||
}, []); | |||||
// Clear filter | |||||
const handleClearFilter = useCallback(() => { | |||||
setSelectedUser(null); | |||||
setPagingController(prev => ({ ...prev, pageNum: 1 })); | |||||
}, []); | |||||
// Columns definition | |||||
const columns = useMemo<Column<AssignmentData>[]>( | |||||
() => [ | |||||
{ | |||||
name: "consoCode", | |||||
label: t("Consolidated Code"), | |||||
}, | |||||
{ | |||||
name: "assignedUserName", | |||||
label: t("Assigned To"), | |||||
renderCell: (params) => { | |||||
if (!params.assignTo) { | |||||
return ( | |||||
<Typography variant="body2" color="text.secondary"> | |||||
{t("Unassigned")} | |||||
</Typography> | |||||
); | |||||
} | |||||
return ( | |||||
<Typography variant="body2" color="primary"> | |||||
{params.assignedUserName} | |||||
</Typography> | |||||
); | |||||
}, | |||||
}, | |||||
{ | |||||
name: "status", | |||||
label: t("Status"), | |||||
renderCell: (params) => { | |||||
return upperFirst(params.status); | |||||
}, | |||||
}, | |||||
{ | |||||
name: "releasedDate", | |||||
label: t("Released Date"), | |||||
renderCell: (params) => { | |||||
if (!params.releasedDate) { | |||||
return ( | |||||
<Typography variant="body2" color="text.secondary"> | |||||
{t("Not Released")} | |||||
</Typography> | |||||
); | |||||
} | |||||
return arrayToDateString(params.releasedDate); | |||||
}, | |||||
}, | |||||
], | |||||
[t], | |||||
); | |||||
return ( | |||||
<Stack spacing={2}> | |||||
{/* Filter Section */} | |||||
<Grid container spacing={2}> | |||||
<Grid item xs={4}> | |||||
<Autocomplete | |||||
options={usernameList} | |||||
getOptionLabel={(option) => option.name} | |||||
value={selectedUser} | |||||
onChange={handleUserChange} | |||||
renderInput={(params) => ( | |||||
<TextField | |||||
{...params} | |||||
label={t("Select User to Filter")} | |||||
variant="outlined" | |||||
fullWidth | |||||
/> | |||||
)} | |||||
renderOption={(props, option) => ( | |||||
<Box component="li" {...props}> | |||||
<Typography variant="body2"> | |||||
{option.name} (ID: {option.id}) | |||||
</Typography> | |||||
</Box> | |||||
)} | |||||
/> | |||||
</Grid> | |||||
<Grid item xs={2}> | |||||
<Button | |||||
variant="outlined" | |||||
onClick={handleClearFilter} | |||||
disabled={!selectedUser} | |||||
> | |||||
{t("Clear Filter")} | |||||
</Button> | |||||
</Grid> | |||||
</Grid> | |||||
{/* Data Table - Match PickExecution exactly */} | |||||
<Grid container spacing={2} sx={{ height: '100%', flex: 1 }}> | |||||
<Grid item xs={12} sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}> | |||||
{isLoading ? ( | |||||
<Box display="flex" justifyContent="center" alignItems="center" flex={1}> | |||||
<CircularProgress size={40} /> | |||||
</Box> | |||||
) : ( | |||||
<SearchResults<AssignmentData> | |||||
items={assignmentData} | |||||
columns={columns} | |||||
pagingController={pagingController} | |||||
setPagingController={setPagingController} | |||||
totalCount={totalCount} | |||||
/> | |||||
)} | |||||
</Grid> | |||||
</Grid> | |||||
</Stack> | |||||
); | |||||
}; | |||||
export default AssignTo; | |||||
@@ -1,6 +1,8 @@ | |||||
{ | { | ||||
"Purchase Order": "採購訂單", | "Purchase Order": "採購訂單", | ||||
"Code": "編號", | "Code": "編號", | ||||
"Pick Order Code": "提料單編號", | |||||
"Item Code": "貨品編號", | |||||
"OrderDate": "下單日期", | "OrderDate": "下單日期", | ||||
"Details": "詳情", | "Details": "詳情", | ||||
"Supplier": "供應商", | "Supplier": "供應商", | ||||
@@ -23,6 +25,7 @@ | |||||
"itemNo": "貨品編號", | "itemNo": "貨品編號", | ||||
"itemName": "貨品名稱", | "itemName": "貨品名稱", | ||||
"qty": "訂單數量", | "qty": "訂單數量", | ||||
"Require Qty": "需求數量", | |||||
"uom": "計量單位", | "uom": "計量單位", | ||||
"total weight": "總重量", | "total weight": "總重量", | ||||
"weight unit": "重量單位", | "weight unit": "重量單位", | ||||
@@ -91,6 +94,7 @@ | |||||
"Pick Order": "提料單", | "Pick Order": "提料單", | ||||
"Type": "類型", | "Type": "類型", | ||||
"Product Type": "貨品類型", | |||||
"Reset": "重置", | "Reset": "重置", | ||||
"Search": "搜尋", | "Search": "搜尋", | ||||
"Pick Orders": "提料單", | "Pick Orders": "提料單", | ||||
@@ -102,7 +106,7 @@ | |||||
"Consolidated Code": "合併編號", | "Consolidated Code": "合併編號", | ||||
"type": "類型", | "type": "類型", | ||||
"Items": "項目", | "Items": "項目", | ||||
"Target Date": "目標日期", | |||||
"Target Date": "需求日期", | |||||
"Released By": "發佈者", | "Released By": "發佈者", | ||||
"Target Date From": "目標日期從", | "Target Date From": "目標日期從", | ||||
"Target Date To": "目標日期到", | "Target Date To": "目標日期到", | ||||
@@ -111,7 +115,8 @@ | |||||
"create": "新增", | "create": "新增", | ||||
"detail": "詳情", | "detail": "詳情", | ||||
"Pick Order Detail": "提料單詳情", | "Pick Order Detail": "提料單詳情", | ||||
"item": "項目", | |||||
"item": "貨品", | |||||
"Unit": "單位", | |||||
"reset": "重置", | "reset": "重置", | ||||
"targetDate": "目標日期", | "targetDate": "目標日期", | ||||
"remove": "移除", | "remove": "移除", | ||||
@@ -120,7 +125,25 @@ | |||||
"suggestedLotNo": "建議批次", | "suggestedLotNo": "建議批次", | ||||
"lotNo": "批次", | "lotNo": "批次", | ||||
"item name": "貨品名稱", | "item name": "貨品名稱", | ||||
"Item Name": "貨品名稱", | |||||
"approval": "審核", | "approval": "審核", | ||||
"lot change": "批次變更", | "lot change": "批次變更", | ||||
"checkout": "出庫" | |||||
"checkout": "出庫", | |||||
"Search Items": "搜尋貨品", | |||||
"Search Results": "搜尋結果", | |||||
"Second Search Results": "第二搜尋結果", | |||||
"Second Search Items": "第二搜尋項目", | |||||
"Second Search": "第二搜尋", | |||||
"Item": "貨品", | |||||
"Order Quantity": "要求數量", | |||||
"Current Stock": "現時庫存", | |||||
"Selected": "已選擇", | |||||
"Select Items": "選擇貨品", | |||||
"Assign": "分派提料單", | |||||
"Release": "放單", | |||||
"Pick Execution": "進行提料", | |||||
"Create Pick Order": "建立貨品提料單", | |||||
"consumable": "食材", | |||||
"Material": "材料", | |||||
"Job Order": "工單" | |||||
} | } |