| @@ -65,6 +65,11 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ defaultTabIndex = 0, printerCom | |||
| urlTicketRaw && urlTicketRaw.trim() !== "" | |||
| ? decodeURIComponent(urlTicketRaw.trim()) | |||
| : null; | |||
| const urlTargetDateRaw = searchParams.get("targetDate"); | |||
| const urlTargetDate = | |||
| urlTargetDateRaw && urlTargetDateRaw.trim() !== "" | |||
| ? decodeURIComponent(urlTargetDateRaw.trim()) | |||
| : null; | |||
| const [tab, setTab] = React.useState<number>(defaultTabIndex); | |||
| const [a4Printer, setA4Printer] = React.useState<PrinterCombo | null>(null); | |||
| @@ -135,9 +140,10 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ defaultTabIndex = 0, printerCom | |||
| setTab(newTab); | |||
| const params = new URLSearchParams(searchParams.toString()); | |||
| 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) { | |||
| params.delete("ticketNo"); | |||
| params.delete("targetDate"); | |||
| } | |||
| const qs = params.toString(); | |||
| router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false }); | |||
| @@ -357,12 +363,13 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ defaultTabIndex = 0, printerCom | |||
| </TabPanel> | |||
| <TabPanel value={tab} index={2}> | |||
| <GoodPickExecutionWorkbenchRecord | |||
| key={`workbench-record-mine-${urlTicketNo ?? ""}`} | |||
| key={`workbench-record-mine-${urlTicketNo ?? ""}-${urlTargetDate ?? ""}`} | |||
| printerCombo={printerCombo} | |||
| listScope="mine" | |||
| a4Printer={a4Printer} | |||
| labelPrinter={labelPrinter} | |||
| initialTicketNo={urlTicketNo} | |||
| initialTargetDate={urlTargetDate} | |||
| /> | |||
| </TabPanel> | |||
| <TabPanel value={tab} index={3}> | |||
| @@ -38,6 +38,7 @@ import { | |||
| import { printDNWorkbench, printDNLabelsWorkbench, printDNLabelsReprintWorkbench } from "@/app/api/do/actions"; | |||
| import { fetchWorkbenchCompletedLotDetails } from "@/app/api/doworkbench/actions"; | |||
| import SearchBox, { Criterion } from "../SearchBox"; | |||
| import { resolveWorkbenchRecordTargetDate } from "@/utils/workbenchTargetDate"; | |||
| type Props = { | |||
| printerCombo: PrinterCombo[]; | |||
| @@ -45,6 +46,7 @@ type Props = { | |||
| a4Printer: PrinterCombo | null; | |||
| labelPrinter: PrinterCombo | null; | |||
| initialTicketNo?: string | null; | |||
| initialTargetDate?: string | null; | |||
| }; | |||
| const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({ | |||
| @@ -53,6 +55,7 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({ | |||
| a4Printer, | |||
| labelPrinter, | |||
| initialTicketNo, | |||
| initialTargetDate, | |||
| }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| @@ -60,9 +63,14 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({ | |||
| const [loading, setLoading] = useState(false); | |||
| 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 [selectedRecord, setSelectedRecord] = useState<CompletedDoPickOrderResponse | null>(null); | |||
| const [detailLotData, setDetailLotData] = useState<any[]>([]); | |||
| @@ -92,13 +100,18 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({ | |||
| }, [currentUserId, listScope]); | |||
| useEffect(() => { | |||
| const today = dayjs().format("YYYY-MM-DD"); | |||
| const targetDate = resolveWorkbenchRecordTargetDate(initialTargetDate); | |||
| const tn = initialTicketNo?.trim() || undefined; | |||
| setSearchQuery((prev) => ({ | |||
| ...prev, | |||
| targetDate, | |||
| ...(tn ? { ticketNo: tn } : {}), | |||
| })); | |||
| void loadData({ | |||
| targetDate: today, | |||
| targetDate, | |||
| ...(tn ? { ticketNo: tn } : {}), | |||
| }); | |||
| }, [loadData, initialTicketNo]); | |||
| }, [loadData, initialTicketNo, initialTargetDate]); | |||
| const searchCriteria: Criterion<any>[] = useMemo( | |||
| () => [ | |||
| @@ -122,6 +135,9 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({ | |||
| paramName: "targetDate", | |||
| type: "date", | |||
| defaultValue: dayjs().format("YYYY-MM-DD"), | |||
| ...(initialTargetDate | |||
| ? { preFilledValue: initialSearchTargetDate } | |||
| : {}), | |||
| }, | |||
| { | |||
| 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>) => { | |||
| @@ -599,7 +615,7 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({ | |||
| <Box> | |||
| <Box sx={{ mb: 2 }}> | |||
| <SearchBox | |||
| key={`workbench-search-${listScope}-${initialTicketNo ?? ""}`} | |||
| key={`workbench-search-${listScope}-${initialTicketNo ?? ""}-${initialTargetDate ?? ""}`} | |||
| criteria={searchCriteria} | |||
| onSearch={handleSearch} | |||
| onReset={handleSearchReset} | |||
| @@ -21,6 +21,7 @@ import { | |||
| Chip, | |||
| } from "@mui/material"; | |||
| import dayjs from 'dayjs'; | |||
| import { normalizeTargetDateInput } from "@/utils/workbenchTargetDate"; | |||
| import TestQrCodeProvider from "@/components/QrCodeScannerProvider/TestQrCodeProvider"; | |||
| import { fetchLotDetail } from "@/app/api/inventory/actions"; | |||
| import React, { useCallback, useEffect, useState, useRef, useMemo, startTransition } from "react"; | |||
| @@ -522,6 +523,7 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); | |||
| const workbenchHierarchicalReadyRef = useRef(false); | |||
| /** 最後一筆 workbench 票號(階層清空或完成後仍可用於導向完成紀錄) */ | |||
| const lastWorkbenchTicketNoRef = useRef<string | null>(null); | |||
| const lastWorkbenchTargetDateRef = useRef<string | null>(null); | |||
| /** 同一筆揀貨完成後只導向「完成紀錄」分頁一次 */ | |||
| const workbenchFinishNavigateDoneRef = useRef(false); | |||
| @@ -734,6 +736,10 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| String(hierarchicalData?.fgInfo?.ticketNo ?? "").trim() || | |||
| lastWorkbenchTicketNoRef.current || | |||
| ""; | |||
| const targetDateForRedirect = | |||
| normalizeTargetDateInput(hierarchicalData?.pickOrders?.[0]?.targetDate) || | |||
| lastWorkbenchTargetDateRef.current || | |||
| ""; | |||
| setCombinedLotData([]); | |||
| setOriginalCombinedData([]); | |||
| setAllLotsCompleted(false); | |||
| @@ -749,10 +755,13 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| !workbenchFinishNavigateDoneRef.current | |||
| ) { | |||
| 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; | |||
| } | |||
| @@ -808,6 +817,8 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| workbenchHierarchicalReadyRef.current = true; | |||
| lastWorkbenchTicketNoRef.current = | |||
| String(fgOrder.ticketNo ?? "").trim() || null; | |||
| lastWorkbenchTargetDateRef.current = | |||
| normalizeTargetDateInput(mergedPickOrder.targetDate); | |||
| workbenchFinishNavigateDoneRef.current = false; | |||
| console.log(" DEBUG fgOrder.lineCountsPerPickOrder:", fgOrder.lineCountsPerPickOrder); | |||
| 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 { OUTPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT, dateStringToDayjs, dayjsToDateString, dayjsToDateTimeString } from "@/app/utils/formatUtil"; | |||
| 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 { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
| import dayjs, { Dayjs } from "dayjs"; | |||
| @@ -18,6 +18,9 @@ interface Props { | |||
| open: boolean; | |||
| bomCombo: BomCombo[]; | |||
| jobTypes: JobTypeResponse[]; | |||
| defaultPlanStart: string; | |||
| rememberPlanStart: boolean; | |||
| onRememberPlanStartChange: (checked: boolean, selectedDate: string | null) => void; | |||
| onClose: () => void; | |||
| onSearch: () => void; | |||
| } | |||
| @@ -26,6 +29,9 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||
| open, | |||
| bomCombo, | |||
| jobTypes, | |||
| defaultPlanStart, | |||
| rememberPlanStart, | |||
| onRememberPlanStartChange, | |||
| onClose, | |||
| onSearch, | |||
| }) => { | |||
| @@ -40,6 +46,12 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||
| }); | |||
| 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 变化 | |||
| const selectedBomId = watch("bomId"); | |||
| /* | |||
| @@ -89,15 +101,9 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||
| console.log("BOM changed to:", value); | |||
| onChange(value.id); | |||
| // 1) 根据 BOM 设置数量 | |||
| if (value.outputQty != null) { | |||
| 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] | |||
| ); | |||
| @@ -456,26 +462,43 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||
| required: "Plan start required!", | |||
| validate: { | |||
| 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 } }) => ( | |||
| // <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> | |||
| @@ -31,6 +31,7 @@ import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | |||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
| import { updateJoPlanStart } from "@/app/api/jo/actions"; | |||
| import { arrayToDayjs } from "@/app/utils/formatUtil"; | |||
| import { useJoCreatePlanStartPrefs } from "@/hooks/useJoCreatePlanStartPrefs"; | |||
| interface Props { | |||
| defaultInputs: SearchJoResultRequest, | |||
| @@ -51,6 +52,11 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| ) | |||
| const [totalCount, setTotalCount] = useState(0) | |||
| const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false) | |||
| const { | |||
| rememberPlanStart, | |||
| defaultPlanStartForCreate, | |||
| handleRememberPlanStartChange, | |||
| } = useJoCreatePlanStartPrefs() | |||
| const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); | |||
| const [detailedJos, setDetailedJos] = useState<Map<number, JobOrder>>(new Map()); | |||
| @@ -763,6 +769,9 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| open={isCreateJoModalOpen} | |||
| bomCombo={bomCombo} | |||
| jobTypes={jobTypes} | |||
| defaultPlanStart={defaultPlanStartForCreate} | |||
| rememberPlanStart={rememberPlanStart} | |||
| onRememberPlanStartChange={handleRememberPlanStartChange} | |||
| onClose={onCloseCreateJoModal} | |||
| onSearch={() => { | |||
| setInputs({ ...defaultInputs }); | |||
| @@ -32,6 +32,7 @@ import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | |||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
| import { updateJoPlanStart } from "@/app/api/jo/actions"; | |||
| import { arrayToDayjs } from "@/app/utils/formatUtil"; | |||
| import { useJoCreatePlanStartPrefs } from "@/hooks/useJoCreatePlanStartPrefs"; | |||
| interface Props { | |||
| defaultInputs: SearchJoResultRequest, | |||
| @@ -52,6 +53,11 @@ const JoWorkbenchSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCo | |||
| ) | |||
| const [totalCount, setTotalCount] = useState(0) | |||
| const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false) | |||
| const { | |||
| rememberPlanStart, | |||
| defaultPlanStartForCreate, | |||
| handleRememberPlanStartChange, | |||
| } = useJoCreatePlanStartPrefs() | |||
| const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); | |||
| const [detailedJos, setDetailedJos] = useState<Map<number, JobOrder>>(new Map()); | |||
| @@ -764,6 +770,9 @@ const JoWorkbenchSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCo | |||
| open={isCreateJoModalOpen} | |||
| bomCombo={bomCombo} | |||
| jobTypes={jobTypes} | |||
| defaultPlanStart={defaultPlanStartForCreate} | |||
| rememberPlanStart={rememberPlanStart} | |||
| onRememberPlanStartChange={handleRememberPlanStartChange} | |||
| onClose={onCloseCreateJoModal} | |||
| onSearch={() => { | |||
| 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(); | |||
| console.time("[i18n] detectLanguage total"); | |||
| console.time("[i18n] getServerSession"); | |||
| //console.time("[i18n] detectLanguage total"); | |||
| //console.time("[i18n] getServerSession"); | |||
| const session = await getServerSession(authOptions); | |||
| console.timeEnd("[i18n] getServerSession"); | |||
| console.time("[i18n] universalLanguageDetect"); | |||
| //console.timeEnd("[i18n] getServerSession"); | |||
| //console.time("[i18n] universalLanguageDetect"); | |||
| const lang = universalLanguageDetect({ | |||
| supportedLanguages: SUPPORTED_LANGUAGES, | |||
| fallbackLanguage: FALLBACK_LANG, | |||
| acceptLanguageHeader: headersList.get("accept-language") || undefined, | |||
| serverCookies: cookiesObj, | |||
| }); | |||
| console.timeEnd("[i18n] universalLanguageDetect"); | |||
| console.timeEnd("[i18n] detectLanguage total"); | |||
| //console.timeEnd("[i18n] universalLanguageDetect"); | |||
| //console.timeEnd("[i18n] detectLanguage total"); | |||
| return lang; | |||
| }; | |||
| @@ -396,6 +396,7 @@ | |||
| "Job Order Type": "工單類型", | |||
| "Estimated Production Date": "預計生產日期", | |||
| "Plan Start": "預計生產日期", | |||
| "Remember plan start as default": "記住為預設日期", | |||
| "Plan Start From": "預計生產日期", | |||
| "Delivery Note Code": "送貨單編號", | |||
| "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"); | |||
| } | |||