274 lines
9.2 KiB
TypeScript
274 lines
9.2 KiB
TypeScript
// routes/tenant/applications/new/index.tsx
|
|
import { define } from "../../../../utils.ts";
|
|
import { page } from "fresh";
|
|
import { getLeasesByTenant } from "../../../../services/leaseService.ts";
|
|
import { createTenantApplication } from "../../../../services/tenantApplicationService.ts";
|
|
import { uploadToLocalBlobStorage } from "../../../../utils/blobStorage.ts";
|
|
import { Lease } from "../../../../db/interfaces.ts";
|
|
import { FormField } from "../../maintenance/new/components/FormField.tsx";
|
|
import { TextAreaField } from "../../maintenance/new/components/TextAreaField.tsx";
|
|
import { FileUpload } from "../../maintenance/new/components/FileUpload.tsx";
|
|
import { AlertMessage } from "../../maintenance/new/components/AlertMessage.tsx";
|
|
import { SelectField } from "../../maintenance/new/components/SelectField.tsx";
|
|
|
|
interface NewApplicationPageData {
|
|
currentLease?: Lease;
|
|
error?: string;
|
|
}
|
|
|
|
const EMPLOYMENT_STATUSES = [
|
|
{ value: "employed", label: "Employed" },
|
|
{ value: "self_employed", label: "Self-Employed" },
|
|
{ value: "student", label: "Student" },
|
|
{ value: "unemployed", label: "Unemployed" },
|
|
];
|
|
|
|
export const handler = define.handlers({
|
|
async GET(ctx) {
|
|
const { user_id } = ctx.state.data;
|
|
|
|
try {
|
|
const leases = await getLeasesByTenant(user_id);
|
|
const currentLease = leases?.[0]; // Assuming tenant has one active lease
|
|
|
|
return page<NewApplicationPageData>({
|
|
currentLease,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error loading new application form:", error);
|
|
return page<NewApplicationPageData>({
|
|
error: "Failed to load form",
|
|
});
|
|
}
|
|
},
|
|
|
|
async POST(ctx) {
|
|
const { user_id } = ctx.state.data;
|
|
|
|
try {
|
|
const formData = await ctx.req.formData();
|
|
const property_id = formData.get("property_id") as string;
|
|
const monthly_income = formData.get("monthly_income") as string;
|
|
const employment_status = formData.get("employment_status") as string;
|
|
const current_address = formData.get("current_address") as string;
|
|
const notes = formData.get("notes") as string;
|
|
|
|
if (
|
|
!property_id || !monthly_income || !employment_status ||
|
|
!current_address
|
|
) {
|
|
return new Response("Missing required fields", { status: 400 });
|
|
}
|
|
|
|
// 1. Handle file uploads
|
|
const documentUrls: { url: string; name: string; type: string }[] = [];
|
|
const files = formData.getAll("documents") as File[];
|
|
|
|
for (const file of files) {
|
|
if (file.size > 0) {
|
|
const blob = new Blob([await file.arrayBuffer()], {
|
|
type: file.type,
|
|
});
|
|
const uploadResult = await uploadToLocalBlobStorage(
|
|
file.name,
|
|
blob,
|
|
"image",
|
|
{ useTimestamp: true },
|
|
);
|
|
if (uploadResult.success && uploadResult.url) {
|
|
documentUrls.push({
|
|
url: uploadResult.url,
|
|
name: file.name,
|
|
type: file.type,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Prepare application data (to be encrypted)
|
|
const applicationData = {
|
|
monthly_income: parseFloat(monthly_income),
|
|
employment_status,
|
|
current_address,
|
|
notes,
|
|
// In a real app, this would be encrypted. For now, we stringify the JSON.
|
|
};
|
|
const application_data_encrypted = JSON.stringify(applicationData);
|
|
|
|
// 3. Create the application
|
|
const [newApplication] = await createTenantApplication(
|
|
property_id,
|
|
user_id,
|
|
application_data_encrypted,
|
|
documentUrls.length > 0 ? documentUrls : null,
|
|
);
|
|
|
|
// Redirect to the new application's detail page
|
|
return new Response("", {
|
|
status: 302,
|
|
headers: { Location: `/tenant/applications/${newApplication.id}` },
|
|
});
|
|
} catch (error) {
|
|
console.error("Error submitting new application:", error);
|
|
return page<NewApplicationPageData>({
|
|
error: "Failed to submit application. Please try again.",
|
|
});
|
|
}
|
|
},
|
|
});
|
|
|
|
export default define.page(function NewTenantApplicationPage({ data }) {
|
|
const { currentLease, error } = data as NewApplicationPageData;
|
|
|
|
if (error && error !== "Failed to load form") {
|
|
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 (!currentLease) {
|
|
return (
|
|
<div class="min-h-screen bg-gray-50 p-8">
|
|
<AlertMessage
|
|
type="error"
|
|
message={error ||
|
|
"You must have an active lease to submit an application. Please contact support."}
|
|
/>
|
|
<div class="mt-6">
|
|
<a
|
|
href="/tenant/dashboard"
|
|
class="text-blue-600 hover:text-blue-500 font-medium"
|
|
>
|
|
Go to Dashboard
|
|
</a>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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">
|
|
<h1 class="text-3xl font-bold text-gray-900">
|
|
New Rental Application
|
|
</h1>
|
|
<p class="mt-1 text-sm text-gray-500">
|
|
Submit a new application for a rental property.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<div class="max-w-4xl mx-auto sm:px-6 lg:px-8 py-8">
|
|
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
|
<div class="p-6">
|
|
<form method="POST" encType="multipart/form-data">
|
|
{/* Hidden Fields */}
|
|
<input
|
|
type="hidden"
|
|
name="property_id"
|
|
value={currentLease.property_id ?? ""}
|
|
/>
|
|
|
|
<div class="space-y-6">
|
|
{error && <AlertMessage type="error" message={error} />}
|
|
|
|
<h2 class="text-xl font-semibold text-gray-900 border-b pb-2">
|
|
Applicant Details
|
|
</h2>
|
|
|
|
<FormField label="Current Address" required>
|
|
<TextAreaField
|
|
id="current_address"
|
|
name="current_address"
|
|
rows={3}
|
|
required
|
|
placeholder="Your current residential address"
|
|
/>
|
|
</FormField>
|
|
|
|
<FormField label="Monthly Income ($)" required>
|
|
<input
|
|
type="number"
|
|
name="monthly_income"
|
|
id="monthly_income"
|
|
required
|
|
min="0"
|
|
step="0.01"
|
|
placeholder="e.g., 5000.00"
|
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</FormField>
|
|
|
|
<FormField label="Employment Status" required>
|
|
<SelectField
|
|
id="employment_status"
|
|
name="employment_status"
|
|
options={EMPLOYMENT_STATUSES}
|
|
required
|
|
/>
|
|
</FormField>
|
|
|
|
<h2 class="text-xl font-semibold text-gray-900 border-b pb-2 mt-8">
|
|
Supporting Documents
|
|
</h2>
|
|
<p class="text-sm text-gray-500">
|
|
Please upload documents such as pay stubs, bank statements, or
|
|
ID.
|
|
</p>
|
|
|
|
<FormField label="Documents (Optional)">
|
|
<FileUpload name="documents" multiple />
|
|
</FormField>
|
|
|
|
<h2 class="text-xl font-semibold text-gray-900 border-b pb-2 mt-8">
|
|
Additional Notes
|
|
</h2>
|
|
|
|
<FormField label="Notes (Optional)">
|
|
<TextAreaField
|
|
id="notes"
|
|
name="notes"
|
|
rows={4}
|
|
placeholder="Any additional information you would like to provide..."
|
|
/>
|
|
</FormField>
|
|
|
|
{/* Submit Button */}
|
|
<div class="flex justify-between pt-6">
|
|
<a
|
|
href="/tenant/applications"
|
|
class="inline-flex justify-center py-2 px-4 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
>
|
|
Cancel
|
|
</a>
|
|
<button
|
|
type="submit"
|
|
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
>
|
|
Submit Application
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|