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

GoodPickExecutionForm.tsx 13 KiB

3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. // FPSMS-frontend/src/components/PickOrderSearch/PickExecutionForm.tsx
  2. "use client";
  3. import {
  4. Box,
  5. Button,
  6. Dialog,
  7. DialogActions,
  8. DialogContent,
  9. DialogTitle,
  10. FormControl,
  11. Grid,
  12. InputLabel,
  13. MenuItem,
  14. Select,
  15. TextField,
  16. Typography,
  17. } from "@mui/material";
  18. import { useCallback, useEffect, useState } from "react";
  19. import { useTranslation } from "react-i18next";
  20. import { GetPickOrderLineInfo, PickExecutionIssueData } from "@/app/api/pickOrder/actions";
  21. import { fetchEscalationCombo } from "@/app/api/user/actions";
  22. import { useRef } from "react";
  23. import dayjs from 'dayjs';
  24. import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  25. interface LotPickData {
  26. id: number;
  27. lotId: number;
  28. lotNo: string;
  29. expiryDate: string;
  30. location: string;
  31. stockUnit: string;
  32. inQty: number;
  33. outQty: number;
  34. holdQty: number;
  35. totalPickedByAllPickOrders: number;
  36. availableQty: number;
  37. requiredQty: number;
  38. actualPickQty: number;
  39. lotStatus: string;
  40. lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected';
  41. stockOutLineId?: number;
  42. stockOutLineStatus?: string;
  43. stockOutLineQty?: number;
  44. }
  45. interface PickExecutionFormProps {
  46. open: boolean;
  47. onClose: () => void;
  48. onSubmit: (data: PickExecutionIssueData) => Promise<void>;
  49. selectedLot: LotPickData | null;
  50. selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null;
  51. pickOrderId?: number;
  52. pickOrderCreateDate: any;
  53. // ✅ Remove these props since we're not handling normal cases
  54. // onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise<void>;
  55. // selectedRowId?: number | null;
  56. }
  57. // 定义错误类型
  58. interface FormErrors {
  59. actualPickQty?: string;
  60. missQty?: string;
  61. badItemQty?: string;
  62. issueRemark?: string;
  63. handledBy?: string;
  64. }
  65. const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
  66. open,
  67. onClose,
  68. onSubmit,
  69. selectedLot,
  70. selectedPickOrderLine,
  71. pickOrderId,
  72. pickOrderCreateDate,
  73. // ✅ Remove these props
  74. // onNormalPickSubmit,
  75. // selectedRowId,
  76. }) => {
  77. const { t } = useTranslation();
  78. const [formData, setFormData] = useState<Partial<PickExecutionIssueData>>({});
  79. const [errors, setErrors] = useState<FormErrors>({});
  80. const [loading, setLoading] = useState(false);
  81. const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]);
  82. // 计算剩余可用数量
  83. const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
  84. const remainingQty = lot.inQty - lot.outQty;
  85. return Math.max(0, remainingQty);
  86. }, []);
  87. const calculateRequiredQty = useCallback((lot: LotPickData) => {
  88. // ✅ Use the original required quantity, not subtracting actualPickQty
  89. // The actualPickQty in the form should be independent of the database value
  90. return lot.requiredQty || 0;
  91. }, []);
  92. const remaining = selectedLot ? calculateRemainingAvailableQty(selectedLot) : 0;
  93. const req = selectedLot ? calculateRequiredQty(selectedLot) : 0;
  94. const ap = Number(formData.actualPickQty) || 0;
  95. const miss = Number(formData.missQty) || 0;
  96. const bad = Number(formData.badItemQty) || 0;
  97. // Max the user can type
  98. const maxPick = Math.min(remaining, req);
  99. const maxIssueTotal = Math.max(0, req - ap); // remaining room for miss+bad
  100. const clamp0 = (v: any) => Math.max(0, Number(v) || 0);
  101. // 获取处理人员列表
  102. useEffect(() => {
  103. const fetchHandlers = async () => {
  104. try {
  105. const escalationCombo = await fetchEscalationCombo();
  106. setHandlers(escalationCombo);
  107. } catch (error) {
  108. console.error("Error fetching handlers:", error);
  109. }
  110. };
  111. fetchHandlers();
  112. }, []);
  113. const initKeyRef = useRef<string | null>(null);
  114. useEffect(() => {
  115. if (!open || !selectedLot || !selectedPickOrderLine || !pickOrderId) return;
  116. // Only initialize once per (pickOrderLineId + lotId) while dialog open
  117. const key = `${selectedPickOrderLine.id}-${selectedLot.lotId}`;
  118. if (initKeyRef.current === key) return;
  119. const getSafeDate = (dateValue: any): string => {
  120. if (!dateValue) return dayjs().format(INPUT_DATE_FORMAT);
  121. try {
  122. const date = dayjs(dateValue);
  123. if (!date.isValid()) {
  124. return dayjs().format(INPUT_DATE_FORMAT);
  125. }
  126. return date.format(INPUT_DATE_FORMAT);
  127. } catch {
  128. return dayjs().format(INPUT_DATE_FORMAT);
  129. }
  130. };
  131. setFormData({
  132. pickOrderId: pickOrderId,
  133. pickOrderCode: selectedPickOrderLine.pickOrderCode,
  134. pickOrderCreateDate: getSafeDate(pickOrderCreateDate),
  135. pickExecutionDate: dayjs().format(INPUT_DATE_FORMAT),
  136. pickOrderLineId: selectedPickOrderLine.id,
  137. itemId: selectedPickOrderLine.itemId,
  138. itemCode: selectedPickOrderLine.itemCode,
  139. itemDescription: selectedPickOrderLine.itemName,
  140. lotId: selectedLot.lotId,
  141. lotNo: selectedLot.lotNo,
  142. storeLocation: selectedLot.location,
  143. requiredQty: selectedLot.requiredQty,
  144. actualPickQty: selectedLot.actualPickQty || 0,
  145. missQty: 0,
  146. badItemQty: 0,
  147. issueRemark: '',
  148. pickerName: '',
  149. handledBy: undefined,
  150. });
  151. initKeyRef.current = key;
  152. }, [open, selectedPickOrderLine?.id, selectedLot?.lotId, pickOrderId, pickOrderCreateDate]);
  153. // Mutually exclusive inputs: picking vs reporting issues
  154. const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => {
  155. setFormData(prev => ({ ...prev, [field]: value }));
  156. // 清除错误
  157. if (errors[field as keyof FormErrors]) {
  158. setErrors(prev => ({ ...prev, [field]: undefined }));
  159. }
  160. }, [errors]);
  161. // ✅ Update form validation to require either missQty > 0 OR badItemQty > 0
  162. const validateForm = (): boolean => {
  163. const newErrors: FormErrors = {};
  164. const req = selectedLot?.requiredQty || 0;
  165. const ap = formData.actualPickQty || 0;
  166. const miss = formData.missQty || 0;
  167. const bad = formData.badItemQty || 0;
  168. if (ap < 0) newErrors.actualPickQty = t('Qty is required');
  169. if (ap > Math.min(remainingAvailableQty, req)) newErrors.actualPickQty = t('Qty is not allowed to be greater than required/available qty');
  170. if (miss < 0) newErrors.missQty = t('Invalid qty');
  171. if (bad < 0) newErrors.badItemQty = t('Invalid qty');
  172. if (ap + miss + bad > req) {
  173. newErrors.actualPickQty = t('Total exceeds required qty');
  174. newErrors.missQty = t('Total exceeds required qty');
  175. }
  176. if (ap === 0 && miss === 0 && bad === 0) {
  177. newErrors.actualPickQty = t('Enter pick qty or issue qty');
  178. newErrors.missQty = t('Enter pick qty or issue qty');
  179. }
  180. setErrors(newErrors);
  181. return Object.keys(newErrors).length === 0;
  182. };
  183. const handleSubmit = async () => {
  184. if (!validateForm() || !formData.pickOrderId) {
  185. return;
  186. }
  187. setLoading(true);
  188. try {
  189. await onSubmit(formData as PickExecutionIssueData);
  190. onClose();
  191. } catch (error) {
  192. console.error('Error submitting pick execution issue:', error);
  193. } finally {
  194. setLoading(false);
  195. }
  196. };
  197. const handleClose = () => {
  198. setFormData({});
  199. setErrors({});
  200. onClose();
  201. };
  202. if (!selectedLot || !selectedPickOrderLine) {
  203. return null;
  204. }
  205. const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot);
  206. const requiredQty = calculateRequiredQty(selectedLot);
  207. return (
  208. <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
  209. <DialogTitle>
  210. {t('Pick Execution Issue Form')} {/* ✅ Always show issue form title */}
  211. </DialogTitle>
  212. <DialogContent>
  213. <Box sx={{ mt: 2 }}>
  214. {/* ✅ Add instruction text */}
  215. <Grid container spacing={2}>
  216. <Grid item xs={12}>
  217. <Box sx={{ p: 2, backgroundColor: '#fff3cd', borderRadius: 1, mb: 2 }}>
  218. <Typography variant="body2" color="warning.main">
  219. <strong>{t('Note:')}</strong> {t('This form is for reporting issues only. You must report either missing items or bad items.')}
  220. </Typography>
  221. </Box>
  222. </Grid>
  223. {/* ✅ Keep the existing form fields */}
  224. <Grid item xs={6}>
  225. <TextField
  226. fullWidth
  227. label={t('Required Qty')}
  228. value={selectedLot?.requiredQty || 0}
  229. disabled
  230. variant="outlined"
  231. // helperText={t('Still need to pick')}
  232. />
  233. </Grid>
  234. <Grid item xs={6}>
  235. <TextField
  236. fullWidth
  237. label={t('Remaining Available Qty')}
  238. value={remainingAvailableQty}
  239. disabled
  240. variant="outlined"
  241. // helperText={t('Available in warehouse')}
  242. />
  243. </Grid>
  244. <Grid item xs={12}>
  245. <TextField
  246. fullWidth
  247. label={t('Actual Pick Qty')}
  248. type="number"
  249. value={formData.actualPickQty ?? ''}
  250. onChange={(e) => handleInputChange('actualPickQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))}
  251. error={!!errors.actualPickQty}
  252. helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(remainingAvailableQty, selectedLot?.requiredQty || 0)}`}
  253. variant="outlined"
  254. />
  255. </Grid>
  256. <Grid item xs={12}>
  257. <TextField
  258. fullWidth
  259. label={t('Missing item Qty')}
  260. type="number"
  261. value={formData.missQty || 0}
  262. onChange={(e) => handleInputChange('missQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))}
  263. error={!!errors.missQty}
  264. variant="outlined"
  265. //disabled={(formData.actualPickQty || 0) > 0}
  266. />
  267. </Grid>
  268. <Grid item xs={12}>
  269. <TextField
  270. fullWidth
  271. label={t('Bad Item Qty')}
  272. type="number"
  273. value={formData.badItemQty || 0}
  274. onChange={(e) => handleInputChange('badItemQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))}
  275. error={!!errors.badItemQty}
  276. variant="outlined"
  277. //disabled={(formData.actualPickQty || 0) > 0}
  278. />
  279. </Grid>
  280. {/* ✅ Show issue description and handler fields when bad items > 0 */}
  281. {(formData.badItemQty && formData.badItemQty > 0) ? (
  282. <>
  283. <Grid item xs={12}>
  284. <TextField
  285. fullWidth
  286. id="issueRemark"
  287. label={t('Issue Remark')}
  288. multiline
  289. rows={4}
  290. value={formData.issueRemark || ''}
  291. onChange={(e) => handleInputChange('issueRemark', e.target.value)}
  292. error={!!errors.issueRemark}
  293. helperText={errors.issueRemark}
  294. //placeholder={t('Describe the issue with bad items')}
  295. variant="outlined"
  296. />
  297. </Grid>
  298. <Grid item xs={12}>
  299. <FormControl fullWidth error={!!errors.handledBy}>
  300. <InputLabel>{t('handler')}</InputLabel>
  301. <Select
  302. value={formData.handledBy ? formData.handledBy.toString() : ''}
  303. onChange={(e) => handleInputChange('handledBy', e.target.value ? parseInt(e.target.value) : undefined)}
  304. label={t('handler')}
  305. >
  306. {handlers.map((handler) => (
  307. <MenuItem key={handler.id} value={handler.id.toString()}>
  308. {handler.name}
  309. </MenuItem>
  310. ))}
  311. </Select>
  312. {errors.handledBy && (
  313. <Typography variant="caption" color="error" sx={{ mt: 0.5, ml: 1.75 }}>
  314. {errors.handledBy}
  315. </Typography>
  316. )}
  317. </FormControl>
  318. </Grid>
  319. </>
  320. ) : (<></>)}
  321. </Grid>
  322. </Box>
  323. </DialogContent>
  324. <DialogActions>
  325. <Button onClick={handleClose} disabled={loading}>
  326. {t('Cancel')}
  327. </Button>
  328. <Button
  329. onClick={handleSubmit}
  330. variant="contained"
  331. disabled={loading}
  332. >
  333. {loading ? t('submitting') : t('submit')}
  334. </Button>
  335. </DialogActions>
  336. </Dialog>
  337. );
  338. };
  339. export default PickExecutionForm;