Adds GET /financial-reports/admin/analytics returning daily/weekly income trends (cash received vs. accrued session income), a multi-month received/payout/expense/profit breakdown, a per-class income-per-session leaderboard, payment-status distribution, and a due-date driven cash-flow forecast (overdue + upcoming installments) to power richer charts on the dashboard's financial reports page.
112 lines
3.5 KiB
JavaScript
112 lines
3.5 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Time-bucketing helpers for financial analytics (daily / weekly / monthly /
|
|
* forward-looking forecast windows). Weeks follow the Iranian convention of
|
|
* starting on Saturday, ending on Friday.
|
|
*/
|
|
|
|
const startOfDay = (date) => {
|
|
const d = new Date(date);
|
|
d.setHours(0, 0, 0, 0);
|
|
return d;
|
|
};
|
|
|
|
const endOfDay = (date) => {
|
|
const d = new Date(date);
|
|
d.setHours(23, 59, 59, 999);
|
|
return d;
|
|
};
|
|
|
|
const addDays = (date, days) => {
|
|
const d = new Date(date);
|
|
d.setDate(d.getDate() + days);
|
|
return d;
|
|
};
|
|
|
|
const addMonths = (date, months) => {
|
|
const d = new Date(date);
|
|
d.setMonth(d.getMonth() + months);
|
|
return d;
|
|
};
|
|
|
|
/** JS getDay(): Sun=0..Sat=6. Iranian week starts Saturday, so Sat -> offset 0, Sun -> 1, ... Fri -> 6. */
|
|
const getWeekStart = (date) => {
|
|
const d = startOfDay(date);
|
|
const offset = (d.getDay() + 1) % 7;
|
|
return addDays(d, -offset);
|
|
};
|
|
|
|
/** Builds `days` daily buckets ending on (and including) `endDate`, oldest first. */
|
|
const buildDailyBuckets = (days, endDate = new Date()) => {
|
|
const end = startOfDay(endDate);
|
|
const buckets = [];
|
|
for (let i = days - 1; i >= 0; i--) {
|
|
const dayStart = addDays(end, -i);
|
|
buckets.push({ start: dayStart, end: endOfDay(dayStart), key: dayStart.toISOString().slice(0, 10) });
|
|
}
|
|
return buckets;
|
|
};
|
|
|
|
/** Builds `weeks` weekly buckets (Sat-Fri) ending on the week containing `endDate`, oldest first. */
|
|
const buildWeeklyBuckets = (weeks, endDate = new Date()) => {
|
|
const currentWeekStart = getWeekStart(endDate);
|
|
const buckets = [];
|
|
for (let i = weeks - 1; i >= 0; i--) {
|
|
const weekStart = addDays(currentWeekStart, -7 * i);
|
|
buckets.push({ start: weekStart, end: endOfDay(addDays(weekStart, 6)), key: weekStart.toISOString().slice(0, 10) });
|
|
}
|
|
return buckets;
|
|
};
|
|
|
|
/** Builds `weeks` forward-looking weekly buckets (Sat-Fri) starting from the week containing `startDate`. */
|
|
const buildForecastWeeklyBuckets = (weeks, startDate = new Date()) => {
|
|
const currentWeekStart = getWeekStart(startDate);
|
|
const buckets = [];
|
|
for (let i = 0; i < weeks; i++) {
|
|
const weekStart = addDays(currentWeekStart, 7 * i);
|
|
buckets.push({ start: weekStart, end: endOfDay(addDays(weekStart, 6)), key: weekStart.toISOString().slice(0, 10) });
|
|
}
|
|
return buckets;
|
|
};
|
|
|
|
/** Builds `months` monthly (Gregorian calendar month) buckets ending on the month containing `endDate`, oldest first. */
|
|
const buildMonthlyBuckets = (months, endDate = new Date()) => {
|
|
const base = new Date(endDate.getFullYear(), endDate.getMonth(), 1, 0, 0, 0, 0);
|
|
const buckets = [];
|
|
for (let i = months - 1; i >= 0; i--) {
|
|
const monthStart = addMonths(base, -i);
|
|
const monthEnd = endOfDay(addDays(addMonths(monthStart, 1), -1));
|
|
buckets.push({
|
|
start: monthStart,
|
|
end: monthEnd,
|
|
year: monthStart.getFullYear(),
|
|
month: monthStart.getMonth() + 1,
|
|
key: `${monthStart.getFullYear()}-${String(monthStart.getMonth() + 1).padStart(2, '0')}`
|
|
});
|
|
}
|
|
return buckets;
|
|
};
|
|
|
|
/** Linear search is fine — bucket arrays used here are always small (<=~90 entries). */
|
|
const findBucketIndex = (buckets, date) => {
|
|
if (!date) return -1;
|
|
const d = date instanceof Date ? date : new Date(date);
|
|
if (Number.isNaN(d.getTime())) return -1;
|
|
const t = d.getTime();
|
|
return buckets.findIndex((b) => t >= b.start.getTime() && t <= b.end.getTime());
|
|
};
|
|
|
|
module.exports = {
|
|
startOfDay,
|
|
endOfDay,
|
|
addDays,
|
|
addMonths,
|
|
getWeekStart,
|
|
buildDailyBuckets,
|
|
buildWeeklyBuckets,
|
|
buildForecastWeeklyBuckets,
|
|
buildMonthlyBuckets,
|
|
findBucketIndex
|
|
};
|