FPSMS-frontend
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 

535 rindas
30 KiB

  1. import { BomCombo } from "@/app/api/bom";
  2. import { JoDetail } from "@/app/api/jo";
  3. import { SaveJo, manualCreateJo } from "@/app/api/jo/actions";
  4. import { OUTPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT, dateStringToDayjs, dayjsToDateString, dayjsToDateTimeString } from "@/app/utils/formatUtil";
  5. import { Check } from "@mui/icons-material";
  6. import { Autocomplete, Box, Button, Card, CircularProgress, Grid, Modal, Stack, TextField, Typography ,FormControl, InputLabel, Select, MenuItem,InputAdornment} from "@mui/material";
  7. import { DatePicker, DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers";
  8. import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
  9. import dayjs, { Dayjs } from "dayjs";
  10. import { isFinite } from "lodash";
  11. import React, { SetStateAction, SyntheticEvent, useCallback, useEffect, useMemo, useState} from "react";
  12. import { Controller, FormProvider, SubmitErrorHandler, SubmitHandler, useForm, useFormContext } from "react-hook-form";
  13. import { useTranslation } from "react-i18next";
  14. import { msg } from "../Swal/CustomAlerts";
  15. import { JobTypeResponse } from "@/app/api/jo/actions";
  16. interface Props {
  17. open: boolean;
  18. bomCombo: BomCombo[];
  19. jobTypes: JobTypeResponse[];
  20. onClose: () => void;
  21. onSearch: () => void;
  22. }
  23. const JoCreateFormModal: React.FC<Props> = ({
  24. open,
  25. bomCombo,
  26. jobTypes,
  27. onClose,
  28. onSearch,
  29. }) => {
  30. const { t } = useTranslation("jo");
  31. const [multiplier, setMultiplier] = useState<number>(1);
  32. const [isSubmitting, setIsSubmitting] = useState(false);
  33. const formProps = useForm<SaveJo>({
  34. mode: "onChange",
  35. defaultValues: {
  36. productionPriority: 50
  37. }
  38. });
  39. const { reset, trigger, watch, control, register, formState: { errors }, setValue } = formProps
  40. // 监听 bomId 变化
  41. const selectedBomId = watch("bomId");
  42. /*
  43. const handleAutoCompleteChange = useCallback(
  44. (event: SyntheticEvent<Element, Event>, value: BomCombo, onChange: (...event: any[]) => void) => {
  45. console.log("BOM changed to:", value);
  46. onChange(value.id);
  47. // 重置倍数为 1
  48. setMultiplier(1);
  49. // 1) 根据 BOM 设置数量(倍数 * outputQty)
  50. if (value.outputQty != null) {
  51. const calculatedQty = 1 * Number(value.outputQty);
  52. formProps.setValue("reqQty", calculatedQty, { shouldValidate: true, shouldDirty: true });
  53. }
  54. // 2) 选 BOM 时,把日期默认设为"今天"
  55. const today = dayjs();
  56. const todayStr = dayjsToDateString(today, "input");
  57. formProps.setValue("planStart", todayStr, { shouldValidate: true, shouldDirty: true });
  58. },
  59. [formProps]
  60. );
  61. */
  62. // 添加 useEffect 来监听倍数变化,自动计算 reqQty
  63. useEffect(() => {
  64. const selectedBom = bomCombo.find(bom => bom.id === selectedBomId);
  65. if (selectedBom && selectedBom.outputQty != null) {
  66. const calculatedQty = multiplier * Number(selectedBom.outputQty);
  67. formProps.setValue("reqQty", calculatedQty, { shouldValidate: true, shouldDirty: true });
  68. }
  69. }, [multiplier, selectedBomId, bomCombo, formProps]);
  70. const onModalClose = useCallback(() => {
  71. if (isSubmitting) return;
  72. reset()
  73. onClose()
  74. setMultiplier(1);
  75. }, [reset, onClose, isSubmitting])
  76. const duplicateLabels = useMemo(() => {
  77. const count = new Map<string, number>();
  78. bomCombo.forEach((b) => count.set(b.label, (count.get(b.label) ?? 0) + 1));
  79. return new Set(Array.from(count.entries()).filter(([, c]) => c > 1).map(([l]) => l));
  80. }, [bomCombo]);
  81. const handleAutoCompleteChange = useCallback(
  82. (event: SyntheticEvent<Element, Event>, value: BomCombo, onChange: (...event: any[]) => void) => {
  83. console.log("BOM changed to:", value);
  84. onChange(value.id);
  85. // 1) 根据 BOM 设置数量
  86. if (value.outputQty != null) {
  87. formProps.setValue("reqQty", Number(value.outputQty), { shouldValidate: true, shouldDirty: true });
  88. }
  89. // 2) 选 BOM 时,把日期默认设为“今天”
  90. const today = dayjs();
  91. const todayStr = dayjsToDateString(today, "input"); // 你已经有的工具函数
  92. formProps.setValue("planStart", todayStr, { shouldValidate: true, shouldDirty: true });
  93. },
  94. [formProps]
  95. );
  96. // 当 BOM 改变时,自动选择匹配的 Job Type
  97. useEffect(() => {
  98. if (!selectedBomId) {
  99. return;
  100. }
  101. const selectedBom = bomCombo.find(bom => bom.id === selectedBomId);
  102. if (!selectedBom) {
  103. return;
  104. }
  105. const description = selectedBom.description;
  106. console.log("Auto-select effect - BOM description:", description);
  107. if (!description) {
  108. console.log("Auto-select effect - No description found, skipping auto-select");
  109. return;
  110. }
  111. const descriptionUpper = description.toUpperCase();
  112. console.log("Auto-selecting Job Type for BOM description:", descriptionUpper);
  113. // 查找匹配的 Job Type
  114. const matchingJobType = jobTypes.find(jt => {
  115. const jobTypeName = jt.name.toUpperCase();
  116. const matches = jobTypeName === descriptionUpper;
  117. console.log(`Checking JobType ${jt.name} (${jobTypeName}) against ${descriptionUpper}: ${matches}`);
  118. return matches;
  119. });
  120. if (matchingJobType) {
  121. console.log("Found matching Job Type, setting jobTypeId to:", matchingJobType.id);
  122. setValue("jobTypeId", matchingJobType.id, { shouldValidate: true, shouldDirty: true });
  123. } else {
  124. console.log("No matching Job Type found for description:", descriptionUpper);
  125. }
  126. }, [selectedBomId, bomCombo, jobTypes, setValue]);
  127. const handleDateTimePickerChange = useCallback((value: Dayjs | null, onChange: (...event: any[]) => void) => {
  128. if (value != null) {
  129. const updatedValue = dayjsToDateTimeString(value)
  130. onChange(updatedValue)
  131. } else {
  132. onChange(value)
  133. }
  134. }, [])
  135. const onSubmit = useCallback<SubmitHandler<SaveJo>>(async (data) => {
  136. if (isSubmitting) return;
  137. setIsSubmitting(true);
  138. try {
  139. data.type = "manual"
  140. if (data.planStart) {
  141. const dateDayjs = dateStringToDayjs(data.planStart)
  142. data.planStart = dayjsToDateTimeString(dateDayjs.startOf('day'))
  143. }
  144. data.jobTypeId = Number(data.jobTypeId);
  145. // 如果 productionPriority 为空或无效,使用默认值 50
  146. data.productionPriority = data.productionPriority != null && !isNaN(data.productionPriority)
  147. ? Number(data.productionPriority)
  148. : 50;
  149. const response = await manualCreateJo(data)
  150. if (response) {
  151. onSearch();
  152. msg(t("update success"));
  153. onModalClose();
  154. }
  155. } catch (e) {
  156. console.error(e);
  157. msg(t("update failed"));
  158. } finally {
  159. setIsSubmitting(false);
  160. }
  161. }, [onSearch, onModalClose, t, isSubmitting])
  162. const onSubmitError = useCallback<SubmitErrorHandler<SaveJo>>((error) => {
  163. console.log(error)
  164. }, [])
  165. const planStart = watch("planStart")
  166. const planEnd = watch("planEnd")
  167. useEffect(() => {
  168. trigger(['planStart', 'planEnd']);
  169. }, [trigger, planStart, planEnd])
  170. return (
  171. <Modal
  172. open={open}
  173. onClose={onModalClose}
  174. >
  175. <Card
  176. style={{
  177. flex: 10,
  178. marginBottom: "20px",
  179. width: "90%",
  180. // height: "80%",
  181. position: "fixed",
  182. top: "50%",
  183. left: "50%",
  184. transform: "translate(-50%, -50%)",
  185. }}
  186. >
  187. <Box
  188. sx={{
  189. display: "flex",
  190. flexDirection: "column",
  191. padding: "20px",
  192. height: "100%", //'30rem',
  193. width: "100%",
  194. "& .actions": {
  195. color: "text.secondary",
  196. },
  197. "& .header": {
  198. // border: 1,
  199. // 'border-width': '1px',
  200. // 'border-color': 'grey',
  201. },
  202. "& .textPrimary": {
  203. color: "text.primary",
  204. },
  205. }}
  206. >
  207. <FormProvider {...formProps}>
  208. <Stack
  209. // spacing={2}
  210. component="form"
  211. onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
  212. >
  213. <LocalizationProvider
  214. dateAdapter={AdapterDayjs}
  215. // TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
  216. adapterLocale="zh-hk"
  217. >
  218. <Grid container spacing={2}>
  219. <Grid item xs={12} sm={12} md={12}>
  220. <Typography variant="h6">{t("Create Job Order")}</Typography>
  221. </Grid>
  222. <Grid item xs={12} sm={12} md={6}>
  223. <Controller
  224. control={control}
  225. name="bomId"
  226. rules={{
  227. required: "Bom required!",
  228. validate: (value) => isFinite(value)
  229. }}
  230. render={({ field, fieldState: { error } }) => (
  231. <Autocomplete
  232. disableClearable
  233. options={bomCombo}
  234. getOptionLabel={(option) => {
  235. if (!option) return "";
  236. if (duplicateLabels.has(option.label)) {
  237. const d = (option.description || "").trim().toUpperCase();
  238. const suffix = d === "WIP" ? t("WIP") : d === "FG" ? t("FG") : option.description ? t(option.description) : "";
  239. return suffix ? `${option.label} (${suffix})` : option.label;
  240. }
  241. return option.label;
  242. }}
  243. onChange={(event, value) => {
  244. handleAutoCompleteChange(event, value, field.onChange);
  245. }}
  246. onBlur={field.onBlur}
  247. renderInput={(params) => (
  248. <TextField
  249. {...params}
  250. error={Boolean(error)}
  251. variant="outlined"
  252. label={t("Bom")}
  253. />
  254. )}
  255. />
  256. )}
  257. />
  258. </Grid>
  259. <Grid item xs={12} sm={12} md={6}>
  260. <Controller
  261. control={control}
  262. name="reqQty"
  263. rules={{
  264. required: "Req. Qty. required!",
  265. validate: (value) => value > 0
  266. }}
  267. render={({ field, fieldState: { error } }) => {
  268. const selectedBom = bomCombo.find(bom => bom.id === formProps.watch("bomId"));
  269. const uom = selectedBom?.outputQtyUom || "";
  270. const outputQty = selectedBom?.outputQty ?? 0;
  271. const calculatedValue = multiplier * outputQty;
  272. return (
  273. <Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
  274. <TextField
  275. label={t("Base Qty")}
  276. fullWidth
  277. type="number"
  278. variant="outlined"
  279. value={outputQty}
  280. disabled
  281. InputProps={{
  282. endAdornment: uom ? (
  283. <InputAdornment position="end">
  284. <Typography variant="body2" sx={{ color: "text.secondary" }}>
  285. {uom}
  286. </Typography>
  287. </InputAdornment>
  288. ) : null
  289. }}
  290. sx={{ flex: 1 }}
  291. />
  292. <Typography variant="body1" sx={{ color: "text.secondary" }}>
  293. ×
  294. </Typography>
  295. <TextField
  296. label={t("Batch Count")}
  297. fullWidth
  298. type="number"
  299. variant="outlined"
  300. value={multiplier}
  301. onChange={(e) => {
  302. const val = e.target.value === "" ? 1 : Math.max(1, Math.floor(Number(e.target.value)));
  303. setMultiplier(val);
  304. }}
  305. inputProps={{
  306. min: 1,
  307. step: 1
  308. }}
  309. sx={{ flex: 1 }}
  310. />
  311. <Typography variant="body1" sx={{ color: "text.secondary" }}>
  312. =
  313. </Typography>
  314. <TextField
  315. {...field}
  316. label={t("Req. Qty")}
  317. fullWidth
  318. error={Boolean(error)}
  319. variant="outlined"
  320. type="number"
  321. value={calculatedValue || ""}
  322. disabled
  323. InputProps={{
  324. endAdornment: uom ? (
  325. <InputAdornment position="end">
  326. <Typography variant="body2" sx={{ color: "text.secondary" }}>
  327. {uom}
  328. </Typography>
  329. </InputAdornment>
  330. ) : null
  331. }}
  332. sx={{ flex: 1 }}
  333. />
  334. </Box>
  335. );
  336. }}
  337. />
  338. </Grid>
  339. <Grid item xs={12} sm={12} md={6}>
  340. <Controller
  341. control={control}
  342. name="jobTypeId"
  343. rules={{ required: t("Job Type required!") as string }}
  344. render={({ field, fieldState: { error } }) => {
  345. //console.log("Job Type Select render - filteredJobTypes:", filteredJobTypes);
  346. //console.log("Current field.value:", field.value);
  347. return (
  348. <FormControl fullWidth error={Boolean(error)}>
  349. <InputLabel>{t("Job Type")}</InputLabel>
  350. <Select
  351. {...field}
  352. label={t("Job Type")}
  353. value={field.value?.toString() ?? ""}
  354. onChange={(event) => {
  355. const value = event.target.value;
  356. console.log("Job Type changed to:", value);
  357. field.onChange(value === "" ? undefined : Number(value));
  358. }}
  359. >
  360. <MenuItem value="">
  361. <em>{t("Please select")}</em>
  362. </MenuItem>
  363. {/* {filteredJobTypes.map((jobType) => (*/}
  364. {jobTypes.map((jobType) => (
  365. <MenuItem key={jobType.id} value={jobType.id.toString()}>
  366. {t(jobType.name)}
  367. </MenuItem>
  368. ))}
  369. </Select>
  370. </FormControl>
  371. );
  372. }}
  373. />
  374. </Grid>
  375. <Grid item xs={12} sm={12} md={6}>
  376. <Controller
  377. control={control}
  378. name="productionPriority"
  379. rules={{
  380. required: t("Production Priority required!") as string,
  381. max: {
  382. value: 100,
  383. message: t("Production Priority cannot exceed 100") as string
  384. },
  385. min: {
  386. value: 1,
  387. message: t("Production Priority must be at least 1") as string
  388. },
  389. validate: (value) => {
  390. if (value === undefined || value === null || isNaN(value)) {
  391. return t("Production Priority required!") as string;
  392. }
  393. return true;
  394. }
  395. }}
  396. render={({ field, fieldState: { error } }) => (
  397. <TextField
  398. {...field}
  399. label={t("Production Priority")}
  400. fullWidth
  401. error={Boolean(error)}
  402. variant="outlined"
  403. type="number"
  404. inputProps={{
  405. min: 1,
  406. max: 100,
  407. step: 1
  408. }}
  409. value={field.value ?? ""}
  410. onChange={(e) => {
  411. const inputValue = e.target.value;
  412. // 允许空字符串(用户正在删除)
  413. if (inputValue === "") {
  414. field.onChange("");
  415. return;
  416. }
  417. // 转换为数字并验证范围
  418. const numValue = Number(inputValue);
  419. if (!isNaN(numValue) && numValue >= 1 && numValue <= 100) {
  420. field.onChange(numValue);
  421. }
  422. }}
  423. />
  424. )}
  425. />
  426. </Grid>
  427. <Grid item xs={12} sm={12} md={6}>
  428. <Controller
  429. control={control}
  430. name="planStart"
  431. rules={{
  432. required: "Plan start required!",
  433. validate: {
  434. isValid: (value) => dateStringToDayjs(value).isValid(),
  435. // isBeforePlanEnd: (value) => {
  436. // const planStartDayjs = dateStringToDayjs(value)
  437. // const planEndDayjs = dateStringToDayjs(planEnd)
  438. // return planStartDayjs.isBefore(planEndDayjs) || planStartDayjs.isSame(planEndDayjs)
  439. // }
  440. }
  441. }}
  442. render={({ field, fieldState: { error } }) => (
  443. // <DateTimePicker
  444. <DatePicker
  445. label={t("Plan Start")}
  446. // views={['year','month','day','hours', 'minutes', 'seconds']}
  447. //format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`}
  448. format={OUTPUT_DATE_FORMAT}
  449. value={field.value ? dateStringToDayjs(field.value) : null}
  450. onChange={(newValue: Dayjs | null) => {
  451. handleDateTimePickerChange(newValue, field.onChange)
  452. }}
  453. slotProps={{ textField: { fullWidth: true, error: Boolean(error) } }}
  454. />
  455. )}
  456. />
  457. </Grid>
  458. {/* <Grid item xs={12} sm={12} md={6}>
  459. <Controller
  460. control={control}
  461. name="planEnd"
  462. rules={{
  463. required: "Plan end required!",
  464. validate: {
  465. isValid: (value) => dateStringToDayjs(value).isValid(),
  466. isBeforePlanEnd: (value) => {
  467. const planStartDayjs = dateStringToDayjs(planStart)
  468. const planEndDayjs = dateStringToDayjs(value)
  469. return planEndDayjs.isAfter(planStartDayjs) || planEndDayjs.isSame(planStartDayjs)
  470. }
  471. }
  472. }}
  473. render={({ field, fieldState: { error } }) => (
  474. <DateTimePicker
  475. label={t("Plan End")}
  476. format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`}
  477. onChange={(newValue: Dayjs | null) => {
  478. handleDateTimePickerChange(newValue, field.onChange)
  479. }}
  480. slotProps={{ textField: { fullWidth: true } }}
  481. />
  482. )}
  483. />
  484. </Grid> */}
  485. </Grid>
  486. <Stack
  487. direction="row"
  488. justifyContent="flex-end"
  489. spacing={2}
  490. sx={{ mt: 2 }}
  491. >
  492. <Button
  493. name="submit"
  494. variant="contained"
  495. startIcon={isSubmitting ? <CircularProgress size={16} color="inherit" /> : <Check />}
  496. type="submit"
  497. disabled={isSubmitting}
  498. >
  499. {isSubmitting ? t("Creating...") : t("Create")}
  500. </Button>
  501. </Stack>
  502. </LocalizationProvider>
  503. </Stack>
  504. </FormProvider>
  505. </Box>
  506. </Card>
  507. </Modal>
  508. )
  509. }
  510. export default JoCreateFormModal;