FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

JoCreateFormModal.tsx 30 KiB

1週間前
3ヶ月前
3ヶ月前
1週間前
3ヶ月前
3ヶ月前
1週間前
3ヶ月前
1週間前
3ヶ月前
1週間前
1週間前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
5ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
5ヶ月前
4ヶ月前
4ヶ月前
5ヶ月前
1週間前
1週間前
1週間前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  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;