diff --git a/islands/DeleteApplicationButton.tsx b/islands/DeleteApplicationButton.tsx
new file mode 100644
index 0000000..ffdef00
--- /dev/null
+++ b/islands/DeleteApplicationButton.tsx
@@ -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 (
+
+ );
+}
diff --git a/islands/DeleteLeaseButton.tsx b/islands/DeleteLeaseButton.tsx
new file mode 100644
index 0000000..fbab0f6
--- /dev/null
+++ b/islands/DeleteLeaseButton.tsx
@@ -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 (
+
+ );
+}
diff --git a/islands/DeletePropertyButton.tsx b/islands/DeletePropertyButton.tsx
new file mode 100644
index 0000000..49b5466
--- /dev/null
+++ b/islands/DeletePropertyButton.tsx
@@ -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 (
+
+ );
+}
diff --git a/islands/DeleteTenantInviteButton.tsx b/islands/DeleteTenantInviteButton.tsx
new file mode 100644
index 0000000..02913f8
--- /dev/null
+++ b/islands/DeleteTenantInviteButton.tsx
@@ -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 (
+
+ );
+}
diff --git a/islands/LeaseApprovalButtons.tsx b/islands/LeaseApprovalButtons.tsx
new file mode 100644
index 0000000..5eab6e8
--- /dev/null
+++ b/islands/LeaseApprovalButtons.tsx
@@ -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 (
+
+
+
+
+ );
+}
diff --git a/islands/LeasedHome.tsx b/islands/LeasedHome.tsx
index 2e9fcc8..fe67073 100644
--- a/islands/LeasedHome.tsx
+++ b/islands/LeasedHome.tsx
@@ -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"
>
-
+
Title: {lease.title}
Rent: {lease.rent}
Deposit: {lease.security_deposit}
diff --git a/routes/api/delete/tenant_invite.ts b/routes/api/delete/tenant_invite.ts
new file mode 100644
index 0000000..bec3db1
--- /dev/null
+++ b/routes/api/delete/tenant_invite.ts
@@ -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);
+ },
+});
diff --git a/routes/api/lease/approve.ts b/routes/api/lease/approve.ts
index 10dfc62..2b7cc33 100644
--- a/routes/api/lease/approve.ts
+++ b/routes/api/lease/approve.ts
@@ -104,7 +104,6 @@ export const handler = define.handlers({
JSON.stringify({
success: true,
lease: approvedLease,
- lease: approvedLease,
}),
{
status: 200,
diff --git a/routes/api/lease/pending-approvals.ts b/routes/api/lease/pending-approvals.ts
index d5e7de0..110747b 100644
--- a/routes/api/lease/pending-approvals.ts
+++ b/routes/api/lease/pending-approvals.ts
@@ -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) {
diff --git a/routes/api/lease/reject.ts b/routes/api/lease/reject.ts
index cb0a11c..b29db0d 100644
--- a/routes/api/lease/reject.ts
+++ b/routes/api/lease/reject.ts
@@ -106,7 +106,6 @@ export const handler = define.handlers({
JSON.stringify({
success: true,
lease: rejectedLease,
- lease: rejectedLease,
}),
{
status: 200,
diff --git a/routes/api/lease/request-approval.ts b/routes/api/lease/request-approval.ts
index 70af612..9f71a1d 100644
--- a/routes/api/lease/request-approval.ts
+++ b/routes/api/lease/request-approval.ts
@@ -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({
diff --git a/routes/landlord/approvals/index.tsx b/routes/landlord/approvals/index.tsx
index 45e6222..e352f28 100644
--- a/routes/landlord/approvals/index.tsx
+++ b/routes/landlord/approvals/index.tsx
@@ -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),
});
diff --git a/routes/landlord/dashboard/index.tsx b/routes/landlord/dashboard/index.tsx
index ee95313..ed23d4c 100644
--- a/routes/landlord/dashboard/index.tsx
+++ b/routes/landlord/dashboard/index.tsx
@@ -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
+
))
@@ -520,26 +533,7 @@ export default define.page(
: "Unknown date"}
-
-
-
-
+
))
)
@@ -554,6 +548,198 @@ export default define.page(
+ {/* Tenant Invites */}
+
+
+
+
+ {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 (
+
+
+
+ Property Invite
+
+
+ {invitedEmails.length} tenant(s) invited
+
+
+ Created {invite.created_on
+ ? new Date(invite.created_on).toLocaleDateString()
+ : "Unknown date"}
+
+
+
+
+ {invite.lease_id ? "Lease Created" : "Pending"}
+
+
+
+ );
+ })
+ )
+ : (
+
+
+ No tenant invites
+
+
+ )}
+
+
+
+
+ {/* Pending Leases */}
+
+
+
+
+ {(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) => (
+
+
+
+ {lease.property_address || lease.title || "Property"}
+
+
+ ${lease.rent || 0}/month
+
+
+ Created {lease.created_on
+ ? new Date(lease.created_on).toLocaleDateString()
+ : "Unknown date"}
+
+
+
+
+ Pending Approval
+
+ {lease.approval_status === "pending_approval" && (
+
+
+
+
+ )}
+
+
+ ))
+ )
+ : (
+
+
+ No pending leases
+
+
+ )}
+
+
+
+
+ {/* Active Leases */}
+
+
+
+
+ {(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) => (
+
+
+
+ {lease.property_address || lease.title || "Property"}
+
+
+ ${lease.rent || 0}/month
+
+
+ Started {lease.start_date
+ ? new Date(lease.start_date).toLocaleDateString()
+ : "Unknown date"}
+
+
+
+
+ Active
+
+
+
+ ))
+ )
+ : (
+
+ )}
+
+
+
+
{/* Maintenance Requests */}
diff --git a/routes/landlord/properties/index.tsx b/routes/landlord/properties/index.tsx
index 1b7cddf..6d3fe0b 100644
--- a/routes/landlord/properties/index.tsx
+++ b/routes/landlord/properties/index.tsx
@@ -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
+
{property.rental_status === "draft" && (
-