progress
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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}
|
||||
@@ -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
37
app/api/settings.py
Normal 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}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}')>"
|
||||
@@ -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")
|
||||
@@ -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
1517
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
Normal file
24
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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
3
static/css/input.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -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
2527
static/css/tailwind.css
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
BIN
static/images/delphi-logo.webp
Normal file
BIN
static/images/delphi-logo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
191
static/js/alerts.js
Normal file
191
static/js/alerts.js
Normal 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 });
|
||||
})();
|
||||
|
||||
|
||||
560
static/js/customers-tailwind.js
Normal file
560
static/js/customers-tailwind.js
Normal 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
18
static/js/financial.js
Normal 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 ...
|
||||
@@ -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();
|
||||
return;
|
||||
}
|
||||
// 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
|
||||
|
||||
@@ -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
119
tailwind.config.js
Normal 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'),
|
||||
],
|
||||
}
|
||||
1303
templates/admin.html
1303
templates/admin.html
File diff suppressed because it is too large
Load Diff
@@ -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="/">
|
||||
Delphi Database System
|
||||
</a>
|
||||
<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>
|
||||
</button>
|
||||
<!-- 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>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
<!-- 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>
|
||||
|
||||
<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
|
||||
</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>
|
||||
<!-- 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>
|
||||
<div id="admin-menu-divider" class="hidden border-t border-neutral-200 dark:border-neutral-700 my-1"></div>
|
||||
<a href="#" onclick="logout()" class="flex items-center gap-2 px-4 py-2 text-sm text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700 transition-colors">
|
||||
<i class="fa-solid fa-right-from-bracket"></i>
|
||||
<span>Logout</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu button -->
|
||||
<button onclick="toggleMobileMenu()" class="md:hidden flex items-center justify-center w-10 h-10 bg-primary-700 hover:bg-primary-800 text-white rounded-lg transition-colors">
|
||||
<i id="mobileMenuIcon" class="fa-solid fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Navigation -->
|
||||
<div id="mobileMenu" class="hidden md:hidden border-t border-primary-700 pt-4 pb-4">
|
||||
<div class="space-y-1">
|
||||
<a href="/customers" class="flex items-center gap-3 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
|
||||
<i class="fa-solid fa-users"></i>
|
||||
<span>Customers</span>
|
||||
</a>
|
||||
<a href="/files" class="flex items-center gap-3 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
|
||||
<i class="fa-solid fa-folder"></i>
|
||||
<span>Files</span>
|
||||
</a>
|
||||
<a href="/financial" class="flex items-center gap-3 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
|
||||
<i class="fa-solid fa-calculator"></i>
|
||||
<span>Ledger</span>
|
||||
</a>
|
||||
<a href="/documents" class="flex items-center gap-3 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
|
||||
<i class="fa-solid fa-file-lines"></i>
|
||||
<span>Documents</span>
|
||||
</a>
|
||||
<a href="/search" class="flex items-center gap-3 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
<span>Search</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
© <span id="currentYear"></span> Delphi Consulting Group Database System
|
||||
<span class="mx-2">|</span>
|
||||
<span id="currentPageDisplay">Loading...</span>
|
||||
</small>
|
||||
<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">
|
||||
© <span id="currentYear"></span> Delphi Consulting Group Database System
|
||||
<span class="mx-2">|</span>
|
||||
<span id="currentPageDisplay">Loading...</span>
|
||||
</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>
|
||||
<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>
|
||||
</ul>
|
||||
|
||||
<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 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="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>
|
||||
<div class="col-md-6">
|
||||
<h6><i class="bi bi-list"></i> Lists/Tables</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><kbd>↑/↓</kbd> - Navigate records</li>
|
||||
<li><kbd>Page Up/Down</kbd> - Page navigation</li>
|
||||
<li><kbd>Home/End</kbd> - First/Last record</li>
|
||||
<li><kbd>Enter</kbd> - Open/Edit record</li>
|
||||
<li><kbd>+/-</kbd> - Change dates</li>
|
||||
</ul>
|
||||
|
||||
<h6><i class="bi bi-tools"></i> System</h6>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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
@@ -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)
|
||||
</button>
|
||||
<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>
|
||||
|
||||
<!-- 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="text-sm font-medium mb-1">Customers</h5>
|
||||
<h2 class="text-2xl font-bold mb-0" id="customer-count">-</h2>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/customers" class="text-primary-200 hover:text-white text-xs font-medium flex items-center gap-1 mt-2 transition-colors">
|
||||
View all
|
||||
<i class="fa-solid fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-success-600 text-white rounded-xl shadow-soft p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center justify-center w-10 h-10 bg-success-700 rounded-lg">
|
||||
<i class="fa-solid fa-folder text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="text-sm font-medium mb-1">Active Files</h5>
|
||||
<h2 class="text-2xl font-bold mb-0" id="file-count">-</h2>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/files" class="text-success-200 hover:text-white text-xs font-medium flex items-center gap-1 mt-2 transition-colors">
|
||||
View all
|
||||
<i class="fa-solid fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-info-600 text-white rounded-xl shadow-soft p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center justify-center w-10 h-10 bg-info-700 rounded-lg">
|
||||
<i class="fa-solid fa-receipt text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="text-sm font-medium mb-1">Transactions</h5>
|
||||
<h2 class="text-2xl font-bold mb-0" id="transaction-count">-</h2>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/financial" class="text-info-200 hover:text-white text-xs font-medium flex items-center gap-1 mt-2 transition-colors">
|
||||
View ledger
|
||||
<i class="fa-solid fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-warning-600 text-white rounded-xl shadow-soft p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center justify-center w-10 h-10 bg-warning-700 rounded-lg">
|
||||
<i class="fa-regular fa-file-lines text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="text-sm font-medium mb-1">Documents</h5>
|
||||
<h2 class="text-2xl font-bold mb-0" id="document-count">-</h2>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/documents" class="text-warning-200 hover:text-white text-xs font-medium flex items-center gap-1 mt-2 transition-colors">
|
||||
View all
|
||||
<i class="fa-solid fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions and Recent Activity -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="md:col-span-2 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft">
|
||||
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
|
||||
<h5 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
|
||||
<i class="fa-solid fa-bolt"></i>
|
||||
<span>Quick Actions</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div class="space-y-3">
|
||||
<button onclick="newCustomer()" class="w-full flex flex-col items-center justify-center p-4 bg-neutral-50 dark:bg-neutral-900/50 hover:bg-neutral-100 dark:hover:bg-neutral-900 rounded-lg border border-neutral-200 dark:border-neutral-700 transition-colors duration-200">
|
||||
<i class="fa-solid fa-user-plus text-2xl text-primary-600 mb-1"></i>
|
||||
<span class="font-medium">New Customer</span>
|
||||
<kbd class="text-xs text-neutral-500 dark:text-neutral-400 mt-1">Ctrl+Shift+C</kbd>
|
||||
</button>
|
||||
<button onclick="newFile()" class="w-full flex flex-col items-center justify-center p-4 bg-neutral-50 dark:bg-neutral-900/50 hover:bg-neutral-100 dark:hover:bg-neutral-900 rounded-lg border border-neutral-200 dark:border-neutral-700 transition-colors duration-200">
|
||||
<i class="fa-solid fa-folder-plus text-2xl text-success-600 mb-1"></i>
|
||||
<span class="font-medium">New File</span>
|
||||
<kbd class="text-xs text-neutral-500 dark:text-neutral-400 mt-1">Ctrl+Shift+F</kbd>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="card-title">Customers</h5>
|
||||
<h2 class="mb-0" id="customer-count">-</h2>
|
||||
<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 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>
|
||||
<a href="/customers" 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-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>
|
||||
</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 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>
|
||||
</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>
|
||||
</button>
|
||||
<button class="btn btn-outline-success btn-lg" onclick="newFile()">
|
||||
<i class="bi bi-folder-plus"></i> New File
|
||||
<small class="d-block text-muted">Ctrl+Shift+F</small>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-outline-info btn-lg" onclick="newTransaction()">
|
||||
<i class="bi bi-plus-circle"></i> New Transaction
|
||||
<small class="d-block text-muted">Ctrl+Shift+T</small>
|
||||
</button>
|
||||
<button class="btn btn-outline-warning btn-lg" onclick="globalSearch()">
|
||||
<i class="bi bi-search"></i> Global Search
|
||||
<small class="d-block text-muted">Ctrl+F</small>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div 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>
|
||||
</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...
|
||||
<!-- 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="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,55 +188,49 @@
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
// 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')
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
document.getElementById('customer-count').textContent = data.total_customers || '0';
|
||||
document.getElementById('file-count').textContent = data.total_files || '0';
|
||||
document.getElementById('transaction-count').textContent = data.total_transactions || '0';
|
||||
document.getElementById('document-count').textContent = data.total_qdros || '0';
|
||||
document.getElementById('db-size').textContent = data.database_size || '-';
|
||||
document.getElementById('last-backup').textContent = data.last_backup || 'Not available';
|
||||
// Load dashboard data
|
||||
async function loadDashboardData() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/stats', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard data:', error);
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
document.getElementById('customer-count').textContent = data.total_customers || '0';
|
||||
document.getElementById('file-count').textContent = data.total_files || '0';
|
||||
document.getElementById('transaction-count').textContent = data.total_transactions || '0';
|
||||
document.getElementById('document-count').textContent = data.total_qdros || '0';
|
||||
document.getElementById('db-size').textContent = data.database_size || '-';
|
||||
document.getElementById('last-backup').textContent = data.last_backup || 'Not available';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Quick action functions
|
||||
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() {
|
||||
window.location.href = '/search';
|
||||
}
|
||||
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 %}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,146 +3,164 @@
|
||||
{% 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 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="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft">
|
||||
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
|
||||
<h5 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
|
||||
<i class="fa-solid fa-circle-info"></i>
|
||||
<span>Current Database Status</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div id="importStatus">
|
||||
<div class="flex items-center justify-center py-4 text-neutral-500 dark:text-neutral-400">
|
||||
<i class="fa-solid fa-rotate-right animate-spin text-xl mr-2"></i>
|
||||
<p>Loading import status...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CSV File Upload Panel -->
|
||||
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft">
|
||||
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
|
||||
<h5 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
|
||||
<i class="fa-regular fa-file-arrow-up"></i>
|
||||
<span>Upload CSV Files</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<form id="importForm" enctype="multipart/form-data">
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-4">
|
||||
<div class="md:col-span-4">
|
||||
<label for="fileType" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Data Type *</label>
|
||||
<select class="w-full px-3 py-3 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200" id="fileType" name="fileType" required>
|
||||
<option value="">Select data type...</option>
|
||||
</select>
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400 mt-1" id="fileTypeDescription"></div>
|
||||
</div>
|
||||
<div class="md:col-span-6">
|
||||
<label for="csvFile" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">CSV File *</label>
|
||||
<input type="file" class="w-full px-3 py-3 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-primary-100 file:text-primary-700 hover:file:bg-primary-200 transition-all duration-200" id="csvFile" name="csvFile" accept=".csv" required>
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400 mt-1">Select the CSV file to import</div>
|
||||
</div>
|
||||
<div class="md:col-span-2 flex items-end">
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded" type="checkbox" id="replaceExisting" name="replaceExisting">
|
||||
<span>Replace existing data</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="button" class="px-4 py-2 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg transition-colors duration-200 flex items-center gap-2" id="validateBtn">
|
||||
<i class="fa-regular fa-circle-check"></i>
|
||||
<span>Validate File</span>
|
||||
</button>
|
||||
<button type="submit" class="px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg transition-colors duration-200 flex items-center gap-2" id="importBtn">
|
||||
<i class="fa-solid fa-upload"></i>
|
||||
<span>Import Data</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Validation Results Panel -->
|
||||
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft hidden" id="validationPanel">
|
||||
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
|
||||
<h5 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
|
||||
<i class="fa-solid fa-clipboard-check"></i>
|
||||
<span>File Validation Results</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="p-6" id="validationResults">
|
||||
<!-- Validation results will be shown here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import Progress Panel -->
|
||||
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft hidden" id="progressPanel">
|
||||
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
|
||||
<h5 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
|
||||
<i class="fa-solid fa-hourglass-half"></i>
|
||||
<span>Import Progress</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="bg-neutral-200 dark:bg-neutral-700 rounded-full h-2 mb-3 overflow-hidden">
|
||||
<div class="bg-primary-600 h-full rounded-full transition-all duration-300" style="width: 0%" id="progressBar"></div>
|
||||
</div>
|
||||
<div id="progressStatus" class="text-sm text-neutral-600 dark:text-neutral-400">Ready to import...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import Results Panel -->
|
||||
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft hidden" id="resultsPanel">
|
||||
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
|
||||
<h5 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
|
||||
<i class="fa-solid fa-circle-check"></i>
|
||||
<span>Import Results</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="p-6" id="importResults">
|
||||
<!-- Import results will be shown here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Management Panel -->
|
||||
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft">
|
||||
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
|
||||
<h5 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
|
||||
<i class="fa-solid fa-database"></i>
|
||||
<span>Data Management</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<button class="btn btn-info" id="refreshStatusBtn">
|
||||
<i class="bi bi-arrow-clockwise"></i> Refresh Status
|
||||
</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>
|
||||
<div class="card-body">
|
||||
<div id="importStatus">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-2">Loading import status...</p>
|
||||
</div>
|
||||
<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="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>
|
||||
|
||||
<!-- 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>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
<option value="">Select data type...</option>
|
||||
</select>
|
||||
<div class="form-text" id="fileTypeDescription"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="csvFile" class="form-label">CSV File *</label>
|
||||
<input type="file" class="form-control" id="csvFile" name="csvFile" accept=".csv" required>
|
||||
<div class="form-text">Select the CSV file to import</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label"> </label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="replaceExisting" name="replaceExisting">
|
||||
<label class="form-check-label" for="replaceExisting">
|
||||
Replace existing data
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-secondary" id="validateBtn">
|
||||
<i class="bi bi-check-circle"></i> Validate File
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" id="importBtn">
|
||||
<i class="bi bi-upload"></i> Import Data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Validation Results Panel -->
|
||||
<div class="card mb-4" id="validationPanel" style="display: none;">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-clipboard-check"></i> File Validation Results</h5>
|
||||
</div>
|
||||
<div class="card-body" id="validationResults">
|
||||
<!-- Validation results will be shown here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import Progress Panel -->
|
||||
<div class="card mb-4" id="progressPanel" style="display: none;">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-hourglass-split"></i> Import Progress</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="progress mb-3">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar" style="width: 0%" id="progressBar">0%</div>
|
||||
</div>
|
||||
<div id="progressStatus">Ready to import...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import Results Panel -->
|
||||
<div class="card mb-4" id="resultsPanel" style="display: none;">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-check-circle-fill"></i> Import Results</h5>
|
||||
</div>
|
||||
<div class="card-body" id="importResults">
|
||||
<!-- Import results will be shown here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Management Panel -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-database"></i> Data Management</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>Clear Table Data</h6>
|
||||
<p class="text-muted small">Remove all records from a specific table (cannot be undone)</p>
|
||||
<div class="input-group">
|
||||
<select class="form-select" id="clearTableType">
|
||||
<option value="">Select table to clear...</option>
|
||||
</select>
|
||||
<button class="btn btn-danger" id="clearTableBtn">
|
||||
<i class="bi bi-trash"></i> Clear Table
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>Quick Actions</h6>
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-outline-warning" id="backupBtn">
|
||||
<i class="bi bi-download"></i> Download Backup
|
||||
</button>
|
||||
<button class="btn btn-outline-info" id="viewLogsBtn">
|
||||
<i class="bi bi-journal-text"></i> View Import Logs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<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="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>
|
||||
@@ -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>
|
||||
<small class="fw-bold">${fileType}</small><br>
|
||||
<small class="text-muted">${info.table_name}</small>
|
||||
</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="bg-white dark:bg-neutral-800 border ${statusBg} rounded-lg p-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<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-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();
|
||||
}
|
||||
}, 5000);
|
||||
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));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -4,78 +4,56 @@
|
||||
<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>
|
||||
|
||||
<form id="loginForm" class="login-form" novalidate>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-person"></i></span>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="invalid-feedback">Please enter your username.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-lock"></i></span>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="invalid-feedback">Please enter your password.</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary" id="loginBtn">
|
||||
<i class="bi bi-box-arrow-in-right"></i> Sign In
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<small class="text-muted">
|
||||
Default credentials: admin / admin123
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Status -->
|
||||
<div class="card login-status mt-3">
|
||||
<div class="card-body text-center py-2">
|
||||
<small class="text-muted">
|
||||
<i class="bi bi-shield-check text-success"></i>
|
||||
Secure connection established
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div 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>
|
||||
|
||||
<div>
|
||||
<button type="submit" id="loginBtn" class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-lg text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition duration-150 ease-in-out">
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
|
||||
Default credentials: admin / admin123
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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() {
|
||||
// Check for logout reason
|
||||
@@ -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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,138 +1,192 @@
|
||||
<!-- 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>
|
||||
<div class="modal-body">
|
||||
<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 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="px-6 py-4 max-h-96 overflow-y-auto scrollbar-thin">
|
||||
<form id="supportForm">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label for="contactName" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Reporter Name *</label>
|
||||
<input type="text" id="contactName" required class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
||||
</div>
|
||||
<div>
|
||||
<label for="contactEmail" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Reporter Email *</label>
|
||||
<input type="email" id="contactEmail" required class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label for="ticketCategory" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Issue Type *</label>
|
||||
<select id="ticketCategory" required class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
||||
<option value="">Select issue type...</option>
|
||||
<option value="bug_report" selected>Bug Report</option>
|
||||
<option value="qa_issue">QA Issue</option>
|
||||
<option value="feature_request">Feature Request</option>
|
||||
<option value="database_issue">Database Issue</option>
|
||||
<option value="system_error">System Error</option>
|
||||
<option value="user_access">User Access</option>
|
||||
<option value="performance">Performance Issue</option>
|
||||
<option value="documentation">Documentation</option>
|
||||
<option value="configuration">Configuration</option>
|
||||
<option value="testing">Testing Request</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="ticketPriority" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Priority</label>
|
||||
<select id="ticketPriority" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
||||
<option value="low">Low</option>
|
||||
<option value="medium" selected>Medium</option>
|
||||
<option value="high">High</option>
|
||||
<option value="urgent">Urgent</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="ticketSubject" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Issue Summary *</label>
|
||||
<input type="text" id="ticketSubject" maxlength="200" required class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
||||
<p class="text-sm text-neutral-500 dark:text-neutral-400 mt-1">Brief summary of the bug/issue</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="ticketDescription" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Detailed Description *</label>
|
||||
<textarea id="ticketDescription" rows="5" required class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200 resize-none" placeholder="Steps to reproduce:
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
Expected behavior:
|
||||
|
||||
Actual behavior:
|
||||
|
||||
Additional context:"></textarea>
|
||||
<p class="text-sm text-neutral-500 dark:text-neutral-400 mt-1">Include steps to reproduce, expected vs actual behavior, error messages, etc.</p>
|
||||
</div>
|
||||
|
||||
<!-- System Information -->
|
||||
<div class="bg-neutral-50 dark:bg-neutral-800/50 rounded-lg border border-neutral-200 dark:border-neutral-700 p-4 mb-4">
|
||||
<h3 class="text-sm font-semibold text-neutral-700 dark:text-neutral-300 mb-3 flex items-center gap-2">
|
||||
<i class="fas fa-info-circle text-info-600"></i>
|
||||
<span>System Information</span>
|
||||
<span class="text-xs font-normal text-neutral-500 dark:text-neutral-400">(automatically included)</span>
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="font-medium text-neutral-600 dark:text-neutral-400">Current Page:</span>
|
||||
<span id="currentPageInfo" class="text-neutral-900 dark:text-neutral-100 ml-1">Loading...</span>
|
||||
</div>
|
||||
<div 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>
|
||||
<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="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="ticketCategory" class="form-label">Issue Type *</label>
|
||||
<select class="form-select" id="ticketCategory" required>
|
||||
<option value="">Select issue type...</option>
|
||||
<option value="bug_report" selected>Bug Report</option>
|
||||
<option value="qa_issue">QA Issue</option>
|
||||
<option value="feature_request">Feature Request</option>
|
||||
<option value="database_issue">Database Issue</option>
|
||||
<option value="system_error">System Error</option>
|
||||
<option value="user_access">User Access</option>
|
||||
<option value="performance">Performance Issue</option>
|
||||
<option value="documentation">Documentation</option>
|
||||
<option value="configuration">Configuration</option>
|
||||
<option value="testing">Testing Request</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="ticketPriority" class="form-label">Priority</label>
|
||||
<select class="form-select" id="ticketPriority">
|
||||
<option value="low">Low</option>
|
||||
<option value="medium" selected>Medium</option>
|
||||
<option value="high">High</option>
|
||||
<option value="urgent">Urgent</option>
|
||||
</select>
|
||||
</div>
|
||||
<div 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="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>
|
||||
|
||||
<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: 1. 2. 3. Expected behavior: Actual behavior: Additional context:"></textarea>
|
||||
<div class="form-text">Include steps to reproduce, expected vs actual behavior, error messages, etc.</div>
|
||||
</div>
|
||||
|
||||
<!-- System Information (auto-populated) -->
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">
|
||||
<i class="fas fa-info-circle me-1"></i>System Information
|
||||
<small class="text-muted">(automatically included)</small>
|
||||
</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<small><strong>Current Page:</strong> <span id="currentPageInfo">Loading...</span></small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<small><strong>Browser:</strong> <span id="browserInfo">Loading...</span></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>Note:</strong> Your issue will be assigned a tracking number and the development team will be notified automatically.
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="submitSupportTicket">
|
||||
<i class="fas fa-bug me-2"></i>Submit Issue
|
||||
</button>
|
||||
</div>
|
||||
<div 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>
|
||||
|
||||
<!-- 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="px-6 py-8 text-center">
|
||||
<div class="mb-6">
|
||||
<div class="w-16 h-16 bg-success-100 dark:bg-success-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i class="fas fa-bug text-2xl text-success-600 dark:text-success-400"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-neutral-900 dark:text-neutral-100 mb-2">Issue logged successfully!</h3>
|
||||
</div>
|
||||
<div 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>
|
||||
<div class="alert alert-success">
|
||||
<strong>Issue ID:</strong> <span id="newTicketNumber"></span>
|
||||
</div>
|
||||
<p>Your issue has been logged and the development team has been notified. You'll receive updates on the resolution progress.</p>
|
||||
<div class="mt-4">
|
||||
<h6>What happens next?</h6>
|
||||
<ul class="list-unstyled text-start">
|
||||
<li><i class="fas fa-check text-success me-2"></i>Issue logged in tracking system</li>
|
||||
<li><i class="fas fa-users text-warning me-2"></i>Development team has been notified</li>
|
||||
<li><i class="fas fa-code text-info me-2"></i>Issue will be triaged and prioritized</li>
|
||||
<li><i class="fas fa-bell text-primary me-2"></i>You'll get status updates via email</li>
|
||||
</ul>
|
||||
|
||||
<div 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');
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
supportSystem.submitTicket();
|
||||
});
|
||||
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>
|
||||
Reference in New Issue
Block a user