feat: add admin controls to manage and delete placed camera pins while protecting municipal portals
This commit is contained in:
+19
-2
@@ -155,8 +155,24 @@
|
||||
function removeReport(reportId) {
|
||||
communityReports = communityReports.filter(r => r.id !== reportId);
|
||||
if (cameraMetadata[reportId]) delete cameraMetadata[reportId];
|
||||
saveReports();
|
||||
saveMetadata();
|
||||
localStorage.setItem(STORAGE_KEY_REPORTS, JSON.stringify(communityReports));
|
||||
localStorage.setItem(STORAGE_KEY_METADATA, JSON.stringify(cameraMetadata));
|
||||
window.dispatchEvent(new CustomEvent('flock_reports_updated', { detail: communityReports }));
|
||||
return true;
|
||||
}
|
||||
|
||||
function clearAllPlacedReports() {
|
||||
communityReports = [];
|
||||
localStorage.setItem(STORAGE_KEY_REPORTS, JSON.stringify([]));
|
||||
// Clean up metadata for custom placed pins
|
||||
Object.keys(cameraMetadata).forEach(id => {
|
||||
if (id.startsWith('rpt-') || id.startsWith('usr-pin-') || id.startsWith('test-') || id.startsWith('cam-test-')) {
|
||||
delete cameraMetadata[id];
|
||||
}
|
||||
});
|
||||
localStorage.setItem(STORAGE_KEY_METADATA, JSON.stringify(cameraMetadata));
|
||||
window.dispatchEvent(new CustomEvent('flock_reports_updated', { detail: [] }));
|
||||
return true;
|
||||
}
|
||||
|
||||
function saveReports() {
|
||||
@@ -369,6 +385,7 @@
|
||||
getReports,
|
||||
addReport,
|
||||
removeReport,
|
||||
clearAllPlacedReports,
|
||||
clearUnverifiedReports,
|
||||
getCameraMeta,
|
||||
confirmCamera,
|
||||
|
||||
+167
-1
@@ -198,7 +198,15 @@
|
||||
btnReportRemovePhoto: document.getElementById('btn-report-remove-photo'),
|
||||
|
||||
// Native Camera / File Input
|
||||
cameraPhotoFileInput: document.getElementById('camera-photo-file-input')
|
||||
cameraPhotoFileInput: document.getElementById('camera-photo-file-input'),
|
||||
|
||||
// Admin Placed Camera Controls
|
||||
camAdminControlBox: document.getElementById('cam-admin-control-box'),
|
||||
btnAdminDeleteCam: document.getElementById('btn-admin-delete-cam'),
|
||||
adminPlacedCount: document.getElementById('admin-placed-count'),
|
||||
adminPlacedCamsList: document.getElementById('admin-placed-cams-list'),
|
||||
btnAdminClearAllPlaced: document.getElementById('btn-admin-clear-all-placed'),
|
||||
btnAdminManagePins: document.getElementById('btn-admin-manage-pins')
|
||||
};
|
||||
|
||||
// --- INITIALIZATION ---
|
||||
@@ -410,6 +418,11 @@
|
||||
Inspect Agency Records & Sharing ↗
|
||||
</button>
|
||||
` : ''}
|
||||
${(cam.isCommunity || cam.id.startsWith('rpt-') || cam.id.startsWith('usr-pin-') || cam.id.startsWith('test-') || cam.id.startsWith('cam-test-')) ? `
|
||||
<button class="popup-inspect-btn" style="background: rgba(239, 68, 68, 0.18); border: 1px solid #ef4444; color: #f87171;" onclick="window.adminDeleteCamera('${escapeHtml(cam.id)}')">
|
||||
🗑️ Admin: Delete Placed Pin
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1250,6 +1263,8 @@
|
||||
document.getElementById('login-vehicle').value = user.vehicle;
|
||||
}
|
||||
|
||||
renderAdminPlacedList();
|
||||
|
||||
if (typeof el.loginModal.showModal === 'function') {
|
||||
el.loginModal.showModal();
|
||||
} else {
|
||||
@@ -1384,6 +1399,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Admin Delete Control: ONLY visible for custom placed pins, NEVER for official scraped municipal portals
|
||||
const isPlaced = cam.isCommunity || cam.id.startsWith('rpt-') || cam.id.startsWith('usr-pin-') || cam.id.startsWith('test-') || cam.id.startsWith('cam-test-');
|
||||
if (el.camAdminControlBox) {
|
||||
el.camAdminControlBox.style.display = isPlaced ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// Show modal
|
||||
if (el.cameraDetailModal) {
|
||||
if (typeof el.cameraDetailModal.showModal === 'function') {
|
||||
@@ -1404,6 +1425,107 @@
|
||||
}
|
||||
}
|
||||
|
||||
// --- ADMIN PLACED CAMERA MANAGEMENT ---
|
||||
function renderAdminPlacedList() {
|
||||
if (!el.adminPlacedCamsList) return;
|
||||
const reports = window.FlockAuth ? window.FlockAuth.getReports() : [];
|
||||
|
||||
// Find all placed cameras from active radar memory (including test cameras)
|
||||
const placedCams = allCameras.filter(c =>
|
||||
c.isCommunity ||
|
||||
c.id.startsWith('rpt-') ||
|
||||
c.id.startsWith('usr-pin-') ||
|
||||
c.id.startsWith('cam-test-') ||
|
||||
c.id.startsWith('test-')
|
||||
);
|
||||
|
||||
// Combine into unique map
|
||||
const combinedMap = new Map();
|
||||
reports.forEach(r => {
|
||||
combinedMap.set(r.id, {
|
||||
id: r.id,
|
||||
name: r.intersection || r.type || 'Placed Camera',
|
||||
lat: r.lat,
|
||||
lng: r.lng,
|
||||
dateStr: new Date(r.timestamp || Date.now()).toLocaleDateString(),
|
||||
photoUrl: r.photoUrl || null
|
||||
});
|
||||
});
|
||||
placedCams.forEach(c => {
|
||||
if (!combinedMap.has(c.id)) {
|
||||
combinedMap.set(c.id, {
|
||||
id: c.id,
|
||||
name: c.name || 'Test Placed Camera',
|
||||
lat: c.lat,
|
||||
lng: c.lng,
|
||||
dateStr: 'Active Session',
|
||||
photoUrl: null
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const items = Array.from(combinedMap.values());
|
||||
if (el.adminPlacedCount) el.adminPlacedCount.textContent = `${items.length} Placed`;
|
||||
|
||||
if (items.length === 0) {
|
||||
el.adminPlacedCamsList.innerHTML = `
|
||||
<div style="padding: 12px; text-align: center; color: #94a3b8; font-size: 0.8rem; background: rgba(30,41,59,0.4); border-radius: 6px;">
|
||||
No custom placed cameras. All active radar cameras are official municipal portals.
|
||||
</div>
|
||||
`;
|
||||
if (el.btnAdminClearAllPlaced) el.btnAdminClearAllPlaced.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
if (el.btnAdminClearAllPlaced) el.btnAdminClearAllPlaced.style.display = 'block';
|
||||
|
||||
el.adminPlacedCamsList.innerHTML = items.map(r => `
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 10px; background: rgba(30,41,59,0.5); border: 1px solid rgba(148,163,184,0.1); border-radius: 6px; font-size: 0.78rem;">
|
||||
<div style="display: flex; align-items: center; gap: 8px; overflow: hidden; max-width: 240px;">
|
||||
${r.photoUrl ? `
|
||||
<img src="${r.photoUrl}" alt="Thumbnail" style="width: 34px; height: 34px; object-fit: cover; border-radius: 4px; border: 1px solid var(--accent-cyan); flex-shrink: 0;">
|
||||
` : `
|
||||
<span style="font-size: 1.1rem; flex-shrink: 0;">📍</span>
|
||||
`}
|
||||
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||
<strong style="color: #f8fafc; display: block; overflow: hidden; text-overflow: ellipsis;">${escapeHtml(r.name)}</strong>
|
||||
<span style="color: #94a3b8; font-size: 0.7rem;">${r.lat.toFixed(4)}, ${r.lng.toFixed(4)} • ${r.dateStr}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn-secondary" style="padding: 5px 10px; font-size: 0.74rem; color: #f87171; border-color: rgba(239,68,68,0.3); cursor: pointer; flex-shrink: 0;" onclick="window.adminDeleteCamera('${escapeHtml(r.id)}')">
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
window.adminDeleteCamera = function(camId) {
|
||||
// SECURITY GUARD: Protect official scraped municipal transparency portals
|
||||
if (camId.startsWith('portal-') || (!camId.startsWith('rpt-') && !camId.startsWith('usr-pin-') && !camId.startsWith('test-') && !camId.startsWith('cam-test-'))) {
|
||||
const targetCam = allCameras.find(c => c.id === camId);
|
||||
if (targetCam && !targetCam.isCommunity) {
|
||||
alert("Protected Municipal Portal: Official city and police department transparency records cannot be deleted. Only custom placed cameras can be removed.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const cam = allCameras.find(c => c.id === camId) || { name: 'Placed Camera', id: camId };
|
||||
if (!confirm(`Admin Action: Permanently delete placed camera "${cam.name || cam.id}" from radar?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.FlockAuth) {
|
||||
window.FlockAuth.removeReport(camId);
|
||||
}
|
||||
allCameras = allCameras.filter(c => c.id !== camId);
|
||||
renderCameraZones();
|
||||
renderAdminPlacedList();
|
||||
closeCameraDetailModal();
|
||||
playLogConfirmationChirp();
|
||||
speakVoiceAlert("Placed camera deleted from radar.");
|
||||
showToast("🗑️ Placed camera permanently removed.");
|
||||
};
|
||||
|
||||
// --- CONTEST CAMERA MODAL ---
|
||||
function openContestModal(cam) {
|
||||
activeModalCamera = cam;
|
||||
@@ -1615,6 +1737,14 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Admin Delete Camera Button (Detail Modal)
|
||||
if (el.btnAdminDeleteCam) {
|
||||
el.btnAdminDeleteCam.addEventListener('click', () => {
|
||||
if (!activeModalCamera) return;
|
||||
window.adminDeleteCamera(activeModalCamera.id);
|
||||
});
|
||||
}
|
||||
|
||||
// Contest Modal Events
|
||||
if (el.contestCloseBtn) el.contestCloseBtn.addEventListener('click', closeContestModal);
|
||||
if (el.btnContestAddPhoto) el.btnContestAddPhoto.addEventListener('click', triggerPhotoCaptureForContest);
|
||||
@@ -1723,6 +1853,42 @@
|
||||
closeLoginModal();
|
||||
});
|
||||
|
||||
// Admin Clear All Placed Pins Button (Keeps 1,439 real municipal portals)
|
||||
if (el.btnAdminClearAllPlaced) {
|
||||
el.btnAdminClearAllPlaced.addEventListener('click', () => {
|
||||
if (!confirm("Admin Action: Clear ALL custom placed pins and radar test markers? (The 1,439 official municipal portals will remain permanent and protected).")) {
|
||||
return;
|
||||
}
|
||||
if (window.FlockAuth) {
|
||||
window.FlockAuth.clearAllPlacedReports();
|
||||
}
|
||||
allCameras = allCameras.filter(c => !c.isCommunity && !c.id.startsWith('rpt-') && !c.id.startsWith('usr-pin-') && !c.id.startsWith('test-') && !c.id.startsWith('cam-test-'));
|
||||
renderCameraZones();
|
||||
renderAdminPlacedList();
|
||||
playLogConfirmationChirp();
|
||||
speakVoiceAlert("All custom placed pins cleared from radar.");
|
||||
showToast("🗑️ All custom placed pins cleared.");
|
||||
});
|
||||
}
|
||||
|
||||
// Admin Placed Pins Shortcut in Map Top Controls
|
||||
if (el.btnAdminManagePins) {
|
||||
el.btnAdminManagePins.addEventListener('click', () => {
|
||||
openLoginModal();
|
||||
setTimeout(() => {
|
||||
const adminSec = document.querySelector('.admin-profile-section');
|
||||
if (adminSec) adminSec.scrollIntoView({ behavior: 'smooth' });
|
||||
}, 120);
|
||||
});
|
||||
}
|
||||
|
||||
// Sync radar when reports updated
|
||||
window.addEventListener('flock_reports_updated', () => {
|
||||
loadAllCameraData();
|
||||
renderCameraZones();
|
||||
renderAdminPlacedList();
|
||||
});
|
||||
|
||||
// Report Form Submit
|
||||
el.reportForm.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user