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