Initial commit
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user