Files
famapp/src/modules/bangs/server/queries.ts
T
ginnoir e1c2a090fb feat: p2 batch 28-31 reminders lists comments bang stats
Calendar events support multiple reminder offsets with presets and per-user defaults.

Lists index adds inline task entry and list property editing.

Generic entity comments on list detail. New bangs.stats dashboard widget.

Closes Gitea #28, #29, #30, #31. Migrations 0022 and 0023.
2026-07-04 22:17:35 -05:00

116 lines
3.4 KiB
TypeScript

import { asc, count, desc, eq, sql } from "drizzle-orm";
import { db } from "@/lib/db";
import { users } from "@/modules/_core/schema";
import { bangEvents } from "../schema";
export type BangStatsDto = {
total: number;
recent: RecentBangDto[];
};
export type RecentBangDto = {
id: string;
occurredOn: string;
recordedByName: string | null;
};
export type BangAggregatesDto = {
thisMonth: number;
thisYear: number;
averageDaysBetween: number | null;
monthlyCounts: { month: string; count: number }[];
};
export async function getBangStatsForScope(
householdId: string,
limit: number,
): Promise<BangStatsDto> {
const [totalRow] = await db
.select({ total: count() })
.from(bangEvents)
.where(eq(bangEvents.householdId, householdId));
const recent = await db
.select({
id: bangEvents.id,
occurredOn: bangEvents.occurredOn,
recordedByName: users.name,
})
.from(bangEvents)
.leftJoin(users, eq(bangEvents.recordedBy, users.id))
.where(eq(bangEvents.householdId, householdId))
.orderBy(desc(bangEvents.occurredOn), desc(bangEvents.createdAt))
.limit(limit);
return {
total: totalRow?.total ?? 0,
recent: recent.map((r) => ({
id: r.id,
occurredOn: r.occurredOn,
recordedByName: r.recordedByName,
})),
};
}
export async function getBangStats(householdId: string, limit: number): Promise<BangStatsDto> {
return getBangStatsForScope(householdId, limit);
}
export async function getBangAggregates(householdId: string): Promise<BangAggregatesDto> {
const now = new Date();
const monthPrefix = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
const yearPrefix = `${now.getFullYear()}-`;
const [thisMonthRow] = await db
.select({ count: sql<number>`count(*)::int` })
.from(bangEvents)
.where(
sql`${bangEvents.householdId} = ${householdId} and ${bangEvents.occurredOn} like ${`${monthPrefix}%`}`,
);
const [thisYearRow] = await db
.select({ count: sql<number>`count(*)::int` })
.from(bangEvents)
.where(
sql`${bangEvents.householdId} = ${householdId} and ${bangEvents.occurredOn} like ${`${yearPrefix}%`}`,
);
const monthlyRows = await db
.select({
month: sql<string>`substring(${bangEvents.occurredOn}, 1, 7)`,
count: sql<number>`count(*)::int`,
})
.from(bangEvents)
.where(eq(bangEvents.householdId, householdId))
.groupBy(sql`substring(${bangEvents.occurredOn}, 1, 7)`)
.orderBy(asc(sql`substring(${bangEvents.occurredOn}, 1, 7)`));
const dateRows = await db
.select({ occurredOn: bangEvents.occurredOn })
.from(bangEvents)
.where(eq(bangEvents.householdId, householdId))
.orderBy(asc(bangEvents.occurredOn));
let averageDaysBetween: number | null = null;
if (dateRows.length >= 2) {
const dates = dateRows.map((row) => parseBangDate(row.occurredOn).getTime());
let totalGapDays = 0;
for (let i = 1; i < dates.length; i++) {
totalGapDays += (dates[i]! - dates[i - 1]!) / (1000 * 60 * 60 * 24);
}
averageDaysBetween = Math.round((totalGapDays / (dates.length - 1)) * 10) / 10;
}
return {
thisMonth: thisMonthRow?.count ?? 0,
thisYear: thisYearRow?.count ?? 0,
averageDaysBetween,
monthlyCounts: monthlyRows.map((row) => ({ month: row.month, count: row.count })),
};
}
function parseBangDate(isoDate: string): Date {
const [year, month, day] = isoDate.split("-").map(Number);
return new Date(year!, month! - 1, day!);
}