This commit is contained in:
HotSwapp
2025-08-09 16:37:57 -05:00
parent 5f74243c8c
commit c2f3c4411d
35 changed files with 9209 additions and 4633 deletions

View File

@@ -1,259 +0,0 @@
/* Delphi Database System - Component Styles */
/* Login Component */
.login-page {
background-color: #f8f9fa;
}
.login-card {
max-width: 400px;
margin: 2rem auto;
}
.login-logo {
height: 60px;
margin-bottom: 1rem;
}
.login-form .input-group-text {
background-color: #e9ecef;
border-right: none;
}
.login-form .form-control {
border-left: none;
}
.login-form .form-control:focus {
border-left: none;
box-shadow: none;
}
.login-status {
margin-top: 1rem;
}
/* Customer Management Component */
.customer-search-panel {
background-color: white;
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.customer-table-container {
background-color: white;
border-radius: 0.5rem;
overflow: hidden;
}
.customer-modal .modal-dialog {
max-width: 90%;
}
.customer-form-section {
margin-bottom: 1.5rem;
}
.customer-form-section .card-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
font-weight: 600;
}
.phone-entry {
background-color: #f8f9fa;
padding: 0.75rem;
border-radius: 0.375rem;
margin-bottom: 0.5rem;
}
.phone-entry:last-child {
margin-bottom: 0;
}
/* Statistics Modal */
.stats-modal .modal-body {
background-color: #f8f9fa;
}
.stats-section {
background-color: white;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 1rem;
}
/* Navigation Component */
.navbar-shortcuts small {
font-size: 0.7rem;
opacity: 0.8;
}
.keyboard-shortcuts-modal .modal-body {
background-color: #f8f9fa;
}
.shortcuts-section {
background-color: white;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 1rem;
}
/* Dashboard Component */
.dashboard-card {
transition: transform 0.2s ease-in-out;
}
.dashboard-card:hover {
transform: translateY(-2px);
}
.dashboard-stats {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 1rem;
}
.recent-activity {
max-height: 400px;
overflow-y: auto;
}
.activity-item {
border-left: 3px solid var(--delphi-primary);
padding-left: 1rem;
margin-bottom: 1rem;
}
.activity-item:last-child {
margin-bottom: 0;
}
/* Form Components */
.form-floating-custom .form-control {
height: calc(3.5rem + 2px);
line-height: 1.25;
}
.form-floating-custom .form-control::placeholder {
color: transparent;
}
.form-floating-custom .form-control:focus ~ .form-label,
.form-floating-custom .form-control:not(:placeholder-shown) ~ .form-label {
opacity: 0.65;
transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
}
/* Search Components */
.search-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: white;
border: 1px solid #dee2e6;
border-top: none;
border-radius: 0 0 0.375rem 0.375rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
z-index: 1000;
max-height: 300px;
overflow-y: auto;
}
.search-result-item {
padding: 0.75rem 1rem;
border-bottom: 1px solid #f8f9fa;
cursor: pointer;
transition: background-color 0.15s ease-in-out;
}
.search-result-item:hover {
background-color: #f8f9fa;
}
.search-result-item:last-child {
border-bottom: none;
}
/* Notification Components */
#notification-container,
.notification-container {
z-index: 1070 !important;
}
#notification-container {
position: fixed;
top: 1rem;
right: 1rem;
width: 300px;
}
.notification {
margin-bottom: 0.5rem;
animation: slideInRight 0.3s ease-out;
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Loading Components */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.loading-spinner {
width: 2rem;
height: 2rem;
border: 0.25rem solid #f3f3f3;
border-top: 0.25rem solid var(--delphi-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
/* Table Components */
.sortable-header {
cursor: pointer;
user-select: none;
}
.sortable-header:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.sortable-header.sort-asc::after {
content: " ↑";
}
.sortable-header.sort-desc::after {
content: " ↓";
}
/* Form Validation */
.invalid-feedback.hidden {
display: none;
}
.invalid-feedback.visible {
display: block;
}

3
static/css/input.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,236 +0,0 @@
/* Delphi Consulting Group Database System - Main Styles */
/* Variables */
:root {
--delphi-primary: #0d6efd;
--delphi-secondary: #6c757d;
--delphi-success: #198754;
--delphi-info: #0dcaf0;
--delphi-warning: #ffc107;
--delphi-danger: #dc3545;
--delphi-dark: #212529;
}
/* Body and base styles */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background-color: #f8f9fa;
}
/* Navigation customizations */
.navbar-brand img {
filter: brightness(0) invert(1);
}
/* Card customizations */
.card {
border: none;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
transition: box-shadow 0.15s ease-in-out;
}
.card:hover {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
/* Button customizations */
.btn {
border-radius: 0.375rem;
font-weight: 500;
}
.btn-lg small {
font-size: 0.75rem;
font-weight: 400;
}
/* Form customizations */
.form-control {
border-radius: 0.375rem;
}
.form-control:focus {
border-color: var(--delphi-primary);
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
}
/* Table customizations */
.table {
background-color: white;
}
.table th {
border-top: none;
background-color: var(--delphi-primary);
color: white;
font-weight: 600;
}
.table tbody tr:hover {
background-color: rgba(13, 110, 253, 0.05);
}
/* Keyboard shortcut styling */
kbd {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
color: #495057;
font-size: 0.8rem;
padding: 0.125rem 0.25rem;
}
.nav-link small {
opacity: 0.7;
font-size: 0.7rem;
}
/* Modal customizations */
.modal-content {
border: none;
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175);
}
.modal-header {
background-color: var(--delphi-primary);
color: white;
}
.modal-header .btn-close {
filter: invert(1);
}
/* Status badges */
.badge {
font-size: 0.8em;
font-weight: 500;
}
/* Utility classes */
.text-primary { color: var(--delphi-primary) !important; }
.text-secondary { color: var(--delphi-secondary) !important; }
.text-success { color: var(--delphi-success) !important; }
.text-info { color: var(--delphi-info) !important; }
.text-warning { color: var(--delphi-warning) !important; }
.text-danger { color: var(--delphi-danger) !important; }
.bg-primary { background-color: var(--delphi-primary) !important; }
.bg-secondary { background-color: var(--delphi-secondary) !important; }
.bg-success { background-color: var(--delphi-success) !important; }
.bg-info { background-color: var(--delphi-info) !important; }
.bg-warning { background-color: var(--delphi-warning) !important; }
.bg-danger { background-color: var(--delphi-danger) !important; }
/* Animation classes */
.fade-in {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Responsive adjustments */
@media (max-width: 768px) {
.container-fluid {
padding-left: 1rem;
padding-right: 1rem;
}
.nav-link small {
display: none;
}
.btn-lg small {
display: none;
}
}
/* Loading spinner */
.spinner {
display: inline-block;
width: 1rem;
height: 1rem;
border: 2px solid #f3f3f3;
border-top: 2px solid var(--delphi-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Error and success messages */
.alert {
border: none;
border-radius: 0.5rem;
}
.alert-dismissible .btn-close {
padding: 1rem 0.75rem;
}
/* Data tables */
.table-responsive {
border-radius: 0.5rem;
overflow: hidden;
}
/* Form sections */
.form-section {
background: white;
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.form-section h5 {
color: var(--delphi-primary);
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #e9ecef;
}
/* Pagination */
.pagination {
margin-bottom: 0;
}
.page-link {
color: var(--delphi-primary);
}
.page-item.active .page-link {
background-color: var(--delphi-primary);
border-color: var(--delphi-primary);
}
/* Visibility utility classes */
.hidden {
display: none !important;
}
.visible {
display: block !important;
}
.visible-inline {
display: inline !important;
}
.visible-inline-block {
display: inline-block !important;
}
/* Customer management specific styles */
.delete-customer-btn {
display: none;
}
.delete-customer-btn.show {
display: inline-block;
}

2527
static/css/tailwind.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,198 +0,0 @@
/* Delphi Database System - Theme Styles */
/* Light Theme (Default) */
:root {
--delphi-primary: #0d6efd;
--delphi-primary-dark: #0b5ed7;
--delphi-primary-light: #6ea8fe;
--delphi-secondary: #6c757d;
--delphi-success: #198754;
--delphi-info: #0dcaf0;
--delphi-warning: #ffc107;
--delphi-danger: #dc3545;
--delphi-light: #f8f9fa;
--delphi-dark: #212529;
/* Background colors */
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #e9ecef;
/* Text colors */
--text-primary: #212529;
--text-secondary: #6c757d;
--text-muted: #868e96;
/* Border colors */
--border-color: #dee2e6;
--border-light: #f8f9fa;
/* Shadow */
--shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
}
/* Dark Theme */
[data-theme="dark"] {
--delphi-primary: #6ea8fe;
--delphi-primary-dark: #0b5ed7;
--delphi-primary-light: #9ec5fe;
--delphi-secondary: #adb5bd;
--delphi-success: #20c997;
--delphi-info: #39d7f0;
--delphi-warning: #ffcd39;
--delphi-danger: #ea868f;
--delphi-light: #495057;
--delphi-dark: #f8f9fa;
/* Background colors */
--bg-primary: #212529;
--bg-secondary: #343a40;
--bg-tertiary: #495057;
/* Text colors */
--text-primary: #f8f9fa;
--text-secondary: #adb5bd;
--text-muted: #6c757d;
/* Border colors */
--border-color: #495057;
--border-light: #343a40;
/* Shadow */
--shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.25);
--shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.35);
--shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.45);
}
/* High Contrast Theme */
[data-theme="high-contrast"] {
--delphi-primary: #0000ff;
--delphi-primary-dark: #000080;
--delphi-primary-light: #4040ff;
--delphi-secondary: #808080;
--delphi-success: #008000;
--delphi-info: #008080;
--delphi-warning: #ff8000;
--delphi-danger: #ff0000;
--delphi-light: #ffffff;
--delphi-dark: #000000;
/* Background colors */
--bg-primary: #ffffff;
--bg-secondary: #f0f0f0;
--bg-tertiary: #e0e0e0;
/* Text colors */
--text-primary: #000000;
--text-secondary: #404040;
--text-muted: #606060;
/* Border colors */
--border-color: #000000;
--border-light: #808080;
/* Shadow */
--shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.5);
--shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.7);
--shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.8);
}
/* Apply theme variables to components */
body {
background-color: var(--bg-secondary);
color: var(--text-primary);
}
.card {
background-color: var(--bg-primary);
border-color: var(--border-color);
box-shadow: var(--shadow-sm);
}
.navbar-dark {
background-color: var(--delphi-primary) !important;
}
.table {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.table th {
background-color: var(--delphi-primary);
border-color: var(--border-color);
}
.table td {
border-color: var(--border-color);
}
.form-control {
background-color: var(--bg-primary);
border-color: var(--border-color);
color: var(--text-primary);
}
.form-control:focus {
border-color: var(--delphi-primary);
box-shadow: 0 0 0 0.2rem rgba(var(--delphi-primary), 0.25);
}
.modal-content {
background-color: var(--bg-primary);
border-color: var(--border-color);
}
.modal-header {
background-color: var(--delphi-primary);
border-color: var(--border-color);
}
.btn-primary {
background-color: var(--delphi-primary);
border-color: var(--delphi-primary);
}
.btn-primary:hover {
background-color: var(--delphi-primary-dark);
border-color: var(--delphi-primary-dark);
}
.alert {
border-color: var(--border-color);
}
/* Theme transition */
* {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
/* Print styles */
@media print {
:root {
--delphi-primary: #000000;
--bg-primary: #ffffff;
--bg-secondary: #ffffff;
--text-primary: #000000;
--border-color: #000000;
}
.navbar, .btn, .modal, .alert {
display: none !important;
}
.card {
border: 1px solid #000000;
box-shadow: none;
}
}
/* Reduced motion preferences */
@media (prefers-reduced-motion: reduce) {
* {
transition: none !important;
animation: none !important;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

191
static/js/alerts.js Normal file
View File

@@ -0,0 +1,191 @@
// Shared alert/notification utility for consistent Tailwind styling and Font Awesome icons
// Provides: window.alerts.show(message, type?, options?) and compatibility shims
(function () {
const TYPE_ALIASES = {
error: 'danger',
success: 'success',
warning: 'warning',
info: 'info',
danger: 'danger'
};
const TYPE_CLASSES = {
success: {
container: 'border-success-200 dark:border-success-800',
icon: 'fa-solid fa-circle-check text-success-600 dark:text-success-400'
},
danger: {
container: 'border-danger-200 dark:border-danger-800',
icon: 'fa-solid fa-triangle-exclamation text-danger-600 dark:text-danger-400'
},
warning: {
container: 'border-warning-200 dark:border-warning-800',
icon: 'fa-solid fa-triangle-exclamation text-warning-600 dark:text-warning-400'
},
info: {
container: 'border-info-200 dark:border-info-800',
icon: 'fa-solid fa-circle-info text-info-600 dark:text-info-400'
}
};
function normalizeType(type) {
const key = String(type || 'info').toLowerCase();
return TYPE_ALIASES[key] || 'info';
}
function getOrCreateContainer(containerId = 'notification-container') {
let container = document.getElementById(containerId);
if (!container) {
container = document.createElement('div');
container.id = containerId;
container.className = 'fixed top-4 right-4 z-50 flex flex-col gap-2 p-0';
document.body.appendChild(container);
}
return container;
}
function show(message, type = 'info', options = {}) {
const tone = normalizeType(type);
const {
duration = 5000,
dismissible = true,
containerId = 'notification-container',
role = 'alert',
ariaLive = 'polite',
html = false,
title = null,
actions = [],
onClose = null,
id = null
} = options;
const container = getOrCreateContainer(containerId);
const wrapper = document.createElement('div');
wrapper.className = `alert-notification max-w-sm w-[22rem] bg-white dark:bg-neutral-800 border rounded-lg shadow-lg p-4 transition-all duration-300 translate-x-4 opacity-0 ${
(TYPE_CLASSES[tone] || TYPE_CLASSES.info).container
}`;
wrapper.setAttribute('role', role);
wrapper.setAttribute('aria-live', ariaLive);
if (id) wrapper.id = id;
const inner = document.createElement('div');
inner.className = 'flex items-start';
const iconWrap = document.createElement('div');
iconWrap.className = 'flex-shrink-0';
const icon = document.createElement('i');
icon.className = (TYPE_CLASSES[tone] || TYPE_CLASSES.info).icon;
iconWrap.appendChild(icon);
const content = document.createElement('div');
content.className = 'ml-3 flex-1';
if (title) {
const titleEl = document.createElement('p');
titleEl.className = 'text-sm font-semibold text-neutral-900 dark:text-neutral-100';
titleEl.textContent = String(title);
content.appendChild(titleEl);
}
const text = document.createElement('div');
text.className = 'text-xs mt-1 text-neutral-800 dark:text-neutral-200';
if (message instanceof Node) {
text.appendChild(message);
} else if (html) {
text.innerHTML = String(message || '');
} else {
text.textContent = String(message || '');
}
content.appendChild(text);
inner.appendChild(iconWrap);
inner.appendChild(content);
if (dismissible) {
const closeWrap = document.createElement('div');
closeWrap.className = 'ml-4 flex-shrink-0';
const closeBtn = document.createElement('button');
closeBtn.setAttribute('aria-label', 'Close');
closeBtn.className = 'text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300 transition-colors';
closeBtn.addEventListener('click', () => {
wrapper.remove();
if (typeof onClose === 'function') onClose();
});
const x = document.createElement('i');
x.className = 'fa-solid fa-xmark';
closeBtn.appendChild(x);
closeWrap.appendChild(closeBtn);
inner.appendChild(closeWrap);
}
// Actions (buttons)
if (Array.isArray(actions) && actions.length > 0) {
const actionsWrap = document.createElement('div');
actionsWrap.className = 'mt-2 flex gap-2 flex-wrap';
actions.forEach((action) => {
if (!action || !action.label) return;
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = String(action.label);
if (action.ariaLabel) btn.setAttribute('aria-label', action.ariaLabel);
btn.className = action.classes || 'px-3 py-1 rounded text-xs transition-colors bg-neutral-200 hover:bg-neutral-300 text-neutral-800 dark:bg-neutral-700 dark:hover:bg-neutral-600 dark:text-neutral-200';
btn.addEventListener('click', (ev) => {
try {
if (typeof action.onClick === 'function') {
action.onClick({ event: ev, wrapper });
}
} finally {
if (action.autoClose !== false) {
wrapper.remove();
if (typeof onClose === 'function') onClose();
}
}
});
actionsWrap.appendChild(btn);
});
content.appendChild(actionsWrap);
}
wrapper.appendChild(inner);
container.appendChild(wrapper);
// Animate in
requestAnimationFrame(() => {
wrapper.classList.remove('translate-x-4', 'opacity-0');
});
if (duration > 0) {
setTimeout(() => {
wrapper.classList.add('translate-x-4', 'opacity-0');
setTimeout(() => {
wrapper.remove();
if (typeof onClose === 'function') onClose();
}, 250);
}, duration);
}
return wrapper;
}
const alerts = {
show,
success: (message, options = {}) => show(message, 'success', options),
error: (message, options = {}) => show(message, 'danger', options),
warning: (message, options = {}) => show(message, 'warning', options),
info: (message, options = {}) => show(message, 'info', options),
getOrCreateContainer
};
// Expose globally
window.alerts = alerts;
// Backward-compatible shims
window.showAlert = (message, type = 'info', duration = 5000) => alerts.show(message, type, { duration });
window.showNotification = (message, type = 'info', duration = 5000) => alerts.show(message, type, { duration });
window.showToast = (message, type = 'info', duration = 3000) => alerts.show(message, type, { duration });
})();

View File

@@ -0,0 +1,560 @@
// Customer management functionality - Tailwind version
let currentPage = 0;
let currentSearch = '';
let isEditing = false;
let editingCustomerId = null;
// Helper function for authenticated API calls
function getAuthHeaders() {
const token = localStorage.getItem('auth_token');
return {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
}
// Modal management functions
function showAddCustomerModal() {
isEditing = false;
editingCustomerId = null;
document.getElementById('customerModalLabel').textContent = 'Add New Customer';
document.getElementById('deleteCustomerBtn').classList.add('hidden');
clearCustomerForm();
document.getElementById('customerModal').classList.remove('hidden');
}
function closeCustomerModal() {
document.getElementById('customerModal').classList.add('hidden');
}
function showEditCustomerModal() {
document.getElementById('customerModalLabel').textContent = 'Edit Customer';
document.getElementById('deleteCustomerBtn').classList.remove('hidden');
document.getElementById('customerModal').classList.remove('hidden');
}
// Enhanced table display function
function displayCustomers(customers) {
const tbody = document.getElementById('customersTableBody');
const emptyState = document.getElementById('emptyState');
tbody.innerHTML = '';
if (!customers || customers.length === 0) {
emptyState.classList.remove('hidden');
return;
}
emptyState.classList.add('hidden');
customers.forEach(customer => {
const row = document.createElement('tr');
row.className = 'hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors duration-150';
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-neutral-900 dark:text-neutral-100">${customer.id}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-8 w-8">
<div class="h-8 w-8 rounded-full bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center">
<span class="text-sm font-medium text-primary-600 dark:text-primary-400">${getInitials(customer)}</span>
</div>
</div>
<div class="ml-3">
<div class="text-sm font-medium text-neutral-900 dark:text-neutral-100">${formatName(customer)}</div>
${customer.title ? `<div class="text-sm text-neutral-500 dark:text-neutral-400">${customer.title}</div>` : ''}
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
${customer.group ? `<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-neutral-100 dark:bg-neutral-700 text-neutral-800 dark:text-neutral-200">${customer.group}</span>` : '<span class="text-neutral-400 dark:text-neutral-500">-</span>'}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-900 dark:text-neutral-100">
${formatLocation(customer)}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-900 dark:text-neutral-100">
${formatPhones(customer.phone_numbers)}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-900 dark:text-neutral-100">
${customer.email ? `<a href="mailto:${customer.email}" class="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 transition-colors">${customer.email}</a>` : '<span class="text-neutral-400 dark:text-neutral-500">-</span>'}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex items-center justify-end space-x-2">
<button onclick="editCustomer('${customer.id}')" class="inline-flex items-center gap-1 px-3 py-1.5 bg-primary-600 text-white hover:bg-primary-700 rounded-lg transition-colors duration-200 text-xs">
<i class="fa-solid fa-pencil"></i>
<span>Edit</span>
</button>
<button onclick="viewCustomer('${customer.id}')" class="inline-flex items-center gap-1 px-3 py-1.5 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg transition-colors duration-200 text-xs">
<i class="fa-regular fa-eye"></i>
<span>View</span>
</button>
</div>
</td>
`;
tbody.appendChild(row);
});
}
// Helper functions
function getInitials(customer) {
const first = customer.first || '';
const last = customer.last || '';
return (first.charAt(0) + last.charAt(0)).toUpperCase();
}
function formatName(customer) {
const parts = [];
if (customer.prefix) parts.push(customer.prefix);
if (customer.first) parts.push(customer.first);
if (customer.middle) parts.push(customer.middle);
parts.push(customer.last);
if (customer.suffix) parts.push(customer.suffix);
return parts.join(' ');
}
function formatLocation(customer) {
const parts = [];
if (customer.city) parts.push(customer.city);
if (customer.abrev) parts.push(customer.abrev);
return parts.join(', ') || '-';
}
function formatPhones(phones) {
if (!phones || phones.length === 0) return '<span class="text-neutral-400 dark:text-neutral-500">-</span>';
return phones.map(p =>
`<div class="mb-1">
<span class="text-xs text-neutral-500 dark:text-neutral-400">${p.location || 'Phone'}:</span>
<span class="ml-1 font-mono text-sm">${p.phone}</span>
</div>`
).join('');
}
// Enhanced pagination
function updatePagination(currentPage, totalPages) {
const pagination = document.getElementById('pagination');
pagination.innerHTML = '';
if (totalPages <= 1) return;
// Previous button
const prevButton = document.createElement('button');
prevButton.className = `px-3 py-2 text-sm rounded-lg transition-colors duration-200 ${
currentPage === 0
? 'bg-neutral-100 dark:bg-neutral-800 text-neutral-400 dark:text-neutral-500 cursor-not-allowed'
: 'bg-white dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-600 border border-neutral-200 dark:border-neutral-600'
}`;
prevButton.innerHTML = '<i class="fa-solid fa-chevron-left"></i>';
prevButton.disabled = currentPage === 0;
prevButton.onclick = () => currentPage > 0 && loadCustomers(currentPage - 1, currentSearch);
pagination.appendChild(prevButton);
// Page numbers
const startPage = Math.max(0, currentPage - 2);
const endPage = Math.min(totalPages - 1, currentPage + 2);
for (let i = startPage; i <= endPage; i++) {
const pageButton = document.createElement('button');
pageButton.className = `px-3 py-2 text-sm rounded-lg transition-colors duration-200 ${
i === currentPage
? 'bg-primary-600 text-white'
: 'bg-white dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-600 border border-neutral-200 dark:border-neutral-600'
}`;
pageButton.textContent = i + 1;
pageButton.onclick = () => loadCustomers(i, currentSearch);
pagination.appendChild(pageButton);
}
// Next button
const nextButton = document.createElement('button');
nextButton.className = `px-3 py-2 text-sm rounded-lg transition-colors duration-200 ${
currentPage === totalPages - 1
? 'bg-neutral-100 dark:bg-neutral-800 text-neutral-400 dark:text-neutral-500 cursor-not-allowed'
: 'bg-white dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-600 border border-neutral-200 dark:border-neutral-600'
}`;
nextButton.innerHTML = '<i class="fa-solid fa-chevron-right"></i>';
nextButton.disabled = currentPage === totalPages - 1;
nextButton.onclick = () => currentPage < totalPages - 1 && loadCustomers(currentPage + 1, currentSearch);
pagination.appendChild(nextButton);
}
// Enhanced alert function (delegates to shared alerts utility)
function showAlert(message, type = 'info') {
if (window.alerts && typeof window.alerts.show === 'function') {
window.alerts.show(message, type);
return;
}
// Fallback
alert(String(message));
}
// Close modal when clicking outside
document.addEventListener('click', function(event) {
const modal = document.getElementById('customerModal');
if (event.target === modal) {
closeCustomerModal();
}
});
// Handle escape key for modal
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
closeCustomerModal();
}
});
// Make functions globally available for backwards compatibility
window.showAddCustomerModal = showAddCustomerModal;
window.closeCustomerModal = closeCustomerModal;
window.showEditCustomerModal = showEditCustomerModal;
window.displayCustomers = displayCustomers;
window.showAlert = showAlert;
// Form handling functions
function clearCustomerForm() {
document.getElementById('customerId').value = '';
document.getElementById('customerId').disabled = false;
document.getElementById('last').value = '';
document.getElementById('first').value = '';
document.getElementById('middle').value = '';
document.getElementById('prefix').value = '';
document.getElementById('suffix').value = '';
document.getElementById('title').value = '';
document.getElementById('group').value = '';
document.getElementById('a1').value = '';
document.getElementById('a2').value = '';
document.getElementById('a3').value = '';
document.getElementById('city').value = '';
document.getElementById('abrev').value = '';
document.getElementById('zip').value = '';
document.getElementById('email').value = '';
document.getElementById('dob').value = '';
document.getElementById('ss_number').value = '';
document.getElementById('legal_status').value = '';
document.getElementById('memo').value = '';
document.getElementById('phoneList').innerHTML = '';
window.currentPhones = []; // Track phones with {id, location, phone, action: 'add'|'update'|'delete'|'none'}
}
async function populateCustomerForm(customer) {
document.getElementById('customerId').value = customer.id;
document.getElementById('customerId').disabled = true;
document.getElementById('last').value = customer.last || '';
document.getElementById('first').value = customer.first || '';
document.getElementById('middle').value = customer.middle || '';
document.getElementById('prefix').value = customer.prefix || '';
document.getElementById('suffix').value = customer.suffix || '';
document.getElementById('title').value = customer.title || '';
document.getElementById('group').value = customer.group || '';
document.getElementById('a1').value = customer.a1 || '';
document.getElementById('a2').value = customer.a2 || '';
document.getElementById('a3').value = customer.a3 || '';
document.getElementById('city').value = customer.city || '';
document.getElementById('abrev').value = customer.abrev || '';
document.getElementById('zip').value = customer.zip || '';
document.getElementById('email').value = customer.email || '';
document.getElementById('dob').value = customer.dob ? new Date(customer.dob).toISOString().split('T')[0] : '';
document.getElementById('ss_number').value = customer.ss_number || '';
document.getElementById('legal_status').value = customer.legal_status || '';
document.getElementById('memo').value = customer.memo || '';
// Populate phones
const phoneList = document.getElementById('phoneList');
phoneList.innerHTML = '';
window.currentPhones = customer.phone_numbers.map(p => ({...p, action: 'none'}));
window.currentPhones.forEach((phone, index) => addPhoneRow(index, phone));
}
function addPhoneRow(index, phone = {location: '', phone: '', action: 'add'}) {
const phoneList = document.getElementById('phoneList');
const row = document.createElement('div');
row.className = 'flex items-end gap-4 mb-2';
row.dataset.index = index;
row.innerHTML = `
<div class="flex-1">
<label class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Location</label>
<input type="text" value="${phone.location || ''}" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200 phone-location">
</div>
<div class="flex-1">
<label class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Phone Number</label>
<input type="tel" value="${phone.phone || ''}" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200 phone-number">
</div>
<button type="button" class="mt-6 px-3 py-2 bg-danger-600 text-white hover:bg-danger-700 rounded-lg transition-colors duration-200 text-sm remove-phone">
<i class="fa-solid fa-trash"></i>
</button>
`;
phoneList.appendChild(row);
// Event listeners for changes
row.querySelector('.phone-location').addEventListener('input', () => updatePhone(index));
row.querySelector('.phone-number').addEventListener('input', () => updatePhone(index));
row.querySelector('.remove-phone').addEventListener('click', () => removePhone(index));
}
function updatePhone(index) {
const row = document.querySelector(`[data-index="${index}"]`);
const location = row.querySelector('.phone-location').value;
const phone = row.querySelector('.phone-number').value;
const current = window.currentPhones[index];
if (current) {
current.location = location;
current.phone = phone;
if (current.action === 'none') current.action = 'update';
}
}
function removePhone(index) {
const row = document.querySelector(`[data-index="${index}"]`);
row.remove();
const phone = window.currentPhones[index];
if (phone.id) {
phone.action = 'delete';
} else {
window.currentPhones.splice(index, 1);
}
// Re-index rows
document.querySelectorAll('#phoneList > div').forEach((r, i) => {
r.dataset.index = i;
});
}
document.getElementById('addPhoneBtn').addEventListener('click', () => {
const index = window.currentPhones.length;
window.currentPhones.push({location: '', phone: '', action: 'add'});
addPhoneRow(index);
});
// Validation
async function validateForm() {
let isValid = true;
const requiredFields = ['customerId', 'last'];
requiredFields.forEach(id => {
const input = document.getElementById(id);
if (!input.value.trim()) {
isValid = false;
input.classList.add('border-danger-500');
} else {
input.classList.remove('border-danger-500');
}
});
// Email validation
const email = document.getElementById('email');
if (email.value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)) {
isValid = false;
email.classList.add('border-danger-500');
} else {
email.classList.remove('border-danger-500');
}
// Check unique ID for create
if (!isEditing) {
try {
const response = await fetch(`/api/customers/${document.getElementById('customerId').value}`);
if (response.ok) {
isValid = false;
showAlert('Customer ID already exists', 'danger');
document.getElementById('customerId').classList.add('border-danger-500');
}
} catch (error) {}
}
return isValid;
}
// Save customer
async function saveCustomer() {
if (!await validateForm()) return;
const customerData = {
id: document.getElementById('customerId').value,
last: document.getElementById('last').value,
first: document.getElementById('first').value,
middle: document.getElementById('middle').value,
prefix: document.getElementById('prefix').value,
suffix: document.getElementById('suffix').value,
title: document.getElementById('title').value,
group: document.getElementById('group').value,
a1: document.getElementById('a1').value,
a2: document.getElementById('a2').value,
a3: document.getElementById('a3').value,
city: document.getElementById('city').value,
abrev: document.getElementById('abrev').value,
zip: document.getElementById('zip').value,
email: document.getElementById('email').value,
dob: document.getElementById('dob').value || null,
ss_number: document.getElementById('ss_number').value,
legal_status: document.getElementById('legal_status').value,
memo: document.getElementById('memo').value
};
try {
let response;
if (isEditing) {
response = await fetch(`/api/customers/${editingCustomerId}`, {
method: 'PUT',
headers: getAuthHeaders(),
body: JSON.stringify(customerData)
});
} else {
response = await fetch('/api/customers/', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(customerData)
});
}
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to save customer');
}
const savedCustomer = await response.json();
// Handle phones
for (const phone of window.currentPhones) {
if (phone.action === 'add') {
await fetch(`/api/customers/${savedCustomer.id}/phones`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({location: phone.location, phone: phone.phone})
});
} else if (phone.action === 'update') {
await fetch(`/api/customers/${savedCustomer.id}/phones/${phone.id}`, {
method: 'PUT',
headers: getAuthHeaders(),
body: JSON.stringify({location: phone.location, phone: phone.phone})
});
} else if (phone.action === 'delete') {
await fetch(`/api/customers/${savedCustomer.id}/phones/${phone.id}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
}
}
showAlert(`Customer ${isEditing ? 'updated' : 'created'} successfully`, 'success');
closeCustomerModal();
loadCustomers(currentPage, currentSearch);
} catch (error) {
showAlert(`Error saving customer: ${error.message}`, 'danger');
}
}
// Edit customer
async function editCustomer(customerId) {
try {
const response = await fetch(`/api/customers/${customerId}`, {
headers: getAuthHeaders()
});
if (!response.ok) throw new Error('Failed to load customer');
const customer = await response.json();
isEditing = true;
editingCustomerId = customerId;
populateCustomerForm(customer);
showEditCustomerModal();
} catch (error) {
showAlert(`Error loading customer: ${error.message}`, 'danger');
}
}
// Delete customer
async function deleteCustomer() {
if (!confirm('Are you sure you want to delete this customer?')) return;
try {
const response = await fetch(`/api/customers/${editingCustomerId}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
if (!response.ok) throw new Error('Failed to delete customer');
showAlert('Customer deleted successfully', 'success');
closeCustomerModal();
loadCustomers(currentPage, currentSearch);
} catch (error) {
showAlert(`Error deleting customer: ${error.message}`, 'danger');
}
}
// Populate datalists
async function populateDatalists() {
try {
const groupsResp = await fetch('/api/customers/groups', {headers: getAuthHeaders()});
const groups = await groupsResp.json();
const groupList = document.getElementById('groupList');
groups.forEach(g => {
const option = document.createElement('option');
option.value = g.group;
groupList.appendChild(option);
});
const statesResp = await fetch('/api/customers/states', {headers: getAuthHeaders()});
const states = await statesResp.json();
const stateList = document.getElementById('stateList');
states.forEach(s => {
const option = document.createElement('option');
option.value = s.state;
stateList.appendChild(option);
});
} catch (error) {
console.error('Error loading datalists:', error);
}
}
// Update setupEventListeners
function setupEventListeners() {
// Existing listeners...
document.getElementById('searchBtn').addEventListener('click', performSearch);
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') performSearch();
});
document.getElementById('phoneSearchBtn').addEventListener('click', performPhoneSearch);
document.getElementById('phoneSearch').addEventListener('keypress', function(e) {
if (e.key === 'Enter') performPhoneSearch();
});
document.getElementById('addCustomerBtn').addEventListener('click', showAddCustomerModal);
document.getElementById('saveCustomerBtn').addEventListener('click', saveCustomer);
document.getElementById('deleteCustomerBtn').addEventListener('click', deleteCustomer);
document.getElementById('statsBtn').addEventListener('click', showStats);
// Form validation on blur for customerId
document.getElementById('customerId').addEventListener('blur', async function() {
if (!isEditing && this.value) {
try {
const response = await fetch(`/api/customers/${this.value}`);
if (response.ok) {
showAlert('Customer ID already exists', 'warning');
this.classList.add('border-danger-500');
} else {
this.classList.remove('border-danger-500');
}
} catch (error) {}
}
});
}
// Update DOMContentLoaded
document.addEventListener('DOMContentLoaded', function() {
const token = localStorage.getItem('auth_token');
if (!token) {
window.location.href = '/login';
return;
}
loadCustomers();
loadGroups();
loadStates();
populateDatalists();
setupEventListeners();
clearCustomerForm(); // Initial clear
});

18
static/js/financial.js Normal file
View File

@@ -0,0 +1,18 @@
// financial.js
// Assuming this is a new file
// Copy and adapt the script from financial.html
// ... add the JS content ...
// Modify modal showing/hiding to use classList.add/remove('hidden') instead of Bootstrap modal
// For example:
function showQuickTimeModal() {
document.getElementById('quickTimeModal').classList.remove('hidden');
}
// Add event listeners for close buttons, etc.
// ... complete the JS ...

View File

@@ -257,7 +257,7 @@ function focusGlobalSearch() {
// Form action functions
function newRecord() {
const newBtn = document.querySelector('.btn-new, [data-action="new"], .btn-primary[href*="new"]');
const newBtn = document.querySelector('.btn-new, [data-action="new"], .bg-primary-600[href*="new"]');
if (newBtn) {
newBtn.click();
} else {
@@ -266,7 +266,7 @@ function newRecord() {
}
function saveRecord() {
const saveBtn = document.querySelector('.btn-save, [data-action="save"], .btn-success[type="submit"]');
const saveBtn = document.querySelector('.btn-save, [data-action="save"], .bg-green-600[type="submit"]');
if (saveBtn) {
saveBtn.click();
} else {
@@ -290,7 +290,7 @@ function editMode() {
}
function completeAction() {
const completeBtn = document.querySelector('.btn-complete, [data-action="complete"], .btn-primary');
const completeBtn = document.querySelector('.btn-complete, [data-action="complete"], .bg-primary-600');
if (completeBtn) {
completeBtn.click();
} else {
@@ -313,7 +313,7 @@ function clearForm() {
}
function deleteRecord() {
const deleteBtn = document.querySelector('.btn-delete, [data-action="delete"], .btn-danger');
const deleteBtn = document.querySelector('.btn-delete, [data-action="delete"], .bg-danger-600');
if (deleteBtn) {
deleteBtn.click();
} else {
@@ -323,17 +323,15 @@ function deleteRecord() {
function cancelAction() {
// Close modals first
const modal = document.querySelector('.modal.show');
if (modal) {
const bsModal = bootstrap.Modal.getInstance(modal);
if (bsModal) {
bsModal.hide();
return;
}
// Close Tailwind-style modals
const openModal = document.querySelector('.fixed.inset-0:not(.hidden)');
if (openModal) {
openModal.classList.add('hidden');
return;
}
// Then try cancel buttons
const cancelBtn = document.querySelector('.btn-cancel, [data-action="cancel"], .btn-secondary');
const cancelBtn = document.querySelector('.btn-cancel, [data-action="cancel"], .bg-neutral-100');
if (cancelBtn) {
cancelBtn.click();
} else {
@@ -345,21 +343,24 @@ function cancelAction() {
function showHelp() {
const helpModal = document.querySelector('#shortcutsModal');
if (helpModal) {
const modal = new bootstrap.Modal(helpModal);
modal.show();
helpModal.classList.remove('hidden');
} else {
showToast('Press F1 to see keyboard shortcuts', 'info');
}
}
function showMenu() {
// Toggle main navigation menu on mobile or show dropdown
const navbarToggler = document.querySelector('.navbar-toggler');
if (navbarToggler && !navbarToggler.classList.contains('collapsed')) {
navbarToggler.click();
} else {
showToast('Menu (F10) - Use Alt+C, Alt+F, Alt+L, Alt+D for navigation', 'info');
// Toggle Tailwind mobile menu if available
if (typeof toggleMobileMenu === 'function') {
toggleMobileMenu();
return;
}
const mobileMenu = document.getElementById('mobileMenu');
if (mobileMenu) {
mobileMenu.classList.toggle('hidden');
return;
}
showToast('Menu (F10) - Use Alt+C, Alt+F, Alt+L, Alt+D for navigation', 'info');
}
function showMemo() {
@@ -446,38 +447,12 @@ function openRecord() {
}
function showToast(message, type = 'info') {
// Create toast element
const toastHtml = `
<div class="toast align-items-center text-white bg-${type}" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">${message}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
`;
// Get or create toast container
let toastContainer = document.querySelector('.toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
document.body.appendChild(toastContainer);
if (window.alerts && typeof window.alerts.show === 'function') {
window.alerts.show(message, type, { duration: 3000 });
return;
}
// Add toast
const toastWrapper = document.createElement('div');
toastWrapper.innerHTML = toastHtml;
const toastElement = toastWrapper.firstElementChild;
toastContainer.appendChild(toastElement);
// Show toast
const toast = new bootstrap.Toast(toastElement, { delay: 3000 });
toast.show();
// Remove toast element after it's hidden
toastElement.addEventListener('hidden.bs.toast', () => {
toastElement.remove();
});
// Fallback
alert(String(message));
}
// Export for use in other scripts

View File

@@ -20,17 +20,7 @@ async function initializeApp() {
window.keyboardShortcuts.initialize();
}
// Initialize tooltips
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// Initialize popovers
const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
popoverTriggerList.map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl);
});
// Remove Bootstrap-dependent tooltips/popovers; use native title/tooltips if needed
// Add form validation classes
initializeFormValidation();
@@ -44,19 +34,19 @@ async function initializeApp() {
// Form validation
function initializeFormValidation() {
// Add Bootstrap validation styles
const forms = document.querySelectorAll('form.needs-validation');
// Native validation handling without Bootstrap classes
const forms = document.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('submit', function(event) {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
form.reportValidity();
}
form.classList.add('was-validated');
});
});
// Real-time validation for specific fields
// Real-time validation for required fields (Tailwind styles)
const requiredFields = document.querySelectorAll('input[required], select[required], textarea[required]');
requiredFields.forEach(field => {
field.addEventListener('blur', function() {
@@ -67,15 +57,8 @@ function initializeFormValidation() {
function validateField(field) {
const isValid = field.checkValidity();
field.classList.remove('is-valid', 'is-invalid');
field.classList.add(isValid ? 'is-valid' : 'is-invalid');
// Show/hide custom feedback
const feedback = field.parentNode.querySelector('.invalid-feedback');
if (feedback) {
feedback.classList.toggle('hidden', isValid);
feedback.classList.toggle('visible', !isValid);
}
field.setAttribute('aria-invalid', String(!isValid));
field.classList.toggle('border-danger-500', !isValid);
}
// API helpers
@@ -157,45 +140,18 @@ function logout() {
window.location.href = '/login';
}
// Notification system
// Notification system (delegates to shared alerts utility)
function showNotification(message, type = 'info', duration = 5000) {
const notificationContainer = getOrCreateNotificationContainer();
const notification = document.createElement('div');
notification.className = `alert alert-${type} alert-dismissible fade show`;
notification.setAttribute('role', 'alert');
notification.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
notificationContainer.appendChild(notification);
// Auto-dismiss after duration
if (duration > 0) {
setTimeout(() => {
notification.remove();
}, duration);
if (window.alerts && typeof window.alerts.show === 'function') {
return window.alerts.show(message, type, { duration });
}
return notification;
}
function getOrCreateNotificationContainer() {
let container = document.querySelector('#notification-container');
if (!container) {
container = document.createElement('div');
container.id = 'notification-container';
container.className = 'position-fixed top-0 end-0 p-3';
container.classList.add('notification-container');
document.body.appendChild(container);
}
return container;
// Fallback if alerts module not yet loaded
return alert(String(message));
}
// Loading states
function showLoading(element, text = 'Loading...') {
const spinner = `<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>`;
const spinner = `<span class="inline-block animate-spin w-4 h-4 border-2 border-current border-t-transparent rounded-full mr-2"></span>`;
const originalContent = element.innerHTML;
element.innerHTML = `${spinner}${text}`;
element.disabled = true;
@@ -271,11 +227,12 @@ function addRowSelection(table) {
tbody.addEventListener('click', function(e) {
const row = e.target.closest('tr');
if (row && e.target.type !== 'checkbox') {
row.classList.toggle('table-active');
const isSelected = row.classList.toggle('bg-neutral-100');
row.classList.toggle('dark:bg-neutral-700', isSelected);
// Trigger custom event
const event = new CustomEvent('rowSelect', {
detail: { row, selected: row.classList.contains('table-active') }
detail: { row, selected: isSelected }
});
table.dispatchEvent(event);
}
@@ -342,18 +299,18 @@ function initializeSearch(searchInput, resultsContainer, searchFunction) {
function displaySearchResults(container, results) {
if (!results || results.length === 0) {
container.innerHTML = '<p class="text-muted">No results found</p>';
container.innerHTML = '<p class="text-neutral-500">No results found</p>';
return;
}
const resultsHtml = results.map(result => `
<div class="search-result p-2 border-bottom">
<div class="d-flex justify-content-between">
<div class="flex justify-between">
<div>
<strong>${result.title}</strong>
<small class="text-muted d-block">${result.description}</small>
<small class="text-neutral-500 block">${result.description}</small>
</div>
<span class="badge bg-secondary">${result.type}</span>
<span class="inline-block px-2 py-0.5 text-xs rounded bg-neutral-200 text-neutral-700">${result.type}</span>
</div>
</div>
`).join('');