2026-01-20 09:02:26 -06:00

271 lines
8.8 KiB
TypeScript

// routes/tenant/applications/[id]/index.tsx
import { define } from "../../../../utils.ts";
import { page } from "fresh";
import { getApplicationById } from "../../../../services/tenantApplicationService.ts";
import { TenantApplication } from "../../../../db/interfaces.ts";
import { AlertMessage } from "../../maintenance/new/components/AlertMessage.tsx";
interface ApplicationDetailPageData {
application?: TenantApplication;
parsedData?: Record<string, unknown>;
documentUrls?: { url: string; name: string; type: string }[];
error?: string;
}
export const handler = define.handlers({
async GET(ctx) {
const { user_id } = ctx.state.data;
const applicationId = ctx.params.id;
if (!user_id) {
return new Response("", {
status: 302,
headers: { Location: "/auth/login" },
});
}
try {
const application = await getApplicationById(applicationId);
if (!application || application.applicant_id !== user_id) {
return page<ApplicationDetailPageData>({
error: "Application not found or unauthorized access.",
});
}
let parsedData: Record<string, unknown> = {};
let documentUrls: { url: string; name: string; type: string }[] = [];
if (application.application_data_encrypted) {
// Assuming application_data_encrypted is a JSON string for now
try {
parsedData = JSON.parse(application.application_data_encrypted);
} catch (e) {
console.error("Failed to parse application data:", e);
parsedData = { error: "Failed to parse application data." };
}
}
if (application.document_urls_json) {
try {
documentUrls = JSON.parse(application.document_urls_json as string);
} catch (e) {
console.error("Failed to parse document URLs:", e);
}
}
return page<ApplicationDetailPageData>({
application,
parsedData,
documentUrls,
});
} catch (error) {
console.error("Error loading application details:", error);
return page<ApplicationDetailPageData>({
error: "Failed to load application details.",
});
}
},
});
function ApplicationStatusPill({ status }: { status: string }) {
let colorClass = "bg-gray-100 text-gray-800";
switch (status.toLowerCase()) {
case "submitted":
case "pending":
colorClass = "bg-yellow-100 text-yellow-800";
break;
case "approved":
colorClass = "bg-green-100 text-green-800";
break;
case "rejected":
colorClass = "bg-red-100 text-red-800";
break;
}
return (
<span
class={`px-3 py-1 inline-flex text-sm leading-5 font-semibold rounded-full ${colorClass} capitalize`}
>
{status}
</span>
);
}
function DetailItem(
{ label, value }: {
label: string;
value: string | number | null | undefined;
},
) {
return (
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">{label}</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{value !== undefined ? value : "N/A"}
</dd>
</div>
);
}
export default define.page(function ApplicationDetailPage({ data }) {
const { application, parsedData, documentUrls, error } =
data as ApplicationDetailPageData;
if (error) {
return (
<div class="min-h-screen bg-gray-50 p-8">
<AlertMessage type="error" message={error} />
<div class="mt-6">
<a
href="/tenant/applications"
class="text-blue-600 hover:text-blue-500 font-medium"
>
Go back to Applications
</a>
</div>
</div>
);
}
if (!application) {
return (
<div class="min-h-screen bg-gray-50 p-8">
<AlertMessage type="error" message="Application data is missing." />
</div>
);
}
const submittedDate = application.submitted_on
? new Date(application.submitted_on).toLocaleDateString()
: "N/A";
const reviewedDate = application.reviewed_on
? new Date(application.reviewed_on).toLocaleDateString()
: "N/A";
const status = application.application_status || "Unknown";
return (
<div class="min-h-screen bg-gray-50">
{/* Header */}
<div class="bg-white shadow">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="py-6 flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-gray-900">
Application Details
</h1>
<p class="mt-1 text-sm text-gray-500">
Application ID: {application.id.substring(0, 8)}...
</p>
</div>
<ApplicationStatusPill status={status} />
</div>
</div>
</div>
{/* Main Content */}
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 py-8">
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
<div class="border-t border-gray-200">
<dl>
<DetailItem label="Status" value={status} />
<DetailItem label="Submitted On" value={submittedDate} />
<DetailItem label="Reviewed On" value={reviewedDate} />
<DetailItem label="Property ID" value={application.property_id} />
<DetailItem
label="Applicant ID"
value={application.applicant_id}
/>
</dl>
</div>
</div>
{/* Application Data */}
<div class="mt-8 bg-white shadow overflow-hidden sm:rounded-lg">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
Application Data
</h3>
<p class="mt-1 max-w-2xl text-sm text-gray-500">
Details provided in the application form.
</p>
</div>
<div class="border-t border-gray-200">
<dl>
<DetailItem
label="Monthly Income"
value={parsedData?.monthly_income
? `$${(parsedData.monthly_income as number).toFixed(2)}`
: "N/A"}
/>
<DetailItem
label="Employment Status"
value={parsedData?.employment_status as string}
/>
<DetailItem
label="Current Address"
value={parsedData?.current_address as string}
/>
<DetailItem label="Notes" value={parsedData?.notes as string} />
</dl>
</div>
</div>
{/* Documents */}
<div class="mt-8 bg-white shadow overflow-hidden sm:rounded-lg">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
Uploaded Documents
</h3>
<p class="mt-1 max-w-2xl text-sm text-gray-500">
Supporting documents submitted with the application.
</p>
</div>
<div class="border-t border-gray-200">
<ul class="divide-y divide-gray-200">
{documentUrls && documentUrls.length > 0
? (
documentUrls.map((doc) => (
<li class="pl-3 pr-4 py-3 flex items-center justify-between text-sm">
<div class="w-0 flex-1 flex items-center">
<svg
class="flex-shrink-0 h-5 w-5 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z"
clip-rule="evenodd"
/>
</svg>
<span class="ml-2 flex-1 w-0 truncate">
{doc.name}
</span>
</div>
<div class="ml-4 flex-shrink-0">
<a
href={doc.url}
target="_blank"
class="font-medium text-blue-600 hover:text-blue-500"
>
Download
</a>
</div>
</li>
))
)
: (
<li class="px-6 py-4 text-sm text-gray-500 text-center">
No documents were uploaded.
</li>
)}
</ul>
</div>
</div>
</div>
</div>
);
});