| @@ -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": "工單" | |||
| } | |||