state before z ai
This commit is contained in:
parent
363514879a
commit
344df1775d
59
islands/DeleteApplicationButton.tsx
Normal file
59
islands/DeleteApplicationButton.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
// islands/DeleteApplicationButton.tsx
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
interface DeleteApplicationButtonProps {
|
||||
applicationId: string;
|
||||
applicantName: string;
|
||||
applicantEmail: string;
|
||||
}
|
||||
|
||||
export default function DeleteApplicationButton({
|
||||
applicationId,
|
||||
applicantName,
|
||||
applicantEmail
|
||||
}: DeleteApplicationButtonProps) {
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const handleDelete = async () => {
|
||||
const confirmed = confirm(
|
||||
`Are you sure you want to delete the application from ${applicantName} (${applicantEmail})? This action cannot be undone.`
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('id', applicationId);
|
||||
formData.append('sender', globalThis.location.pathname);
|
||||
|
||||
const response = await fetch('/api/delete/tenant_application', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.redirected) {
|
||||
globalThis.location.href = response.url;
|
||||
} else if (response.ok) {
|
||||
globalThis.location.reload();
|
||||
} else {
|
||||
alert('Failed to delete application. Please try again.');
|
||||
}
|
||||
} catch (_error) {
|
||||
alert('Error deleting application');
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
class="text-xs bg-red-100 text-red-800 px-2 py-1 rounded hover:bg-red-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
56
islands/DeleteLeaseButton.tsx
Normal file
56
islands/DeleteLeaseButton.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
// islands/DeleteLeaseButton.tsx
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
interface DeleteLeaseButtonProps {
|
||||
leaseId: string;
|
||||
propertyAddress: string;
|
||||
tenantName?: string;
|
||||
}
|
||||
|
||||
export default function DeleteLeaseButton({ leaseId, propertyAddress, tenantName }: DeleteLeaseButtonProps) {
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const handleDelete = async () => {
|
||||
const tenantInfo = tenantName ? ` for ${tenantName}` : "";
|
||||
const confirmed = confirm(
|
||||
`Are you sure you want to delete the lease for "${propertyAddress}"${tenantInfo}? This action cannot be undone.`
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('lease_id', leaseId);
|
||||
formData.append('sender', globalThis.location.pathname);
|
||||
|
||||
const response = await fetch('/api/delete/lease', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.redirected) {
|
||||
globalThis.location.href = response.url;
|
||||
} else if (response.ok) {
|
||||
globalThis.location.reload();
|
||||
} else {
|
||||
alert('Failed to delete lease. Please try again.');
|
||||
}
|
||||
} catch (_error) {
|
||||
alert('Error deleting lease');
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
class="text-sm text-red-600 hover:text-red-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
54
islands/DeletePropertyButton.tsx
Normal file
54
islands/DeletePropertyButton.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
// islands/DeletePropertyButton.tsx
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
interface DeletePropertyButtonProps {
|
||||
propertyId: string;
|
||||
propertyAddress: string;
|
||||
}
|
||||
|
||||
export default function DeletePropertyButton({ propertyId, propertyAddress }: DeletePropertyButtonProps) {
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const handleDelete = async () => {
|
||||
const confirmed = confirm(
|
||||
`Are you sure you want to delete "${propertyAddress}"? This action cannot be undone and will remove all associated data including leases, applications, and maintenance requests.`
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('property_id', propertyId);
|
||||
formData.append('sender', globalThis.location.pathname);
|
||||
|
||||
const response = await fetch('/api/delete/property', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.redirected) {
|
||||
globalThis.location.href = response.url;
|
||||
} else if (response.ok) {
|
||||
globalThis.location.reload();
|
||||
} else {
|
||||
alert('Failed to delete property. Please make sure it has no active leases or applications.');
|
||||
}
|
||||
} catch (_error) {
|
||||
alert('Error deleting property');
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
class="text-sm text-red-600 hover:text-red-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
63
islands/DeleteTenantInviteButton.tsx
Normal file
63
islands/DeleteTenantInviteButton.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
// islands/DeleteTenantInviteButton.tsx
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
interface DeleteTenantInviteButtonProps {
|
||||
inviteId: string;
|
||||
propertyAddress?: string;
|
||||
invitedEmails?: string[];
|
||||
}
|
||||
|
||||
export default function DeleteTenantInviteButton({
|
||||
inviteId,
|
||||
propertyAddress,
|
||||
invitedEmails
|
||||
}: DeleteTenantInviteButtonProps) {
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const handleDelete = async () => {
|
||||
const propertyInfo = propertyAddress ? `"${propertyAddress}"` : "this property";
|
||||
const emailCount = invitedEmails?.length || 0;
|
||||
const emailInfo = emailCount > 0 ? ` with ${emailCount} invited email${emailCount > 1 ? 's' : ''}` : "";
|
||||
|
||||
const confirmed = confirm(
|
||||
`Are you sure you want to delete the tenant invite for ${propertyInfo}${emailInfo}? This action cannot be undone.`
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('invite_id', inviteId);
|
||||
formData.append('sender', globalThis.location.pathname);
|
||||
|
||||
const response = await fetch('/api/delete/tenant_invite', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.redirected) {
|
||||
globalThis.location.href = response.url;
|
||||
} else if (response.ok) {
|
||||
globalThis.location.reload();
|
||||
} else {
|
||||
alert('Failed to delete tenant invite. Please try again.');
|
||||
}
|
||||
} catch (_error) {
|
||||
alert('Error deleting tenant invite');
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
class="text-sm text-red-600 hover:text-red-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
78
islands/LeaseApprovalButtons.tsx
Normal file
78
islands/LeaseApprovalButtons.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
// islands/LeaseApprovalButtons.tsx
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
interface LeaseApprovalButtonsProps {
|
||||
approvalId: string;
|
||||
}
|
||||
|
||||
export default function LeaseApprovalButtons({ approvalId }: LeaseApprovalButtonsProps) {
|
||||
const [isApproving, setIsApproving] = useState(false);
|
||||
const [isRejecting, setIsRejecting] = useState(false);
|
||||
|
||||
const handleApprove = async () => {
|
||||
setIsApproving(true);
|
||||
try {
|
||||
const response = await fetch(`/api/lease/approve`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ lease_id: approvalId })
|
||||
});
|
||||
if (response.ok) {
|
||||
globalThis.location.reload();
|
||||
} else {
|
||||
alert('Failed to approve lease');
|
||||
}
|
||||
} catch (_error) {
|
||||
alert('Error approving lease');
|
||||
} finally {
|
||||
setIsApproving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReject = async () => {
|
||||
const reason = prompt('Rejection reason (optional):');
|
||||
if (reason === null) return; // User cancelled
|
||||
|
||||
setIsRejecting(true);
|
||||
try {
|
||||
const response = await fetch(`/api/lease/reject`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
lease_id: approvalId,
|
||||
rejection_reason: reason || ''
|
||||
})
|
||||
});
|
||||
if (response.ok) {
|
||||
globalThis.location.reload();
|
||||
} else {
|
||||
alert('Failed to reject lease');
|
||||
}
|
||||
} catch (_error) {
|
||||
alert('Error rejecting lease');
|
||||
} finally {
|
||||
setIsRejecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleApprove}
|
||||
disabled={isApproving || isRejecting}
|
||||
class="text-xs bg-green-100 text-green-800 px-2 py-1 rounded hover:bg-green-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isApproving ? 'Approving...' : 'Approve'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleReject}
|
||||
disabled={isApproving || isRejecting}
|
||||
class="text-xs bg-red-100 text-red-800 px-2 py-1 rounded hover:bg-red-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isRejecting ? 'Rejecting...' : 'Reject'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import { Lease, Property } from "../db/interfaces.ts";
|
||||
import { PopAddLeaseDialog } from "./dialogs/PopAddLeaseDialog.tsx";
|
||||
import DeleteLeaseButton from "./DeleteLeaseButton.tsx";
|
||||
|
||||
interface LeasedHomeProps {
|
||||
leases: Lease[];
|
||||
@ -85,47 +86,12 @@ export default function LeasedHome({ leases, properties }: LeasedHomeProps) {
|
||||
key={lease.id}
|
||||
class="border rounded p-4 bg-white shadow relative group"
|
||||
>
|
||||
<form
|
||||
method="POST"
|
||||
action="/api/delete/lease"
|
||||
className="absolute top-1/2 -right-0 transform -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200"
|
||||
style={{ display: "inline" }}
|
||||
onSubmit={(e) => {
|
||||
// Set sender input to current page
|
||||
const form = e.currentTarget;
|
||||
form.sender.value = location.pathname;
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="lease_id" value={lease.id} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="sender"
|
||||
value={typeof location !== "undefined"
|
||||
? location.pathname
|
||||
: ""}
|
||||
<div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
||||
<DeleteLeaseButton
|
||||
leaseId={lease.id}
|
||||
propertyAddress={lease.property_address || lease.title || 'Unknown Property'}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
aria-label="Delete lease"
|
||||
className="text-red-700"
|
||||
style={{ background: "none", border: "none", padding: 0 }}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="font-semibold">Title: {lease.title}</div>
|
||||
<div>Rent: {lease.rent}</div>
|
||||
<div>Deposit: {lease.security_deposit}</div>
|
||||
|
||||
31
routes/api/delete/tenant_invite.ts
Normal file
31
routes/api/delete/tenant_invite.ts
Normal file
@ -0,0 +1,31 @@
|
||||
// routes/api/delete/tenant_invite.ts
|
||||
import { define } from "../../../utils.ts";
|
||||
import { deleteTenantInvite } from "../../../services/tenantInviteService.ts";
|
||||
import { SessionCookie } from "../../auth/callback.ts";
|
||||
|
||||
export const handler = define.handlers({
|
||||
async POST(ctx) {
|
||||
const formData = await ctx.req.formData();
|
||||
const invite_id = formData.get("invite_id");
|
||||
const sender = formData.get("sender");
|
||||
|
||||
if (!invite_id || !sender) {
|
||||
return new Response("Missing invite_id or sender", { status: 400 });
|
||||
}
|
||||
|
||||
await deleteTenantInvite(invite_id.toString());
|
||||
|
||||
// Redirect back to the sender URL (must be absolute)
|
||||
const senderPath = sender.toString();
|
||||
let redirectUrl = senderPath;
|
||||
try {
|
||||
if (senderPath.startsWith("/")) {
|
||||
const url = new URL(ctx.req.url);
|
||||
redirectUrl = url.origin + senderPath;
|
||||
}
|
||||
} catch (_) {
|
||||
// fallback: just use senderPath
|
||||
}
|
||||
return Response.redirect(redirectUrl, 303);
|
||||
},
|
||||
});
|
||||
@ -104,7 +104,6 @@ export const handler = define.handlers({
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
lease: approvedLease,
|
||||
lease: approvedLease,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
|
||||
@ -11,9 +11,8 @@ export const handler = define.handlers({
|
||||
async GET(ctx) {
|
||||
try {
|
||||
const stateData: SessionCookie = ctx.state.data;
|
||||
const { user_id } = stateData;
|
||||
|
||||
if (!user_id) {
|
||||
|
||||
if (!stateData || !stateData.user_id) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Unauthorized" }),
|
||||
{
|
||||
@ -23,6 +22,8 @@ export const handler = define.handlers({
|
||||
);
|
||||
}
|
||||
|
||||
const { user_id } = stateData;
|
||||
|
||||
const pendingLeases = await getPendingApprovals(user_id);
|
||||
|
||||
if (!pendingLeases || pendingLeases.length === 0) {
|
||||
|
||||
@ -106,7 +106,6 @@ export const handler = define.handlers({
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
lease: rejectedLease,
|
||||
lease: rejectedLease,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
|
||||
@ -16,6 +16,7 @@ export const handler = define.handlers({
|
||||
async POST(ctx) {
|
||||
try {
|
||||
const stateData: SessionCookie = ctx.state.data;
|
||||
console.log('stateData', stateData);
|
||||
const { user_id } = stateData;
|
||||
|
||||
if (!user_id) {
|
||||
@ -113,16 +114,6 @@ export const handler = define.handlers({
|
||||
monthly_rent: property.monthly_rent ?? undefined,
|
||||
});
|
||||
}
|
||||
await sendLeaseApprovalRequest(invite.landlord_id, {
|
||||
lease_id: lease.id,
|
||||
property_address: property.address_1
|
||||
? `${property.address_1}, ${property.city}, ${property.state}`
|
||||
: property.title || "Property",
|
||||
tenant_name: tenant?.name || "Tenant",
|
||||
landlord_name: landlord?.name || "Landlord",
|
||||
monthly_rent: property.monthly_rent,
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
|
||||
@ -48,19 +48,21 @@ interface LandlordApprovalsProps {
|
||||
export const handler = define.handlers({
|
||||
async GET(ctx) {
|
||||
const stateData: SessionCookie = ctx.state.data;
|
||||
const { user_id } = stateData;
|
||||
|
||||
if (!user_id) {
|
||||
|
||||
if (!stateData || !stateData.user_id) {
|
||||
return new Response("", {
|
||||
status: 302,
|
||||
headers: { Location: "/auth/login" },
|
||||
});
|
||||
}
|
||||
|
||||
const { user_id } = stateData;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/lease/pending-approvals", {
|
||||
const response = await fetch(`http://${ctx.req.headers.get("host")}/api/lease/pending-approvals`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cookie": ctx.req.headers.get("cookie") || "",
|
||||
},
|
||||
});
|
||||
|
||||
@ -116,8 +118,8 @@ export const handler = define.handlers({
|
||||
}
|
||||
|
||||
const apiEndpoint = action === "approve"
|
||||
? "/api/lease/approve"
|
||||
: "/api/lease/reject";
|
||||
? `http://${ctx.req.headers.get("host")}/api/lease/approve`
|
||||
: `http://${ctx.req.headers.get("host")}/api/lease/reject`;
|
||||
|
||||
const body = action === "reject"
|
||||
? { lease_id, rejection_reason }
|
||||
@ -127,6 +129,7 @@ export const handler = define.handlers({
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cookie": ctx.req.headers.get("cookie") || "",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
// routes/landlord/dashboard/index.tsx
|
||||
import { define } from "../../../utils.ts";
|
||||
import { getLandlordDashboardData, getPendingLeaseApprovals } from "../../../services/landlordService.ts";
|
||||
import { getTenantInvitesByLandlord } from "../../../services/tenantInviteService.ts";
|
||||
import { DashboardTenantApplication } from "../../../services/tenantApplicationService.ts";
|
||||
import { DashboardMaintenanceRequest } from "../../../services/maintenanceRequestService.ts";
|
||||
import { DashboardMessage } from "../../../services/messageService.ts";
|
||||
import { TenantInvite, Lease } from "../../../db/interfaces.ts";
|
||||
import { page } from "fresh";
|
||||
import { SessionCookie } from "../../auth/callback.ts";
|
||||
import { MakeNewDate } from "../../../helpers.ts";
|
||||
import LeaseApprovalButtons from "../../../islands/LeaseApprovalButtons.tsx";
|
||||
import DeleteApplicationButton from "../../../islands/DeleteApplicationButton.tsx";
|
||||
|
||||
// Flexible interface for dashboard data - allows any structure
|
||||
interface FlexibleDashboardData {
|
||||
@ -76,12 +80,14 @@ export const handler = define.handlers({
|
||||
try {
|
||||
const dashboardData = await getLandlordDashboardData(user_id);
|
||||
const pendingApprovals = await getPendingLeaseApprovals(user_id);
|
||||
const tenantInvites = await getTenantInvitesByLandlord(user_id);
|
||||
|
||||
return page({
|
||||
stateData,
|
||||
dashboardData: {
|
||||
...dashboardData,
|
||||
pendingApprovals,
|
||||
tenantInvites,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@ -124,6 +130,8 @@ export default define.page(
|
||||
recentMessages,
|
||||
monthlyRevenue,
|
||||
pendingLeaseApprovals,
|
||||
tenantInvites,
|
||||
leases,
|
||||
} = dashboardData || {};
|
||||
|
||||
return (
|
||||
@ -470,6 +478,11 @@ export default define.page(
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
<DeleteApplicationButton
|
||||
applicationId={application.id}
|
||||
applicantName={application.applicant_name || 'Unknown'}
|
||||
applicantEmail={application.applicant_email || 'Unknown'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
@ -520,26 +533,7 @@ export default define.page(
|
||||
: "Unknown date"}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<form method="POST" action="/api/lease/approve">
|
||||
<input type="hidden" name="lease_id" value={approval.id} />
|
||||
<button
|
||||
type="submit"
|
||||
class="text-xs bg-green-100 text-green-800 px-2 py-1 rounded hover:bg-green-200"
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="/api/lease/reject">
|
||||
<input type="hidden" name="lease_id" value={approval.id} />
|
||||
<button
|
||||
type="submit"
|
||||
class="text-xs bg-red-100 text-red-800 px-2 py-1 rounded hover:bg-red-200"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<LeaseApprovalButtons approvalId={approval.id} />
|
||||
</div>
|
||||
))
|
||||
)
|
||||
@ -554,6 +548,198 @@ export default define.page(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tenant Invites */}
|
||||
<div class="bg-white 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">
|
||||
Tenant Invites
|
||||
</h3>
|
||||
<a
|
||||
href="/landlord/tenants/invites"
|
||||
class="text-sm text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
View all
|
||||
</a>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
{tenantInvites && Array.isArray(tenantInvites) && tenantInvites.length > 0
|
||||
? (
|
||||
tenantInvites.slice(0, 3).map((invite: TenantInvite) => {
|
||||
// Parse invited_emails string to get count
|
||||
const invitedEmails = invite.invited_emails ?
|
||||
invite.invited_emails.split(',').filter(email => email.trim()) : [];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={invite.id}
|
||||
class="flex items-center justify-between p-3 bg-purple-50 rounded-lg"
|
||||
>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">
|
||||
Property Invite
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
{invitedEmails.length} tenant(s) invited
|
||||
</p>
|
||||
<p class="text-xs text-gray-400">
|
||||
Created {invite.created_on
|
||||
? new Date(invite.created_on).toLocaleDateString()
|
||||
: "Unknown date"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
|
||||
{invite.lease_id ? "Lease Created" : "Pending"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)
|
||||
: (
|
||||
<div class="text-center py-4">
|
||||
<p class="text-sm text-gray-500">
|
||||
No tenant invites
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pending Leases */}
|
||||
<div class="bg-white 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">
|
||||
Pending Leases
|
||||
</h3>
|
||||
<a
|
||||
href="/landlord/leases"
|
||||
class="text-sm text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
View all
|
||||
</a>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
{(leases as unknown as Lease[]) && Array.isArray(leases) && (leases as unknown as Lease[]).length > 0
|
||||
? (
|
||||
(leases as unknown as Lease[]).filter((lease: Lease) => lease.approval_status === "pending_approval").slice(0, 3).map((lease: Lease) => (
|
||||
<div
|
||||
key={lease.id}
|
||||
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
|
||||
>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">
|
||||
{lease.property_address || lease.title || "Property"}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
${lease.rent || 0}/month
|
||||
</p>
|
||||
<p class="text-xs text-gray-400">
|
||||
Created {lease.created_on
|
||||
? new Date(lease.created_on).toLocaleDateString()
|
||||
: "Unknown date"}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<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 Approval
|
||||
</span>
|
||||
{lease.approval_status === "pending_approval" && (
|
||||
<div class="flex space-x-1">
|
||||
<form method="POST" action="/landlord/approvals" class="inline">
|
||||
<input type="hidden" name="lease_id" value={lease.id} />
|
||||
<input type="hidden" name="action" value="approve" />
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex items-center px-2 py-1 border border-transparent text-xs leading-3 font-medium rounded text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-green-500"
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="/landlord/approvals" class="inline">
|
||||
<input type="hidden" name="lease_id" value={lease.id} />
|
||||
<input type="hidden" name="action" value="reject" />
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex items-center px-2 py-1 border border-transparent text-xs leading-3 font-medium rounded text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-red-500"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)
|
||||
: (
|
||||
<div class="text-center py-4">
|
||||
<p class="text-sm text-gray-500">
|
||||
No pending leases
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active Leases */}
|
||||
<div class="bg-white 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">
|
||||
Active Leases
|
||||
</h3>
|
||||
<a
|
||||
href="/landlord/leases"
|
||||
class="text-sm text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
View all
|
||||
</a>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
{(leases as unknown as Lease[]) && Array.isArray(leases) && (leases as unknown as Lease[]).length > 0
|
||||
? (
|
||||
(leases as unknown as Lease[]).filter((lease: Lease) => lease.lease_status === "active").slice(0, 3).map((lease: Lease) => (
|
||||
<div
|
||||
key={lease.id}
|
||||
class="flex items-center justify-between p-3 bg-green-50 rounded-lg"
|
||||
>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">
|
||||
{lease.property_address || lease.title || "Property"}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
${lease.rent || 0}/month
|
||||
</p>
|
||||
<p class="text-xs text-gray-400">
|
||||
Started {lease.start_date
|
||||
? new Date(lease.start_date).toLocaleDateString()
|
||||
: "Unknown date"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
Active
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)
|
||||
: (
|
||||
<div class="text-center py-4">
|
||||
<p class="text-sm text-gray-500">
|
||||
No active leases
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Maintenance Requests */}
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
|
||||
@ -4,6 +4,7 @@ import { SessionCookie } from "../../auth/callback.ts";
|
||||
import { getPropertiesByUser } from "../../../services/propertyService.ts";
|
||||
import { page } from "fresh";
|
||||
import { Property } from "../../../db/interfaces.ts";
|
||||
import DeletePropertyButton from "../../../islands/DeletePropertyButton.tsx";
|
||||
|
||||
export const handler = define.handlers({
|
||||
async GET(ctx) {
|
||||
@ -147,10 +148,14 @@ export default define.page(function LandlordProperties({ data }) {
|
||||
>
|
||||
Applications
|
||||
</a>
|
||||
<DeletePropertyButton
|
||||
propertyId={property.id}
|
||||
propertyAddress={property.address_1 || 'Unknown Address'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{property.rental_status === "draft" && (
|
||||
<button class="text-sm bg-green-100 text-green-800 px-3 py-1 rounded-full hover:bg-green-200">
|
||||
<button type="button" class="text-sm bg-green-100 text-green-800 px-3 py-1 rounded-full hover:bg-green-200">
|
||||
Publish
|
||||
</button>
|
||||
)}
|
||||
|
||||
240
routes/landlord/tenants/invites.tsx
Normal file
240
routes/landlord/tenants/invites.tsx
Normal file
@ -0,0 +1,240 @@
|
||||
// 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>
|
||||
);
|
||||
});
|
||||
@ -132,9 +132,12 @@ export const handler = define.handlers({
|
||||
|
||||
await startGuestOnboarding(userId, "tenant");
|
||||
|
||||
const response = await fetch("/api/lease/request-approval", {
|
||||
const response = await fetch(`http://${ctx.req.headers.get("host")}/api/lease/request-approval`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cookie": ctx.req.headers.get("cookie") || "",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
invite_id: invite.id,
|
||||
property_id: invite.property_id,
|
||||
|
||||
@ -21,7 +21,7 @@ export const handler = define.handlers({
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/retrieve/payment_methods", {
|
||||
const response = await fetch(`http://${ctx.req.headers.get("host")}/api/retrieve/payment_methods`, {
|
||||
headers: {
|
||||
"Cookie": ctx.req.headers.get("Cookie") || "",
|
||||
},
|
||||
|
||||
@ -71,9 +71,12 @@ export const handler = define.handlers({
|
||||
const lease_id = formData.get("lease_id") as string;
|
||||
|
||||
// Call the payment intent creation endpoint
|
||||
const intentResponse = await fetch("/api/payments/create_intent", {
|
||||
const intentResponse = await fetch(`http://${ctx.req.headers.get("host")}/api/payments/create_intent`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cookie": ctx.req.headers.get("cookie") || "",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
lease_id,
|
||||
amount,
|
||||
|
||||
@ -157,9 +157,30 @@ let e2eLeases: Lease[] = [
|
||||
rent: 1800,
|
||||
security_deposit: 1800,
|
||||
lease_status: "active",
|
||||
approval_status: "approved",
|
||||
approval_requested_at: BASE_TIME - 1000 * 60 * 60 * 24 * 115,
|
||||
approved_at: BASE_TIME - 1000 * 60 * 60 * 24 * 114,
|
||||
approved_by: e2eUserIds.landlord,
|
||||
created_on: BASE_TIME - 1000 * 60 * 60 * 24 * 120,
|
||||
updated_on: BASE_TIME - 1000 * 60 * 60 * 24 * 5,
|
||||
},
|
||||
{
|
||||
id: "e2e-lease-pending-1",
|
||||
title: "Pending Oak Avenue Lease",
|
||||
property_id: "e2e-prop-1",
|
||||
property_address: "123 Oak Ave, Seattle, WA",
|
||||
tenant_id: e2eUserIds.tenant,
|
||||
owner_id: e2eUserIds.landlord,
|
||||
start_date: BASE_TIME + 1000 * 60 * 60 * 24 * 30,
|
||||
end_date: BASE_TIME + 1000 * 60 * 60 * 24 * 395,
|
||||
rent: 2200,
|
||||
security_deposit: 2200,
|
||||
lease_status: "draft",
|
||||
approval_status: "pending_approval",
|
||||
approval_requested_at: BASE_TIME - 1000 * 60 * 60 * 24 * 2,
|
||||
created_on: BASE_TIME - 1000 * 60 * 60 * 24 * 3,
|
||||
updated_on: BASE_TIME - 1000 * 60 * 60 * 24 * 2,
|
||||
},
|
||||
];
|
||||
|
||||
const extraE2EMessages: Message[] = Array.from({ length: 9 }, (_, index) => ({
|
||||
@ -1040,6 +1061,19 @@ export function getE2ETwitterInvitesByEmail(email: string) {
|
||||
}).map((invite) => ({ ...invite }));
|
||||
}
|
||||
|
||||
export function getE2ETenantInvitesByLandlord(landlordId: string) {
|
||||
return e2eTenantInvites
|
||||
.filter((invite) => invite.landlord_id === landlordId)
|
||||
.sort((a, b) => (b.created_on || 0) - (a.created_on || 0));
|
||||
}
|
||||
|
||||
export function deleteE2ETenantInvite(inviteId: string) {
|
||||
const index = e2eTenantInvites.findIndex((invite) => invite.id === inviteId);
|
||||
if (index !== -1) {
|
||||
e2eTenantInvites.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
export function getE2EPaymentMethodsByUser(userId: string) {
|
||||
return e2ePaymentMethods.filter((method) => method.user_id === userId);
|
||||
}
|
||||
@ -1220,12 +1254,13 @@ export function getE2ELandlordDashboardData(userId: string) {
|
||||
maintenanceRequests,
|
||||
recentMessages,
|
||||
monthlyRevenue,
|
||||
leases,
|
||||
};
|
||||
}
|
||||
|
||||
export function getE2EPendingApprovals() {
|
||||
return e2eLeases.filter((lease) =>
|
||||
lease.approval_status === "pending" || lease.approval_status === "requested"
|
||||
lease.approval_status === "pending_approval" || lease.approval_status === "pending" || lease.approval_status === "requested"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -75,6 +75,7 @@ export interface LandlordDashboardData {
|
||||
maintenanceRequests?: DashboardMaintenanceRequest[];
|
||||
recentMessages?: DashboardMessage[];
|
||||
monthlyRevenue?: number;
|
||||
leases?: Lease[];
|
||||
}
|
||||
|
||||
// Extended dashboard interface with additional UI data arrays
|
||||
@ -422,6 +423,7 @@ export async function getLandlordDashboardData(
|
||||
(sum, property) => sum + (property.monthly_rent || 0),
|
||||
0,
|
||||
),
|
||||
leases: leasesResult,
|
||||
};
|
||||
|
||||
return dashboardData;
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
isE2EEnabled,
|
||||
getE2EPendingApprovals,
|
||||
addE2ELeaseWithApproval,
|
||||
getE2ETwitterInviteById,
|
||||
} from "./e2eFixtures.ts";
|
||||
|
||||
// export interface Lease {
|
||||
@ -324,9 +325,21 @@ export async function requestLeaseApproval(
|
||||
invite_id: string,
|
||||
) {
|
||||
if (isE2EEnabled()) {
|
||||
return [addE2ELeaseWithApproval(tenant_id, property_id, invite_id)];
|
||||
// For E2E, get the invite to extract landlord_id
|
||||
const invite = getE2ETwitterInviteById(invite_id);
|
||||
const landlord_id = invite?.landlord_id || "e2e-landlord-1";
|
||||
|
||||
return [addE2ELeaseWithApproval(property_id, tenant_id, landlord_id, "pending_approval")];
|
||||
}
|
||||
|
||||
// Get the invite to extract landlord_id
|
||||
const invite = await db.select()
|
||||
.from(tenant_invites)
|
||||
.where(eq(tenant_invites.id, invite_id))
|
||||
.limit(1);
|
||||
|
||||
const landlord_id = invite[0]?.landlord_id || null;
|
||||
|
||||
const now = Date.now();
|
||||
return await db.insert(leases)
|
||||
.values({
|
||||
@ -334,7 +347,7 @@ export async function requestLeaseApproval(
|
||||
title: `Lease for Property ${property_id}`,
|
||||
property_id,
|
||||
tenant_id,
|
||||
owner_id: null,
|
||||
owner_id: landlord_id,
|
||||
start_date: now,
|
||||
end_date: now + 365 * 24 * 60 * 60 * 1000,
|
||||
rent: null,
|
||||
|
||||
@ -49,6 +49,7 @@ export function InitiPGSessionMiddleware() {
|
||||
return define.middleware(async (ctx: Context<any>) => {
|
||||
const url = new URL(ctx.req.url);
|
||||
|
||||
console.log('path', url.pathname);
|
||||
if (isE2EEnabled()) {
|
||||
const e2eUser = ctx.req.headers.get("x-e2e-user") ??
|
||||
url.searchParams.get("e2e-user");
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
// services/tenantInviteService.ts
|
||||
// Handle tenant invite creation and updates
|
||||
|
||||
import { and, eq, ilike } from "drizzle-orm";
|
||||
import { and, eq, ilike, desc } from "drizzle-orm";
|
||||
import { db } from "../db/db.ts";
|
||||
import { tenant_invites } from "../db/schema.ts";
|
||||
import type { TenantInvite } from "../db/interfaces.ts";
|
||||
import {
|
||||
addE2ETenantInvite,
|
||||
deleteE2ETenantInvite,
|
||||
getE2ETwitterInviteById,
|
||||
getE2ETwitterInviteByProperty,
|
||||
getE2ETwitterInvitesByEmail,
|
||||
getE2ETenantInvitesByLandlord,
|
||||
isE2EEnabled,
|
||||
updateE2ETwitterInvite,
|
||||
} from "./e2eFixtures.ts";
|
||||
@ -146,3 +148,28 @@ export async function removeTenantInviteEmail(inviteId: string, email: string) {
|
||||
});
|
||||
return { removed: true, invite: updated[0] ?? invite };
|
||||
}
|
||||
|
||||
export async function getTenantInvitesByLandlord(landlordId: string): Promise<TenantInvite[]> {
|
||||
if (isE2EEnabled()) {
|
||||
return getE2ETenantInvitesByLandlord(landlordId);
|
||||
}
|
||||
|
||||
const result = await db
|
||||
.select()
|
||||
.from(tenant_invites)
|
||||
.where(eq(tenant_invites.landlord_id, landlordId))
|
||||
.orderBy(desc(tenant_invites.created_on));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function deleteTenantInvite(inviteId: string): Promise<void> {
|
||||
if (isE2EEnabled()) {
|
||||
deleteE2ETenantInvite(inviteId);
|
||||
return;
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(tenant_invites)
|
||||
.where(eq(tenant_invites.id, inviteId));
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user