Implement calendar module
This commit is contained in:
@@ -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(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user