diff --git a/app.py b/app.py index 3526b94..b9f38f8 100644 --- a/app.py +++ b/app.py @@ -3483,7 +3483,7 @@ def delete_qr_code(qr_id): print(f"❌ Traceback: {traceback.format_exc()}") flash('Error deleting QR code. Please try again.', 'error') return redirect(url_for('dashboard')) - + @app.route('/qr/') def qr_destination(qr_url): """QR code destination page where staff check in - PRESERVING EXACT ROUTE""" @@ -3792,6 +3792,52 @@ def toggle_qr_status(qr_id): 'message': 'Error updating QR code status. Please try again.' }), 500 +@app.route('/qr-codes//copy-url', methods=['POST']) +@login_required +def copy_qr_url(qr_id): + """Log QR code URL copy action""" + try: + qr_code = QRCode.query.get_or_404(qr_id) + + # Log URL copy action + logger_handler.logger.info(f"User {session.get('username', 'unknown')} copied URL for QR code {qr_code.name} (ID: {qr_id})") + + return jsonify({ + 'success': True, + 'message': f'QR code URL copied to clipboard!', + 'url': f"{request.url_root}qr/{qr_code.qr_url}" + }) + + except Exception as e: + logger_handler.logger.error(f"Error copying QR URL for ID {qr_id}: {e}") + return jsonify({ + 'success': False, + 'message': 'Error copying QR code URL.' + }), 500 + +@app.route('/qr-codes//open-link', methods=['POST']) +@login_required +def open_qr_link(qr_id): + """Log QR code link open action""" + try: + qr_code = QRCode.query.get_or_404(qr_id) + + # Log link open action + logger_handler.logger.info(f"User {session.get('username', 'unknown')} opened link for QR code {qr_code.name} (ID: {qr_id})") + + return jsonify({ + 'success': True, + 'message': f'Opening QR code link...', + 'url': f"{request.url_root}qr/{qr_code.qr_url}" + }) + + except Exception as e: + logger_handler.logger.error(f"Error opening QR link for ID {qr_id}: {e}") + return jsonify({ + 'success': False, + 'message': 'Error opening QR code link.' + }), 500 + @app.route('/qr-codes//activate', methods=['POST']) @login_required def activate_qr_code(qr_id): @@ -5512,6 +5558,16 @@ def get_employee_name(employee_id): print(f"⚠️ Error getting employee name for ID {employee_id}: {e}") return f"Employee {employee_id}" +def get_qr_code_checkin_count(qr_code_id): + """Helper function to get total check-ins count for a QR code""" + try: + count = AttendanceData.query.filter_by(qr_code_id=qr_code_id).count() + logger_handler.logger.info(f"QR Code {qr_code_id} total check-ins: {count}") + return count + except Exception as e: + logger_handler.logger.error(f"Error getting check-ins count for QR {qr_code_id}: {e}") + return 0 + @app.context_processor def inject_payroll_utils(): """Inject payroll utility functions into templates""" @@ -5520,6 +5576,13 @@ def inject_payroll_utils(): 'format_hours': lambda hours: f"{hours:.2f}" if hours else "0.00" } +@app.context_processor +def inject_dashboard_utils(): + """Inject dashboard utility functions into templates""" + return { + 'get_qr_code_checkin_count': get_qr_code_checkin_count + } + @app.route('/statistics') @login_required def qr_statistics(): diff --git a/static/css/dashboard.css b/static/css/dashboard.css index ac777f1..8b736bb 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -1270,3 +1270,70 @@ max-width: calc(100vw - 2rem); } } + +/* Enhanced QR Action Buttons */ +.qr-action-btn.copy { + background: linear-gradient(135deg, #10b981, #059669); + color: white; +} + +.qr-action-btn.copy:hover { + background: linear-gradient(135deg, #059669, #047857); + transform: translateY(-1px); +} + +.qr-action-btn.link { + background: linear-gradient(135deg, #3b82f6, #2563eb); + color: white; +} + +.qr-action-btn.link:hover { + background: linear-gradient(135deg, #2563eb, #1d4ed8); + transform: translateY(-1px); +} + +/* Check-ins Counter Styling */ +.qr-checkins-count { + background: rgba(59, 130, 246, 0.1); + border: 1px solid rgba(59, 130, 246, 0.2); + border-radius: var(--radius-md); + padding: 0.25rem 0.5rem; + font-weight: 500; + color: #1e40af; +} + +.qr-checkins-count i { + color: #3b82f6; +} + +/* Responsive Action Buttons */ +@media (max-width: 768px) { + .qr-actions-compact { + flex-wrap: wrap; + gap: 0.25rem; + } + + .qr-action-btn { + min-width: 32px; + height: 32px; + font-size: 0.75rem; + } +} + +/* Enhanced QR Actions Layout */ +.qr-actions-compact { + display: flex; + gap: 0.5rem; + align-items: center; + flex-wrap: wrap; +} + +.qr-card-actions { + display: flex; + gap: 0.5rem; + justify-content: center; + flex-wrap: wrap; + padding: var(--spacing-3); + border-top: 1px solid var(--gray-200); + background: var(--gray-50); +} diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 2059702..ea4ffb7 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -188,10 +188,13 @@ class DashboardManager { try { // Show loading state - const deleteBtn = document.querySelector(`[onclick*="deleteQRCode(${qrId}"]`); + const deleteBtn = document.querySelector( + `[onclick*="deleteQRCode(${qrId}"]` + ); if (deleteBtn) { deleteBtn.disabled = true; - deleteBtn.innerHTML = ' Deleting...'; + deleteBtn.innerHTML = + ' Deleting...'; } const response = await fetch(`/qr-codes/${qrId}/delete`, { @@ -216,26 +219,25 @@ class DashboardManager { // Use simple alert instead of problematic showToast alert(`QR code "${qrName}" deleted successfully!`); - } else { throw new Error(`Server error: ${response.status}`); } - } catch (error) { console.error("Delete error:", error); - + // Restore button if there was an error - const deleteBtn = document.querySelector(`[onclick*="deleteQRCode(${qrId}"]`); + const deleteBtn = document.querySelector( + `[onclick*="deleteQRCode(${qrId}"]` + ); if (deleteBtn) { deleteBtn.disabled = false; deleteBtn.innerHTML = ''; } - + // Use simple alert instead of problematic showToast alert("Failed to delete QR code. Please try again."); } } - // Show custom delete confirmation dialog showDeleteConfirmation(qrName) { @@ -299,19 +301,26 @@ class DashboardManager { async bulkDeleteQRCodes() { if (this.selectedQRCodes.size === 0) return; - const confirmed = await this.showBulkDeleteConfirmation(this.selectedQRCodes.size); + const confirmed = await this.showBulkDeleteConfirmation( + this.selectedQRCodes.size + ); if (!confirmed) return; - const deletePromises = Array.from(this.selectedQRCodes).map(qrId => { + const deletePromises = Array.from(this.selectedQRCodes).map((qrId) => { const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`); - const qrName = qrItem ? qrItem.querySelector('.qr-name')?.textContent || 'Unknown' : 'Unknown'; + const qrName = qrItem + ? qrItem.querySelector(".qr-name")?.textContent || "Unknown" + : "Unknown"; return this.deleteQRCode(qrId, qrName); }); try { await Promise.all(deletePromises); this.selectedQRCodes.clear(); - window.showToast(`Successfully deleted ${deletePromises.length} QR codes`, "success"); + window.showToast( + `Successfully deleted ${deletePromises.length} QR codes`, + "success" + ); } catch (error) { console.error("Bulk delete error:", error); window.showToast("Some QR codes could not be deleted", "error"); @@ -437,6 +446,96 @@ class DashboardManager { counter.textContent = `${qrItems.length} results`; } } + + showToast(message, type = "info") { + // Create toast if showToast doesn't exist globally + if (typeof window.showToast === "function") { + window.showToast(message, type); + } else { + // Fallback to console or simple alert + console.log(`${type.toUpperCase()}: ${message}`); + // Or use a simple notification + const toast = document.createElement("div"); + toast.className = `toast toast-${type}`; + toast.textContent = message; + toast.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: ${ + type === "success" + ? "#10b981" + : type === "error" + ? "#ef4444" + : "#3b82f6" + }; + color: white; + padding: 12px 16px; + border-radius: 8px; + z-index: 9999; + font-weight: 500; + box-shadow: 0 4px 12px rgba(0,0,0,0.1); + `; + + document.body.appendChild(toast); + + setTimeout(() => { + toast.style.opacity = "0"; + toast.style.transform = "translateX(100%)"; + setTimeout(() => toast.remove(), 300); + }, 3000); + } + } + + // Copy QR Code URL + async copyQRUrl(qrId) { + try { + const response = await fetch(`/qr-codes/${qrId}/copy-url`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + + const data = await response.json(); + + if (data.success && data.url) { + // Copy to clipboard + await navigator.clipboard.writeText(data.url); + this.showToast("QR code URL copied to clipboard!", "success"); + } else { + this.showToast("Failed to copy URL", "error"); + } + } catch (error) { + console.error("Copy URL error:", error); + this.showToast("Failed to copy URL", "error"); + } + } + + // Open QR Code Link + async openQRLink(qrId) { + try { + const response = await fetch(`/qr-codes/${qrId}/open-link`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + + const data = await response.json(); + + if (data.success && data.url) { + // Open in new tab + window.open(data.url, "_blank"); + this.showToast("QR code link opened!", "success"); + } else { + this.showToast("Failed to open link", "error"); + } + } catch (error) { + console.error("Open link error:", error); + this.showToast("Failed to open link", "error"); + } + } } // Initialize dashboard when DOM is loaded @@ -454,4 +553,13 @@ document.addEventListener("DOMContentLoaded", function () { window.dashboardManager.toggleQRItem(element); window.copyQRData = (name, location, address, event) => window.dashboardManager.copyQRData(name, location, address, event); -}); \ No newline at end of file +}); + +// Global functions for new features +function copyQRUrl(qrId) { + window.dashboardManager?.copyQRUrl(qrId); +} + +function openQRLink(qrId) { + window.dashboardManager?.openQRLink(qrId); +} diff --git a/templates/dashboard.html b/templates/dashboard.html index c6f90db..e0866b5 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -1121,7 +1121,14 @@ Management{% endblock %} {% block extra_head %} {{ qr.location_address }} - {% endif %} {% if qr.qr_url %} + {% endif %} + +
+ + {{ get_qr_code_checkin_count(qr.id) }} check-ins +
+ + {% if qr.qr_url %}
{{ qr.qr_url }} @@ -1140,6 +1147,22 @@ Management{% endblock %} {% block extra_head %}
+ + + +
- {% endif %} {% if qr.qr_url %} + {% endif %} + +
+ + {{ get_qr_code_checkin_count(qr.id) }} check-ins +
+ + {% if qr.qr_url %}
+ + + +