|
@@ -25,6 +25,7 @@ import { getCurrencySymbol } from '../utils/currency';
|
|
|
import { formatDateInput, parseUTCDate, type DateFormat } from '../utils/date';
|
|
import { formatDateInput, parseUTCDate, type DateFormat } from '../utils/date';
|
|
|
import { formatSlotLabel } from '../utils/amsHelpers';
|
|
import { formatSlotLabel } from '../utils/amsHelpers';
|
|
|
import { filterSpoolsByQuery } from '../utils/inventorySearch';
|
|
import { filterSpoolsByQuery } from '../utils/inventorySearch';
|
|
|
|
|
+import { aggregateGroupSpool } from '../utils/inventoryGrouping';
|
|
|
|
|
|
|
|
type ArchiveFilter = 'active' | 'archived';
|
|
type ArchiveFilter = 'active' | 'archived';
|
|
|
type UsageFilter = 'all' | 'used' | 'new' | 'lowstock';
|
|
type UsageFilter = 'all' | 'used' | 'new' | 'lowstock';
|
|
@@ -1584,6 +1585,12 @@ function InventoryPage({ spoolmanMode = false, spoolmanModeReady = true }: { spo
|
|
|
{pagedItems.map((item) => {
|
|
{pagedItems.map((item) => {
|
|
|
if (item.type === 'group') {
|
|
if (item.type === 'group') {
|
|
|
const { key, spools: groupSpools, representative: rep } = item;
|
|
const { key, spools: groupSpools, representative: rep } = item;
|
|
|
|
|
+ // Total remaining filament across the group (#1368) — the
|
|
|
|
|
+ // headline number for the collapsed card, vs one member's.
|
|
|
|
|
+ const groupRemaining = groupSpools.reduce(
|
|
|
|
|
+ (sum, s) => sum + Math.max(0, s.label_weight - s.weight_used),
|
|
|
|
|
+ 0,
|
|
|
|
|
+ );
|
|
|
const groupBannerStyle = buildFilamentBackground({
|
|
const groupBannerStyle = buildFilamentBackground({
|
|
|
rgba: rep.rgba,
|
|
rgba: rep.rgba,
|
|
|
extraColors: rep.extra_colors,
|
|
extraColors: rep.extra_colors,
|
|
@@ -1613,7 +1620,9 @@ function InventoryPage({ spoolmanMode = false, spoolmanModeReady = true }: { spo
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex items-center gap-2">
|
|
|
- <span className="text-sm text-bambu-gray">{formatWeight(rep.label_weight)}</span>
|
|
|
|
|
|
|
+ <span className="text-sm text-bambu-gray" title={t('inventory.remaining')}>
|
|
|
|
|
+ {formatWeight(groupRemaining)}
|
|
|
|
|
+ </span>
|
|
|
<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: groupSpools.length })}
|
|
{t('inventory.groupedSpools', { count: groupSpools.length })}
|
|
|
</span>
|
|
</span>
|
|
@@ -1717,15 +1726,18 @@ function InventoryPage({ spoolmanMode = false, spoolmanModeReady = true }: { spo
|
|
|
<tbody>
|
|
<tbody>
|
|
|
{pagedItems.map((item) => {
|
|
{pagedItems.map((item) => {
|
|
|
if (item.type === 'group') {
|
|
if (item.type === 'group') {
|
|
|
- const { key, spools: groupSpools, representative: rep } = item;
|
|
|
|
|
|
|
+ const { key, spools: groupSpools } = item;
|
|
|
const isExpanded = expandedGroups.has(key);
|
|
const isExpanded = expandedGroups.has(key);
|
|
|
- const remaining = Math.max(0, rep.label_weight - rep.weight_used);
|
|
|
|
|
- const pct = rep.label_weight > 0 ? (remaining / rep.label_weight) * 100 : 0;
|
|
|
|
|
|
|
+ // Header row shows group totals (#1368): an aggregate
|
|
|
|
|
+ // spool plus remaining / pct summed across all members.
|
|
|
|
|
+ const headerSpool = aggregateGroupSpool(groupSpools);
|
|
|
|
|
+ const remaining = Math.max(0, headerSpool.label_weight - headerSpool.weight_used);
|
|
|
|
|
+ const pct = headerSpool.label_weight > 0 ? (remaining / headerSpool.label_weight) * 100 : 0;
|
|
|
return (
|
|
return (
|
|
|
<SpoolTableGroup
|
|
<SpoolTableGroup
|
|
|
key={`group-${key}`}
|
|
key={`group-${key}`}
|
|
|
spools={groupSpools}
|
|
spools={groupSpools}
|
|
|
- representative={rep}
|
|
|
|
|
|
|
+ headerSpool={headerSpool}
|
|
|
remaining={remaining}
|
|
remaining={remaining}
|
|
|
pct={pct}
|
|
pct={pct}
|
|
|
isExpanded={isExpanded}
|
|
isExpanded={isExpanded}
|
|
@@ -2180,12 +2192,14 @@ function SpoolTableRow({
|
|
|
|
|
|
|
|
/* Grouped spool rows for table view */
|
|
/* Grouped spool rows for table view */
|
|
|
function SpoolTableGroup({
|
|
function SpoolTableGroup({
|
|
|
- spools, representative, remaining, pct, isExpanded, onToggle,
|
|
|
|
|
|
|
+ spools, headerSpool, remaining, pct, isExpanded, onToggle,
|
|
|
onEdit, onCopy, onArchive, onDelete, onPrintLabel, onResetUsage,
|
|
onEdit, onCopy, onArchive, onDelete, onPrintLabel, onResetUsage,
|
|
|
visibleColumns, assignmentMap, catalogMap, currencySymbol, dateFormat, t, onSyncWeight,
|
|
visibleColumns, assignmentMap, catalogMap, currencySymbol, dateFormat, t, onSyncWeight,
|
|
|
}: {
|
|
}: {
|
|
|
spools: InventorySpool[];
|
|
spools: InventorySpool[];
|
|
|
- representative: InventorySpool;
|
|
|
|
|
|
|
+ // Aggregate of all members (summed quantities, shared identity) — rendered
|
|
|
|
|
+ // in the collapsed header row so it shows group totals (#1368).
|
|
|
|
|
+ headerSpool: InventorySpool;
|
|
|
remaining: number;
|
|
remaining: number;
|
|
|
pct: number;
|
|
pct: number;
|
|
|
isExpanded: boolean;
|
|
isExpanded: boolean;
|
|
@@ -2216,14 +2230,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, catalogMap, currencySymbol, dateFormat, t, onSyncWeight })}
|
|
|
|
|
|
|
+ {columnCells[colId]?.({ spool: headerSpool, 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, catalogMap, currencySymbol, dateFormat, t, onSyncWeight })
|
|
|
|
|
|
|
+ columnCells[colId]?.({ spool: headerSpool, remaining, pct, assignmentMap, catalogMap, currencySymbol, dateFormat, t, onSyncWeight })
|
|
|
)}
|
|
)}
|
|
|
</td>
|
|
</td>
|
|
|
))}
|
|
))}
|