Files
2026-04-23 21:38:24 -04:00

313 lines
12 KiB
JavaScript

// Job Photos JavaScript
const jobPhotoModule = {
jobId: null,
allPhotos: [],
currentPhotoIndex: 0,
_tagApi: null,
init: function(jobId, tagSuggestions) {
this.jobId = jobId;
this._tagSuggestions = tagSuggestions || [];
this.loadJobPhotos();
this.setupDragDrop();
this.setupFileInput();
// Initialise the tag widget each time the upload modal opens so it's always fresh
const uploadModal = document.getElementById('uploadPhotoModal');
if (uploadModal) {
uploadModal.addEventListener('show.bs.modal', () => {
this._tagApi = initTagInput('photoTagsHidden', 'photoTagsContainer', {
suggestions: this._tagSuggestions
});
});
}
},
loadJobPhotos: function() {
console.log('loadJobPhotos called');
// Add cache-busting parameter to prevent browser from using cached data
fetch(`/Jobs/GetJobPhotos?jobId=${this.jobId}&_t=${Date.now()}`, {
cache: 'no-cache',
headers: {
'Cache-Control': 'no-cache'
}
})
.then(response => response.json())
.then(data => {
console.log('GetJobPhotos response:', data);
if (data.success) {
this.allPhotos = data.photos;
this.renderPhotoGallery(data.photos);
document.getElementById('photoCount').textContent = data.photos.length;
}
})
.catch(error => console.error('Error loading photos:', error));
},
renderPhotoGallery: function(photos) {
const gallery = document.getElementById('photoGallery');
if (!gallery) {
console.error('photoGallery element not found in DOM');
return;
}
// Clear gallery
gallery.innerHTML = '';
if (photos.length === 0) {
// Show "no photos" message
gallery.innerHTML = `
<div class="col-12 text-center py-5" id="noPhotosMessage">
<i class="bi bi-camera" style="font-size: 3rem; opacity: 0.2;"></i>
<p class="text-muted mt-2 mb-0">No photos uploaded yet</p>
<small class="text-muted">Click "Upload Photo" to add photos</small>
</div>
`;
return;
}
// Add photo cards
photos.forEach((photo, index) => {
const col = document.createElement('div');
col.className = 'col-md-4 col-sm-6';
col.innerHTML = `
<div class="photo-card" onclick="jobPhotoModule.viewPhoto(${index})">
<img src="/Jobs/GetPhoto/${photo.id}" alt="${this.escapeHtml(photo.caption || 'Job photo')}" class="photo-thumbnail">
<div class="photo-overlay">
<div class="photo-type-badge badge bg-${this.getPhotoTypeBadgeClass(photo.photoType)}">
${photo.photoTypeDisplay}
</div>
${photo.caption ? `<p class="photo-caption">${this.escapeHtml(photo.caption)}</p>` : ''}
</div>
</div>
`;
gallery.appendChild(col);
});
},
viewPhoto: function(index, isNavigating = false) {
this.currentPhotoIndex = index;
const photo = this.allPhotos[index];
document.getElementById('viewPhotoImage').src = `/Jobs/GetPhoto/${photo.id}`;
document.getElementById('viewPhotoTitle').textContent = photo.photoTypeDisplay + ' Photo';
document.getElementById('photoPosition').textContent = `Photo ${index + 1} of ${this.allPhotos.length}`;
document.getElementById('photoDetailCaption').textContent = photo.caption || 'No caption';
document.getElementById('photoDetailType').textContent = photo.photoTypeDisplay;
document.getElementById('photoDetailDate').textContent = new Date(photo.uploadedDate).toLocaleDateString();
document.getElementById('photoDetailUploader').textContent = photo.uploadedByName;
// Tags
const tagsRow = document.getElementById('photoDetailTagsRow');
const tagsSpan = document.getElementById('photoDetailTags');
if (photo.tagsList && photo.tagsList.length > 0) {
tagsSpan.innerHTML = photo.tagsList.map(t =>
`<span class="badge bg-primary bg-opacity-10 text-primary me-1">${this.escapeHtml(t)}</span>`
).join('');
tagsRow.style.display = '';
} else {
tagsRow.style.display = 'none';
}
// Only show modal if not navigating (prevents backdrop duplication)
if (!isNavigating) {
const modalElement = document.getElementById('viewPhotoModal');
const modal = bootstrap.Modal.getInstance(modalElement) || new bootstrap.Modal(modalElement);
modal.show();
}
},
navigatePhoto: function(direction) {
this.currentPhotoIndex += direction;
if (this.currentPhotoIndex < 0) this.currentPhotoIndex = this.allPhotos.length - 1;
if (this.currentPhotoIndex >= this.allPhotos.length) this.currentPhotoIndex = 0;
// Pass true to indicate we're navigating (don't re-show modal)
this.viewPhoto(this.currentPhotoIndex, true);
},
uploadPhoto: function() {
const fileInput = document.getElementById('photoFile');
const caption = document.getElementById('photoCaption').value;
const photoType = document.getElementById('photoType').value;
if (!fileInput.files || fileInput.files.length === 0) {
showWarning('Please select a photo to upload', 'No File Selected');
return;
}
const formData = new FormData();
formData.append('jobId', this.jobId);
formData.append('photo', fileInput.files[0]);
formData.append('caption', caption);
formData.append('photoType', photoType);
formData.append('tags', document.getElementById('photoTagsHidden').value);
const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
fetch('/Jobs/UploadPhoto', {
method: 'POST',
headers: {
'RequestVerificationToken': token
},
body: formData
})
.then(response => response.json())
.then(data => {
console.log('Upload response:', data);
if (data.success) {
const modalElement = document.getElementById('uploadPhotoModal');
const modal = bootstrap.Modal.getInstance(modalElement);
modal.hide();
// Wait for modal animation to complete
setTimeout(() => {
console.log('Reloading photos after upload...');
this.loadJobPhotos();
this.clearPhotoSelection();
this.showToast('Photo uploaded successfully', 'success');
}, 400);
} else {
this.showToast(data.message || 'Error uploading photo', 'danger');
}
})
.catch(error => {
console.error('Error:', error);
this.showToast('An error occurred while uploading', 'danger');
});
},
deletePhoto: function() {
if (!confirm('Are you sure you want to delete this photo?')) return;
const photo = this.allPhotos[this.currentPhotoIndex];
const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
fetch('/Jobs/DeletePhoto?id=' + photo.id, {
method: 'POST',
headers: {
'RequestVerificationToken': token,
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
console.log('Delete response:', data);
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('viewPhotoModal')).hide();
// Wait for modal animation to complete
setTimeout(() => {
console.log('Reloading photos after delete...');
this.loadJobPhotos();
this.showToast('Photo deleted successfully', 'success');
}, 400);
} else {
this.showToast(data.message || 'Error deleting photo', 'danger');
}
})
.catch(error => {
console.error('Error:', error);
this.showToast('An error occurred while deleting', 'danger');
});
},
setupDragDrop: function() {
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('photoFile');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, (e) => {
e.preventDefault();
e.stopPropagation();
}, false);
});
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, () => {
dropZone.classList.add('drag-over');
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, () => {
dropZone.classList.remove('drag-over');
}, false);
});
dropZone.addEventListener('drop', (e) => {
const dt = e.dataTransfer;
const files = dt.files;
fileInput.files = files;
this.handleFileSelect({ target: fileInput });
}, false);
},
setupFileInput: function() {
document.getElementById('photoFile').addEventListener('change', (e) => this.handleFileSelect(e));
},
handleFileSelect: function(e) {
const file = e.target.files[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
showError('Please select an image file', 'Invalid File Type');
return;
}
if (file.size > 10 * 1024 * 1024) {
showError('File size must be less than 10 MB', 'File Too Large');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
document.getElementById('previewImage').src = e.target.result;
document.getElementById('photoPreview').classList.remove('d-none');
document.getElementById('dropZone').style.display = 'none';
};
reader.readAsDataURL(file);
},
clearPhotoSelection: function() {
document.getElementById('photoFile').value = '';
document.getElementById('photoCaption').value = '';
document.getElementById('photoType').value = '1';
document.getElementById('photoPreview').classList.add('d-none');
document.getElementById('dropZone').style.display = 'block';
if (this._tagApi) this._tagApi.clear();
},
getPhotoTypeBadgeClass: function(type) {
const classes = {
0: 'info', // Before
1: 'primary', // Progress
2: 'success', // After
3: 'warning', // Quality Check
4: 'danger', // Issue
5: 'success' // Completed
};
return classes[type] || 'secondary';
},
escapeHtml: function(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
},
showToast: function(message, type) {
const alertClass = `alert-${type}`;
const alert = document.createElement('div');
alert.className = `alert ${alertClass} alert-dismissible fade show position-fixed top-0 end-0 m-3`;
alert.style.zIndex = '9999';
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alert);
setTimeout(() => alert.remove(), 3000);
}
};