1086 lines
37 KiB
JavaScript

// Free Alberta Food - Admin Panel
class AdminPanel {
constructor() {
this.password = null;
this.currentSection = 'updates';
this.currentStatus = 'pending';
this.listingStatus = 'pending';
this.geocodingFilter = 'all';
this.requests = [];
this.listings = [];
this.geoResources = [];
this.init();
}
init() {
this.bindEvents();
this.checkAuth();
}
bindEvents() {
// Login form
document.getElementById('loginForm').addEventListener('submit', (e) => {
e.preventDefault();
this.login();
});
// Logout button
document.getElementById('logoutBtn').addEventListener('click', () => this.logout());
// Section tabs (Update Requests vs New Listings)
document.querySelectorAll('.admin-section-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
this.switchSection(e.target.dataset.section);
});
});
// Status filter tabs
document.querySelectorAll('.admin-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
const section = e.target.dataset.section;
const status = e.target.dataset.status;
const filter = e.target.dataset.filter;
if (section === 'updates') {
this.switchTab(status);
} else if (section === 'listings') {
this.switchListingTab(status);
} else if (section === 'geocoding') {
this.switchGeocodingTab(filter);
}
});
});
// Request modal
document.querySelectorAll('#requestModal .modal-overlay, #requestModal .modal-close').forEach(el => {
el.addEventListener('click', () => this.closeModal('requestModal'));
});
// Listing modal
document.querySelectorAll('#listingModal .modal-overlay, #listingModal .modal-close').forEach(el => {
el.addEventListener('click', () => this.closeModal('listingModal'));
});
// Geocoding modal
document.querySelectorAll('#geocodingModal .modal-overlay, #geocodingModal .modal-close').forEach(el => {
el.addEventListener('click', () => this.closeModal('geocodingModal'));
});
// Escape key to close modals
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.closeModal('requestModal');
this.closeModal('listingModal');
this.closeModal('geocodingModal');
}
});
// Event delegation for requests list
document.getElementById('requestsList').addEventListener('click', (e) => {
const viewBtn = e.target.closest('.view-request-btn');
if (viewBtn) {
const requestId = parseInt(viewBtn.dataset.requestId);
this.openRequestModal(requestId);
}
});
// Event delegation for listings list
document.getElementById('listingsList').addEventListener('click', (e) => {
const viewBtn = e.target.closest('.view-listing-btn');
if (viewBtn) {
const listingId = parseInt(viewBtn.dataset.listingId);
this.openListingModal(listingId);
}
});
// Event delegation for request modal actions
document.getElementById('requestModalBody').addEventListener('click', (e) => {
const approveBtn = e.target.closest('.approve-btn');
if (approveBtn) {
const requestId = parseInt(approveBtn.dataset.requestId);
this.approveRequest(requestId);
return;
}
const rejectBtn = e.target.closest('.reject-btn');
if (rejectBtn) {
const requestId = parseInt(rejectBtn.dataset.requestId);
this.rejectRequest(requestId);
}
});
// Event delegation for listing modal actions
document.getElementById('listingModalBody').addEventListener('click', (e) => {
const approveBtn = e.target.closest('.approve-listing-btn');
if (approveBtn) {
const listingId = parseInt(approveBtn.dataset.listingId);
this.approveListingSubmission(listingId);
return;
}
const rejectBtn = e.target.closest('.reject-listing-btn');
if (rejectBtn) {
const listingId = parseInt(rejectBtn.dataset.listingId);
this.rejectListingSubmission(listingId);
}
});
// Event delegation for geocoding list
document.getElementById('geocodingList').addEventListener('click', (e) => {
const viewBtn = e.target.closest('.view-geocoding-btn');
if (viewBtn) {
const resourceId = parseInt(viewBtn.dataset.resourceId);
this.openGeocodingModal(resourceId);
}
});
// Event delegation for geocoding modal actions
document.getElementById('geocodingModalBody').addEventListener('click', (e) => {
const regeocodeBtn = e.target.closest('.regeocode-btn');
if (regeocodeBtn) {
const resourceId = parseInt(regeocodeBtn.dataset.resourceId);
this.regeocodeResource(resourceId, regeocodeBtn);
}
});
}
switchSection(section) {
this.currentSection = section;
// Update section tab active states
document.querySelectorAll('.admin-section-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.section === section);
});
// Show/hide sections
document.getElementById('updatesSection').classList.toggle('hidden', section !== 'updates');
document.getElementById('listingsSection').classList.toggle('hidden', section !== 'listings');
document.getElementById('geocodingSection').classList.toggle('hidden', section !== 'geocoding');
// Load data for the active section
if (section === 'updates') {
this.loadCounts();
this.loadRequests();
} else if (section === 'listings') {
this.loadListingCounts();
this.loadListings();
} else if (section === 'geocoding') {
this.loadGeocodingStats();
this.loadGeoResources();
}
}
checkAuth() {
const savedPassword = sessionStorage.getItem('adminPassword');
if (savedPassword) {
this.password = savedPassword;
this.showDashboard();
}
}
async login() {
const passwordInput = document.getElementById('adminPassword');
const password = passwordInput.value;
const errorDiv = document.getElementById('loginError');
try {
const response = await fetch('/api/admin/auth', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ password })
});
if (response.ok) {
this.password = password;
sessionStorage.setItem('adminPassword', password);
errorDiv.classList.add('hidden');
this.showDashboard();
} else {
const data = await response.json();
errorDiv.textContent = data.error || 'Invalid password';
errorDiv.classList.remove('hidden');
}
} catch (error) {
console.error('Login failed:', error);
errorDiv.textContent = 'Login failed. Please try again.';
errorDiv.classList.remove('hidden');
}
}
logout() {
this.password = null;
sessionStorage.removeItem('adminPassword');
document.getElementById('loginSection').classList.remove('hidden');
document.getElementById('adminDashboard').classList.add('hidden');
document.getElementById('logoutBtn').classList.add('hidden');
document.getElementById('adminPassword').value = '';
}
showDashboard() {
document.getElementById('loginSection').classList.add('hidden');
document.getElementById('adminDashboard').classList.remove('hidden');
document.getElementById('logoutBtn').classList.remove('hidden');
// Load data for the current section
if (this.currentSection === 'updates') {
this.loadCounts();
this.loadRequests();
} else if (this.currentSection === 'listings') {
this.loadListingCounts();
this.loadListings();
} else if (this.currentSection === 'geocoding') {
this.loadGeocodingStats();
this.loadGeoResources();
}
}
async loadCounts() {
try {
const response = await fetch('/api/admin/update-requests/counts', {
headers: {
'Authorization': `Bearer ${this.password}`
}
});
if (response.ok) {
const data = await response.json();
document.getElementById('pendingCount').textContent = data.counts.pending;
document.getElementById('approvedCount').textContent = data.counts.approved;
document.getElementById('rejectedCount').textContent = data.counts.rejected;
}
} catch (error) {
console.error('Failed to load counts:', error);
}
}
async loadRequests() {
const container = document.getElementById('requestsList');
const noResults = document.getElementById('noRequests');
const loading = document.getElementById('loadingRequests');
container.innerHTML = '';
noResults.classList.add('hidden');
loading.classList.remove('hidden');
try {
const response = await fetch(`/api/admin/update-requests?status=${this.currentStatus}`, {
headers: {
'Authorization': `Bearer ${this.password}`
}
});
if (response.status === 401) {
this.logout();
return;
}
const data = await response.json();
this.requests = data.requests;
loading.classList.add('hidden');
if (this.requests.length === 0) {
noResults.classList.remove('hidden');
return;
}
container.innerHTML = this.requests.map(request => `
<div class="request-card">
<div class="request-card-header">
<div>
<h3 class="request-resource-name">${this.escapeHtml(request.current_name)}</h3>
<span class="request-date">${this.formatDate(request.created_at)}</span>
</div>
<span class="status-badge ${request.status}">${request.status}</span>
</div>
<div class="request-card-info">
<div class="request-submitter">
<strong>From:</strong> ${this.escapeHtml(request.submitter_email)}
${request.submitter_name ? ` (${this.escapeHtml(request.submitter_name)})` : ''}
</div>
<div class="request-changes">
<strong>Changes:</strong> ${this.getChangesSummary(request)}
</div>
</div>
<div class="request-card-actions">
<button class="resource-action-btn view-request-btn" data-request-id="${request.id}">View Details</button>
</div>
</div>
`).join('');
} catch (error) {
console.error('Failed to load requests:', error);
loading.classList.add('hidden');
container.innerHTML = '<div class="error-message">Failed to load requests. Please try again.</div>';
}
}
switchTab(status) {
this.currentStatus = status;
// Update active tab (only for updates section)
document.querySelectorAll('.admin-tab[data-section="updates"]').forEach(tab => {
tab.classList.toggle('active', tab.dataset.status === status);
});
this.loadRequests();
}
switchListingTab(status) {
this.listingStatus = status;
// Update active tab (only for listings section)
document.querySelectorAll('.admin-tab[data-section="listings"]').forEach(tab => {
tab.classList.toggle('active', tab.dataset.status === status);
});
this.loadListings();
}
getChangesSummary(request) {
const fields = [
{ key: 'proposed_name', label: 'Name' },
{ key: 'proposed_resource_type', label: 'Type' },
{ key: 'proposed_address', label: 'Address' },
{ key: 'proposed_city', label: 'City' },
{ key: 'proposed_phone', label: 'Phone' },
{ key: 'proposed_email', label: 'Email' },
{ key: 'proposed_website', label: 'Website' },
{ key: 'proposed_hours_of_operation', label: 'Hours' },
{ key: 'proposed_description', label: 'Description' },
{ key: 'proposed_eligibility', label: 'Eligibility' },
{ key: 'proposed_services_offered', label: 'Services' }
];
const changes = fields
.filter(f => request[f.key] !== null)
.map(f => f.label);
if (changes.length === 0) {
return request.additional_notes ? 'Notes only' : 'No changes specified';
}
return changes.join(', ');
}
openRequestModal(requestId) {
const request = this.requests.find(r => r.id === requestId);
if (!request) return;
const modalBody = document.getElementById('requestModalBody');
const fields = [
{ key: 'name', label: 'Name' },
{ key: 'resource_type', label: 'Type' },
{ key: 'address', label: 'Address' },
{ key: 'city', label: 'City' },
{ key: 'phone', label: 'Phone' },
{ key: 'email', label: 'Email' },
{ key: 'website', label: 'Website' },
{ key: 'hours_of_operation', label: 'Hours of Operation' },
{ key: 'description', label: 'Description' },
{ key: 'eligibility', label: 'Eligibility' },
{ key: 'services_offered', label: 'Services Offered' }
];
const changesHtml = fields.map(f => {
const current = request[`current_${f.key}`];
const proposed = request[`proposed_${f.key}`];
if (proposed === null) return '';
return `
<div class="diff-row">
<div class="diff-label">${f.label}</div>
<div class="diff-values">
<div class="diff-current">
<span class="diff-tag">Current</span>
<span class="diff-value">${this.escapeHtml(current) || '<em>Not set</em>'}</span>
</div>
<div class="diff-arrow">→</div>
<div class="diff-proposed">
<span class="diff-tag">Proposed</span>
<span class="diff-value">${this.escapeHtml(proposed)}</span>
</div>
</div>
</div>
`;
}).filter(html => html !== '').join('');
modalBody.innerHTML = `
<div class="request-detail">
<div class="request-detail-header">
<h2>Update Request #${request.id}</h2>
<span class="status-badge ${request.status}">${request.status}</span>
</div>
<div class="modal-section">
<div class="modal-section-title">Resource</div>
<div class="modal-section-content">
<strong>${this.escapeHtml(request.current_name)}</strong>
${request.current_city ? `<br>${this.escapeHtml(request.current_city)}` : ''}
</div>
</div>
<div class="modal-section">
<div class="modal-section-title">Submitted By</div>
<div class="modal-section-content">
${this.escapeHtml(request.submitter_email)}
${request.submitter_name ? ` (${this.escapeHtml(request.submitter_name)})` : ''}
<br><small>${this.formatDate(request.created_at)}</small>
</div>
</div>
${changesHtml ? `
<div class="modal-section">
<div class="modal-section-title">Proposed Changes</div>
<div class="diff-container">
${changesHtml}
</div>
</div>
` : ''}
${request.additional_notes ? `
<div class="modal-section">
<div class="modal-section-title">Additional Notes</div>
<div class="modal-section-content">${this.escapeHtml(request.additional_notes)}</div>
</div>
` : ''}
${request.status === 'pending' ? `
<div class="modal-section">
<div class="modal-section-title">Admin Notes (optional)</div>
<textarea id="adminNotes" class="admin-notes-input" rows="2" placeholder="Add notes about your decision..."></textarea>
</div>
<div class="modal-actions">
<button class="resource-action-btn reject-btn" data-request-id="${request.id}">Reject</button>
<button class="resource-action-btn primary approve-btn" data-request-id="${request.id}">Approve & Apply Changes</button>
</div>
` : `
${request.admin_notes ? `
<div class="modal-section">
<div class="modal-section-title">Admin Notes</div>
<div class="modal-section-content">${this.escapeHtml(request.admin_notes)}</div>
</div>
` : ''}
<div class="modal-section">
<div class="modal-section-title">Reviewed</div>
<div class="modal-section-content">
${request.reviewed_at ? this.formatDate(request.reviewed_at) : 'N/A'}
${request.reviewed_by ? ` by ${this.escapeHtml(request.reviewed_by)}` : ''}
</div>
</div>
`}
</div>
`;
document.getElementById('requestModal').classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
closeModal(modalId) {
document.getElementById(modalId).classList.add('hidden');
document.body.style.overflow = '';
}
async approveRequest(requestId) {
const adminNotes = document.getElementById('adminNotes')?.value.trim() || null;
if (!confirm('Are you sure you want to approve this request and apply the changes?')) {
return;
}
try {
const response = await fetch(`/api/admin/update-requests/${requestId}/approve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.password}`
},
body: JSON.stringify({ admin_notes: adminNotes })
});
if (response.ok) {
alert('Request approved and changes applied successfully!');
this.closeModal();
this.loadCounts();
this.loadRequests();
} else {
const data = await response.json();
alert(data.error || 'Failed to approve request');
}
} catch (error) {
console.error('Failed to approve request:', error);
alert('Failed to approve request. Please try again.');
}
}
async rejectRequest(requestId) {
const adminNotes = document.getElementById('adminNotes')?.value.trim() || null;
if (!confirm('Are you sure you want to reject this request?')) {
return;
}
try {
const response = await fetch(`/api/admin/update-requests/${requestId}/reject`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.password}`
},
body: JSON.stringify({ admin_notes: adminNotes })
});
if (response.ok) {
alert('Request rejected.');
this.closeModal();
this.loadCounts();
this.loadRequests();
} else {
const data = await response.json();
alert(data.error || 'Failed to reject request');
}
} catch (error) {
console.error('Failed to reject request:', error);
alert('Failed to reject request. Please try again.');
}
}
// ==========================================
// Listing Submissions Methods
// ==========================================
async loadListingCounts() {
try {
const response = await fetch('/api/admin/listing-submissions/counts', {
headers: {
'Authorization': `Bearer ${this.password}`
}
});
if (response.ok) {
const data = await response.json();
document.getElementById('listingPendingCount').textContent = data.counts.pending;
document.getElementById('listingApprovedCount').textContent = data.counts.approved;
document.getElementById('listingRejectedCount').textContent = data.counts.rejected;
}
} catch (error) {
console.error('Failed to load listing counts:', error);
}
}
async loadListings() {
const container = document.getElementById('listingsList');
const noResults = document.getElementById('noListings');
const loading = document.getElementById('loadingListings');
container.innerHTML = '';
noResults.classList.add('hidden');
loading.classList.remove('hidden');
try {
const response = await fetch(`/api/admin/listing-submissions?status=${this.listingStatus}`, {
headers: {
'Authorization': `Bearer ${this.password}`
}
});
if (response.status === 401) {
this.logout();
return;
}
const data = await response.json();
this.listings = data.submissions;
loading.classList.add('hidden');
if (this.listings.length === 0) {
noResults.classList.remove('hidden');
return;
}
container.innerHTML = this.listings.map(listing => `
<div class="request-card">
<div class="request-card-header">
<div>
<h3 class="request-resource-name">${this.escapeHtml(listing.name)}</h3>
<span class="request-date">${this.formatDate(listing.created_at)}</span>
</div>
<span class="status-badge ${listing.status}">${listing.status}</span>
</div>
<div class="request-card-info">
<div class="request-submitter">
<strong>From:</strong> ${this.escapeHtml(listing.submitter_email)}
${listing.submitter_name ? ` (${this.escapeHtml(listing.submitter_name)})` : ''}
</div>
<div class="request-changes">
<strong>Type:</strong> ${this.formatType(listing.resource_type)}
${listing.city ? ` | <strong>City:</strong> ${this.escapeHtml(listing.city)}` : ''}
</div>
</div>
<div class="request-card-actions">
<button class="resource-action-btn view-listing-btn" data-listing-id="${listing.id}">View Details</button>
</div>
</div>
`).join('');
} catch (error) {
console.error('Failed to load listings:', error);
loading.classList.add('hidden');
container.innerHTML = '<div class="error-message">Failed to load submissions. Please try again.</div>';
}
}
openListingModal(listingId) {
const listing = this.listings.find(l => l.id === listingId);
if (!listing) return;
const modalBody = document.getElementById('listingModalBody');
const fields = [
{ key: 'name', label: 'Name' },
{ key: 'resource_type', label: 'Type', format: (v) => this.formatType(v) },
{ key: 'address', label: 'Address' },
{ key: 'city', label: 'City' },
{ key: 'phone', label: 'Phone' },
{ key: 'email', label: 'Email' },
{ key: 'website', label: 'Website' },
{ key: 'hours_of_operation', label: 'Hours of Operation' },
{ key: 'description', label: 'Description' },
{ key: 'eligibility', label: 'Eligibility' },
{ key: 'services_offered', label: 'Services Offered' }
];
const fieldsHtml = fields.map(f => {
const value = listing[f.key];
if (!value) return '';
const displayValue = f.format ? f.format(value) : this.escapeHtml(value);
return `
<div class="listing-field">
<div class="listing-field-label">${f.label}</div>
<div class="listing-field-value">${displayValue}</div>
</div>
`;
}).filter(html => html !== '').join('');
modalBody.innerHTML = `
<div class="request-detail">
<div class="request-detail-header">
<h2>New Listing Submission #${listing.id}</h2>
<span class="status-badge ${listing.status}">${listing.status}</span>
</div>
<div class="modal-section">
<div class="modal-section-title">Submitted By</div>
<div class="modal-section-content">
${this.escapeHtml(listing.submitter_email)}
${listing.submitter_name ? ` (${this.escapeHtml(listing.submitter_name)})` : ''}
<br><small>${this.formatDate(listing.created_at)}</small>
</div>
</div>
<div class="modal-section">
<div class="modal-section-title">Listing Details</div>
<div class="listing-fields">
${fieldsHtml}
</div>
</div>
${listing.additional_notes ? `
<div class="modal-section">
<div class="modal-section-title">Additional Notes</div>
<div class="modal-section-content">${this.escapeHtml(listing.additional_notes)}</div>
</div>
` : ''}
${listing.status === 'pending' ? `
<div class="modal-section">
<div class="modal-section-title">Admin Notes (optional)</div>
<textarea id="listingAdminNotes" class="admin-notes-input" rows="2" placeholder="Add notes about your decision..."></textarea>
</div>
<div class="modal-actions">
<button class="resource-action-btn reject-listing-btn" data-listing-id="${listing.id}">Reject</button>
<button class="resource-action-btn primary approve-listing-btn" data-listing-id="${listing.id}">Approve & Publish</button>
</div>
` : `
${listing.admin_notes ? `
<div class="modal-section">
<div class="modal-section-title">Admin Notes</div>
<div class="modal-section-content">${this.escapeHtml(listing.admin_notes)}</div>
</div>
` : ''}
<div class="modal-section">
<div class="modal-section-title">Reviewed</div>
<div class="modal-section-content">
${listing.reviewed_at ? this.formatDate(listing.reviewed_at) : 'N/A'}
${listing.reviewed_by ? ` by ${this.escapeHtml(listing.reviewed_by)}` : ''}
${listing.created_resource_id ? `<br>Created Resource ID: ${listing.created_resource_id}` : ''}
</div>
</div>
`}
</div>
`;
document.getElementById('listingModal').classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
async approveListingSubmission(listingId) {
const adminNotes = document.getElementById('listingAdminNotes')?.value.trim() || null;
if (!confirm('Are you sure you want to approve this listing and publish it?')) {
return;
}
try {
const response = await fetch(`/api/admin/listing-submissions/${listingId}/approve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.password}`
},
body: JSON.stringify({ admin_notes: adminNotes })
});
if (response.ok) {
const data = await response.json();
alert(`Listing approved and published! New Resource ID: ${data.resourceId}`);
this.closeModal('listingModal');
this.loadListingCounts();
this.loadListings();
} else {
const data = await response.json();
alert(data.error || 'Failed to approve listing');
}
} catch (error) {
console.error('Failed to approve listing:', error);
alert('Failed to approve listing. Please try again.');
}
}
async rejectListingSubmission(listingId) {
const adminNotes = document.getElementById('listingAdminNotes')?.value.trim() || null;
if (!confirm('Are you sure you want to reject this listing?')) {
return;
}
try {
const response = await fetch(`/api/admin/listing-submissions/${listingId}/reject`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.password}`
},
body: JSON.stringify({ admin_notes: adminNotes })
});
if (response.ok) {
alert('Listing submission rejected.');
this.closeModal('listingModal');
this.loadListingCounts();
this.loadListings();
} else {
const data = await response.json();
alert(data.error || 'Failed to reject listing');
}
} catch (error) {
console.error('Failed to reject listing:', error);
alert('Failed to reject listing. Please try again.');
}
}
formatType(type) {
const types = {
'food_bank': 'Food Bank',
'community_meal': 'Community Meal',
'hamper': 'Food Hamper',
'pantry': 'Food Pantry',
'soup_kitchen': 'Soup Kitchen',
'mobile_food': 'Mobile Food',
'grocery_program': 'Grocery Program',
'other': 'Other'
};
return types[type] || type;
}
formatDate(dateStr) {
const date = new Date(dateStr);
return date.toLocaleDateString('en-CA', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ==========================================
// Geocoding Management Methods
// ==========================================
switchGeocodingTab(filter) {
this.geocodingFilter = filter;
// Update active tab (only for geocoding section)
document.querySelectorAll('.admin-tab[data-section="geocoding"]').forEach(tab => {
tab.classList.toggle('active', tab.dataset.filter === filter);
});
this.loadGeoResources();
}
async loadGeocodingStats() {
try {
const response = await fetch('/api/resources/map', {
headers: {
'Authorization': `Bearer ${this.password}`
}
});
if (response.ok) {
const data = await response.json();
const resources = data.resources || [];
// Calculate stats
let total = 0, high = 0, medium = 0, low = 0;
resources.forEach(r => {
total++;
const confidence = r.geocode_confidence || 0;
if (confidence >= 80) {
high++;
} else if (confidence >= 50) {
medium++;
} else {
low++;
}
});
document.getElementById('geoTotalCount').textContent = total;
document.getElementById('geoHighCount').textContent = high;
document.getElementById('geoMediumCount').textContent = medium;
document.getElementById('geoLowCount').textContent = low;
}
} catch (error) {
console.error('Failed to load geocoding stats:', error);
}
}
async loadGeoResources() {
const container = document.getElementById('geocodingList');
const noResults = document.getElementById('noGeoResources');
const loading = document.getElementById('loadingGeoResources');
container.innerHTML = '';
noResults.classList.add('hidden');
loading.classList.remove('hidden');
try {
const response = await fetch('/api/resources/map', {
headers: {
'Authorization': `Bearer ${this.password}`
}
});
if (response.status === 401) {
this.logout();
return;
}
const data = await response.json();
let resources = data.resources || [];
// Apply filter
if (this.geocodingFilter === 'low') {
resources = resources.filter(r => (r.geocode_confidence || 0) < 50);
} else if (this.geocodingFilter === 'missing') {
resources = resources.filter(r => !r.latitude || !r.longitude);
}
// Sort by confidence (lowest first) for better workflow
resources.sort((a, b) => (a.geocode_confidence || 0) - (b.geocode_confidence || 0));
this.geoResources = resources;
loading.classList.add('hidden');
if (resources.length === 0) {
noResults.classList.remove('hidden');
return;
}
container.innerHTML = resources.map(resource => `
<div class="request-card">
<div class="request-card-header">
<div>
<h3 class="request-resource-name">${this.escapeHtml(resource.name)}</h3>
<span class="request-date">${this.escapeHtml(resource.city || 'No city')}</span>
</div>
${this.getConfidenceBadge(resource.geocode_confidence, true)}
</div>
<div class="request-card-info">
<div class="request-submitter">
<strong>Address:</strong> ${this.escapeHtml(resource.address) || '<em>Not set</em>'}
</div>
<div class="request-changes">
<strong>Coords:</strong> ${resource.latitude && resource.longitude
? `${parseFloat(resource.latitude).toFixed(4)}, ${parseFloat(resource.longitude).toFixed(4)}`
: '<em>Missing</em>'}
${resource.geocode_provider ? ` | <strong>Provider:</strong> ${this.escapeHtml(resource.geocode_provider)}` : ''}
</div>
</div>
<div class="request-card-actions">
<button class="resource-action-btn view-geocoding-btn" data-resource-id="${resource.id}">View & Edit</button>
</div>
</div>
`).join('');
} catch (error) {
console.error('Failed to load resources:', error);
loading.classList.add('hidden');
container.innerHTML = '<div class="error-message">Failed to load resources. Please try again.</div>';
}
}
openGeocodingModal(resourceId) {
const resource = this.geoResources.find(r => r.id === resourceId);
if (!resource) return;
const modalBody = document.getElementById('geocodingModalBody');
const hasCoords = resource.latitude && resource.longitude;
const confidence = resource.geocode_confidence || 0;
modalBody.innerHTML = `
<div class="request-detail">
<div class="request-detail-header">
<h2>${this.escapeHtml(resource.name)}</h2>
${this.getConfidenceBadge(confidence, true)}
</div>
<div class="modal-section">
<div class="modal-section-title">Address Information</div>
<div class="listing-fields">
<div class="listing-field">
<div class="listing-field-label">Address</div>
<div class="listing-field-value">${this.escapeHtml(resource.address) || '<em>Not set</em>'}</div>
</div>
<div class="listing-field">
<div class="listing-field-label">City</div>
<div class="listing-field-value">${this.escapeHtml(resource.city) || '<em>Not set</em>'}</div>
</div>
</div>
</div>
<div class="modal-section">
<div class="modal-section-title">Geocoding Details</div>
<div class="listing-fields">
<div class="listing-field">
<div class="listing-field-label">Latitude</div>
<div class="listing-field-value">${hasCoords ? parseFloat(resource.latitude).toFixed(6) : '<em>Missing</em>'}</div>
</div>
<div class="listing-field">
<div class="listing-field-label">Longitude</div>
<div class="listing-field-value">${hasCoords ? parseFloat(resource.longitude).toFixed(6) : '<em>Missing</em>'}</div>
</div>
<div class="listing-field">
<div class="listing-field-label">Confidence</div>
<div class="listing-field-value">${confidence}%</div>
</div>
<div class="listing-field">
<div class="listing-field-label">Provider</div>
<div class="listing-field-value">${this.escapeHtml(resource.geocode_provider) || '<em>Unknown</em>'}</div>
</div>
</div>
</div>
<div class="modal-actions">
<button class="resource-action-btn primary regeocode-btn" data-resource-id="${resource.id}">
Re-geocode This Resource
</button>
</div>
</div>
`;
document.getElementById('geocodingModal').classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
async regeocodeResource(resourceId, button) {
const originalText = button.textContent;
button.textContent = 'Geocoding...';
button.disabled = true;
try {
const response = await fetch(`/api/admin/resources/${resourceId}/regeocode`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.password}`
}
});
const data = await response.json();
if (response.ok && data.success) {
const result = data.result;
alert(`Successfully re-geocoded!\n\nLatitude: ${result.latitude.toFixed(6)}\nLongitude: ${result.longitude.toFixed(6)}\nConfidence: ${result.confidence}%\nProvider: ${result.provider}${result.warnings?.length ? '\n\nWarnings:\n' + result.warnings.join('\n') : ''}`);
// Refresh the geocoding data
this.loadGeocodingStats();
this.loadGeoResources();
this.closeModal('geocodingModal');
} else {
alert(data.error || 'Failed to re-geocode resource');
button.textContent = originalText;
button.disabled = false;
}
} catch (error) {
console.error('Failed to re-geocode resource:', error);
alert('Failed to re-geocode resource. Please try again.');
button.textContent = originalText;
button.disabled = false;
}
}
getConfidenceBadge(confidence, showLabel = false) {
const conf = confidence || 0;
let level, color;
if (conf >= 80) {
level = 'High';
color = '#10b981';
} else if (conf >= 50) {
level = 'Medium';
color = '#f59e0b';
} else {
level = 'Low';
color = '#ef4444';
}
const label = showLabel ? `${level} (${conf}%)` : `${conf}%`;
return `<span class="status-badge" style="background-color:${color};color:white;">${label}</span>`;
}
}
// Initialize admin panel
const adminPanel = new AdminPanel();