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>
48 lines
1.4 KiB
TypeScript
48 lines
1.4 KiB
TypeScript
interface OccupancyRingProps {
|
||
rate: number // 0–100
|
||
occupied: number
|
||
total: number
|
||
size?: number
|
||
}
|
||
|
||
export function OccupancyRing({ rate, occupied, total, size = 80 }: OccupancyRingProps) {
|
||
const radius = (size - 12) / 2
|
||
const circumference = 2 * Math.PI * radius
|
||
const filled = (rate / 100) * circumference
|
||
const empty = circumference - filled
|
||
|
||
const color =
|
||
rate >= 80 ? "#10b981" : rate >= 50 ? "#f59e0b" : "#ef4444"
|
||
|
||
return (
|
||
<div className="flex flex-col items-center gap-1">
|
||
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className="-rotate-90">
|
||
{/* Track */}
|
||
<circle
|
||
cx={size / 2}
|
||
cy={size / 2}
|
||
r={radius}
|
||
fill="none"
|
||
stroke="rgba(255,255,255,0.06)"
|
||
strokeWidth={10}
|
||
/>
|
||
{/* Fill */}
|
||
<circle
|
||
cx={size / 2}
|
||
cy={size / 2}
|
||
r={radius}
|
||
fill="none"
|
||
stroke={color}
|
||
strokeWidth={10}
|
||
strokeDasharray={`${filled} ${empty}`}
|
||
strokeLinecap="round"
|
||
/>
|
||
</svg>
|
||
<div className="-mt-[calc(80px/2+20px)] flex flex-col items-center" style={{ marginTop: -(size / 2 + 14) }}>
|
||
<span className="text-xl font-bold text-white">{rate}%</span>
|
||
<span className="text-xs text-white/40">{occupied}/{total}</span>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|