import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { ChevronRight, Loader2, Search, Sparkles, Trash2, Wand2 } from 'lucide-react'; import { api } from '../api/client'; import type { ProfileDefault } from '../api/client'; import { useToast } from '../contexts/ToastContext'; import { Card, CardContent, CardHeader } from './Card'; import { Button } from './Button'; import { ConfirmModal } from './ConfirmModal'; /** * Settings > Filament > Profile defaults (#1326): what new spools of each * product start with. A default is saved from a spool's Printers tab ("Save as * default"), which is also how one is changed: this card lists them, applies * one to the spools already in stock, and deletes one. There can be one per * product and colour, so each is a single line and its profiles fold out. */ export function ProfileDefaultsSettings() { const { t } = useTranslation(); const { showToast } = useToast(); const queryClient = useQueryClient(); const [deleting, setDeleting] = useState(null); const [search, setSearch] = useState(''); const [expanded, setExpanded] = useState>(new Set()); const { data: defaults = [], isLoading } = useQuery({ queryKey: ['profile-defaults'], queryFn: api.getProfileDefaults, }); const { data: printers = [] } = useQuery({ queryKey: ['printers'], queryFn: api.getPrinters }); // Applying writes to the spools of the inventory in use. const { data: spoolmanSettings } = useQuery({ queryKey: ['spoolman-settings'], queryFn: api.getSpoolmanSettings, staleTime: 5 * 60 * 1000, }); const spoolmanMode = spoolmanSettings?.spoolman_enabled === 'true' && !!spoolmanSettings?.spoolman_url; const printerName = (id: number) => printers.find((p) => p.id === id)?.name ?? `#${id}`; const applyMutation = useMutation({ mutationFn: (row: ProfileDefault) => api.applyProfileDefault(row.id, spoolmanMode), onSuccess: async ({ updated }) => { showToast(t('inventory.profileDefaults.applied', { count: updated }), 'success'); await queryClient.invalidateQueries({ queryKey: ['inventory-spools'] }); await queryClient.invalidateQueries({ queryKey: ['spoolman-inventory-spools'] }); }, onError: () => showToast(t('inventory.profileDefaults.applyFailed'), 'error'), }); // One request per default, one after the other. The order does not matter: each // spool resolves its own colour's values, and nothing a spool has is overwritten. const applyAllMutation = useMutation({ mutationFn: async (rows: ProfileDefault[]) => { let updated = 0; let failed = 0; for (const row of rows) { try { updated += (await api.applyProfileDefault(row.id, spoolmanMode)).updated; } catch { failed += 1; } } return { updated, failed }; }, onSuccess: async ({ updated, failed }) => { if (failed > 0) showToast(t('inventory.profileDefaults.applyAllFailed', { failed, updated }), 'error'); else showToast(t('inventory.profileDefaults.applied', { count: updated }), 'success'); await queryClient.invalidateQueries({ queryKey: ['inventory-spools'] }); await queryClient.invalidateQueries({ queryKey: ['spoolman-inventory-spools'] }); }, }); const applying = applyMutation.isPending || applyAllMutation.isPending; const deleteMutation = useMutation({ mutationFn: (row: ProfileDefault) => api.deleteProfileDefault(row.id), onSuccess: async () => { setDeleting(null); await queryClient.invalidateQueries({ queryKey: ['profile-defaults'] }); }, onError: () => { setDeleting(null); showToast(t('inventory.profileDefaults.deleteFailed'), 'error'); }, }); const productOf = (row: ProfileDefault) => [row.brand, row.material, row.subtype].filter((part) => part.trim()).join(' '); const colourOf = (row: ProfileDefault) => row.color_name || t('inventory.profileDefaults.allColours'); // A list that can hold one row per product and colour in stock: sorted, searchable, // one line each with the details folded away. const query = search.trim().toLowerCase(); const rows = defaults .map((row) => ({ row, product: productOf(row), colour: colourOf(row) })) .filter( ({ row, product, colour }) => !query || `${product} ${colour} ${row.slicer_filament_name || row.slicer_filament || ''}`.toLowerCase().includes(query), ) .sort((a, b) => a.product.localeCompare(b.product) || a.colour.localeCompare(b.colour)); const toggle = (id: number) => setExpanded((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); return (

{t('inventory.profileDefaults.title')} {defaults.length > 0 && ({defaults.length})}

{defaults.length > 0 && ( )}

{t('inventory.profileDefaults.description')}

{isLoading ? (
) : defaults.length === 0 ? (

{t('inventory.profileDefaults.empty')}

) : ( <>
setSearch(e.target.value)} />
    {rows.length === 0 ? (
  • {t('inventory.profileDefaults.noMatch')}
  • ) : ( rows.map(({ row, product, colour }) => { const open = expanded.has(row.id); const preset = row.slicer_filament_name || row.slicer_filament; return (
  • {open && (
      {preset && (
    • {t('inventory.filamentPreset')}: {preset}
    • )} {row.presets.map((p) => (
    • {p.printer_model} {p.nozzle_diameter ? ` ${p.nozzle_diameter} mm` : ''}: {p.slicer_filament_name || p.slicer_filament}
    • ))} {row.k_profiles.map((k) => (
    • {`${printerName(k.printer_id)} ${k.nozzle_diameter ?? '0.4'} mm: ${k.name || '-'} K=${k.k_value.toFixed(3)}`} {(k.extruder ?? 0) > 0 ? ` (${t('inventory.leftNozzle')})` : ''}
    • ))}
    )}
  • ); }) )}
)}
{deleting && ( deleteMutation.mutate(deleting)} onCancel={() => setDeleting(null)} /> )}
); }