Files

126 lines
4.6 KiB
TypeScript
Raw Permalink Normal View History

import { redirect } from "next/navigation"
import { and, eq, gte, lte } from "drizzle-orm"
import { db } from "@/lib/db"
import { profiles, rent_payments, leases, inspections } from "@/lib/db/schema"
import { getSessionUser } from "@/lib/session"
import { getAccountContext } from "@/lib/account"
import { CalendarClient, type CalEvent } from "./calendar-client"
export const metadata = { title: "Calendar" }
const RENT_COLOR = (s: string) =>
s === "paid" ? "#10b981" : s === "overdue" ? "#ef4444" : "#6366f1"
const LEASE_COLOR = "#f59e0b"
const INSPECTION_COLOR = "#14b8a6"
export default async function CalendarPage() {
const user = await getSessionUser()
if (!user) redirect("/login")
const ctx = await getAccountContext(user.id)
const ownerId = ctx.ownerId
const now = new Date()
const rangeStart = new Date(now.getFullYear(), now.getMonth() - 3, 1)
const rangeEnd = new Date(now.getFullYear(), now.getMonth() + 12, 0)
const iso = (d: Date) => d.toISOString().slice(0, 10)
const [payments, leaseList, inspectionList, profile] = await Promise.all([
db.query.rent_payments.findMany({
where: and(
eq(rent_payments.user_id, ownerId),
gte(rent_payments.due_date, iso(rangeStart)),
lte(rent_payments.due_date, iso(rangeEnd))
),
columns: { id: true, due_date: true, amount: true, status: true },
with: { tenant: { columns: { first_name: true, last_name: true } }, property: { columns: { name: true } }, unit: { columns: { unit_number: true } } },
}),
db.query.leases.findMany({
where: and(eq(leases.user_id, ownerId), eq(leases.status, "active")),
columns: { id: true, lease_end: true },
with: { tenant: { columns: { first_name: true, last_name: true } }, property: { columns: { name: true } } },
}),
db.query.inspections.findMany({
where: and(eq(inspections.user_id, ownerId), gte(inspections.date, iso(rangeStart)), lte(inspections.date, iso(rangeEnd))),
columns: { id: true, date: true, type: true, status: true },
with: { property: { columns: { name: true } }, unit: { columns: { unit_number: true } } },
}),
db.query.profiles.findFirst({ where: eq(profiles.id, ownerId), columns: { calendar_token: true } }),
])
const events: CalEvent[] = [
...payments.map((p) => {
const who = `${p.tenant?.first_name ?? ""} ${p.tenant?.last_name ?? ""}`.trim() || "Tenant"
const color = RENT_COLOR(p.status)
return {
id: `rent-${p.id}`,
title: `${who} · $${Number(p.amount).toLocaleString("en-US")}`,
start: p.due_date!,
allDay: true,
backgroundColor: color,
borderColor: color,
editable: ctx.canWrite,
extendedProps: {
type: "rent" as const,
entityId: p.id,
status: p.status,
subtitle: `${p.property?.name ?? ""}${p.unit ? ` · Unit ${p.unit.unit_number}` : ""}`,
href: "/rent",
},
}
}),
...leaseList.map((l) => {
const who = `${l.tenant?.first_name ?? ""} ${l.tenant?.last_name ?? ""}`.trim() || "Tenant"
return {
id: `lease-${l.id}`,
title: `Lease ends: ${who}`,
start: l.lease_end!,
allDay: true,
backgroundColor: LEASE_COLOR,
borderColor: LEASE_COLOR,
editable: false,
extendedProps: {
type: "lease" as const,
entityId: l.id,
subtitle: l.property?.name ?? "",
href: `/leases/${l.id}`,
},
}
}),
...inspectionList.map((ins) => {
const label = ins.type.replace("_", "-")
return {
id: `insp-${ins.id}`,
title: `${label} inspection`,
start: ins.date!,
allDay: true,
backgroundColor: INSPECTION_COLOR,
borderColor: INSPECTION_COLOR,
editable: ctx.canWrite,
extendedProps: {
type: "inspection" as const,
entityId: ins.id,
status: ins.status,
subtitle: `${ins.property?.name ?? ""}${ins.unit ? ` · Unit ${ins.unit.unit_number}` : ""}`,
href: "/inspections",
},
}
}),
]
const base = process.env.NEXT_PUBLIC_APP_URL ?? ""
const subscribeUrl = profile?.calendar_token ? `${base}/api/calendar/${profile.calendar_token}.ics` : ""
return (
<div className="max-w-6xl mx-auto">
<div className="mb-6">
<h2 className="text-lg font-bold text-white">Calendar</h2>
<p className="text-sm text-white/40 mt-0.5">
Rent due dates, lease expirations, and inspections subscribe to sync with Google, Apple, or Outlook.
</p>
</div>
<CalendarClient events={events} canWrite={ctx.canWrite} subscribeUrl={subscribeUrl} />
</div>
)
}