Files
property-management-network/app/(dashboard)/tenants/[tenantId]/page.tsx
T

187 lines
9.2 KiB
TypeScript
Raw Normal View History

import { notFound, redirect } from "next/navigation"
import { and, eq, desc } from "drizzle-orm"
import { db } from "@/lib/db"
import { tenants as tenantsTable, rent_payments, maintenance_requests, leases as leasesTable } from "@/lib/db/schema"
import { getSessionUser } from "@/lib/session"
import Link from "next/link"
import { formatCurrency, formatDate } from "@/lib/utils"
import { RentStatusBadge } from "@/components/dashboard/rent-status-badge"
import { MaintenanceStatusBadge, PriorityBadge } from "@/components/dashboard/maintenance-status-badge"
import { Mail, Phone } from "lucide-react"
import { CopyButton } from "@/components/shared/copy-button"
import { SendReminderButton } from "@/components/shared/send-reminder-button"
export default async function TenantDetailPage({ params }: { params: Promise<{ tenantId: string }> }) {
const user = await getSessionUser()
if (!user) redirect("/login")
const { tenantId } = await params
const tenant = await db.query.tenants.findFirst({
where: and(eq(tenantsTable.id, tenantId), eq(tenantsTable.user_id, user.id)),
with: {
unit: { columns: { unit_number: true, rent_amount: true, bedrooms: true, bathrooms: true } },
property: { columns: { name: true, address_line1: true, city: true, state: true } },
},
})
if (!tenant) notFound()
const [payments, maintenanceRequests, leases] = await Promise.all([
db
.select()
.from(rent_payments)
.where(and(eq(rent_payments.user_id, user.id), eq(rent_payments.tenant_id, tenantId)))
.orderBy(desc(rent_payments.due_date))
.limit(6),
db
.select()
.from(maintenance_requests)
.where(and(eq(maintenance_requests.user_id, user.id), eq(maintenance_requests.tenant_id, tenantId)))
.orderBy(desc(maintenance_requests.created_at))
.limit(5),
db
.select()
.from(leasesTable)
.where(and(eq(leasesTable.user_id, user.id), eq(leasesTable.tenant_id, tenantId)))
.orderBy(desc(leasesTable.created_at))
.limit(1),
])
const activeLease = leases?.[0]
const portalUrl = `${process.env.NEXT_PUBLIC_APP_URL}/tenant-portal/${tenant.portal_token}`
return (
<div className="mx-auto max-w-4xl space-y-6">
{/* Breadcrumb */}
<div className="flex items-center gap-2 text-sm text-white/40">
<Link href="/tenants" className="hover:text-white transition">Tenants</Link>
<span>/</span>
<span className="text-white/70">{tenant.first_name} {tenant.last_name}</span>
</div>
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex items-center gap-4">
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-indigo-600/20 text-lg font-bold text-indigo-400">
{tenant.first_name[0]}{tenant.last_name[0]}
</div>
<div>
<h2 className="text-xl font-bold text-white">{tenant.first_name} {tenant.last_name}</h2>
<div className="flex items-center gap-3 mt-1">
{tenant.email && <span className="flex items-center gap-1 text-sm text-white/50"><Mail className="h-3.5 w-3.5" />{tenant.email}</span>}
{tenant.phone && <span className="flex items-center gap-1 text-sm text-white/50"><Phone className="h-3.5 w-3.5" />{tenant.phone}</span>}
</div>
</div>
</div>
<Link
href={`/tenants/${tenantId}/edit`}
className="rounded-lg border border-white/10 px-4 py-2 text-sm text-white/60 hover:border-white/20 hover:text-white transition"
>
Edit
</Link>
</div>
<div className="grid gap-6 lg:grid-cols-3">
<div className="lg:col-span-2 space-y-6">
{/* Rent payments */}
<div className="rounded-xl border border-white/[0.06] bg-[#16161f]">
<div className="flex items-center justify-between border-b border-white/[0.06] px-5 py-4">
<h3 className="text-sm font-semibold text-white">Rent Payments</h3>
<Link href="/rent/new" className="text-xs text-indigo-400 hover:text-indigo-300">+ Record</Link>
</div>
{!payments?.length ? (
<p className="px-5 py-6 text-sm text-white/30">No payments recorded.</p>
) : (
<div className="divide-y divide-white/[0.04]">
{payments.map((p: any) => (
<div key={p.id} className="flex items-center justify-between px-5 py-3">
<div>
<p className="text-sm text-white">{formatDate(p.due_date)}</p>
{p.paid_date && <p className="text-xs text-white/40">Paid {formatDate(p.paid_date)}</p>}
</div>
<div className="flex items-center gap-3">
<span className="text-sm font-semibold text-white">{formatCurrency(p.amount)}</span>
<RentStatusBadge status={p.status} />
{(p.status === "pending" || p.status === "overdue") && tenant.email && (
<SendReminderButton tenantId={tenant.id} paymentId={p.id} />
)}
</div>
</div>
))}
</div>
)}
</div>
{/* Maintenance */}
<div className="rounded-xl border border-white/[0.06] bg-[#16161f]">
<div className="flex items-center justify-between border-b border-white/[0.06] px-5 py-4">
<h3 className="text-sm font-semibold text-white">Maintenance Requests</h3>
<Link href="/maintenance/new" className="text-xs text-indigo-400 hover:text-indigo-300">+ New</Link>
</div>
{!maintenanceRequests?.length ? (
<p className="px-5 py-6 text-sm text-white/30">No maintenance requests.</p>
) : (
<div className="divide-y divide-white/[0.04]">
{maintenanceRequests.map((r: any) => (
<Link key={r.id} href={`/maintenance/${r.id}`} className="flex items-center justify-between px-5 py-3 hover:bg-white/[0.02] transition">
<div>
<p className="text-sm text-white">{r.title}</p>
<p className="text-xs text-white/40">{formatDate(r.created_at)}</p>
</div>
<div className="flex items-center gap-2">
<PriorityBadge priority={r.priority} />
<MaintenanceStatusBadge status={r.status} />
</div>
</Link>
))}
</div>
)}
</div>
</div>
{/* Sidebar */}
<div className="space-y-4">
{/* Unit info */}
<div className="rounded-xl border border-white/[0.06] bg-[#16161f] p-5 space-y-3">
<h3 className="text-xs font-medium uppercase tracking-wider text-white/30">Unit</h3>
<p className="text-sm font-medium text-white">{tenant.property?.name}</p>
{tenant.unit && <p className="text-sm text-white/60">Unit {tenant.unit.unit_number} · {tenant.unit.bedrooms}bd/{tenant.unit.bathrooms}ba</p>}
{tenant.unit?.rent_amount && <p className="text-sm font-bold text-white">{formatCurrency(tenant.unit.rent_amount)}<span className="text-xs text-white/40">/mo</span></p>}
{tenant.move_in_date && <p className="text-xs text-white/40">Moved in {formatDate(tenant.move_in_date)}</p>}
</div>
{/* Active lease */}
{activeLease && (
<div className="rounded-xl border border-white/[0.06] bg-[#16161f] p-5 space-y-2">
<h3 className="text-xs font-medium uppercase tracking-wider text-white/30">Lease</h3>
<p className="text-xs text-white/50">{formatDate(activeLease.lease_start)} {formatDate(activeLease.lease_end)}</p>
<p className="text-xs text-white/50 capitalize">{activeLease.status}</p>
</div>
)}
{/* Tenant portal */}
<div className="rounded-xl border border-indigo-500/20 bg-indigo-500/5 p-5 space-y-2">
<h3 className="text-xs font-medium uppercase tracking-wider text-indigo-400/60">Tenant Portal</h3>
<p className="text-xs text-white/40">Share this link with the tenant to submit maintenance requests.</p>
<div className="flex items-center gap-2 rounded-lg bg-white/5 px-3 py-2">
<span className="flex-1 truncate text-xs text-white/60">/tenant-portal/{tenant.portal_token?.slice(0, 12)}</span>
<CopyButton text={portalUrl} />
<a href={portalUrl} target="_blank" rel="noopener noreferrer" className="text-xs text-indigo-400 hover:text-indigo-300">Open</a>
</div>
</div>
{/* Emergency contact */}
{tenant.emergency_contact_name && (
<div className="rounded-xl border border-white/[0.06] bg-[#16161f] p-5 space-y-2">
<h3 className="text-xs font-medium uppercase tracking-wider text-white/30">Emergency Contact</h3>
<p className="text-sm text-white">{tenant.emergency_contact_name}</p>
{tenant.emergency_contact_phone && <p className="text-xs text-white/50">{tenant.emergency_contact_phone}</p>}
</div>
)}
</div>
</div>
</div>
)
}