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
- **Backend**: Python 3.12, FastAPI, SQLAlchemy 2.0+
- **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
- **Validation**: Pydantic v2

View File

@@ -20,7 +20,8 @@ from app.auth.schemas import (
Token,
UserCreate,
UserResponse,
LoginRequest
LoginRequest,
ThemePreferenceUpdate
)
from app.config import settings
@@ -117,3 +118,22 @@ async def list_users(
"""List all users (admin only)"""
users = db.query(User).all()
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
"""
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 import or_, func, and_, desc, asc, text
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.user import User
from app.auth.security import get_current_user
from app.models.additional import Document
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 ""))
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
is_active: bool
is_admin: bool
theme_preference: Optional[str] = "light"
class Config:
from_attributes = True
class ThemePreferenceUpdate(BaseModel):
"""Theme preference update schema"""
theme_preference: str
class Token(BaseModel):
"""Token response schema"""
access_token: str

View File

@@ -34,6 +34,7 @@ app.add_middleware(
# Mount static files
app.mount("/static", StaticFiles(directory="static"), name="static")
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
# 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.import_data import router as import_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(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(import_router, prefix="/api/import", tags=["import"])
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)

View File

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

View File

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

View File

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

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">
<title>{% block title %}{{ title }}{% endblock %}</title>
<!-- 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">
<!-- Icons (Font Awesome) -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.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">
<!-- Tailwind CSS -->
<!-- Custom Tailwind CSS -->
<link href="/static/css/tailwind.css" rel="stylesheet">
<style>
/* Footer Enhancements */
footer .btn-outline-primary:hover {
background-color: #0d6efd;
border-color: #0d6efd;
color: white;
transform: translateY(-1px);
transition: all 0.2s ease;
}
{% block bridge_css %}{% endblock %}
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 %}
</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 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/">
<nav class="bg-primary-600 border-b border-primary-700 shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<!-- Brand -->
<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">
<span class="navbar-toggler-icon"></span>
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-1">
<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>
<!-- Right side items -->
<div class="flex items-center space-x-3">
<!-- Theme Toggle -->
<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="fas fa-sun dark:hidden text-sm"></i>
<i class="fas fa-moon hidden dark:block text-sm"></i>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="/customers" data-shortcut="Alt+C">
<i class="bi bi-people"></i> Customers <small>(Alt+C)</small>
<!-- User Dropdown -->
<div class="relative" id="userDropdown">
<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="fa-solid fa-circle-user"></i>
<span>User</span>
<i class="fa-solid fa-chevron-down text-xs"></i>
</button>
<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">
<div class="py-1">
<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">
<i class="fa-solid fa-gear"></i>
<span>Admin</span>
</a>
</li>
<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>
<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>
</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>
</div>
</div>
</div>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle"></i> User
<!-- 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>
<ul class="dropdown-menu">
<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>
<li id="admin-menu-divider" style="display: none;"><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="logout()"><i class="bi bi-box-arrow-right"></i> Logout</a></li>
</ul>
</li>
</ul>
<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>
</nav>
<!-- Main Content -->
<main class="flex-grow-1">
<div class="container-fluid mt-3 mb-4">
<main class="flex-grow">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{% block content %}{% endblock %}
</div>
</main>
<!-- Footer -->
<footer class="mt-auto py-3 border-top shadow-sm" style="background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border-color: #dee2e6 !important;">
<div class="container">
<div class="row align-items-center">
<div class="col-md-6">
<small class="text-muted">
<footer class="mt-auto border-t border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div class="flex flex-col sm:flex-row items-center justify-between gap-4">
<div class="text-sm text-neutral-500 dark:text-neutral-400">
&copy; <span id="currentYear"></span> Delphi Consulting Group Database System
<span class="mx-2">|</span>
<span id="currentPageDisplay">Loading...</span>
</small>
</div>
<div class="col-md-6 text-end">
<button type="button" class="btn btn-outline-primary btn-sm me-3" onclick="openSupportModal()">
<i class="fas fa-bug me-1"></i>Report Issue
<div class="flex items-center gap-4">
<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"></i>
<span>Report Issue</span>
</button>
<small class="text-muted">
Found a bug? <a href="#" onclick="openSupportModal()" class="text-primary text-decoration-none">Report Issue</a>
</small>
<div class="text-sm text-neutral-500 dark:text-neutral-400">
Found a bug?
<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>
@@ -143,71 +154,220 @@
{% include 'support_modal.html' %}
<!-- Keyboard Shortcuts Help Modal -->
<div class="modal fade" id="shortcutsModal" tabindex="-1" aria-labelledby="shortcutsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="shortcutsModalLabel">
<i class="bi bi-keyboard"></i> Keyboard Shortcuts
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div id="shortcutsModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-4xl w-full max-h-screen overflow-hidden">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h2 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<i class="fa-solid fa-keyboard"></i>
<span>Keyboard Shortcuts</span>
</h2>
<button onclick="closeShortcutsModal()" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300 transition-colors">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<h6><i class="bi bi-house"></i> Navigation</h6>
<ul class="list-unstyled">
<li><kbd>Alt+C</kbd> - Customers/Rolodex</li>
<li><kbd>Alt+F</kbd> - File Cabinet</li>
<li><kbd>Alt+L</kbd> - Ledger/Financial</li>
<li><kbd>Alt+D</kbd> - Documents/QDROs</li>
<li><kbd>Alt+A</kbd> - Admin Panel</li>
<li><kbd>Ctrl+F</kbd> - Global Search</li>
<div class="px-6 py-4 max-h-96 overflow-y-auto scrollbar-thin">
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<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-house"></i>
<span>Navigation</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">Customers/Rolodex</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+C</kbd>
</li>
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">File Cabinet</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+F</kbd>
</li>
<li class="flex items-center justify-between">
<span class="text-neutral-600 dark:text-neutral-400">Ledger/Financial</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+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>
</div>
<h6><i class="bi bi-pencil"></i> Forms</h6>
<ul class="list-unstyled">
<li><kbd>Ctrl+N</kbd> - New Record</li>
<li><kbd>Ctrl+S</kbd> - Save</li>
<li><kbd>F9</kbd> - Edit Mode</li>
<li><kbd>F2</kbd> - Complete/Save</li>
<li><kbd>F8</kbd> - Clear/Cancel</li>
<li><kbd>Del</kbd> - Delete Record</li>
<li><kbd>Esc</kbd> - Cancel/Close</li>
<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-pencil"></i>
<span>Forms</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">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 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>
</div>
<h6><i class="bi bi-tools"></i> System</h6>
<ul class="list-unstyled">
<li><kbd>F1</kbd> - Help (this dialog)</li>
<li><kbd>F10</kbd> - Menu</li>
<li><kbd>Alt+M</kbd> - Memo/Notes</li>
<li><kbd>Alt+T</kbd> - Time Tracker</li>
<li><kbd>Alt+B</kbd> - Balance Summary</li>
<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>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</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="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>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Custom Navigation JS -->
<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 -->
<script src="/static/js/alerts.js"></script>
<script src="/static/js/keyboard-shortcuts.js"></script>
<script src="/static/js/main.js"></script>
@@ -272,14 +432,14 @@
const user = await response.json();
if (user.is_admin) {
// Show admin menu items
document.getElementById('admin-menu-item').style.display = 'block';
document.getElementById('admin-menu-divider').style.display = 'block';
document.getElementById('admin-menu-item').classList.remove('hidden');
document.getElementById('admin-menu-divider').classList.remove('hidden');
}
// 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) {
userDropdown.innerHTML = `<i class="bi bi-person-circle"></i> ${user.full_name}`;
userDropdown.textContent = user.full_name;
}
}
} catch (error) {
@@ -368,28 +528,136 @@
}
}
function setupActivityMonitoring() {
async function setupActivityMonitoring() {
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
const activityEvents = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'];
activityEvents.forEach(event => {
document.addEventListener(event, () => {
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(() => {
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) {
// User has been inactive for 4+ hours, logout
if (timeSinceActivity > warningMs && !warningShown) {
showInactivityWarning();
warningShown = true;
}
if (timeSinceActivity > logoutMs) {
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
@@ -402,12 +670,101 @@
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
window.authManager = {
checkTokenValidity,
refreshTokenIfNeeded,
logout
};
window.themeManager = {
toggleTheme,
initializeTheme,
saveThemePreference,
loadUserThemePreference
};
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,174 +1,184 @@
{% extends "base.html" %}
{% block title %}Dashboard - {{ super() }}{% endblock %}
{% block title %}Dashboard - Delphi Database{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-speedometer2"></i> Dashboard</h1>
<div>
<button class="btn btn-outline-secondary btn-sm" onclick="showShortcuts()">
<i class="bi bi-keyboard"></i> Shortcuts (F1)
<div class="space-y-6">
<!-- Page Header -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div class="flex items-center gap-3">
<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-gauge-high text-lg"></i>
</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>
<!-- Quick Stats Cards -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="me-3">
<i class="bi bi-people fs-1"></i>
<!-- Quick Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="bg-primary-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-primary-700 rounded-lg">
<i class="fa-solid fa-users text-xl"></i>
</div>
<div>
<h5 class="card-title">Customers</h5>
<h2 class="mb-0" id="customer-count">-</h2>
<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-white-50 small">
View all <i class="bi bi-arrow-right"></i>
<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>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="me-3">
<i class="bi bi-folder fs-1"></i>
<!-- 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>
<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="col-md-3">
<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>
<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 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 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 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>
<div class="space-y-3">
<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">
<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 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 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>
<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-clock-rotate-left"></i>
<span>Recent Activity</span>
</h5>
</div>
<div class="p-6" id="recent-activity">
<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>
<p>Loading recent activity...</p>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-clock-history"></i> Recent Activity</h5>
<!-- System Status -->
<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>System Information</span>
</h5>
</div>
<div class="card-body">
<div id="recent-activity">
<p class="text-muted text-center">
<i class="bi bi-hourglass-split"></i><br>
Loading recent activity...
<div class="p-6">
<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>
</div>
</div>
</div>
</div>
</div>
<!-- System Status -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-info-circle"></i> System Information</h5>
</div>
<div class="card-body">
<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 class="space-y-3 text-sm">
<p class="flex justify-between">
<strong>Last Backup:</strong>
<span id="last-backup">Not available</span>
</p>
<p class="flex justify-between">
<strong>Database Size:</strong>
<span id="db-size">-</span>
</p>
<p class="flex justify-between">
<strong>Status:</strong>
<span id="system-status" class="px-2 py-1 bg-success-600 text-white text-xs font-medium rounded-full">Healthy</span>
</p>
</div>
</div>
</div>
@@ -178,13 +188,12 @@
{% block extra_scripts %}
<script>
// Load dashboard data
async function loadDashboardData() {
// Load dashboard data
async function loadDashboardData() {
try {
// This would typically be authenticated API calls
const response = await fetch('/api/admin/stats', {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
}
});
@@ -200,33 +209,28 @@
} catch (error) {
console.error('Error loading dashboard data:', error);
}
}
}
// Quick action functions
function newCustomer() {
window.location.href = '/customers/new';
}
// Quick action functions
function newCustomer() {
window.location.href = '/customers';
}
function newFile() {
window.location.href = '/files/new';
}
function newFile() {
window.location.href = '/files';
}
function newTransaction() {
window.location.href = '/financial/new';
}
function newTransaction() {
window.location.href = '/financial';
}
function globalSearch() {
function globalSearch() {
window.location.href = '/search';
}
}
function showShortcuts() {
const modal = new bootstrap.Modal(document.getElementById('shortcutsModal'));
modal.show();
}
// Load data on page load
document.addEventListener('DOMContentLoaded', function() {
// loadDashboardData(); // Uncomment when authentication is implemented
});
// Load data on page load
document.addEventListener('DOMContentLoaded', function() {
loadDashboardData(); // Uncomment when authentication is implemented
});
</script>
{% endblock %}

View File

@@ -3,74 +3,72 @@
{% block title %}Document Management - Delphi Database{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2><i class="bi bi-file-earmark-text"></i> Document Management</h2>
<div>
<button class="btn btn-success" id="newTemplateBtn">
<i class="bi bi-plus-circle"></i> New Template (Ctrl+N)
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div class="space-y-6">
<!-- Page Header -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div class="flex items-center gap-3">
<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-regular fa-file-lines text-lg"></i>
</div>
<h1 class="text-2xl font-bold text-neutral-900 dark:text-neutral-100">Document Management</h1>
</div>
<div class="flex items-center gap-3">
<button id="newTemplateBtn" class="flex items-center gap-2 px-4 py-2 bg-success-600 text-white hover:bg-success-700 rounded-lg transition-colors duration-200">
<i class="fa-solid fa-circle-plus"></i>
<span>New Template</span>
<kbd class="hidden sm:inline-block ml-2 px-1.5 py-0.5 bg-success-700 rounded text-xs">Ctrl+N</kbd>
</button>
<button class="btn btn-primary" id="generateDocBtn">
<i class="bi bi-file-plus"></i> Generate Document
<button id="generateDocBtn" 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="fa-regular fa-file-lines"></i>
<span>Generate Document</span>
</button>
<button class="btn btn-warning" id="newQdroBtn">
<i class="bi bi-file-earmark-ruled"></i> New QDRO
<button id="newQdroBtn" class="flex items-center gap-2 px-4 py-2 bg-warning-600 text-white hover:bg-warning-700 rounded-lg transition-colors duration-200">
<i class="fa-regular fa-file-lines"></i>
<span>New QDRO</span>
</button>
<button class="btn btn-info" id="statsBtn">
<i class="bi bi-graph-up"></i> Statistics
<button id="statsBtn" class="flex items-center gap-2 px-4 py-2 bg-info-600 text-white hover:bg-info-700 rounded-lg transition-colors duration-200">
<i class="fa-solid fa-chart-line"></i>
<span>Statistics</span>
</button>
</div>
</div>
<!-- Document Management Tabs -->
<ul class="nav nav-tabs" id="documentTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="templates-tab" data-bs-toggle="tab" data-bs-target="#templates"
type="button" role="tab" aria-controls="templates" aria-selected="true">
<i class="bi bi-file-text"></i> Templates
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-md overflow-hidden">
<nav class="flex border-b border-neutral-200 dark:border-neutral-700">
<button class="flex-1 py-4 px-6 text-center border-b-2 border-transparent hover:border-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition duration-300" onclick="openTab(event, 'templates')" id="templates-tab">
<i class="fa-regular fa-file-lines mr-2"></i> Templates
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="qdros-tab" data-bs-toggle="tab" data-bs-target="#qdros"
type="button" role="tab" aria-controls="qdros" aria-selected="false">
<i class="bi bi-file-earmark-ruled"></i> QDROs
<button class="flex-1 py-4 px-6 text-center border-b-2 border-transparent hover:border-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition duration-300" onclick="openTab(event, 'qdros')" id="qdros-tab">
<i class="fa-regular fa-file-lines mr-2"></i> QDROs
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="generated-tab" data-bs-toggle="tab" data-bs-target="#generated"
type="button" role="tab" aria-controls="generated" aria-selected="false">
<i class="bi bi-file-pdf"></i> Generated Documents
<button class="flex-1 py-4 px-6 text-center border-b-2 border-transparent hover:border-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition duration-300" onclick="openTab(event, 'generated')" id="generated-tab">
<i class="fa-regular fa-file-pdf mr-2"></i> Generated Documents
</button>
</li>
</ul>
</nav>
<div class="tab-content" id="documentTabContent">
<!-- Templates Tab -->
<div class="tab-pane fade show active" id="templates" role="tabpanel" aria-labelledby="templates-tab">
<div class="card mt-3">
<div class="card-header">
<div class="row align-items-center">
<div class="col-md-8">
<h5 class="mb-0"><i class="bi bi-file-text"></i> Document Templates</h5>
<div id="templates" class="tabcontent p-6">
<div class="mt-3 bg-white dark:bg-neutral-800 rounded-xl shadow-soft border border-neutral-200 dark:border-neutral-700">
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 items-center">
<div class="md:col-span-2">
<h5 class="mb-0 font-semibold flex items-center gap-2"><i class="fa-regular fa-file-lines"></i> Document Templates</h5>
</div>
<div class="col-md-4">
<div class="input-group">
<input type="text" class="form-control form-control-sm" id="templateSearch" placeholder="Search templates...">
<select class="form-select form-select-sm" id="categoryFilter">
<div class="md:col-span-1">
<div class="flex gap-2">
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="templateSearch" placeholder="Search templates...">
<select class="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="categoryFilter">
<option value="">All Categories</option>
</select>
<button class="btn btn-outline-secondary btn-sm" id="refreshTemplatesBtn">
<i class="bi bi-arrow-clockwise"></i>
</button>
<button class="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-700" id="refreshTemplatesBtn"><i class="fa-solid fa-rotate-right"></i></button>
</div>
</div>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" id="templatesTable">
<div class="p-6">
<div class="overflow-x-auto">
<table class="w-full text-sm text-left border border-neutral-200 dark:border-neutral-700 rounded-lg overflow-hidden" id="templatesTable">
<thead>
<tr>
<th>Template ID</th>
@@ -90,33 +88,28 @@
</div>
</div>
<!-- QDROs Tab -->
<div class="tab-pane fade" id="qdros" role="tabpanel" aria-labelledby="qdros-tab">
<div class="card mt-3">
<div class="card-header">
<div class="row align-items-center">
<div class="col-md-6">
<h5 class="mb-0"><i class="bi bi-file-earmark-ruled"></i> QDRO Documents</h5>
</div>
<div class="col-md-6">
<div class="input-group">
<input type="text" class="form-control form-control-sm" id="qdroSearch" placeholder="Search QDROs...">
<select class="form-select form-select-sm" id="qdroStatusFilter">
<div id="qdros" class="tabcontent p-6 hidden">
<div class="mt-3 bg-white dark:bg-neutral-800 rounded-xl shadow-soft border border-neutral-200 dark:border-neutral-700">
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 items-center">
<div><h5 class="mb-0 font-semibold"><i class="fa-regular fa-file-lines"></i> QDRO Documents</h5></div>
<div>
<div class="flex gap-2">
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroSearch" placeholder="Search QDROs...">
<select class="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroStatusFilter">
<option value="">All Status</option>
<option value="DRAFT">Draft</option>
<option value="APPROVED">Approved</option>
<option value="FILED">Filed</option>
</select>
<button class="btn btn-outline-secondary btn-sm" id="refreshQdrosBtn">
<i class="bi bi-arrow-clockwise"></i>
</button>
<button class="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-700" id="refreshQdrosBtn"><i class="fa-solid fa-rotate-right"></i></button>
</div>
</div>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" id="qdrosTable">
<div class="p-6">
<div class="overflow-x-auto">
<table class="w-full text-sm text-left border border-neutral-200 dark:border-neutral-700 rounded-lg overflow-hidden" id="qdrosTable">
<thead>
<tr>
<th>File #</th>
@@ -138,46 +131,39 @@
</div>
</div>
<!-- Generated Documents Tab -->
<div class="tab-pane fade" id="generated" role="tabpanel" aria-labelledby="generated-tab">
<div class="card mt-3">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-file-pdf"></i> Generated Documents</h5>
</div>
<div class="card-body">
<div id="generatedDocuments">
<p class="text-muted">Generated documents will appear here...</p>
</div>
</div>
<div id="generated" class="tabcontent p-6 hidden">
<div class="mt-3 bg-white dark:bg-neutral-800 rounded-xl shadow-soft border border-neutral-200 dark:border-neutral-700">
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700"><h5 class="mb-0 font-semibold"><i class="fa-regular fa-file-pdf"></i> Generated Documents</h5></div>
<div class="p-6"><div id="generatedDocuments"><p class="text-neutral-500">Generated documents will appear here...</p></div></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Template Editor Modal -->
<div class="modal fade" id="templateModal" tabindex="-1" aria-labelledby="templateModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="templateModalLabel">Template Editor</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="templateModal">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-5xl w-full max-h-[90vh] overflow-y-auto">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold" id="templateModalLabel">Template Editor</h5>
<button type="button" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300" onclick="closeModal('templateModal')"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="modal-body">
<div class="px-6 py-4">
<form id="templateForm">
<div class="row g-3">
<div class="col-md-6">
<label for="templateId" class="form-label">Template ID *</label>
<input type="text" class="form-control" id="templateId" name="form_id" required>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="md:col-span-1">
<label for="templateId" class="block text-sm font-medium mb-1">Template ID *</label>
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="templateId" name="form_id" required>
</div>
<div class="col-md-6">
<label for="templateName" class="form-label">Template Name *</label>
<input type="text" class="form-control" id="templateName" name="form_name" required>
<div class="md:col-span-1">
<label for="templateName" class="block text-sm font-medium mb-1">Template Name *</label>
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="templateName" name="form_name" required>
</div>
<div class="col-md-12">
<label for="templateCategory" class="form-label">Category</label>
<select class="form-select" id="templateCategory" name="category">
<div class="md:col-span-2">
<label for="templateCategory" class="block text-sm font-medium mb-1">Category</label>
<select class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="templateCategory" name="category">
<option value="GENERAL">General</option>
<option value="LETTERS">Letters</option>
<option value="CONTRACTS">Contracts</option>
@@ -186,247 +172,227 @@
<option value="NOTICES">Notices</option>
</select>
</div>
<div class="col-12">
<label for="templateContent" class="form-label">Template Content</label>
<div class="md:col-span-2">
<label for="templateContent" class="block text-sm font-medium mb-1">Template Content</label>
<div class="mb-2">
<small class="text-muted">
<small class="text-neutral-500">
Use {{VARIABLE_NAME}} or ^VARIABLE for merge fields. Available variables: FILE_NO, CLIENT_FIRST, CLIENT_LAST, CLIENT_FULL, MATTER, OPENED, ATTORNEY, TODAY
</small>
</div>
<textarea class="form-control" id="templateContent" name="content" rows="15"
<textarea class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="templateContent" name="content" rows="15"
placeholder="Enter your template content here. Use {{CLIENT_FULL}} for client name, {{FILE_NO}} for file number, etc."></textarea>
</div>
<div class="col-12">
<div class="row">
<div class="col-md-6">
<button type="button" class="btn btn-outline-secondary btn-sm" id="insertVariableBtn">
<i class="bi bi-plus"></i> Insert Variable
<div class="md:col-span-2">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<button type="button" class="px-3 py-1.5 border border-neutral-300 dark:border-neutral-600 rounded text-sm hover:bg-neutral-100 dark:hover:bg-neutral-700" id="insertVariableBtn">
<i class="fa-solid fa-plus"></i> Insert Variable
</button>
<button type="button" class="btn btn-outline-info btn-sm" id="previewTemplateBtn">
<i class="bi bi-eye"></i> Preview
<button type="button" class="px-3 py-1.5 border border-info-600 text-info-700 dark:text-info-300 rounded text-sm hover:bg-info-50 dark:hover:bg-info-900/20" id="previewTemplateBtn">
<i class="fa-regular fa-eye"></i> Preview
</button>
</div>
<div class="col-md-6 text-end">
<div id="variableCount" class="text-muted small">Variables detected: 0</div>
<div class="text-right">
<div id="variableCount" class="text-neutral-500 text-sm">Variables detected: 0</div>
</div>
</div>
</div>
</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="saveTemplateBtn">
<i class="bi bi-check-circle"></i> Save Template
</button>
</div>
<div class="flex items-center justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<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" onclick="closeModal('templateModal')">Cancel</button>
<button type="button" class="px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg" id="saveTemplateBtn"><i class="fa-regular fa-circle-check"></i> Save Template</button>
</div>
</div>
</div>
<!-- Document Generation Modal -->
<div class="modal fade" id="generateModal" tabindex="-1" aria-labelledby="generateModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="generateModalLabel">Generate Document</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="generateModal">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold" id="generateModalLabel">Generate Document</h5>
<button type="button" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300" onclick="closeModal('generateModal')"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="modal-body">
<div class="px-6 py-4">
<form id="generateForm">
<div class="row g-3">
<div class="col-md-6">
<label for="generateTemplate" class="form-label">Select Template *</label>
<select class="form-select" id="generateTemplate" name="template_id" required>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label for="generateTemplate" class="block text-sm font-medium mb-1">Select Template *</label>
<select class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="generateTemplate" name="template_id" required>
<option value="">Choose template...</option>
</select>
</div>
<div class="col-md-6">
<label for="generateFile" class="form-label">File Number *</label>
<div class="input-group">
<input type="text" class="form-control" id="generateFile" name="file_no" required>
<button class="btn btn-outline-secondary" type="button" id="selectGenerateFileBtn">
<i class="bi bi-search"></i>
<div>
<label for="generateFile" class="block text-sm font-medium mb-1">File Number *</label>
<div class="flex gap-2">
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="generateFile" name="file_no" required>
<button class="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-700" type="button" id="selectGenerateFileBtn">
<i class="fa-solid fa-magnifying-glass"></i>
</button>
</div>
<div class="form-text" id="generateFileInfo">Enter file number or browse to select</div>
<div class="text-sm text-neutral-500" id="generateFileInfo">Enter file number or browse to select</div>
</div>
<div class="col-md-6">
<label for="outputFormat" class="form-label">Output Format</label>
<select class="form-select" id="outputFormat" name="output_format">
<div>
<label for="outputFormat" class="block text-sm font-medium mb-1">Output Format</label>
<select class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="outputFormat" name="output_format">
<option value="PDF">PDF</option>
<option value="DOCX">Word Document</option>
<option value="HTML">HTML</option>
</select>
</div>
<div class="col-md-6">
<div class="form-check mt-4">
<input class="form-check-input" type="checkbox" id="useCustomVars">
<label class="form-check-label" for="useCustomVars">
<div>
<div class="mt-6 flex items-center gap-2">
<input class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-neutral-300 rounded" type="checkbox" id="useCustomVars">
<label class="text-sm" for="useCustomVars">
Use custom variables
</label>
</div>
</div>
<div class="col-12" id="customVariablesSection" style="display: none;">
<label class="form-label">Custom Variables</label>
<div class="md:col-span-2" id="customVariablesSection" style="display: none;">
<label class="block text-sm font-medium mb-1">Custom Variables</label>
<div id="customVariables">
<!-- Custom variables will be added here -->
</div>
<button type="button" class="btn btn-sm btn-outline-secondary" id="addVariableBtn">
<i class="bi bi-plus"></i> Add Variable
<button type="button" class="mt-2 px-3 py-1.5 border border-neutral-300 dark:border-neutral-600 rounded text-sm hover:bg-neutral-100 dark:hover:bg-neutral-700" id="addVariableBtn">
<i class="fa-solid fa-plus"></i> Add Variable
</button>
</div>
<div class="col-12">
<label for="templatePreview" class="form-label">Template Preview</label>
<textarea class="form-control" id="templatePreview" readonly rows="8" placeholder="Select a template to see preview..."></textarea>
<div class="md:col-span-2">
<label for="templatePreview" class="block text-sm font-medium mb-1">Template Preview</label>
<textarea class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="templatePreview" readonly rows="8" placeholder="Select a template to see preview..."></textarea>
</div>
</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="generateDocumentBtn">
<i class="bi bi-file-plus"></i> Generate Document
</button>
</div>
<div class="flex items-center justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<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" onclick="closeModal('generateModal')">Cancel</button>
<button type="button" class="px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg" id="generateDocumentBtn"><i class="fa-regular fa-file-lines"></i> Generate Document</button>
</div>
</div>
</div>
<!-- QDRO Modal -->
<div class="modal fade" id="qdroModal" tabindex="-1" aria-labelledby="qdroModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="qdroModalLabel">QDRO Editor</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="qdroModal">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-6xl w-full">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold" id="qdroModalLabel">QDRO Editor</h5>
<button type="button" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300" onclick="closeModal('qdroModal')"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="modal-body">
<div class="px-6 py-4">
<form id="qdroForm">
<div class="row g-3">
<div class="col-md-6">
<label for="qdroFileNo" class="form-label">File Number *</label>
<div class="input-group">
<input type="text" class="form-control" id="qdroFileNo" name="file_no" required>
<button class="btn btn-outline-secondary" type="button" id="selectQdroFileBtn">
<i class="bi bi-search"></i>
</button>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="qdroFileNo" class="block text-sm font-medium mb-1">File Number *</label>
<div class="flex gap-2">
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroFileNo" name="file_no" required>
<button class="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-700" type="button" id="selectQdroFileBtn"><i class="fa-solid fa-magnifying-glass"></i></button>
</div>
</div>
<div class="col-md-3">
<label for="qdroVersion" class="form-label">Version</label>
<input type="text" class="form-control" id="qdroVersion" name="version" value="01">
<div>
<label for="qdroVersion" class="block text-sm font-medium mb-1">Version</label>
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroVersion" name="version" value="01">
</div>
<div class="col-md-3">
<label for="qdroStatus" class="form-label">Status</label>
<select class="form-select" id="qdroStatus" name="status">
<div>
<label for="qdroStatus" class="block text-sm font-medium mb-1">Status</label>
<select class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroStatus" name="status">
<option value="DRAFT">Draft</option>
<option value="APPROVED">Approved</option>
<option value="FILED">Filed</option>
</select>
</div>
<div class="col-md-6">
<label for="qdroParticipant" class="form-label">Participant Name</label>
<input type="text" class="form-control" id="qdroParticipant" name="participant_name">
<div>
<label for="qdroParticipant" class="block text-sm font-medium mb-1">Participant Name</label>
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroParticipant" name="participant_name">
</div>
<div class="col-md-6">
<label for="qdroSpouse" class="form-label">Spouse Name</label>
<input type="text" class="form-control" id="qdroSpouse" name="spouse_name">
<div>
<label for="qdroSpouse" class="block text-sm font-medium mb-1">Spouse Name</label>
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroSpouse" name="spouse_name">
</div>
<div class="col-md-6">
<label for="qdroPlanName" class="form-label">Plan Name</label>
<input type="text" class="form-control" id="qdroPlanName" name="plan_name">
<div>
<label for="qdroPlanName" class="block text-sm font-medium mb-1">Plan Name</label>
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroPlanName" name="plan_name">
</div>
<div class="col-md-6">
<label for="qdroPlanAdmin" class="form-label">Plan Administrator</label>
<input type="text" class="form-control" id="qdroPlanAdmin" name="plan_administrator">
<div>
<label for="qdroPlanAdmin" class="block text-sm font-medium mb-1">Plan Administrator</label>
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroPlanAdmin" name="plan_administrator">
</div>
<div class="col-md-4">
<label for="qdroCreated" class="form-label">Created Date</label>
<input type="date" class="form-control" id="qdroCreated" name="created_date">
<div>
<label for="qdroCreated" class="block text-sm font-medium mb-1">Created Date</label>
<input type="date" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroCreated" name="created_date">
</div>
<div class="col-md-4">
<label for="qdroApproved" class="form-label">Approved Date</label>
<input type="date" class="form-control" id="qdroApproved" name="approved_date">
<div>
<label for="qdroApproved" class="block text-sm font-medium mb-1">Approved Date</label>
<input type="date" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroApproved" name="approved_date">
</div>
<div class="col-md-4">
<label for="qdroFiled" class="form-label">Filed Date</label>
<input type="date" class="form-control" id="qdroFiled" name="filed_date">
<div>
<label for="qdroFiled" class="block text-sm font-medium mb-1">Filed Date</label>
<input type="date" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroFiled" name="filed_date">
</div>
<div class="col-12">
<label for="qdroTitle" class="form-label">QDRO Title</label>
<input type="text" class="form-control" id="qdroTitle" name="title" placeholder="Enter QDRO title">
<div class="md:col-span-2">
<label for="qdroTitle" class="block text-sm font-medium mb-1">QDRO Title</label>
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroTitle" name="title" placeholder="Enter QDRO title">
</div>
<div class="col-12">
<label for="qdroContent" class="form-label">QDRO Content</label>
<textarea class="form-control" id="qdroContent" name="content" rows="10" placeholder="Enter QDRO content or generate from template..."></textarea>
<div class="md:col-span-2">
<label for="qdroContent" class="block text-sm font-medium mb-1">QDRO Content</label>
<textarea class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroContent" name="content" rows="10" placeholder="Enter QDRO content or generate from template..."></textarea>
</div>
<div class="col-12">
<label for="qdroNotes" class="form-label">Notes</label>
<textarea class="form-control" id="qdroNotes" name="notes" rows="3" placeholder="Additional notes..."></textarea>
<div class="md:col-span-2">
<label for="qdroNotes" class="block text-sm font-medium mb-1">Notes</label>
<textarea class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroNotes" name="notes" rows="3" placeholder="Additional notes..."></textarea>
</div>
</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-warning" id="generateFromTemplateBtn">
<i class="bi bi-file-text"></i> Generate from Template
</button>
<button type="button" class="btn btn-primary" id="saveQdroBtn">
<i class="bi bi-check-circle"></i> Save QDRO
</button>
</div>
<div class="flex items-center justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<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" onclick="closeModal('qdroModal')">Cancel</button>
<button type="button" class="px-4 py-2 bg-warning-600 text-white hover:bg-warning-700 rounded-lg" id="generateFromTemplateBtn"><i class="fa-regular fa-file-lines"></i> Generate from Template</button>
<button type="button" class="px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg" id="saveQdroBtn"><i class="fa-regular fa-circle-check"></i> Save QDRO</button>
</div>
</div>
</div>
<!-- Statistics Modal -->
<div class="modal fade" id="statsModal" tabindex="-1" aria-labelledby="statsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="statsModalLabel">Document Statistics</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="statsModal">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-3xl w-full max-h-[85vh] overflow-y-auto">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold" id="statsModalLabel">Document Statistics</h5>
<button type="button" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300" onclick="closeModal('statsModal')"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<div class="card bg-primary text-white">
<div class="card-body text-center">
<h3 id="totalTemplatesCount">0</h3>
<small>Total Templates</small>
<div class="px-6 py-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<div class="rounded-lg shadow-soft bg-primary-600 text-white p-4 text-center">
<div class="text-3xl font-semibold" id="totalTemplatesCount">0</div>
<div class="text-sm opacity-90">Total Templates</div>
</div>
</div>
<div>
<div class="rounded-lg shadow-soft bg-success-600 text-white p-4 text-center">
<div class="text-3xl font-semibold" id="totalQdrosCount">0</div>
<div class="text-sm opacity-90">Total QDROs</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card bg-success text-white">
<div class="card-body text-center">
<h3 id="totalQdrosCount">0</h3>
<small>Total QDROs</small>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
<div>
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-soft border border-neutral-200 dark:border-neutral-700">
<div class="px-4 py-2 border-b border-neutral-200 dark:border-neutral-700">
<h6 class="font-semibold">Templates by Category</h6>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6>Templates by Category</h6>
</div>
<div class="card-body">
<div class="p-4">
<div id="categoriesBreakdown">
<!-- Categories will be loaded here -->
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6>Recent Activity</h6>
<div>
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-soft border border-neutral-200 dark:border-neutral-700">
<div class="px-4 py-2 border-b border-neutral-200 dark:border-neutral-700">
<h6 class="font-semibold">Recent Activity</h6>
</div>
<div class="card-body">
<div class="p-4">
<div id="recentActivity">
<!-- Recent activity will be loaded here -->
</div>
@@ -435,52 +401,33 @@
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
<div class="flex items-center justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<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" onclick="closeModal('statsModal')">Close</button>
</div>
</div>
</div>
<!-- Variable Selector Modal -->
<div class="modal fade" id="variableModal" tabindex="-1" aria-labelledby="variableModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="variableModalLabel">Insert Variable</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="variableModal">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-lg w-full max-h-[80vh] overflow-y-auto">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold" id="variableModalLabel">Insert Variable</h5>
<button type="button" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300" onclick="closeModal('variableModal')"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="modal-body">
<div class="list-group">
<button type="button" class="list-group-item list-group-item-action variable-item" data-var="FILE_NO">
<strong>{{FILE_NO}}</strong> - File Number
</button>
<button type="button" class="list-group-item list-group-item-action variable-item" data-var="CLIENT_FULL">
<strong>{{CLIENT_FULL}}</strong> - Full Client Name
</button>
<button type="button" class="list-group-item list-group-item-action variable-item" data-var="CLIENT_FIRST">
<strong>{{CLIENT_FIRST}}</strong> - Client First Name
</button>
<button type="button" class="list-group-item list-group-item-action variable-item" data-var="CLIENT_LAST">
<strong>{{CLIENT_LAST}}</strong> - Client Last Name
</button>
<button type="button" class="list-group-item list-group-item-action variable-item" data-var="MATTER">
<strong>{{MATTER}}</strong> - Matter Description
</button>
<button type="button" class="list-group-item list-group-item-action variable-item" data-var="OPENED">
<strong>{{OPENED}}</strong> - Date File Opened
</button>
<button type="button" class="list-group-item list-group-item-action variable-item" data-var="ATTORNEY">
<strong>{{ATTORNEY}}</strong> - Attorney/Employee
</button>
<button type="button" class="list-group-item list-group-item-action variable-item" data-var="TODAY">
<strong>{{TODAY}}</strong> - Today's Date
</button>
<div class="px-6 py-4">
<div class="grid grid-cols-1 gap-2">
<button type="button" class="px-3 py-2 text-left border rounded hover:bg-neutral-50 dark:hover:bg-neutral-700 variable-item" data-var="FILE_NO"><strong>{{FILE_NO}}</strong> - File Number</button>
<button type="button" class="px-3 py-2 text-left border rounded hover:bg-neutral-50 dark:hover:bg-neutral-700 variable-item" data-var="CLIENT_FULL"><strong>{{CLIENT_FULL}}</strong> - Full Client Name</button>
<button type="button" class="px-3 py-2 text-left border rounded hover:bg-neutral-50 dark:hover:bg-neutral-700 variable-item" data-var="CLIENT_FIRST"><strong>{{CLIENT_FIRST}}</strong> - Client First Name</button>
<button type="button" class="px-3 py-2 text-left border rounded hover:bg-neutral-50 dark:hover:bg-neutral-700 variable-item" data-var="CLIENT_LAST"><strong>{{CLIENT_LAST}}</strong> - Client Last Name</button>
<button type="button" class="px-3 py-2 text-left border rounded hover:bg-neutral-50 dark:hover:bg-neutral-700 variable-item" data-var="MATTER"><strong>{{MATTER}}</strong> - Matter Description</button>
<button type="button" class="px-3 py-2 text-left border rounded hover:bg-neutral-50 dark:hover:bg-neutral-700 variable-item" data-var="OPENED"><strong>{{OPENED}}</strong> - Date File Opened</button>
<button type="button" class="px-3 py-2 text-left border rounded hover:bg-neutral-50 dark:hover:bg-neutral-700 variable-item" data-var="ATTORNEY"><strong>{{ATTORNEY}}</strong> - Attorney/Employee</button>
<button type="button" class="px-3 py-2 text-left border rounded hover:bg-neutral-50 dark:hover:bg-neutral-700 variable-item" data-var="TODAY"><strong>{{TODAY}}</strong> - Today's Date</button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</div>
<div class="flex items-center justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<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" onclick="closeModal('variableModal')">Cancel</button>
</div>
</div>
</div>
@@ -563,8 +510,7 @@ function setupEventHandlers() {
// Insert variable button
document.getElementById('insertVariableBtn').addEventListener('click', function() {
const modal = new bootstrap.Modal(document.getElementById('variableModal'));
modal.show();
openModal('variableModal');
});
// Variable selector
@@ -579,8 +525,7 @@ function setupEventHandlers() {
textarea.focus();
textarea.setSelectionRange(cursorPos + varName.length + 4, cursorPos + varName.length + 4);
const modal = bootstrap.Modal.getInstance(document.getElementById('variableModal'));
modal.hide();
closeModal('variableModal');
updateVariableCount();
});
@@ -658,29 +603,21 @@ function createTemplateRow(template) {
const variableCount = Object.keys(template.variables || {}).length;
row.innerHTML = `
<td><code>${template.form_id}</code></td>
<td>${template.form_name}</td>
<td><span class="badge bg-secondary">${template.category}</span></td>
<td><span class="badge bg-info">${variableCount} vars</span></td>
<td>
<span class="badge ${template.active ? 'bg-success' : 'bg-warning'}">
<td class="px-4 py-2"><code>${template.form_id}</code></td>
<td class="px-4 py-2">${template.form_name}</td>
<td class="px-4 py-2"><span class="inline-block px-2 py-0.5 text-xs rounded bg-neutral-100 text-neutral-700 border border-neutral-300">${template.category}</span></td>
<td class="px-4 py-2"><span class="inline-block px-2 py-0.5 text-xs rounded bg-blue-100 text-blue-700 border border-blue-400">${variableCount} vars</span></td>
<td class="px-4 py-2">
<span class="inline-block px-2 py-0.5 text-xs rounded ${template.active ? 'bg-green-100 text-green-700 border border-green-400' : 'bg-yellow-100 text-yellow-700 border border-yellow-500'}">
${template.active ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="editTemplate('${template.form_id}')" title="Edit">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-outline-success" onclick="previewTemplate('${template.form_id}')" title="Preview">
<i class="bi bi-eye"></i>
</button>
<button class="btn btn-outline-info" onclick="generateFromTemplate('${template.form_id}')" title="Generate">
<i class="bi bi-file-plus"></i>
</button>
<button class="btn btn-outline-danger" onclick="deleteTemplate('${template.form_id}')" title="Delete">
<i class="bi bi-trash"></i>
</button>
<td class="px-4 py-2">
<div class="flex items-center gap-2">
<button class="px-2 py-1 border border-primary-600 text-primary-600 rounded hover:bg-blue-100" onclick="editTemplate('${template.form_id}')" title="Edit"><i class="fa-solid fa-pencil"></i></button>
<button class="px-2 py-1 border border-success-600 text-success-600 rounded hover:bg-green-100" onclick="previewTemplate('${template.form_id}')" title="Preview"><i class="fa-regular fa-eye"></i></button>
<button class="px-2 py-1 border border-info-600 text-info-600 rounded hover:bg-blue-100" onclick="generateFromTemplate('${template.form_id}')" title="Generate"><i class="fa-regular fa-file-lines"></i></button>
<button class="px-2 py-1 border border-danger-600 text-danger-600 rounded hover:bg-red-100" onclick="deleteTemplate('${template.form_id}')" title="Delete"><i class="fa-solid fa-trash"></i></button>
</div>
</td>
`;
@@ -728,22 +665,16 @@ function createQdroRow(qdro) {
<td>${qdro.spouse_name || ''}</td>
<td>${qdro.plan_name || ''}</td>
<td>
<span class="badge ${getStatusBadgeClass(qdro.status)}">
<span class="${getStatusBadgeClass(qdro.status)}">
${qdro.status}
</span>
</td>
<td>${qdro.created_date ? new Date(qdro.created_date).toLocaleDateString() : ''}</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="editQdro(${qdro.id})" title="Edit">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-outline-info" onclick="viewQdro(${qdro.id})" title="View">
<i class="bi bi-eye"></i>
</button>
<button class="btn btn-outline-danger" onclick="deleteQdro(${qdro.id})" title="Delete">
<i class="bi bi-trash"></i>
</button>
<div class="flex items-center gap-2">
<button class="px-2 py-1 border border-blue-600 text-blue-600 rounded hover:bg-blue-100" onclick="editQdro(${qdro.id})" title="Edit"><i class="fa-solid fa-pencil"></i></button>
<button class="px-2 py-1 border border-cyan-600 text-cyan-600 rounded hover:bg-blue-100" onclick="viewQdro(${qdro.id})" title="View"><i class="fa-regular fa-eye"></i></button>
<button class="px-2 py-1 border border-red-600 text-red-600 rounded hover:bg-red-100" onclick="deleteQdro(${qdro.id})" title="Delete"><i class="fa-solid fa-trash"></i></button>
</div>
</td>
`;
@@ -753,10 +684,10 @@ function createQdroRow(qdro) {
function getStatusBadgeClass(status) {
switch (status) {
case 'DRAFT': return 'bg-warning';
case 'APPROVED': return 'bg-success';
case 'FILED': return 'bg-primary';
default: return 'bg-secondary';
case 'DRAFT': return 'inline-block px-2 py-0.5 text-xs rounded bg-yellow-100 text-yellow-700 border border-yellow-500';
case 'APPROVED': return 'inline-block px-2 py-0.5 text-xs rounded bg-green-100 text-green-700 border border-green-400';
case 'FILED': return 'inline-block px-2 py-0.5 text-xs rounded bg-blue-100 text-blue-700 border border-blue-400';
default: return 'inline-block px-2 py-0.5 text-xs rounded bg-neutral-100 text-neutral-700 border border-neutral-300';
}
}
@@ -784,7 +715,7 @@ async function loadCategories() {
}
function openTemplateModal(templateId = null) {
const modal = new bootstrap.Modal(document.getElementById('templateModal'));
const form = document.getElementById('templateForm');
// Reset form
@@ -799,7 +730,7 @@ function openTemplateModal(templateId = null) {
document.getElementById('templateId').value = 'TPL_' + Date.now();
}
modal.show();
openModal('templateModal');
updateVariableCount();
}
@@ -853,8 +784,7 @@ async function saveTemplate() {
}
showAlert('Template saved successfully', 'success');
const modal = bootstrap.Modal.getInstance(document.getElementById('templateModal'));
modal.hide();
closeModal('templateModal');
loadTemplates();
} catch (error) {
console.error('Error saving template:', error);
@@ -898,8 +828,8 @@ async function loadDocumentStats() {
Object.entries(stats.templates_by_category).forEach(([category, count]) => {
const div = document.createElement('div');
div.className = 'd-flex justify-content-between mb-1';
div.innerHTML = `<span>${category}</span><span class="badge bg-secondary">${count}</span>`;
div.className = 'flex items-center justify-between mb-1';
div.innerHTML = `<span>${category}</span><span class="inline-block px-2 py-0.5 text-xs rounded bg-neutral-200 text-neutral-700">${count}</span>`;
categoriesDiv.appendChild(div);
});
@@ -908,22 +838,21 @@ async function loadDocumentStats() {
activityDiv.innerHTML = '';
if (stats.recent_activity.length === 0) {
activityDiv.innerHTML = '<p class="text-muted">No recent activity</p>';
activityDiv.innerHTML = '<p class="text-neutral-500">No recent activity</p>';
} else {
stats.recent_activity.forEach(activity => {
const div = document.createElement('div');
div.className = 'mb-2 p-2 border rounded';
div.innerHTML = `
<small class="text-muted">${activity.type}</small><br>
<small class="text-neutral-500">${activity.type}</small><br>
<strong>File: ${activity.file_no}</strong><br>
<span class="badge ${getStatusBadgeClass(activity.status)}">${activity.status}</span>
<span class="${getStatusBadgeClass(activity.status)}">${activity.status}</span>
`;
activityDiv.appendChild(div);
});
}
const modal = new bootstrap.Modal(document.getElementById('statsModal'));
modal.show();
openModal('statsModal');
} catch (error) {
console.error('Error loading statistics:', error);
showAlert('Error loading statistics: ' + error.message, 'danger');
@@ -944,22 +873,13 @@ function debounce(func, wait) {
}
function showAlert(message, type = 'info') {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed top-0 end-0 m-3`;
alertDiv.style.zIndex = '9999';
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();
if (window.alerts && typeof window.alerts.show === 'function') {
window.alerts.show(message, type);
} else if (window.showNotification) {
window.showNotification(message, type);
} else {
alert(String(message));
}
}, 5000);
}
// Placeholder functions for additional features
@@ -995,9 +915,8 @@ async function deleteTemplate(templateId) {
}
function openGenerateModal() {
const modal = new bootstrap.Modal(document.getElementById('generateModal'));
loadTemplatesForGeneration();
modal.show();
openModal('generateModal');
}
async function loadTemplatesForGeneration() {
@@ -1078,8 +997,7 @@ async function generateDocument() {
const result = await response.json();
showAlert(`Document generated successfully: ${result.file_name}`, 'success');
const modal = bootstrap.Modal.getInstance(document.getElementById('generateModal'));
modal.hide();
closeModal('generateModal');
// Optionally trigger download
if (confirm('Document generated successfully. Download now?')) {
@@ -1094,17 +1012,17 @@ async function generateDocument() {
function addCustomVariableInput() {
const container = document.getElementById('customVariables');
const div = document.createElement('div');
div.className = 'row g-2 mb-2 custom-var-input';
div.className = 'grid grid-cols-12 gap-2 mb-2 custom-var-input';
div.innerHTML = `
<div class="col-md-5">
<input type="text" class="form-control form-control-sm var-name" placeholder="Variable name">
<div class="col-span-12 md:col-span-5">
<input type="text" class="w-full px-3 py-1.5 border border-neutral-300 dark:border-neutral-600 rounded var-name" placeholder="Variable name">
</div>
<div class="col-md-5">
<input type="text" class="form-control form-control-sm var-value" placeholder="Variable value">
<div class="col-span-12 md:col-span-5">
<input type="text" class="w-full px-3 py-1.5 border border-neutral-300 dark:border-neutral-600 rounded var-value" placeholder="Variable value">
</div>
<div class="col-md-2">
<button type="button" class="btn btn-sm btn-outline-danger" onclick="this.parentNode.parentNode.remove()">
<i class="bi bi-x"></i>
<div class="col-span-12 md:col-span-2 flex items-center">
<button type="button" class="px-2 py-1 border border-red-600 text-red-600 rounded hover:bg-red-100 text-sm" onclick="this.closest('.custom-var-input').remove()">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
`;
@@ -1112,7 +1030,6 @@ function addCustomVariableInput() {
}
function openQdroModal(qdroId = null) {
const modal = new bootstrap.Modal(document.getElementById('qdroModal'));
const form = document.getElementById('qdroForm');
form.reset();
@@ -1122,7 +1039,7 @@ function openQdroModal(qdroId = null) {
document.getElementById('qdroCreated').value = new Date().toISOString().split('T')[0];
}
modal.show();
openModal('qdroModal');
}
async function saveQdro() {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,151 +3,169 @@
{% block title %}Data Import - Delphi Database{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2><i class="bi bi-upload"></i> Data Import</h2>
<div>
<button class="btn btn-info" id="refreshStatusBtn">
<i class="bi bi-arrow-clockwise"></i> Refresh Status
<div class="space-y-6">
<!-- Page Header -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div class="flex items-center gap-3">
<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="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Current Database Status</h5>
<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="card-body">
<div class="p-6">
<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 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="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-file-earmark-arrow-up"></i> Upload CSV Files</h5>
<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="card-body">
<div class="p-6">
<form id="importForm" enctype="multipart/form-data">
<div class="row g-3">
<div class="col-md-4">
<label for="fileType" class="form-label">Data Type *</label>
<select class="form-select" id="fileType" name="fileType" required>
<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="form-text" id="fileTypeDescription"></div>
<div class="text-sm text-neutral-500 dark:text-neutral-400 mt-1" 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 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="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
<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>
<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
<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="btn btn-primary" id="importBtn">
<i class="bi bi-upload"></i> Import Data
<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>
</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 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="card-body" id="validationResults">
<div class="p-6" 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 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="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 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">Ready to import...</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="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 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="card-body" id="importResults">
<div class="p-6" 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 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="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">
<div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h6 class="text-base font-semibold mb-2">Clear Table Data</h6>
<p class="text-sm text-neutral-500 dark:text-neutral-400 mb-3">Remove all records from a specific table (cannot be undone)</p>
<div class="flex gap-3">
<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">
<option value="">Select table to clear...</option>
</select>
<button class="btn btn-danger" id="clearTableBtn">
<i class="bi bi-trash"></i> Clear Table
<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">
<i class="fa-solid fa-trash"></i>
<span>Clear Table</span>
</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
<div>
<h6 class="text-base font-semibold mb-2">Quick Actions</h6>
<div class="space-y-3">
<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">
<i class="fa-solid fa-download"></i>
<span>Download Backup</span>
</button>
<button class="btn btn-outline-info" id="viewLogsBtn">
<i class="bi bi-journal-text"></i> View Import Logs
<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">
<i class="fa-regular fa-file-lines"></i>
<span>View Import Logs</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
@@ -250,50 +268,46 @@ async function loadImportStatus() {
} catch (error) {
console.error('Error loading import status:', error);
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) {
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;
Object.entries(status).forEach(([fileType, info], index) => {
Object.entries(status).forEach(([fileType, info]) => {
totalRecords += info.record_count || 0;
const statusClass = info.error ? 'danger' : (info.record_count > 0 ? 'success' : 'secondary');
const statusIcon = info.error ? 'exclamation-triangle' : (info.record_count > 0 ? 'check-circle' : 'circle');
if (index % 3 === 0 && index > 0) {
html += '</div><div class="row mt-2">';
}
const statusClass = info.error ? 'danger' : (info.record_count > 0 ? 'success' : 'neutral');
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' :
'bg-neutral-100 dark:bg-neutral-900/30 border-neutral-200 dark:border-neutral-800');
const statusIcon = info.error ? 'triangle-exclamation text-danger-600' :
(info.record_count > 0 ? 'circle-check text-success-600' : 'circle text-neutral-600');
html += `
<div class="col-md-4">
<div class="card border-${statusClass}">
<div class="card-body p-2">
<div class="d-flex justify-content-between align-items-center">
<div class="bg-white dark:bg-neutral-800 border ${statusBg} rounded-lg p-4">
<div class="flex justify-between items-start">
<div>
<small class="fw-bold">${fileType}</small><br>
<small class="text-muted">${info.table_name}</small>
<h6 class="font-semibold text-sm text-neutral-900 dark:text-neutral-100">${fileType}</h6>
<p class="text-xs text-neutral-600 dark:text-neutral-400">${info.table_name}</p>
</div>
<div class="text-end">
<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 class="text-right">
<i class="fa-solid fa-${statusIcon} text-lg"></i>
<p class="font-bold text-sm text-neutral-900 dark:text-neutral-100 mt-1">${info.record_count || 0}</p>
</div>
</div>
${info.error ? `<p class="text-xs text-danger-600 dark:text-danger-400 mt-2">${info.error}</p>` : ''}
</div>
`;
});
html += '</div>';
html += `<div class="mt-3 text-center">
<strong>Total Records: ${totalRecords.toLocaleString()}</strong>
html += `<div class="mt-4 text-center">
<span class="font-medium text-neutral-900 dark:text-neutral-100">Total Records: ${totalRecords.toLocaleString()}</span>
</div>`;
container.innerHTML = html;
@@ -310,7 +324,7 @@ async function validateFile() {
const fileInput = document.getElementById('csvFile');
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;
}
@@ -350,46 +364,45 @@ function displayValidationResults(result) {
// Overall status
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 += `
<div class="alert alert-${statusClass}">
<i class="bi bi-${statusIcon}"></i>
File validation ${result.valid ? 'passed' : 'failed'}
<div class="p-4 bg-${statusClass}-100 dark:bg-${statusClass}-900/30 rounded-lg mb-4">
<i class="fa-solid fa-${statusIcon} mr-2"></i>
<span class="font-medium">File validation ${result.valid ? 'passed' : 'failed'}</span>
</div>
`;
// 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) {
html += `<div class="alert alert-warning">
<strong>Missing columns:</strong> ${result.headers.missing.join(', ')}
html += `<div class="p-3 bg-warning-100 dark:bg-warning-900/30 rounded-lg mb-2">
<strong class="text-warning-700 dark:text-warning-300">Missing columns:</strong> ${result.headers.missing.join(', ')}
</div>`;
}
if (result.headers.extra.length > 0) {
html += `<div class="alert alert-info">
<strong>Extra columns:</strong> ${result.headers.extra.join(', ')}
html += `<div class="p-3 bg-info-100 dark:bg-info-900/30 rounded-lg mb-2">
<strong class="text-info-700 dark:text-info-300">Extra columns:</strong> ${result.headers.extra.join(', ')}
</div>`;
}
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
if (result.sample_data && result.sample_data.length > 0) {
html += '<h6>Sample Data (First 10 rows)</h6>';
html += '<div class="table-responsive">';
html += '<table class="table table-sm table-striped">';
html += '<thead><tr>';
html += '<h6 class="text-sm font-semibold mb-2 mt-4">Sample Data (First 10 rows)</h6>';
html += '<div class="overflow-x-auto"><table class="w-full text-sm text-neutral-900 dark:text-neutral-100">';
html += '<thead><tr class="bg-neutral-100 dark:bg-neutral-700">';
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 => {
html += '<tr>';
Object.values(row).forEach(value => {
html += `<td class="small">${value || ''}</td>`;
html += `<td class="px-3 py-2">${value || ''}</td>`;
});
html += '</tr>';
});
@@ -398,19 +411,19 @@ function displayValidationResults(result) {
// Validation errors
if (result.validation_errors && result.validation_errors.length > 0) {
html += '<h6>Data Issues Found</h6>';
html += '<div class="alert alert-warning">';
html += '<h6 class="text-sm font-semibold mb-2 mt-4">Data Issues Found</h6>';
html += '<div class="p-3 bg-warning-100 dark:bg-warning-900/30 rounded-lg">';
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) {
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>';
}
container.innerHTML = html;
panel.style.display = 'block';
panel.classList.remove('hidden');
}
async function handleImport(event) {
@@ -426,7 +439,7 @@ async function handleImport(event) {
const replaceExisting = document.getElementById('replaceExisting').checked;
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;
}
@@ -475,30 +488,30 @@ function displayImportResults(result) {
const successClass = result.errors && result.errors.length > 0 ? 'warning' : 'success';
let html = `
<div class="alert alert-${successClass}">
<h6><i class="bi bi-check-circle"></i> Import Completed</h6>
<p class="mb-0">
<strong>File Type:</strong> ${result.file_type}<br>
<strong>Records Imported:</strong> ${result.imported_count}<br>
<strong>Errors:</strong> ${result.total_errors || 0}
</p>
<div class="p-4 bg-${successClass}-100 dark:bg-${successClass}-900/30 rounded-lg mb-4">
<h6 class="font-semibold flex items-center gap-2"><i class="fa-regular fa-circle-check"></i> Import Completed</h6>
<div class="text-sm mt-2 space-y-1">
<p><strong>File Type:</strong> ${result.file_type}</p>
<p><strong>Records Imported:</strong> ${result.imported_count}</p>
<p><strong>Errors:</strong> ${result.total_errors || 0}</p>
</div>
</div>
`;
if (result.errors && result.errors.length > 0) {
html += '<h6>Import Errors</h6>';
html += '<div class="alert alert-danger">';
html += '<h6 class="text-sm font-semibold mb-2">Import Errors</h6>';
html += '<div class="p-3 bg-danger-100 dark:bg-danger-900/30 rounded-lg">';
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) {
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>';
}
container.innerHTML = html;
panel.style.display = 'block';
panel.classList.remove('hidden');
}
function showProgress(show, message = '') {
@@ -509,10 +522,9 @@ function showProgress(show, message = '') {
if (show) {
status.textContent = message;
bar.style.width = '100%';
bar.textContent = 'Processing...';
panel.style.display = 'block';
panel.classList.remove('hidden');
} else {
panel.style.display = 'none';
panel.classList.add('hidden');
}
}
@@ -560,25 +572,13 @@ function viewLogs() {
}
function showAlert(message, type = 'info') {
// Create and show Bootstrap alert
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
alertDiv.style.top = '20px';
alertDiv.style.right = '20px';
alertDiv.style.zIndex = '9999';
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();
if (window.alerts && typeof window.alerts.show === 'function') {
window.alerts.show(message, type);
} else if (window.showNotification) {
window.showNotification(message, type);
} else {
alert(String(message));
}
}, 5000);
}
</script>
{% endblock %}

View File

@@ -4,77 +4,55 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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>
<body class="login-page">
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<div class="card login-card shadow-sm mt-5">
<div class="card-body p-5">
<div class="text-center mb-4">
<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 class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 px-4">
<div class="max-w-md w-full space-y-8 bg-white dark:bg-neutral-800 p-8 rounded-xl shadow-md">
<div class="text-center">
<img src="/static/images/delphi-logo.webp" alt="Delphi Consulting Group" class="mx-auto h-20 w-auto">
<h2 class="mt-6 text-xl font-normal text-gray-900 dark:text-white">Delphi Database System</h2>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Sign in to access the system</p>
</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>
<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
<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>
<div class="text-center mt-4">
<small class="text-muted">
<p class="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
Default credentials: admin / admin123
</small>
</p>
</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>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
@@ -102,19 +80,22 @@
// Validate form
if (!loginForm.checkValidity()) {
e.stopPropagation();
loginForm.classList.add('was-validated');
loginForm.reportValidity();
return;
}
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
console.log('Attempting login with username:', username);
// Show loading state
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;
try {
console.log('Sending request to /api/auth/login');
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
@@ -126,12 +107,22 @@
})
});
console.log('Response status:', response.status);
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Login failed');
let errorMessage = '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();
console.log('Login successful, token:', data.access_token);
// Store token
localStorage.setItem('auth_token', data.access_token);
@@ -181,30 +172,11 @@
}
function showAlert(message, type = 'info') {
// Remove existing alerts
const existingAlerts = document.querySelectorAll('.alert');
existingAlerts.forEach(alert => alert.remove());
// 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);
if (window.alerts && typeof window.alerts.show === 'function') {
window.alerts.show(message, type);
return;
}
alert(String(message));
}
</script>
</body>

View File

@@ -3,115 +3,103 @@
{% block title %}Advanced Search - Delphi Database{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2><i class="bi bi-search"></i> Advanced Search</h2>
<div>
<button class="btn btn-success" id="savedSearchBtn">
<i class="bi bi-bookmark-star"></i> Saved Searches
</button>
<button class="btn btn-info" id="searchHistoryBtn">
<i class="bi bi-clock-history"></i> Search History
</button>
<button class="btn btn-secondary" id="clearAllBtn">
<i class="bi bi-x-circle"></i> Clear All
</button>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div class="flex items-center justify-between mb-3">
<h2 class="text-2xl font-bold"><i class="fa-solid fa-magnifying-glass"></i> Advanced Search</h2>
<div class="flex gap-2">
<button class="px-4 py-2 bg-success-600 text-white rounded hover:bg-success-700" id="savedSearchBtn"><i class="fa-solid fa-bookmark"></i> Saved Searches</button>
<button class="px-4 py-2 bg-info-600 text-white rounded hover:bg-info-700" id="searchHistoryBtn"><i class="fa-solid fa-clock-rotate-left"></i> Search History</button>
<button class="px-4 py-2 bg-neutral-200 dark:bg-neutral-700 rounded hover:bg-neutral-300 dark:hover:bg-neutral-600" id="clearAllBtn"><i class="fa-solid fa-circle-xmark"></i> Clear All</button>
</div>
</div>
<div class="row">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<!-- Search Form Panel -->
<div class="col-lg-4">
<div class="card sticky-top" style="top: 20px;">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-funnel"></i> Search Criteria</h5>
<div class="lg:col-span-1">
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow border border-neutral-200 dark:border-neutral-700 sticky top-5">
<div class="px-4 py-3 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="mb-0 text-lg font-semibold"><i class="fa-solid fa-filter"></i> Search Criteria</h5>
</div>
<div class="card-body">
<div class="p-4">
<form id="advancedSearchForm">
<!-- Basic Search -->
<div class="mb-3">
<label for="searchQuery" class="form-label">Search Terms</label>
<div class="input-group">
<input type="text" class="form-control" id="searchQuery"
<div class="mb-4">
<label for="searchQuery" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Search Terms</label>
<div class="relative">
<input type="text" 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 placeholder-neutral-400 dark:placeholder-neutral-500 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200" id="searchQuery"
placeholder="Enter search terms..." autocomplete="off">
<button class="btn btn-outline-secondary" type="button" id="voiceSearchBtn" title="Voice Search">
<i class="bi bi-mic"></i>
<button class="absolute right-0 top-0 h-full px-3 text-neutral-400 hover:text-primary-600 dark:text-neutral-500 dark:hover:text-primary-400 transition-colors" type="button" id="voiceSearchBtn" title="Voice Search">
<i class="fa-solid fa-microphone"></i>
</button>
</div>
<div id="searchSuggestions" class="dropdown-menu w-100" style="max-height: 200px; overflow-y: auto;">
<div id="searchSuggestions" class="hidden absolute z-10 w-full bg-white dark:bg-neutral-800 rounded-lg shadow-lg border border-neutral-200 dark:border-neutral-700 max-h-48 overflow-y-auto">
<!-- Suggestions will appear here -->
</div>
</div>
<!-- Search Options -->
<div class="mb-3">
<label class="form-label">Search Options</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="exactPhrase">
<label class="form-check-label" for="exactPhrase">Exact phrase</label>
<div class="mb-4">
<label class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Search Options</label>
<div class="flex items-center mb-2">
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500" type="checkbox" id="exactPhrase">
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="exactPhrase">Exact phrase</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="caseSensitive">
<label class="form-check-label" for="caseSensitive">Case sensitive</label>
<div class="flex items-center mb-2">
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500" type="checkbox" id="caseSensitive">
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="caseSensitive">Case sensitive</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="wholeWords">
<label class="form-check-label" for="wholeWords">Whole words only</label>
<div class="flex items-center">
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500" type="checkbox" id="wholeWords">
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="wholeWords">Whole words only</label>
</div>
</div>
<!-- Search Types -->
<div class="mb-3">
<label class="form-label">Search In</label>
<div class="row">
<div class="col-6">
<div class="form-check">
<input class="form-check-input search-type" type="checkbox" id="searchCustomers" value="customer" checked>
<label class="form-check-label" for="searchCustomers">Customers</label>
<div class="mb-4">
<label class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Search In</label>
<div class="flex flex-wrap -mx-2">
<div class="w-1/2 px-2">
<div class="flex items-center mb-2">
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500 search-type" type="checkbox" id="searchCustomers" value="customer" checked>
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="searchCustomers">Customers</label>
</div>
<div class="form-check">
<input class="form-check-input search-type" type="checkbox" id="searchFiles" value="file" checked>
<label class="form-check-label" for="searchFiles">Files</label>
<div class="flex items-center mb-2">
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500 search-type" type="checkbox" id="searchFiles" value="file" checked>
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="searchFiles">Files</label>
</div>
<div class="form-check">
<input class="form-check-input search-type" type="checkbox" id="searchLedger" value="ledger" checked>
<label class="form-check-label" for="searchLedger">Financial</label>
<div class="flex items-center">
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500 search-type" type="checkbox" id="searchLedger" value="ledger" checked>
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="searchLedger">Financial</label>
</div>
</div>
<div class="col-6">
<div class="form-check">
<input class="form-check-input search-type" type="checkbox" id="searchQdros" value="qdro" checked>
<label class="form-check-label" for="searchQdros">QDROs</label>
<div class="w-1/2 px-2">
<div class="flex items-center mb-2">
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500 search-type" type="checkbox" id="searchQdros" value="qdro" checked>
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="searchQdros">QDROs</label>
</div>
<div class="form-check">
<input class="form-check-input search-type" type="checkbox" id="searchDocuments" value="document" checked>
<label class="form-check-label" for="searchDocuments">Documents</label>
<div class="flex items-center mb-2">
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500 search-type" type="checkbox" id="searchDocuments" value="document" checked>
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="searchDocuments">Documents</label>
</div>
<div class="form-check">
<input class="form-check-input search-type" type="checkbox" id="searchPhones" value="phone">
<label class="form-check-label" for="searchPhones">Phones</label>
<div class="flex items-center">
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500 search-type" type="checkbox" id="searchPhones" value="phone">
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="searchPhones">Phones</label>
</div>
</div>
</div>
</div>
<!-- Advanced Filters -->
<div class="accordion" id="filtersAccordion">
<div id="filtersAccordion">
<!-- Date Filters -->
<div class="accordion-item">
<h2 class="accordion-header" id="dateFiltersHeader">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#dateFilters" aria-expanded="false">
<i class="bi bi-calendar3"></i>&nbsp; Date Filters
</button>
</h2>
<div id="dateFilters" class="accordion-collapse collapse" data-bs-parent="#filtersAccordion">
<div class="accordion-body">
<div class="mb-3">
<label for="dateField" class="form-label">Date Field</label>
<select class="form-select" id="dateField">
<details class="border rounded mb-2">
<summary class="px-4 py-2 cursor-pointer flex items-center gap-2 font-medium">
<i class="fa-solid fa-calendar-days"></i>&nbsp; Date Filters
</summary>
<div class="px-4 py-2">
<div class="mb-4">
<label for="dateField" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Date Field</label>
<select 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" id="dateField">
<option value="">Select date field...</option>
<option value="created">Created Date</option>
<option value="updated">Updated Date</option>
@@ -119,184 +107,168 @@
<option value="closed">File Closed Date</option>
</select>
</div>
<div class="row">
<div class="col-6">
<label for="dateFrom" class="form-label">From</label>
<input type="date" class="form-control" id="dateFrom">
<div class="flex flex-wrap -mx-2">
<div class="w-1/2 px-2">
<label for="dateFrom" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">From</label>
<input type="date" class="w-full px-3 py-2 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="dateFrom">
</div>
<div class="col-6">
<label for="dateTo" class="form-label">To</label>
<input type="date" class="form-control" id="dateTo">
</div>
</div>
<div class="mt-2">
<button type="button" class="btn btn-sm btn-outline-secondary" id="datePresetToday">Today</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="datePresetWeek">This Week</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="datePresetMonth">This Month</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="datePresetYear">This Year</button>
<div class="w-1/2 px-2">
<label for="dateTo" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">To</label>
<input type="date" class="w-full px-3 py-2 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="dateTo">
</div>
</div>
<div class="mt-2 flex gap-2">
<button type="button" class="px-3 py-1 text-xs font-medium text-neutral-700 dark:text-neutral-300 bg-neutral-200 dark:bg-neutral-700 hover:bg-neutral-300 dark:hover:bg-neutral-600 rounded transition-colors" id="datePresetToday">Today</button>
<button type="button" class="px-3 py-1 text-xs font-medium text-neutral-700 dark:text-neutral-300 bg-neutral-200 dark:bg-neutral-700 hover:bg-neutral-300 dark:hover:bg-neutral-600 rounded transition-colors" id="datePresetWeek">This Week</button>
<button type="button" class="px-3 py-1 text-xs font-medium text-neutral-700 dark:text-neutral-300 bg-neutral-200 dark:bg-neutral-700 hover:bg-neutral-300 dark:hover:bg-neutral-600 rounded transition-colors" id="datePresetMonth">This Month</button>
<button type="button" class="px-3 py-1 text-xs font-medium text-neutral-700 dark:text-neutral-300 bg-neutral-200 dark:bg-neutral-700 hover:bg-neutral-300 dark:hover:bg-neutral-600 rounded transition-colors" id="datePresetYear">This Year</button>
</div>
</div>
</details>
<!-- Amount Filters -->
<div class="accordion-item">
<h2 class="accordion-header" id="amountFiltersHeader">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#amountFilters" aria-expanded="false">
<i class="bi bi-currency-dollar"></i>&nbsp; Amount Filters
</button>
</h2>
<div id="amountFilters" class="accordion-collapse collapse" data-bs-parent="#filtersAccordion">
<div class="accordion-body">
<div class="mb-3">
<label for="amountField" class="form-label">Amount Field</label>
<select class="form-select" id="amountField">
<details class="border rounded mb-2">
<summary class="px-4 py-2 cursor-pointer flex items-center gap-2 font-medium">
<i class="fa-solid fa-dollar-sign"></i>&nbsp; Amount Filters
</summary>
<div class="px-4 py-2">
<div class="mb-4">
<label for="amountField" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Amount Field</label>
<select 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" id="amountField">
<option value="">Select amount field...</option>
<option value="amount">Transaction Amount</option>
<option value="balance">Account Balance</option>
<option value="total_charges">Total Charges</option>
</select>
</div>
<div class="row">
<div class="col-6">
<label for="amountMin" class="form-label">Minimum</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" class="form-control" id="amountMin" step="0.01" min="0">
</div>
</div>
<div class="col-6">
<label for="amountMax" class="form-label">Maximum</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" class="form-control" id="amountMax" step="0.01" min="0">
<div class="flex flex-wrap -mx-2">
<div class="w-1/2 px-2">
<label for="amountMin" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Minimum</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 px-1 bg-neutral-200 dark:bg-neutral-700 text-neutral-500 dark:text-neutral-400 rounded-l-md">
$
</span>
<input type="number" class="w-full pl-10 pr-3 py-2 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="amountMin" step="0.01" min="0">
</div>
</div>
<div class="w-1/2 px-2">
<label for="amountMax" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Maximum</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 px-1 bg-neutral-200 dark:bg-neutral-700 text-neutral-500 dark:text-neutral-400 rounded-l-md">
$
</span>
<input type="number" class="w-full pl-10 pr-3 py-2 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="amountMax" step="0.01" min="0">
</div>
</div>
</div>
</div>
</details>
<!-- Category Filters -->
<div class="accordion-item">
<h2 class="accordion-header" id="categoryFiltersHeader">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#categoryFilters" aria-expanded="false">
<i class="bi bi-tags"></i>&nbsp; Category Filters
</button>
</h2>
<div id="categoryFilters" class="accordion-collapse collapse" data-bs-parent="#filtersAccordion">
<div class="accordion-body">
<div class="mb-3">
<label for="fileTypes" class="form-label">File Types</label>
<select class="form-select" id="fileTypes" multiple>
<details class="border rounded mb-2">
<summary class="px-4 py-2 cursor-pointer flex items-center gap-2 font-medium">
<i class="fa-solid fa-tags"></i>&nbsp; Category Filters
</summary>
<div class="px-4 py-2">
<div class="mb-4">
<label for="fileTypes" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">File Types</label>
<select 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" id="fileTypes" multiple>
<!-- Options loaded dynamically -->
</select>
</div>
<div class="mb-3">
<label for="fileStatuses" class="form-label">File Statuses</label>
<select class="form-select" id="fileStatuses" multiple>
<div class="mb-4">
<label for="fileStatuses" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">File Statuses</label>
<select 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" id="fileStatuses" multiple>
<!-- Options loaded dynamically -->
</select>
</div>
<div class="mb-3">
<label for="employees" class="form-label">Employees</label>
<select class="form-select" id="employees" multiple>
<div class="mb-4">
<label for="employees" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Employees</label>
<select 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" id="employees" multiple>
<!-- Options loaded dynamically -->
</select>
</div>
<div class="mb-3">
<label for="transactionTypes" class="form-label">Transaction Types</label>
<select class="form-select" id="transactionTypes" multiple>
<div class="mb-4">
<label for="transactionTypes" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Transaction Types</label>
<select 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" id="transactionTypes" multiple>
<!-- Options loaded dynamically -->
</select>
</div>
<div class="mb-3">
<label for="states" class="form-label">States</label>
<select class="form-select" id="states" multiple>
<div class="mb-4">
<label for="states" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">States</label>
<select 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" id="states" multiple>
<!-- Options loaded dynamically -->
</select>
</div>
</div>
</div>
</div>
</details>
<!-- Boolean Filters -->
<div class="accordion-item">
<h2 class="accordion-header" id="booleanFiltersHeader">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#booleanFilters" aria-expanded="false">
<i class="bi bi-toggles"></i>&nbsp; Additional Filters
</button>
</h2>
<div id="booleanFilters" class="accordion-collapse collapse" data-bs-parent="#filtersAccordion">
<div class="accordion-body">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="activeOnly" checked>
<label class="form-check-label" for="activeOnly">Active records only</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="hasBalance">
<label class="form-check-label" for="hasBalance">Has outstanding balance</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isBilled">
<label class="form-check-label" for="isBilled">Billed items only</label>
<details class="border rounded mb-2">
<summary class="px-4 py-2 cursor-pointer flex items-center gap-2 font-medium">
<i class="fa-solid fa-sliders"></i>&nbsp; Additional Filters
</summary>
<div class="px-4 py-2">
<div class="flex items-center mb-2">
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500" type="checkbox" id="activeOnly" checked>
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="activeOnly">Active records only</label>
</div>
<div class="flex items-center">
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500" type="checkbox" id="hasBalance">
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="hasBalance">Has outstanding balance</label>
</div>
<div class="flex items-center">
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500" type="checkbox" id="isBilled">
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="isBilled">Billed items only</label>
</div>
</div>
</details>
</div>
<!-- Search Actions -->
<div class="mt-4 d-grid gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-search"></i> Search
<div class="mt-4 grid gap-2">
<button type="submit" class="w-full px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg transition-colors flex items-center justify-center gap-2">
<i class="fa-solid fa-magnifying-glass"></i> Search
</button>
<div class="row g-2">
<div class="col-6">
<button type="button" class="btn btn-outline-secondary w-100" id="saveSearchBtn">
<i class="bi bi-bookmark-plus"></i> Save Search
<div class="grid grid-cols-2 gap-2">
<button type="button" class="w-full px-4 py-2 text-neutral-700 dark:text-neutral-300 bg-neutral-200 dark:bg-neutral-700 hover:bg-neutral-300 dark:hover:bg-neutral-600 rounded-lg transition-colors flex items-center justify-center gap-2" id="saveSearchBtn">
<i class="fa-solid fa-bookmark"></i> Save Search
</button>
</div>
<div class="col-6">
<button type="button" class="btn btn-outline-secondary w-100" id="resetSearchBtn">
<i class="bi bi-arrow-clockwise"></i> Reset
<button type="button" class="w-full px-4 py-2 text-neutral-700 dark:text-neutral-300 bg-neutral-200 dark:bg-neutral-700 hover:bg-neutral-300 dark:hover:bg-neutral-600 rounded-lg transition-colors flex items-center justify-center gap-2" id="resetSearchBtn">
<i class="fa-solid fa-rotate-right"></i> Reset
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- Results Panel -->
<div class="col-lg-8">
<div class="lg:col-span-2">
<!-- Search Status Bar -->
<div class="card mb-3">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-6">
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-md mb-3">
<div class="p-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 items-center">
<div>
<div id="searchStatus">
<span class="text-muted">Enter search terms to begin</span>
<span class="text-neutral-500">Enter search terms to begin</span>
</div>
</div>
<div class="col-md-6">
<div class="row align-items-center">
<div class="col-md-6">
<label for="sortBy" class="form-label mb-0">Sort by:</label>
<select class="form-select form-select-sm" id="sortBy">
<div>
<div class="grid grid-cols-2 gap-3 items-center">
<div>
<label for="sortBy" class="block text-sm font-medium mb-1">Sort by:</label>
<select class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="sortBy">
<option value="relevance">Relevance</option>
<option value="date">Date</option>
<option value="amount">Amount</option>
<option value="title">Title</option>
</select>
</div>
<div class="col-md-6">
<label for="sortOrder" class="form-label mb-0">Order:</label>
<select class="form-select form-select-sm" id="sortOrder">
<div>
<label for="sortOrder" class="block text-sm font-medium mb-1">Order:</label>
<select class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="sortOrder">
<option value="desc">Descending</option>
<option value="asc">Ascending</option>
</select>
@@ -308,9 +280,9 @@
</div>
<!-- Facets/Filters Summary -->
<div class="card mb-3" id="facetsCard" style="display: none;">
<div class="card-body">
<h6 class="card-title">Filter Results</h6>
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-md mb-3" id="facetsCard" style="display: none;">
<div class="p-4">
<h6 class="font-semibold mb-2">Filter Results</h6>
<div id="facetsContainer">
<!-- Facets will be displayed here -->
</div>
@@ -318,15 +290,15 @@
</div>
<!-- Search Results -->
<div class="card">
<div class="card-body">
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-md">
<div class="p-4">
<div id="searchResults">
<div class="text-center text-muted p-5">
<i class="bi bi-search display-1"></i>
<div class="text-center text-neutral-500 p-5">
<i class="fa-solid fa-magnifying-glass text-6xl"></i>
<h4>Advanced Search</h4>
<p>Use the search form on the left to find customers, files, transactions, documents, and more across the entire database.</p>
<div class="mt-3">
<small class="text-muted">
<small class="text-neutral-500">
<strong>Quick Tips:</strong><br>
• Use quotes for exact phrases: "John Smith"<br>
• Use filters to narrow results<br>
@@ -339,20 +311,12 @@
<!-- Loading Indicator -->
<div id="searchLoading" class="text-center p-4" style="display: none;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Searching...</span>
</div>
<div class="inline-block w-6 h-6 border-2 border-neutral-300 border-t-transparent rounded-full animate-spin"></div>
<div class="mt-2">Searching database...</div>
</div>
<!-- Pagination -->
<nav id="searchPagination" aria-label="Search results pagination" style="display: none;">
<ul class="pagination justify-content-center">
<!-- Pagination will be populated here -->
</ul>
</nav>
</div>
</div>
<nav id="searchPagination" aria-label="Search results pagination" class="flex items-center justify-center" style="display: none;"></nav>
</div>
</div>
</div>
@@ -360,52 +324,45 @@
</div>
<!-- Save Search Modal -->
<div class="modal fade" id="saveSearchModal" tabindex="-1" aria-labelledby="saveSearchModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="saveSearchModalLabel">Save Search</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div id="saveSearchModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-md w-full">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold" id="saveSearchModalLabel">Save Search</h5>
<button type="button" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300" onclick="closeSaveSearchModal()"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="modal-body">
<div class="px-6 py-4">
<form id="saveSearchForm">
<div class="mb-3">
<label for="searchName" class="form-label">Search Name *</label>
<input type="text" class="form-control" id="searchName" required placeholder="Enter a name for this search">
<label for="searchName" class="block text-sm font-medium mb-1">Search Name *</label>
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="searchName" required placeholder="Enter a name for this search">
</div>
<div class="mb-3">
<label for="searchDescription" class="form-label">Description</label>
<textarea class="form-control" id="searchDescription" rows="3" placeholder="Optional description of this search"></textarea>
<label for="searchDescription" class="block text-sm font-medium mb-1">Description</label>
<textarea class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="searchDescription" rows="3" placeholder="Optional description of this search"></textarea>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isPublicSearch">
<label class="form-check-label" for="isPublicSearch">
Make this search public (visible to all users)
<label class="flex items-center gap-2">
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded" type="checkbox" id="isPublicSearch">
<span class="text-sm">Make this search public (visible to all users)</span>
</label>
</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="confirmSaveSearch">
<i class="bi bi-bookmark-plus"></i> Save Search
</button>
</div>
<div class="flex items-center justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<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" onclick="closeSaveSearchModal()">Cancel</button>
<button type="button" class="px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg" id="confirmSaveSearch" onclick="saveCurrentSearch()"><i class="fa-solid fa-bookmark"></i> Save Search</button>
</div>
</div>
</div>
<!-- Saved Searches Modal -->
<div class="modal fade" id="savedSearchesModal" tabindex="-1" aria-labelledby="savedSearchesModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="savedSearchesModalLabel">Saved Searches</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="savedSearchesModal">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-4xl w-full">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold" id="savedSearchesModalLabel">Saved Searches</h5>
<button type="button" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300" onclick="closeModal('savedSearchesModal')"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="modal-body">
<div class="table-responsive">
<table class="table table-hover">
<div class="px-6 py-4">
<div class="overflow-x-auto">
<table class="w-full text-sm text-left border border-neutral-200 dark:border-neutral-700 rounded-lg overflow-hidden">
<thead>
<tr>
<th>Name</th>
@@ -421,29 +378,27 @@
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
<div class="flex items-center justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<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" onclick="closeModal('savedSearchesModal')">Close</button>
</div>
</div>
</div>
<!-- Search Statistics Modal -->
<div class="modal fade" id="searchStatsModal" tabindex="-1" aria-labelledby="searchStatsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="searchStatsModalLabel">Search Statistics</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="searchStatsModal">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-4xl w-full">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold" id="searchStatsModalLabel">Search Statistics</h5>
<button type="button" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300" onclick="closeModal('searchStatsModal')"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="modal-body">
<div class="row" id="searchStatsContent">
<div class="px-6 py-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4" id="searchStatsContent">
<!-- Statistics will be loaded here -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
<div class="flex items-center justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<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" onclick="closeModal('searchStatsModal')">Close</button>
</div>
</div>
</div>
@@ -476,9 +431,11 @@ function initializeSearch() {
// Initialize search suggestions dropdown
const suggestionsDropdown = document.getElementById('searchSuggestions');
suggestionsDropdown.addEventListener('click', function(e) {
if (e.target.classList.contains('dropdown-item')) {
document.getElementById('searchQuery').value = e.target.textContent;
suggestionsDropdown.classList.remove('show');
const item = e.target.closest('a');
if (item && suggestionsDropdown.contains(item)) {
e.preventDefault();
document.getElementById('searchQuery').value = item.textContent.trim();
suggestionsDropdown.classList.add('hidden');
performSearch();
}
});
@@ -560,7 +517,7 @@ function setupEventHandlers() {
loadSearchSuggestions(this.value);
}, 300);
} else {
document.getElementById('searchSuggestions').classList.remove('show');
document.getElementById('searchSuggestions').classList.add('hidden');
}
});
@@ -574,8 +531,7 @@ function setupEventHandlers() {
document.getElementById('resetSearchBtn').addEventListener('click', resetSearch);
document.getElementById('saveSearchBtn').addEventListener('click', () => {
if (Object.keys(currentSearchCriteria).length > 0) {
const modal = new bootstrap.Modal(document.getElementById('saveSearchModal'));
modal.show();
showSaveSearchModal();
} else {
showAlert('Please perform a search before saving', 'warning');
}
@@ -635,25 +591,25 @@ function displaySearchSuggestions(suggestions) {
dropdown.innerHTML = '';
if (suggestions.length === 0) {
dropdown.classList.remove('show');
dropdown.classList.add('hidden');
return;
}
suggestions.forEach(suggestion => {
const item = document.createElement('a');
item.className = 'dropdown-item';
item.className = 'block px-3 py-2 hover:bg-neutral-50';
item.href = '#';
item.innerHTML = `
<div class="d-flex justify-content-between">
<div class="flex justify-between">
<span>${suggestion.text}</span>
<small class="text-muted">${suggestion.category}</small>
<small class="text-neutral-500">${suggestion.category}</small>
</div>
${suggestion.description ? `<small class="text-muted">${suggestion.description}</small>` : ''}
${suggestion.description ? `<small class="text-neutral-500">${suggestion.description}</small>` : ''}
`;
dropdown.appendChild(item);
});
dropdown.classList.add('show');
dropdown.classList.remove('hidden');
}
async function performSearch(offset = 0) {
@@ -796,7 +752,7 @@ function displaySearchResults(data) {
const executionTime = data.stats?.search_execution_time || 0;
statusElement.innerHTML = `
<strong>${data.total_results}</strong> results found
<small class="text-muted">(${executionTime.toFixed(3)}s)</small>
<small class="text-neutral-500">(${executionTime.toFixed(3)}s)</small>
`;
// Display facets
@@ -810,8 +766,8 @@ function displaySearchResults(data) {
// Display results
if (data.results.length === 0) {
resultsContainer.innerHTML = `
<div class="text-center text-muted p-5">
<i class="bi bi-search display-1"></i>
<div class="text-center text-neutral-500 p-5">
<i class="fa-solid fa-magnifying-glass text-6xl"></i>
<h4>No Results Found</h4>
<p>Try adjusting your search terms or filters</p>
</div>
@@ -826,24 +782,24 @@ function displaySearchResults(data) {
const typeBadge = getTypeBadge(result.type);
resultsHTML += `
<div class="search-result-item border-bottom py-3">
<div class="d-flex">
<div class="flex-shrink-0 me-3">
<i class="${typeIcon} fs-4 text-primary"></i>
<div class="search-result-item border-b py-3">
<div class="flex">
<div class="flex-shrink-0 mr-3">
<i class="${typeIcon} text-primary-600 text-xl"></i>
</div>
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-start mb-1">
<div class="flex-1">
<div class="flex justify-between items-start mb-1">
<h6 class="mb-1">
<a href="${result.url}" class="text-decoration-none">${result.title}</a>
<a href="${result.url}" class="hover:underline">${result.title}</a>
${typeBadge}
</h6>
<div class="text-end">
${result.relevance_score ? `<small class="text-muted">Score: ${result.relevance_score.toFixed(1)}</small>` : ''}
${result.updated_at ? `<br><small class="text-muted">${formatDate(result.updated_at)}</small>` : ''}
<div class="text-right">
${result.relevance_score ? `<small class="text-neutral-500">Score: ${result.relevance_score.toFixed(1)}</small>` : ''}
${result.updated_at ? `<br><small class="text-neutral-500">${formatDate(result.updated_at)}</small>` : ''}
</div>
</div>
<p class="mb-1 text-muted">${result.description}</p>
${result.highlight ? `<div class="small text-info"><strong>Match:</strong> ${result.highlight}</div>` : ''}
<p class="mb-1 text-neutral-500">${result.description}</p>
${result.highlight ? `<div class="text-sm text-info-600"><strong>Match:</strong> ${result.highlight}</div>` : ''}
${displayResultMetadata(result.metadata)}
</div>
</div>
@@ -869,7 +825,7 @@ function displayFacets(facets) {
<div class="facet-group mb-2">
<strong>${facetName.replace('_', ' ').toUpperCase()}:</strong>
${Object.entries(facetData).map(([value, count]) =>
`<span class="badge bg-secondary ms-1">${value} (${count})</span>`
`<span class="inline-block px-2 py-0.5 text-xs rounded bg-neutral-200 text-neutral-700 ml-1">${value} (${count})</span>`
).join('')}
</div>
`;
@@ -881,67 +837,71 @@ function displayFacets(facets) {
function displayPagination(pageInfo) {
const paginationContainer = document.getElementById('searchPagination');
const pagination = paginationContainer.querySelector('.pagination');
pagination.innerHTML = '';
paginationContainer.innerHTML = '';
// Previous button
const prevItem = document.createElement('li');
prevItem.className = `page-item ${pageInfo.has_previous ? '' : 'disabled'}`;
prevItem.innerHTML = `<a class="page-link" href="#" data-page="${pageInfo.current_page - 1}">Previous</a>`;
pagination.appendChild(prevItem);
const prevBtn = document.createElement('button');
prevBtn.className = `mx-1 px-3 py-1 rounded border ${pageInfo.has_previous ? 'border-neutral-300 hover:bg-neutral-100' : 'border-neutral-200 text-neutral-400 cursor-not-allowed'}`;
prevBtn.textContent = 'Previous';
prevBtn.dataset.page = pageInfo.current_page - 1;
prevBtn.disabled = !pageInfo.has_previous;
paginationContainer.appendChild(prevBtn);
// Page numbers
const startPage = Math.max(1, pageInfo.current_page - 2);
const endPage = Math.min(pageInfo.total_pages, pageInfo.current_page + 2);
for (let page = startPage; page <= endPage; page++) {
const pageItem = document.createElement('li');
pageItem.className = `page-item ${page === pageInfo.current_page ? 'active' : ''}`;
pageItem.innerHTML = `<a class="page-link" href="#" data-page="${page}">${page}</a>`;
pagination.appendChild(pageItem);
const pageBtn = document.createElement('button');
pageBtn.className = `mx-1 px-3 py-1 rounded border ${page === pageInfo.current_page ? 'bg-primary-600 text-white border-primary-600' : 'border-neutral-300 hover:bg-neutral-100'}`;
pageBtn.textContent = page;
pageBtn.dataset.page = page;
paginationContainer.appendChild(pageBtn);
}
// Next button
const nextItem = document.createElement('li');
nextItem.className = `page-item ${pageInfo.has_next ? '' : 'disabled'}`;
nextItem.innerHTML = `<a class="page-link" href="#" data-page="${pageInfo.current_page + 1}">Next</a>`;
pagination.appendChild(nextItem);
const nextBtn = document.createElement('button');
nextBtn.className = `mx-1 px-3 py-1 rounded border ${pageInfo.has_next ? 'border-neutral-300 hover:bg-neutral-100' : 'border-neutral-200 text-neutral-400 cursor-not-allowed'}`;
nextBtn.textContent = 'Next';
nextBtn.dataset.page = pageInfo.current_page + 1;
nextBtn.disabled = !pageInfo.has_next;
paginationContainer.appendChild(nextBtn);
// Add click handlers
pagination.addEventListener('click', function(e) {
paginationContainer.addEventListener('click', function(e) {
e.preventDefault();
if (e.target.classList.contains('page-link') && !e.target.parentElement.classList.contains('disabled')) {
if (e.target.tagName === 'BUTTON' && !e.target.disabled) {
const page = parseInt(e.target.dataset.page);
const offset = (page - 1) * currentSearchCriteria.limit;
performSearch(offset);
}
});
paginationContainer.style.display = 'block';
paginationContainer.style.display = 'flex';
}
function getTypeIcon(type) {
const icons = {
'customer': 'bi-person-fill',
'file': 'bi-folder-fill',
'ledger': 'bi-calculator',
'qdro': 'bi-file-earmark-ruled',
'document': 'bi-file-earmark-text',
'template': 'bi-file-text',
'phone': 'bi-telephone'
'customer': 'fa-solid fa-user',
'file': 'fa-solid fa-folder',
'ledger': 'fa-solid fa-calculator',
'qdro': 'fa-regular fa-file-lines',
'document': 'fa-regular fa-file-lines',
'template': 'fa-regular fa-file-lines',
'phone': 'fa-solid fa-phone'
};
return icons[type] || 'bi-file';
return icons[type] || 'fa-regular fa-file';
}
function getTypeBadge(type) {
const badges = {
'customer': '<span class="badge bg-primary ms-2">Customer</span>',
'file': '<span class="badge bg-success ms-2">File</span>',
'ledger': '<span class="badge bg-warning ms-2">Financial</span>',
'qdro': '<span class="badge bg-info ms-2">QDRO</span>',
'document': '<span class="badge bg-secondary ms-2">Document</span>',
'template': '<span class="badge bg-secondary ms-2">Template</span>',
'phone': '<span class="badge bg-dark ms-2">Phone</span>'
'customer': '<span class="inline-block px-2 py-0.5 text-xs rounded bg-primary-100 text-primary-700 ml-2">Customer</span>',
'file': '<span class="inline-block px-2 py-0.5 text-xs rounded bg-success-50 text-success-700 ml-2">File</span>',
'ledger': '<span class="inline-block px-2 py-0.5 text-xs rounded bg-warning-50 text-warning-700 ml-2">Financial</span>',
'qdro': '<span class="inline-block px-2 py-0.5 text-xs rounded bg-info-50 text-info-700 ml-2">QDRO</span>',
'document': '<span class="inline-block px-2 py-0.5 text-xs rounded bg-neutral-200 text-neutral-700 ml-2">Document</span>',
'template': '<span class="inline-block px-2 py-0.5 text-xs rounded bg-neutral-200 text-neutral-700 ml-2">Template</span>',
'phone': '<span class="inline-block px-2 py-0.5 text-xs rounded bg-neutral-800 text-neutral-100 ml-2">Phone</span>'
};
return badges[type] || '';
}
@@ -949,11 +909,11 @@ function getTypeBadge(type) {
function displayResultMetadata(metadata) {
if (!metadata) return '';
let metadataHTML = '<div class="small text-muted mt-1">';
let metadataHTML = '<div class="text-sm text-neutral-500 mt-1">';
Object.entries(metadata).forEach(([key, value]) => {
if (value && key !== 'phones') { // Skip complex objects
metadataHTML += `<span class="me-3"><strong>${key.replace('_', ' ')}:</strong> ${value}</span>`;
metadataHTML += `<span class="mr-3"><strong>${key.replace('_', ' ')}:</strong> ${value}</span>`;
}
});
@@ -1010,13 +970,13 @@ function resetSearch() {
// Clear results
document.getElementById('searchResults').innerHTML = `
<div class="text-center text-muted p-5">
<i class="bi bi-search display-1"></i>
<div class="text-center text-neutral-500 p-5">
<i class="fa-solid fa-magnifying-glass text-6xl"></i>
<h4>Advanced Search</h4>
<p>Use the search form on the left to find customers, files, transactions, documents, and more across the entire database.</p>
</div>
`;
document.getElementById('searchStatus').innerHTML = '<span class="text-muted">Enter search terms to begin</span>';
document.getElementById('searchStatus').innerHTML = '<span class="text-neutral-500">Enter search terms to begin</span>';
document.getElementById('facetsCard').style.display = 'none';
document.getElementById('searchPagination').style.display = 'none';
@@ -1057,8 +1017,7 @@ async function saveCurrentSearch() {
showAlert('Search saved successfully', 'success');
// Close modal and reset form
const modal = bootstrap.Modal.getInstance(document.getElementById('saveSearchModal'));
modal.hide();
closeSaveSearchModal();
document.getElementById('saveSearchForm').reset();
} catch (error) {
@@ -1076,7 +1035,7 @@ async function loadSavedSearches() {
tableBody.innerHTML = '';
if (savedSearches.length === 0) {
tableBody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No saved searches found</td></tr>';
tableBody.innerHTML = '<tr><td colspan="5" class="text-center text-neutral-500">No saved searches found</td></tr>';
} else {
savedSearches.forEach(search => {
const row = document.createElement('tr');
@@ -1086,13 +1045,9 @@ async function loadSavedSearches() {
<td>${search.last_used ? formatDate(search.last_used) : 'Never'}</td>
<td>${search.use_count || 0}</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="loadSavedSearch(${search.id})" title="Load Search">
<i class="bi bi-play"></i>
</button>
<button class="btn btn-outline-danger" onclick="deleteSavedSearch(${search.id})" title="Delete Search">
<i class="bi bi-trash"></i>
</button>
<div class="flex items-center gap-2">
<button class="px-2 py-1 border border-blue-600 text-blue-600 rounded hover:bg-blue-100" onclick="loadSavedSearch(${search.id})" title="Load Search"><i class="fa-solid fa-play"></i></button>
<button class="px-2 py-1 border border-red-600 text-red-600 rounded hover:bg-red-100" onclick="deleteSavedSearch(${search.id})" title="Delete Search"><i class="fa-solid fa-trash"></i></button>
</div>
</td>
`;
@@ -1100,8 +1055,7 @@ async function loadSavedSearches() {
});
}
const modal = new bootstrap.Modal(document.getElementById('savedSearchesModal'));
modal.show();
openModal('savedSearchesModal');
} catch (error) {
console.error('Error loading saved searches:', error);
@@ -1150,8 +1104,7 @@ function loadSavedSearch(searchId) {
document.getElementById('sortOrder').value = criteria.sort_order || 'desc';
// Close modal and perform search
const modal = bootstrap.Modal.getInstance(document.getElementById('savedSearchesModal'));
modal.hide();
closeModal('savedSearchesModal');
// Update use count
search.use_count = (search.use_count || 0) + 1;
@@ -1173,22 +1126,13 @@ function deleteSavedSearch(searchId) {
// Utility functions
function showAlert(message, type = 'info') {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed top-0 end-0 m-3`;
alertDiv.style.zIndex = '9999';
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();
if (window.alerts && typeof window.alerts.show === 'function') {
window.alerts.show(message, type);
} else if (window.showNotification) {
window.showNotification(message, type);
} else {
alert(String(message));
}
}, 5000);
}
// Export functionality
@@ -1201,5 +1145,13 @@ function exportSearchResults() {
// This would typically call an export API endpoint
showAlert('Export functionality will be implemented in a future update', 'info');
}
function showSaveSearchModal() {
document.getElementById('saveSearchModal').classList.remove('hidden');
}
function closeSaveSearchModal() {
document.getElementById('saveSearchModal').classList.add('hidden');
}
</script>
{% endblock %}

View File

@@ -1,30 +1,33 @@
<!-- Support Ticket Modal -->
<div class="modal fade" id="supportModal" tabindex="-1" aria-labelledby="supportModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="supportModalLabel">
<i class="fas fa-bug me-2"></i>Submit Internal Issue
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
<div id="supportModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-4xl w-full max-h-screen overflow-hidden">
<div class="flex items-center justify-between px-6 py-4 bg-primary-600 text-white">
<h2 class="text-xl font-semibold flex items-center gap-2">
<i class="fas fa-bug"></i>
<span>Submit Internal Issue</span>
</h2>
<button onclick="closeSupportModal()" class="text-primary-200 hover:text-white transition-colors">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div class="modal-body">
<div class="px-6 py-4 max-h-96 overflow-y-auto scrollbar-thin">
<form id="supportForm">
<div class="row">
<div class="col-md-6 mb-3">
<label for="contactName" class="form-label">Reporter Name *</label>
<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 class="col-md-6 mb-3">
<label for="contactEmail" class="form-label">Reporter Email *</label>
<input type="email" class="form-control" id="contactEmail" required>
<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="row">
<div class="col-md-6 mb-3">
<label for="ticketCategory" class="form-label">Issue Type *</label>
<select class="form-select" id="ticketCategory" required>
<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>
@@ -38,9 +41,9 @@
<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">
<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>
@@ -49,90 +52,141 @@
</div>
</div>
<div class="mb-3">
<label for="ticketSubject" class="form-label">Issue Summary *</label>
<input type="text" class="form-control" id="ticketSubject" maxlength="200" required>
<div class="form-text">Brief summary of the bug/issue</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-3">
<label for="ticketDescription" class="form-label">Detailed Description *</label>
<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 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 (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>
<!-- 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>
<span class="font-medium text-neutral-600 dark:text-neutral-400">Browser:</span>
<span id="browserInfo" class="text-neutral-900 dark:text-neutral-100 ml-1">Loading...</span>
</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 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">
<i class="fas fa-info-circle text-info-600 dark:text-info-400 mt-0.5"></i>
<div>
<p class="font-medium">Note:</p>
<p class="text-sm mt-1">Your issue will be assigned a tracking number and the development team will be notified automatically.</p>
</div>
</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
<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="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">
Cancel
</button>
<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>
<span>Submit Issue</span>
</button>
</div>
</div>
</div>
</div>
<!-- Support Ticket Success Modal -->
<div class="modal fade" id="supportSuccessModal" tabindex="-1" aria-labelledby="supportSuccessLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title" id="supportSuccessLabel">
<i class="fas fa-check-circle me-2"></i>Issue Submitted Successfully
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
<div id="supportSuccessModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-2xl w-full">
<div class="flex items-center justify-between px-6 py-4 bg-success-600 text-white">
<h2 class="text-xl font-semibold flex items-center gap-2">
<i class="fas fa-check-circle"></i>
<span>Issue Submitted Successfully</span>
</h2>
<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="modal-body text-center">
<div class="mb-3">
<i class="fas fa-bug fa-3x text-success mb-3"></i>
<h4>Issue logged successfully!</h4>
<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>
<div class="alert alert-success">
<strong>Issue ID:</strong> <span id="newTicketNumber"></span>
<h3 class="text-xl font-semibold text-neutral-900 dark:text-neutral-100 mb-2">Issue logged successfully!</h3>
</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 class="bg-success-50 dark:bg-success-900/20 border border-success-200 dark:border-success-800 rounded-lg p-4 mb-6">
<div class="flex items-center justify-center gap-2">
<span class="font-medium text-success-800 dark:text-success-300">Issue ID:</span>
<span id="newTicketNumber" class="font-mono font-semibold text-success-900 dark:text-success-200"></span>
</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 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>
<script>
// Support ticket functionality
// Support ticket functionality - Tailwind version
let supportSystem = {
currentPageInfo: 'Unknown',
browserInfo: 'Unknown',
@@ -176,45 +230,57 @@ let supportSystem = {
this.browserInfo = `${browserName} (${navigator.platform})`;
// Update modal display
document.getElementById('currentPageInfo').textContent = this.currentPageInfo;
document.getElementById('browserInfo').textContent = this.browserInfo;
const currentPageElement = document.getElementById('currentPageInfo');
const browserElement = document.getElementById('browserInfo');
if (currentPageElement) currentPageElement.textContent = this.currentPageInfo;
if (browserElement) browserElement.textContent = this.browserInfo;
},
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
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
const form = document.getElementById('supportForm');
if (form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
supportSystem.submitTicket();
});
}
},
populateUserInfo: function() {
// Try to get current user info from the global app state
if (window.app && window.app.user) {
const user = window.app.user;
document.getElementById('contactName').value = `${user.first_name || ''} ${user.last_name || ''}`.trim() || user.username;
document.getElementById('contactEmail').value = user.email;
const nameInput = document.getElementById('contactName');
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() {
const form = document.getElementById('supportForm');
if (!form.checkValidity()) {
form.classList.add('was-validated');
// Show validation errors
form.reportValidity();
return;
}
const submitBtn = document.getElementById('submitSupportTicket');
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Submitting...';
const originalHTML = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="fas fa-spinner animate-spin"></i><span class="ml-2">Submitting...</span>';
submitBtn.disabled = true;
try {
@@ -241,15 +307,14 @@ let supportSystem = {
if (response.ok) {
// Hide support modal
bootstrap.Modal.getInstance(document.getElementById('supportModal')).hide();
closeSupportModal();
// Show success modal
document.getElementById('newTicketNumber').textContent = result.ticket_number;
new bootstrap.Modal(document.getElementById('supportSuccessModal')).show();
document.getElementById('supportSuccessModal').classList.remove('hidden');
// Reset form
form.reset();
form.classList.remove('was-validated');
} else {
throw new Error(result.detail || 'Failed to submit ticket');
@@ -257,30 +322,63 @@ let supportSystem = {
} catch (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 {
submitBtn.innerHTML = originalText;
submitBtn.innerHTML = originalHTML;
submitBtn.disabled = false;
}
},
showAlert: function(message, type = 'info') {
// Use existing notification system if available
if (window.showNotification) {
window.showNotification(message, type);
// Use existing alert system if available
if (window.showAlert) {
window.showAlert(message, type);
} else {
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
document.addEventListener('DOMContentLoaded', function() {
supportSystem.init();
});
// Global function to open support modal
window.openSupportModal = function() {
new bootstrap.Modal(document.getElementById('supportModal')).show();
};
// Make function globally available
window.openSupportModal = openSupportModal;
</script>