mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 03:54:17 +02:00
Add spool inventory: AMS slot assignment, usage tracking, and remaining weight editing
Built-in spool inventory with AMS slot assignment for non-BL spools, automatic filament usage tracking via 3MF estimates, and remaining weight editing in the spool form. Inventory features: - AMS slot assignment: assign/unassign manual spools to AMS tray slots - Filter out Bambu Lab spools (RFID-managed) from assignment modal - Filter out already-assigned spools (one spool per slot) - Auto-unlink manual assignments when a BL spool is inserted - Hide assign/unassign UI on BL-occupied slots - Case-insensitive fingerprint matching for auto-unlink logic Usage tracking: - 3MF-based filament consumption tracking for non-BL spools - Extract per-filament used_g from archived 3MF slice_info - Map 3MF slot_id to AMS (ams_id, tray_id) positions - Scale estimates by print progress for failed/aborted prints - Skip BL spools (tracked via AMS remain% delta, Path 1) - Prevent double-counting with handled_trays set Other: - Add remaining weight field to spool edit form - Fix hardcoded "Empty" string in AssignSpoolModal (now i18n) - Add backend unit tests for usage_tracker (11 tests) - Add frontend tests for AssignSpoolModal filters (5 tests) - Update CHANGELOG, README, wiki docs, and website
This commit is contained in:
@@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { X, Loader2, Save, Beaker, Palette } from 'lucide-react';
|
||||
import { api } from '../api/client';
|
||||
import type { InventorySpool, SlicerSetting, SpoolCatalogEntry } from '../api/client';
|
||||
import type { InventorySpool, SlicerSetting, SpoolCatalogEntry, LocalPreset } from '../api/client';
|
||||
import { Button } from './Button';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import type { SpoolFormData, PrinterWithCalibrations, ColorPreset } from './spool-form/types';
|
||||
@@ -45,6 +45,12 @@ export function SpoolFormModal({ isOpen, onClose, spool, printersWithCalibration
|
||||
// Spool catalog
|
||||
const [spoolCatalog, setSpoolCatalog] = useState<SpoolCatalogEntry[]>([]);
|
||||
|
||||
// Local presets (OrcaSlicer imports)
|
||||
const [localPresets, setLocalPresets] = useState<LocalPreset[]>([]);
|
||||
|
||||
// Color catalog
|
||||
const [colorCatalog, setColorCatalog] = useState<{ manufacturer: string; color_name: string; hex_color: string; material: string | null }[]>([]);
|
||||
|
||||
// Color state
|
||||
const [recentColors, setRecentColors] = useState<ColorPreset[]>([]);
|
||||
|
||||
@@ -89,6 +95,8 @@ export function SpoolFormModal({ isOpen, onClose, spool, printersWithCalibration
|
||||
};
|
||||
fetchData();
|
||||
api.getSpoolCatalog().then(setSpoolCatalog).catch(console.error);
|
||||
api.getColorCatalog().then(setColorCatalog).catch(console.error);
|
||||
api.getLocalPresets().then(r => setLocalPresets(r.filament)).catch(console.error);
|
||||
|
||||
// Fetch printer calibrations if not provided via props
|
||||
if (printersWithCalibrations.length === 0) {
|
||||
@@ -130,18 +138,18 @@ export function SpoolFormModal({ isOpen, onClose, spool, printersWithCalibration
|
||||
})();
|
||||
}
|
||||
}
|
||||
}, [isOpen]);
|
||||
}, [isOpen, printersWithCalibrations.length]);
|
||||
|
||||
// Build filament options from cloud presets
|
||||
// Build filament options: cloud → local → fallback
|
||||
const filamentOptions = useMemo(
|
||||
() => buildFilamentOptions(cloudPresets, new Set()),
|
||||
[cloudPresets],
|
||||
() => buildFilamentOptions(cloudPresets, new Set(), localPresets),
|
||||
[cloudPresets, localPresets],
|
||||
);
|
||||
|
||||
// Extract brands from presets
|
||||
const availableBrands = useMemo(
|
||||
() => extractBrandsFromPresets(cloudPresets),
|
||||
[cloudPresets],
|
||||
() => extractBrandsFromPresets(cloudPresets, localPresets),
|
||||
[cloudPresets, localPresets],
|
||||
);
|
||||
|
||||
// Find selected preset option
|
||||
@@ -162,6 +170,7 @@ export function SpoolFormModal({ isOpen, onClose, spool, printersWithCalibration
|
||||
rgba: spool.rgba || '808080FF',
|
||||
label_weight: spool.label_weight || 1000,
|
||||
core_weight: spool.core_weight || 250,
|
||||
weight_used: spool.weight_used || 0,
|
||||
slicer_filament: spool.slicer_filament || '',
|
||||
note: spool.note || '',
|
||||
});
|
||||
@@ -214,11 +223,11 @@ export function SpoolFormModal({ isOpen, onClose, spool, printersWithCalibration
|
||||
mutationFn: (data: Record<string, unknown>) =>
|
||||
api.createSpool(data as Parameters<typeof api.createSpool>[0]),
|
||||
onSuccess: async (newSpool) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['inventory-spools'] });
|
||||
// Save K-profiles if any selected
|
||||
if (selectedProfiles.size > 0 && newSpool?.id) {
|
||||
await saveKProfiles(newSpool.id);
|
||||
}
|
||||
await queryClient.invalidateQueries({ queryKey: ['inventory-spools'] });
|
||||
showToast(t('inventory.spoolCreated'), 'success');
|
||||
onClose();
|
||||
},
|
||||
@@ -231,11 +240,11 @@ export function SpoolFormModal({ isOpen, onClose, spool, printersWithCalibration
|
||||
mutationFn: (data: Record<string, unknown>) =>
|
||||
api.updateSpool(spool!.id, data as Parameters<typeof api.updateSpool>[1]),
|
||||
onSuccess: async () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['inventory-spools'] });
|
||||
// Save K-profiles
|
||||
if (spool?.id) {
|
||||
await saveKProfiles(spool.id);
|
||||
}
|
||||
await queryClient.invalidateQueries({ queryKey: ['inventory-spools'] });
|
||||
showToast(t('inventory.spoolUpdated'), 'success');
|
||||
onClose();
|
||||
},
|
||||
@@ -290,6 +299,16 @@ export function SpoolFormModal({ isOpen, onClose, spool, printersWithCalibration
|
||||
}
|
||||
};
|
||||
|
||||
// Close on Escape key
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = () => {
|
||||
@@ -314,7 +333,7 @@ export function SpoolFormModal({ isOpen, onClose, spool, printersWithCalibration
|
||||
rgba: formData.rgba || null,
|
||||
label_weight: formData.label_weight,
|
||||
core_weight: formData.core_weight,
|
||||
weight_used: spool?.weight_used ?? 0,
|
||||
weight_used: formData.weight_used,
|
||||
slicer_filament: formData.slicer_filament || null,
|
||||
slicer_filament_name: presetName,
|
||||
nozzle_temp_min: null,
|
||||
@@ -421,6 +440,7 @@ export function SpoolFormModal({ isOpen, onClose, spool, printersWithCalibration
|
||||
updateField={updateField}
|
||||
recentColors={recentColors}
|
||||
onColorUsed={handleColorUsed}
|
||||
catalogColors={colorCatalog}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user