Merge branch 'master' of https://github.com/nguyen-ngo/QR_Code_Management
This commit is contained in:
+228
-24
@@ -4,31 +4,163 @@
|
||||
*/
|
||||
|
||||
/* Dashboard Header */
|
||||
.dashboard-header {
|
||||
.header-stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--spacing-8);
|
||||
padding: var(--spacing-6);
|
||||
background: var(--white);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.header-stats-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
min-width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-stat-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
backdrop-filter: blur(10px);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard-header::before {
|
||||
.header-stat-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--primary-color),
|
||||
var(--primary-hover)
|
||||
);
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.header-stat-card.primary::before {
|
||||
background: linear-gradient(90deg, #2563eb, #1d4ed8);
|
||||
}
|
||||
|
||||
.header-stat-card.success::before {
|
||||
background: linear-gradient(90deg, #10b981, #047857);
|
||||
}
|
||||
|
||||
.header-stat-card.danger::before {
|
||||
background: linear-gradient(90deg, #ef4444, #dc2626);
|
||||
}
|
||||
|
||||
.header-stat-card.info::before {
|
||||
background: linear-gradient(90deg, #8b5cf6, #7c3aed);
|
||||
}
|
||||
|
||||
.header-stat-card:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.header-stat-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 0.375rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
color: #ffffff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-stat-card.primary .header-stat-icon {
|
||||
background: linear-gradient(135deg, #2563eb, #1d4ed8);
|
||||
}
|
||||
|
||||
.header-stat-card.success .header-stat-icon {
|
||||
background: linear-gradient(135deg, #10b981, #047857);
|
||||
}
|
||||
|
||||
.header-stat-card.danger .header-stat-icon {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
}
|
||||
|
||||
.header-stat-card.info .header-stat-icon {
|
||||
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||
}
|
||||
|
||||
.header-stat-content h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin-bottom: 0.125rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.header-stat-content p {
|
||||
color: #64748b;
|
||||
font-size: 0.75rem;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Update existing dashboard header to accommodate new layout */
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
background: linear-gradient(135deg, #a0aff1 0%, #ebdef8 100%);
|
||||
border-radius: 0.75rem;
|
||||
color: white;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
flex-wrap: wrap;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
/* Responsive Design for Header */
|
||||
@media (max-width: 1024px) {
|
||||
.dashboard-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.header-stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
min-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-header {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.header-stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.header-stat-card {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.header-stat-content h3 {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.header-stat-content p {
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-section h1 {
|
||||
@@ -892,7 +1024,7 @@
|
||||
}
|
||||
|
||||
.qr-url {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
|
||||
background: var(--gray-100);
|
||||
padding: var(--spacing-1) var(--spacing-2);
|
||||
border-radius: var(--radius-sm);
|
||||
@@ -927,7 +1059,8 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.qr-status, .qr-project {
|
||||
.qr-status,
|
||||
.qr-project {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-1);
|
||||
@@ -1088,8 +1221,12 @@
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
@@ -1108,28 +1245,95 @@
|
||||
.detail-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
||||
.qr-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.qr-actions .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
.bulk-actions-bar {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.bulk-actions-buttons {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
.modal-content {
|
||||
margin: var(--spacing-4);
|
||||
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);
|
||||
}
|
||||
|
||||
+556
-314
@@ -1,21 +1,32 @@
|
||||
class DashboardManager {
|
||||
/**
|
||||
* Unified Dashboard JavaScript for QR Code Management
|
||||
* static/js/dashboard.js
|
||||
*/
|
||||
|
||||
class ProjectDashboardManager {
|
||||
constructor() {
|
||||
this.expandedProjects = new Set();
|
||||
this.currentModalQR = null;
|
||||
this.selectedQRCodes = new Set();
|
||||
this.allExpanded = false;
|
||||
this.init();
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
init() {
|
||||
initialize() {
|
||||
const saved = localStorage.getItem("expandedProjects");
|
||||
if (saved) {
|
||||
this.expandedProjects = new Set(JSON.parse(saved));
|
||||
this.restoreProjectStates();
|
||||
}
|
||||
|
||||
this.setupEventListeners();
|
||||
this.addScrollAnimations();
|
||||
}
|
||||
|
||||
animateOut(element, callback) {
|
||||
element.classList.add("fade-out");
|
||||
setTimeout(() => {
|
||||
if (callback) callback();
|
||||
element.style.display = "none";
|
||||
}, 300);
|
||||
restoreProjectStates() {
|
||||
this.expandedProjects.forEach((projectId) => {
|
||||
this.expandProject(projectId, false);
|
||||
});
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
@@ -25,6 +36,7 @@ class DashboardManager {
|
||||
// ESC to close modals
|
||||
if (e.key === "Escape") {
|
||||
this.closeQRModal();
|
||||
this.closeImageLightbox();
|
||||
}
|
||||
|
||||
// Ctrl/Cmd + F to focus search
|
||||
@@ -63,135 +75,453 @@ class DashboardManager {
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
const qrItems = document.querySelectorAll(".qr-item");
|
||||
const qrItems = document.querySelectorAll(".qr-item, .qr-card");
|
||||
qrItems.forEach((item) => observer.observe(item));
|
||||
}
|
||||
|
||||
// FIXED: QR Code Toggle Status
|
||||
toggleQRCodeStatus(qrId) {
|
||||
const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`);
|
||||
if (!qrItem) return;
|
||||
// Project Management Functions
|
||||
toggleProject(projectId) {
|
||||
const isExpanded = this.expandedProjects.has(projectId);
|
||||
|
||||
const currentStatus = qrItem.dataset.status;
|
||||
const newStatus = currentStatus === "active" ? "inactive" : "active";
|
||||
|
||||
// Show loading state
|
||||
const toggleBtn = document.getElementById(`toggle-btn-${qrId}`);
|
||||
const toggleIcon = document.getElementById(`toggle-icon-${qrId}`);
|
||||
|
||||
if (toggleBtn && toggleIcon) {
|
||||
toggleBtn.classList.add("status-loading");
|
||||
toggleIcon.className = "fas fa-spinner fa-spin";
|
||||
toggleBtn.disabled = true;
|
||||
if (isExpanded) {
|
||||
this.collapseProject(projectId);
|
||||
} else {
|
||||
this.expandProject(projectId);
|
||||
}
|
||||
|
||||
// FIXED: Use correct endpoint with POST method for JSON response
|
||||
fetch(`/qr-codes/${qrId}/toggle-status`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.success) {
|
||||
// Update UI with new status
|
||||
this.updateQRStatus(qrId, result.new_status ? "active" : "inactive");
|
||||
window.showToast(result.message, "success");
|
||||
} else {
|
||||
throw new Error(result.message || "Failed to update status");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Status update error:", error);
|
||||
window.showToast("Failed to update QR code status", "error");
|
||||
})
|
||||
.finally(() => {
|
||||
// Remove loading state
|
||||
if (toggleBtn && toggleIcon) {
|
||||
toggleBtn.classList.remove("status-loading");
|
||||
toggleBtn.disabled = false;
|
||||
// Restore icon based on current status
|
||||
const currentStatus = qrItem.dataset.status;
|
||||
toggleIcon.className = `fas ${
|
||||
currentStatus === "active" ? "fa-pause" : "fa-play"
|
||||
}`;
|
||||
}
|
||||
});
|
||||
this.saveExpandedState();
|
||||
}
|
||||
|
||||
// Update QR status in UI
|
||||
updateQRStatus(qrId, newStatus) {
|
||||
const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`);
|
||||
if (!qrItem) return;
|
||||
expandProject(projectId, animate = true) {
|
||||
const projectQR = document.getElementById(`project-qr-${projectId}`);
|
||||
const toggle = document.getElementById(`toggle-${projectId}`);
|
||||
const header = toggle?.closest(".project-header");
|
||||
|
||||
// Update data attribute
|
||||
qrItem.dataset.status = newStatus;
|
||||
if (projectQR && toggle) {
|
||||
projectQR.classList.add("expanded");
|
||||
toggle.classList.add("expanded");
|
||||
header?.classList.add("expanded");
|
||||
this.expandedProjects.add(projectId);
|
||||
|
||||
// Update status badge
|
||||
const statusBadge = qrItem.querySelector(".qr-status");
|
||||
if (statusBadge) {
|
||||
statusBadge.className = `qr-status ${newStatus}`;
|
||||
statusBadge.innerHTML = `
|
||||
<i class="fas ${
|
||||
newStatus === "active" ? "fa-check-circle" : "fa-times-circle"
|
||||
}"></i>
|
||||
${newStatus === "active" ? "Active" : "Inactive"}
|
||||
`;
|
||||
if (animate) {
|
||||
setTimeout(() => {
|
||||
projectQR.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
// Update toggle buttons
|
||||
this.updateToggleButton(qrId, newStatus);
|
||||
}
|
||||
|
||||
// Update toggle button appearance
|
||||
updateToggleButton(qrId, status) {
|
||||
const toggleBtn = document.getElementById(`toggle-btn-${qrId}`);
|
||||
const toggleIcon = document.getElementById(`toggle-icon-${qrId}`);
|
||||
const detailToggleBtn = document.getElementById(
|
||||
`detail-toggle-btn-${qrId}`
|
||||
collapseProject(projectId) {
|
||||
const projectQR = document.getElementById(`project-qr-${projectId}`);
|
||||
const toggle = document.getElementById(`toggle-${projectId}`);
|
||||
const header = toggle?.closest(".project-header");
|
||||
|
||||
if (projectQR && toggle) {
|
||||
projectQR.classList.remove("expanded");
|
||||
toggle.classList.remove("expanded");
|
||||
header?.classList.remove("expanded");
|
||||
this.expandedProjects.delete(projectId);
|
||||
}
|
||||
}
|
||||
|
||||
saveExpandedState() {
|
||||
localStorage.setItem(
|
||||
"expandedProjects",
|
||||
JSON.stringify([...this.expandedProjects])
|
||||
);
|
||||
}
|
||||
|
||||
if (toggleBtn && toggleIcon) {
|
||||
// Update quick action button
|
||||
toggleBtn.className = `action-btn btn-status ${
|
||||
status === "active" ? "btn-deactivate" : "btn-activate"
|
||||
}`;
|
||||
toggleBtn.title = `${
|
||||
status === "active" ? "Deactivate" : "Activate"
|
||||
} QR Code`;
|
||||
toggleIcon.className = `fas ${
|
||||
status === "active" ? "fa-pause" : "fa-play"
|
||||
}`;
|
||||
}
|
||||
// QR Code Status Toggle
|
||||
async toggleQRCodeStatus(qrId) {
|
||||
try {
|
||||
const response = await fetch(`/qr-codes/${qrId}/toggle-status`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
});
|
||||
|
||||
if (detailToggleBtn) {
|
||||
// Update detail action button
|
||||
detailToggleBtn.className = `btn ${
|
||||
status === "active" ? "btn-warning" : "btn-success"
|
||||
}`;
|
||||
detailToggleBtn.innerHTML = `
|
||||
<i class="fas ${status === "active" ? "fa-pause" : "fa-play"}"></i>
|
||||
${status === "active" ? "Deactivate" : "Activate"} QR Code
|
||||
`;
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showToast(result.message, "success");
|
||||
|
||||
// Reload page after short delay to show updated status
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
throw new Error(result.message || "Failed to toggle QR code status");
|
||||
}
|
||||
} else {
|
||||
throw new Error("Failed to toggle QR code status");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Toggle status failed:", error);
|
||||
this.showToast("Failed to update QR code status", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// QR Modal Functions
|
||||
openQRModalFromData(element) {
|
||||
const qrData = {
|
||||
name: element.dataset.qrName,
|
||||
image: element.querySelector("img").src,
|
||||
location: element.dataset.qrLocation,
|
||||
address: element.dataset.qrAddress,
|
||||
event: element.dataset.qrEvent,
|
||||
qr_url: element.dataset.qrUrl,
|
||||
};
|
||||
|
||||
this.openQRModal(qrData);
|
||||
}
|
||||
|
||||
openQRModal(qrData) {
|
||||
const modal = document.getElementById("qrModal");
|
||||
const modalImage = document.getElementById("modalQRImage");
|
||||
const modalTitle = document.getElementById("modalTitle");
|
||||
const modalQRName = document.getElementById("modalQRName");
|
||||
const modalQRLocation = document.getElementById("modalQRLocation");
|
||||
const modalQRAddress = document.getElementById("modalQRAddress");
|
||||
const modalQREvent = document.getElementById("modalQREvent");
|
||||
const modalQRDestination = document.getElementById("modalQRDestination");
|
||||
|
||||
if (modal && modalImage && modalTitle) {
|
||||
modalTitle.textContent = `QR Code: ${qrData.name}`;
|
||||
modalImage.src = qrData.image;
|
||||
modalImage.alt = `QR Code for ${qrData.name}`;
|
||||
|
||||
if (modalQRName) modalQRName.textContent = qrData.name || "-";
|
||||
if (modalQRLocation) modalQRLocation.textContent = qrData.location || "-";
|
||||
if (modalQRAddress) modalQRAddress.textContent = qrData.address || "-";
|
||||
if (modalQREvent)
|
||||
modalQREvent.textContent = qrData.event || "No event specified";
|
||||
|
||||
if (modalQRDestination && qrData.qr_url) {
|
||||
const destinationUrl = `${window.location.origin}/qr/${qrData.qr_url}`;
|
||||
const linkElement = modalQRDestination.querySelector("a");
|
||||
if (linkElement) {
|
||||
linkElement.href = destinationUrl;
|
||||
linkElement.innerHTML = `
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
${destinationUrl}
|
||||
`;
|
||||
}
|
||||
} else if (modalQRDestination) {
|
||||
modalQRDestination.innerHTML =
|
||||
'<span style="color: var(--gray-500); font-style: italic;">No destination URL available</span>';
|
||||
}
|
||||
|
||||
this.currentModalQR = {
|
||||
name: qrData.name,
|
||||
image: qrData.image,
|
||||
location: qrData.location,
|
||||
address: qrData.address,
|
||||
event: qrData.event,
|
||||
qr_url: qrData.qr_url,
|
||||
destination_url: qrData.qr_url
|
||||
? `${window.location.origin}/qr/${qrData.qr_url}`
|
||||
: null,
|
||||
};
|
||||
|
||||
modal.style.display = "flex";
|
||||
|
||||
document.addEventListener("keydown", this.handleModalKeydown.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
closeQRModal() {
|
||||
const modal = document.getElementById("qrModal");
|
||||
if (modal) {
|
||||
modal.style.display = "none";
|
||||
this.currentModalQR = null;
|
||||
|
||||
document.removeEventListener(
|
||||
"keydown",
|
||||
this.handleModalKeydown.bind(this)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handleModalKeydown(event) {
|
||||
if (event.key === "Escape") {
|
||||
this.closeQRModal();
|
||||
}
|
||||
}
|
||||
|
||||
// Download Functions
|
||||
downloadModalQR() {
|
||||
if (this.currentModalQR) {
|
||||
const base64Data = this.currentModalQR.image.includes("base64,")
|
||||
? this.currentModalQR.image.split("base64,")[1]
|
||||
: this.currentModalQR.image;
|
||||
this.downloadQR(base64Data, this.currentModalQR.name);
|
||||
}
|
||||
}
|
||||
|
||||
downloadQRFromCard(button) {
|
||||
const qrCard = button.closest(".qr-card") || button.closest(".qr-item");
|
||||
const img = qrCard.querySelector("img");
|
||||
const qrName =
|
||||
qrCard.dataset.qrName ||
|
||||
qrCard.querySelector(".qr-name")?.textContent ||
|
||||
"qr_code";
|
||||
|
||||
if (img && img.src) {
|
||||
const base64Data = img.src.includes("base64,")
|
||||
? img.src.split("base64,")[1]
|
||||
: img.src;
|
||||
this.downloadQR(base64Data, qrName);
|
||||
}
|
||||
}
|
||||
|
||||
downloadQR(base64Image, filename) {
|
||||
try {
|
||||
const base64Data = base64Image.includes("base64,")
|
||||
? base64Image.split("base64,")[1]
|
||||
: base64Image;
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = `data:image/png;base64,${base64Data}`;
|
||||
link.download = `${filename
|
||||
.replace(/[^a-z0-9]/gi, "_")
|
||||
.toLowerCase()}_qr_code.png`;
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
this.showToast("QR code downloaded successfully!", "success");
|
||||
} catch (error) {
|
||||
console.error("Download error:", error);
|
||||
this.showToast("Failed to download QR code", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Copy Functions
|
||||
copyModalQRData() {
|
||||
if (this.currentModalQR) {
|
||||
const data = `QR Code: ${this.currentModalQR.name}\nLocation: ${
|
||||
this.currentModalQR.location
|
||||
}\nAddress: ${this.currentModalQR.address}\nEvent: ${
|
||||
this.currentModalQR.event
|
||||
}${
|
||||
this.currentModalQR.destination_url
|
||||
? `\nQR Link: ${this.currentModalQR.destination_url}`
|
||||
: ""
|
||||
}`;
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(data)
|
||||
.then(() => {
|
||||
this.showToast("QR code information copied to clipboard!", "success");
|
||||
})
|
||||
.catch(() => {
|
||||
this.fallbackCopyText(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
copyQRDestination() {
|
||||
if (this.currentModalQR && this.currentModalQR.destination_url) {
|
||||
navigator.clipboard
|
||||
.writeText(this.currentModalQR.destination_url)
|
||||
.then(() => {
|
||||
this.showToast("QR destination link copied to clipboard!", "success");
|
||||
})
|
||||
.catch(() => {
|
||||
this.showToast("Failed to copy QR link", "error");
|
||||
});
|
||||
} else {
|
||||
this.showToast("No QR destination link available", "warning");
|
||||
}
|
||||
}
|
||||
|
||||
// FIXED: 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");
|
||||
|
||||
// Log the action
|
||||
console.log(`QR URL copied for ID: ${qrId}`);
|
||||
} else {
|
||||
this.showToast("Failed to copy URL", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Copy URL error:", error);
|
||||
this.showToast("Failed to copy URL", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// FIXED: 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");
|
||||
|
||||
// Log the action
|
||||
console.log(`QR link opened for ID: ${qrId}`);
|
||||
} else {
|
||||
this.showToast("Failed to open link", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Open link error:", error);
|
||||
this.showToast("Failed to open link", "error");
|
||||
}
|
||||
}
|
||||
|
||||
fallbackCopyText(text) {
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
this.showToast("QR code information copied to clipboard!", "success");
|
||||
} catch (err) {
|
||||
this.showToast("Failed to copy to clipboard", "error");
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
// Image Lightbox Functions
|
||||
openImageLightbox(previewElement, qrName) {
|
||||
console.log("Opening lightbox for:", qrName); // Debug log
|
||||
|
||||
const img = previewElement.querySelector("img");
|
||||
|
||||
if (img && img.src) {
|
||||
const lightbox = document.getElementById("imageLightbox");
|
||||
const lightboxImage = document.getElementById("lightboxImage");
|
||||
const lightboxInfo = document.getElementById("lightboxInfo");
|
||||
|
||||
console.log(
|
||||
"Lightbox elements found:",
|
||||
!!lightbox,
|
||||
!!lightboxImage,
|
||||
!!lightboxInfo
|
||||
); // Debug log
|
||||
|
||||
if (lightbox && lightboxImage && lightboxInfo) {
|
||||
lightboxImage.src = img.src;
|
||||
lightboxImage.alt = img.alt;
|
||||
lightboxInfo.textContent = `QR Code: ${qrName}`;
|
||||
|
||||
lightbox.style.display = "flex";
|
||||
console.log("Lightbox should be visible now"); // Debug log
|
||||
|
||||
// Add keyboard listener for ESC key
|
||||
document.addEventListener(
|
||||
"keydown",
|
||||
this.handleLightboxKeydown.bind(this)
|
||||
);
|
||||
} else {
|
||||
console.error("Lightbox elements not found");
|
||||
}
|
||||
} else {
|
||||
console.error("Image element not found or no src");
|
||||
}
|
||||
}
|
||||
|
||||
closeImageLightbox() {
|
||||
console.log("Closing lightbox"); // Debug log
|
||||
const lightbox = document.getElementById("imageLightbox");
|
||||
if (lightbox) {
|
||||
lightbox.style.display = "none";
|
||||
|
||||
// Remove keyboard listener
|
||||
document.removeEventListener(
|
||||
"keydown",
|
||||
this.handleLightboxKeydown.bind(this)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handleLightboxKeydown(event) {
|
||||
if (event.key === "Escape") {
|
||||
this.closeImageLightbox();
|
||||
}
|
||||
}
|
||||
|
||||
// QR Item Toggle functionality (for selection)
|
||||
toggleQRItem(element) {
|
||||
const qrId = element.dataset.qrId;
|
||||
if (this.selectedQRCodes.has(qrId)) {
|
||||
this.selectedQRCodes.delete(qrId);
|
||||
element.classList.remove("selected");
|
||||
} else {
|
||||
this.selectedQRCodes.add(qrId);
|
||||
element.classList.add("selected");
|
||||
}
|
||||
|
||||
// Update bulk action buttons if they exist
|
||||
this.updateBulkActionButtons();
|
||||
}
|
||||
|
||||
updateBulkActionButtons() {
|
||||
const bulkActions = document.querySelector(".bulk-actions");
|
||||
if (bulkActions) {
|
||||
bulkActions.style.display =
|
||||
this.selectedQRCodes.size > 0 ? "flex" : "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Copy QR data functionality
|
||||
copyQRData(name, location, address, event) {
|
||||
const data = `QR Code: ${name}\nLocation: ${location}\nAddress: ${address}\nEvent: ${event}`;
|
||||
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard
|
||||
.writeText(data)
|
||||
.then(() =>
|
||||
this.showToast("QR code information copied to clipboard!", "success")
|
||||
)
|
||||
.catch(() => this.fallbackCopyText(data));
|
||||
} else {
|
||||
this.fallbackCopyText(data);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete QR Code
|
||||
async deleteQRCode(qrId, qrName) {
|
||||
if (!confirm(`Delete "${qrName}"? This cannot be undone.`)) return;
|
||||
|
||||
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`, {
|
||||
@@ -201,7 +531,6 @@ class DashboardManager {
|
||||
},
|
||||
});
|
||||
|
||||
// Don't try to parse as JSON - just check if request was successful
|
||||
if (response.ok) {
|
||||
// Remove QR item from page immediately
|
||||
const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`);
|
||||
@@ -210,187 +539,95 @@ class DashboardManager {
|
||||
qrItem.style.opacity = "0";
|
||||
setTimeout(() => {
|
||||
qrItem.remove();
|
||||
if (this.updateResultsCount) this.updateResultsCount();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Use simple alert instead of problematic showToast
|
||||
alert(`QR code "${qrName}" deleted successfully!`);
|
||||
|
||||
// Show success message
|
||||
this.showToast(`QR code "${qrName}" deleted successfully!`, "success");
|
||||
} 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 error message
|
||||
this.showToast("Failed to delete QR code. Please try again.", "error");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Show custom delete confirmation dialog
|
||||
showDeleteConfirmation(qrName) {
|
||||
return new Promise((resolve) => {
|
||||
const modal = document.createElement("div");
|
||||
modal.className = "modal";
|
||||
modal.style.display = "flex";
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content confirmation-modal">
|
||||
<div class="modal-header">
|
||||
<h3><i class="fas fa-exclamation-triangle text-warning"></i> Confirm Deletion</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p><strong>Are you sure you want to permanently delete "${qrName}"?</strong></p>
|
||||
<p class="text-muted">This action cannot be undone.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-danger" onclick="confirmDelete()">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="cancelDelete()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
window.confirmDelete = () => {
|
||||
document.body.removeChild(modal);
|
||||
delete window.confirmDelete;
|
||||
delete window.cancelDelete;
|
||||
resolve(true);
|
||||
// Toast notification system
|
||||
showToast(message, type = "info") {
|
||||
const toast = document.createElement("div");
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: ${
|
||||
type === "success"
|
||||
? "#10b981"
|
||||
: type === "error"
|
||||
? "#ef4444"
|
||||
: type === "warning"
|
||||
? "#f59e0b"
|
||||
: "#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);
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
`;
|
||||
|
||||
window.cancelDelete = () => {
|
||||
document.body.removeChild(modal);
|
||||
delete window.confirmDelete;
|
||||
delete window.cancelDelete;
|
||||
resolve(false);
|
||||
};
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Close on ESC key
|
||||
const escHandler = (e) => {
|
||||
if (e.key === "Escape") {
|
||||
window.cancelDelete();
|
||||
document.removeEventListener("keydown", escHandler);
|
||||
// Animate in
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = "1";
|
||||
toast.style.transform = "translateX(0)";
|
||||
}, 100);
|
||||
|
||||
// Animate out
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = "0";
|
||||
toast.style.transform = "translateX(100%)";
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(toast)) {
|
||||
toast.remove();
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", escHandler);
|
||||
|
||||
// Close on backdrop click
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target === modal) {
|
||||
window.cancelDelete();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Bulk delete functionality
|
||||
async bulkDeleteQRCodes() {
|
||||
if (this.selectedQRCodes.size === 0) return;
|
||||
|
||||
const confirmed = await this.showBulkDeleteConfirmation(this.selectedQRCodes.size);
|
||||
if (!confirmed) return;
|
||||
|
||||
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';
|
||||
return this.deleteQRCode(qrId, qrName);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(deletePromises);
|
||||
this.selectedQRCodes.clear();
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
showBulkDeleteConfirmation(count) {
|
||||
return new Promise((resolve) => {
|
||||
const modal = document.createElement("div");
|
||||
modal.className = "modal";
|
||||
modal.style.display = "flex";
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content confirmation-modal">
|
||||
<div class="modal-header">
|
||||
<h3><i class="fas fa-exclamation-triangle text-warning"></i> Confirm Bulk Deletion</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p><strong>Are you sure you want to permanently delete ${count} QR codes?</strong></p>
|
||||
<p class="text-muted">This action cannot be undone.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-danger" onclick="confirmBulkDelete()">
|
||||
<i class="fas fa-trash"></i> Delete All
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="cancelBulkDelete()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
window.confirmBulkDelete = () => {
|
||||
document.body.removeChild(modal);
|
||||
delete window.confirmBulkDelete;
|
||||
delete window.cancelBulkDelete;
|
||||
resolve(true);
|
||||
};
|
||||
|
||||
window.cancelBulkDelete = () => {
|
||||
document.body.removeChild(modal);
|
||||
delete window.confirmBulkDelete;
|
||||
delete window.cancelBulkDelete;
|
||||
resolve(false);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// QR Modal functions
|
||||
openQRModal(qrData) {
|
||||
const modal = document.getElementById("qrModal");
|
||||
const modalImage = document.getElementById("modalQRImage");
|
||||
const modalTitle = document.getElementById("modalTitle");
|
||||
|
||||
if (modal && modalImage && modalTitle) {
|
||||
modalTitle.textContent = `QR Code: ${qrData.name}`;
|
||||
modalImage.src = qrData.image;
|
||||
modal.style.display = "flex";
|
||||
}
|
||||
}
|
||||
|
||||
closeQRModal() {
|
||||
const modal = document.getElementById("qrModal");
|
||||
if (modal) {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Expand/collapse functionality
|
||||
toggleExpandAll() {
|
||||
this.allExpanded = !this.allExpanded;
|
||||
const qrItems = document.querySelectorAll(".qr-item");
|
||||
const expandToggle = document.getElementById("expandAllToggle");
|
||||
|
||||
this.allExpanded = !this.allExpanded;
|
||||
|
||||
qrItems.forEach((item) => {
|
||||
if (this.allExpanded) {
|
||||
item.classList.add("expanded");
|
||||
} else {
|
||||
item.classList.remove("expanded");
|
||||
const details = item.querySelector(".qr-details");
|
||||
if (details) {
|
||||
if (this.allExpanded) {
|
||||
details.style.display = "block";
|
||||
item.classList.add("expanded");
|
||||
} else {
|
||||
details.style.display = "none";
|
||||
item.classList.remove("expanded");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -400,58 +637,63 @@ class DashboardManager {
|
||||
: '<i class="fas fa-expand-alt"></i> Expand All';
|
||||
}
|
||||
}
|
||||
|
||||
toggleQRItem(element) {
|
||||
element.classList.toggle("expanded");
|
||||
}
|
||||
|
||||
// Copy QR data to clipboard
|
||||
copyQRData(name, location, address, event) {
|
||||
const data = `QR Code: ${name}\nLocation: ${location}\nAddress: ${address}\nEvent: ${event}`;
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(data)
|
||||
.then(() => {
|
||||
window.showToast("QR code information copied to clipboard!", "success");
|
||||
})
|
||||
.catch(() => {
|
||||
window.showToast("Failed to copy to clipboard", "error");
|
||||
});
|
||||
}
|
||||
|
||||
// Update results display
|
||||
updateResultsDisplay(count) {
|
||||
const resultsDisplay = document.getElementById("resultsDisplay");
|
||||
if (resultsDisplay) {
|
||||
resultsDisplay.textContent = `${count} QR codes found`;
|
||||
}
|
||||
}
|
||||
|
||||
updateResultsCount() {
|
||||
const qrItems = document.querySelectorAll(
|
||||
'.qr-item[style*="block"], .qr-item:not([style*="none"])'
|
||||
);
|
||||
const counter = document.querySelector(".results-counter");
|
||||
|
||||
if (counter) {
|
||||
counter.textContent = `${qrItems.length} results`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global variable to hold dashboard manager instance
|
||||
let dashboardManager;
|
||||
|
||||
// Initialize dashboard when DOM is loaded
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
window.dashboardManager = new DashboardManager();
|
||||
dashboardManager = new ProjectDashboardManager();
|
||||
|
||||
// Global functions for inline event handlers
|
||||
// Register all global functions for template compatibility
|
||||
window.toggleProject = (projectId) =>
|
||||
dashboardManager.toggleProject(projectId);
|
||||
window.toggleQRCodeStatus = (qrId) =>
|
||||
window.dashboardManager.toggleQRCodeStatus(qrId);
|
||||
window.deleteQRCode = (qrId, qrName) =>
|
||||
window.dashboardManager.deleteQRCode(qrId, qrName);
|
||||
window.openQRModal = (qrData) => window.dashboardManager.openQRModal(qrData);
|
||||
window.closeQRModal = () => window.dashboardManager.closeQRModal();
|
||||
window.toggleQRItem = (element) =>
|
||||
window.dashboardManager.toggleQRItem(element);
|
||||
dashboardManager.toggleQRCodeStatus(qrId);
|
||||
window.openQRModalFromData = (element) =>
|
||||
dashboardManager.openQRModalFromData(element);
|
||||
window.openQRModal = (qrData) => dashboardManager.openQRModal(qrData);
|
||||
window.closeQRModal = () => dashboardManager.closeQRModal();
|
||||
window.downloadModalQR = () => dashboardManager.downloadModalQR();
|
||||
window.copyModalQRData = () => dashboardManager.copyModalQRData();
|
||||
window.copyQRDestination = () => dashboardManager.copyQRDestination();
|
||||
window.downloadQRFromCard = (button) =>
|
||||
dashboardManager.downloadQRFromCard(button);
|
||||
window.openImageLightbox = (element, qrName) =>
|
||||
dashboardManager.openImageLightbox(element, qrName);
|
||||
window.closeImageLightbox = () => dashboardManager.closeImageLightbox();
|
||||
window.toggleQRItem = (element) => dashboardManager.toggleQRItem(element);
|
||||
window.copyQRData = (name, location, address, event) =>
|
||||
window.dashboardManager.copyQRData(name, location, address, event);
|
||||
});
|
||||
dashboardManager.copyQRData(name, location, address, event);
|
||||
window.deleteQRCode = (qrId, qrName) =>
|
||||
dashboardManager.deleteQRCode(qrId, qrName);
|
||||
|
||||
// FIXED: Global functions for copy/open link functionality
|
||||
window.copyQRUrl = function (qrId) {
|
||||
if (dashboardManager && dashboardManager.copyQRUrl) {
|
||||
dashboardManager.copyQRUrl(qrId);
|
||||
} else {
|
||||
console.error(
|
||||
"ProjectDashboardManager not initialized or copyQRUrl method missing"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
window.openQRLink = function (qrId) {
|
||||
if (dashboardManager && dashboardManager.openQRLink) {
|
||||
dashboardManager.openQRLink(qrId);
|
||||
} else {
|
||||
console.error(
|
||||
"ProjectDashboardManager not initialized or openQRLink method missing"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
console.log(
|
||||
"Project Dashboard initialized successfully with copy/open link functionality"
|
||||
);
|
||||
console.log("Dashboard keyboard shortcuts:");
|
||||
console.log("Ctrl/Cmd + F: Focus search");
|
||||
console.log("Escape: Close modal");
|
||||
});
|
||||
|
||||
@@ -55,8 +55,6 @@ const translations = {
|
||||
|
||||
// DOM Content Loaded Event (PRESERVED FROM ORIGINAL)
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
console.log("🎯 QR Destination page loaded");
|
||||
|
||||
// CRITICAL: Initialize systems in correct order
|
||||
initializeLanguage();
|
||||
initializeLocationServicesCheck();
|
||||
@@ -75,22 +73,15 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
// ENHANCED STAFF ID PERSISTENCE FUNCTIONALITY
|
||||
function initializeStaffIdPersistence() {
|
||||
console.log("👤 Initializing staff ID persistence functionality");
|
||||
|
||||
// Load last staff ID from localStorage
|
||||
lastStaffId = loadLastStaffId();
|
||||
if (lastStaffId) {
|
||||
console.log(`📱 Found last staff ID: ${lastStaffId}`);
|
||||
|
||||
// Automatically fill the last staff ID
|
||||
const employeeIdInput = document.getElementById("employee_id");
|
||||
if (employeeIdInput) {
|
||||
employeeIdInput.value = lastStaffId;
|
||||
validateEmployeeId();
|
||||
console.log(`✅ Auto-filled staff ID: ${lastStaffId}`);
|
||||
}
|
||||
} else {
|
||||
console.log("📱 No previous staff ID found");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,10 +91,8 @@ function loadLastStaffId() {
|
||||
if (saved && saved.trim().length >= 2) {
|
||||
return saved.trim().toUpperCase();
|
||||
}
|
||||
console.log("📱 No valid last staff ID found");
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("❌ Error loading last staff ID from localStorage:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -111,7 +100,6 @@ function loadLastStaffId() {
|
||||
function saveLastStaffId(staffId) {
|
||||
try {
|
||||
if (!staffId || typeof staffId !== "string" || staffId.trim().length < 2) {
|
||||
console.log("⚠️ Invalid staff ID, not saving");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -120,11 +108,9 @@ function saveLastStaffId(staffId) {
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem("qr_last_staff_id", cleanId);
|
||||
console.log(`💾 Last staff ID saved: ${cleanId}`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("❌ Error saving last staff ID to localStorage:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -155,14 +141,9 @@ function handleFormSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (isSubmitting) {
|
||||
console.log(
|
||||
"⏳ Check-in already in progress, ignoring duplicate submission"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("🎯 Form submission triggered");
|
||||
|
||||
const employeeId = document.getElementById("employee_id")?.value?.trim();
|
||||
|
||||
if (!employeeId) {
|
||||
@@ -187,10 +168,7 @@ function handleFormSubmit(event) {
|
||||
|
||||
// ENHANCED CHECK-IN SUBMISSION WITH MULTIPLE CHECK-IN SUPPORT
|
||||
function submitCheckin() {
|
||||
console.log("🚀 Starting check-in submission process");
|
||||
|
||||
if (isSubmitting) {
|
||||
console.log("⏳ Already submitting, aborting");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -206,9 +184,6 @@ function submitCheckin() {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`👤 Employee ID: ${employeeId}`);
|
||||
console.log(`📍 User location:`, userLocation);
|
||||
|
||||
// Prepare form data
|
||||
const formData = new FormData();
|
||||
formData.append("employee_id", employeeId);
|
||||
@@ -228,8 +203,6 @@ function submitCheckin() {
|
||||
const currentUrl = window.location.pathname;
|
||||
const checkinUrl = `${currentUrl}/checkin`;
|
||||
|
||||
console.log("🎯 Submitting to URL:", checkinUrl);
|
||||
|
||||
fetch(checkinUrl, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
@@ -238,15 +211,12 @@ function submitCheckin() {
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
console.log("📡 Server response status:", response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
console.log("📊 Server response data:", data);
|
||||
handleCheckinResponse(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("❌ Check-in error:", error);
|
||||
handleCheckinError(error);
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -261,7 +231,6 @@ function handleCheckinResponse(data) {
|
||||
handleCheckinSuccess(data);
|
||||
} else {
|
||||
const errorMsg = data.message || "Submission failed";
|
||||
console.log("❌ Submission failed:", errorMsg);
|
||||
|
||||
// NEW: Handle different types of check-in failures
|
||||
if (errorMsg.toLowerCase().includes("already submitted")) {
|
||||
@@ -280,8 +249,6 @@ function handleCheckinResponse(data) {
|
||||
|
||||
// ENHANCED SUCCESS HANDLING WITH MULTIPLE CHECK-IN INFO
|
||||
function handleCheckinSuccess(data) {
|
||||
console.log("✅ Submitted successfully!");
|
||||
|
||||
const responseData = data.data || data || {};
|
||||
const checkinCount = responseData.checkin_count_today || 1;
|
||||
const checkinSequence = responseData.checkin_sequence || "Check-in";
|
||||
@@ -384,8 +351,6 @@ function addCheckInAgainOption() {
|
||||
|
||||
// NEW: Reset form for new check-in
|
||||
function resetForNewCheckin() {
|
||||
console.log("🔄 Resetting for new check-in");
|
||||
|
||||
// Show form again
|
||||
const form = document.getElementById("checkinForm");
|
||||
if (form) {
|
||||
@@ -510,10 +475,11 @@ function validateEmployeeId() {
|
||||
|
||||
// All location and language functions remain unchanged from original
|
||||
function initializeLocation() {
|
||||
console.log("📍 Initializing location services");
|
||||
|
||||
// Check if Android enhanced location handler is available
|
||||
if (typeof AndroidLocationHandler !== 'undefined' && AndroidLocationHandler.isAndroidDevice()) {
|
||||
if (
|
||||
typeof AndroidLocationHandler !== "undefined" &&
|
||||
AndroidLocationHandler.isAndroidDevice()
|
||||
) {
|
||||
console.log("📱 Using Android-enhanced location initialization");
|
||||
AndroidLocationHandler.initializeAndroidLocation();
|
||||
} else {
|
||||
@@ -578,8 +544,6 @@ function handleLocationSuccess(position) {
|
||||
address: null,
|
||||
};
|
||||
|
||||
console.log("📍 Location data:", userLocation);
|
||||
|
||||
// Reverse geocode to get address
|
||||
reverseGeocode(userLocation.latitude, userLocation.longitude);
|
||||
|
||||
@@ -587,14 +551,11 @@ function handleLocationSuccess(position) {
|
||||
}
|
||||
|
||||
function handleLocationError(error) {
|
||||
console.log("❌ Location error:", error.message);
|
||||
userLocation.source = "manual";
|
||||
locationRequestActive = false;
|
||||
}
|
||||
|
||||
function reverseGeocode(lat, lng) {
|
||||
console.log(`🌍 Reverse geocoding for: ${lat}, ${lng}`);
|
||||
|
||||
// The server will use Google Maps API first, then fall back to OpenStreetMap
|
||||
// This provides better accuracy and address formatting
|
||||
const url = "/api/reverse-geocode"; // You may want to create this endpoint
|
||||
@@ -613,9 +574,7 @@ function reverseGeocode(lat, lng) {
|
||||
.then((data) => {
|
||||
if (data && data.display_name) {
|
||||
userLocation.address = data.display_name;
|
||||
console.log(`✅ Reverse geocoded address: ${userLocation.address}`);
|
||||
} else {
|
||||
console.log(`⚠️ No address found, using coordinates as fallback`);
|
||||
userLocation.address = `${lat.toFixed(10)}, ${lng.toFixed(10)}`;
|
||||
}
|
||||
})
|
||||
@@ -628,13 +587,10 @@ function reverseGeocode(lat, lng) {
|
||||
|
||||
// ENHANCED LANGUAGE FUNCTIONALITY WITH PERSISTENCE
|
||||
function initializeLanguage() {
|
||||
console.log("🌐 Initializing language functionality with persistence");
|
||||
|
||||
// Load saved language preference from localStorage
|
||||
const savedLanguage = loadLanguagePreference();
|
||||
if (savedLanguage && savedLanguage !== currentLanguage) {
|
||||
currentLanguage = savedLanguage;
|
||||
console.log(`📱 Restored saved language preference: ${currentLanguage}`);
|
||||
}
|
||||
|
||||
// Set up language toggle button event listener
|
||||
@@ -645,7 +601,6 @@ function initializeLanguage() {
|
||||
|
||||
// Apply initial translations based on loaded language
|
||||
applyTranslations();
|
||||
console.log(`✅ Language system initialized with: ${currentLanguage}`);
|
||||
}
|
||||
|
||||
function toggleLanguage() {
|
||||
@@ -659,10 +614,6 @@ function toggleLanguage() {
|
||||
// Apply translations immediately
|
||||
applyTranslations();
|
||||
|
||||
console.log(
|
||||
`🌐 Language switched to: ${currentLanguage} (saved to localStorage)`
|
||||
);
|
||||
|
||||
// Optional: Show brief confirmation message
|
||||
showLanguageChangeConfirmation();
|
||||
}
|
||||
@@ -674,24 +625,14 @@ function loadLanguagePreference() {
|
||||
|
||||
// Validate saved language is supported
|
||||
if (savedLanguage && translations.hasOwnProperty(savedLanguage)) {
|
||||
console.log(`📱 Found saved language preference: ${savedLanguage}`);
|
||||
return savedLanguage;
|
||||
} else if (savedLanguage) {
|
||||
console.log(
|
||||
`⚠️ Invalid saved language preference: ${savedLanguage}, using default`
|
||||
);
|
||||
// Clean up invalid preference
|
||||
localStorage.removeItem("qr_staff_language");
|
||||
} else {
|
||||
console.log("📱 No saved language preference found, using default");
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"❌ Error loading language preference from localStorage:",
|
||||
error
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -706,13 +647,8 @@ function saveLanguagePreference(language) {
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem("qr_staff_language", language);
|
||||
console.log(`💾 Language preference saved: ${language}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"❌ Error saving language preference to localStorage:",
|
||||
error
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -774,18 +710,14 @@ function startClock() {
|
||||
}
|
||||
|
||||
function checkLocationServicesStatus() {
|
||||
console.log("📱 Checking location services status...");
|
||||
|
||||
// Check if geolocation is supported
|
||||
if (!navigator.geolocation) {
|
||||
console.log("❌ Geolocation not supported by this browser");
|
||||
showLocationServicesWarning("not_supported");
|
||||
return;
|
||||
}
|
||||
|
||||
// Test location access with a quick check
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.log("⏰ Location permission check timed out");
|
||||
showLocationServicesWarning("timeout");
|
||||
}, 3000); // 3 second timeout
|
||||
|
||||
@@ -793,13 +725,11 @@ function checkLocationServicesStatus() {
|
||||
(position) => {
|
||||
// Success - location services are working
|
||||
clearTimeout(timeoutId);
|
||||
console.log("✅ Location services are available and enabled");
|
||||
hideLocationServicesWarning();
|
||||
},
|
||||
(error) => {
|
||||
// Error - location services may be disabled
|
||||
clearTimeout(timeoutId);
|
||||
console.log("❌ Location services error:", error.message);
|
||||
|
||||
switch (error.code) {
|
||||
case error.PERMISSION_DENIED:
|
||||
@@ -894,9 +824,6 @@ function showLocationServicesWarning(errorType) {
|
||||
if (container) {
|
||||
container.insertBefore(warningBanner, container.firstChild);
|
||||
}
|
||||
|
||||
// Log warning event
|
||||
console.log(`⚠️ Location services warning displayed: ${errorType}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -906,7 +833,6 @@ function hideLocationServicesWarning() {
|
||||
const existingWarning = document.getElementById("locationServicesWarning");
|
||||
if (existingWarning) {
|
||||
existingWarning.remove();
|
||||
console.log("✅ Location services warning hidden");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user