Files
property-management-network/app/(dashboard)/onboarding/page.tsx
T
Leon SerfatyandClaude Opus 4.8 c9968531e4 Consolidate audit-fixes branch: webhooks, integrations, and deploy hardening
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>
2026-07-02 13:42:34 -04:00

124 lines
5.2 KiB
TypeScript

import { redirect } from "next/navigation"
import Link from "next/link"
import { eq, sql } from "drizzle-orm"
import { db } from "@/lib/db"
import { profiles, properties, tenants, leases, rent_payments } from "@/lib/db/schema"
import { getSessionUser } from "@/lib/session"
import { completeOnboarding } from "@/app/actions/onboarding"
import { Building2, Users, FileText, CreditCard, CheckCircle2, ArrowRight, Sparkles } from "lucide-react"
export const dynamic = "force-dynamic"
async function count(table: typeof properties | typeof tenants | typeof leases | typeof rent_payments, userId: string) {
const [row] = await db.select({ c: sql<number>`count(*)::int` }).from(table).where(eq(table.user_id, userId))
return row?.c ?? 0
}
export default async function OnboardingPage() {
const user = await getSessionUser()
if (!user) redirect("/login")
const profile = await db.query.profiles.findFirst({
where: eq(profiles.id, user.id),
columns: { onboarding_completed: true, full_name: true },
})
// Users who already finished onboarding don't need this screen.
if (profile?.onboarding_completed) redirect("/dashboard")
const [propCount, tenantCount, leaseCount, rentCount] = await Promise.all([
count(properties, user.id),
count(tenants, user.id),
count(leases, user.id),
count(rent_payments, user.id),
])
const steps = [
{ label: "Add your first property", desc: "Create a building or unit to manage.", href: "/properties/new", icon: Building2, done: propCount > 0 },
{ label: "Add a tenant", desc: "Record who's renting from you.", href: "/tenants/new", icon: Users, done: tenantCount > 0 },
{ label: "Set up a lease", desc: "Track term, rent, and deposit.", href: "/leases/new", icon: FileText, done: leaseCount > 0 },
{ label: "Record a rent payment", desc: "Log or generate the first payment.", href: "/rent/new", icon: CreditCard, done: rentCount > 0 },
]
const doneCount = steps.filter((s) => s.done).length
const firstName = profile?.full_name?.split(" ")[0]
return (
<div className="mx-auto max-w-2xl py-4">
<div className="mb-6 flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-indigo-600 shadow-lg shadow-indigo-500/25">
<Sparkles className="h-5 w-5 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold text-white">
Welcome{firstName ? `, ${firstName}` : ""} 👋
</h1>
<p className="text-sm text-white/50">Let&apos;s get your portfolio set up in four quick steps.</p>
</div>
</div>
{/* Progress */}
<div className="mb-6">
<div className="mb-1.5 flex items-center justify-between text-xs text-white/50">
<span>{doneCount} of {steps.length} complete</span>
<span>{Math.round((doneCount / steps.length) * 100)}%</span>
</div>
<div className="h-1.5 w-full overflow-hidden rounded-full bg-white/[0.06]">
<div
className="h-full rounded-full bg-gradient-to-r from-indigo-500 to-violet-500 transition-all"
style={{ width: `${(doneCount / steps.length) * 100}%` }}
/>
</div>
</div>
{/* Steps */}
<ol className="space-y-3">
{steps.map((step, i) => (
<li
key={step.label}
className={`flex items-center gap-4 rounded-2xl border p-4 transition-colors ${
step.done
? "border-emerald-500/20 bg-emerald-500/[0.04]"
: "border-white/[0.06] bg-[#16161f]"
}`}
>
<div
className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-xl ${
step.done ? "bg-emerald-500/15 text-emerald-400" : "bg-white/[0.04] text-white/50"
}`}
>
{step.done ? <CheckCircle2 className="h-5 w-5" /> : <step.icon className="h-5 w-5" />}
</div>
<div className="min-w-0 flex-1">
<p className={`text-sm font-semibold ${step.done ? "text-white/60 line-through" : "text-white"}`}>
{i + 1}. {step.label}
</p>
<p className="text-xs text-white/40">{step.desc}</p>
</div>
{step.done ? (
<span className="shrink-0 text-xs font-medium text-emerald-400">Done</span>
) : (
<Link
href={step.href}
className="flex shrink-0 items-center gap-1 rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-indigo-500"
>
Add <ArrowRight className="h-3.5 w-3.5" />
</Link>
)}
</li>
))}
</ol>
{/* Finish */}
<form action={completeOnboarding} className="mt-6 flex items-center justify-between gap-3">
<p className="text-xs text-white/40">You can always finish these later from the dashboard.</p>
<button
type="submit"
className="shrink-0 rounded-lg border border-white/10 bg-white/5 px-4 py-2 text-sm font-semibold text-white transition hover:bg-white/10"
>
{doneCount === steps.length ? "Finish setup" : "Skip to dashboard"}
</button>
</form>
</div>
)
}