Property Manager
https://cdn.tailwindcss.com
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;
}
}
User ID: Loading…
Properties
Tenants
Income
Expenses
Reports
Manage Properties
Property Name
Address
Type
Select Type
Residential
Commercial
Land
Other
Purchase Date
Add Property
Your Properties
No properties added yet.
| Name |
Address |
Type |
Purchase Date |
Actions |
Manage Tenants
Tenant Name
Contact Info
Property
Select Property
Lease Start Date
Lease End Date
Monthly Rent ($)
Security Deposit ($)
Add Tenant
Your Tenants
No tenants added yet.
| Name |
Contact |
Property |
Lease Start |
Lease End |
Rent |
Deposit |
Actions |
Track Income
Property
Select Property
Category
Select Category
Rent
Late Fee
Other Income
Amount ($)
Date
Description (Optional)
Add Income
Income Records
Track Expenses
Property
Select Property
Category
Select Category
Repairs
Utilities
Property Tax
Insurance
Mortgage
Maintenance
Other Expense
Amount ($)
Date
Description (Optional)
Add Expense
Expense Records
No expense records yet.
| Property |
Category |
Amount |
Date |
Description |
Actions |
Financial Reports
Detailed Breakdown (All Transactions)
No transactions to report yet.
| Type |
Property |
Category |
Amount |
Date |
Description |
// 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 = ‘Select Property’;
expensePropertySelect.innerHTML = ‘Select Property’;
tenantPropertySelect.innerHTML = ‘Select Property’; // 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 = `
${property.name} |
${property.address} |
${property.type} |
${property.purchaseDate || ‘N/A’} |
Delete
|
`;
}
/**
* 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 = `
${tenant.name} |
${tenant.contact} |
${propertyName} |
${tenant.leaseStartDate} |
${tenant.leaseEndDate || ‘N/A’} |
$${tenant.rentAmount.toFixed(2)} |
$${tenant.depositAmount ? tenant.depositAmount.toFixed(2) : ‘N/A’} |
Delete
|
`;
}
/**
* 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 = `
${propertyName} |
${transaction.category} |
$${transaction.amount.toFixed(2)} |
${transaction.date} |
${transaction.description || ‘N/A’} |
Delete
|
`;
}
/**
* 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 {
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 = `
${transaction.type} |
${propertyName} |
${transaction.category} |
$${transaction.amount.toFixed(2)} |
${transaction.date} |
${transaction.description || ‘N/A’} |
`;
}
/**
* 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
};