| @@ -65,6 +65,11 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ defaultTabIndex = 0, printerCom | |||||
| urlTicketRaw && urlTicketRaw.trim() !== "" | urlTicketRaw && urlTicketRaw.trim() !== "" | ||||
| ? decodeURIComponent(urlTicketRaw.trim()) | ? decodeURIComponent(urlTicketRaw.trim()) | ||||
| : null; | : null; | ||||
| const urlTargetDateRaw = searchParams.get("targetDate"); | |||||
| const urlTargetDate = | |||||
| urlTargetDateRaw && urlTargetDateRaw.trim() !== "" | |||||
| ? decodeURIComponent(urlTargetDateRaw.trim()) | |||||
| : null; | |||||
| const [tab, setTab] = React.useState<number>(defaultTabIndex); | const [tab, setTab] = React.useState<number>(defaultTabIndex); | ||||
| const [a4Printer, setA4Printer] = React.useState<PrinterCombo | null>(null); | const [a4Printer, setA4Printer] = React.useState<PrinterCombo | null>(null); | ||||
| @@ -135,9 +140,10 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ defaultTabIndex = 0, printerCom | |||||
| setTab(newTab); | setTab(newTab); | ||||
| const params = new URLSearchParams(searchParams.toString()); | const params = new URLSearchParams(searchParams.toString()); | ||||
| params.set("tab", String(newTab)); | params.set("tab", String(newTab)); | ||||
| /* ticketNo deep-link only for "Finished Good Record" (mine) */ | |||||
| /* ticketNo / targetDate deep-link only for "Finished Good Record" (mine) */ | |||||
| if (newTab !== 2) { | if (newTab !== 2) { | ||||
| params.delete("ticketNo"); | params.delete("ticketNo"); | ||||
| params.delete("targetDate"); | |||||
| } | } | ||||
| const qs = params.toString(); | const qs = params.toString(); | ||||
| router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false }); | router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false }); | ||||
| @@ -357,12 +363,13 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ defaultTabIndex = 0, printerCom | |||||
| </TabPanel> | </TabPanel> | ||||
| <TabPanel value={tab} index={2}> | <TabPanel value={tab} index={2}> | ||||
| <GoodPickExecutionWorkbenchRecord | <GoodPickExecutionWorkbenchRecord | ||||
| key={`workbench-record-mine-${urlTicketNo ?? ""}`} | |||||
| key={`workbench-record-mine-${urlTicketNo ?? ""}-${urlTargetDate ?? ""}`} | |||||
| printerCombo={printerCombo} | printerCombo={printerCombo} | ||||
| listScope="mine" | listScope="mine" | ||||
| a4Printer={a4Printer} | a4Printer={a4Printer} | ||||
| labelPrinter={labelPrinter} | labelPrinter={labelPrinter} | ||||
| initialTicketNo={urlTicketNo} | initialTicketNo={urlTicketNo} | ||||
| initialTargetDate={urlTargetDate} | |||||
| /> | /> | ||||
| </TabPanel> | </TabPanel> | ||||
| <TabPanel value={tab} index={3}> | <TabPanel value={tab} index={3}> | ||||
| @@ -38,6 +38,7 @@ import { | |||||
| import { printDNWorkbench, printDNLabelsWorkbench, printDNLabelsReprintWorkbench } from "@/app/api/do/actions"; | import { printDNWorkbench, printDNLabelsWorkbench, printDNLabelsReprintWorkbench } from "@/app/api/do/actions"; | ||||
| import { fetchWorkbenchCompletedLotDetails } from "@/app/api/doworkbench/actions"; | import { fetchWorkbenchCompletedLotDetails } from "@/app/api/doworkbench/actions"; | ||||
| import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
| import { resolveWorkbenchRecordTargetDate } from "@/utils/workbenchTargetDate"; | |||||
| type Props = { | type Props = { | ||||
| printerCombo: PrinterCombo[]; | printerCombo: PrinterCombo[]; | ||||
| @@ -45,6 +46,7 @@ type Props = { | |||||
| a4Printer: PrinterCombo | null; | a4Printer: PrinterCombo | null; | ||||
| labelPrinter: PrinterCombo | null; | labelPrinter: PrinterCombo | null; | ||||
| initialTicketNo?: string | null; | initialTicketNo?: string | null; | ||||
| initialTargetDate?: string | null; | |||||
| }; | }; | ||||
| const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({ | const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({ | ||||
| @@ -53,6 +55,7 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({ | |||||
| a4Printer, | a4Printer, | ||||
| labelPrinter, | labelPrinter, | ||||
| initialTicketNo, | initialTicketNo, | ||||
| initialTargetDate, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("pickOrder"); | const { t } = useTranslation("pickOrder"); | ||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | const { data: session } = useSession() as { data: SessionWithTokens | null }; | ||||
| @@ -60,9 +63,14 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({ | |||||
| const [loading, setLoading] = useState(false); | const [loading, setLoading] = useState(false); | ||||
| const [records, setRecords] = useState<CompletedDoPickOrderResponse[]>([]); | const [records, setRecords] = useState<CompletedDoPickOrderResponse[]>([]); | ||||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({ | |||||
| targetDate: dayjs().format("YYYY-MM-DD"), | |||||
| }); | |||||
| const initialSearchTargetDate = useMemo( | |||||
| () => resolveWorkbenchRecordTargetDate(initialTargetDate), | |||||
| [initialTargetDate], | |||||
| ); | |||||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>(() => ({ | |||||
| targetDate: resolveWorkbenchRecordTargetDate(initialTargetDate), | |||||
| })); | |||||
| const [showDetailView, setShowDetailView] = useState(false); | const [showDetailView, setShowDetailView] = useState(false); | ||||
| const [selectedRecord, setSelectedRecord] = useState<CompletedDoPickOrderResponse | null>(null); | const [selectedRecord, setSelectedRecord] = useState<CompletedDoPickOrderResponse | null>(null); | ||||
| const [detailLotData, setDetailLotData] = useState<any[]>([]); | const [detailLotData, setDetailLotData] = useState<any[]>([]); | ||||
| @@ -92,13 +100,18 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({ | |||||
| }, [currentUserId, listScope]); | }, [currentUserId, listScope]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| const today = dayjs().format("YYYY-MM-DD"); | |||||
| const targetDate = resolveWorkbenchRecordTargetDate(initialTargetDate); | |||||
| const tn = initialTicketNo?.trim() || undefined; | const tn = initialTicketNo?.trim() || undefined; | ||||
| setSearchQuery((prev) => ({ | |||||
| ...prev, | |||||
| targetDate, | |||||
| ...(tn ? { ticketNo: tn } : {}), | |||||
| })); | |||||
| void loadData({ | void loadData({ | ||||
| targetDate: today, | |||||
| targetDate, | |||||
| ...(tn ? { ticketNo: tn } : {}), | ...(tn ? { ticketNo: tn } : {}), | ||||
| }); | }); | ||||
| }, [loadData, initialTicketNo]); | |||||
| }, [loadData, initialTicketNo, initialTargetDate]); | |||||
| const searchCriteria: Criterion<any>[] = useMemo( | const searchCriteria: Criterion<any>[] = useMemo( | ||||
| () => [ | () => [ | ||||
| @@ -122,6 +135,9 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({ | |||||
| paramName: "targetDate", | paramName: "targetDate", | ||||
| type: "date", | type: "date", | ||||
| defaultValue: dayjs().format("YYYY-MM-DD"), | defaultValue: dayjs().format("YYYY-MM-DD"), | ||||
| ...(initialTargetDate | |||||
| ? { preFilledValue: initialSearchTargetDate } | |||||
| : {}), | |||||
| }, | }, | ||||
| { | { | ||||
| label: t("Ticket No"), | label: t("Ticket No"), | ||||
| @@ -132,7 +148,7 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({ | |||||
| : {}), | : {}), | ||||
| }, | }, | ||||
| ], | ], | ||||
| [t, initialTicketNo], | |||||
| [t, initialTicketNo, initialTargetDate, initialSearchTargetDate], | |||||
| ); | ); | ||||
| const handleSearch = useCallback((query: Record<string, any>) => { | const handleSearch = useCallback((query: Record<string, any>) => { | ||||
| @@ -599,7 +615,7 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({ | |||||
| <Box> | <Box> | ||||
| <Box sx={{ mb: 2 }}> | <Box sx={{ mb: 2 }}> | ||||
| <SearchBox | <SearchBox | ||||
| key={`workbench-search-${listScope}-${initialTicketNo ?? ""}`} | |||||
| key={`workbench-search-${listScope}-${initialTicketNo ?? ""}-${initialTargetDate ?? ""}`} | |||||
| criteria={searchCriteria} | criteria={searchCriteria} | ||||
| onSearch={handleSearch} | onSearch={handleSearch} | ||||
| onReset={handleSearchReset} | onReset={handleSearchReset} | ||||
| @@ -21,6 +21,7 @@ import { | |||||
| Chip, | Chip, | ||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import dayjs from 'dayjs'; | import dayjs from 'dayjs'; | ||||
| import { normalizeTargetDateInput } from "@/utils/workbenchTargetDate"; | |||||
| import TestQrCodeProvider from "@/components/QrCodeScannerProvider/TestQrCodeProvider"; | import TestQrCodeProvider from "@/components/QrCodeScannerProvider/TestQrCodeProvider"; | ||||
| import { fetchLotDetail } from "@/app/api/inventory/actions"; | import { fetchLotDetail } from "@/app/api/inventory/actions"; | ||||
| import React, { useCallback, useEffect, useState, useRef, useMemo, startTransition } from "react"; | import React, { useCallback, useEffect, useState, useRef, useMemo, startTransition } from "react"; | ||||
| @@ -522,6 +523,7 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); | |||||
| const workbenchHierarchicalReadyRef = useRef(false); | const workbenchHierarchicalReadyRef = useRef(false); | ||||
| /** 最後一筆 workbench 票號(階層清空或完成後仍可用於導向完成紀錄) */ | /** 最後一筆 workbench 票號(階層清空或完成後仍可用於導向完成紀錄) */ | ||||
| const lastWorkbenchTicketNoRef = useRef<string | null>(null); | const lastWorkbenchTicketNoRef = useRef<string | null>(null); | ||||
| const lastWorkbenchTargetDateRef = useRef<string | null>(null); | |||||
| /** 同一筆揀貨完成後只導向「完成紀錄」分頁一次 */ | /** 同一筆揀貨完成後只導向「完成紀錄」分頁一次 */ | ||||
| const workbenchFinishNavigateDoneRef = useRef(false); | const workbenchFinishNavigateDoneRef = useRef(false); | ||||
| @@ -734,6 +736,10 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| String(hierarchicalData?.fgInfo?.ticketNo ?? "").trim() || | String(hierarchicalData?.fgInfo?.ticketNo ?? "").trim() || | ||||
| lastWorkbenchTicketNoRef.current || | lastWorkbenchTicketNoRef.current || | ||||
| ""; | ""; | ||||
| const targetDateForRedirect = | |||||
| normalizeTargetDateInput(hierarchicalData?.pickOrders?.[0]?.targetDate) || | |||||
| lastWorkbenchTargetDateRef.current || | |||||
| ""; | |||||
| setCombinedLotData([]); | setCombinedLotData([]); | ||||
| setOriginalCombinedData([]); | setOriginalCombinedData([]); | ||||
| setAllLotsCompleted(false); | setAllLotsCompleted(false); | ||||
| @@ -749,10 +755,13 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| !workbenchFinishNavigateDoneRef.current | !workbenchFinishNavigateDoneRef.current | ||||
| ) { | ) { | ||||
| workbenchFinishNavigateDoneRef.current = true; | workbenchFinishNavigateDoneRef.current = true; | ||||
| router.replace( | |||||
| `${pathname}?tab=2&ticketNo=${encodeURIComponent(ticketForRedirect)}`, | |||||
| { scroll: false }, | |||||
| ); | |||||
| const redirectParams = new URLSearchParams(); | |||||
| redirectParams.set("tab", "2"); | |||||
| redirectParams.set("ticketNo", ticketForRedirect); | |||||
| if (targetDateForRedirect) { | |||||
| redirectParams.set("targetDate", targetDateForRedirect); | |||||
| } | |||||
| router.replace(`${pathname}?${redirectParams.toString()}`, { scroll: false }); | |||||
| } | } | ||||
| return; | return; | ||||
| } | } | ||||
| @@ -808,6 +817,8 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| workbenchHierarchicalReadyRef.current = true; | workbenchHierarchicalReadyRef.current = true; | ||||
| lastWorkbenchTicketNoRef.current = | lastWorkbenchTicketNoRef.current = | ||||
| String(fgOrder.ticketNo ?? "").trim() || null; | String(fgOrder.ticketNo ?? "").trim() || null; | ||||
| lastWorkbenchTargetDateRef.current = | |||||
| normalizeTargetDateInput(mergedPickOrder.targetDate); | |||||
| workbenchFinishNavigateDoneRef.current = false; | workbenchFinishNavigateDoneRef.current = false; | ||||
| console.log(" DEBUG fgOrder.lineCountsPerPickOrder:", fgOrder.lineCountsPerPickOrder); | console.log(" DEBUG fgOrder.lineCountsPerPickOrder:", fgOrder.lineCountsPerPickOrder); | ||||
| console.log(" DEBUG fgOrder.pickOrderCodes:", fgOrder.pickOrderCodes); | console.log(" DEBUG fgOrder.pickOrderCodes:", fgOrder.pickOrderCodes); | ||||
| @@ -3,7 +3,7 @@ import { JoDetail } from "@/app/api/jo"; | |||||
| import { SaveJo, manualCreateJo } from "@/app/api/jo/actions"; | import { SaveJo, manualCreateJo } from "@/app/api/jo/actions"; | ||||
| import { OUTPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT, dateStringToDayjs, dayjsToDateString, dayjsToDateTimeString } from "@/app/utils/formatUtil"; | import { OUTPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT, dateStringToDayjs, dayjsToDateString, dayjsToDateTimeString } from "@/app/utils/formatUtil"; | ||||
| import { Check } from "@mui/icons-material"; | import { Check } from "@mui/icons-material"; | ||||
| import { Autocomplete, Box, Button, Card, CircularProgress, Grid, Modal, Stack, TextField, Typography ,FormControl, InputLabel, Select, MenuItem,InputAdornment} from "@mui/material"; | |||||
| import { Autocomplete, Box, Button, Card, Checkbox, CircularProgress, FormControlLabel, Grid, Modal, Stack, TextField, Typography ,FormControl, InputLabel, Select, MenuItem,InputAdornment} from "@mui/material"; | |||||
| import { DatePicker, DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers"; | import { DatePicker, DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers"; | ||||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | ||||
| import dayjs, { Dayjs } from "dayjs"; | import dayjs, { Dayjs } from "dayjs"; | ||||
| @@ -18,6 +18,9 @@ interface Props { | |||||
| open: boolean; | open: boolean; | ||||
| bomCombo: BomCombo[]; | bomCombo: BomCombo[]; | ||||
| jobTypes: JobTypeResponse[]; | jobTypes: JobTypeResponse[]; | ||||
| defaultPlanStart: string; | |||||
| rememberPlanStart: boolean; | |||||
| onRememberPlanStartChange: (checked: boolean, selectedDate: string | null) => void; | |||||
| onClose: () => void; | onClose: () => void; | ||||
| onSearch: () => void; | onSearch: () => void; | ||||
| } | } | ||||
| @@ -26,6 +29,9 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||||
| open, | open, | ||||
| bomCombo, | bomCombo, | ||||
| jobTypes, | jobTypes, | ||||
| defaultPlanStart, | |||||
| rememberPlanStart, | |||||
| onRememberPlanStartChange, | |||||
| onClose, | onClose, | ||||
| onSearch, | onSearch, | ||||
| }) => { | }) => { | ||||
| @@ -40,6 +46,12 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||||
| }); | }); | ||||
| const { reset, trigger, watch, control, register, formState: { errors }, setValue } = formProps | const { reset, trigger, watch, control, register, formState: { errors }, setValue } = formProps | ||||
| useEffect(() => { | |||||
| if (!open) return; | |||||
| const dateDayjs = dateStringToDayjs(defaultPlanStart); | |||||
| setValue("planStart", dayjsToDateTimeString(dateDayjs.startOf("day")), { shouldValidate: true }); | |||||
| }, [open, defaultPlanStart, setValue]); | |||||
| // 监听 bomId 变化 | // 监听 bomId 变化 | ||||
| const selectedBomId = watch("bomId"); | const selectedBomId = watch("bomId"); | ||||
| /* | /* | ||||
| @@ -89,15 +101,9 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||||
| console.log("BOM changed to:", value); | console.log("BOM changed to:", value); | ||||
| onChange(value.id); | onChange(value.id); | ||||
| // 1) 根据 BOM 设置数量 | |||||
| if (value.outputQty != null) { | if (value.outputQty != null) { | ||||
| formProps.setValue("reqQty", Number(value.outputQty), { shouldValidate: true, shouldDirty: true }); | formProps.setValue("reqQty", Number(value.outputQty), { shouldValidate: true, shouldDirty: true }); | ||||
| } | } | ||||
| // 2) 选 BOM 时,把日期默认设为“今天” | |||||
| const today = dayjs(); | |||||
| const todayStr = dayjsToDateString(today, "input"); // 你已经有的工具函数 | |||||
| formProps.setValue("planStart", todayStr, { shouldValidate: true, shouldDirty: true }); | |||||
| }, | }, | ||||
| [formProps] | [formProps] | ||||
| ); | ); | ||||
| @@ -456,26 +462,43 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||||
| required: "Plan start required!", | required: "Plan start required!", | ||||
| validate: { | validate: { | ||||
| isValid: (value) => dateStringToDayjs(value).isValid(), | isValid: (value) => dateStringToDayjs(value).isValid(), | ||||
| // isBeforePlanEnd: (value) => { | |||||
| // const planStartDayjs = dateStringToDayjs(value) | |||||
| // const planEndDayjs = dateStringToDayjs(planEnd) | |||||
| // return planStartDayjs.isBefore(planEndDayjs) || planStartDayjs.isSame(planEndDayjs) | |||||
| // } | |||||
| } | } | ||||
| }} | }} | ||||
| render={({ field, fieldState: { error } }) => ( | render={({ field, fieldState: { error } }) => ( | ||||
| // <DateTimePicker | |||||
| <DatePicker | |||||
| label={t("Plan Start")} | |||||
| // views={['year','month','day','hours', 'minutes', 'seconds']} | |||||
| //format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`} | |||||
| format={OUTPUT_DATE_FORMAT} | |||||
| value={field.value ? dateStringToDayjs(field.value) : null} | |||||
| onChange={(newValue: Dayjs | null) => { | |||||
| handleDateTimePickerChange(newValue, field.onChange) | |||||
| }} | |||||
| slotProps={{ textField: { fullWidth: true, error: Boolean(error) } }} | |||||
| /> | |||||
| <Stack direction="row" alignItems="flex-start" spacing={1}> | |||||
| <Box sx={{ flex: 1 }}> | |||||
| <DatePicker | |||||
| label={t("Plan Start")} | |||||
| format={OUTPUT_DATE_FORMAT} | |||||
| value={field.value ? dateStringToDayjs(field.value) : null} | |||||
| onChange={(newValue: Dayjs | null) => { | |||||
| handleDateTimePickerChange(newValue, field.onChange); | |||||
| if (rememberPlanStart && newValue) { | |||||
| onRememberPlanStartChange(true, dayjsToDateString(newValue, "input")); | |||||
| } | |||||
| }} | |||||
| slotProps={{ textField: { fullWidth: true, error: Boolean(error) } }} | |||||
| /> | |||||
| </Box> | |||||
| <FormControlLabel | |||||
| control={ | |||||
| <Checkbox | |||||
| checked={rememberPlanStart} | |||||
| onChange={(e) => { | |||||
| const checked = e.target.checked; | |||||
| const current = watch("planStart"); | |||||
| const dateStr = | |||||
| current && dateStringToDayjs(current).isValid() | |||||
| ? dayjsToDateString(dateStringToDayjs(current), "input") | |||||
| : defaultPlanStart; | |||||
| onRememberPlanStartChange(checked, checked ? dateStr : null); | |||||
| }} | |||||
| /> | |||||
| } | |||||
| label={t("Remember plan start as default")} | |||||
| sx={{ mt: 1, whiteSpace: "nowrap" }} | |||||
| /> | |||||
| </Stack> | |||||
| )} | )} | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| @@ -31,6 +31,7 @@ import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | |||||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | ||||
| import { updateJoPlanStart } from "@/app/api/jo/actions"; | import { updateJoPlanStart } from "@/app/api/jo/actions"; | ||||
| import { arrayToDayjs } from "@/app/utils/formatUtil"; | import { arrayToDayjs } from "@/app/utils/formatUtil"; | ||||
| import { useJoCreatePlanStartPrefs } from "@/hooks/useJoCreatePlanStartPrefs"; | |||||
| interface Props { | interface Props { | ||||
| defaultInputs: SearchJoResultRequest, | defaultInputs: SearchJoResultRequest, | ||||
| @@ -51,6 +52,11 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| ) | ) | ||||
| const [totalCount, setTotalCount] = useState(0) | const [totalCount, setTotalCount] = useState(0) | ||||
| const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false) | const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false) | ||||
| const { | |||||
| rememberPlanStart, | |||||
| defaultPlanStartForCreate, | |||||
| handleRememberPlanStartChange, | |||||
| } = useJoCreatePlanStartPrefs() | |||||
| 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()); | ||||
| @@ -763,6 +769,9 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| open={isCreateJoModalOpen} | open={isCreateJoModalOpen} | ||||
| bomCombo={bomCombo} | bomCombo={bomCombo} | ||||
| jobTypes={jobTypes} | jobTypes={jobTypes} | ||||
| defaultPlanStart={defaultPlanStartForCreate} | |||||
| rememberPlanStart={rememberPlanStart} | |||||
| onRememberPlanStartChange={handleRememberPlanStartChange} | |||||
| onClose={onCloseCreateJoModal} | onClose={onCloseCreateJoModal} | ||||
| onSearch={() => { | onSearch={() => { | ||||
| setInputs({ ...defaultInputs }); | setInputs({ ...defaultInputs }); | ||||
| @@ -32,6 +32,7 @@ import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | |||||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | ||||
| import { updateJoPlanStart } from "@/app/api/jo/actions"; | import { updateJoPlanStart } from "@/app/api/jo/actions"; | ||||
| import { arrayToDayjs } from "@/app/utils/formatUtil"; | import { arrayToDayjs } from "@/app/utils/formatUtil"; | ||||
| import { useJoCreatePlanStartPrefs } from "@/hooks/useJoCreatePlanStartPrefs"; | |||||
| interface Props { | interface Props { | ||||
| defaultInputs: SearchJoResultRequest, | defaultInputs: SearchJoResultRequest, | ||||
| @@ -52,6 +53,11 @@ const JoWorkbenchSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCo | |||||
| ) | ) | ||||
| const [totalCount, setTotalCount] = useState(0) | const [totalCount, setTotalCount] = useState(0) | ||||
| const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false) | const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false) | ||||
| const { | |||||
| rememberPlanStart, | |||||
| defaultPlanStartForCreate, | |||||
| handleRememberPlanStartChange, | |||||
| } = useJoCreatePlanStartPrefs() | |||||
| 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()); | ||||
| @@ -764,6 +770,9 @@ const JoWorkbenchSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCo | |||||
| open={isCreateJoModalOpen} | open={isCreateJoModalOpen} | ||||
| bomCombo={bomCombo} | bomCombo={bomCombo} | ||||
| jobTypes={jobTypes} | jobTypes={jobTypes} | ||||
| defaultPlanStart={defaultPlanStartForCreate} | |||||
| rememberPlanStart={rememberPlanStart} | |||||
| onRememberPlanStartChange={handleRememberPlanStartChange} | |||||
| onClose={onCloseCreateJoModal} | onClose={onCloseCreateJoModal} | ||||
| onSearch={() => { | onSearch={() => { | ||||
| setInputs({ ...defaultInputs }); | setInputs({ ...defaultInputs }); | ||||
| @@ -0,0 +1,32 @@ | |||||
| import { useCallback, useMemo, useState } from "react"; | |||||
| import dayjs from "dayjs"; | |||||
| import { dayjsToDateString } from "@/app/utils/formatUtil"; | |||||
| import { | |||||
| JoCreatePlanStartPrefs, | |||||
| loadJoCreatePlanStartPrefs, | |||||
| saveJoCreatePlanStartPrefs, | |||||
| } from "@/utils/joCreatePlanStartPrefs"; | |||||
| export function useJoCreatePlanStartPrefs() { | |||||
| const [prefs, setPrefs] = useState<JoCreatePlanStartPrefs>(loadJoCreatePlanStartPrefs); | |||||
| const defaultPlanStartForCreate = useMemo( | |||||
| () => prefs.planStart ?? dayjsToDateString(dayjs(), "input"), | |||||
| [prefs.planStart], | |||||
| ); | |||||
| const handleRememberPlanStartChange = useCallback((checked: boolean, selectedDate: string | null) => { | |||||
| const next: JoCreatePlanStartPrefs = { | |||||
| enabled: checked, | |||||
| planStart: checked && selectedDate ? selectedDate : null, | |||||
| }; | |||||
| setPrefs(next); | |||||
| saveJoCreatePlanStartPrefs(next); | |||||
| }, []); | |||||
| return { | |||||
| rememberPlanStart: prefs.enabled, | |||||
| defaultPlanStartForCreate, | |||||
| handleRememberPlanStartChange, | |||||
| }; | |||||
| } | |||||
| @@ -19,19 +19,19 @@ export const detectLanguage = async (): Promise<string> => { | |||||
| {}, | {}, | ||||
| ); | ); | ||||
| const headersList = headers(); | const headersList = headers(); | ||||
| console.time("[i18n] detectLanguage total"); | |||||
| console.time("[i18n] getServerSession"); | |||||
| //console.time("[i18n] detectLanguage total"); | |||||
| //console.time("[i18n] getServerSession"); | |||||
| const session = await getServerSession(authOptions); | const session = await getServerSession(authOptions); | ||||
| console.timeEnd("[i18n] getServerSession"); | |||||
| console.time("[i18n] universalLanguageDetect"); | |||||
| //console.timeEnd("[i18n] getServerSession"); | |||||
| //console.time("[i18n] universalLanguageDetect"); | |||||
| const lang = universalLanguageDetect({ | const lang = universalLanguageDetect({ | ||||
| supportedLanguages: SUPPORTED_LANGUAGES, | supportedLanguages: SUPPORTED_LANGUAGES, | ||||
| fallbackLanguage: FALLBACK_LANG, | fallbackLanguage: FALLBACK_LANG, | ||||
| acceptLanguageHeader: headersList.get("accept-language") || undefined, | acceptLanguageHeader: headersList.get("accept-language") || undefined, | ||||
| serverCookies: cookiesObj, | serverCookies: cookiesObj, | ||||
| }); | }); | ||||
| console.timeEnd("[i18n] universalLanguageDetect"); | |||||
| console.timeEnd("[i18n] detectLanguage total"); | |||||
| //console.timeEnd("[i18n] universalLanguageDetect"); | |||||
| //console.timeEnd("[i18n] detectLanguage total"); | |||||
| return lang; | return lang; | ||||
| }; | }; | ||||
| @@ -396,6 +396,7 @@ | |||||
| "Job Order Type": "工單類型", | "Job Order Type": "工單類型", | ||||
| "Estimated Production Date": "預計生產日期", | "Estimated Production Date": "預計生產日期", | ||||
| "Plan Start": "預計生產日期", | "Plan Start": "預計生產日期", | ||||
| "Remember plan start as default": "記住為預設日期", | |||||
| "Plan Start From": "預計生產日期", | "Plan Start From": "預計生產日期", | ||||
| "Delivery Note Code": "送貨單編號", | "Delivery Note Code": "送貨單編號", | ||||
| "Plan Start To": "預計生產日期至", | "Plan Start To": "預計生產日期至", | ||||
| @@ -0,0 +1,47 @@ | |||||
| import dayjs from "dayjs"; | |||||
| export const JO_CREATE_PLAN_START_KEY = "fpsms.jo.create.rememberPlanStart"; | |||||
| export type JoCreatePlanStartPrefs = { | |||||
| enabled: boolean; | |||||
| planStart: string | null; | |||||
| }; | |||||
| const DEFAULT_PREFS: JoCreatePlanStartPrefs = { enabled: false, planStart: null }; | |||||
| function isValidInputDate(value: string | null | undefined): value is string { | |||||
| return Boolean(value && dayjs(value).isValid()); | |||||
| } | |||||
| export function loadJoCreatePlanStartPrefs(): JoCreatePlanStartPrefs { | |||||
| if (typeof window === "undefined") { | |||||
| return DEFAULT_PREFS; | |||||
| } | |||||
| try { | |||||
| const raw = sessionStorage.getItem(JO_CREATE_PLAN_START_KEY); | |||||
| if (!raw) { | |||||
| return DEFAULT_PREFS; | |||||
| } | |||||
| const parsed = JSON.parse(raw) as JoCreatePlanStartPrefs; | |||||
| if (!parsed.enabled) { | |||||
| return { enabled: false, planStart: null }; | |||||
| } | |||||
| if (isValidInputDate(parsed.planStart)) { | |||||
| return { enabled: true, planStart: parsed.planStart }; | |||||
| } | |||||
| } catch { | |||||
| // ignore invalid storage | |||||
| } | |||||
| return DEFAULT_PREFS; | |||||
| } | |||||
| export function saveJoCreatePlanStartPrefs(prefs: JoCreatePlanStartPrefs): void { | |||||
| if (typeof window === "undefined") { | |||||
| return; | |||||
| } | |||||
| try { | |||||
| sessionStorage.setItem(JO_CREATE_PLAN_START_KEY, JSON.stringify(prefs)); | |||||
| } catch { | |||||
| // ignore quota / private mode errors | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,23 @@ | |||||
| import dayjs from "dayjs"; | |||||
| import { arrayToDateString } from "@/app/utils/formatUtil"; | |||||
| /** Normalize API targetDate (string or date array) to YYYY-MM-DD for search / URL. */ | |||||
| export function normalizeTargetDateInput(value: unknown): string | null { | |||||
| if (value == null || value === "") { | |||||
| return null; | |||||
| } | |||||
| try { | |||||
| if (Array.isArray(value)) { | |||||
| const s = arrayToDateString(value, "input"); | |||||
| return dayjs(s).isValid() ? dayjs(s).format("YYYY-MM-DD") : null; | |||||
| } | |||||
| const d = dayjs(String(value)); | |||||
| return d.isValid() ? d.format("YYYY-MM-DD") : null; | |||||
| } catch { | |||||
| return null; | |||||
| } | |||||
| } | |||||
| export function resolveWorkbenchRecordTargetDate(initial?: string | null): string { | |||||
| return normalizeTargetDateInput(initial) ?? dayjs().format("YYYY-MM-DD"); | |||||
| } | |||||