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

GoodPickExecutionForm.tsx 13 KiB

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