397 lines
15 KiB
TypeScript
397 lines
15 KiB
TypeScript
// routes/landlord/onboarding/property-setup.tsx
|
|
import { define } from "../../../utils.ts";
|
|
import { page } from "fresh";
|
|
import type { SessionCookie } from "../../auth/callback.ts";
|
|
import { properties } from "../../../db/schema.ts";
|
|
import { db } from "../../../db/db.ts";
|
|
import { markRoleOnboarded } from "../../../services/onboardingService.ts";
|
|
import { addE2EProperty, isE2EEnabled } from "../../../services/e2eFixtures.ts";
|
|
|
|
export const handler = define.handlers({
|
|
async GET(ctx) {
|
|
const stateData = ctx.state.data as SessionCookie;
|
|
const { user_id } = stateData;
|
|
|
|
if (!user_id) {
|
|
return new Response("", {
|
|
status: 302,
|
|
headers: { Location: "/auth/login" },
|
|
});
|
|
}
|
|
|
|
return page({ stateData });
|
|
},
|
|
|
|
async POST(ctx) {
|
|
const stateData = ctx.state.data as SessionCookie;
|
|
const { user_id } = stateData;
|
|
|
|
if (!user_id) {
|
|
return new Response("", {
|
|
status: 302,
|
|
headers: { Location: "/auth/login" },
|
|
});
|
|
}
|
|
|
|
const formData = await ctx.req.formData();
|
|
|
|
const now = Date.now();
|
|
const id = crypto.randomUUID();
|
|
|
|
const propertyPayload = {
|
|
id,
|
|
owner_id: user_id,
|
|
address_1: (formData.get("address_1") || "").toString(),
|
|
address_2: (formData.get("address_2") || "").toString(),
|
|
city: (formData.get("city") || "").toString(),
|
|
state: (formData.get("state") || "").toString(),
|
|
zip: (formData.get("zip") || "").toString(),
|
|
country: (formData.get("country") || "").toString(),
|
|
property_type: (formData.get("property_type") || "").toString(),
|
|
bedrooms: parseInt((formData.get("bedrooms") || "0").toString(), 10) || 0,
|
|
bathrooms: parseInt((formData.get("bathrooms") || "0").toString(), 10) ||
|
|
0,
|
|
monthly_rent:
|
|
parseFloat((formData.get("monthly_rent") || "0").toString()) || 0,
|
|
security_deposit:
|
|
parseFloat((formData.get("security_deposit") || "0").toString()) || 0,
|
|
lease_duration_months: parseInt(
|
|
(formData.get("lease_duration_months") || "12").toString(),
|
|
10,
|
|
) || 12,
|
|
amenities: (formData.get("amenities") || "").toString(),
|
|
description: (formData.get("description") || "").toString(),
|
|
rental_status: "draft",
|
|
created_on: now,
|
|
updated_on: now,
|
|
};
|
|
|
|
if (isE2EEnabled()) {
|
|
addE2EProperty(user_id, propertyPayload);
|
|
} else {
|
|
await db.insert(properties).values(propertyPayload);
|
|
}
|
|
|
|
// Mark onboarding complete (step 0 or null effectively means finished)
|
|
await markRoleOnboarded(user_id, "landlord");
|
|
|
|
return new Response("", {
|
|
status: 302,
|
|
headers: { Location: "/landlord/dashboard" },
|
|
});
|
|
},
|
|
});
|
|
|
|
export default define.page(function PropertySetup({ data }) {
|
|
const { stateData, error } = data as {
|
|
stateData: SessionCookie;
|
|
error?: string;
|
|
};
|
|
|
|
return (
|
|
<div class="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
|
<div class="max-w-3xl mx-auto">
|
|
<div class="text-center mb-8">
|
|
<h2 class="text-3xl font-extrabold text-gray-900">
|
|
Add Your First Rental Property
|
|
</h2>
|
|
<p class="mt-2 text-sm text-gray-600">
|
|
Enter details for at least one property to complete onboarding.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="bg-white shadow sm:rounded-lg">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
{error && (
|
|
<div class="mb-6 bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<form method="POST" class="space-y-6">
|
|
<div>
|
|
<h3 class="text-lg font-medium text-gray-900 mb-4">
|
|
Property Address
|
|
</h3>
|
|
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
|
<div class="sm:col-span-2">
|
|
<label
|
|
for="address_1"
|
|
class="block text-sm font-medium text-gray-700"
|
|
>
|
|
Street Address *
|
|
</label>
|
|
<input
|
|
id="address_1"
|
|
name="address_1"
|
|
type="text"
|
|
required
|
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div class="sm:col-span-2">
|
|
<label
|
|
for="address_2"
|
|
class="block text-sm font-medium text-gray-700"
|
|
>
|
|
Apartment, suite, etc. (optional)
|
|
</label>
|
|
<input
|
|
id="address_2"
|
|
name="address_2"
|
|
type="text"
|
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="city"
|
|
class="block text-sm font-medium text-gray-700"
|
|
>
|
|
City *
|
|
</label>
|
|
<input
|
|
id="city"
|
|
name="city"
|
|
type="text"
|
|
required
|
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="state"
|
|
class="block text-sm font-medium text-gray-700"
|
|
>
|
|
State/Province *
|
|
</label>
|
|
<input
|
|
id="state"
|
|
name="state"
|
|
type="text"
|
|
required
|
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="zip"
|
|
class="block text-sm font-medium text-gray-700"
|
|
>
|
|
ZIP/Postal Code *
|
|
</label>
|
|
<input
|
|
id="zip"
|
|
name="zip"
|
|
type="text"
|
|
required
|
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="country"
|
|
class="block text-sm font-medium text-gray-700"
|
|
>
|
|
Country *
|
|
</label>
|
|
<select
|
|
id="country"
|
|
name="country"
|
|
required
|
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
>
|
|
<option value="US">United States</option>
|
|
<option value="CA">Canada</option>
|
|
<option value="UK">United Kingdom</option>
|
|
<option value="AU">Australia</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 class="text-lg font-medium text-gray-900 mb-4">
|
|
Property Details
|
|
</h3>
|
|
<div class="grid grid-cols-1 gap-6 sm:grid-cols-3">
|
|
<div>
|
|
<label
|
|
for="property_type"
|
|
class="block text-sm font-medium text-gray-700"
|
|
>
|
|
Property Type *
|
|
</label>
|
|
<select
|
|
id="property_type"
|
|
name="property_type"
|
|
required
|
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
>
|
|
<option value="">Select type</option>
|
|
<option value="apartment">Apartment</option>
|
|
<option value="house">House</option>
|
|
<option value="condo">Condo</option>
|
|
<option value="townhouse">Townhouse</option>
|
|
<option value="studio">Studio</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="bedrooms"
|
|
class="block text-sm font-medium text-gray-700"
|
|
>
|
|
Bedrooms *
|
|
</label>
|
|
<select
|
|
id="bedrooms"
|
|
name="bedrooms"
|
|
required
|
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
>
|
|
<option value="0">Studio</option>
|
|
<option value="1">1 Bedroom</option>
|
|
<option value="2">2 Bedrooms</option>
|
|
<option value="3">3 Bedrooms</option>
|
|
<option value="4">4 Bedrooms</option>
|
|
<option value="5">5+ Bedrooms</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="bathrooms"
|
|
class="block text-sm font-medium text-gray-700"
|
|
>
|
|
Bathrooms *
|
|
</label>
|
|
<select
|
|
id="bathrooms"
|
|
name="bathrooms"
|
|
required
|
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
>
|
|
<option value="1">1 Bathroom</option>
|
|
<option value="2">2 Bathrooms</option>
|
|
<option value="3">3 Bathrooms</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="monthly_rent"
|
|
class="block text-sm font-medium text-gray-700"
|
|
>
|
|
Monthly Rent ($) *
|
|
</label>
|
|
<input
|
|
id="monthly_rent"
|
|
name="monthly_rent"
|
|
type="number"
|
|
min="0"
|
|
step="0.01"
|
|
required
|
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="security_deposit"
|
|
class="block text-sm font-medium text-gray-700"
|
|
>
|
|
Security Deposit ($) *
|
|
</label>
|
|
<input
|
|
id="security_deposit"
|
|
name="security_deposit"
|
|
type="number"
|
|
min="0"
|
|
step="0.01"
|
|
required
|
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div class="sm:col-span-3">
|
|
<label
|
|
for="lease_duration_months"
|
|
class="block text-sm font-medium text-gray-700"
|
|
>
|
|
Lease Duration (months) *
|
|
</label>
|
|
<select
|
|
id="lease_duration_months"
|
|
name="lease_duration_months"
|
|
required
|
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
>
|
|
<option value="6">6 months</option>
|
|
<option value="12" selected>12 months</option>
|
|
<option value="18">18 months</option>
|
|
<option value="24">24 months</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 class="text-lg font-medium text-gray-900 mb-4">
|
|
Additional Information
|
|
</h3>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label
|
|
for="amenities"
|
|
class="block text-sm font-medium text-gray-700"
|
|
>
|
|
Amenities (comma-separated)
|
|
</label>
|
|
<input
|
|
id="amenities"
|
|
name="amenities"
|
|
type="text"
|
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="description"
|
|
class="block text-sm font-medium text-gray-700"
|
|
>
|
|
Property Description
|
|
</label>
|
|
<textarea
|
|
id="description"
|
|
name="description"
|
|
rows={4}
|
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
>
|
|
</textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end pt-6">
|
|
<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"
|
|
>
|
|
Save Property & Finish
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|