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>
98 lines
3.7 KiB
TypeScript
98 lines
3.7 KiB
TypeScript
"use client"
|
|
|
|
import { formatCurrency } from "@/lib/utils"
|
|
|
|
interface MonthData {
|
|
month: string
|
|
label: string
|
|
amount: number
|
|
}
|
|
|
|
interface RevenueChartProps {
|
|
data: MonthData[]
|
|
thisMonth: number
|
|
pending: number
|
|
}
|
|
|
|
export function RevenueChart({ data, thisMonth, pending }: RevenueChartProps) {
|
|
const max = Math.max(...data.map((d) => d.amount), 1)
|
|
const total = data.reduce((sum, d) => sum + d.amount, 0)
|
|
const currentMonthKey = new Date().toISOString().slice(0, 7)
|
|
const lastEntry = data[data.length - 1]
|
|
|
|
return (
|
|
<div className="rounded-2xl border border-white/[0.06] bg-[#16161f] p-5 h-full">
|
|
{/* Header */}
|
|
<div className="flex items-start justify-between mb-6">
|
|
<div>
|
|
<p className="text-xs font-medium uppercase tracking-wider text-white/40 mb-1">Revenue Overview</p>
|
|
<p className="text-2xl font-bold text-white tabular-nums">{formatCurrency(thisMonth)}</p>
|
|
<p className="text-xs text-white/40 mt-0.5">
|
|
Collected this month
|
|
{pending > 0 && <span className="text-amber-400 ml-1.5">· {formatCurrency(pending)} pending</span>}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-4 text-xs text-white/40">
|
|
<div className="flex items-center gap-1.5">
|
|
<div className="h-2 w-2 rounded-full bg-indigo-500" />
|
|
Collected
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bar chart */}
|
|
<div className="flex items-end justify-between gap-2 h-28">
|
|
{data.map((d) => {
|
|
const isCurrentMonth = d.month === currentMonthKey
|
|
const heightPct = max > 0 ? (d.amount / max) * 100 : 0
|
|
const minHeight = d.amount > 0 ? 8 : 4
|
|
|
|
return (
|
|
<div key={d.month} className="group flex flex-col items-center gap-1.5 flex-1">
|
|
{/* Tooltip */}
|
|
<div className="opacity-0 group-hover:opacity-100 transition-opacity text-[10px] text-white/70 font-medium whitespace-nowrap -mt-6 absolute">
|
|
{formatCurrency(d.amount)}
|
|
</div>
|
|
|
|
{/* Bar */}
|
|
<div className="relative w-full flex items-end" style={{ height: "100px" }}>
|
|
<div
|
|
className={`w-full rounded-t-lg transition-all duration-500 ${
|
|
isCurrentMonth
|
|
? "bg-gradient-to-t from-indigo-600 to-indigo-400"
|
|
: d.amount > 0
|
|
? "bg-white/[0.12] group-hover:bg-white/[0.2]"
|
|
: "bg-white/[0.04]"
|
|
}`}
|
|
style={{ height: `${Math.max(heightPct, minHeight)}%` }}
|
|
/>
|
|
</div>
|
|
|
|
{/* Label */}
|
|
<span className={`text-[10px] font-medium ${isCurrentMonth ? "text-indigo-400" : "text-white/30"}`}>
|
|
{d.label}
|
|
</span>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Summary row */}
|
|
<div className="mt-5 pt-4 border-t border-white/[0.06] grid grid-cols-3 gap-4">
|
|
<div>
|
|
<p className="text-[10px] text-white/30 uppercase tracking-wider mb-1">6-mo total</p>
|
|
<p className="text-sm font-bold text-white tabular-nums">{formatCurrency(total)}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] text-white/30 uppercase tracking-wider mb-1">Monthly avg</p>
|
|
<p className="text-sm font-bold text-white tabular-nums">{formatCurrency(Math.round(total / 6))}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] text-white/30 uppercase tracking-wider mb-1">Best month</p>
|
|
<p className="text-sm font-bold text-white tabular-nums">{formatCurrency(max === 1 ? 0 : max)}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|