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>
107 lines
3.6 KiB
TypeScript
107 lines
3.6 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import { Plus, CheckCircle } from "lucide-react"
|
|
import { Select } from "@/components/ui/select"
|
|
|
|
const CATEGORIES = [
|
|
{ value: "plumbing", label: "Plumbing" },
|
|
{ value: "electrical", label: "Electrical" },
|
|
{ value: "hvac", label: "HVAC" },
|
|
{ value: "appliance", label: "Appliance" },
|
|
{ value: "structural", label: "Structural" },
|
|
{ value: "pest", label: "Pest" },
|
|
{ value: "general", label: "General" },
|
|
]
|
|
|
|
interface Props {
|
|
tenantId: string
|
|
propertyId: string
|
|
unitId: string | null
|
|
portalToken: string
|
|
}
|
|
|
|
export function TenantMaintenanceForm({ tenantId, propertyId, unitId, portalToken }: Props) {
|
|
const [open, setOpen] = useState(false)
|
|
const [loading, setLoading] = useState(false)
|
|
const [done, setDone] = useState(false)
|
|
const [error, setError] = useState("")
|
|
|
|
const cls = "w-full rounded-lg border border-white/10 bg-white/5 px-4 py-2.5 text-sm text-white placeholder-white/30 outline-none ring-indigo-500 transition focus:border-indigo-500/50 focus:ring-1"
|
|
|
|
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
e.preventDefault()
|
|
setLoading(true)
|
|
setError("")
|
|
const fd = new FormData(e.currentTarget)
|
|
|
|
const res = await fetch("/api/maintenance", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
title: fd.get("title"),
|
|
description: fd.get("description"),
|
|
category: fd.get("category"),
|
|
priority: "medium",
|
|
property_id: propertyId,
|
|
unit_id: unitId ?? undefined,
|
|
tenant_id: tenantId,
|
|
portal_token: portalToken, // verified server-side — no user_id from client
|
|
}),
|
|
})
|
|
|
|
const data = await res.json()
|
|
setLoading(false)
|
|
if (!res.ok) {
|
|
setError(typeof data.error === "string" ? data.error : "Something went wrong")
|
|
return
|
|
}
|
|
setDone(true)
|
|
setOpen(false)
|
|
}
|
|
|
|
if (done) {
|
|
return (
|
|
<div className="flex items-center gap-2 text-sm text-emerald-400">
|
|
<CheckCircle className="h-4 w-4" />
|
|
Request submitted! Your landlord will be in touch.
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!open) {
|
|
return (
|
|
<button
|
|
onClick={() => setOpen(true)}
|
|
className="flex items-center gap-2 rounded-lg border border-white/10 px-4 py-2 text-sm text-white/60 hover:border-white/20 hover:text-white transition"
|
|
>
|
|
<Plus className="h-4 w-4" /> Submit New Request
|
|
</button>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<p className="text-sm font-medium text-white">New Maintenance Request</p>
|
|
{error && <p className="text-xs text-red-400">{error}</p>}
|
|
<div>
|
|
<input name="title" required placeholder="Brief title (e.g. Leaking faucet)" className={cls} />
|
|
</div>
|
|
<div>
|
|
<textarea name="description" rows={3} placeholder="Describe the issue..." className={cls + " resize-none"} />
|
|
</div>
|
|
<div>
|
|
<Select name="category" defaultValue="plumbing" options={CATEGORIES} />
|
|
</div>
|
|
<div className="flex gap-3">
|
|
<button type="button" onClick={() => setOpen(false)} className="rounded-lg border border-white/10 px-4 py-2 text-sm text-white/50 hover:text-white transition">
|
|
Cancel
|
|
</button>
|
|
<button type="submit" disabled={loading} className="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500 disabled:opacity-50 transition">
|
|
{loading ? "Submitting..." : "Submit"}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)
|
|
}
|