Initial import: property management SaaS + security hardening + admin dashboard
Property Management Network — Next.js 16 (App Router), Better Auth, Drizzle ORM over PostgreSQL, Stripe, OpenAI, Resend. Includes: - Security hardening: access-control/IDOR fixes, TLS-by-default DB layer, constant-time cron auth, strict security headers, atomic AI quota gating, HTML/email output encoding, demo-backdoor disabled in production. - Superadmin dashboard at /admin (overview/MRR, server-paginated users with ban/impersonate/plan/delete, billing, platform activity + admin audit log, AI usage, system health) via the Better Auth admin plugin. - Seed/migration utility scripts under scripts/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { redirect } from "next/navigation"
|
||||
import { and, desc, eq, gte, inArray } from "drizzle-orm"
|
||||
import { db } from "@/lib/db"
|
||||
import {
|
||||
ai_recommendations,
|
||||
ai_predictions,
|
||||
activity_log,
|
||||
rent_payments,
|
||||
units as unitsTable,
|
||||
maintenance_requests,
|
||||
} from "@/lib/db/schema"
|
||||
import { getSessionUser } from "@/lib/session"
|
||||
import { AiDashboardClient } from "./ai-dashboard-client"
|
||||
|
||||
export const metadata = { title: "AI Dashboard" }
|
||||
|
||||
export default async function AiDashboardPage() {
|
||||
const user = await getSessionUser()
|
||||
if (!user) redirect("/login")
|
||||
|
||||
const now = new Date()
|
||||
const threeMonthsAgo = new Date(now)
|
||||
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3)
|
||||
|
||||
const [recs, predictions, activityLog, payments, units, maintenance] = await Promise.all([
|
||||
db
|
||||
.select()
|
||||
.from(ai_recommendations)
|
||||
.where(eq(ai_recommendations.user_id, user.id))
|
||||
.orderBy(desc(ai_recommendations.created_at))
|
||||
.limit(3),
|
||||
db
|
||||
.select()
|
||||
.from(ai_predictions)
|
||||
.where(eq(ai_predictions.user_id, user.id))
|
||||
.orderBy(desc(ai_predictions.created_at))
|
||||
.limit(3),
|
||||
db
|
||||
.select()
|
||||
.from(activity_log)
|
||||
.where(and(eq(activity_log.user_id, user.id), eq(activity_log.type, "ai_action")))
|
||||
.orderBy(desc(activity_log.created_at))
|
||||
.limit(5),
|
||||
db
|
||||
.select({ amount: rent_payments.amount, status: rent_payments.status })
|
||||
.from(rent_payments)
|
||||
.where(
|
||||
and(
|
||||
eq(rent_payments.user_id, user.id),
|
||||
gte(rent_payments.due_date, threeMonthsAgo.toISOString().slice(0, 10))
|
||||
)
|
||||
),
|
||||
db
|
||||
.select({ status: unitsTable.status })
|
||||
.from(unitsTable)
|
||||
.where(eq(unitsTable.user_id, user.id)),
|
||||
db
|
||||
.select({ status: maintenance_requests.status, priority: maintenance_requests.priority })
|
||||
.from(maintenance_requests)
|
||||
.where(
|
||||
and(
|
||||
eq(maintenance_requests.user_id, user.id),
|
||||
inArray(maintenance_requests.status, ["open", "in_progress"])
|
||||
)
|
||||
),
|
||||
])
|
||||
|
||||
const allRecsData = await db
|
||||
.select({ status: ai_recommendations.status, action_data: ai_recommendations.action_data })
|
||||
.from(ai_recommendations)
|
||||
.where(eq(ai_recommendations.user_id, user.id))
|
||||
const approvedRecs = allRecsData.filter((r) => r.status === "approved")
|
||||
|
||||
let totalImpact = 0
|
||||
for (const r of approvedRecs) {
|
||||
totalImpact += Number(r.action_data?.estimated_value ?? 0)
|
||||
}
|
||||
|
||||
const occupiedUnits = units?.filter((u: any) => u.status === "occupied").length ?? 0
|
||||
const totalUnits = units?.length ?? 0
|
||||
const occupancyRate = totalUnits > 0 ? Math.round((occupiedUnits / totalUnits) * 100) : 0
|
||||
const totalRevenue = payments?.filter((p: any) => p.status === "paid").reduce((s: number, p: any) => s + Number(p.amount), 0) ?? 0
|
||||
const overdueAmount = payments?.filter((p: any) => p.status === "overdue").reduce((s: number, p: any) => s + Number(p.amount), 0) ?? 0
|
||||
const criticalMaintenance = maintenance?.filter((m: any) => m.priority === "emergency" || m.priority === "high").length ?? 0
|
||||
const riskAlerts = predictions?.filter((p: any) => ["critical", "high"].includes(p.risk_level)).length ?? 0
|
||||
|
||||
return (
|
||||
<AiDashboardClient
|
||||
recentRecs={recs ?? []}
|
||||
recentPredictions={predictions ?? []}
|
||||
activityLog={activityLog ?? []}
|
||||
stats={{
|
||||
totalImpact,
|
||||
approvedRecs: approvedRecs.length,
|
||||
pendingRecs: allRecsData.filter((r) => r.status === "pending").length,
|
||||
occupancyRate,
|
||||
totalRevenue,
|
||||
overdueAmount,
|
||||
criticalMaintenance,
|
||||
riskAlerts,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user