Number of cards per list and sum of custom number field in list head.

Thanks to xet7 !

Fixes #3796
This commit is contained in:
Lauri Ojansivu 2025-12-22 21:06:44 +02:00
parent 4292302c3c
commit e569c2957e
29 changed files with 239 additions and 142 deletions

View file

@ -368,6 +368,18 @@ body.list-resizing-active * {
text-overflow: ellipsis;
word-wrap: break-word;
}
/* Sum badge shown before list title */
.list-header .list-sum-badge {
display: inline-block;
margin-right: 8px;
padding: 0;
border-radius: 0;
background: transparent;
color: #8c8c8c;
font-weight: bold;
font-size: 12px;
vertical-align: middle;
}
.list-rotated {
width: 1.3vw;
height: 35vh;
@ -750,6 +762,9 @@ body.list-resizing-active * {
grid-row: 2;
grid-column: 2;
align-self: start;
text-align: left;
padding-left: 0;
margin-left: 0;
font-size: 16px !important;
line-height: 1.2;
}
@ -964,6 +979,9 @@ body.list-resizing-active * {
grid-row: 2 !important;
grid-column: 2 !important;
align-self: start !important;
text-align: left !important;
padding-left: 0 !important;
margin-left: 0 !important;
font-size: 16px !important;
line-height: 1.2 !important;
}

View file

@ -16,11 +16,50 @@ BlazeComponent.extendComponent({
},
customFieldsSum() {
const ret = ReactiveCache.getCustomFields({
boardIds: { $in: [Session.get('currentBoard')] },
const list = Template.currentData();
if (!list) return [];
const boardId = Session.get('currentBoard');
const fields = ReactiveCache.getCustomFields({
boardIds: { $in: [boardId] },
showSumAtTopOfList: true,
});
return ret;
if (!fields || !fields.length) return [];
const cards = ReactiveCache.getCards({
listId: list._id,
archived: false,
});
const result = fields.map(field => {
let sum = 0;
if (cards && cards.length) {
cards.forEach(card => {
const cfs = (card.customFields || []);
const cf = cfs.find(f => f && f._id === field._id);
if (!cf || cf.value === null || cf.value === undefined) return;
let v = cf.value;
if (typeof v === 'string') {
// try to parse string numbers, accept comma decimal
const parsed = parseFloat(v.replace(',', '.'));
if (isNaN(parsed)) return;
v = parsed;
}
if (typeof v === 'number' && isFinite(v)) {
sum += v;
}
});
}
return {
_id: field._id,
name: field.name,
type: field.type,
settings: field.settings || {},
value: sum,
};
});
return result;
},
openForm(options) {
@ -254,6 +293,22 @@ BlazeComponent.extendComponent({
},
}).register('listBody');
// Helpers for listBody template context
Template.listBody.helpers({
formattedCurrencyCustomFieldValue(val) {
// `this` is the custom field sum object from customFieldsSum each-iteration
const field = this || {};
const code = (field.settings && field.settings.currencyCode) || 'USD';
try {
const n = typeof val === 'number' ? val : parseFloat(val);
if (!isFinite(n)) return val;
return new Intl.NumberFormat(undefined, { style: 'currency', currency: code }).format(n);
} catch (e) {
return `${code} ${val}`;
}
},
});
function toggleValueInReactiveArray(reactiveValue, value) {
const array = reactiveValue.get();
const valueIndex = array.indexOf(value);

View file

@ -26,6 +26,9 @@ template(name="listHeader")
|/#{wipLimit.value})
if showCardsCountForList cards.length
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
if hasNumberFieldsSum
|  
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
else
if collapsed
a.js-collapse(title="{{_ 'uncollapse'}}")
@ -44,6 +47,9 @@ template(name="listHeader")
unless collapsed
if showCardsCountForList cards.length
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
if hasNumberFieldsSum
|  
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
if isMiniScreen
if currentList
if isWatching

View file

@ -142,7 +142,48 @@ BlazeComponent.extendComponent({
Template.listHeader.helpers({
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
}
},
numberFieldsSum() {
const list = Template.currentData();
if (!list) return 0;
const boardId = Session.get('currentBoard');
const fields = ReactiveCache.getCustomFields({
boardIds: { $in: [boardId] },
showSumAtTopOfList: true,
type: 'number',
});
if (!fields || !fields.length) return 0;
const cards = ReactiveCache.getCards({ listId: list._id, archived: false });
let total = 0;
if (cards && cards.length) {
cards.forEach(card => {
const cfs = (card.customFields || []);
fields.forEach(field => {
const cf = cfs.find(f => f && f._id === field._id);
if (!cf || cf.value === null || cf.value === undefined) return;
let v = cf.value;
if (typeof v === 'string') {
const parsed = parseFloat(v.replace(',', '.'));
if (isNaN(parsed)) return;
v = parsed;
}
if (typeof v === 'number' && isFinite(v)) {
total += v;
}
});
});
}
return total;
},
hasNumberFieldsSum() {
const boardId = Session.get('currentBoard');
const fields = ReactiveCache.getCustomFields({
boardIds: { $in: [boardId] },
showSumAtTopOfList: true,
type: 'number',
});
return !!(fields && fields.length);
},
});
Template.listActionPopup.helpers({

View file

@ -3,6 +3,6 @@ template(name="minilist")
class="minicard-{{colorClass}}")
.minicard-title
.handle
.fa.fa-arrows
span.drag-handle(title="{{_ 'dragList'}}") ↕️
+viewer
= title