Files
maps-saas/apps/dashboard/js/refinement.js
T
2026-04-16 03:39:49 +03:00

184 lines
8.1 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;
}
tbody.innerHTML = refinement.state.candidates.map(c => `
<tr class="hover:bg-white/[0.02] transition-colors border-b border-white/[0.03]">
<td class="px-8 py-6">
<div class="font-bold text-white">${c.name_ar || c.name}</div>
<div class="text-[10px] text-blue-400 font-extrabold uppercase mt-1">ID: ${c.id}</div>
</td>
<td class="px-8 py-6">
<span class="px-2 py-1 rounded-lg bg-blue-500/10 text-blue-400 text-[10px] font-black uppercase tracking-wider">${c.category || 'General'}</span>
</td>
<td class="px-8 py-6">
<div class="text-sm text-slate-300 font-medium">${c.governorate_name || 'Unknown Region'}</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}')" 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();
},
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);
}
}
};