mirror of
https://github.com/xodivorce/infra-xodivorce-in.git
synced 2026-02-04 16:12:23 +05:30
feat: initial admin panel with Gemini integration
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
#### A real-time map-based reporting system for campus infrastructure issues, built to improve visibility, accountability, and resolution efficiency.
|
#### A real-time map-based reporting system for campus infrastructure issues, built to improve visibility, accountability, and resolution efficiency.
|
||||||
|
|
||||||
[](https://github.com/xodivorce/infra-xodivorce-in/)
|
[](https://github.com/xodivorce/infra-xodivorce-in/)
|
||||||
[](https://github.com/xodivorce/infra-xodivorce-in/)
|
[](https://github.com/xodivorce/infra-xodivorce-in/)
|
||||||
[](https://github.com/xodivorce/infra-xodivorce-in/)
|
[](https://github.com/xodivorce/infra-xodivorce-in/)
|
||||||
|
|
||||||
> **🥰 Like this project? Please consider giving it a Star (🌟) on GitHub to show us your appreciation. Thank you!**
|
> **🥰 Like this project? Please consider giving it a Star (🌟) on GitHub to show us your appreciation. Thank you!**
|
||||||
|
|||||||
@@ -43,26 +43,26 @@ $inactiveClass = 'text-neutral-400 hover:text-white hover:bg-neutral-800/60 bord
|
|||||||
Admin Overview
|
Admin Overview
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="?page=activity-logs"
|
<a href="?page=admin-panel"
|
||||||
class="group flex items-center px-4 py-3 text-sm font-medium rounded-xl <?php echo $page === 'activity-logs' ? $activeClass : $inactiveClass; ?>">
|
class="group flex items-center px-4 py-3 text-sm font-medium rounded-xl <?php echo $page === 'admin-panel' ? $activeClass : $inactiveClass; ?>">
|
||||||
<svg class="w-5 h-5 mr-3 <?php echo $page === 'activity-logs' ? 'text-blue-400' : 'text-neutral-500 group-hover:text-white'; ?> transition-colors"
|
<svg class="w-5 h-5 mr-3 <?php echo $page === 'admin-panel' ? 'text-blue-400' : 'text-neutral-500 group-hover:text-white'; ?> transition-colors"
|
||||||
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
Activity Logs
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="?page=admin"
|
|
||||||
class="group flex items-center px-4 py-3 text-sm font-medium rounded-xl <?php echo $page === 'admin' ? $activeClass : $inactiveClass; ?>">
|
|
||||||
<svg class="w-5 h-5 mr-3 <?php echo $page === 'admin' ? 'text-blue-400' : 'text-neutral-500 group-hover:text-white'; ?> transition-colors"
|
|
||||||
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
System Admin
|
Admin Panel
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="?page=system-logs"
|
||||||
|
class="group flex items-center px-4 py-3 text-sm font-medium rounded-xl <?php echo $page === 'system-logs' ? $activeClass : $inactiveClass; ?>">
|
||||||
|
<svg class="w-5 h-5 mr-3 <?php echo $page === 'system-logs' ? 'text-blue-400' : 'text-neutral-500 group-hover:text-white'; ?> transition-colors"
|
||||||
|
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
System Logs
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -1,438 +0,0 @@
|
|||||||
<section class="space-y-6">
|
|
||||||
<header>
|
|
||||||
<h1 class="text-2xl font-semibold text-white">Admin Panel</h1>
|
|
||||||
<p class="mt-1 text-sm text-neutral-400">System management and administrative controls</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
||||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700 p-6">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-neutral-400">Total Users</p>
|
|
||||||
<p class="mt-2 text-3xl font-semibold text-white">1,247</p>
|
|
||||||
</div>
|
|
||||||
<div class="w-12 h-12 bg-blue-900 rounded-lg flex items-center justify-center">
|
|
||||||
<svg class="w-6 h-6 text-blue-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z">
|
|
||||||
</path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="mt-4 text-xs text-neutral-500">Active: 1,189 | Suspended: 58</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700 p-6">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-neutral-400">Pending Reports</p>
|
|
||||||
<p class="mt-2 text-3xl font-semibold text-white">34</p>
|
|
||||||
</div>
|
|
||||||
<div class="w-12 h-12 bg-yellow-900 rounded-lg flex items-center justify-center">
|
|
||||||
<svg class="w-6 h-6 text-yellow-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
|
|
||||||
</path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="mt-4 text-xs text-neutral-500">High: 8 | Medium: 15 | Low: 11</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700 p-6">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-neutral-400">Active Incidents</p>
|
|
||||||
<p class="mt-2 text-3xl font-semibold text-white">7</p>
|
|
||||||
</div>
|
|
||||||
<div class="w-12 h-12 bg-red-900 rounded-lg flex items-center justify-center">
|
|
||||||
<svg class="w-6 h-6 text-red-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z">
|
|
||||||
</path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="mt-4 text-xs text-neutral-500">Critical: 2 | Warning: 5</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700 p-6">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-neutral-400">System Health</p>
|
|
||||||
<p class="mt-2 text-3xl font-semibold text-green-400">98.5%</p>
|
|
||||||
</div>
|
|
||||||
<div class="w-12 h-12 bg-green-900 rounded-lg flex items-center justify-center">
|
|
||||||
<svg class="w-6 h-6 text-green-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="mt-4 text-xs text-neutral-500">All services operational</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700">
|
|
||||||
<div class="px-6 py-4 border-b border-neutral-700">
|
|
||||||
<h2 class="text-lg font-semibold text-white">User Management</h2>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="w-full">
|
|
||||||
<thead class="bg-neutral-900 border-b border-neutral-700">
|
|
||||||
<tr>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-400 uppercase tracking-wider">
|
|
||||||
User ID</th>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-400 uppercase tracking-wider">
|
|
||||||
Name</th>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-400 uppercase tracking-wider">
|
|
||||||
Email</th>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-400 uppercase tracking-wider">
|
|
||||||
Role</th>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-400 uppercase tracking-wider">
|
|
||||||
Status</th>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-400 uppercase tracking-wider">
|
|
||||||
Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y divide-neutral-700">
|
|
||||||
<tr class="hover:bg-neutral-750">
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-300">#1001</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">John Anderson</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-400">john.anderson@company.com</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-900 text-purple-200">Admin</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-900 text-green-200">Active</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
|
||||||
<button class="text-blue-400 hover:text-blue-300 mr-3">View</button>
|
|
||||||
<button class="text-red-400 hover:text-red-300">Disable</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="hover:bg-neutral-750">
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-300">#1002</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">Sarah Mitchell</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-400">sarah.mitchell@company.com</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-900 text-blue-200">Moderator</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-900 text-green-200">Active</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
|
||||||
<button class="text-blue-400 hover:text-blue-300 mr-3">View</button>
|
|
||||||
<button class="text-red-400 hover:text-red-300">Disable</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="hover:bg-neutral-750">
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-300">#1003</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">Michael Chen</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-400">michael.chen@company.com</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-neutral-700 text-neutral-300">User</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-900 text-green-200">Active</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
|
||||||
<button class="text-blue-400 hover:text-blue-300 mr-3">View</button>
|
|
||||||
<button class="text-red-400 hover:text-red-300">Disable</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="hover:bg-neutral-750">
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-300">#1004</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">Emily Rodriguez</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-400">emily.rodriguez@company.com
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-neutral-700 text-neutral-300">User</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-900 text-red-200">Suspended</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
|
||||||
<button class="text-blue-400 hover:text-blue-300 mr-3">View</button>
|
|
||||||
<button class="text-green-400 hover:text-green-300">Enable</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="hover:bg-neutral-750">
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-300">#1005</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">David Thompson</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-400">david.thompson@company.com</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-900 text-blue-200">Moderator</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-900 text-green-200">Active</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
|
||||||
<button class="text-blue-400 hover:text-blue-300 mr-3">View</button>
|
|
||||||
<button class="text-red-400 hover:text-red-300">Disable</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700">
|
|
||||||
<div class="px-6 py-4 border-b border-neutral-700">
|
|
||||||
<h2 class="text-lg font-semibold text-white">Report Moderation</h2>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="w-full">
|
|
||||||
<thead class="bg-neutral-900 border-b border-neutral-700">
|
|
||||||
<tr>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-400 uppercase tracking-wider">
|
|
||||||
Report ID</th>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-400 uppercase tracking-wider">
|
|
||||||
Title</th>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-400 uppercase tracking-wider">
|
|
||||||
Submitted By</th>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-400 uppercase tracking-wider">
|
|
||||||
Priority</th>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-400 uppercase tracking-wider">
|
|
||||||
Status</th>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-400 uppercase tracking-wider">
|
|
||||||
Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y divide-neutral-700">
|
|
||||||
<tr class="hover:bg-neutral-750">
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-300">#2045</td>
|
|
||||||
<td class="px-6 py-4 text-sm text-white">Database Connection Issues</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-400">alex.kim@company.com</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-900 text-red-200">High</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-900 text-yellow-200">Pending</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
|
||||||
<button class="text-green-400 hover:text-green-300 mr-2">Approve</button>
|
|
||||||
<button class="text-orange-400 hover:text-orange-300 mr-2">Escalate</button>
|
|
||||||
<button class="text-red-400 hover:text-red-300">Delete</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="hover:bg-neutral-750">
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-300">#2044</td>
|
|
||||||
<td class="px-6 py-4 text-sm text-white">API Response Time Degradation</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-400">maria.santos@company.com</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-900 text-yellow-200">Medium</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-900 text-yellow-200">Pending</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
|
||||||
<button class="text-green-400 hover:text-green-300 mr-2">Approve</button>
|
|
||||||
<button class="text-orange-400 hover:text-orange-300 mr-2">Escalate</button>
|
|
||||||
<button class="text-red-400 hover:text-red-300">Delete</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="hover:bg-neutral-750">
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-300">#2043</td>
|
|
||||||
<td class="px-6 py-4 text-sm text-white">UI Rendering Bug</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-400">james.wilson@company.com</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-900 text-blue-200">Low</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-900 text-yellow-200">Pending</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
|
||||||
<button class="text-green-400 hover:text-green-300 mr-2">Approve</button>
|
|
||||||
<button class="text-orange-400 hover:text-orange-300 mr-2">Escalate</button>
|
|
||||||
<button class="text-red-400 hover:text-red-300">Delete</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="hover:bg-neutral-750">
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-300">#2042</td>
|
|
||||||
<td class="px-6 py-4 text-sm text-white">Memory Leak in Production</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-400">lisa.park@company.com</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-900 text-red-200">High</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-900 text-yellow-200">Pending</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
|
||||||
<button class="text-green-400 hover:text-green-300 mr-2">Approve</button>
|
|
||||||
<button class="text-orange-400 hover:text-orange-300 mr-2">Escalate</button>
|
|
||||||
<button class="text-red-400 hover:text-red-300">Delete</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700">
|
|
||||||
<div class="px-6 py-4 border-b border-neutral-700">
|
|
||||||
<h2 class="text-lg font-semibold text-white">System Controls</h2>
|
|
||||||
</div>
|
|
||||||
<div class="p-6 space-y-4">
|
|
||||||
<div class="flex items-center justify-between p-4 bg-neutral-900 rounded-lg border border-neutral-700">
|
|
||||||
<div>
|
|
||||||
<h3 class="text-sm font-medium text-white">Maintenance Mode</h3>
|
|
||||||
<p class="mt-1 text-xs text-neutral-400">Enable maintenance mode to perform system updates</p>
|
|
||||||
</div>
|
|
||||||
<label class="relative inline-flex items-center cursor-pointer">
|
|
||||||
<input type="checkbox" class="sr-only peer">
|
|
||||||
<div
|
|
||||||
class="w-11 h-6 bg-neutral-700 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-500 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-neutral-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600">
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<button
|
|
||||||
class="flex items-center justify-center px-4 py-3 bg-neutral-900 border border-neutral-700 rounded-lg text-sm font-medium text-white hover:bg-neutral-750 transition-colors">
|
|
||||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16">
|
|
||||||
</path>
|
|
||||||
</svg>
|
|
||||||
Clear Cache
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="flex items-center justify-center px-4 py-3 bg-neutral-900 border border-neutral-700 rounded-lg text-sm font-medium text-white hover:bg-neutral-750 transition-colors">
|
|
||||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15">
|
|
||||||
</path>
|
|
||||||
</svg>
|
|
||||||
Force Status Refresh
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="flex items-center justify-center px-4 py-3 bg-yellow-900 border border-yellow-700 rounded-lg text-sm font-medium text-yellow-200 hover:bg-yellow-800 transition-colors">
|
|
||||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15">
|
|
||||||
</path>
|
|
||||||
</svg>
|
|
||||||
Restart Services
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hidden bg-neutral-800 rounded-lg border border-neutral-700 p-12">
|
|
||||||
<div class="flex flex-col items-center justify-center text-center">
|
|
||||||
<svg class="w-16 h-16 text-neutral-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4">
|
|
||||||
</path>
|
|
||||||
</svg>
|
|
||||||
<h3 class="text-lg font-medium text-white mb-1">No data available</h3>
|
|
||||||
<p class="text-sm text-neutral-400">Administrative data will appear here when available</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hidden bg-neutral-800 rounded-lg border border-neutral-700 overflow-hidden">
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="w-full">
|
|
||||||
<thead class="bg-neutral-900 border-b border-neutral-700">
|
|
||||||
<tr>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-400 uppercase tracking-wider">
|
|
||||||
User ID</th>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-400 uppercase tracking-wider">
|
|
||||||
Name</th>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-400 uppercase tracking-wider">
|
|
||||||
Email</th>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-400 uppercase tracking-wider">
|
|
||||||
Role</th>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-400 uppercase tracking-wider">
|
|
||||||
Status</th>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-400 uppercase tracking-wider">
|
|
||||||
Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y divide-neutral-700">
|
|
||||||
<tr class="animate-pulse">
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="h-4 bg-neutral-700 rounded w-16"></div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="h-4 bg-neutral-700 rounded w-32"></div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="h-4 bg-neutral-700 rounded w-48"></div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="h-5 bg-neutral-700 rounded-full w-16"></div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="h-5 bg-neutral-700 rounded-full w-16"></div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="h-4 bg-neutral-700 rounded w-24"></div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="animate-pulse">
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="h-4 bg-neutral-700 rounded w-16"></div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="h-4 bg-neutral-700 rounded w-28"></div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="h-4 bg-neutral-700 rounded w-44"></div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="h-5 bg-neutral-700 rounded-full w-16"></div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="h-5 bg-neutral-700 rounded-full w-16"></div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="h-4 bg-neutral-700 rounded w-24"></div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="animate-pulse">
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="h-4 bg-neutral-700 rounded w-16"></div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="h-4 bg-neutral-700 rounded w-36"></div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="h-4 bg-neutral-700 rounded w-52"></div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="h-5 bg-neutral-700 rounded-full w-16"></div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="h-5 bg-neutral-700 rounded-full w-16"></div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="h-4 bg-neutral-700 rounded w-24"></div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
516
src/assets/admin/_admin_panel.php
Executable file
516
src/assets/admin/_admin_panel.php
Executable file
@@ -0,0 +1,516 @@
|
|||||||
|
<?php
|
||||||
|
if (!isset($_SESSION['is_admin']) || $_SESSION['is_admin'] != 1) {
|
||||||
|
echo "<div class='text-red-500 p-6'>Access Denied</div>";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'update_status') {
|
||||||
|
$report_id = intval($_POST['report_id']);
|
||||||
|
$new_status = $_POST['new_status'];
|
||||||
|
$allowed_statuses = ['Opened', 'In Progress', 'Resolved'];
|
||||||
|
|
||||||
|
if (in_array($new_status, $allowed_statuses)) {
|
||||||
|
$stmt = $conn->prepare("UPDATE reports SET status = ? WHERE id = ?");
|
||||||
|
$stmt->bind_param("si", $new_status, $report_id);
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo "<script>window.location.href = window.location.href;</script>";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql_users = "SELECT COUNT(*) as total FROM users";
|
||||||
|
$total_users = $conn->query($sql_users)->fetch_assoc()['total'] ?? 0;
|
||||||
|
|
||||||
|
$sql_active = "SELECT COUNT(DISTINCT user_id) as active FROM reports";
|
||||||
|
$active_users = $conn->query($sql_active)->fetch_assoc()['active'] ?? 0;
|
||||||
|
|
||||||
|
$sql_pending = "SELECT COUNT(*) as pending FROM reports WHERE status = 'Opened'";
|
||||||
|
$pending_reports = $conn->query($sql_pending)->fetch_assoc()['pending'] ?? 0;
|
||||||
|
|
||||||
|
$sql_resolved = "SELECT COUNT(*) as resolved FROM reports WHERE status = 'Resolved'";
|
||||||
|
$resolved_reports = $conn->query($sql_resolved)->fetch_assoc()['resolved'] ?? 0;
|
||||||
|
|
||||||
|
$sql_reports = "SELECT r.id, r.title, r.category, r.priority, r.status, r.created_at, r.location, r.image_path, u.username as user
|
||||||
|
FROM reports r
|
||||||
|
JOIN users u ON r.user_id = u.id
|
||||||
|
ORDER BY r.created_at DESC";
|
||||||
|
$result = $conn->query($sql_reports);
|
||||||
|
|
||||||
|
$reports = [];
|
||||||
|
if ($result->num_rows > 0) {
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$reports[] = [
|
||||||
|
'id' => $row['id'],
|
||||||
|
'title' => htmlspecialchars($row['title']),
|
||||||
|
'user' => htmlspecialchars($row['user']),
|
||||||
|
'category' => htmlspecialchars($row['category']),
|
||||||
|
'priority' => htmlspecialchars($row['priority']),
|
||||||
|
'status' => htmlspecialchars($row['status']),
|
||||||
|
'date' => $row['created_at'],
|
||||||
|
'image' => htmlspecialchars($row['image_path']),
|
||||||
|
'location' => htmlspecialchars($row['location'])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<section class="space-y-6" x-data="{ currentTab: 'reports' }">
|
||||||
|
<header>
|
||||||
|
<h1 class="text-2xl font-semibold text-white">Admin Panel</h1>
|
||||||
|
<p class="mt-1 text-sm text-neutral-400">System management and administrative controls</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
<div class="bg-neutral-800 rounded-lg border border-neutral-700 p-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-neutral-400">Total Users</p>
|
||||||
|
<p class="mt-2 text-3xl font-semibold text-white"><?php echo number_format($total_users); ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-purple-900 rounded-lg flex items-center justify-center">
|
||||||
|
<svg class="w-6 h-6 text-purple-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 text-xs text-neutral-500">Registered accounts</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-neutral-800 rounded-lg border border-neutral-700 p-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-neutral-400">Active Reporters</p>
|
||||||
|
<p class="mt-2 text-3xl font-semibold text-white"><?php echo number_format($active_users); ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-blue-900 rounded-lg flex items-center justify-center">
|
||||||
|
<svg class="w-6 h-6 text-blue-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 text-xs text-neutral-500">Users who submitted reports</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-neutral-800 rounded-lg border border-neutral-700 p-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-neutral-400">Pending Issues</p>
|
||||||
|
<p class="mt-2 text-3xl font-semibold text-white"><?php echo number_format($pending_reports); ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-red-900 rounded-lg flex items-center justify-center">
|
||||||
|
<svg class="w-6 h-6 text-red-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 text-xs text-neutral-500">Requires attention</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-neutral-800 rounded-lg border border-neutral-700 p-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-neutral-400">Issues Resolved</p>
|
||||||
|
<p class="mt-2 text-3xl font-semibold text-green-400"><?php echo number_format($resolved_reports); ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-green-900 rounded-lg flex items-center justify-center">
|
||||||
|
<svg class="w-6 h-6 text-green-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 text-xs text-neutral-500">Successfully closed reports</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-6 pb-10">
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="bg-neutral-900 p-1 rounded-xl border border-neutral-800 inline-flex">
|
||||||
|
<button onclick="switchTab('reports')" id="tab-btn-reports" class="px-4 py-2 text-sm font-medium rounded-lg bg-neutral-800 text-white shadow-sm border border-neutral-700 transition-all cursor-default flex items-center gap-2">
|
||||||
|
Campus Reports
|
||||||
|
</button>
|
||||||
|
<button onclick="switchTab('gemini')" id="tab-btn-gemini" class="px-4 py-2 text-sm font-medium rounded-lg text-neutral-400 hover:text-white hover:bg-neutral-800 transition-all cursor-pointer flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4 text-purple-400" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M19 11h-6V5h-2v6H5v2h6v6h2v-6h6z" />
|
||||||
|
</svg>
|
||||||
|
Ask Gemini
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="view-reports" class="space-y-6">
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold text-white">Report Moderation</h2>
|
||||||
|
<p class="mt-1 text-sm text-neutral-400">Manage facility and infrastructure issues</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-neutral-800 rounded-xl border border-neutral-700 p-4 shadow-sm">
|
||||||
|
<div class="flex flex-col lg:flex-row gap-4 justify-between">
|
||||||
|
<div class="flex overflow-x-auto pb-2 lg:pb-0 gap-2 no-scrollbar" id="status-filters">
|
||||||
|
<button onclick="setFilter('status', 'All')" id="filter-all" class="px-4 py-2 bg-blue-600/10 text-blue-400 border border-blue-600/20 text-sm font-medium rounded-lg whitespace-nowrap transition-all">All Reports</button>
|
||||||
|
<button onclick="setFilter('status', 'Opened')" id="filter-opened" class="px-4 py-2 hover:bg-neutral-700 text-neutral-400 hover:text-white text-sm font-medium rounded-lg transition-colors whitespace-nowrap border border-transparent">Opened</button>
|
||||||
|
<button onclick="setFilter('status', 'In Progress')" id="filter-progress" class="px-4 py-2 hover:bg-neutral-700 text-neutral-400 hover:text-white text-sm font-medium rounded-lg transition-colors whitespace-nowrap border border-transparent">In Progress</button>
|
||||||
|
<button onclick="setFilter('status', 'Resolved')" id="filter-fixed" class="px-4 py-2 hover:bg-neutral-700 text-neutral-400 hover:text-white text-sm font-medium rounded-lg transition-colors whitespace-nowrap border border-transparent">Resolved</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row gap-3">
|
||||||
|
<div class="relative min-w-[200px]">
|
||||||
|
<select onchange="setFilter('date', this.value)" class="w-full appearance-none px-4 py-2 bg-neutral-900 border border-neutral-700 rounded-lg text-sm text-neutral-300 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition-all cursor-pointer">
|
||||||
|
<option value="all">All time</option>
|
||||||
|
<option value="7">Last 7 days</option>
|
||||||
|
<option value="30">Last 30 days</option>
|
||||||
|
</select>
|
||||||
|
<svg class="absolute right-3 top-2.5 h-4 w-4 text-neutral-500 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative w-full sm:w-64">
|
||||||
|
<input type="text" placeholder="Search..." onkeyup="setFilter('search', this.value)" class="w-full pl-10 pr-4 py-2 bg-neutral-900 border border-neutral-700 rounded-lg text-sm text-white placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition-all">
|
||||||
|
<svg class="absolute left-3 top-2.5 h-4 w-4 text-neutral-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hidden md:block bg-neutral-800 rounded-xl border border-neutral-700 overflow-hidden shadow-sm">
|
||||||
|
<div class="overflow-x-auto custom-scrollbar">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead class="bg-neutral-900/50 border-b border-neutral-700">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-semibold text-neutral-400 uppercase tracking-wider w-12">ID</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-semibold text-neutral-400 uppercase tracking-wider w-1/4">Title</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-semibold text-neutral-400 uppercase tracking-wider">Category</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-semibold text-neutral-400 uppercase tracking-wider">Priority</th>
|
||||||
|
<th class="px-6 py-4 text-center text-xs font-semibold text-neutral-400 uppercase tracking-wider">Image</th>
|
||||||
|
<th class="px-6 py-4 text-center text-xs font-semibold text-neutral-400 uppercase tracking-wider">Location</th>
|
||||||
|
<th class="px-6 py-4 text-center text-xs font-semibold text-neutral-400 uppercase tracking-wider w-32">Date</th>
|
||||||
|
<th class="px-6 py-4 text-center text-xs font-semibold text-neutral-400 uppercase tracking-wider w-48">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="reports-table-body" class="divide-y divide-neutral-700/50">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-neutral-900 border-t border-neutral-700 px-6 py-4 flex items-center justify-between">
|
||||||
|
<span class="text-sm text-neutral-400" id="pagination-info">Showing page 1 of 1</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button id="btn-prev" onclick="changePage(-1)" class="px-3 py-1 bg-neutral-800 hover:bg-neutral-700 border border-neutral-700 rounded-lg text-sm text-white transition-colors disabled:opacity-50 disabled:cursor-not-allowed">Previous</button>
|
||||||
|
<button id="btn-next" onclick="changePage(1)" class="px-3 py-1 bg-neutral-800 hover:bg-neutral-700 border border-neutral-700 rounded-lg text-sm text-white transition-colors disabled:opacity-50 disabled:cursor-not-allowed">Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="view-gemini" class="hidden h-[75vh] flex flex-col bg-neutral-800 rounded-xl border border-neutral-700 overflow-hidden relative shadow-2xl">
|
||||||
|
<div class="flex-none px-6 py-4 border-b border-neutral-700 bg-neutral-900/50 backdrop-blur flex items-center gap-3">
|
||||||
|
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-blue-600 to-purple-600 flex items-center justify-center shadow-lg shadow-purple-900/20">
|
||||||
|
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-bold text-white tracking-tight">Gemini Assistant</h2>
|
||||||
|
<p class="text-xs text-neutral-400">Analysis & Response Generation</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 overflow-y-auto p-6 space-y-6 custom-scrollbar bg-neutral-800" id="chat-container">
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-blue-600 to-purple-600 flex-shrink-0 flex items-center justify-center mt-1">
|
||||||
|
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1 max-w-[80%]">
|
||||||
|
<span class="text-xs font-bold text-purple-400 ml-1">Gemini</span>
|
||||||
|
<div class="bg-neutral-700/50 border border-neutral-600 rounded-2xl rounded-tl-none px-5 py-3 text-neutral-200 text-sm leading-relaxed shadow-sm">
|
||||||
|
Hello Admin. I can assist with prioritizing facility issues, drafting responses to students, and analyzing infrastructure trends.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-none p-4 bg-neutral-900 border-t border-neutral-700">
|
||||||
|
<div class="relative flex items-end gap-2 max-w-4xl mx-auto">
|
||||||
|
<textarea id="gemini-prompt" rows="1" placeholder="Ask about specific reports..." class="w-full pl-5 pr-14 py-3.5 bg-neutral-800 border border-neutral-700 rounded-2xl text-white placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 transition-all resize-none custom-scrollbar text-sm leading-relaxed" style="min-height: 52px; max-height: 150px;"></textarea>
|
||||||
|
<button type="button" onclick="sendGeminiRequest()" id="send-btn" class="absolute right-2 bottom-2.5 p-2 bg-purple-600 hover:bg-purple-500 text-white rounded-xl transition-all shadow-lg shadow-purple-900/20">
|
||||||
|
<svg class="w-5 h-5 transform -rotate-90" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const reportsData = <?php echo json_encode($reports); ?>;
|
||||||
|
|
||||||
|
let state = {
|
||||||
|
filterStatus: 'All',
|
||||||
|
filterDate: 'all',
|
||||||
|
filterSearch: '',
|
||||||
|
currentPage: 1,
|
||||||
|
itemsPerPage: 5
|
||||||
|
};
|
||||||
|
|
||||||
|
function youtubeSearchLink(query) {
|
||||||
|
return `https://www.youtube.com/results?search_query=${encodeURIComponent(query)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGeminiText(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
|
||||||
|
text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||||
|
|
||||||
|
let safeText = text
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">");
|
||||||
|
|
||||||
|
safeText = safeText.replace(/\*\*(.*?)\*\*/g, '<strong class="text-white font-semibold">$1</strong>');
|
||||||
|
|
||||||
|
safeText = safeText.replace(
|
||||||
|
/(https?:\/\/[^\s<]+)/g,
|
||||||
|
'<a href="$1" target="_blank" rel="noopener noreferrer" class="text-blue-400 hover:text-blue-300 underline break-all">$1</a>'
|
||||||
|
);
|
||||||
|
|
||||||
|
const lines = safeText.split('\n');
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
lines.forEach(line => {
|
||||||
|
const trimLine = line.trim();
|
||||||
|
|
||||||
|
if (trimLine.startsWith('### ')) {
|
||||||
|
html += `<div class="text-white font-semibold mt-3 mb-1 text-sm">${line.replace('### ', '')}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (trimLine.startsWith('## ')) {
|
||||||
|
html += `<div class="text-white font-bold mt-4 mb-2 text-base">${line.replace('## ', '')}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (trimLine.match(/^(\*{3,}|-{3,})$/)) {
|
||||||
|
html += `<hr class="my-3 border-neutral-600">`;
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (trimLine.match(/^[\-\*]\s+/)) {
|
||||||
|
const content = line.replace(/^\s*[\-\*]\s+/, '');
|
||||||
|
html += `<div class="flex gap-2 ml-2 mt-1"><span class="text-blue-400 shrink-0 leading-relaxed">•</span><span class="text-neutral-200 leading-relaxed">${content}</span></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (trimLine.match(/^\d+\.\s+/)) {
|
||||||
|
const match = line.match(/^(\s*\d+\.)\s+(.*)/);
|
||||||
|
if (match) {
|
||||||
|
html += `<div class="flex gap-2 ml-2 mt-1"><span class="font-mono text-blue-400 shrink-0 leading-relaxed">${match[1]}</span><span class="text-neutral-200 leading-relaxed">${match[2]}</span></div>`;
|
||||||
|
} else {
|
||||||
|
html += `<div class="mb-1 text-neutral-200">${line}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (trimLine.length > 0) {
|
||||||
|
html += `<div class="mb-1 text-neutral-200 leading-relaxed">${line}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
html += `<div class="h-2"></div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFilter(type, value) {
|
||||||
|
if (type === 'status') state.filterStatus = value;
|
||||||
|
if (type === 'date') state.filterDate = value;
|
||||||
|
if (type === 'search') state.filterSearch = value.toLowerCase();
|
||||||
|
|
||||||
|
state.currentPage = 1;
|
||||||
|
renderReports();
|
||||||
|
updateFilterUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePage(direction) {
|
||||||
|
state.currentPage += direction;
|
||||||
|
renderReports();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFilterUI() {
|
||||||
|
const statuses = ['All', 'Opened', 'In Progress', 'Resolved'];
|
||||||
|
const btnMap = { 'All': 'filter-all', 'Opened': 'filter-opened', 'In Progress': 'filter-progress', 'Resolved': 'filter-fixed' };
|
||||||
|
const activeClass = "px-4 py-2 bg-blue-600/10 text-blue-400 border border-blue-600/20 text-sm font-medium rounded-lg whitespace-nowrap transition-all";
|
||||||
|
const inactiveClass = "px-4 py-2 hover:bg-neutral-700 text-neutral-400 hover:text-white text-sm font-medium rounded-lg transition-colors whitespace-nowrap border border-transparent";
|
||||||
|
|
||||||
|
statuses.forEach(s => {
|
||||||
|
const btn = document.getElementById(btnMap[s]);
|
||||||
|
if (btn) btn.className = (state.filterStatus === s) ? activeClass : inactiveClass;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function advanceStatus(id, currentStatus) {
|
||||||
|
let nextStatus = '';
|
||||||
|
if (currentStatus === 'Opened') nextStatus = 'In Progress';
|
||||||
|
else if (currentStatus === 'In Progress') nextStatus = 'Resolved';
|
||||||
|
else return;
|
||||||
|
|
||||||
|
const form = document.createElement('form');
|
||||||
|
form.method = 'POST';
|
||||||
|
form.action = '';
|
||||||
|
const inputAction = document.createElement('input'); inputAction.type = 'hidden'; inputAction.name = 'action'; inputAction.value = 'update_status'; form.appendChild(inputAction);
|
||||||
|
const inputId = document.createElement('input'); inputId.type = 'hidden'; inputId.name = 'report_id'; inputId.value = id; form.appendChild(inputId);
|
||||||
|
const inputStatus = document.createElement('input'); inputStatus.type = 'hidden'; inputStatus.name = 'new_status'; inputStatus.value = nextStatus; form.appendChild(inputStatus);
|
||||||
|
document.body.appendChild(form);
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function askAboutReportAdmin(id, title, category, priority, status) {
|
||||||
|
switchTab('gemini');
|
||||||
|
const promptInput = document.getElementById('gemini-prompt');
|
||||||
|
promptInput.value = `Report ID: #${id}\nTitle: "${title}"\nCategory: ${category}\nPriority: ${priority}\nStatus: ${status}\n\nAnalyze this report and suggest internal admin actions:`;
|
||||||
|
promptInput.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderReports() {
|
||||||
|
const tbody = document.getElementById('reports-table-body');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
let filtered = reportsData.filter(item => {
|
||||||
|
if (state.filterStatus !== 'All' && item.status !== state.filterStatus) return false;
|
||||||
|
const searchStr = state.filterSearch;
|
||||||
|
if (searchStr) {
|
||||||
|
const matchesId = item.id.toString().includes(searchStr);
|
||||||
|
const matchesTitle = item.title.toLowerCase().includes(searchStr);
|
||||||
|
const matchesUser = item.user.toLowerCase().includes(searchStr);
|
||||||
|
if (!matchesId && !matchesTitle && !matchesUser) return false;
|
||||||
|
}
|
||||||
|
if (state.filterDate !== 'all') {
|
||||||
|
const itemDate = new Date(item.date);
|
||||||
|
const limitDate = new Date();
|
||||||
|
limitDate.setDate(limitDate.getDate() - parseInt(state.filterDate));
|
||||||
|
if (itemDate < limitDate) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(filtered.length / state.itemsPerPage) || 1;
|
||||||
|
if (state.currentPage > totalPages) state.currentPage = totalPages;
|
||||||
|
if (state.currentPage < 1) state.currentPage = 1;
|
||||||
|
|
||||||
|
const start = (state.currentPage - 1) * state.itemsPerPage;
|
||||||
|
const pageData = filtered.slice(start, start + state.itemsPerPage);
|
||||||
|
|
||||||
|
if (pageData.length === 0) {
|
||||||
|
tbody.innerHTML = `<tr><td colspan="8" class="px-6 py-10 text-center text-neutral-500">No reports found matching criteria</td></tr>`;
|
||||||
|
} else {
|
||||||
|
pageData.forEach(row => {
|
||||||
|
const dateObj = new Date(row.date);
|
||||||
|
const dateStr = dateObj.toLocaleDateString('en-US', { day: 'numeric', month: 'short', year: 'numeric' });
|
||||||
|
let priorityColor = row.priority === 'High' ? 'text-red-400' : (row.priority === 'Medium' ? 'text-orange-400' : 'text-blue-400');
|
||||||
|
|
||||||
|
let actionBtn = '';
|
||||||
|
if (row.status === 'Opened') {
|
||||||
|
actionBtn = `<button onclick="advanceStatus(${row.id}, '${row.status}')" class="group w-32 relative py-1.5 px-3 bg-yellow-600/10 hover:bg-yellow-600/20 text-yellow-500 border border-yellow-500/30 hover:border-yellow-500/50 rounded-lg text-xs font-semibold transition-all shadow-lg shadow-yellow-900/10 flex items-center justify-between mx-auto"><span class="flex items-center gap-1.5"><span class="w-1.5 h-1.5 rounded-full bg-yellow-500"></span>In Progress</span><svg class="w-3 h-3 transform group-hover:translate-x-0.5 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" /></svg></button>`;
|
||||||
|
} else if (row.status === 'In Progress') {
|
||||||
|
actionBtn = `<button onclick="advanceStatus(${row.id}, '${row.status}')" class="group w-32 relative py-1.5 px-3 bg-green-600/10 hover:bg-green-600/20 text-green-400 border border-green-500/30 hover:border-green-500/50 rounded-lg text-xs font-semibold transition-all shadow-lg shadow-green-900/10 flex items-center justify-between mx-auto"><span class="flex items-center gap-1.5"><span class="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse"></span>Resolved</span><svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg></button>`;
|
||||||
|
} else {
|
||||||
|
actionBtn = `<div class="w-32 py-1.5 px-3 bg-blue-500/10 border border-blue-500/20 text-blue-400 rounded-lg text-xs font-semibold text-center shadow-sm cursor-default flex items-center justify-center gap-2 mx-auto"><span class="w-1.5 h-1.5 rounded-full bg-blue-500 shadow-[0_0_8px_rgba(59,130,246,0.6)]"></span>Resolved</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let geminiBtn = '';
|
||||||
|
if (row.status !== 'Resolved') {
|
||||||
|
geminiBtn = `<button onclick="askAboutReportAdmin(${row.id},'${row.title.replace(/'/g, "\\'")}','${row.category.replace(/'/g, "\\'")}','${row.priority}','${row.status}')" class="w-32 mt-2 py-1 px-3 text-purple-400 hover:text-purple-300 hover:bg-purple-500/10 rounded-md text-[10px] font-medium transition-colors flex items-center justify-center gap-1.5 border border-transparent hover:border-purple-500/20 mx-auto"><svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>Ask Gemini</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewImageLink = `./core/actions/submit_report.php?view_id=${row.image}`;
|
||||||
|
const html = `<tr class="hover:bg-neutral-700/30 transition-colors duration-200"><td class="px-6 py-4 text-sm font-mono text-neutral-300">#${row.id}</td><td class="px-6 py-4"><div class="text-sm font-medium text-white">${row.title}</div><div class="text-xs text-neutral-400 mt-0.5">By ${row.user}</div></td><td class="px-6 py-4 text-sm text-neutral-200">${row.category}</td><td class="px-6 py-4 text-sm font-medium ${priorityColor}">${row.priority}</td><td class="px-6 py-4 text-center"><a href="${viewImageLink}" target="_blank" class="inline-block p-2 hover:bg-neutral-700 rounded-lg text-neutral-400 transition-colors"><svg class="h-5 w-5 mx-auto text-violet-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg></a></td><td class="px-6 py-4 text-center"><a href="${row.location}" target="_blank" class="inline-block p-2 hover:bg-neutral-700 rounded-lg text-blue-400 transition-colors"><svg class="h-5 w-5 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /></svg></a></td><td class="px-6 py-4 text-center text-sm text-neutral-400">${dateStr}</td><td class="px-6 py-4 w-48 text-center"><div class="flex flex-col items-center">${actionBtn}${geminiBtn}</div></td></tr>`;
|
||||||
|
tbody.innerHTML += html;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.getElementById('pagination-info').innerHTML = `Showing page <span class="text-white font-medium">${state.currentPage}</span> of ${totalPages}`;
|
||||||
|
document.getElementById('btn-prev').disabled = state.currentPage === 1;
|
||||||
|
document.getElementById('btn-next').disabled = state.currentPage === totalPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(tabId) {
|
||||||
|
const btnReports = document.getElementById('tab-btn-reports');
|
||||||
|
const btnGemini = document.getElementById('tab-btn-gemini');
|
||||||
|
const activeClass = "px-4 py-2 text-sm font-medium rounded-lg bg-neutral-800 text-white shadow-sm border border-neutral-700 cursor-default flex items-center gap-2 transition-all";
|
||||||
|
const inactiveClass = "px-4 py-2 text-sm font-medium rounded-lg text-neutral-400 hover:text-white hover:bg-neutral-800 border-transparent cursor-pointer flex items-center gap-2 transition-all";
|
||||||
|
|
||||||
|
if (tabId === 'reports') {
|
||||||
|
btnReports.className = activeClass;
|
||||||
|
btnGemini.className = inactiveClass;
|
||||||
|
document.getElementById('view-reports').classList.remove('hidden');
|
||||||
|
document.getElementById('view-gemini').classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
btnGemini.className = activeClass;
|
||||||
|
btnReports.className = inactiveClass;
|
||||||
|
document.getElementById('view-reports').classList.add('hidden');
|
||||||
|
document.getElementById('view-gemini').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendGeminiRequest() {
|
||||||
|
const input = document.getElementById('gemini-prompt');
|
||||||
|
const chatContainer = document.getElementById('chat-container');
|
||||||
|
const prompt = input.value.trim();
|
||||||
|
const sendBtn = document.getElementById('send-btn');
|
||||||
|
|
||||||
|
if (!prompt) return;
|
||||||
|
|
||||||
|
chatContainer.innerHTML += `
|
||||||
|
<div class="flex gap-4 flex-row-reverse">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-neutral-700 flex-shrink-0 flex items-center justify-center mt-1">
|
||||||
|
<svg class="w-4 h-4 text-neutral-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /></svg>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1 max-w-[80%]">
|
||||||
|
<div class="bg-blue-600 rounded-2xl rounded-tr-none px-5 py-3 text-white text-sm leading-relaxed shadow-md">${prompt.replace(/\n/g, '<br>')}</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
input.value = '';
|
||||||
|
input.disabled = true;
|
||||||
|
sendBtn.disabled = true;
|
||||||
|
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('./core/actions/admin_gemini.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({ prompt: prompt })
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
let replyText = data.reply ? renderGeminiText(data.reply) : `<span class="text-red-400">Error: ${data.error || "Unknown error"}</span>`;
|
||||||
|
|
||||||
|
chatContainer.innerHTML += `
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-blue-600 to-purple-600 flex-shrink-0 flex items-center justify-center mt-1">
|
||||||
|
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1 max-w-[80%]">
|
||||||
|
<span class="text-xs font-bold text-purple-400 ml-1">Gemini</span>
|
||||||
|
<div class="bg-neutral-700/50 border border-neutral-600 rounded-2xl rounded-tl-none px-5 py-3 text-neutral-200 text-sm leading-relaxed shadow-sm">
|
||||||
|
${replyText}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
chatContainer.innerHTML += `<div class="text-red-500 text-center text-xs mt-2">Connection Error: ${e.message}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.disabled = false;
|
||||||
|
sendBtn.disabled = false;
|
||||||
|
input.focus();
|
||||||
|
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderReports();
|
||||||
|
</script>
|
||||||
@@ -152,6 +152,24 @@ function askAboutReport(id, title, category) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function askAboutReportAdmin(id, title, category, priority, status) {
|
||||||
|
switchTab("gemini");
|
||||||
|
const inputField = document.getElementById("gemini-prompt");
|
||||||
|
|
||||||
|
if (inputField) {
|
||||||
|
inputField.value = `Regarding Report #${id} ("${title}") in category "${category}":
|
||||||
|
Provide a quick internal steps to resolve or debug this issue.
|
||||||
|
Structure your answer with these exact headers (keep bullet points short and technical):
|
||||||
|
|
||||||
|
1. Immediate Actions to take on the issue
|
||||||
|
2. Debug / Troubleshooting Steps`;
|
||||||
|
|
||||||
|
inputField.style.height = "auto";
|
||||||
|
inputField.style.height = inputField.scrollHeight + "px";
|
||||||
|
inputField.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function validateFile(input) {
|
function validateFile(input) {
|
||||||
const file = input.files[0];
|
const file = input.files[0];
|
||||||
const labelMain = document.getElementById("file-label-main");
|
const labelMain = document.getElementById("file-label-main");
|
||||||
|
|||||||
205
src/core/actions/admin_gemini.php
Normal file
205
src/core/actions/admin_gemini.php
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$basePath = dirname(dirname(__DIR__));
|
||||||
|
require_once $basePath . '/core/init.php';
|
||||||
|
require_once $basePath . '/core/connection.php';
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||||
|
|
||||||
|
if (empty($_SESSION) || !isset($_SESSION['is_admin']) || $_SESSION['is_admin'] != 1) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['error' => 'Admin access only']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['error' => 'Method not allowed']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = file_get_contents('php://input');
|
||||||
|
$data = json_decode($raw, true);
|
||||||
|
$prompt = trim($data['prompt'] ?? '');
|
||||||
|
|
||||||
|
if ($prompt === '') {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Prompt required']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKey = $_ENV['GEMINI_API_KEY'] ?? getenv('GEMINI_API_KEY');
|
||||||
|
if (!$apiKey) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Gemini API key missing']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
System prompt: strict admin role. Instruct model to output:
|
||||||
|
1) Plain readable analysis
|
||||||
|
2) A JSON block STRICTLY wrapped between ###JSON_START### and ###JSON_END###
|
||||||
|
with a defined schema so server can parse reliably.
|
||||||
|
3) A YouTube search query + suggested video title.
|
||||||
|
4) The mandatory footer contact lines.
|
||||||
|
*/
|
||||||
|
$domain = $_ENV['DOMAIN'] ?? 'YourCampus';
|
||||||
|
$app_name = $_ENV['APP_NAME'] ?? 'Campus Dashboard';
|
||||||
|
$helpEmail = $_ENV['IT_HELPDESK_EMAIL'] ?? 'it@example.com';
|
||||||
|
$helpPhone = $_ENV['IT_HELPDESK_PHONE'] ?? '+000';
|
||||||
|
$secEmail = $_ENV['SECURITY_EMAIL'] ?? 'security@example.com';
|
||||||
|
$secPhone = $_ENV['SECURITY_PHONE'] ?? '+000';
|
||||||
|
$healthEmail = $_ENV['HEALTH_EMAIL'] ?? 'health@example.com';
|
||||||
|
$healthPhone = $_ENV['HEALTH_PHONE'] ?? '+000';
|
||||||
|
$mgmtEmail = $_ENV['MANAGEMENT_EMAIL'] ?? 'mgmt@example.com';
|
||||||
|
$mgmtPhone = $_ENV['MANAGEMENT_PHONE'] ?? '+000';
|
||||||
|
$libEmail = $_ENV['LIBRARY_EMAIL'] ?? 'library@example.com';
|
||||||
|
$libPhone = $_ENV['LIBRARY_PHONE'] ?? '+000';
|
||||||
|
|
||||||
|
|
||||||
|
$systemPrompt = <<<PROMPT
|
||||||
|
You are the AI assistant for '{$domain}', a campus facility Admin dashboard.
|
||||||
|
Your main goal is to help Admins with campus facility and infrastructure issues only
|
||||||
|
(WiFi & Network, Electrical, Water & Plumbing, HVAC (AC/Heating), Furniture & Fixtures, Cleaning & Janitorial, Security & Safety, Road & Pathway Damage, Library & Study, Lost & Stolen, Medical/Health Issue or Others).
|
||||||
|
|
||||||
|
**IDENTITY:**
|
||||||
|
1. **{$app_name}** is the name of the college/university you serve.
|
||||||
|
2. You are part of the **{$app_name} Admin Dashboard** system.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### SCOPE ENFORCEMENT (IMPORTANT):
|
||||||
|
1. **You are strictly a facility Admin assistant.**
|
||||||
|
Your ONLY purpose is to help Admins report and suggest about handling campus facility or infrastructure issues.
|
||||||
|
You MUST NOT assist with academic, personal, entertainment, or unrelated topics.
|
||||||
|
|
||||||
|
2. **Refusal of Inappropriate or Off-Topic Queries**
|
||||||
|
If a user asks about inappropriate, illegal, entertainment, gambling, gaming, or unrelated topics:
|
||||||
|
Respond ONLY with:
|
||||||
|
> "I cannot assist with that. I am designed to help with campus facility and infrastructure issues only."
|
||||||
|
|
||||||
|
---
|
||||||
|
### RESPONSE GUIDELINES:
|
||||||
|
- Keep answers concise and professional
|
||||||
|
- Use bullet points
|
||||||
|
- Do NOT reference external websites, except youtube.com for video suggestions.
|
||||||
|
- Always provide a YouTube search query and suggested video title for further learning.
|
||||||
|
- Do NOT invent policies, departments, or contacts
|
||||||
|
|
||||||
|
### MANDATORY FOOTER (STRICT):
|
||||||
|
At the very end of EVERY response, you MUST do the following IN ORDER:
|
||||||
|
|
||||||
|
1. Identify the issue category
|
||||||
|
2. Provide a Suggested YouTube Search Query as <how-to style query>. Do NOT fabricate direct links.
|
||||||
|
PROMPT;
|
||||||
|
|
||||||
|
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={$apiKey}";
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'systemInstruction' => [
|
||||||
|
'parts' => [['text' => $systemPrompt]]
|
||||||
|
],
|
||||||
|
'contents' => [
|
||||||
|
[
|
||||||
|
'role' => 'user',
|
||||||
|
'parts' => [['text' => $prompt]]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'generationConfig' => [
|
||||||
|
'temperature' => 0.25,
|
||||||
|
'maxOutputTokens' => 700
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||||
|
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||||
|
CURLOPT_TIMEOUT => 30
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$curlError = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($curlError) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Connection failed', 'details' => $curlError]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = json_decode($response, true);
|
||||||
|
if ($result === null) {
|
||||||
|
// invalid JSON from API
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Invalid API response', 'raw' => $response]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($result['error'])) {
|
||||||
|
http_response_code($result['error']['code'] ?? 500);
|
||||||
|
echo json_encode([
|
||||||
|
'error' => $result['error'],
|
||||||
|
'raw_response' => $result
|
||||||
|
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Merge all text parts reliably */
|
||||||
|
$fullText = '';
|
||||||
|
if (!empty($result['candidates'])) {
|
||||||
|
foreach ($result['candidates'] as $cand) {
|
||||||
|
if (!empty($cand['content']['parts'])) {
|
||||||
|
foreach ($cand['content']['parts'] as $part) {
|
||||||
|
if (!empty($part['text'])) $fullText .= $part['text'] . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$fullText = trim($fullText);
|
||||||
|
|
||||||
|
if ($fullText === '') {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Gemini returned empty response', 'debug' => $result]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Try to extract the structured JSON between markers ###JSON_START### ... ###JSON_END### */
|
||||||
|
$structured = null;
|
||||||
|
$jsonStart = strpos($fullText, '###JSON_START###');
|
||||||
|
$jsonEnd = strpos($fullText, '###JSON_END###');
|
||||||
|
|
||||||
|
if ($jsonStart !== false && $jsonEnd !== false && $jsonEnd > $jsonStart) {
|
||||||
|
$jsonBlock = substr($fullText, $jsonStart + strlen('###JSON_START###'), $jsonEnd - ($jsonStart + strlen('###JSON_START###')));
|
||||||
|
$jsonBlock = trim($jsonBlock);
|
||||||
|
|
||||||
|
// try to json_decode; if fails, attempt to clean common markdown fences
|
||||||
|
$decoded = json_decode($jsonBlock, true);
|
||||||
|
if ($decoded === null) {
|
||||||
|
// remove triple backticks if present
|
||||||
|
$jsonBlockClean = preg_replace('/^```json\s*/i', '', $jsonBlock);
|
||||||
|
$jsonBlockClean = preg_replace('/```$/', '', $jsonBlockClean);
|
||||||
|
$jsonBlockClean = trim($jsonBlockClean);
|
||||||
|
$decoded = json_decode($jsonBlockClean, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($decoded !== null) {
|
||||||
|
$structured = $decoded;
|
||||||
|
} else {
|
||||||
|
// keep raw block as fallback
|
||||||
|
$structured = ['raw_json_block' => $jsonBlock];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Build final response */
|
||||||
|
$responsePayload = [
|
||||||
|
'reply' => $fullText,
|
||||||
|
'structured' => $structured,
|
||||||
|
'debug' => null
|
||||||
|
];
|
||||||
|
|
||||||
|
echo json_encode($responsePayload, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);
|
||||||
77
src/core/actions/mail/admin_report_config.php
Normal file
77
src/core/actions/mail/admin_report_config.php
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
function sendReportStatusEmail($conn, $report_id, $new_status) {
|
||||||
|
// Fetch user email and report details
|
||||||
|
$stmt = $conn->prepare("
|
||||||
|
SELECT r.title, u.email, u.username
|
||||||
|
FROM reports r
|
||||||
|
JOIN users u ON r.user_id = u.id
|
||||||
|
WHERE r.id = ?
|
||||||
|
");
|
||||||
|
$stmt->bind_param("i", $report_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
if ($result->num_rows === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $result->fetch_assoc();
|
||||||
|
$to = $data['email'];
|
||||||
|
$username = htmlspecialchars($data['username']);
|
||||||
|
$title = htmlspecialchars($data['title']);
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
$from_email = "no-reply@yourcampus.com"; // CHANGE THIS
|
||||||
|
$headers = "MIME-Version: 1.0" . "\r\n";
|
||||||
|
$headers .= "Content-type:text/html;charset=UTF-8" . "\r\n";
|
||||||
|
$headers .= "From: <" . $from_email . ">" . "\r\n";
|
||||||
|
|
||||||
|
$subject = "";
|
||||||
|
$body_content = "";
|
||||||
|
$color_code = "";
|
||||||
|
|
||||||
|
if ($new_status === 'In Progress') {
|
||||||
|
$subject = "Update: Report #$report_id is In Progress";
|
||||||
|
$color_code = "#eab308"; // Yellow/Orange
|
||||||
|
$body_content = "Good news! Your report regarding <strong>'$title'</strong> has been reviewed and our team is currently working on it.";
|
||||||
|
} elseif ($new_status === 'Resolved') {
|
||||||
|
$subject = "Resolved: Report #$report_id has been fixed";
|
||||||
|
$color_code = "#22c55e"; // Green
|
||||||
|
$body_content = "Great news! The issue <strong>'$title'</strong> has been successfully resolved. Thank you for helping improve our campus.";
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML Email Template
|
||||||
|
$message = "
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
.container { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 8px; }
|
||||||
|
.header { background-color: $color_code; color: white; padding: 10px 20px; border-radius: 6px 6px 0 0; }
|
||||||
|
.content { padding: 20px; color: #333; line-height: 1.6; }
|
||||||
|
.footer { margin-top: 20px; font-size: 12px; color: #777; border-top: 1px solid #eee; padding-top: 10px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class='container'>
|
||||||
|
<div class='header'>
|
||||||
|
<h2>Status Update</h2>
|
||||||
|
</div>
|
||||||
|
<div class='content'>
|
||||||
|
<p>Hello $username,</p>
|
||||||
|
<p>$body_content</p>
|
||||||
|
<p><strong>Current Status:</strong> <span style='color: $color_code; font-weight: bold;'>$new_status</span></p>
|
||||||
|
</div>
|
||||||
|
<div class='footer'>
|
||||||
|
<p>This is an automated message. Please do not reply directly to this email.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
";
|
||||||
|
|
||||||
|
// Send Mail
|
||||||
|
return mail($to, $subject, $message, $headers);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -30,18 +30,18 @@ $routes = [
|
|||||||
'file' => './assets/admin/_admin_overview.php',
|
'file' => './assets/admin/_admin_overview.php',
|
||||||
'nav' => true,
|
'nav' => true,
|
||||||
],
|
],
|
||||||
'activity-logs' => [
|
'system-logs' => [
|
||||||
'role' => 'admin',
|
'role' => 'admin',
|
||||||
'label' => 'Activity Logs',
|
'label' => 'System Logs',
|
||||||
'icon' => 'clock',
|
'icon' => 'clock',
|
||||||
'file' => './assets/admin/_activity_logs.php',
|
'file' => './assets/admin/_system_logs.php',
|
||||||
'nav' => true,
|
'nav' => true,
|
||||||
],
|
],
|
||||||
'admin' => [
|
'admin-panel' => [
|
||||||
'role' => 'admin',
|
'role' => 'admin',
|
||||||
'label' => 'Admin',
|
'label' => 'Admin Panel',
|
||||||
'icon' => 'settings',
|
'icon' => 'settings',
|
||||||
'file' => './assets/admin/_admin.php',
|
'file' => './assets/admin/_admin_panel.php',
|
||||||
'nav' => true,
|
'nav' => true,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user