Initial Codes

This commit is contained in:
NguyenND
2025-07-29 17:02:08 -04:00
committed by GitHub
parent 946b18f142
commit 7459d0fd15
34 changed files with 23480 additions and 0 deletions
+327
View File
@@ -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 %}
+144
View File
@@ -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>&copy; 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>
+609
View File
@@ -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
+590
View File
@@ -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 %}
+392
View File
@@ -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
+742
View File
@@ -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 %}
+79
View File
@@ -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 %}
+73
View File
@@ -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 %}
+77
View File
@@ -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 %}
+73
View File
@@ -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
+186
View File
@@ -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>
+79
View File
@@ -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 %}
+181
View File
@@ -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
+2102
View File
File diff suppressed because it is too large Load Diff