Update dashboard
This commit is contained in:
@@ -3483,7 +3483,7 @@ def delete_qr_code(qr_id):
|
|||||||
print(f"❌ Traceback: {traceback.format_exc()}")
|
print(f"❌ Traceback: {traceback.format_exc()}")
|
||||||
flash('Error deleting QR code. Please try again.', 'error')
|
flash('Error deleting QR code. Please try again.', 'error')
|
||||||
return redirect(url_for('dashboard'))
|
return redirect(url_for('dashboard'))
|
||||||
|
|
||||||
@app.route('/qr/<string:qr_url>')
|
@app.route('/qr/<string:qr_url>')
|
||||||
def qr_destination(qr_url):
|
def qr_destination(qr_url):
|
||||||
"""QR code destination page where staff check in - PRESERVING EXACT ROUTE"""
|
"""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.'
|
'message': 'Error updating QR code status. Please try again.'
|
||||||
}), 500
|
}), 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'])
|
@app.route('/qr-codes/<int:qr_id>/activate', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def activate_qr_code(qr_id):
|
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}")
|
print(f"⚠️ Error getting employee name for ID {employee_id}: {e}")
|
||||||
return f"Employee {employee_id}"
|
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
|
@app.context_processor
|
||||||
def inject_payroll_utils():
|
def inject_payroll_utils():
|
||||||
"""Inject payroll utility functions into templates"""
|
"""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"
|
'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')
|
@app.route('/statistics')
|
||||||
@login_required
|
@login_required
|
||||||
def qr_statistics():
|
def qr_statistics():
|
||||||
|
|||||||
@@ -1270,3 +1270,70 @@
|
|||||||
max-width: calc(100vw - 2rem);
|
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 {
|
try {
|
||||||
// Show loading state
|
// Show loading state
|
||||||
const deleteBtn = document.querySelector(`[onclick*="deleteQRCode(${qrId}"]`);
|
const deleteBtn = document.querySelector(
|
||||||
|
`[onclick*="deleteQRCode(${qrId}"]`
|
||||||
|
);
|
||||||
if (deleteBtn) {
|
if (deleteBtn) {
|
||||||
deleteBtn.disabled = true;
|
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`, {
|
const response = await fetch(`/qr-codes/${qrId}/delete`, {
|
||||||
@@ -216,26 +219,25 @@ class DashboardManager {
|
|||||||
|
|
||||||
// Use simple alert instead of problematic showToast
|
// Use simple alert instead of problematic showToast
|
||||||
alert(`QR code "${qrName}" deleted successfully!`);
|
alert(`QR code "${qrName}" deleted successfully!`);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Server error: ${response.status}`);
|
throw new Error(`Server error: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Delete error:", error);
|
console.error("Delete error:", error);
|
||||||
|
|
||||||
// Restore button if there was an error
|
// Restore button if there was an error
|
||||||
const deleteBtn = document.querySelector(`[onclick*="deleteQRCode(${qrId}"]`);
|
const deleteBtn = document.querySelector(
|
||||||
|
`[onclick*="deleteQRCode(${qrId}"]`
|
||||||
|
);
|
||||||
if (deleteBtn) {
|
if (deleteBtn) {
|
||||||
deleteBtn.disabled = false;
|
deleteBtn.disabled = false;
|
||||||
deleteBtn.innerHTML = '<i class="fas fa-trash"></i>';
|
deleteBtn.innerHTML = '<i class="fas fa-trash"></i>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use simple alert instead of problematic showToast
|
// Use simple alert instead of problematic showToast
|
||||||
alert("Failed to delete QR code. Please try again.");
|
alert("Failed to delete QR code. Please try again.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Show custom delete confirmation dialog
|
// Show custom delete confirmation dialog
|
||||||
showDeleteConfirmation(qrName) {
|
showDeleteConfirmation(qrName) {
|
||||||
@@ -299,19 +301,26 @@ class DashboardManager {
|
|||||||
async bulkDeleteQRCodes() {
|
async bulkDeleteQRCodes() {
|
||||||
if (this.selectedQRCodes.size === 0) return;
|
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;
|
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 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);
|
return this.deleteQRCode(qrId, qrName);
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(deletePromises);
|
await Promise.all(deletePromises);
|
||||||
this.selectedQRCodes.clear();
|
this.selectedQRCodes.clear();
|
||||||
window.showToast(`Successfully deleted ${deletePromises.length} QR codes`, "success");
|
window.showToast(
|
||||||
|
`Successfully deleted ${deletePromises.length} QR codes`,
|
||||||
|
"success"
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Bulk delete error:", error);
|
console.error("Bulk delete error:", error);
|
||||||
window.showToast("Some QR codes could not be deleted", "error");
|
window.showToast("Some QR codes could not be deleted", "error");
|
||||||
@@ -437,6 +446,96 @@ class DashboardManager {
|
|||||||
counter.textContent = `${qrItems.length} results`;
|
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
|
// Initialize dashboard when DOM is loaded
|
||||||
@@ -454,4 +553,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
window.dashboardManager.toggleQRItem(element);
|
window.dashboardManager.toggleQRItem(element);
|
||||||
window.copyQRData = (name, location, address, event) =>
|
window.copyQRData = (name, location, address, event) =>
|
||||||
window.dashboardManager.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>
|
<i class="fas fa-home"></i>
|
||||||
<span>{{ qr.location_address }}</span>
|
<span>{{ qr.location_address }}</span>
|
||||||
</div>
|
</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">
|
<div class="qr-detail-item">
|
||||||
<i class="fas fa-link"></i>
|
<i class="fas fa-link"></i>
|
||||||
<span>{{ qr.qr_url }}</span>
|
<span>{{ qr.qr_url }}</span>
|
||||||
@@ -1140,6 +1147,22 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="qr-card-actions">
|
<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
|
<button
|
||||||
class="qr-action-btn download"
|
class="qr-action-btn download"
|
||||||
onclick="event.stopPropagation(); downloadQRFromCard(this)"
|
onclick="event.stopPropagation(); downloadQRFromCard(this)"
|
||||||
@@ -1243,7 +1266,14 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
<i class="fas fa-home"></i>
|
<i class="fas fa-home"></i>
|
||||||
<span>{{ qr.location_address }}</span>
|
<span>{{ qr.location_address }}</span>
|
||||||
</div>
|
</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">
|
<div class="qr-detail-item">
|
||||||
<i class="fas fa-link"></i>
|
<i class="fas fa-link"></i>
|
||||||
<span class="qr-url-short"
|
<span class="qr-url-short"
|
||||||
@@ -1267,6 +1297,22 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="qr-actions-compact">
|
<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
|
<button
|
||||||
class="qr-action-btn download"
|
class="qr-action-btn download"
|
||||||
onclick="event.stopPropagation(); downloadQRFromCard(this)"
|
onclick="event.stopPropagation(); downloadQRFromCard(this)"
|
||||||
|
|||||||
Reference in New Issue
Block a user