Deploy config: - .do/app.yaml: build the Dockerfile directly from GitHub (deploy_on_push) instead of a pre-built DOCR image; NEXT_PUBLIC_* set RUN_AND_BUILD_TIME with the propertymanagement.network domain so they bake into the client bundle; add custom domains block (apex + www); wire Sentry DSN (server + browser). Included pending work from the audit-fixes branch: - AI provider abstraction (OpenAI/Anthropic, admin-selectable; Anthropic default) - Per-landlord e-signature (DocuSign OAuth + Dropbox Sign) + migration 0010 - Outbound webhooks / Zapier integration - PayPal removal (Stripe-only billing) - Storage hardening (fail-loud when Spaces unconfigured), security fixes Verified: full production Docker build (same build-args as DO) passes clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
124 lines
4.6 KiB
TypeScript
124 lines
4.6 KiB
TypeScript
"use client"
|
|
|
|
import { useTransition } from "react"
|
|
import Link from "next/link"
|
|
import { toast } from "sonner"
|
|
import { PenLine, CheckCircle2, Clock, XCircle, AlertTriangle, Download } from "lucide-react"
|
|
import { formatDate } from "@/lib/utils"
|
|
import { sendLeaseForSignatureAction } from "@/app/actions/esign"
|
|
|
|
type Req = {
|
|
id: string
|
|
provider: string
|
|
status: string
|
|
signer_email: string
|
|
document_name: string | null
|
|
signed_document_url: string | null
|
|
sent_at: string | null
|
|
completed_at: string | null
|
|
last_error: string | null
|
|
}
|
|
type Prov = { id: string; label: string }
|
|
|
|
const STATUS: Record<string, { label: string; cls: string; Icon: typeof Clock }> = {
|
|
sent: { label: "Awaiting signature", cls: "text-amber-400 bg-amber-500/10 border-amber-500/20", Icon: Clock },
|
|
signed: { label: "Signed", cls: "text-emerald-400 bg-emerald-500/10 border-emerald-500/20", Icon: CheckCircle2 },
|
|
declined: { label: "Declined", cls: "text-red-400 bg-red-500/10 border-red-500/20", Icon: XCircle },
|
|
voided: { label: "Voided", cls: "text-white/40 bg-white/5 border-white/10", Icon: XCircle },
|
|
error: { label: "Failed", cls: "text-red-400 bg-red-500/10 border-red-500/20", Icon: AlertTriangle },
|
|
}
|
|
const PROVIDER_LABEL: Record<string, string> = { docusign: "DocuSign", dropbox_sign: "Dropbox Sign" }
|
|
|
|
export function EsignLease({
|
|
leaseId,
|
|
connected,
|
|
requests,
|
|
canSend,
|
|
disabledReason,
|
|
}: {
|
|
leaseId: string
|
|
connected: Prov[]
|
|
requests: Req[]
|
|
canSend: boolean
|
|
disabledReason: string
|
|
}) {
|
|
const [pending, start] = useTransition()
|
|
|
|
function send(provider: string) {
|
|
start(async () => {
|
|
try {
|
|
await sendLeaseForSignatureAction(leaseId, provider)
|
|
toast.success("Lease sent for signature")
|
|
} catch (e) {
|
|
toast.error((e as Error).message || "Couldn't send for signature")
|
|
}
|
|
})
|
|
}
|
|
|
|
return (
|
|
<div className="rounded-2xl border border-white/[0.06] bg-[#16161f] p-5">
|
|
<div className="mb-3 flex items-center gap-2">
|
|
<PenLine className="h-4 w-4 text-indigo-400" />
|
|
<h3 className="text-sm font-semibold text-white">E-signature</h3>
|
|
</div>
|
|
|
|
{requests.length > 0 && (
|
|
<ul className="mb-4 space-y-2">
|
|
{requests.map((r) => {
|
|
const s = STATUS[r.status] ?? STATUS.sent
|
|
return (
|
|
<li key={r.id} className="flex items-center justify-between gap-3 rounded-xl border border-white/[0.05] bg-white/[0.02] px-3 py-2.5">
|
|
<div className="min-w-0">
|
|
<p className="truncate text-xs font-medium text-white/80">{PROVIDER_LABEL[r.provider] ?? r.provider} · {r.signer_email}</p>
|
|
<p className="text-[11px] text-white/35">
|
|
Sent {r.sent_at ? formatDate(r.sent_at) : "—"}
|
|
{r.completed_at ? ` · Signed ${formatDate(r.completed_at)}` : ""}
|
|
{r.status === "error" && r.last_error ? ` · ${r.last_error}` : ""}
|
|
</p>
|
|
{r.signed_document_url && (
|
|
<a
|
|
href={r.signed_document_url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="mt-1 inline-flex items-center gap-1 text-[11px] text-indigo-400 transition hover:text-indigo-300"
|
|
>
|
|
<Download className="h-3 w-3" /> Signed document
|
|
</a>
|
|
)}
|
|
</div>
|
|
<span className={`flex shrink-0 items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] font-medium ${s.cls}`}>
|
|
<s.Icon className="h-3 w-3" /> {s.label}
|
|
</span>
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
)}
|
|
|
|
{connected.length === 0 ? (
|
|
<p className="text-xs text-white/30">
|
|
<Link href="/settings/integrations" className="text-indigo-400 hover:text-indigo-300">
|
|
Connect DocuSign or Dropbox Sign
|
|
</Link>{" "}
|
|
in Settings → Integrations to send leases for signature.
|
|
</p>
|
|
) : canSend ? (
|
|
<div className="flex flex-wrap gap-2">
|
|
{connected.map((p) => (
|
|
<button
|
|
key={p.id}
|
|
onClick={() => send(p.id)}
|
|
disabled={pending}
|
|
className="flex items-center gap-1.5 rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-indigo-500 disabled:opacity-50"
|
|
>
|
|
<PenLine className="h-3.5 w-3.5" /> Send via {p.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-xs text-white/30">{disabledReason}</p>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|