409 lines
12 KiB
JavaScript
409 lines
12 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();
|
|
}
|
|
|
|
// 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);
|
|
});
|
|
|
|
// Add form validation classes
|
|
initializeFormValidation();
|
|
|
|
// Initialize API helpers
|
|
setupAPIHelpers();
|
|
|
|
app.initialized = true;
|
|
console.log('Delphi Database System initialized');
|
|
}
|
|
|
|
// Form validation
|
|
function initializeFormValidation() {
|
|
// Add Bootstrap validation styles
|
|
const forms = document.querySelectorAll('form.needs-validation');
|
|
forms.forEach(form => {
|
|
form.addEventListener('submit', function(event) {
|
|
if (!form.checkValidity()) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
form.classList.add('was-validated');
|
|
});
|
|
});
|
|
|
|
// Real-time validation for specific fields
|
|
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.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);
|
|
}
|
|
}
|
|
|
|
// 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
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// 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 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') {
|
|
row.classList.toggle('table-active');
|
|
|
|
// Trigger custom event
|
|
const event = new CustomEvent('rowSelect', {
|
|
detail: { row, selected: row.classList.contains('table-active') }
|
|
});
|
|
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-muted">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>
|
|
<strong>${result.title}</strong>
|
|
<small class="text-muted d-block">${result.description}</small>
|
|
</div>
|
|
<span class="badge bg-secondary">${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; |