Files
delphi-database/static/js/main.js
HotSwapp c2f3c4411d progress
2025-08-09 16:37:57 -05:00

366 lines
10 KiB
JavaScript

/**
* Main JavaScript for Delphi Consulting Group Database System
*/
// Global application state
const app = {
token: localStorage.getItem('auth_token'),
user: null,
initialized: false
};
// Initialize application
document.addEventListener('DOMContentLoaded', function() {
initializeApp();
});
async function initializeApp() {
// Initialize keyboard shortcuts
if (window.keyboardShortcuts) {
window.keyboardShortcuts.initialize();
}
// Remove Bootstrap-dependent tooltips/popovers; use native title/tooltips if needed
// Add form validation classes
initializeFormValidation();
// Initialize API helpers
setupAPIHelpers();
app.initialized = true;
console.log('Delphi Database System initialized');
}
// Form validation
function initializeFormValidation() {
// 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();
}
});
});
// 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() {
validateField(field);
});
});
}
function validateField(field) {
const isValid = field.checkValidity();
field.setAttribute('aria-invalid', String(!isValid));
field.classList.toggle('border-danger-500', !isValid);
}
// API helpers
function setupAPIHelpers() {
// Set up default headers for all API calls
window.apiHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
if (app.token) {
window.apiHeaders['Authorization'] = `Bearer ${app.token}`;
}
}
// API utility functions
async function apiCall(url, options = {}) {
const config = {
headers: { ...window.apiHeaders, ...options.headers },
...options
};
try {
const response = await fetch(url, config);
if (response.status === 401) {
// Token expired or invalid
logout();
throw new Error('Authentication required');
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({ detail: 'Request failed' }));
throw new Error(errorData.detail || `HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('API call failed:', error);
showNotification(`Error: ${error.message}`, 'error');
throw error;
}
}
async function apiGet(url) {
return apiCall(url, { method: 'GET' });
}
async function apiPost(url, data) {
return apiCall(url, {
method: 'POST',
body: JSON.stringify(data)
});
}
async function apiPut(url, data) {
return apiCall(url, {
method: 'PUT',
body: JSON.stringify(data)
});
}
async function apiDelete(url) {
return apiCall(url, { method: 'DELETE' });
}
// Authentication functions
function setAuthToken(token) {
app.token = token;
localStorage.setItem('auth_token', token);
window.apiHeaders['Authorization'] = `Bearer ${token}`;
}
function logout() {
app.token = null;
app.user = null;
localStorage.removeItem('auth_token');
delete window.apiHeaders['Authorization'];
window.location.href = '/login';
}
// Notification system (delegates to shared alerts utility)
function showNotification(message, type = 'info', duration = 5000) {
if (window.alerts && typeof window.alerts.show === 'function') {
return window.alerts.show(message, type, { duration });
}
// Fallback if alerts module not yet loaded
return alert(String(message));
}
// Loading states
function showLoading(element, text = 'Loading...') {
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;
element.dataset.originalContent = originalContent;
}
function hideLoading(element) {
if (element.dataset.originalContent) {
element.innerHTML = element.dataset.originalContent;
delete element.dataset.originalContent;
}
element.disabled = false;
}
// Table helpers
function initializeDataTable(tableId, options = {}) {
const table = document.getElementById(tableId);
if (!table) return null;
// Add sorting capability
const headers = table.querySelectorAll('th[data-sort]');
headers.forEach(header => {
header.classList.add('sortable-header');
header.addEventListener('click', () => sortTable(table, header));
});
// Add row selection if enabled
if (options.selectable) {
addRowSelection(table);
}
return table;
}
function sortTable(table, header) {
const columnIndex = Array.from(header.parentNode.children).indexOf(header);
const sortType = header.dataset.sort;
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
const isAscending = !header.classList.contains('sort-asc');
// Remove sort classes from all headers
table.querySelectorAll('th').forEach(th => {
th.classList.remove('sort-asc', 'sort-desc');
});
// Add sort class to current header
header.classList.add(isAscending ? 'sort-asc' : 'sort-desc');
rows.sort((a, b) => {
const aValue = a.children[columnIndex].textContent.trim();
const bValue = b.children[columnIndex].textContent.trim();
let comparison = 0;
if (sortType === 'number') {
comparison = parseFloat(aValue) - parseFloat(bValue);
} else if (sortType === 'date') {
comparison = new Date(aValue) - new Date(bValue);
} else {
comparison = aValue.localeCompare(bValue);
}
return isAscending ? comparison : -comparison;
});
// Re-append sorted rows
rows.forEach(row => tbody.appendChild(row));
}
function addRowSelection(table) {
const tbody = table.querySelector('tbody');
tbody.addEventListener('click', function(e) {
const row = e.target.closest('tr');
if (row && e.target.type !== 'checkbox') {
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: isSelected }
});
table.dispatchEvent(event);
}
});
}
// Form helpers
function serializeForm(form) {
const formData = new FormData(form);
const data = {};
for (let [key, value] of formData.entries()) {
// Handle multiple values (checkboxes, multi-select)
if (data.hasOwnProperty(key)) {
if (!Array.isArray(data[key])) {
data[key] = [data[key]];
}
data[key].push(value);
} else {
data[key] = value;
}
}
return data;
}
function populateForm(form, data) {
Object.keys(data).forEach(key => {
const field = form.querySelector(`[name="${key}"]`);
if (field) {
if (field.type === 'checkbox' || field.type === 'radio') {
field.checked = data[key];
} else {
field.value = data[key];
}
}
});
}
// Search functionality
function initializeSearch(searchInput, resultsContainer, searchFunction) {
let searchTimeout;
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
const query = this.value.trim();
if (query.length < 2) {
resultsContainer.innerHTML = '';
return;
}
searchTimeout = setTimeout(async () => {
try {
showLoading(resultsContainer, 'Searching...');
const results = await searchFunction(query);
displaySearchResults(resultsContainer, results);
} catch (error) {
resultsContainer.innerHTML = '<p class="text-danger">Search failed</p>';
}
}, 300);
});
}
function displaySearchResults(container, results) {
if (!results || results.length === 0) {
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="flex justify-between">
<div>
<strong>${result.title}</strong>
<small class="text-neutral-500 block">${result.description}</small>
</div>
<span class="inline-block px-2 py-0.5 text-xs rounded bg-neutral-200 text-neutral-700">${result.type}</span>
</div>
</div>
`).join('');
container.innerHTML = resultsHtml;
}
// Utility functions
function formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount);
}
function formatDate(date) {
return new Intl.DateTimeFormat('en-US').format(new Date(date));
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
function throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
}
}
// Export global functions
window.app = app;
window.showNotification = showNotification;
window.apiGet = apiGet;
window.apiPost = apiPost;
window.apiPut = apiPut;
window.apiDelete = apiDelete;
window.formatCurrency = formatCurrency;
window.formatDate = formatDate;