maybe good
This commit is contained in:
259
static/css/components.css
Normal file
259
static/css/components.css
Normal file
@@ -0,0 +1,259 @@
|
||||
/* 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;
|
||||
}
|
||||
236
static/css/main.css
Normal file
236
static/css/main.css
Normal file
@@ -0,0 +1,236 @@
|
||||
/* 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;
|
||||
}
|
||||
198
static/css/themes.css
Normal file
198
static/css/themes.css
Normal file
@@ -0,0 +1,198 @@
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
489
static/js/keyboard-shortcuts.js
Normal file
489
static/js/keyboard-shortcuts.js
Normal file
@@ -0,0 +1,489 @@
|
||||
/**
|
||||
* Keyboard Shortcuts for Delphi Consulting Group Database System
|
||||
* Replicates legacy Pascal system shortcuts for user familiarity
|
||||
*/
|
||||
|
||||
let keyboardShortcutsEnabled = true;
|
||||
|
||||
function initializeKeyboardShortcuts() {
|
||||
document.addEventListener('keydown', handleKeyboardShortcuts);
|
||||
console.log('Keyboard shortcuts initialized');
|
||||
}
|
||||
|
||||
function handleKeyboardShortcuts(event) {
|
||||
if (!keyboardShortcutsEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't process shortcuts if user is typing in input fields
|
||||
const activeElement = document.activeElement;
|
||||
const isInputField = ['INPUT', 'TEXTAREA', 'SELECT'].includes(activeElement.tagName) ||
|
||||
activeElement.contentEditable === 'true';
|
||||
|
||||
// Allow specific shortcuts even in input fields
|
||||
const allowedInInputs = ['F1', 'Escape'];
|
||||
const keyName = getKeyName(event);
|
||||
|
||||
if (isInputField && !allowedInInputs.includes(keyName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle shortcuts based on key combination
|
||||
const shortcut = getShortcutKey(event);
|
||||
|
||||
switch (shortcut) {
|
||||
// Help
|
||||
case 'F1':
|
||||
event.preventDefault();
|
||||
showHelp();
|
||||
break;
|
||||
|
||||
// Navigation shortcuts
|
||||
case 'Alt+C':
|
||||
event.preventDefault();
|
||||
navigateTo('/customers');
|
||||
break;
|
||||
case 'Alt+F':
|
||||
event.preventDefault();
|
||||
navigateTo('/files');
|
||||
break;
|
||||
case 'Alt+L':
|
||||
event.preventDefault();
|
||||
navigateTo('/financial');
|
||||
break;
|
||||
case 'Alt+D':
|
||||
event.preventDefault();
|
||||
navigateTo('/documents');
|
||||
break;
|
||||
case 'Alt+A':
|
||||
event.preventDefault();
|
||||
navigateTo('/admin');
|
||||
break;
|
||||
|
||||
// Global search
|
||||
case 'Ctrl+F':
|
||||
event.preventDefault();
|
||||
focusGlobalSearch();
|
||||
break;
|
||||
|
||||
// Form shortcuts
|
||||
case 'Ctrl+N':
|
||||
event.preventDefault();
|
||||
newRecord();
|
||||
break;
|
||||
case 'Ctrl+S':
|
||||
event.preventDefault();
|
||||
saveRecord();
|
||||
break;
|
||||
case 'F9':
|
||||
event.preventDefault();
|
||||
editMode();
|
||||
break;
|
||||
case 'F2':
|
||||
event.preventDefault();
|
||||
completeAction();
|
||||
break;
|
||||
case 'F8':
|
||||
event.preventDefault();
|
||||
clearForm();
|
||||
break;
|
||||
case 'Delete':
|
||||
if (!isInputField) {
|
||||
event.preventDefault();
|
||||
deleteRecord();
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
event.preventDefault();
|
||||
cancelAction();
|
||||
break;
|
||||
|
||||
// Legacy system shortcuts
|
||||
case 'F10':
|
||||
event.preventDefault();
|
||||
showMenu();
|
||||
break;
|
||||
case 'Alt+M':
|
||||
event.preventDefault();
|
||||
showMemo();
|
||||
break;
|
||||
case 'Alt+T':
|
||||
event.preventDefault();
|
||||
toggleTimer();
|
||||
break;
|
||||
case 'Alt+B':
|
||||
event.preventDefault();
|
||||
showBalanceSummary();
|
||||
break;
|
||||
|
||||
// Quick creation shortcuts
|
||||
case 'Ctrl+Shift+C':
|
||||
event.preventDefault();
|
||||
newCustomer();
|
||||
break;
|
||||
case 'Ctrl+Shift+F':
|
||||
event.preventDefault();
|
||||
newFile();
|
||||
break;
|
||||
case 'Ctrl+Shift+T':
|
||||
event.preventDefault();
|
||||
newTransaction();
|
||||
break;
|
||||
|
||||
// Date navigation (legacy system feature)
|
||||
case '+':
|
||||
if (!isInputField && isDateField(activeElement)) {
|
||||
event.preventDefault();
|
||||
changeDateBy(1);
|
||||
}
|
||||
break;
|
||||
case '-':
|
||||
if (!isInputField && isDateField(activeElement)) {
|
||||
event.preventDefault();
|
||||
changeDateBy(-1);
|
||||
}
|
||||
break;
|
||||
|
||||
// Table navigation
|
||||
case 'ArrowUp':
|
||||
if (!isInputField && isInTable()) {
|
||||
event.preventDefault();
|
||||
navigateTable('up');
|
||||
}
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (!isInputField && isInTable()) {
|
||||
event.preventDefault();
|
||||
navigateTable('down');
|
||||
}
|
||||
break;
|
||||
case 'PageUp':
|
||||
if (!isInputField && isInTable()) {
|
||||
event.preventDefault();
|
||||
navigateTable('pageup');
|
||||
}
|
||||
break;
|
||||
case 'PageDown':
|
||||
if (!isInputField && isInTable()) {
|
||||
event.preventDefault();
|
||||
navigateTable('pagedown');
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
if (!isInputField && isInTable()) {
|
||||
event.preventDefault();
|
||||
navigateTable('home');
|
||||
}
|
||||
break;
|
||||
case 'End':
|
||||
if (!isInputField && isInTable()) {
|
||||
event.preventDefault();
|
||||
navigateTable('end');
|
||||
}
|
||||
break;
|
||||
case 'Enter':
|
||||
if (!isInputField && isInTable()) {
|
||||
event.preventDefault();
|
||||
openRecord();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function getShortcutKey(event) {
|
||||
const parts = [];
|
||||
|
||||
if (event.ctrlKey) parts.push('Ctrl');
|
||||
if (event.altKey) parts.push('Alt');
|
||||
if (event.shiftKey) parts.push('Shift');
|
||||
|
||||
let key = event.key;
|
||||
|
||||
// Handle special keys
|
||||
switch (event.keyCode) {
|
||||
case 112: key = 'F1'; break;
|
||||
case 113: key = 'F2'; break;
|
||||
case 114: key = 'F3'; break;
|
||||
case 115: key = 'F4'; break;
|
||||
case 116: key = 'F5'; break;
|
||||
case 117: key = 'F6'; break;
|
||||
case 118: key = 'F7'; break;
|
||||
case 119: key = 'F8'; break;
|
||||
case 120: key = 'F9'; break;
|
||||
case 121: key = 'F10'; break;
|
||||
case 122: key = 'F11'; break;
|
||||
case 123: key = 'F12'; break;
|
||||
case 46: key = 'Delete'; break;
|
||||
case 27: key = 'Escape'; break;
|
||||
case 33: key = 'PageUp'; break;
|
||||
case 34: key = 'PageDown'; break;
|
||||
case 35: key = 'End'; break;
|
||||
case 36: key = 'Home'; break;
|
||||
case 37: key = 'ArrowLeft'; break;
|
||||
case 38: key = 'ArrowUp'; break;
|
||||
case 39: key = 'ArrowRight'; break;
|
||||
case 40: key = 'ArrowDown'; break;
|
||||
case 13: key = 'Enter'; break;
|
||||
case 187: key = '+'; break; // Plus key
|
||||
case 189: key = '-'; break; // Minus key
|
||||
}
|
||||
|
||||
parts.push(key);
|
||||
return parts.join('+');
|
||||
}
|
||||
|
||||
function getKeyName(event) {
|
||||
switch (event.keyCode) {
|
||||
case 112: return 'F1';
|
||||
case 27: return 'Escape';
|
||||
default: return event.key;
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation functions
|
||||
function navigateTo(url) {
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
function focusGlobalSearch() {
|
||||
const searchInput = document.querySelector('#global-search, .search-input, [name="search"]');
|
||||
if (searchInput) {
|
||||
searchInput.focus();
|
||||
searchInput.select();
|
||||
} else {
|
||||
navigateTo('/search');
|
||||
}
|
||||
}
|
||||
|
||||
// Form action functions
|
||||
function newRecord() {
|
||||
const newBtn = document.querySelector('.btn-new, [data-action="new"], .btn-primary[href*="new"]');
|
||||
if (newBtn) {
|
||||
newBtn.click();
|
||||
} else {
|
||||
showToast('New record shortcut not available on this page', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
function saveRecord() {
|
||||
const saveBtn = document.querySelector('.btn-save, [data-action="save"], .btn-success[type="submit"]');
|
||||
if (saveBtn) {
|
||||
saveBtn.click();
|
||||
} else {
|
||||
// Try to submit the main form
|
||||
const form = document.querySelector('form.main-form, form');
|
||||
if (form) {
|
||||
form.submit();
|
||||
} else {
|
||||
showToast('Save shortcut not available on this page', 'info');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function editMode() {
|
||||
const editBtn = document.querySelector('.btn-edit, [data-action="edit"]');
|
||||
if (editBtn) {
|
||||
editBtn.click();
|
||||
} else {
|
||||
showToast('Edit mode shortcut not available on this page', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
function completeAction() {
|
||||
const completeBtn = document.querySelector('.btn-complete, [data-action="complete"], .btn-primary');
|
||||
if (completeBtn) {
|
||||
completeBtn.click();
|
||||
} else {
|
||||
saveRecord(); // Fallback to save
|
||||
}
|
||||
}
|
||||
|
||||
function clearForm() {
|
||||
const clearBtn = document.querySelector('.btn-clear, [data-action="clear"]');
|
||||
if (clearBtn) {
|
||||
clearBtn.click();
|
||||
} else {
|
||||
// Clear all form inputs
|
||||
const form = document.querySelector('form');
|
||||
if (form) {
|
||||
form.reset();
|
||||
showToast('Form cleared', 'info');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deleteRecord() {
|
||||
const deleteBtn = document.querySelector('.btn-delete, [data-action="delete"], .btn-danger');
|
||||
if (deleteBtn) {
|
||||
deleteBtn.click();
|
||||
} else {
|
||||
showToast('Delete shortcut not available on this page', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
function cancelAction() {
|
||||
// Close modals first
|
||||
const modal = document.querySelector('.modal.show');
|
||||
if (modal) {
|
||||
const bsModal = bootstrap.Modal.getInstance(modal);
|
||||
if (bsModal) {
|
||||
bsModal.hide();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Then try cancel buttons
|
||||
const cancelBtn = document.querySelector('.btn-cancel, [data-action="cancel"], .btn-secondary');
|
||||
if (cancelBtn) {
|
||||
cancelBtn.click();
|
||||
} else {
|
||||
window.history.back();
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy system specific functions
|
||||
function showHelp() {
|
||||
const helpModal = document.querySelector('#shortcutsModal');
|
||||
if (helpModal) {
|
||||
const modal = new bootstrap.Modal(helpModal);
|
||||
modal.show();
|
||||
} 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');
|
||||
}
|
||||
}
|
||||
|
||||
function showMemo() {
|
||||
const memoBtn = document.querySelector('[data-action="memo"], .btn-memo');
|
||||
if (memoBtn) {
|
||||
memoBtn.click();
|
||||
} else {
|
||||
showToast('Memo function not available on this page', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTimer() {
|
||||
const timerBtn = document.querySelector('[data-action="timer"], .btn-timer');
|
||||
if (timerBtn) {
|
||||
timerBtn.click();
|
||||
} else {
|
||||
showToast('Timer function not available on this page', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
function showBalanceSummary() {
|
||||
const balanceBtn = document.querySelector('[data-action="balance"], .btn-balance');
|
||||
if (balanceBtn) {
|
||||
balanceBtn.click();
|
||||
} else {
|
||||
showToast('Balance summary not available on this page', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// Quick creation functions
|
||||
function newCustomer() {
|
||||
navigateTo('/customers/new');
|
||||
}
|
||||
|
||||
function newFile() {
|
||||
navigateTo('/files/new');
|
||||
}
|
||||
|
||||
function newTransaction() {
|
||||
navigateTo('/financial/new');
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function isDateField(element) {
|
||||
if (!element) return false;
|
||||
return element.type === 'date' ||
|
||||
element.classList.contains('date-field') ||
|
||||
element.getAttribute('data-type') === 'date';
|
||||
}
|
||||
|
||||
function isInTable() {
|
||||
const activeElement = document.activeElement;
|
||||
return activeElement && (
|
||||
activeElement.closest('table') ||
|
||||
activeElement.classList.contains('table-row') ||
|
||||
activeElement.getAttribute('role') === 'gridcell'
|
||||
);
|
||||
}
|
||||
|
||||
function changeDateBy(days) {
|
||||
const activeElement = document.activeElement;
|
||||
if (isDateField(activeElement)) {
|
||||
const currentDate = new Date(activeElement.value || Date.now());
|
||||
currentDate.setDate(currentDate.getDate() + days);
|
||||
activeElement.value = currentDate.toISOString().split('T')[0];
|
||||
activeElement.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
|
||||
function navigateTable(direction) {
|
||||
// Table navigation implementation would depend on the specific table structure
|
||||
showToast(`Table navigation: ${direction}`, 'info');
|
||||
}
|
||||
|
||||
function openRecord() {
|
||||
const activeElement = document.activeElement;
|
||||
const row = activeElement.closest('tr, .table-row');
|
||||
if (row) {
|
||||
const link = row.querySelector('a, [data-action="open"]');
|
||||
if (link) {
|
||||
link.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
|
||||
// Export for use in other scripts
|
||||
window.keyboardShortcuts = {
|
||||
initialize: initializeKeyboardShortcuts,
|
||||
enable: () => { keyboardShortcutsEnabled = true; },
|
||||
disable: () => { keyboardShortcutsEnabled = false; },
|
||||
showHelp: showHelp
|
||||
};
|
||||
409
static/js/main.js
Normal file
409
static/js/main.js
Normal file
@@ -0,0 +1,409 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user