241 lines
9.0 KiB
TypeScript
241 lines
9.0 KiB
TypeScript
// routes/landlord/tenants/invites.tsx
|
|
import { define } from "../../../utils.ts";
|
|
import { page } from "fresh";
|
|
import type { SessionCookie } from "../../auth/callback.ts";
|
|
import type { Property, TenantInvite } from "../../../db/interfaces.ts";
|
|
import { getPropertiesByOwner } from "../../../services/propertyService.ts";
|
|
import { getTenantInvitesByLandlord } from "../../../services/tenantInviteService.ts";
|
|
import { getPropertyById } from "../../../services/propertyService.ts";
|
|
import DeleteTenantInviteButton from "../../../islands/DeleteTenantInviteButton.tsx";
|
|
|
|
interface InvitesPageData {
|
|
stateData: SessionCookie;
|
|
properties: Property[];
|
|
invites: (TenantInvite & { property?: Property })[];
|
|
error?: string;
|
|
}
|
|
|
|
function parseInvitedEmails(value?: string | null) {
|
|
if (!value) return [];
|
|
try {
|
|
const parsed = JSON.parse(value);
|
|
if (Array.isArray(parsed)) {
|
|
return parsed.filter((item) => typeof item === "string");
|
|
}
|
|
} catch (_error) {
|
|
// Fall back to comma-separated parsing.
|
|
}
|
|
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
}
|
|
|
|
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" },
|
|
});
|
|
}
|
|
|
|
try {
|
|
const properties = await getPropertiesByOwner(user_id);
|
|
const invites = await getTenantInvitesByLandlord(user_id);
|
|
|
|
// Enrich invites with property data
|
|
const enrichedInvites = await Promise.all(
|
|
invites.map(async (invite) => {
|
|
const property = invite.property_id
|
|
? await getPropertyById(invite.property_id)
|
|
: null;
|
|
return { ...invite, property };
|
|
})
|
|
);
|
|
|
|
return page({
|
|
stateData,
|
|
properties,
|
|
invites: enrichedInvites,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error loading tenant invites:", error);
|
|
return page({
|
|
stateData,
|
|
properties: [],
|
|
invites: [],
|
|
error: "Unable to load tenant invites. Please try again.",
|
|
});
|
|
}
|
|
},
|
|
});
|
|
|
|
export default define.page(function TenantInvitesPage({ data }) {
|
|
const { stateData, properties, invites, error } = data as InvitesPageData;
|
|
|
|
return (
|
|
<div class="min-h-screen bg-gray-50">
|
|
<div class="bg-white shadow">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex justify-between items-center py-6">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900">
|
|
Tenant Invites
|
|
</h1>
|
|
<p class="mt-1 text-sm text-gray-500">
|
|
Manage your tenant invitations and track their status
|
|
</p>
|
|
</div>
|
|
<div class="flex space-x-3">
|
|
<a
|
|
href="/landlord/tenants/invite"
|
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
|
|
>
|
|
Create New Invite
|
|
</a>
|
|
<a
|
|
href="/landlord/dashboard"
|
|
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
|
>
|
|
Back to Dashboard
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
{error && (
|
|
<div class="mb-6 bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{invites.length === 0
|
|
? (
|
|
<div class="text-center py-12">
|
|
<svg
|
|
class="mx-auto h-12 w-12 text-gray-400"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
|
/>
|
|
</svg>
|
|
<h3 class="mt-2 text-sm font-medium text-gray-900">
|
|
No tenant invites
|
|
</h3>
|
|
<p class="mt-1 text-sm text-gray-500">
|
|
Get started by creating your first tenant invite.
|
|
</p>
|
|
<div class="mt-6">
|
|
<a
|
|
href="/landlord/tenants/invite"
|
|
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
|
|
>
|
|
Create Invite
|
|
</a>
|
|
</div>
|
|
</div>
|
|
)
|
|
: (
|
|
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
{invites.map((invite) => {
|
|
const invitedEmails = parseInvitedEmails(invite.invited_emails);
|
|
const propertyAddress = invite.property?.address_1 || invite.property?.title || 'Unknown Property';
|
|
|
|
return (
|
|
<div
|
|
key={invite.id}
|
|
class="bg-white overflow-hidden shadow rounded-lg"
|
|
>
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg leading-6 font-medium text-gray-900 truncate">
|
|
{propertyAddress}
|
|
</h3>
|
|
<div class="flex space-x-2">
|
|
<a
|
|
href={`/landlord/tenants/invite?invite_id=${invite.id}`}
|
|
class="text-sm text-blue-600 hover:text-blue-500"
|
|
>
|
|
Edit
|
|
</a>
|
|
<DeleteTenantInviteButton
|
|
inviteId={invite.id}
|
|
propertyAddress={propertyAddress}
|
|
invitedEmails={invitedEmails}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-500">Invited Emails:</span>
|
|
<div class="mt-1">
|
|
{invitedEmails.length > 0
|
|
? (
|
|
<ul class="text-sm text-gray-600 space-y-1">
|
|
{invitedEmails.map((email, index) => (
|
|
<li key={index} class="truncate">{email}</li>
|
|
))}
|
|
</ul>
|
|
)
|
|
: (
|
|
<p class="text-sm text-gray-400">No emails added</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-500">Available On:</span>
|
|
<p class="text-sm text-gray-600">
|
|
{invite.available_on
|
|
? new Date(invite.available_on).toLocaleDateString()
|
|
: "Immediately"}
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-500">Status:</span>
|
|
<div class="mt-1">
|
|
{invite.lease_id
|
|
? (
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
Lease Created
|
|
</span>
|
|
)
|
|
: (
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
|
Pending
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-500">Created:</span>
|
|
<p class="text-sm text-gray-600">
|
|
{invite.created_on
|
|
? new Date(invite.created_on).toLocaleDateString()
|
|
: "Unknown"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|