Explorar el Código

jo search

do finsih swtihc tab
production
CANCERYS\kw093 hace 1 mes
padre
commit
0eaee16db7
Se han modificado 11 ficheros con 222 adiciones y 44 borrados
  1. +9
    -2
      src/components/DoWorkbench/DoWorkbenchTabs.tsx
  2. +24
    -8
      src/components/DoWorkbench/GoodPickExecutionWorkbenchRecord.tsx
  3. +15
    -4
      src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx
  4. +47
    -24
      src/components/JoSearch/JoCreateFormModal.tsx
  5. +9
    -0
      src/components/JoSearch/JoSearch.tsx
  6. +9
    -0
      src/components/JoWorkbench/JoWorkbenchSearch.tsx
  7. +32
    -0
      src/hooks/useJoCreatePlanStartPrefs.ts
  8. +6
    -6
      src/i18n/index.tsx
  9. +1
    -0
      src/i18n/zh/jo.json
  10. +47
    -0
      src/utils/joCreatePlanStartPrefs.ts
  11. +23
    -0
      src/utils/workbenchTargetDate.ts

+ 9
- 2
src/components/DoWorkbench/DoWorkbenchTabs.tsx Ver fichero

@@ -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}>


+ 24
- 8
src/components/DoWorkbench/GoodPickExecutionWorkbenchRecord.tsx Ver fichero

@@ -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}


+ 15
- 4
src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx Ver fichero

@@ -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);


+ 47
- 24
src/components/JoSearch/JoCreateFormModal.tsx Ver fichero

@@ -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>


+ 9
- 0
src/components/JoSearch/JoSearch.tsx Ver fichero

@@ -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 });


+ 9
- 0
src/components/JoWorkbench/JoWorkbenchSearch.tsx Ver fichero

@@ -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 });


+ 32
- 0
src/hooks/useJoCreatePlanStartPrefs.ts Ver fichero

@@ -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,
};
}

+ 6
- 6
src/i18n/index.tsx Ver fichero

@@ -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;
};



+ 1
- 0
src/i18n/zh/jo.json Ver fichero

@@ -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": "預計生產日期至",


+ 47
- 0
src/utils/joCreatePlanStartPrefs.ts Ver fichero

@@ -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
}
}

+ 23
- 0
src/utils/workbenchTargetDate.ts Ver fichero

@@ -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");
}

Cargando…
Cancelar
Guardar