Browse Source

Add material mismatch check

Matteo Parenti 3 months ago
parent
commit
51d3a92263

+ 56 - 2
frontend/src/components/AssignSpoolModal.tsx

@@ -5,6 +5,7 @@ import { X, Loader2, Package, Check, Search } from 'lucide-react';
 import { api } from '../api/client';
 import type { InventorySpool, SpoolAssignment } from '../api/client';
 import { Button } from './Button';
+import { ConfirmModal } from './ConfirmModal';
 import { useToast } from '../contexts/ToastContext';
 
 interface AssignSpoolModalProps {
@@ -26,6 +27,8 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr
   const { showToast } = useToast();
   const [selectedSpoolId, setSelectedSpoolId] = useState<number | null>(null);
   const [searchFilter, setSearchFilter] = useState('');
+  const [pendingAssignId, setPendingAssignId] = useState<number | null>(null);
+  const [showMismatchConfirm, setShowMismatchConfirm] = useState(false);
 
   const { data: spools, isLoading } = useQuery({
     queryKey: ['inventory-spools'],
@@ -53,6 +56,8 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr
       });
       queryClient.invalidateQueries({ queryKey: ['spool-assignments'] });
       showToast(t('inventory.assignSuccess'), 'success');
+      setShowMismatchConfirm(false);
+      setPendingAssignId(null);
       onClose();
     },
     onError: (error: Error) => {
@@ -84,10 +89,37 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr
     );
   });
 
+  const normalizeMaterial = (value: string | undefined | null) =>
+    (value ?? '').trim().toUpperCase();
+
+  const materialsMatch = (spoolMaterial: string, trayMaterial: string) => {
+    const normalizedSpool = normalizeMaterial(spoolMaterial);
+    const normalizedTray = normalizeMaterial(trayMaterial);
+    if (!normalizedSpool || !normalizedTray) return true;
+    if (normalizedTray.includes(normalizedSpool)) return true;
+    if (normalizedSpool.includes(normalizedTray)) return true;
+    return false;
+  };
+
   const handleAssign = () => {
-    if (selectedSpoolId) {
-      assignMutation.mutate(selectedSpoolId);
+    if (!selectedSpoolId) return;
+    const selectedSpool = spools?.find((spool: InventorySpool) => spool.id === selectedSpoolId);
+    if (selectedSpool && trayInfo?.type) {
+      const mismatch = !materialsMatch(selectedSpool.material, trayInfo.type);
+      if (mismatch) {
+        setPendingAssignId(selectedSpoolId);
+        setShowMismatchConfirm(true);
+        return;
+      }
     }
+    assignMutation.mutate(selectedSpoolId);
+  };
+
+  const handleConfirmMismatch = () => {
+    if (!pendingAssignId) return;
+    assignMutation.mutate(pendingAssignId);
+    setShowMismatchConfirm(false);
+    setPendingAssignId(null);
   };
 
   return (
@@ -226,6 +258,28 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr
           </div>
         )}
       </div>
+
+      {showMismatchConfirm && trayInfo && selectedSpoolId && (
+        <ConfirmModal
+          title={t('inventory.assignMismatchTitle')}
+          message={t('inventory.assignMismatchMessage', {
+            spoolMaterial: spools?.find((spool: InventorySpool) => spool.id === selectedSpoolId)?.material ?? '',
+            trayMaterial: trayInfo.type,
+            location: trayInfo.location,
+          })}
+          confirmText={t('inventory.assignMismatchConfirm')}
+          confirmClassName="!text-black"
+          variant="warning"
+          isLoading={assignMutation.isPending}
+          onConfirm={handleConfirmMismatch}
+          onCancel={() => {
+            if (!assignMutation.isPending) {
+              setShowMismatchConfirm(false);
+              setPendingAssignId(null);
+            }
+          }}
+        />
+      )}
     </div>
   );
 }

+ 3 - 1
frontend/src/components/ConfirmModal.tsx

