From 3ca91eb6d1d2885b0b13d1130bcdbfe446d4c411 Mon Sep 17 00:00:00 2001 From: Keybored <78739882+Keybored02@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:30:36 +0100 Subject: [PATCH] [Feature] Add material mismatch and insufficient filament checks (#720) [Feature] Add material mismatch and insufficient filament checks (#720) --- backend/app/api/routes/settings.py | 1 + backend/app/schemas/settings.py | 5 + frontend/src/api/client.ts | 2 + frontend/src/components/AssignSpoolModal.tsx | 180 ++++++++++++++++++- frontend/src/components/ConfirmModal.tsx | 4 +- frontend/src/components/PrintModal/index.tsx | 136 +++++++++++++- frontend/src/components/SpoolmanSettings.tsx | 1 + frontend/src/i18n/locales/de.ts | 12 ++ frontend/src/i18n/locales/en.ts | 12 ++ frontend/src/i18n/locales/fr.ts | 14 +- frontend/src/i18n/locales/it.ts | 14 +- frontend/src/i18n/locales/ja.ts | 13 +- frontend/src/i18n/locales/pt-BR.ts | 12 ++ frontend/src/i18n/locales/zh-CN.ts | 12 ++ frontend/src/pages/PrintersPage.tsx | 14 +- frontend/src/pages/SettingsPage.tsx | 27 +++ 16 files changed, 436 insertions(+), 23 deletions(-) diff --git a/backend/app/api/routes/settings.py b/backend/app/api/routes/settings.py index f07ad8d61..556d8acb7 100644 --- a/backend/app/api/routes/settings.py +++ b/backend/app/api/routes/settings.py @@ -87,6 +87,7 @@ async def get_settings( "spoolman_enabled", "spoolman_disable_weight_sync", "spoolman_report_partial_usage", + "disable_filament_warnings", "check_updates", "check_printer_firmware", "include_beta_updates", diff --git a/backend/app/schemas/settings.py b/backend/app/schemas/settings.py index c2e276d6d..e36eb706c 100644 --- a/backend/app/schemas/settings.py +++ b/backend/app/schemas/settings.py @@ -31,6 +31,10 @@ class AppSettings(BaseModel): default=True, description="Report Partial Usage for Failed Prints. When a print fails or is cancelled, report the estimated filament used up to that point based on layer progress.", ) + disable_filament_warnings: bool = Field( + default=False, + description="Disable insufficient filament warnings when printing or queueing prints", + ) # Updates check_updates: bool = Field(default=True, description="Automatically check for updates on startup") @@ -190,6 +194,7 @@ class AppSettingsUpdate(BaseModel): spoolman_sync_mode: str | None = None spoolman_disable_weight_sync: bool | None = None spoolman_report_partial_usage: bool | None = None + disable_filament_warnings: bool | None = None check_updates: bool | None = None check_printer_firmware: bool | None = None include_beta_updates: bool | None = None diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 07924aa35..2a5fb1159 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -810,6 +810,8 @@ export interface AppSettings { // Date/time format settings date_format: 'system' | 'us' | 'eu' | 'iso'; time_format: 'system' | '12h' | '24h'; + // Filament tracking + disable_filament_warnings: boolean; // Disable filament warnings (print insufficiency and assignment mismatch) // Default printer default_printer_id: number | null; // Dark mode theme settings diff --git a/frontend/src/components/AssignSpoolModal.tsx b/frontend/src/components/AssignSpoolModal.tsx index 222e84391..869c6fde4 100644 --- a/frontend/src/components/AssignSpoolModal.tsx +++ b/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 { @@ -15,6 +16,8 @@ interface AssignSpoolModalProps { trayId: number; trayInfo?: { type: string; + material?: string; + profile?: string; color: string; location: string; }; @@ -26,6 +29,15 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr const { showToast } = useToast(); const [selectedSpoolId, setSelectedSpoolId] = useState(null); const [searchFilter, setSearchFilter] = useState(''); + const [pendingAssignId, setPendingAssignId] = useState(null); + const [showMismatchConfirm, setShowMismatchConfirm] = useState(false); + const [mismatchDetails, setMismatchDetails] = useState<{ + type: 'material' | 'partial' | 'profile' | 'material_profile' | 'partial_profile'; + spoolMaterial: string; + trayMaterial: string; + spoolProfile?: string; + trayProfile?: string; + } | null>(null); const { data: spools, isLoading } = useQuery({ queryKey: ['inventory-spools'], @@ -39,6 +51,12 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr enabled: isOpen, }); + const { data: settings } = useQuery({ + queryKey: ['settings'], + queryFn: () => api.getSettings(), + enabled: isOpen, + }); + const assignMutation = useMutation({ mutationFn: (spoolId: number) => api.assignSpool({ spool_id: spoolId, printer_id: printerId, ams_id: amsId, tray_id: trayId }), @@ -53,6 +71,9 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr }); queryClient.invalidateQueries({ queryKey: ['spool-assignments'] }); showToast(t('inventory.assignSuccess'), 'success'); + setShowMismatchConfirm(false); + setPendingAssignId(null); + setMismatchDetails(null); onClose(); }, onError: (error: Error) => { @@ -60,6 +81,38 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr }, }); + // --- Material/profile mismatch logic --- + const normalizeValue = (value: string | undefined | null) => + (value ?? '').trim().toUpperCase(); + + const checkMaterialMatch = ( + spoolMaterial: string | undefined | null, + trayMaterial: string | undefined | null + ): 'exact' | 'partial' | 'none' => { + const normalizedSpool = normalizeValue(spoolMaterial); + const normalizedTray = normalizeValue(trayMaterial); + + if (!normalizedSpool || !normalizedTray) return 'none'; + if (normalizedSpool === normalizedTray) return 'exact'; + if (normalizedTray.includes(normalizedSpool) || normalizedSpool.includes(normalizedTray)) { + return 'partial'; + } + + return 'none'; + }; + + const checkProfileMatch = ( + spoolProfile: string | undefined | null, + trayProfile: string | undefined | null + ): boolean => { + const normalizedSpoolProfile = normalizeValue(spoolProfile); + const normalizedTrayProfile = normalizeValue(trayProfile); + + if (!normalizedSpoolProfile || !normalizedTrayProfile) return false; + + return normalizedSpoolProfile === normalizedTrayProfile; + }; + if (!isOpen) return null; // Filter out spools already assigned to other slots @@ -87,17 +140,63 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr }); const handleAssign = () => { - if (selectedSpoolId) { - assignMutation.mutate(selectedSpoolId); + if (!selectedSpoolId) return; + const selectedSpool = spools?.find((spool: InventorySpool) => spool.id === selectedSpoolId); + if (!selectedSpool) { + showToast(t('inventory.assignFailed'), 'error'); + return; } + + if (!settings?.disable_filament_warnings && trayInfo) { + const trayMaterial = trayInfo.material || trayInfo.type; + const materialMatchResult = checkMaterialMatch(selectedSpool.material, trayMaterial); + const spoolProfile = selectedSpool.slicer_filament_name || selectedSpool.slicer_filament; + const trayProfile = trayInfo.profile || trayInfo.type; + const profileMatches = checkProfileMatch(spoolProfile, trayProfile); + + // Always evaluate both checks; if both fail, show a combined warning. + if (materialMatchResult !== 'exact' || !profileMatches) { + let mismatchType: 'material' | 'partial' | 'profile' | 'material_profile' | 'partial_profile' = 'profile'; + + if (materialMatchResult === 'none' && !profileMatches) { + mismatchType = 'material_profile'; + } else if (materialMatchResult === 'partial' && !profileMatches) { + mismatchType = 'partial_profile'; + } else if (materialMatchResult === 'none') { + mismatchType = 'material'; + } else if (materialMatchResult === 'partial') { + mismatchType = 'partial'; + } + + setPendingAssignId(selectedSpoolId); + setMismatchDetails({ + type: mismatchType, + spoolMaterial: selectedSpool.material || '', + trayMaterial: trayMaterial || '', + spoolProfile: spoolProfile || undefined, + trayProfile: trayProfile || undefined, + }); + setShowMismatchConfirm(true); + return; + } + } + assignMutation.mutate(selectedSpoolId); + }; + + const handleConfirmMismatch = () => { + if (!pendingAssignId) return; + assignMutation.mutate(pendingAssignId); + setShowMismatchConfirm(false); + setPendingAssignId(null); }; return ( -
-
+ <> +
+
{/* Header */} @@ -222,12 +321,77 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr
+ {assignMutation.isError && (
{(assignMutation.error as Error).message}
)} +
-
+
+ + {showMismatchConfirm && trayInfo && selectedSpoolId && mismatchDetails && (() => { + let message = ''; + + if (mismatchDetails.type === 'material') { + message = t('inventory.assignMismatchMessage', { + spoolMaterial: mismatchDetails.spoolMaterial, + trayMaterial: mismatchDetails.trayMaterial, + location: trayInfo.location, + }); + } else if (mismatchDetails.type === 'partial') { + message = t('inventory.assignPartialMismatchMessage', { + spoolMaterial: mismatchDetails.spoolMaterial, + trayMaterial: mismatchDetails.trayMaterial, + location: trayInfo.location, + }); + } else if (mismatchDetails.type === 'material_profile') { + message = `${t('inventory.assignMismatchMessage', { + spoolMaterial: mismatchDetails.spoolMaterial, + trayMaterial: mismatchDetails.trayMaterial, + location: trayInfo.location, + })}\n\n${t('inventory.assignProfileMismatchMessage', { + spoolProfile: mismatchDetails.spoolProfile || t('common.unknown'), + trayProfile: mismatchDetails.trayProfile || t('common.unknown'), + location: trayInfo.location, + })}`; + } else if (mismatchDetails.type === 'partial_profile') { + message = `${t('inventory.assignPartialMismatchMessage', { + spoolMaterial: mismatchDetails.spoolMaterial, + trayMaterial: mismatchDetails.trayMaterial, + location: trayInfo.location, + })}\n\n${t('inventory.assignProfileMismatchMessage', { + spoolProfile: mismatchDetails.spoolProfile || t('common.unknown'), + trayProfile: mismatchDetails.trayProfile || t('common.unknown'), + location: trayInfo.location, + })}`; + } else if (mismatchDetails.type === 'profile') { + message = t('inventory.assignProfileMismatchMessage', { + spoolProfile: mismatchDetails.spoolProfile || t('common.unknown'), + trayProfile: mismatchDetails.trayProfile || t('common.unknown'), + location: trayInfo.location, + }); + } + + return ( + { + if (!assignMutation.isPending) { + setShowMismatchConfirm(false); + setPendingAssignId(null); + setMismatchDetails(null); + } + }} + /> + ); + })()} + ); } diff --git a/frontend/src/components/ConfirmModal.tsx b/frontend/src/components/ConfirmModal.tsx index 93da3b54e..a9f697f48 100644 --- a/frontend/src/components/ConfirmModal.tsx +++ b/frontend/src/components/ConfirmModal.tsx @@ -51,7 +51,7 @@ export function ConfirmModal({ }, warning: { icon: 'text-yellow-400', - button: 'bg-yellow-500 hover:bg-yellow-600', + button: 'bg-yellow-500 hover:bg-yellow-600 text-black', }, default: { icon: 'text-bambu-green', @@ -77,7 +77,7 @@ export function ConfirmModal({

{title}

-

{message}

+

{message}

diff --git a/frontend/src/components/PrintModal/index.tsx b/frontend/src/components/PrintModal/index.tsx index 954f87be2..5d374e6dd 100644 --- a/frontend/src/components/PrintModal/index.tsx +++ b/frontend/src/components/PrintModal/index.tsx @@ -2,18 +2,19 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { AlertCircle, AlertTriangle, Calendar, Loader2, Pencil, Printer, X } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import type { PrintQueueItemCreate, PrintQueueItemUpdate } from '../../api/client'; +import type { PrintQueueItemCreate, PrintQueueItemUpdate, SpoolAssignment } from '../../api/client'; import { api } from '../../api/client'; import { useAuth } from '../../contexts/AuthContext'; +import { Card, CardContent } from '../Card'; +import { Button } from '../Button'; +import { ConfirmModal } from '../ConfirmModal'; import { useToast } from '../../contexts/ToastContext'; -import { useFilamentMapping } from '../../hooks/useFilamentMapping'; +import { buildLoadedFilaments, useFilamentMapping } from '../../hooks/useFilamentMapping'; import { useMultiPrinterFilamentMapping, type PerPrinterConfig } from '../../hooks/useMultiPrinterFilamentMapping'; import { getColorName } from '../../utils/colors'; -import { isPlaceholderDate } from '../../utils/amsHelpers'; import { getCurrencySymbol } from '../../utils/currency'; import { toDateTimeLocalValue, parseUTCDate } from '../../utils/date'; -import { Button } from '../Button'; -import { Card, CardContent } from '../Card'; +import { getGlobalTrayId, isPlaceholderDate } from '../../utils/amsHelpers'; import { FilamentMapping } from './FilamentMapping'; import { FilamentOverride } from './FilamentOverride'; import { PlateSelector } from './PlateSelector'; @@ -56,6 +57,13 @@ export function PrintModal({ // Determine if we're printing a library file const isLibraryFile = !!libraryFileId && !archiveId; + type FilamentWarningItem = { + printerName: string; + slotLabel: string; + requiredGrams: number; + remainingGrams: number; + }; + // Multiple printer selection (used for all modes now) const [selectedPrinters, setSelectedPrinters] = useState(() => { // Initialize with the queue item's printer if editing @@ -191,6 +199,8 @@ export function PrintModal({ const [isSubmitting, setIsSubmitting] = useState(false); const [submitProgress, setSubmitProgress] = useState({ current: 0, total: 0 }); + const [filamentWarningItems, setFilamentWarningItems] = useState(null); + // Track which printers have had the "Expand custom mapping by default" setting applied // This ensures the setting only affects initial state, not preventing unchecking const [initialExpandApplied, setInitialExpandApplied] = useState>(new Set()); @@ -214,6 +224,13 @@ export function PrintModal({ queryFn: api.getPrinters, }); + const { data: spoolAssignments } = useQuery({ + queryKey: ['spool-assignments'], + queryFn: () => api.getAssignments(), + staleTime: 30 * 1000, + enabled: ((mode === 'reprint' || mode === 'add-to-queue') && assignmentMode === 'printer') || (isLibraryFile && mode === 'reprint'), + }); + // Fetch archive details to get sliced_for_model const { data: archiveDetails } = useQuery({ queryKey: ['archive', archiveId], @@ -396,6 +413,36 @@ export function PrintModal({ const isMultiPlate = platesData?.is_multi_plate ?? false; const plates = platesData?.plates ?? []; + const spoolAssignmentsByPrinter = useMemo(() => { + const map = new Map>(); + if (!spoolAssignments) return map; + spoolAssignments.forEach((assignment) => { + const isExternal = assignment.ams_id === 255; + const globalTrayId = getGlobalTrayId( + assignment.ams_id, + assignment.tray_id, + isExternal + ); + const printerMap = map.get(assignment.printer_id) ?? new Map(); + printerMap.set(globalTrayId, assignment); + map.set(assignment.printer_id, printerMap); + }); + return map; + }, [spoolAssignments]); + + const filamentWarningMessage = useMemo(() => { + if (!filamentWarningItems || filamentWarningItems.length === 0) return ''; + const lines = filamentWarningItems.map((item) => + t('printModal.insufficientFilamentLine', { + printer: item.printerName, + slot: item.slotLabel, + required: Math.round(item.requiredGrams), + remaining: Math.round(item.remainingGrams), + }) + ); + return [t('printModal.insufficientFilamentMessage'), ...lines].join('\n'); + }, [filamentWarningItems, t]); + // Add to queue mutation (single printer) const addToQueueMutation = useMutation({ mutationFn: (data: PrintQueueItemCreate) => api.addToQueue(data), @@ -415,9 +462,71 @@ export function PrintModal({ }, }); - const handleSubmit = async (e?: React.FormEvent) => { + const handleSubmit = async (e?: React.FormEvent, options?: { skipFilamentCheck?: boolean }) => { e?.preventDefault(); + if ( + !options?.skipFilamentCheck && + !settings?.disable_filament_warnings && + (mode === 'reprint' || mode === 'add-to-queue') && + assignmentMode === 'printer' + ) { + const warningItems: FilamentWarningItem[] = []; + const filamentReqs = effectiveFilamentReqs?.filaments ?? []; + + if (filamentReqs.length > 0 && spoolAssignmentsByPrinter.size > 0) { + const getRemainingWeight = (labelWeight: number, weightUsed: number) => { + if (!Number.isFinite(labelWeight) || labelWeight <= 0) return null; + if (!Number.isFinite(weightUsed) || weightUsed < 0) return null; + return Math.max(0, labelWeight - weightUsed); + }; + + for (const printerId of selectedPrinters) { + const printerMapping = selectedPrinters.length > 1 + ? multiPrinterMapping.getFinalMapping(printerId) + : amsMapping; + if (!printerMapping) continue; + + const printerStatusForWarning = selectedPrinters.length > 1 + ? multiPrinterMapping.printerResults.find((result) => result.printerId === printerId)?.status + : printerStatus; + + const loadedFilaments = buildLoadedFilaments(printerStatusForWarning); + const slotLabelByTray = new Map(loadedFilaments.map((f) => [f.globalTrayId, f.label])); + const assignments = spoolAssignmentsByPrinter.get(printerId); + const printerName = printers?.find((p) => p.id === printerId)?.name ?? `Printer ${printerId}`; + + if (!assignments) continue; + + filamentReqs.forEach((req) => { + if (!req.slot_id || req.slot_id <= 0) return; + const globalTrayId = printerMapping[req.slot_id - 1]; + if (!Number.isFinite(globalTrayId) || globalTrayId < 0) return; + + const assignment = assignments.get(globalTrayId); + const spool = assignment?.spool; + if (!spool) return; + + const remainingGrams = getRemainingWeight(spool.label_weight, spool.weight_used); + if (remainingGrams === null) return; + if (remainingGrams >= req.used_grams) return; + + warningItems.push({ + printerName, + slotLabel: slotLabelByTray.get(globalTrayId) ?? `Slot ${req.slot_id}`, + requiredGrams: req.used_grams, + remainingGrams, + }); + }); + } + } + + if (warningItems.length > 0) { + setFilamentWarningItems(warningItems); + return; + } + } + // Validate printer/model selection if (assignmentMode === 'printer' && selectedPrinters.length === 0) { showToast('Please select at least one printer', 'error'); @@ -892,6 +1001,21 @@ export function PrintModal({ + + {filamentWarningItems && filamentWarningItems.length > 0 && ( + { + setFilamentWarningItems(null); + void handleSubmit(undefined, { skipFilamentCheck: true }); + }} + onCancel={() => setFilamentWarningItems(null)} + /> + )}
); } diff --git a/frontend/src/components/SpoolmanSettings.tsx b/frontend/src/components/SpoolmanSettings.tsx index 3a7ee9b69..94aaee0b3 100644 --- a/frontend/src/components/SpoolmanSettings.tsx +++ b/frontend/src/components/SpoolmanSettings.tsx @@ -89,6 +89,7 @@ export function SpoolmanSettings() { queryClient.invalidateQueries({ queryKey: ['spoolman-settings'] }); queryClient.invalidateQueries({ queryKey: ['spoolman-status'] }); queryClient.invalidateQueries({ queryKey: ['spool-assignments'] }); + queryClient.invalidateQueries({ queryKey: ['settings'] }); showToast(t('settings.toast.settingsSaved')); }, }); diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index 40add35e8..68f30773f 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -1330,6 +1330,9 @@ export default { // Filament Tracking Mode filamentTracking: 'Filament-Verfolgung', filamentTrackingDesc: 'Wählen Sie, wie Sie Ihre Filamentspulen verfolgen möchten. Sie können das integrierte Inventar oder einen externen Spoolman-Server verwenden.', + filamentChecks: 'Filament-Prüfungen', + disableFilamentWarnings: 'Filament-Warnungen deaktivieren', + disableFilamentWarningsDesc: 'Keine Warnungen über unzureichendes Filament beim Drucken oder Einreihen anzeigen', trackingModeBuiltIn: 'Integriertes Inventar', trackingModeBuiltInDesc: 'RFID-Erkennung und Verbrauchserfassung inklusive', trackingModeSpoolmanDesc: 'Externer Filament-Management-Server', @@ -2979,6 +2982,11 @@ export default { historyCleared: 'Verbrauchshistorie gelöscht', fillSourceLabel: '(Inv)', lowStockThresholdError: 'Der Schwellenwert muss zwischen 0.1 und 99.9 liegen', + assignMismatchTitle: 'Material stimmt nicht überein', + assignMismatchMessage: 'Das ausgewählte Spulenmaterial "{{spoolMaterial}}" stimmt nicht mit dem Tray-Material "{{trayMaterial}}" für {{location}} überein. Trotzdem zuweisen?', + assignMismatchConfirm: 'Trotzdem zuweisen', + assignPartialMismatchMessage: 'Das Spulenmaterial "{{spoolMaterial}}" ist ähnlich, stimmt aber nicht genau mit "{{trayMaterial}}" in {{location}} überein. Möchten Sie fortfahren?', + assignProfileMismatchMessage: 'Das Spulenprofil "{{spoolProfile}}" stimmt nicht mit dem Fachprofil "{{trayProfile}}" in {{location}} überein. Möchten Sie fortfahren?', }, // Timelapse @@ -3050,6 +3058,10 @@ export default { originalFilament: 'Original', overrideWith: 'Ersetzen mit', resetToOriginal: 'Auf Original zurücksetzen', + insufficientFilamentTitle: 'Nicht genug Filament', + insufficientFilamentMessage: 'Einige zugewiesene Spulen haben weniger Filament als dieser Druck benötigt:', + insufficientFilamentLine: '{{printer}} - {{slot}}: benötigt {{required}}g, verbleibend {{remaining}}g', + printAnyway: 'Trotzdem drucken', forceColorMatch: 'Farbe erzwingen', }, diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index f252f3a23..fb81467b0 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -1330,6 +1330,9 @@ export default { // Filament Tracking Mode filamentTracking: 'Filament Tracking', filamentTrackingDesc: 'Choose how to track your filament spools. You can use the built-in inventory or connect an external Spoolman server.', + filamentChecks: 'Filament checks', + disableFilamentWarnings: 'Disable filament warnings', + disableFilamentWarningsDesc: 'Don\'t show warnings about insufficient filament when printing or queueing', trackingModeBuiltIn: 'Built-in Inventory', trackingModeBuiltInDesc: 'RFID auto-matching and usage tracking included', trackingModeSpoolmanDesc: 'External filament management server', @@ -2983,6 +2986,11 @@ export default { historyCleared: 'Usage history cleared', fillSourceLabel: '(Inv)', lowStockThresholdError: 'Threshold must be between 0.1 and 99.9', + assignMismatchTitle: 'Material mismatch', + assignMismatchMessage: 'The selected spool material "{{spoolMaterial}}" does not match the tray material "{{trayMaterial}}" for {{location}}. Assign anyway?', + assignMismatchConfirm: 'Assign Anyway', + assignPartialMismatchMessage: 'The spool material "{{spoolMaterial}}" is similar to but not exactly matching "{{trayMaterial}}" in {{location}}. Do you want to proceed?', + assignProfileMismatchMessage: 'The spool profile "{{spoolProfile}}" does not match the tray profile "{{trayProfile}}" in {{location}}. Do you want to proceed?', }, // Timelapse @@ -3054,6 +3062,10 @@ export default { originalFilament: 'Original', overrideWith: 'Override with', resetToOriginal: 'Reset to original', + insufficientFilamentTitle: 'Not enough filament', + insufficientFilamentMessage: 'Some assigned spools have less filament remaining than this print needs:', + insufficientFilamentLine: '{{printer}} - {{slot}}: needs {{required}}g, remaining {{remaining}}g', + printAnyway: 'Print anyway', forceColorMatch: 'Force color match', }, diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index f2a002177..0eed7132e 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -1330,6 +1330,9 @@ export default { // Filament Tracking Mode filamentTracking: 'Suivi de Filament', filamentTrackingDesc: 'Choisissez comment suivre vos bobines. Utilisez l\'inventaire intégré ou connectez un serveur Spoolman.', + filamentChecks: 'Vérifications du filament', + disableFilamentWarnings: 'Désactiver les avertissements de filament', + disableFilamentWarningsDesc: 'Ne pas afficher les avertissements de filament insuffisant lors de l\'impression ou de la mise en file d\'attente', trackingModeBuiltIn: 'Inventaire Intégré', trackingModeBuiltInDesc: 'Correspondance RFID et suivi de consommation inclus', trackingModeSpoolmanDesc: 'Serveur de gestion externe', @@ -2970,6 +2973,11 @@ export default { historyCleared: 'Historique effacé', fillSourceLabel: '(Inv)', lowStockThresholdError: 'Le seuil doit être compris entre 0.1 et 99.9', + assignMismatchTitle: 'Incompatibilité de matériau', + assignMismatchMessage: 'Le matériau de la bobine sélectionnée "{{spoolMaterial}}" ne correspond pas au matériau du plateau "{{trayMaterial}}" pour {{location}}. Assigner quand même ?', + assignMismatchConfirm: 'Assigner quand même', + assignPartialMismatchMessage: 'Le matériau de la bobine "{{spoolMaterial}}" est similaire, mais ne correspond pas exactement à "{{trayMaterial}}" dans {{location}}. Voulez-vous continuer ?', + assignProfileMismatchMessage: 'Le profil de la bobine "{{spoolProfile}}" ne correspond pas au profil du plateau "{{trayProfile}}" dans {{location}}. Voulez-vous continuer ?', }, // Timelapse @@ -3041,7 +3049,11 @@ export default { originalFilament: 'Original', overrideWith: 'Remplacer par', resetToOriginal: 'Revenir à l\'original', - forceColorMatch: 'Forcer la correspondance couleur', + insufficientFilamentTitle: 'Filament insuffisant', + insufficientFilamentMessage: 'Certaines bobines assignées ont moins de filament restant que nécessaire pour cette impression :', + insufficientFilamentLine: '{{printer}} - {{slot}} : nécessite {{required}}g, restant {{remaining}}g', + printAnyway: 'Imprimer quand même', + forceColorMatch: 'Forcer correspondance des couleurs', }, // Backup diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index 1c7aaa7e4..836cc3f87 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/frontend/src/i18n/locales/it.ts @@ -1330,6 +1330,9 @@ export default { // Filament Tracking Mode filamentTracking: 'Tracciamento filamento', filamentTrackingDesc: 'Scegli come tracciare le bobine di filamento. Puoi usare l\'inventario integrato o collegare un server Spoolman esterno.', + filamentChecks: 'Controlli filamento', + disableFilamentWarnings: 'Disabilita avvisi filamento', + disableFilamentWarningsDesc: 'Non mostrare avvisi per filamento insufficiente durante la stampa o l\'accodamento', trackingModeBuiltIn: 'Inventario integrato', trackingModeBuiltInDesc: 'Riconoscimento RFID automatico e tracciamento dell\'uso inclusi', trackingModeSpoolmanDesc: 'Server esterno per la gestione del filamento', @@ -2969,6 +2972,11 @@ export default { historyCleared: 'Cronologia utilizzo cancellata', fillSourceLabel: '(Inv)', lowStockThresholdError: 'La soglia deve essere tra 0.1 e 99.9', + assignMismatchTitle: 'Materiale non corrispondente', + assignMismatchMessage: 'Il materiale della bobina selezionata "{{spoolMaterial}}" non corrisponde al materiale del vassoio "{{trayMaterial}}" per {{location}}. Assegnare comunque?', + assignMismatchConfirm: 'Assegna comunque', + assignPartialMismatchMessage: 'Il materiale della bobina "{{spoolMaterial}}" è simile ma non corrisponde esattamente a "{{trayMaterial}}" in {{location}}. Vuoi procedere?', + assignProfileMismatchMessage: 'Il profilo della bobina "{{spoolProfile}}" non corrisponde al profilo del vassoio "{{trayProfile}}" in {{location}}. Vuoi procedere?', }, // Timelapse @@ -3040,7 +3048,11 @@ export default { originalFilament: 'Originale', overrideWith: 'Sostituisci con', resetToOriginal: 'Ripristina originale', - forceColorMatch: 'Corrispondenza colore forzata', + insufficientFilamentTitle: 'Filamento insufficiente', + insufficientFilamentMessage: 'Alcune bobine assegnate hanno meno filamento rimanente di quanto necessario per questa stampa:', + insufficientFilamentLine: '{{printer}} - {{slot}}: necessita di {{required}}g, rimanenti {{remaining}}g', + printAnyway: 'Stampa comunque', + forceColorMatch: 'Forza corrispondenza colore', }, // Backup diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index c812bcaad..4d72904b3 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -112,7 +112,6 @@ export default { left: '左', right: '右', }, - // Printers page printers: { title: 'プリンター', @@ -1330,6 +1329,9 @@ export default { // Filament Tracking Mode filamentTracking: 'フィラメント追跡', filamentTrackingDesc: 'フィラメントスプールの追跡方法を選択してください。内蔵インベントリまたは外部Spoolmanサーバーを使用できます。', + filamentChecks: 'フィラメントチェック', + disableFilamentWarnings: 'フィラメント警告を無効化', + disableFilamentWarningsDesc: '印刷またはキュー追加時にフィラメント不足の警告を表示しない', trackingModeBuiltIn: '内蔵インベントリ', trackingModeBuiltInDesc: 'RFID自動検出と使用量追跡を含む', trackingModeSpoolmanDesc: '外部フィラメント管理サーバー', @@ -2983,6 +2985,11 @@ export default { historyCleared: '使用履歴がクリアされました', fillSourceLabel: '(Inv)', lowStockThresholdError: 'しきい値は0.1から99.9の間でなければなりません', + assignMismatchTitle: '材料の不一致', + assignMismatchMessage: '選択したスプールの材料「{{spoolMaterial}}」は、{{location}} のトレイ材料「{{trayMaterial}}」と一致しません。割り当てますか?', + assignMismatchConfirm: '強制的に割り当て', + assignPartialMismatchMessage: 'スプールの材料「{{spoolMaterial}}」は「{{trayMaterial}}」に似ていますが、{{location}} と完全には一致しません。続行しますか?', + assignProfileMismatchMessage: 'スプールのプロファイル「{{spoolProfile}}」は {{location}} のトレイプロファイル「{{trayProfile}}」と一致しません。続行しますか?', }, // Timelapse @@ -3054,6 +3061,10 @@ export default { originalFilament: 'オリジナル', overrideWith: '変更先', resetToOriginal: 'オリジナルに戻す', + insufficientFilamentTitle: 'フィラメントが不足しています', + insufficientFilamentMessage: '割り当てられたスプールの一部は、この印刷に必要な量より残量が少ないです:', + insufficientFilamentLine: '{{printer}} - {{slot}}: 必要 {{required}}g、残り {{remaining}}g', + printAnyway: 'それでも印刷', forceColorMatch: 'カラーマッチを強制', }, diff --git a/frontend/src/i18n/locales/pt-BR.ts b/frontend/src/i18n/locales/pt-BR.ts index b18b2ec36..65b4326d9 100644 --- a/frontend/src/i18n/locales/pt-BR.ts +++ b/frontend/src/i18n/locales/pt-BR.ts @@ -1330,6 +1330,9 @@ export default { // Filament Tracking Mode filamentTracking: 'Rastreamento de Filamento', filamentTrackingDesc: 'Escolha como rastrear seus rolos de filamento. Você pode usar o inventário interno ou conectar a um servidor Spoolman externo.', + filamentChecks: 'Verificações de filamento', + disableFilamentWarnings: 'Desativar avisos de filamento', + disableFilamentWarningsDesc: 'Não mostrar avisos sobre filamento insuficiente ao imprimir ou adicionar à fila', trackingModeBuiltIn: 'Inventário Interno', trackingModeBuiltInDesc: 'Correspondência automática de RFID e rastreamento de uso incluídos', trackingModeSpoolmanDesc: 'Servidor de gerenciamento de filamento externo', @@ -2969,6 +2972,11 @@ export default { historyCleared: 'Histórico de uso limpo', fillSourceLabel: '(Inv)', lowStockThresholdError: 'O limite deve estar entre 0.1 e 99.9', + assignMismatchTitle: 'Incompatibilidade de material', + assignMismatchMessage: 'O material do carretel selecionado "{{spoolMaterial}}" não corresponde ao material da bandeja "{{trayMaterial}}" para {{location}}. Atribuir mesmo assim?', + assignMismatchConfirm: 'Atribuir mesmo assim', + assignPartialMismatchMessage: 'O material do carretel "{{spoolMaterial}}" é semelhante, mas não corresponde exatamente a "{{trayMaterial}}" em {{location}}. Deseja prosseguir?', + assignProfileMismatchMessage: 'O perfil do carretel "{{spoolProfile}}" não corresponde ao perfil da bandeja "{{trayProfile}}" em {{location}}. Deseja prosseguir?', }, // Timelapse @@ -3040,6 +3048,10 @@ export default { originalFilament: 'Original', overrideWith: 'Substituir por', resetToOriginal: 'Restaurar original', + insufficientFilamentTitle: 'Filamento insuficiente', + insufficientFilamentMessage: 'Alguns dos carretéis atribuídos têm menos filamento restante do que o necessário para esta impressão:', + insufficientFilamentLine: '{{printer}} - {{slot}}: necessário {{required}}g, restante {{remaining}}g', + printAnyway: 'Imprimir mesmo assim', forceColorMatch: 'Forçar correspondência de cor', }, diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index d2968034f..77172e6d2 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -1330,6 +1330,9 @@ export default { // Filament Tracking Mode filamentTracking: '耗材追踪', filamentTrackingDesc: '选择如何追踪您的耗材。您可以使用内置库存或连接外部 Spoolman 服务器。', + filamentChecks: '耗材检查', + disableFilamentWarnings: '禁用耗材警告', + disableFilamentWarningsDesc: '在打印或加入队列时不显示耗材不足警告', trackingModeBuiltIn: '内置库存', trackingModeBuiltInDesc: '包含 RFID 自动匹配和用量追踪', trackingModeSpoolmanDesc: '外部耗材管理服务器', @@ -2851,6 +2854,11 @@ export default { unassignSpool: '取消分配', assignSuccess: '耗材已分配,AMS 槽位已配置', assignFailed: '分配耗材失败', + assignMismatchTitle: '材料不匹配', + assignMismatchMessage: '所选线轴材料 "{{spoolMaterial}}" 与 {{location}} 的料槽材料 "{{trayMaterial}}" 不匹配。仍要分配吗?', + assignMismatchConfirm: '仍然分配', + assignPartialMismatchMessage: '线轴材料 "{{spoolMaterial}}" 与 {{location}} 的 "{{trayMaterial}}" 相近但不完全一致。是否继续?', + assignProfileMismatchMessage: '线轴配置 "{{spoolProfile}}" 与 {{location}} 的料槽配置 "{{trayProfile}}" 不一致。是否继续?', selectSpool: '选择要分配到此槽位的耗材', assigned: '已分配', assigning: '分配中...', @@ -3040,6 +3048,10 @@ export default { originalFilament: '原始', overrideWith: '覆盖为', resetToOriginal: '恢复为原始', + insufficientFilamentTitle: '耗材不足', + insufficientFilamentMessage: '部分已分配线轴的剩余耗材少于本次打印所需:', + insufficientFilamentLine: '{{printer}} - {{slot}}:需要 {{required}}g,剩余 {{remaining}}g', + printAnyway: '仍然打印', forceColorMatch: '强制颜色匹配', }, diff --git a/frontend/src/pages/PrintersPage.tsx b/frontend/src/pages/PrintersPage.tsx index 343c0bc74..c35ecd2b7 100644 --- a/frontend/src/pages/PrintersPage.tsx +++ b/frontend/src/pages/PrintersPage.tsx @@ -1610,7 +1610,7 @@ function PrinterCard({ printerId: number; amsId: number; trayId: number; - trayInfo: { type: string; color: string; location: string }; + trayInfo: { type: string; color: string; location: string; material?: string; profile?: string }; } | null>(null); const [configureSlotModal, setConfigureSlotModal] = useState<{ amsId: number; @@ -3330,7 +3330,9 @@ function PrinterCard({ amsId: ams.id, trayId: slotIdx, trayInfo: { - type: filamentData.profile, + type: tray?.tray_type || filamentData.profile, + material: tray?.tray_type ?? undefined, + profile: filamentData.profile, color: filamentData.colorHex || '', location: `${getAmsLabel(ams.id, ams.tray.length)} Slot ${slotIdx + 1}`, }, @@ -3644,7 +3646,9 @@ function PrinterCard({ amsId: ams.id, trayId: htSlotId, trayInfo: { - type: filamentData.profile, + type: tray?.tray_type || filamentData.profile, + material: tray?.tray_type ?? undefined, + profile: filamentData.profile, color: filamentData.colorHex || '', location: getAmsLabel(ams.id, ams.tray.length), }, @@ -3855,7 +3859,9 @@ function PrinterCard({ amsId: 255, trayId: slotTrayId, trayInfo: { - type: extFilamentData.profile, + type: extTray.tray_type || extFilamentData.profile, + material: extTray.tray_type ?? undefined, + profile: extFilamentData.profile, color: extFilamentData.colorHex || '', location: extLabel || t('printers.external'), }, diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index d8ac33ec7..d4c279a2b 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -709,6 +709,7 @@ export function SettingsPage() { settings.ams_temp_good !== localSettings.ams_temp_good || settings.ams_temp_fair !== localSettings.ams_temp_fair || settings.ams_history_retention_days !== localSettings.ams_history_retention_days || + settings.disable_filament_warnings !== localSettings.disable_filament_warnings || (settings.queue_drying_enabled ?? false) !== (localSettings.queue_drying_enabled ?? false) || (settings.queue_drying_block ?? false) !== (localSettings.queue_drying_block ?? false) || (settings.ambient_drying_enabled ?? false) !== (localSettings.ambient_drying_enabled ?? false) || @@ -779,6 +780,7 @@ export function SettingsPage() { ams_temp_good: localSettings.ams_temp_good, ams_temp_fair: localSettings.ams_temp_fair, ams_history_retention_days: localSettings.ams_history_retention_days, + disable_filament_warnings: localSettings.disable_filament_warnings, queue_drying_enabled: localSettings.queue_drying_enabled, queue_drying_block: localSettings.queue_drying_block, ambient_drying_enabled: localSettings.ambient_drying_enabled, @@ -3240,6 +3242,31 @@ export function SettingsPage() {
+ + +

{t('settings.filamentChecks')}

+
+ +
+
+

{t('settings.disableFilamentWarnings')}

+

+ {t('settings.disableFilamentWarningsDesc')} +

+
+ +
+
+
+

{t('settings.amsDisplayThresholds')}