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.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { count, desc, eq } from "drizzle-orm";
|
||||
import { asc, count, desc, eq, sql } from "drizzle-orm";
|
||||
import { db } from "@/lib/db";
|
||||
import { users } from "@/modules/_core/schema";
|
||||
import { bangEvents } from "../schema";
|
||||
@@ -14,6 +14,13 @@ export type RecentBangDto = {
|
||||
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,
|
||||
@@ -48,3 +55,61 @@ export async function getBangStatsForScope(
|
||||
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!);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user