| @@ -22,7 +22,8 @@ export interface SaveJoResponse { | |||
| export interface SearchJoResultRequest extends Pageable { | |||
| code: string; | |||
| itemName?: string; | |||
| planStart?: string; | |||
| planStartTo?: string; | |||
| } | |||
| @@ -25,6 +25,7 @@ export interface JobOrder { | |||
| pickLines?: JoDetailPickLine[]; | |||
| status: JoStatus; | |||
| planStart?: number[]; | |||
| planStartTo?: string; | |||
| planEnd?: number[]; | |||
| type: string; | |||
| // TODO pack below into StockInLineInfo | |||
| @@ -58,7 +58,7 @@ const DashboardPage: React.FC<Props> = ({ | |||
| <Grid item xs={12}> | |||
| <CollapsibleCard title={t("Progress chart")}> | |||
| <CardContent> | |||
| <Grid container spacing={2}> | |||
| <Grid container spacing={3}> | |||
| <Grid item xs={12} md={4}> | |||
| <DashboardProgressChart /> | |||
| </Grid> | |||
| @@ -393,15 +393,15 @@ if(orderStartDate != ""){ | |||
| const result = await Swal.fire( | |||
| { | |||
| icon: "info", | |||
| icon: "question", | |||
| title: t("Batch Release"), | |||
| html: t("Selected Shop(s): ") + extractedIdsCount.toString() + `</p>`+ | |||
| t("Selected Item(s): ") + extractedItemsCount.toString() + `</p>`, | |||
| showCancelButton: true, | |||
| confirmButtonText: t("Confirm"), | |||
| cancelButtonText: t("Cancel"), | |||
| confirmButtonColor: "#638a01", | |||
| cancelButtonColor: "#d33" | |||
| confirmButtonColor: "#8dba00", | |||
| cancelButtonColor: "#F04438" | |||
| }); | |||
| if (result.isConfirmed) { | |||
| Swal.fire({ | |||
| @@ -69,7 +69,6 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| const fetchReleasedOrderCount = useCallback(async () => { | |||
| try { | |||
| const releasedOrders = await fetchReleasedDoPickOrders(); | |||
| // Count only orders that have a valid doOrderId | |||
| const validCount = releasedOrders.filter(order => order.doOrderId).length; | |||
| setReleasedOrderCount(validCount); | |||
| } catch (error) { | |||
| @@ -105,7 +104,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| if(response.success){ | |||
| Swal.fire({ | |||
| position: "bottom-end", | |||
| icon: "info", | |||
| icon: "success", | |||
| text: t("Printed Successfully."), | |||
| showConfirmButton: false, | |||
| timer: 1500 | |||
| @@ -130,21 +129,30 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| console.log("Found released orders:", releasedOrders); | |||
| const confirmResult = await Swal.fire({ | |||
| title: t("Confirm Print"), | |||
| text: t(`Do you want to print ${releasedOrders.length} draft(s)?`), | |||
| title: t("Batch Print"), | |||
| text: t("Confirm print: (") + releasedOrders.length.toString() + t("piece(s))"), | |||
| icon: "question", | |||
| showCancelButton: true, | |||
| confirmButtonText: t("Yes, print"), | |||
| confirmButtonText: t("Confirm"), | |||
| cancelButtonText: t("Cancel"), | |||
| confirmButtonColor: "#3085d6", | |||
| cancelButtonColor: "#d33" | |||
| confirmButtonColor: "#8dba00", | |||
| cancelButtonColor: "#F04438" | |||
| }); | |||
| // If user cancels, exit the function | |||
| if (!confirmResult.isConfirmed) { | |||
| return; | |||
| } | |||
| Swal.fire({ | |||
| title: t("Printing..."), | |||
| text: t("Please wait..."), | |||
| allowOutsideClick: false, | |||
| allowEscapeKey: false, | |||
| didOpen: () => { | |||
| Swal.showLoading(); | |||
| } | |||
| }); | |||
| for (const order of releasedOrders) { | |||
| const { doOrderId, pickOrderId } = order; | |||
| @@ -170,7 +178,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| Swal.fire({ | |||
| position: "bottom-end", | |||
| icon: "success", | |||
| text: t(`Printed ${releasedOrders.length} draft(s) successfully.`), | |||
| text: t("Printed Successfully."), | |||
| showConfirmButton: false, | |||
| timer: 1500 | |||
| }); | |||
| @@ -183,6 +191,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| const handleDN = useCallback(async () =>{ | |||
| const askNumofCarton = await Swal.fire({ | |||
| title: t("Enter the number of cartons: "), | |||
| icon: "info", | |||
| input: "number", | |||
| inputPlaceholder: t("Number of cartons"), | |||
| inputAttributes:{ | |||
| @@ -201,6 +210,8 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| showCancelButton: true, | |||
| confirmButtonText: t("Confirm"), | |||
| cancelButtonText: t("Cancel"), | |||
| confirmButtonColor: "#8dba00", | |||
| cancelButtonColor: "#F04438", | |||
| showLoaderOnConfirm: true, | |||
| allowOutsideClick: () => !Swal.isLoading() | |||
| }); | |||
| @@ -233,7 +244,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| if(response.success){ | |||
| Swal.fire({ | |||
| position: "bottom-end", | |||
| icon: "info", | |||
| icon: "success", | |||
| text: t("Printed Successfully."), | |||
| showConfirmButton: false, | |||
| timer: 1500 | |||
| @@ -250,6 +261,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| const handleDNandLabel = useCallback(async () =>{ | |||
| const askNumofCarton = await Swal.fire({ | |||
| title: t("Enter the number of cartons: "), | |||
| icon: "info", | |||
| input: "number", | |||
| inputPlaceholder: t("Number of cartons"), | |||
| inputAttributes:{ | |||
| @@ -268,6 +280,8 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| showCancelButton: true, | |||
| confirmButtonText: t("Confirm"), | |||
| cancelButtonText: t("Cancel"), | |||
| confirmButtonColor: "#8dba00", | |||
| cancelButtonColor: "#F04438", | |||
| showLoaderOnConfirm: true, | |||
| allowOutsideClick: () => !Swal.isLoading() | |||
| }); | |||
| @@ -310,7 +324,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| if(LabelsResponse.success && DNResponse.success){ | |||
| Swal.fire({ | |||
| position: "bottom-end", | |||
| icon: "info", | |||
| icon: "success", | |||
| text: t("Printed Successfully."), | |||
| showConfirmButton: false, | |||
| timer: 1500 | |||
| @@ -332,6 +346,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| const handleLabel = useCallback(async () =>{ | |||
| const askNumofCarton = await Swal.fire({ | |||
| title: t("Enter the number of cartons: "), | |||
| icon: "info", | |||
| input: "number", | |||
| inputPlaceholder: t("Number of cartons"), | |||
| inputAttributes:{ | |||
| @@ -350,6 +365,8 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| showCancelButton: true, | |||
| confirmButtonText: t("Confirm"), | |||
| cancelButtonText: t("Cancel"), | |||
| confirmButtonColor: "#8dba00", | |||
| cancelButtonColor: "#F04438", | |||
| showLoaderOnConfirm: true, | |||
| allowOutsideClick: () => !Swal.isLoading() | |||
| }); | |||
| @@ -380,7 +397,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| if(response.success){ | |||
| Swal.fire({ | |||
| position: "bottom-end", | |||
| icon: "info", | |||
| icon: "success", | |||
| text: t("Printed Successfully."), | |||
| showConfirmButton: false, | |||
| timer: 1500 | |||
| @@ -454,19 +471,40 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||
| } else if (res.code === "USER_BUSY") { | |||
| console.warn("⚠️ User already has pick orders in progress:", res.message); | |||
| Swal.fire({ | |||
| icon: "warning", | |||
| title: t("Warning"), | |||
| text: t("You already have a pick order in progess. Please complete it first before taking next pick order."), | |||
| confirmButtonText: t("Confirm"), | |||
| confirmButtonColor: "#8dba00" | |||
| }); | |||
| // ✅ Show warning but still refresh to show existing orders | |||
| alert(`Warning: ${res.message}`); | |||
| //alert(`Warning: ${res.message}`); | |||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||
| } else if (res.code === "NO_ORDERS") { | |||
| console.log("ℹ️ No available pick orders for store", storeId); | |||
| alert(`Info: ${res.message}`); | |||
| Swal.fire({ | |||
| icon: "info", | |||
| title: t("Info"), | |||
| text: t("No available pick order(s) for this floor."), | |||
| confirmButtonText: t("Confirm"), | |||
| confirmButtonColor: "#8dba00" | |||
| }); | |||
| //alert(`Info: ${res.message}`); | |||
| } else { | |||
| console.log("ℹ️ Assignment result:", res.message); | |||
| alert(`Info: ${res.message}`); | |||
| } | |||
| } catch (error) { | |||
| console.error("❌ Error assigning by store:", error); | |||
| alert("Error occurred during assignment"); | |||
| Swal.fire({ | |||
| icon: "error", | |||
| title: t("Error"), | |||
| text: t("Error occurred during assignment."), | |||
| confirmButtonText: t("Confirm"), | |||
| confirmButtonColor: "#8dba00" | |||
| }); | |||
| //alert("Error occurred during assignment"); | |||
| } finally { | |||
| setIsAssigning(false); | |||
| } | |||
| @@ -181,6 +181,7 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||
| render={({ field, fieldState: { error } }) => ( | |||
| <DateTimePicker | |||
| label={t("Plan Start")} | |||
| views={['year','month','day','hours', 'minutes', 'seconds']} | |||
| format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`} | |||
| onChange={(newValue: Dayjs | null) => { | |||
| handleDateTimePickerChange(newValue, field.onChange) | |||
| @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; | |||
| import { Criterion } from "../SearchBox"; | |||
| import SearchResults, { Column, defaultPagingController } from "../SearchResults/SearchResults"; | |||
| import { EditNote } from "@mui/icons-material"; | |||
| import { arrayToDateString, integerFormatter } from "@/app/utils/formatUtil"; | |||
| import { arrayToDateString, arrayToDateTimeString, integerFormatter } from "@/app/utils/formatUtil"; | |||
| import { orderBy, uniqBy, upperFirst } from "lodash"; | |||
| import SearchBox from "../SearchBox/SearchBox"; | |||
| import { useRouter } from "next/navigation"; | |||
| @@ -31,10 +31,7 @@ interface Props { | |||
| bomCombo: BomCombo[] | |||
| } | |||
| type SearchQuery = Partial<Omit<JobOrder, "id">> & { | |||
| planStartFrom?: string; | |||
| planStartTo?: string; | |||
| }; | |||
| type SearchQuery = Partial<Omit<JobOrder, "id">>; | |||
| type SearchParamNames = keyof SearchQuery; | |||
| @@ -49,6 +46,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo }) => { | |||
| const [totalCount, setTotalCount] = useState(0) | |||
| const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false) | |||
| // console.log(inputs) | |||
| const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); | |||
| const [detailedJos, setDetailedJos] = useState<Map<number, JobOrder>>(new Map()); | |||
| @@ -138,7 +136,8 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo }) => { | |||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => [ | |||
| { label: t("Code"), paramName: "code", type: "text" }, | |||
| { label: t("Item Name"), paramName: "itemName", type: "text" }, | |||
| { label: t("Item Name"), paramName: "itemName", type: "text" }, | |||
| { label: t("Plan Start"), label2: t("Plan Start To"), paramName: "planStart", type: "datetimeRange" }, | |||
| ], [t]) | |||
| const columns = useMemo<Column<JobOrder>[]>( | |||
| @@ -194,7 +193,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo }) => { | |||
| align: "left", | |||
| headerAlign: "left", | |||
| renderCell: (row) => { | |||
| return row.planStart ? arrayToDateString(row.planStart, "output") : '-' | |||
| return row.planStart ? arrayToDateTimeString(row.planStart) : '-' | |||
| } | |||
| }, | |||
| { | |||
| @@ -273,6 +272,8 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo }) => { | |||
| const params: SearchJoResultRequest = { | |||
| code: query.code, | |||
| itemName: query.itemName, | |||
| planStart: query.planStart, | |||
| planStartTo: query.planStartTo, | |||
| pageNum: pagingController.pageNum - 1, | |||
| pageSize: pagingController.pageSize, | |||
| } | |||
| @@ -364,7 +365,9 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo }) => { | |||
| const onSearch = useCallback((query: Record<SearchParamNames, string>) => { | |||
| setInputs(() => ({ | |||
| code: query.code, | |||
| itemName: query.itemName | |||
| itemName: query.itemName, | |||
| planStart: query.planStart, | |||
| planStartTo: query.planStartTo | |||
| })) | |||
| refetchData(query, "search"); | |||
| }, []) | |||
| @@ -11,7 +11,7 @@ interface SubComponents { | |||
| const JoSearchWrapper: React.FC & SubComponents = async () => { | |||
| const defaultInputs: SearchJoResultRequest = { | |||
| code: "", | |||
| name: "", | |||
| itemName: "", | |||
| } | |||
| const [ | |||
| @@ -321,7 +321,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| if(response.success){ | |||
| Swal.fire({ | |||
| position: "bottom-end", | |||
| icon: "info", | |||
| icon: "success", | |||
| text: t("Printed Successfully."), | |||
| showConfirmButton: false, | |||
| timer: 1500 | |||
| @@ -380,7 +380,7 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId | |||
| <Grid container> | |||
| <Typography variant="h4" sx={{ fontWeight: 'bold', color: 'black' }} noWrap> | |||
| {warehouseId > 0 ? `${warehouse.find((w) => w.id == warehouseId)?.name}` | |||
| : `${warehouse.find((w) => w.id == 1)?.name} (預設)`} | |||
| : `${warehouse.find((w) => w.id == 1)?.name} (建議)`} | |||
| </Typography> | |||
| </Grid> | |||
| @@ -15,7 +15,7 @@ import CardActions from "@mui/material/CardActions"; | |||
| import Button from "@mui/material/Button"; | |||
| import RestartAlt from "@mui/icons-material/RestartAlt"; | |||
| import Search from "@mui/icons-material/Search"; | |||
| import dayjs from "dayjs"; | |||
| import dayjs, { Dayjs } from "dayjs"; | |||
| import "dayjs/locale/zh-hk"; | |||
| import { DatePicker } from "@mui/x-date-pickers/DatePicker"; | |||
| import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; | |||
| @@ -29,6 +29,8 @@ import { | |||
| } from "@mui/material"; | |||
| import MultiSelect from "@/components/SearchBox/MultiSelect"; | |||
| import { intersectionWith } from "lodash"; | |||
| import { INPUT_DATE_FORMAT, INPUT_TIME_FORMAT, OUTPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT, dayjsToDateTimeString } from "@/app/utils/formatUtil"; | |||
| import { DateTimePicker } from "@mui/x-date-pickers"; | |||
| interface BaseCriterion<T extends string> { | |||
| label: string; | |||
| @@ -86,6 +88,10 @@ interface DateRangeCriterion<T extends string> extends BaseCriterion<T> { | |||
| type: "dateRange"; | |||
| } | |||
| interface DatetimeRangeCriterion<T extends string> extends BaseCriterion<T> { | |||
| type: "datetimeRange"; | |||
| } | |||
| interface DateCriterion<T extends string> extends BaseCriterion<T> { | |||
| type: "date"; | |||
| } | |||
| @@ -95,6 +101,7 @@ export type Criterion<T extends string> = | |||
| | SelectCriterion<T> | |||
| | SelectWithLabelCriterion<T> | |||
| | DateRangeCriterion<T> | |||
| | DatetimeRangeCriterion<T> | |||
| | DateCriterion<T> | |||
| | MultiSelectCriterion<T> | |||
| | AutocompleteCriterion<T>; | |||
| @@ -135,7 +142,7 @@ function SearchBox<T extends string>({ | |||
| : "", | |||
| }; | |||
| if (c.type === "dateRange") { | |||
| if (c.type === "dateRange" || c.type === "datetimeRange") { | |||
| tempCriteria = { | |||
| ...tempCriteria, | |||
| [c.paramName]: c.defaultValue ?? "", | |||
| @@ -216,6 +223,24 @@ function SearchBox<T extends string>({ | |||
| }; | |||
| }, []); | |||
| const makeDatetimeChangeHandler = useCallback((paramName: T) => { | |||
| return (value: Dayjs | null) => { | |||
| setInputs((i) => ({ | |||
| ...i, | |||
| [paramName]: value ? dayjsToDateTimeString(value) : null | |||
| })); | |||
| }; | |||
| }, []); | |||
| const makeDatetimeToChangeHandler = useCallback((paramName: T) => { | |||
| return (value: Dayjs | null) => { | |||
| setInputs((i) => ({ | |||
| ...i, | |||
| [paramName + "To"]: value ? dayjsToDateTimeString(value) : null | |||
| })); | |||
| }; | |||
| }, []); | |||
| const handleReset = () => { | |||
| setInputs(defaultInputs); | |||
| onReset?.(); | |||
| @@ -397,6 +422,7 @@ function SearchBox<T extends string>({ | |||
| <Box display="flex"> | |||
| <FormControl fullWidth> | |||
| <DatePicker | |||
| format={`${OUTPUT_DATE_FORMAT}`} | |||
| label={t(c.label)} | |||
| onChange={makeDateChangeHandler(c.paramName)} | |||
| value={ | |||
| @@ -416,6 +442,7 @@ function SearchBox<T extends string>({ | |||
| </Box> | |||
| <FormControl fullWidth> | |||
| <DatePicker | |||
| format={`${OUTPUT_DATE_FORMAT}`} | |||
| label={c.label2 ? t(c.label2) : null} | |||
| onChange={makeDateToChangeHandler(c.paramName)} | |||
| value={ | |||
| @@ -428,6 +455,50 @@ function SearchBox<T extends string>({ | |||
| </Box> | |||
| </LocalizationProvider> | |||
| )} | |||
| {c.type === "datetimeRange" && ( | |||
| <LocalizationProvider | |||
| dateAdapter={AdapterDayjs} | |||
| // TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD | |||
| adapterLocale="zh-hk" | |||
| > | |||
| <Box display="flex"> | |||
| <FormControl fullWidth> | |||
| <DateTimePicker | |||
| views={['year','month','day','hours', 'minutes', 'seconds']} | |||
| format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`} | |||
| label={t(c.label)} | |||
| onChange={makeDatetimeChangeHandler(c.paramName)} | |||
| value={ | |||
| dayjs(inputs[c.paramName]).isValid() | |||
| ? dayjs(inputs[c.paramName]) | |||
| : null | |||
| } | |||
| /> | |||
| </FormControl> | |||
| <Box | |||
| display="flex" | |||
| alignItems="center" | |||
| justifyContent="center" | |||
| marginInline={2} | |||
| > | |||
| {"-"} | |||
| </Box> | |||
| <FormControl fullWidth> | |||
| <DateTimePicker | |||
| views={['year','month','day','hours', 'minutes', 'seconds']} | |||
| format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`} | |||
| label={c.label2 ? t(c.label2) : null} | |||
| onChange={makeDatetimeToChangeHandler(c.paramName)} | |||
| value={ | |||
| dayjs(inputs[`${c.paramName}To`]).isValid() | |||
| ? dayjs(inputs[`${c.paramName}To`]) | |||
| : null | |||
| } | |||
| /> | |||
| </FormControl> | |||
| </Box> | |||
| </LocalizationProvider> | |||
| )} | |||
| {c.type === "date" && ( | |||
| <LocalizationProvider | |||
| dateAdapter={AdapterDayjs} | |||
| @@ -437,6 +508,7 @@ function SearchBox<T extends string>({ | |||
| <Box display="flex"> | |||
| <FormControl fullWidth> | |||
| <DatePicker | |||
| format={`${OUTPUT_DATE_FORMAT}`} | |||
| label={t(c.label)} | |||
| onChange={makeDateChangeHandler(c.paramName)} | |||
| /> | |||
| @@ -56,5 +56,5 @@ | |||
| "No": "無", | |||
| "Responsible Escalation List": "負責的上報列表", | |||
| "show completed logs": "顯示已完成上報", | |||
| "Rows per page": "每頁行數" | |||
| "Rows per page": "每頁行數" | |||
| } | |||
| @@ -271,5 +271,6 @@ | |||
| "Total (Verified + Bad + Missing) must equal Required quantity": "驗證數量 + 不良數量 + 缺失數量必須等於需求數量", | |||
| "BOM Status": "材料預備狀況", | |||
| "Estimated Production Date": "預計生產日期", | |||
| "Plan Start": "預計生產日期" | |||
| "Plan Start": "預計生產日期", | |||
| "Plan Start To": "預計生產日期(至)" | |||
| } | |||
| @@ -367,9 +367,17 @@ | |||
| "Enter missing quantity (required if no bad items)": "請輸入缺少數量(如果沒有不良項目)", | |||
| "Submit All Scanned": "提交所有已掃描項目", | |||
| "Submitting...": "提交中...", | |||
| "COMPLETED": "已完成" | |||
| "COMPLETED": "已完成", | |||
| "Confirm print: (": "確認列印全部草稿?(總數量:", | |||
| "piece(s))": "份)", | |||
| "Printing...": "列印中", | |||
| "Please wait...": "請稍後", | |||
| "No available pick order(s) for this floor.": "此樓層沒有可用的提料單", | |||
| "You already have a pick order in progess. Please complete it first before taking next pick order.": "請先完成目前的提料單,再提取下一張", | |||
| "Error occurred during assignment.": "提料單分配錯誤", | |||
| "Info": "消息", | |||
| "Warning": "警告", | |||
| "Error": "錯誤", | |||
| "Batch Print": "批量列印" | |||
| } | |||
| @@ -97,7 +97,7 @@ | |||
| "acceptedWeight": "接受重量", | |||
| "productionDate": "生產日期", | |||
| "reportQty": "上報數量", | |||
| "Default Warehouse": "預設倉庫", | |||
| "Default Warehouse": "建議倉位", | |||
| "Select warehouse": "選擇倉庫", | |||
| "Putaway Detail": "上架詳情", | |||
| "Delivery Detail": "來貨詳情", | |||