Implement calendar module

This commit is contained in:
ginnoir
2026-05-06 03:10:28 -05:00
parent 8cc2ef0732
commit 744c1119a9
18 changed files with 1414 additions and 32 deletions
+187
View File
@@ -0,0 +1,187 @@
"use server";
import { and, asc, eq, gte, inArray, lte, or, sql } from "drizzle-orm";
import { z } from "zod";
import { db } from "@/lib/db";
import { getCurrentSession } from "@/lib/session";
import { householdMembers } from "@/modules/_core/schema";
import { calendarEvents, calendars } from "../schema";
export type CalendarDto = {
id: string;
name: string;
color: string | null;
visibility: "private" | "household";
ownerId: string;
};
export type CalendarEventDto = {
id: string;
calendarId: string;
title: string;
startAt: string;
endAt: string;
allDay: boolean;
location: string | null;
notes: string | null;
};
const listEventsSchema = z.object({
from: z.coerce.date(),
to: z.coerce.date(),
calendarIds: z.union([z.literal("all"), z.array(z.string().uuid())]),
});
export async function canSeeCalendar(userId: string, calendarId: string) {
const [row] = await db
.select({
calendarId: calendars.id,
visibility: calendars.visibility,
ownerId: calendars.ownerId,
memberUserId: householdMembers.userId,
})
.from(calendars)
.leftJoin(
householdMembers,
and(
eq(householdMembers.householdId, calendars.householdId),
eq(householdMembers.userId, userId),
),
)
.where(eq(calendars.id, calendarId))
.limit(1);
if (!row) return false;
if (row.visibility === "private") return row.ownerId === userId;
return row.memberUserId === userId;
}
export async function listCalendars(): Promise<CalendarDto[]> {
const { user, household } = await getCurrentSession();
const rows = await db
.select({
id: calendars.id,
name: calendars.name,
color: calendars.color,
visibility: calendars.visibility,
ownerId: calendars.ownerId,
})
.from(calendars)
.where(
and(
eq(calendars.householdId, household.id),
or(eq(calendars.visibility, "household"), eq(calendars.ownerId, user.id)),
),
)
.orderBy(asc(calendars.name));
return rows.map((row) => ({
...row,
visibility: row.visibility as "private" | "household",
}));
}
export async function listEvents(input: {
from: Date | string;
to: Date | string;
calendarIds: "all" | string[];
}): Promise<CalendarEventDto[]> {
const parsed = listEventsSchema.parse(input);
const visibleCalendars = await listCalendars();
const visibleIds = new Set(visibleCalendars.map((calendar) => calendar.id));
const calendarIds =
parsed.calendarIds === "all"
? [...visibleIds]
: parsed.calendarIds.filter((id) => visibleIds.has(id));
if (calendarIds.length === 0) return [];
const rows = await db
.select({
id: calendarEvents.id,
calendarId: calendarEvents.calendarId,
title: calendarEvents.title,
startAt: calendarEvents.startAt,
endAt: calendarEvents.endAt,
allDay: calendarEvents.allDay,
location: calendarEvents.location,
notes: calendarEvents.notes,
})
.from(calendarEvents)
.where(
and(
inArray(calendarEvents.calendarId, calendarIds),
lte(calendarEvents.startAt, parsed.to),
gte(calendarEvents.endAt, parsed.from),
),
)
.orderBy(asc(calendarEvents.startAt));
return rows.map(toEventDto);
}
export async function searchCalendars(query: string, householdId: string) {
const rows = await db
.select({ id: calendars.id, name: calendars.name })
.from(calendars)
.where(
and(
eq(calendars.householdId, householdId),
sql`${calendars.name} ilike ${`%${query}%`}`,
),
)
.limit(10);
return rows.map((row) => ({
id: row.id,
title: row.name,
url: `/calendar?id=${row.id}`,
}));
}
export async function searchEvents(query: string, householdId: string) {
const rows = await db
.select({
id: calendarEvents.id,
title: calendarEvents.title,
location: calendarEvents.location,
notes: calendarEvents.notes,
})
.from(calendarEvents)
.innerJoin(calendars, eq(calendarEvents.calendarId, calendars.id))
.where(
and(
eq(calendars.householdId, householdId),
or(
sql`${calendarEvents.title} ilike ${`%${query}%`}`,
sql`${calendarEvents.location} ilike ${`%${query}%`}`,
sql`${calendarEvents.notes} ilike ${`%${query}%`}`,
),
),
)
.limit(10);
return rows.map((row) => ({
id: row.id,
title: row.title,
url: `/calendar/events/${row.id}`,
excerpt: row.location ?? row.notes ?? undefined,
}));
}
function toEventDto(row: {
id: string;
calendarId: string;
title: string;
startAt: Date;
endAt: Date;
allDay: boolean;
location: string | null;
notes: string | null;
}): CalendarEventDto {
return {
...row,
startAt: row.startAt.toISOString(),
endAt: row.endAt.toISOString(),
};
}