Pest-detection / backend /admin.html
Fadhili Sumaye
Update admin dashboard footer to show Administered by Fadhili and Didas
cb1034b
Raw
History Blame Contribute Delete
18.9 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pest Detection Admin Panel</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #0b0f19;
--card-bg: #151d30;
--card-border: #232d45;
--text-main: #f3f4f6;
--text-muted: #9ca3af;
--accent-primary: #10b981;
--accent-secondary: #059669;
--accent-glow: rgba(16, 185, 129, 0.15);
--danger: #ef4444;
--warning: #f59e0b;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Outfit', sans-serif;
}
body {
background-color: var(--bg-color);
color: var(--text-main);
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
background: linear-gradient(135deg, #111827 0%, #1f2937 100%);
border-bottom: 1px solid var(--card-border);
padding: 1.5rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}
.logo-container {
display: flex;
align-items: center;
gap: 0.75rem;
}
.logo-icon {
font-size: 2rem;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
background: linear-gradient(to right, #10b981, #34d399);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.nav-stats {
display: flex;
gap: 1.5rem;
}
.stat-badge {
background-color: var(--card-bg);
border: 1px solid var(--card-border);
padding: 0.5rem 1rem;
border-radius: 9999px;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.stat-count {
font-weight: 700;
color: var(--accent-primary);
}
.stat-count.pending {
color: var(--warning);
}
main {
flex: 1;
max-width: 1400px;
width: 100%;
margin: 0 auto;
padding: 2rem;
}
.notification-banner {
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.2);
padding: 1rem;
border-radius: 0.75rem;
margin-bottom: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.notification-banner button {
background-color: var(--accent-primary);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
}
.notification-banner button:hover {
background-color: var(--accent-secondary);
}
.section-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.reports-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(600px, 1fr));
gap: 2rem;
margin-bottom: 3rem;
}
@media (max-width: 768px) {
.reports-grid {
grid-template-columns: 1fr;
}
}
.report-card {
background-color: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 1rem;
overflow: hidden;
display: flex;
transition: transform 0.3s ease, box-shadow 0.3s ease;
position: relative;
}
.report-card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 20px rgba(0,0,0,0.3), 0 0 15px var(--accent-glow);
}
.report-card.resolved-card {
opacity: 0.85;
}
.report-image-container {
width: 45%;
position: relative;
background-color: #0d121f;
}
.report-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.report-details {
width: 55%;
padding: 1.5rem;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.meta-info {
font-size: 0.8rem;
color: var(--text-muted);
margin-bottom: 0.75rem;
}
.meta-item {
margin-bottom: 0.25rem;
word-break: break-all;
}
.meta-label {
font-weight: 500;
color: var(--text-main);
}
.report-form {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
label {
font-size: 0.8rem;
font-weight: 500;
color: var(--text-muted);
}
input[type="text"], textarea {
background-color: #0b0f19;
border: 1px solid var(--card-border);
border-radius: 0.375rem;
padding: 0.5rem 0.75rem;
color: var(--text-main);
font-size: 0.875rem;
outline: none;
transition: border-color 0.2s ease;
}
input[type="text"]:focus, textarea:focus {
border-color: var(--accent-primary);
}
textarea {
resize: none;
height: 80px;
}
.btn-resolve {
background-color: var(--accent-primary);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
cursor: pointer;
font-weight: 600;
font-size: 0.875rem;
transition: all 0.2s ease;
margin-top: 0.25rem;
}
.btn-resolve:hover {
background-color: var(--accent-secondary);
}
.resolved-display {
border-top: 1px solid var(--card-border);
padding-top: 1rem;
margin-top: 1rem;
}
.resolved-badge {
background-color: rgba(16, 185, 129, 0.15);
color: var(--accent-primary);
border: 1px solid rgba(16, 185, 129, 0.3);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
display: inline-block;
margin-bottom: 0.5rem;
}
.resolved-title {
font-weight: 600;
margin-bottom: 0.25rem;
}
.resolved-treatment {
font-size: 0.875rem;
color: var(--text-muted);
}
.empty-state {
grid-column: 1 / -1;
text-align: center;
padding: 4rem 2rem;
background-color: var(--card-bg);
border: 1px dashed var(--card-border);
border-radius: 1rem;
color: var(--text-muted);
}
.empty-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
footer {
text-align: center;
padding: 2rem;
color: var(--text-muted);
font-size: 0.875rem;
border-top: 1px solid var(--card-border);
}
</style>
</head>
<body>
<header>
<div class="logo-container">
<span class="logo-icon">🌾</span>
<div>
<h1>Pest Detection Admin</h1>
<p style="font-size: 0.75rem; color: var(--text-muted);">Real-time Crop Diagnostics Console</p>
</div>
</div>
<div class="nav-stats">
<div class="stat-badge">
<span>Pending Review:</span>
<span class="stat-count pending" id="pending-count">0</span>
</div>
<div class="stat-badge">
<span>Resolved:</span>
<span class="stat-count" id="resolved-count">0</span>
</div>
</div>
</header>
<main>
<div class="notification-banner" id="notif-banner" style="display: none;">
<p>🔔 Enable desktop notifications to receive instant audio and visual alerts when users report unrecognized pests.</p>
<button onclick="requestNotificationPermission()">Enable Notifications</button>
</div>
<section>
<h2 class="section-title">🕒 Pending User Submissions</h2>
<div class="reports-grid" id="pending-reports-grid">
<div class="empty-state">
<div class="empty-icon">🎉</div>
<p>All clear! There are no pending unrecognized pest reports.</p>
</div>
</div>
</section>
<section style="margin-top: 4rem;">
<h2 class="section-title">✅ Resolved Submissions</h2>
<div class="reports-grid" id="resolved-reports-grid">
<div class="empty-state">
<p>No reports resolved yet.</p>
</div>
</div>
</section>
</main>
<footer>
<p>Administered by Fadhili and Didas</p>
</footer>
<audio id="alert-sound" src="https://assets.mixkit.co/active_storage/sfx/911/911-500.wav" preload="auto"></audio>
<script>
let previousPendingIds = new Set();
let isInitialLoad = true;
async function fetchReports() {
try {
const response = await fetch('/admin/api/reports');
const data = await response.json();
if (data.status === 'success') {
renderReports(data.reports);
}
} catch (err) {
console.error("Failed to fetch reports:", err);
}
}
function renderReports(reports) {
const pendingGrid = document.getElementById('pending-reports-grid');
const resolvedGrid = document.getElementById('resolved-reports-grid');
const pendingReports = reports.filter(r => r.status === 'pending');
const resolvedReports = reports.filter(r => r.status === 'resolved');
// Update stats in header
document.getElementById('pending-count').textContent = pendingReports.length;
document.getElementById('resolved-count').textContent = resolvedReports.length;
// Update page tab title badge count
if (pendingReports.length > 0) {
document.title = `(${pendingReports.length}) Pest Detection Admin`;
} else {
document.title = "Pest Detection Admin Panel";
}
// Check for new pending reports to trigger alerts
const currentPendingIds = new Set(pendingReports.map(r => r.id));
if (!isInitialLoad) {
let hasNew = false;
for (let id of currentPendingIds) {
if (!previousPendingIds.has(id)) {
hasNew = true;
break;
}
}
if (hasNew) {
playAlert();
triggerNotification(pendingReports.length);
}
}
previousPendingIds = currentPendingIds;
isInitialLoad = false;
// Render Pending Grid
if (pendingReports.length === 0) {
pendingGrid.innerHTML = `
<div class="empty-state">
<div class="empty-icon">🎉</div>
<p>All clear! There are no pending unrecognized pest reports.</p>
</div>
`;
} else {
pendingGrid.innerHTML = pendingReports.map(r => `
<div class="report-card" id="card-${r.id}">
<div class="report-image-container">
<img class="report-image" src="/admin/image/${r.image_path}" alt="Unrecognized pest">
</div>
<div class="report-details">
<div>
<div class="meta-info">
<div class="meta-item"><span class="meta-label">ID:</span> #${r.id}</div>
<div class="meta-item"><span class="meta-label">Device:</span> ${r.device_id}</div>
<div class="meta-item"><span class="meta-label">Date:</span> ${new Date(r.created_at).toLocaleString()}</div>
</div>
</div>
<form class="report-form" onsubmit="resolveReport(event, ${r.id})">
<div class="form-group">
<label for="pest-${r.id}">Identified Pest Name</label>
<input type="text" id="pest-${r.id}" required placeholder="e.g. Stem Borer / Maize Weevil">
</div>
<div class="form-group">
<label for="treat-${r.id}">Treatment Recommendation Plan</label>
<textarea id="treat-${r.id}" required placeholder="Provide step-by-step chemical/biological treatment plan..."></textarea>
</div>
<button type="submit" class="btn-resolve">Resolve & Notify Farmer</button>
</form>
</div>
</div>
`).join('');
}
// Render Resolved Grid
if (resolvedReports.length === 0) {
resolvedGrid.innerHTML = `
<div class="empty-state">
<p>No reports resolved yet.</p>
</div>
`;
} else {
resolvedGrid.innerHTML = resolvedReports.map(r => `
<div class="report-card resolved-card">
<div class="report-image-container">
<img class="report-image" src="/admin/image/${r.image_path}" alt="Resolved pest">
</div>
<div class="report-details">
<div>
<span class="resolved-badge">Resolved</span>
<div class="meta-info">
<div class="meta-item"><span class="meta-label">ID:</span> #${r.id}</div>
<div class="meta-item"><span class="meta-label">Device:</span> ${r.device_id}</div>
<div class="meta-item"><span class="meta-label">Date:</span> ${new Date(r.created_at).toLocaleString()}</div>
</div>
<div class="resolved-display">
<div class="resolved-title">${r.pest_name}</div>
<div class="resolved-treatment">${r.treatment}</div>
</div>
</div>
</div>
</div>
`).join('');
}
}
async function resolveReport(event, id) {
event.preventDefault();
const pestName = document.getElementById(`pest-${id}`).value;
const treatment = document.getElementById(`treat-${id}`).value;
const formData = new FormData();
formData.append('pest_name', pestName);
formData.append('treatment', treatment);
try {
const response = await fetch(`/admin/api/resolve/${id}`, {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.status === 'success') {
// Remove card with visual fade effect
const card = document.getElementById(`card-${id}`);
card.style.transition = "opacity 0.5s ease, transform 0.5s ease";
card.style.opacity = 0;
card.style.transform = "scale(0.9)";
setTimeout(() => {
fetchReports();
}, 500);
}
} catch (err) {
alert("Failed to resolve report: " + err);
}
}
function playAlert() {
const sound = document.getElementById('alert-sound');
if (sound) {
sound.play().catch(e => console.log("Audio play blocked by browser."));
}
}
function requestNotificationPermission() {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
document.getElementById('notif-banner').style.display = 'none';
new Notification("Notifications Enabled!", {
body: "You will now receive alerts for new unrecognized pest scans.",
icon: "🌾"
});
}
});
}
function checkNotificationPermission() {
if (Notification.permission === 'default') {
document.getElementById('notif-banner').style.display = 'flex';
}
}
function triggerNotification(pendingCount) {
if (Notification.permission === 'granted') {
new Notification("New Unrecognized Pest Scan!", {
body: `There are currently ${pendingCount} pending reports waiting for identification.`,
icon: "🌾"
});
}
}
// Initialize and poll
checkNotificationPermission();
fetchReports();
setInterval(fetchReports, 10000);
</script>
</body>
</html>