ghost 5.121.0 → 5.123.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/tryghost-i18n-5.123.0.tgz +0 -0
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +14017 -14035
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-03f7aa4d.mjs → CodeEditorView-aba69ca9.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
- package/core/built/admin/assets/admin-x-settings/{index-eea9098a.mjs → index-87586072.mjs} +4 -4
- package/core/built/admin/assets/admin-x-settings/{index-b0484a21.mjs → index-b3cfde97.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{modals-85a92c66.mjs → modals-890d62ee.mjs} +1669 -1618
- package/core/built/admin/assets/{chunk.383.77dd3c8cbac5f464c6da.js → chunk.383.3dcd1102188f9d5e0467.js} +7 -7
- package/core/built/admin/assets/{chunk.524.22c1abc09947848be03d.js → chunk.524.f1c003b0bae2aa6a9805.js} +7 -7
- package/core/built/admin/assets/{chunk.582.a7cbc2ce76922c8870bc.js → chunk.582.4d677cde396354a18509.js} +9 -9
- package/core/built/admin/assets/{ghost-1c91950af9d6f9cee826820f056e0025.js → ghost-4dac0876cd7426ba504e4143b9772689.js} +141 -132
- package/core/built/admin/assets/posts/posts.js +71680 -41896
- package/core/built/admin/assets/stats/stats.js +35194 -35499
- package/core/built/admin/index.html +4 -4
- package/core/frontend/public/ghost-stats.min.js +3 -3
- package/core/frontend/src/ghost-stats/ghost-stats.js +0 -12
- package/core/server/api/endpoints/previews.js +25 -4
- package/core/server/api/endpoints/stats.js +50 -0
- package/core/server/data/migrations/versions/5.122/2025-06-03-19-32-57-change-default-for-newsletters-button-color.js +37 -0
- package/core/server/data/schema/schema.js +1 -1
- package/core/server/data/tinybird/endpoints/api_top_pages.pipe +1 -0
- package/core/server/data/tinybird/fixtures/analytics_events.ndjson +31 -31
- package/core/server/data/tinybird/pipes/mv_hits.pipe +51 -11
- package/core/server/data/tinybird/tests/api_top_pages.yaml +7 -0
- package/core/server/models/newsletter.js +1 -0
- package/core/server/services/adapter-manager/AdapterManager.js +5 -1
- package/core/server/services/comments/CommentsServiceEmails.js +8 -6
- package/core/server/services/email-service/EmailRenderer.js +80 -23
- package/core/server/services/email-service/email-templates/partials/paywall.hbs +56 -33
- package/core/server/services/email-service/email-templates/partials/styles.hbs +129 -4
- package/core/server/services/email-service/email-templates/template.hbs +1 -1
- package/core/server/services/koenig/node-renderers/button-renderer.js +1 -21
- package/core/server/services/koenig/node-renderers/call-to-action-renderer.js +10 -44
- package/core/server/services/koenig/node-renderers/header-v2-renderer.js +15 -31
- package/core/server/services/koenig/node-renderers/horizontalrule-renderer.js +43 -2
- package/core/server/services/koenig/node-renderers/product-renderer.js +0 -1
- package/core/server/services/koenig/render-partials/email-button.js +127 -21
- package/core/server/services/koenig/render-utils/stylex.js +104 -0
- package/core/server/services/stats/MembersStatsService.js +129 -5
- package/core/server/services/stats/PostsStatsService.js +281 -1
- package/core/server/services/stats/StatsService.js +23 -1
- package/core/server/services/stats/utils/tinybird.js +25 -5
- package/core/server/web/api/endpoints/admin/routes.js +2 -0
- package/package.json +13 -13
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +851 -310
- package/components/tryghost-i18n-5.121.0.tgz +0 -0
- package/core/frontend/src/utils/session-storage.js +0 -68
- /package/core/built/admin/assets/{chunk.383.77dd3c8cbac5f464c6da.js.LICENSE.txt → chunk.383.3dcd1102188f9d5e0467.js.LICENSE.txt} +0 -0
|
@@ -1,34 +1,60 @@
|
|
|
1
1
|
const clsx = require('clsx');
|
|
2
|
+
const {textColorForBackgroundColor} = require('@tryghost/color-utils');
|
|
2
3
|
const {html} = require('../render-utils/tagged-template-fns.js');
|
|
4
|
+
const stylex = require('../render-utils/stylex.js');
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
|
-
* @
|
|
6
|
-
* @
|
|
7
|
-
* @
|
|
8
|
-
* @
|
|
9
|
-
* @
|
|
10
|
-
* @
|
|
11
|
-
* @
|
|
7
|
+
* @typedef {Object} EmailButtonOptions
|
|
8
|
+
* @property {string} [url=''] - The URL the button links to
|
|
9
|
+
* @property {string} [text=''] - The text displayed on the button
|
|
10
|
+
* @property {string} [alignment] - The alignment of the button
|
|
11
|
+
* @property {string} [buttonWidth=''] - The width of the button
|
|
12
|
+
* @property {string} [color=''] - The color of the button, no color defaults to newsletter button color setting
|
|
13
|
+
* @property {'fill'|'outline'} [style='fill'] - The style of the button
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const defaultOptions = {
|
|
17
|
+
url: '',
|
|
18
|
+
text: '',
|
|
19
|
+
alignment: '',
|
|
20
|
+
buttonWidth: '',
|
|
21
|
+
color: '',
|
|
22
|
+
style: /** @type {'fill'|'outline'} */ ('fill')
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {EmailButtonOptions} buttonOptions
|
|
27
|
+
* @returns {EmailButtonOptions}
|
|
28
|
+
*/
|
|
29
|
+
function _getOptions(buttonOptions = {}) {
|
|
30
|
+
// merge with defaults
|
|
31
|
+
// but we don't want undefined values to override default values
|
|
32
|
+
return {
|
|
33
|
+
...defaultOptions,
|
|
34
|
+
...Object.fromEntries(
|
|
35
|
+
Object.entries(buttonOptions).filter(([, value]) => value !== undefined)
|
|
36
|
+
)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {EmailButtonOptions} buttonOptions
|
|
12
42
|
* @returns {string}
|
|
13
43
|
*/
|
|
14
|
-
function renderEmailButton({
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const buttonClasses = clsx(
|
|
22
|
-
'btn',
|
|
23
|
-
color === 'accent' && 'btn-accent'
|
|
24
|
-
);
|
|
44
|
+
function renderEmailButton(buttonOptions = {}) {
|
|
45
|
+
const options = _getOptions(buttonOptions);
|
|
46
|
+
const {url, text, alignment, buttonWidth} = options;
|
|
47
|
+
|
|
48
|
+
const buttonClasses = _getButtonClasses(options);
|
|
49
|
+
const buttonStyle = _getButtonStyle(options);
|
|
50
|
+
const linkStyle = _getLinkStyle(options);
|
|
25
51
|
|
|
26
52
|
return html`
|
|
27
53
|
<table class="${buttonClasses}" border="0" cellspacing="0" cellpadding="0"${alignment ? ` align="${alignment}"` : ''}>
|
|
28
54
|
<tbody>
|
|
29
55
|
<tr>
|
|
30
|
-
<td align="center"${buttonWidth ? ` width="${buttonWidth}"` : ''}>
|
|
31
|
-
<a href="${url}">${text}</a>
|
|
56
|
+
<td align="center"${buttonWidth ? ` width="${buttonWidth}"` : ''}${buttonStyle ? ` style="${buttonStyle}"` : ''}>
|
|
57
|
+
<a href="${url}"${linkStyle ? ` style="${linkStyle}"` : ''}>${text}</a>
|
|
32
58
|
</td>
|
|
33
59
|
</tr>
|
|
34
60
|
</tbody>
|
|
@@ -36,6 +62,86 @@ function renderEmailButton({
|
|
|
36
62
|
`;
|
|
37
63
|
}
|
|
38
64
|
|
|
65
|
+
/**
|
|
66
|
+
* @param {EmailButtonOptions} options
|
|
67
|
+
* @returns {boolean}
|
|
68
|
+
*/
|
|
69
|
+
function _isColoredFill({color, style}) {
|
|
70
|
+
return color && color !== 'accent' && color !== 'transparent' && style === 'fill';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {EmailButtonOptions} options
|
|
75
|
+
* @returns {boolean}
|
|
76
|
+
*/
|
|
77
|
+
function _isColoredOutline({color, style}) {
|
|
78
|
+
return color && color !== 'accent' && style === 'outline';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {EmailButtonOptions} options
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
function _getTextColor({color, style}) {
|
|
86
|
+
if (_isColoredFill({color, style})) {
|
|
87
|
+
return textColorForBackgroundColor(color).hex();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return '';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {EmailButtonOptions} options
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
function _getButtonClasses({color}) {
|
|
98
|
+
return clsx(
|
|
99
|
+
'btn',
|
|
100
|
+
color === 'accent' && 'btn-accent'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {EmailButtonOptions} options
|
|
106
|
+
* @returns {string}
|
|
107
|
+
*/
|
|
108
|
+
function _getButtonStyle({color, style}) {
|
|
109
|
+
return stylex(
|
|
110
|
+
_isColoredFill({color, style}) && {
|
|
111
|
+
backgroundColor: color
|
|
112
|
+
},
|
|
113
|
+
_isColoredOutline({color, style}) && {
|
|
114
|
+
border: `1px solid ${color}`,
|
|
115
|
+
backgroundColor: 'transparent',
|
|
116
|
+
color: `${color} !important`
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {EmailButtonOptions} options
|
|
123
|
+
* @returns {string}
|
|
124
|
+
*/
|
|
125
|
+
function _getLinkStyle({color, style}) {
|
|
126
|
+
const textColor = _getTextColor({color, style});
|
|
127
|
+
|
|
128
|
+
return stylex(
|
|
129
|
+
_isColoredFill({color, style}) && textColor && {
|
|
130
|
+
color: `${textColor} !important`
|
|
131
|
+
},
|
|
132
|
+
_isColoredOutline({color, style}) && {
|
|
133
|
+
color: `${color} !important`
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
39
138
|
module.exports = {
|
|
40
|
-
renderEmailButton
|
|
139
|
+
renderEmailButton,
|
|
140
|
+
_getOptions,
|
|
141
|
+
_isColoredFill,
|
|
142
|
+
_isColoredOutline,
|
|
143
|
+
_getTextColor,
|
|
144
|
+
_getButtonClasses,
|
|
145
|
+
_getButtonStyle,
|
|
146
|
+
_getLinkStyle
|
|
41
147
|
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a camelCase string to kebab-case
|
|
3
|
+
* @param {string} str - The string to convert
|
|
4
|
+
* @returns {string} The kebab-case string
|
|
5
|
+
*/
|
|
6
|
+
function toKebabCase(str) {
|
|
7
|
+
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Formats a CSS value, adding units where necessary
|
|
12
|
+
* @param {string} key - The CSS property name
|
|
13
|
+
* @param {*} value - The value to format
|
|
14
|
+
* @returns {string} The formatted CSS value
|
|
15
|
+
*/
|
|
16
|
+
function formatValue(key, value) {
|
|
17
|
+
if (value === null || value === undefined || value === '') {
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Properties that should get px units for numeric values
|
|
22
|
+
const needsPx = /^(width|height|top|right|bottom|left|margin|padding|fontSize|borderRadius|marginTop|marginRight|marginBottom|marginLeft|paddingTop|paddingRight|paddingBottom|paddingLeft|borderTopWidth|borderRightWidth|borderBottomWidth|borderLeftWidth)$/;
|
|
23
|
+
|
|
24
|
+
// Properties that should never get px units
|
|
25
|
+
const noPx = /^(lineHeight|opacity|zIndex|flexGrow|flexShrink|order)$/;
|
|
26
|
+
|
|
27
|
+
if (typeof value === 'number') {
|
|
28
|
+
if (needsPx.test(key)) {
|
|
29
|
+
return `${value}px`;
|
|
30
|
+
}
|
|
31
|
+
if (!noPx.test(key)) {
|
|
32
|
+
return `${value}px`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return String(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parses a CSS string into an object of style properties
|
|
41
|
+
* @param {string} cssString - The CSS string to parse
|
|
42
|
+
* @returns {Object} Object containing style properties
|
|
43
|
+
*/
|
|
44
|
+
function parseCssString(cssString) {
|
|
45
|
+
if (typeof cssString !== 'string') {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const styles = {};
|
|
50
|
+
const declarations = cssString.split(';').filter(Boolean);
|
|
51
|
+
|
|
52
|
+
declarations.forEach((declaration) => {
|
|
53
|
+
const [property, value] = declaration.split(':').map(part => part.trim());
|
|
54
|
+
if (property && value) {
|
|
55
|
+
// Convert kebab-case to camelCase for consistency
|
|
56
|
+
const camelCaseProperty = property.replace(/-([a-z])/g, g => g[1].toUpperCase());
|
|
57
|
+
styles[camelCaseProperty] = value;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return styles;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Combines multiple style objects and CSS strings into a single CSS string
|
|
66
|
+
* @param {...(Object|string)} styles - Style objects or CSS strings to combine
|
|
67
|
+
* @returns {string} Combined CSS string
|
|
68
|
+
*/
|
|
69
|
+
function stylex(...styles) {
|
|
70
|
+
const mergedStyles = {};
|
|
71
|
+
|
|
72
|
+
// Process each style argument
|
|
73
|
+
styles.forEach((style) => {
|
|
74
|
+
if (!style) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (typeof style === 'string') {
|
|
79
|
+
// Handle CSS strings
|
|
80
|
+
const parsedStyles = parseCssString(style);
|
|
81
|
+
Object.assign(mergedStyles, parsedStyles);
|
|
82
|
+
} else if (typeof style === 'object') {
|
|
83
|
+
// Handle style objects
|
|
84
|
+
Object.entries(style).forEach(([key, value]) => {
|
|
85
|
+
if (value !== null && value !== false) {
|
|
86
|
+
mergedStyles[key] = value;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Convert to CSS string with spaces after semicolons
|
|
93
|
+
const styleString = Object.entries(mergedStyles)
|
|
94
|
+
.map(([key, value]) => {
|
|
95
|
+
const formattedValue = formatValue(key, value);
|
|
96
|
+
return formattedValue ? `${toKebabCase(key)}: ${formattedValue}` : '';
|
|
97
|
+
})
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.join('; ');
|
|
100
|
+
|
|
101
|
+
return styleString ? styleString + ';' : '';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = stylex;
|
|
@@ -65,7 +65,7 @@ class MembersStatsService {
|
|
|
65
65
|
) as free_delta`))
|
|
66
66
|
.where('created_at', '>=', formattedStartDate)
|
|
67
67
|
.groupByRaw('DATE(created_at)');
|
|
68
|
-
return rows;
|
|
68
|
+
return /** @type {MemberStatusDelta[]} */ (/** @type {unknown} */ (rows));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/**
|
|
@@ -79,14 +79,138 @@ class MembersStatsService {
|
|
|
79
79
|
|
|
80
80
|
// Fetch current total amounts and start counting from there
|
|
81
81
|
const totals = await this.getCount();
|
|
82
|
-
let {paid, free, comped} = totals;
|
|
83
82
|
|
|
84
|
-
// Get today in UTC (
|
|
85
|
-
const today = moment().format('YYYY-MM-DD');
|
|
83
|
+
// Get today in UTC (consistent with frontend)
|
|
84
|
+
const today = moment.utc().format('YYYY-MM-DD');
|
|
85
|
+
|
|
86
|
+
// When startDate is provided, always return complete range
|
|
87
|
+
if (options.startDate) {
|
|
88
|
+
return this._generateCompleteRange(rows, totals, options.startDate, today);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Use original sparse logic only for default case (no startDate)
|
|
92
|
+
return this._generateSparseRange(rows, totals, today);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Generate complete date range with forward-filling for frontend
|
|
97
|
+
* @param {Array} rows - Event data from database
|
|
98
|
+
* @param {Object} totals - Current member totals
|
|
99
|
+
* @param {string|Date} startDate - Start date for range
|
|
100
|
+
* @param {string} today - Today's date in YYYY-MM-DD format
|
|
101
|
+
* @returns {Object} Complete date range data
|
|
102
|
+
*/
|
|
103
|
+
_generateCompleteRange(rows, totals, startDate, today) {
|
|
104
|
+
const startDateMoment = moment.utc(startDate).startOf('day');
|
|
105
|
+
const endDateMoment = moment.utc(today).startOf('day');
|
|
106
|
+
|
|
107
|
+
// Create a map of events by date for fast lookup
|
|
108
|
+
const eventsMap = new Map();
|
|
109
|
+
rows.forEach((row) => {
|
|
110
|
+
const date = moment(row.date).format('YYYY-MM-DD');
|
|
111
|
+
eventsMap.set(date, row);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Sort rows chronologically to calculate historical totals
|
|
115
|
+
rows.sort((a, b) => moment(a.date).valueOf() - moment(b.date).valueOf());
|
|
116
|
+
|
|
117
|
+
// Work backwards from current totals to build historical data for event dates
|
|
118
|
+
let runningPaid = totals.paid;
|
|
119
|
+
let runningFree = totals.free;
|
|
120
|
+
let runningComped = totals.comped;
|
|
86
121
|
|
|
122
|
+
const historicalTotalsMap = new Map();
|
|
123
|
+
|
|
124
|
+
// Calculate totals for each event date by working backwards
|
|
125
|
+
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
126
|
+
const row = rows[i];
|
|
127
|
+
const date = moment(row.date).format('YYYY-MM-DD');
|
|
128
|
+
|
|
129
|
+
if (date > today) {
|
|
130
|
+
continue; // Skip future dates
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Store the totals for this date
|
|
134
|
+
historicalTotalsMap.set(date, {
|
|
135
|
+
paid: Math.max(0, runningPaid),
|
|
136
|
+
free: Math.max(0, runningFree),
|
|
137
|
+
comped: Math.max(0, runningComped),
|
|
138
|
+
paid_subscribed: row.paid_subscribed,
|
|
139
|
+
paid_canceled: row.paid_canceled
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Update running totals for previous days
|
|
143
|
+
runningPaid -= row.paid_subscribed - row.paid_canceled;
|
|
144
|
+
runningFree -= row.free_delta;
|
|
145
|
+
runningComped -= row.comped_delta;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Generate complete date range from day before startDate to today (includes baseline)
|
|
149
|
+
const results = [];
|
|
150
|
+
const currentDate = moment(startDateMoment).subtract(1, 'day');
|
|
151
|
+
|
|
152
|
+
// Track the last known values for forward-filling
|
|
153
|
+
let lastKnownTotals = {
|
|
154
|
+
paid: Math.max(0, runningPaid), // Historical baseline before all events
|
|
155
|
+
free: Math.max(0, runningFree),
|
|
156
|
+
comped: Math.max(0, runningComped)
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
while (currentDate.isSameOrBefore(endDateMoment)) {
|
|
160
|
+
const dateStr = currentDate.format('YYYY-MM-DD');
|
|
161
|
+
|
|
162
|
+
if (historicalTotalsMap.has(dateStr)) {
|
|
163
|
+
// Use actual event data and update our last known totals
|
|
164
|
+
const eventData = historicalTotalsMap.get(dateStr);
|
|
165
|
+
lastKnownTotals = {
|
|
166
|
+
paid: eventData.paid,
|
|
167
|
+
free: eventData.free,
|
|
168
|
+
comped: eventData.comped
|
|
169
|
+
};
|
|
170
|
+
results.push({
|
|
171
|
+
date: dateStr,
|
|
172
|
+
paid: eventData.paid,
|
|
173
|
+
free: eventData.free,
|
|
174
|
+
comped: eventData.comped,
|
|
175
|
+
paid_subscribed: eventData.paid_subscribed,
|
|
176
|
+
paid_canceled: eventData.paid_canceled
|
|
177
|
+
});
|
|
178
|
+
} else {
|
|
179
|
+
// Forward-fill with last known totals (no events on this day)
|
|
180
|
+
results.push({
|
|
181
|
+
date: dateStr,
|
|
182
|
+
paid: lastKnownTotals.paid,
|
|
183
|
+
free: lastKnownTotals.free,
|
|
184
|
+
comped: lastKnownTotals.comped,
|
|
185
|
+
paid_subscribed: 0,
|
|
186
|
+
paid_canceled: 0
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
currentDate.add(1, 'day');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
data: results,
|
|
195
|
+
meta: {
|
|
196
|
+
totals
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Generate sparse date range (original behavior for backward compatibility)
|
|
203
|
+
* @param {Array} rows - Event data from database
|
|
204
|
+
* @param {Object} totals - Current member totals
|
|
205
|
+
* @param {string} today - Today's date in YYYY-MM-DD format
|
|
206
|
+
* @returns {Object} Sparse date range data
|
|
207
|
+
*/
|
|
208
|
+
_generateSparseRange(rows, totals, today) {
|
|
209
|
+
let {paid, free, comped} = totals;
|
|
87
210
|
const cumulativeResults = [];
|
|
88
211
|
|
|
89
|
-
rows.sort((a, b) =>
|
|
212
|
+
rows.sort((a, b) => moment(a.date).valueOf() - moment(b.date).valueOf());
|
|
213
|
+
|
|
90
214
|
// Loop in reverse order (needed to have correct sorted result)
|
|
91
215
|
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
92
216
|
const row = rows[i];
|