From 0eaee16db7ef7bda6d664d10dd022debb8ec01cf Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Mon, 18 May 2026 15:05:44 +0800 Subject: [PATCH] jo search do finsih swtihc tab --- .../DoWorkbench/DoWorkbenchTabs.tsx | 11 ++- .../GoodPickExecutionWorkbenchRecord.tsx | 32 ++++++--- .../WorkbenchGoodPickExecutionDetail.tsx | 19 +++-- src/components/JoSearch/JoCreateFormModal.tsx | 71 ++++++++++++------- src/components/JoSearch/JoSearch.tsx | 9 +++ .../JoWorkbench/JoWorkbenchSearch.tsx | 9 +++ src/hooks/useJoCreatePlanStartPrefs.ts | 32 +++++++++ src/i18n/index.tsx | 12 ++-- src/i18n/zh/jo.json | 1 + src/utils/joCreatePlanStartPrefs.ts | 47 ++++++++++++ src/utils/workbenchTargetDate.ts | 23 ++++++ 11 files changed, 222 insertions(+), 44 deletions(-) create mode 100644 src/hooks/useJoCreatePlanStartPrefs.ts create mode 100644 src/utils/joCreatePlanStartPrefs.ts create mode 100644 src/utils/workbenchTargetDate.ts diff --git a/src/components/DoWorkbench/DoWorkbenchTabs.tsx b/src/components/DoWorkbench/DoWorkbenchTabs.tsx index f117c45..a8c24c3 100644 --- a/src/components/DoWorkbench/DoWorkbenchTabs.tsx +++ b/src/components/DoWorkbench/DoWorkbenchTabs.tsx @@ -65,6 +65,11 @@ const DoWorkbenchTabsInner: React.FC = ({ 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(defaultTabIndex); const [a4Printer, setA4Printer] = React.useState(null); @@ -135,9 +140,10 @@ const DoWorkbenchTabsInner: React.FC = ({ 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 = ({ defaultTabIndex = 0, printerCom diff --git a/src/components/DoWorkbench/GoodPickExecutionWorkbenchRecord.tsx b/src/components/DoWorkbench/GoodPickExecutionWorkbenchRecord.tsx index 9f46403..d4a08e6 100644 --- a/src/components/DoWorkbench/GoodPickExecutionWorkbenchRecord.tsx +++ b/src/components/DoWorkbench/GoodPickExecutionWorkbenchRecord.tsx @@ -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 = ({ @@ -53,6 +55,7 @@ const GoodPickExecutionWorkbenchRecord: React.FC = ({ a4Printer, labelPrinter, initialTicketNo, + initialTargetDate, }) => { const { t } = useTranslation("pickOrder"); const { data: session } = useSession() as { data: SessionWithTokens | null }; @@ -60,9 +63,14 @@ const GoodPickExecutionWorkbenchRecord: React.FC = ({ const [loading, setLoading] = useState(false); const [records, setRecords] = useState([]); - const [searchQuery, setSearchQuery] = useState>({ - targetDate: dayjs().format("YYYY-MM-DD"), - }); + const initialSearchTargetDate = useMemo( + () => resolveWorkbenchRecordTargetDate(initialTargetDate), + [initialTargetDate], + ); + + const [searchQuery, setSearchQuery] = useState>(() => ({ + targetDate: resolveWorkbenchRecordTargetDate(initialTargetDate), + })); const [showDetailView, setShowDetailView] = useState(false); const [selectedRecord, setSelectedRecord] = useState(null); const [detailLotData, setDetailLotData] = useState([]); @@ -92,13 +100,18 @@ const GoodPickExecutionWorkbenchRecord: React.FC = ({ }, [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[] = useMemo( () => [ @@ -122,6 +135,9 @@ const GoodPickExecutionWorkbenchRecord: React.FC = ({ 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 = ({ : {}), }, ], - [t, initialTicketNo], + [t, initialTicketNo, initialTargetDate, initialSearchTargetDate], ); const handleSearch = useCallback((query: Record) => { @@ -599,7 +615,7 @@ const GoodPickExecutionWorkbenchRecord: React.FC = ({ (null); + const lastWorkbenchTargetDateRef = useRef(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); diff --git a/src/components/JoSearch/JoCreateFormModal.tsx b/src/components/JoSearch/JoCreateFormModal.tsx index b84acea..033e348 100644 --- a/src/components/JoSearch/JoCreateFormModal.tsx +++ b/src/components/JoSearch/JoCreateFormModal.tsx @@ -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 = ({ open, bomCombo, jobTypes, + defaultPlanStart, + rememberPlanStart, + onRememberPlanStartChange, onClose, onSearch, }) => { @@ -40,6 +46,12 @@ const JoCreateFormModal: React.FC = ({ }); 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 = ({ 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 = ({ 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 } }) => ( - // { - handleDateTimePickerChange(newValue, field.onChange) - }} - slotProps={{ textField: { fullWidth: true, error: Boolean(error) } }} - /> + + + { + handleDateTimePickerChange(newValue, field.onChange); + if (rememberPlanStart && newValue) { + onRememberPlanStartChange(true, dayjsToDateString(newValue, "input")); + } + }} + slotProps={{ textField: { fullWidth: true, error: Boolean(error) } }} + /> + + { + 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" }} + /> + )} /> diff --git a/src/components/JoSearch/JoSearch.tsx b/src/components/JoSearch/JoSearch.tsx index 580887f..34dc8f9 100644 --- a/src/components/JoSearch/JoSearch.tsx +++ b/src/components/JoSearch/JoSearch.tsx @@ -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 = ({ defaultInputs, bomCombo, printerCombo, jobT ) const [totalCount, setTotalCount] = useState(0) const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false) + const { + rememberPlanStart, + defaultPlanStartForCreate, + handleRememberPlanStartChange, + } = useJoCreatePlanStartPrefs() const [inventoryData, setInventoryData] = useState([]); const [detailedJos, setDetailedJos] = useState>(new Map()); @@ -763,6 +769,9 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT open={isCreateJoModalOpen} bomCombo={bomCombo} jobTypes={jobTypes} + defaultPlanStart={defaultPlanStartForCreate} + rememberPlanStart={rememberPlanStart} + onRememberPlanStartChange={handleRememberPlanStartChange} onClose={onCloseCreateJoModal} onSearch={() => { setInputs({ ...defaultInputs }); diff --git a/src/components/JoWorkbench/JoWorkbenchSearch.tsx b/src/components/JoWorkbench/JoWorkbenchSearch.tsx index e3b188b..f5ca4cb 100644 --- a/src/components/JoWorkbench/JoWorkbenchSearch.tsx +++ b/src/components/JoWorkbench/JoWorkbenchSearch.tsx @@ -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 = ({ defaultInputs, bomCombo, printerCo ) const [totalCount, setTotalCount] = useState(0) const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false) + const { + rememberPlanStart, + defaultPlanStartForCreate, + handleRememberPlanStartChange, + } = useJoCreatePlanStartPrefs() const [inventoryData, setInventoryData] = useState([]); const [detailedJos, setDetailedJos] = useState>(new Map()); @@ -764,6 +770,9 @@ const JoWorkbenchSearch: React.FC = ({ defaultInputs, bomCombo, printerCo open={isCreateJoModalOpen} bomCombo={bomCombo} jobTypes={jobTypes} + defaultPlanStart={defaultPlanStartForCreate} + rememberPlanStart={rememberPlanStart} + onRememberPlanStartChange={handleRememberPlanStartChange} onClose={onCloseCreateJoModal} onSearch={() => { setInputs({ ...defaultInputs }); diff --git a/src/hooks/useJoCreatePlanStartPrefs.ts b/src/hooks/useJoCreatePlanStartPrefs.ts new file mode 100644 index 0000000..14dad7f --- /dev/null +++ b/src/hooks/useJoCreatePlanStartPrefs.ts @@ -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(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, + }; +} diff --git a/src/i18n/index.tsx b/src/i18n/index.tsx index 1e624ab..c04fcce 100644 --- a/src/i18n/index.tsx +++ b/src/i18n/index.tsx @@ -19,19 +19,19 @@ export const detectLanguage = async (): Promise => { {}, ); 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; }; diff --git a/src/i18n/zh/jo.json b/src/i18n/zh/jo.json index 5629a2f..eccb36c 100644 --- a/src/i18n/zh/jo.json +++ b/src/i18n/zh/jo.json @@ -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": "預計生產日期至", diff --git a/src/utils/joCreatePlanStartPrefs.ts b/src/utils/joCreatePlanStartPrefs.ts new file mode 100644 index 0000000..599ee68 --- /dev/null +++ b/src/utils/joCreatePlanStartPrefs.ts @@ -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 + } +} diff --git a/src/utils/workbenchTargetDate.ts b/src/utils/workbenchTargetDate.ts new file mode 100644 index 0000000..54b485d --- /dev/null +++ b/src/utils/workbenchTargetDate.ts @@ -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"); +}