mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 21:47:39 +02:00
Issue #162
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Printer as PrinterIcon,
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Circle,
|
||||
RefreshCw,
|
||||
Wand2,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
import { api } from '../../api/client';
|
||||
import { getColorName } from '../../utils/colors';
|
||||
@@ -16,7 +17,7 @@ import {
|
||||
normalizeColorForCompare,
|
||||
colorsAreSimilar,
|
||||
} from '../../utils/amsHelpers';
|
||||
import type { PrinterSelectorProps } from './types';
|
||||
import type { PrinterSelectorProps, AssignmentMode } from './types';
|
||||
import type { PrinterMappingResult, PerPrinterConfig } from '../../hooks/useMultiPrinterFilamentMapping';
|
||||
import type { FilamentRequirement, LoadedFilament } from '../../hooks/useFilamentMapping';
|
||||
|
||||
@@ -29,6 +30,16 @@ interface PrinterSelectorWithMappingProps extends PrinterSelectorProps {
|
||||
onAutoConfigurePrinter?: (printerId: number) => void;
|
||||
/** Callback to update printer config */
|
||||
onUpdatePrinterConfig?: (printerId: number, config: Partial<PerPrinterConfig>) => void;
|
||||
/** Current assignment mode */
|
||||
assignmentMode?: AssignmentMode;
|
||||
/** Handler for assignment mode change */
|
||||
onAssignmentModeChange?: (mode: AssignmentMode) => void;
|
||||
/** Selected target model (when assignmentMode is 'model') */
|
||||
targetModel?: string | null;
|
||||
/** Handler for target model change */
|
||||
onTargetModelChange?: (model: string | null) => void;
|
||||
/** Suggested model from sliced file (for pre-selection) */
|
||||
slicedForModel?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,9 +223,42 @@ export function PrinterSelector({
|
||||
filamentReqs,
|
||||
onAutoConfigurePrinter,
|
||||
onUpdatePrinterConfig,
|
||||
assignmentMode = 'printer',
|
||||
onAssignmentModeChange,
|
||||
targetModel,
|
||||
onTargetModelChange,
|
||||
slicedForModel,
|
||||
}: PrinterSelectorWithMappingProps) {
|
||||
// State for showing all printers vs only matching model
|
||||
const [showAllPrinters, setShowAllPrinters] = useState(false);
|
||||
|
||||
// Filter printers based on showInactive flag
|
||||
const displayPrinters = showInactive ? printers : printers.filter((p) => p.is_active);
|
||||
const activePrinters = showInactive ? printers : printers.filter((p) => p.is_active);
|
||||
|
||||
// Filter by sliced model (only in printer mode, when slicedForModel is set)
|
||||
const displayPrinters = useMemo(() => {
|
||||
if (assignmentMode !== 'printer' || !slicedForModel || showAllPrinters) {
|
||||
return activePrinters;
|
||||
}
|
||||
// Filter to only show printers matching the sliced model
|
||||
const matching = activePrinters.filter((p) => p.model === slicedForModel);
|
||||
// If no matching printers, show all
|
||||
return matching.length > 0 ? matching : activePrinters;
|
||||
}, [activePrinters, assignmentMode, slicedForModel, showAllPrinters]);
|
||||
|
||||
// Check if there are hidden printers due to model filtering
|
||||
const hiddenPrinterCount = activePrinters.length - displayPrinters.length;
|
||||
|
||||
// Get unique models from available printers (for model-based assignment)
|
||||
const uniqueModels = useMemo(() => {
|
||||
const models = activePrinters
|
||||
.map(p => p.model)
|
||||
.filter((m): m is string => Boolean(m));
|
||||
return [...new Set(models)].sort();
|
||||
}, [activePrinters]);
|
||||
|
||||
// Check if model-based assignment is available (need callbacks and multiple printers of same model)
|
||||
const modelAssignmentAvailable = onAssignmentModeChange && onTargetModelChange && uniqueModels.length > 0;
|
||||
|
||||
const showMappingOptions = allowMultiple &&
|
||||
selectedPrinterIds.length > 1 &&
|
||||
@@ -285,8 +329,56 @@ export function PrinterSelector({
|
||||
|
||||
return (
|
||||
<div className="space-y-2 mb-6">
|
||||
{/* Multi-select header */}
|
||||
{allowMultiple && displayPrinters.length > 1 && (
|
||||
{/* Assignment mode toggle (model vs specific printer) */}
|
||||
{modelAssignmentAvailable && (
|
||||
<div className="flex gap-2 mb-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onAssignmentModeChange!('printer');
|
||||
onTargetModelChange!(null);
|
||||
}}
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-lg border transition-colors ${
|
||||
assignmentMode === 'printer'
|
||||
? 'border-bambu-green bg-bambu-green/10 text-white'
|
||||
: 'border-bambu-dark-tertiary bg-bambu-dark text-bambu-gray hover:border-bambu-gray'
|
||||
}`}
|
||||
>
|
||||
<PrinterIcon className="w-4 h-4" />
|
||||
<span className="text-sm">Specific Printer</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onAssignmentModeChange!('model');
|
||||
onMultiSelect([]);
|
||||
// Pre-select the sliced-for model if available, otherwise first model
|
||||
const defaultModel = slicedForModel && uniqueModels.includes(slicedForModel)
|
||||
? slicedForModel
|
||||
: uniqueModels[0];
|
||||
onTargetModelChange!(defaultModel);
|
||||
}}
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-lg border transition-colors ${
|
||||
assignmentMode === 'model'
|
||||
? 'border-bambu-green bg-bambu-green/10 text-white'
|
||||
: 'border-bambu-dark-tertiary bg-bambu-dark text-bambu-gray hover:border-bambu-gray'
|
||||
}`}
|
||||
>
|
||||
<Users className="w-4 h-4" />
|
||||
<span className="text-sm">Any {slicedForModel || 'Model'}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Model info (when in model mode) */}
|
||||
{assignmentMode === 'model' && modelAssignmentAvailable && targetModel && (
|
||||
<p className="text-xs text-bambu-gray mb-4">
|
||||
Scheduler will assign to first available idle {targetModel} printer
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Multi-select header (only in printer mode) */}
|
||||
{assignmentMode === 'printer' && allowMultiple && displayPrinters.length > 1 && (
|
||||
<div className="flex items-center justify-between text-xs text-bambu-gray mb-2">
|
||||
<span>
|
||||
{selectedCount === 0
|
||||
@@ -316,7 +408,8 @@ export function PrinterSelector({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{displayPrinters.map((printer) => {
|
||||
{/* Printer list (only in printer mode) */}
|
||||
{assignmentMode === 'printer' && displayPrinters.map((printer) => {
|
||||
const selected = isSelected(printer.id);
|
||||
const mappingResult = getPrinterMappingResult(printer.id);
|
||||
const hasOverride = mappingResult && !mappingResult.config.useDefault;
|
||||
@@ -430,13 +523,45 @@ export function PrinterSelector({
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Warning when no printer selected */}
|
||||
{selectedCount === 0 && (
|
||||
{/* Show hidden printers toggle */}
|
||||
{assignmentMode === 'printer' && hiddenPrinterCount > 0 && !showAllPrinters && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAllPrinters(true)}
|
||||
className="text-xs text-bambu-gray hover:text-white transition-colors mt-2 flex items-center gap-1"
|
||||
>
|
||||
<AlertTriangle className="w-3 h-3 text-yellow-400" />
|
||||
{hiddenPrinterCount} other printer{hiddenPrinterCount > 1 ? 's' : ''} hidden (different model) —
|
||||
<span className="underline">show all</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Show matching only toggle */}
|
||||
{assignmentMode === 'printer' && showAllPrinters && slicedForModel && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAllPrinters(false)}
|
||||
className="text-xs text-bambu-gray hover:text-white transition-colors mt-2"
|
||||
>
|
||||
<span className="underline">Show only {slicedForModel} printers</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Warning when no printer selected (only in printer mode) */}
|
||||
{assignmentMode === 'printer' && selectedCount === 0 && (
|
||||
<p className="text-xs text-orange-400 mt-1 flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
Select at least one printer
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Warning when no model selected (only in model mode) */}
|
||||
{assignmentMode === 'model' && !targetModel && (
|
||||
<p className="text-xs text-orange-400 mt-1 flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
Select a target printer model
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { X, Printer, Loader2, Calendar, Pencil, AlertCircle } from 'lucide-react';
|
||||
import { X, Printer, Loader2, Calendar, Pencil, AlertCircle, AlertTriangle } from 'lucide-react';
|
||||
import { api } from '../../api/client';
|
||||
import type { PrintQueueItemCreate, PrintQueueItemUpdate } from '../../api/client';
|
||||
import { Card, CardContent } from '../Card';
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
PrintOptions,
|
||||
ScheduleOptions,
|
||||
ScheduleType,
|
||||
AssignmentMode,
|
||||
} from './types';
|
||||
import { DEFAULT_PRINT_OPTIONS, DEFAULT_SCHEDULE_OPTIONS } from './types';
|
||||
|
||||
@@ -117,6 +118,23 @@ export function PrintModal({
|
||||
// Per-printer override configs (for multi-printer selection)
|
||||
const [perPrinterConfigs, setPerPrinterConfigs] = useState<Record<number, PerPrinterConfig>>({});
|
||||
|
||||
// Assignment mode: 'printer' (specific) or 'model' (any of model)
|
||||
const [assignmentMode, setAssignmentMode] = useState<AssignmentMode>(() => {
|
||||
// Initialize from queue item if editing with target_model
|
||||
if (mode === 'edit-queue-item' && queueItem?.target_model) {
|
||||
return 'model';
|
||||
}
|
||||
return 'printer';
|
||||
});
|
||||
|
||||
// Target model for model-based assignment
|
||||
const [targetModel, setTargetModel] = useState<string | null>(() => {
|
||||
if (mode === 'edit-queue-item' && queueItem?.target_model) {
|
||||
return queueItem.target_model;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Track initial values for clearing mappings on change (edit mode only)
|
||||
const [initialPrinterIds] = useState(() => (mode === 'edit-queue-item' && queueItem?.printer_id ? [queueItem.printer_id] : []));
|
||||
const [initialPlateId] = useState(() => (mode === 'edit-queue-item' && queueItem ? queueItem.plate_id : null));
|
||||
@@ -145,6 +163,16 @@ export function PrintModal({
|
||||
queryFn: api.getPrinters,
|
||||
});
|
||||
|
||||
// Fetch archive details to get sliced_for_model
|
||||
const { data: archiveDetails } = useQuery({
|
||||
queryKey: ['archive', archiveId],
|
||||
queryFn: () => api.getArchive(archiveId!),
|
||||
enabled: !!archiveId && !isLibraryFile,
|
||||
});
|
||||
|
||||
// Get sliced_for_model from archive or library file
|
||||
const slicedForModel = archiveDetails?.sliced_for_model || null;
|
||||
|
||||
// Fetch plates for archives
|
||||
const { data: archivePlatesData, isError: archivePlatesError } = useQuery({
|
||||
queryKey: ['archive-plates', archiveId],
|
||||
@@ -304,14 +332,20 @@ export function PrintModal({
|
||||
const handleSubmit = async (e?: React.FormEvent) => {
|
||||
e?.preventDefault();
|
||||
|
||||
// Validate printer selection
|
||||
if (selectedPrinters.length === 0) {
|
||||
// Validate printer/model selection
|
||||
if (assignmentMode === 'printer' && selectedPrinters.length === 0) {
|
||||
showToast('Please select at least one printer', 'error');
|
||||
return;
|
||||
}
|
||||
if (assignmentMode === 'model' && !targetModel) {
|
||||
showToast('Please select a target printer model', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
setSubmitProgress({ current: 0, total: selectedPrinters.length });
|
||||
// For model-based assignment, we just make one API call
|
||||
const totalCount = assignmentMode === 'model' ? 1 : selectedPrinters.length;
|
||||
setSubmitProgress({ current: 0, total: totalCount });
|
||||
|
||||
const results: { success: number; failed: number; errors: string[] } = {
|
||||
success: 0,
|
||||
@@ -332,15 +366,16 @@ export function PrintModal({
|
||||
};
|
||||
|
||||
// Common queue data for add-to-queue and edit modes
|
||||
const getQueueData = (printerId: number): PrintQueueItemCreate => ({
|
||||
printer_id: printerId,
|
||||
const getQueueData = (printerId: number | null): PrintQueueItemCreate => ({
|
||||
printer_id: assignmentMode === 'printer' ? printerId : null,
|
||||
target_model: assignmentMode === 'model' ? targetModel : null,
|
||||
// Use library_file_id for library files, archive_id for archives
|
||||
archive_id: isLibraryFile ? undefined : archiveId,
|
||||
library_file_id: isLibraryFile ? libraryFileId : undefined,
|
||||
require_previous_success: scheduleOptions.requirePreviousSuccess,
|
||||
auto_off_after: scheduleOptions.autoOffAfter,
|
||||
manual_start: scheduleOptions.scheduleType === 'manual',
|
||||
ams_mapping: getMappingForPrinter(printerId),
|
||||
ams_mapping: printerId ? getMappingForPrinter(printerId) : undefined,
|
||||
plate_id: selectedPlate,
|
||||
scheduled_time: scheduleOptions.scheduleType === 'scheduled' && scheduleOptions.scheduledTime
|
||||
? new Date(scheduleOptions.scheduledTime).toISOString()
|
||||
@@ -348,35 +383,24 @@ export function PrintModal({
|
||||
...printOptions,
|
||||
});
|
||||
|
||||
for (let i = 0; i < selectedPrinters.length; i++) {
|
||||
const printerId = selectedPrinters[i];
|
||||
setSubmitProgress({ current: i + 1, total: selectedPrinters.length });
|
||||
|
||||
// Model-based assignment: single API call
|
||||
if (assignmentMode === 'model') {
|
||||
setSubmitProgress({ current: 1, total: 1 });
|
||||
try {
|
||||
if (mode === 'reprint') {
|
||||
// Reprint mode - start print immediately
|
||||
const printerMapping = getMappingForPrinter(printerId);
|
||||
if (isLibraryFile) {
|
||||
await api.printLibraryFile(libraryFileId!, printerId, {
|
||||
ams_mapping: printerMapping,
|
||||
...printOptions,
|
||||
});
|
||||
} else {
|
||||
await api.reprintArchive(archiveId!, printerId, {
|
||||
plate_id: selectedPlate ?? undefined,
|
||||
ams_mapping: printerMapping,
|
||||
...printOptions,
|
||||
});
|
||||
}
|
||||
} else if (mode === 'edit-queue-item' && i === 0) {
|
||||
// Edit mode - update the original queue item for the first printer
|
||||
const printerMapping = getMappingForPrinter(printerId);
|
||||
// Model-based reprint not supported (need specific printer for immediate print)
|
||||
showToast('Model-based assignment only works with queue mode', 'error');
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
} else if (mode === 'edit-queue-item') {
|
||||
// Edit mode - update with target_model
|
||||
const updateData: PrintQueueItemUpdate = {
|
||||
printer_id: printerId,
|
||||
printer_id: null,
|
||||
target_model: targetModel,
|
||||
require_previous_success: scheduleOptions.requirePreviousSuccess,
|
||||
auto_off_after: scheduleOptions.autoOffAfter,
|
||||
manual_start: scheduleOptions.scheduleType === 'manual',
|
||||
ams_mapping: printerMapping,
|
||||
ams_mapping: undefined,
|
||||
plate_id: selectedPlate,
|
||||
scheduled_time: scheduleOptions.scheduleType === 'scheduled' && scheduleOptions.scheduledTime
|
||||
? new Date(scheduleOptions.scheduledTime).toISOString()
|
||||
@@ -385,14 +409,63 @@ export function PrintModal({
|
||||
};
|
||||
await updateQueueMutation.mutateAsync(updateData);
|
||||
} else {
|
||||
// Add-to-queue mode OR edit mode with additional printers
|
||||
await addToQueueMutation.mutateAsync(getQueueData(printerId));
|
||||
// Add-to-queue mode with model-based assignment
|
||||
await addToQueueMutation.mutateAsync(getQueueData(null));
|
||||
}
|
||||
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}`);
|
||||
results.errors.push((error as Error).message);
|
||||
}
|
||||
} else {
|
||||
// Printer-based assignment: loop through selected printers
|
||||
for (let i = 0; i < selectedPrinters.length; i++) {
|
||||
const printerId = selectedPrinters[i];
|
||||
setSubmitProgress({ current: i + 1, total: selectedPrinters.length });
|
||||
|
||||
try {
|
||||
if (mode === 'reprint') {
|
||||
// Reprint mode - start print immediately
|
||||
const printerMapping = getMappingForPrinter(printerId);
|
||||
if (isLibraryFile) {
|
||||
await api.printLibraryFile(libraryFileId!, printerId, {
|
||||
ams_mapping: printerMapping,
|
||||
...printOptions,
|
||||
});
|
||||
} else {
|
||||
await api.reprintArchive(archiveId!, printerId, {
|
||||
plate_id: selectedPlate ?? undefined,
|
||||
ams_mapping: printerMapping,
|
||||
...printOptions,
|
||||
});
|
||||
}
|
||||
} else if (mode === 'edit-queue-item' && i === 0) {
|
||||
// Edit mode - update the original queue item for the first printer
|
||||
const printerMapping = getMappingForPrinter(printerId);
|
||||
const updateData: PrintQueueItemUpdate = {
|
||||
printer_id: printerId,
|
||||
target_model: null,
|
||||
require_previous_success: scheduleOptions.requirePreviousSuccess,
|
||||
auto_off_after: scheduleOptions.autoOffAfter,
|
||||
manual_start: scheduleOptions.scheduleType === 'manual',
|
||||
ams_mapping: printerMapping,
|
||||
plate_id: selectedPlate,
|
||||
scheduled_time: scheduleOptions.scheduleType === 'scheduled' && scheduleOptions.scheduledTime
|
||||
? new Date(scheduleOptions.scheduledTime).toISOString()
|
||||
: null,
|
||||
...printOptions,
|
||||
};
|
||||
await updateQueueMutation.mutateAsync(updateData);
|
||||
} else {
|
||||
// Add-to-queue mode OR edit mode with additional printers
|
||||
await addToQueueMutation.mutateAsync(getQueueData(printerId));
|
||||
}
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,11 +473,15 @@ export function PrintModal({
|
||||
|
||||
// Show result toast
|
||||
if (results.failed === 0) {
|
||||
const action = mode === 'reprint' ? 'sent to' : (mode === 'edit-queue-item' ? 'updated/queued for' : 'queued for');
|
||||
if (results.success === 1) {
|
||||
showToast(mode === 'edit-queue-item' ? 'Queue item updated' : `Print ${action} printer`);
|
||||
if (assignmentMode === 'model') {
|
||||
showToast(mode === 'edit-queue-item' ? 'Queue item updated' : `Queued for any ${targetModel}`);
|
||||
} else {
|
||||
showToast(`Print ${action} ${results.success} printers`);
|
||||
const action = mode === 'reprint' ? 'sent to' : (mode === 'edit-queue-item' ? 'updated/queued for' : 'queued for');
|
||||
if (results.success === 1) {
|
||||
showToast(mode === 'edit-queue-item' ? 'Queue item updated' : `Print ${action} printer`);
|
||||
} else {
|
||||
showToast(`Print ${action} ${results.success} printers`);
|
||||
}
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ['queue'] });
|
||||
onSuccess?.();
|
||||
@@ -422,14 +499,18 @@ export function PrintModal({
|
||||
const canSubmit = useMemo(() => {
|
||||
if (isPending) return false;
|
||||
|
||||
// Need at least one selected printer
|
||||
if (selectedPrinters.length === 0) return false;
|
||||
// Need valid printer/model selection
|
||||
if (assignmentMode === 'printer' && selectedPrinters.length === 0) return false;
|
||||
if (assignmentMode === 'model' && !targetModel) return false;
|
||||
|
||||
// Model-based assignment only works in queue modes (not immediate reprint)
|
||||
if (assignmentMode === 'model' && mode === 'reprint') return false;
|
||||
|
||||
// For multi-plate archive files, need a selected plate (library files skip this)
|
||||
if (!isLibraryFile && isMultiPlate && !selectedPlate) return false;
|
||||
|
||||
return true;
|
||||
}, [selectedPrinters.length, isMultiPlate, selectedPlate, isPending, isLibraryFile]);
|
||||
}, [selectedPrinters.length, assignmentMode, targetModel, mode, isMultiPlate, selectedPlate, isPending, isLibraryFile]);
|
||||
|
||||
// Modal title and action button text based on mode
|
||||
const getModalConfig = () => {
|
||||
@@ -541,8 +622,29 @@ export function PrintModal({
|
||||
filamentReqs={effectiveFilamentReqs}
|
||||
onAutoConfigurePrinter={multiPrinterMapping.autoConfigurePrinter}
|
||||
onUpdatePrinterConfig={multiPrinterMapping.updatePrinterConfig}
|
||||
assignmentMode={mode === 'reprint' ? 'printer' : assignmentMode}
|
||||
onAssignmentModeChange={mode !== 'reprint' ? setAssignmentMode : undefined}
|
||||
targetModel={targetModel}
|
||||
onTargetModelChange={mode !== 'reprint' ? setTargetModel : undefined}
|
||||
slicedForModel={slicedForModel}
|
||||
/>
|
||||
|
||||
{/* Compatibility warning when sliced model doesn't match selected printer */}
|
||||
{slicedForModel && assignmentMode === 'printer' && selectedPrinters.length === 1 && (() => {
|
||||
const selectedPrinter = printers?.find(p => p.id === selectedPrinters[0]);
|
||||
if (selectedPrinter && selectedPrinter.model && slicedForModel !== selectedPrinter.model) {
|
||||
return (
|
||||
<div className="p-3 mb-2 bg-yellow-500/10 border border-yellow-500/30 rounded-lg flex items-center gap-2">
|
||||
<AlertTriangle className="w-4 h-4 text-yellow-400 flex-shrink-0" />
|
||||
<span className="text-sm text-yellow-400">
|
||||
File was sliced for {slicedForModel}, but printing on {selectedPrinter.model}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
|
||||
{/* Warning when archive data couldn't be loaded */}
|
||||
{archiveDataMissing && (
|
||||
<div className="flex items-start gap-2 p-3 mb-2 bg-orange-500/10 border border-orange-500/30 rounded-lg text-sm">
|
||||
@@ -565,7 +667,7 @@ export function PrintModal({
|
||||
)}
|
||||
|
||||
{/* Print options */}
|
||||
{(mode === 'reprint' || effectivePrinterCount > 0) && (
|
||||
{(mode === 'reprint' || effectivePrinterCount > 0 || (assignmentMode === 'model' && targetModel)) && (
|
||||
<PrintOptionsPanel options={printOptions} onChange={setPrintOptions} />
|
||||
)}
|
||||
|
||||
|
||||
@@ -104,6 +104,13 @@ export interface PlatesResponse {
|
||||
plates: PlateInfo[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Assignment mode for queue items.
|
||||
* - 'printer': Assign to specific printer(s)
|
||||
* - 'model': Assign to any printer of a specific model (load balancing)
|
||||
*/
|
||||
export type AssignmentMode = 'printer' | 'model';
|
||||
|
||||
/**
|
||||
* Props for the PrinterSelector component.
|
||||
*/
|
||||
@@ -115,6 +122,16 @@ export interface PrinterSelectorProps {
|
||||
allowMultiple?: boolean;
|
||||
/** Show inactive printers (for edit mode where original assignment may be inactive) */
|
||||
showInactive?: boolean;
|
||||
/** Current assignment mode */
|
||||
assignmentMode?: AssignmentMode;
|
||||
/** Handler for assignment mode change */
|
||||
onAssignmentModeChange?: (mode: AssignmentMode) => void;
|
||||
/** Selected target model (when assignmentMode is 'model') */
|
||||
targetModel?: string | null;
|
||||
/** Handler for target model change */
|
||||
onTargetModelChange?: (model: string | null) => void;
|
||||
/** Suggested model from sliced file (for pre-selection) */
|
||||
slicedForModel?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user