|
- import { BomCombo } from "@/app/api/bom";
- 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 { DatePicker, DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers";
- import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
- import dayjs, { Dayjs } from "dayjs";
- import { isFinite } from "lodash";
- import React, { SetStateAction, SyntheticEvent, useCallback, useEffect, useMemo, useState} from "react";
- import { Controller, FormProvider, SubmitErrorHandler, SubmitHandler, useForm, useFormContext } from "react-hook-form";
- import { useTranslation } from "react-i18next";
- import { msg } from "../Swal/CustomAlerts";
- import { JobTypeResponse } from "@/app/api/jo/actions";
-
- interface Props {
- open: boolean;
- bomCombo: BomCombo[];
- jobTypes: JobTypeResponse[];
- onClose: () => void;
- onSearch: () => void;
- }
-
- const JoCreateFormModal: React.FC<Props> = ({
- open,
- bomCombo,
- jobTypes,
- onClose,
- onSearch,
- }) => {
- const { t } = useTranslation("jo");
- const [multiplier, setMultiplier] = useState<number>(1);
- const [isSubmitting, setIsSubmitting] = useState(false);
- const formProps = useForm<SaveJo>({
- mode: "onChange",
- defaultValues: {
- productionPriority: 50
- }
- });
- const { reset, trigger, watch, control, register, formState: { errors }, setValue } = formProps
-
- // 监听 bomId 变化
- const selectedBomId = watch("bomId");
- /*
- const handleAutoCompleteChange = useCallback(
- (event: SyntheticEvent<Element, Event>, value: BomCombo, onChange: (...event: any[]) => void) => {
- console.log("BOM changed to:", value);
- onChange(value.id);
-
- // 重置倍数为 1
- setMultiplier(1);
-
- // 1) 根据 BOM 设置数量(倍数 * outputQty)
- if (value.outputQty != null) {
- const calculatedQty = 1 * Number(value.outputQty);
- formProps.setValue("reqQty", calculatedQty, { shouldValidate: true, shouldDirty: true });
- }
-
- // 2) 选 BOM 时,把日期默认设为"今天"
- const today = dayjs();
- const todayStr = dayjsToDateString(today, "input");
- formProps.setValue("planStart", todayStr, { shouldValidate: true, shouldDirty: true });
- },
- [formProps]
- );
- */
- // 添加 useEffect 来监听倍数变化,自动计算 reqQty
- useEffect(() => {
- const selectedBom = bomCombo.find(bom => bom.id === selectedBomId);
- if (selectedBom && selectedBom.outputQty != null) {
- const calculatedQty = multiplier * Number(selectedBom.outputQty);
- formProps.setValue("reqQty", calculatedQty, { shouldValidate: true, shouldDirty: true });
- }
- }, [multiplier, selectedBomId, bomCombo, formProps]);
- const onModalClose = useCallback(() => {
- if (isSubmitting) return;
- reset()
- onClose()
- setMultiplier(1);
- }, [reset, onClose, isSubmitting])
- const duplicateLabels = useMemo(() => {
- const count = new Map<string, number>();
- bomCombo.forEach((b) => count.set(b.label, (count.get(b.label) ?? 0) + 1));
- return new Set(Array.from(count.entries()).filter(([, c]) => c > 1).map(([l]) => l));
- }, [bomCombo]);
- const handleAutoCompleteChange = useCallback(
- (event: SyntheticEvent<Element, Event>, value: BomCombo, onChange: (...event: any[]) => void) => {
- 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]
- );
-
-
- // 当 BOM 改变时,自动选择匹配的 Job Type
- useEffect(() => {
- if (!selectedBomId) {
- return;
- }
-
- const selectedBom = bomCombo.find(bom => bom.id === selectedBomId);
- if (!selectedBom) {
- return;
- }
-
- const description = selectedBom.description;
- console.log("Auto-select effect - BOM description:", description);
-
- if (!description) {
- console.log("Auto-select effect - No description found, skipping auto-select");
- return;
- }
-
- const descriptionUpper = description.toUpperCase();
- console.log("Auto-selecting Job Type for BOM description:", descriptionUpper);
-
- // 查找匹配的 Job Type
- const matchingJobType = jobTypes.find(jt => {
- const jobTypeName = jt.name.toUpperCase();
- const matches = jobTypeName === descriptionUpper;
- console.log(`Checking JobType ${jt.name} (${jobTypeName}) against ${descriptionUpper}: ${matches}`);
- return matches;
- });
-
- if (matchingJobType) {
- console.log("Found matching Job Type, setting jobTypeId to:", matchingJobType.id);
- setValue("jobTypeId", matchingJobType.id, { shouldValidate: true, shouldDirty: true });
- } else {
- console.log("No matching Job Type found for description:", descriptionUpper);
- }
- }, [selectedBomId, bomCombo, jobTypes, setValue]);
-
- const handleDateTimePickerChange = useCallback((value: Dayjs | null, onChange: (...event: any[]) => void) => {
- if (value != null) {
- const updatedValue = dayjsToDateTimeString(value)
- onChange(updatedValue)
- } else {
- onChange(value)
- }
- }, [])
-
- const onSubmit = useCallback<SubmitHandler<SaveJo>>(async (data) => {
- if (isSubmitting) return;
- setIsSubmitting(true);
- try {
- data.type = "manual"
- if (data.planStart) {
- const dateDayjs = dateStringToDayjs(data.planStart)
- data.planStart = dayjsToDateTimeString(dateDayjs.startOf('day'))
- }
- data.jobTypeId = Number(data.jobTypeId);
- // 如果 productionPriority 为空或无效,使用默认值 50
- data.productionPriority = data.productionPriority != null && !isNaN(data.productionPriority)
- ? Number(data.productionPriority)
- : 50;
- const response = await manualCreateJo(data)
- if (response) {
- onSearch();
- msg(t("update success"));
- onModalClose();
- }
- } catch (e) {
- console.error(e);
- msg(t("update failed"));
- } finally {
- setIsSubmitting(false);
- }
- }, [onSearch, onModalClose, t, isSubmitting])
-
- const onSubmitError = useCallback<SubmitErrorHandler<SaveJo>>((error) => {
- console.log(error)
- }, [])
-
- const planStart = watch("planStart")
- const planEnd = watch("planEnd")
- useEffect(() => {
- trigger(['planStart', 'planEnd']);
- }, [trigger, planStart, planEnd])
-
- return (
- <Modal
- open={open}
- onClose={onModalClose}
- >
- <Card
- style={{
- flex: 10,
- marginBottom: "20px",
- width: "90%",
- // height: "80%",
- position: "fixed",
- top: "50%",
- left: "50%",
- transform: "translate(-50%, -50%)",
- }}
- >
- <Box
- sx={{
- display: "flex",
- flexDirection: "column",
- padding: "20px",
- height: "100%", //'30rem',
- width: "100%",
- "& .actions": {
- color: "text.secondary",
- },
- "& .header": {
- // border: 1,
- // 'border-width': '1px',
- // 'border-color': 'grey',
- },
- "& .textPrimary": {
- color: "text.primary",
- },
- }}
- >
- <FormProvider {...formProps}>
- <Stack
- // spacing={2}
- component="form"
- onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
- >
- <LocalizationProvider
- dateAdapter={AdapterDayjs}
- // TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
- adapterLocale="zh-hk"
- >
- <Grid container spacing={2}>
- <Grid item xs={12} sm={12} md={12}>
- <Typography variant="h6">{t("Create Job Order")}</Typography>
- </Grid>
- <Grid item xs={12} sm={12} md={6}>
- <Controller
- control={control}
- name="bomId"
- rules={{
- required: "Bom required!",
- validate: (value) => isFinite(value)
- }}
- render={({ field, fieldState: { error } }) => (
- <Autocomplete
- disableClearable
- options={bomCombo}
- getOptionLabel={(option) => {
- if (!option) return "";
- if (duplicateLabels.has(option.label)) {
- const d = (option.description || "").trim().toUpperCase();
- const suffix = d === "WIP" ? t("WIP") : d === "FG" ? t("FG") : option.description ? t(option.description) : "";
- return suffix ? `${option.label} (${suffix})` : option.label;
- }
- return option.label;
- }}
- onChange={(event, value) => {
- handleAutoCompleteChange(event, value, field.onChange);
- }}
- onBlur={field.onBlur}
- renderInput={(params) => (
- <TextField
- {...params}
- error={Boolean(error)}
- variant="outlined"
- label={t("Bom")}
- />
- )}
- />
- )}
- />
- </Grid>
- <Grid item xs={12} sm={12} md={6}>
- <Controller
- control={control}
- name="reqQty"
- rules={{
- required: "Req. Qty. required!",
- validate: (value) => value > 0
- }}
- render={({ field, fieldState: { error } }) => {
- const selectedBom = bomCombo.find(bom => bom.id === formProps.watch("bomId"));
- const uom = selectedBom?.outputQtyUom || "";
- const outputQty = selectedBom?.outputQty ?? 0;
- const calculatedValue = multiplier * outputQty;
-
- return (
- <Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
- <TextField
- label={t("Base Qty")}
- fullWidth
- type="number"
- variant="outlined"
- value={outputQty}
- disabled
- InputProps={{
- endAdornment: uom ? (
- <InputAdornment position="end">
- <Typography variant="body2" sx={{ color: "text.secondary" }}>
- {uom}
- </Typography>
- </InputAdornment>
- ) : null
- }}
- sx={{ flex: 1 }}
- />
-
- <Typography variant="body1" sx={{ color: "text.secondary" }}>
- ×
- </Typography>
- <TextField
- label={t("Batch Count")}
- fullWidth
- type="number"
- variant="outlined"
- value={multiplier}
- onChange={(e) => {
- const val = e.target.value === "" ? 1 : Math.max(1, Math.floor(Number(e.target.value)));
- setMultiplier(val);
- }}
- inputProps={{
- min: 1,
- step: 1
- }}
- sx={{ flex: 1 }}
- />
- <Typography variant="body1" sx={{ color: "text.secondary" }}>
- =
- </Typography>
- <TextField
- {...field}
- label={t("Req. Qty")}
- fullWidth
- error={Boolean(error)}
- variant="outlined"
- type="number"
- value={calculatedValue || ""}
- disabled
- InputProps={{
- endAdornment: uom ? (
- <InputAdornment position="end">
- <Typography variant="body2" sx={{ color: "text.secondary" }}>
- {uom}
- </Typography>
- </InputAdornment>
- ) : null
- }}
- sx={{ flex: 1 }}
- />
- </Box>
- );
- }}
- />
- </Grid>
- <Grid item xs={12} sm={12} md={6}>
- <Controller
- control={control}
- name="jobTypeId"
- rules={{ required: t("Job Type required!") as string }}
- render={({ field, fieldState: { error } }) => {
- //console.log("Job Type Select render - filteredJobTypes:", filteredJobTypes);
- //console.log("Current field.value:", field.value);
-
- return (
- <FormControl fullWidth error={Boolean(error)}>
- <InputLabel>{t("Job Type")}</InputLabel>
- <Select
- {...field}
- label={t("Job Type")}
- value={field.value?.toString() ?? ""}
- onChange={(event) => {
- const value = event.target.value;
- console.log("Job Type changed to:", value);
- field.onChange(value === "" ? undefined : Number(value));
- }}
- >
- <MenuItem value="">
- <em>{t("Please select")}</em>
- </MenuItem>
- {/* {filteredJobTypes.map((jobType) => (*/}
- {jobTypes.map((jobType) => (
- <MenuItem key={jobType.id} value={jobType.id.toString()}>
- {t(jobType.name)}
- </MenuItem>
- ))}
- </Select>
- </FormControl>
- );
- }}
- />
- </Grid>
- <Grid item xs={12} sm={12} md={6}>
- <Controller
- control={control}
- name="productionPriority"
- rules={{
- required: t("Production Priority required!") as string,
- max: {
- value: 100,
- message: t("Production Priority cannot exceed 100") as string
- },
- min: {
- value: 1,
- message: t("Production Priority must be at least 1") as string
- },
- validate: (value) => {
- if (value === undefined || value === null || isNaN(value)) {
- return t("Production Priority required!") as string;
- }
- return true;
- }
- }}
- render={({ field, fieldState: { error } }) => (
- <TextField
- {...field}
- label={t("Production Priority")}
- fullWidth
- error={Boolean(error)}
- variant="outlined"
- type="number"
- inputProps={{
- min: 1,
- max: 100,
- step: 1
- }}
- value={field.value ?? ""}
- onChange={(e) => {
- const inputValue = e.target.value;
- // 允许空字符串(用户正在删除)
- if (inputValue === "") {
- field.onChange("");
- return;
- }
- // 转换为数字并验证范围
- const numValue = Number(inputValue);
- if (!isNaN(numValue) && numValue >= 1 && numValue <= 100) {
- field.onChange(numValue);
- }
- }}
- />
- )}
- />
- </Grid>
- <Grid item xs={12} sm={12} md={6}>
- <Controller
- control={control}
- name="planStart"
- rules={{
- 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) } }}
- />
- )}
- />
- </Grid>
- {/* <Grid item xs={12} sm={12} md={6}>
- <Controller
- control={control}
- name="planEnd"
- rules={{
- required: "Plan end required!",
- validate: {
- isValid: (value) => dateStringToDayjs(value).isValid(),
- isBeforePlanEnd: (value) => {
- const planStartDayjs = dateStringToDayjs(planStart)
- const planEndDayjs = dateStringToDayjs(value)
- return planEndDayjs.isAfter(planStartDayjs) || planEndDayjs.isSame(planStartDayjs)
- }
- }
- }}
- render={({ field, fieldState: { error } }) => (
- <DateTimePicker
- label={t("Plan End")}
- format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`}
- onChange={(newValue: Dayjs | null) => {
- handleDateTimePickerChange(newValue, field.onChange)
- }}
- slotProps={{ textField: { fullWidth: true } }}
- />
- )}
- />
- </Grid> */}
- </Grid>
- <Stack
- direction="row"
- justifyContent="flex-end"
- spacing={2}
- sx={{ mt: 2 }}
- >
- <Button
- name="submit"
- variant="contained"
- startIcon={isSubmitting ? <CircularProgress size={16} color="inherit" /> : <Check />}
- type="submit"
- disabled={isSubmitting}
- >
- {isSubmitting ? t("Creating...") : t("Create")}
- </Button>
- </Stack>
- </LocalizationProvider>
- </Stack>
- </FormProvider>
- </Box>
- </Card>
- </Modal>
- )
- }
-
- export default JoCreateFormModal;
|