Initial Codes
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Attendance Report - QR Code Management{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="attendance-page">
|
||||
<div class="attendance-header">
|
||||
<div class="header-content">
|
||||
<h1>
|
||||
<i class="fas fa-chart-line"></i>
|
||||
Attendance Report
|
||||
</h1>
|
||||
<p>Monitor and analyze staff attendance across all locations</p>
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
<button onclick="exportAttendance()" class="btn btn-success">
|
||||
<i class="fas fa-download"></i>
|
||||
Export Data
|
||||
</button>
|
||||
<button onclick="refreshReport()" class="btn btn-secondary">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="stats-section">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card primary">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-user-check"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ stats.total_checkins or 0 }}</h3>
|
||||
<p>Total Check-ins</p>
|
||||
<span class="stat-trend">All time</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card success">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-users"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ stats.unique_employees or 0 }}</h3>
|
||||
<p>Unique Employees</p>
|
||||
<span class="stat-trend">Have checked in</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card info">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ stats.active_locations or 0 }}</h3>
|
||||
<p>Active Locations</p>
|
||||
<span class="stat-trend">With check-ins</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card warning">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-calendar-day"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ stats.today_checkins or 0 }}</h3>
|
||||
<p>Today's Check-ins</p>
|
||||
<span class="stat-trend">{{ "now"|strftime('%B %d') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="filters-section">
|
||||
<div class="filters-card">
|
||||
<div class="filters-header">
|
||||
<h3>
|
||||
<i class="fas fa-filter"></i>
|
||||
Filter Records
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<form method="GET" class="filters-form" id="filtersForm">
|
||||
<div class="filter-row">
|
||||
<div class="filter-group">
|
||||
<label for="date">
|
||||
<i class="fas fa-calendar"></i>
|
||||
Date
|
||||
</label>
|
||||
<input type="date"
|
||||
id="date"
|
||||
name="date"
|
||||
value="{{ date_filter }}"
|
||||
max="{{ 'now'|strftime('%Y-%m-%d') }}">
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="location">
|
||||
<i class="fas fa-building"></i>
|
||||
Location
|
||||
</label>
|
||||
<select id="location" name="location">
|
||||
<option value="">All Locations</option>
|
||||
{% for location in locations %}
|
||||
<option value="{{ location }}"
|
||||
{{ 'selected' if location == location_filter else '' }}>
|
||||
{{ location }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="employee">
|
||||
<i class="fas fa-id-badge"></i>
|
||||
Employee ID
|
||||
</label>
|
||||
<input type="text"
|
||||
id="employee"
|
||||
name="employee"
|
||||
value="{{ employee_filter }}"
|
||||
placeholder="Enter Employee ID">
|
||||
</div>
|
||||
|
||||
<div class="filter-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-search"></i>
|
||||
Apply Filters
|
||||
</button>
|
||||
<button type="button" onclick="clearFilters()" class="btn btn-outline">
|
||||
<i class="fas fa-times"></i>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Attendance Table -->
|
||||
<div class="attendance-table-section">
|
||||
<div class="table-header">
|
||||
<h3>
|
||||
<i class="fas fa-list"></i>
|
||||
Attendance Records
|
||||
</h3>
|
||||
<div class="table-controls">
|
||||
<div class="entries-per-page">
|
||||
<label>Show:</label>
|
||||
<select id="entriesPerPage" onchange="changeEntriesPerPage()">
|
||||
<option value="25">25</option>
|
||||
<option value="50" selected>50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="all">All</option>
|
||||
</select>
|
||||
<span>entries</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
{% if attendance_records %}
|
||||
<table class="attendance-table" id="attendanceTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th onclick="sortTable(0)">#</th>
|
||||
<th onclick="sortTable(1)">Employee ID <i class="fas fa-sort"></i></th>
|
||||
<th onclick="sortTable(2)">Location <i class="fas fa-sort"></i></th>
|
||||
<th onclick="sortTable(3)">Event <i class="fas fa-sort"></i></th>
|
||||
<th onclick="sortTable(4)">Date <i class="fas fa-sort"></i></th>
|
||||
<th onclick="sortTable(5)">Time <i class="fas fa-sort"></i></th>
|
||||
<th onclick="sortTable(6)">Device <i class="fas fa-sort"></i></th>
|
||||
<th onclick="sortTable(7)">Status <i class="fas fa-sort"></i></th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for record in attendance_records %}
|
||||
<tr data-record-id="{{ record.id }}">
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>
|
||||
<div class="employee-info">
|
||||
<span class="employee-id">{{ record.employee_id }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="location-info">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
{{ record.location_name }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="event-info">
|
||||
{{ record.location_event }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="date-info">
|
||||
{{ record.check_in_date.strftime('%Y-%m-%d') }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="time-info">
|
||||
{{ record.check_in_time.strftime('%H:%M') }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="device-info">
|
||||
<i class="fas fa-mobile-alt"></i>
|
||||
<span title="{{ record.device_info }}">
|
||||
{{ record.device_info[:20] }}{% if record.device_info|length > 20 %}...{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge {{ record.status }}">
|
||||
<i class="fas {{ 'fa-check-circle' if record.status == 'present' else 'fa-times-circle' }}"></i>
|
||||
{{ record.status.title() }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="record-actions">
|
||||
<button onclick="viewRecordDetails({{ record.id }})"
|
||||
class="action-btn btn-view"
|
||||
title="View Details">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
<button onclick="editRecord({{ record.id }})"
|
||||
class="action-btn btn-edit"
|
||||
title="Edit Record">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button onclick="deleteRecord({{ record.id }})"
|
||||
class="action-btn btn-delete"
|
||||
title="Delete Record">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<i class="fas fa-clipboard-list"></i>
|
||||
</div>
|
||||
<h3>No Attendance Records Found</h3>
|
||||
<p>{% if date_filter or location_filter or employee_filter %}
|
||||
No records match your current filters. Try adjusting the filter criteria.
|
||||
{% else %}
|
||||
No staff have checked in yet. QR codes need to be scanned to generate attendance data.
|
||||
{% endif %}</p>
|
||||
<button onclick="clearFilters()" class="btn btn-primary">
|
||||
<i class="fas fa-refresh"></i>
|
||||
Clear Filters
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pagination-container" id="paginationContainer">
|
||||
<!-- Pagination will be dynamically generated -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts Section -->
|
||||
<div class="charts-section">
|
||||
<div class="charts-grid">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<h3>
|
||||
<i class="fas fa-chart-bar"></i>
|
||||
Daily Check-ins (Last 7 Days)
|
||||
</h3>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="dailyChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<h3>
|
||||
<i class="fas fa-chart-pie"></i>
|
||||
Check-ins by Location
|
||||
</h3>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="locationChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Record Details Modal -->
|
||||
<div id="recordModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="modalTitle">Attendance Record Details</h3>
|
||||
<button class="modal-close" onclick="closeRecordModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" id="modalBody">
|
||||
<!-- Dynamic content will be loaded here -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button onclick="closeRecordModal()" class="btn btn-secondary">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/attendance_report.js') }}"></script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,144 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{% block title %}QR Code Management System{% endblock %}</title>
|
||||
|
||||
<!-- Main CSS -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="{{ url_for('static', filename='css/style.css') }}"
|
||||
/>
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Page-specific CSS -->
|
||||
{% block extra_head %}{% endblock %}
|
||||
|
||||
<!-- Favicon -->
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/x-icon"
|
||||
href="{{ url_for('static', filename='favicon.ico') }}"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation Bar -->
|
||||
{% if session.user_id %}
|
||||
<nav class="navbar">
|
||||
<div class="nav-container">
|
||||
<div class="nav-brand">
|
||||
<i class="fas fa-qrcode"></i>
|
||||
<span>QR Manager</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-menu" id="navMenu">
|
||||
<a href="{{ url_for('dashboard') }}" class="nav-link">
|
||||
<i class="fas fa-tachometer-alt"></i> Dashboard
|
||||
</a>
|
||||
|
||||
{% if session.role == 'admin' %}
|
||||
<a href="{{ url_for('users') }}" class="nav-link">
|
||||
<i class="fas fa-users"></i> Users
|
||||
</a>
|
||||
<a href="{{ url_for('attendance_report') }}" class="nav-link">
|
||||
<i class="fas fa-chart-line"></i> Reports
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ url_for('create_qr_code') }}" class="nav-link">
|
||||
<i class="fas fa-plus"></i> Create QR
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('profile') }}" class="nav-link">
|
||||
<i class="fas fa-user"></i> Profile
|
||||
</a>
|
||||
|
||||
<!-- Theme Toggle -->
|
||||
<button
|
||||
id="themeToggle"
|
||||
class="nav-link"
|
||||
style="background: none; border: none"
|
||||
>
|
||||
<i class="fas fa-moon"></i>
|
||||
</button>
|
||||
|
||||
<a href="{{ url_for('logout') }}" class="nav-link logout">
|
||||
<i class="fas fa-sign-out-alt"></i> Logout
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<div class="nav-toggle" id="mobile-menu">
|
||||
<span class="bar"></span>
|
||||
<span class="bar"></span>
|
||||
<span class="bar"></span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<!-- Flash Messages -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %} {% if
|
||||
messages %}
|
||||
<div class="flash-messages">
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">
|
||||
<i
|
||||
class="fas {% if category == 'success' %}fa-check-circle{% elif category == 'error' %}fa-exclamation-circle{% elif category == 'info' %}fa-info-circle{% else %}fa-exclamation-triangle{% endif %}"
|
||||
></i>
|
||||
{{ message }}
|
||||
<button
|
||||
class="alert-close"
|
||||
onclick="this.parentElement.style.display='none'"
|
||||
>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %} {% endwith %}
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="container">{% block content %}{% endblock %}</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<p>© 2025 QR Code Management System. All rights reserved.</p>
|
||||
<div class="footer-links">
|
||||
<a href="#" class="footer-link">Privacy Policy</a>
|
||||
<a href="#" class="footer-link">Terms of Service</a>
|
||||
<a href="#" class="footer-link">Support</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Main JavaScript -->
|
||||
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
|
||||
|
||||
<!-- Page-specific JavaScript -->
|
||||
{% block extra_scripts %}{% endblock %}
|
||||
|
||||
<!-- Global JavaScript Variables -->
|
||||
<script>
|
||||
// Pass Flask session data to JavaScript
|
||||
window.qrConfig = {
|
||||
currentUserId: {{ session.user_id|default('null') }},
|
||||
currentUserRole: '{{ session.role|default('') }}',
|
||||
currentUserName: '{{ session.full_name|default('') }}',
|
||||
csrfToken: '{{ csrf_token() if csrf_token else '' }}'
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,609 @@
|
||||
{% extends "base.html" %} {% block title %}Confirm Delete - QR Code Management{%
|
||||
endblock %} {% block extra_head %}
|
||||
<style>
|
||||
.confirmation-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 70vh;
|
||||
padding: var(--spacing-8) 0;
|
||||
}
|
||||
|
||||
.confirmation-card {
|
||||
background: var(--white);
|
||||
border-radius: var(--radius-2xl);
|
||||
box-shadow: var(--shadow-xl);
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.confirmation-header {
|
||||
background: linear-gradient(135deg, var(--danger-color), #b91c1c);
|
||||
color: var(--white);
|
||||
padding: var(--spacing-8);
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: var(--radius-full);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto var(--spacing-4);
|
||||
font-size: 2rem;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.confirmation-header h2 {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.confirmation-body {
|
||||
padding: var(--spacing-8);
|
||||
}
|
||||
|
||||
.qr-details-section {
|
||||
display: flex;
|
||||
gap: var(--spacing-6);
|
||||
margin-bottom: var(--spacing-8);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.qr-preview {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.qr-preview img {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 2px solid var(--gray-200);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.deletion-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.deletion-details h3 {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 700;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--spacing-4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
margin-bottom: var(--spacing-3);
|
||||
padding: var(--spacing-3);
|
||||
background: var(--gray-50);
|
||||
border-radius: var(--radius);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-weight: 600;
|
||||
color: var(--gray-600);
|
||||
min-width: 100px;
|
||||
margin-right: var(--spacing-3);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: var(--gray-900);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.warning-message {
|
||||
background: linear-gradient(135deg, var(--danger-light), #fecaca);
|
||||
border: 2px solid var(--danger-color);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-6);
|
||||
margin-bottom: var(--spacing-6);
|
||||
}
|
||||
|
||||
.warning-message p {
|
||||
margin: 0 0 var(--spacing-3) 0;
|
||||
color: var(--danger-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.warning-message p:first-child {
|
||||
font-size: var(--font-size-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.warning-message ul {
|
||||
margin: var(--spacing-4) 0 0 0;
|
||||
padding-left: var(--spacing-6);
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.warning-message li {
|
||||
margin-bottom: var(--spacing-2);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.confirmation-actions {
|
||||
background: var(--gray-50);
|
||||
padding: var(--spacing-6);
|
||||
display: flex;
|
||||
gap: var(--spacing-4);
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.btn-delete-confirm {
|
||||
background: var(--danger-color);
|
||||
color: var(--white);
|
||||
border: 2px solid var(--danger-color);
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-delete-confirm:hover {
|
||||
background: #b91c1c;
|
||||
border-color: #b91c1c;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.btn-delete-confirm::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.2),
|
||||
transparent
|
||||
);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.btn-delete-confirm:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.qr-status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-1);
|
||||
padding: var(--spacing-1) var(--spacing-2);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.qr-status-badge.active {
|
||||
background: var(--success-light);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.qr-status-badge.inactive {
|
||||
background: var(--danger-light);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.countdown-timer {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--gray-600);
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
.countdown-number {
|
||||
background: var(--primary-color);
|
||||
color: var(--white);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: var(--radius-full);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.confirmation-page {
|
||||
padding: var(--spacing-4);
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.qr-details-section {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr-preview {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.confirmation-actions {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.confirmation-actions .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-1);
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
min-width: auto;
|
||||
margin-right: 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation for page entry */
|
||||
.confirmation-card {
|
||||
animation: slideInScale 0.4s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInScale {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %} {% block content %}
|
||||
<div class="confirmation-page">
|
||||
<div class="confirmation-card">
|
||||
<div class="confirmation-header">
|
||||
<div class="warning-icon">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
</div>
|
||||
<h2>Confirm QR Code Deletion</h2>
|
||||
</div>
|
||||
|
||||
<div class="confirmation-body">
|
||||
<div class="qr-details-section">
|
||||
<div class="qr-preview">
|
||||
<img
|
||||
src="data:image/png;base64,{{ qr_code.qr_code_image }}"
|
||||
alt="QR Code for {{ qr_code.name }}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="deletion-details">
|
||||
<h3>
|
||||
{{ qr_code.name }}
|
||||
<span
|
||||
class="qr-status-badge {{ 'active' if qr_code.active_status else 'inactive' }}"
|
||||
>
|
||||
<i
|
||||
class="fas {{ 'fa-check-circle' if qr_code.active_status else 'fa-times-circle' }}"
|
||||
></i>
|
||||
{{ 'Active' if qr_code.active_status else 'Inactive' }}
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Location:</span>
|
||||
<span class="detail-value">{{ qr_code.location }}</span>
|
||||
</div>
|
||||
|
||||
{% if qr_code.location_address %}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Address:</span>
|
||||
<span class="detail-value">{{ qr_code.location_address }}</span>
|
||||
</div>
|
||||
{% endif %} {% if qr_code.location_event %}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Event:</span>
|
||||
<span class="detail-value">{{ qr_code.location_event }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Created:</span>
|
||||
<span class="detail-value"
|
||||
>{{ qr_code.created_date.strftime('%B %d, %Y at %H:%M') }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Created by:</span>
|
||||
<span class="detail-value">{{ qr_code.creator.full_name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">QR Code ID:</span>
|
||||
<span class="detail-value">#{{ qr_code.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="warning-message">
|
||||
<p>
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<strong>Warning: This action cannot be undone!</strong>
|
||||
</p>
|
||||
<p>Permanently deleting this QR code will:</p>
|
||||
<ul>
|
||||
<li>Remove the QR code completely from the database</li>
|
||||
<li>Make the QR code link permanently inaccessible</li>
|
||||
<li>Delete all associated scan data and statistics</li>
|
||||
<li>Remove any references to this QR code in reports</li>
|
||||
</ul>
|
||||
<p style="margin-top: var(--spacing-4); font-style: italic">
|
||||
<strong>Alternative:</strong> Consider deactivating the QR code
|
||||
instead if you might need it later.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="confirmation-actions">
|
||||
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Cancel & Go Back
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="{{ url_for('deactivate_qr_code', qr_id=qr_code.id) }}"
|
||||
class="btn btn-warning"
|
||||
>
|
||||
<i class="fas fa-pause"></i>
|
||||
Deactivate Instead
|
||||
</a>
|
||||
|
||||
<form method="POST" style="display: inline" id="deleteForm">
|
||||
<button type="button" class="btn btn-delete-confirm" id="deleteBtn">
|
||||
<i class="fas fa-trash"></i>
|
||||
Delete Permanently
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Enhanced Confirmation Modal -->
|
||||
<div id="finalConfirmModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 style="color: var(--danger-color)">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
Final Confirmation Required
|
||||
</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
<strong
|
||||
>Are you absolutely sure you want to delete "{{ qr_code.name
|
||||
}}"?</strong
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
This action is <strong>irreversible</strong> and will permanently remove
|
||||
all data associated with this QR code.
|
||||
</p>
|
||||
|
||||
<div class="confirmation-input">
|
||||
<label for="confirmText" class="form-label">
|
||||
Type <strong>"DELETE {{ qr_code.name.upper() }}"</strong> to confirm:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="confirmText"
|
||||
class="form-input"
|
||||
placeholder="Type DELETE {{ qr_code.name.upper() }}"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="countdown-timer" id="countdownTimer" style="display: none">
|
||||
<span class="countdown-number" id="countdownNumber">5</span>
|
||||
<span
|
||||
>Deletion will be enabled in
|
||||
<span id="countdownText">5</span> seconds...</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
onclick="closeFinalConfirmModal()"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" id="finalDeleteBtn" disabled>
|
||||
<i class="fas fa-trash"></i>
|
||||
Confirm Deletion
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %} {% block extra_scripts %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const deleteBtn = document.getElementById("deleteBtn");
|
||||
const deleteForm = document.getElementById("deleteForm");
|
||||
const finalConfirmModal = document.getElementById("finalConfirmModal");
|
||||
const confirmText = document.getElementById("confirmText");
|
||||
const finalDeleteBtn = document.getElementById("finalDeleteBtn");
|
||||
const countdownTimer = document.getElementById("countdownTimer");
|
||||
const countdownNumber = document.getElementById("countdownNumber");
|
||||
const countdownText = document.getElementById("countdownText");
|
||||
|
||||
const expectedText = "DELETE {{ qr_code.name.upper() }}";
|
||||
let countdownInterval;
|
||||
let countdownValue = 5;
|
||||
|
||||
// Show final confirmation modal
|
||||
deleteBtn.addEventListener("click", function () {
|
||||
finalConfirmModal.style.display = "flex";
|
||||
setTimeout(() => finalConfirmModal.classList.add("show"), 10);
|
||||
|
||||
// Start countdown
|
||||
startCountdown();
|
||||
|
||||
// Focus on input
|
||||
setTimeout(() => confirmText.focus(), 100);
|
||||
});
|
||||
|
||||
// Close modal function
|
||||
window.closeFinalConfirmModal = function () {
|
||||
finalConfirmModal.classList.remove("show");
|
||||
setTimeout(() => {
|
||||
finalConfirmModal.style.display = "none";
|
||||
resetModal();
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// Start countdown timer
|
||||
function startCountdown() {
|
||||
countdownValue = 5;
|
||||
countdownTimer.style.display = "flex";
|
||||
updateCountdownDisplay();
|
||||
|
||||
countdownInterval = setInterval(() => {
|
||||
countdownValue--;
|
||||
updateCountdownDisplay();
|
||||
|
||||
if (countdownValue <= 0) {
|
||||
clearInterval(countdownInterval);
|
||||
countdownTimer.style.display = "none";
|
||||
checkConfirmationText();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function updateCountdownDisplay() {
|
||||
countdownNumber.textContent = countdownValue;
|
||||
countdownText.textContent = countdownValue;
|
||||
}
|
||||
|
||||
// Reset modal state
|
||||
function resetModal() {
|
||||
confirmText.value = "";
|
||||
finalDeleteBtn.disabled = true;
|
||||
countdownTimer.style.display = "none";
|
||||
if (countdownInterval) {
|
||||
clearInterval(countdownInterval);
|
||||
}
|
||||
}
|
||||
|
||||
// Check confirmation text
|
||||
function checkConfirmationText() {
|
||||
const isTextCorrect = confirmText.value.trim() === expectedText;
|
||||
const isCountdownFinished = countdownValue <= 0;
|
||||
|
||||
finalDeleteBtn.disabled = !(isTextCorrect && isCountdownFinished);
|
||||
|
||||
if (isTextCorrect && isCountdownFinished) {
|
||||
finalDeleteBtn.classList.add("btn-danger");
|
||||
finalDeleteBtn.classList.remove("btn-secondary");
|
||||
} else {
|
||||
finalDeleteBtn.classList.remove("btn-danger");
|
||||
finalDeleteBtn.classList.add("btn-secondary");
|
||||
}
|
||||
}
|
||||
|
||||
// Monitor text input
|
||||
confirmText.addEventListener("input", function () {
|
||||
if (countdownValue <= 0) {
|
||||
checkConfirmationText();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle final deletion
|
||||
finalDeleteBtn.addEventListener("click", function () {
|
||||
if (!finalDeleteBtn.disabled) {
|
||||
// Show loading state
|
||||
finalDeleteBtn.innerHTML =
|
||||
'<i class="fas fa-spinner fa-spin"></i> Deleting...';
|
||||
finalDeleteBtn.disabled = true;
|
||||
|
||||
// Submit the form
|
||||
deleteForm.submit();
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal on escape key
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape" && finalConfirmModal.style.display === "flex") {
|
||||
closeFinalConfirmModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal when clicking outside
|
||||
finalConfirmModal.addEventListener("click", function (e) {
|
||||
if (e.target === finalConfirmModal) {
|
||||
closeFinalConfirmModal();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.confirmation-input {
|
||||
margin: var(--spacing-6) 0;
|
||||
}
|
||||
|
||||
.confirmation-input .form-input {
|
||||
font-family: "Courier New", monospace;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.confirmation-input .form-input:focus {
|
||||
border-color: var(--danger-color);
|
||||
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
|
||||
}
|
||||
|
||||
#finalDeleteBtn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#finalDeleteBtn:not(:disabled) {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.7);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(220, 38, 38, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(220, 38, 38, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,590 @@
|
||||
{% extends "base.html" %} {% block title %}Create User - QR Code Management{%
|
||||
endblock %} {% block content %}
|
||||
<div class="form-page">
|
||||
<div class="form-card">
|
||||
<div class="form-header">
|
||||
<h1>Create New User</h1>
|
||||
<p>Add a new user to the QR management system</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" class="form" id="createUserForm">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="full_name">
|
||||
<i class="fas fa-id-card"></i>
|
||||
Full Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="full_name"
|
||||
name="full_name"
|
||||
required
|
||||
placeholder="Enter user's full name"
|
||||
/>
|
||||
<small class="form-help"
|
||||
>Legal name as it should appear in the system</small
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">
|
||||
<i class="fas fa-envelope"></i>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
required
|
||||
placeholder="user@company.com"
|
||||
/>
|
||||
<small class="form-help"
|
||||
>Primary email for notifications and account recovery</small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="username">
|
||||
<i class="fas fa-user"></i>
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
required
|
||||
placeholder="Enter username"
|
||||
pattern="[a-zA-Z0-9_]+"
|
||||
title="Username can only contain letters, numbers, and underscores"
|
||||
/>
|
||||
<small class="form-help"
|
||||
>Must be unique. Letters, numbers, and underscores only</small
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="role">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
User Role
|
||||
</label>
|
||||
<select id="role" name="role" required>
|
||||
<option value="">Select Role</option>
|
||||
<option value="staff">Staff User</option>
|
||||
<option value="admin">Administrator</option>
|
||||
</select>
|
||||
<small class="form-help"
|
||||
>Staff can manage QR codes; Admin has full system access</small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">
|
||||
<i class="fas fa-lock"></i>
|
||||
Initial Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
minlength="6"
|
||||
placeholder="Enter initial password"
|
||||
/>
|
||||
<div class="password-strength" id="passwordStrength"></div>
|
||||
<small class="form-help"
|
||||
>Minimum 6 characters. User should change on first login</small
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Role Information Panel -->
|
||||
<div class="info-panel" id="roleInfo" style="display: none">
|
||||
<div class="info-header">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<span>Role Permissions</span>
|
||||
</div>
|
||||
<div class="info-content" id="roleContent">
|
||||
<!-- Dynamic content based on selected role -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Creation Summary -->
|
||||
<div class="creation-summary" id="creationSummary" style="display: none">
|
||||
<h3>
|
||||
<i class="fas fa-check-circle"></i>
|
||||
User Creation Summary
|
||||
</h3>
|
||||
<div class="summary-grid">
|
||||
<div class="summary-item">
|
||||
<strong>Name:</strong> <span id="summaryName">-</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<strong>Email:</strong> <span id="summaryEmail">-</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<strong>Username:</strong> <span id="summaryUsername">-</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<strong>Role:</strong> <span id="summaryRole">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('users') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back to Users
|
||||
</a>
|
||||
<button type="button" class="btn btn-info" id="previewUser">
|
||||
<i class="fas fa-eye"></i>
|
||||
Preview User
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-user-plus"></i>
|
||||
Create User
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %} {% block extra_scripts %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const form = document.getElementById("createUserForm");
|
||||
const roleSelect = document.getElementById("role");
|
||||
const roleInfo = document.getElementById("roleInfo");
|
||||
const roleContent = document.getElementById("roleContent");
|
||||
const previewBtn = document.getElementById("previewUser");
|
||||
const creationSummary = document.getElementById("creationSummary");
|
||||
|
||||
// Form inputs
|
||||
const fullNameInput = document.getElementById("full_name");
|
||||
const emailInput = document.getElementById("email");
|
||||
const usernameInput = document.getElementById("username");
|
||||
const passwordInput = document.getElementById("password");
|
||||
const passwordStrength = document.getElementById("passwordStrength");
|
||||
|
||||
// Role information
|
||||
const rolePermissions = {
|
||||
staff: {
|
||||
title: "Staff User Permissions",
|
||||
permissions: [
|
||||
"Create and edit QR codes",
|
||||
"View all QR codes in the system",
|
||||
"Download QR code images",
|
||||
"Update personal profile information",
|
||||
"Access dashboard and reports",
|
||||
],
|
||||
restrictions: [
|
||||
"Cannot delete QR codes",
|
||||
"Cannot manage other users",
|
||||
"Cannot access admin settings",
|
||||
],
|
||||
},
|
||||
admin: {
|
||||
title: "Administrator Permissions",
|
||||
permissions: [
|
||||
"Full QR code management (create, edit, delete)",
|
||||
"Complete user management capabilities",
|
||||
"System configuration access",
|
||||
"View all system analytics",
|
||||
"Bulk operations and data export",
|
||||
"Access to all admin features",
|
||||
],
|
||||
restrictions: ["With great power comes great responsibility!"],
|
||||
},
|
||||
};
|
||||
|
||||
// Show role information when role is selected
|
||||
roleSelect.addEventListener("change", function () {
|
||||
const selectedRole = this.value;
|
||||
|
||||
if (selectedRole && rolePermissions[selectedRole]) {
|
||||
const permissions = rolePermissions[selectedRole];
|
||||
|
||||
roleContent.innerHTML = `
|
||||
<h4>${permissions.title}</h4>
|
||||
<div class="permissions-grid">
|
||||
<div class="permissions-column">
|
||||
<h5><i class="fas fa-check text-success"></i> Allowed Actions</h5>
|
||||
<ul>
|
||||
${permissions.permissions
|
||||
.map((perm) => `<li>${perm}</li>`)
|
||||
.join("")}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="permissions-column">
|
||||
<h5><i class="fas fa-times text-danger"></i> Restrictions</h5>
|
||||
<ul>
|
||||
${permissions.restrictions
|
||||
.map((rest) => `<li>${rest}</li>`)
|
||||
.join("")}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
roleInfo.style.display = "block";
|
||||
setTimeout(() => roleInfo.classList.add("fade-in"), 10);
|
||||
} else {
|
||||
roleInfo.style.display = "none";
|
||||
roleInfo.classList.remove("fade-in");
|
||||
}
|
||||
});
|
||||
|
||||
// Password strength checker
|
||||
passwordInput.addEventListener("input", function () {
|
||||
const password = this.value;
|
||||
let strength = 0;
|
||||
let feedback = [];
|
||||
|
||||
// Check password criteria
|
||||
if (password.length >= 6) strength++;
|
||||
else feedback.push("At least 6 characters");
|
||||
|
||||
if (/[A-Z]/.test(password)) strength++;
|
||||
else feedback.push("One uppercase letter");
|
||||
|
||||
if (/[a-z]/.test(password)) strength++;
|
||||
else feedback.push("One lowercase letter");
|
||||
|
||||
if (/[0-9]/.test(password)) strength++;
|
||||
else feedback.push("One number");
|
||||
|
||||
if (/[^A-Za-z0-9]/.test(password)) strength++;
|
||||
else feedback.push("One special character");
|
||||
|
||||
// Update strength display
|
||||
let strengthText = "";
|
||||
let strengthClass = "";
|
||||
|
||||
if (strength < 2) {
|
||||
strengthText = "Weak";
|
||||
strengthClass = "weak";
|
||||
} else if (strength < 4) {
|
||||
strengthText = "Medium";
|
||||
strengthClass = "medium";
|
||||
} else {
|
||||
strengthText = "Strong";
|
||||
strengthClass = "strong";
|
||||
}
|
||||
|
||||
passwordStrength.className = `password-strength ${strengthClass}`;
|
||||
passwordStrength.textContent = `Strength: ${strengthText}`;
|
||||
|
||||
if (feedback.length > 0 && password.length > 0) {
|
||||
passwordStrength.textContent += ` (Missing: ${feedback.join(", ")})`;
|
||||
}
|
||||
});
|
||||
|
||||
// Username validation and formatting
|
||||
usernameInput.addEventListener("input", function () {
|
||||
// Remove invalid characters
|
||||
this.value = this.value.replace(/[^a-zA-Z0-9_]/g, "");
|
||||
|
||||
// Convert to lowercase for consistency
|
||||
this.value = this.value.toLowerCase();
|
||||
});
|
||||
|
||||
// Auto-generate username from full name
|
||||
fullNameInput.addEventListener("input", function () {
|
||||
if (!usernameInput.value) {
|
||||
const generatedUsername = this.value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-zA-Z0-9\s]/g, "")
|
||||
.replace(/\s+/g, "_")
|
||||
.substring(0, 20);
|
||||
|
||||
usernameInput.value = generatedUsername;
|
||||
}
|
||||
});
|
||||
|
||||
// Preview user functionality
|
||||
previewBtn.addEventListener("click", function () {
|
||||
const formData = {
|
||||
full_name: fullNameInput.value.trim(),
|
||||
email: emailInput.value.trim(),
|
||||
username: usernameInput.value.trim(),
|
||||
role: roleSelect.value,
|
||||
};
|
||||
|
||||
// Validate required fields
|
||||
if (
|
||||
!formData.full_name ||
|
||||
!formData.email ||
|
||||
!formData.username ||
|
||||
!formData.role
|
||||
) {
|
||||
alert("Please fill in all required fields before previewing.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update summary
|
||||
document.getElementById("summaryName").textContent = formData.full_name;
|
||||
document.getElementById("summaryEmail").textContent = formData.email;
|
||||
document.getElementById("summaryUsername").textContent =
|
||||
formData.username;
|
||||
document.getElementById("summaryRole").textContent =
|
||||
formData.role.charAt(0).toUpperCase() + formData.role.slice(1);
|
||||
|
||||
// Show summary
|
||||
creationSummary.style.display = "block";
|
||||
setTimeout(() => creationSummary.classList.add("fade-in"), 10);
|
||||
|
||||
// Scroll to summary
|
||||
creationSummary.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
});
|
||||
|
||||
// Form validation
|
||||
form.addEventListener("submit", function (e) {
|
||||
const fullName = fullNameInput.value.trim();
|
||||
const email = emailInput.value.trim();
|
||||
const username = usernameInput.value.trim();
|
||||
const password = passwordInput.value;
|
||||
const role = roleSelect.value;
|
||||
|
||||
// Comprehensive validation
|
||||
if (!fullName || fullName.length < 2) {
|
||||
e.preventDefault();
|
||||
alert("Full name must be at least 2 characters long.");
|
||||
fullNameInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!email || !emailRegex.test(email)) {
|
||||
e.preventDefault();
|
||||
alert("Please enter a valid email address.");
|
||||
emailInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!username || username.length < 3) {
|
||||
e.preventDefault();
|
||||
alert("Username must be at least 3 characters long.");
|
||||
usernameInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password || password.length < 6) {
|
||||
e.preventDefault();
|
||||
alert("Password must be at least 6 characters long.");
|
||||
passwordInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!role) {
|
||||
e.preventDefault();
|
||||
alert("Please select a user role.");
|
||||
roleSelect.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
submitBtn.innerHTML =
|
||||
'<i class="fas fa-spinner fa-spin"></i> Creating User...';
|
||||
submitBtn.disabled = true;
|
||||
});
|
||||
|
||||
// Real-time email validation
|
||||
emailInput.addEventListener("blur", function () {
|
||||
const email = this.value.trim();
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const formGroup = this.closest(".form-group");
|
||||
|
||||
if (email && !emailRegex.test(email)) {
|
||||
formGroup.classList.add("has-error");
|
||||
if (!formGroup.querySelector(".error-message")) {
|
||||
const errorMsg = document.createElement("div");
|
||||
errorMsg.className = "error-message";
|
||||
errorMsg.textContent = "Please enter a valid email address";
|
||||
formGroup.appendChild(errorMsg);
|
||||
}
|
||||
} else {
|
||||
formGroup.classList.remove("has-error");
|
||||
const errorMsg = formGroup.querySelector(".error-message");
|
||||
if (errorMsg) {
|
||||
errorMsg.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Character counters
|
||||
function addCharacterCounter(input, maxLength) {
|
||||
const formGroup = input.closest(".form-group");
|
||||
const counter = document.createElement("div");
|
||||
counter.className = "character-counter";
|
||||
|
||||
function updateCounter() {
|
||||
const currentLength = input.value.length;
|
||||
counter.textContent = `${currentLength}${
|
||||
maxLength ? "/" + maxLength : ""
|
||||
} characters`;
|
||||
|
||||
if (maxLength && currentLength > maxLength * 0.8) {
|
||||
counter.classList.add("warning");
|
||||
} else {
|
||||
counter.classList.remove("warning");
|
||||
}
|
||||
}
|
||||
|
||||
input.addEventListener("input", updateCounter);
|
||||
formGroup.appendChild(counter);
|
||||
updateCounter();
|
||||
}
|
||||
|
||||
// Add character counters
|
||||
addCharacterCounter(fullNameInput, 100);
|
||||
addCharacterCounter(usernameInput, 50);
|
||||
|
||||
// Auto-capitalize full name
|
||||
fullNameInput.addEventListener("input", function () {
|
||||
this.value = this.value.replace(/\b\w/g, (l) => l.toUpperCase());
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Additional styles for create user form */
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.info-panel {
|
||||
margin: 1.5rem 0;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, #eff6ff, #dbeafe);
|
||||
border: 1px solid #3b82f6;
|
||||
border-radius: var(--radius-xl);
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.info-panel.fade-in {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.info-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #1e40af;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.permissions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.permissions-column h5 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.permissions-column ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.permissions-column li {
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid rgba(59, 130, 246, 0.1);
|
||||
font-size: 0.875rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.permissions-column li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #059669 !important;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #dc2626 !important;
|
||||
}
|
||||
|
||||
.creation-summary {
|
||||
margin: 2rem 0;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, #f0fdf4, #dcfce7);
|
||||
border: 2px solid #22c55e;
|
||||
border-radius: var(--radius-xl);
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.creation-summary.fade-in {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.creation-summary h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #15803d;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
padding: 0.75rem;
|
||||
background: white;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.summary-item strong {
|
||||
color: #374151;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.summary-item span {
|
||||
color: #059669;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.permissions-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,392 @@
|
||||
{% extends "base.html" %} {% block title %}Dashboard - QR Code Management{%
|
||||
endblock %} {% block extra_head %}
|
||||
<!-- Dashboard-specific CSS -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="{{ url_for('static', filename='css/dashboard.css') }}"
|
||||
/>
|
||||
{% endblock %} {% block content %}
|
||||
<div class="dashboard-container">
|
||||
<!-- Dashboard Header -->
|
||||
<div class="dashboard-header">
|
||||
<div class="welcome-section">
|
||||
<h1>Welcome back, {{ session.full_name }}!</h1>
|
||||
<p>Manage your QR codes and monitor system activity</p>
|
||||
<div class="user-info-badge">
|
||||
<div class="role-indicator {{ session.role }}">
|
||||
<i
|
||||
class="fas {{ 'fa-crown' if session.role == 'admin' else 'fa-user' }}"
|
||||
></i>
|
||||
{{ session.role.title() }}
|
||||
</div>
|
||||
{% if session.last_login_date %}
|
||||
<div class="last-login">
|
||||
<i class="fas fa-clock"></i>
|
||||
Last login: {{ session.last_login_date.strftime('%b %d, %Y at %H:%M')
|
||||
}}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-actions">
|
||||
<a href="{{ url_for('create_qr_code') }}" class="btn btn-primary btn-lg">
|
||||
<i class="fas fa-plus"></i>
|
||||
Create QR Code
|
||||
</a>
|
||||
{% if session.role == 'admin' %}
|
||||
<a href="{{ url_for('users') }}" class="btn btn-secondary btn-lg">
|
||||
<i class="fas fa-users"></i>
|
||||
Manage Users
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Section -->
|
||||
{% if session.role == 'admin' %}
|
||||
<div class="stats-section">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card primary">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-qrcode"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ qr_codes|length }}</h3>
|
||||
<p>Total QR Codes</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card success">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3>
|
||||
{{ qr_codes|selectattr('active_status', 'equalto', True)|list|length
|
||||
}}
|
||||
</h3>
|
||||
<p>Active QR Codes</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card warning">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-users"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ total_users or 0 }}</h3>
|
||||
<p>Total Users</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card danger">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-pause-circle"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3>
|
||||
{{ qr_codes|selectattr('active_status', 'equalto',
|
||||
False)|list|length }}
|
||||
</h3>
|
||||
<p>Inactive QR Codes</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- QR Codes Section -->
|
||||
<div class="qr-codes-section">
|
||||
<div class="section-header">
|
||||
<h2>Your QR Codes</h2>
|
||||
<div class="section-controls">
|
||||
<div class="search-box">
|
||||
<i class="fas fa-search"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="qrSearch"
|
||||
placeholder="Search QR codes..."
|
||||
data-container="qrGrid"
|
||||
/>
|
||||
</div>
|
||||
<select id="statusFilter" class="filter-select">
|
||||
<option value="">All Status</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
</select>
|
||||
<div class="results-counter">{{ qr_codes|length }} results</div>
|
||||
{% if session.role == 'admin' %}
|
||||
<button id="expandAllToggle" class="btn btn-outline">
|
||||
<i class="fas fa-expand-alt"></i>
|
||||
Expand All
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if qr_codes %}
|
||||
<!-- Bulk Actions Bar (initially hidden) -->
|
||||
<div id="bulkActionsBar" class="bulk-actions-bar">
|
||||
<div class="bulk-actions-info">
|
||||
<i class="fas fa-check-square"></i>
|
||||
<span id="selectedCount">0</span> QR codes selected
|
||||
</div>
|
||||
<div class="bulk-actions-buttons">
|
||||
{% if session.role == 'admin' %}
|
||||
<button id="bulkDeleteBtn" class="btn btn-danger btn-sm">
|
||||
<i class="fas fa-trash"></i>
|
||||
Delete Selected
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QR Codes Grid -->
|
||||
<div class="qr-grid" id="qrGrid">
|
||||
{% for qr in qr_codes %}
|
||||
<div
|
||||
class="qr-item"
|
||||
data-qr-id="{{ qr.id }}"
|
||||
data-name="{{ qr.name.lower() }}"
|
||||
data-location="{{ qr.location.lower() }}"
|
||||
data-status="{{ 'active' if qr.active_status else 'inactive' }}"
|
||||
onclick="toggleQRItem(this)"
|
||||
>
|
||||
<!-- QR Item Header -->
|
||||
<div class="qr-item-header">
|
||||
{% if session.role == 'admin' %}
|
||||
<input
|
||||
type="checkbox"
|
||||
class="qr-checkbox"
|
||||
value="{{ qr.id }}"
|
||||
onclick="event.stopPropagation()"
|
||||
/>
|
||||
{% endif %}
|
||||
<div
|
||||
class="qr-code-preview"
|
||||
onclick="event.stopPropagation(); openQRModal({
|
||||
name: '{{ qr.name }}',
|
||||
image: '{{ qr.qr_code_image }}'
|
||||
})"
|
||||
>
|
||||
<img
|
||||
src="data:image/png;base64,{{ qr.qr_code_image }}"
|
||||
alt="QR Code for {{ qr.name }}"
|
||||
/>
|
||||
</div>
|
||||
<div class="qr-item-info">
|
||||
<div class="qr-item-title">
|
||||
{{ qr.name }}
|
||||
<span
|
||||
class="qr-status {{ 'active' if qr.active_status else 'inactive' }}"
|
||||
>
|
||||
<i
|
||||
class="fas {{ 'fa-check-circle' if qr.active_status else 'fa-times-circle' }}"
|
||||
></i>
|
||||
{{ 'Active' if qr.active_status else 'Inactive' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="qr-item-meta">
|
||||
<span>
|
||||
<i class="fas fa-calendar"></i>
|
||||
{{ qr.created_date.strftime('%b %d, %Y') }}
|
||||
</span>
|
||||
<span>
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
{{ qr.location }}
|
||||
</span>
|
||||
<span>
|
||||
<i class="fas fa-user"></i>
|
||||
{{ qr.creator.full_name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="qr-item-actions">
|
||||
<div class="quick-actions">
|
||||
{% if session.role == 'admin' %}
|
||||
<button
|
||||
class="action-btn btn-status {{ 'btn-deactivate' if qr.active_status else 'btn-activate' }}"
|
||||
onclick="event.stopPropagation(); toggleQRCodeStatus('{{ qr.id }}')"
|
||||
title="{{ 'Deactivate' if qr.active_status else 'Activate' }} QR Code"
|
||||
id="toggle-btn-{{ qr.id }}"
|
||||
>
|
||||
<i
|
||||
class="fas {{ 'fa-pause' if qr.active_status else 'fa-play' }}"
|
||||
id="toggle-icon-{{ qr.id }}"
|
||||
></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<button
|
||||
class="action-btn btn-download"
|
||||
onclick="event.stopPropagation(); downloadQR('{{ qr.qr_code_image }}', '{{ qr.name }}')"
|
||||
title="Download QR Code"
|
||||
>
|
||||
<i class="fas fa-download"></i>
|
||||
</button>
|
||||
|
||||
<a
|
||||
href="{{ url_for('edit_qr_code', qr_id=qr.id) }}"
|
||||
class="action-btn btn-edit"
|
||||
onclick="event.stopPropagation()"
|
||||
title="Edit QR Code"
|
||||
>
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
|
||||
{% if session.role == 'admin' %}
|
||||
<button
|
||||
class="action-btn btn-delete"
|
||||
onclick="event.stopPropagation(); deleteQRCode('{{ qr.id }}', '{{ qr.name }}')"
|
||||
title="Delete QR Code"
|
||||
>
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expanded Details (initially hidden) -->
|
||||
<div class="qr-item-details">
|
||||
<div class="qr-details-content">
|
||||
<div class="qr-details-info">
|
||||
<div class="info-item">
|
||||
<span class="label">Location Address:</span>
|
||||
<span class="value"
|
||||
>{{ qr.location_address or 'Not specified' }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Event:</span>
|
||||
<span class="value"
|
||||
>{{ qr.location_event or 'Not specified' }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Created by:</span>
|
||||
<span class="value">{{ qr.creator.full_name }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Created on:</span>
|
||||
<span class="value"
|
||||
>{{ qr.created_date.strftime('%B %d, %Y at %H:%M') }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">QR ID:</span>
|
||||
<span class="value">#{{ qr.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="qr-details-preview">
|
||||
<div
|
||||
class="qr-details-image"
|
||||
onclick="openQRModal({
|
||||
name: '{{ qr.name }}',
|
||||
image: '{{ qr.qr_code_image }}'
|
||||
})"
|
||||
>
|
||||
<img
|
||||
src="data:image/png;base64,{{ qr.qr_code_image }}"
|
||||
alt="QR Code for {{ qr.name }}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detail Actions -->
|
||||
<div class="details-actions">
|
||||
{% if session.role == 'admin' %}
|
||||
<button
|
||||
onclick="toggleQRCodeStatus({{ qr.id }})"
|
||||
class="btn {{ 'btn-warning' if qr.active_status else 'btn-success' }}"
|
||||
id="detail-toggle-btn-{{ qr.id }}"
|
||||
>
|
||||
<i
|
||||
class="fas {{ 'fa-pause' if qr.active_status else 'fa-play' }}"
|
||||
></i>
|
||||
{{ 'Deactivate' if qr.active_status else 'Activate' }} QR Code
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<button
|
||||
onclick="downloadQR('{{ qr.qr_code_image }}', '{{ qr.name }}')"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
<i class="fas fa-download"></i>
|
||||
Download QR Code
|
||||
</button>
|
||||
|
||||
<a
|
||||
href="{{ url_for('edit_qr_code', qr_id=qr.id) }}"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
<i class="fas fa-edit"></i>
|
||||
Edit Details
|
||||
</a>
|
||||
|
||||
<button
|
||||
onclick="copyQRData('{{ qr.name }}', '{{ qr.location }}', '{{ qr.location_address }}', '{{ qr.location_event }}')"
|
||||
class="btn btn-outline"
|
||||
>
|
||||
<i class="fas fa-copy"></i>
|
||||
Copy Info
|
||||
</button>
|
||||
|
||||
{% if session.role == 'admin' %}
|
||||
<button
|
||||
onclick="deleteQRCode({{ qr.id }}, '{{ qr.name }}')"
|
||||
class="btn btn-danger"
|
||||
>
|
||||
<i class="fas fa-trash"></i>
|
||||
Delete QR Code
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Empty State -->
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<i class="fas fa-qrcode"></i>
|
||||
</div>
|
||||
<h3>No QR Codes Yet</h3>
|
||||
<p>Get started by creating your first QR code</p>
|
||||
<a href="{{ url_for('create_qr_code') }}" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i>
|
||||
Create Your First QR Code
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QR Code Modal -->
|
||||
<div id="qrModal" class="modal">
|
||||
<div class="modal-content qr-modal">
|
||||
<div class="modal-header">
|
||||
<h3 id="modalTitle">QR Code Preview</h3>
|
||||
<button class="modal-close" onclick="closeQRModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="qr-modal-image">
|
||||
<img id="modalQRImage" src="" alt="QR Code" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button onclick="downloadModalQR()" class="btn btn-primary">
|
||||
<i class="fas fa-download"></i>
|
||||
Download
|
||||
</button>
|
||||
<button onclick="closeQRModal()" class="btn btn-secondary">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %} {% block extra_scripts %}
|
||||
<!-- Dashboard-specific JavaScript -->
|
||||
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
|
||||
{% endblock %}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,742 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Edit User - QR Code Management{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-page">
|
||||
<div class="form-card">
|
||||
<div class="form-header">
|
||||
<h1>Edit User</h1>
|
||||
<p>Update user information and permissions</p>
|
||||
</div>
|
||||
|
||||
<!-- Current User Info Display -->
|
||||
<div class="current-user-info">
|
||||
<div class="user-avatar-section">
|
||||
<div class="user-avatar large">
|
||||
<i class="fas fa-user"></i>
|
||||
<div class="avatar-status {{ user.role }}">
|
||||
<i class="fas {{ 'fa-crown' if user.role == 'admin' else 'fa-user-tie' }}"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-basic-info">
|
||||
<h3>{{ user.full_name }}</h3>
|
||||
<p>@{{ user.username }}</p>
|
||||
<span class="role-badge {{ user.role }}">
|
||||
{{ user.role.title() }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-stats-quick">
|
||||
<div class="stat-quick">
|
||||
<span class="stat-number">{{ user.created_qr_codes.count() }}</span>
|
||||
<span class="stat-label">QR Codes</span>
|
||||
</div>
|
||||
<div class="stat-quick">
|
||||
<span class="stat-number">{{ user.created_date.strftime('%b %Y') }}</span>
|
||||
<span class="stat-label">Joined</span>
|
||||
</div>
|
||||
<div class="stat-quick">
|
||||
<span class="stat-number">
|
||||
{% if user.last_login_date %}
|
||||
{{ user.last_login_date.strftime('%m/%d') }}
|
||||
{% else %}
|
||||
Never
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="stat-label">Last Login</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" class="form" id="editUserForm">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="full_name">
|
||||
<i class="fas fa-id-card"></i>
|
||||
Full Name
|
||||
</label>
|
||||
<input type="text" id="full_name" name="full_name"
|
||||
value="{{ user.full_name }}" required>
|
||||
<small class="form-help">User's display name in the system</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">
|
||||
<i class="fas fa-envelope"></i>
|
||||
Email Address
|
||||
</label>
|
||||
<input type="email" id="email" name="email"
|
||||
value="{{ user.email }}" required>
|
||||
<small class="form-help">Primary email for notifications</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="username_display">
|
||||
<i class="fas fa-user"></i>
|
||||
Username
|
||||
</label>
|
||||
<input type="text" id="username_display"
|
||||
value="{{ user.username }}" disabled>
|
||||
<small class="form-help">Username cannot be changed</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="role">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
User Role
|
||||
</label>
|
||||
<select id="role" name="role" required>
|
||||
<option value="staff" {{ 'selected' if user.role == 'staff' else '' }}>Staff User</option>
|
||||
<option value="admin" {{ 'selected' if user.role == 'admin' else '' }}>Administrator</option>
|
||||
</select>
|
||||
<small class="form-help">Changes role permissions immediately</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Role Change Warning -->
|
||||
<div class="role-warning" id="roleWarning" style="display: none;">
|
||||
<div class="warning-header">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<span>Role Change Warning</span>
|
||||
</div>
|
||||
<div class="warning-content" id="warningContent">
|
||||
<!-- Dynamic warning content -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Section -->
|
||||
<div class="password-section">
|
||||
<h3>
|
||||
<i class="fas fa-key"></i>
|
||||
Password Management
|
||||
</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new_password">
|
||||
<i class="fas fa-lock"></i>
|
||||
New Password (Optional)
|
||||
</label>
|
||||
<input type="password" id="new_password" name="new_password"
|
||||
minlength="6" placeholder="Leave blank to keep current password">
|
||||
<div class="password-strength" id="passwordStrength"></div>
|
||||
<small class="form-help">Only enter a password if you want to change it</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirm_password">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
Confirm New Password
|
||||
</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password">
|
||||
<div class="password-match" id="passwordMatch"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Account Status Section -->
|
||||
<div class="status-section">
|
||||
<h3>
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Account Status
|
||||
</h3>
|
||||
|
||||
<div class="status-display">
|
||||
<div class="status-item">
|
||||
<strong>Current Status:</strong>
|
||||
<span class="status-badge {{ 'active' if user.active_status else 'inactive' }}">
|
||||
<i class="fas {{ 'fa-check-circle' if user.active_status else 'fa-times-circle' }}"></i>
|
||||
{{ 'Active' if user.active_status else 'Inactive' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="status-item">
|
||||
<strong>Account Created:</strong>
|
||||
<span>{{ user.created_date.strftime('%B %d, %Y at %H:%M') }}</span>
|
||||
</div>
|
||||
|
||||
{% if user.creator %}
|
||||
<div class="status-item">
|
||||
<strong>Created By:</strong>
|
||||
<span>{{ user.creator.full_name }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('users') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back to Users
|
||||
</a>
|
||||
|
||||
{% if not user.active_status %}
|
||||
<a href="{{ url_for('reactivate_user', user_id=user.id) }}"
|
||||
class="btn btn-success"
|
||||
onclick="return confirm('Reactivate this user account?')">
|
||||
<i class="fas fa-user-check"></i>
|
||||
Reactivate User
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('editUserForm');
|
||||
const roleSelect = document.getElementById('role');
|
||||
const roleWarning = document.getElementById('roleWarning');
|
||||
const warningContent = document.getElementById('warningContent');
|
||||
const newPasswordInput = document.getElementById('new_password');
|
||||
const confirmPasswordInput = document.getElementById('confirm_password');
|
||||
const passwordStrength = document.getElementById('passwordStrength');
|
||||
const passwordMatch = document.getElementById('passwordMatch');
|
||||
|
||||
const originalRole = '{{ user.role }}';
|
||||
const isCurrentUser = {{ 'true' if user.id == session.user_id else 'false' }};
|
||||
|
||||
// Role change detection and warnings
|
||||
roleSelect.addEventListener('change', function() {
|
||||
const newRole = this.value;
|
||||
|
||||
if (newRole !== originalRole) {
|
||||
let warningMessage = '';
|
||||
|
||||
if (originalRole === 'admin' && newRole === 'staff') {
|
||||
if (isCurrentUser) {
|
||||
warningMessage = `
|
||||
<strong>⚠️ You are demoting yourself!</strong><br>
|
||||
This will remove your admin privileges and you will lose access to:
|
||||
<ul>
|
||||
<li>User management</li>
|
||||
<li>System administration</li>
|
||||
<li>Advanced settings</li>
|
||||
</ul>
|
||||
<strong>You will need another admin to restore your privileges.</strong>
|
||||
`;
|
||||
} else {
|
||||
warningMessage = `
|
||||
<strong>Demoting Admin to Staff</strong><br>
|
||||
This user will lose admin privileges and access to:
|
||||
<ul>
|
||||
<li>User management</li>
|
||||
<li>System administration</li>
|
||||
<li>Advanced settings</li>
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
} else if (originalRole === 'staff' && newRole === 'admin') {
|
||||
warningMessage = `
|
||||
<strong>Promoting Staff to Admin</strong><br>
|
||||
This user will gain full system access including:
|
||||
<ul>
|
||||
<li>User management</li>
|
||||
<li>System administration</li>
|
||||
<li>All QR code operations</li>
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
|
||||
warningContent.innerHTML = warningMessage;
|
||||
roleWarning.style.display = 'block';
|
||||
setTimeout(() => roleWarning.classList.add('fade-in'), 10);
|
||||
} else {
|
||||
roleWarning.style.display = 'none';
|
||||
roleWarning.classList.remove('fade-in');
|
||||
}
|
||||
});
|
||||
|
||||
// Password strength checker
|
||||
newPasswordInput.addEventListener('input', function() {
|
||||
const password = this.value;
|
||||
|
||||
if (!password) {
|
||||
passwordStrength.textContent = '';
|
||||
passwordStrength.className = 'password-strength';
|
||||
return;
|
||||
}
|
||||
|
||||
let strength = 0;
|
||||
let feedback = [];
|
||||
|
||||
if (password.length >= 6) strength++;
|
||||
else feedback.push('At least 6 characters');
|
||||
|
||||
if (password.length >= 8) strength++;
|
||||
else if (password.length >= 6) feedback.push('8+ characters recommended');
|
||||
|
||||
if (/[A-Z]/.test(password)) strength++;
|
||||
else feedback.push('One uppercase letter');
|
||||
|
||||
if (/[a-z]/.test(password)) strength++;
|
||||
else feedback.push('One lowercase letter');
|
||||
|
||||
if (/[0-9]/.test(password)) strength++;
|
||||
else feedback.push('One number');
|
||||
|
||||
if (/[^A-Za-z0-9]/.test(password)) strength++;
|
||||
else feedback.push('One special character');
|
||||
|
||||
let strengthText = '';
|
||||
let strengthClass = '';
|
||||
|
||||
if (strength < 3) {
|
||||
strengthText = 'Weak';
|
||||
strengthClass = 'weak';
|
||||
} else if (strength < 5) {
|
||||
strengthText = 'Medium';
|
||||
strengthClass = 'medium';
|
||||
} else {
|
||||
strengthText = 'Strong';
|
||||
strengthClass = 'strong';
|
||||
}
|
||||
|
||||
passwordStrength.className = `password-strength ${strengthClass}`;
|
||||
passwordStrength.textContent = `Strength: ${strengthText}`;
|
||||
|
||||
if (feedback.length > 0) {
|
||||
passwordStrength.textContent += ` (Missing: ${feedback.join(', ')})`;
|
||||
}
|
||||
|
||||
checkPasswordMatch();
|
||||
});
|
||||
|
||||
// Password match checker
|
||||
function checkPasswordMatch() {
|
||||
if (confirmPasswordInput.value && newPasswordInput.value) {
|
||||
if (newPasswordInput.value === confirmPasswordInput.value) {
|
||||
passwordMatch.textContent = '✓ Passwords match';
|
||||
passwordMatch.className = 'password-match match';
|
||||
} else {
|
||||
passwordMatch.textContent = '✗ Passwords do not match';
|
||||
passwordMatch.className = 'password-match no-match';
|
||||
}
|
||||
} else {
|
||||
passwordMatch.textContent = '';
|
||||
passwordMatch.className = 'password-match';
|
||||
}
|
||||
}
|
||||
|
||||
confirmPasswordInput.addEventListener('input', checkPasswordMatch);
|
||||
|
||||
// Form validation
|
||||
form.addEventListener('submit', function(e) {
|
||||
const newPassword = newPasswordInput.value;
|
||||
const confirmPassword = confirmPasswordInput.value;
|
||||
|
||||
// Password validation if provided
|
||||
if (newPassword) {
|
||||
if (newPassword.length < 6) {
|
||||
e.preventDefault();
|
||||
alert('New password must be at least 6 characters long.');
|
||||
newPasswordInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
e.preventDefault();
|
||||
alert('New passwords do not match.');
|
||||
confirmPasswordInput.focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Role change confirmation
|
||||
const newRole = roleSelect.value;
|
||||
if (newRole !== originalRole) {
|
||||
const roleChangeConfirm = isCurrentUser && newRole === 'staff'
|
||||
? confirm('⚠️ You are about to demote yourself from admin to staff. You will lose admin privileges. Are you sure?')
|
||||
: confirm(`Change user role from ${originalRole} to ${newRole}?`);
|
||||
|
||||
if (!roleChangeConfirm) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Saving Changes...';
|
||||
submitBtn.disabled = true;
|
||||
});
|
||||
|
||||
// Auto-capitalize full name
|
||||
const fullNameInput = document.getElementById('full_name');
|
||||
fullNameInput.addEventListener('input', function() {
|
||||
this.value = this.value.replace(/\b\w/g, l => l.toUpperCase());
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Additional styles for edit user form */
|
||||
.current-user-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, var(--gray-50), var(--white));
|
||||
border-radius: var(--radius-xl);
|
||||
border: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.user-avatar-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.user-avatar.large {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: var(--gray-200);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2rem;
|
||||
color: var(--gray-600);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.avatar-status {
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
right: -2px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.875rem;
|
||||
border: 2px solid var(--white);
|
||||
}
|
||||
|
||||
.avatar-status.admin {
|
||||
background: #fbbf24;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.avatar-status.staff {
|
||||
background: var(--gray-400);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.user-basic-info h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.user-basic-info p {
|
||||
color: var(--gray-500);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.user-stats-quick {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.stat-quick {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
display: block;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--gray-500);
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.role-warning {
|
||||
margin: 1.5rem 0;
|
||||
padding: 1rem;
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
border: 1px solid #f59e0b;
|
||||
border-radius: var(--radius-lg);
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.role-warning.fade-in {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.warning-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #d97706;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.warning-content {
|
||||
color: #92400e;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.warning-content ul {
|
||||
margin: 0.5rem 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.warning-content li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.password-section {
|
||||
margin: 2rem 0;
|
||||
padding: 1.5rem;
|
||||
background: var(--gray-50);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.password-section h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.status-section {
|
||||
margin: 2rem 0;
|
||||
padding: 1.5rem;
|
||||
background: var(--gray-50);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.status-section h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.status-display {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
background: var(--white);
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.status-item strong {
|
||||
color: var(--gray-700);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-item span {
|
||||
color: var(--gray-900);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-badge.active {
|
||||
background: rgba(5, 150, 105, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.status-badge.inactive {
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.role-badge.admin {
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.role-badge.staff {
|
||||
background: rgba(100, 116, 139, 0.1);
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.password-strength {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: var(--radius);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.password-strength.weak {
|
||||
background-color: rgba(220, 38, 38, 0.1);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.password-strength.medium {
|
||||
background-color: rgba(217, 119, 6, 0.1);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.password-strength.strong {
|
||||
background-color: rgba(5, 150, 105, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.password-match {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.password-match.match {
|
||||
background-color: rgba(5, 150, 105, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.password-match.no-match {
|
||||
background-color: rgba(220, 38, 38, 0.1);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid var(--gray-200);
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: var(--font-size-base);
|
||||
transition: var(--transition);
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.form-group input:disabled {
|
||||
background-color: var(--gray-100);
|
||||
color: var(--gray-500);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-help {
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.current-user-info {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.user-stats-quick {
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,79 @@
|
||||
<!-- templates/errors/403.html -->
|
||||
{% extends "base.html" %} {% block title %}Access Forbidden - QR Code
|
||||
Management{% endblock %} {% block content %}
|
||||
<div class="error-page">
|
||||
<div class="error-container">
|
||||
<div class="error-code">403</div>
|
||||
<div class="error-message">
|
||||
<h1>Access Forbidden</h1>
|
||||
<p>You don't have permission to access this resource.</p>
|
||||
<p class="error-detail">This action requires administrator privileges.</p>
|
||||
</div>
|
||||
<div class="error-actions">
|
||||
<a href="{{ url_for('dashboard') }}" class="btn btn-primary">
|
||||
<i class="fas fa-home"></i>
|
||||
Go to Dashboard
|
||||
</a>
|
||||
{% if session.role == 'staff' %}
|
||||
<a href="{{ url_for('profile') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-user"></i>
|
||||
View Profile
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.error-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 60vh;
|
||||
padding: var(--spacing-8);
|
||||
}
|
||||
|
||||
.error-container {
|
||||
text-align: center;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.error-code {
|
||||
font-size: 8rem;
|
||||
font-weight: 900;
|
||||
color: var(--warning-color);
|
||||
line-height: 1;
|
||||
margin-bottom: var(--spacing-4);
|
||||
background: linear-gradient(135deg, var(--warning-color), #b45309);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.error-message h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.error-message p {
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--gray-600);
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.error-detail {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--warning-color);
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-8) !important;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-4);
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,73 @@
|
||||
<!-- templates/errors/404.html -->
|
||||
{% extends "base.html" %} {% block title %}Page Not Found - QR Code Management{%
|
||||
endblock %} {% block content %}
|
||||
<div class="error-page">
|
||||
<div class="error-container">
|
||||
<div class="error-code">404</div>
|
||||
<div class="error-message">
|
||||
<h1>Page Not Found</h1>
|
||||
<p>The page you're looking for doesn't exist or has been moved.</p>
|
||||
</div>
|
||||
<div class="error-actions">
|
||||
<a href="{{ url_for('dashboard') }}" class="btn btn-primary">
|
||||
<i class="fas fa-home"></i>
|
||||
Go to Dashboard
|
||||
</a>
|
||||
<a href="javascript:history.back()" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Go Back
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.error-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 60vh;
|
||||
padding: var(--spacing-8);
|
||||
}
|
||||
|
||||
.error-container {
|
||||
text-align: center;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.error-code {
|
||||
font-size: 8rem;
|
||||
font-weight: 900;
|
||||
color: var(--primary-color);
|
||||
line-height: 1;
|
||||
margin-bottom: var(--spacing-4);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--primary-color),
|
||||
var(--primary-hover)
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.error-message h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.error-message p {
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--gray-600);
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-4);
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,77 @@
|
||||
<!-- templates/errors/500.html -->
|
||||
{% extends "base.html" %} {% block title %}Server Error - QR Code Management{%
|
||||
endblock %} {% block content %}
|
||||
<div class="error-page">
|
||||
<div class="error-container">
|
||||
<div class="error-code">500</div>
|
||||
<div class="error-message">
|
||||
<h1>Internal Server Error</h1>
|
||||
<p>Something went wrong on our end. We're working to fix it.</p>
|
||||
<p class="error-detail">Please try again in a few moments.</p>
|
||||
</div>
|
||||
<div class="error-actions">
|
||||
<a href="{{ url_for('dashboard') }}" class="btn btn-primary">
|
||||
<i class="fas fa-home"></i>
|
||||
Go to Dashboard
|
||||
</a>
|
||||
<button onclick="location.reload()" class="btn btn-secondary">
|
||||
<i class="fas fa-redo"></i>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.error-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 60vh;
|
||||
padding: var(--spacing-8);
|
||||
}
|
||||
|
||||
.error-container {
|
||||
text-align: center;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.error-code {
|
||||
font-size: 8rem;
|
||||
font-weight: 900;
|
||||
color: var(--danger-color);
|
||||
line-height: 1;
|
||||
margin-bottom: var(--spacing-4);
|
||||
background: linear-gradient(135deg, var(--danger-color), #b91c1c);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.error-message h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.error-message p {
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--gray-600);
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.error-detail {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--danger-color);
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-8) !important;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-4);
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,73 @@
|
||||
{% extends "base.html" %} {% block title %}Login - QR Code Management{% endblock
|
||||
%} {% block content %}
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<div class="auth-logo">
|
||||
<i class="fas fa-qrcode"></i>
|
||||
</div>
|
||||
<h1>QR Manager</h1>
|
||||
<p>Sign in to your account</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" class="auth-form">
|
||||
<div class="form-group">
|
||||
<label for="username">
|
||||
<i class="fas fa-user"></i>
|
||||
Username
|
||||
</label>
|
||||
<input type="text" id="username" name="username" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">
|
||||
<i class="fas fa-lock"></i>
|
||||
Password
|
||||
</label>
|
||||
<input type="password" id="password" name="password" required />
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-full">
|
||||
<i class="fas fa-sign-in-alt"></i>
|
||||
Sign In
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p>
|
||||
Don't have an account?
|
||||
<a href="{{ url_for('register') }}" class="auth-link">Register here</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %} {% block extra_scripts %}
|
||||
<script>
|
||||
// Add form validation and user experience improvements
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const form = document.querySelector(".auth-form");
|
||||
const inputs = form.querySelectorAll("input");
|
||||
|
||||
// Add focus effects
|
||||
inputs.forEach((input) => {
|
||||
input.addEventListener("focus", function () {
|
||||
this.parentElement.classList.add("focused");
|
||||
});
|
||||
|
||||
input.addEventListener("blur", function () {
|
||||
if (!this.value) {
|
||||
this.parentElement.classList.remove("focused");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Form submission with loading state
|
||||
form.addEventListener("submit", function () {
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
submitBtn.innerHTML =
|
||||
'<i class="fas fa-spinner fa-spin"></i> Signing In...';
|
||||
submitBtn.disabled = true;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,186 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Check In - {{ qr_code.name }}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/qr_destination.css') }}">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
</head>
|
||||
<body>
|
||||
<div class="destination-container">
|
||||
<!-- Header Section -->
|
||||
<div class="destination-header">
|
||||
<div class="header-icon">
|
||||
<i class="fas fa-qrcode"></i>
|
||||
</div>
|
||||
<h1>{{ qr_code.location_event }}</h1>
|
||||
<p class="location-info">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
{{ qr_code.location }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- QR Code Info Card -->
|
||||
<div class="info-card">
|
||||
<div class="info-header">
|
||||
<h2>
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Event Information
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="info-details">
|
||||
<div class="detail-item">
|
||||
<div class="detail-icon">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
</div>
|
||||
<div class="detail-content">
|
||||
<strong>Event:</strong>
|
||||
<span>{{ qr_code.location_event }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<div class="detail-icon">
|
||||
<i class="fas fa-building"></i>
|
||||
</div>
|
||||
<div class="detail-content">
|
||||
<strong>Location:</strong>
|
||||
<span>{{ qr_code.location }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<div class="detail-icon">
|
||||
<i class="fas fa-home"></i>
|
||||
</div>
|
||||
<div class="detail-content">
|
||||
<strong>Address:</strong>
|
||||
<span>{{ qr_code.location_address }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<div class="detail-icon">
|
||||
<i class="fas fa-clock"></i>
|
||||
</div>
|
||||
<div class="detail-content">
|
||||
<strong>Current Time:</strong>
|
||||
<span id="currentTime">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Check-in Form -->
|
||||
<div class="checkin-card">
|
||||
<div class="checkin-header">
|
||||
<h2>
|
||||
<i class="fas fa-user-check"></i>
|
||||
Staff Check-In
|
||||
</h2>
|
||||
<p>Please enter your Employee ID to check in</p>
|
||||
</div>
|
||||
|
||||
<form id="checkinForm" class="checkin-form">
|
||||
<div class="form-group">
|
||||
<label for="employee_id">
|
||||
<i class="fas fa-id-badge"></i>
|
||||
Employee ID
|
||||
</label>
|
||||
<input type="text"
|
||||
id="employee_id"
|
||||
name="employee_id"
|
||||
required
|
||||
placeholder="Enter your Employee ID"
|
||||
autocomplete="off"
|
||||
maxlength="20">
|
||||
<small class="form-help">Use your official employee ID (3-20 characters)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="btn-content">
|
||||
<i class="fas fa-check"></i>
|
||||
Check In Now
|
||||
</span>
|
||||
<div class="btn-loader" style="display: none;">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
Processing...
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Status Messages -->
|
||||
<div id="statusMessage" class="status-message" style="display: none;">
|
||||
<!-- Dynamic status messages will appear here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Card (Hidden by default) -->
|
||||
<div id="successCard" class="success-card" style="display: none;">
|
||||
<div class="success-icon">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</div>
|
||||
<h2>Check-In Successful!</h2>
|
||||
<div class="success-details">
|
||||
<div class="success-item">
|
||||
<strong>Employee ID:</strong>
|
||||
<span id="successEmployeeId">-</span>
|
||||
</div>
|
||||
<div class="success-item">
|
||||
<strong>Location:</strong>
|
||||
<span id="successLocation">-</span>
|
||||
</div>
|
||||
<div class="success-item">
|
||||
<strong>Event:</strong>
|
||||
<span id="successEvent">-</span>
|
||||
</div>
|
||||
<div class="success-item">
|
||||
<strong>Time:</strong>
|
||||
<span id="successTime">-</span>
|
||||
</div>
|
||||
<div class="success-item">
|
||||
<strong>Date:</strong>
|
||||
<span id="successDate">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="success-actions">
|
||||
<button onclick="checkInAnother()" class="btn btn-secondary">
|
||||
<i class="fas fa-plus"></i>
|
||||
Check In Another Employee
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="destination-footer">
|
||||
<p>
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
Secure attendance tracking system
|
||||
</p>
|
||||
<small>© 2025 QR Management System</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div id="loadingOverlay" class="loading-overlay" style="display: none;">
|
||||
<div class="loading-spinner">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
<p>Processing check-in...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/qr_destination.js') }}"></script>
|
||||
<script>
|
||||
// Initialize page with QR code URL
|
||||
window.qrUrl = '{{ qr_code.qr_url }}';
|
||||
window.locationName = '{{ qr_code.location }}';
|
||||
window.eventName = '{{ qr_code.location_event }}';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,79 @@
|
||||
{% extends "base.html" %} {% block title %}QR Code Not Found - QR Code
|
||||
Management{% endblock %} {% block content %}
|
||||
<div class="error-page">
|
||||
<div class="error-container">
|
||||
<div class="error-icon">
|
||||
<i class="fas fa-qrcode"></i>
|
||||
</div>
|
||||
<div class="error-message">
|
||||
<h1>QR Code Not Found</h1>
|
||||
<p>
|
||||
The QR code you're looking for doesn't exist or has been deactivated.
|
||||
</p>
|
||||
<p class="error-detail">
|
||||
Please check with the person who provided this QR code.
|
||||
</p>
|
||||
</div>
|
||||
<div class="error-actions">
|
||||
{% if session.user_id %}
|
||||
<a href="{{ url_for('dashboard') }}" class="btn btn-primary">
|
||||
<i class="fas fa-home"></i>
|
||||
Go to Dashboard
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('login') }}" class="btn btn-primary">
|
||||
<i class="fas fa-sign-in-alt"></i>
|
||||
Login
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.error-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 60vh;
|
||||
padding: var(--spacing-8);
|
||||
}
|
||||
|
||||
.error-container {
|
||||
text-align: center;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 6rem;
|
||||
color: var(--gray-400);
|
||||
margin-bottom: var(--spacing-6);
|
||||
}
|
||||
|
||||
.error-message h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.error-message p {
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--gray-600);
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.error-detail {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--gray-500);
|
||||
font-style: italic;
|
||||
margin-bottom: var(--spacing-8) !important;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-4);
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,181 @@
|
||||
{% extends "base.html" %} {% block title %}Register - QR Code Management{%
|
||||
endblock %} {% block content %}
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<div class="auth-logo">
|
||||
<i class="fas fa-user-plus"></i>
|
||||
</div>
|
||||
<h1>Create Account</h1>
|
||||
<p>Join our QR management platform</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" class="auth-form" id="registerForm">
|
||||
<div class="form-group">
|
||||
<label for="full_name">
|
||||
<i class="fas fa-id-card"></i>
|
||||
Full Name
|
||||
</label>
|
||||
<input type="text" id="full_name" name="full_name" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">
|
||||
<i class="fas fa-envelope"></i>
|
||||
Email Address
|
||||
</label>
|
||||
<input type="email" id="email" name="email" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">
|
||||
<i class="fas fa-user"></i>
|
||||
Username
|
||||
</label>
|
||||
<input type="text" id="username" name="username" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">
|
||||
<i class="fas fa-lock"></i>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
minlength="6"
|
||||
/>
|
||||
<div class="password-strength" id="passwordStrength"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirm_password">
|
||||
<i class="fas fa-lock"></i>
|
||||
Confirm Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirm_password"
|
||||
name="confirm_password"
|
||||
required
|
||||
/>
|
||||
<div class="password-match" id="passwordMatch"></div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-full">
|
||||
<i class="fas fa-user-plus"></i>
|
||||
Create Account
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p>
|
||||
Already have an account?
|
||||
<a href="{{ url_for('login') }}" class="auth-link">Sign in here</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %} {% block extra_scripts %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const form = document.getElementById("registerForm");
|
||||
const password = document.getElementById("password");
|
||||
const confirmPassword = document.getElementById("confirm_password");
|
||||
const passwordStrength = document.getElementById("passwordStrength");
|
||||
const passwordMatch = document.getElementById("passwordMatch");
|
||||
|
||||
// Password strength checker
|
||||
password.addEventListener("input", function () {
|
||||
const value = this.value;
|
||||
let strength = 0;
|
||||
let feedback = [];
|
||||
|
||||
if (value.length >= 6) strength++;
|
||||
else feedback.push("At least 6 characters");
|
||||
|
||||
if (/[A-Z]/.test(value)) strength++;
|
||||
else feedback.push("One uppercase letter");
|
||||
|
||||
if (/[a-z]/.test(value)) strength++;
|
||||
else feedback.push("One lowercase letter");
|
||||
|
||||
if (/[0-9]/.test(value)) strength++;
|
||||
else feedback.push("One number");
|
||||
|
||||
if (/[^A-Za-z0-9]/.test(value)) strength++;
|
||||
else feedback.push("One special character");
|
||||
|
||||
let strengthText = "";
|
||||
let strengthClass = "";
|
||||
|
||||
if (strength < 2) {
|
||||
strengthText = "Weak";
|
||||
strengthClass = "weak";
|
||||
} else if (strength < 4) {
|
||||
strengthText = "Medium";
|
||||
strengthClass = "medium";
|
||||
} else {
|
||||
strengthText = "Strong";
|
||||
strengthClass = "strong";
|
||||
}
|
||||
|
||||
passwordStrength.className = `password-strength ${strengthClass}`;
|
||||
passwordStrength.textContent = `Strength: ${strengthText}`;
|
||||
|
||||
if (feedback.length > 0 && value.length > 0) {
|
||||
passwordStrength.textContent += ` (Missing: ${feedback.join(", ")})`;
|
||||
}
|
||||
});
|
||||
|
||||
// Password match checker
|
||||
function checkPasswordMatch() {
|
||||
if (confirmPassword.value) {
|
||||
if (password.value === confirmPassword.value) {
|
||||
passwordMatch.textContent = "Passwords match";
|
||||
passwordMatch.className = "password-match match";
|
||||
} else {
|
||||
passwordMatch.textContent = "Passwords do not match";
|
||||
passwordMatch.className = "password-match no-match";
|
||||
}
|
||||
} else {
|
||||
passwordMatch.textContent = "";
|
||||
passwordMatch.className = "password-match";
|
||||
}
|
||||
}
|
||||
|
||||
password.addEventListener("input", checkPasswordMatch);
|
||||
confirmPassword.addEventListener("input", checkPasswordMatch);
|
||||
|
||||
// Form submission validation
|
||||
form.addEventListener("submit", function (e) {
|
||||
if (password.value !== confirmPassword.value) {
|
||||
e.preventDefault();
|
||||
alert("Passwords do not match!");
|
||||
return;
|
||||
}
|
||||
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
submitBtn.innerHTML =
|
||||
'<i class="fas fa-spinner fa-spin"></i> Creating Account...';
|
||||
submitBtn.disabled = true;
|
||||
});
|
||||
|
||||
// Add focus effects
|
||||
const inputs = form.querySelectorAll("input");
|
||||
inputs.forEach((input) => {
|
||||
input.addEventListener("focus", function () {
|
||||
this.parentElement.classList.add("focused");
|
||||
});
|
||||
|
||||
input.addEventListener("blur", function () {
|
||||
if (!this.value) {
|
||||
this.parentElement.classList.remove("focused");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user