Update dashboard
This commit is contained in:
@@ -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/<string:qr_url>')
|
||||
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/<int:qr_id>/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/<int:qr_id>/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/<int:qr_id>/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():
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+121
-13
@@ -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 = '<i class="fas fa-spinner fa-spin"></i> Deleting...';
|
||||
deleteBtn.innerHTML =
|
||||
'<i class="fas fa-spinner fa-spin"></i> 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 = '<i class="fas fa-trash"></i>';
|
||||
}
|
||||
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
|
||||
// Global functions for new features
|
||||
function copyQRUrl(qrId) {
|
||||
window.dashboardManager?.copyQRUrl(qrId);
|
||||
}
|
||||
|
||||
function openQRLink(qrId) {
|
||||
window.dashboardManager?.openQRLink(qrId);
|
||||
}
|
||||
|
||||
@@ -1121,7 +1121,14 @@ Management{% endblock %} {% block extra_head %}
|
||||
<i class="fas fa-home"></i>
|
||||
<span>{{ qr.location_address }}</span>
|
||||
</div>
|
||||
{% endif %} {% if qr.qr_url %}
|
||||
{% endif %}
|
||||
|
||||
<div class="qr-detail-item qr-checkins-count">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>{{ get_qr_code_checkin_count(qr.id) }} check-ins</span>
|
||||
</div>
|
||||
|
||||
{% if qr.qr_url %}
|
||||
<div class="qr-detail-item">
|
||||
<i class="fas fa-link"></i>
|
||||
<span>{{ qr.qr_url }}</span>
|
||||
@@ -1140,6 +1147,22 @@ Management{% endblock %} {% block extra_head %}
|
||||
</div>
|
||||
|
||||
<div class="qr-card-actions">
|
||||
<button
|
||||
class="qr-action-btn copy"
|
||||
onclick="event.stopPropagation(); copyQRUrl({{ qr.id }})"
|
||||
title="Copy QR Code URL"
|
||||
>
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="qr-action-btn link"
|
||||
onclick="event.stopPropagation(); openQRLink({{ qr.id }})"
|
||||
title="Open QR Code Link"
|
||||
>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="qr-action-btn download"
|
||||
onclick="event.stopPropagation(); downloadQRFromCard(this)"
|
||||
@@ -1243,7 +1266,14 @@ Management{% endblock %} {% block extra_head %}
|
||||
<i class="fas fa-home"></i>
|
||||
<span>{{ qr.location_address }}</span>
|
||||
</div>
|
||||
{% endif %} {% if qr.qr_url %}
|
||||
{% endif %}
|
||||
|
||||
<div class="qr-detail-item qr-checkins-count">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>{{ get_qr_code_checkin_count(qr.id) }} check-ins</span>
|
||||
</div>
|
||||
|
||||
{% if qr.qr_url %}
|
||||
<div class="qr-detail-item">
|
||||
<i class="fas fa-link"></i>
|
||||
<span class="qr-url-short"
|
||||
@@ -1267,6 +1297,22 @@ Management{% endblock %} {% block extra_head %}
|
||||
</div>
|
||||
|
||||
<div class="qr-actions-compact">
|
||||
<button
|
||||
class="qr-action-btn copy"
|
||||
onclick="event.stopPropagation(); copyQRUrl({{ qr.id }})"
|
||||
title="Copy QR Code URL"
|
||||
>
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="qr-action-btn link"
|
||||
onclick="event.stopPropagation(); openQRLink({{ qr.id }})"
|
||||
title="Open QR Code Link"
|
||||
>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="qr-action-btn download"
|
||||
onclick="event.stopPropagation(); downloadQRFromCard(this)"
|
||||
|
||||
Reference in New Issue
Block a user