Add cost tracking for spools and usage history

This commit is contained in:
Matteo Parenti
2026-02-19 14:13:18 +01:00
parent c856badb84
commit 04ffca204f
24 changed files with 958 additions and 3 deletions
@@ -1,9 +1,10 @@
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { Circle, Check, AlertTriangle, RefreshCw, ChevronDown, ChevronUp } from 'lucide-react';
import { api } from '../../api/client';
import { useFilamentMapping } from '../../hooks/useFilamentMapping';
import { getGlobalTrayId } from '../../utils/amsHelpers';
import { getColorName } from '../../utils/colors';
import type { FilamentMappingProps } from './types';
@@ -16,6 +17,8 @@ export function FilamentMapping({
filamentReqs,
manualMappings,
onManualMappingChange,
currencySymbol,
defaultCostPerKg,
defaultExpanded = false,
}: FilamentMappingProps & { defaultExpanded?: boolean }) {
const { t } = useTranslation();
@@ -30,9 +33,41 @@ export function FilamentMapping({
enabled: !!printerId,
});
const { data: assignments } = useQuery({
queryKey: ['spool-assignments', printerId],
queryFn: () => api.getAssignments(printerId),
enabled: !!printerId,
});
const { loadedFilaments, filamentComparison, hasTypeMismatch, hasColorMismatch } =
useFilamentMapping(filamentReqs, printerStatus, manualMappings);
const trayCostMap = useMemo(() => {
const map = new Map<number, number | null>();
for (const assignment of assignments || []) {
const isExternal = assignment.ams_id === 255;
const globalTrayId = isExternal
? 254 + assignment.tray_id
: getGlobalTrayId(assignment.ams_id, assignment.tray_id, false);
map.set(globalTrayId, assignment.spool?.cost_per_kg ?? null);
}
return map;
}, [assignments]);
const totalCost = useMemo(() => {
let total = 0;
for (const item of filamentComparison) {
const trayId = item.loaded?.globalTrayId;
if (trayId == null) continue;
const assignedCost = trayCostMap.get(trayId) ?? null;
const costPerKg = assignedCost ?? defaultCostPerKg;
if (costPerKg > 0) {
total += (item.used_grams / 1000) * costPerKg;
}
}
return total;
}, [filamentComparison, trayCostMap, defaultCostPerKg]);
const hasFilamentReqs = filamentReqs?.filaments && filamentReqs.filaments.length > 0;
const isDualNozzle = filamentReqs?.filaments?.some((f) => f.nozzle_id != null) ?? false;
@@ -90,7 +125,7 @@ export function FilamentMapping({
className="flex items-center gap-2 text-sm text-bambu-gray hover:text-white transition-colors w-full"
>
<Circle className="w-4 h-4" fill={statusColor} stroke="none" />
<span>Filament Mapping</span>
<span>{t('printModal.filamentMapping')}</span>
{hasTypeMismatch ? (
<span className="text-xs text-orange-400">(Type not found)</span>
) : hasColorMismatch ? (
@@ -181,6 +216,9 @@ export function FilamentMapping({
)}
</div>
))}
<div className="text-xs text-bambu-gray">
{t('printModal.totalCost')} <span className="text-white">{currencySymbol}{totalCost.toFixed(2)}</span>
</div>
{hasTypeMismatch && (
<p className="text-xs text-orange-400 mt-2">Required filament type not found in printer.</p>
)}
@@ -10,6 +10,7 @@ import { useToast } from '../../contexts/ToastContext';
import { useFilamentMapping } from '../../hooks/useFilamentMapping';
import { useMultiPrinterFilamentMapping, type PerPrinterConfig } from '../../hooks/useMultiPrinterFilamentMapping';
import { isPlaceholderDate } from '../../utils/amsHelpers';
import { getCurrencySymbol } from '../../utils/currency';
import { toDateTimeLocalValue } from '../../utils/date';
import { PrinterSelector } from './PrinterSelector';
import { PlateSelector } from './PlateSelector';
@@ -170,6 +171,9 @@ export function PrintModal({
queryFn: api.getSettings,
});
const currencySymbol = getCurrencySymbol(settings?.currency || 'USD');
const defaultCostPerKg = settings?.default_filament_cost ?? 0;
const { data: printers, isLoading: loadingPrinters } = useQuery({
queryKey: ['printers'],
queryFn: api.getPrinters,
@@ -687,6 +691,8 @@ export function PrintModal({
manualMappings={manualMappings}
onManualMappingChange={setManualMappings}
defaultExpanded={settings?.per_printer_mapping_expanded ?? false}
currencySymbol={currencySymbol}
defaultCostPerKg={defaultCostPerKg}
/>
)}
@@ -171,6 +171,8 @@ export interface FilamentMappingProps {
filamentReqs: FilamentReqsData | undefined;
manualMappings: Record<number, number>;
onManualMappingChange: (mappings: Record<number, number>) => void;
currencySymbol: string;
defaultCostPerKg: number;
}
/**