221 lines
7.3 KiB
TypeScript
221 lines
7.3 KiB
TypeScript
import { useState } from "preact/hooks";
|
|
import type { ConfirmAction } from "./LeaseApprovalConfirmDialog.tsx";
|
|
import LeaseApprovalConfirmDialog from "./LeaseApprovalConfirmDialog.tsx";
|
|
|
|
export interface PendingApprovalItem {
|
|
id: string;
|
|
property?: {
|
|
title?: string | null;
|
|
address_1?: string | null;
|
|
city?: string | null;
|
|
state?: string | null;
|
|
zip?: string | null;
|
|
monthly_rent?: number | null;
|
|
bedrooms?: number | null;
|
|
bathrooms?: number | null;
|
|
} | null;
|
|
property_address?: string | null;
|
|
tenant?: { name?: string | null; email?: string | null } | null;
|
|
approval_requested_at?: number | null;
|
|
}
|
|
|
|
interface LandlordPendingApprovalsProps {
|
|
approvals: PendingApprovalItem[];
|
|
}
|
|
|
|
function formatAddress(prop: PendingApprovalItem["property"]): string | null {
|
|
if (!prop) return null;
|
|
const parts = [
|
|
prop.address_1,
|
|
[prop.city, prop.state].filter(Boolean).join(", "),
|
|
prop.zip,
|
|
].filter(Boolean);
|
|
return parts.length > 0 ? parts.join(", ") : null;
|
|
}
|
|
|
|
function formatRentBeds(prop: PendingApprovalItem["property"]): string | null {
|
|
if (!prop) return null;
|
|
const hasRent = prop.monthly_rent != null;
|
|
const hasBeds = prop.bedrooms != null || prop.bathrooms != null;
|
|
if (!hasRent && !hasBeds) return null;
|
|
const rent = hasRent
|
|
? `$${Number(prop.monthly_rent).toLocaleString()}/mo`
|
|
: null;
|
|
const beds = hasBeds
|
|
? `${prop.bedrooms ?? "—"} bed, ${prop.bathrooms ?? "—"} bath`
|
|
: null;
|
|
return [rent, beds].filter(Boolean).join(" · ");
|
|
}
|
|
|
|
export default function LandlordPendingApprovals({
|
|
approvals,
|
|
}: LandlordPendingApprovalsProps) {
|
|
const [confirmState, setConfirmState] = useState<
|
|
{
|
|
action: ConfirmAction;
|
|
leaseId: string;
|
|
} | null
|
|
>(null);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
const handleApproveClick = (e: Event, leaseId: string) => {
|
|
e.preventDefault();
|
|
setConfirmState({ action: "approve", leaseId });
|
|
};
|
|
|
|
const handleRejectClick = (e: Event, leaseId: string) => {
|
|
e.preventDefault();
|
|
setConfirmState({ action: "reject", leaseId });
|
|
};
|
|
|
|
const handleConfirm = async () => {
|
|
if (!confirmState) return;
|
|
setSubmitting(true);
|
|
const url = confirmState.action === "approve"
|
|
? "/api/lease/approve"
|
|
: "/api/lease/reject";
|
|
const body = confirmState.action === "reject"
|
|
? { lease_id: confirmState.leaseId, rejection_reason: "" }
|
|
: { lease_id: confirmState.leaseId };
|
|
try {
|
|
const res = await fetch(url, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (res.redirected) {
|
|
window.location.href = res.url;
|
|
return;
|
|
}
|
|
if (res.ok) {
|
|
window.location.reload();
|
|
return;
|
|
}
|
|
} finally {
|
|
setSubmitting(false);
|
|
setConfirmState(null);
|
|
}
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
if (!submitting) setConfirmState(null);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div class="dashboard-card dashboard-card--no-overflow">
|
|
<div class="dashboard-card-body">
|
|
<div class="dashboard-section-header">
|
|
<h3 class="dashboard-section-title">
|
|
Pending Lease Approvals
|
|
</h3>
|
|
<a href="/landlord/approvals" class="dashboard-section-link">
|
|
View all
|
|
</a>
|
|
</div>
|
|
<div class="dashboard-list-spaced">
|
|
{approvals && approvals.length > 0
|
|
? (
|
|
approvals.slice(0, 3).map((approval) => {
|
|
const prop = approval.property;
|
|
const addressLine = formatAddress(prop);
|
|
const rentBeds = formatRentBeds(prop);
|
|
return (
|
|
<div
|
|
key={approval.id}
|
|
class="dashboard-row dashboard-row--blue"
|
|
>
|
|
<div class="dashboard-approval-details">
|
|
<p class="dashboard-row-label">
|
|
{prop?.title || approval.property_address ||
|
|
"Property"}
|
|
</p>
|
|
{addressLine && (
|
|
<p class="dashboard-row-muted">{addressLine}</p>
|
|
)}
|
|
{rentBeds && (
|
|
<p class="dashboard-row-meta">{rentBeds}</p>
|
|
)}
|
|
<p class="dashboard-row-muted">
|
|
Tenant: {approval.tenant?.name ||
|
|
approval.tenant?.email || "Unknown"}
|
|
</p>
|
|
<p class="dashboard-row-meta">
|
|
Requested {approval.approval_requested_at
|
|
? new Date(
|
|
approval.approval_requested_at as number,
|
|
)
|
|
.toLocaleDateString()
|
|
: "Unknown date"}
|
|
</p>
|
|
</div>
|
|
<div class="dashboard-actions-inline">
|
|
<form
|
|
method="POST"
|
|
action="/api/lease/approve"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
handleApproveClick(e, approval.id);
|
|
}}
|
|
>
|
|
<input
|
|
type="hidden"
|
|
name="lease_id"
|
|
value={approval.id}
|
|
/>
|
|
<button
|
|
type="submit"
|
|
class="dashboard-badge dashboard-badge--green"
|
|
disabled={submitting}
|
|
>
|
|
Approve
|
|
</button>
|
|
</form>
|
|
<form
|
|
method="POST"
|
|
action="/api/lease/reject"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
handleRejectClick(e, approval.id);
|
|
}}
|
|
>
|
|
<input
|
|
type="hidden"
|
|
name="lease_id"
|
|
value={approval.id}
|
|
/>
|
|
<button
|
|
type="submit"
|
|
class="dashboard-badge dashboard-badge--red"
|
|
disabled={submitting}
|
|
>
|
|
Reject
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
)
|
|
: (
|
|
<div class="dashboard-empty">
|
|
<p class="dashboard-empty-text">
|
|
No pending lease approvals
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<LeaseApprovalConfirmDialog
|
|
open={confirmState !== null}
|
|
action={confirmState?.action ?? "approve"}
|
|
leaseId={confirmState?.leaseId ?? ""}
|
|
onConfirm={handleConfirm}
|
|
onCancel={handleCancel}
|
|
/>
|
|
</>
|
|
);
|
|
}
|