<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Property Manager</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
background-color: #f0f4f8; /* Light blue-gray background */
color: #1e293b; /* Dark slate text */
min-height: 100vh;
display: flex;
flex-direction: column;
}
.container {
background-color: #ffffff;
border-radius: 1.5rem;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
padding: 2rem;
margin: 2rem auto;
width: 95%;
max-width: 1200px;
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.tab-button {
padding: 0.75rem 1.5rem;
border-radius: 0.75rem;
font-weight: 600;
transition: all 0.2s ease-in-out;
cursor: pointer;
background-color: #e2e8f0; /* Light gray */
color: #475569; /* Slate gray */
}
.tab-button.active {
background-color: #4f46e5; /* Indigo */
color: white;
box-shadow: 0 4px 10px rgba(79, 70, 229, 0.3);
}
.tab-button:hover:not(.active) {
background-color: #cbd5e1; /* Lighter gray on hover */
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #334155;
}
.form-group input[type="text"],
.form-group input[type="number"],
.form-group input[type="date"],
.form-group select,
.form-group textarea {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid #cbd5e1;
border-radius: 0.75rem;
background-color: #f8fafc;
font-size: 1rem;
color: #334155;
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #6366f1; /* Blue-indigo */
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
}
.btn-primary {
background-color: #4f46e5; /* Indigo */
color: white;
border: none;
border-radius: 0.75rem;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out;
box-shadow: 0 4px 10px rgba(79, 70, 229, 0.3);
}
.btn-primary:hover {
background-color: #4338ca; /* Darker indigo */
transform: translateY(-1px);
}
.btn-primary:active {
transform: translateY(0);
}
.message-box {
background-color: #fff;
border-radius: 0.75rem;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
max-width: 400px;
text-align: center;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1000;
display: none; /* Hidden by default */
}
.message-box-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
display: none; /* Hidden by default */
}
.message-box-content {
margin-bottom: 1rem;
color: #334155;
}
.message-box-button {
background-color: #4f46e5;
color: white;
border: none;
border-radius: 0.5rem;
padding: 0.5rem 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.message-box-button:hover {
background-color: #4338ca;
}
.loading-spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #4f46e5;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 1rem auto;
display: none; /* Hidden by default */
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
color: #ef4444; /* Red for errors */
margin-top: 0.5rem;
font-size: 0.9rem;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 1.5rem;
background-color: #ffffff;
border-radius: 0.75rem;
overflow: hidden; /* Ensures rounded corners apply to content */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
th, td {
padding: 1rem 1.25rem;
text-align: left;
border-bottom: 1px solid #e2e8f0;
}
th {
background-color: #f8fafc;
font-weight: 600;
color: #334155;
text-transform: uppercase;
font-size: 0.85rem;
}
tr:last-child td {
border-bottom: none;
}
.action-buttons button {
background-color: #ef4444; /* Red for delete */
color: white;
border: none;
border-radius: 0.5rem;
padding: 0.4rem 0.8rem;
font-size: 0.8rem;
cursor: pointer;
transition: background-color 0.2s;
}
.action-buttons button:hover {
background-color: #dc2626;
}
.action-buttons button.edit-btn {
background-color: #22c55e; /* Green for edit */
margin-right: 0.5rem;
}
.action-buttons button.edit-btn:hover {
background-color: #16a34a;
}
.report-summary-card {
background-color: #edf2f7; /* Light gray-blue */
border-radius: 0.75rem;
padding: 1.5rem;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.report-summary-card h4 {
font-size: 1.125rem;
font-weight: 600;
color: #475569;
margin-bottom: 0.5rem;
}
.report-summary-card p {
font-size: 2.25rem;
font-weight: 700;
color: #1e293b;
}
.report-summary-card.income p {
color: #22c55e; /* Green */
}
.report-summary-card.expense p {
color: #ef4444; /* Red */
}
.report-summary-card.profit p {
color: #3b82f6; /* Blue */
}
/* Responsive adjustments */
@media (max-width: 768px) {
.container {
padding: 1rem;
margin: 1rem auto;
}
.tab-buttons {
flex-direction: column;
}
.tab-button {
width: 100%;
margin-bottom: 0.5rem;
}
.form-grid {
grid-template-columns: 1fr;
}
th, td {
padding: 0.75rem 0.8rem;
font-size: 0.9rem;
}
table thead {
display: none; /* Hide table headers on small screens */
}
table, tbody, tr, td {
display: block;
width: 100%;
}
tr {
margin-bottom: 1rem;
border: 1px solid #e2e8f0;
border-radius: 0.75rem;
padding: 1rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
td {
text-align: right;
padding-left: 50%;
position: relative;
border: none;
}
td::before {
content: attr(data-label);
position: absolute;
left: 0;
width: 50%;
padding-left: 1rem;
font-weight: 600;
text-align: left;
color: #334155;
}
.action-buttons {
text-align: left;
padding-left: 1rem;
margin-top: 1rem;
}
.report-summary-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<h1 class="text-4xl font-extrabold text-gray-900 mb-6 text-center">Property Management Software</h1>
<div class="text-sm text-gray-600 text-center mb-4">
User ID: <span id="userIdDisplay" class="font-mono text-blue-600">Loading...</span>
</div>
<div class="flex flex-wrap justify-center gap-4 mb-8 tab-buttons">
<button id="propertiesTab" class="tab-button active">Properties</button>
<button id="tenantsTab" class="tab-button">Tenants</button>
<button id="incomeTab" class="tab-button">Income</button>
<button id="expensesTab" class="tab-button">Expenses</button>
<button id="reportsTab" class="tab-button">Reports</button>
</div>
<section id="propertiesSection" class="tab-content">
<h2 class="text-2xl font-bold text-gray-800 mb-6">Manage Properties</h2>
<form id="addPropertyForm" class="bg-gray-50 p-6 rounded-xl shadow-inner mb-8 grid grid-cols-1 md:grid-cols-2 gap-4 form-grid">
<div class="form-group">
<label for="propertyName">Property Name</label>
<input type="text" id="propertyName" placeholder="e.g., Downtown Loft" required>
</div>
<div class="form-group">
<label for="propertyAddress">Address</label>
<input type="text" id="propertyAddress" placeholder="e.g., 123 Main St, City, State" required>
</div>
<div class="form-group">
<label for="propertyType">Type</label>
<select id="propertyType" required>
<option value="">Select Type</option>
<option value="Residential">Residential</option>
<option value="Commercial">Commercial</option>
<option value="Land">Land</option>
<option value="Other">Other</option>
</select>
</div>
<div class="form-group">
<label for="propertyPurchaseDate">Purchase Date</label>
<input type="date" id="propertyPurchaseDate">
</div>
<div class="md:col-span-2 text-right">
<button type="submit" class="btn-primary">Add Property</button>
</div>
</form>
<h3 class="text-xl font-bold text-gray-800 mb-4">Your Properties</h3>
<div id="propertiesList" class="relative">
<div class="loading-spinner" id="propertiesLoading"></div>
<p id="noPropertiesMessage" class="text-gray-500 text-center py-4 hidden">No properties added yet.</p>
<table id="propertiesTable" class="min-w-full hidden">
<thead>
<tr>
<th>Name</th>
<th>Address</th>
<th>Type</th>
<th>Purchase Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</section>
<section id="tenantsSection" class="tab-content hidden">
<h2 class="text-2xl font-bold text-gray-800 mb-6">Manage Tenants</h2>
<form id="addTenantForm" class="bg-gray-50 p-6 rounded-xl shadow-inner mb-8 grid grid-cols-1 md:grid-cols-2 gap-4 form-grid">
<div class="form-group">
<label for="tenantName">Tenant Name</label>
<input type="text" id="tenantName" placeholder="e.g., John Doe" required>
</div>
<div class="form-group">
<label for="tenantContact">Contact Info</label>
<input type="text" id="tenantContact" placeholder="e.g., john.doe@example.com or 555-123-4567" required>
</div>
<div class="form-group">
<label for="tenantProperty">Property</label>
<select id="tenantProperty" required>
<option value="">Select Property</option>
</select>
</div>
<div class="form-group">
<label for="leaseStartDate">Lease Start Date</label>
<input type="date" id="leaseStartDate" required>
</div>
<div class="form-group">
<label for="leaseEndDate">Lease End Date</label>
<input type="date" id="leaseEndDate">
</div>
<div class="form-group">
<label for="tenantRentAmount">Monthly Rent ($)</label>
<input type="number" id="tenantRentAmount" step="0.01" min="0" placeholder="e.g., 1500.00" required>
</div>
<div class="form-group">
<label for="tenantDepositAmount">Security Deposit ($)</label>
<input type="number" id="tenantDepositAmount" step="0.01" min="0" placeholder="e.g., 1500.00">
</div>
<div class="md:col-span-2 text-right">
<button type="submit" class="btn-primary">Add Tenant</button>
</div>
</form>
<h3 class="text-xl font-bold text-gray-800 mb-4">Your Tenants</h3>
<div id="tenantsList" class="relative">
<div class="loading-spinner" id="tenantsLoading"></div>
<p id="noTenantsMessage" class="text-gray-500 text-center py-4 hidden">No tenants added yet.</p>
<table id="tenantsTable" class="min-w-full hidden">
<thead>
<tr>
<th>Name</th>
<th>Contact</th>
<th>Property</th>
<th>Lease Start</th>
<th>Lease End</th>
<th>Rent</th>
<th>Deposit</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</section>
<section id="incomeSection" class="tab-content hidden">
<h2 class="text-2xl font-bold text-gray-800 mb-6">Track Income</h2>
<form id="addIncomeForm" class="bg-gray-50 p-6 rounded-xl shadow-inner mb-8 grid grid-cols-1 md:grid-cols-2 gap-4 form-grid">
<div class="form-group">
<label for="incomeProperty">Property</label>
<select id="incomeProperty" required>
<option value="">Select Property</option>
</select>
</div>
<div class="form-group">
<label for="incomeCategory">Category</label>
<select id="incomeCategory" required>
<option value="">Select Category</option>
<option value="Rent">Rent</option>
<option value="Late Fee">Late Fee</option>
<option value="Other Income">Other Income</option>
</select>
</div>
<div class="form-group">
<label for="incomeAmount">Amount ($)</label>
<input type="number" id="incomeAmount" step="0.01" min="0" placeholder="e.g., 1200.00" required>
</div>
<div class="form-group">
<label for="incomeDate">Date</label>
<input type="date" id="incomeDate" required>
</div>
<div class="form-group md:col-span-2">
<label for="incomeDescription">Description (Optional)</label>
<textarea id="incomeDescription" rows="2" placeholder="e.g., July rent payment"></textarea>
</div>
<div class="md:col-span-2 text-right">
<button type="submit" class="btn-primary">Add Income</button>
</div>
</form>
<h3 class="text-xl font-bold text-gray-800 mb-4">Income Records</h3>
<div id="incomeList" class="relative">
<div class="loading-spinner" id="incomeLoading"></div>
<p id="noIncomeMessage" class="text-gray-500 text-center py-4 hidden">No income records yet.</p>
<table id="incomeTable" class="min-w-full hidden">
<thead>
<tr>
<th>Property</th>
<th>Category</th>
<th>Amount</th>
<th>Date</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</section>
<section id="expensesSection" class="tab-content hidden">
<h2 class="text-2xl font-bold text-gray-800 mb-6">Track Expenses</h2>
<form id="addExpenseForm" class="bg-gray-50 p-6 rounded-xl shadow-inner mb-8 grid grid-cols-1 md:grid-cols-2 gap-4 form-grid">
<div class="form-group">
<label for="expenseProperty">Property</label>
<select id="expenseProperty" required>
<option value="">Select Property</option>
</select>
</div>
<div class="form-group">
<label for="expenseCategory">Category</label>
<select id="expenseCategory" required>
<option value="">Select Category</option>
<option value="Repairs">Repairs</option>
<option value="Utilities">Utilities</option>
<option value="Property Tax">Property Tax</option>
<option value="Insurance">Insurance</option>
<option value="Mortgage">Mortgage</option>
<option value="Maintenance">Maintenance</option>
<option value="Other Expense">Other Expense</option>
</select>
</div>
<div class="form-group">
<label for="expenseAmount">Amount ($)</label>
<input type="number" id="expenseAmount" step="0.01" min="0" placeholder="e.g., 250.50" required>
</div>
<div class="form-group">
<label for="expenseDate">Date</label>
<input type="date" id="expenseDate" required>
</div>
<div class="form-group md:col-span-2">
<label for="expenseDescription">Description (Optional)</label>
<textarea id="expenseDescription" rows="2" placeholder="e.g., Plumbing repair for bathroom"></textarea>
</div>
<div class="md:col-span-2 text-right">
<button type="submit" class="btn-primary">Add Expense</button>
</div>
</form>
<h3 class="text-xl font-bold text-gray-800 mb-4">Expense Records</h3>
<div id="expensesList" class="relative">
<div class="loading-spinner" id="expensesLoading"></div>
<p id="noExpensesMessage" class="text-gray-500 text-center py-4 hidden">No expense records yet.</p>
<table id="expensesTable" class="min-w-full hidden">
<thead>
<tr>
<th>Property</th>
<th>Category</th>
<th>Amount</th>
<th>Date</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</section>
<section id="reportsSection" class="tab-content hidden">
<h2 class="text-2xl font-bold text-gray-800 mb-6">Financial Reports</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 report-summary-grid">
<div class="report-summary-card income">
<h4>Total Income</h4>
<p id="totalIncome">$0.00</p>
</div>
<div class="report-summary-card expense">
<h4>Total Expenses</h4>
<p id="totalExpenses">$0.00</p>
</div>
<div class="report-summary-card profit">
<h4>Net Profit</h4>
<p id="netProfit">$0.00</p>
</div>
</div>
<h3 class="text-xl font-bold text-gray-800 mt-8 mb-4">Detailed Breakdown (All Transactions)</h3>
<div id="allTransactionsList" class="relative">
<div class="loading-spinner" id="reportsLoading"></div>
<p id="noReportsMessage" class="text-gray-500 text-center py-4 hidden">No transactions to report yet.</p>
<table id="allTransactionsTable" class="min-w-full hidden">
<thead>
<tr>
<th>Type</th>
<th>Property</th>
<th>Category</th>
<th>Amount</th>
<th>Date</th>
<th>Description</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</section>
</div>
<div id="messageBoxOverlay" class="message-box-overlay"></div>
<div id="messageBox" class="message-box">
<div id="messageBoxContent" class="message-box-content"></div>
<button id="messageBoxClose" class="message-box-button">OK</button>
</div>
<script type="module">
// Import Firebase modules
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
import { getFirestore, collection, addDoc, getDocs, doc, deleteDoc, onSnapshot, query, where, serverTimestamp, updateDoc } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
// Global variables provided by the Canvas environment
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {};
const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;
// Firebase App and Services
let app;
let db;
let auth;
let currentUserId = null;
let isAuthReady = false;
// DOM Elements
const propertiesTab = document.getElementById('propertiesTab');
const tenantsTab = document.getElementById('tenantsTab'); // New
const incomeTab = document.getElementById('incomeTab');
const expensesTab = document.getElementById('expensesTab');
const reportsTab = document.getElementById('reportsTab'); // New
const propertiesSection = document.getElementById('propertiesSection');
const tenantsSection = document.getElementById('tenantsSection'); // New
const incomeSection = document.getElementById('incomeSection');
const expensesSection = document.getElementById('expensesSection');
const reportsSection = document.getElementById('reportsSection'); // New
const addPropertyForm = document.getElementById('addPropertyForm');
const propertiesTableBody = document.querySelector('#propertiesTable tbody');
const propertiesTable = document.getElementById('propertiesTable');
const noPropertiesMessage = document.getElementById('noPropertiesMessage');
const propertiesLoading = document.getElementById('propertiesLoading');
const addTenantForm = document.getElementById('addTenantForm'); // New
const tenantPropertySelect = document.getElementById('tenantProperty'); // New
const tenantsTableBody = document.querySelector('#tenantsTable tbody'); // New
const tenantsTable = document.getElementById('tenantsTable'); // New
const noTenantsMessage = document.getElementById('noTenantsMessage'); // New
const tenantsLoading = document.getElementById('tenantsLoading'); // New
const addIncomeForm = document.getElementById('addIncomeForm');
const incomePropertySelect = document.getElementById('incomeProperty');
const incomeTableBody = document.querySelector('#incomeTable tbody');
const incomeTable = document.getElementById('incomeTable');
const noIncomeMessage = document.getElementById('noIncomeMessage');
const incomeLoading = document.getElementById('incomeLoading');
const addExpenseForm = document.getElementById('addExpenseForm');
const expensePropertySelect = document.getElementById('expenseProperty');
const expensesTableBody = document.querySelector('#expensesTable tbody');
const expensesTable = document.getElementById('expensesTable');
const noExpensesMessage = document.getElementById('noExpensesMessage');
const expensesLoading = document.getElementById('expensesLoading');
const totalIncomeDisplay = document.getElementById('totalIncome'); // New
const totalExpensesDisplay = document.getElementById('totalExpenses'); // New
const netProfitDisplay = document.getElementById('netProfit'); // New
const allTransactionsTableBody = document.querySelector('#allTransactionsTable tbody'); // New
const allTransactionsTable = document.getElementById('allTransactionsTable'); // New
const noReportsMessage = document.getElementById('noReportsMessage'); // New
const reportsLoading = document.getElementById('reportsLoading'); // New
const userIdDisplay = document.getElementById('userIdDisplay');
const messageBox = document.getElementById('messageBox');
const messageBoxOverlay = document.getElementById('messageBoxOverlay');
const messageBoxContent = document.getElementById('messageBoxContent');
const messageBoxClose = document.getElementById('messageBoxClose');
let allProperties = []; // Store properties to populate select dropdowns
let allTenants = []; // Store tenants
let allTransactions = []; // Store all income and expense transactions for reporting
let unsubscribeProperties = null;
let unsubscribeTenants = null; // New
let unsubscribeIncome = null;
let unsubscribeExpenses = null;
/**
* Displays a custom message box instead of alert/confirm.
* @param {string} message - The message to display.
* @param {function} [onClose] - Optional callback function when the OK button is clicked.
*/
function showMessageBox(message, onClose = null) {
messageBoxContent.textContent = message;
messageBox.style.display = 'block';
messageBoxOverlay.style.display = 'block';
const closeHandler = () => {
messageBox.style.display = 'none';
messageBoxOverlay.style.display = 'none';
messageBoxClose.removeEventListener('click', closeHandler);
if (onClose) {
onClose();
}
};
messageBoxClose.addEventListener('click', closeHandler);
}
/**
* Initializes Firebase and sets up authentication.
*/
async function initializeFirebase() {
try {
app = initializeApp(firebaseConfig);
db = getFirestore(app);
auth = getAuth(app);
// Listen for auth state changes
onAuthStateChanged(auth, async (user) => {
if (user) {
currentUserId = user.uid;
userIdDisplay.textContent = currentUserId;
isAuthReady = true;
console.log("Firebase authenticated. User ID:", currentUserId);
// Once authenticated, start listening to data
setupFirestoreListeners();
} else {
// Not authenticated, try to sign in
if (initialAuthToken) {
await signInWithCustomToken(auth, initialAuthToken);
} else {
await signInAnonymously(auth);
}
}
});
} catch (error) {
console.error("Error initializing Firebase or authenticating:", error);
showMessageBox("Failed to initialize the application. Please try again later.");
}
}
/**
* Sets up real-time listeners for properties, income, and expenses.
* This function should only be called once authentication is ready.
*/
function setupFirestoreListeners() {
if (!isAuthReady || !currentUserId) {
console.warn("Authentication not ready, skipping Firestore listeners setup.");
return;
}
// Unsubscribe from previous listeners if they exist
if (unsubscribeProperties) unsubscribeProperties();
if (unsubscribeTenants) unsubscribeTenants(); // New
if (unsubscribeIncome) unsubscribeIncome();
if (unsubscribeExpenses) unsubscribeExpenses();
// Listen for properties
propertiesLoading.style.display = 'block';
const propertiesQuery = query(collection(db, `artifacts/${appId}/users/${currentUserId}/properties`));
unsubscribeProperties = onSnapshot(propertiesQuery, (snapshot) => {
allProperties = [];
propertiesTableBody.innerHTML = '';
if (snapshot.empty) {
noPropertiesMessage.classList.remove('hidden');
propertiesTable.classList.add('hidden');
} else {
noPropertiesMessage.classList.add('hidden');
propertiesTable.classList.remove('hidden');
snapshot.forEach(doc => {
const property = { id: doc.id, ...doc.data() };
allProperties.push(property);
renderPropertyRow(property);
});
}
populatePropertySelects(); // Update dropdowns when properties change
propertiesLoading.style.display = 'none';
}, (error) => {
console.error("Error fetching properties:", error);
showMessageBox("Error loading properties. Please refresh the page.");
propertiesLoading.style.display = 'none';
});
// Listen for tenants (New)
tenantsLoading.style.display = 'block';
const tenantsQuery = query(collection(db, `artifacts/${appId}/users/${currentUserId}/tenants`));
unsubscribeTenants = onSnapshot(tenantsQuery, (snapshot) => {
allTenants = [];
tenantsTableBody.innerHTML = '';
if (snapshot.empty) {
noTenantsMessage.classList.remove('hidden');
tenantsTable.classList.add('hidden');
} else {
noTenantsMessage.classList.add('hidden');
tenantsTable.classList.remove('hidden');
snapshot.forEach(doc => {
const tenant = { id: doc.id, ...doc.data() };
allTenants.push(tenant);
renderTenantRow(tenant);
});
}
tenantsLoading.style.display = 'none';
}, (error) => {
console.error("Error fetching tenants:", error);
showMessageBox("Error loading tenants. Please refresh the page.");
tenantsLoading.style.display = 'none';
});
// Listen for all transactions (income and expenses combined for reporting)
reportsLoading.style.display = 'block';
const allTransactionsQuery = query(collection(db, `artifacts/${appId}/users/${currentUserId}/transactions`));
onSnapshot(allTransactionsQuery, (snapshot) => {
allTransactions = [];
snapshot.forEach(doc => {
allTransactions.push({ id: doc.id, ...doc.data() });
});
generateReport(); // Generate report whenever transactions change
reportsLoading.style.display = 'none';
}, (error) => {
console.error("Error fetching all transactions for reports:", error);
showMessageBox("Error loading transaction data for reports. Please refresh the page.");
reportsLoading.style.display = 'none';
});
// Listen for income
incomeLoading.style.display = 'block';
const incomeQuery = query(collection(db, `artifacts/${appId}/users/${currentUserId}/transactions`), where("type", "==", "income"));
unsubscribeIncome = onSnapshot(incomeQuery, (snapshot) => {
incomeTableBody.innerHTML = '';
if (snapshot.empty) {
noIncomeMessage.classList.remove('hidden');
incomeTable.classList.add('hidden');
} else {
noIncomeMessage.classList.add('hidden');
incomeTable.classList.remove('hidden');
snapshot.forEach(doc => {
const income = { id: doc.id, ...doc.data() };
renderTransactionRow(income, incomeTableBody);
});
}
incomeLoading.style.display = 'none';
}, (error) => {
console.error("Error fetching income:", error);
showMessageBox("Error loading income records. Please refresh the page.");
incomeLoading.style.display = 'none';
});
// Listen for expenses
expensesLoading.style.display = 'block';
const expensesQuery = query(collection(db, `artifacts/${appId}/users/${currentUserId}/transactions`), where("type", "==", "expense"));
unsubscribeExpenses = onSnapshot(expensesQuery, (snapshot) => {
expensesTableBody.innerHTML = '';
if (snapshot.empty) {
noExpensesMessage.classList.remove('hidden');
expensesTable.classList.add('hidden');
} else {
noExpensesMessage.classList.add('hidden');
expensesTable.classList.remove('hidden');
snapshot.forEach(doc => {
const expense = { id: doc.id, ...doc.data() };
renderTransactionRow(expense, expensesTableBody);
});
}
expensesLoading.style.display = 'none';
}, (error) => {
console.error("Error fetching expenses:", error);
showMessageBox("Error loading expense records. Please refresh the page.");
expensesLoading.style.display = 'none';
});
}
/**
* Populates the property select dropdowns for income, expense, and tenant forms.
*/
function populatePropertySelects() {
incomePropertySelect.innerHTML = '<option value="">Select Property</option>';
expensePropertySelect.innerHTML = '<option value="">Select Property</option>';
tenantPropertySelect.innerHTML = '<option value="">Select Property</option>'; // New
allProperties.forEach(property => {
const optionIncome = document.createElement('option');
optionIncome.value = property.id;
optionIncome.textContent = property.name;
incomePropertySelect.appendChild(optionIncome);
const optionExpense = document.createElement('option');
optionExpense.value = property.id;
optionExpense.textContent = property.name;
expensePropertySelect.appendChild(optionExpense);
const optionTenant = document.createElement('option'); // New
optionTenant.value = property.id;
optionTenant.textContent = property.name;
tenantPropertySelect.appendChild(optionTenant);
});
}
/**
* Renders a single property row in the properties table.
* @param {object} property - The property object.
*/
function renderPropertyRow(property) {
const row = propertiesTableBody.insertRow();
row.innerHTML = `
<td data-label="Name">${property.name}</td>
<td data-label="Address">${property.address}</td>
<td data-label="Type">${property.type}</td>
<td data-label="Purchase Date">${property.purchaseDate || 'N/A'}</td>
<td data-label="Actions" class="action-buttons">
<button class="delete-btn" data-id="${property.id}" data-type="property">Delete</button>
</td>
`;
}
/**
* Renders a single tenant row in the tenants table. (New)
* @param {object} tenant - The tenant object.
*/
function renderTenantRow(tenant) {
const row = tenantsTableBody.insertRow();
const propertyName = allProperties.find(p => p.id === tenant.propertyId)?.name || 'Unknown Property';
row.innerHTML = `
<td data-label="Name">${tenant.name}</td>
<td data-label="Contact">${tenant.contact}</td>
<td data-label="Property">${propertyName}</td>
<td data-label="Lease Start">${tenant.leaseStartDate}</td>
<td data-label="Lease End">${tenant.leaseEndDate || 'N/A'}</td>
<td data-label="Rent">$${tenant.rentAmount.toFixed(2)}</td>
<td data-label="Deposit">$${tenant.depositAmount ? tenant.depositAmount.toFixed(2) : 'N/A'}</td>
<td data-label="Actions" class="action-buttons">
<button class="delete-btn" data-id="${tenant.id}" data-type="tenant">Delete</button>
</td>
`;
}
/**
* Renders a single transaction (income or expense) row in its respective table.
* @param {object} transaction - The transaction object.
* @param {HTMLElement} tableBody - The tbody element to append the row to.
*/
function renderTransactionRow(transaction, tableBody) {
const row = tableBody.insertRow();
const propertyName = allProperties.find(p => p.id === transaction.propertyId)?.name || 'Unknown Property';
row.innerHTML = `
<td data-label="Property">${propertyName}</td>
<td data-label="Category">${transaction.category}</td>
<td data-label="Amount">$${transaction.amount.toFixed(2)}</td>
<td data-label="Date">${transaction.date}</td>
<td data-label="Description">${transaction.description || 'N/A'}</td>
<td data-label="Actions" class="action-buttons">
<button class="delete-btn" data-id="${transaction.id}" data-type="transaction">Delete</button>
</td>
`;
}
/**
* Handles adding a new property to Firestore.
* @param {Event} event - The form submission event.
*/
async function handleAddProperty(event) {
event.preventDefault();
if (!isAuthReady) {
showMessageBox("Application is still initializing. Please wait.");
return;
}
const name = document.getElementById('propertyName').value.trim();
const address = document.getElementById('propertyAddress').value.trim();
const type = document.getElementById('propertyType').value;
const purchaseDate = document.getElementById('propertyPurchaseDate').value;
if (!name || !address || !type) {
showMessageBox("Please fill in all required property fields.");
return;
}
const submitButton = event.target.querySelector('button[type="submit"]');
submitButton.disabled = true;
try {
await addDoc(collection(db, `artifacts/${appId}/users/${currentUserId}/properties`), {
name,
address,
type,
purchaseDate: purchaseDate || null,
userId: currentUserId,
createdAt: serverTimestamp()
});
addPropertyForm.reset();
showMessageBox("Property added successfully!");
} catch (error) {
console.error("Error adding property:", error);
showMessageBox("Failed to add property. Please try again.");
} finally {
submitButton.disabled = false;
}
}
/**
* Handles adding a new tenant to Firestore. (New)
* @param {Event} event - The form submission event.
*/
async function handleAddTenant(event) {
event.preventDefault();
if (!isAuthReady) {
showMessageBox("Application is still initializing. Please wait.");
return;
}
const name = document.getElementById('tenantName').value.trim();
const contact = document.getElementById('tenantContact').value.trim();
const propertyId = document.getElementById('tenantProperty').value;
const leaseStartDate = document.getElementById('leaseStartDate').value;
const leaseEndDate = document.getElementById('leaseEndDate').value;
const rentAmount = parseFloat(document.getElementById('tenantRentAmount').value);
const depositAmount = parseFloat(document.getElementById('tenantDepositAmount').value);
if (!name || !contact || !propertyId || !leaseStartDate || isNaN(rentAmount) || rentAmount <= 0) {
showMessageBox("Please fill in all required tenant fields correctly (Name, Contact, Property, Lease Start Date, Monthly Rent).");
return;
}
const submitButton = event.target.querySelector('button[type="submit"]');
submitButton.disabled = true;
try {
await addDoc(collection(db, `artifacts/${appId}/users/${currentUserId}/tenants`), {
name,
contact,
propertyId,
leaseStartDate,
leaseEndDate: leaseEndDate || null,
rentAmount,
depositAmount: isNaN(depositAmount) ? 0 : depositAmount,
userId: currentUserId,
createdAt: serverTimestamp()
});
addTenantForm.reset();
showMessageBox("Tenant added successfully!");
} catch (error) {
console.error("Error adding tenant:", error);
showMessageBox("Failed to add tenant. Please try again.");
} finally {
submitButton.disabled = false;
}
}
/**
* Handles adding a new income record to Firestore.
* @param {Event} event - The form submission event.
*/
async function handleAddIncome(event) {
event.preventDefault();
if (!isAuthReady) {
showMessageBox("Application is still initializing. Please wait.");
return;
}
const propertyId = incomePropertySelect.value;
const category = document.getElementById('incomeCategory').value;
const amount = parseFloat(document.getElementById('incomeAmount').value);
const date = document.getElementById('incomeDate').value;
const description = document.getElementById('incomeDescription').value.trim();
if (!propertyId || !category || isNaN(amount) || amount <= 0 || !date) {
showMessageBox("Please fill in all required income fields correctly.");
return;
}
const submitButton = event.target.querySelector('button[type="submit"]');
submitButton.disabled = true;
try {
await addDoc(collection(db, `artifacts/${appId}/users/${currentUserId}/transactions`), {
propertyId,
type: 'income',
category,
amount,
date,
description: description || null,
userId: currentUserId,
createdAt: serverTimestamp()
});
addIncomeForm.reset();
showMessageBox("Income record added successfully!");
} catch (error) {
console.error("Error adding income:", error);
showMessageBox("Failed to add income record. Please try again.");
} finally {
submitButton.disabled = false;
}
}
/**
* Handles adding a new expense record to Firestore.
* @param {Event} event - The form submission event.
*/
async function handleAddExpense(event) {
event.preventDefault();
if (!isAuthReady) {
showMessageBox("Application is still initializing. Please wait.");
return;
}
const propertyId = expensePropertySelect.value;
const category = document.getElementById('expenseCategory').value;
const amount = parseFloat(document.getElementById('expenseAmount').value);
const date = document.getElementById('expenseDate').value;
const description = document.getElementById('expenseDescription').value.trim();
if (!propertyId || !category || isNaN(amount) || amount <= 0 || !date) {
showMessageBox("Please fill in all required expense fields correctly.");
return;
}
const submitButton = event.target.querySelector('button[type="submit"]');
submitButton.disabled = true;
try {
await addDoc(collection(db, `artifacts/${appId}/users/${currentUserId}/transactions`), {
propertyId,
type: 'expense',
category,
amount,
date,
description: description || null,
userId: currentUserId,
createdAt: serverTimestamp()
});
addExpenseForm.reset();
showMessageBox("Expense record added successfully!");
} catch (error) {
console.error("Error adding expense:", error);
showMessageBox("Failed to add expense record. Please try again.");
} finally {
submitButton.disabled = false;
}
}
/**
* Handles deleting a property, tenant, or transaction.
* @param {string} id - The document ID to delete.
* @param {string} type - 'property', 'tenant', or 'transaction'.
*/
async function handleDelete(id, type) {
if (!isAuthReady) {
showMessageBox("Application is still initializing. Please wait.");
return;
}
let collectionPath;
switch (type) {
case 'property':
collectionPath = `artifacts/${appId}/users/${currentUserId}/properties`;
break;
case 'tenant':
collectionPath = `artifacts/${appId}/users/${currentUserId}/tenants`;
break;
case 'transaction':
collectionPath = `artifacts/${appId}/users/${currentUserId}/transactions`;
break;
default:
console.error("Invalid type for delete operation:", type);
showMessageBox("An unexpected error occurred during deletion.");
return;
}
// Custom confirmation dialog
showMessageBox(`Are you sure you want to delete this ${type}? This action cannot be undone.`, async () => {
try {
await deleteDoc(doc(db, collectionPath, id));
showMessageBox(`${type.charAt(0).toUpperCase() + type.slice(1)} deleted successfully!`);
} catch (error) {
console.error(`Error deleting ${type}:`, error);
showMessageBox(`Failed to delete ${type}. Please try again.`);
}
});
}
/**
* Generates and displays the financial report. (New)
*/
function generateReport() {
let totalIncome = 0;
let totalExpenses = 0;
allTransactionsTableBody.innerHTML = ''; // Clear previous report
if (allTransactions.length === 0) {
noReportsMessage.classList.remove('hidden');
allTransactionsTable.classList.add('hidden');
totalIncomeDisplay.textContent = '$0.00';
totalExpensesDisplay.textContent = '$0.00';
netProfitDisplay.textContent = '$0.00';
return;
} else {
noReportsMessage.classList.add('hidden');
allTransactionsTable.classList.remove('hidden');
}
// Sort transactions by date for the detailed breakdown
const sortedTransactions = [...allTransactions].sort((a, b) => new Date(a.date) - new Date(b.date));
sortedTransactions.forEach(transaction => {
if (transaction.type === 'income') {
totalIncome += transaction.amount;
} else if (transaction.type === 'expense') {
totalExpenses += transaction.amount;
}
renderReportTransactionRow(transaction);
});
const netProfit = totalIncome - totalExpenses;
totalIncomeDisplay.textContent = `$${totalIncome.toFixed(2)}`;
totalExpensesDisplay.textContent = `$${totalExpenses.toFixed(2)}`;
netProfitDisplay.textContent = `$${netProfit.toFixed(2)}`;
}
/**
* Renders a single transaction row in the all transactions report table. (New)
* @param {object} transaction - The transaction object.
*/
function renderReportTransactionRow(transaction) {
const row = allTransactionsTableBody.insertRow();
const propertyName = allProperties.find(p => p.id === transaction.propertyId)?.name || 'Unknown Property';
const amountClass = transaction.type === 'income' ? 'text-green-600' : 'text-red-600';
row.innerHTML = `
<td data-label="Type" class="capitalize">${transaction.type}</td>
<td data-label="Property">${propertyName}</td>
<td data-label="Category">${transaction.category}</td>
<td data-label="Amount" class="${amountClass}">$${transaction.amount.toFixed(2)}</td>
<td data-label="Date">${transaction.date}</td>
<td data-label="Description">${transaction.description || 'N/A'}</td>
`;
}
/**
* Switches between different sections (tabs).
* @param {string} sectionId - The ID of the section to show.
*/
function showSection(sectionId) {
// Hide all sections
propertiesSection.classList.add('hidden');
tenantsSection.classList.add('hidden'); // New
incomeSection.classList.add('hidden');
expensesSection.classList.add('hidden');
reportsSection.classList.add('hidden'); // New
// Deactivate all tab buttons
propertiesTab.classList.remove('active');
tenantsTab.classList.remove('active'); // New
incomeTab.classList.remove('active');
expensesTab.classList.remove('active');
reportsTab.classList.remove('active'); // New
// Show the selected section and activate its button
document.getElementById(sectionId).classList.remove('hidden');
document.getElementById(sectionId.replace('Section', 'Tab')).classList.add('active');
// If switching to reports, ensure report is generated
if (sectionId === 'reportsSection') {
generateReport();
}
}
// Event Listeners for Tabs
propertiesTab.addEventListener('click', () => showSection('propertiesSection'));
tenantsTab.addEventListener('click', () => showSection('tenantsSection')); // New
incomeTab.addEventListener('click', () => showSection('incomeSection'));
expensesTab.addEventListener('click', () => showSection('expensesSection'));
reportsTab.addEventListener('click', () => showSection('reportsSection')); // New
// Event Listeners for Forms
addPropertyForm.addEventListener('submit', handleAddProperty);
addTenantForm.addEventListener('submit', handleAddTenant); // New
addIncomeForm.addEventListener('submit', handleAddIncome);
addExpenseForm.addEventListener('submit', handleAddExpense);
// Event listener for delete buttons (delegated to document for dynamic elements)
document.addEventListener('click', (event) => {
if (event.target.classList.contains('delete-btn')) {
const id = event.target.dataset.id;
const type = event.target.dataset.type; // 'property', 'tenant', or 'transaction'
handleDelete(id, type);
}
});
// Initialize the app when the window loads
window.onload = () => {
initializeFirebase();
showSection('propertiesSection'); // Show properties section by default
};
</script>
</body>
</html>