Просмотр исходного кода

- Added basic printer controls
- Reprint/Stop
- Pause/Resume
- AMS slot re-read

maziggy 8 месяцев назад
Родитель
Сommit
f7994df84f
2 измененных файлов с 409 добавлено и 54 удалено
  1. 21 0
      frontend/src/api/client.ts
  2. 388 54
      frontend/src/pages/PrintersPage.tsx

+ 21 - 0
frontend/src/api/client.ts

@@ -1275,6 +1275,27 @@ export const api = {
       method: 'POST',
     }),
 
+  // Print Control
+  stopPrint: (printerId: number) =>
+    request<{ success: boolean; message: string }>(`/printers/${printerId}/print/stop`, {
+      method: 'POST',
+    }),
+  pausePrint: (printerId: number) =>
+    request<{ success: boolean; message: string }>(`/printers/${printerId}/print/pause`, {
+      method: 'POST',
+    }),
+  resumePrint: (printerId: number) =>
+    request<{ success: boolean; message: string }>(`/printers/${printerId}/print/resume`, {
+      method: 'POST',
+    }),
+
+  // AMS Control
+  refreshAmsSlot: (printerId: number, amsId: number, slotId: number) =>
+    request<{ success: boolean; message: string }>(
+      `/printers/${printerId}/ams/${amsId}/slot/${slotId}/refresh`,
+      { method: 'POST' }
+    ),
+
   // MQTT Debug Logging
   enableMQTTLogging: (printerId: number) =>
     request<{ logging_enabled: boolean }>(`/printers/${printerId}/logging/enable`, {

+ 388 - 54
frontend/src/pages/PrintersPage.tsx

@@ -28,6 +28,9 @@ import {
   Video,
   Search,
   Loader2,
+  Square,
+  Pause,
+  Play,
 } from 'lucide-react';
 import { useNavigate } from 'react-router-dom';
 import { api, discoveryApi } from '../api/client';
@@ -41,6 +44,7 @@ import { HMSErrorModal, filterKnownHMSErrors } from '../components/HMSErrorModal
 import { PrinterQueueWidget } from '../components/PrinterQueueWidget';
 import { AMSHistoryModal } from '../components/AMSHistoryModal';
 import { FilamentHoverCard, EmptySlotHoverCard } from '../components/FilamentHoverCard';
+import { useToast } from '../contexts/ToastContext';
 
 // Bambu Lab color code mapping (color suffix from tray_id_name -> color name)
 // tray_id_name format: "A00-Y2" where Y2 is the color code
@@ -663,6 +667,7 @@ function PrinterCard({
 }) {
   const queryClient = useQueryClient();
   const navigate = useNavigate();
+  const { showToast } = useToast();
   const [showMenu, setShowMenu] = useState(false);
   const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
   const [deleteArchives, setDeleteArchives] = useState(true);
@@ -672,6 +677,9 @@ function PrinterCard({
   const [showPowerOnConfirm, setShowPowerOnConfirm] = useState(false);
   const [showPowerOffConfirm, setShowPowerOffConfirm] = useState(false);
   const [showHMSModal, setShowHMSModal] = useState(false);
+  const [showStopConfirm, setShowStopConfirm] = useState(false);
+  const [showPauseConfirm, setShowPauseConfirm] = useState(false);
+  const [showResumeConfirm, setShowResumeConfirm] = useState(false);
   const [amsHistoryModal, setAmsHistoryModal] = useState<{
     amsId: number;
     amsLabel: string;
@@ -828,6 +836,127 @@ function PrinterCard({
     },
   });
 
+  // Print control mutations
+  const stopPrintMutation = useMutation({
+    mutationFn: () => api.stopPrint(printer.id),
+    onSuccess: () => {
+      showToast('Print stopped');
+      queryClient.invalidateQueries({ queryKey: ['printerStatus', printer.id] });
+    },
+    onError: (error: Error) => showToast(error.message || 'Failed to stop print', 'error'),
+  });
+
+  const pausePrintMutation = useMutation({
+    mutationFn: () => api.pausePrint(printer.id),
+    onSuccess: () => {
+      showToast('Print paused');
+      queryClient.invalidateQueries({ queryKey: ['printerStatus', printer.id] });
+    },
+    onError: (error: Error) => showToast(error.message || 'Failed to pause print', 'error'),
+  });
+
+  const resumePrintMutation = useMutation({
+    mutationFn: () => api.resumePrint(printer.id),
+    onSuccess: () => {
+      showToast('Print resumed');
+      queryClient.invalidateQueries({ queryKey: ['printerStatus', printer.id] });
+    },
+    onError: (error: Error) => showToast(error.message || 'Failed to resume print', 'error'),
+  });
+
+  // State for tracking which AMS slot is being refreshed
+  const [refreshingSlot, setRefreshingSlot] = useState<{ amsId: number; slotId: number } | null>(null);
+  // Track if we've seen the printer enter "busy" state (ams_status_main !== 0)
+  const seenBusyStateRef = useRef<boolean>(false);
+  // Fallback timeout ref
+  const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+  // Minimum display time passed
+  const minTimePassedRef = useRef<boolean>(false);
+
+  // AMS slot refresh mutation
+  const refreshAmsSlotMutation = useMutation({
+    mutationFn: ({ amsId, slotId }: { amsId: number; slotId: number }) =>
+      api.refreshAmsSlot(printer.id, amsId, slotId),
+    onMutate: ({ amsId, slotId }) => {
+      // Clear any existing timeout
+      if (refreshTimeoutRef.current) {
+        clearTimeout(refreshTimeoutRef.current);
+      }
+      // Reset state
+      seenBusyStateRef.current = false;
+      minTimePassedRef.current = false;
+      setRefreshingSlot({ amsId, slotId });
+      // Minimum display time (2 seconds)
+      setTimeout(() => {
+        minTimePassedRef.current = true;
+      }, 2000);
+      // Fallback timeout (30 seconds max)
+      refreshTimeoutRef.current = setTimeout(() => {
+        setRefreshingSlot(null);
+      }, 30000);
+    },
+    onSuccess: (data) => {
+      showToast(data.message || 'RFID re-read initiated');
+    },
+    onError: (error: Error) => {
+      showToast(error.message || 'Failed to re-read RFID', 'error');
+      if (refreshTimeoutRef.current) {
+        clearTimeout(refreshTimeoutRef.current);
+      }
+      setRefreshingSlot(null);
+    },
+  });
+
+  // Watch ams_status_main to detect when RFID read completes
+  // ams_status_main: 0=idle, 2=rfid_identifying
+  const deferredClearRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+
+  useEffect(() => {
+    if (!refreshingSlot) return;
+
+    const amsStatus = status?.ams_status_main ?? 0;
+
+    // Track when we see non-idle state (printer is working)
+    if (amsStatus !== 0) {
+      seenBusyStateRef.current = true;
+      // Cancel any deferred clear since we're back to busy
+      if (deferredClearRef.current) {
+        clearTimeout(deferredClearRef.current);
+        deferredClearRef.current = null;
+      }
+    }
+
+    // When we've seen busy and now idle, clear (with min time check)
+    if (seenBusyStateRef.current && amsStatus === 0) {
+      if (minTimePassedRef.current) {
+        // Min time passed - clear now
+        if (refreshTimeoutRef.current) {
+          clearTimeout(refreshTimeoutRef.current);
+        }
+        setRefreshingSlot(null);
+      } else {
+        // Schedule clear after min time (2 seconds from start)
+        if (!deferredClearRef.current) {
+          deferredClearRef.current = setTimeout(() => {
+            if (refreshTimeoutRef.current) {
+              clearTimeout(refreshTimeoutRef.current);
+            }
+            setRefreshingSlot(null);
+          }, 2000);
+        }
+      }
+    }
+
+    return () => {
+      if (deferredClearRef.current) {
+        clearTimeout(deferredClearRef.current);
+      }
+    };
+  }, [status?.ams_status_main, refreshingSlot]);
+
+  // State for AMS slot menu
+  const [amsSlotMenu, setAmsSlotMenu] = useState<{ amsId: number; slotId: number } | null>(null);
+
   if (shouldHide) {
     return null;
   }
@@ -1195,50 +1324,102 @@ function PrinterCard({
               </>
             )}
 
-            {/* Temperatures */}
+            {/* Temperatures + Print Controls */}
             {status.temperatures && viewMode === 'expanded' && (() => {
               // Use actual heater states from MQTT stream
               const nozzleHeating = status.temperatures.nozzle_heating || status.temperatures.nozzle_2_heating || false;
               const bedHeating = status.temperatures.bed_heating || false;
               const chamberHeating = status.temperatures.chamber_heating || false;
 
+              // Determine print state for control buttons
+              const isRunning = status.state === 'RUNNING';
+              const isPaused = status.state === 'PAUSED' || status.state === 'PAUSE';
+              const isPrinting = isRunning || isPaused;
+              const isControlBusy = stopPrintMutation.isPending || pausePrintMutation.isPending || resumePrintMutation.isPending;
+
               return (
-                <div className="grid grid-cols-3 gap-3">
-                  {/* Nozzle temp - combined for dual nozzle */}
-                  <div className="text-center p-2 bg-bambu-dark rounded-lg">
-                    <HeaterThermometer className="w-4 h-4 mx-auto mb-1" color="text-orange-400" isHeating={nozzleHeating} />
-                    {status.temperatures.nozzle_2 !== undefined ? (
-                      <>
-                        <p className="text-xs text-bambu-gray">Left / Right</p>
-                        <p className="text-sm text-white">
-                          {Math.round(status.temperatures.nozzle || 0)}°C / {Math.round(status.temperatures.nozzle_2 || 0)}°C
+                <div className="flex gap-3">
+                  {/* Temperature cards */}
+                  <div className="flex-1 grid grid-cols-3 gap-2">
+                    {/* Nozzle temp - combined for dual nozzle */}
+                    <div className="text-center p-2 bg-bambu-dark rounded-lg">
+                      <HeaterThermometer className="w-4 h-4 mx-auto mb-1" color="text-orange-400" isHeating={nozzleHeating} />
+                      {status.temperatures.nozzle_2 !== undefined ? (
+                        <>
+                          <p className="text-[10px] text-bambu-gray">L / R</p>
+                          <p className="text-xs text-white">
+                            {Math.round(status.temperatures.nozzle || 0)}° / {Math.round(status.temperatures.nozzle_2 || 0)}°
+                          </p>
+                        </>
+                      ) : (
+                        <>
+                          <p className="text-[10px] text-bambu-gray">Nozzle</p>
+                          <p className="text-xs text-white">
+                            {Math.round(status.temperatures.nozzle || 0)}°C
+                          </p>
+                        </>
+                      )}
+                    </div>
+                    <div className="text-center p-2 bg-bambu-dark rounded-lg">
+                      <HeaterThermometer className="w-4 h-4 mx-auto mb-1" color="text-blue-400" isHeating={bedHeating} />
+                      <p className="text-[10px] text-bambu-gray">Bed</p>
+                      <p className="text-xs text-white">
+                        {Math.round(status.temperatures.bed || 0)}°C
+                      </p>
+                    </div>
+                    {status.temperatures.chamber !== undefined ? (
+                      <div className="text-center p-2 bg-bambu-dark rounded-lg">
+                        <HeaterThermometer className="w-4 h-4 mx-auto mb-1" color="text-green-400" isHeating={chamberHeating} />
+                        <p className="text-[10px] text-bambu-gray">Chamber</p>
+                        <p className="text-xs text-white">
+                          {Math.round(status.temperatures.chamber || 0)}°C
                         </p>
-                      </>
+                      </div>
                     ) : (
-                      <>
-                        <p className="text-xs text-bambu-gray">Nozzle</p>
-                        <p className="text-sm text-white">
-                          {Math.round(status.temperatures.nozzle || 0)}°C
-                        </p>
-                      </>
+                      <div /> /* Empty placeholder to maintain grid */
                     )}
                   </div>
-                  <div className="text-center p-2 bg-bambu-dark rounded-lg">
-                    <HeaterThermometer className="w-4 h-4 mx-auto mb-1" color="text-blue-400" isHeating={bedHeating} />
-                    <p className="text-xs text-bambu-gray">Bed</p>
-                    <p className="text-sm text-white">
-                      {Math.round(status.temperatures.bed || 0)}°C
-                    </p>
+
+                  {/* Print control buttons */}
+                  <div className="flex flex-col justify-center gap-1.5 w-20">
+                    {/* Stop button - visible when printing/paused */}
+                    <button
+                      onClick={() => setShowStopConfirm(true)}
+                      disabled={!isPrinting || isControlBusy}
+                      className={`
+                        flex items-center justify-center gap-1 px-2 py-1.5 rounded-lg text-xs font-medium
+                        transition-colors
+                        ${isPrinting
+                          ? 'bg-red-500/20 text-red-400 hover:bg-red-500/30'
+                          : 'bg-bambu-dark text-bambu-gray/50 cursor-not-allowed'
+                        }
+                      `}
+                      title="Stop print"
+                    >
+                      <Square className="w-3 h-3" />
+                      Stop
+                    </button>
+
+                    {/* Pause/Resume button */}
+                    <button
+                      onClick={() => isPaused ? setShowResumeConfirm(true) : setShowPauseConfirm(true)}
+                      disabled={!isPrinting || isControlBusy}
+                      className={`
+                        flex items-center justify-center gap-1 px-2 py-1.5 rounded-lg text-xs font-medium
+                        transition-colors
+                        ${isPrinting
+                          ? isPaused
+                            ? 'bg-bambu-green/20 text-bambu-green hover:bg-bambu-green/30'
+                            : 'bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/30'
+                          : 'bg-bambu-dark text-bambu-gray/50 cursor-not-allowed'
+                        }
+                      `}
+                      title={isPaused ? 'Resume print' : 'Pause print'}
+                    >
+                      {isPaused ? <Play className="w-3 h-3" /> : <Pause className="w-3 h-3" />}
+                      {isPaused ? 'Resume' : 'Pause'}
+                    </button>
                   </div>
-                  {status.temperatures.chamber !== undefined && (
-                    <div className="text-center p-2 bg-bambu-dark rounded-lg">
-                      <HeaterThermometer className="w-4 h-4 mx-auto mb-1" color="text-green-400" isHeating={chamberHeating} />
-                      <p className="text-xs text-bambu-gray">Chamber</p>
-                      <p className="text-sm text-white">
-                        {Math.round(status.temperatures.chamber || 0)}°C
-                      </p>
-                    </div>
-                  )}
                 </div>
               );
             })()}
@@ -1340,9 +1521,14 @@ function PrinterCard({
                                   fillLevel: hasFillLevel ? tray.remain : null,
                                 } : null;
 
-                                const slotContent = (
+                                // Check if this specific slot is being refreshed
+                                const isRefreshing = refreshingSlot?.amsId === ams.id &&
+                                  refreshingSlot?.slotId === slotIdx;
+
+                                // Slot visual content (goes inside hover card)
+                                const slotVisual = (
                                   <div
-                                    className={`bg-bambu-dark-tertiary rounded p-1 text-center cursor-default ${isEmpty ? 'opacity-50' : ''} ${isActive ? 'ring-2 ring-bambu-green ring-offset-1 ring-offset-bambu-dark' : ''}`}
+                                    className={`bg-bambu-dark-tertiary rounded p-1 text-center ${isEmpty ? 'opacity-50' : ''} ${isActive ? 'ring-2 ring-bambu-green ring-offset-1 ring-offset-bambu-dark' : ''}`}
                                   >
                                     <div
                                       className="w-3.5 h-3.5 rounded-full mx-auto mb-0.5 border-2"
@@ -1372,14 +1558,60 @@ function PrinterCard({
                                   </div>
                                 );
 
-                                return filamentData ? (
-                                  <FilamentHoverCard key={slotIdx} data={filamentData}>
-                                    {slotContent}
-                                  </FilamentHoverCard>
-                                ) : (
-                                  <EmptySlotHoverCard key={slotIdx}>
-                                    {slotContent}
-                                  </EmptySlotHoverCard>
+                                // Wrapper with menu button, dropdown, and loading overlay (outside hover card)
+                                return (
+                                  <div key={slotIdx} className="relative group">
+                                    {/* Loading overlay during RFID re-read */}
+                                    {isRefreshing && (
+                                      <div className="absolute inset-0 bg-bambu-dark-tertiary/80 rounded flex items-center justify-center z-20">
+                                        <RefreshCw className="w-4 h-4 text-bambu-green animate-spin" />
+                                      </div>
+                                    )}
+                                    {/* Menu button - appears on hover, hidden when printer busy */}
+                                    {status?.state !== 'RUNNING' && (
+                                      <button
+                                        onClick={(e) => {
+                                          e.stopPropagation();
+                                          setAmsSlotMenu(
+                                            amsSlotMenu?.amsId === ams.id && amsSlotMenu?.slotId === slotIdx
+                                              ? null
+                                              : { amsId: ams.id, slotId: slotIdx }
+                                          );
+                                        }}
+                                        className="absolute -top-1 -right-1 w-4 h-4 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity z-10 hover:bg-bambu-dark-tertiary"
+                                        title="Slot options"
+                                      >
+                                        <MoreVertical className="w-2.5 h-2.5 text-bambu-gray" />
+                                      </button>
+                                    )}
+                                    {/* Dropdown menu */}
+                                    {status?.state !== 'RUNNING' && amsSlotMenu?.amsId === ams.id && amsSlotMenu?.slotId === slotIdx && (
+                                      <div className="absolute top-full left-0 mt-1 z-50 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg shadow-xl py-1 min-w-[120px]">
+                                        <button
+                                          className="w-full px-3 py-1.5 text-left text-xs text-white hover:bg-bambu-dark-tertiary flex items-center gap-2"
+                                          onClick={(e) => {
+                                            e.stopPropagation();
+                                            refreshAmsSlotMutation.mutate({ amsId: ams.id, slotId: slotIdx });
+                                            setAmsSlotMenu(null);
+                                          }}
+                                          disabled={isRefreshing}
+                                        >
+                                          <RefreshCw className={`w-3 h-3 ${isRefreshing ? 'animate-spin' : ''}`} />
+                                          Re-read RFID
+                                        </button>
+                                      </div>
+                                    )}
+                                    {/* Hover card wraps only the visual content */}
+                                    {filamentData ? (
+                                      <FilamentHoverCard data={filamentData}>
+                                        {slotVisual}
+                                      </FilamentHoverCard>
+                                    ) : (
+                                      <EmptySlotHoverCard>
+                                        {slotVisual}
+                                      </EmptySlotHoverCard>
+                                    )}
+                                  </div>
                                 );
                               })}
                             </div>
@@ -1419,9 +1651,15 @@ function PrinterCard({
                           fillLevel: hasFillLevel ? tray.remain : null,
                         } : null;
 
-                        const slotContent = (
+                        const htSlotId = tray?.id ?? 0;
+                        // Check if this specific slot is being refreshed
+                        const isHtRefreshing = refreshingSlot?.amsId === ams.id &&
+                          refreshingSlot?.slotId === htSlotId;
+
+                        // Slot visual content (goes inside hover card)
+                        const slotVisual = (
                           <div
-                            className={`bg-bambu-dark-tertiary rounded p-1 text-center cursor-default ${isEmpty ? 'opacity-50' : ''} ${isActive ? 'ring-2 ring-bambu-green ring-offset-1 ring-offset-bambu-dark' : ''}`}
+                            className={`bg-bambu-dark-tertiary rounded p-1 text-center ${isEmpty ? 'opacity-50' : ''} ${isActive ? 'ring-2 ring-bambu-green ring-offset-1 ring-offset-bambu-dark' : ''}`}
                           >
                             <div
                               className="w-3.5 h-3.5 rounded-full mx-auto mb-0.5 border-2"
@@ -1464,16 +1702,59 @@ function PrinterCard({
                             </div>
                             {/* Row 2: Slot (left) + Stats (right stacked) */}
                             <div className="flex gap-1.5">
-                              {/* Slot - takes remaining width */}
-                              {filamentData ? (
-                                <FilamentHoverCard data={filamentData} className="flex-1">
-                                  {slotContent}
-                                </FilamentHoverCard>
-                              ) : (
-                                <EmptySlotHoverCard className="flex-1">
-                                  {slotContent}
-                                </EmptySlotHoverCard>
-                              )}
+                              {/* Slot wrapper with menu button, dropdown, and loading overlay */}
+                              <div className="relative group flex-1">
+                                {/* Loading overlay during RFID re-read */}
+                                {isHtRefreshing && (
+                                  <div className="absolute inset-0 bg-bambu-dark-tertiary/80 rounded flex items-center justify-center z-20">
+                                    <RefreshCw className="w-4 h-4 text-bambu-green animate-spin" />
+                                  </div>
+                                )}
+                                {/* Menu button - appears on hover, hidden when printer busy */}
+                                {status?.state !== 'RUNNING' && (
+                                  <button
+                                    onClick={(e) => {
+                                      e.stopPropagation();
+                                      setAmsSlotMenu(
+                                        amsSlotMenu?.amsId === ams.id && amsSlotMenu?.slotId === htSlotId
+                                          ? null
+                                          : { amsId: ams.id, slotId: htSlotId }
+                                      );
+                                    }}
+                                    className="absolute -top-1 -right-1 w-4 h-4 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity z-10 hover:bg-bambu-dark-tertiary"
+                                    title="Slot options"
+                                  >
+                                    <MoreVertical className="w-2.5 h-2.5 text-bambu-gray" />
+                                  </button>
+                                )}
+                                {/* Dropdown menu */}
+                                {status?.state !== 'RUNNING' && amsSlotMenu?.amsId === ams.id && amsSlotMenu?.slotId === htSlotId && (
+                                  <div className="absolute top-full left-0 mt-1 z-50 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg shadow-xl py-1 min-w-[120px]">
+                                    <button
+                                      className="w-full px-3 py-1.5 text-left text-xs text-white hover:bg-bambu-dark-tertiary flex items-center gap-2"
+                                      onClick={(e) => {
+                                        e.stopPropagation();
+                                        refreshAmsSlotMutation.mutate({ amsId: ams.id, slotId: htSlotId });
+                                        setAmsSlotMenu(null);
+                                      }}
+                                      disabled={isHtRefreshing}
+                                    >
+                                      <RefreshCw className={`w-3 h-3 ${isHtRefreshing ? 'animate-spin' : ''}`} />
+                                      Re-read RFID
+                                    </button>
+                                  </div>
+                                )}
+                                {/* Hover card wraps only the visual content */}
+                                {filamentData ? (
+                                  <FilamentHoverCard data={filamentData}>
+                                    {slotVisual}
+                                  </FilamentHoverCard>
+                                ) : (
+                                  <EmptySlotHoverCard>
+                                    {slotVisual}
+                                  </EmptySlotHoverCard>
+                                )}
+                              </div>
                               {/* Stats stacked vertically: Temp on top, Humidity below */}
                               {(ams.humidity != null || ams.temp != null) && (
                                 <div className="flex flex-col justify-center gap-1 shrink-0">
@@ -1749,6 +2030,51 @@ function PrinterCard({
         />
       )}
 
+      {/* Stop Print Confirmation */}
+      {showStopConfirm && (
+        <ConfirmModal
+          title="Stop Print"
+          message={`Are you sure you want to stop the current print on "${printer.name}"? This will cancel the print job.`}
+          confirmText="Stop Print"
+          variant="danger"
+          onConfirm={() => {
+            stopPrintMutation.mutate();
+            setShowStopConfirm(false);
+          }}
+          onCancel={() => setShowStopConfirm(false)}
+        />
+      )}
+
+      {/* Pause Print Confirmation */}
+      {showPauseConfirm && (
+        <ConfirmModal
+          title="Pause Print"
+          message={`Are you sure you want to pause the current print on "${printer.name}"?`}
+          confirmText="Pause Print"
+          variant="default"
+          onConfirm={() => {
+            pausePrintMutation.mutate();
+            setShowPauseConfirm(false);
+          }}
+          onCancel={() => setShowPauseConfirm(false)}
+        />
+      )}
+
+      {/* Resume Print Confirmation */}
+      {showResumeConfirm && (
+        <ConfirmModal
+          title="Resume Print"
+          message={`Are you sure you want to resume the print on "${printer.name}"?`}
+          confirmText="Resume Print"
+          variant="default"
+          onConfirm={() => {
+            resumePrintMutation.mutate();
+            setShowResumeConfirm(false);
+          }}
+          onCancel={() => setShowResumeConfirm(false)}
+        />
+      )}
+
       {/* HMS Error Modal */}
       {showHMSModal && (
         <HMSErrorModal
@@ -1779,6 +2105,14 @@ function PrinterCard({
           onClose={() => setShowEditModal(false)}
         />
       )}
+
+      {/* AMS Slot Menu Backdrop - closes menu when clicking outside */}
+      {amsSlotMenu && (
+        <div
+          className="fixed inset-0 z-40"
+          onClick={() => setAmsSlotMenu(null)}
+        />
+      )}
     </Card>
   );
 }