96 lines
2.6 KiB
TypeScript
96 lines
2.6 KiB
TypeScript
export type DailyCountRow = { day: Date | string; count: number };
|
|
|
|
export type DailyActivityPoint = {
|
|
date: string;
|
|
count: number;
|
|
};
|
|
|
|
export type DailyActivityResponse = {
|
|
days: number;
|
|
total: number;
|
|
items: DailyActivityPoint[];
|
|
};
|
|
|
|
/** Local calendar day at 00:00:00, `offsetDays` before today (0 = today). */
|
|
export function startOfLocalDay(offsetDays = 0): Date {
|
|
const date = new Date();
|
|
date.setHours(0, 0, 0, 0);
|
|
date.setDate(date.getDate() - offsetDays);
|
|
return date;
|
|
}
|
|
|
|
/** Start of the current local week. `weekStartsOn`: 0 Sun … 6 Sat. Default Saturday (Iran). */
|
|
export function startOfLocalWeek(weekStartsOn = 6): Date {
|
|
const today = startOfLocalDay(0);
|
|
const diff = (today.getDay() - weekStartsOn + 7) % 7;
|
|
return startOfLocalDay(diff);
|
|
}
|
|
|
|
/** First day of the current local month at 00:00:00. */
|
|
export function startOfLocalMonth(): Date {
|
|
const date = new Date();
|
|
date.setDate(1);
|
|
date.setHours(0, 0, 0, 0);
|
|
return date;
|
|
}
|
|
|
|
function pad2(n: number) {
|
|
return String(n).padStart(2, '0');
|
|
}
|
|
|
|
/** Calendar date key from a PG `date` (UTC midnight) or ISO string. */
|
|
function toDateKey(value: Date | string): string {
|
|
if (typeof value === 'string') {
|
|
return value.slice(0, 10);
|
|
}
|
|
return `${value.getUTCFullYear()}-${pad2(value.getUTCMonth() + 1)}-${pad2(value.getUTCDate())}`;
|
|
}
|
|
|
|
function toLocalDateKey(date: Date): string {
|
|
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
|
|
}
|
|
|
|
/** Fill a continuous series of the last `days` local calendar days. */
|
|
export function buildDailyActivitySeries(
|
|
days: number,
|
|
rows: DailyCountRow[],
|
|
): DailyActivityResponse {
|
|
const byDay = new Map<string, number>();
|
|
for (const row of rows) {
|
|
byDay.set(toDateKey(row.day), Number(row.count) || 0);
|
|
}
|
|
|
|
const items: DailyActivityPoint[] = [];
|
|
let total = 0;
|
|
const end = startOfLocalDay(0);
|
|
|
|
for (let i = days - 1; i >= 0; i -= 1) {
|
|
const date = new Date(end);
|
|
date.setDate(end.getDate() - i);
|
|
const key = toLocalDateKey(date);
|
|
const count = byDay.get(key) ?? 0;
|
|
total += count;
|
|
items.push({ date: key, count });
|
|
}
|
|
|
|
return { days, total, items };
|
|
}
|
|
|
|
export type DualDailyActivityResponse = {
|
|
days: number;
|
|
primary: DailyActivityResponse;
|
|
secondary: DailyActivityResponse;
|
|
};
|
|
|
|
export function buildDualDailyActivitySeries(
|
|
days: number,
|
|
primaryRows: DailyCountRow[],
|
|
secondaryRows: DailyCountRow[],
|
|
): DualDailyActivityResponse {
|
|
return {
|
|
days,
|
|
primary: buildDailyActivitySeries(days, primaryRows),
|
|
secondary: buildDailyActivitySeries(days, secondaryRows),
|
|
};
|
|
}
|