Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Accounts Management</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| } | |
| /* Custom styles for accessibility and RPA bots */ | |
| [data-automation-id] { | |
| /* This is a hook for automation tools */ | |
| } | |
| .status-active { | |
| color: #16a34a; /* green-600 */ | |
| background-color: #dcfce7; /* green-100 */ | |
| } | |
| .status-pending { | |
| color: #d97706; /* amber-600 */ | |
| background-color: #fef3c7; /* amber-100 */ | |
| } | |
| .status-closed { | |
| color: #dc2626; /* red-600 */ | |
| background-color: #fee2e2; /* red-100 */ | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 text-gray-800"> | |
| <div class="container mx-auto p-4 sm:p-6 lg:p-8"> | |
| <header class="mb-8"> | |
| <h1 class="text-3xl font-bold text-gray-900" data-automation-id="main-header">Accounts Dashboard</h1> | |
| <p class="text-gray-600 mt-1">Manage and track your customer accounts.</p> | |
| </header> | |
| <div id="summary-section" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-8" data-automation-id="summary-section"> | |
| <div class="bg-white p-6 rounded-lg shadow-md"> | |
| <h2 class="text-sm font-medium text-gray-500">Total Accounts</h2> | |
| <p id="total-accounts" class="text-3xl font-semibold text-gray-900" data-automation-id="total-accounts-value">0</p> | |
| </div> | |
| <div class="bg-white p-6 rounded-lg shadow-md"> | |
| <h2 class="text-sm font-medium text-gray-500">Total Revenue</h2> | |
| <p id="total-revenue" class="text-3xl font-semibold text-gray-900" data-automation-id="total-revenue-value">$0.00</p> | |
| </div> | |
| <div class="bg-white p-6 rounded-lg shadow-md"> | |
| <h2 class="text-sm font-medium text-gray-500">Active Accounts</h2> | |
| <p id="active-accounts" class="text-3xl font-semibold text-gray-900" data-automation-id="active-accounts-value">0</p> | |
| </div> | |
| </div> | |
| <div class="bg-white p-6 rounded-lg shadow-md"> | |
| <div class="flex flex-col sm:flex-row justify-between items-center mb-6 gap-4"> | |
| <div class="relative w-full sm:w-auto"> | |
| <input type="text" id="search-input" placeholder="Search accounts..." class="pl-10 pr-4 py-2 border rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-blue-500" data-automation-id="search-input"> | |
| <svg class="w-5 h-5 text-gray-400 absolute left-3 top-1/2 transform -translate-y-1/2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> | |
| </svg> | |
| </div> | |
| <div class="flex gap-2 w-full sm:w-auto"> | |
| <button id="add-account-btn" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition w-full sm:w-auto" data-automation-id="add-account-button">Add Account</button> | |
| <button id="export-csv-btn" class="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition w-full sm:w-auto" data-automation-id="export-csv-button">Export to CSV</button> | |
| </div> | |
| </div> | |
| <div class="overflow-x-auto"> | |
| <table id="accounts-table" class="min-w-full bg-white" data-automation-id="accounts-table"> | |
| <thead class="bg-gray-100"> | |
| <tr> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Account Name</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Revenue</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">State</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Industry</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody id="accounts-table-body" class="divide-y divide-gray-200"> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="account-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden" data-automation-id="account-modal"> | |
| <div class="relative top-20 mx-auto p-5 border w-full max-w-md shadow-lg rounded-md bg-white"> | |
| <div class="mt-3 text-center"> | |
| <h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">Add Account</h3> | |
| <div class="mt-2 px-7 py-3"> | |
| <form id="account-form"> | |
| <input type="hidden" id="account-id"> | |
| <input id="account-name" class="mb-3 px-3 py-2 text-gray-700 border rounded-lg w-full focus:outline-none" type="text" placeholder="Account Name" required data-automation-id="modal-account-name"> | |
| <input id="account-revenue" class="mb-3 px-3 py-2 text-gray-700 border rounded-lg w-full focus:outline-none" type="number" placeholder="Revenue" required data-automation-id="modal-account-revenue"> | |
| <input id="account-state" class="mb-3 px-3 py-2 text-gray-700 border rounded-lg w-full focus:outline-none" type="text" placeholder="State" required data-automation-id="modal-account-state"> | |
| <input id="account-industry" class="mb-3 px-3 py-2 text-gray-700 border rounded-lg w-full focus:outline-none" type="text" placeholder="Industry" required data-automation-id="modal-account-industry"> | |
| <select id="account-status" class="mb-3 px-3 py-2 text-gray-700 border rounded-lg w-full focus:outline-none" required data-automation-id="modal-account-status"> | |
| <option value="Active">Active</option> | |
| <option value="Pending">Pending</option> | |
| <option value="Closed">Closed</option> | |
| </select> | |
| </form> | |
| </div> | |
| <div class="items-center px-4 py-3"> | |
| <button id="save-account-btn" class="px-4 py-2 bg-blue-500 text-white text-base font-medium rounded-md w-full shadow-sm hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-300" data-automation-id="modal-save-button">Save</button> | |
| <button id="cancel-btn" class="mt-2 px-4 py-2 bg-gray-200 text-gray-800 text-base font-medium rounded-md w-full shadow-sm hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-300" data-automation-id="modal-cancel-button">Cancel</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="delete-confirm-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden" data-automation-id="delete-confirm-modal"> | |
| <div class="relative top-20 mx-auto p-5 border w-full max-w-md shadow-lg rounded-md bg-white"> | |
| <div class="mt-3 text-center"> | |
| <div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100"> | |
| <svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> | |
| </svg> | |
| </div> | |
| <h3 class="text-lg leading-6 font-medium text-gray-900 mt-4">Delete Account</h3> | |
| <div class="mt-2 px-7 py-3"> | |
| <p class="text-sm text-gray-500">Are you sure you want to delete this account? This action cannot be undone.</p> | |
| </div> | |
| <div class="items-center px-4 py-3 flex gap-2"> | |
| <button id="confirm-delete-btn" class="px-4 py-2 bg-red-600 text-white text-base font-medium rounded-md w-full shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-300" data-automation-id="modal-confirm-delete-button">Delete</button> | |
| <button id="cancel-delete-btn" class="px-4 py-2 bg-gray-200 text-gray-800 text-base font-medium rounded-md w-full shadow-sm hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-300" data-automation-id="modal-cancel-delete-button">Cancel</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function () { | |
| // --- STATE MANAGEMENT --- | |
| let accounts = [ | |
| { id: 1, name: 'Innovate Corp LLC', revenue: 250000, state: 'CA', industry: 'Technology', status: 'Active' }, | |
| { id: 2, name: 'HealthFirst Inc', revenue: 500000, state: 'NY', industry: 'Healthcare', status: 'Active' } | |
| ]; | |
| let accountIdToDelete = null; // (NEW) To track which account to delete | |
| // --- DOM ELEMENTS --- | |
| const tableBody = document.getElementById('accounts-table-body'); | |
| const searchInput = document.getElementById('search-input'); | |
| const exportCsvBtn = document.getElementById('export-csv-btn'); | |
| const addAccountBtn = document.getElementById('add-account-btn'); | |
| // Add/Edit Modal Elements | |
| const modal = document.getElementById('account-modal'); | |
| const modalTitle = document.getElementById('modal-title'); | |
| const cancelButton = document.getElementById('cancel-btn'); | |
| const saveAccountBtn = document.getElementById('save-account-btn'); | |
| const accountForm = document.getElementById('account-form'); | |
| const accountIdInput = document.getElementById('account-id'); | |
| const accountNameInput = document.getElementById('account-name'); | |
| const accountRevenueInput = document.getElementById('account-revenue'); | |
| const accountStateInput = document.getElementById('account-state'); | |
| const accountIndustryInput = document.getElementById('account-industry'); | |
| const accountStatusInput = document.getElementById('account-status'); | |
| // (NEW) Delete Modal Elements | |
| const deleteConfirmModal = document.getElementById('delete-confirm-modal'); | |
| const confirmDeleteBtn = document.getElementById('confirm-delete-btn'); | |
| const cancelDeleteBtn = document.getElementById('cancel-delete-btn'); | |
| // --- FUNCTIONS --- | |
| /** | |
| * Renders the accounts in the table | |
| * @param {Array} accountsToRender - The array of accounts to display | |
| */ | |
| function renderTable(accountsToRender) { | |
| tableBody.innerHTML = ''; | |
| if (accountsToRender.length === 0) { | |
| tableBody.innerHTML = `<tr><td colspan="6" class="text-center py-4">No accounts found.</td></tr>`; | |
| return; | |
| } | |
| accountsToRender.forEach(acc => { | |
| const statusClass = getStatusClass(acc.status); | |
| const row = document.createElement('tr'); | |
| row.setAttribute('data-account-id', acc.id); // For easy selection by bots | |
| row.innerHTML = ` | |
| <td class="px-6 py-4 whitespace-nowrap" data-label="Account Name">${acc.name}</td> | |
| <td class="px-6 py-4 whitespace-nowrap" data-label="Revenue">${formatCurrency(acc.revenue)}</td> | |
| <td class="px-6 py-4 whitespace-nowrap" data-label="State">${acc.state}</td> | |
| <td class="px-6 py-4 whitespace-nowrap" data-label="Industry">${acc.industry}</td> | |
| <td class="px-6 py-4 whitespace-nowrap" data-label="Status"> | |
| <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${statusClass}"> | |
| ${acc.status} | |
| </span> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm font-medium" data-label="Actions"> | |
| <button class="text-indigo-600 hover:text-indigo-900 mr-3 edit-btn" data-id="${acc.id}" data-automation-id="edit-btn-${acc.id}">Edit</button> | |
| <button class="text-red-600 hover:text-red-900 delete-btn" data-id="${acc.id}" data-automation-id="delete-btn-${acc.id}">Delete</button> | |
| </td> | |
| `; | |
| tableBody.appendChild(row); | |
| }); | |
| updateSummary(); | |
| } | |
| /** | |
| * Updates the summary cards with the latest data | |
| */ | |
| function updateSummary() { | |
| const totalAccounts = accounts.length; | |
| const totalRevenue = accounts.reduce((sum, acc) => sum + acc.revenue, 0); | |
| const activeAccounts = accounts.filter(acc => acc.status === 'Active').length; | |
| document.getElementById('total-accounts').textContent = totalAccounts; | |
| document.getElementById('total-revenue').textContent = formatCurrency(totalRevenue); | |
| document.getElementById('active-accounts').textContent = activeAccounts; | |
| } | |
| /** | |
| * Gets the CSS class for the status badge | |
| * @param {string} status - The status of the account | |
| * @returns {string} The CSS class | |
| */ | |
| function getStatusClass(status) { | |
| switch (status) { | |
| case 'Active': return 'status-active'; | |
| case 'Pending': return 'status-pending'; | |
| case 'Closed': return 'status-closed'; | |
| default: return 'bg-gray-100 text-gray-800'; | |
| } | |
| } | |
| /** | |
| * Formats a number as currency | |
| * @param {number} value - The number to format | |
| * @returns {string} The formatted currency string | |
| */ | |
| function formatCurrency(value) { | |
| return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value); | |
| } | |
| /** | |
| * Handles the search/filter functionality | |
| */ | |
| function handleSearch() { | |
| const searchTerm = searchInput.value.toLowerCase(); | |
| const filteredAccounts = accounts.filter(acc => | |
| Object.values(acc).some(val => | |
| String(val).toLowerCase().includes(searchTerm) | |
| ) | |
| ); | |
| renderTable(filteredAccounts); | |
| } | |
| /** | |
| * Exports the table data to a CSV file | |
| */ | |
| function exportToCsv() { | |
| const headers = ['ID', 'Account Name', 'Revenue', 'State', 'Industry', 'Status']; | |
| const rows = accounts.map(acc => [ | |
| acc.id, | |
| `"${acc.name.replace(/"/g, '""')}"`, | |
| acc.revenue, | |
| acc.state, | |
| acc.industry, | |
| acc.status | |
| ].join(',')); | |
| const csvContent = "data:text/csv;charset=utf-8," + [headers.join(','), ...rows].join('\n'); | |
| const encodedUri = encodeURI(csvContent); | |
| const link = document.createElement("a"); | |
| link.setAttribute("href", encodedUri); | |
| link.setAttribute("download", "accounts.csv"); | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| } | |
| /** | |
| * Shows the add/edit modal | |
| * @param {Object|null} account - The account to edit, or null to add a new one | |
| */ | |
| function showModal(account = null) { | |
| accountForm.reset(); | |
| if (account) { | |
| modalTitle.textContent = 'Edit Account'; | |
| accountIdInput.value = account.id; | |
| accountNameInput.value = account.name; | |
| accountRevenueInput.value = account.revenue; | |
| accountStateInput.value = account.state; | |
| accountIndustryInput.value = account.industry; | |
| accountStatusInput.value = account.status; | |
| } else { | |
| modalTitle.textContent = 'Add Account'; | |
| accountIdInput.value = ''; | |
| } | |
| modal.classList.remove('hidden'); | |
| } | |
| /** | |
| * Hides the add/edit modal | |
| */ | |
| function hideModal() { | |
| modal.classList.add('hidden'); | |
| } | |
| /** | |
| * Handles saving an account (add or edit) | |
| * @param {Event} e - The form submit event | |
| */ | |
| function saveAccount(e) { | |
| e.preventDefault(); | |
| const id = accountIdInput.value; | |
| const accountData = { | |
| name: accountNameInput.value, | |
| revenue: parseFloat(accountRevenueInput.value), | |
| state: accountStateInput.value, | |
| industry: accountIndustryInput.value, | |
| status: accountStatusInput.value, | |
| }; | |
| if (id) { // Editing existing account | |
| const index = accounts.findIndex(acc => acc.id == id); | |
| if (index !== -1) { | |
| accounts[index] = { ...accounts[index], ...accountData }; | |
| } | |
| } else { // Adding new account | |
| accountData.id = accounts.length > 0 ? Math.max(...accounts.map(a => a.id)) + 1 : 1; | |
| accounts.push(accountData); | |
| } | |
| hideModal(); | |
| renderTable(accounts); | |
| } | |
| /** | |
| * (NEW) Shows the delete confirmation modal | |
| * @param {number} id - The ID of the account to be deleted | |
| */ | |
| function showDeleteConfirmModal(id) { | |
| accountIdToDelete = id; | |
| deleteConfirmModal.classList.remove('hidden'); | |
| } | |
| /** | |
| * (NEW) Hides the delete confirmation modal | |
| */ | |
| function hideDeleteConfirmModal() { | |
| accountIdToDelete = null; | |
| deleteConfirmModal.classList.add('hidden'); | |
| } | |
| /** | |
| * (NEW) Actually performs the account deletion | |
| */ | |
| function performDelete() { | |
| if (accountIdToDelete !== null) { | |
| accounts = accounts.filter(acc => acc.id !== accountIdToDelete); | |
| renderTable(accounts); | |
| hideDeleteConfirmModal(); | |
| } | |
| } | |
| // --- EVENT LISTENERS --- | |
| searchInput.addEventListener('input', handleSearch); | |
| exportCsvBtn.addEventListener('click', exportToCsv); | |
| addAccountBtn.addEventListener('click', () => showModal()); | |
| cancelButton.addEventListener('click', hideModal); | |
| accountForm.addEventListener('submit', saveAccount); | |
| saveAccountBtn.addEventListener('click', () => accountForm.requestSubmit()); | |
| // (NEW) Listeners for the delete confirmation modal | |
| confirmDeleteBtn.addEventListener('click', performDelete); | |
| cancelDeleteBtn.addEventListener('click', hideDeleteConfirmModal); | |
| tableBody.addEventListener('click', function(e) { | |
| if (e.target.classList.contains('edit-btn')) { | |
| const id = parseInt(e.target.dataset.id); | |
| const account = accounts.find(acc => acc.id === id); | |
| showModal(account); | |
| } | |
| if (e.target.classList.contains('delete-btn')) { | |
| const id = parseInt(e.target.dataset.id); | |
| // (MODIFIED) Show the custom modal instead of the browser confirm | |
| showDeleteConfirmModal(id); | |
| } | |
| }); | |
| // --- INITIAL RENDER --- | |
| renderTable(accounts); | |
| }); | |
| </script> | |
| </body> | |
| </html> |