Files
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

160 lines
6.9 KiB
TypeScript

import { redirect } from "next/navigation"
import { eq, desc } from "drizzle-orm"
import { db } from "@/lib/db"
import { properties as propertiesTable } from "@/lib/db/schema"
import { getSessionUser } from "@/lib/session"
import { getEffectiveOwnerId } from "@/lib/account"
import Link from "next/link"
import { Building2, MapPin, BedDouble, ArrowRight, TrendingUp } from "lucide-react"
import { EmptyState } from "@/components/shared/empty-state"
import { PropertyMap } from "@/components/maps/property-map"
import { formatCurrency, getOccupancyRate } from "@/lib/utils"
export const metadata = { title: "Properties" }
export default async function PropertiesPage() {
const user = await getSessionUser()
if (!user) redirect("/login")
const ownerId = await getEffectiveOwnerId(user.id)
const properties = await db.query.properties.findMany({
where: eq(propertiesTable.user_id, ownerId),
with: {
units: { columns: { id: true, status: true, rent_amount: true } },
},
orderBy: desc(propertiesTable.created_at),
})
const totalMonthly = (properties ?? []).reduce((sum: number, p: any) => {
return sum + (p.units ?? []).filter((u: any) => u.status === "occupied").reduce((s: number, u: any) => s + Number(u.rent_amount), 0)
}, 0)
const mapMarkers = (properties ?? [])
.filter((p: any) => p.latitude != null && p.longitude != null)
.map((p: any) => ({
id: p.id,
name: p.name,
lat: p.latitude as number,
lng: p.longitude as number,
subtitle: `${p.address_line1 ? `${p.address_line1}, ` : ""}${p.city}${p.state ? `, ${p.state}` : ""}`,
href: `/properties/${p.id}`,
}))
return (
<div className="space-y-6">
{/* Page header */}
<div className="flex items-end justify-between">
<div>
<h2 className="text-lg font-semibold text-white">Properties</h2>
<p className="text-sm text-white/40 mt-0.5">
{properties?.length ?? 0} {(properties?.length ?? 0) === 1 ? "property" : "properties"}
{totalMonthly > 0 && <span className="ml-2 text-emerald-400">· {formatCurrency(totalMonthly)}/mo</span>}
</p>
</div>
</div>
{mapMarkers.length > 0 && (
<div className="rounded-2xl border border-white/[0.06] bg-[#16161f] overflow-hidden">
<PropertyMap className="h-80 w-full" markers={mapMarkers} />
</div>
)}
{!properties?.length ? (
<EmptyState
icon={Building2}
title="No properties yet"
description="Add your first rental property to start managing tenants, rent, and maintenance."
action={{ label: "Add property", href: "/properties/new" }}
/>
) : (
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
{(properties as any[]).map((property) => {
const units = property.units ?? []
const occupied = units.filter((u: any) => u.status === "occupied").length
const totalUnits = units.length
const occupancy = getOccupancyRate(occupied, totalUnits)
const monthlyRent = units
.filter((u: any) => u.status === "occupied")
.reduce((sum: number, u: any) => sum + Number(u.rent_amount), 0)
const potential = units.reduce((sum: number, u: any) => sum + Number(u.rent_amount), 0)
const occupancyColor =
occupancy === 100 ? "text-emerald-400" :
occupancy >= 50 ? "text-amber-400" :
"text-red-400"
const barColor =
occupancy === 100 ? "bg-emerald-500" :
occupancy >= 50 ? "bg-amber-500" :
"bg-red-500"
return (
<Link
key={property.id}
href={`/properties/${property.id}`}
className="group relative rounded-2xl border border-white/[0.06] bg-[#16161f] p-5 transition-all duration-200 hover:border-indigo-500/30 hover:bg-[#1a1a2e] hover:shadow-xl hover:shadow-indigo-500/5"
>
{/* Top row */}
<div className="flex items-start justify-between mb-4">
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-indigo-500/10 ring-1 ring-inset ring-indigo-500/20">
<Building2 className="h-5 w-5 text-indigo-400" />
</div>
<span className={`rounded-full px-2.5 py-0.5 text-xs font-semibold ${
occupancy === 100 ? "bg-emerald-500/10 text-emerald-400 ring-1 ring-emerald-500/20" :
occupancy >= 50 ? "bg-amber-500/10 text-amber-400 ring-1 ring-amber-500/20" :
"bg-red-500/10 text-red-400 ring-1 ring-red-500/20"
}`}>
{occupancy}% full
</span>
</div>
{/* Name + address */}
<div>
<h3 className="font-semibold text-white group-hover:text-indigo-200 transition-colors">{property.name}</h3>
<div className="mt-1 flex items-center gap-1 text-xs text-white/40">
<MapPin className="h-3 w-3 shrink-0" />
<span className="truncate">{property.address_line1 ? `${property.address_line1}, ` : ""}{property.city}{property.state ? `, ${property.state}` : ""}</span>
</div>
</div>
{/* Occupancy bar */}
<div className="mt-4">
<div className="h-1 w-full rounded-full bg-white/[0.06]">
<div
className={`h-1 rounded-full transition-all ${barColor}`}
style={{ width: `${Math.max(occupancy, 2)}%` }}
/>
</div>
<div className="mt-1 flex items-center justify-between text-[10px] text-white/30">
<span>{occupied} of {totalUnits} units occupied</span>
<span>{totalUnits - occupied} vacant</span>
</div>
</div>
{/* Stats */}
<div className="mt-4 grid grid-cols-2 gap-3 border-t border-white/[0.06] pt-4">
<div>
<p className="text-xs text-white/30">Collected / mo</p>
<p className="mt-0.5 text-sm font-semibold text-white">{formatCurrency(monthlyRent)}</p>
</div>
<div>
<p className="text-xs text-white/30">Potential / mo</p>
<p className="mt-0.5 text-sm font-semibold text-white/60">{formatCurrency(potential)}</p>
</div>
</div>
{/* Hover arrow */}
<div className="mt-3 flex items-center gap-1 text-xs text-indigo-400/0 group-hover:text-indigo-400/80 transition-all">
<span>View details</span>
<ArrowRight className="h-3 w-3" />
</div>
</Link>
)
})}
</div>
)}
</div>
)
}