@@ -67,6 +67,25 @@ export interface isCorrectMachineUsedResponse<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) => { | |||
const isExist = await serverFetchJson<IsOperatorExistResponse<Operator>>( | |||
`${BASE_API_URL}/jop/isOperatorExist`, | |||
@@ -102,7 +102,7 @@ export interface GetPickOrderLineInfo { | |||
itemId: number; | |||
itemCode: string; | |||
itemName: string; | |||
availableQty: number; | |||
availableQty: number| null; | |||
requiredQty: number; | |||
uomCode: string; | |||
uomDesc: string; | |||
@@ -142,7 +142,17 @@ export interface PickOrderLotDetailResponse { | |||
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 () => { | |||
return serverFetchJson<GetPickOrderInfoResponse>( | |||
`${BASE_API_URL}/pickOrder/detail`, | |||
@@ -182,7 +192,7 @@ export const createPickOrder = async (data: SavePickOrderRequest) => { | |||
return po; | |||
} | |||
export const consolidatePickOrder = async (ids: number[]) => { | |||
export const assignPickOrder = async (ids: number[]) => { | |||
const pickOrder = await serverFetchJson<any>( | |||
`${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( | |||
async (queryParams?: Record<string, any>) => { | |||
if (queryParams) { | |||
@@ -57,6 +57,7 @@ export interface ItemCombo { | |||
uomId: number, | |||
uom: string, | |||
group?: string, | |||
currentStockBalance?: number, | |||
} | |||
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 CreatePickOrderModal from "./CreatePickOrderModal"; | |||
import NewCreateItem from "./newcreatitem"; | |||
import AssignAndRelease from "./AssignAndRelease"; | |||
import AssignTo from "./assignTo"; | |||
import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions"; | |||
import { fetchPickOrderClient } from "@/app/api/pickOrder/actions"; | |||
@@ -116,42 +118,22 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
"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) { | |||
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"), | |||
paramName: "targetDate", | |||
type: "date", | |||
}); | |||
} else { | |||
baseCriteria.splice(1, 0, { | |||
label: t("Target Date From"), | |||
label2: t("Target Date To"), | |||
@@ -159,9 +141,36 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
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) { | |||
baseCriteria.splice(4, 0, { | |||
baseCriteria.push({ | |||
label: t("Status"), | |||
paramName: "status", | |||
type: "autocomplete", | |||
@@ -177,7 +186,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
), | |||
}); | |||
} | |||
return baseCriteria; | |||
}, | |||
[pickOrders, t, tabIndex, items], | |||
@@ -241,6 +250,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
</Grid> | |||
</Grid> | |||
</Stack> | |||
{/* | |||
<SearchBox | |||
criteria={searchCriteria} | |||
onSearch={(query) => { | |||
@@ -298,22 +308,29 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
} | |||
}} | |||
/> | |||
*/} | |||
<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("Consolidated Pick Orders")} iconPosition="end" /> | |||
<Tab label={t("Pick Execution")} iconPosition="end" /> | |||
<Tab label={t("Create Item")} iconPosition="end" /> | |||
</Tabs> | |||
{tabIndex === 0 && ( | |||
{tabIndex === 4 && ( | |||
<PickOrders | |||
filteredPickOrders={filteredPickOrders} | |||
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": "採購訂單", | |||
"Code": "編號", | |||
"Pick Order Code": "提料單編號", | |||
"Item Code": "貨品編號", | |||
"OrderDate": "下單日期", | |||
"Details": "詳情", | |||
"Supplier": "供應商", | |||
@@ -23,6 +25,7 @@ | |||
"itemNo": "貨品編號", | |||
"itemName": "貨品名稱", | |||
"qty": "訂單數量", | |||
"Require Qty": "需求數量", | |||
"uom": "計量單位", | |||
"total weight": "總重量", | |||
"weight unit": "重量單位", | |||
@@ -91,6 +94,7 @@ | |||
"Pick Order": "提料單", | |||
"Type": "類型", | |||
"Product Type": "貨品類型", | |||
"Reset": "重置", | |||
"Search": "搜尋", | |||
"Pick Orders": "提料單", | |||
@@ -102,7 +106,7 @@ | |||
"Consolidated Code": "合併編號", | |||
"type": "類型", | |||
"Items": "項目", | |||
"Target Date": "目標日期", | |||
"Target Date": "需求日期", | |||
"Released By": "發佈者", | |||
"Target Date From": "目標日期從", | |||
"Target Date To": "目標日期到", | |||
@@ -111,7 +115,8 @@ | |||
"create": "新增", | |||
"detail": "詳情", | |||
"Pick Order Detail": "提料單詳情", | |||
"item": "項目", | |||
"item": "貨品", | |||
"Unit": "單位", | |||
"reset": "重置", | |||
"targetDate": "目標日期", | |||
"remove": "移除", | |||
@@ -120,7 +125,25 @@ | |||
"suggestedLotNo": "建議批次", | |||
"lotNo": "批次", | |||
"item name": "貨品名稱", | |||
"Item Name": "貨品名稱", | |||
"approval": "審核", | |||
"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": "工單" | |||
} |