Files
GOV_QR_Codes_Management/templates/edit_qr_code.html
T
2025-07-29 17:02:08 -04:00

1885 lines
54 KiB
HTML

{% extends "base.html" %}
{% block title %}Edit QR Code - QR Code Management{% endblock %}
{% block content %}
<div class="form-page">
<div class="form-card">
<div class="form-header">
<h1>
<i class="fas fa-edit"></i>
Edit QR Code
</h1>
<p>Modify the information for "{{ qr_code.name }}"</p>
</div>
<!-- Current QR Code Display -->
<div class="current-qr-section">
<div class="current-qr-header">
<h3>
<i class="fas fa-qrcode"></i>
Current QR Code
</h3>
<p>This is the current version of your QR code</p>
</div>
<div class="current-qr-display">
<div class="current-qr-image">
<img src="data:image/png;base64,{{ qr_code.qr_code_image }}"
alt="{{ qr_code.name }} QR Code"
onclick="showCurrentQRModal()">
<div class="image-overlay">
<i class="fas fa-search-plus"></i>
<span>Click to enlarge</span>
</div>
</div>
<div class="current-qr-info">
<div class="qr-meta">
<div class="meta-item">
<i class="fas fa-calendar-plus"></i>
<span>Created: {{ qr_code.created_date.strftime('%B %d, %Y') }}</span>
</div>
<div class="meta-item">
<i class="fas fa-user"></i>
<span>By: {{ qr_code.creator.full_name }}</span>
</div>
<div class="meta-item">
<i class="fas fa-hashtag"></i>
<span>ID: #{{ qr_code.id }}</span>
</div>
</div>
<div class="current-details">
<div class="detail-row">
<strong>Location:</strong>
<span>{{ qr_code.location }}</span>
</div>
<div class="detail-row">
<strong>Event:</strong>
<span>{{ qr_code.location_event }}</span>
</div>
<div class="detail-row">
<strong>Address:</strong>
<span class="address-text">{{ qr_code.location_address }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Form -->
<form method="POST" class="form" id="editQRForm">
<!-- Change Detection Warning -->
<div class="change-warning" id="changeWarning" style="display: none;">
<div class="warning-header">
<i class="fas fa-exclamation-triangle"></i>
<span>Changes Detected</span>
</div>
<div class="warning-content">
<p>You have made changes to the QR code information. A new QR code will be generated to reflect these changes.</p>
<div class="changes-list" id="changesList">
<!-- Dynamic change list will be populated here -->
</div>
</div>
</div>
<!-- Basic Information Section -->
<div class="form-section">
<div class="section-header">
<h3>
<i class="fas fa-info-circle"></i>
Basic Information
</h3>
<p>Update the essential details for your QR code</p>
</div>
<div class="form-group">
<label for="name">
<i class="fas fa-tag"></i>
QR Code Name
</label>
<input type="text" id="name" name="name" required
value="{{ qr_code.name }}"
placeholder="Enter a descriptive name for this QR code"
maxlength="100"
data-original="{{ qr_code.name }}">
<small class="form-help">This name helps you identify the QR code in your dashboard</small>
<div class="character-counter">
<span id="nameCounter">{{ qr_code.name|length }}/100</span>
</div>
</div>
<div class="form-group">
<label for="location">
<i class="fas fa-map-marker-alt"></i>
Location Name
</label>
<input type="text" id="location" name="location" required
value="{{ qr_code.location }}"
placeholder="e.g., Conference Room A, Main Lobby, etc."
maxlength="100"
data-original="{{ qr_code.location }}">
<small class="form-help">The name of the physical location where this QR code will be used</small>
<div class="character-counter">
<span id="locationCounter">{{ qr_code.location|length }}/100</span>
</div>
</div>
</div>
<!-- Location Details Section -->
<div class="form-section">
<div class="section-header">
<h3>
<i class="fas fa-building"></i>
Location Details
</h3>
<p>Update the complete address and event information</p>
</div>
<div class="form-group">
<label for="location_address">
<i class="fas fa-home"></i>
Complete Address
</label>
<textarea id="location_address" name="location_address" required
placeholder="Enter the full address including street, city, state, and ZIP code"
rows="3" maxlength="500"
data-original="{{ qr_code.location_address }}">{{ qr_code.location_address }}</textarea>
<small class="form-help">Include all address details for accurate location identification</small>
<div class="character-counter">
<span id="addressCounter">{{ qr_code.location_address|length }}/500</span>
</div>
</div>
<div class="form-group">
<label for="location_event">
<i class="fas fa-calendar-alt"></i>
Event or Purpose
</label>
<input type="text" id="location_event" name="location_event" required
value="{{ qr_code.location_event }}"
placeholder="e.g., Weekly Team Meeting, Customer Check-in, Training Session"
maxlength="200"
data-original="{{ qr_code.location_event }}">
<small class="form-help">Describe the event or purpose for this QR code</small>
<div class="character-counter">
<span id="eventCounter">{{ qr_code.location_event|length }}/200</span>
</div>
</div>
</div>
<!-- Preview New QR Code Section -->
<div class="form-section">
<div class="section-header">
<h3>
<i class="fas fa-eye"></i>
Preview Updated QR Code
</h3>
<p>See how your QR code will look with the new information</p>
</div>
<div class="preview-container">
<div class="preview-area" id="previewArea">
<div class="preview-placeholder">
<i class="fas fa-qrcode"></i>
<p>Make changes above to see the updated QR code preview</p>
<button type="button" class="btn btn-secondary btn-sm" id="generatePreview">
<i class="fas fa-sync-alt"></i>
Generate Preview
</button>
</div>
<canvas id="previewCanvas" width="250" height="250" style="display: none;"></canvas>
</div>
<div class="preview-info" id="previewInfo" style="display: none;">
<h4>Updated Information</h4>
<div class="preview-details">
<div class="preview-item">
<strong>Name:</strong> <span id="previewName">-</span>
</div>
<div class="preview-item">
<strong>Location:</strong> <span id="previewLocation">-</span>
</div>
<div class="preview-item">
<strong>Event:</strong> <span id="previewEvent">-</span>
</div>
<div class="preview-item">
<strong>Address:</strong> <span id="previewAddress">-</span>
</div>
</div>
<div class="preview-actions">
<button type="button" class="btn btn-info btn-sm" onclick="showPreviewModal()">
<i class="fas fa-expand-alt"></i>
View Full Size
</button>
<button type="button" class="btn btn-success btn-sm" onclick="downloadPreview()">
<i class="fas fa-download"></i>
Download Preview
</button>
</div>
</div>
</div>
</div>
<!-- Data Comparison Section -->
<div class="form-section">
<div class="section-header">
<h3>
<i class="fas fa-exchange-alt"></i>
Data Comparison
</h3>
<p>Compare the current and updated QR code data</p>
</div>
<div class="comparison-container">
<div class="comparison-side">
<h4>
<i class="fas fa-history"></i>
Current Data
</h4>
<div class="data-preview">
<pre id="currentDataPreview">Event: {{ qr_code.location_event }}
Location: {{ qr_code.location }}
Address: {{ qr_code.location_address }}</pre>
</div>
</div>
<div class="comparison-divider">
<i class="fas fa-arrow-right"></i>
</div>
<div class="comparison-side">
<h4>
<i class="fas fa-edit"></i>
Updated Data
</h4>
<div class="data-preview">
<pre id="updatedDataPreview">Make changes above to see updated data...</pre>
</div>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="form-actions">
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i>
Back to Dashboard
</a>
<button type="button" class="btn btn-warning" id="resetChanges">
<i class="fas fa-undo"></i>
Reset Changes
</button>
<button type="submit" class="btn btn-primary" id="saveChanges">
<i class="fas fa-save"></i>
Save Changes
</button>
</div>
</form>
</div>
</div>
<!-- Current QR Code Modal -->
<div id="currentQRModal" class="modal">
<div class="modal-content qr-modal">
<div class="modal-header">
<h3>Current QR Code - {{ qr_code.name }}</h3>
<button class="modal-close" onclick="closeCurrentQRModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<div class="modal-qr-display">
<div class="modal-qr-image">
<img src="data:image/png;base64,{{ qr_code.qr_code_image }}"
alt="{{ qr_code.name }} QR Code">
</div>
<div class="modal-qr-details">
<h4>QR Code Information</h4>
<div class="modal-detail-item">
<i class="fas fa-tag"></i>
<span><strong>Name:</strong> {{ qr_code.name }}</span>
</div>
<div class="modal-detail-item">
<i class="fas fa-map-marker-alt"></i>
<span><strong>Location:</strong> {{ qr_code.location }}</span>
</div>
<div class="modal-detail-item">
<i class="fas fa-calendar-alt"></i>
<span><strong>Event:</strong> {{ qr_code.location_event }}</span>
</div>
<div class="modal-detail-item">
<i class="fas fa-home"></i>
<span><strong>Address:</strong> {{ qr_code.location_address }}</span>
</div>
<div class="modal-detail-item">
<i class="fas fa-calendar-plus"></i>
<span><strong>Created:</strong> {{ qr_code.created_date.strftime('%B %d, %Y at %H:%M') }}</span>
</div>
<div class="modal-detail-item">
<i class="fas fa-user"></i>
<span><strong>Created by:</strong> {{ qr_code.creator.full_name }}</span>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button onclick="downloadCurrentQR()" class="btn btn-info">
<i class="fas fa-download"></i>
Download Current
</button>
<button onclick="closeCurrentQRModal()" class="btn btn-secondary">
Close
</button>
</div>
</div>
</div>
<!-- Preview QR Code Modal -->
<div id="previewQRModal" class="modal">
<div class="modal-content qr-modal">
<div class="modal-header">
<h3>Updated QR Code Preview</h3>
<button class="modal-close" onclick="closePreviewModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<div class="modal-qr-preview">
<div class="modal-qr-image">
<canvas id="modalPreviewCanvas" width="350" height="350"></canvas>
</div>
<div class="modal-qr-info">
<h4>Updated QR Code Information</h4>
<div class="modal-info-grid">
<div class="info-item">
<strong>Name:</strong>
<span id="modalPreviewName">-</span>
</div>
<div class="info-item">
<strong>Location:</strong>
<span id="modalPreviewLocation">-</span>
</div>
<div class="info-item">
<strong>Event:</strong>
<span id="modalPreviewEvent">-</span>
</div>
<div class="info-item">
<strong>Address:</strong>
<span id="modalPreviewAddress">-</span>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button onclick="downloadPreviewQR()" class="btn btn-success">
<i class="fas fa-download"></i>
Download Preview
</button>
<button onclick="closePreviewModal()" class="btn btn-secondary">
Close Preview
</button>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<!-- QR Code Generation Library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrious/4.0.2/qrious.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
initializeQRCodeEditing();
});
// Global variables
let originalData = {};
let hasChanges = false;
function initializeQRCodeEditing() {
const form = document.getElementById('editQRForm');
// Store original data
storeOriginalData();
// Initialize character counters
initializeCharacterCounters();
// Initialize change detection
initializeChangeDetection();
// Initialize preview functionality
initializePreviewFunctionality();
// Form submission handling
form.addEventListener('submit', function(e) {
if (!validateForm()) {
e.preventDefault();
return false;
}
if (!hasChanges) {
e.preventDefault();
showToast('No changes detected. Please modify the information to update the QR code.', 'info');
return false;
}
// Confirm changes before submission
if (!confirmSaveChanges()) {
e.preventDefault();
return false;
}
// 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;
});
// Reset changes functionality
document.getElementById('resetChanges').addEventListener('click', resetAllChanges);
// Generate preview functionality
document.getElementById('generatePreview').addEventListener('click', function() {
if (validateForm()) {
generateQRPreview();
}
});
}
function storeOriginalData() {
originalData = {
name: document.getElementById('name').dataset.original,
location: document.getElementById('location').dataset.original,
location_address: document.getElementById('location_address').dataset.original,
location_event: document.getElementById('location_event').dataset.original
};
}
function initializeCharacterCounters() {
const counters = [
{ input: 'name', counter: 'nameCounter', max: 100 },
{ input: 'location', counter: 'locationCounter', max: 100 },
{ input: 'location_address', counter: 'addressCounter', max: 500 },
{ input: 'location_event', counter: 'eventCounter', max: 200 }
];
counters.forEach(item => {
const input = document.getElementById(item.input);
const counter = document.getElementById(item.counter);
if (input && counter) {
input.addEventListener('input', function() {
const length = this.value.length;
counter.textContent = `${length}/${item.max}`;
if (length > item.max * 0.9) {
counter.classList.add('warning');
} else {
counter.classList.remove('warning');
}
});
}
});
}
function initializeChangeDetection() {
const inputs = document.querySelectorAll('#name, #location, #location_address, #location_event');
inputs.forEach(input => {
input.addEventListener('input', function() {
detectChanges();
updateDataComparison();
});
input.addEventListener('blur', function() {
validateField(this);
});
});
}
function detectChanges() {
const currentData = {
name: document.getElementById('name').value.trim(),
location: document.getElementById('location').value.trim(),
location_address: document.getElementById('location_address').value.trim(),
location_event: document.getElementById('location_event').value.trim()
};
const changes = [];
let hasAnyChanges = false;
Object.keys(originalData).forEach(key => {
if (currentData[key] !== originalData[key]) {
changes.push({
field: key,
original: originalData[key],
current: currentData[key]
});
hasAnyChanges = true;
}
});
hasChanges = hasAnyChanges;
updateChangeWarning(changes);
updateSaveButton();
}
function updateChangeWarning(changes) {
const warningDiv = document.getElementById('changeWarning');
const changesList = document.getElementById('changesList');
if (changes.length > 0) {
let changesHTML = '<ul>';
changes.forEach(change => {
const fieldName = getFieldDisplayName(change.field);
changesHTML += `
<li>
<strong>${fieldName}:</strong>
<br>
<span class="change-from">From: "${change.original}"</span>
<br>
<span class="change-to">To: "${change.current}"</span>
</li>
`;
});
changesHTML += '</ul>';
changesList.innerHTML = changesHTML;
warningDiv.style.display = 'block';
warningDiv.classList.add('fade-in');
} else {
warningDiv.style.display = 'none';
warningDiv.classList.remove('fade-in');
}
}
function getFieldDisplayName(fieldName) {
const fieldNames = {
name: 'QR Code Name',
location: 'Location Name',
location_address: 'Complete Address',
location_event: 'Event or Purpose'
};
return fieldNames[fieldName] || fieldName;
}
function updateSaveButton() {
const saveBtn = document.getElementById('saveChanges');
if (hasChanges) {
saveBtn.classList.remove('btn-primary');
saveBtn.classList.add('btn-success');
saveBtn.innerHTML = '<i class="fas fa-save"></i> Save Changes';
saveBtn.disabled = false;
} else {
saveBtn.classList.remove('btn-success');
saveBtn.classList.add('btn-primary');
saveBtn.innerHTML = '<i class="fas fa-save"></i> No Changes';
saveBtn.disabled = true;
}
}
function updateDataComparison() {
const currentData = {
name: document.getElementById('name').value.trim(),
location: document.getElementById('location').value.trim(),
location_address: document.getElementById('location_address').value.trim(),
location_event: document.getElementById('location_event').value.trim()
};
let updatedDataText = '';
if (currentData.location_event) updatedDataText += `Event: ${currentData.location_event}\n`;
if (currentData.location) updatedDataText += `Location: ${currentData.location}\n`;
if (currentData.location_address) updatedDataText += `Address: ${currentData.location_address}`;
document.getElementById('updatedDataPreview').textContent =
updatedDataText || 'No data to preview...';
}
function initializePreviewFunctionality() {
// Auto-update preview info when form changes
const inputs = document.querySelectorAll('#name, #location, #location_address, #location_event');
inputs.forEach(input => {
input.addEventListener('input', updatePreviewInfo);
});
}
function updatePreviewInfo() {
const name = document.getElementById('name').value.trim() || '-';
const location = document.getElementById('location').value.trim() || '-';
const event = document.getElementById('location_event').value.trim() || '-';
const address = document.getElementById('location_address').value.trim() || '-';
document.getElementById('previewName').textContent = name;
document.getElementById('previewLocation').textContent = location;
document.getElementById('previewEvent').textContent = event;
document.getElementById('previewAddress').textContent = address;
}
function generateQRPreview() {
const name = document.getElementById('name').value.trim();
const location = document.getElementById('location').value.trim();
const address = document.getElementById('location_address').value.trim();
const event = document.getElementById('location_event').value.trim();
if (!name || !location || !address || !event) {
showToast('Please fill in all required fields before generating preview', 'warning');
return;
}
// Generate QR code data
const qrData = `Event: ${event}\nLocation: ${location}\nAddress: ${address}`;
// Generate QR code
const canvas = document.getElementById('previewCanvas');
const qr = new QRious({
element: canvas,
value: qrData,
size: 250,
level: 'M'
});
// Show preview
document.querySelector('.preview-placeholder').style.display = 'none';
canvas.style.display = 'block';
document.getElementById('previewInfo').style.display = 'block';
// Update preview info
updatePreviewInfo();
showToast('QR code preview generated successfully!', 'success');
}
function showPreviewModal() {
const canvas = document.getElementById('previewCanvas');
if (canvas.style.display === 'none') {
showToast('Please generate a preview first', 'warning');
return;
}
// Copy canvas to modal
const modalCanvas = document.getElementById('modalPreviewCanvas');
const modalContext = modalCanvas.getContext('2d');
modalContext.clearRect(0, 0, modalCanvas.width, modalCanvas.height);
modalContext.drawImage(canvas, 0, 0, modalCanvas.width, modalCanvas.height);
// Update modal info
document.getElementById('modalPreviewName').textContent = document.getElementById('previewName').textContent;
document.getElementById('modalPreviewLocation').textContent = document.getElementById('previewLocation').textContent;
document.getElementById('modalPreviewEvent').textContent = document.getElementById('previewEvent').textContent;
document.getElementById('modalPreviewAddress').textContent = document.getElementById('previewAddress').textContent;
// Show modal
const modal = document.getElementById('previewQRModal');
modal.style.display = 'flex';
setTimeout(() => modal.classList.add('show'), 10);
}
function closePreviewModal() {
const modal = document.getElementById('previewQRModal');
modal.classList.remove('show');
setTimeout(() => {
modal.style.display = 'none';
}, 200);
}
function showCurrentQRModal() {
const modal = document.getElementById('currentQRModal');
modal.style.display = 'flex';
setTimeout(() => modal.classList.add('show'), 10);
}
function closeCurrentQRModal() {
const modal = document.getElementById('currentQRModal');
modal.classList.remove('show');
setTimeout(() => {
modal.style.display = 'none';
}, 200);
}
function downloadCurrentQR() {
const qrImage = '{{ qr_code.qr_code_image }}';
const qrName = '{{ qr_code.name }}';
const link = document.createElement('a');
link.href = 'data:image/png;base64,' + qrImage;
link.download = qrName.replace(/[^a-z0-9]/gi, '_').toLowerCase() + '_current_qr.png';
link.click();
showToast('Current QR code downloaded!', 'success');
}
function downloadPreview() {
const canvas = document.getElementById('previewCanvas');
if (canvas.style.display === 'none') {
showToast('Please generate a preview first', 'warning');
return;
}
const name = document.getElementById('name').value.trim();
const link = document.createElement('a');
link.download = name.replace(/[^a-z0-9]/gi, '_').toLowerCase() + '_updated_preview.png';
link.href = canvas.toDataURL();
link.click();
showToast('Preview QR code downloaded!', 'success');
}
function downloadPreviewQR() {
const canvas = document.getElementById('modalPreviewCanvas');
const name = document.getElementById('name').value.trim();
const link = document.createElement('a');
link.download = name.replace(/[^a-z0-9]/gi, '_').toLowerCase() + '_updated_preview.png';
link.href = canvas.toDataURL();
link.click();
showToast('Preview QR code downloaded!', 'success');
}
function resetAllChanges() {
if (!hasChanges) {
showToast('No changes to reset', 'info');
return;
}
if (!confirm('Are you sure you want to reset all changes? This will restore the original values.')) {
return;
}
// Reset all form fields to original values
document.getElementById('name').value = originalData.name;
document.getElementById('location').value = originalData.location;
document.getElementById('location_address').value = originalData.location_address;
document.getElementById('location_event').value = originalData.location_event;
// Update character counters
document.getElementById('nameCounter').textContent = `${originalData.name.length}/100`;
document.getElementById('locationCounter').textContent = `${originalData.location.length}/100`;
document.getElementById('addressCounter').textContent = `${originalData.location_address.length}/500`;
document.getElementById('eventCounter').textContent = `${originalData.location_event.length}/200`;
// Reset preview
document.querySelector('.preview-placeholder').style.display = 'block';
document.getElementById('previewCanvas').style.display = 'none';
document.getElementById('previewInfo').style.display = 'none';
// Clear errors
clearAllErrors();
// Re-detect changes (should be none now)
detectChanges();
updateDataComparison();
showToast('All changes have been reset', 'success');
}
function validateForm() {
const requiredFields = [
{ id: 'name', name: 'QR Code Name' },
{ id: 'location', name: 'Location Name' },
{ id: 'location_address', name: 'Complete Address' },
{ id: 'location_event', name: 'Event or Purpose' }
];
let isValid = true;
requiredFields.forEach(field => {
if (!validateField(document.getElementById(field.id))) {
isValid = false;
}
});
return isValid;
}
function validateField(input) {
const value = input.value.trim();
if (input.hasAttribute('required') && !value) {
showFieldError(input, 'This field is required');
return false;
}
// Specific field validations
if (input.id === 'name' && value && value.length < 3) {
showFieldError(input, 'Name must be at least 3 characters');
return false;
}
if (input.id === 'location_address' && value && value.length < 10) {
showFieldError(input, 'Please provide a complete address');
return false;
}
clearFieldError(input);
return true;
}
function showFieldError(input, message) {
const formGroup = input.closest('.form-group');
formGroup.classList.add('has-error');
let errorElement = formGroup.querySelector('.error-message');
if (!errorElement) {
errorElement = document.createElement('div');
errorElement.className = 'error-message';
formGroup.appendChild(errorElement);
}
errorElement.textContent = message;
}
function clearFieldError(input) {
const formGroup = input.closest('.form-group');
formGroup.classList.remove('has-error');
const errorElement = formGroup.querySelector('.error-message');
if (errorElement) {
errorElement.remove();
}
}
function clearAllErrors() {
const errorMessages = document.querySelectorAll('.error-message');
errorMessages.forEach(error => error.remove());
const errorGroups = document.querySelectorAll('.form-group.has-error');
errorGroups.forEach(group => group.classList.remove('has-error'));
}
function confirmSaveChanges() {
const changeCount = Object.keys(originalData).filter(key => {
const currentValue = document.getElementById(key).value.trim();
return currentValue !== originalData[key];
}).length;
return confirm(
`You have made ${changeCount} change${changeCount !== 1 ? 's' : ''} to this QR code.\n\n` +
'Saving will generate a new QR code with the updated information.\n\n' +
'Are you sure you want to continue?'
);
}
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.innerHTML = `
<div class="toast-content">
<i class="fas ${getToastIcon(type)}"></i>
<span>${message}</span>
</div>
`;
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
padding: 1rem;
z-index: 1000;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
border-left: 4px solid ${getToastColor(type)};
max-width: 400px;
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '1';
toast.style.transform = 'translateX(0)';
}, 100);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(100%)';
setTimeout(() => {
if (document.body.contains(toast)) {
document.body.removeChild(toast);
}
}, 300);
}, 4000);
}
function getToastIcon(type) {
const icons = {
success: 'fa-check-circle',
error: 'fa-exclamation-circle',
warning: 'fa-exclamation-triangle',
info: 'fa-info-circle'
};
return icons[type] || icons.info;
}
function getToastColor(type) {
const colors = {
success: '#059669',
error: '#dc2626',
warning: '#d97706',
info: '#0891b2'
};
return colors[type] || colors.info;
}
// Close modals when clicking outside
document.addEventListener('click', function(e) {
const previewModal = document.getElementById('previewQRModal');
const currentModal = document.getElementById('currentQRModal');
if (e.target === previewModal) {
closePreviewModal();
}
if (e.target === currentModal) {
closeCurrentQRModal();
}
});
// Close modals with Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closePreviewModal();
closeCurrentQRModal();
}
});
// Warn user about unsaved changes when leaving page
window.addEventListener('beforeunload', function(e) {
if (hasChanges) {
e.preventDefault();
e.returnValue = '';
return 'You have unsaved changes. Are you sure you want to leave?';
}
});
</script>
<style>
/* Edit QR Code Page Specific Styles */
.form-page {
max-width: 900px;
margin: 0 auto;
padding: 0 1rem;
}
.form-card {
background: var(--white);
border-radius: var(--radius-2xl);
box-shadow: var(--shadow);
padding: 2rem;
margin-bottom: 2rem;
}
.form-header {
text-align: center;
margin-bottom: 2rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--gray-200);
}
.form-header h1 {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-size: 2rem;
font-weight: 700;
color: var(--gray-900);
margin-bottom: 0.5rem;
}
.form-header p {
color: var(--gray-500);
font-size: 1rem;
}
/* Current QR Code Section */
.current-qr-section {
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);
}
.current-qr-header {
text-align: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--gray-200);
}
.current-qr-header h3 {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-size: 1.25rem;
font-weight: 600;
color: var(--gray-800);
margin-bottom: 0.5rem;
}
.current-qr-header p {
color: var(--gray-600);
font-size: 0.875rem;
}
.current-qr-display {
display: grid;
grid-template-columns: auto 1fr;
gap: 2rem;
align-items: center;
}
.current-qr-image {
position: relative;
background: var(--white);
padding: 1rem;
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
cursor: pointer;
transition: var(--transition);
}
.current-qr-image:hover {
transform: scale(1.02);
box-shadow: var(--shadow-lg);
}
.current-qr-image img {
width: 150px;
height: 150px;
border-radius: var(--radius);
}
.image-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
color: var(--white);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: var(--radius-lg);
opacity: 0;
transition: var(--transition);
}
.current-qr-image:hover .image-overlay {
opacity: 1;
}
.image-overlay i {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.current-qr-info {
display: grid;
gap: 1.5rem;
}
.qr-meta {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.meta-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: var(--gray-600);
background: var(--white);
padding: 0.5rem 0.75rem;
border-radius: var(--radius);
border: 1px solid var(--gray-200);
}
.meta-item i {
color: var(--primary-color);
}
.current-details {
display: grid;
gap: 0.75rem;
}
.detail-row {
display: grid;
grid-template-columns: 100px 1fr;
gap: 0.5rem;
font-size: 0.875rem;
padding: 0.5rem;
background: var(--white);
border-radius: var(--radius);
border: 1px solid var(--gray-200);
}
.detail-row strong {
color: var(--gray-700);
font-weight: 600;
}
.detail-row span {
color: var(--gray-900);
}
.address-text {
word-break: break-word;
line-height: 1.4;
}
/* Change Warning Section */
.change-warning {
margin-bottom: 2rem;
padding: 1rem;
background: rgba(217, 119, 6, 0.1);
border: 1px solid var(--warning-color);
border-radius: var(--radius-lg);
opacity: 0;
transform: translateY(-10px);
transition: all 0.3s ease;
}
.change-warning.fade-in {
opacity: 1;
transform: translateY(0);
}
.warning-header {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
color: var(--warning-color);
margin-bottom: 0.75rem;
}
.warning-content p {
color: #92400e;
margin-bottom: 1rem;
font-size: 0.875rem;
}
.changes-list ul {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 0.75rem;
}
.changes-list li {
padding: 0.75rem;
background: var(--white);
border-radius: var(--radius);
border-left: 3px solid var(--warning-color);
font-size: 0.875rem;
}
.change-from {
color: var(--danger-color);
font-style: italic;
}
.change-to {
color: var(--success-color);
font-weight: 500;
}
/* Form Sections */
.form-section {
margin-bottom: 2rem;
padding: 1.5rem;
background: var(--gray-50);
border-radius: var(--radius-xl);
border: 1px solid var(--gray-200);
}
.section-header {
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--gray-200);
}
.section-header h3 {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.25rem;
font-weight: 600;
color: var(--gray-800);
margin-bottom: 0.5rem;
}
.section-header p {
color: var(--gray-600);
font-size: 0.875rem;
}
/* Form Groups */
.form-group {
position: relative;
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,
.form-group textarea {
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,
.form-group textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
.form-help {
display: block;
margin-top: 0.5rem;
font-size: 0.75rem;
color: var(--gray-500);
}
/* Character Counters */
.character-counter {
position: absolute;
bottom: -1.5rem;
right: 0;
font-size: 0.75rem;
color: var(--gray-400);
}
.character-counter.warning {
color: var(--warning-color);
font-weight: 600;
}
/* Preview Container */
.preview-container {
display: grid;
grid-template-columns: auto 1fr;
gap: 2rem;
align-items: center;
}
.preview-area {
display: flex;
justify-content: center;
align-items: center;
min-height: 250px;
background: var(--white);
border-radius: var(--radius-lg);
border: 2px dashed var(--gray-300);
padding: 1rem;
}
.preview-placeholder {
text-align: center;
color: var(--gray-500);
}
.preview-placeholder i {
font-size: 3rem;
margin-bottom: 1rem;
color: var(--gray-400);
}
.preview-placeholder p {
margin-bottom: 1rem;
font-size: 0.875rem;
}
.preview-info {
padding: 1rem;
background: var(--white);
border-radius: var(--radius-lg);
border: 1px solid var(--gray-200);
}
.preview-info h4 {
font-size: 1.125rem;
font-weight: 600;
color: var(--gray-900);
margin-bottom: 1rem;
}
.preview-details {
display: grid;
gap: 0.5rem;
margin-bottom: 1rem;
}
.preview-item {
display: grid;
grid-template-columns: 80px 1fr;
gap: 0.5rem;
font-size: 0.875rem;
}
.preview-item strong {
color: var(--gray-700);
font-weight: 600;
}
.preview-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
/* Data Comparison */
.comparison-container {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 1rem;
align-items: center;
}
.comparison-side h4 {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1rem;
font-weight: 600;
color: var(--gray-800);
margin-bottom: 0.75rem;
}
.data-preview {
background: var(--white);
border: 1px solid var(--gray-200);
border-radius: var(--radius);
padding: 1rem;
min-height: 120px;
}
.data-preview pre {
margin: 0;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.875rem;
color: var(--gray-700);
white-space: pre-wrap;
word-wrap: break-word;
line-height: 1.4;
}
.comparison-divider {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: var(--primary-color);
color: var(--white);
border-radius: 50%;
font-size: 1.125rem;
}
/* Form Actions */
.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);
}
/* Error States */
.form-group.has-error input,
.form-group.has-error select,
.form-group.has-error textarea {
border-color: var(--danger-color);
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
}
.error-message {
margin-top: 0.5rem;
font-size: 0.75rem;
color: var(--danger-color);
font-weight: 500;
}
/* Button States */
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.btn.btn-success {
background: var(--success-color);
color: var(--white);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(5, 150, 105, 0.4); }
70% { box-shadow: 0 0 0 10px rgba(5, 150, 105, 0); }
100% { box-shadow: 0 0 0 0 rgba(5, 150, 105, 0); }
}
/* Modal Styles */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
z-index: 2000;
align-items: center;
justify-content: center;
padding: 1rem;
opacity: 0;
transition: opacity 0.3s ease;
}
.modal.show {
opacity: 1;
}
.modal-content.qr-modal {
max-width: 700px;
width: 100%;
}
.modal-qr-display,
.modal-qr-preview {
display: grid;
grid-template-columns: auto 1fr;
gap: 2rem;
align-items: center;
}
.modal-qr-image {
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
background: var(--gray-50);
border-radius: var(--radius-lg);
border: 1px solid var(--gray-200);
}
.modal-qr-image img {
max-width: 250px;
height: auto;
border-radius: var(--radius);
}
.modal-qr-details,
.modal-qr-info {
display: grid;
gap: 1rem;
}
.modal-qr-details h4,
.modal-qr-info h4 {
font-size: 1.125rem;
font-weight: 600;
color: var(--gray-900);
margin-bottom: 1rem;
}
.modal-detail-item {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.5rem;
background: var(--white);
border-radius: var(--radius);
border: 1px solid var(--gray-200);
font-size: 0.875rem;
}
.modal-detail-item i {
color: var(--primary-color);
width: 16px;
margin-top: 0.125rem;
}
.modal-info-grid {
display: grid;
gap: 0.75rem;
}
.info-item {
display: grid;
grid-template-columns: 100px 1fr;
gap: 0.5rem;
font-size: 0.875rem;
}
.info-item strong {
color: var(--gray-700);
font-weight: 600;
}
.info-item span {
color: var(--gray-900);
word-break: break-word;
}
/* Responsive Design */
@media (max-width: 768px) {
.form-page {
padding: 0 0.5rem;
}
.form-card {
padding: 1.5rem;
}
.form-section {
padding: 1rem;
}
.current-qr-display {
grid-template-columns: 1fr;
gap: 1rem;
text-align: center;
}
.qr-meta {
justify-content: center;
}
.detail-row {
grid-template-columns: 1fr;
gap: 0.25rem;
text-align: center;
}
.preview-container {
grid-template-columns: 1fr;
gap: 1rem;
}
.comparison-container {
grid-template-columns: 1fr;
gap: 1rem;
}
.comparison-divider {
transform: rotate(90deg);
}
.form-actions {
flex-direction: column;
align-items: stretch;
}
.modal-qr-display,
.modal-qr-preview {
grid-template-columns: 1fr;
gap: 1rem;
text-align: center;
}
.info-item {
grid-template-columns: 1fr;
gap: 0.25rem;
}
.character-counter {
position: static;
text-align: right;
margin-top: 0.5rem;
}
}
@media (max-width: 480px) {
.form-header h1 {
font-size: 1.5rem;
flex-direction: column;
gap: 0.25rem;
}
.section-header h3 {
font-size: 1.125rem;
flex-direction: column;
gap: 0.25rem;
text-align: center;
}
.modal-content {
margin: 0.5rem;
max-height: calc(100vh - 1rem);
}
.current-qr-image img {
width: 120px;
height: 120px;
}
.preview-actions {
flex-direction: column;
}
}
/* Loading States */
.btn.loading {
position: relative;
pointer-events: none;
}
.btn.loading::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 16px;
height: 16px;
margin: -8px 0 0 -8px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Focus Styles for Accessibility */
.btn:focus,
input:focus,
select:focus,
textarea:focus {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}
/* High Contrast Mode Support */
@media (prefers-contrast: high) {
.form-section {
border: 2px solid var(--gray-600);
}
.current-qr-section {
border: 2px solid var(--primary-color);
}
.change-warning {
border: 2px solid var(--warning-color);
}
}
/* Reduced Motion Support */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
.change-warning {
opacity: 1;
transform: none;
}
.btn.btn-success {
animation: none;
}
}
/* Print Styles */
@media print {
.form-actions,
.modal,
.character-counter,
.preview-actions,
.change-warning {
display: none !important;
}
.form-page {
max-width: none;
padding: 0;
}
.form-card {
box-shadow: none;
border: 1px solid #000;
}
.form-section {
break-inside: avoid;
box-shadow: none;
border: 1px solid #000;
}
}
/* Toast Notification Styles */
.toast {
position: fixed;
top: 20px;
right: 20px;
background: white;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
padding: 1rem;
z-index: 1000;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
max-width: 400px;
border-left: 4px solid;
}
.toast.show {
opacity: 1;
transform: translateX(0);
}
.toast-content {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 500;
}
.toast.toast-success { border-left-color: var(--success-color); }
.toast.toast-error { border-left-color: var(--danger-color); }
.toast.toast-warning { border-left-color: var(--warning-color); }
.toast.toast-info { border-left-color: var(--info-color); }
.toast.toast-success .toast-content { color: var(--success-color); }
.toast.toast-error .toast-content { color: var(--danger-color); }
.toast.toast-warning .toast-content { color: var(--warning-color); }
.toast.toast-info .toast-content { color: var(--info-color); }
/* Fade-in Animation */
.fade-in {
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Canvas Styling */
canvas {
max-width: 100%;
height: auto;
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
}
/* Enhanced Modal Styles */
.modal-content {
background: var(--white);
border-radius: var(--radius-2xl);
box-shadow: var(--shadow-xl);
max-height: 90vh;
overflow-y: auto;
animation: modalSlideUp 0.3s ease-out;
}
@keyframes modalSlideUp {
from {
opacity: 0;
transform: translateY(30px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid var(--gray-200);
}
.modal-header h3 {
font-size: 1.25rem;
font-weight: 700;
color: var(--gray-900);
}
.modal-close {
background: none;
border: none;
padding: 0.5rem;
border-radius: var(--radius);
cursor: pointer;
color: var(--gray-500);
transition: var(--transition);
}
.modal-close:hover {
background-color: var(--gray-100);
color: var(--gray-700);
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding: 1.5rem;
border-top: 1px solid var(--gray-200);
}
</style>
{% endblock %}