Files
property-management-network/app/tenant-portal/[token]/tenant-maintenance-form.tsx
Leon SerfatyandClaude Opus 4.8 857b9a7811 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>
2026-06-23 20:36:07 -04:00

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>
)
}