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

[Feature] Add spool name column and filter to filament inventory (#740)

  Add a "Spool" column to the filament inventory table that displays
  the spool catalog entry name associated with each spool. The column
  is hidden by default and can be enabled via the column visibility
  menu. Also add a spool name filter dropdown next to the brand filter,
  shown when any spools have catalog entries assigned. No backend
  changes needed — catalog data is fetched and joined on the frontend.
maziggy 5 месяцев назад
Родитель
Сommit
96966cd3d2

+ 3 - 0
CHANGELOG.md

@@ -19,6 +19,9 @@ All notable changes to Bambuddy will be documented in this file.
 - **User Notification Ruff/Lint Fixes** ([#693](https://github.com/maziggy/bambuddy/pull/693)) — Fixed missing `timezone` import in email timestamp, unused lambda argument, PEP 8 blank line spacing for `mark_printer_stopped_by_user`, and SQLAlchemy forward reference in `UserEmailPreference` model.
 - **User Notification Ruff/Lint Fixes** ([#693](https://github.com/maziggy/bambuddy/pull/693)) — Fixed missing `timezone` import in email timestamp, unused lambda argument, PEP 8 blank line spacing for `mark_printer_stopped_by_user`, and SQLAlchemy forward reference in `UserEmailPreference` model.
 - **Ntfy Notifications Fail With Non-ASCII Characters** ([#742](https://github.com/maziggy/bambuddy/issues/742)) — Ntfy notifications with camera snapshots failed when the printer name or filename contained non-ASCII characters (e.g. accented letters, CJK). The `Title` and `Message` HTTP headers were passed as Python strings, causing httpx to reject them with `UnicodeEncodeError`. Fixed by encoding header values as UTF-8 bytes, which ntfy handles correctly. Test notifications were unaffected because they use a hardcoded ASCII title and no image attachment. Reported by @user.
 - **Ntfy Notifications Fail With Non-ASCII Characters** ([#742](https://github.com/maziggy/bambuddy/issues/742)) — Ntfy notifications with camera snapshots failed when the printer name or filename contained non-ASCII characters (e.g. accented letters, CJK). The `Title` and `Message` HTTP headers were passed as Python strings, causing httpx to reject them with `UnicodeEncodeError`. Fixed by encoding header values as UTF-8 bytes, which ntfy handles correctly. Test notifications were unaffected because they use a hardcoded ASCII title and no image attachment. Reported by @user.
 
 
+### Added
+- **Spool Name Column & Filter in Filament Inventory** ([#740](https://github.com/maziggy/bambuddy/issues/740)) — Added a "Spool" column to the filament inventory table that displays the spool catalog entry name (e.g. "Bambu Lab AMS Tray", "Sunlu 1kg"). Enable it via the column visibility menu. Sortable and hidden by default. Also added a spool name filter dropdown next to the brand filter for quick filtering by spool type. Requested by @DMoenning.
+
 ### Changed
 ### Changed
 
 
 ### Improved
 ### Improved

+ 1 - 0
frontend/src/i18n/locales/de.ts

@@ -2858,6 +2858,7 @@ export default {
     weightUsed: 'Verbraucht',
     weightUsed: 'Verbraucht',
     currentWeight: 'Restgewicht',
     currentWeight: 'Restgewicht',
     measuredWeight: 'Gemessenes Gewicht',
     measuredWeight: 'Gemessenes Gewicht',
+    spoolName: 'Spule',
     costPerKg: 'Kosten pro kg',
     costPerKg: 'Kosten pro kg',
     measuredWeightError: 'Das gemessene Gewicht muss zwischen {{min}}g und {{max}}g liegen.',
     measuredWeightError: 'Das gemessene Gewicht muss zwischen {{min}}g und {{max}}g liegen.',
     slicerFilament: 'Slicer-Filament',
     slicerFilament: 'Slicer-Filament',

+ 1 - 0
frontend/src/i18n/locales/en.ts

@@ -2858,6 +2858,7 @@ export default {
     weightUsed: 'Used',
     weightUsed: 'Used',
     currentWeight: 'Remaining Weight',
     currentWeight: 'Remaining Weight',
     measuredWeight: 'Measured Weight',
     measuredWeight: 'Measured Weight',
+    spoolName: 'Spool',
     costPerKg: 'Cost per kg',
     costPerKg: 'Cost per kg',
     measuredWeightError: 'Measured weight must be between {{min}}g and {{max}}g.',
     measuredWeightError: 'Measured weight must be between {{min}}g and {{max}}g.',
     slicerFilament: 'Slicer Filament',
     slicerFilament: 'Slicer Filament',

+ 1 - 0
frontend/src/i18n/locales/fr.ts

@@ -2845,6 +2845,7 @@ export default {
     weightUsed: 'Consommé',
     weightUsed: 'Consommé',
     currentWeight: 'Poids restant',
     currentWeight: 'Poids restant',
     measuredWeight: 'Poids mesuré',
     measuredWeight: 'Poids mesuré',
+    spoolName: 'Bobine',
     costPerKg: 'Coût par kg',
     costPerKg: 'Coût par kg',
     measuredWeightError: 'Le poids mesuré doit être entre {{min}}g et {{max}}g.',
     measuredWeightError: 'Le poids mesuré doit être entre {{min}}g et {{max}}g.',
     slicerFilament: 'Filament Slicer',
     slicerFilament: 'Filament Slicer',

+ 1 - 0
frontend/src/i18n/locales/it.ts

@@ -2844,6 +2844,7 @@ export default {
     weightUsed: 'Utilizzato',
     weightUsed: 'Utilizzato',
     currentWeight: 'Peso Rimanente',
     currentWeight: 'Peso Rimanente',
     measuredWeight: 'Peso Misurato',
     measuredWeight: 'Peso Misurato',
+    spoolName: 'Bobina',
     costPerKg: 'Costo per kg',
     costPerKg: 'Costo per kg',
     measuredWeightError: 'Il peso misurato deve essere compreso tra {{min}}g e {{max}}g.',
     measuredWeightError: 'Il peso misurato deve essere compreso tra {{min}}g e {{max}}g.',
     slicerFilament: 'Filamento Slicer',
     slicerFilament: 'Filamento Slicer',

+ 1 - 0
frontend/src/i18n/locales/ja.ts

@@ -2857,6 +2857,7 @@ export default {
     weightUsed: '使用量',
     weightUsed: '使用量',
     currentWeight: '残量',
     currentWeight: '残量',
     measuredWeight: '計測重量',
     measuredWeight: '計測重量',
+    spoolName: 'スプール',
     costPerKg: 'kgあたりのコスト',
     costPerKg: 'kgあたりのコスト',
     measuredWeightError: '計測重量は{{min}}gから{{max}}gの間で入力してください。',
     measuredWeightError: '計測重量は{{min}}gから{{max}}gの間で入力してください。',
     slicerFilament: 'スライサーフィラメント',
     slicerFilament: 'スライサーフィラメント',

+ 1 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -2844,6 +2844,7 @@ export default {
     weightUsed: 'Usado',
     weightUsed: 'Usado',
     currentWeight: 'Peso Restante',
     currentWeight: 'Peso Restante',
     measuredWeight: 'Peso Medido',
     measuredWeight: 'Peso Medido',
+    spoolName: 'Bobina',
     costPerKg: 'Custo por kg',
     costPerKg: 'Custo por kg',
     measuredWeightError: 'O peso medido deve estar entre {{min}}g e {{max}}g.',
     measuredWeightError: 'O peso medido deve estar entre {{min}}g e {{max}}g.',
     slicerFilament: 'Filamento do Fatiador',
     slicerFilament: 'Filamento do Fatiador',

+ 1 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -2844,6 +2844,7 @@ export default {
     weightUsed: '已使用',
     weightUsed: '已使用',
     currentWeight: '剩余重量',
     currentWeight: '剩余重量',
     measuredWeight: '称量重量',
     measuredWeight: '称量重量',
+    spoolName: '线轴',
     costPerKg: '每公斤成本',
     costPerKg: '每公斤成本',
     measuredWeightError: '称量重量必须在 {{min}}g 到 {{max}}g 之间。',
     measuredWeightError: '称量重量必须在 {{min}}g 到 {{max}}g 之间。',
     slicerFilament: '切片耗材',
     slicerFilament: '切片耗材',

+ 66 - 8
frontend/src/pages/InventoryPage.tsx

@@ -8,7 +8,7 @@ import {
   ArrowUp, ArrowDown, ArrowUpDown, Group, ChevronDown, Check, RefreshCw,
   ArrowUp, ArrowDown, ArrowUpDown, Group, ChevronDown, Check, RefreshCw,
 } from 'lucide-react';
 } from 'lucide-react';
 import { api, spoolbuddyApi } from '../api/client';
 import { api, spoolbuddyApi } from '../api/client';
-import type { InventorySpool, SpoolAssignment } from '../api/client';
+import type { InventorySpool, SpoolAssignment, SpoolCatalogEntry } from '../api/client';
 import { Button } from '../components/Button';
 import { Button } from '../components/Button';
 import { SpoolFormModal } from '../components/SpoolFormModal';
 import { SpoolFormModal } from '../components/SpoolFormModal';
 import { ConfirmModal } from '../components/ConfirmModal';
 import { ConfirmModal } from '../components/ConfirmModal';
@@ -62,6 +62,7 @@ const DEFAULT_COLUMNS: ColumnConfig[] = [
   { id: 'tag_type', label: 'Linked Tag Type', visible: false },
   { id: 'tag_type', label: 'Linked Tag Type', visible: false },
   { id: 'stock', label: 'Stock', visible: false },
   { id: 'stock', label: 'Stock', visible: false },
   { id: 'remaining', label: 'Remaining', visible: true },
   { id: 'remaining', label: 'Remaining', visible: true },
+  { id: 'spool_name', label: 'Spool', visible: false },
   { id: 'cost_per_kg', label: 'Cost/kg', visible: false },
   { id: 'cost_per_kg', label: 'Cost/kg', visible: false },
   { id: 'weight_check', label: 'Weight Check', visible: false },
   { id: 'weight_check', label: 'Weight Check', visible: false },
 ];
 ];
@@ -124,6 +125,7 @@ type CellCtx = {
   remaining: number;
   remaining: number;
   pct: number;
   pct: number;
   assignmentMap: Record<number, SpoolAssignment>;
   assignmentMap: Record<number, SpoolAssignment>;
+  catalogMap: Record<number, SpoolCatalogEntry>;
   currencySymbol: string;
   currencySymbol: string;
   dateFormat: DateFormat;
   dateFormat: DateFormat;
   t: TFn;
   t: TFn;
@@ -157,6 +159,7 @@ const columnHeaders: Record<string, (t: TFn) => string> = {
   tag_type: () => 'Linked Tag Type',
   tag_type: () => 'Linked Tag Type',
   stock: (t) => t('inventory.stock'),
   stock: (t) => t('inventory.stock'),
   remaining: (t) => t('inventory.remaining'),
   remaining: (t) => t('inventory.remaining'),
+  spool_name: (t) => t('inventory.spoolName'),
   cost_per_kg: (t) => t('inventory.costPerKg'),
   cost_per_kg: (t) => t('inventory.costPerKg'),
   weight_check: (t) => t('inventory.weightCheck'),
   weight_check: (t) => t('inventory.weightCheck'),
 };
 };
@@ -283,6 +286,10 @@ const columnCells: Record<string, (ctx: CellCtx) => ReactNode> = {
       <span className="text-xs text-bambu-gray min-w-[40px] text-right">{Math.round(remaining)}g</span>
       <span className="text-xs text-bambu-gray min-w-[40px] text-right">{Math.round(remaining)}g</span>
     </div>
     </div>
   ),
   ),
+  spool_name: ({ spool, catalogMap }) => {
+    const entry = spool.core_weight_catalog_id != null ? catalogMap[spool.core_weight_catalog_id] : undefined;
+    return <span className="text-sm text-bambu-gray">{entry?.name || '-'}</span>;
+  },
   cost_per_kg: ({ spool, currencySymbol }) => (
   cost_per_kg: ({ spool, currencySymbol }) => (
     <span className="text-sm text-bambu-gray">
     <span className="text-sm text-bambu-gray">
       {spool.cost_per_kg != null ? `${currencySymbol}${spool.cost_per_kg.toFixed(2)}` : '-'}
       {spool.cost_per_kg != null ? `${currencySymbol}${spool.cost_per_kg.toFixed(2)}` : '-'}
@@ -371,6 +378,7 @@ const columnSortValues: Record<string, (spool: InventorySpool, assignmentMap: Re
   data_origin: (s) => (s.data_origin || '').toLowerCase(),
   data_origin: (s) => (s.data_origin || '').toLowerCase(),
   tag_type: (s) => (s.tag_type || '').toLowerCase(),
   tag_type: (s) => (s.tag_type || '').toLowerCase(),
   stock: (s) => s.slicer_filament ? 1 : 0,
   stock: (s) => s.slicer_filament ? 1 : 0,
+  spool_name: (s) => s.core_weight_catalog_id ?? 0,
   cost_per_kg: (s) => s.cost_per_kg ?? 0,
   cost_per_kg: (s) => s.cost_per_kg ?? 0,
   weight_check: (s) => {
   weight_check: (s) => {
     if (s.last_scale_weight == null) return -1;
     if (s.last_scale_weight == null) return -1;
@@ -433,6 +441,7 @@ function InventoryPage() {
   const [usageFilter, setUsageFilter] = useState<UsageFilter>('all');
   const [usageFilter, setUsageFilter] = useState<UsageFilter>('all');
   const [materialFilter, setMaterialFilter] = useState('');
   const [materialFilter, setMaterialFilter] = useState('');
   const [brandFilter, setBrandFilter] = useState('');
   const [brandFilter, setBrandFilter] = useState('');
+  const [spoolFilter, setSpoolFilter] = useState('');
   const [stockFilter, setStockFilter] = useState<'all' | 'stock' | 'configured'>('all');
   const [stockFilter, setStockFilter] = useState<'all' | 'stock' | 'configured'>('all');
   const [search, setSearch] = useState('');
   const [search, setSearch] = useState('');
   const [viewMode, setViewMode] = useState<ViewMode>('table');
   const [viewMode, setViewMode] = useState<ViewMode>('table');
@@ -478,6 +487,11 @@ function InventoryPage() {
     refetchInterval: 30000,
     refetchInterval: 30000,
   });
   });
 
 
+  const { data: catalogEntries } = useQuery({
+    queryKey: ['spool-catalog'],
+    queryFn: () => api.getSpoolCatalog(),
+  });
+
   const deleteMutation = useMutation({
   const deleteMutation = useMutation({
     mutationFn: (id: number) => api.deleteSpool(id),
     mutationFn: (id: number) => api.deleteSpool(id),
     onSuccess: () => {
     onSuccess: () => {
@@ -575,6 +589,15 @@ function InventoryPage() {
     return map;
     return map;
   }, [assignments]);
   }, [assignments]);
 
 
+  // Map catalog_id -> catalog entry for spool name column
+  const catalogMap = useMemo(() => {
+    const map: Record<number, SpoolCatalogEntry> = {};
+    for (const e of catalogEntries || []) {
+      map[e.id] = e;
+    }
+    return map;
+  }, [catalogEntries]);
+
   // Top materials by weight for stat card pills
   // Top materials by weight for stat card pills
   const topMaterials = useMemo(() => {
   const topMaterials = useMemo(() => {
     if (!stats) return [];
     if (!stats) return [];
@@ -617,6 +640,12 @@ function InventoryPage() {
       filtered = filtered.filter((s) => s.brand === brandFilter);
       filtered = filtered.filter((s) => s.brand === brandFilter);
     }
     }
 
 
+    // Spool name dropdown
+    if (spoolFilter) {
+      const catalogId = Number(spoolFilter);
+      filtered = filtered.filter((s) => s.core_weight_catalog_id === catalogId);
+    }
+
     // Stock filter
     // Stock filter
     if (stockFilter === 'stock') {
     if (stockFilter === 'stock') {
       filtered = filtered.filter((s) => !s.slicer_filament);
       filtered = filtered.filter((s) => !s.slicer_filament);
@@ -638,7 +667,7 @@ function InventoryPage() {
     }
     }
 
 
     return filtered;
     return filtered;
-  }, [spools, archiveFilter, usageFilter, materialFilter, brandFilter, stockFilter, search, lowStockThreshold]);
+  }, [spools, archiveFilter, usageFilter, materialFilter, brandFilter, spoolFilter, stockFilter, search, lowStockThreshold]);
 
 
   // Reset page on filter changes
   // Reset page on filter changes
   const resetPage = () => setPageIndex(0);
   const resetPage = () => setPageIndex(0);
@@ -646,9 +675,14 @@ function InventoryPage() {
   // Unique values for filter dropdowns
   // Unique values for filter dropdowns
   const uniqueMaterials = [...new Set(spools?.map((s) => s.material) || [])].sort();
   const uniqueMaterials = [...new Set(spools?.map((s) => s.material) || [])].sort();
   const uniqueBrands = [...new Set(spools?.map((s) => s.brand).filter(Boolean) || [])].sort() as string[];
   const uniqueBrands = [...new Set(spools?.map((s) => s.brand).filter(Boolean) || [])].sort() as string[];
+  const uniqueSpoolCatalogIds = [...new Set(spools?.map((s) => s.core_weight_catalog_id).filter((id): id is number => id != null) || [])].sort((a, b) => {
+    const nameA = (catalogMap[a]?.name || '').toLowerCase();
+    const nameB = (catalogMap[b]?.name || '').toLowerCase();
+    return nameA.localeCompare(nameB);
+  });
 
 
   // Check if any filters are non-default
   // Check if any filters are non-default
-  const hasActiveFilters = archiveFilter !== 'active' || usageFilter !== 'all' || !!materialFilter || !!brandFilter || stockFilter !== 'all' || !!search;
+  const hasActiveFilters = archiveFilter !== 'active' || usageFilter !== 'all' || !!materialFilter || !!brandFilter || !!spoolFilter || stockFilter !== 'all' || !!search;
 
 
   const handleColumnConfigSave = (config: ColumnConfig[]) => {
   const handleColumnConfigSave = (config: ColumnConfig[]) => {
     setColumnConfig(config);
     setColumnConfig(config);
@@ -769,6 +803,7 @@ function InventoryPage() {
     setUsageFilter('all');
     setUsageFilter('all');
     setMaterialFilter('');
     setMaterialFilter('');
     setBrandFilter('');
     setBrandFilter('');
+    setSpoolFilter('');
     setStockFilter('all');
     setStockFilter('all');
     setSearch('');
     setSearch('');
     resetPage();
     resetPage();
@@ -1124,6 +1159,24 @@ function InventoryPage() {
           ))}
           ))}
         </select>
         </select>
 
 
+        {/* Spool name dropdown chip */}
+        {uniqueSpoolCatalogIds.length > 0 && (
+          <select
+            value={spoolFilter}
+            onChange={(e) => { setSpoolFilter(e.target.value); resetPage(); }}
+            className={`px-3 py-1.5 rounded-lg border text-xs font-medium transition-colors cursor-pointer focus:outline-none ${
+              spoolFilter
+                ? 'bg-bambu-green/20 text-bambu-green border-bambu-green/30'
+                : 'bg-transparent text-bambu-gray border-bambu-dark-tertiary hover:bg-bambu-dark-tertiary'
+            }`}
+          >
+            <option value="">{t('inventory.spoolName')}</option>
+            {uniqueSpoolCatalogIds.map((id) => (
+              <option key={id} value={id}>{catalogMap[id]?.name || `#${id}`}</option>
+            ))}
+          </select>
+        )}
+
         {/* Clear filters */}
         {/* Clear filters */}
         {hasActiveFilters && (
         {hasActiveFilters && (
           <>
           <>
@@ -1303,6 +1356,7 @@ function InventoryPage() {
                           onDelete={(id) => setConfirmAction({ type: 'delete', spoolId: id })}
                           onDelete={(id) => setConfirmAction({ type: 'delete', spoolId: id })}
                           visibleColumns={visibleColumns}
                           visibleColumns={visibleColumns}
                           assignmentMap={assignmentMap}
                           assignmentMap={assignmentMap}
+                          catalogMap={catalogMap}
                           currencySymbol={currencySymbol}
                           currencySymbol={currencySymbol}
                           dateFormat={dateFormat}
                           dateFormat={dateFormat}
                           t={t}
                           t={t}
@@ -1325,6 +1379,7 @@ function InventoryPage() {
                         onDelete={() => setConfirmAction({ type: 'delete', spoolId: spool.id })}
                         onDelete={() => setConfirmAction({ type: 'delete', spoolId: spool.id })}
                         visibleColumns={visibleColumns}
                         visibleColumns={visibleColumns}
                         assignmentMap={assignmentMap}
                         assignmentMap={assignmentMap}
+                        catalogMap={catalogMap}
                         currencySymbol={currencySymbol}
                         currencySymbol={currencySymbol}
                         dateFormat={dateFormat}
                         dateFormat={dateFormat}
                         t={t}
                         t={t}
@@ -1605,7 +1660,7 @@ function SpoolCard({
 /* Single spool row for table view */
 /* Single spool row for table view */
 function SpoolTableRow({
 function SpoolTableRow({
   spool, remaining, pct, onEdit, onRestore, onArchive, onDelete,
   spool, remaining, pct, onEdit, onRestore, onArchive, onDelete,
-  visibleColumns, assignmentMap, currencySymbol, dateFormat, t, onSyncWeight,
+  visibleColumns, assignmentMap, catalogMap, currencySymbol, dateFormat, t, onSyncWeight,
 }: {
 }: {
   spool: InventorySpool;
   spool: InventorySpool;
   remaining: number;
   remaining: number;
@@ -1616,6 +1671,7 @@ function SpoolTableRow({
   onDelete: () => void;
   onDelete: () => void;
   visibleColumns: string[];
   visibleColumns: string[];
   assignmentMap: Record<number, SpoolAssignment>;
   assignmentMap: Record<number, SpoolAssignment>;
+  catalogMap: Record<number, SpoolCatalogEntry>;
   currencySymbol: string;
   currencySymbol: string;
   dateFormat: DateFormat;
   dateFormat: DateFormat;
   t: TFn;
   t: TFn;
@@ -1630,7 +1686,7 @@ function SpoolTableRow({
     >
     >
       {visibleColumns.map((colId) => (
       {visibleColumns.map((colId) => (
         <td key={colId} className="py-3 px-4">
         <td key={colId} className="py-3 px-4">
-          {columnCells[colId]?.({ spool, remaining, pct, assignmentMap, currencySymbol, dateFormat, t, onSyncWeight })}
+          {columnCells[colId]?.({ spool, remaining, pct, assignmentMap, catalogMap, currencySymbol, dateFormat, t, onSyncWeight })}
         </td>
         </td>
       ))}
       ))}
       <td className="py-3 px-4">
       <td className="py-3 px-4">
@@ -1660,7 +1716,7 @@ function SpoolTableRow({
 function SpoolTableGroup({
 function SpoolTableGroup({
   spools, representative, remaining, pct, isExpanded, onToggle,
   spools, representative, remaining, pct, isExpanded, onToggle,
   onEdit, onArchive, onDelete,
   onEdit, onArchive, onDelete,
-  visibleColumns, assignmentMap, currencySymbol, dateFormat, t, onSyncWeight,
+  visibleColumns, assignmentMap, catalogMap, currencySymbol, dateFormat, t, onSyncWeight,
 }: {
 }: {
   spools: InventorySpool[];
   spools: InventorySpool[];
   representative: InventorySpool;
   representative: InventorySpool;
@@ -1673,6 +1729,7 @@ function SpoolTableGroup({
   onDelete: (id: number) => void;
   onDelete: (id: number) => void;
   visibleColumns: string[];
   visibleColumns: string[];
   assignmentMap: Record<number, SpoolAssignment>;
   assignmentMap: Record<number, SpoolAssignment>;
+  catalogMap: Record<number, SpoolCatalogEntry>;
   currencySymbol: string;
   currencySymbol: string;
   dateFormat: DateFormat;
   dateFormat: DateFormat;
   t: TFn;
   t: TFn;
@@ -1690,14 +1747,14 @@ function SpoolTableGroup({
             {idx === 0 ? (
             {idx === 0 ? (
               <div className="flex items-center gap-2">
               <div className="flex items-center gap-2">
                 <ChevronDown className={`w-4 h-4 text-bambu-gray transition-transform ${isExpanded ? '' : '-rotate-90'}`} />
                 <ChevronDown className={`w-4 h-4 text-bambu-gray transition-transform ${isExpanded ? '' : '-rotate-90'}`} />
-                {columnCells[colId]?.({ spool: representative, remaining, pct, assignmentMap, currencySymbol, dateFormat, t, onSyncWeight })}
+                {columnCells[colId]?.({ spool: representative, remaining, pct, assignmentMap, catalogMap, currencySymbol, dateFormat, t, onSyncWeight })}
               </div>
               </div>
             ) : colId === 'id' ? (
             ) : colId === 'id' ? (
               <span className="text-xs font-medium bg-bambu-green/20 text-bambu-green px-2 py-0.5 rounded-full">
               <span className="text-xs font-medium bg-bambu-green/20 text-bambu-green px-2 py-0.5 rounded-full">
                 {t('inventory.groupedSpools', { count: spools.length })}
                 {t('inventory.groupedSpools', { count: spools.length })}
               </span>
               </span>
             ) : (
             ) : (
-              columnCells[colId]?.({ spool: representative, remaining, pct, assignmentMap, currencySymbol, dateFormat, t, onSyncWeight })
+              columnCells[colId]?.({ spool: representative, remaining, pct, assignmentMap, catalogMap, currencySymbol, dateFormat, t, onSyncWeight })
             )}
             )}
           </td>
           </td>
         ))}
         ))}
@@ -1723,6 +1780,7 @@ function SpoolTableGroup({
             onDelete={() => onDelete(spool.id)}
             onDelete={() => onDelete(spool.id)}
             visibleColumns={visibleColumns}
             visibleColumns={visibleColumns}
             assignmentMap={assignmentMap}
             assignmentMap={assignmentMap}
+            catalogMap={catalogMap}
             currencySymbol={currencySymbol}
             currencySymbol={currencySymbol}
             dateFormat={dateFormat}
             dateFormat={dateFormat}
             t={t}
             t={t}

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-CHMyQdwG.js


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-D1Pa7vjK.js"></script>
+    <script type="module" crossorigin src="/assets/index-CHMyQdwG.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-hKLgfdEQ.css">
     <link rel="stylesheet" crossorigin href="/assets/index-hKLgfdEQ.css">
   </head>
   </head>
   <body>
   <body>

Некоторые файлы не были показаны из-за большого количества измененных файлов