Files
property-management-network/components/forms/late-notice-button.tsx
T
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

139 lines
4.7 KiB
TypeScript

"use client"
import { useState } from "react"
import { FileWarning } from "lucide-react"
import { toast } from "sonner"
interface Props {
payment: {
id: string
amount: number
due_date: string
status: string
}
tenant: {
first_name: string
last_name: string
email?: string | null
}
property: { name: string; address_line1?: string; city?: string; state?: string }
unit?: { unit_number: string } | null
}
export function LateNoticeButton({ payment, tenant, property, unit }: Props) {
const [loading, setLoading] = useState(false)
async function generateNotice() {
setLoading(true)
try {
const { jsPDF } = await import("jspdf")
const doc = new jsPDF({ unit: "pt", format: "a4" })
const pageW = doc.internal.pageSize.getWidth()
const margin = 60
const today = new Date().toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })
const daysLate = Math.floor((Date.now() - new Date(payment.due_date).getTime()) / (1000 * 60 * 60 * 24))
// Header
doc.setFillColor(239, 68, 68)
doc.rect(0, 0, pageW, 8, "F")
doc.setFontSize(11)
doc.setFont("helvetica", "normal")
doc.setTextColor(120, 120, 140)
doc.text("Property Management Network", margin, 40)
doc.text(today, pageW - margin, 40, { align: "right" })
// Title
doc.setFont("helvetica", "bold")
doc.setFontSize(20)
doc.setTextColor(239, 68, 68)
doc.text("LATE RENT NOTICE", margin, 90)
doc.setDrawColor(239, 68, 68, 0.3)
doc.line(margin, 100, pageW - margin, 100)
// Tenant info
doc.setFontSize(11)
doc.setFont("helvetica", "normal")
doc.setTextColor(40, 40, 60)
doc.text(`To: ${tenant.first_name} ${tenant.last_name}`, margin, 130)
if (tenant.email) doc.text(tenant.email, margin, 148)
doc.text(`${property.name}${unit ? ` — Unit ${unit.unit_number}` : ""}`, margin, 166)
if (property.address_line1) {
doc.text(`${property.address_line1}${property.city ? `, ${property.city}` : ""}${property.state ? `, ${property.state}` : ""}`, margin, 184)
}
// Body
doc.setFontSize(11)
doc.setTextColor(60, 60, 80)
const body = [
`Dear ${tenant.first_name} ${tenant.last_name},`,
"",
`This notice is to inform you that your rent payment of $${Number(payment.amount).toFixed(2)} was due`,
`on ${new Date(payment.due_date).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}`,
`and is now ${daysLate} day${daysLate !== 1 ? "s" : ""} past due.`,
"",
"Please arrange payment immediately to avoid further action. If you have already",
"made this payment, please disregard this notice and contact your landlord.",
"",
"If you are experiencing financial difficulties, please contact us as soon as",
"possible to discuss payment arrangements.",
]
let y = 230
body.forEach((line) => {
doc.text(line, margin, y)
y += 18
})
// Payment box
doc.setFillColor(254, 242, 242)
doc.roundedRect(margin, y + 10, pageW - margin * 2, 70, 8, 8, "F")
doc.setFontSize(10)
doc.setTextColor(153, 27, 27)
doc.text("Amount Due", margin + 20, y + 35)
doc.setFontSize(20)
doc.setFont("helvetica", "bold")
doc.text(`$${Number(payment.amount).toFixed(2)}`, margin + 20, y + 62)
doc.setFontSize(10)
doc.setFont("helvetica", "normal")
doc.text(`Due since: ${new Date(payment.due_date).toLocaleDateString()}`, pageW - margin - 20, y + 48, { align: "right" })
// Signature
y += 120
doc.setFontSize(10)
doc.setTextColor(100, 100, 120)
doc.text("Sincerely,", margin, y)
doc.text("Property Management Network", margin, y + 20)
doc.text("propertymanagement.network", margin, y + 36)
// Footer
doc.setFontSize(8)
doc.setTextColor(180, 180, 200)
doc.text("This is an official notice. Please retain for your records.", margin, 780)
const filename = `late-notice-${tenant.last_name.toLowerCase()}-${payment.due_date}.pdf`
doc.save(filename)
toast.success("Late notice downloaded")
} catch {
toast.error("Failed to generate notice")
} finally {
setLoading(false)
}
}
if (payment.status !== "overdue") return null
return (
<button
onClick={generateNotice}
disabled={loading}
title="Download Late Notice"
className="flex items-center gap-1.5 rounded-lg border border-red-500/20 px-2.5 py-1.5 text-xs text-red-400 hover:border-red-500/40 hover:bg-red-500/5 transition disabled:opacity-40"
>
<FileWarning className="h-3.5 w-3.5" />
{loading ? "…" : "Notice"}
</button>
)
}