import { notFound } from "next/navigation" import { and, desc, eq } from "drizzle-orm" import { db } from "@/lib/db" import { tenants, rent_payments, maintenance_requests, leases } from "@/lib/db/schema" import { formatCurrency, formatDate } from "@/lib/utils" import { Home, DollarSign, Wrench, FileText } from "lucide-react" import { getBranding } from "@/lib/branding" import { TenantMaintenanceForm } from "./tenant-maintenance-form" export const dynamic = "force-dynamic" export const metadata = { title: "Tenant Portal", robots: { index: false, follow: false }, } export default async function TenantPortalPage({ params }: { params: Promise<{ token: string }> }) { const { token } = await params // Find tenant by portal token (PUBLIC — no session filter) const tenant = (await db.query.tenants.findFirst({ where: eq(tenants.portal_token, token), with: { unit: { columns: { unit_number: true, rent_amount: true, property_id: true }, with: { property: { columns: { name: true, address_line1: true, city: true, state: true } }, }, }, }, })) as any if (!tenant) notFound() // White-label: apply the landlord's (portal owner's) branding, gated by plan. const branding = await getBranding(tenant.user_id) const accent = branding.enabled && branding.color ? branding.color : null const property = tenant.unit?.property const unit = tenant.unit // Get rent payments for this tenant const payments = await db .select({ amount: rent_payments.amount, due_date: rent_payments.due_date, paid_date: rent_payments.paid_date, status: rent_payments.status, payment_method: rent_payments.payment_method, }) .from(rent_payments) .where(eq(rent_payments.tenant_id, tenant.id)) .orderBy(desc(rent_payments.due_date)) .limit(12) // Get maintenance requests for this tenant const maintenance = await db .select({ title: maintenance_requests.title, status: maintenance_requests.status, priority: maintenance_requests.priority, created_at: maintenance_requests.created_at, resolved_at: maintenance_requests.resolved_at, }) .from(maintenance_requests) .where(eq(maintenance_requests.tenant_id, tenant.id)) .orderBy(desc(maintenance_requests.created_at)) // Get active lease for this tenant const lease = await db.query.leases.findFirst({ where: and(eq(leases.tenant_id, tenant.id), eq(leases.status, "active")), columns: { lease_start: true, lease_end: true, rent_amount: true, security_deposit: true, lease_type: true, status: true, }, }) const paymentStatusColors: Record = { paid: "text-emerald-400 bg-emerald-500/10 border-emerald-500/20", pending: "text-amber-400 bg-amber-500/10 border-amber-500/20", overdue: "text-red-400 bg-red-500/10 border-red-500/20", partial: "text-blue-400 bg-blue-500/10 border-blue-500/20", } const maintenanceStatusColors: Record = { open: "text-amber-400 bg-amber-500/10 border-amber-500/20", in_progress: "text-blue-400 bg-blue-500/10 border-blue-500/20", resolved: "text-emerald-400 bg-emerald-500/10 border-emerald-500/20", closed: "text-white/40 bg-white/5 border-white/10", } return (
{/* Header */}
{branding.enabled && branding.logoUrl ? ( // eslint-disable-next-line @next/next/no-img-element {branding.brandName ) : ( )}

{branding.enabled && branding.brandName ? branding.brandName : "Tenant Portal"}

{tenant.first_name} {tenant.last_name}

{property && ( <>

{property.name}

{property.address_line1}{property.city ? `, ${property.city}` : ""}{property.state ? `, ${property.state}` : ""}

)} {unit &&

Unit {unit.unit_number} · {formatCurrency(unit.rent_amount)}/mo

} {tenant.email &&

{tenant.email}

}
{/* Lease Info */} {lease && (

Current Lease

{[ { label: "Start Date", value: formatDate(lease.lease_start) }, { label: "End Date", value: formatDate(lease.lease_end) }, { label: "Monthly Rent", value: formatCurrency(lease.rent_amount) }, { label: "Deposit", value: lease.security_deposit ? formatCurrency(lease.security_deposit) : "—" }, ].map((item) => (

{item.label}

{item.value}

))}
)} {/* Rent Payments */}

Rent History

{!payments?.length ? (

No payment records yet

) : (
{payments.map((p, i) => (

Due {formatDate(p.due_date)}

{p.paid_date &&

Paid {formatDate(p.paid_date)}

}

{formatCurrency(p.amount)}

{p.status}
))}
)}
{/* Maintenance */}

Maintenance Requests

{!maintenance?.length ? (

No maintenance requests

) : (
{maintenance.map((m, i) => (

{m.title}

{formatDate(m.created_at)}

{m.status.replace("_", " ")}
))}
)}

{branding.enabled && branding.hidePoweredBy ? "This is a private link for your use only" : "Powered by Property Management Network · This is a private link for your use only"}

) }