271 lines
8.8 KiB
TypeScript
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>
|
|
);
|
|
});
|