210 lines
9.9 KiB
JavaScript
210 lines
9.9 KiB
JavaScript
/**
|
|
* Map Refinement (Place Audit) logic
|
|
*/
|
|
|
|
const refinement = {
|
|
state: {
|
|
candidates: []
|
|
},
|
|
|
|
init: async () => {
|
|
console.log('📍 Initializing Map Refinement Logic...');
|
|
await refinement.fetchCandidates();
|
|
},
|
|
|
|
fetchCandidates: async () => {
|
|
const headers = auth.getAuthHeader();
|
|
const url = '/api/map-refinement/places/candidates?status=PENDING';
|
|
console.log('🔍 [PlaceAudit] Fetching:', url);
|
|
console.log('🔑 [PlaceAudit] Auth headers:', JSON.stringify(headers));
|
|
|
|
const tbody = document.getElementById('refinement-table-body');
|
|
|
|
try {
|
|
const res = await fetch(url, { headers });
|
|
console.log('📡 [PlaceAudit] Response status:', res.status);
|
|
|
|
if (res.ok) {
|
|
refinement.state.candidates = await res.json();
|
|
console.log('✅ [PlaceAudit] Loaded', refinement.state.candidates.length, 'candidates');
|
|
refinement.renderTable();
|
|
} else {
|
|
const errorText = await res.text();
|
|
console.error('❌ [PlaceAudit] API Error:', res.status, errorText);
|
|
if (tbody) {
|
|
tbody.innerHTML = `<tr><td colspan="5" class="py-20 text-center text-red-400 font-bold">
|
|
API Error ${res.status}: ${errorText.substring(0, 100)}
|
|
</td></tr>`;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ [PlaceAudit] Network Error:', error);
|
|
if (tbody) {
|
|
tbody.innerHTML = `<tr><td colspan="5" class="py-20 text-center text-red-400 font-bold">
|
|
Network Error: ${error.message}
|
|
</td></tr>`;
|
|
}
|
|
}
|
|
},
|
|
|
|
renderTable: () => {
|
|
const tbody = document.getElementById('refinement-table-body');
|
|
if (!tbody) return;
|
|
|
|
if (refinement.state.candidates.length === 0) {
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="5" class="py-20 text-center text-slate-500">
|
|
<div class="flex flex-col items-center gap-4">
|
|
<i data-lucide="map-pin" class="w-8 h-8 opacity-20"></i>
|
|
<p>No pending location suggestions.</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
lucide.createIcons();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
tbody.innerHTML = refinement.state.candidates.map(c => {
|
|
const trustColor = c.confidence_score >= 80 ? 'emerald' : (c.confidence_score >= 50 ? 'amber' : 'slate');
|
|
const trustText = c.confidence_score >= 80 ? 'High Trust' : (c.confidence_score >= 50 ? 'Partial Match' : 'Unverified');
|
|
|
|
// Safety checks for coordinates
|
|
const lat = typeof c.latitude === 'number' ? c.latitude.toFixed(4) : 'N/A';
|
|
const lng = typeof c.longitude === 'number' ? c.longitude.toFixed(4) : 'N/A';
|
|
|
|
return `
|
|
<tr class="border-b border-white/5 hover:bg-white/[0.02] transition-colors group">
|
|
<td class="px-8 py-6">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 rounded-xl bg-${trustColor}-500/10 flex items-center justify-center text-${trustColor}-400">
|
|
<i data-lucide="${c.confidence_score >= 50 ? 'shield-check' : 'shield-alert'}" class="w-5 h-5"></i>
|
|
</div>
|
|
<div>
|
|
<div class="font-bold text-white">${c.name_ar || c.name || 'Untitled'}</div>
|
|
<div class="text-xs text-slate-500 mt-1">${c.category || 'Location'}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-8 py-6">
|
|
<div class="text-sm text-slate-300 font-medium">${c.verified_name || '<span class="text-slate-600 italic">No external match</span>'}</div>
|
|
<div class="flex items-center gap-2 mt-1.5">
|
|
<span class="px-2 py-0.5 rounded-full bg-${trustColor}-500/10 text-${trustColor}-400 text-[10px] font-bold uppercase tracking-wider border border-${trustColor}-500/20">
|
|
${trustText} (${c.confidence_score || 0}%)
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td class="px-8 py-6">
|
|
<div class="text-sm text-slate-300 font-mono">${lat}, ${lng}</div>
|
|
<div class="text-[11px] text-slate-500 mt-1">${c.neighborhood_name || 'Unknown Neighborhood'}</div>
|
|
</td>
|
|
<td class="px-8 py-6 text-right space-x-2">
|
|
<button onclick="refinement.showMapModal('${c.latitude}', '${c.longitude}', '${(c.name_ar || c.name || "Location").replace(/'/g, "\\'")}')" class="p-2.5 rounded-xl bg-blue-500/10 text-blue-400 hover:bg-blue-500/20 transition-all" title="View on Google Maps">
|
|
<i data-lucide="eye" class="w-4 h-4"></i>
|
|
</button>
|
|
<button onclick="refinement.reject('${c.id}')" class="p-2.5 rounded-xl bg-red-500/10 text-red-400 hover:bg-red-500/20 transition-all" title="Reject">
|
|
<i data-lucide="x" class="w-4 h-4"></i>
|
|
</button>
|
|
<button onclick="refinement.approve('${c.id}')" class="p-2.5 rounded-xl bg-emerald-500/10 text-emerald-400 hover:bg-emerald-500/20 transition-all border border-emerald-500/20 shadow-lg shadow-emerald-500/10" title="Approve">
|
|
<i data-lucide="check" class="w-4 h-4"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
|
|
lucide.createIcons();
|
|
} catch (error) {
|
|
console.error('❌ [PlaceAudit] Rendering error:', error);
|
|
tbody.innerHTML = `<tr><td colspan="4" class="py-20 text-center text-red-500">Rendering Error: See console for details</td></tr>`;
|
|
}
|
|
},
|
|
|
|
showMapModal: (lat, lng, name) => {
|
|
// Remove existing modal if any
|
|
const existing = document.getElementById('map-preview-modal');
|
|
if (existing) existing.remove();
|
|
|
|
const modal = document.createElement('div');
|
|
modal.id = 'map-preview-modal';
|
|
modal.className = 'fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-sm animate-in fade-in duration-300';
|
|
modal.innerHTML = `
|
|
<div class="bg-[#101014] border border-white/10 rounded-[2.5rem] w-full max-w-5xl overflow-hidden shadow-2xl animate-in zoom-in-95 duration-300">
|
|
<div class="px-8 py-6 border-b border-white/5 flex justify-between items-center bg-white/[0.02]">
|
|
<div>
|
|
<h3 class="text-xl font-bold text-white">${name}</h3>
|
|
<p class="text-xs text-slate-500 font-mono mt-1">${lat}, ${lng}</p>
|
|
</div>
|
|
<button onclick="document.getElementById('map-preview-modal').remove()" class="p-2 hover:bg-white/10 rounded-full transition-colors">
|
|
<i data-lucide="x" class="w-6 h-6 text-slate-400"></i>
|
|
</button>
|
|
</div>
|
|
<div class="aspect-video w-full bg-slate-900">
|
|
<iframe
|
|
width="100%"
|
|
height="100%"
|
|
frameborder="0"
|
|
scrolling="no"
|
|
marginheight="0"
|
|
marginwidth="0"
|
|
src="https://maps.google.com/maps?q=${lat},${lng}&t=k&z=19&ie=UTF8&iwloc=&output=embed">
|
|
</iframe>
|
|
</div>
|
|
<div class="px-8 py-6 bg-white/[0.01] flex justify-between items-center">
|
|
<p class="text-xs text-slate-500 font-medium">Verify location visual details against satellite imagery</p>
|
|
<a href="https://www.google.com/maps/search/?api=1&query=${lat},${lng}" target="_blank" class="px-4 py-2 bg-blue-600/10 text-blue-400 rounded-lg text-xs font-bold hover:bg-blue-600/20 transition-all border border-blue-500/10">
|
|
Open in Full Google Maps
|
|
</a>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(modal);
|
|
lucide.createIcons();
|
|
},
|
|
|
|
approve: async (id) => {
|
|
if (!confirm('Are you sure you want to approve this location and add it to the production map?')) return;
|
|
|
|
const headers = auth.getAuthHeader();
|
|
try {
|
|
const res = await fetch(`/api/map-refinement/places/candidates/${id}/approve`, {
|
|
method: 'PATCH',
|
|
headers
|
|
});
|
|
if (res.ok) {
|
|
await refinement.fetchCandidates();
|
|
} else {
|
|
alert('Approval failed.');
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
},
|
|
|
|
reject: async (id) => {
|
|
const reason = prompt('Please enter a reason for rejection:');
|
|
if (reason === null) return;
|
|
|
|
const headers = auth.getAuthHeader();
|
|
try {
|
|
const res = await fetch(`/api/map-refinement/places/candidates/${id}/reject`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
...headers,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ reason })
|
|
});
|
|
if (res.ok) {
|
|
await refinement.fetchCandidates();
|
|
} else {
|
|
alert('Rejection failed.');
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
}
|
|
};
|