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; } }

Property Management Software

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

OK
// Import Firebase modules import { initializeApp } from “https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js&#8221;; import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from “https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js&#8221;; import { getFirestore, collection, addDoc, getDocs, doc, deleteDoc, onSnapshot, query, where, serverTimestamp, updateDoc } from “https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js&#8221;; // 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 };