|
@@ -1,6 +1,6 @@
|
|
|
import { useState, useEffect, useMemo } from 'react';
|
|
import { useState, useEffect, useMemo } from 'react';
|
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
|
-import { X, Printer, Loader2, Calendar, Pencil } from 'lucide-react';
|
|
|
|
|
|
|
+import { X, Printer, Loader2, Calendar, Pencil, AlertCircle } from 'lucide-react';
|
|
|
import { api } from '../../api/client';
|
|
import { api } from '../../api/client';
|
|
|
import type { PrintQueueItemCreate, PrintQueueItemUpdate } from '../../api/client';
|
|
import type { PrintQueueItemCreate, PrintQueueItemUpdate } from '../../api/client';
|
|
|
import { Card, CardContent } from '../Card';
|
|
import { Card, CardContent } from '../Card';
|
|
@@ -23,9 +23,9 @@ import { DEFAULT_PRINT_OPTIONS, DEFAULT_SCHEDULE_OPTIONS } from './types';
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Unified PrintModal component that handles three modes:
|
|
* Unified PrintModal component that handles three modes:
|
|
|
- * - 'reprint': Immediate print from archive
|
|
|
|
|
- * - 'add-to-queue': Schedule print to queue
|
|
|
|
|
- * - 'edit-queue-item': Edit existing queue item
|
|
|
|
|
|
|
+ * - 'reprint': Immediate print from archive (supports multi-printer)
|
|
|
|
|
+ * - 'add-to-queue': Schedule print to queue (supports multi-printer)
|
|
|
|
|
+ * - 'edit-queue-item': Edit existing queue item (single printer only)
|
|
|
*/
|
|
*/
|
|
|
export function PrintModal({
|
|
export function PrintModal({
|
|
|
mode,
|
|
mode,
|
|
@@ -38,7 +38,7 @@ export function PrintModal({
|
|
|
const queryClient = useQueryClient();
|
|
const queryClient = useQueryClient();
|
|
|
const { showToast } = useToast();
|
|
const { showToast } = useToast();
|
|
|
|
|
|
|
|
- // Initialize state based on mode
|
|
|
|
|
|
|
+ // Single printer selection (for edit mode and backward compatibility)
|
|
|
const [selectedPrinter, setSelectedPrinter] = useState<number | null>(() => {
|
|
const [selectedPrinter, setSelectedPrinter] = useState<number | null>(() => {
|
|
|
if (mode === 'edit-queue-item' && queueItem) {
|
|
if (mode === 'edit-queue-item' && queueItem) {
|
|
|
return queueItem.printer_id;
|
|
return queueItem.printer_id;
|
|
@@ -46,6 +46,9 @@ export function PrintModal({
|
|
|
return null;
|
|
return null;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ // Multiple printer selection (for reprint and add-to-queue modes)
|
|
|
|
|
+ const [selectedPrinters, setSelectedPrinters] = useState<number[]>([]);
|
|
|
|
|
+
|
|
|
const [selectedPlate, setSelectedPlate] = useState<number | null>(() => {
|
|
const [selectedPlate, setSelectedPlate] = useState<number | null>(() => {
|
|
|
if (mode === 'edit-queue-item' && queueItem) {
|
|
if (mode === 'edit-queue-item' && queueItem) {
|
|
|
return queueItem.plate_id;
|
|
return queueItem.plate_id;
|
|
@@ -68,7 +71,6 @@ export function PrintModal({
|
|
|
|
|
|
|
|
const [scheduleOptions, setScheduleOptions] = useState<ScheduleOptions>(() => {
|
|
const [scheduleOptions, setScheduleOptions] = useState<ScheduleOptions>(() => {
|
|
|
if (mode === 'edit-queue-item' && queueItem) {
|
|
if (mode === 'edit-queue-item' && queueItem) {
|
|
|
- // Determine schedule type from queue item
|
|
|
|
|
let scheduleType: ScheduleType = 'asap';
|
|
let scheduleType: ScheduleType = 'asap';
|
|
|
if (queueItem.manual_start) {
|
|
if (queueItem.manual_start) {
|
|
|
scheduleType = 'manual';
|
|
scheduleType = 'manual';
|
|
@@ -76,7 +78,6 @@ export function PrintModal({
|
|
|
scheduleType = 'scheduled';
|
|
scheduleType = 'scheduled';
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Convert scheduled time to local datetime-local format
|
|
|
|
|
let scheduledTime = '';
|
|
let scheduledTime = '';
|
|
|
if (queueItem.scheduled_time && !isPlaceholderDate(queueItem.scheduled_time)) {
|
|
if (queueItem.scheduled_time && !isPlaceholderDate(queueItem.scheduled_time)) {
|
|
|
const date = new Date(queueItem.scheduled_time);
|
|
const date = new Date(queueItem.scheduled_time);
|
|
@@ -111,6 +112,18 @@ export function PrintModal({
|
|
|
const [initialPrinterId] = useState(() => (mode === 'edit-queue-item' && queueItem ? queueItem.printer_id : null));
|
|
const [initialPrinterId] = useState(() => (mode === 'edit-queue-item' && queueItem ? queueItem.printer_id : null));
|
|
|
const [initialPlateId] = useState(() => (mode === 'edit-queue-item' && queueItem ? queueItem.plate_id : null));
|
|
const [initialPlateId] = useState(() => (mode === 'edit-queue-item' && queueItem ? queueItem.plate_id : null));
|
|
|
|
|
|
|
|
|
|
+ // Submission state for multi-printer
|
|
|
|
|
+ const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
|
+ const [submitProgress, setSubmitProgress] = useState({ current: 0, total: 0 });
|
|
|
|
|
+
|
|
|
|
|
+ // Determine if we're in multi-printer mode
|
|
|
|
|
+ const isMultiPrinterMode = mode !== 'edit-queue-item';
|
|
|
|
|
+ const effectivePrinterCount = isMultiPrinterMode ? selectedPrinters.length : (selectedPrinter ? 1 : 0);
|
|
|
|
|
+ // For filament mapping, use first selected printer (mapping applies to all)
|
|
|
|
|
+ const effectivePrinterId = isMultiPrinterMode
|
|
|
|
|
+ ? (selectedPrinters.length > 0 ? selectedPrinters[0] : null)
|
|
|
|
|
+ : selectedPrinter;
|
|
|
|
|
+
|
|
|
// Queries
|
|
// Queries
|
|
|
const { data: printers, isLoading: loadingPrinters } = useQuery({
|
|
const { data: printers, isLoading: loadingPrinters } = useQuery({
|
|
|
queryKey: ['printers'],
|
|
queryKey: ['printers'],
|
|
@@ -128,13 +141,14 @@ export function PrintModal({
|
|
|
enabled: selectedPlate !== null || !platesData?.is_multi_plate,
|
|
enabled: selectedPlate !== null || !platesData?.is_multi_plate,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ // Only fetch printer status when single printer selected (for filament mapping)
|
|
|
const { data: printerStatus } = useQuery({
|
|
const { data: printerStatus } = useQuery({
|
|
|
- queryKey: ['printer-status', selectedPrinter],
|
|
|
|
|
- queryFn: () => api.getPrinterStatus(selectedPrinter!),
|
|
|
|
|
- enabled: !!selectedPrinter,
|
|
|
|
|
|
|
+ queryKey: ['printer-status', effectivePrinterId],
|
|
|
|
|
+ queryFn: () => api.getPrinterStatus(effectivePrinterId!),
|
|
|
|
|
+ enabled: !!effectivePrinterId,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // Get AMS mapping from hook
|
|
|
|
|
|
|
+ // Get AMS mapping from hook (only when single printer selected)
|
|
|
const { amsMapping } = useFilamentMapping(filamentReqs, printerStatus, manualMappings);
|
|
const { amsMapping } = useFilamentMapping(filamentReqs, printerStatus, manualMappings);
|
|
|
|
|
|
|
|
// Auto-select first plate for single-plate files
|
|
// Auto-select first plate for single-plate files
|
|
@@ -144,70 +158,41 @@ export function PrintModal({
|
|
|
}
|
|
}
|
|
|
}, [platesData, selectedPlate]);
|
|
}, [platesData, selectedPlate]);
|
|
|
|
|
|
|
|
- // Auto-select first printer when only one available (add-to-queue mode)
|
|
|
|
|
|
|
+ // Auto-select first printer when only one available (non-multi mode)
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
- if (mode === 'add-to-queue' && printers?.length === 1 && !selectedPrinter) {
|
|
|
|
|
- setSelectedPrinter(printers[0].id);
|
|
|
|
|
|
|
+ if (mode === 'edit-queue-item') return;
|
|
|
|
|
+ const activePrinters = printers?.filter(p => p.is_active) || [];
|
|
|
|
|
+ if (activePrinters.length === 1 && selectedPrinters.length === 0) {
|
|
|
|
|
+ setSelectedPrinters([activePrinters[0].id]);
|
|
|
}
|
|
}
|
|
|
- }, [mode, printers, selectedPrinter]);
|
|
|
|
|
|
|
+ }, [mode, printers, selectedPrinters.length]);
|
|
|
|
|
|
|
|
// Clear manual mappings when printer or plate changes
|
|
// Clear manual mappings when printer or plate changes
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
if (mode === 'edit-queue-item') {
|
|
if (mode === 'edit-queue-item') {
|
|
|
- // Only clear if changed from initial values
|
|
|
|
|
if (selectedPrinter !== initialPrinterId || selectedPlate !== initialPlateId) {
|
|
if (selectedPrinter !== initialPrinterId || selectedPlate !== initialPlateId) {
|
|
|
setManualMappings({});
|
|
setManualMappings({});
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
- // Always clear on change for non-edit modes
|
|
|
|
|
setManualMappings({});
|
|
setManualMappings({});
|
|
|
}
|
|
}
|
|
|
- }, [mode, selectedPrinter, selectedPlate, initialPrinterId, initialPlateId]);
|
|
|
|
|
|
|
+ }, [mode, selectedPrinter, selectedPrinters, selectedPlate, initialPrinterId, initialPlateId]);
|
|
|
|
|
|
|
|
// Close on Escape key
|
|
// Close on Escape key
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
|
- if (e.key === 'Escape') onClose();
|
|
|
|
|
|
|
+ if (e.key === 'Escape' && !isSubmitting) onClose();
|
|
|
};
|
|
};
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
|
- }, [onClose]);
|
|
|
|
|
|
|
+ }, [onClose, isSubmitting]);
|
|
|
|
|
|
|
|
const isMultiPlate = platesData?.is_multi_plate ?? false;
|
|
const isMultiPlate = platesData?.is_multi_plate ?? false;
|
|
|
const plates = platesData?.plates ?? [];
|
|
const plates = platesData?.plates ?? [];
|
|
|
|
|
|
|
|
- // Reprint mutation
|
|
|
|
|
- const reprintMutation = useMutation({
|
|
|
|
|
- mutationFn: () => {
|
|
|
|
|
- if (!selectedPrinter) throw new Error('No printer selected');
|
|
|
|
|
- return api.reprintArchive(archiveId, selectedPrinter, {
|
|
|
|
|
- plate_id: selectedPlate ?? undefined,
|
|
|
|
|
- ams_mapping: amsMapping,
|
|
|
|
|
- ...printOptions,
|
|
|
|
|
- });
|
|
|
|
|
- },
|
|
|
|
|
- onSuccess: () => {
|
|
|
|
|
- showToast('Print started');
|
|
|
|
|
- onSuccess?.();
|
|
|
|
|
- onClose();
|
|
|
|
|
- },
|
|
|
|
|
- onError: (error: Error) => {
|
|
|
|
|
- showToast(error.message || 'Failed to start print', 'error');
|
|
|
|
|
- },
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // Add to queue mutation
|
|
|
|
|
|
|
+ // Add to queue mutation (single printer)
|
|
|
const addToQueueMutation = useMutation({
|
|
const addToQueueMutation = useMutation({
|
|
|
mutationFn: (data: PrintQueueItemCreate) => api.addToQueue(data),
|
|
mutationFn: (data: PrintQueueItemCreate) => api.addToQueue(data),
|
|
|
- onSuccess: () => {
|
|
|
|
|
- queryClient.invalidateQueries({ queryKey: ['queue'] });
|
|
|
|
|
- showToast('Added to print queue');
|
|
|
|
|
- onSuccess?.();
|
|
|
|
|
- onClose();
|
|
|
|
|
- },
|
|
|
|
|
- onError: (error: Error) => {
|
|
|
|
|
- showToast(error.message || 'Failed to add to queue', 'error');
|
|
|
|
|
- },
|
|
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// Update queue item mutation
|
|
// Update queue item mutation
|
|
@@ -224,38 +209,11 @@ export function PrintModal({
|
|
|
},
|
|
},
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- const handleSubmit = (e?: React.FormEvent) => {
|
|
|
|
|
|
|
+ const handleSubmit = async (e?: React.FormEvent) => {
|
|
|
e?.preventDefault();
|
|
e?.preventDefault();
|
|
|
|
|
|
|
|
- if (mode === 'reprint') {
|
|
|
|
|
- if (!selectedPrinter) {
|
|
|
|
|
- showToast('Please select a printer', 'error');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- reprintMutation.mutate();
|
|
|
|
|
- } else if (mode === 'add-to-queue') {
|
|
|
|
|
- if (!selectedPrinter) {
|
|
|
|
|
- showToast('Please select a printer', 'error');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const data: PrintQueueItemCreate = {
|
|
|
|
|
- printer_id: selectedPrinter,
|
|
|
|
|
- archive_id: archiveId,
|
|
|
|
|
- require_previous_success: scheduleOptions.requirePreviousSuccess,
|
|
|
|
|
- auto_off_after: scheduleOptions.autoOffAfter,
|
|
|
|
|
- manual_start: scheduleOptions.scheduleType === 'manual',
|
|
|
|
|
- ams_mapping: amsMapping,
|
|
|
|
|
- plate_id: selectedPlate,
|
|
|
|
|
- ...printOptions,
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- if (scheduleOptions.scheduleType === 'scheduled' && scheduleOptions.scheduledTime) {
|
|
|
|
|
- data.scheduled_time = new Date(scheduleOptions.scheduledTime).toISOString();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- addToQueueMutation.mutate(data);
|
|
|
|
|
- } else if (mode === 'edit-queue-item') {
|
|
|
|
|
|
|
+ if (mode === 'edit-queue-item') {
|
|
|
|
|
+ // Edit mode - single printer update
|
|
|
const data: PrintQueueItemUpdate = {
|
|
const data: PrintQueueItemUpdate = {
|
|
|
printer_id: selectedPrinter,
|
|
printer_id: selectedPrinter,
|
|
|
require_previous_success: scheduleOptions.requirePreviousSuccess,
|
|
require_previous_success: scheduleOptions.requirePreviousSuccess,
|
|
@@ -273,56 +231,150 @@ export function PrintModal({
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
updateQueueMutation.mutate(data);
|
|
updateQueueMutation.mutate(data);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Multi-printer modes (reprint or add-to-queue)
|
|
|
|
|
+ if (selectedPrinters.length === 0) {
|
|
|
|
|
+ showToast('Please select at least one printer', 'error');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setIsSubmitting(true);
|
|
|
|
|
+ setSubmitProgress({ current: 0, total: selectedPrinters.length });
|
|
|
|
|
+
|
|
|
|
|
+ const results: { success: number; failed: number; errors: string[] } = {
|
|
|
|
|
+ success: 0,
|
|
|
|
|
+ failed: 0,
|
|
|
|
|
+ errors: [],
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < selectedPrinters.length; i++) {
|
|
|
|
|
+ const printerId = selectedPrinters[i];
|
|
|
|
|
+ setSubmitProgress({ current: i + 1, total: selectedPrinters.length });
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Use the same AMS mapping for all printers (configured via UI based on first printer)
|
|
|
|
|
+ // This assumes all printers have the same filament configuration
|
|
|
|
|
+ if (mode === 'reprint') {
|
|
|
|
|
+ await api.reprintArchive(archiveId, printerId, {
|
|
|
|
|
+ plate_id: selectedPlate ?? undefined,
|
|
|
|
|
+ ams_mapping: amsMapping,
|
|
|
|
|
+ ...printOptions,
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // add-to-queue mode
|
|
|
|
|
+ const data: PrintQueueItemCreate = {
|
|
|
|
|
+ printer_id: printerId,
|
|
|
|
|
+ archive_id: archiveId,
|
|
|
|
|
+ require_previous_success: scheduleOptions.requirePreviousSuccess,
|
|
|
|
|
+ auto_off_after: scheduleOptions.autoOffAfter,
|
|
|
|
|
+ manual_start: scheduleOptions.scheduleType === 'manual',
|
|
|
|
|
+ ams_mapping: amsMapping,
|
|
|
|
|
+ plate_id: selectedPlate,
|
|
|
|
|
+ ...printOptions,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (scheduleOptions.scheduleType === 'scheduled' && scheduleOptions.scheduledTime) {
|
|
|
|
|
+ data.scheduled_time = new Date(scheduleOptions.scheduledTime).toISOString();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await addToQueueMutation.mutateAsync(data);
|
|
|
|
|
+ }
|
|
|
|
|
+ results.success++;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ results.failed++;
|
|
|
|
|
+ const printerName = printers?.find(p => p.id === printerId)?.name || `Printer ${printerId}`;
|
|
|
|
|
+ results.errors.push(`${printerName}: ${(error as Error).message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setIsSubmitting(false);
|
|
|
|
|
+
|
|
|
|
|
+ // Show result toast
|
|
|
|
|
+ if (results.failed === 0) {
|
|
|
|
|
+ const action = mode === 'reprint' ? 'sent to' : 'queued for';
|
|
|
|
|
+ if (results.success === 1) {
|
|
|
|
|
+ showToast(`Print ${action} printer`);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ showToast(`Print ${action} ${results.success} printers`);
|
|
|
|
|
+ }
|
|
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['queue'] });
|
|
|
|
|
+ onSuccess?.();
|
|
|
|
|
+ onClose();
|
|
|
|
|
+ } else if (results.success === 0) {
|
|
|
|
|
+ showToast(`Failed: ${results.errors[0]}`, 'error');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ showToast(`${results.success} succeeded, ${results.failed} failed`, 'error');
|
|
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['queue'] });
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const isPending =
|
|
|
|
|
- reprintMutation.isPending || addToQueueMutation.isPending || updateQueueMutation.isPending;
|
|
|
|
|
|
|
+ const isPending = isSubmitting || updateQueueMutation.isPending;
|
|
|
|
|
|
|
|
const canSubmit = useMemo(() => {
|
|
const canSubmit = useMemo(() => {
|
|
|
|
|
+ if (isPending) return false;
|
|
|
|
|
+
|
|
|
// For edit mode, printer can be null (unassigned)
|
|
// For edit mode, printer can be null (unassigned)
|
|
|
if (mode === 'edit-queue-item') {
|
|
if (mode === 'edit-queue-item') {
|
|
|
- return !isPending && (printers?.length ?? 0) > 0;
|
|
|
|
|
|
|
+ return (printers?.length ?? 0) > 0;
|
|
|
}
|
|
}
|
|
|
- // For reprint and add-to-queue, need a selected printer
|
|
|
|
|
- if (!selectedPrinter) return false;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // For reprint and add-to-queue, need at least one selected printer
|
|
|
|
|
+ if (selectedPrinters.length === 0) return false;
|
|
|
|
|
+
|
|
|
// For multi-plate files, need a selected plate
|
|
// For multi-plate files, need a selected plate
|
|
|
if (isMultiPlate && !selectedPlate) return false;
|
|
if (isMultiPlate && !selectedPlate) return false;
|
|
|
- return !isPending;
|
|
|
|
|
- }, [mode, selectedPrinter, isMultiPlate, selectedPlate, isPending, printers]);
|
|
|
|
|
|
|
+
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }, [mode, selectedPrinters.length, isMultiPlate, selectedPlate, isPending, printers]);
|
|
|
|
|
|
|
|
// Modal title and action button text based on mode
|
|
// Modal title and action button text based on mode
|
|
|
- const modalConfig = {
|
|
|
|
|
- reprint: {
|
|
|
|
|
- title: 'Re-print',
|
|
|
|
|
- icon: Printer,
|
|
|
|
|
- submitText: 'Print',
|
|
|
|
|
- submitIcon: Printer,
|
|
|
|
|
- loadingText: 'Sending...',
|
|
|
|
|
- },
|
|
|
|
|
- 'add-to-queue': {
|
|
|
|
|
- title: 'Schedule Print',
|
|
|
|
|
- icon: Calendar,
|
|
|
|
|
- submitText: 'Add to Queue',
|
|
|
|
|
- submitIcon: Calendar,
|
|
|
|
|
- loadingText: 'Adding...',
|
|
|
|
|
- },
|
|
|
|
|
- 'edit-queue-item': {
|
|
|
|
|
|
|
+ const getModalConfig = () => {
|
|
|
|
|
+ const printerCount = isMultiPrinterMode ? selectedPrinters.length : 1;
|
|
|
|
|
+
|
|
|
|
|
+ if (mode === 'reprint') {
|
|
|
|
|
+ return {
|
|
|
|
|
+ title: 'Re-print',
|
|
|
|
|
+ icon: Printer,
|
|
|
|
|
+ submitText: printerCount > 1 ? `Print to ${printerCount} Printers` : 'Print',
|
|
|
|
|
+ submitIcon: Printer,
|
|
|
|
|
+ loadingText: submitProgress.total > 1
|
|
|
|
|
+ ? `Sending ${submitProgress.current}/${submitProgress.total}...`
|
|
|
|
|
+ : 'Sending...',
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ if (mode === 'add-to-queue') {
|
|
|
|
|
+ return {
|
|
|
|
|
+ title: 'Schedule Print',
|
|
|
|
|
+ icon: Calendar,
|
|
|
|
|
+ submitText: printerCount > 1 ? `Queue to ${printerCount} Printers` : 'Add to Queue',
|
|
|
|
|
+ submitIcon: Calendar,
|
|
|
|
|
+ loadingText: submitProgress.total > 1
|
|
|
|
|
+ ? `Adding ${submitProgress.current}/${submitProgress.total}...`
|
|
|
|
|
+ : 'Adding...',
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
title: 'Edit Queue Item',
|
|
title: 'Edit Queue Item',
|
|
|
icon: Pencil,
|
|
icon: Pencil,
|
|
|
submitText: 'Save Changes',
|
|
submitText: 'Save Changes',
|
|
|
submitIcon: Pencil,
|
|
submitIcon: Pencil,
|
|
|
loadingText: 'Saving...',
|
|
loadingText: 'Saving...',
|
|
|
- },
|
|
|
|
|
- }[mode];
|
|
|
|
|
|
|
+ };
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
|
|
+ const modalConfig = getModalConfig();
|
|
|
const TitleIcon = modalConfig.icon;
|
|
const TitleIcon = modalConfig.icon;
|
|
|
const SubmitIcon = modalConfig.submitIcon;
|
|
const SubmitIcon = modalConfig.submitIcon;
|
|
|
|
|
|
|
|
|
|
+ // Show filament mapping only when single printer selected
|
|
|
|
|
+ const showFilamentMapping = effectivePrinterId && (isMultiPlate ? selectedPlate !== null : true);
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<div
|
|
<div
|
|
|
className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
|
|
className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
|
|
|
- onClick={onClose}
|
|
|
|
|
|
|
+ onClick={isSubmitting ? undefined : onClose}
|
|
|
>
|
|
>
|
|
|
<Card
|
|
<Card
|
|
|
className="w-full max-w-lg max-h-[90vh] overflow-y-auto"
|
|
className="w-full max-w-lg max-h-[90vh] overflow-y-auto"
|
|
@@ -339,7 +391,7 @@ export function PrintModal({
|
|
|
<TitleIcon className="w-5 h-5 text-bambu-green" />
|
|
<TitleIcon className="w-5 h-5 text-bambu-green" />
|
|
|
<h2 className="text-lg font-semibold text-white">{modalConfig.title}</h2>
|
|
<h2 className="text-lg font-semibold text-white">{modalConfig.title}</h2>
|
|
|
</div>
|
|
</div>
|
|
|
- <Button variant="ghost" size="sm" onClick={onClose}>
|
|
|
|
|
|
|
+ <Button variant="ghost" size="sm" onClick={onClose} disabled={isSubmitting}>
|
|
|
<X className="w-5 h-5" />
|
|
<X className="w-5 h-5" />
|
|
|
</Button>
|
|
</Button>
|
|
|
</div>
|
|
</div>
|
|
@@ -349,7 +401,8 @@ export function PrintModal({
|
|
|
<p className={`text-sm text-bambu-gray ${mode === 'reprint' ? 'mb-4' : ''}`}>
|
|
<p className={`text-sm text-bambu-gray ${mode === 'reprint' ? 'mb-4' : ''}`}>
|
|
|
{mode === 'reprint' ? (
|
|
{mode === 'reprint' ? (
|
|
|
<>
|
|
<>
|
|
|
- Send <span className="text-white">{archiveName}</span> to a printer
|
|
|
|
|
|
|
+ Send <span className="text-white">{archiveName}</span> to{' '}
|
|
|
|
|
+ {isMultiPrinterMode ? 'printer(s)' : 'a printer'}
|
|
|
</>
|
|
</>
|
|
|
) : (
|
|
) : (
|
|
|
<>
|
|
<>
|
|
@@ -363,11 +416,24 @@ export function PrintModal({
|
|
|
<PrinterSelector
|
|
<PrinterSelector
|
|
|
printers={printers || []}
|
|
printers={printers || []}
|
|
|
selectedPrinterId={selectedPrinter}
|
|
selectedPrinterId={selectedPrinter}
|
|
|
|
|
+ selectedPrinterIds={selectedPrinters}
|
|
|
onSelect={setSelectedPrinter}
|
|
onSelect={setSelectedPrinter}
|
|
|
|
|
+ onMultiSelect={setSelectedPrinters}
|
|
|
isLoading={loadingPrinters}
|
|
isLoading={loadingPrinters}
|
|
|
allowUnassigned={mode === 'edit-queue-item'}
|
|
allowUnassigned={mode === 'edit-queue-item'}
|
|
|
|
|
+ allowMultiple={isMultiPrinterMode}
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
|
|
+ {/* Multi-printer filament mapping note */}
|
|
|
|
|
+ {isMultiPrinterMode && selectedPrinters.length > 1 && (
|
|
|
|
|
+ <div className="flex items-start gap-2 p-3 mb-2 bg-blue-500/10 border border-blue-500/30 rounded-lg text-sm">
|
|
|
|
|
+ <AlertCircle className="w-4 h-4 text-blue-400 mt-0.5 flex-shrink-0" />
|
|
|
|
|
+ <p className="text-blue-400">
|
|
|
|
|
+ Slot mapping below applies to all {selectedPrinters.length} printers. Ensure they have matching filament configurations.
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
{/* Plate selection */}
|
|
{/* Plate selection */}
|
|
|
<PlateSelector
|
|
<PlateSelector
|
|
|
plates={plates}
|
|
plates={plates}
|
|
@@ -376,10 +442,10 @@ export function PrintModal({
|
|
|
onSelect={setSelectedPlate}
|
|
onSelect={setSelectedPlate}
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
- {/* Filament mapping - show when printer selected and plate ready */}
|
|
|
|
|
- {selectedPrinter && (isMultiPlate ? selectedPlate !== null : true) && (
|
|
|
|
|
|
|
+ {/* Filament mapping - show when single printer selected and plate ready */}
|
|
|
|
|
+ {showFilamentMapping && (
|
|
|
<FilamentMapping
|
|
<FilamentMapping
|
|
|
- printerId={selectedPrinter}
|
|
|
|
|
|
|
+ printerId={effectivePrinterId!}
|
|
|
archiveId={archiveId}
|
|
archiveId={archiveId}
|
|
|
selectedPlate={selectedPlate}
|
|
selectedPlate={selectedPlate}
|
|
|
isMultiPlate={isMultiPlate}
|
|
isMultiPlate={isMultiPlate}
|
|
@@ -389,7 +455,7 @@ export function PrintModal({
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
{/* Print options */}
|
|
{/* Print options */}
|
|
|
- {(mode === 'reprint' || selectedPrinter) && (
|
|
|
|
|
|
|
+ {(mode === 'reprint' || effectivePrinterCount > 0) && (
|
|
|
<PrintOptionsPanel options={printOptions} onChange={setPrintOptions} />
|
|
<PrintOptionsPanel options={printOptions} onChange={setPrintOptions} />
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
@@ -399,16 +465,15 @@ export function PrintModal({
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
{/* Error message */}
|
|
{/* Error message */}
|
|
|
- {(reprintMutation.isError || addToQueueMutation.isError || updateQueueMutation.isError) && (
|
|
|
|
|
|
|
+ {updateQueueMutation.isError && (
|
|
|
<div className="mb-4 p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-sm text-red-400">
|
|
<div className="mb-4 p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-sm text-red-400">
|
|
|
- {((reprintMutation.error || addToQueueMutation.error || updateQueueMutation.error) as Error)?.message ||
|
|
|
|
|
- 'Failed to complete operation'}
|
|
|
|
|
|
|
+ {(updateQueueMutation.error as Error)?.message || 'Failed to complete operation'}
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
{/* Actions */}
|
|
{/* Actions */}
|
|
|
<div className={`flex gap-3 ${mode === 'reprint' ? '' : 'pt-2'}`}>
|
|
<div className={`flex gap-3 ${mode === 'reprint' ? '' : 'pt-2'}`}>
|
|
|
- <Button type="button" variant="secondary" onClick={onClose} className="flex-1">
|
|
|
|
|
|
|
+ <Button type="button" variant="secondary" onClick={onClose} className="flex-1" disabled={isSubmitting}>
|
|
|
Cancel
|
|
Cancel
|
|
|
</Button>
|
|
</Button>
|
|
|
<Button
|
|
<Button
|