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

@@ -17,7 +17,7 @@ Modern database system for legal practice management, financial tracking, and do
## 🛠️ Technology Stack ## 🛠️ Technology Stack
- **Backend**: Python 3.12, FastAPI, SQLAlchemy 2.0+ - **Backend**: Python 3.12, FastAPI, SQLAlchemy 2.0+
- **Database**: SQLite (single file) - **Database**: SQLite (single file)
- **Frontend**: Jinja2 templates, Bootstrap 5.3, vanilla JavaScript - **Frontend**: Jinja2 templates, Tailwind CSS 3, vanilla JavaScript
- **Authentication**: JWT with bcrypt password hashing - **Authentication**: JWT with bcrypt password hashing
- **Validation**: Pydantic v2 - **Validation**: Pydantic v2

View File

@@ -20,7 +20,8 @@ from app.auth.schemas import (
Token, Token,
UserCreate, UserCreate,
UserResponse, UserResponse,
LoginRequest LoginRequest,
ThemePreferenceUpdate
) )
from app.config import settings from app.config import settings
@@ -117,3 +118,22 @@ async def list_users(
"""List all users (admin only)""" """List all users (admin only)"""
users = db.query(User).all() users = db.query(User).all()
return users return users
@router.post("/theme-preference")
async def update_theme_preference(
theme_data: ThemePreferenceUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Update user's theme preference"""
if theme_data.theme_preference not in ['light', 'dark']:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Theme preference must be 'light' or 'dark'"
)
current_user.theme_preference = theme_data.theme_preference
db.commit()
return {"message": "Theme preference updated successfully", "theme": theme_data.theme_preference}

View File

@@ -2,7 +2,7 @@
Document Management API endpoints - QDROs, Templates, and General Documents Document Management API endpoints - QDROs, Templates, and General Documents
""" """
from typing import List, Optional, Dict, Any from typing import List, Optional, Dict, Any
from fastapi import APIRouter, Depends, HTTPException, status, Query, UploadFile, File from fastapi import APIRouter, Depends, HTTPException, status, Query, UploadFile, File, Form
from sqlalchemy.orm import Session, joinedload from sqlalchemy.orm import Session, joinedload
from sqlalchemy import or_, func, and_, desc, asc, text from sqlalchemy import or_, func, and_, desc, asc, text
from datetime import date, datetime from datetime import date, datetime
@@ -17,6 +17,7 @@ from app.models.rolodex import Rolodex
from app.models.lookups import FormIndex, FormList, Footer, Employee from app.models.lookups import FormIndex, FormList, Footer, Employee
from app.models.user import User from app.models.user import User
from app.auth.security import get_current_user from app.auth.security import get_current_user
from app.models.additional import Document
router = APIRouter() router = APIRouter()
@@ -663,3 +664,104 @@ def _merge_template_variables(content: str, variables: Dict[str, Any]) -> str:
merged = merged.replace(f"^{var_name}", str(value or "")) merged = merged.replace(f"^{var_name}", str(value or ""))
return merged return merged
@router.post("/upload/{file_no}")
async def upload_document(
file_no: str,
file: UploadFile = File(...),
description: Optional[str] = Form(None),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Upload a document to a file"""
file_obj = db.query(FileModel).filter(FileModel.file_no == file_no).first()
if not file_obj:
raise HTTPException(status_code=404, detail="File not found")
if not file.filename:
raise HTTPException(status_code=400, detail="No file uploaded")
allowed_types = [
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"image/jpeg",
"image/png"
]
if file.content_type not in allowed_types:
raise HTTPException(status_code=400, detail="Invalid file type")
max_size = 10 * 1024 * 1024 # 10MB
content = await file.read()
if len(content) > max_size:
raise HTTPException(status_code=400, detail="File too large")
upload_dir = f"uploads/{file_no}"
os.makedirs(upload_dir, exist_ok=True)
ext = file.filename.split(".")[-1]
unique_name = f"{uuid.uuid4()}.{ext}"
path = f"{upload_dir}/{unique_name}"
with open(path, "wb") as f:
f.write(content)
doc = Document(
file_no=file_no,
filename=file.filename,
path=path,
description=description,
type=file.content_type,
size=len(content),
uploaded_by=current_user.username
)
db.add(doc)
db.commit()
db.refresh(doc)
return doc
@router.get("/{file_no}/uploaded")
async def list_uploaded_documents(
file_no: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""List uploaded documents for a file"""
docs = db.query(Document).filter(Document.file_no == file_no).all()
return docs
@router.delete("/uploaded/{doc_id}")
async def delete_document(
doc_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Delete an uploaded document"""
doc = db.query(Document).filter(Document.id == doc_id).first()
if not doc:
raise HTTPException(status_code=404, detail="Document not found")
if os.path.exists(doc.path):
os.remove(doc.path)
db.delete(doc)
db.commit()
return {"message": "Document deleted successfully"}
@router.put("/uploaded/{doc_id}")
async def update_document(
doc_id: int,
description: str = Form(...),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Update document description"""
doc = db.query(Document).filter(Document.id == doc_id).first()
if not doc:
raise HTTPException(status_code=404, detail="Document not found")
doc.description = description
db.commit()
db.refresh(doc)
return doc

37
app/api/settings.py Normal file
View File

@@ -0,0 +1,37 @@
"""
Public (authenticated) settings endpoints for client configuration
"""
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.database.base import get_db
from app.auth.security import get_current_user
from app.models.user import User
from app.models.lookups import SystemSetup
router = APIRouter()
@router.get("/inactivity_warning_minutes")
async def get_inactivity_warning_minutes(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Returns the inactivity warning threshold in minutes (default 240)."""
default_minutes = 240
setting = (
db.query(SystemSetup)
.filter(SystemSetup.setting_key == "inactivity_warning_minutes")
.first()
)
if not setting:
return {"minutes": default_minutes}
try:
minutes = int(setting.setting_value)
except Exception:
minutes = default_minutes
return {"minutes": minutes}

View File

@@ -30,11 +30,17 @@ class UserResponse(UserBase):
id: int id: int
is_active: bool is_active: bool
is_admin: bool is_admin: bool
theme_preference: Optional[str] = "light"
class Config: class Config:
from_attributes = True from_attributes = True
class ThemePreferenceUpdate(BaseModel):
"""Theme preference update schema"""
theme_preference: str
class Token(BaseModel): class Token(BaseModel):
"""Token response schema""" """Token response schema"""
access_token: str access_token: str

View File

@@ -34,6 +34,7 @@ app.add_middleware(
# Mount static files # Mount static files
app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/static", StaticFiles(directory="static"), name="static")
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
# Templates # Templates
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
@@ -48,6 +49,7 @@ from app.api.search import router as search_router
from app.api.admin import router as admin_router from app.api.admin import router as admin_router
from app.api.import_data import router as import_router from app.api.import_data import router as import_router
from app.api.support import router as support_router from app.api.support import router as support_router
from app.api.settings import router as settings_router
app.include_router(auth_router, prefix="/api/auth", tags=["authentication"]) app.include_router(auth_router, prefix="/api/auth", tags=["authentication"])
app.include_router(customers_router, prefix="/api/customers", tags=["customers"]) app.include_router(customers_router, prefix="/api/customers", tags=["customers"])
@@ -58,6 +60,7 @@ app.include_router(search_router, prefix="/api/search", tags=["search"])
app.include_router(admin_router, prefix="/api/admin", tags=["admin"]) app.include_router(admin_router, prefix="/api/admin", tags=["admin"])
app.include_router(import_router, prefix="/api/import", tags=["import"]) app.include_router(import_router, prefix="/api/import", tags=["import"])
app.include_router(support_router, prefix="/api/support", tags=["support"]) app.include_router(support_router, prefix="/api/support", tags=["support"])
app.include_router(settings_router, prefix="/api/settings", tags=["settings"])
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)

View File

@@ -8,7 +8,7 @@ from .files import File
from .ledger import Ledger from .ledger import Ledger
from .qdro import QDRO from .qdro import QDRO
from .audit import AuditLog, LoginAttempt from .audit import AuditLog, LoginAttempt
from .additional import Deposit, Payment, FileNote, FormVariable, ReportVariable from .additional import Deposit, Payment, FileNote, FormVariable, ReportVariable, Document
from .support import SupportTicket, TicketResponse, TicketStatus, TicketPriority, TicketCategory from .support import SupportTicket, TicketResponse, TicketStatus, TicketPriority, TicketCategory
from .pensions import ( from .pensions import (
Pension, PensionSchedule, MarriageHistory, DeathBenefit, Pension, PensionSchedule, MarriageHistory, DeathBenefit,
@@ -23,7 +23,7 @@ from .lookups import (
__all__ = [ __all__ = [
"BaseModel", "User", "Rolodex", "Phone", "File", "Ledger", "QDRO", "BaseModel", "User", "Rolodex", "Phone", "File", "Ledger", "QDRO",
"AuditLog", "LoginAttempt", "AuditLog", "LoginAttempt",
"Deposit", "Payment", "FileNote", "FormVariable", "ReportVariable", "Deposit", "Payment", "FileNote", "FormVariable", "ReportVariable", "Document",
"SupportTicket", "TicketResponse", "TicketStatus", "TicketPriority", "TicketCategory", "SupportTicket", "TicketResponse", "TicketStatus", "TicketPriority", "TicketCategory",
"Pension", "PensionSchedule", "MarriageHistory", "DeathBenefit", "Pension", "PensionSchedule", "MarriageHistory", "DeathBenefit",
"SeparationAgreement", "LifeTable", "NumberTable", "SeparationAgreement", "LifeTable", "NumberTable",

View File

@@ -1,7 +1,7 @@
""" """
Additional models for complete legacy system coverage Additional models for complete legacy system coverage
""" """
from sqlalchemy import Column, Integer, String, Text, Date, Float, ForeignKey from sqlalchemy import Column, Integer, String, Text, Date, Float, ForeignKey, func, DateTime
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.models.base import BaseModel from app.models.base import BaseModel
@@ -96,3 +96,24 @@ class ReportVariable(BaseModel):
def __repr__(self): def __repr__(self):
return f"<ReportVariable(identifier='{self.identifier}')>" return f"<ReportVariable(identifier='{self.identifier}')>"
class Document(BaseModel):
__tablename__ = "documents"
id = Column(Integer, primary_key=True, index=True)
file_no = Column(String(45), ForeignKey("files.file_no"), nullable=False, index=True)
filename = Column(String(255), nullable=False)
path = Column(String(512), nullable=False)
description = Column(Text)
type = Column(String(50))
size = Column(Integer)
uploaded_by = Column(String, ForeignKey("users.username"))
upload_date = Column(DateTime, default=func.now())
# Relationships
file = relationship("File", back_populates="documents")
user = relationship("User")
def __repr__(self):
return f"<Document(id={self.id}, filename='{self.filename}', file_no='{self.file_no}')>"

View File

@@ -65,3 +65,4 @@ class File(BaseModel):
separation_agreements = relationship("SeparationAgreement", back_populates="file", cascade="all, delete-orphan") separation_agreements = relationship("SeparationAgreement", back_populates="file", cascade="all, delete-orphan")
payments = relationship("Payment", back_populates="file", cascade="all, delete-orphan") payments = relationship("Payment", back_populates="file", cascade="all, delete-orphan")
notes = relationship("FileNote", back_populates="file", cascade="all, delete-orphan") notes = relationship("FileNote", back_populates="file", cascade="all, delete-orphan")
documents = relationship("Document", back_populates="file", cascade="all, delete-orphan")

View File

@@ -25,6 +25,9 @@ class User(BaseModel):
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
is_admin = Column(Boolean, default=False) is_admin = Column(Boolean, default=False)
# User Preferences
theme_preference = Column(String(10), default='light') # 'light', 'dark'
# Activity tracking # Activity tracking
last_login = Column(DateTime(timezone=True)) last_login = Column(DateTime(timezone=True))
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())

1517
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "delphi-database",
"version": "1.0.0",
"description": "A modern Python web application built with FastAPI to replace the legacy Pascal-based database system. This system maintains the familiar keyboard shortcuts and workflows while providing a robust, modular backend with a clean web interface.",
"main": "tailwind.config.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/HotSwapp/delphi-database.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/HotSwapp/delphi-database/issues"
},
"homepage": "https://github.com/HotSwapp/delphi-database#readme",
"devDependencies": {
"@tailwindcss/forms": "^0.5.10",
"tailwindcss": "^3.4.10"
}
}

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 // Form action functions
function newRecord() { 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) { if (newBtn) {
newBtn.click(); newBtn.click();
} else { } else {
@@ -266,7 +266,7 @@ function newRecord() {
} }
function saveRecord() { 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) { if (saveBtn) {
saveBtn.click(); saveBtn.click();
} else { } else {
@@ -290,7 +290,7 @@ function editMode() {
} }
function completeAction() { 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) { if (completeBtn) {
completeBtn.click(); completeBtn.click();
} else { } else {
@@ -313,7 +313,7 @@ function clearForm() {
} }
function deleteRecord() { 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) { if (deleteBtn) {
deleteBtn.click(); deleteBtn.click();
} else { } else {
@@ -323,17 +323,15 @@ function deleteRecord() {
function cancelAction() { function cancelAction() {
// Close modals first // Close modals first
const modal = document.querySelector('.modal.show'); // Close Tailwind-style modals
if (modal) { const openModal = document.querySelector('.fixed.inset-0:not(.hidden)');
const bsModal = bootstrap.Modal.getInstance(modal); if (openModal) {
if (bsModal) { openModal.classList.add('hidden');
bsModal.hide(); return;
return;
}
} }
// Then try cancel buttons // 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) { if (cancelBtn) {
cancelBtn.click(); cancelBtn.click();
} else { } else {
@@ -345,21 +343,24 @@ function cancelAction() {
function showHelp() { function showHelp() {
const helpModal = document.querySelector('#shortcutsModal'); const helpModal = document.querySelector('#shortcutsModal');
if (helpModal) { if (helpModal) {
const modal = new bootstrap.Modal(helpModal); helpModal.classList.remove('hidden');
modal.show();
} else { } else {
showToast('Press F1 to see keyboard shortcuts', 'info'); showToast('Press F1 to see keyboard shortcuts', 'info');
} }
} }
function showMenu() { function showMenu() {
// Toggle main navigation menu on mobile or show dropdown // Toggle Tailwind mobile menu if available
const navbarToggler = document.querySelector('.navbar-toggler'); if (typeof toggleMobileMenu === 'function') {
if (navbarToggler && !navbarToggler.classList.contains('collapsed')) { toggleMobileMenu();
navbarToggler.click(); return;
} else {
showToast('Menu (F10) - Use Alt+C, Alt+F, Alt+L, Alt+D for navigation', 'info');
} }
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() { function showMemo() {
@@ -446,38 +447,12 @@ function openRecord() {
} }
function showToast(message, type = 'info') { function showToast(message, type = 'info') {
// Create toast element if (window.alerts && typeof window.alerts.show === 'function') {
const toastHtml = ` window.alerts.show(message, type, { duration: 3000 });
<div class="toast align-items-center text-white bg-${type}" role="alert" aria-live="assertive" aria-atomic="true"> return;
<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);
} }
// Fallback
// Add toast alert(String(message));
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 // Export for use in other scripts

View File

@@ -20,17 +20,7 @@ async function initializeApp() {
window.keyboardShortcuts.initialize(); window.keyboardShortcuts.initialize();
} }
// Initialize tooltips // Remove Bootstrap-dependent tooltips/popovers; use native title/tooltips if needed
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 // Add form validation classes
initializeFormValidation(); initializeFormValidation();
@@ -44,19 +34,19 @@ async function initializeApp() {
// Form validation // Form validation
function initializeFormValidation() { function initializeFormValidation() {
// Add Bootstrap validation styles // Native validation handling without Bootstrap classes
const forms = document.querySelectorAll('form.needs-validation'); const forms = document.querySelectorAll('form');
forms.forEach(form => { forms.forEach(form => {
form.addEventListener('submit', function(event) { form.addEventListener('submit', function(event) {
if (!form.checkValidity()) { if (!form.checkValidity()) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); 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]'); const requiredFields = document.querySelectorAll('input[required], select[required], textarea[required]');
requiredFields.forEach(field => { requiredFields.forEach(field => {
field.addEventListener('blur', function() { field.addEventListener('blur', function() {
@@ -67,15 +57,8 @@ function initializeFormValidation() {
function validateField(field) { function validateField(field) {
const isValid = field.checkValidity(); const isValid = field.checkValidity();
field.classList.remove('is-valid', 'is-invalid'); field.setAttribute('aria-invalid', String(!isValid));
field.classList.add(isValid ? 'is-valid' : 'is-invalid'); field.classList.toggle('border-danger-500', !isValid);
// 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 // API helpers
@@ -157,45 +140,18 @@ function logout() {
window.location.href = '/login'; window.location.href = '/login';
} }
// Notification system // Notification system (delegates to shared alerts utility)
function showNotification(message, type = 'info', duration = 5000) { function showNotification(message, type = 'info', duration = 5000) {
const notificationContainer = getOrCreateNotificationContainer(); if (window.alerts && typeof window.alerts.show === 'function') {
return window.alerts.show(message, type, { duration });
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);
} }
// Fallback if alerts module not yet loaded
return notification; return alert(String(message));
}
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 // Loading states
function showLoading(element, text = 'Loading...') { 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; const originalContent = element.innerHTML;
element.innerHTML = `${spinner}${text}`; element.innerHTML = `${spinner}${text}`;
element.disabled = true; element.disabled = true;
@@ -271,11 +227,12 @@ function addRowSelection(table) {
tbody.addEventListener('click', function(e) { tbody.addEventListener('click', function(e) {
const row = e.target.closest('tr'); const row = e.target.closest('tr');
if (row && e.target.type !== 'checkbox') { 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 // Trigger custom event
const event = new CustomEvent('rowSelect', { const event = new CustomEvent('rowSelect', {
detail: { row, selected: row.classList.contains('table-active') } detail: { row, selected: isSelected }
}); });
table.dispatchEvent(event); table.dispatchEvent(event);
} }
@@ -342,18 +299,18 @@ function initializeSearch(searchInput, resultsContainer, searchFunction) {
function displaySearchResults(container, results) { function displaySearchResults(container, results) {
if (!results || results.length === 0) { 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; return;
} }
const resultsHtml = results.map(result => ` const resultsHtml = results.map(result => `
<div class="search-result p-2 border-bottom"> <div class="search-result p-2 border-bottom">
<div class="d-flex justify-content-between"> <div class="flex justify-between">
<div> <div>
<strong>${result.title}</strong> <strong>${result.title}</strong>
<small class="text-muted d-block">${result.description}</small> <small class="text-neutral-500 block">${result.description}</small>
</div> </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>
</div> </div>
`).join(''); `).join('');

119
tailwind.config.js Normal file
View File

@@ -0,0 +1,119 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./templates/**/*.html",
"./static/js/**/*.js",
],
darkMode: 'class',
theme: {
extend: {
colors: {
// Primary brand colors
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
950: '#172554',
},
// Neutral grays
neutral: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
950: '#020617',
},
// Semantic colors
success: {
50: '#f0fdf4',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
},
warning: {
50: '#fffbeb',
500: '#f59e0b',
600: '#d97706',
700: '#b45309',
},
danger: {
50: '#fef2f2',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
},
info: {
50: '#f0f9ff',
500: '#06b6d4',
600: '#0891b2',
700: '#0e7490',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
},
fontSize: {
'xs': ['0.75rem', { lineHeight: '1rem' }],
'sm': ['0.875rem', { lineHeight: '1.25rem' }],
'base': ['1rem', { lineHeight: '1.5rem' }],
'lg': ['1.125rem', { lineHeight: '1.75rem' }],
'xl': ['1.25rem', { lineHeight: '1.75rem' }],
'2xl': ['1.5rem', { lineHeight: '2rem' }],
'3xl': ['1.875rem', { lineHeight: '2.25rem' }],
},
spacing: {
'18': '4.5rem',
'88': '22rem',
'128': '32rem',
},
borderRadius: {
'xl': '0.75rem',
'2xl': '1rem',
'3xl': '1.5rem',
},
boxShadow: {
'soft': '0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04)',
'medium': '0 4px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
'large': '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
},
animation: {
'fade-in': 'fadeIn 0.3s ease-in-out',
'slide-in': 'slideIn 0.3s ease-in-out',
'scale-in': 'scaleIn 0.2s ease-in-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0', transform: 'translateY(8px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
slideIn: {
'0%': { opacity: '0', transform: 'translateY(20px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
scaleIn: {
'0%': { opacity: '0', transform: 'scale(0.95)' },
'100%': { opacity: '1', transform: 'scale(1)' },
},
},
backdropBlur: {
xs: '2px',
},
},
},
plugins: [
require('@tailwindcss/forms'),
],
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,135 +5,146 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ title }}{% endblock %}</title> <title>{% block title %}{{ title }}{% endblock %}</title>
<!-- Bootstrap 5.3 CDN --> <!-- Icons (Font Awesome) -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<!-- Custom CSS --> <!-- Tailwind CSS -->
<link href="/static/css/main.css" rel="stylesheet"> <!-- Custom Tailwind CSS -->
<link href="/static/css/themes.css" rel="stylesheet"> <link href="/static/css/tailwind.css" rel="stylesheet">
<link href="/static/css/components.css" rel="stylesheet">
<style> {% block bridge_css %}{% endblock %}
/* Footer Enhancements */
footer .btn-outline-primary:hover {
background-color: #0d6efd;
border-color: #0d6efd;
color: white;
transform: translateY(-1px);
transition: all 0.2s ease;
}
footer .text-primary:hover {
color: #0056b3 !important;
transition: color 0.2s ease;
}
footer small {
color: #6c757d !important;
}
#currentPageDisplay {
color: #495057 !important;
font-weight: 500;
}
/* Responsive footer adjustments */
@media (max-width: 768px) {
footer .row {
text-align: center !important;
}
footer .col-md-6:first-child {
margin-bottom: 0.5rem;
}
}
</style>
{% block extra_head %}{% endblock %} {% block extra_head %}{% endblock %}
</head> </head>
<body class="d-flex flex-column min-vh-100"> <body class="flex flex-col min-h-screen bg-neutral-50 dark:bg-neutral-900 text-neutral-900 dark:text-neutral-50 antialiased">
<!-- Navigation --> <!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <nav class="bg-primary-600 border-b border-primary-700 shadow-sm">
<div class="container"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<a class="navbar-brand" href="/"> <div class="flex justify-between items-center h-16">
Delphi Database System <!-- Brand -->
</a> <div class="flex items-center">
<a href="/" class="text-white font-semibold text-xl hover:text-primary-100 transition-colors">
Delphi Database System
</a>
</div>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"> <!-- Desktop Navigation -->
<span class="navbar-toggler-icon"></span> <div class="hidden md:flex items-center space-x-1">
</button> <a href="/customers" data-shortcut="Alt+C" class="flex items-center gap-2 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
<i class="fa-solid fa-users"></i>
<span>Customers</span>
</a>
<a href="/files" data-shortcut="Alt+F" class="flex items-center gap-2 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
<i class="fa-solid fa-folder"></i>
<span>Files</span>
</a>
<a href="/financial" data-shortcut="Alt+L" class="flex items-center gap-2 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
<i class="fa-solid fa-calculator"></i>
<span>Ledger</span>
</a>
<a href="/documents" data-shortcut="Alt+D" class="flex items-center gap-2 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
<i class="fa-solid fa-file-lines"></i>
<span>Documents</span>
</a>
<a href="/search" data-shortcut="Ctrl+F" class="flex items-center gap-2 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
<i class="fa-solid fa-magnifying-glass"></i>
<span>Search</span>
</a>
</div>
<div class="collapse navbar-collapse" id="navbarNav"> <!-- Right side items -->
<ul class="navbar-nav me-auto"> <div class="flex items-center space-x-3">
<li class="nav-item"> <!-- Theme Toggle -->
<a class="nav-link" href="/customers" data-shortcut="Alt+C"> <button onclick="toggleTheme()" title="Toggle dark mode" class="flex items-center justify-center w-10 h-10 bg-primary-700 hover:bg-primary-800 text-white rounded-lg transition-colors duration-200">
<i class="bi bi-people"></i> Customers <small>(Alt+C)</small> <i class="fas fa-sun dark:hidden text-sm"></i>
</a> <i class="fas fa-moon hidden dark:block text-sm"></i>
</li> </button>
<li class="nav-item">
<a class="nav-link" href="/files" data-shortcut="Alt+F">
<i class="bi bi-folder"></i> Files <small>(Alt+F)</small>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/financial" data-shortcut="Alt+L">
<i class="bi bi-calculator"></i> Ledger <small>(Alt+L)</small>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/documents" data-shortcut="Alt+D">
<i class="bi bi-file-text"></i> Documents <small>(Alt+D)</small>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/search" data-shortcut="Ctrl+F">
<i class="bi bi-search"></i> Search <small>(Ctrl+F)</small>
</a>
</li>
</ul>
<ul class="navbar-nav"> <!-- User Dropdown -->
<li class="nav-item dropdown"> <div class="relative" id="userDropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"> <button onclick="toggleUserMenu()" class="flex items-center gap-2 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
<i class="bi bi-person-circle"></i> User <i class="fa-solid fa-circle-user"></i>
</a> <span>User</span>
<ul class="dropdown-menu"> <i class="fa-solid fa-chevron-down text-xs"></i>
<li id="admin-menu-item" style="display: none;"><a class="dropdown-item" href="/admin" data-shortcut="Alt+A"><i class="bi bi-gear"></i> Admin <small>(Alt+A)</small></a></li> </button>
<li id="admin-menu-divider" style="display: none;"><hr class="dropdown-divider"></li> <div id="userMenu" class="hidden absolute right-0 mt-2 w-48 bg-white dark:bg-neutral-800 rounded-lg shadow-lg border border-neutral-200 dark:border-neutral-700 z-50">
<li><a class="dropdown-item" href="#" onclick="logout()"><i class="bi bi-box-arrow-right"></i> Logout</a></li> <div class="py-1">
</ul> <a id="admin-menu-item" href="/admin" class="hidden flex items-center gap-2 px-4 py-2 text-sm text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700 transition-colors">
</li> <i class="fa-solid fa-gear"></i>
</ul> <span>Admin</span>
</a>
<div id="admin-menu-divider" class="hidden border-t border-neutral-200 dark:border-neutral-700 my-1"></div>
<a href="#" onclick="logout()" class="flex items-center gap-2 px-4 py-2 text-sm text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700 transition-colors">
<i class="fa-solid fa-right-from-bracket"></i>
<span>Logout</span>
</a>
</div>
</div>
</div>
<!-- Mobile menu button -->
<button onclick="toggleMobileMenu()" class="md:hidden flex items-center justify-center w-10 h-10 bg-primary-700 hover:bg-primary-800 text-white rounded-lg transition-colors">
<i id="mobileMenuIcon" class="fa-solid fa-bars"></i>
</button>
</div>
</div>
<!-- Mobile Navigation -->
<div id="mobileMenu" class="hidden md:hidden border-t border-primary-700 pt-4 pb-4">
<div class="space-y-1">
<a href="/customers" class="flex items-center gap-3 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
<i class="fa-solid fa-users"></i>
<span>Customers</span>
</a>
<a href="/files" class="flex items-center gap-3 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
<i class="fa-solid fa-folder"></i>
<span>Files</span>
</a>
<a href="/financial" class="flex items-center gap-3 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
<i class="fa-solid fa-calculator"></i>
<span>Ledger</span>
</a>
<a href="/documents" class="flex items-center gap-3 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
<i class="fa-solid fa-file-lines"></i>
<span>Documents</span>
</a>
<a href="/search" class="flex items-center gap-3 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
<i class="fa-solid fa-magnifying-glass"></i>
<span>Search</span>
</a>
</div>
</div> </div>
</div> </div>
</nav> </nav>
<!-- Main Content --> <!-- Main Content -->
<main class="flex-grow-1"> <main class="flex-grow">
<div class="container-fluid mt-3 mb-4"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
</main> </main>
<!-- Footer --> <!-- Footer -->
<footer class="mt-auto py-3 border-top shadow-sm" style="background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border-color: #dee2e6 !important;"> <footer class="mt-auto border-t border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800">
<div class="container"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div class="row align-items-center"> <div class="flex flex-col sm:flex-row items-center justify-between gap-4">
<div class="col-md-6"> <div class="text-sm text-neutral-500 dark:text-neutral-400">
<small class="text-muted"> &copy; <span id="currentYear"></span> Delphi Consulting Group Database System
&copy; <span id="currentYear"></span> Delphi Consulting Group Database System <span class="mx-2">|</span>
<span class="mx-2">|</span> <span id="currentPageDisplay">Loading...</span>
<span id="currentPageDisplay">Loading...</span>
</small>
</div> </div>
<div class="col-md-6 text-end"> <div class="flex items-center gap-4">
<button type="button" class="btn btn-outline-primary btn-sm me-3" onclick="openSupportModal()"> <button type="button" onclick="openSupportModal()" class="bg-primary-600 text-white hover:bg-primary-700 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors duration-200 flex items-center gap-2">
<i class="fas fa-bug me-1"></i>Report Issue <i class="fas fa-bug"></i>
<span>Report Issue</span>
</button> </button>
<small class="text-muted"> <div class="text-sm text-neutral-500 dark:text-neutral-400">
Found a bug? <a href="#" onclick="openSupportModal()" class="text-primary text-decoration-none">Report Issue</a> Found a bug?
</small> <button onclick="openSupportModal()" class="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 transition-colors duration-200 underline">
Report Issue
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -143,71 +154,220 @@
{% include 'support_modal.html' %} {% include 'support_modal.html' %}
<!-- Keyboard Shortcuts Help Modal --> <!-- Keyboard Shortcuts Help Modal -->
<div class="modal fade" id="shortcutsModal" tabindex="-1" aria-labelledby="shortcutsModalLabel" aria-hidden="true"> <div id="shortcutsModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div class="modal-dialog modal-lg"> <div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-4xl w-full max-h-screen overflow-hidden">
<div class="modal-content"> <div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<div class="modal-header"> <h2 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<h5 class="modal-title" id="shortcutsModalLabel"> <i class="fa-solid fa-keyboard"></i>
<i class="bi bi-keyboard"></i> Keyboard Shortcuts <span>Keyboard Shortcuts</span>
</h5> </h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button onclick="closeShortcutsModal()" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300 transition-colors">
</div> <i class="fa-solid fa-xmark text-xl"></i>
<div class="modal-body"> </button>
<div class="row"> </div>
<div class="col-md-6"> <div class="px-6 py-4 max-h-96 overflow-y-auto scrollbar-thin">
<h6><i class="bi bi-house"></i> Navigation</h6> <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<ul class="list-unstyled"> <div class="space-y-6">
<li><kbd>Alt+C</kbd> - Customers/Rolodex</li> <div>
<li><kbd>Alt+F</kbd> - File Cabinet</li> <h3 class="flex items-center gap-2 text-sm font-semibold text-neutral-700 dark:text-neutral-300 mb-3">
<li><kbd>Alt+L</kbd> - Ledger/Financial</li> <i class="fa-solid fa-house"></i>
<li><kbd>Alt+D</kbd> - Documents/QDROs</li> <span>Navigation</span>
<li><kbd>Alt+A</kbd> - Admin Panel</li> </h3>
<li><kbd>Ctrl+F</kbd> - Global Search</li> <ul class="space-y-2 text-sm">
</ul> <li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">Customers/Rolodex</span>
<h6><i class="bi bi-pencil"></i> Forms</h6> <kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Alt+C</kbd>
<ul class="list-unstyled"> </li>
<li><kbd>Ctrl+N</kbd> - New Record</li> <li class="flex items-center justify-between">
<li><kbd>Ctrl+S</kbd> - Save</li> <span class="text-neutral-600 dark:text-neutral-400">File Cabinet</span>
<li><kbd>F9</kbd> - Edit Mode</li> <kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Alt+F</kbd>
<li><kbd>F2</kbd> - Complete/Save</li> </li>
<li><kbd>F8</kbd> - Clear/Cancel</li> <li class="flex items-center justify-between">
<li><kbd>Del</kbd> - Delete Record</li> <span class="text-neutral-600 dark:text-neutral-400">Ledger/Financial</span>
<li><kbd>Esc</kbd> - Cancel/Close</li> <kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Alt+L</kbd>
</li>
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">Documents/QDROs</span>
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Alt+D</kbd>
</li>
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">Admin Panel</span>
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Alt+A</kbd>
</li>
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">Global Search</span>
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Ctrl+F</kbd>
</li>
</ul> </ul>
</div> </div>
<div class="col-md-6">
<h6><i class="bi bi-list"></i> Lists/Tables</h6>
<ul class="list-unstyled">
<li><kbd>↑/↓</kbd> - Navigate records</li>
<li><kbd>Page Up/Down</kbd> - Page navigation</li>
<li><kbd>Home/End</kbd> - First/Last record</li>
<li><kbd>Enter</kbd> - Open/Edit record</li>
<li><kbd>+/-</kbd> - Change dates</li>
</ul>
<h6><i class="bi bi-tools"></i> System</h6> <div>
<ul class="list-unstyled"> <h3 class="flex items-center gap-2 text-sm font-semibold text-neutral-700 dark:text-neutral-300 mb-3">
<li><kbd>F1</kbd> - Help (this dialog)</li> <i class="fa-solid fa-pencil"></i>
<li><kbd>F10</kbd> - Menu</li> <span>Forms</span>
<li><kbd>Alt+M</kbd> - Memo/Notes</li> </h3>
<li><kbd>Alt+T</kbd> - Time Tracker</li> <ul class="space-y-2 text-sm">
<li><kbd>Alt+B</kbd> - Balance Summary</li> <li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">New Record</span>
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Ctrl+N</kbd>
</li>
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">Save</span>
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Ctrl+S</kbd>
</li>
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">Edit Mode</span>
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">F9</kbd>
</li>
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">Complete/Save</span>
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">F2</kbd>
</li>
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">Clear/Cancel</span>
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">F8</kbd>
</li>
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">Delete Record</span>
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Del</kbd>
</li>
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">Cancel/Close</span>
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Esc</kbd>
</li>
</ul>
</div>
</div>
<div class="space-y-6">
<div>
<h3 class="flex items-center gap-2 text-sm font-semibold text-neutral-700 dark:text-neutral-300 mb-3">
<i class="fa-solid fa-list"></i>
<span>Lists/Tables</span>
</h3>
<ul class="space-y-2 text-sm">
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">Navigate records</span>
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">↑/↓</kbd>
</li>
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">Page navigation</span>
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Page Up/Down</kbd>
</li>
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">First/Last record</span>
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Home/End</kbd>
</li>
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">Open/Edit record</span>
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Enter</kbd>
</li>
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">Change dates</span>
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">+/-</kbd>
</li>
</ul>
</div>
<div>
<h3 class="flex items-center gap-2 text-sm font-semibold text-neutral-700 dark:text-neutral-300 mb-3">
<i class="fa-solid fa-screwdriver-wrench"></i>
<span>System</span>
</h3>
<ul class="space-y-2 text-sm">
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">Help (this dialog)</span>
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">F1</kbd>
</li>
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">Menu</span>
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">F10</kbd>
</li>
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">Memo/Notes</span>
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Alt+M</kbd>
</li>
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">Time Tracker</span>
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Alt+T</kbd>
</li>
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">Balance Summary</span>
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Alt+B</kbd>
</li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
<div class="modal-footer"> </div>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800/50">
</div> <button onclick="closeShortcutsModal()" class="px-4 py-2 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">
Close
</button>
</div> </div>
</div> </div>
</div> </div>
<!-- Bootstrap JS --> <!-- Custom Navigation JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script>
function toggleUserMenu() {
const menu = document.getElementById('userMenu');
menu.classList.toggle('hidden');
}
function toggleMobileMenu() {
const menu = document.getElementById('mobileMenu');
const icon = document.getElementById('mobileMenuIcon');
menu.classList.toggle('hidden');
icon.classList.toggle('fa-bars');
icon.classList.toggle('fa-xmark');
}
function openShortcutsModal() {
document.getElementById('shortcutsModal').classList.remove('hidden');
}
function closeShortcutsModal() {
document.getElementById('shortcutsModal').classList.add('hidden');
}
// Close menus when clicking outside
document.addEventListener('click', function(event) {
const dropdown = document.getElementById('userDropdown');
const menu = document.getElementById('userMenu');
const shortcutsModal = document.getElementById('shortcutsModal');
if (!dropdown.contains(event.target)) {
menu.classList.add('hidden');
}
// Close shortcuts modal when clicking outside
if (event.target === shortcutsModal) {
closeShortcutsModal();
}
});
// Handle escape key for modal
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
closeShortcutsModal();
}
});
</script>
<script>
// Global modal helpers for Tailwind-based modals
function openModal(id) {
const el = document.getElementById(id);
if (el) el.classList.remove('hidden');
}
function closeModal(id) {
const el = document.getElementById(id);
if (el) el.classList.add('hidden');
}
</script>
<!-- Custom JavaScript --> <!-- Custom JavaScript -->
<script src="/static/js/alerts.js"></script>
<script src="/static/js/keyboard-shortcuts.js"></script> <script src="/static/js/keyboard-shortcuts.js"></script>
<script src="/static/js/main.js"></script> <script src="/static/js/main.js"></script>
@@ -272,14 +432,14 @@
const user = await response.json(); const user = await response.json();
if (user.is_admin) { if (user.is_admin) {
// Show admin menu items // Show admin menu items
document.getElementById('admin-menu-item').style.display = 'block'; document.getElementById('admin-menu-item').classList.remove('hidden');
document.getElementById('admin-menu-divider').style.display = 'block'; document.getElementById('admin-menu-divider').classList.remove('hidden');
} }
// Update user display name if available // Update user display name if available
const userDropdown = document.querySelector('.nav-link.dropdown-toggle'); const userDropdown = document.querySelector('#userDropdown button span');
if (user.full_name && userDropdown) { if (user.full_name && userDropdown) {
userDropdown.innerHTML = `<i class="bi bi-person-circle"></i> ${user.full_name}`; userDropdown.textContent = user.full_name;
} }
} }
} catch (error) { } catch (error) {
@@ -368,28 +528,136 @@
} }
} }
function setupActivityMonitoring() { async function setupActivityMonitoring() {
let lastActivity = Date.now(); let lastActivity = Date.now();
let warningShown = false;
let inactivityWarningMinutes = 240; // default 4 hours
const inactivityGraceMinutes = 5; // auto-logout after warning + 5 minutes
let inactivityAlertEl = null;
try {
const minutes = await getInactivityWarningMinutes();
if (Number.isFinite(minutes) && minutes > 0) {
inactivityWarningMinutes = minutes;
}
} catch (e) {
console.debug('Using default inactivity warning minutes');
}
// Track user activity // Track user activity
const activityEvents = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart']; const activityEvents = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'];
activityEvents.forEach(event => { activityEvents.forEach(event => {
document.addEventListener(event, () => { document.addEventListener(event, () => {
lastActivity = Date.now(); lastActivity = Date.now();
warningShown = false; // Reset warning flag on activity
hideInactivityWarning();
}); });
}); });
// Check every 30 minutes if user has been inactive for more than 4 hours function showInactivityWarning() {
hideInactivityWarning();
const msg = `You've been inactive. Your session may expire due to inactivity.`;
if (window.alerts && typeof window.alerts.show === 'function') {
inactivityAlertEl = window.alerts.show(msg, 'warning', {
title: 'Session Warning',
html: false,
duration: 0,
dismissible: true,
id: 'inactivity-warning',
actions: [
{
label: 'Stay Logged In',
classes: 'bg-warning-600 hover:bg-warning-700 text-white text-xs px-3 py-1 rounded',
onClick: () => extendSession(),
autoClose: true
},
{
label: 'Dismiss',
classes: 'bg-neutral-200 hover:bg-neutral-300 text-neutral-800 dark:bg-neutral-700 dark:hover:bg-neutral-600 dark:text-neutral-200 text-xs px-3 py-1 rounded',
onClick: () => hideInactivityWarning(),
autoClose: true
}
]
});
} else {
// Fallback
alert('Session Warning: ' + msg);
}
// Auto-hide after 2 minutes if no action taken
setTimeout(() => {
hideInactivityWarning();
}, 2 * 60 * 1000);
}
function hideInactivityWarning() {
const el = document.getElementById('inactivity-warning');
if (el && el.remove) el.remove();
inactivityAlertEl = null;
}
function extendSession() {
refreshTokenIfNeeded();
hideInactivityWarning();
showSessionExtendedNotification();
}
// Check every 5 minutes for inactivity
setInterval(() => { setInterval(() => {
const now = Date.now(); const now = Date.now();
const fourHours = 4 * 60 * 60 * 1000; const warningMs = inactivityWarningMinutes * 60 * 1000;
const logoutMs = (inactivityWarningMinutes + inactivityGraceMinutes) * 60 * 1000;
const timeSinceActivity = now - lastActivity;
if (now - lastActivity > fourHours) { if (timeSinceActivity > warningMs && !warningShown) {
// User has been inactive for 4+ hours, logout showInactivityWarning();
warningShown = true;
}
if (timeSinceActivity > logoutMs) {
logout('Session expired due to inactivity'); logout('Session expired due to inactivity');
} }
}, 30 * 60 * 1000); // Check every 30 minutes }, 5 * 60 * 1000); // Check every 5 minutes
}
async function getInactivityWarningMinutes() {
const token = localStorage.getItem('auth_token');
if (!token) return 240;
const resp = await fetch('/api/settings/inactivity_warning_minutes', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!resp.ok) return 240;
const data = await resp.json();
if (typeof data.minutes === 'number') return data.minutes;
const parsed = parseInt(data.setting_value || data.minutes, 10);
return Number.isFinite(parsed) ? parsed : 240;
}
function showSessionExtendedNotification() {
if (window.alerts && typeof window.alerts.success === 'function') {
window.alerts.success('Your session has been refreshed successfully.', {
title: 'Session Extended',
duration: 3000
});
return;
}
// Fallback
const notification = document.createElement('div');
notification.className = 'fixed top-4 right-4 bg-green-100 border-l-4 border-green-500 text-green-700 p-4 rounded-lg shadow-lg z-50 max-w-sm';
notification.innerHTML = `
<div class="flex items-center">
<div class="flex-shrink-0">
<i class="fas fa-check-circle text-green-500"></i>
</div>
<div class="ml-3">
<p class="text-sm font-medium">Session Extended</p>
<p class="text-xs mt-1">Your session has been refreshed successfully.</p>
</div>
</div>
`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
} }
// Enhanced logout function // Enhanced logout function
@@ -402,12 +670,101 @@
window.location.href = '/login'; window.location.href = '/login';
} }
// Theme Management
function toggleTheme() {
const html = document.documentElement;
const isDark = html.classList.contains('dark');
if (isDark) {
html.classList.remove('dark');
localStorage.setItem('theme-preference', 'light');
saveThemePreference('light');
} else {
html.classList.add('dark');
localStorage.setItem('theme-preference', 'dark');
saveThemePreference('dark');
}
}
function initializeTheme() {
// Check for saved theme preference
const savedTheme = localStorage.getItem('theme-preference');
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
// Use saved theme, or default to system preference
const theme = savedTheme || (prefersDark ? 'dark' : 'light');
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
// Load user's theme preference from server if authenticated
loadUserThemePreference();
}
async function saveThemePreference(theme) {
const token = localStorage.getItem('auth_token');
if (!token || isLoginPage()) return;
try {
await fetch('/api/auth/theme-preference', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ theme_preference: theme })
});
} catch (error) {
console.log('Could not save theme preference to server:', error.message);
}
}
async function loadUserThemePreference() {
const token = localStorage.getItem('auth_token');
if (!token || isLoginPage()) return;
try {
const response = await fetch('/api/auth/me', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const user = await response.json();
if (user.theme_preference) {
if (user.theme_preference === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
localStorage.setItem('theme-preference', user.theme_preference);
}
}
} catch (error) {
console.log('Could not load theme preference from server:', error.message);
}
}
// Initialize theme before other scripts
initializeTheme();
// Make functions globally available // Make functions globally available
window.authManager = { window.authManager = {
checkTokenValidity, checkTokenValidity,
refreshTokenIfNeeded, refreshTokenIfNeeded,
logout logout
}; };
window.themeManager = {
toggleTheme,
initializeTheme,
saveThemePreference,
loadUserThemePreference
};
</script> </script>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,174 +1,184 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Dashboard - {{ super() }}{% endblock %} {% block title %}Dashboard - Delphi Database{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="space-y-6">
<div class="col-12"> <!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<h1><i class="bi bi-speedometer2"></i> Dashboard</h1> <div class="flex items-center gap-3">
<div> <div class="flex items-center justify-center w-10 h-10 bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 rounded-xl">
<button class="btn btn-outline-secondary btn-sm" onclick="showShortcuts()"> <i class="fa-solid fa-gauge-high text-lg"></i>
<i class="bi bi-keyboard"></i> Shortcuts (F1)
</button>
</div> </div>
<h1 class="text-2xl font-bold text-neutral-900 dark:text-neutral-100">Dashboard</h1>
</div>
<div class="flex items-center gap-3">
<button onclick="openShortcutsModal()" class="bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors duration-200 flex items-center gap-2">
<i class="fa-solid fa-keyboard"></i>
<span>Shortcuts (F1)</span>
</button>
</div> </div>
</div> </div>
</div>
<!-- Quick Stats Cards --> <!-- Quick Stats Cards -->
<div class="row mb-4"> <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="col-md-3"> <div class="bg-primary-600 text-white rounded-xl shadow-soft p-4">
<div class="card bg-primary text-white"> <div class="flex items-center gap-3">
<div class="card-body"> <div class="flex items-center justify-center w-10 h-10 bg-primary-700 rounded-lg">
<div class="d-flex align-items-center"> <i class="fa-solid fa-users text-xl"></i>
<div class="me-3"> </div>
<i class="bi bi-people fs-1"></i> <div>
<h5 class="text-sm font-medium mb-1">Customers</h5>
<h2 class="text-2xl font-bold mb-0" id="customer-count">-</h2>
</div>
</div>
<a href="/customers" class="text-primary-200 hover:text-white text-xs font-medium flex items-center gap-1 mt-2 transition-colors">
View all
<i class="fa-solid fa-arrow-right"></i>
</a>
</div>
<div class="bg-success-600 text-white rounded-xl shadow-soft p-4">
<div class="flex items-center gap-3">
<div class="flex items-center justify-center w-10 h-10 bg-success-700 rounded-lg">
<i class="fa-solid fa-folder text-xl"></i>
</div>
<div>
<h5 class="text-sm font-medium mb-1">Active Files</h5>
<h2 class="text-2xl font-bold mb-0" id="file-count">-</h2>
</div>
</div>
<a href="/files" class="text-success-200 hover:text-white text-xs font-medium flex items-center gap-1 mt-2 transition-colors">
View all
<i class="fa-solid fa-arrow-right"></i>
</a>
</div>
<div class="bg-info-600 text-white rounded-xl shadow-soft p-4">
<div class="flex items-center gap-3">
<div class="flex items-center justify-center w-10 h-10 bg-info-700 rounded-lg">
<i class="fa-solid fa-receipt text-xl"></i>
</div>
<div>
<h5 class="text-sm font-medium mb-1">Transactions</h5>
<h2 class="text-2xl font-bold mb-0" id="transaction-count">-</h2>
</div>
</div>
<a href="/financial" class="text-info-200 hover:text-white text-xs font-medium flex items-center gap-1 mt-2 transition-colors">
View ledger
<i class="fa-solid fa-arrow-right"></i>
</a>
</div>
<div class="bg-warning-600 text-white rounded-xl shadow-soft p-4">
<div class="flex items-center gap-3">
<div class="flex items-center justify-center w-10 h-10 bg-warning-700 rounded-lg">
<i class="fa-regular fa-file-lines text-xl"></i>
</div>
<div>
<h5 class="text-sm font-medium mb-1">Documents</h5>
<h2 class="text-2xl font-bold mb-0" id="document-count">-</h2>
</div>
</div>
<a href="/documents" class="text-warning-200 hover:text-white text-xs font-medium flex items-center gap-1 mt-2 transition-colors">
View all
<i class="fa-solid fa-arrow-right"></i>
</a>
</div>
</div>
<!-- Quick Actions and Recent Activity -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="md:col-span-2 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft">
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<i class="fa-solid fa-bolt"></i>
<span>Quick Actions</span>
</h5>
</div>
<div class="p-6">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-3">
<button onclick="newCustomer()" class="w-full flex flex-col items-center justify-center p-4 bg-neutral-50 dark:bg-neutral-900/50 hover:bg-neutral-100 dark:hover:bg-neutral-900 rounded-lg border border-neutral-200 dark:border-neutral-700 transition-colors duration-200">
<i class="fa-solid fa-user-plus text-2xl text-primary-600 mb-1"></i>
<span class="font-medium">New Customer</span>
<kbd class="text-xs text-neutral-500 dark:text-neutral-400 mt-1">Ctrl+Shift+C</kbd>
</button>
<button onclick="newFile()" class="w-full flex flex-col items-center justify-center p-4 bg-neutral-50 dark:bg-neutral-900/50 hover:bg-neutral-100 dark:hover:bg-neutral-900 rounded-lg border border-neutral-200 dark:border-neutral-700 transition-colors duration-200">
<i class="fa-solid fa-folder-plus text-2xl text-success-600 mb-1"></i>
<span class="font-medium">New File</span>
<kbd class="text-xs text-neutral-500 dark:text-neutral-400 mt-1">Ctrl+Shift+F</kbd>
</button>
</div> </div>
<div> <div class="space-y-3">
<h5 class="card-title">Customers</h5> <button onclick="newTransaction()" class="w-full flex flex-col items-center justify-center p-4 bg-neutral-50 dark:bg-neutral-900/50 hover:bg-neutral-100 dark:hover:bg-neutral-900 rounded-lg border border-neutral-200 dark:border-neutral-700 transition-colors duration-200">
<h2 class="mb-0" id="customer-count">-</h2> <i class="fa-solid fa-circle-plus text-2xl text-info-600 mb-1"></i>
<span class="font-medium">New Transaction</span>
<kbd class="text-xs text-neutral-500 dark:text-neutral-400 mt-1">Ctrl+Shift+T</kbd>
</button>
<button onclick="globalSearch()" class="w-full flex flex-col items-center justify-center p-4 bg-neutral-50 dark:bg-neutral-900/50 hover:bg-neutral-100 dark:hover:bg-neutral-900 rounded-lg border border-neutral-200 dark:border-neutral-700 transition-colors duration-200">
<i class="fa-solid fa-magnifying-glass text-2xl text-warning-600 mb-1"></i>
<span class="font-medium">Global Search</span>
<kbd class="text-xs text-neutral-500 dark:text-neutral-400 mt-1">Ctrl+F</kbd>
</button>
</div> </div>
</div> </div>
<a href="/customers" class="text-white-50 small">
View all <i class="bi bi-arrow-right"></i>
</a>
</div> </div>
</div> </div>
</div>
<div class="col-md-3"> <div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft">
<div class="card bg-success text-white"> <div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<div class="card-body"> <h5 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<div class="d-flex align-items-center"> <i class="fa-solid fa-clock-rotate-left"></i>
<div class="me-3"> <span>Recent Activity</span>
<i class="bi bi-folder fs-1"></i> </h5>
</div>
<div>
<h5 class="card-title">Active Files</h5>
<h2 class="mb-0" id="file-count">-</h2>
</div>
</div>
<a href="/files" class="text-white-50 small">
View all <i class="bi bi-arrow-right"></i>
</a>
</div> </div>
</div> <div class="p-6" id="recent-activity">
</div> <div class="flex flex-col items-center justify-center py-4 text-neutral-500 dark:text-neutral-400">
<i class="fa-solid fa-hourglass-half text-2xl mb-2"></i>
<div class="col-md-3"> <p>Loading recent activity...</p>
<div class="card bg-info text-white">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="me-3">
<i class="bi bi-receipt fs-1"></i>
</div>
<div>
<h5 class="card-title">Transactions</h5>
<h2 class="mb-0" id="transaction-count">-</h2>
</div>
</div>
<a href="/financial" class="text-white-50 small">
View ledger <i class="bi bi-arrow-right"></i>
</a>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-dark">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="me-3">
<i class="bi bi-file-text fs-1"></i>
</div>
<div>
<h5 class="card-title">Documents</h5>
<h2 class="mb-0" id="document-count">-</h2>
</div>
</div>
<a href="/documents" class="text-dark-50 small">
View all <i class="bi bi-arrow-right"></i>
</a>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="row mb-4">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-lightning"></i> Quick Actions</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<div class="d-grid gap-2">
<button class="btn btn-outline-primary btn-lg" onclick="newCustomer()">
<i class="bi bi-person-plus"></i> New Customer
<small class="d-block text-muted">Ctrl+Shift+C</small>
</button>
<button class="btn btn-outline-success btn-lg" onclick="newFile()">
<i class="bi bi-folder-plus"></i> New File
<small class="d-block text-muted">Ctrl+Shift+F</small>
</button>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="d-grid gap-2">
<button class="btn btn-outline-info btn-lg" onclick="newTransaction()">
<i class="bi bi-plus-circle"></i> New Transaction
<small class="d-block text-muted">Ctrl+Shift+T</small>
</button>
<button class="btn btn-outline-warning btn-lg" onclick="globalSearch()">
<i class="bi bi-search"></i> Global Search
<small class="d-block text-muted">Ctrl+F</small>
</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-4"> <!-- System Status -->
<div class="card"> <div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft">
<div class="card-header"> <div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5><i class="bi bi-clock-history"></i> Recent Activity</h5> <h5 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
</div> <i class="fa-solid fa-circle-info"></i>
<div class="card-body"> <span>System Information</span>
<div id="recent-activity"> </h5>
<p class="text-muted text-center"> </div>
<i class="bi bi-hourglass-split"></i><br> <div class="p-6">
Loading recent activity... <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-3 text-sm">
<p class="flex justify-between">
<strong>System:</strong>
<span>Delphi Consulting Group Database System</span>
</p>
<p class="flex justify-between">
<strong>Version:</strong>
<span>1.0.0</span>
</p>
<p class="flex justify-between">
<strong>Database:</strong>
<span>SQLite</span>
</p> </p>
</div> </div>
</div> <div class="space-y-3 text-sm">
</div> <p class="flex justify-between">
</div> <strong>Last Backup:</strong>
</div> <span id="last-backup">Not available</span>
</p>
<!-- System Status --> <p class="flex justify-between">
<div class="row"> <strong>Database Size:</strong>
<div class="col-12"> <span id="db-size">-</span>
<div class="card"> </p>
<div class="card-header"> <p class="flex justify-between">
<h5><i class="bi bi-info-circle"></i> System Information</h5> <strong>Status:</strong>
</div> <span id="system-status" class="px-2 py-1 bg-success-600 text-white text-xs font-medium rounded-full">Healthy</span>
<div class="card-body"> </p>
<div class="row">
<div class="col-md-6">
<p><strong>System:</strong> Delphi Consulting Group Database System</p>
<p><strong>Version:</strong> 1.0.0</p>
<p><strong>Database:</strong> SQLite</p>
</div>
<div class="col-md-6">
<p><strong>Last Backup:</strong> <span id="last-backup">Not available</span></p>
<p><strong>Database Size:</strong> <span id="db-size">-</span></p>
<p><strong>Status:</strong> <span id="system-status" class="badge bg-success">Healthy</span></p>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -178,55 +188,49 @@
{% block extra_scripts %} {% block extra_scripts %}
<script> <script>
// Load dashboard data // Load dashboard data
async function loadDashboardData() { async function loadDashboardData() {
try { try {
// This would typically be authenticated API calls const response = await fetch('/api/admin/stats', {
const response = await fetch('/api/admin/stats', { headers: {
headers: { 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
});
if (response.ok) {
const data = await response.json();
document.getElementById('customer-count').textContent = data.total_customers || '0';
document.getElementById('file-count').textContent = data.total_files || '0';
document.getElementById('transaction-count').textContent = data.total_transactions || '0';
document.getElementById('document-count').textContent = data.total_qdros || '0';
document.getElementById('db-size').textContent = data.database_size || '-';
document.getElementById('last-backup').textContent = data.last_backup || 'Not available';
} }
} catch (error) { });
console.error('Error loading dashboard data:', error);
if (response.ok) {
const data = await response.json();
document.getElementById('customer-count').textContent = data.total_customers || '0';
document.getElementById('file-count').textContent = data.total_files || '0';
document.getElementById('transaction-count').textContent = data.total_transactions || '0';
document.getElementById('document-count').textContent = data.total_qdros || '0';
document.getElementById('db-size').textContent = data.database_size || '-';
document.getElementById('last-backup').textContent = data.last_backup || 'Not available';
} }
} catch (error) {
console.error('Error loading dashboard data:', error);
} }
}
// Quick action functions // Quick action functions
function newCustomer() { function newCustomer() {
window.location.href = '/customers/new'; window.location.href = '/customers';
} }
function newFile() { function newFile() {
window.location.href = '/files/new'; window.location.href = '/files';
} }
function newTransaction() { function newTransaction() {
window.location.href = '/financial/new'; window.location.href = '/financial';
} }
function globalSearch() { function globalSearch() {
window.location.href = '/search'; window.location.href = '/search';
} }
function showShortcuts() { // Load data on page load
const modal = new bootstrap.Modal(document.getElementById('shortcutsModal')); document.addEventListener('DOMContentLoaded', function() {
modal.show(); loadDashboardData(); // Uncomment when authentication is implemented
} });
// Load data on page load
document.addEventListener('DOMContentLoaded', function() {
// loadDashboardData(); // Uncomment when authentication is implemented
});
</script> </script>
{% endblock %} {% endblock %}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,146 +3,164 @@
{% block title %}Data Import - Delphi Database{% endblock %} {% block title %}Data Import - Delphi Database{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="space-y-6">
<div class="row"> <!-- Page Header -->
<div class="col-12"> <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="flex items-center gap-3">
<h2><i class="bi bi-upload"></i> Data Import</h2> <div class="flex items-center justify-center w-10 h-10 bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 rounded-xl">
<i class="fa-solid fa-upload text-lg"></i>
</div>
<h1 class="text-2xl font-bold text-neutral-900 dark:text-neutral-100">Data Import</h1>
</div>
<div class="flex items-center gap-3">
<button id="refreshStatusBtn" class="bg-info-600 text-white hover:bg-info-700 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors duration-200 flex items-center gap-2">
<i class="fa-solid fa-rotate-right"></i>
<span>Refresh Status</span>
</button>
</div>
</div>
<!-- Import Status Panel -->
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft">
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<i class="fa-solid fa-circle-info"></i>
<span>Current Database Status</span>
</h5>
</div>
<div class="p-6">
<div id="importStatus">
<div class="flex items-center justify-center py-4 text-neutral-500 dark:text-neutral-400">
<i class="fa-solid fa-rotate-right animate-spin text-xl mr-2"></i>
<p>Loading import status...</p>
</div>
</div>
</div>
</div>
<!-- CSV File Upload Panel -->
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft">
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<i class="fa-regular fa-file-arrow-up"></i>
<span>Upload CSV Files</span>
</h5>
</div>
<div class="p-6">
<form id="importForm" enctype="multipart/form-data">
<div class="grid grid-cols-1 md:grid-cols-12 gap-4">
<div class="md:col-span-4">
<label for="fileType" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Data Type *</label>
<select class="w-full px-3 py-3 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" id="fileType" name="fileType" required>
<option value="">Select data type...</option>
</select>
<div class="text-sm text-neutral-500 dark:text-neutral-400 mt-1" id="fileTypeDescription"></div>
</div>
<div class="md:col-span-6">
<label for="csvFile" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">CSV File *</label>
<input type="file" class="w-full px-3 py-3 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-primary-100 file:text-primary-700 hover:file:bg-primary-200 transition-all duration-200" id="csvFile" name="csvFile" accept=".csv" required>
<div class="text-sm text-neutral-500 dark:text-neutral-400 mt-1">Select the CSV file to import</div>
</div>
<div class="md:col-span-2 flex items-end">
<label class="inline-flex items-center gap-2">
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded" type="checkbox" id="replaceExisting" name="replaceExisting">
<span>Replace existing data</span>
</label>
</div>
</div>
<div class="mt-4">
<div class="flex items-center gap-3">
<button type="button" class="px-4 py-2 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 flex items-center gap-2" id="validateBtn">
<i class="fa-regular fa-circle-check"></i>
<span>Validate File</span>
</button>
<button type="submit" class="px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg transition-colors duration-200 flex items-center gap-2" id="importBtn">
<i class="fa-solid fa-upload"></i>
<span>Import Data</span>
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Validation Results Panel -->
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft hidden" id="validationPanel">
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<i class="fa-solid fa-clipboard-check"></i>
<span>File Validation Results</span>
</h5>
</div>
<div class="p-6" id="validationResults">
<!-- Validation results will be shown here -->
</div>
</div>
<!-- Import Progress Panel -->
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft hidden" id="progressPanel">
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<i class="fa-solid fa-hourglass-half"></i>
<span>Import Progress</span>
</h5>
</div>
<div class="p-6">
<div class="bg-neutral-200 dark:bg-neutral-700 rounded-full h-2 mb-3 overflow-hidden">
<div class="bg-primary-600 h-full rounded-full transition-all duration-300" style="width: 0%" id="progressBar"></div>
</div>
<div id="progressStatus" class="text-sm text-neutral-600 dark:text-neutral-400">Ready to import...</div>
</div>
</div>
<!-- Import Results Panel -->
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft hidden" id="resultsPanel">
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<i class="fa-solid fa-circle-check"></i>
<span>Import Results</span>
</h5>
</div>
<div class="p-6" id="importResults">
<!-- Import results will be shown here -->
</div>
</div>
<!-- Data Management Panel -->
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft">
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<i class="fa-solid fa-database"></i>
<span>Data Management</span>
</h5>
</div>
<div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<button class="btn btn-info" id="refreshStatusBtn"> <h6 class="text-base font-semibold mb-2">Clear Table Data</h6>
<i class="bi bi-arrow-clockwise"></i> Refresh Status <p class="text-sm text-neutral-500 dark:text-neutral-400 mb-3">Remove all records from a specific table (cannot be undone)</p>
</button> <div class="flex gap-3">
</div> <select class="flex-grow px-3 py-3 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" id="clearTableType">
</div> <option value="">Select table to clear...</option>
</select>
<!-- Import Status Panel --> <button class="px-4 py-3 bg-danger-600 text-white hover:bg-danger-700 rounded-lg transition-colors duration-200 flex items-center gap-2 whitespace-nowrap" id="clearTableBtn">
<div class="card mb-4"> <i class="fa-solid fa-trash"></i>
<div class="card-header"> <span>Clear Table</span>
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Current Database Status</h5> </button>
</div>
<div class="card-body">
<div id="importStatus">
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading import status...</p>
</div>
</div> </div>
</div> </div>
</div> <div>
<h6 class="text-base font-semibold mb-2">Quick Actions</h6>
<!-- CSV File Upload Panel --> <div class="space-y-3">
<div class="card mb-4"> <button class="w-full px-4 py-3 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 flex items-center justify-center gap-2" id="backupBtn">
<div class="card-header"> <i class="fa-solid fa-download"></i>
<h5 class="mb-0"><i class="bi bi-file-earmark-arrow-up"></i> Upload CSV Files</h5> <span>Download Backup</span>
</div> </button>
<div class="card-body"> <button class="w-full px-4 py-3 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 flex items-center justify-center gap-2" id="viewLogsBtn">
<form id="importForm" enctype="multipart/form-data"> <i class="fa-regular fa-file-lines"></i>
<div class="row g-3"> <span>View Import Logs</span>
<div class="col-md-4"> </button>
<label for="fileType" class="form-label">Data Type *</label>
<select class="form-select" id="fileType" name="fileType" required>
<option value="">Select data type...</option>
</select>
<div class="form-text" id="fileTypeDescription"></div>
</div>
<div class="col-md-6">
<label for="csvFile" class="form-label">CSV File *</label>
<input type="file" class="form-control" id="csvFile" name="csvFile" accept=".csv" required>
<div class="form-text">Select the CSV file to import</div>
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="replaceExisting" name="replaceExisting">
<label class="form-check-label" for="replaceExisting">
Replace existing data
</label>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<div class="btn-group" role="group">
<button type="button" class="btn btn-secondary" id="validateBtn">
<i class="bi bi-check-circle"></i> Validate File
</button>
<button type="submit" class="btn btn-primary" id="importBtn">
<i class="bi bi-upload"></i> Import Data
</button>
</div>
</div>
</div>
</form>
</div>
</div>
<!-- Validation Results Panel -->
<div class="card mb-4" id="validationPanel" style="display: none;">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-clipboard-check"></i> File Validation Results</h5>
</div>
<div class="card-body" id="validationResults">
<!-- Validation results will be shown here -->
</div>
</div>
<!-- Import Progress Panel -->
<div class="card mb-4" id="progressPanel" style="display: none;">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-hourglass-split"></i> Import Progress</h5>
</div>
<div class="card-body">
<div class="progress mb-3">
<div class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 0%" id="progressBar">0%</div>
</div>
<div id="progressStatus">Ready to import...</div>
</div>
</div>
<!-- Import Results Panel -->
<div class="card mb-4" id="resultsPanel" style="display: none;">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-check-circle-fill"></i> Import Results</h5>
</div>
<div class="card-body" id="importResults">
<!-- Import results will be shown here -->
</div>
</div>
<!-- Data Management Panel -->
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-database"></i> Data Management</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Clear Table Data</h6>
<p class="text-muted small">Remove all records from a specific table (cannot be undone)</p>
<div class="input-group">
<select class="form-select" id="clearTableType">
<option value="">Select table to clear...</option>
</select>
<button class="btn btn-danger" id="clearTableBtn">
<i class="bi bi-trash"></i> Clear Table
</button>
</div>
</div>
<div class="col-md-6">
<h6>Quick Actions</h6>
<div class="d-grid gap-2">
<button class="btn btn-outline-warning" id="backupBtn">
<i class="bi bi-download"></i> Download Backup
</button>
<button class="btn btn-outline-info" id="viewLogsBtn">
<i class="bi bi-journal-text"></i> View Import Logs
</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -250,50 +268,46 @@ async function loadImportStatus() {
} catch (error) { } catch (error) {
console.error('Error loading import status:', error); console.error('Error loading import status:', error);
document.getElementById('importStatus').innerHTML = document.getElementById('importStatus').innerHTML =
'<div class="alert alert-danger">Error loading import status: ' + error.message + '</div>'; '<div class="p-4 bg-danger-100 dark:bg-danger-900/30 text-danger-700 dark:text-danger-300 rounded-lg">Error loading import status: ' + error.message + '</div>';
} }
} }
function displayImportStatus(status) { function displayImportStatus(status) {
const container = document.getElementById('importStatus'); const container = document.getElementById('importStatus');
let html = '<div class="row">'; let html = '<div class="grid grid-cols-1 md:grid-cols-3 gap-4">';
let totalRecords = 0; let totalRecords = 0;
Object.entries(status).forEach(([fileType, info], index) => { Object.entries(status).forEach(([fileType, info]) => {
totalRecords += info.record_count || 0; totalRecords += info.record_count || 0;
const statusClass = info.error ? 'danger' : (info.record_count > 0 ? 'success' : 'secondary'); const statusClass = info.error ? 'danger' : (info.record_count > 0 ? 'success' : 'neutral');
const statusIcon = info.error ? 'exclamation-triangle' : (info.record_count > 0 ? 'check-circle' : 'circle'); const statusBg = info.error ? 'bg-danger-100 dark:bg-danger-900/30 border-danger-200 dark:border-danger-800' :
(info.record_count > 0 ? 'bg-success-100 dark:bg-success-900/30 border-success-200 dark:border-success-800' :
if (index % 3 === 0 && index > 0) { 'bg-neutral-100 dark:bg-neutral-900/30 border-neutral-200 dark:border-neutral-800');
html += '</div><div class="row mt-2">'; const statusIcon = info.error ? 'triangle-exclamation text-danger-600' :
} (info.record_count > 0 ? 'circle-check text-success-600' : 'circle text-neutral-600');
html += ` html += `
<div class="col-md-4"> <div class="bg-white dark:bg-neutral-800 border ${statusBg} rounded-lg p-4">
<div class="card border-${statusClass}"> <div class="flex justify-between items-start">
<div class="card-body p-2"> <div>
<div class="d-flex justify-content-between align-items-center"> <h6 class="font-semibold text-sm text-neutral-900 dark:text-neutral-100">${fileType}</h6>
<div> <p class="text-xs text-neutral-600 dark:text-neutral-400">${info.table_name}</p>
<small class="fw-bold">${fileType}</small><br> </div>
<small class="text-muted">${info.table_name}</small> <div class="text-right">
</div> <i class="fa-solid fa-${statusIcon} text-lg"></i>
<div class="text-end"> <p class="font-bold text-sm text-neutral-900 dark:text-neutral-100 mt-1">${info.record_count || 0}</p>
<i class="bi bi-${statusIcon} text-${statusClass}"></i><br>
<small class="fw-bold">${info.record_count || 0}</small>
</div>
</div>
${info.error ? `<div class="text-danger small mt-1">${info.error}</div>` : ''}
</div> </div>
</div> </div>
${info.error ? `<p class="text-xs text-danger-600 dark:text-danger-400 mt-2">${info.error}</p>` : ''}
</div> </div>
`; `;
}); });
html += '</div>'; html += '</div>';
html += `<div class="mt-3 text-center"> html += `<div class="mt-4 text-center">
<strong>Total Records: ${totalRecords.toLocaleString()}</strong> <span class="font-medium text-neutral-900 dark:text-neutral-100">Total Records: ${totalRecords.toLocaleString()}</span>
</div>`; </div>`;
container.innerHTML = html; container.innerHTML = html;
@@ -310,7 +324,7 @@ async function validateFile() {
const fileInput = document.getElementById('csvFile'); const fileInput = document.getElementById('csvFile');
if (!fileType || !fileInput.files[0]) { if (!fileType || !fileInput.files[0]) {
showAlert('Please select both file type and CSV file', 'warning'); showAlert('Please select both data type and CSV file', 'warning');
return; return;
} }
@@ -350,46 +364,45 @@ function displayValidationResults(result) {
// Overall status // Overall status
const statusClass = result.valid ? 'success' : 'danger'; const statusClass = result.valid ? 'success' : 'danger';
const statusIcon = result.valid ? 'check-circle-fill' : 'exclamation-triangle-fill'; const statusIcon = result.valid ? 'circle-check text-success-600' : 'triangle-exclamation text-danger-600';
html += ` html += `
<div class="alert alert-${statusClass}"> <div class="p-4 bg-${statusClass}-100 dark:bg-${statusClass}-900/30 rounded-lg mb-4">
<i class="bi bi-${statusIcon}"></i> <i class="fa-solid fa-${statusIcon} mr-2"></i>
File validation ${result.valid ? 'passed' : 'failed'} <span class="font-medium">File validation ${result.valid ? 'passed' : 'failed'}</span>
</div> </div>
`; `;
// Headers validation // Headers validation
html += '<h6>Column Headers</h6>'; html += '<h6 class="text-sm font-semibold mb-2">Column Headers</h6>';
if (result.headers.missing.length > 0) { if (result.headers.missing.length > 0) {
html += `<div class="alert alert-warning"> html += `<div class="p-3 bg-warning-100 dark:bg-warning-900/30 rounded-lg mb-2">
<strong>Missing columns:</strong> ${result.headers.missing.join(', ')} <strong class="text-warning-700 dark:text-warning-300">Missing columns:</strong> ${result.headers.missing.join(', ')}
</div>`; </div>`;
} }
if (result.headers.extra.length > 0) { if (result.headers.extra.length > 0) {
html += `<div class="alert alert-info"> html += `<div class="p-3 bg-info-100 dark:bg-info-900/30 rounded-lg mb-2">
<strong>Extra columns:</strong> ${result.headers.extra.join(', ')} <strong class="text-info-700 dark:text-info-300">Extra columns:</strong> ${result.headers.extra.join(', ')}
</div>`; </div>`;
} }
if (result.headers.missing.length === 0 && result.headers.extra.length === 0) { if (result.headers.missing.length === 0 && result.headers.extra.length === 0) {
html += '<div class="alert alert-success">All expected columns found</div>'; html += '<div class="p-3 bg-success-100 dark:bg-success-900/30 rounded-lg mb-2">All expected columns found</div>';
} }
// Sample data // Sample data
if (result.sample_data && result.sample_data.length > 0) { if (result.sample_data && result.sample_data.length > 0) {
html += '<h6>Sample Data (First 10 rows)</h6>'; html += '<h6 class="text-sm font-semibold mb-2 mt-4">Sample Data (First 10 rows)</h6>';
html += '<div class="table-responsive">'; html += '<div class="overflow-x-auto"><table class="w-full text-sm text-neutral-900 dark:text-neutral-100">';
html += '<table class="table table-sm table-striped">'; html += '<thead><tr class="bg-neutral-100 dark:bg-neutral-700">';
html += '<thead><tr>';
Object.keys(result.sample_data[0]).forEach(header => { Object.keys(result.sample_data[0]).forEach(header => {
html += `<th>${header}</th>`; html += `<th class="px-3 py-2 text-left font-medium">${header}</th>`;
}); });
html += '</tr></thead><tbody>'; html += '</tr></thead><tbody class="divide-y divide-neutral-200 dark:divide-neutral-700">';
result.sample_data.forEach(row => { result.sample_data.forEach(row => {
html += '<tr>'; html += '<tr>';
Object.values(row).forEach(value => { Object.values(row).forEach(value => {
html += `<td class="small">${value || ''}</td>`; html += `<td class="px-3 py-2">${value || ''}</td>`;
}); });
html += '</tr>'; html += '</tr>';
}); });
@@ -398,19 +411,19 @@ function displayValidationResults(result) {
// Validation errors // Validation errors
if (result.validation_errors && result.validation_errors.length > 0) { if (result.validation_errors && result.validation_errors.length > 0) {
html += '<h6>Data Issues Found</h6>'; html += '<h6 class="text-sm font-semibold mb-2 mt-4">Data Issues Found</h6>';
html += '<div class="alert alert-warning">'; html += '<div class="p-3 bg-warning-100 dark:bg-warning-900/30 rounded-lg">';
result.validation_errors.forEach(error => { result.validation_errors.forEach(error => {
html += `<div>Row ${error.row}, Field "${error.field}": ${error.error}</div>`; html += `<div class="text-sm"><strong>Row ${error.row}, Field "${error.field}":</strong> ${error.error}</div>`;
}); });
if (result.total_errors > result.validation_errors.length) { if (result.total_errors > result.validation_errors.length) {
html += `<div class="mt-2"><strong>... and ${result.total_errors - result.validation_errors.length} more errors</strong></div>`; html += `<div class="mt-2 text-sm font-medium">... and ${result.total_errors - result.validation_errors.length} more errors</div>`;
} }
html += '</div>'; html += '</div>';
} }
container.innerHTML = html; container.innerHTML = html;
panel.style.display = 'block'; panel.classList.remove('hidden');
} }
async function handleImport(event) { async function handleImport(event) {
@@ -426,7 +439,7 @@ async function handleImport(event) {
const replaceExisting = document.getElementById('replaceExisting').checked; const replaceExisting = document.getElementById('replaceExisting').checked;
if (!fileType || !fileInput.files[0]) { if (!fileType || !fileInput.files[0]) {
showAlert('Please select both file type and CSV file', 'warning'); showAlert('Please select both data type and CSV file', 'warning');
return; return;
} }
@@ -475,30 +488,30 @@ function displayImportResults(result) {
const successClass = result.errors && result.errors.length > 0 ? 'warning' : 'success'; const successClass = result.errors && result.errors.length > 0 ? 'warning' : 'success';
let html = ` let html = `
<div class="alert alert-${successClass}"> <div class="p-4 bg-${successClass}-100 dark:bg-${successClass}-900/30 rounded-lg mb-4">
<h6><i class="bi bi-check-circle"></i> Import Completed</h6> <h6 class="font-semibold flex items-center gap-2"><i class="fa-regular fa-circle-check"></i> Import Completed</h6>
<p class="mb-0"> <div class="text-sm mt-2 space-y-1">
<strong>File Type:</strong> ${result.file_type}<br> <p><strong>File Type:</strong> ${result.file_type}</p>
<strong>Records Imported:</strong> ${result.imported_count}<br> <p><strong>Records Imported:</strong> ${result.imported_count}</p>
<strong>Errors:</strong> ${result.total_errors || 0} <p><strong>Errors:</strong> ${result.total_errors || 0}</p>
</p> </div>
</div> </div>
`; `;
if (result.errors && result.errors.length > 0) { if (result.errors && result.errors.length > 0) {
html += '<h6>Import Errors</h6>'; html += '<h6 class="text-sm font-semibold mb-2">Import Errors</h6>';
html += '<div class="alert alert-danger">'; html += '<div class="p-3 bg-danger-100 dark:bg-danger-900/30 rounded-lg">';
result.errors.forEach(error => { result.errors.forEach(error => {
html += `<div><strong>Row ${error.row}:</strong> ${error.error}</div>`; html += `<div class="text-sm"><strong>Row ${error.row}:</strong> ${error.error}</div>`;
}); });
if (result.total_errors > result.errors.length) { if (result.total_errors > result.errors.length) {
html += `<div class="mt-2"><strong>... and ${result.total_errors - result.errors.length} more errors</strong></div>`; html += `<div class="mt-2 text-sm font-medium">... and ${result.total_errors - result.errors.length} more errors</div>`;
} }
html += '</div>'; html += '</div>';
} }
container.innerHTML = html; container.innerHTML = html;
panel.style.display = 'block'; panel.classList.remove('hidden');
} }
function showProgress(show, message = '') { function showProgress(show, message = '') {
@@ -509,10 +522,9 @@ function showProgress(show, message = '') {
if (show) { if (show) {
status.textContent = message; status.textContent = message;
bar.style.width = '100%'; bar.style.width = '100%';
bar.textContent = 'Processing...'; panel.classList.remove('hidden');
panel.style.display = 'block';
} else { } else {
panel.style.display = 'none'; panel.classList.add('hidden');
} }
} }
@@ -560,25 +572,13 @@ function viewLogs() {
} }
function showAlert(message, type = 'info') { function showAlert(message, type = 'info') {
// Create and show Bootstrap alert if (window.alerts && typeof window.alerts.show === 'function') {
const alertDiv = document.createElement('div'); window.alerts.show(message, type);
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`; } else if (window.showNotification) {
alertDiv.style.top = '20px'; window.showNotification(message, type);
alertDiv.style.right = '20px'; } else {
alertDiv.style.zIndex = '9999'; alert(String(message));
alertDiv.innerHTML = ` }
${message}
<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"alert\"></button>
`;
document.body.appendChild(alertDiv);
// Auto-dismiss after 5 seconds
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
} }
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -4,78 +4,56 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Delphi Consulting Group Database System</title> <title>Login - Delphi Consulting Group Database System</title>
<script src="/static/js/alerts.js"></script>
<!-- Tailwind CSS -->
<link href="/static/css/tailwind.css" rel="stylesheet">
<!-- Icons (Font Awesome) -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<!-- Bootstrap 5.3 CDN -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<!-- Custom CSS -->
<link href="/static/css/main.css" rel="stylesheet">
<link href="/static/css/themes.css" rel="stylesheet">
<link href="/static/css/components.css" rel="stylesheet">
</head> </head>
<body class="login-page"> <body class="login-page">
<div class="container"> <div class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 px-4">
<div class="row justify-content-center"> <div class="max-w-md w-full space-y-8 bg-white dark:bg-neutral-800 p-8 rounded-xl shadow-md">
<div class="col-md-6 col-lg-4"> <div class="text-center">
<div class="card login-card shadow-sm mt-5"> <img src="/static/images/delphi-logo.webp" alt="Delphi Consulting Group" class="mx-auto h-20 w-auto">
<div class="card-body p-5"> <h2 class="mt-6 text-xl font-normal text-gray-900 dark:text-white">Delphi Database System</h2>
<div class="text-center mb-4"> <p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Sign in to access the system</p>
<img src="/static/images/delphi-logo.webp" alt="Delphi Consulting Group" height="60" class="mb-3">
<h2 class="h4 mb-3">Delphi Database System</h2>
<p class="text-muted">Sign in to access the system</p>
</div>
<form id="loginForm" class="login-form" novalidate>
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-person"></i></span>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="invalid-feedback">Please enter your username.</div>
</div>
<div class="mb-4">
<label for="password" class="form-label">Password</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-lock"></i></span>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="invalid-feedback">Please enter your password.</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary" id="loginBtn">
<i class="bi bi-box-arrow-in-right"></i> Sign In
</button>
</div>
</form>
<div class="text-center mt-4">
<small class="text-muted">
Default credentials: admin / admin123
</small>
</div>
</div>
</div>
<!-- System Status -->
<div class="card login-status mt-3">
<div class="card-body text-center py-2">
<small class="text-muted">
<i class="bi bi-shield-check text-success"></i>
Secure connection established
</small>
</div>
</div>
</div> </div>
<form class="mt-8 space-y-6" id="loginForm">
<div class="space-y-4">
<div>
<label for="username" class="sr-only">Username</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-400">
<i class="fa-solid fa-user"></i>
</div>
<input id="username" name="username" type="text" required class="appearance-none rounded-lg relative block w-full px-3 py-2 pl-10 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm" placeholder="Username">
</div>
</div>
<div>
<label for="password" class="sr-only">Password</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-400">
<i class="fa-solid fa-lock"></i>
</div>
<input id="password" name="password" type="password" required class="appearance-none rounded-lg relative block w-full px-3 py-2 pl-10 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm" placeholder="Password">
</div>
</div>
</div>
<div>
<button type="submit" id="loginBtn" class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-lg text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition duration-150 ease-in-out">
Sign in
</button>
</div>
</form>
<p class="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
Default credentials: admin / admin123
</p>
</div> </div>
</div> </div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Check for logout reason // Check for logout reason
@@ -102,19 +80,22 @@
// Validate form // Validate form
if (!loginForm.checkValidity()) { if (!loginForm.checkValidity()) {
e.stopPropagation(); e.stopPropagation();
loginForm.classList.add('was-validated'); loginForm.reportValidity();
return; return;
} }
const username = document.getElementById('username').value; const username = document.getElementById('username').value;
const password = document.getElementById('password').value; const password = document.getElementById('password').value;
console.log('Attempting login with username:', username);
// Show loading state // Show loading state
const originalText = loginBtn.innerHTML; const originalText = loginBtn.innerHTML;
loginBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Signing in...'; loginBtn.innerHTML = '<span class="inline-block animate-spin w-4 h-4 border-2 border-white border-t-transparent rounded-full mr-2"></span>Signing in...';
loginBtn.disabled = true; loginBtn.disabled = true;
try { try {
console.log('Sending request to /api/auth/login');
const response = await fetch('/api/auth/login', { const response = await fetch('/api/auth/login', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -126,12 +107,22 @@
}) })
}); });
console.log('Response status:', response.status);
if (!response.ok) { if (!response.ok) {
const error = await response.json(); let errorMessage = 'Login failed';
throw new Error(error.detail || 'Login failed'); try {
const errorData = await response.json();
console.log('Error data:', errorData);
errorMessage = errorData.detail || errorMessage;
} catch (e) {
console.log('Failed to parse error response');
}
throw new Error(errorMessage);
} }
const data = await response.json(); const data = await response.json();
console.log('Login successful, token:', data.access_token);
// Store token // Store token
localStorage.setItem('auth_token', data.access_token); localStorage.setItem('auth_token', data.access_token);
@@ -181,30 +172,11 @@
} }
function showAlert(message, type = 'info') { function showAlert(message, type = 'info') {
// Remove existing alerts if (window.alerts && typeof window.alerts.show === 'function') {
const existingAlerts = document.querySelectorAll('.alert'); window.alerts.show(message, type);
existingAlerts.forEach(alert => alert.remove()); return;
// Create new alert
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show mt-3`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// Insert before the form
const form = document.getElementById('loginForm');
form.parentNode.insertBefore(alertDiv, form);
// Auto-dismiss success messages
if (type === 'success') {
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
} }
alert(String(message));
} }
</script> </script>
</body> </body>

File diff suppressed because it is too large Load Diff

View File

@@ -1,138 +1,192 @@
<!-- Support Ticket Modal --> <!-- Support Ticket Modal -->
<div class="modal fade" id="supportModal" tabindex="-1" aria-labelledby="supportModalLabel" aria-hidden="true"> <div id="supportModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div class="modal-dialog modal-lg"> <div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-4xl w-full max-h-screen overflow-hidden">
<div class="modal-content"> <div class="flex items-center justify-between px-6 py-4 bg-primary-600 text-white">
<div class="modal-header bg-primary text-white"> <h2 class="text-xl font-semibold flex items-center gap-2">
<h5 class="modal-title" id="supportModalLabel"> <i class="fas fa-bug"></i>
<i class="fas fa-bug me-2"></i>Submit Internal Issue <span>Submit Internal Issue</span>
</h5> </h2>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button> <button onclick="closeSupportModal()" class="text-primary-200 hover:text-white transition-colors">
</div> <i class="fa-solid fa-xmark text-xl"></i>
<div class="modal-body"> </button>
<form id="supportForm"> </div>
<div class="row">
<div class="col-md-6 mb-3"> <div class="px-6 py-4 max-h-96 overflow-y-auto scrollbar-thin">
<label for="contactName" class="form-label">Reporter Name *</label> <form id="supportForm">
<input type="text" class="form-control" id="contactName" required> <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label for="contactName" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Reporter Name *</label>
<input type="text" id="contactName" required 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">
</div>
<div>
<label for="contactEmail" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Reporter Email *</label>
<input type="email" id="contactEmail" required 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">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label for="ticketCategory" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Issue Type *</label>
<select id="ticketCategory" required 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">
<option value="">Select issue type...</option>
<option value="bug_report" selected>Bug Report</option>
<option value="qa_issue">QA Issue</option>
<option value="feature_request">Feature Request</option>
<option value="database_issue">Database Issue</option>
<option value="system_error">System Error</option>
<option value="user_access">User Access</option>
<option value="performance">Performance Issue</option>
<option value="documentation">Documentation</option>
<option value="configuration">Configuration</option>
<option value="testing">Testing Request</option>
</select>
</div>
<div>
<label for="ticketPriority" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Priority</label>
<select id="ticketPriority" 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">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
</div>
<div class="mb-4">
<label for="ticketSubject" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Issue Summary *</label>
<input type="text" id="ticketSubject" maxlength="200" required 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">
<p class="text-sm text-neutral-500 dark:text-neutral-400 mt-1">Brief summary of the bug/issue</p>
</div>
<div class="mb-4">
<label for="ticketDescription" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Detailed Description *</label>
<textarea id="ticketDescription" rows="5" required 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 resize-none" placeholder="Steps to reproduce:
1.
2.
3.
Expected behavior:
Actual behavior:
Additional context:"></textarea>
<p class="text-sm text-neutral-500 dark:text-neutral-400 mt-1">Include steps to reproduce, expected vs actual behavior, error messages, etc.</p>
</div>
<!-- System Information -->
<div class="bg-neutral-50 dark:bg-neutral-800/50 rounded-lg border border-neutral-200 dark:border-neutral-700 p-4 mb-4">
<h3 class="text-sm font-semibold text-neutral-700 dark:text-neutral-300 mb-3 flex items-center gap-2">
<i class="fas fa-info-circle text-info-600"></i>
<span>System Information</span>
<span class="text-xs font-normal text-neutral-500 dark:text-neutral-400">(automatically included)</span>
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div>
<span class="font-medium text-neutral-600 dark:text-neutral-400">Current Page:</span>
<span id="currentPageInfo" class="text-neutral-900 dark:text-neutral-100 ml-1">Loading...</span>
</div> </div>
<div class="col-md-6 mb-3"> <div>
<label for="contactEmail" class="form-label">Reporter Email *</label> <span class="font-medium text-neutral-600 dark:text-neutral-400">Browser:</span>
<input type="email" class="form-control" id="contactEmail" required> <span id="browserInfo" class="text-neutral-900 dark:text-neutral-100 ml-1">Loading...</span>
</div> </div>
</div> </div>
</div>
<div class="row"> <div class="flex items-start gap-3 p-4 bg-info-50 dark:bg-info-900/20 border border-info-200 dark:border-info-800 rounded-lg text-info-800 dark:text-info-300">
<div class="col-md-6 mb-3"> <i class="fas fa-info-circle text-info-600 dark:text-info-400 mt-0.5"></i>
<label for="ticketCategory" class="form-label">Issue Type *</label> <div>
<select class="form-select" id="ticketCategory" required> <p class="font-medium">Note:</p>
<option value="">Select issue type...</option> <p class="text-sm mt-1">Your issue will be assigned a tracking number and the development team will be notified automatically.</p>
<option value="bug_report" selected>Bug Report</option>
<option value="qa_issue">QA Issue</option>
<option value="feature_request">Feature Request</option>
<option value="database_issue">Database Issue</option>
<option value="system_error">System Error</option>
<option value="user_access">User Access</option>
<option value="performance">Performance Issue</option>
<option value="documentation">Documentation</option>
<option value="configuration">Configuration</option>
<option value="testing">Testing Request</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="ticketPriority" class="form-label">Priority</label>
<select class="form-select" id="ticketPriority">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
</div> </div>
</div>
</form>
</div>
<div class="mb-3"> <div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800/50">
<label for="ticketSubject" class="form-label">Issue Summary *</label> <button onclick="closeSupportModal()" class="px-4 py-2 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">
<input type="text" class="form-control" id="ticketSubject" maxlength="200" required> Cancel
<div class="form-text">Brief summary of the bug/issue</div> </button>
</div> <button type="button" id="submitSupportTicket" class="flex items-center gap-2 px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg transition-colors duration-200">
<i class="fas fa-bug"></i>
<div class="mb-3"> <span>Submit Issue</span>
<label for="ticketDescription" class="form-label">Detailed Description *</label> </button>
<textarea class="form-control" id="ticketDescription" rows="5" required placeholder="Steps to reproduce:&#10;1. &#10;2. &#10;3. &#10;&#10;Expected behavior:&#10;&#10;Actual behavior:&#10;&#10;Additional context:"></textarea>
<div class="form-text">Include steps to reproduce, expected vs actual behavior, error messages, etc.</div>
</div>
<!-- System Information (auto-populated) -->
<div class="card bg-light mb-3">
<div class="card-body">
<h6 class="card-title">
<i class="fas fa-info-circle me-1"></i>System Information
<small class="text-muted">(automatically included)</small>
</h6>
<div class="row">
<div class="col-md-6">
<small><strong>Current Page:</strong> <span id="currentPageInfo">Loading...</span></small>
</div>
<div class="col-md-6">
<small><strong>Browser:</strong> <span id="browserInfo">Loading...</span></small>
</div>
</div>
</div>
</div>
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
<strong>Note:</strong> Your issue will be assigned a tracking number and the development team will be notified automatically.
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="submitSupportTicket">
<i class="fas fa-bug me-2"></i>Submit Issue
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Support Ticket Success Modal --> <!-- Support Ticket Success Modal -->
<div class="modal fade" id="supportSuccessModal" tabindex="-1" aria-labelledby="supportSuccessLabel" aria-hidden="true"> <div id="supportSuccessModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div class="modal-dialog"> <div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-2xl w-full">
<div class="modal-content"> <div class="flex items-center justify-between px-6 py-4 bg-success-600 text-white">
<div class="modal-header bg-success text-white"> <h2 class="text-xl font-semibold flex items-center gap-2">
<h5 class="modal-title" id="supportSuccessLabel"> <i class="fas fa-check-circle"></i>
<i class="fas fa-check-circle me-2"></i>Issue Submitted Successfully <span>Issue Submitted Successfully</span>
</h5> </h2>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button> <button onclick="closeSupportSuccessModal()" class="text-success-200 hover:text-white transition-colors">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div class="px-6 py-8 text-center">
<div class="mb-6">
<div class="w-16 h-16 bg-success-100 dark:bg-success-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
<i class="fas fa-bug text-2xl text-success-600 dark:text-success-400"></i>
</div>
<h3 class="text-xl font-semibold text-neutral-900 dark:text-neutral-100 mb-2">Issue logged successfully!</h3>
</div> </div>
<div class="modal-body text-center">
<div class="mb-3"> <div class="bg-success-50 dark:bg-success-900/20 border border-success-200 dark:border-success-800 rounded-lg p-4 mb-6">
<i class="fas fa-bug fa-3x text-success mb-3"></i> <div class="flex items-center justify-center gap-2">
<h4>Issue logged successfully!</h4> <span class="font-medium text-success-800 dark:text-success-300">Issue ID:</span>
</div> <span id="newTicketNumber" class="font-mono font-semibold text-success-900 dark:text-success-200"></span>
<div class="alert alert-success">
<strong>Issue ID:</strong> <span id="newTicketNumber"></span>
</div>
<p>Your issue has been logged and the development team has been notified. You'll receive updates on the resolution progress.</p>
<div class="mt-4">
<h6>What happens next?</h6>
<ul class="list-unstyled text-start">
<li><i class="fas fa-check text-success me-2"></i>Issue logged in tracking system</li>
<li><i class="fas fa-users text-warning me-2"></i>Development team has been notified</li>
<li><i class="fas fa-code text-info me-2"></i>Issue will be triaged and prioritized</li>
<li><i class="fas fa-bell text-primary me-2"></i>You'll get status updates via email</li>
</ul>
</div> </div>
</div> </div>
<div class="modal-footer">
<button type="button" class="btn btn-success" data-bs-dismiss="modal">Close</button> <p class="text-neutral-600 dark:text-neutral-400 mb-6">
Your issue has been logged and the development team has been notified. You'll receive updates on the resolution progress.
</p>
<div class="text-left">
<h4 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4 text-center">What happens next?</h4>
<div class="space-y-3">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-success-100 dark:bg-success-900/30 rounded-full flex items-center justify-center flex-shrink-0">
<i class="fas fa-check text-success-600 dark:text-success-400 text-sm"></i>
</div>
<span class="text-neutral-700 dark:text-neutral-300">Issue logged in tracking system</span>
</div>
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-warning-100 dark:bg-warning-900/30 rounded-full flex items-center justify-center flex-shrink-0">
<i class="fas fa-users text-warning-600 dark:text-warning-400 text-sm"></i>
</div>
<span class="text-neutral-700 dark:text-neutral-300">Development team has been notified</span>
</div>
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-info-100 dark:bg-info-900/30 rounded-full flex items-center justify-center flex-shrink-0">
<i class="fas fa-code text-info-600 dark:text-info-400 text-sm"></i>
</div>
<span class="text-neutral-700 dark:text-neutral-300">Issue will be triaged and prioritized</span>
</div>
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-primary-100 dark:bg-primary-900/30 rounded-full flex items-center justify-center flex-shrink-0">
<i class="fas fa-bell text-primary-600 dark:text-primary-400 text-sm"></i>
</div>
<span class="text-neutral-700 dark:text-neutral-300">You'll get status updates via email</span>
</div>
</div>
</div> </div>
</div> </div>
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800/50">
<button onclick="closeSupportSuccessModal()" class="px-4 py-2 bg-success-600 text-white hover:bg-success-700 rounded-lg transition-colors duration-200">
Close
</button>
</div>
</div> </div>
</div> </div>
<script> <script>
// Support ticket functionality // Support ticket functionality - Tailwind version
let supportSystem = { let supportSystem = {
currentPageInfo: 'Unknown', currentPageInfo: 'Unknown',
browserInfo: 'Unknown', browserInfo: 'Unknown',
@@ -176,45 +230,57 @@ let supportSystem = {
this.browserInfo = `${browserName} (${navigator.platform})`; this.browserInfo = `${browserName} (${navigator.platform})`;
// Update modal display // Update modal display
document.getElementById('currentPageInfo').textContent = this.currentPageInfo; const currentPageElement = document.getElementById('currentPageInfo');
document.getElementById('browserInfo').textContent = this.browserInfo; const browserElement = document.getElementById('browserInfo');
if (currentPageElement) currentPageElement.textContent = this.currentPageInfo;
if (browserElement) browserElement.textContent = this.browserInfo;
}, },
setupEventListeners: function() { setupEventListeners: function() {
// Auto-populate user info if logged in
const supportModal = document.getElementById('supportModal');
supportModal.addEventListener('show.bs.modal', this.populateUserInfo.bind(this));
// Submit button // Submit button
document.getElementById('submitSupportTicket').addEventListener('click', this.submitTicket.bind(this)); const submitBtn = document.getElementById('submitSupportTicket');
if (submitBtn) {
submitBtn.addEventListener('click', this.submitTicket.bind(this));
}
// Form validation // Form validation
const form = document.getElementById('supportForm'); const form = document.getElementById('supportForm');
form.addEventListener('submit', function(e) { if (form) {
e.preventDefault(); form.addEventListener('submit', function(e) {
supportSystem.submitTicket(); e.preventDefault();
}); supportSystem.submitTicket();
});
}
}, },
populateUserInfo: function() { populateUserInfo: function() {
// Try to get current user info from the global app state // Try to get current user info from the global app state
if (window.app && window.app.user) { if (window.app && window.app.user) {
const user = window.app.user; const user = window.app.user;
document.getElementById('contactName').value = `${user.first_name || ''} ${user.last_name || ''}`.trim() || user.username; const nameInput = document.getElementById('contactName');
document.getElementById('contactEmail').value = user.email; const emailInput = document.getElementById('contactEmail');
if (nameInput && !nameInput.value) {
nameInput.value = `${user.first_name || ''} ${user.last_name || ''}`.trim() || user.username;
}
if (emailInput && !emailInput.value) {
emailInput.value = user.email;
}
} }
}, },
submitTicket: async function() { submitTicket: async function() {
const form = document.getElementById('supportForm'); const form = document.getElementById('supportForm');
if (!form.checkValidity()) { if (!form.checkValidity()) {
form.classList.add('was-validated'); // Show validation errors
form.reportValidity();
return; return;
} }
const submitBtn = document.getElementById('submitSupportTicket'); const submitBtn = document.getElementById('submitSupportTicket');
const originalText = submitBtn.innerHTML; const originalHTML = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Submitting...'; submitBtn.innerHTML = '<i class="fas fa-spinner animate-spin"></i><span class="ml-2">Submitting...</span>';
submitBtn.disabled = true; submitBtn.disabled = true;
try { try {
@@ -241,15 +307,14 @@ let supportSystem = {
if (response.ok) { if (response.ok) {
// Hide support modal // Hide support modal
bootstrap.Modal.getInstance(document.getElementById('supportModal')).hide(); closeSupportModal();
// Show success modal // Show success modal
document.getElementById('newTicketNumber').textContent = result.ticket_number; document.getElementById('newTicketNumber').textContent = result.ticket_number;
new bootstrap.Modal(document.getElementById('supportSuccessModal')).show(); document.getElementById('supportSuccessModal').classList.remove('hidden');
// Reset form // Reset form
form.reset(); form.reset();
form.classList.remove('was-validated');
} else { } else {
throw new Error(result.detail || 'Failed to submit ticket'); throw new Error(result.detail || 'Failed to submit ticket');
@@ -257,30 +322,63 @@ let supportSystem = {
} catch (error) { } catch (error) {
console.error('Error submitting support ticket:', error); console.error('Error submitting support ticket:', error);
this.showAlert('Failed to submit support ticket: ' + error.message, 'error'); this.showAlert('Failed to submit support ticket: ' + error.message, 'danger');
} finally { } finally {
submitBtn.innerHTML = originalText; submitBtn.innerHTML = originalHTML;
submitBtn.disabled = false; submitBtn.disabled = false;
} }
}, },
showAlert: function(message, type = 'info') { showAlert: function(message, type = 'info') {
// Use existing notification system if available // Use existing alert system if available
if (window.showNotification) { if (window.showAlert) {
window.showNotification(message, type); window.showAlert(message, type);
} else { } else {
alert(message); alert(message);
} }
} }
}; };
// Modal control functions
function openSupportModal() {
supportSystem.populateUserInfo();
document.getElementById('supportModal').classList.remove('hidden');
}
function closeSupportModal() {
document.getElementById('supportModal').classList.add('hidden');
}
function closeSupportSuccessModal() {
document.getElementById('supportSuccessModal').classList.add('hidden');
}
// Close modals when clicking outside
document.addEventListener('click', function(event) {
const supportModal = document.getElementById('supportModal');
const successModal = document.getElementById('supportSuccessModal');
if (event.target === supportModal) {
closeSupportModal();
}
if (event.target === successModal) {
closeSupportSuccessModal();
}
});
// Handle escape key for modals
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
closeSupportModal();
closeSupportSuccessModal();
}
});
// Initialize when DOM is loaded // Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
supportSystem.init(); supportSystem.init();
}); });
// Global function to open support modal // Make function globally available
window.openSupportModal = function() { window.openSupportModal = openSupportModal;
new bootstrap.Modal(document.getElementById('supportModal')).show();
};
</script> </script>