Initial commit: Clean Mold Cost Calculator application

- Flask-based mold cost calculation application
- PostgreSQL database with SQLAlchemy ORM
- Docker/Podman containerization
- Comprehensive documentation in docs/
- Clean, organized codebase without obsolete files
- Production-ready deployment configuration
- Enhanced pytest configuration with coverage reporting
- Master/Development branch structure
This commit is contained in:
Gan, Jimmy
2025-06-24 02:30:09 +08:00
commit 6c0d5a4e63
108 changed files with 15911 additions and 0 deletions
+130
View File
@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Admin Panel{% endblock %}</title>
{% block styles %}
<!-- Bootstrap CSS -->
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap-icons.css') }}">
<!-- Admin Styles -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/admin.css') }}">
<!-- Custom Styles -->
<style>
:root {
--primary-color: #0d6efd;
--secondary-color: #6c757d;
--success-color: #198754;
--warning-color: #ffc107;
--danger-color: #dc3545;
--light-color: #f8f9fa;
--dark-color: #212529;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f8f9fa;
}
.navbar {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.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);
}
.btn {
border-radius: 0.375rem;
}
.table {
border-radius: 0.375rem;
overflow: hidden;
}
.alert {
border: none;
border-radius: 0.375rem;
}
</style>
{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('admin.dashboard') }}">
<i class="bi bi-gear-fill me-2"></i>Admin Panel
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.dashboard' %}active{% endif %}"
href="{{ url_for('admin.dashboard') }}">
<i class="bi bi-graph-up me-1"></i>Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.users' %}active{% endif %}"
href="{{ url_for('admin.users') }}">
<i class="bi bi-people me-1"></i>Users
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.quotations' %}active{% endif %}"
href="{{ url_for('admin.quotations') }}">
<i class="bi bi-file-earmark-text me-1"></i>Quotations
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.cost_factors' %}active{% endif %}"
href="{{ url_for('admin.cost_factors') }}">
<i class="bi bi-calculator me-1"></i>Cost Factors
</a>
</li>
</ul>
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.logout') }}">
<i class="bi bi-box-arrow-right me-1"></i>Logout
</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container-fluid py-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
{% block scripts %}
<!-- Bootstrap Bundle with Popper -->
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
<!-- jQuery -->
<script src="{{ url_for('static', filename='js/jquery.min.js') }}"></script>
{% endblock %}
</body>
</html>
@@ -0,0 +1,198 @@
{% extends "admin/base.html" %}
{% block admin_title %}Cost Factor History{% endblock %}
{% block admin_header %}Change History: {{ factor.name }}{% endblock %}
{% block admin_actions %}
<a href="{{ url_for('admin.cost_factors') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Cost Factors
</a>
<a href="{{ url_for('admin.edit_cost_factor', factor_id=factor.id) }}" class="btn btn-primary">
<i class="fas fa-edit"></i> Edit Factor
</a>
{% endblock %}
{% block admin_content %}
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Factor Information</h5>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-4">Name</dt>
<dd class="col-sm-8">{{ factor.name }}</dd>
<dt class="col-sm-4">Category</dt>
<dd class="col-sm-8">
<span class="badge bg-primary">{{ factor.category }}</span>
</dd>
<dt class="col-sm-4">Current Value</dt>
<dd class="col-sm-8">
<span class="badge bg-success">{{ "%.4f"|format(factor.factor_value) }}</span>
</dd>
<dt class="col-sm-4">Status</dt>
<dd class="col-sm-8">
{% if factor.is_active %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-danger">Inactive</span>
{% endif %}
</dd>
<dt class="col-sm-4">Created</dt>
<dd class="col-sm-8">{{ factor.created_at.strftime('%Y-%m-%d %H:%M') if factor.created_at else 'Unknown' }}</dd>
<dt class="col-sm-4">Last Updated</dt>
<dd class="col-sm-8">{{ factor.updated_at.strftime('%Y-%m-%d %H:%M') if factor.updated_at else 'Never' }}</dd>
</dl>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Change History</h5>
</div>
<div class="card-body">
{% if history %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Date & Time</th>
<th>Changed By</th>
<th>Old Value</th>
<th>New Value</th>
<th>Change</th>
<th>Reason</th>
</tr>
</thead>
<tbody>
{% for change in history %}
<tr>
<td>{{ change.changed_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
{% if change.user %}
<span class="badge bg-info">{{ change.user.username }}</span>
{% else %}
<span class="text-muted">Unknown</span>
{% endif %}
</td>
<td>
{% if change.old_value is not none %}
<span class="badge bg-secondary">{{ "%.4f"|format(change.old_value) }}</span>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>
<span class="badge bg-success">{{ "%.4f"|format(change.new_value) }}</span>
</td>
<td>
{% if change.old_value is not none %}
{% set diff = change.new_value - change.old_value %}
{% if diff > 0 %}
<span class="badge bg-danger">+{{ "%.4f"|format(diff) }}</span>
{% elif diff < 0 %}
<span class="badge bg-warning">{{ "%.4f"|format(diff) }}</span>
{% else %}
<span class="badge bg-secondary">No change</span>
{% endif %}
{% else %}
<span class="badge bg-info">Initial</span>
{% endif %}
</td>
<td>
{% if change.change_reason %}
<small class="text-muted">{{ change.change_reason }}</small>
{% else %}
<span class="text-muted">No reason provided</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="fas fa-history fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No Changes Yet</h5>
<p class="text-muted">This factor hasn't been modified since it was created.</p>
</div>
{% endif %}
</div>
</div>
{% if history %}
<div class="card mt-3">
<div class="card-header">
<h5 class="mb-0">Summary</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="text-center">
<h4 class="text-primary">{{ history|length }}</h4>
<small class="text-muted">Total Changes</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h4 class="text-success">{{ history|selectattr('change_reason')|list|length }}</h4>
<small class="text-muted">With Reasons</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h4 class="text-info">{{ history|selectattr('user')|list|length }}</h4>
<small class="text-muted">By Known Users</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h4 class="text-warning">{{ (history|last).changed_at.strftime('%Y-%m-%d') if history else 'N/A' }}</h4>
<small class="text-muted">Last Modified</small>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block styles %}
{{ super() }}
<style>
.badge {
font-size: 0.875em;
}
dl.row dt {
font-weight: 600;
color: #6c757d;
}
dl.row dd {
margin-bottom: 0.5rem;
}
.table th {
background-color: #f8f9fa;
border-top: none;
font-weight: 600;
}
.text-center h4 {
margin-bottom: 0.25rem;
}
</style>
{% endblock %}
+558
View File
@@ -0,0 +1,558 @@
{% extends "admin/base.html" %}
{% block title %}Cost Factors Management{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0">
<i class="bi bi-gear-fill me-2 text-primary"></i>Cost Factors Management
</h1>
<p class="text-muted mb-0">Configure calculation parameters and factors</p>
</div>
<div class="d-flex gap-2">
<button onclick="resetFactors()" class="btn btn-warning">
<i class="bi bi-arrow-clockwise me-1"></i>Reset to Defaults
</button>
<a href="{{ url_for('admin.dashboard') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back to Dashboard
</a>
</div>
</div>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
<i class="bi bi-info-circle me-2"></i>{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Calculation Logic Diagram -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-light border-0">
<h3 class="card-title mb-0">
<i class="bi bi-diagram-3 me-2 text-dark"></i>Calculation Logic
</h3>
</div>
<div class="card-body">
<div class="mermaid">
graph TD
A[Base Cost] --> B[Base Factors]
B --> C[Mold Factors]
C --> D[Complexity Factors]
D --> E[Texture Factors]
E --> F[Tax Factors]
F --> G[Final Cost]
B --> B1[Base Factor 1]
B --> B2[Base Factor 2]
C --> C1[Mold Factor 1]
C --> C2[Mold Factor 2]
D --> D1[Complexity Factor 1]
D --> D2[Complexity Factor 2]
E --> E1[Texture Factor 1]
E --> E2[Texture Factor 2]
F --> F1[Tax Rate]
</div>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-xl-2 col-md-4 col-sm-6 mb-3">
<div class="card statistics-card border-0 shadow-sm h-100">
<div class="card-body text-center">
<div class="icon-wrapper bg-primary bg-opacity-10 mx-auto mb-3">
<i class="bi bi-calculator text-primary fs-1"></i>
</div>
<h4 class="mb-1">{{ base_factors|length if base_factors else 0 }}</h4>
<p class="text-muted mb-0 small">Base Factors</p>
</div>
</div>
</div>
<div class="col-xl-2 col-md-4 col-sm-6 mb-3">
<div class="card statistics-card border-0 shadow-sm h-100">
<div class="card-body text-center">
<div class="icon-wrapper bg-success bg-opacity-10 mx-auto mb-3">
<i class="bi bi-building text-success fs-1"></i>
</div>
<h4 class="mb-1">{{ mold_factors|length if mold_factors else 0 }}</h4>
<p class="text-muted mb-0 small">Mold Factors</p>
</div>
</div>
</div>
<div class="col-xl-2 col-md-4 col-sm-6 mb-3">
<div class="card statistics-card border-0 shadow-sm h-100">
<div class="card-body text-center">
<div class="icon-wrapper bg-warning bg-opacity-10 mx-auto mb-3">
<i class="bi bi-puzzle text-warning fs-1"></i>
</div>
<h4 class="mb-1">{{ complexity_factors|length if complexity_factors else 0 }}</h4>
<p class="text-muted mb-0 small">Complexity</p>
</div>
</div>
</div>
<div class="col-xl-2 col-md-4 col-sm-6 mb-3">
<div class="card statistics-card border-0 shadow-sm h-100">
<div class="card-body text-center">
<div class="icon-wrapper bg-info bg-opacity-10 mx-auto mb-3">
<i class="bi bi-palette text-info fs-1"></i>
</div>
<h4 class="mb-1">{{ texture_factors|length if texture_factors else 0 }}</h4>
<p class="text-muted mb-0 small">Texture</p>
</div>
</div>
</div>
<div class="col-xl-2 col-md-4 col-sm-6 mb-3">
<div class="card statistics-card border-0 shadow-sm h-100">
<div class="card-body text-center">
<div class="icon-wrapper bg-danger bg-opacity-10 mx-auto mb-3">
<i class="bi bi-percent text-danger fs-1"></i>
</div>
<h4 class="mb-1">{{ tax_factors|length if tax_factors else 0 }}</h4>
<p class="text-muted mb-0 small">Tax Factors</p>
</div>
</div>
</div>
</div>
<!-- Cost Factors Sections -->
<div class="row">
<div class="col-12">
<!-- Base Factors -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-primary bg-opacity-10 border-0">
<h3 class="card-title mb-0">
<i class="bi bi-layers me-2 text-primary"></i>Base Factors
<span class="badge bg-primary ms-2">{{ base_factors|length if base_factors else 0 }}</span>
</h3>
</div>
<div class="card-body">
{% if base_factors %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="border-0">Factor Name</th>
<th class="border-0">Value</th>
<th class="border-0">Description</th>
<th class="border-0 text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for factor in base_factors %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">
<i class="bi bi-calculator text-primary"></i>
</div>
<div>
<div class="fw-medium">{{ factor.name }}</div>
<small class="text-muted">{{ factor.category }}</small>
</div>
</div>
</td>
<td>
<span class="badge bg-primary fs-6">{{ "%.2f"|format(factor.factor_value) }}</span>
</td>
<td>{{ factor.description or 'Base calculation factor' }}</td>
<td class="text-end">
<a href="{{ url_for('admin.edit_cost_factor', factor_id=factor.id) }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil me-1"></i>Edit
</a>
<a href="{{ url_for('admin.cost_factor_history', factor_id=factor.id) }}" class="btn btn-sm btn-outline-info">
<i class="bi bi-clock-history me-1"></i>History
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-exclamation-triangle fs-1 text-warning mb-3"></i>
<p class="text-muted mb-0">No base factors found.</p>
</div>
{% endif %}
</div>
</div>
<!-- Mold Factors -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-success bg-opacity-10 border-0">
<h3 class="card-title mb-0">
<i class="bi bi-building me-2 text-success"></i>Mold Factors
<span class="badge bg-success ms-2">{{ mold_factors|length if mold_factors else 0 }}</span>
</h3>
</div>
<div class="card-body">
{% if mold_factors %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="border-0">Factor Name</th>
<th class="border-0">Value</th>
<th class="border-0">Description</th>
<th class="border-0 text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for factor in mold_factors %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar-sm bg-success bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">
<i class="bi bi-box text-success"></i>
</div>
<div>
<div class="fw-medium">{{ factor.name }}</div>
<small class="text-muted">{{ factor.category }}</small>
</div>
</div>
</td>
<td>
<span class="badge bg-success fs-6">{{ "%.2f"|format(factor.factor_value) }}</span>
</td>
<td>{{ factor.description or 'Mold-related factor' }}</td>
<td class="text-end">
<a href="{{ url_for('admin.edit_cost_factor', factor_id=factor.id) }}" class="btn btn-sm btn-outline-success">
<i class="bi bi-pencil me-1"></i>Edit
</a>
<a href="{{ url_for('admin.cost_factor_history', factor_id=factor.id) }}" class="btn btn-sm btn-outline-info">
<i class="bi bi-clock-history me-1"></i>History
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-exclamation-triangle fs-1 text-warning mb-3"></i>
<p class="text-muted mb-0">No mold factors found.</p>
</div>
{% endif %}
</div>
</div>
<!-- Complexity Factors -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-warning bg-opacity-10 border-0">
<h3 class="card-title mb-0">
<i class="bi bi-puzzle me-2 text-warning"></i>Complexity Factors
<span class="badge bg-warning ms-2">{{ complexity_factors|length if complexity_factors else 0 }}</span>
</h3>
</div>
<div class="card-body">
{% if complexity_factors %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="border-0">Factor Name</th>
<th class="border-0">Value</th>
<th class="border-0">Description</th>
<th class="border-0 text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for factor in complexity_factors %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar-sm bg-warning bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">
<i class="bi bi-gear text-warning"></i>
</div>
<div>
<div class="fw-medium">{{ factor.name }}</div>
<small class="text-muted">{{ factor.category }}</small>
</div>
</div>
</td>
<td>
<span class="badge bg-warning fs-6">{{ "%.2f"|format(factor.factor_value) }}</span>
</td>
<td>{{ factor.description or 'Complexity-related factor' }}</td>
<td class="text-end">
<a href="{{ url_for('admin.edit_cost_factor', factor_id=factor.id) }}" class="btn btn-sm btn-outline-warning">
<i class="bi bi-pencil me-1"></i>Edit
</a>
<a href="{{ url_for('admin.cost_factor_history', factor_id=factor.id) }}" class="btn btn-sm btn-outline-info">
<i class="bi bi-clock-history me-1"></i>History
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-exclamation-triangle fs-1 text-warning mb-3"></i>
<p class="text-muted mb-0">No complexity factors found.</p>
</div>
{% endif %}
</div>
</div>
<!-- Texture Factors -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-info bg-opacity-10 border-0">
<h3 class="card-title mb-0">
<i class="bi bi-brush me-2 text-info"></i>Texture Factors
<span class="badge bg-info ms-2">{{ texture_factors|length if texture_factors else 0 }}</span>
</h3>
</div>
<div class="card-body">
{% if texture_factors %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="border-0">Factor Name</th>
<th class="border-0">Value</th>
<th class="border-0">Description</th>
<th class="border-0 text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for factor in texture_factors %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar-sm bg-info bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">
<i class="bi bi-palette text-info"></i>
</div>
<div>
<div class="fw-medium">{{ factor.name }}</div>
<small class="text-muted">{{ factor.category }}</small>
</div>
</div>
</td>
<td>
<span class="badge bg-info fs-6">{{ "%.2f"|format(factor.factor_value) }}</span>
</td>
<td>{{ factor.description or 'Texture-related factor' }}</td>
<td class="text-end">
<a href="{{ url_for('admin.edit_cost_factor', factor_id=factor.id) }}" class="btn btn-sm btn-outline-info">
<i class="bi bi-pencil me-1"></i>Edit
</a>
<a href="{{ url_for('admin.cost_factor_history', factor_id=factor.id) }}" class="btn btn-sm btn-outline-info">
<i class="bi bi-clock-history me-1"></i>History
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-exclamation-triangle fs-1 text-warning mb-3"></i>
<p class="text-muted mb-0">No texture factors found.</p>
</div>
{% endif %}
</div>
</div>
<!-- Tax Factors -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-danger bg-opacity-10 border-0">
<h3 class="card-title mb-0">
<i class="bi bi-percent me-2 text-danger"></i>Tax Factors
<span class="badge bg-danger ms-2">{{ tax_factors|length if tax_factors else 0 }}</span>
</h3>
</div>
<div class="card-body">
{% if tax_factors %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="border-0">Country/Region</th>
<th class="border-0">Tax Rate</th>
<th class="border-0 text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for factor in tax_factors %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar-sm bg-danger bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">
<i class="bi bi-flag text-danger"></i>
</div>
<div>
<div class="fw-medium">{{ factor.name }}</div>
<small class="text-muted">{{ factor.description or 'Tax rate' }}</small>
</div>
</div>
</td>
<td>
<span class="badge bg-danger fs-6">{{ "%.1f"|format(factor.factor_value * 100) }}%</span>
</td>
<td class="text-end">
<a href="{{ url_for('admin.edit_cost_factor', factor_id=factor.id) }}" class="btn btn-sm btn-outline-danger">
<i class="bi bi-pencil me-1"></i>Edit
</a>
<a href="{{ url_for('admin.cost_factor_history', factor_id=factor.id) }}" class="btn btn-sm btn-outline-info">
<i class="bi bi-clock-history me-1"></i>History
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-exclamation-triangle fs-1 text-warning mb-3"></i>
<p class="text-muted mb-0">No tax factors found.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Add Mermaid.js for diagram rendering -->
<script src="{{ url_for('static', filename='js/mermaid.min.js') }}"></script>
<script>
mermaid.initialize({
startOnLoad: true,
theme: 'default',
securityLevel: 'loose',
fontFamily: '"Helvetica Neue", Arial, sans-serif'
});
document.addEventListener('DOMContentLoaded', function() {
console.log('Cost Factors Template Loaded');
try {
const debugInfo = {
baseFactors: {{ base_factors|length if base_factors else 0 }},
moldFactors: {{ mold_factors|length if mold_factors else 0 }},
complexityFactors: {{ complexity_factors|length if complexity_factors else 0 }},
textureFactors: {{ texture_factors|length if texture_factors else 0 }},
taxFactors: {{ tax_factors|length if tax_factors else 0 }}
};
console.log('Debug Info:', debugInfo);
} catch (error) {
console.error('Error in debug logging:', error);
}
});
function resetFactors() {
if (confirm('Are you sure you want to reset all cost factors to their default values? This action cannot be undone.')) {
fetch('{{ url_for("admin.reset_cost_factors") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
if (response.ok) {
window.location.reload();
} else {
alert('Error resetting factors. Please try again.');
}
}).catch(error => {
console.error('Error:', error);
alert('Error resetting factors. Please try again.');
});
}
}
</script>
{% block styles %}
{{ super() }}
<style>
/* Card Styles */
.card {
transition: transform 0.2s, box-shadow 0.2s;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
/* Table Styles */
.table th {
font-weight: 600;
color: #495057;
}
.table td {
vertical-align: middle;
}
/* Badge Styles */
.badge {
padding: 0.5em 0.8em;
}
.badge.fs-6 {
font-size: 0.875rem !important;
}
/* Icon Styles */
.avatar-sm i {
font-size: 1.25rem;
}
/* Animation */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.card {
animation: fadeIn 0.3s ease-out;
}
/* Statistics Cards */
.statistics-card {
border-radius: 1rem;
overflow: hidden;
}
.statistics-card .icon-wrapper {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
/* Mermaid Diagram */
.mermaid {
background: white;
padding: 1rem;
border-radius: 0.5rem;
}
/* Responsive Design */
@media (max-width: 768px) {
.statistics-card {
margin-bottom: 1rem;
}
.table-responsive {
margin: 0 -1rem;
}
}
</style>
{% endblock %}
{% endblock %}
+245
View File
@@ -0,0 +1,245 @@
{% extends "admin/base.html" %}
{% block title %}Admin Dashboard{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0">
<i class="bi bi-speedometer2 me-2 text-primary"></i>Admin Dashboard
</h1>
<p class="text-muted mb-0">Overview of system statistics and recent activity</p>
</div>
</div>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
<i class="bi bi-info-circle me-2"></i>{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="text-muted mb-1">Total Users</h6>
<h3 class="mb-0">{{ total_users }}</h3>
</div>
<div class="bg-primary bg-opacity-10 rounded-circle p-3">
<i class="bi bi-people text-primary fs-1"></i>
</div>
</div>
<div class="mt-3">
<span class="badge bg-success">
<i class="bi bi-check-circle me-1"></i>{{ active_users }} active
</span>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="text-muted mb-1">Total Quotations</h6>
<h3 class="mb-0">{{ total_quotations }}</h3>
</div>
<div class="bg-success bg-opacity-10 rounded-circle p-3">
<i class="bi bi-file-earmark-text text-success fs-1"></i>
</div>
</div>
<div class="mt-3">
<span class="badge bg-info">
<i class="bi bi-graph-up me-1"></i>All time
</span>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="text-muted mb-1">Active Regions</h6>
<h3 class="mb-0">{{ active_regions }}</h3>
</div>
<div class="bg-warning bg-opacity-10 rounded-circle p-3">
<i class="bi bi-globe text-warning fs-1"></i>
</div>
</div>
<div class="mt-3">
<span class="badge bg-warning">
<i class="bi bi-flag me-1"></i>Active regions
</span>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="text-muted mb-1">Recent Quotes</h6>
<h3 class="mb-0">{{ recent_quotations|length }}</h3>
</div>
<div class="bg-info bg-opacity-10 rounded-circle p-3">
<i class="bi bi-clock text-info fs-1"></i>
</div>
</div>
<div class="mt-3">
<span class="badge bg-info">
<i class="bi bi-calendar me-1"></i>Last 5 quotes
</span>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Recent Activity -->
<div class="col-lg-8 mb-4">
<div class="card border-0 shadow-sm">
<div class="card-header bg-light border-0">
<h5 class="card-title mb-0">
<i class="bi bi-graph-up me-2 text-primary"></i>Recent Activity
</h5>
</div>
<div class="card-body">
{% if recent_quotations %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>User</th>
<th>Project</th>
<th>Amount</th>
<th>Date</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for quotation in recent_quotations %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 32px; height: 32px;">
<i class="bi bi-person text-primary"></i>
</div>
<div>
<div class="fw-medium">{{ quotation.user.username }}</div>
<small class="text-muted">{{ quotation.user.email }}</small>
</div>
</div>
</td>
<td>{{ quotation.project_name }}</td>
<td>
<span class="badge bg-success">${{ "%.2f"|format(quotation.total_cost) }}</span>
</td>
<td>{{ quotation.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<span class="badge bg-primary">Completed</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="text-center mt-3">
<a href="{{ url_for('admin.quotations') }}" class="btn btn-outline-primary">
<i class="bi bi-eye me-1"></i>View All Quotations
</a>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-file-earmark-text fs-1 text-muted mb-3"></i>
<p class="text-muted mb-0">No recent quotations found.</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="col-lg-4 mb-4">
<div class="card border-0 shadow-sm">
<div class="card-header bg-light border-0">
<h5 class="card-title mb-0">
<i class="bi bi-lightning me-2 text-warning"></i>Quick Actions
</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{{ url_for('admin.users') }}" class="btn btn-outline-primary">
<i class="bi bi-people me-2"></i>Manage Users
</a>
<a href="{{ url_for('admin.quotations') }}" class="btn btn-outline-success">
<i class="bi bi-file-earmark-text me-2"></i>View Quotations
</a>
<a href="{{ url_for('admin.cost_factors') }}" class="btn btn-outline-warning">
<i class="bi bi-gear me-2"></i>Cost Factors
</a>
<a href="{{ url_for('admin.cost_factors') }}" class="btn btn-outline-info">
<i class="bi bi-calculator me-2"></i>Calculator Settings
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.card {
transition: transform 0.2s, box-shadow 0.2s;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
.avatar-sm {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.table th {
font-weight: 600;
color: #495057;
}
.table td {
vertical-align: middle;
}
.badge {
font-size: 0.875em;
}
.btn {
border-radius: 0.375rem;
}
</style>
{% endblock %}
+238
View File
@@ -0,0 +1,238 @@
{% extends "admin/base.html" %}
{% block title %}Edit Cost Factor: {{ factor.name }}{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0">
<i class="bi bi-pencil-square me-2 text-primary"></i>Edit Cost Factor
</h1>
<p class="text-muted mb-0">{{ factor.name }} ({{ factor.category }})</p>
</div>
<div class="d-flex gap-2">
<a href="{{ url_for('admin.cost_factors') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i> Back to Cost Factors
</a>
</div>
</div>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
<i class="bi bi-info-circle me-2"></i>{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="row">
<div class="col-lg-8">
<div class="card border-0 shadow-sm">
<div class="card-header bg-light border-0">
<h5 class="card-title mb-0">
<i class="bi bi-gear me-2"></i>Edit Factor Details
</h5>
</div>
<div class="card-body">
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="category" class="form-label fw-semibold">Category</label>
<input type="text" class="form-control bg-light" id="category" value="{{ factor.category }}" readonly>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="name" class="form-label fw-semibold">Name</label>
<input type="text" class="form-control bg-light" id="name" value="{{ factor.name }}" readonly>
</div>
</div>
</div>
<div class="mb-3">
<label for="factor_value" class="form-label fw-semibold">
Factor Value <span class="text-danger">*</span>
</label>
<input type="number" class="form-control form-control-lg" id="factor_value" name="factor_value"
value="{{ "%.4f"|format(factor.factor_value) }}" step="0.0001" min="0" required>
<div class="form-text">
{% if factor.category == 'tax' %}
<i class="bi bi-info-circle me-1"></i>Enter as decimal (e.g., 0.06 for 6%)
{% elif factor.category == 'base' %}
<i class="bi bi-info-circle me-1"></i>Enter the base price value
{% else %}
<i class="bi bi-info-circle me-1"></i>Enter the multiplier factor
{% endif %}
</div>
</div>
{% if factor.calculation_formula %}
<div class="mb-3">
<label for="calculation_formula" class="form-label fw-semibold">Calculation Formula</label>
<input type="text" class="form-control bg-light" id="calculation_formula"
value="{{ factor.calculation_formula }}" readonly>
</div>
{% endif %}
{% if factor.percentage %}
<div class="mb-3">
<label for="percentage" class="form-label fw-semibold">Percentage</label>
<input type="text" class="form-control bg-light" id="percentage"
value="{{ factor.percentage }}" readonly>
</div>
{% endif %}
<div class="mb-3">
<label for="description" class="form-label fw-semibold">Description</label>
<textarea class="form-control bg-light" id="description" rows="3" readonly>{{ factor.description }}</textarea>
</div>
<div class="mb-4">
<label for="change_reason" class="form-label fw-semibold">
<i class="bi bi-chat-text me-1"></i>Reason for Change
</label>
<textarea class="form-control" id="change_reason" name="change_reason" rows="3"
placeholder="Please provide a reason for this change (optional but recommended)"></textarea>
<div class="form-text">
<i class="bi bi-clock-history me-1"></i>This will be recorded in the change history for audit purposes.
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('admin.cost_factors') }}" class="btn btn-outline-secondary">
<i class="bi bi-x-circle me-1"></i> Cancel
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle me-1"></i> Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-light border-0">
<h5 class="card-title mb-0">
<i class="bi bi-info-circle me-2"></i>Current Information
</h5>
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-4 fw-semibold">Category</dt>
<dd class="col-sm-8">
<span class="badge bg-primary">{{ factor.category }}</span>
</dd>
<dt class="col-sm-4 fw-semibold">Name</dt>
<dd class="col-sm-8">{{ factor.name }}</dd>
<dt class="col-sm-4 fw-semibold">Current Value</dt>
<dd class="col-sm-8">
<span class="badge bg-success fs-6">{{ "%.4f"|format(factor.factor_value) }}</span>
</dd>
<dt class="col-sm-4 fw-semibold">Status</dt>
<dd class="col-sm-8">
{% if factor.is_active %}
<span class="badge bg-success">
<i class="bi bi-check-circle me-1"></i>Active
</span>
{% else %}
<span class="badge bg-danger">
<i class="bi bi-x-circle me-1"></i>Inactive
</span>
{% endif %}
</dd>
<dt class="col-sm-4 fw-semibold">Created</dt>
<dd class="col-sm-8">{{ factor.created_at.strftime('%Y-%m-%d %H:%M') if factor.created_at else 'Unknown' }}</dd>
<dt class="col-sm-4 fw-semibold">Last Updated</dt>
<dd class="col-sm-8">{{ factor.updated_at.strftime('%Y-%m-%d %H:%M') if factor.updated_at else 'Never' }}</dd>
{% if factor.updater %}
<dt class="col-sm-4 fw-semibold">Updated By</dt>
<dd class="col-sm-8">{{ factor.updater.username }}</dd>
{% endif %}
</dl>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-header bg-light border-0">
<h5 class="card-title mb-0">
<i class="bi bi-lightning me-2"></i>Quick Actions
</h5>
</div>
<div class="card-body">
<a href="{{ url_for('admin.cost_factor_history', factor_id=factor.id) }}"
class="btn btn-outline-info btn-sm w-100 mb-2">
<i class="bi bi-clock-history me-1"></i> View Change History
</a>
<a href="{{ url_for('admin.cost_factors') }}"
class="btn btn-outline-secondary btn-sm w-100">
<i class="bi bi-list me-1"></i> Back to All Factors
</a>
</div>
</div>
</div>
</div>
</div>
<style>
.form-control[readonly] {
background-color: #f8f9fa !important;
border-color: #dee2e6;
color: #6c757d;
}
.form-control:focus {
border-color: #0d6efd;
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
}
.badge {
font-size: 0.875em;
}
dl.row dt {
color: #6c757d;
font-size: 0.9rem;
}
dl.row dd {
margin-bottom: 0.75rem;
font-size: 0.9rem;
}
.card {
transition: transform 0.2s, box-shadow 0.2s;
}
.card:hover {
transform: translateY(-1px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
.btn {
border-radius: 0.375rem;
}
.form-control-lg {
font-size: 1.1rem;
padding: 0.75rem 1rem;
}
</style>
{% endblock %}
+146
View File
@@ -0,0 +1,146 @@
{% extends "admin/base.html" %}
{% block admin_title %}Quotation Details{% endblock %}
{% block admin_header %}Quotation Details{% endblock %}
{% block admin_actions %}
<a href="{{ url_for('admin.quotations') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Quotations
</a>
{% endblock %}
{% block admin_content %}
{% from "_quotation_details.html" import render_quotation_details %}
{{ render_quotation_details(quotation) }}
{% endblock %}
{% block styles %}
{{ super() }}
<style>
.quotation-details {
display: flex;
flex-direction: column;
gap: 30px;
}
.detail-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.detail-card h3 {
margin: 0 0 20px;
color: #2c3e50;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 5px;
}
.detail-item label {
color: #6c757d;
font-size: 0.9rem;
}
.detail-item span {
font-size: 1.1rem;
color: #2c3e50;
}
.cost-breakdown {
display: flex;
flex-direction: column;
gap: 15px;
}
.cost-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #dee2e6;
}
.cost-item:last-child {
border-bottom: none;
}
.cost-item label {
color: #6c757d;
font-size: 1rem;
}
.cost-item span {
font-size: 1.1rem;
color: #2c3e50;
font-weight: 500;
}
.cost-item.total {
margin-top: 10px;
padding-top: 20px;
border-top: 2px solid #dee2e6;
}
.cost-item.total label {
font-size: 1.2rem;
font-weight: 600;
color: #2c3e50;
}
.cost-item.total span {
font-size: 1.4rem;
font-weight: 700;
color: #28a745;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
display: inline-flex;
align-items: center;
gap: 5px;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.badge {
padding: 5px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.badge-success {
background-color: #28a745;
color: white;
}
@media (max-width: 768px) {
.detail-grid {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
+174
View File
@@ -0,0 +1,174 @@
{% extends "admin/base.html" %}
{% block title %}Quotations - Admin{% endblock %}
{% block content %}
<div class="admin-container">
<div class="admin-card">
<div class="admin-card-header">
<h2 class="mb-0">Quotations Management</h2>
</div>
<div class="admin-card-body">
<div class="admin-filters">
<form method="get" class="admin-filter-form" id="quotationFilterForm">
<div class="admin-form-group">
<select name="country" class="admin-form-control">
<option value="">All Countries</option>
{% for country in countries %}
<option value="{{ country }}" {% if filters.country == country %}selected{% endif %}>
{{ country }}
</option>
{% endfor %}
</select>
</div>
<div class="admin-form-group">
<select name="mold_type" class="admin-form-control">
<option value="">All Mold Types</option>
{% for type in mold_types %}
<option value="{{ type }}" {% if filters.mold_type == type %}selected{% endif %}>
{{ type }}
</option>
{% endfor %}
</select>
</div>
<div class="admin-form-group">
<input type="date" name="date_from" id="date_from" class="admin-form-control"
value="{{ filters.date_from.strftime('%Y-%m-%d') if filters.date_from else '' }}"
placeholder="From Date">
</div>
<div class="admin-form-group">
<input type="date" name="date_to" id="date_to" class="admin-form-control"
value="{{ filters.date_to.strftime('%Y-%m-%d') if filters.date_to else '' }}"
placeholder="To Date">
</div>
<div class="admin-form-group">
<button type="submit" class="admin-btn admin-btn-primary">Filter</button>
<a href="{{ url_for('admin.quotations') }}" class="admin-btn admin-btn-secondary">Clear</a>
</div>
</form>
</div>
{% if quotations.items %}
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th>ID</th>
<th>User</th>
<th>Model</th>
<th>Country</th>
<th>Mold Type</th>
<th>Base Price</th>
<th>Total Cost</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for quotation in quotations.items %}
<tr>
<td>{{ quotation.id }}</td>
<td>{{ quotation.user.username }}</td>
<td>{{ quotation.model_name }}</td>
<td>{{ quotation.mold_shop_country }}</td>
<td>{{ quotation.mold_main_type }}</td>
<td>${{ "%.2f"|format(quotation.base_price) }}</td>
<td>${{ "%.2f"|format(quotation.total_cost) }}</td>
<td>{{ quotation.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<a href="{{ url_for('admin.quotation_detail', id=quotation.id) }}"
class="admin-btn admin-btn-info admin-btn-sm">
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if quotations.pages > 1 %}
<div class="admin-pagination">
{% if quotations.has_prev %}
<a href="{{ url_for('admin.quotations', page=quotations.prev_num, **filters) }}"
class="admin-pagination-link">Previous</a>
{% endif %}
<span class="admin-pagination-info">
Page {{ quotations.page }} of {{ quotations.pages }}
</span>
{% if quotations.has_next %}
<a href="{{ url_for('admin.quotations', page=quotations.next_num, **filters) }}"
class="admin-pagination-link">Next</a>
{% endif %}
</div>
{% endif %}
{% else %}
<div class="text-center py-4">
<p class="text-muted mb-0">No quotations found matching your criteria.</p>
{% if filters.country or filters.mold_type or filters.date_from or filters.date_to %}
<a href="{{ url_for('admin.quotations') }}" class="admin-btn admin-btn-primary mt-3">Clear Filters</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Function to get today's date in YYYY-MM-DD format
function getTodayDate() {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// Get date input elements
const dateFromInput = document.getElementById('date_from');
const dateToInput = document.getElementById('date_to');
const form = document.getElementById('quotationFilterForm');
// Set default values if empty
if (!dateFromInput.value) {
dateFromInput.value = getTodayDate();
}
if (!dateToInput.value) {
dateToInput.value = getTodayDate();
}
// Prevent form submission if dates are invalid
form.addEventListener('submit', function(event) {
if (!dateFromInput.value || !dateToInput.value) {
event.preventDefault();
if (!dateFromInput.value) dateFromInput.value = getTodayDate();
if (!dateToInput.value) dateToInput.value = getTodayDate();
}
});
// Add event listeners to handle invalid dates
dateFromInput.addEventListener('change', function() {
if (!this.value) this.value = getTodayDate();
});
dateToInput.addEventListener('change', function() {
if (!this.value) this.value = getTodayDate();
});
});
</script>
{% endblock %}
{% block styles %}
{{ super() }}
<link rel="stylesheet" href="{{ url_for('static', filename='css/admin.css') }}">
{% endblock %}
+1
View File
@@ -0,0 +1 @@
+253
View File
@@ -0,0 +1,253 @@
{% extends "admin/base.html" %}
{% block title %}Users - Admin Panel{% endblock %}
{% block content %}
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form method="get" class="row g-3 align-items-end">
<div class="col-md-8">
<label for="search" class="form-label">Search Users</label>
<input type="text" id="search" name="search" placeholder="Search by username or email..."
value="{{ request.args.get('search', '') }}" class="form-control">
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-search me-2"></i>Search
</button>
</div>
</form>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-header bg-transparent border-0">
<h5 class="card-title mb-0">
<i class="fas fa-users me-2 text-primary"></i>
User Management
</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="border-0">ID</th>
<th class="border-0">Username</th>
<th class="border-0">Email</th>
<th class="border-0">Status</th>
<th class="border-0">Role</th>
<th class="border-0">Last Login</th>
<th class="border-0">Created</th>
<th class="border-0 text-center">Actions</th>
</tr>
</thead>
<tbody>
{% for user in users.items %}
<tr>
<td>
<span class="badge bg-secondary">#{{ user.id }}</span>
</td>
<td>
<div class="d-flex align-items-center">
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px;">
<i class="fas fa-user text-primary"></i>
</div>
<span class="fw-medium">{{ user.username }}</span>
</div>
</td>
<td>
<span class="text-muted">{{ user.email }}</span>
</td>
<td>
<span class="badge bg-{{ 'success' if user.is_active else 'danger' }}">
<i class="fas fa-{{ 'check-circle' if user.is_active else 'times-circle' }} me-1"></i>
{{ 'Active' if user.is_active else 'Inactive' }}
</span>
</td>
<td>
<span class="badge bg-{{ 'primary' if user.is_admin else 'secondary' }}">
<i class="fas fa-{{ 'crown' if user.is_admin else 'user' }} me-1"></i>
{{ 'Admin' if user.is_admin else 'User' }}
</span>
</td>
<td>
<small class="text-muted">
{{ user.last_login.strftime('%m/%d/%Y %H:%M') if user.last_login else 'Never' }}
</small>
</td>
<td>
<small class="text-muted">
{{ user.created_at.strftime('%m/%d/%Y') }}
</small>
</td>
<td class="text-center">
<div class="btn-group" role="group">
<a href="{{ url_for('admin.user_detail', user_id=user.id) }}"
class="btn btn-outline-info btn-sm"
title="View Details">
<i class="fas fa-eye"></i>
</a>
<button type="button"
class="btn btn-{{ 'warning' if user.is_active else 'success' }} btn-sm"
onclick="toggleUserStatus({{ user.id }}, '{{ 'warning' if user.is_active else 'success' }}')"
title="{{ 'Deactivate' if user.is_active else 'Activate' }} User">
<i class="fas fa-{{ 'ban' if user.is_active else 'check' }}"></i>
</button>
{% if user.id != current_user.id %}
<button type="button"
class="btn btn-outline-danger btn-sm"
onclick="deleteUser({{ user.id }})"
title="Delete User">
<i class="fas fa-trash"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Pagination -->
{% if users.pages > 1 %}
<div class="d-flex justify-content-between align-items-center mt-4">
<div class="text-muted">
Showing {{ users.items|length }} of {{ users.total }} users
</div>
<nav aria-label="User pagination">
<ul class="pagination pagination-sm mb-0">
{% if users.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.users', page=users.prev_num, search=request.args.get('search', '')) }}">
<i class="fas fa-chevron-left"></i>
</a>
</li>
{% endif %}
{% for page_num in users.iter_pages() %}
{% if page_num %}
{% if page_num != users.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.users', page=page_num, search=request.args.get('search', '')) }}">
{{ page_num }}
</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if users.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.users', page=users.next_num, search=request.args.get('search', '')) }}">
<i class="fas fa-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
function toggleUserStatus(userId, status) {
const button = event.target.closest('button');
const isActive = status === 'warning';
const action = isActive ? 'deactivate' : 'activate';
if (!confirm(`Are you sure you want to ${action} this user?`)) {
return;
}
fetch(`/admin/user/${userId}/toggle`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
location.reload();
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while updating user status');
});
}
function deleteUser(userId) {
if (!confirm('Are you sure you want to delete this user? This action cannot be undone.')) {
return;
}
fetch('/admin/delete_user', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify({ user_id: userId })
})
.then(response => response.json())
.then(data => {
if (data.message) {
location.reload();
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while deleting the user');
});
}
</script>
{% endblock %}
{% block styles %}
{{ super() }}
<style>
.avatar-sm {
font-size: 0.875rem;
}
.table th {
font-weight: 600;
color: #495057;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.table td {
vertical-align: middle;
}
.btn-group .btn {
margin-right: 2px;
}
.btn-group .btn:last-child {
margin-right: 0;
}
</style>
{% endblock %}