2026-02-14 02:20:19 +00:00

310 lines
10 KiB
TypeScript

// routes/tenant/property/index.tsx
import { define } from "../../../utils.ts";
import { page } from "fresh";
import { getLeasesByTenant } from "../../../services/leaseService.ts";
import { getPropertyById } from "../../../services/propertyService.ts";
import { getMediaByProperty } from "../../../services/propertyMediaService.ts";
import { getDocumentsByLease } from "../../../services/leaseDocumentService.ts";
import {
Lease,
LeaseDocument,
Property,
PropertyMedia,
} from "../../../db/interfaces.ts";
import { AlertMessage } from "../maintenance/new/components/AlertMessage.tsx";
interface PropertyPageData {
lease?: Lease;
property?: Property;
propertyPhotos?: PropertyMedia[];
leaseDocuments?: LeaseDocument[];
error?: string;
}
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 the first lease is the current active one
if (!currentLease || !currentLease.property_id) {
return page<PropertyPageData>({
error: "No active lease found for a property.",
});
}
const property = await getPropertyById(currentLease.property_id);
const propertyPhotos = await getMediaByProperty(currentLease.property_id);
const leaseDocuments = await getDocumentsByLease(currentLease.id);
return page<PropertyPageData>({
lease: currentLease,
property,
propertyPhotos: propertyPhotos.filter((m: PropertyMedia) =>
m.media_type === "photo"
),
leaseDocuments,
});
} catch (error) {
console.error("Error loading property details:", error);
return page<PropertyPageData>({
error: "Failed to load property details.",
});
}
},
});
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>
);
}
function formatPropertyAddress(property: Property) {
const street = property.address_1?.trim() || "";
const cityState = [property.city, property.state].filter(Boolean).join(", ");
const zip = property.zip?.trim() || "";
const region = [cityState, zip].filter(Boolean).join(" ").trim();
return [street, region].filter(Boolean).join(", ").trim();
}
function parseList(raw: string | null | undefined): string[] {
if (!raw) return [];
try {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) {
return parsed.map((item) => String(item)).filter(Boolean);
}
} catch {
// Fall through to comma-separated parsing.
}
return raw.split(",").map((item) => item.trim()).filter(Boolean);
}
function DocumentList({ documents }: { documents: LeaseDocument[] }) {
return (
<ul class="divide-y divide-gray-200 border border-gray-200 rounded-md">
{documents.length > 0
? (
documents.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="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.file_name || "Document"} ({doc.document_type})
</span>
</div>
<div class="ml-4 shrink-0">
{doc.file_url
? (
<a
href={doc.file_url}
target="_blank"
class="font-medium text-blue-600 hover:text-blue-500"
>
View
</a>
)
: <span class="text-gray-400">Unavailable</span>}
</div>
</li>
))
)
: (
<li class="px-6 py-4 text-sm text-gray-500 text-center">
No documents found.
</li>
)}
</ul>
);
}
export default define.page(
function TenantPropertyPage({ data }: { data: PropertyPageData }) {
const { lease, property, propertyPhotos, leaseDocuments, error } =
data as PropertyPageData;
if (error) {
return (
<div class="min-h-screen bg-gray-50 p-8">
<AlertMessage type="error">{error}</AlertMessage>
<div class="mt-6">
<a
href="/tenant/dashboard"
class="text-blue-600 hover:text-blue-500 font-medium"
>
Go to Dashboard
</a>
</div>
</div>
);
}
if (!property) {
return (
<div class="min-h-screen bg-gray-50 p-8">
<AlertMessage type="error">
Property details could not be loaded.
</AlertMessage>
</div>
);
}
const addressLine = formatPropertyAddress(property);
const amenities = parseList(property.amenities);
const rules = parseList(lease?.late_fee_rules ?? "");
const photoItems = (propertyPhotos ?? []).filter(
(photo): photo is PropertyMedia & { file_url: string } =>
Boolean(photo.file_url),
);
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">
Your Current Residence
</h1>
<p class="mt-1 text-sm text-gray-500">
{addressLine || "Address not available"}
</p>
</div>
</div>
</div>
{/* Main Content */}
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 py-8 space-y-8">
{/* Property Photos */}
{photoItems.length > 0 && (
<div class="bg-white shadow overflow-hidden sm:rounded-lg p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4 border-b pb-2">
Property Photos
</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{photoItems.map((photo) => (
<img
src={photo.file_url}
alt={photo.file_name || "Property Photo"}
class="w-full h-48 object-cover rounded-lg shadow-md"
/>
))}
</div>
</div>
)}
{/* Property Details */}
<div class="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">
Property Details
</h3>
</div>
<div class="border-t border-gray-200">
<dl>
<DetailItem
label="Address"
value={addressLine || "Address not available"}
/>
<DetailItem
label="Unit Number"
value={property.address_2}
/>
<DetailItem label="Bedrooms" value={property.bedrooms} />
<DetailItem label="Bathrooms" value={property.bathrooms} />
<DetailItem
label="Square Footage"
value={property.square_footage}
/>
<DetailItem label="Year Built" value={property.year_built} />
</dl>
</div>
</div>
{/* Amenities */}
<div class="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">
Amenities
</h3>
</div>
<div class="border-t border-gray-200 p-6">
{amenities.length > 0
? (
<ul class="list-disc list-inside space-y-1 text-sm text-gray-700">
{amenities.map((amenity: string) => (
<li key={amenity}>{amenity}</li>
))}
</ul>
)
: <p class="text-sm text-gray-500">No amenities listed.</p>}
</div>
</div>
{/* Rules */}
<div class="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">
Community Rules
</h3>
</div>
<div class="border-t border-gray-200 p-6">
{rules.length > 0
? (
<ul class="list-disc list-inside space-y-1 text-sm text-gray-700">
{rules.map((rule: string) => <li key={rule}>{rule}</li>)}
</ul>
)
: (
<p class="text-sm text-gray-500">
No specific rules listed.
</p>
)}
</div>
</div>
{/* Lease Documents */}
<div class="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">
Lease Documents
</h3>
<p class="mt-1 max-w-2xl text-sm text-gray-500">
Your current lease agreement and related documents.
</p>
</div>
<div class="border-t border-gray-200">
<DocumentList documents={leaseDocuments || []} />
</div>
</div>
</div>
</div>
);
},
);