Batch commit of the pending working tree on security/audit-fixes-2026-07. Major areas: - Outbound webhooks / Zapier: schema + signed delivery with retries, public v1 API (REST-hook subscribe/unsubscribe), settings UI, cron drain. - Deploy hardening: email via SMTP2GO (Resend fully removed), verified DB TLS (DATABASE_SSL=require + DATABASE_CA), storage fails loud in production when Spaces is unconfigured instead of silently using ephemeral disk. - Integrations & features (concurrent work): accounting (QuickBooks/Xero), e-signature (DocuSign/Dropbox Sign), PayPal, geocoding/maps, onboarding, expanded legal pages. - DB migrations 0006–0009. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
126 lines
4.6 KiB
TypeScript
126 lines
4.6 KiB
TypeScript
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>
|
|
)
|
|
}
|