Photo mode feature added.

This commit is contained in:
serversdwn
2025-12-15 18:46:26 +00:00
parent 6db958ffa6
commit 191dceff2b
4 changed files with 375 additions and 4 deletions

View File

@@ -159,6 +159,26 @@
</div>
</div>
<!-- Recent Photos Section -->
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6 mb-8" id="recent-photos-card">
<div class="flex items-center justify-between mb-4 cursor-pointer md:cursor-default" onclick="toggleCard('recent-photos')">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Recent Photos</h2>
<div class="flex items-center gap-2">
<svg class="w-6 h-6 text-seismo-orange" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
<svg class="w-5 h-5 text-gray-500 transition-transform md:hidden chevron" id="recent-photos-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</div>
</div>
<div class="card-content" id="recent-photos-content">
<div id="recentPhotosGallery" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
<p class="text-sm text-gray-500 dark:text-gray-400 col-span-full">Loading recent photos...</p>
</div>
</div>
</div>
<!-- Fleet Status Section with Tabs -->
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6" id="fleet-status-card">
@@ -471,6 +491,46 @@ function parseLocation(location) {
// TODO: Add geocoding support for address strings
return null;
}
// Load and display recent photos
async function loadRecentPhotos() {
try {
const response = await fetch('/api/recent-photos?limit=12');
if (!response.ok) {
throw new Error('Failed to load recent photos');
}
const data = await response.json();
const gallery = document.getElementById('recentPhotosGallery');
if (data.photos && data.photos.length > 0) {
gallery.innerHTML = '';
data.photos.forEach(photo => {
const photoDiv = document.createElement('div');
photoDiv.className = 'relative group';
photoDiv.innerHTML = `
<a href="/unit/${photo.unit_id}" class="block">
<img src="${photo.path}" alt="${photo.unit_id}"
class="w-full h-32 object-cover rounded-lg shadow hover:shadow-lg transition-shadow">
<div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-2 rounded-b-lg">
<p class="text-white text-xs font-semibold">${photo.unit_id}</p>
</div>
</a>
`;
gallery.appendChild(photoDiv);
});
} else {
gallery.innerHTML = '<p class="text-sm text-gray-500 dark:text-gray-400 col-span-full">No photos uploaded yet. Upload photos from unit detail pages.</p>';
}
} catch (error) {
console.error('Error loading recent photos:', error);
document.getElementById('recentPhotosGallery').innerHTML = '<p class="text-sm text-red-500 col-span-full">Failed to load recent photos</p>';
}
}
// Load recent photos on page load and refresh every 30 seconds
loadRecentPhotos();
setInterval(loadRecentPhotos, 30000);
</script>
{% endblock %}

View File

@@ -177,6 +177,38 @@
<label class="text-sm font-medium text-gray-500 dark:text-gray-400">Notes</label>
<p id="viewNote" class="mt-1 text-gray-900 dark:text-white whitespace-pre-wrap">--</p>
</div>
<!-- Photos -->
<div class="border-t border-gray-200 dark:border-gray-700 pt-6">
<div class="flex justify-between items-start mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Photos</h3>
<div class="flex flex-col sm:flex-row gap-2">
<!-- Take Photo Button (Camera) -->
<label for="photoCameraUpload" class="px-4 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg transition-colors cursor-pointer flex items-center gap-2 text-sm sm:text-base">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span class="hidden sm:inline">Take Photo</span>
<span class="sm:hidden">Camera</span>
</label>
<!-- Choose from Library Button -->
<label for="photoLibraryUpload" class="px-4 py-2 bg-seismo-navy hover:bg-blue-800 text-white rounded-lg transition-colors cursor-pointer flex items-center gap-2 text-sm sm:text-base">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
<span class="hidden sm:inline">Choose Photo</span>
<span class="sm:hidden">Library</span>
</label>
<input type="file" id="photoCameraUpload" accept="image/*" capture="environment" class="hidden" onchange="uploadPhoto(this.files[0])">
<input type="file" id="photoLibraryUpload" accept="image/*" class="hidden" onchange="uploadPhoto(this.files[0])">
</div>
</div>
<div id="photoGallery" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<p class="text-sm text-gray-500 dark:text-gray-400 col-span-full">Loading photos...</p>
</div>
<div id="uploadStatus" class="hidden mt-4 p-4 rounded-lg"></div>
</div>
</div>
</div>
@@ -632,7 +664,107 @@ function parseLocation(location) {
return null;
}
// Load and display photos
async function loadPhotos() {
try {
const response = await fetch(`/api/unit/${unitId}/photos`);
if (!response.ok) {
throw new Error('Failed to load photos');
}
const data = await response.json();
const gallery = document.getElementById('photoGallery');
if (data.photos && data.photos.length > 0) {
gallery.innerHTML = '';
data.photo_urls.forEach((url, index) => {
const photoDiv = document.createElement('div');
photoDiv.className = 'relative group';
photoDiv.innerHTML = `
<img src="${url}" alt="Unit photo ${index + 1}"
class="w-full h-48 object-cover rounded-lg shadow cursor-pointer hover:shadow-lg transition-shadow"
onclick="window.open('${url}', '_blank')">
${index === 0 ? '<span class="absolute top-2 left-2 bg-seismo-orange text-white text-xs px-2 py-1 rounded">Primary</span>' : ''}
`;
gallery.appendChild(photoDiv);
});
} else {
gallery.innerHTML = '<p class="text-sm text-gray-500 dark:text-gray-400 col-span-full">No photos yet. Add a photo to get started.</p>';
}
} catch (error) {
console.error('Error loading photos:', error);
document.getElementById('photoGallery').innerHTML = '<p class="text-sm text-red-500 col-span-full">Failed to load photos</p>';
}
}
// Upload photo with EXIF metadata extraction
async function uploadPhoto(file) {
if (!file) return;
const statusDiv = document.getElementById('uploadStatus');
statusDiv.className = 'mt-4 p-4 rounded-lg bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200';
statusDiv.textContent = 'Uploading photo and extracting metadata...';
statusDiv.classList.remove('hidden');
const formData = new FormData();
formData.append('photo', file);
try {
const response = await fetch(`/api/unit/${unitId}/upload-photo`, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Upload failed');
}
const result = await response.json();
// Show success message with metadata info
let message = 'Photo uploaded successfully!';
if (result.metadata && result.metadata.coordinates) {
message += ` GPS location detected: ${result.metadata.coordinates}`;
if (result.coordinates_updated) {
message += ' (Unit coordinates updated automatically)';
}
} else {
message += ' No GPS data found in photo.';
}
statusDiv.className = 'mt-4 p-4 rounded-lg bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200';
statusDiv.textContent = message;
// Reload photos and unit data
await loadPhotos();
if (result.coordinates_updated) {
await loadUnitData();
}
// Hide status after 5 seconds
setTimeout(() => {
statusDiv.classList.add('hidden');
}, 5000);
// Reset both file inputs
document.getElementById('photoCameraUpload').value = '';
document.getElementById('photoLibraryUpload').value = '';
} catch (error) {
console.error('Error uploading photo:', error);
statusDiv.className = 'mt-4 p-4 rounded-lg bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200';
statusDiv.textContent = `Error uploading photo: ${error.message}`;
// Hide error after 5 seconds
setTimeout(() => {
statusDiv.classList.add('hidden');
}, 5000);
}
}
// Load data when page loads
loadUnitData();
loadUnitData().then(() => {
loadPhotos();
});
</script>
{% endblock %}