@@ -11,6 +11,7 @@ interface ConfirmModalProps {
   cancelText?: string;
   cancelVariant?: 'primary' | 'secondary' | 'danger' | 'ghost';
   cardClassName?: string;
+  confirmClassName?: string;
   variant?: 'danger' | 'warning' | 'default';
   isLoading?: boolean;
   loadingText?: string;
@@ -25,6 +26,7 @@ export function ConfirmModal({
   cancelText,
   cancelVariant,
   cardClassName,
+  confirmClassName,
   variant = 'default',
   isLoading = false,
   loadingText,
@@ -91,7 +93,7 @@ export function ConfirmModal({
             </Button>
             <Button
               onClick={onConfirm}
-              className={`flex-1 ${styles.button}`}
+              className={`flex-1 ${styles.button}${confirmClassName ? ` ${confirmClassName}` : ''}`}
               disabled={isLoading}
             >
               {isLoading ? (

+ 3 - 0
frontend/src/i18n/locales/de.ts

@@ -2483,6 +2483,9 @@ export default {
     unassignSpool: 'Zuweisung aufheben',
     assignSuccess: 'Spule zugewiesen und AMS-Slot konfiguriert',
     assignFailed: 'Spulenzuweisung fehlgeschlagen',
+    assignMismatchTitle: 'Material stimmt nicht überein',
+    assignMismatchMessage: 'Das Material der ausgewählten Spule ({{spoolMaterial}}) stimmt nicht mit dem AMS-Profil ({{trayMaterial}}) für {{location}} überein. Trotzdem zuweisen?',
+    assignMismatchConfirm: 'Trotzdem zuweisen',
     selectSpool: 'Wählen Sie eine Spule für diesen Slot',
     assigned: 'Zugewiesen',
     assigning: 'Wird zugewiesen...',

+ 3 - 0
frontend/src/i18n/locales/en.ts

@@ -2483,6 +2483,9 @@ export default {
     unassignSpool: 'Unassign',
     assignSuccess: 'Spool assigned and AMS slot configured',
     assignFailed: 'Failed to assign spool',
+    assignMismatchTitle: 'Material mismatch',
+    assignMismatchMessage: 'The selected spool material ({{spoolMaterial}}) does not match the AMS profile ({{trayMaterial}}) for {{location}}. Assign anyway?',
+    assignMismatchConfirm: 'Assign Anyway',
     selectSpool: 'Select a spool to assign to this slot',
     assigned: 'Assigned',
     assigning: 'Assigning...',

+ 3 - 0
frontend/src/i18n/locales/fr.ts

@@ -2479,6 +2479,9 @@ export default {
     unassignSpool: 'Désassigner',
     assignSuccess: 'Bobine assignée et slot AMS configuré',
     assignFailed: 'Échec assignation',
+    assignMismatchTitle: 'Matériau non correspondant',
+    assignMismatchMessage: 'Le matériau de la bobine sélectionnée ({{spoolMaterial}}) ne correspond pas au profil AMS ({{trayMaterial}}) pour {{location}}. Assigner quand même ?',
+    assignMismatchConfirm: 'Assigner quand même',
     selectSpool: 'Choisir une bobine pour ce slot',
     assigned: 'Assigné',
     assigning: 'Assignation...',

+ 3 - 0
frontend/src/i18n/locales/it.ts

@@ -2303,6 +2303,9 @@ export default {
     unassignSpool: 'Deassegna',
     assignSuccess: 'Bobina assegnata e slot AMS configurato',
     assignFailed: 'Assegnazione bobina fallita',
+    assignMismatchTitle: 'Materiale non corrispondente',
+    assignMismatchMessage: 'Il materiale della bobina selezionata ({{spoolMaterial}}) non corrisponde al profilo AMS ({{trayMaterial}}) per {{location}}. Assegnare comunque?',
+    assignMismatchConfirm: 'Assegna comunque',
     selectSpool: 'Seleziona una bobina da assegnare a questo slot',
     assigned: 'Assegnato',
     assigning: 'Assegnazione...',

+ 3 - 0
frontend/src/i18n/locales/ja.ts

@@ -2414,6 +2414,9 @@ export default {
     unassignSpool: '割り当て解除',
     assignSuccess: 'スプールを割り当て、AMSスロットを設定しました',
     assignFailed: 'スプールの割り当てに失敗しました',
+    assignMismatchTitle: '素材が一致しません',
+    assignMismatchMessage: '選択したスプールの素材({{spoolMaterial}})が、{{location}} のAMSプロファイル({{trayMaterial}})と一致しません。割り当てを続行しますか?',
+    assignMismatchConfirm: '続行する',
     selectSpool: 'このスロットに割り当てるスプールを選択',
     assigned: '割り当て済み',
     assigning: '割り当て中...',