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