0xarchit commited on
Commit
c2bc4c7
·
1 Parent(s): 9bf5220

frontend v2 refactor and enhancements

Browse files
.gitignore CHANGED
@@ -19,4 +19,5 @@ User/android/app/release/
19
  User/android/app/debug/
20
 
21
  .vscode
22
- node_modules/
 
 
19
  User/android/app/debug/
20
 
21
  .vscode
22
+ node_modules/
23
+ .next
Frontend/app/admin/departments/[id]/page.tsx CHANGED
@@ -48,12 +48,11 @@ export default function DepartmentDetailPage() {
48
  const [loading, setLoading] = useState(true);
49
  const [showAddMember, setShowAddMember] = useState(false);
50
 
51
- // Form State
52
  const [newMember, setNewMember] = useState({
53
  name: "",
54
  email: "",
55
  role: "worker",
56
- password: "", // Required by backend
57
  phone: "",
58
  city: "",
59
  max_workload: 10,
@@ -88,7 +87,7 @@ export default function DepartmentDetailPage() {
88
  await apiPost("/admin/members", {
89
  ...newMember,
90
  department_id: department.id,
91
- locality: "General", // Default for now
92
  });
93
  setShowAddMember(false);
94
  setNewMember({
@@ -100,7 +99,7 @@ export default function DepartmentDetailPage() {
100
  city: "",
101
  max_workload: 10,
102
  });
103
- loadData(department.id); // Refresh list
104
  alert("Member added successfully!");
105
  } catch (error: any) {
106
  alert(error.message || "Failed to add member");
@@ -110,8 +109,6 @@ export default function DepartmentDetailPage() {
110
  const handleDeleteMember = async (memberId: string) => {
111
  if (!confirm("Are you sure you want to remove this member?")) return;
112
  try {
113
- // Assuming there is a delete endpoint, though strictly not in the original brief, it's good UX
114
- // backend/api/routes/admin.py lines 612-624 supports DELETE /members/{member_id}
115
  const token = localStorage.getItem("supabase_token");
116
  await fetch(
117
  `${process.env.NEXT_PUBLIC_API_URL}/admin/members/${memberId}`,
@@ -128,7 +125,7 @@ export default function DepartmentDetailPage() {
128
 
129
  if (loading) {
130
  return (
131
- <div className="p-8 text-center text-slate-500">
132
  Loading Department...
133
  </div>
134
  );
@@ -139,8 +136,7 @@ export default function DepartmentDetailPage() {
139
  }
140
 
141
  return (
142
- <div className="max-w-6xl mx-auto space-y-8 p-6">
143
- {/* Header */}
144
  <div>
145
  <Link
146
  href="/admin/departments"
@@ -148,10 +144,10 @@ export default function DepartmentDetailPage() {
148
  >
149
  <ArrowLeft className="w-4 h-4" /> Back to Departments
150
  </Link>
151
- <div className="flex justify-between items-start">
152
  <div>
153
  <div className="flex items-center gap-3 mb-1">
154
- <h1 className="text-3xl font-bold text-slate-900">
155
  {department.name}
156
  </h1>
157
  <span className="bg-slate-100 text-slate-600 px-2 py-1 rounded text-sm font-mono font-bold border border-slate-200">
@@ -175,9 +171,8 @@ export default function DepartmentDetailPage() {
175
  </div>
176
  </div>
177
 
178
- {/* Stats Cards */}
179
  <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
180
- <div className="card border-l-4 border-l-blue-500 p-6">
181
  <div className="flex justify-between items-center">
182
  <div>
183
  <p className="text-sm font-bold text-slate-400 uppercase tracking-wider">
@@ -187,34 +182,31 @@ export default function DepartmentDetailPage() {
187
  {members.length}
188
  </h3>
189
  </div>
190
- <div className="h-10 w-10 bg-blue-50 rounded-lg flex items-center justify-center text-blue-600">
191
  <Users className="w-6 h-6" />
192
  </div>
193
  </div>
194
  </div>
195
- {/* Add more stats if available */}
196
  </div>
197
 
198
- {/* Members Section */}
199
  <div className="space-y-6">
200
  <div className="flex justify-between items-center">
201
- <h2 className="text-xl font-bold text-slate-900 flex items-center gap-2">
202
  <Users className="w-5 h-5 text-slate-400" />
203
  Department Members
204
  </h2>
205
  <button
206
  onClick={() => setShowAddMember(true)}
207
- className="btn-primary flex items-center gap-2"
208
  >
209
  <Plus className="w-4 h-4" /> Add Worker
210
  </button>
211
  </div>
212
 
213
- {/* Add Member Form (Inline/Expandable) */}
214
  {showAddMember && (
215
- <div className="card bg-slate-50 border-slate-200 animate-in fade-in slide-in-from-top-4">
216
- <div className="flex justify-between items-center mb-6 pb-4 border-b border-slate-200">
217
- <h3 className="text-lg font-bold text-slate-800">
218
  Add New Member
219
  </h3>
220
  <button
@@ -234,7 +226,7 @@ export default function DepartmentDetailPage() {
234
  </label>
235
  <input
236
  required
237
- className="input w-full bg-white"
238
  placeholder="John Doe"
239
  value={newMember.name}
240
  onChange={(e) =>
@@ -249,7 +241,7 @@ export default function DepartmentDetailPage() {
249
  <input
250
  required
251
  type="email"
252
- className="input w-full bg-white"
253
  placeholder="john@city.gov"
254
  value={newMember.email}
255
  onChange={(e) =>
@@ -262,7 +254,7 @@ export default function DepartmentDetailPage() {
262
  Role
263
  </label>
264
  <select
265
- className="input w-full bg-white"
266
  value={newMember.role}
267
  onChange={(e) =>
268
  setNewMember({ ...newMember, role: e.target.value })
@@ -280,7 +272,7 @@ export default function DepartmentDetailPage() {
280
  <input
281
  required
282
  type="password"
283
- className="input w-full bg-white"
284
  placeholder="••••••••"
285
  value={newMember.password}
286
  onChange={(e) =>
@@ -293,7 +285,7 @@ export default function DepartmentDetailPage() {
293
  Phone (Optional)
294
  </label>
295
  <input
296
- className="input w-full bg-white"
297
  placeholder="+1 234..."
298
  value={newMember.phone}
299
  onChange={(e) =>
@@ -306,7 +298,7 @@ export default function DepartmentDetailPage() {
306
  City (Optional)
307
  </label>
308
  <input
309
- className="input w-full bg-white"
310
  placeholder="East District"
311
  value={newMember.city}
312
  onChange={(e) =>
@@ -318,11 +310,14 @@ export default function DepartmentDetailPage() {
318
  <button
319
  type="button"
320
  onClick={() => setShowAddMember(false)}
321
- className="btn-secondary"
322
  >
323
  Cancel
324
  </button>
325
- <button type="submit" className="btn-primary">
 
 
 
326
  Create Member Account
327
  </button>
328
  </div>
@@ -330,10 +325,9 @@ export default function DepartmentDetailPage() {
330
  </div>
331
  )}
332
 
333
- {/* Members List */}
334
- <div className="card p-0 overflow-hidden">
335
  <table className="w-full text-left">
336
- <thead className="bg-slate-50 border-b border-slate-200">
337
  <tr>
338
  <th className="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">
339
  Name / Email
@@ -356,7 +350,7 @@ export default function DepartmentDetailPage() {
356
  {members.map((member) => (
357
  <tr
358
  key={member.id}
359
- className="hover:bg-slate-50/50 transition-colors"
360
  >
361
  <td className="px-6 py-4">
362
  <div className="flex items-center gap-3">
@@ -403,7 +397,7 @@ export default function DepartmentDetailPage() {
403
  <div className="flex items-center gap-2">
404
  <div className="w-24 h-2 bg-slate-100 rounded-full overflow-hidden">
405
  <div
406
- className={`h-full rounded-full ${member.current_workload > 5 ? "bg-orange-500" : "bg-green-500"}`}
407
  style={{
408
  width: `${(member.current_workload / (member.max_workload || 10)) * 100}%`,
409
  }}
 
48
  const [loading, setLoading] = useState(true);
49
  const [showAddMember, setShowAddMember] = useState(false);
50
 
 
51
  const [newMember, setNewMember] = useState({
52
  name: "",
53
  email: "",
54
  role: "worker",
55
+ password: "",
56
  phone: "",
57
  city: "",
58
  max_workload: 10,
 
87
  await apiPost("/admin/members", {
88
  ...newMember,
89
  department_id: department.id,
90
+ locality: "General",
91
  });
92
  setShowAddMember(false);
93
  setNewMember({
 
99
  city: "",
100
  max_workload: 10,
101
  });
102
+ loadData(department.id);
103
  alert("Member added successfully!");
104
  } catch (error: any) {
105
  alert(error.message || "Failed to add member");
 
109
  const handleDeleteMember = async (memberId: string) => {
110
  if (!confirm("Are you sure you want to remove this member?")) return;
111
  try {
 
 
112
  const token = localStorage.getItem("supabase_token");
113
  await fetch(
114
  `${process.env.NEXT_PUBLIC_API_URL}/admin/members/${memberId}`,
 
125
 
126
  if (loading) {
127
  return (
128
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 text-center text-slate-500">
129
  Loading Department...
130
  </div>
131
  );
 
136
  }
137
 
138
  return (
139
+ <div className="max-w-7xl mx-auto space-y-8 px-4 sm:px-6 lg:px-8 py-8">
 
140
  <div>
141
  <Link
142
  href="/admin/departments"
 
144
  >
145
  <ArrowLeft className="w-4 h-4" /> Back to Departments
146
  </Link>
147
+ <div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4">
148
  <div>
149
  <div className="flex items-center gap-3 mb-1">
150
+ <h1 className="text-3xl font-black text-slate-900">
151
  {department.name}
152
  </h1>
153
  <span className="bg-slate-100 text-slate-600 px-2 py-1 rounded text-sm font-mono font-bold border border-slate-200">
 
171
  </div>
172
  </div>
173
 
 
174
  <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
175
+ <div className="bg-white/80 backdrop-blur-md rounded-2xl border border-slate-200/70 shadow-urban-sm p-6">
176
  <div className="flex justify-between items-center">
177
  <div>
178
  <p className="text-sm font-bold text-slate-400 uppercase tracking-wider">
 
182
  {members.length}
183
  </h3>
184
  </div>
185
+ <div className="h-10 w-10 bg-urban-primary/10 rounded-lg flex items-center justify-center text-urban-primary">
186
  <Users className="w-6 h-6" />
187
  </div>
188
  </div>
189
  </div>
 
190
  </div>
191
 
 
192
  <div className="space-y-6">
193
  <div className="flex justify-between items-center">
194
+ <h2 className="text-xl font-black text-slate-900 flex items-center gap-2">
195
  <Users className="w-5 h-5 text-slate-400" />
196
  Department Members
197
  </h2>
198
  <button
199
  onClick={() => setShowAddMember(true)}
200
+ className="px-4 py-2 bg-urban-primary text-white font-semibold rounded-xl hover:bg-emerald-600 transition flex items-center gap-2"
201
  >
202
  <Plus className="w-4 h-4" /> Add Worker
203
  </button>
204
  </div>
205
 
 
206
  {showAddMember && (
207
+ <div className="bg-white/80 backdrop-blur-md rounded-2xl border border-slate-200/70 shadow-urban-sm p-6">
208
+ <div className="flex justify-between items-center mb-6 pb-4 border-b border-slate-200/70">
209
+ <h3 className="text-lg font-black text-slate-800">
210
  Add New Member
211
  </h3>
212
  <button
 
226
  </label>
227
  <input
228
  required
229
+ className="w-full px-4 py-2.5 bg-white/70 border border-slate-300 rounded-xl text-slate-900 focus:ring-4 focus:ring-urban-primary/10 focus:border-urban-primary/40 outline-none"
230
  placeholder="John Doe"
231
  value={newMember.name}
232
  onChange={(e) =>
 
241
  <input
242
  required
243
  type="email"
244
+ className="w-full px-4 py-2.5 bg-white/70 border border-slate-300 rounded-xl text-slate-900 focus:ring-4 focus:ring-urban-primary/10 focus:border-urban-primary/40 outline-none"
245
  placeholder="john@city.gov"
246
  value={newMember.email}
247
  onChange={(e) =>
 
254
  Role
255
  </label>
256
  <select
257
+ className="w-full px-4 py-2.5 bg-white/70 border border-slate-300 rounded-xl text-slate-900 focus:ring-4 focus:ring-urban-primary/10 focus:border-urban-primary/40 outline-none"
258
  value={newMember.role}
259
  onChange={(e) =>
260
  setNewMember({ ...newMember, role: e.target.value })
 
272
  <input
273
  required
274
  type="password"
275
+ className="w-full px-4 py-2.5 bg-white/70 border border-slate-300 rounded-xl text-slate-900 focus:ring-4 focus:ring-urban-primary/10 focus:border-urban-primary/40 outline-none"
276
  placeholder="••••••••"
277
  value={newMember.password}
278
  onChange={(e) =>
 
285
  Phone (Optional)
286
  </label>
287
  <input
288
+ className="w-full px-4 py-2.5 bg-white/70 border border-slate-300 rounded-xl text-slate-900 focus:ring-4 focus:ring-urban-primary/10 focus:border-urban-primary/40 outline-none"
289
  placeholder="+1 234..."
290
  value={newMember.phone}
291
  onChange={(e) =>
 
298
  City (Optional)
299
  </label>
300
  <input
301
+ className="w-full px-4 py-2.5 bg-white/70 border border-slate-300 rounded-xl text-slate-900 focus:ring-4 focus:ring-urban-primary/10 focus:border-urban-primary/40 outline-none"
302
  placeholder="East District"
303
  value={newMember.city}
304
  onChange={(e) =>
 
310
  <button
311
  type="button"
312
  onClick={() => setShowAddMember(false)}
313
+ className="px-5 py-2.5 bg-white/80 text-slate-700 font-semibold rounded-xl border border-slate-300 hover:bg-slate-50 transition"
314
  >
315
  Cancel
316
  </button>
317
+ <button
318
+ type="submit"
319
+ className="px-5 py-2.5 bg-urban-primary text-white font-semibold rounded-xl hover:bg-emerald-600 transition"
320
+ >
321
  Create Member Account
322
  </button>
323
  </div>
 
325
  </div>
326
  )}
327
 
328
+ <div className="bg-white/80 backdrop-blur-md rounded-2xl border border-slate-200/70 shadow-urban-sm p-0 overflow-hidden">
 
329
  <table className="w-full text-left">
330
+ <thead className="bg-slate-50/80 border-b border-slate-200/70">
331
  <tr>
332
  <th className="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">
333
  Name / Email
 
350
  {members.map((member) => (
351
  <tr
352
  key={member.id}
353
+ className="hover:bg-urban-primary/5 transition-colors"
354
  >
355
  <td className="px-6 py-4">
356
  <div className="flex items-center gap-3">
 
397
  <div className="flex items-center gap-2">
398
  <div className="w-24 h-2 bg-slate-100 rounded-full overflow-hidden">
399
  <div
400
+ className={`h-full rounded-full ${member.current_workload > 5 ? "bg-amber-500" : "bg-urban-primary"}`}
401
  style={{
402
  width: `${(member.current_workload / (member.max_workload || 10)) * 100}%`,
403
  }}
Frontend/app/admin/departments/page.tsx CHANGED
@@ -1,6 +1,7 @@
1
  "use client";
2
- import { useEffect, useState } from "react";
3
- import { apiGet, apiPost } from "@/lib/api";
 
4
  import { Building2, Plus, Search } from "lucide-react";
5
 
6
  interface Department {
@@ -14,8 +15,6 @@ interface Department {
14
  }
15
 
16
  export default function DepartmentsPage() {
17
- const [departments, setDepartments] = useState<Department[]>([]);
18
- const [loading, setLoading] = useState(true);
19
  const [showForm, setShowForm] = useState(false);
20
  const [formData, setFormData] = useState({
21
  name: "",
@@ -23,21 +22,9 @@ export default function DepartmentsPage() {
23
  description: "",
24
  default_sla_hours: 48,
25
  });
26
-
27
- useEffect(() => {
28
- fetchDepartments();
29
- }, []);
30
-
31
- const fetchDepartments = async () => {
32
- try {
33
- const data = await apiGet<Department[]>("/admin/departments");
34
- setDepartments(data);
35
- } catch (error) {
36
- console.error("Failed to fetch departments:", error);
37
- } finally {
38
- setLoading(false);
39
- }
40
- };
41
 
42
  const handleSubmit = async (e: React.FormEvent) => {
43
  e.preventDefault();
@@ -50,7 +37,7 @@ export default function DepartmentsPage() {
50
  description: "",
51
  default_sla_hours: 48,
52
  });
53
- fetchDepartments();
54
  } catch (error: unknown) {
55
  const message =
56
  error instanceof Error ? error.message : "Failed to create department";
@@ -60,31 +47,33 @@ export default function DepartmentsPage() {
60
 
61
  if (loading) {
62
  return (
63
- <div className="text-slate-600 font-medium">Loading Departments...</div>
 
 
64
  );
65
  }
66
 
67
  return (
68
- <div className="space-y-6">
69
  <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
70
  <div>
71
- <h2 className="text-2xl font-bold text-slate-900">Departments</h2>
72
- <p className="text-sm text-slate-500">
73
  Organizational units and SLA configurations.
74
  </p>
75
  </div>
76
  <button
77
  onClick={() => setShowForm(true)}
78
- className="px-4 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition shadow-sm flex items-center gap-2"
79
  >
80
  <Plus className="w-4 h-4" /> New Department
81
  </button>
82
  </div>
83
 
84
  {showForm && (
85
- <div className="bg-white rounded-xl shadow-lg border border-slate-200 overflow-hidden animate-in fade-in slide-in-from-top-4">
86
- <div className="bg-slate-50 px-6 py-4 border-b border-slate-200">
87
- <h2 className="text-lg font-bold text-slate-800">
88
  Create New Department
89
  </h2>
90
  </div>
@@ -100,7 +89,7 @@ export default function DepartmentsPage() {
100
  onChange={(e) =>
101
  setFormData({ ...formData, name: e.target.value })
102
  }
103
- className="w-full px-4 py-2 bg-white border border-slate-300 rounded-lg text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
104
  placeholder="e.g., Public Works Department"
105
  required
106
  />
@@ -118,7 +107,7 @@ export default function DepartmentsPage() {
118
  code: e.target.value.toUpperCase(),
119
  })
120
  }
121
- className="w-full px-4 py-2 bg-white border border-slate-300 rounded-lg text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
122
  placeholder="e.g., PWD"
123
  required
124
  />
@@ -138,7 +127,7 @@ export default function DepartmentsPage() {
138
  onChange={(e) =>
139
  setFormData({ ...formData, description: e.target.value })
140
  }
141
- className="w-full px-4 py-2 bg-white border border-slate-300 rounded-lg text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
142
  rows={2}
143
  placeholder="Brief description of responsibilities..."
144
  />
@@ -161,21 +150,21 @@ export default function DepartmentsPage() {
161
  default_sla_hours: parseInt(e.target.value),
162
  })
163
  }
164
- className="w-32 px-4 py-2 bg-white border border-slate-300 rounded-lg text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
165
  />
166
  </div>
167
 
168
  <div className="flex gap-3 pt-2">
169
  <button
170
  type="submit"
171
- className="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition shadow-sm"
172
  >
173
  Create Department
174
  </button>
175
  <button
176
  type="button"
177
  onClick={() => setShowForm(false)}
178
- className="px-6 py-2 bg-white text-slate-700 font-medium rounded-lg border border-slate-300 hover:bg-slate-50 transition"
179
  >
180
  Cancel
181
  </button>
@@ -186,7 +175,7 @@ export default function DepartmentsPage() {
186
 
187
  <div className="space-y-4">
188
  {departments.length === 0 ? (
189
- <div className="text-center py-16 bg-white rounded-xl border border-slate-200">
190
  <Building2 className="w-12 h-12 mx-auto text-slate-300" />
191
  <p className="text-slate-500 mt-4 text-lg">No departments found.</p>
192
  <p className="text-slate-400 text-sm">
@@ -194,9 +183,9 @@ export default function DepartmentsPage() {
194
  </p>
195
  </div>
196
  ) : (
197
- <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
198
  <table className="min-w-full divide-y divide-slate-200">
199
- <thead className="bg-slate-50">
200
  <tr>
201
  <th className="px-6 py-4 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">
202
  Department
@@ -214,10 +203,13 @@ export default function DepartmentsPage() {
214
  </thead>
215
  <tbody className="divide-y divide-slate-200">
216
  {departments.map((dept) => (
217
- <tr key={dept.id} className="hover:bg-slate-50 transition">
 
 
 
218
  <td className="px-6 py-4 whitespace-nowrap">
219
  <div className="flex items-center">
220
- <div className="shrink-0 h-10 w-10 rounded-lg bg-blue-100 text-blue-600 flex items-center justify-center font-bold text-sm">
221
  {dept.code}
222
  </div>
223
  <div className="ml-4">
@@ -242,7 +234,7 @@ export default function DepartmentsPage() {
242
  <span
243
  className={`px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${
244
  dept.is_active
245
- ? "bg-green-100 text-green-800"
246
  : "bg-red-100 text-red-800"
247
  }`}
248
  >
 
1
  "use client";
2
+ import { useState } from "react";
3
+ import { apiPost } from "@/lib/api";
4
+ import { useCachedFetch } from "@/hooks/useCachedFetch";
5
  import { Building2, Plus, Search } from "lucide-react";
6
 
7
  interface Department {
 
15
  }
16
 
17
  export default function DepartmentsPage() {
 
 
18
  const [showForm, setShowForm] = useState(false);
19
  const [formData, setFormData] = useState({
20
  name: "",
 
22
  description: "",
23
  default_sla_hours: 48,
24
  });
25
+ const { data, loading, revalidate } =
26
+ useCachedFetch<Department[]>("/admin/departments");
27
+ const departments = data || [];
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
  const handleSubmit = async (e: React.FormEvent) => {
30
  e.preventDefault();
 
37
  description: "",
38
  default_sla_hours: 48,
39
  });
40
+ revalidate();
41
  } catch (error: unknown) {
42
  const message =
43
  error instanceof Error ? error.message : "Failed to create department";
 
47
 
48
  if (loading) {
49
  return (
50
+ <div className="text-slate-600 font-medium max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
51
+ Loading Departments...
52
+ </div>
53
  );
54
  }
55
 
56
  return (
57
+ <div className="space-y-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
58
  <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
59
  <div>
60
+ <h2 className="text-2xl font-black text-slate-900">Departments</h2>
61
+ <p className="text-sm text-slate-500 font-medium">
62
  Organizational units and SLA configurations.
63
  </p>
64
  </div>
65
  <button
66
  onClick={() => setShowForm(true)}
67
+ className="px-4 py-2 bg-urban-primary text-white font-semibold rounded-xl hover:bg-emerald-600 transition shadow-sm flex items-center gap-2"
68
  >
69
  <Plus className="w-4 h-4" /> New Department
70
  </button>
71
  </div>
72
 
73
  {showForm && (
74
+ <div className="bg-white/80 backdrop-blur-md rounded-2xl shadow-urban-md border border-slate-200/70 overflow-hidden">
75
+ <div className="bg-slate-50/80 px-6 py-4 border-b border-slate-200/70">
76
+ <h2 className="text-lg font-black text-slate-800">
77
  Create New Department
78
  </h2>
79
  </div>
 
89
  onChange={(e) =>
90
  setFormData({ ...formData, name: e.target.value })
91
  }
92
+ className="w-full px-4 py-2.5 bg-white/70 border border-slate-300 rounded-xl text-slate-900 focus:ring-4 focus:ring-urban-primary/10 focus:border-urban-primary/40 outline-none"
93
  placeholder="e.g., Public Works Department"
94
  required
95
  />
 
107
  code: e.target.value.toUpperCase(),
108
  })
109
  }
110
+ className="w-full px-4 py-2.5 bg-white/70 border border-slate-300 rounded-xl text-slate-900 focus:ring-4 focus:ring-urban-primary/10 focus:border-urban-primary/40 outline-none"
111
  placeholder="e.g., PWD"
112
  required
113
  />
 
127
  onChange={(e) =>
128
  setFormData({ ...formData, description: e.target.value })
129
  }
130
+ className="w-full px-4 py-2.5 bg-white/70 border border-slate-300 rounded-xl text-slate-900 focus:ring-4 focus:ring-urban-primary/10 focus:border-urban-primary/40 outline-none"
131
  rows={2}
132
  placeholder="Brief description of responsibilities..."
133
  />
 
150
  default_sla_hours: parseInt(e.target.value),
151
  })
152
  }
153
+ className="w-32 px-4 py-2.5 bg-white/70 border border-slate-300 rounded-xl text-slate-900 focus:ring-4 focus:ring-urban-primary/10 focus:border-urban-primary/40 outline-none"
154
  />
155
  </div>
156
 
157
  <div className="flex gap-3 pt-2">
158
  <button
159
  type="submit"
160
+ className="px-6 py-2.5 bg-urban-primary text-white font-semibold rounded-xl hover:bg-emerald-600 transition shadow-sm"
161
  >
162
  Create Department
163
  </button>
164
  <button
165
  type="button"
166
  onClick={() => setShowForm(false)}
167
+ className="px-6 py-2.5 bg-white/80 text-slate-700 font-semibold rounded-xl border border-slate-300 hover:bg-slate-50 transition"
168
  >
169
  Cancel
170
  </button>
 
175
 
176
  <div className="space-y-4">
177
  {departments.length === 0 ? (
178
+ <div className="text-center py-16 bg-white/70 backdrop-blur-md rounded-2xl border border-slate-200/70">
179
  <Building2 className="w-12 h-12 mx-auto text-slate-300" />
180
  <p className="text-slate-500 mt-4 text-lg">No departments found.</p>
181
  <p className="text-slate-400 text-sm">
 
183
  </p>
184
  </div>
185
  ) : (
186
+ <div className="bg-white/70 backdrop-blur-md rounded-2xl shadow-urban-sm border border-slate-200/70 overflow-hidden">
187
  <table className="min-w-full divide-y divide-slate-200">
188
+ <thead className="bg-slate-50/80">
189
  <tr>
190
  <th className="px-6 py-4 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">
191
  Department
 
203
  </thead>
204
  <tbody className="divide-y divide-slate-200">
205
  {departments.map((dept) => (
206
+ <tr
207
+ key={dept.id}
208
+ className="hover:bg-urban-primary/5 transition"
209
+ >
210
  <td className="px-6 py-4 whitespace-nowrap">
211
  <div className="flex items-center">
212
+ <div className="shrink-0 h-10 w-10 rounded-xl bg-urban-primary/10 text-urban-primary flex items-center justify-center font-bold text-sm">
213
  {dept.code}
214
  </div>
215
  <div className="ml-4">
 
234
  <span
235
  className={`px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${
236
  dept.is_active
237
+ ? "bg-emerald-100 text-emerald-800"
238
  : "bg-red-100 text-red-800"
239
  }`}
240
  >
Frontend/app/admin/heatmap/page.tsx CHANGED
@@ -1,6 +1,6 @@
1
  "use client";
2
- import { useEffect, useState } from "react";
3
- import { apiGet } from "@/lib/api";
4
  import { Map } from "lucide-react";
5
 
6
  interface HeatmapData {
@@ -10,23 +10,10 @@ interface HeatmapData {
10
  }
11
 
12
  export default function HeatmapPage() {
13
- const [data, setData] = useState<HeatmapData[]>([]);
14
- const [loading, setLoading] = useState(true);
15
-
16
- useEffect(() => {
17
- fetchHeatmap();
18
- }, []);
19
-
20
- const fetchHeatmap = async () => {
21
- try {
22
- const heatmapData = await apiGet<HeatmapData[]>("/admin/stats/heatmap");
23
- setData(heatmapData);
24
- } catch (error) {
25
- console.error("Failed to fetch heatmap:", error);
26
- } finally {
27
- setLoading(false);
28
- }
29
- };
30
 
31
  const getIntensityColor = (count: number, max: number) => {
32
  const intensity = count / max;
@@ -37,61 +24,87 @@ export default function HeatmapPage() {
37
  };
38
 
39
  if (loading) {
40
- return <div className="text-slate-600 font-medium">Loading Analytics...</div>;
 
 
 
 
41
  }
42
 
43
- const maxCount = Math.max(...data.map((d) => d.count), 1);
44
 
45
  return (
46
- <div className="space-y-6">
47
- <div>
48
- <h2 className="text-2xl font-bold text-slate-900">Geographic Heatmap</h2>
49
- <p className="text-sm text-slate-500">Distribution of issues across city districts.</p>
50
- </div>
 
 
 
 
51
 
52
- <div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm mb-8">
53
- <div className="flex items-center justify-between">
54
- <h2 className="text-lg font-bold text-slate-900">Issue Density by City</h2>
55
- <div className="flex gap-4 text-xs font-semibold uppercase text-slate-500">
56
- <div className="flex items-center gap-2"><div className="w-3 h-3 rounded-full bg-emerald-500"></div> Low</div>
57
- <div className="flex items-center gap-2"><div className="w-3 h-3 rounded-full bg-amber-400"></div> Medium</div>
58
- <div className="flex items-center gap-2"><div className="w-3 h-3 rounded-full bg-orange-500"></div> High</div>
59
- <div className="flex items-center gap-2"><div className="w-3 h-3 rounded-full bg-red-600"></div> Critical</div>
60
  </div>
61
- </div>
 
 
 
 
 
 
 
 
 
 
62
  </div>
63
 
64
- {data.length === 0 ? (
65
- <div className="text-center py-16 bg-white rounded-xl border border-slate-200">
66
- <Map className="w-12 h-12 mx-auto text-slate-300" />
67
  <p className="text-slate-500 mt-2">No location data available yet.</p>
68
  </div>
69
  ) : (
70
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
71
- {data.map((item) => (
72
  <div
73
  key={item.city}
74
- className="group relative bg-white overflow-hidden rounded-xl border border-slate-200 shadow-sm hover:shadow-md transition-all"
75
  >
76
  <div className="absolute top-0 left-0 w-full h-1.5 bg-slate-100">
77
- <div
78
- className={`h-full ${getIntensityColor(item.count, maxCount)}`}
79
- style={{ width: `${(item.count / maxCount) * 100}%` }}
80
- ></div>
81
  </div>
82
 
83
  <div className="p-6">
84
- <div className="flex justify-between items-start mb-4">
85
- <h3 className="text-lg font-bold text-slate-900">{item.city}</h3>
86
- <span className={`w-8 h-8 flex items-center justify-center rounded-lg text-white font-bold text-sm ${getIntensityColor(item.count, maxCount)}`}>
87
- {Math.round((item.count / maxCount) * 10)}
88
- </span>
89
- </div>
90
-
91
- <div className="flex items-baseline gap-1">
92
- <span className="text-3xl font-extrabold text-slate-900">{item.count}</span>
93
- <span className="text-sm font-medium text-slate-500">issues</span>
94
- </div>
 
 
 
 
 
 
 
 
95
  </div>
96
  </div>
97
  ))}
 
1
  "use client";
2
+ import { useMemo } from "react";
3
+ import { useCachedFetch } from "@/hooks/useCachedFetch";
4
  import { Map } from "lucide-react";
5
 
6
  interface HeatmapData {
 
10
  }
11
 
12
  export default function HeatmapPage() {
13
+ const { data, loading } = useCachedFetch<HeatmapData[]>(
14
+ "/admin/stats/heatmap",
15
+ );
16
+ const heatmapData = useMemo(() => data || [], [data]);
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  const getIntensityColor = (count: number, max: number) => {
19
  const intensity = count / max;
 
24
  };
25
 
26
  if (loading) {
27
+ return (
28
+ <div className="text-slate-600 font-medium max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
29
+ Loading Analytics...
30
+ </div>
31
+ );
32
  }
33
 
34
+ const maxCount = Math.max(...heatmapData.map((d) => d.count), 1);
35
 
36
  return (
37
+ <div className="space-y-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
38
+ <div>
39
+ <h2 className="text-2xl font-black text-slate-900">
40
+ Geographic Heatmap
41
+ </h2>
42
+ <p className="text-sm text-slate-500 font-medium">
43
+ Distribution of issues across city districts.
44
+ </p>
45
+ </div>
46
 
47
+ <div className="bg-white/70 backdrop-blur-md p-6 rounded-2xl border border-slate-200/70 shadow-urban-sm">
48
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
49
+ <h2 className="text-lg font-black text-slate-900">
50
+ Issue Density by City
51
+ </h2>
52
+ <div className="flex flex-wrap gap-4 text-xs font-semibold uppercase text-slate-500">
53
+ <div className="flex items-center gap-2">
54
+ <div className="w-3 h-3 rounded-full bg-emerald-500"></div> Low
55
  </div>
56
+ <div className="flex items-center gap-2">
57
+ <div className="w-3 h-3 rounded-full bg-amber-400"></div> Medium
58
+ </div>
59
+ <div className="flex items-center gap-2">
60
+ <div className="w-3 h-3 rounded-full bg-orange-500"></div> High
61
+ </div>
62
+ <div className="flex items-center gap-2">
63
+ <div className="w-3 h-3 rounded-full bg-red-600"></div> Critical
64
+ </div>
65
+ </div>
66
+ </div>
67
  </div>
68
 
69
+ {heatmapData.length === 0 ? (
70
+ <div className="text-center py-16 bg-white/70 backdrop-blur-md rounded-2xl border border-slate-200/70">
71
+ <Map className="w-12 h-12 mx-auto text-slate-300" />
72
  <p className="text-slate-500 mt-2">No location data available yet.</p>
73
  </div>
74
  ) : (
75
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
76
+ {heatmapData.map((item) => (
77
  <div
78
  key={item.city}
79
+ className="group relative bg-white/80 backdrop-blur-md overflow-hidden rounded-2xl border border-slate-200/70 shadow-urban-sm hover:shadow-urban-md transition-all"
80
  >
81
  <div className="absolute top-0 left-0 w-full h-1.5 bg-slate-100">
82
+ <div
83
+ className={`h-full ${getIntensityColor(item.count, maxCount)}`}
84
+ style={{ width: `${(item.count / maxCount) * 100}%` }}
85
+ ></div>
86
  </div>
87
 
88
  <div className="p-6">
89
+ <div className="flex justify-between items-start mb-4">
90
+ <h3 className="text-lg font-bold text-slate-900">
91
+ {item.city}
92
+ </h3>
93
+ <span
94
+ className={`w-8 h-8 flex items-center justify-center rounded-lg text-white font-bold text-sm ${getIntensityColor(item.count, maxCount)}`}
95
+ >
96
+ {Math.round((item.count / maxCount) * 10)}
97
+ </span>
98
+ </div>
99
+
100
+ <div className="flex items-baseline gap-1">
101
+ <span className="text-3xl font-extrabold text-slate-900">
102
+ {item.count}
103
+ </span>
104
+ <span className="text-sm font-medium text-slate-500">
105
+ issues
106
+ </span>
107
+ </div>
108
  </div>
109
  </div>
110
  ))}
Frontend/app/admin/issues/[id]/page.tsx CHANGED
@@ -194,15 +194,21 @@ export default function IssueDetailPage() {
194
 
195
  if (loading)
196
  return (
197
- <div className="p-8 text-center text-slate-500">Loading details...</div>
 
 
198
  );
199
  if (!data)
200
- return <div className="p-8 text-center text-red-500">Issue not found</div>;
 
 
 
 
201
 
202
  const { issue, department, worker, events } = data;
203
 
204
  return (
205
- <div className="max-w-7xl mx-auto space-y-6">
206
  <div className="flex items-center justify-between">
207
  <div className="flex items-center gap-4">
208
  <Link
@@ -212,7 +218,7 @@ export default function IssueDetailPage() {
212
  <ArrowLeft className="w-5 h-5" />
213
  </Link>
214
  <div>
215
- <h1 className="text-2xl font-bold text-slate-900 flex items-center gap-3">
216
  Issue #{issue.id.slice(0, 8)}
217
  <span
218
  className={`text-sm px-2.5 py-0.5 rounded-full border font-medium uppercase tracking-wide
@@ -240,14 +246,14 @@ export default function IssueDetailPage() {
240
  <button
241
  onClick={() => handleReview("rejected")}
242
  disabled={actionLoading}
243
- className="px-4 py-2 text-sm font-medium text-red-600 bg-white border border-red-200 rounded-lg hover:bg-red-50"
244
  >
245
  Reject & Close
246
  </button>
247
  <button
248
  onClick={() => handleReview("approved")}
249
  disabled={actionLoading}
250
- className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700"
251
  >
252
  Approve & Assign
253
  </button>
@@ -259,14 +265,14 @@ export default function IssueDetailPage() {
259
  <button
260
  onClick={() => handleResolutionReview("reject")}
261
  disabled={actionLoading}
262
- className="px-4 py-2 text-sm font-medium text-red-600 bg-white border border-red-200 rounded-lg hover:bg-red-50"
263
  >
264
  Reject Incomplete Work
265
  </button>
266
  <button
267
  onClick={() => handleResolutionReview("approve")}
268
  disabled={actionLoading}
269
- className="px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700"
270
  >
271
  Verify & Approve
272
  </button>
@@ -284,8 +290,8 @@ export default function IssueDetailPage() {
284
 
285
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
286
  <div className="lg:col-span-2 space-y-6">
287
- <div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
288
- <h3 className="text-lg font-semibold text-slate-900 mb-4 flex items-center gap-2">
289
  <ImageIcon className="w-5 h-5 text-slate-400" />
290
  Evidence Photos
291
  </h3>
@@ -298,7 +304,7 @@ export default function IssueDetailPage() {
298
  {issue.image_urls.map((url, idx) => (
299
  <div
300
  key={idx}
301
- className="aspect-video bg-slate-100 rounded-lg overflow-hidden border border-slate-200 relative group"
302
  >
303
  <img
304
  src={url}
@@ -329,14 +335,14 @@ export default function IssueDetailPage() {
329
  {issue.annotated_urls.map((url, idx) => (
330
  <div
331
  key={idx}
332
- className="aspect-video bg-slate-100 rounded-lg overflow-hidden border border-blue-200 relative group"
333
  >
334
  <img
335
  src={url}
336
  alt={`Analyzed ${idx + 1}`}
337
  className="w-full h-full object-cover"
338
  />
339
- <div className="absolute top-2 left-2 bg-blue-500 text-white text-xs px-2 py-0.5 rounded-full font-semibold">
340
  AI Detected
341
  </div>
342
  <div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
@@ -361,13 +367,13 @@ export default function IssueDetailPage() {
361
  Work Completion Proof
362
  </h4>
363
  <div className="grid grid-cols-2 gap-4">
364
- <div className="aspect-video bg-slate-100 rounded-lg overflow-hidden border border-green-200 relative group">
365
  <img
366
  src={issue.proof_image_url}
367
  alt="Work Proof"
368
  className="w-full h-full object-cover"
369
  />
370
- <div className="absolute top-2 left-2 bg-green-500 text-white text-xs px-2 py-0.5 rounded-full font-semibold">
371
  Worker Proof
372
  </div>
373
  <div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
@@ -386,15 +392,15 @@ export default function IssueDetailPage() {
386
  )}
387
 
388
  {issue.image_urls.length === 0 && !issue.proof_image_url && (
389
- <div className="py-8 text-center text-slate-400 bg-slate-50 rounded-lg border border-dashed border-slate-300">
390
  No images attached
391
  </div>
392
  )}
393
  </div>
394
  </div>
395
 
396
- <div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
397
- <h3 className="text-lg font-semibold text-slate-900 mb-4 flex items-center gap-2">
398
  <Activity className="w-5 h-5 text-slate-400" />
399
  Analysis & Details
400
  </h3>
@@ -457,7 +463,7 @@ export default function IssueDetailPage() {
457
  </label>
458
  <button
459
  onClick={() => setEditingPriority(!editingPriority)}
460
- className="p-1 opacity-0 group-hover:opacity-100 hover:bg-slate-100 rounded text-slate-400 hover:text-blue-500 transition-all"
461
  aria-label="Edit Priority"
462
  >
463
  <Pencil className="w-3 h-3" />
@@ -472,7 +478,7 @@ export default function IssueDetailPage() {
472
  onChange={(e) =>
473
  setSelectedPriority(Number(e.target.value))
474
  }
475
- className="bg-white border rounded px-2 py-1 text-sm flex-1"
476
  >
477
  <option value="1">P1 - Critical</option>
478
  <option value="2">P2 - High</option>
@@ -483,7 +489,7 @@ export default function IssueDetailPage() {
483
  onClick={() =>
484
  handleUpdate({ priority: selectedPriority })
485
  }
486
- className="p-1.5 bg-blue-100 text-blue-600 rounded hover:bg-blue-200"
487
  aria-label="Save"
488
  >
489
  <Save className="w-4 h-4" />
@@ -519,14 +525,14 @@ export default function IssueDetailPage() {
519
  </div>
520
 
521
  <div className="space-y-6">
522
- <div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
523
- <h3 className="text-sm font-bold text-slate-900 uppercase tracking-wide mb-4">
524
  Assignment
525
  </h3>
526
 
527
  <div className="space-y-4">
528
  <div className="flex items-start gap-3">
529
- <div className="w-9 h-9 rounded-full bg-blue-100 flex items-center justify-center text-blue-600">
530
  <Building2 className="w-5 h-5" />
531
  </div>
532
  <div>
@@ -541,7 +547,7 @@ export default function IssueDetailPage() {
541
  <div className="absolute right-0 top-0 opacity-0 group-hover:opacity-100 transition-opacity">
542
  <button
543
  onClick={() => setEditingAssignment(!editingAssignment)}
544
- className="p-1 hover:bg-slate-100 rounded text-slate-400 hover:text-blue-500"
545
  aria-label="Edit Assignment"
546
  >
547
  <Pencil className="w-3 h-3" />
@@ -559,7 +565,7 @@ export default function IssueDetailPage() {
559
  aria-label="Select worker"
560
  value={selectedWorker}
561
  onChange={(e) => setSelectedWorker(e.target.value)}
562
- className="bg-white border rounded px-2 py-1 text-sm w-full"
563
  >
564
  <option value="">Unassigned</option>
565
  {workers.map((w) => (
@@ -575,13 +581,13 @@ export default function IssueDetailPage() {
575
  assigned_member_id: selectedWorker || null,
576
  })
577
  }
578
- className="flex-1 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700"
579
  >
580
  Save
581
  </button>
582
  <button
583
  onClick={() => setEditingAssignment(false)}
584
- className="flex-1 py-1 bg-slate-200 text-slate-700 text-xs rounded hover:bg-slate-300"
585
  >
586
  Cancel
587
  </button>
@@ -604,7 +610,7 @@ export default function IssueDetailPage() {
604
 
605
  {issue.sla_deadline && (
606
  <div className="pt-4 border-t border-slate-100">
607
- <div className="flex items-center gap-2 text-amber-600 bg-amber-50 px-3 py-2 rounded-lg">
608
  <Clock className="w-4 h-4" />
609
  <div>
610
  <div className="text-[10px] font-bold uppercase">
@@ -620,8 +626,8 @@ export default function IssueDetailPage() {
620
  </div>
621
  </div>
622
 
623
- <div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm relative overflow-hidden">
624
- <h3 className="text-sm font-bold text-slate-900 uppercase tracking-wide mb-6">
625
  Activity Audit
626
  </h3>
627
 
@@ -632,7 +638,7 @@ export default function IssueDetailPage() {
632
  <div className="space-y-0 text-sm relative border-l-2 border-slate-100 ml-2">
633
  {events.map((event, idx) => (
634
  <div key={idx} className="pl-6 pb-6 relative last:pb-0">
635
- <div className="absolute -left-2.25 top-0 w-4 h-4 rounded-full bg-white border-2 border-blue-500"></div>
636
  <div className="flex flex-col">
637
  <span className="font-semibold text-slate-900">
638
  {event.agent || "System"}
 
194
 
195
  if (loading)
196
  return (
197
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 text-center text-slate-500">
198
+ Loading details...
199
+ </div>
200
  );
201
  if (!data)
202
+ return (
203
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 text-center text-red-500">
204
+ Issue not found
205
+ </div>
206
+ );
207
 
208
  const { issue, department, worker, events } = data;
209
 
210
  return (
211
+ <div className="max-w-7xl mx-auto space-y-8 px-4 sm:px-6 lg:px-8 py-8">
212
  <div className="flex items-center justify-between">
213
  <div className="flex items-center gap-4">
214
  <Link
 
218
  <ArrowLeft className="w-5 h-5" />
219
  </Link>
220
  <div>
221
+ <h1 className="text-2xl font-black text-slate-900 flex items-center gap-3">
222
  Issue #{issue.id.slice(0, 8)}
223
  <span
224
  className={`text-sm px-2.5 py-0.5 rounded-full border font-medium uppercase tracking-wide
 
246
  <button
247
  onClick={() => handleReview("rejected")}
248
  disabled={actionLoading}
249
+ className="px-4 py-2 text-sm font-semibold text-red-600 bg-white border border-red-200 rounded-xl hover:bg-red-50"
250
  >
251
  Reject & Close
252
  </button>
253
  <button
254
  onClick={() => handleReview("approved")}
255
  disabled={actionLoading}
256
+ className="px-4 py-2 text-sm font-semibold text-white bg-urban-primary rounded-xl hover:bg-emerald-600"
257
  >
258
  Approve & Assign
259
  </button>
 
265
  <button
266
  onClick={() => handleResolutionReview("reject")}
267
  disabled={actionLoading}
268
+ className="px-4 py-2 text-sm font-semibold text-red-600 bg-white border border-red-200 rounded-xl hover:bg-red-50"
269
  >
270
  Reject Incomplete Work
271
  </button>
272
  <button
273
  onClick={() => handleResolutionReview("approve")}
274
  disabled={actionLoading}
275
+ className="px-4 py-2 text-sm font-semibold text-white bg-emerald-600 rounded-xl hover:bg-emerald-700"
276
  >
277
  Verify & Approve
278
  </button>
 
290
 
291
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
292
  <div className="lg:col-span-2 space-y-6">
293
+ <div className="bg-white/80 backdrop-blur-md p-6 rounded-3xl border border-slate-200/70 shadow-urban-sm">
294
+ <h3 className="text-lg font-black text-slate-900 mb-4 flex items-center gap-2">
295
  <ImageIcon className="w-5 h-5 text-slate-400" />
296
  Evidence Photos
297
  </h3>
 
304
  {issue.image_urls.map((url, idx) => (
305
  <div
306
  key={idx}
307
+ className="aspect-video bg-slate-100 rounded-2xl overflow-hidden border border-slate-200 relative group"
308
  >
309
  <img
310
  src={url}
 
335
  {issue.annotated_urls.map((url, idx) => (
336
  <div
337
  key={idx}
338
+ className="aspect-video bg-slate-100 rounded-2xl overflow-hidden border border-urban-primary/30 relative group"
339
  >
340
  <img
341
  src={url}
342
  alt={`Analyzed ${idx + 1}`}
343
  className="w-full h-full object-cover"
344
  />
345
+ <div className="absolute top-2 left-2 bg-urban-primary text-white text-xs px-2 py-0.5 rounded-full font-semibold">
346
  AI Detected
347
  </div>
348
  <div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
 
367
  Work Completion Proof
368
  </h4>
369
  <div className="grid grid-cols-2 gap-4">
370
+ <div className="aspect-video bg-slate-100 rounded-2xl overflow-hidden border border-emerald-200 relative group">
371
  <img
372
  src={issue.proof_image_url}
373
  alt="Work Proof"
374
  className="w-full h-full object-cover"
375
  />
376
+ <div className="absolute top-2 left-2 bg-emerald-500 text-white text-xs px-2 py-0.5 rounded-full font-semibold">
377
  Worker Proof
378
  </div>
379
  <div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
 
392
  )}
393
 
394
  {issue.image_urls.length === 0 && !issue.proof_image_url && (
395
+ <div className="py-8 text-center text-slate-400 bg-slate-50 rounded-2xl border border-dashed border-slate-300">
396
  No images attached
397
  </div>
398
  )}
399
  </div>
400
  </div>
401
 
402
+ <div className="bg-white/80 backdrop-blur-md p-6 rounded-3xl border border-slate-200/70 shadow-urban-sm">
403
+ <h3 className="text-lg font-black text-slate-900 mb-4 flex items-center gap-2">
404
  <Activity className="w-5 h-5 text-slate-400" />
405
  Analysis & Details
406
  </h3>
 
463
  </label>
464
  <button
465
  onClick={() => setEditingPriority(!editingPriority)}
466
+ className="p-1 opacity-0 group-hover:opacity-100 hover:bg-slate-100 rounded text-slate-400 hover:text-urban-primary transition-all"
467
  aria-label="Edit Priority"
468
  >
469
  <Pencil className="w-3 h-3" />
 
478
  onChange={(e) =>
479
  setSelectedPriority(Number(e.target.value))
480
  }
481
+ className="bg-white border border-slate-200 rounded-lg px-2 py-1 text-sm flex-1 focus:ring-4 focus:ring-urban-primary/10 focus:border-urban-primary/40"
482
  >
483
  <option value="1">P1 - Critical</option>
484
  <option value="2">P2 - High</option>
 
489
  onClick={() =>
490
  handleUpdate({ priority: selectedPriority })
491
  }
492
+ className="p-1.5 bg-urban-primary/10 text-urban-primary rounded hover:bg-urban-primary/20"
493
  aria-label="Save"
494
  >
495
  <Save className="w-4 h-4" />
 
525
  </div>
526
 
527
  <div className="space-y-6">
528
+ <div className="bg-white/80 backdrop-blur-md p-6 rounded-3xl border border-slate-200/70 shadow-urban-sm">
529
+ <h3 className="text-sm font-black text-slate-900 uppercase tracking-wide mb-4">
530
  Assignment
531
  </h3>
532
 
533
  <div className="space-y-4">
534
  <div className="flex items-start gap-3">
535
+ <div className="w-9 h-9 rounded-full bg-urban-primary/10 flex items-center justify-center text-urban-primary">
536
  <Building2 className="w-5 h-5" />
537
  </div>
538
  <div>
 
547
  <div className="absolute right-0 top-0 opacity-0 group-hover:opacity-100 transition-opacity">
548
  <button
549
  onClick={() => setEditingAssignment(!editingAssignment)}
550
+ className="p-1 hover:bg-slate-100 rounded text-slate-400 hover:text-urban-primary"
551
  aria-label="Edit Assignment"
552
  >
553
  <Pencil className="w-3 h-3" />
 
565
  aria-label="Select worker"
566
  value={selectedWorker}
567
  onChange={(e) => setSelectedWorker(e.target.value)}
568
+ className="bg-white border border-slate-200 rounded-lg px-2 py-1 text-sm w-full focus:ring-4 focus:ring-urban-primary/10 focus:border-urban-primary/40"
569
  >
570
  <option value="">Unassigned</option>
571
  {workers.map((w) => (
 
581
  assigned_member_id: selectedWorker || null,
582
  })
583
  }
584
+ className="flex-1 py-1 bg-urban-primary text-white text-xs rounded-lg hover:bg-emerald-600"
585
  >
586
  Save
587
  </button>
588
  <button
589
  onClick={() => setEditingAssignment(false)}
590
+ className="flex-1 py-1 bg-slate-200 text-slate-700 text-xs rounded-lg hover:bg-slate-300"
591
  >
592
  Cancel
593
  </button>
 
610
 
611
  {issue.sla_deadline && (
612
  <div className="pt-4 border-t border-slate-100">
613
+ <div className="flex items-center gap-2 text-amber-600 bg-amber-50 px-3 py-2 rounded-xl">
614
  <Clock className="w-4 h-4" />
615
  <div>
616
  <div className="text-[10px] font-bold uppercase">
 
626
  </div>
627
  </div>
628
 
629
+ <div className="bg-white/80 backdrop-blur-md p-6 rounded-3xl border border-slate-200/70 shadow-urban-sm relative overflow-hidden">
630
+ <h3 className="text-sm font-black text-slate-900 uppercase tracking-wide mb-6">
631
  Activity Audit
632
  </h3>
633
 
 
638
  <div className="space-y-0 text-sm relative border-l-2 border-slate-100 ml-2">
639
  {events.map((event, idx) => (
640
  <div key={idx} className="pl-6 pb-6 relative last:pb-0">
641
+ <div className="absolute -left-2.25 top-0 w-4 h-4 rounded-full bg-white border-2 border-urban-primary"></div>
642
  <div className="flex flex-col">
643
  <span className="font-semibold text-slate-900">
644
  {event.agent || "System"}
Frontend/app/admin/issues/page.tsx CHANGED
@@ -37,10 +37,10 @@ interface IssuesResponse {
37
  export default function IssuesPage() {
38
  const router = useRouter();
39
  const searchParams = useSearchParams();
40
-
41
  const [page, setPage] = useState(1);
42
  const limit = 10;
43
-
44
  const [search, setSearch] = useState("");
45
  const [status, setStatus] = useState<string>("");
46
  const [priority, setPriority] = useState<string>("");
@@ -79,7 +79,8 @@ export default function IssuesPage() {
79
  return `/admin/issues?${query.toString()}`;
80
  }, [page, limit, sort, order, debouncedSearch, status, priority]);
81
 
82
- const { data: issuesData, loading } = useCachedFetch<IssuesResponse>(queryUrl);
 
83
 
84
  const issues = issuesData?.items || [];
85
  const meta: Meta = {
@@ -119,31 +120,31 @@ export default function IssuesPage() {
119
  };
120
 
121
  return (
122
- <div className="space-y-6">
123
  <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
124
  <div>
125
- <h1 className="text-2xl font-bold text-slate-900 tracking-tight">
126
  Issue Management
127
  </h1>
128
- <p className="text-slate-500 text-sm">
129
  Monitor, assign, and resolve reported city issues.
130
  </p>
131
  </div>
132
  <div className="flex items-center gap-2">
133
  <button
134
  onClick={() => setStatus("pending_verification")}
135
- className="px-4 py-2 bg-orange-100 text-orange-800 text-sm font-medium rounded-lg hover:bg-orange-200 transition flex items-center gap-2 border border-orange-200"
136
  >
137
  <AlertCircle className="w-4 h-4" />
138
  Pending Reviews
139
  </button>
140
- <button className="bg-slate-900 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-slate-800 transition">
141
  Export CSV
142
  </button>
143
  </div>
144
  </div>
145
 
146
- <div className="bg-white/60 backdrop-blur-md rounded-2xl border border-slate-200/60 shadow-urban-sm p-4 transition-all">
147
  <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
148
  <div className="md:col-span-2 relative group">
149
  <Search className="absolute left-3 top-2.5 h-4 w-4 text-slate-400 group-focus-within:text-blue-500 transition-colors" />
@@ -152,7 +153,7 @@ export default function IssuesPage() {
152
  placeholder="Search by ID, description, or location..."
153
  value={search}
154
  onChange={(e) => setSearch(e.target.value)}
155
- className="w-full pl-10 pr-4 py-2 text-sm border border-slate-200 bg-white/50 rounded-xl focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500/50 transition-all font-sans"
156
  />
157
  </div>
158
 
@@ -160,7 +161,7 @@ export default function IssuesPage() {
160
  aria-label="Filter by Status"
161
  value={status}
162
  onChange={(e) => setStatus(e.target.value)}
163
- className="px-3 py-2 text-sm border border-slate-200 bg-white/50 rounded-xl focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500/50 transition-all font-sans"
164
  >
165
  <option value="">All Statuses</option>
166
  <option value="reported">Reported</option>
@@ -177,7 +178,7 @@ export default function IssuesPage() {
177
  aria-label="Filter by Priority"
178
  value={priority}
179
  onChange={(e) => setPriority(e.target.value)}
180
- className="px-3 py-2 text-sm border border-slate-200 bg-white/50 rounded-xl focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500/50 transition-all font-sans"
181
  >
182
  <option value="">All Priorities</option>
183
  <option value="1">Critical (P1)</option>
@@ -193,7 +194,7 @@ export default function IssuesPage() {
193
  Loading issues...
194
  </div>
195
  ) : (
196
- <div className="overflow-x-auto rounded-xl border border-slate-200/50">
197
  <table className="w-full text-left border-collapse">
198
  <thead>
199
  <tr className="border-b border-slate-200/60 text-xs uppercase text-slate-500 bg-slate-50/80 font-mono tracking-wider">
@@ -240,7 +241,7 @@ export default function IssuesPage() {
240
  issues.map((issue) => (
241
  <tr
242
  key={issue.id}
243
- className="group hover:bg-blue-50/30 transition-colors duration-200"
244
  >
245
  <td className="px-4 py-3">
246
  <div className="flex items-center gap-3">
@@ -258,10 +259,10 @@ export default function IssuesPage() {
258
  )}
259
  </div>
260
  <div>
261
- <div className="text-sm font-semibold text-slate-900 truncate max-w-50 font-display">
262
  {issue.category || "Uncategorized Issue"}
263
  </div>
264
- <div className="text-xs text-slate-500 truncate max-w-50 font-sans">
265
  {issue.description || "No description provided"}
266
  </div>
267
  </div>
@@ -301,7 +302,9 @@ export default function IssuesPage() {
301
  <div className="w-6 h-6 rounded-full bg-blue-100 flex items-center justify-center text-[10px] font-bold text-blue-700 ring-2 ring-white shadow-sm">
302
  {issue.assigned_to.charAt(0)}
303
  </div>
304
- <span className="font-medium text-slate-700">{issue.assigned_to}</span>
 
 
305
  </div>
306
  ) : (
307
  <span className="text-slate-400 italic text-xs">
@@ -315,7 +318,9 @@ export default function IssuesPage() {
315
  )}
316
  </td>
317
  <td className="px-4 py-3 text-sm text-slate-600">
318
- <span className="font-medium">{new Date(issue.created_at).toLocaleDateString()}</span>
 
 
319
  <div className="text-xs text-slate-400 font-mono">
320
  {new Date(issue.created_at).toLocaleTimeString([], {
321
  hour: "2-digit",
@@ -326,7 +331,7 @@ export default function IssuesPage() {
326
  <td className="px-4 py-3 text-right">
327
  <Link
328
  href={`/admin/issues/${issue.id}`}
329
- className="inline-flex items-center gap-1.5 text-xs font-medium text-blue-600 hover:text-blue-800 bg-blue-50 px-2.5 py-1.5 rounded-lg border border-blue-100 hover:bg-blue-100 transition-colors"
330
  >
331
  View
332
  <ChevronRight className="w-3.5 h-3.5" />
@@ -350,20 +355,22 @@ export default function IssuesPage() {
350
  <span className="font-semibold text-slate-900">
351
  {Math.min(meta.page * meta.limit, meta.total)}
352
  </span>{" "}
353
- of <span className="font-semibold text-slate-900">{meta.total}</span> results
 
 
354
  </div>
355
  <div className="flex gap-2">
356
  <button
357
  onClick={() => handlePageChange(meta.page - 1)}
358
  disabled={meta.page === 1}
359
- className="px-3.5 py-1.5 text-sm font-medium border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-slate-700"
360
  >
361
  Previous
362
  </button>
363
  <button
364
  onClick={() => handlePageChange(meta.page + 1)}
365
  disabled={meta.page === meta.pages}
366
- className="px-3.5 py-1.5 text-sm font-medium border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-slate-700"
367
  >
368
  Next
369
  </button>
 
37
  export default function IssuesPage() {
38
  const router = useRouter();
39
  const searchParams = useSearchParams();
40
+
41
  const [page, setPage] = useState(1);
42
  const limit = 10;
43
+
44
  const [search, setSearch] = useState("");
45
  const [status, setStatus] = useState<string>("");
46
  const [priority, setPriority] = useState<string>("");
 
79
  return `/admin/issues?${query.toString()}`;
80
  }, [page, limit, sort, order, debouncedSearch, status, priority]);
81
 
82
+ const { data: issuesData, loading } =
83
+ useCachedFetch<IssuesResponse>(queryUrl);
84
 
85
  const issues = issuesData?.items || [];
86
  const meta: Meta = {
 
120
  };
121
 
122
  return (
123
+ <div className="space-y-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
124
  <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
125
  <div>
126
+ <h1 className="text-2xl font-black text-slate-900 tracking-tight">
127
  Issue Management
128
  </h1>
129
+ <p className="text-slate-500 text-sm font-medium">
130
  Monitor, assign, and resolve reported city issues.
131
  </p>
132
  </div>
133
  <div className="flex items-center gap-2">
134
  <button
135
  onClick={() => setStatus("pending_verification")}
136
+ className="px-4 py-2 bg-amber-100 text-amber-800 text-sm font-semibold rounded-xl hover:bg-amber-200 transition flex items-center gap-2 border border-amber-200"
137
  >
138
  <AlertCircle className="w-4 h-4" />
139
  Pending Reviews
140
  </button>
141
+ <button className="bg-slate-900 text-white px-4 py-2 rounded-xl text-sm font-semibold hover:bg-slate-800 transition">
142
  Export CSV
143
  </button>
144
  </div>
145
  </div>
146
 
147
+ <div className="bg-white/70 backdrop-blur-md rounded-3xl border border-slate-200/70 shadow-urban-sm p-4 sm:p-6 transition-all">
148
  <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
149
  <div className="md:col-span-2 relative group">
150
  <Search className="absolute left-3 top-2.5 h-4 w-4 text-slate-400 group-focus-within:text-blue-500 transition-colors" />
 
153
  placeholder="Search by ID, description, or location..."
154
  value={search}
155
  onChange={(e) => setSearch(e.target.value)}
156
+ className="w-full pl-10 pr-4 py-2.5 text-sm border border-slate-200 bg-white/70 rounded-xl focus:outline-none focus:ring-4 focus:ring-urban-primary/10 focus:border-urban-primary/40 transition-all font-sans"
157
  />
158
  </div>
159
 
 
161
  aria-label="Filter by Status"
162
  value={status}
163
  onChange={(e) => setStatus(e.target.value)}
164
+ className="px-3 py-2.5 text-sm border border-slate-200 bg-white/70 rounded-xl focus:outline-none focus:ring-4 focus:ring-urban-primary/10 focus:border-urban-primary/40 transition-all font-sans"
165
  >
166
  <option value="">All Statuses</option>
167
  <option value="reported">Reported</option>
 
178
  aria-label="Filter by Priority"
179
  value={priority}
180
  onChange={(e) => setPriority(e.target.value)}
181
+ className="px-3 py-2.5 text-sm border border-slate-200 bg-white/70 rounded-xl focus:outline-none focus:ring-4 focus:ring-urban-primary/10 focus:border-urban-primary/40 transition-all font-sans"
182
  >
183
  <option value="">All Priorities</option>
184
  <option value="1">Critical (P1)</option>
 
194
  Loading issues...
195
  </div>
196
  ) : (
197
+ <div className="overflow-x-auto rounded-2xl border border-slate-200/60 bg-white/60">
198
  <table className="w-full text-left border-collapse">
199
  <thead>
200
  <tr className="border-b border-slate-200/60 text-xs uppercase text-slate-500 bg-slate-50/80 font-mono tracking-wider">
 
241
  issues.map((issue) => (
242
  <tr
243
  key={issue.id}
244
+ className="group hover:bg-urban-primary/5 transition-colors duration-200"
245
  >
246
  <td className="px-4 py-3">
247
  <div className="flex items-center gap-3">
 
259
  )}
260
  </div>
261
  <div>
262
+ <div className="text-sm font-semibold text-slate-900 truncate max-w-50">
263
  {issue.category || "Uncategorized Issue"}
264
  </div>
265
+ <div className="text-xs text-slate-500 truncate max-w-50">
266
  {issue.description || "No description provided"}
267
  </div>
268
  </div>
 
302
  <div className="w-6 h-6 rounded-full bg-blue-100 flex items-center justify-center text-[10px] font-bold text-blue-700 ring-2 ring-white shadow-sm">
303
  {issue.assigned_to.charAt(0)}
304
  </div>
305
+ <span className="font-medium text-slate-700">
306
+ {issue.assigned_to}
307
+ </span>
308
  </div>
309
  ) : (
310
  <span className="text-slate-400 italic text-xs">
 
318
  )}
319
  </td>
320
  <td className="px-4 py-3 text-sm text-slate-600">
321
+ <span className="font-medium">
322
+ {new Date(issue.created_at).toLocaleDateString()}
323
+ </span>
324
  <div className="text-xs text-slate-400 font-mono">
325
  {new Date(issue.created_at).toLocaleTimeString([], {
326
  hour: "2-digit",
 
331
  <td className="px-4 py-3 text-right">
332
  <Link
333
  href={`/admin/issues/${issue.id}`}
334
+ className="inline-flex items-center gap-1.5 text-xs font-semibold text-urban-primary hover:text-emerald-700 bg-urban-primary/10 px-2.5 py-1.5 rounded-lg border border-urban-primary/20 hover:bg-urban-primary/20 transition-colors"
335
  >
336
  View
337
  <ChevronRight className="w-3.5 h-3.5" />
 
355
  <span className="font-semibold text-slate-900">
356
  {Math.min(meta.page * meta.limit, meta.total)}
357
  </span>{" "}
358
+ of{" "}
359
+ <span className="font-semibold text-slate-900">{meta.total}</span>{" "}
360
+ results
361
  </div>
362
  <div className="flex gap-2">
363
  <button
364
  onClick={() => handlePageChange(meta.page - 1)}
365
  disabled={meta.page === 1}
366
+ className="px-3.5 py-1.5 text-sm font-semibold border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-slate-700"
367
  >
368
  Previous
369
  </button>
370
  <button
371
  onClick={() => handlePageChange(meta.page + 1)}
372
  disabled={meta.page === meta.pages}
373
+ className="px-3.5 py-1.5 text-sm font-semibold border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-slate-700"
374
  >
375
  Next
376
  </button>
Frontend/app/admin/page.tsx CHANGED
@@ -1,4 +1,4 @@
1
- "use client"
2
  import Link from "next/link";
3
  import { useCachedFetch } from "@/hooks/useCachedFetch";
4
  import {
@@ -49,9 +49,9 @@ export default function AdminDashboard() {
49
 
50
  if (loading) {
51
  return (
52
- <div className="space-y-6">
53
- <Skeleton className="h-8 w-48 mb-6" />
54
- <div className="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-6 gap-6">
55
  {Array.from({ length: 6 }).map((_, i) => (
56
  <Skeleton key={i} className="h-32 rounded-2xl" />
57
  ))}
@@ -70,23 +70,28 @@ export default function AdminDashboard() {
70
  stats?.issues_activity && stats.issues_activity.length > 0;
71
 
72
  return (
73
- <div className="space-y-6">
74
  <div className="flex items-center justify-between">
75
- <h2 className="text-2xl font-bold text-slate-900 tracking-tight">
76
- Overview
77
- </h2>
 
 
 
 
 
78
  </div>
79
 
80
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-6">
81
  <StatCard
82
  title="Departments"
83
  value={stats?.departments || 0}
84
- icon={<Building2 className="w-5 h-5 text-blue-600" />}
85
  />
86
  <StatCard
87
  title="Total Staff"
88
  value={stats?.members || 0}
89
- icon={<Users className="w-5 h-5 text-purple-600" />}
90
  />
91
  <StatCard
92
  title="Total Issues"
@@ -106,7 +111,7 @@ export default function AdminDashboard() {
106
  <StatCard
107
  title="Needs Review"
108
  value={stats?.verification_needed || 0}
109
- icon={<ClipboardCheck className="w-5 h-5 text-indigo-600" />}
110
  alert={(stats?.verification_needed || 0) > 0}
111
  />
112
  </Link>
@@ -118,9 +123,9 @@ export default function AdminDashboard() {
118
  </div>
119
 
120
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
121
- <div className="lg:col-span-2 bg-white/60 backdrop-blur-md p-6 rounded-2xl border border-slate-200/60 shadow-urban-sm hover:shadow-urban-md transition-all">
122
- <h3 className="text-lg font-bold text-slate-900 mb-6 flex items-center gap-2">
123
- <span className="w-1 h-6 bg-blue-500 rounded-full"></span>
124
  Weekly Activity
125
  </h3>
126
  {hasActivityData ? (
@@ -139,13 +144,21 @@ export default function AdminDashboard() {
139
  dataKey="name"
140
  axisLine={false}
141
  tickLine={false}
142
- tick={{ fill: "#64748B", fontSize: 12, fontFamily: 'var(--font-fira-sans)' }}
 
 
 
 
143
  dy={10}
144
  />
145
  <YAxis
146
  axisLine={false}
147
  tickLine={false}
148
- tick={{ fill: "#64748B", fontSize: 12, fontFamily: 'var(--font-fira-sans)' }}
 
 
 
 
149
  />
150
  <Tooltip
151
  cursor={{ fill: "#F1F5F9" }}
@@ -155,21 +168,24 @@ export default function AdminDashboard() {
155
  boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1)",
156
  backgroundColor: "rgba(255, 255, 255, 0.9)",
157
  backdropFilter: "blur(4px)",
158
- fontFamily: 'var(--font-fira-sans)',
159
  }}
160
  />
161
- <Legend iconType="circle" wrapperStyle={{ fontFamily: 'var(--font-fira-sans)' }} />
 
 
 
162
  <Bar
163
  dataKey="reported"
164
  name="Reported"
165
- fill="#3B82F6"
166
  radius={[4, 4, 0, 0]}
167
  barSize={20}
168
  />
169
  <Bar
170
  dataKey="resolved"
171
  name="Resolved"
172
- fill="#10B981"
173
  radius={[4, 4, 0, 0]}
174
  barSize={20}
175
  />
@@ -183,9 +199,9 @@ export default function AdminDashboard() {
183
  )}
184
  </div>
185
 
186
- <div className="bg-white/60 backdrop-blur-md p-6 rounded-2xl border border-slate-200/60 shadow-urban-sm hover:shadow-urban-md transition-all">
187
- <h3 className="text-lg font-bold text-slate-900 mb-6 flex items-center gap-2">
188
- <span className="w-1 h-6 bg-purple-500 rounded-full"></span>
189
  Issues by Category
190
  </h3>
191
  {hasChartData ? (
@@ -209,17 +225,21 @@ export default function AdminDashboard() {
209
  />
210
  ))}
211
  </Pie>
212
- <Tooltip
213
  contentStyle={{
214
  borderRadius: "12px",
215
  border: "1px solid rgba(226, 232, 240, 0.8)",
216
  boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1)",
217
  backgroundColor: "rgba(255, 255, 255, 0.9)",
218
  backdropFilter: "blur(4px)",
219
- fontFamily: 'var(--font-fira-sans)',
220
  }}
221
  />
222
- <Legend verticalAlign="bottom" height={36} wrapperStyle={{ fontFamily: 'var(--font-fira-sans)' }} />
 
 
 
 
223
  </PieChart>
224
  </ResponsiveContainer>
225
  </div>
@@ -246,7 +266,7 @@ function StatCard({
246
  alert?: boolean;
247
  }) {
248
  return (
249
- <div className="bg-white/60 backdrop-blur-md p-6 rounded-2xl border border-slate-200/60 shadow-urban-sm hover:shadow-urban-md transition-all hover:-translate-y-1 group">
250
  <div className="flex justify-between items-start mb-4">
251
  <div className="p-3 bg-white rounded-xl border border-slate-100 shadow-sm group-hover:scale-110 transition-transform duration-300">
252
  {icon}
 
1
+ "use client";
2
  import Link from "next/link";
3
  import { useCachedFetch } from "@/hooks/useCachedFetch";
4
  import {
 
49
 
50
  if (loading) {
51
  return (
52
+ <div className="space-y-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
53
+ <Skeleton className="h-8 w-48" />
54
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-6">
55
  {Array.from({ length: 6 }).map((_, i) => (
56
  <Skeleton key={i} className="h-32 rounded-2xl" />
57
  ))}
 
70
  stats?.issues_activity && stats.issues_activity.length > 0;
71
 
72
  return (
73
+ <div className="space-y-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
74
  <div className="flex items-center justify-between">
75
+ <div>
76
+ <h2 className="text-2xl font-black text-slate-900 tracking-tight">
77
+ Overview
78
+ </h2>
79
+ <p className="text-sm text-slate-500 font-medium">
80
+ City operations at a glance
81
+ </p>
82
+ </div>
83
  </div>
84
 
85
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-6">
86
  <StatCard
87
  title="Departments"
88
  value={stats?.departments || 0}
89
+ icon={<Building2 className="w-5 h-5 text-urban-primary" />}
90
  />
91
  <StatCard
92
  title="Total Staff"
93
  value={stats?.members || 0}
94
+ icon={<Users className="w-5 h-5 text-amber-500" />}
95
  />
96
  <StatCard
97
  title="Total Issues"
 
111
  <StatCard
112
  title="Needs Review"
113
  value={stats?.verification_needed || 0}
114
+ icon={<ClipboardCheck className="w-5 h-5 text-urban-primary" />}
115
  alert={(stats?.verification_needed || 0) > 0}
116
  />
117
  </Link>
 
123
  </div>
124
 
125
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
126
+ <div className="lg:col-span-2 bg-white/70 backdrop-blur-md p-6 rounded-3xl border border-slate-200/70 shadow-urban-sm hover:shadow-urban-md transition-all">
127
+ <h3 className="text-lg font-black text-slate-900 mb-6 flex items-center gap-3">
128
+ <span className="w-1.5 h-6 bg-urban-primary rounded-full"></span>
129
  Weekly Activity
130
  </h3>
131
  {hasActivityData ? (
 
144
  dataKey="name"
145
  axisLine={false}
146
  tickLine={false}
147
+ tick={{
148
+ fill: "#64748B",
149
+ fontSize: 12,
150
+ fontFamily: "var(--font-fira-sans)",
151
+ }}
152
  dy={10}
153
  />
154
  <YAxis
155
  axisLine={false}
156
  tickLine={false}
157
+ tick={{
158
+ fill: "#64748B",
159
+ fontSize: 12,
160
+ fontFamily: "var(--font-fira-sans)",
161
+ }}
162
  />
163
  <Tooltip
164
  cursor={{ fill: "#F1F5F9" }}
 
168
  boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1)",
169
  backgroundColor: "rgba(255, 255, 255, 0.9)",
170
  backdropFilter: "blur(4px)",
171
+ fontFamily: "var(--font-fira-sans)",
172
  }}
173
  />
174
+ <Legend
175
+ iconType="circle"
176
+ wrapperStyle={{ fontFamily: "var(--font-fira-sans)" }}
177
+ />
178
  <Bar
179
  dataKey="reported"
180
  name="Reported"
181
+ fill="#0ea5a4"
182
  radius={[4, 4, 0, 0]}
183
  barSize={20}
184
  />
185
  <Bar
186
  dataKey="resolved"
187
  name="Resolved"
188
+ fill="#22c55e"
189
  radius={[4, 4, 0, 0]}
190
  barSize={20}
191
  />
 
199
  )}
200
  </div>
201
 
202
+ <div className="bg-white/70 backdrop-blur-md p-6 rounded-3xl border border-slate-200/70 shadow-urban-sm hover:shadow-urban-md transition-all">
203
+ <h3 className="text-lg font-black text-slate-900 mb-6 flex items-center gap-3">
204
+ <span className="w-1.5 h-6 bg-amber-500 rounded-full"></span>
205
  Issues by Category
206
  </h3>
207
  {hasChartData ? (
 
225
  />
226
  ))}
227
  </Pie>
228
+ <Tooltip
229
  contentStyle={{
230
  borderRadius: "12px",
231
  border: "1px solid rgba(226, 232, 240, 0.8)",
232
  boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1)",
233
  backgroundColor: "rgba(255, 255, 255, 0.9)",
234
  backdropFilter: "blur(4px)",
235
+ fontFamily: "var(--font-fira-sans)",
236
  }}
237
  />
238
+ <Legend
239
+ verticalAlign="bottom"
240
+ height={36}
241
+ wrapperStyle={{ fontFamily: "var(--font-fira-sans)" }}
242
+ />
243
  </PieChart>
244
  </ResponsiveContainer>
245
  </div>
 
266
  alert?: boolean;
267
  }) {
268
  return (
269
+ <div className="bg-white/70 backdrop-blur-md p-6 rounded-2xl border border-slate-200/70 shadow-urban-sm hover:shadow-urban-md transition-all hover:-translate-y-1 group">
270
  <div className="flex justify-between items-start mb-4">
271
  <div className="p-3 bg-white rounded-xl border border-slate-100 shadow-sm group-hover:scale-110 transition-transform duration-300">
272
  {icon}
Frontend/app/admin/review/page.tsx CHANGED
@@ -1,6 +1,7 @@
1
  "use client";
2
- import { useEffect, useState } from "react";
3
- import { apiGet, apiPost } from "@/lib/api";
 
4
  import { CheckCircle2, XCircle } from "lucide-react";
5
 
6
  interface Issue {
@@ -16,28 +17,29 @@ interface Issue {
16
  }
17
 
18
  export default function ManualReviewPage() {
19
- const [issues, setIssues] = useState<Issue[]>([]);
20
- const [loading, setLoading] = useState(true);
21
-
22
- useEffect(() => {
23
- fetchPendingIssues();
24
- }, []);
25
 
26
- const fetchPendingIssues = async () => {
27
- try {
28
- const data = await apiGet<{ items: Issue[] }>("/issues?state=reported");
29
- setIssues(data.items || []);
30
- } catch (error) {
31
- console.error("Failed to fetch issues:", error);
32
- } finally {
33
- setLoading(false);
34
- }
35
- };
36
 
37
  const handleReview = async (id: string, status: "approved" | "rejected") => {
38
  try {
39
- const data = await apiPost<{ message: string }>(`/admin/issues/${id}/review`, { status });
40
- setIssues(prev => prev.filter(i => i.id !== id));
 
 
 
 
 
 
 
 
41
  alert(data.message);
42
  } catch (error) {
43
  console.error("Review failed", error);
@@ -46,71 +48,93 @@ export default function ManualReviewPage() {
46
  };
47
 
48
  if (loading) {
49
- return <div className="text-slate-600 font-medium">Loading Reviews...</div>;
 
 
 
 
50
  }
51
 
52
  return (
53
- <div className="space-y-6">
54
- <div className="flex justify-between items-center">
55
- <div>
56
- <h2 className="text-2xl font-bold text-slate-900">Manual Review Queue</h2>
57
- <p className="text-sm text-slate-500">Validate incoming citizen reports before assignment.</p>
58
- </div>
59
- <div className="bg-blue-600 text-white text-xs font-bold px-3 py-1.5 rounded-full shadow-sm">
60
- {issues.length} Pending
61
- </div>
 
 
 
 
62
  </div>
63
 
64
- {issues.length === 0 ? (
65
- <div className="text-center py-20 bg-white rounded-xl border border-slate-200 shadow-sm">
66
- <CheckCircle2 className="w-12 h-12 mx-auto text-green-400" />
67
- <p className="text-slate-900 font-medium mt-4 text-lg">All caught up!</p>
 
 
68
  <p className="text-slate-500">No issues pending manual review.</p>
69
  </div>
70
  ) : (
71
  <div className="grid gap-6">
72
- {issues.map((issue) => (
73
- <div key={issue.id} className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden flex flex-col md:flex-row">
 
 
 
74
  <div className="md:w-1/3 h-64 md:h-auto bg-slate-100 relative">
75
- {issue.images?.[0] ? (
76
- <img
77
- src={issue.images[0].annotated_path || issue.images[0].file_path}
78
- alt="Evidence"
79
- className="w-full h-full object-cover"
80
- />
81
- ) : (
82
- <div className="flex items-center justify-center h-full text-slate-400">No Image</div>
83
- )}
 
 
 
 
 
84
  </div>
85
 
86
  <div className="p-6 md:w-2/3 flex flex-col justify-between">
87
- <div>
88
- <div className="flex justify-between items-start mb-2">
89
- <span className="px-2.5 py-0.5 rounded-full bg-slate-100 text-slate-600 text-xs font-bold uppercase">
90
- {issue.city}
91
- </span>
92
- <span className="text-xs text-slate-400">
93
- {new Date(issue.created_at).toLocaleDateString()}
94
- </span>
95
- </div>
96
- <h3 className="text-lg font-bold text-slate-900 mb-2">{issue.description || "No description"}</h3>
97
- <p className="text-slate-600 text-sm mb-4">{issue.full_address || issue.locality}</p>
98
  </div>
 
 
 
 
 
 
 
99
 
100
- <div className="flex gap-4 mt-4 pt-4 border-t border-slate-100">
101
- <button
102
- onClick={() => handleReview(issue.id, "approved")}
103
- className="flex-1 bg-green-600 hover:bg-green-700 text-white font-semibold py-2 px-4 rounded-lg transition shadow-sm flex items-center justify-center gap-2"
104
- >
105
- <CheckCircle2 className="w-4 h-4" /> Approve & Assign
106
- </button>
107
- <button
108
- onClick={() => handleReview(issue.id, "rejected")}
109
- className="flex-1 bg-white border border-red-200 text-red-600 hover:bg-red-50 font-semibold py-2 px-4 rounded-lg transition flex items-center justify-center gap-2"
110
- >
111
- <XCircle className="w-4 h-4" /> Reject
112
- </button>
113
- </div>
114
  </div>
115
  </div>
116
  ))}
 
1
  "use client";
2
+ import { useMemo, useState } from "react";
3
+ import { apiPost } from "@/lib/api";
4
+ import { useCachedFetch } from "@/hooks/useCachedFetch";
5
  import { CheckCircle2, XCircle } from "lucide-react";
6
 
7
  interface Issue {
 
17
  }
18
 
19
  export default function ManualReviewPage() {
20
+ const [removedIds, setRemovedIds] = useState<Set<string>>(new Set());
21
+ const { data, loading, revalidate } = useCachedFetch<{ items: Issue[] }>(
22
+ "/issues?state=reported",
23
+ );
 
 
24
 
25
+ const issues = useMemo(() => data?.items || [], [data]);
26
+ const visibleIssues = useMemo(
27
+ () => issues.filter((issue) => !removedIds.has(issue.id)),
28
+ [issues, removedIds],
29
+ );
 
 
 
 
 
30
 
31
  const handleReview = async (id: string, status: "approved" | "rejected") => {
32
  try {
33
+ const data = await apiPost<{ message: string }>(
34
+ `/admin/issues/${id}/review`,
35
+ { status },
36
+ );
37
+ setRemovedIds((prev) => {
38
+ const next = new Set(prev);
39
+ next.add(id);
40
+ return next;
41
+ });
42
+ revalidate();
43
  alert(data.message);
44
  } catch (error) {
45
  console.error("Review failed", error);
 
48
  };
49
 
50
  if (loading) {
51
+ return (
52
+ <div className="text-slate-600 font-medium max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
53
+ Loading Reviews...
54
+ </div>
55
+ );
56
  }
57
 
58
  return (
59
+ <div className="space-y-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
60
+ <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
61
+ <div>
62
+ <h2 className="text-2xl font-black text-slate-900">
63
+ Manual Review Queue
64
+ </h2>
65
+ <p className="text-sm text-slate-500 font-medium">
66
+ Validate incoming citizen reports before assignment.
67
+ </p>
68
+ </div>
69
+ <div className="bg-urban-primary text-white text-xs font-bold px-3 py-1.5 rounded-full shadow-sm">
70
+ {visibleIssues.length} Pending
71
+ </div>
72
  </div>
73
 
74
+ {visibleIssues.length === 0 ? (
75
+ <div className="text-center py-20 bg-white/70 backdrop-blur-md rounded-2xl border border-slate-200/70 shadow-urban-sm">
76
+ <CheckCircle2 className="w-12 h-12 mx-auto text-emerald-500" />
77
+ <p className="text-slate-900 font-medium mt-4 text-lg">
78
+ All caught up!
79
+ </p>
80
  <p className="text-slate-500">No issues pending manual review.</p>
81
  </div>
82
  ) : (
83
  <div className="grid gap-6">
84
+ {visibleIssues.map((issue) => (
85
+ <div
86
+ key={issue.id}
87
+ className="bg-white/80 backdrop-blur-md rounded-2xl border border-slate-200/70 shadow-urban-sm overflow-hidden flex flex-col md:flex-row"
88
+ >
89
  <div className="md:w-1/3 h-64 md:h-auto bg-slate-100 relative">
90
+ {issue.images?.[0] ? (
91
+ <img
92
+ src={
93
+ issue.images[0].annotated_path ||
94
+ issue.images[0].file_path
95
+ }
96
+ alt="Evidence"
97
+ className="w-full h-full object-cover"
98
+ />
99
+ ) : (
100
+ <div className="flex items-center justify-center h-full text-slate-400">
101
+ No Image
102
+ </div>
103
+ )}
104
  </div>
105
 
106
  <div className="p-6 md:w-2/3 flex flex-col justify-between">
107
+ <div>
108
+ <div className="flex justify-between items-start mb-2">
109
+ <span className="px-2.5 py-0.5 rounded-full bg-slate-100 text-slate-600 text-xs font-bold uppercase">
110
+ {issue.city}
111
+ </span>
112
+ <span className="text-xs text-slate-400">
113
+ {new Date(issue.created_at).toLocaleDateString()}
114
+ </span>
 
 
 
115
  </div>
116
+ <h3 className="text-lg font-bold text-slate-900 mb-2">
117
+ {issue.description || "No description"}
118
+ </h3>
119
+ <p className="text-slate-600 text-sm mb-4">
120
+ {issue.full_address || issue.locality}
121
+ </p>
122
+ </div>
123
 
124
+ <div className="flex flex-col sm:flex-row gap-3 mt-4 pt-4 border-t border-slate-100/70">
125
+ <button
126
+ onClick={() => handleReview(issue.id, "approved")}
127
+ className="flex-1 bg-emerald-600 hover:bg-emerald-700 text-white font-semibold py-2.5 px-4 rounded-xl transition shadow-sm flex items-center justify-center gap-2"
128
+ >
129
+ <CheckCircle2 className="w-4 h-4" /> Approve & Assign
130
+ </button>
131
+ <button
132
+ onClick={() => handleReview(issue.id, "rejected")}
133
+ className="flex-1 bg-white border border-red-200 text-red-600 hover:bg-red-50 font-semibold py-2.5 px-4 rounded-xl transition flex items-center justify-center gap-2"
134
+ >
135
+ <XCircle className="w-4 h-4" /> Reject
136
+ </button>
137
+ </div>
138
  </div>
139
  </div>
140
  ))}
Frontend/app/admin/workers/page.tsx CHANGED
@@ -40,10 +40,22 @@ interface WorkerPerformance {
40
  }
41
 
42
  export default function WorkersPage() {
43
- const { data: departmentsData, loading: deptLoading, revalidate: revalidateDept } = useCachedFetch<Department[]>("/admin/departments");
44
- const { data: workersData, loading: workersLoading, revalidate: revalidateWorkers } = useCachedFetch<Worker[]>("/admin/members");
45
- const { data: perfData, loading: perfLoading, revalidate: revalidatePerf } = useCachedFetch<WorkerPerformance[]>("/admin/workers/performance");
46
-
 
 
 
 
 
 
 
 
 
 
 
 
47
  const [showForm, setShowForm] = useState(false);
48
  const [formData, setFormData] = useState({
49
  name: "",
@@ -56,11 +68,11 @@ export default function WorkersPage() {
56
  const [search, setSearch] = useState("");
57
 
58
  const departments = departmentsData || [];
59
-
60
  const workers = useMemo(() => {
61
  if (!workersData) return [];
62
- const perfMap = new Map((perfData || []).map(p => [p.id, p]));
63
-
64
  return workersData.map((w) => {
65
  const perf = perfMap.get(w.id);
66
  return {
@@ -106,14 +118,14 @@ export default function WorkersPage() {
106
 
107
  if (loading) {
108
  return (
109
- <div className="space-y-6">
110
  <div className="flex justify-between items-center mb-6">
111
  <Skeleton className="h-10 w-64" />
112
  <Skeleton className="h-10 w-32" />
113
  </div>
114
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
115
  {Array.from({ length: 6 }).map((_, i) => (
116
- <Skeleton key={i} className="h-48 rounded-xl" />
117
  ))}
118
  </div>
119
  </div>
@@ -130,28 +142,28 @@ export default function WorkersPage() {
130
  );
131
 
132
  return (
133
- <div className="space-y-6">
134
  <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
135
  <div>
136
- <h2 className="text-2xl font-bold text-slate-900">
137
  Workforce Management
138
  </h2>
139
- <p className="text-sm text-slate-500">
140
  Manage field workers, assign tasks, and monitor performance.
141
  </p>
142
  </div>
143
  <button
144
  onClick={() => setShowForm(true)}
145
- className="px-4 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition shadow-sm flex items-center gap-2"
146
  >
147
  <Plus className="w-4 h-4" /> Enroll Worker
148
  </button>
149
  </div>
150
 
151
  {showForm && (
152
- <div className="bg-white rounded-xl shadow-lg border border-slate-200 overflow-hidden animate-in fade-in slide-in-from-top-4">
153
- <div className="bg-slate-50 px-6 py-4 border-b border-slate-200">
154
- <h2 className="text-lg font-bold text-slate-800">
155
  New Worker Enrollment
156
  </h2>
157
  </div>
@@ -167,7 +179,7 @@ export default function WorkersPage() {
167
  onChange={(e) =>
168
  setFormData({ ...formData, name: e.target.value })
169
  }
170
- className="w-full px-4 py-2 bg-white border border-slate-300 rounded-lg text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
171
  placeholder="e.g. John Doe"
172
  required
173
  />
@@ -182,7 +194,7 @@ export default function WorkersPage() {
182
  onChange={(e) =>
183
  setFormData({ ...formData, email: e.target.value })
184
  }
185
- className="w-full px-4 py-2 bg-white border border-slate-300 rounded-lg text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
186
  placeholder="worker@city.gov"
187
  required
188
  />
@@ -199,7 +211,7 @@ export default function WorkersPage() {
199
  onChange={(e) =>
200
  setFormData({ ...formData, password: e.target.value })
201
  }
202
- className="w-full px-4 py-2 bg-white border border-slate-300 rounded-lg text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
203
  required
204
  minLength={8}
205
  placeholder="••••••••"
@@ -215,7 +227,7 @@ export default function WorkersPage() {
215
  onChange={(e) =>
216
  setFormData({ ...formData, department_id: e.target.value })
217
  }
218
- className="w-full px-4 py-2 bg-white border border-slate-300 rounded-lg text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
219
  required
220
  >
221
  <option value="">Select Department...</option>
@@ -231,14 +243,14 @@ export default function WorkersPage() {
231
  <div className="flex gap-3 pt-2">
232
  <button
233
  type="submit"
234
- className="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition shadow-sm"
235
  >
236
  Enroll Worker
237
  </button>
238
  <button
239
  type="button"
240
  onClick={() => setShowForm(false)}
241
- className="px-6 py-2 bg-white text-slate-700 font-medium rounded-lg border border-slate-300 hover:bg-slate-50 transition"
242
  >
243
  Cancel
244
  </button>
@@ -249,16 +261,16 @@ export default function WorkersPage() {
249
 
250
  <div className="flex gap-4 mb-4">
251
  <div className="relative flex-1 max-w-sm">
252
- <Search className="absolute left-3 top-2.5 h-4 w-4 text-slate-400" />
253
  <input
254
  type="text"
255
  placeholder="Search workers by name or email..."
256
  value={search}
257
  onChange={(e) => setSearch(e.target.value)}
258
- className="w-full pl-9 pr-4 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:border-blue-500 outline-none"
259
  />
260
  </div>
261
- <button className="px-3 py-2 bg-white border border-slate-200 rounded-lg text-slate-600 flex items-center gap-2 hover:bg-slate-50">
262
  <Filter className="w-4 h-4" /> Filter
263
  </button>
264
  </div>
@@ -271,7 +283,7 @@ export default function WorkersPage() {
271
  </div>
272
 
273
  {filteredWorkers.length === 0 ? (
274
- <div className="text-center py-16 bg-white rounded-xl border border-slate-200">
275
  <HardHat className="w-12 h-12 mx-auto text-slate-300" />
276
  <p className="text-slate-500 mt-2">No field workers found.</p>
277
  </div>
@@ -280,11 +292,11 @@ export default function WorkersPage() {
280
  {filteredWorkers.map((worker) => (
281
  <div
282
  key={worker.id}
283
- className="bg-white p-5 rounded-xl border border-slate-200 shadow-sm hover:shadow-md transition-all"
284
  >
285
  <div className="flex justify-between items-start mb-4">
286
  <div className="flex items-center gap-3">
287
- <div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center text-lg font-bold text-slate-700 border border-slate-200">
288
  {worker.name.charAt(0)}
289
  </div>
290
  <div>
@@ -301,7 +313,7 @@ export default function WorkersPage() {
301
  )}
302
  </div>
303
 
304
- <div className="py-3 border-t border-slate-100 mb-3 space-y-2">
305
  <div className="flex justify-between text-sm">
306
  <span className="text-slate-500">Department</span>
307
  <span className="font-medium text-slate-900">
@@ -343,7 +355,7 @@ export default function WorkersPage() {
343
  ? "bg-red-500"
344
  : worker.current_workload > worker.max_workload * 0.7
345
  ? "bg-amber-500"
346
- : "bg-blue-600"
347
  }`}
348
  style={{
349
  width: `${Math.min(
 
40
  }
41
 
42
  export default function WorkersPage() {
43
+ const {
44
+ data: departmentsData,
45
+ loading: deptLoading,
46
+ revalidate: revalidateDept,
47
+ } = useCachedFetch<Department[]>("/admin/departments");
48
+ const {
49
+ data: workersData,
50
+ loading: workersLoading,
51
+ revalidate: revalidateWorkers,
52
+ } = useCachedFetch<Worker[]>("/admin/members");
53
+ const {
54
+ data: perfData,
55
+ loading: perfLoading,
56
+ revalidate: revalidatePerf,
57
+ } = useCachedFetch<WorkerPerformance[]>("/admin/workers/performance");
58
+
59
  const [showForm, setShowForm] = useState(false);
60
  const [formData, setFormData] = useState({
61
  name: "",
 
68
  const [search, setSearch] = useState("");
69
 
70
  const departments = departmentsData || [];
71
+
72
  const workers = useMemo(() => {
73
  if (!workersData) return [];
74
+ const perfMap = new Map((perfData || []).map((p) => [p.id, p]));
75
+
76
  return workersData.map((w) => {
77
  const perf = perfMap.get(w.id);
78
  return {
 
118
 
119
  if (loading) {
120
  return (
121
+ <div className="space-y-6 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
122
  <div className="flex justify-between items-center mb-6">
123
  <Skeleton className="h-10 w-64" />
124
  <Skeleton className="h-10 w-32" />
125
  </div>
126
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
127
  {Array.from({ length: 6 }).map((_, i) => (
128
+ <Skeleton key={i} className="h-48 rounded-2xl" />
129
  ))}
130
  </div>
131
  </div>
 
142
  );
143
 
144
  return (
145
+ <div className="space-y-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
146
  <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
147
  <div>
148
+ <h2 className="text-2xl font-black text-slate-900">
149
  Workforce Management
150
  </h2>
151
+ <p className="text-sm text-slate-500 font-medium">
152
  Manage field workers, assign tasks, and monitor performance.
153
  </p>
154
  </div>
155
  <button
156
  onClick={() => setShowForm(true)}
157
+ className="px-4 py-2 bg-urban-primary text-white font-semibold rounded-xl hover:bg-emerald-600 transition shadow-sm flex items-center gap-2"
158
  >
159
  <Plus className="w-4 h-4" /> Enroll Worker
160
  </button>
161
  </div>
162
 
163
  {showForm && (
164
+ <div className="bg-white/80 backdrop-blur-md rounded-2xl shadow-urban-md border border-slate-200/70 overflow-hidden">
165
+ <div className="bg-slate-50/80 px-6 py-4 border-b border-slate-200/70">
166
+ <h2 className="text-lg font-black text-slate-800">
167
  New Worker Enrollment
168
  </h2>
169
  </div>
 
179
  onChange={(e) =>
180
  setFormData({ ...formData, name: e.target.value })
181
  }
182
+ className="w-full px-4 py-2.5 bg-white/70 border border-slate-300 rounded-xl text-slate-900 focus:ring-4 focus:ring-urban-primary/10 focus:border-urban-primary/40 outline-none"
183
  placeholder="e.g. John Doe"
184
  required
185
  />
 
194
  onChange={(e) =>
195
  setFormData({ ...formData, email: e.target.value })
196
  }
197
+ className="w-full px-4 py-2.5 bg-white/70 border border-slate-300 rounded-xl text-slate-900 focus:ring-4 focus:ring-urban-primary/10 focus:border-urban-primary/40 outline-none"
198
  placeholder="worker@city.gov"
199
  required
200
  />
 
211
  onChange={(e) =>
212
  setFormData({ ...formData, password: e.target.value })
213
  }
214
+ className="w-full px-4 py-2.5 bg-white/70 border border-slate-300 rounded-xl text-slate-900 focus:ring-4 focus:ring-urban-primary/10 focus:border-urban-primary/40 outline-none"
215
  required
216
  minLength={8}
217
  placeholder="••••••••"
 
227
  onChange={(e) =>
228
  setFormData({ ...formData, department_id: e.target.value })
229
  }
230
+ className="w-full px-4 py-2.5 bg-white/70 border border-slate-300 rounded-xl text-slate-900 focus:ring-4 focus:ring-urban-primary/10 focus:border-urban-primary/40 outline-none"
231
  required
232
  >
233
  <option value="">Select Department...</option>
 
243
  <div className="flex gap-3 pt-2">
244
  <button
245
  type="submit"
246
+ className="px-6 py-2.5 bg-urban-primary text-white font-semibold rounded-xl hover:bg-emerald-600 transition shadow-sm"
247
  >
248
  Enroll Worker
249
  </button>
250
  <button
251
  type="button"
252
  onClick={() => setShowForm(false)}
253
+ className="px-6 py-2.5 bg-white/80 text-slate-700 font-semibold rounded-xl border border-slate-300 hover:bg-slate-50 transition"
254
  >
255
  Cancel
256
  </button>
 
261
 
262
  <div className="flex gap-4 mb-4">
263
  <div className="relative flex-1 max-w-sm">
264
+ <Search className="absolute left-3 top-3 h-4 w-4 text-slate-400" />
265
  <input
266
  type="text"
267
  placeholder="Search workers by name or email..."
268
  value={search}
269
  onChange={(e) => setSearch(e.target.value)}
270
+ className="w-full pl-9 pr-4 py-2.5 bg-white/70 border border-slate-200 rounded-xl text-sm focus:border-urban-primary/40 focus:ring-4 focus:ring-urban-primary/10 outline-none"
271
  />
272
  </div>
273
+ <button className="px-3 py-2.5 bg-white/70 border border-slate-200 rounded-xl text-slate-600 flex items-center gap-2 hover:bg-slate-50">
274
  <Filter className="w-4 h-4" /> Filter
275
  </button>
276
  </div>
 
283
  </div>
284
 
285
  {filteredWorkers.length === 0 ? (
286
+ <div className="text-center py-16 bg-white/70 backdrop-blur-md rounded-2xl border border-slate-200/70">
287
  <HardHat className="w-12 h-12 mx-auto text-slate-300" />
288
  <p className="text-slate-500 mt-2">No field workers found.</p>
289
  </div>
 
292
  {filteredWorkers.map((worker) => (
293
  <div
294
  key={worker.id}
295
+ className="bg-white/80 backdrop-blur-md p-5 rounded-2xl border border-slate-200/70 shadow-urban-sm hover:shadow-urban-md transition-all"
296
  >
297
  <div className="flex justify-between items-start mb-4">
298
  <div className="flex items-center gap-3">
299
+ <div className="w-10 h-10 rounded-full bg-urban-primary/10 flex items-center justify-center text-lg font-bold text-urban-primary border border-urban-primary/20">
300
  {worker.name.charAt(0)}
301
  </div>
302
  <div>
 
313
  )}
314
  </div>
315
 
316
+ <div className="py-3 border-t border-slate-100/70 mb-3 space-y-2">
317
  <div className="flex justify-between text-sm">
318
  <span className="text-slate-500">Department</span>
319
  <span className="font-medium text-slate-900">
 
355
  ? "bg-red-500"
356
  : worker.current_workload > worker.max_workload * 0.7
357
  ? "bg-amber-500"
358
+ : "bg-urban-primary"
359
  }`}
360
  style={{
361
  width: `${Math.min(
Frontend/app/globals.css CHANGED
@@ -1,11 +1,11 @@
1
  @import "tailwindcss";
2
 
3
  @theme {
4
- --color-urban-primary: #3B82F6;
5
- --color-urban-secondary: #60A5FA;
6
- --color-urban-cta: #F97316;
7
- --color-urban-bg: #F8FAFC;
8
- --color-urban-text: #1E293B;
9
 
10
  --color-slate-50: #f8fafc;
11
  --color-slate-100: #f1f5f9;
@@ -18,14 +18,14 @@
18
  --color-slate-800: #1e293b;
19
  --color-slate-900: #0f172a;
20
  --color-slate-950: #020617;
21
-
22
- --font-fira-code: 'Fira Code', monospace;
23
- --font-fira-sans: 'Fira Sans', sans-serif;
24
-
25
- --shadow-urban-sm: 0 1px 2px rgba(0,0,0,0.05);
26
- --shadow-urban-md: 0 4px 6px rgba(0,0,0,0.1);
27
- --shadow-urban-lg: 0 10px 15px rgba(0,0,0,0.1);
28
- --shadow-urban-xl: 0 20px 25px rgba(0,0,0,0.15);
29
  }
30
 
31
  :root {
@@ -37,12 +37,33 @@
37
  }
38
 
39
  body {
40
- background: var(--background);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  color: var(--foreground);
42
  font-family: var(--font-fira-sans);
43
  }
44
 
45
- h1, h2, h3, h4, h5, h6 {
 
 
 
 
 
46
  font-family: var(--font-fira-code);
47
  }
48
 
@@ -62,5 +83,71 @@ input:focus,
62
  select:focus,
63
  textarea:focus {
64
  border-color: var(--primary);
65
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); /* urban-primary 10% */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  }
 
1
  @import "tailwindcss";
2
 
3
  @theme {
4
+ --color-urban-primary: #0ea5a4;
5
+ --color-urban-secondary: #f59e0b;
6
+ --color-urban-cta: #0f172a;
7
+ --color-urban-bg: #f6f7fb;
8
+ --color-urban-text: #0f172a;
9
 
10
  --color-slate-50: #f8fafc;
11
  --color-slate-100: #f1f5f9;
 
18
  --color-slate-800: #1e293b;
19
  --color-slate-900: #0f172a;
20
  --color-slate-950: #020617;
21
+
22
+ --font-fira-code: "Fira Code", monospace;
23
+ --font-fira-sans: "Fira Sans", sans-serif;
24
+
25
+ --shadow-urban-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
26
+ --shadow-urban-md: 0 4px 6px rgba(0, 0, 0, 0.1);
27
+ --shadow-urban-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
28
+ --shadow-urban-xl: 0 20px 25px rgba(0, 0, 0, 0.15);
29
  }
30
 
31
  :root {
 
37
  }
38
 
39
  body {
40
+ background:
41
+ radial-gradient(
42
+ 1200px 600px at 10% -10%,
43
+ rgba(14, 165, 164, 0.18),
44
+ transparent 60%
45
+ ),
46
+ radial-gradient(
47
+ 1000px 500px at 90% -20%,
48
+ rgba(245, 158, 11, 0.18),
49
+ transparent 55%
50
+ ),
51
+ radial-gradient(
52
+ 800px 400px at 50% 100%,
53
+ rgba(59, 130, 246, 0.12),
54
+ transparent 55%
55
+ ),
56
+ var(--background);
57
  color: var(--foreground);
58
  font-family: var(--font-fira-sans);
59
  }
60
 
61
+ h1,
62
+ h2,
63
+ h3,
64
+ h4,
65
+ h5,
66
+ h6 {
67
  font-family: var(--font-fira-code);
68
  }
69
 
 
83
  select:focus,
84
  textarea:focus {
85
  border-color: var(--primary);
86
+ box-shadow: 0 0 0 3px rgba(14, 165, 164, 0.12);
87
+ }
88
+
89
+ ::selection {
90
+ background: rgba(14, 165, 164, 0.2);
91
+ color: #0f172a;
92
+ }
93
+
94
+ .bg-mesh {
95
+ background-image:
96
+ radial-gradient(
97
+ circle at 20% 20%,
98
+ rgba(14, 165, 164, 0.2),
99
+ transparent 45%
100
+ ),
101
+ radial-gradient(
102
+ circle at 80% 10%,
103
+ rgba(245, 158, 11, 0.18),
104
+ transparent 45%
105
+ ),
106
+ radial-gradient(
107
+ circle at 30% 80%,
108
+ rgba(59, 130, 246, 0.15),
109
+ transparent 45%
110
+ );
111
+ }
112
+
113
+ .bg-grid {
114
+ background-image:
115
+ linear-gradient(rgba(15, 23, 42, 0.04) 1px, transparent 1px),
116
+ linear-gradient(90deg, rgba(15, 23, 42, 0.04) 1px, transparent 1px);
117
+ background-size: 32px 32px;
118
+ }
119
+
120
+ .glass-panel {
121
+ background: rgba(255, 255, 255, 0.75);
122
+ backdrop-filter: blur(18px);
123
+ border: 1px solid rgba(148, 163, 184, 0.25);
124
+ }
125
+
126
+ .fade-up {
127
+ animation: fadeUp 0.7s ease-out both;
128
+ }
129
+
130
+ .float-slow {
131
+ animation: floatSlow 6s ease-in-out infinite;
132
+ }
133
+
134
+ @keyframes fadeUp {
135
+ from {
136
+ opacity: 0;
137
+ transform: translateY(18px);
138
+ }
139
+ to {
140
+ opacity: 1;
141
+ transform: translateY(0);
142
+ }
143
+ }
144
+
145
+ @keyframes floatSlow {
146
+ 0%,
147
+ 100% {
148
+ transform: translateY(0);
149
+ }
150
+ 50% {
151
+ transform: translateY(-10px);
152
+ }
153
  }
Frontend/app/page.tsx CHANGED
@@ -1,18 +1,18 @@
1
  "use client";
2
  import Link from "next/link";
3
- import {
4
- Smartphone,
5
- Zap,
6
- Shield,
7
- ChevronRight,
8
- Radio,
9
- Activity,
10
- MapPin,
11
  CheckCircle2,
12
  Building2,
13
  Users,
14
  LayoutDashboard,
15
- LogOut
16
  } from "lucide-react";
17
  import { useAuth } from "@/components/AuthProvider";
18
 
@@ -20,50 +20,45 @@ export default function LandingPage() {
20
  const { user, role, signOut } = useAuth();
21
 
22
  const getDashboardLink = () => {
23
- if (role === 'admin') return '/admin';
24
- if (role === 'worker') return '/worker';
25
- return '/user';
26
  };
27
 
28
  return (
29
- <div className="min-h-screen bg-slate-50 text-slate-900 font-sans selection:bg-blue-100 selection:text-blue-900 overflow-x-hidden">
30
- {/* Background Mesh Gradients */}
31
- <div className="fixed inset-0 z-0 opacity-60 pointer-events-none">
32
- <div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-blue-200/50 rounded-full blur-[100px]" />
33
- <div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-indigo-200/50 rounded-full blur-[100px]" />
34
- <div className="absolute top-[20%] right-[10%] w-[30%] h-[30%] bg-cyan-200/40 rounded-full blur-[80px]" />
35
- </div>
36
 
37
- {/* Navigation */}
38
- <nav className="fixed top-0 w-full z-50 border-b border-slate-200 bg-white/80 backdrop-blur-xl transition-all">
39
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
40
  <div className="flex justify-between h-20 items-center">
41
  <div className="flex items-center gap-3 group cursor-default">
42
  <div className="relative">
43
  <div className="absolute inset-0 bg-blue-500/20 blur-md opacity-0 group-hover:opacity-100 transition-opacity" />
44
- <div className="relative w-10 h-10 bg-gradient-to-br from-blue-600 to-indigo-600 rounded-xl flex items-center justify-center text-white font-bold text-xl shadow-lg shadow-blue-500/30">
45
- <span className="text-white">U</span>
46
  </div>
47
  </div>
48
  <span className="text-xl font-bold tracking-tight text-slate-900">
49
- Urban<span className="text-blue-600">Lens</span>
50
  </span>
51
  </div>
52
-
53
  <div className="hidden md:flex items-center gap-8">
54
- <NavLink href="#features">Features</NavLink>
55
- <NavLink href="#stats">Live Data</NavLink>
56
- <NavLink href="#roadmap">Roadmap</NavLink>
57
  </div>
58
 
59
  <div className="flex gap-4 items-center">
60
  {user ? (
61
  <>
62
- <button
63
  onClick={() => signOut()}
64
  className="hidden sm:flex px-4 py-2 text-slate-500 hover:text-red-600 transition-colors"
65
  >
66
- <LogOut className="w-5 h-5" />
67
  </button>
68
  <Link
69
  href={getDashboardLink()}
@@ -79,11 +74,11 @@ export default function LandingPage() {
79
  href="/signin"
80
  className="px-5 py-2 text-slate-600 hover:text-slate-900 font-medium transition-colors hover:bg-slate-100 rounded-lg"
81
  >
82
- Agent Login
83
  </Link>
84
  <Link
85
  href="/signup"
86
- className="group relative px-6 py-2.5 bg-blue-600 hover:bg-blue-500 text-white font-semibold rounded-lg transition-all shadow-lg shadow-blue-500/30 overflow-hidden hover:-translate-y-0.5"
87
  >
88
  <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700" />
89
  <span className="relative flex items-center gap-2">
@@ -97,70 +92,123 @@ export default function LandingPage() {
97
  </div>
98
  </nav>
99
 
100
- <main className="relative z-10 pt-20">
101
- {/* Hero Section */}
102
- <div className="relative border-b border-slate-200 bg-gradient-to-b from-transparent to-white/50">
103
- <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-32 text-center">
104
-
105
- <div className="inline-flex items-center gap-3 px-4 py-1.5 rounded-full bg-white border border-slate-200 shadow-sm mb-8 animate-fade-in-up">
106
- <span className="relative flex h-2 w-2">
107
- <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
108
- <span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
109
- </span>
110
- <span className="text-sm font-bold text-slate-600 font-mono tracking-wide">
111
- SYSTEM ONLINE
112
- </span>
113
- </div>
114
 
115
- <h1 className="text-5xl md:text-7xl font-extrabold text-slate-900 tracking-tight mb-8 leading-tight">
116
- City Infrastructure, <br />
117
- <span className="bg-gradient-to-r from-blue-600 via-indigo-600 to-cyan-600 bg-clip-text text-transparent">
118
- Reimagined by Intelligence.
119
- </span>
120
- </h1>
121
 
122
- <p className="text-xl text-slate-600 mb-12 max-w-2xl mx-auto leading-relaxed">
123
- The advanced civic reporting platform powered by AI vision analysis,
124
- geo-spatial deduplication, and automated workforce routing.
125
- </p>
 
126
 
127
- <div className="flex flex-col sm:flex-row gap-6 justify-center items-center mb-20">
128
- <Link
129
- href="/signup"
130
- className="px-8 py-4 bg-slate-900 text-white font-bold rounded-xl shadow-xl shadow-slate-900/20 hover:shadow-slate-900/30 transition-all hover:-translate-y-1 flex items-center gap-2"
131
- >
132
- Report an Issue
133
- <Smartphone className="w-5 h-5" />
134
- </Link>
135
- <Link
136
- href="/signin"
137
- className="px-8 py-4 bg-white text-slate-700 font-semibold rounded-xl border border-slate-200 hover:bg-slate-50 transition-all shadow-lg shadow-slate-200/50 hover:border-slate-300 flex items-center gap-2"
138
- >
139
- Track Status
140
- <Activity className="w-5 h-5 text-blue-600" />
141
- </Link>
142
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
- {/* Live Stats Ticker */}
145
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-5xl mx-auto">
146
- <StatCard label="ISSUES RESOLVED" value="12,405" icon={CheckCircle2} color="text-emerald-500 bg-emerald-50" />
147
- <StatCard label="ACTIVE AGENTS" value="84" icon={Users} color="text-blue-500 bg-blue-50" />
148
- <StatCard label="AVG. REACTION" value="1.2hrs" icon={Zap} color="text-amber-500 bg-amber-50" />
149
- <StatCard label="CITIES LIVE" value="3" icon={Building2} color="text-purple-500 bg-purple-50" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  </div>
151
  </div>
152
  </div>
153
 
154
- {/* Features Grid */}
155
- <div id="features" className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-32">
 
 
156
  <div className="text-center mb-16">
157
- <h2 className="text-3xl font-bold text-slate-900 mb-4">Core Capabilities</h2>
 
 
158
  <p className="text-slate-500 max-w-2xl mx-auto text-lg">
159
- Built on a microservices architecture designed for scale, security, and speed.
 
160
  </p>
161
  </div>
162
-
163
- <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
164
  <FeatureCard
165
  icon={<Radio className="w-8 h-8 text-blue-600" />}
166
  title="Geo-Spatial AI"
@@ -181,13 +229,68 @@ export default function LandingPage() {
181
  />
182
  </div>
183
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  </main>
185
 
186
- <footer className="border-t border-slate-200 bg-white py-12 relative z-10">
187
  <div className="max-w-7xl mx-auto px-4 text-center">
188
  <div className="flex justify-center items-center gap-2 mb-8 opacity-75 hover:opacity-100 transition-opacity">
189
- <div className="w-6 h-6 bg-slate-900 rounded-md flex items-center justify-center text-xs font-bold font-mono text-white">U</div>
190
- <span className="text-sm font-bold tracking-wide text-slate-900">CityTracker SYSTEMS</span>
 
 
 
 
191
  </div>
192
  <p className="text-slate-500 text-sm">
193
  © 2026 Dept. of Public Works. Secure. Efficient. Transparent.
@@ -198,35 +301,56 @@ export default function LandingPage() {
198
  );
199
  }
200
 
201
- function NavLink({ href, children }: { href: string; children: React.ReactNode }) {
 
 
 
 
 
 
202
  return (
203
- <Link href={href} className="text-sm font-medium text-slate-500 hover:text-blue-600 transition-colors">
 
 
 
204
  {children}
205
  </Link>
206
- )
207
  }
208
 
209
- function StatCard({ label, value, icon: Icon, color }: { label: string, value: string, icon: any, color: string }) {
 
 
 
 
 
 
 
 
 
 
210
  return (
211
  <div className="p-4 rounded-2xl bg-white border border-slate-200 shadow-sm hover:shadow-md hover:border-slate-300 transition-all group">
212
  <div className="flex items-center gap-3 mb-2">
213
  <div className={`p-1.5 rounded-lg ${color}`}>
214
  <Icon className="w-4 h-4" />
215
  </div>
216
- <span className="text-xs font-mono text-slate-500 font-bold tracking-wider">{label}</span>
 
 
217
  </div>
218
  <div className="text-2xl font-bold text-slate-900 group-hover:scale-105 transition-transform origin-left font-mono">
219
  {value}
220
  </div>
221
  </div>
222
- )
223
  }
224
 
225
  function FeatureCard({
226
  icon,
227
  title,
228
  desc,
229
- colorClass
230
  }: {
231
  icon: React.ReactNode;
232
  title: string;
@@ -235,11 +359,17 @@ function FeatureCard({
235
  }) {
236
  return (
237
  <div className="p-8 bg-white rounded-3xl border border-slate-200 shadow-xl shadow-slate-200/50 hover:shadow-2xl hover:shadow-slate-200/80 hover:-translate-y-1 transition-all group">
238
- <div className={`w-16 h-16 ${colorClass} rounded-2xl flex items-center justify-center mb-6 border border-slate-100 group-hover:scale-110 transition-transform duration-300`}>
 
 
239
  {icon}
240
  </div>
241
- <h3 className="text-xl font-bold text-slate-900 mb-3 tracking-tight">{title}</h3>
242
- <p className="text-slate-500 leading-relaxed text-sm font-medium">{desc}</p>
 
 
 
 
243
  </div>
244
  );
245
  }
 
1
  "use client";
2
  import Link from "next/link";
3
+ import {
4
+ Smartphone,
5
+ Zap,
6
+ Shield,
7
+ ChevronRight,
8
+ Radio,
9
+ Activity,
10
+ MapPin,
11
  CheckCircle2,
12
  Building2,
13
  Users,
14
  LayoutDashboard,
15
+ LogOut,
16
  } from "lucide-react";
17
  import { useAuth } from "@/components/AuthProvider";
18
 
 
20
  const { user, role, signOut } = useAuth();
21
 
22
  const getDashboardLink = () => {
23
+ if (role === "admin") return "/admin";
24
+ if (role === "worker") return "/worker";
25
+ return "/user";
26
  };
27
 
28
  return (
29
+ <div className="min-h-screen text-slate-900 font-sans selection:bg-emerald-100 selection:text-slate-900 overflow-x-hidden">
30
+ <div className="fixed inset-0 z-0 opacity-70 pointer-events-none bg-mesh" />
31
+ <div className="fixed inset-0 z-0 opacity-50 pointer-events-none bg-grid" />
 
 
 
 
32
 
33
+ <nav className="fixed top-0 w-full z-50 border-b border-slate-200/70 bg-white/70 backdrop-blur-xl transition-all">
 
34
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
35
  <div className="flex justify-between h-20 items-center">
36
  <div className="flex items-center gap-3 group cursor-default">
37
  <div className="relative">
38
  <div className="absolute inset-0 bg-blue-500/20 blur-md opacity-0 group-hover:opacity-100 transition-opacity" />
39
+ <div className="relative w-10 h-10 bg-gradient-to-br from-emerald-500 to-cyan-600 rounded-xl flex items-center justify-center text-white shadow-lg shadow-emerald-500/30">
40
+ <MapPin className="w-5 h-5 text-white" />
41
  </div>
42
  </div>
43
  <span className="text-xl font-bold tracking-tight text-slate-900">
44
+ City<span className="text-emerald-600">Track</span>
45
  </span>
46
  </div>
47
+
48
  <div className="hidden md:flex items-center gap-8">
49
+ <NavLink href="#features">Features</NavLink>
50
+ <NavLink href="#stats">Live Data</NavLink>
51
+ <NavLink href="#roadmap">Roadmap</NavLink>
52
  </div>
53
 
54
  <div className="flex gap-4 items-center">
55
  {user ? (
56
  <>
57
+ <button
58
  onClick={() => signOut()}
59
  className="hidden sm:flex px-4 py-2 text-slate-500 hover:text-red-600 transition-colors"
60
  >
61
+ <LogOut className="w-5 h-5" />
62
  </button>
63
  <Link
64
  href={getDashboardLink()}
 
74
  href="/signin"
75
  className="px-5 py-2 text-slate-600 hover:text-slate-900 font-medium transition-colors hover:bg-slate-100 rounded-lg"
76
  >
77
+ Login
78
  </Link>
79
  <Link
80
  href="/signup"
81
+ className="group relative px-6 py-2.5 bg-emerald-600 hover:bg-emerald-500 text-white font-semibold rounded-lg transition-all shadow-lg shadow-emerald-500/30 overflow-hidden hover:-translate-y-0.5"
82
  >
83
  <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700" />
84
  <span className="relative flex items-center gap-2">
 
92
  </div>
93
  </nav>
94
 
95
+ <main className="relative z-10 pt-24">
96
+ <div className="relative border-b border-slate-200/70">
97
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24 lg:py-32">
98
+ <div className="grid gap-12 lg:grid-cols-[1.1fr_0.9fr] items-center">
99
+ <div className="text-center lg:text-left">
100
+ <div className="inline-flex items-center gap-3 px-4 py-1.5 rounded-full bg-white border border-slate-200 shadow-sm mb-8 fade-up">
101
+ <span className="relative flex h-2 w-2">
102
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
103
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
104
+ </span>
105
+ <span className="text-sm font-bold text-slate-600 font-mono tracking-wide">
106
+ SYSTEM ONLINE
107
+ </span>
108
+ </div>
109
 
110
+ <h1 className="text-5xl md:text-6xl lg:text-7xl font-extrabold text-slate-900 tracking-tight mb-6 leading-[1.05]">
111
+ City infrastructure,
112
+ <span className="block bg-gradient-to-r from-emerald-600 via-cyan-600 to-blue-600 bg-clip-text text-transparent">
113
+ remapped by intelligence.
114
+ </span>
115
+ </h1>
116
 
117
+ <p className="text-lg md:text-xl text-slate-600 mb-10 max-w-2xl lg:max-w-none leading-relaxed">
118
+ A civic response system that blends vision AI,
119
+ geo-deduplication, and automated routing to turn every report
120
+ into a verified, trackable action.
121
+ </p>
122
 
123
+ <div className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start items-center">
124
+ <Link
125
+ href="/signup"
126
+ className="px-7 py-3.5 bg-slate-900 text-white font-bold rounded-xl shadow-xl shadow-slate-900/20 hover:shadow-slate-900/30 transition-all hover:-translate-y-1 flex items-center gap-2"
127
+ >
128
+ Report an Issue
129
+ <Smartphone className="w-5 h-5" />
130
+ </Link>
131
+ <Link
132
+ href="/signin"
133
+ className="px-7 py-3.5 bg-white text-slate-700 font-semibold rounded-xl border border-slate-200 hover:bg-slate-50 transition-all shadow-lg shadow-slate-200/50 hover:border-slate-300 flex items-center gap-2"
134
+ >
135
+ Track Status
136
+ <Activity className="w-5 h-5 text-emerald-600" />
137
+ </Link>
138
+ </div>
139
+ </div>
140
+
141
+ <div className="glass-panel rounded-3xl p-6 md:p-8 shadow-urban-xl float-slow">
142
+ <div className="flex items-center justify-between mb-6">
143
+ <div>
144
+ <p className="text-xs uppercase tracking-[0.2em] text-slate-500 font-semibold">
145
+ Ops Pulse
146
+ </p>
147
+ <p className="text-2xl font-bold text-slate-900">
148
+ Live Command Board
149
+ </p>
150
+ </div>
151
+ <div className="h-10 w-10 rounded-2xl bg-emerald-100 text-emerald-700 flex items-center justify-center">
152
+ <Activity className="w-5 h-5" />
153
+ </div>
154
+ </div>
155
 
156
+ <div id="stats" className="grid grid-cols-2 gap-4">
157
+ <StatCard
158
+ label="ISSUES RESOLVED"
159
+ value="12,405"
160
+ icon={CheckCircle2}
161
+ color="text-emerald-600 bg-emerald-50"
162
+ />
163
+ <StatCard
164
+ label="ACTIVE AGENTS"
165
+ value="84"
166
+ icon={Users}
167
+ color="text-cyan-600 bg-cyan-50"
168
+ />
169
+ <StatCard
170
+ label="AVG. REACTION"
171
+ value="1.2hrs"
172
+ icon={Zap}
173
+ color="text-amber-600 bg-amber-50"
174
+ />
175
+ <StatCard
176
+ label="CITIES LIVE"
177
+ value="3"
178
+ icon={Building2}
179
+ color="text-slate-700 bg-slate-100"
180
+ />
181
+ </div>
182
+
183
+ <div className="mt-6 rounded-2xl bg-slate-900 text-white p-4">
184
+ <div className="flex items-center justify-between text-xs uppercase tracking-[0.2em] text-slate-300">
185
+ <span>Routing</span>
186
+ <span className="text-emerald-400">Auto-assign</span>
187
+ </div>
188
+ <p className="mt-3 text-sm text-slate-200">
189
+ 18 active tasks prioritized and dispatched to nearest teams.
190
+ </p>
191
+ </div>
192
+ </div>
193
  </div>
194
  </div>
195
  </div>
196
 
197
+ <div
198
+ id="features"
199
+ className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-28"
200
+ >
201
  <div className="text-center mb-16">
202
+ <h2 className="text-3xl md:text-4xl font-bold text-slate-900 mb-4">
203
+ Core Capabilities
204
+ </h2>
205
  <p className="text-slate-500 max-w-2xl mx-auto text-lg">
206
+ Built on a microservices architecture designed for scale,
207
+ security, and speed.
208
  </p>
209
  </div>
210
+
211
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
212
  <FeatureCard
213
  icon={<Radio className="w-8 h-8 text-blue-600" />}
214
  title="Geo-Spatial AI"
 
229
  />
230
  </div>
231
  </div>
232
+
233
+ <div
234
+ id="roadmap"
235
+ className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-28"
236
+ >
237
+ <div className="glass-panel rounded-3xl p-8 md:p-12">
238
+ <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
239
+ <div>
240
+ <p className="text-xs uppercase tracking-[0.2em] text-slate-500 font-semibold">
241
+ Roadmap
242
+ </p>
243
+ <h3 className="text-3xl font-bold text-slate-900 mt-2">
244
+ Next deployments
245
+ </h3>
246
+ <p className="text-slate-500 mt-3 max-w-xl">
247
+ Expanding to new municipal zones with predictive maintenance,
248
+ citizen feedback loops, and real-time response SLAs.
249
+ </p>
250
+ </div>
251
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-4 w-full md:w-auto">
252
+ <div className="rounded-2xl bg-white border border-slate-200 p-4">
253
+ <p className="text-xs uppercase tracking-wide text-slate-400 font-semibold">
254
+ Phase 1
255
+ </p>
256
+ <p className="text-lg font-bold text-slate-900 mt-2">
257
+ North Zone
258
+ </p>
259
+ <p className="text-sm text-slate-500 mt-1">Q2 rollout</p>
260
+ </div>
261
+ <div className="rounded-2xl bg-white border border-slate-200 p-4">
262
+ <p className="text-xs uppercase tracking-wide text-slate-400 font-semibold">
263
+ Phase 2
264
+ </p>
265
+ <p className="text-lg font-bold text-slate-900 mt-2">
266
+ Predictive SLA
267
+ </p>
268
+ <p className="text-sm text-slate-500 mt-1">Q3 launch</p>
269
+ </div>
270
+ <div className="rounded-2xl bg-white border border-slate-200 p-4">
271
+ <p className="text-xs uppercase tracking-wide text-slate-400 font-semibold">
272
+ Phase 3
273
+ </p>
274
+ <p className="text-lg font-bold text-slate-900 mt-2">
275
+ Citizen Portal
276
+ </p>
277
+ <p className="text-sm text-slate-500 mt-1">Q4 release</p>
278
+ </div>
279
+ </div>
280
+ </div>
281
+ </div>
282
+ </div>
283
  </main>
284
 
285
+ <footer className="border-t border-slate-200 bg-white/80 py-12 relative z-10">
286
  <div className="max-w-7xl mx-auto px-4 text-center">
287
  <div className="flex justify-center items-center gap-2 mb-8 opacity-75 hover:opacity-100 transition-opacity">
288
+ <div className="w-6 h-6 bg-slate-900 rounded-md flex items-center justify-center text-white">
289
+ <MapPin className="w-3.5 h-3.5" />
290
+ </div>
291
+ <span className="text-sm font-bold tracking-wide text-slate-900">
292
+ CityTracker SYSTEMS
293
+ </span>
294
  </div>
295
  <p className="text-slate-500 text-sm">
296
  © 2026 Dept. of Public Works. Secure. Efficient. Transparent.
 
301
  );
302
  }
303
 
304
+ function NavLink({
305
+ href,
306
+ children,
307
+ }: {
308
+ href: string;
309
+ children: React.ReactNode;
310
+ }) {
311
  return (
312
+ <Link
313
+ href={href}
314
+ className="text-sm font-medium text-slate-500 hover:text-emerald-600 transition-colors"
315
+ >
316
  {children}
317
  </Link>
318
+ );
319
  }
320
 
321
+ function StatCard({
322
+ label,
323
+ value,
324
+ icon: Icon,
325
+ color,
326
+ }: {
327
+ label: string;
328
+ value: string;
329
+ icon: any;
330
+ color: string;
331
+ }) {
332
  return (
333
  <div className="p-4 rounded-2xl bg-white border border-slate-200 shadow-sm hover:shadow-md hover:border-slate-300 transition-all group">
334
  <div className="flex items-center gap-3 mb-2">
335
  <div className={`p-1.5 rounded-lg ${color}`}>
336
  <Icon className="w-4 h-4" />
337
  </div>
338
+ <span className="text-xs font-mono text-slate-500 font-bold tracking-wider">
339
+ {label}
340
+ </span>
341
  </div>
342
  <div className="text-2xl font-bold text-slate-900 group-hover:scale-105 transition-transform origin-left font-mono">
343
  {value}
344
  </div>
345
  </div>
346
+ );
347
  }
348
 
349
  function FeatureCard({
350
  icon,
351
  title,
352
  desc,
353
+ colorClass,
354
  }: {
355
  icon: React.ReactNode;
356
  title: string;
 
359
  }) {
360
  return (
361
  <div className="p-8 bg-white rounded-3xl border border-slate-200 shadow-xl shadow-slate-200/50 hover:shadow-2xl hover:shadow-slate-200/80 hover:-translate-y-1 transition-all group">
362
+ <div
363
+ className={`w-16 h-16 ${colorClass} rounded-2xl flex items-center justify-center mb-6 border border-slate-100 group-hover:scale-110 transition-transform duration-300`}
364
+ >
365
  {icon}
366
  </div>
367
+ <h3 className="text-xl font-bold text-slate-900 mb-3 tracking-tight">
368
+ {title}
369
+ </h3>
370
+ <p className="text-slate-500 leading-relaxed text-sm font-medium">
371
+ {desc}
372
+ </p>
373
  </div>
374
  );
375
  }
Frontend/app/user/issues/[id]/page.tsx CHANGED
@@ -1,16 +1,13 @@
1
  "use client";
2
- import { useEffect, useState } from "react";
3
- import { useParams, useRouter } from "next/navigation";
4
 
5
  export const runtime = "edge";
6
- import { apiGet } from "@/lib/api";
 
7
  import {
8
  ArrowLeft,
9
  MapPin,
10
- Calendar,
11
- CheckCircle2,
12
  Clock,
13
- AlertTriangle,
14
  ImageIcon,
15
  Activity,
16
  Maximize2,
@@ -28,45 +25,40 @@ interface Issue {
28
  id: string;
29
  description: string;
30
  state: string;
31
- city: string;
32
- locality: string;
33
  created_at: string;
34
- full_address: string;
35
- priority: number;
36
- category: string;
37
- confidence: number;
38
- image_urls: string[];
39
- annotated_urls: string[];
40
- validation_source: string;
41
- sla_deadline?: string;
42
- is_duplicate: boolean;
43
- history?: IssueEvent[];
 
 
 
 
 
 
 
 
 
 
 
 
44
  }
45
 
46
  export default function UserIssueDetailPage() {
47
  const params = useParams();
48
- const router = useRouter();
49
- const [issue, setIssue] = useState<Issue | null>(null);
50
- const [loading, setLoading] = useState(true);
51
-
52
- useEffect(() => {
53
- if (params.id) {
54
- fetchIssueDetails(params.id as string);
55
- }
56
- }, [params.id]);
57
-
58
- const fetchIssueDetails = async (id: string) => {
59
- try {
60
- // Using generic endpoint - typically users can access their own issues
61
- const data = await apiGet<Issue>(`/issues/${id}`);
62
- setIssue(data);
63
- } catch (error) {
64
- console.error("Failed to fetch issue details:", error);
65
- // alert("Failed to load issue details.");
66
- } finally {
67
- setLoading(false);
68
- }
69
- };
70
 
71
  const getStateBadge = (state: string) => {
72
  const styles: Record<string, string> = {
@@ -76,6 +68,10 @@ export default function UserIssueDetailPage() {
76
  resolved: "bg-emerald-100 text-emerald-800 border-emerald-200",
77
  closed: "bg-slate-100 text-slate-600 border-slate-200",
78
  rejected: "bg-red-100 text-red-800 border-red-200",
 
 
 
 
79
  };
80
  return (
81
  <span
@@ -86,287 +82,303 @@ export default function UserIssueDetailPage() {
86
  );
87
  };
88
 
89
- if (loading) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  return (
91
- <div className="min-h-screen bg-urban-bg flex items-center justify-center">
92
- <div className="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-urban-primary"></div>
 
 
 
 
 
 
 
 
 
93
  </div>
94
  );
95
  }
96
 
97
  if (!issue) {
98
  return (
99
- <div className="min-h-screen bg-urban-bg p-8 flex flex-col items-center justify-center">
100
  <h2 className="text-xl font-bold text-slate-700">Issue Not Found</h2>
101
- <Link href="/user" className="mt-4 text-urban-primary hover:underline">
 
 
 
102
  Return to Dashboard
103
  </Link>
104
  </div>
105
  );
106
  }
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  return (
109
- <div className="min-h-screen bg-urban-bg font-sans pb-12">
110
- {/* Ambient Background */}
111
- <div className="fixed inset-0 pointer-events-none overflow-hidden z-0">
112
- <div className="absolute top-[-10%] right-[-10%] w-[40%] h-[40%] bg-blue-400/10 rounded-full blur-[100px]"></div>
113
- <div className="absolute bottom-[-10%] left-[-10%] w-[30%] h-[30%] bg-urban-primary/5 rounded-full blur-[80px]"></div>
114
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
- <main className="relative z-10 max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
117
  {/* Header */}
118
- <div className="mb-8">
119
- <Link
120
- href="/user"
121
- className="inline-flex items-center gap-2 text-slate-500 hover:text-urban-primary mb-4 transition-colors font-medium"
122
- >
123
- <ArrowLeft className="w-4 h-4" /> Back to Dashboard
124
- </Link>
125
- <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
126
- <div>
127
- <div className="flex items-center gap-3 mb-2">
128
- <span className="font-mono text-slate-400 bg-white/50 px-2 py-0.5 rounded text-sm border border-slate-200">
129
- #{issue.id.slice(0, 8)}
130
  </span>
131
  {getStateBadge(issue.state)}
 
 
 
132
  </div>
133
- <h1 className="text-3xl font-bold text-slate-900 tracking-tight">
134
  {issue.category || "Reported Issue"}
135
  </h1>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  </div>
137
- {issue.priority && (
138
- <div className="text-right">
139
- <div className="text-xs text-slate-400 font-bold uppercase tracking-wider mb-1">
140
- Priority Level
 
141
  </div>
142
  <div
143
- className={`text-lg font-bold px-4 py-1 rounded-full border border-slate-200 inline-block bg-white shadow-sm ${
144
  issue.priority === 1
145
- ? "text-red-600 border-red-100 bg-red-50"
146
  : issue.priority === 2
147
- ? "text-orange-600 border-orange-100 bg-orange-50"
148
- : "text-slate-700"
149
  }`}
150
  >
151
  {issue.priority === 1
152
  ? "Critical"
153
  : issue.priority === 2
154
- ? "High"
155
- : "Normal"}
 
 
 
 
 
 
 
 
 
 
 
156
  </div>
157
  </div>
158
  )}
159
  </div>
160
  </div>
161
 
162
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
163
- {/* Left Column: Details & Evidence */}
164
- <div className="lg:col-span-2 space-y-8">
165
- {/* Analysis & Location */}
166
- <div className="card bg-white/80 backdrop-blur-md">
167
- <div className="flex items-center gap-3 mb-6 pb-4 border-b border-slate-100">
168
- <div className="p-2 bg-blue-50 text-blue-600 rounded-lg">
169
- <Activity className="w-5 h-5" />
170
- </div>
171
- <h2 className="text-lg font-bold text-slate-800">
172
- Issue Details & Analysis
173
- </h2>
174
- </div>
175
-
176
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
177
- <div>
178
- <label className="text-xs font-bold text-slate-400 uppercase tracking-wider block mb-2">
179
- Description
180
- </label>
181
- <p className="text-slate-800 font-medium leading-relaxed bg-slate-50 p-3 rounded-lg border border-slate-100">
182
- {issue.description || "No description provided."}
183
- </p>
184
- </div>
185
- <div>
186
- <label className="text-xs font-bold text-slate-400 uppercase tracking-wider block mb-2">
187
- AI Confidence
188
- </label>
189
- <div className="flex items-center gap-2">
190
- <div className="h-2 flex-1 bg-slate-100 rounded-full overflow-hidden">
191
- <div
192
- className="h-full bg-urban-primary rounded-full"
193
- style={{ width: `${(issue.confidence || 0) * 100}%` }}
194
- ></div>
195
- </div>
196
- <span className="font-mono text-sm font-bold text-slate-700">
197
- {issue.confidence
198
- ? `${(issue.confidence * 100).toFixed(1)}%`
199
- : "N/A"}
200
- </span>
201
- </div>
202
- </div>
203
  </div>
 
 
 
 
204
 
205
- <div>
206
- <label className="text-xs font-bold text-slate-400 uppercase tracking-wider block mb-2">
207
- Location
208
- </label>
209
- <div className="flex items-start gap-3 bg-slate-50 p-3 rounded-lg border border-slate-100">
210
- <MapPin className="w-5 h-5 text-urban-primary shrink-0 mt-0.5" />
211
- <div>
212
- <p className="text-slate-900 font-medium">
213
- {issue.full_address || issue.locality}
 
 
 
 
 
 
 
 
 
 
214
  </p>
215
- <p className="text-xs text-slate-500 mt-1">{issue.city}</p>
216
  </div>
217
- </div>
218
- </div>
219
  </div>
 
220
 
221
- {/* Evidence Gallery */}
222
- <div className="card bg-white/80 backdrop-blur-md">
223
- <div className="flex items-center gap-3 mb-6 pb-4 border-b border-slate-100">
224
- <div className="p-2 bg-purple-50 text-purple-600 rounded-lg">
225
- <ImageIcon className="w-5 h-5" />
226
- </div>
227
- <h2 className="text-lg font-bold text-slate-800">
228
- Evidence & AI Vision
229
- </h2>
230
  </div>
 
 
 
 
231
 
232
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
233
- {/* Original Image */}
234
  <div className="space-y-3">
235
- <div className="flex justify-between items-center">
236
- <h3 className="text-sm font-bold text-slate-700">
237
- Original Photo
238
- </h3>
239
- </div>
240
- <div className="relative aspect-video bg-slate-100 rounded-xl overflow-hidden border border-slate-200 shadow-sm group">
241
- {issue.image_urls?.[0] ? (
242
- <>
243
- <img
244
- src={issue.image_urls[0]}
245
- alt="Original Issue"
246
- className="w-full h-full object-cover"
247
- />
248
- <div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors flex items-center justify-center opacity-0 group-hover:opacity-100">
249
- <a
250
- href={issue.image_urls[0]}
251
- target="_blank"
252
- rel="noopener noreferrer"
253
- className="bg-white/90 backdrop-blur text-slate-900 px-4 py-2 rounded-full text-sm font-bold shadow-lg flex items-center gap-2 transform translate-y-2 group-hover:translate-y-0 transition-all"
254
- >
255
- <Maximize2 className="w-4 h-4" /> View Full Size
256
- </a>
257
- </div>
258
- </>
259
- ) : (
260
- <div className="flex items-center justify-center h-full text-slate-400">
261
- <ImageIcon className="w-8 h-8 opacity-50" />
262
- </div>
263
- )}
264
  </div>
265
  </div>
 
 
 
 
 
 
266
 
267
- {/* AI Annotated Image */}
268
  <div className="space-y-3">
269
- <div className="flex justify-between items-center">
270
- <h3 className="text-sm font-bold text-urban-primary flex items-center gap-2">
271
- <span className="w-2 h-2 rounded-full bg-urban-primary animate-pulse"></span>
272
- Vision Agent Analysis
273
- </h3>
274
- </div>
275
- <div className="relative aspect-video bg-slate-900 rounded-xl overflow-hidden border-2 border-urban-primary/20 shadow-lg shadow-urban-primary/5 group">
276
- {issue.annotated_urls?.[0] ? (
277
- <>
278
- <img
279
- src={issue.annotated_urls[0]}
280
- alt="AI Analysis"
281
- className="w-full h-full object-cover"
282
- />
283
- <div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors flex items-center justify-center opacity-0 group-hover:opacity-100">
284
- <a
285
- href={issue.annotated_urls[0]}
286
- target="_blank"
287
- rel="noopener noreferrer"
288
- className="bg-urban-primary/90 backdrop-blur text-white px-4 py-2 rounded-full text-sm font-bold shadow-lg flex items-center gap-2 transform translate-y-2 group-hover:translate-y-0 transition-all"
289
- >
290
- <Maximize2 className="w-4 h-4" /> Inspect Analysis
291
- </a>
292
- </div>
293
- </>
294
- ) : (
295
- <div className="flex flex-col items-center justify-center h-full text-slate-500 bg-slate-50/5">
296
- <Activity className="w-8 h-8 opacity-50 mb-2" />
297
- <span className="text-xs">
298
- Processing visualization...
299
- </span>
300
- </div>
301
- )}
302
-
303
- {/* Badge Overlay */}
304
- <div className="absolute bottom-3 right-3">
305
- <span className="bg-black/60 backdrop-blur-md text-white text-[10px] font-mono px-2 py-1 rounded border border-white/10 flex items-center gap-1.5">
306
- <div className="w-1.5 h-1.5 bg-green-400 rounded-full"></div>
307
- Object Detection v2
308
- </span>
309
- </div>
310
  </div>
311
  </div>
312
- </div>
313
- </div>
314
- </div>
315
-
316
- {/* Right Column: Timeline */}
317
- <div className="space-y-8">
318
- <div className="card bg-white/80 backdrop-blur-md h-full">
319
- <div className="flex items-center gap-3 mb-6 pb-4 border-b border-slate-100">
320
- <div className="p-2 bg-slate-100 text-slate-600 rounded-lg">
321
- <Clock className="w-5 h-5" />
322
- </div>
323
- <h2 className="text-lg font-bold text-slate-800">
324
- Status Timeline
325
- </h2>
326
- </div>
327
 
328
- <div className="relative pl-4 border-l-2 border-slate-100 space-y-8 py-2">
329
- {issue.state === "reported" && (
330
- <div className="relative">
331
- <span className="absolute -left-[21px] top-1 w-4 h-4 rounded-full border-2 border-blue-500 bg-white ring-4 ring-blue-50"></span>
332
- <h4 className="text-sm font-bold text-slate-900">
333
- Report Submitted
334
- </h4>
335
- <p className="text-xs text-slate-500 mt-1">
336
- Issue has been received and is being processed by our AI
337
- systems.
338
- </p>
339
- <span className="text-[10px] font-mono text-slate-400 mt-2 block">
340
- {new Date(issue.created_at).toLocaleString()}
341
- </span>
342
  </div>
343
- )}
344
-
345
- {issue.state === "resolved" && (
346
- <div className="relative">
347
- <span className="absolute -left-[21px] top-1 w-4 h-4 rounded-full border-2 border-emerald-500 bg-emerald-500 ring-4 ring-emerald-50"></span>
348
- <h4 className="text-sm font-bold text-emerald-700">
349
- Issue Resolved
350
- </h4>
351
- <p className="text-xs text-slate-500 mt-1">
352
- Work has been completed and verified.
353
- </p>
354
  </div>
355
- )}
356
-
357
- {/* Default Start */}
358
- <div className="relative opacity-60">
359
- <span className="absolute -left-[21px] top-1 w-4 h-4 rounded-full border-2 border-slate-300 bg-white"></span>
360
- <h4 className="text-sm font-bold text-slate-600">
361
- Issue Created
362
- </h4>
363
- <span className="text-[10px] font-mono text-slate-400 mt-1 block">
364
- {new Date(issue.created_at).toLocaleString()}
365
- </span>
366
  </div>
367
- </div>
368
  </div>
369
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  </div>
371
  </main>
372
  </div>
 
1
  "use client";
2
+ import { useParams } from "next/navigation";
 
3
 
4
  export const runtime = "edge";
5
+ import { useCachedFetch } from "@/hooks/useCachedFetch";
6
+ import { Skeleton } from "@/components/ui/Skeleton";
7
  import {
8
  ArrowLeft,
9
  MapPin,
 
 
10
  Clock,
 
11
  ImageIcon,
12
  Activity,
13
  Maximize2,
 
25
  id: string;
26
  description: string;
27
  state: string;
28
+ city: string | null;
29
+ locality: string | null;
30
  created_at: string;
31
+ updated_at: string;
32
+ full_address: string | null;
33
+ priority: number | null;
34
+ priority_reason: string | null;
35
+ category: string | null;
36
+ confidence: number | null;
37
+ detections_count: number | null;
38
+ validation_source: string | null;
39
+ geo_status: string | null;
40
+ is_duplicate: boolean | null;
41
+ parent_issue_id: string | null;
42
+ nearby_count: number | null;
43
+ department: string | null;
44
+ assigned_member: string | null;
45
+ image_urls: string[] | null;
46
+ annotated_urls: string[] | null;
47
+ proof_image_url: string | null;
48
+ sla_hours: number | null;
49
+ sla_deadline: string | null;
50
+ agent_flow: IssueEvent[] | null;
51
+ latitude: number | null;
52
+ longitude: number | null;
53
  }
54
 
55
  export default function UserIssueDetailPage() {
56
  const params = useParams();
57
+ const issueId = typeof params.id === "string" ? params.id : params.id?.[0];
58
+ const { data: issue, loading } = useCachedFetch<Issue>(
59
+ issueId ? `/issues/${issueId}` : "",
60
+ );
61
+ const contentLoading = loading && !issue;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
  const getStateBadge = (state: string) => {
64
  const styles: Record<string, string> = {
 
68
  resolved: "bg-emerald-100 text-emerald-800 border-emerald-200",
69
  closed: "bg-slate-100 text-slate-600 border-slate-200",
70
  rejected: "bg-red-100 text-red-800 border-red-200",
71
+ escalated: "bg-red-100 text-red-800 border-red-200",
72
+ validated: "bg-emerald-100 text-emerald-800 border-emerald-200",
73
+ pending_confirmation: "bg-slate-200 text-slate-700 border-slate-300",
74
+ pending_verification: "bg-indigo-100 text-indigo-800 border-indigo-200",
75
  };
76
  return (
77
  <span
 
82
  );
83
  };
84
 
85
+ const formatText = (value: string | null | undefined) =>
86
+ value && value.trim() ? value : "—";
87
+
88
+ const formatDate = (value: string | null | undefined) =>
89
+ value ? new Date(value).toLocaleString() : "—";
90
+
91
+ const formatBool = (value: boolean | null | undefined) =>
92
+ value === null || value === undefined ? "—" : value ? "Yes" : "No";
93
+
94
+ const formatNumber = (value: number | null | undefined) =>
95
+ value === null || value === undefined ? "—" : value.toString();
96
+
97
+ const formatConfidence = (value: number | null | undefined) =>
98
+ value === null || value === undefined
99
+ ? "—"
100
+ : `${(value * 100).toFixed(1)}%`;
101
+
102
+ const formatCoords = (value: number | null | undefined) =>
103
+ value === null || value === undefined ? "—" : value.toFixed(6);
104
+
105
+ if (contentLoading) {
106
  return (
107
+ <div className="min-h-screen bg-slate-50 flex items-center justify-center">
108
+ <div className="w-full max-w-7xl px-4 sm:px-6 lg:px-8">
109
+ <Skeleton className="h-20 w-full mb-8 rounded-3xl" />
110
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-10">
111
+ <div className="lg:col-span-2 space-y-8">
112
+ <Skeleton className="h-100 w-full rounded-3xl" />
113
+ <Skeleton className="h-50 w-full rounded-3xl" />
114
+ </div>
115
+ <Skeleton className="h-150 w-full rounded-3xl" />
116
+ </div>
117
+ </div>
118
  </div>
119
  );
120
  }
121
 
122
  if (!issue) {
123
  return (
124
+ <div className="min-h-screen bg-transparent p-8 flex flex-col items-center justify-center">
125
  <h2 className="text-xl font-bold text-slate-700">Issue Not Found</h2>
126
+ <Link
127
+ href="/user"
128
+ className="mt-4 text-urban-primary hover:underline font-bold"
129
+ >
130
  Return to Dashboard
131
  </Link>
132
  </div>
133
  );
134
  }
135
 
136
+ const addressText =
137
+ issue.full_address ||
138
+ [issue.locality, issue.city].filter(Boolean).join(", ") ||
139
+ "—";
140
+
141
+ const dataRows = [
142
+ { label: "Issue ID", value: issue.id },
143
+ { label: "State", value: formatText(issue.state) },
144
+ { label: "Category", value: formatText(issue.category) },
145
+ { label: "Description", value: formatText(issue.description) },
146
+ { label: "Full Address", value: addressText },
147
+ { label: "Locality", value: formatText(issue.locality) },
148
+ { label: "City", value: formatText(issue.city) },
149
+ { label: "Priority", value: formatNumber(issue.priority) },
150
+ { label: "Priority Reason", value: formatText(issue.priority_reason) },
151
+ { label: "Confidence", value: formatConfidence(issue.confidence) },
152
+ { label: "Detections", value: formatNumber(issue.detections_count) },
153
+ { label: "Validation Source", value: formatText(issue.validation_source) },
154
+ { label: "Geo Status", value: formatText(issue.geo_status) },
155
+ { label: "Duplicate", value: formatBool(issue.is_duplicate) },
156
+ { label: "Parent Issue", value: formatText(issue.parent_issue_id) },
157
+ { label: "Nearby Count", value: formatNumber(issue.nearby_count) },
158
+ { label: "Department", value: formatText(issue.department) },
159
+ { label: "Assigned Member", value: formatText(issue.assigned_member) },
160
+ { label: "SLA Hours", value: formatNumber(issue.sla_hours) },
161
+ { label: "SLA Deadline", value: formatDate(issue.sla_deadline) },
162
+ { label: "Created", value: formatDate(issue.created_at) },
163
+ { label: "Updated", value: formatDate(issue.updated_at) },
164
+ { label: "Latitude", value: formatCoords(issue.latitude) },
165
+ { label: "Longitude", value: formatCoords(issue.longitude) },
166
+ ];
167
+
168
  return (
169
+ <div className="min-h-screen bg-transparent font-sans pb-20">
170
+ <nav className="bg-white/80 backdrop-blur-md border-b border-slate-200 sticky top-0 z-50">
171
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
172
+ <div className="flex justify-between h-20 items-center">
173
+ <Link href="/user" className="flex items-center gap-2 group">
174
+ <div className="w-10 h-10 bg-slate-100 rounded-xl flex items-center justify-center group-hover:bg-urban-primary transition-colors">
175
+ <ArrowLeft className="w-5 h-5 text-slate-600 group-hover:text-white" />
176
+ </div>
177
+ <div>
178
+ <h1 className="text-xl font-bold text-slate-900 leading-tight">
179
+ Issue Details
180
+ </h1>
181
+ <p className="text-[10px] font-bold text-urban-primary uppercase tracking-widest">
182
+ Back to Dashboard
183
+ </p>
184
+ </div>
185
+ </Link>
186
+ </div>
187
+ </div>
188
+ </nav>
189
 
190
+ <main className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
191
  {/* Header */}
192
+ <div className="mb-10 bg-white/50 backdrop-blur-sm p-8 rounded-3xl border border-white shadow-xl shadow-slate-200/50">
193
+ <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-8">
194
+ <div className="flex-1">
195
+ <div className="flex flex-wrap items-center gap-4 mb-4">
196
+ <span className="font-mono text-xs font-bold text-slate-400 bg-slate-100 px-3 py-1.5 rounded-full border border-slate-200 uppercase tracking-tighter">
197
+ ID: {issue.id.slice(0, 12)}...
 
 
 
 
 
 
198
  </span>
199
  {getStateBadge(issue.state)}
200
+ <span className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em] px-3 py-1 border-l border-slate-200">
201
+ {issue.category || "General"}
202
+ </span>
203
  </div>
204
+ <h1 className="text-3xl md:text-4xl font-black text-slate-900 tracking-tight leading-tight uppercase">
205
  {issue.category || "Reported Issue"}
206
  </h1>
207
+ <p className="text-slate-600 font-medium text-lg mt-3 max-w-3xl">
208
+ {issue.description || "No description provided."}
209
+ </p>
210
+ <div className="flex flex-wrap items-center gap-6 mt-6">
211
+ <div className="flex items-center gap-2 text-slate-500 font-medium bg-slate-50 px-3 py-1.5 rounded-xl border border-slate-100">
212
+ <MapPin className="w-4 h-4 text-urban-primary" />
213
+ <span className="text-sm">{addressText}</span>
214
+ </div>
215
+ <div className="flex items-center gap-2 text-slate-500 font-medium bg-slate-50 px-3 py-1.5 rounded-xl border border-slate-100">
216
+ <Clock className="w-4 h-4 text-amber-500" />
217
+ <span className="text-sm">
218
+ Reported {new Date(issue.created_at).toLocaleDateString()}
219
+ </span>
220
+ </div>
221
+ </div>
222
  </div>
223
+
224
+ {issue.priority !== null && (
225
+ <div className="flex flex-col items-center lg:items-end gap-3 p-6 bg-white/70 backdrop-blur-sm rounded-2xl border border-slate-200 shadow-sm min-w-50">
226
+ <div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">
227
+ Priority Rating
228
  </div>
229
  <div
230
+ className={`text-2xl font-black ${
231
  issue.priority === 1
232
+ ? "text-red-500"
233
  : issue.priority === 2
234
+ ? "text-orange-500"
235
+ : "text-emerald-500"
236
  }`}
237
  >
238
  {issue.priority === 1
239
  ? "Critical"
240
  : issue.priority === 2
241
+ ? "High Priority"
242
+ : "Standard"}
243
+ </div>
244
+ <div className="w-full h-1.5 bg-slate-100 rounded-full mt-2 overflow-hidden">
245
+ <div
246
+ className={`h-full transition-all duration-1000 ${
247
+ issue.priority === 1
248
+ ? "bg-red-500 w-full"
249
+ : issue.priority === 2
250
+ ? "bg-orange-500 w-2/3"
251
+ : "bg-emerald-500 w-1/3"
252
+ }`}
253
+ />
254
  </div>
255
  </div>
256
  )}
257
  </div>
258
  </div>
259
 
260
+ <div className="max-w-5xl mx-auto space-y-10">
261
+ {/* Metadata Grid */}
262
+ <div className="bg-white/70 backdrop-blur-md rounded-3xl border border-slate-200 p-8 shadow-sm">
263
+ <div className="flex items-center gap-3 mb-8">
264
+ <div className="w-10 h-10 bg-urban-primary/10 rounded-xl flex items-center justify-center">
265
+ <Activity className="w-5 h-5 text-urban-primary" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  </div>
267
+ <h2 className="text-xl font-black text-slate-900 uppercase tracking-tight">
268
+ System Analysis
269
+ </h2>
270
+ </div>
271
 
272
+ <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8">
273
+ {dataRows
274
+ .filter(
275
+ (row) =>
276
+ row.value !== "—" &&
277
+ row.value !== "" &&
278
+ row.value !== null &&
279
+ row.value !== undefined,
280
+ )
281
+ .map((row, i) => (
282
+ <div key={i} className="group">
283
+ <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1 group-hover:text-urban-primary transition-colors">
284
+ {row.label}
285
+ </label>
286
+ <p
287
+ className="text-sm font-bold text-slate-700 truncate"
288
+ title={row.value?.toString()}
289
+ >
290
+ {row.value}
291
  </p>
 
292
  </div>
293
+ ))}
 
294
  </div>
295
+ </div>
296
 
297
+ <div className="bg-white/70 backdrop-blur-md rounded-3xl border border-slate-200 p-8 shadow-sm">
298
+ <div className="flex items-center gap-3 mb-8">
299
+ <div className="w-10 h-10 bg-purple-100 rounded-xl flex items-center justify-center">
300
+ <ImageIcon className="w-5 h-5 text-purple-600" />
 
 
 
 
 
301
  </div>
302
+ <h2 className="text-xl font-black text-slate-900 uppercase tracking-tight">
303
+ Visual Evidence
304
+ </h2>
305
+ </div>
306
 
307
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
308
+ {issue.image_urls?.[0] ? (
309
  <div className="space-y-3">
310
+ <p className="text-xs font-black text-slate-400 uppercase tracking-widest">
311
+ Original Capture
312
+ </p>
313
+ <div className="relative aspect-video rounded-2xl overflow-hidden border border-slate-200 group">
314
+ <img
315
+ src={issue.image_urls[0]}
316
+ alt="Evidence"
317
+ className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
318
+ />
319
+ <div className="absolute inset-0 bg-linear-to-t from-black/40 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  </div>
321
  </div>
322
+ ) : (
323
+ <div className="h-40 bg-slate-50 rounded-2xl border-2 border-dashed border-slate-200 flex flex-col items-center justify-center text-slate-400">
324
+ <ImageIcon className="w-8 h-8 mb-2 opacity-20" />
325
+ <p className="text-xs font-bold">No evidence attached</p>
326
+ </div>
327
+ )}
328
 
329
+ {issue.annotated_urls?.[0] && (
330
  <div className="space-y-3">
331
+ <p className="text-xs font-black text-urban-primary uppercase tracking-widest">
332
+ AI Vision Analysis
333
+ </p>
334
+ <div className="relative aspect-video rounded-2xl overflow-hidden border border-urban-primary/30 group">
335
+ <img
336
+ src={issue.annotated_urls[0]}
337
+ alt="AI Analysis"
338
+ className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
339
+ />
340
+ <div className="absolute inset-0 bg-linear-to-t from-urban-primary/40 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
  </div>
342
  </div>
343
+ )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
 
345
+ {issue.proof_image_url ? (
346
+ <div className="space-y-3">
347
+ <p className="text-xs font-black text-slate-400 uppercase tracking-widest">
348
+ Resolution Proof
349
+ </p>
350
+ <div className="relative aspect-video rounded-2xl overflow-hidden border border-emerald-200 group">
351
+ <img
352
+ src={issue.proof_image_url}
353
+ alt="Proof"
354
+ className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
355
+ />
356
+ <div className="absolute inset-0 bg-linear-to-t from-emerald-900/40 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
 
 
357
  </div>
358
+ </div>
359
+ ) : (
360
+ <div className="h-40 bg-slate-50 rounded-2xl border-2 border-dashed border-slate-200 flex flex-col items-center justify-center text-slate-400">
361
+ <div className="w-8 h-8 bg-slate-200 rounded-full flex items-center justify-center mb-2 animate-pulse">
362
+ <Clock className="w-4 h-4 text-slate-400" />
 
 
 
 
 
 
363
  </div>
364
+ <p className="text-xs font-bold">Awaiting resolution proof</p>
 
 
 
 
 
 
 
 
 
 
365
  </div>
366
+ )}
367
  </div>
368
  </div>
369
+
370
+ <div className="bg-white rounded-3xl p-8 border border-slate-200 shadow-sm max-w-2xl mx-auto text-center">
371
+ <h3 className="text-xs font-black text-slate-400 uppercase tracking-widest mb-4">
372
+ Support
373
+ </h3>
374
+ <p className="text-sm text-slate-600 mb-6 font-medium leading-relaxed">
375
+ If this issue remains unresolved or was flagged incorrectly,
376
+ please contact your local department office.
377
+ </p>
378
+ <button className="w-full md:w-auto px-12 py-4 bg-slate-100 hover:bg-slate-200 text-slate-900 text-xs font-black rounded-2xl uppercase tracking-widest transition-all">
379
+ Contact Support
380
+ </button>
381
+ </div>
382
  </div>
383
  </main>
384
  </div>
Frontend/app/user/page.tsx CHANGED
@@ -1,28 +1,26 @@
1
  "use client";
2
- import { useEffect, useState } from "react";
 
3
  import { useRouter } from "next/navigation";
4
  import { useAuth } from "@/components/AuthProvider";
5
- import { apiGet } from "@/lib/api";
6
  import { useCachedFetch } from "@/hooks/useCachedFetch";
7
  import { Smartphone, FileText, MapPin } from "lucide-react";
8
-
9
- const API_URL = process.env.NEXT_PUBLIC_API_URL;
10
- if (!API_URL) throw new Error("Missing NEXT_PUBLIC_API_URL");
11
 
12
  interface Issue {
13
  id: string;
14
  description: string;
15
- priority: number;
16
  state: string;
17
- city: string;
18
- locality: string;
 
19
  created_at: string;
20
- primary_category: string;
21
- classification?: {
22
- primary_category: string;
23
- confidence: number;
24
- };
25
- images?: { file_path: string; annotated_path: string }[];
26
  }
27
 
28
  interface IssuesResponse {
@@ -43,12 +41,11 @@ export default function UserDashboard() {
43
 
44
  // Only fetch if user ID is available
45
  const fetchUrl = user?.id ? `/issues?user_id=${user.id}` : "";
46
- const { data: issuesResponse, loading: issuesLoading } = useCachedFetch<IssuesResponse>(fetchUrl);
 
47
 
48
  const issues = issuesResponse?.items || [];
49
-
50
- // Create a combined loading state, but prioritize showing dashboard shell if auth is done
51
- const contentLoading = authLoading || (issuesLoading && issues.length === 0);
52
 
53
  const getStateBadge = (state: string) => {
54
  const styles: Record<string, string> = {
@@ -57,6 +54,10 @@ export default function UserDashboard() {
57
  in_progress: "bg-orange-100 text-orange-800",
58
  resolved: "bg-green-100 text-green-800",
59
  closed: "bg-slate-100 text-slate-600",
 
 
 
 
60
  };
61
  return (
62
  <span
@@ -76,24 +77,38 @@ export default function UserDashboard() {
76
  }
77
 
78
  return (
79
- <div className="min-h-screen bg-slate-50 font-sans">
80
- <nav className="bg-white border-b border-slate-200 sticky top-0 z-50">
81
- <div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
82
- <div className="flex justify-between h-16 items-center">
83
- <div>
84
- <h1 className="text-xl font-bold text-slate-900">My Reports</h1>
85
- <p className="text-xs text-slate-500">Citizen Portal</p>
86
- </div>
87
- <div className="flex items-center gap-4">
88
- <div className="text-right hidden sm:block">
89
- <p className="text-sm font-medium text-slate-900">
90
- {user?.user_metadata?.full_name || user?.email || "User"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  </p>
92
- <p className="text-xs text-slate-500">{user?.email}</p>
93
  </div>
94
  <button
95
  onClick={signOut}
96
- className="px-4 py-2 bg-white border border-slate-200 text-red-600 font-medium rounded-lg hover:bg-red-50 hover:border-red-100 transition shadow-sm"
97
  >
98
  Sign Out
99
  </button>
@@ -102,114 +117,203 @@ export default function UserDashboard() {
102
  </div>
103
  </nav>
104
 
105
- <main className="max-w-5xl mx-auto px-4 py-8">
106
- <div className="mb-8 p-6 bg-blue-50 border border-blue-100 rounded-xl flex items-start gap-4">
107
- <div className="p-2 bg-blue-100 rounded-lg">
108
- <Smartphone className="w-6 h-6 text-blue-600" />
109
- </div>
110
- <div>
111
- <h3 className="text-blue-900 font-bold text-lg mb-1">
112
- Make a New Report
113
  </h3>
114
- <p className="text-blue-700 leading-relaxed">
115
- To ensure data accuracy and GPS verification, new issues must be
116
- reported through the official{" "}
117
- <strong>City Issue Mobile App</strong>.
118
  </p>
119
  </div>
 
 
 
 
 
 
 
 
 
 
 
120
  </div>
121
 
122
- <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
123
- <div className="p-6 bg-white rounded-xl border border-slate-200 shadow-sm">
124
- <p className="text-slate-500 text-sm font-medium uppercase tracking-wide">
125
  Total Reports
126
  </p>
127
- <p className="text-3xl font-extrabold text-slate-900 mt-2">
128
- {issues.length}
129
- </p>
 
 
 
 
130
  </div>
131
- <div className="p-6 bg-white rounded-xl border border-slate-200 shadow-sm">
132
- <p className="text-slate-500 text-sm font-medium uppercase tracking-wide">
133
  Resolved
134
  </p>
135
- <p className="text-3xl font-extrabold text-emerald-600 mt-2">
136
- {issues.filter((i) => i.state === "resolved").length}
137
- </p>
 
 
 
 
138
  </div>
139
- <div className="p-6 bg-white rounded-xl border border-slate-200 shadow-sm">
140
- <p className="text-slate-500 text-sm font-medium uppercase tracking-wide">
141
- In Progress
142
  </p>
143
- <p className="text-3xl font-extrabold text-amber-500 mt-2">
144
- {
145
- issues.filter((i) =>
146
- ["assigned", "in_progress"].includes(i.state),
147
- ).length
148
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  </div>
151
  </div>
152
 
153
- <h2 className="text-xl font-bold text-slate-900 mb-6">
154
- Recent Activity
155
- </h2>
156
-
157
- {issues.length === 0 ? (
158
- <div className="text-center py-16 bg-white rounded-xl border border-slate-200 shadow-sm">
159
- <FileText className="w-12 h-12 mx-auto text-slate-300" />
160
- <p className="text-slate-900 font-medium text-lg mt-4">
161
- No reports submitted yet
162
  </p>
163
- <p className="text-slate-500 mt-2">
164
- Download the mobile app to start contributing to your city.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  </p>
166
  </div>
167
  ) : (
168
- <div className="space-y-4">
169
  {issues.map((issue) => (
170
- <div
171
  key={issue.id}
172
- className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm hover:shadow-md transition-shadow"
 
173
  >
174
- <div className="flex flex-col sm:flex-row justify-between items-start gap-4 mb-4">
175
  <div className="flex items-center gap-3">
176
  {getStateBadge(issue.state)}
177
- <span className="text-xs font-bold text-slate-500 uppercase tracking-wide px-2 border-l border-slate-200">
178
- {issue.classification?.primary_category ||
179
- issue.primary_category ||
180
- "General Issue"}
181
  </span>
182
  </div>
183
- <span className="text-sm text-slate-400 font-medium">
184
  {new Date(issue.created_at).toLocaleDateString(undefined, {
185
- month: "long",
186
  day: "numeric",
187
  year: "numeric",
188
  })}
189
  </span>
190
  </div>
191
 
192
- <h3 className="text-lg font-bold text-slate-900 mb-2">
193
  {issue.description || "No description provided"}
194
  </h3>
195
 
196
- <div className="flex items-center gap-2 text-sm text-slate-500">
197
- <span className="flex items-center gap-1">
198
- <MapPin className="w-4 h-4 text-slate-400" />
199
- {issue.locality ? `${issue.locality}, ` : ""}
200
- {issue.city}
201
- </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  </div>
203
- </div>
204
  ))}
205
  </div>
206
  )}
207
  </main>
208
 
209
- <footer className="mt-12 py-8 bg-white border-t border-slate-200">
210
- <div className="max-w-5xl mx-auto px-4 text-center">
211
- <p className="text-sm text-slate-400">
212
- 2026 City Department of Public Works - Secure Citizen Portal
 
 
 
213
  </p>
214
  </div>
215
  </footer>
 
1
  "use client";
2
+ import { useEffect } from "react";
3
+ import Link from "next/link";
4
  import { useRouter } from "next/navigation";
5
  import { useAuth } from "@/components/AuthProvider";
 
6
  import { useCachedFetch } from "@/hooks/useCachedFetch";
7
  import { Smartphone, FileText, MapPin } from "lucide-react";
8
+ import { Skeleton } from "@/components/ui/Skeleton";
 
 
9
 
10
  interface Issue {
11
  id: string;
12
  description: string;
13
+ priority: number | null;
14
  state: string;
15
+ city: string | null;
16
+ locality: string | null;
17
+ full_address: string | null;
18
  created_at: string;
19
+ category: string | null;
20
+ confidence: number | null;
21
+ image_urls: string[];
22
+ annotated_urls: string[];
23
+ sla_deadline: string | null;
 
24
  }
25
 
26
  interface IssuesResponse {
 
41
 
42
  // Only fetch if user ID is available
43
  const fetchUrl = user?.id ? `/issues?user_id=${user.id}` : "";
44
+ const { data: issuesResponse, loading: issuesLoading } =
45
+ useCachedFetch<IssuesResponse>(fetchUrl);
46
 
47
  const issues = issuesResponse?.items || [];
48
+ const contentLoading = issuesLoading && !issuesResponse;
 
 
49
 
50
  const getStateBadge = (state: string) => {
51
  const styles: Record<string, string> = {
 
54
  in_progress: "bg-orange-100 text-orange-800",
55
  resolved: "bg-green-100 text-green-800",
56
  closed: "bg-slate-100 text-slate-600",
57
+ escalated: "bg-red-100 text-red-800",
58
+ validated: "bg-emerald-100 text-emerald-800",
59
+ pending_confirmation: "bg-slate-200 text-slate-700",
60
+ pending_verification: "bg-indigo-100 text-indigo-800",
61
  };
62
  return (
63
  <span
 
77
  }
78
 
79
  return (
80
+ <div className="min-h-screen bg-transparent font-sans">
81
+ <nav className="bg-white/80 backdrop-blur-md border-b border-slate-200 sticky top-0 z-50">
82
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
83
+ <div className="flex justify-between h-20 items-center">
84
+ <Link href="/" className="flex items-center gap-2 group">
85
+ <div className="w-10 h-10 bg-urban-primary rounded-xl flex items-center justify-center shadow-lg shadow-urban-primary/20 group-hover:scale-105 transition-transform">
86
+ <MapPin className="text-white w-6 h-6" />
87
+ </div>
88
+ <div>
89
+ <h1 className="text-xl font-bold text-slate-900 leading-tight">
90
+ CityTrack
91
+ </h1>
92
+ <p className="text-[10px] font-bold text-urban-primary uppercase tracking-widest">
93
+ Citizen Portal
94
+ </p>
95
+ </div>
96
+ </Link>
97
+
98
+ <div className="flex items-center gap-6">
99
+ <div className="text-right hidden md:block border-r border-slate-200 pr-6">
100
+ <p className="text-sm font-bold text-slate-900">
101
+ {user?.user_metadata?.full_name ||
102
+ user?.email?.split("@")[0] ||
103
+ "Citizen"}
104
+ </p>
105
+ <p className="text-xs text-slate-500 font-medium">
106
+ {user?.email}
107
  </p>
 
108
  </div>
109
  <button
110
  onClick={signOut}
111
+ className="px-5 py-2.5 bg-slate-900 text-white text-sm font-bold rounded-xl hover:bg-slate-800 transition-all shadow-lg shadow-slate-900/10 active:scale-95"
112
  >
113
  Sign Out
114
  </button>
 
117
  </div>
118
  </nav>
119
 
120
+ <main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
121
+ <div className="mb-12 p-8 bg-gradient-to-br from-urban-primary to-emerald-600 rounded-2xl shadow-xl shadow-urban-primary/10 flex flex-col md:flex-row items-center justify-between gap-8 relative overflow-hidden group">
122
+ <div className="absolute top-0 right-0 w-64 h-64 bg-white/10 rounded-full blur-3xl -mr-32 -mt-32" />
123
+ <div className="relative z-10 text-center md:text-left">
124
+ <h3 className="text-white font-bold text-2xl mb-2">
125
+ Report an Issue
 
 
126
  </h3>
127
+ <p className="text-emerald-50 max-w-xl leading-relaxed font-medium">
128
+ Notice something wrong in your neighborhood? Use our mobile app to
129
+ report issues with instant GPS tagging and AI verification.
 
130
  </p>
131
  </div>
132
+ <div className="relative z-10 flex flex-col sm:flex-row items-center gap-4">
133
+ <div className="flex items-center gap-3 bg-white/10 backdrop-blur-sm border border-white/20 px-4 py-3 rounded-xl">
134
+ <Smartphone className="w-6 h-6 text-white" />
135
+ <div className="text-left">
136
+ <p className="text-[10px] font-bold text-emerald-100 uppercase tracking-wider">
137
+ Available for
138
+ </p>
139
+ <p className="text-sm font-bold text-white">Android & iOS</p>
140
+ </div>
141
+ </div>
142
+ </div>
143
  </div>
144
 
145
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
146
+ <div className="p-6 bg-white/80 backdrop-blur-sm rounded-2xl border border-slate-200 shadow-sm hover:shadow-md transition-shadow">
147
+ <p className="text-slate-500 text-[10px] font-bold uppercase tracking-widest mb-1">
148
  Total Reports
149
  </p>
150
+ {contentLoading ? (
151
+ <Skeleton className="mt-2 h-10 w-24" />
152
+ ) : (
153
+ <p className="text-4xl font-black text-slate-900">
154
+ {issues.length}
155
+ </p>
156
+ )}
157
  </div>
158
+ <div className="p-6 bg-white/80 backdrop-blur-sm rounded-2xl border border-slate-200 shadow-sm hover:shadow-md transition-shadow">
159
+ <p className="text-emerald-600 text-[10px] font-bold uppercase tracking-widest mb-1">
160
  Resolved
161
  </p>
162
+ {contentLoading ? (
163
+ <Skeleton className="mt-2 h-10 w-24" />
164
+ ) : (
165
+ <p className="text-4xl font-black text-emerald-600">
166
+ {issues.filter((i) => i.state === "resolved").length}
167
+ </p>
168
+ )}
169
  </div>
170
+ <div className="p-6 bg-white/80 backdrop-blur-sm rounded-2xl border border-slate-200 shadow-sm hover:shadow-md transition-shadow">
171
+ <p className="text-amber-500 text-[10px] font-bold uppercase tracking-widest mb-1">
172
+ Active Cases
173
  </p>
174
+ {contentLoading ? (
175
+ <Skeleton className="mt-2 h-10 w-24" />
176
+ ) : (
177
+ <p className="text-4xl font-black text-amber-500">
178
+ {
179
+ issues.filter((i) =>
180
+ [
181
+ "reported",
182
+ "assigned",
183
+ "in_progress",
184
+ "pending_verification",
185
+ ].includes(i.state),
186
+ ).length
187
+ }
188
+ </p>
189
+ )}
190
+ </div>
191
+ <div className="p-6 bg-white/80 backdrop-blur-sm rounded-2xl border border-slate-200 shadow-sm hover:shadow-md transition-shadow">
192
+ <p className="text-urban-primary text-[10px] font-bold uppercase tracking-widest mb-1">
193
+ Efficiency
194
  </p>
195
+ {contentLoading ? (
196
+ <Skeleton className="mt-2 h-10 w-24" />
197
+ ) : (
198
+ <p className="text-4xl font-black text-urban-primary">
199
+ {issues.length > 0
200
+ ? Math.round(
201
+ (issues.filter((i) => i.state === "resolved").length /
202
+ issues.length) *
203
+ 100,
204
+ )
205
+ : 0}
206
+ %
207
+ </p>
208
+ )}
209
  </div>
210
  </div>
211
 
212
+ <div className="flex items-center justify-between mb-8">
213
+ <div>
214
+ <h2 className="text-2xl font-black text-slate-900">My Activity</h2>
215
+ <p className="text-sm text-slate-500 font-medium">
216
+ List of all reports submitted by you
 
 
 
 
217
  </p>
218
+ </div>
219
+ </div>
220
+
221
+ {contentLoading ? (
222
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
223
+ {Array.from({ length: 4 }).map((_, idx) => (
224
+ <div
225
+ key={`skeleton-${idx}`}
226
+ className="bg-white/60 p-6 rounded-2xl border border-slate-200 shadow-sm"
227
+ >
228
+ <div className="flex justify-between mb-4">
229
+ <Skeleton className="h-6 w-24 rounded-full" />
230
+ <Skeleton className="h-4 w-28" />
231
+ </div>
232
+ <Skeleton className="h-7 w-3/4 mb-4" />
233
+ <div className="space-y-2">
234
+ <Skeleton className="h-4 w-full" />
235
+ <Skeleton className="h-4 w-1/2" />
236
+ </div>
237
+ </div>
238
+ ))}
239
+ </div>
240
+ ) : issues.length === 0 ? (
241
+ <div className="text-center py-20 bg-white/40 backdrop-blur-sm rounded-2xl border-2 border-dashed border-slate-200">
242
+ <div className="w-20 h-20 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-6">
243
+ <FileText className="w-10 h-10 text-slate-300" />
244
+ </div>
245
+ <p className="text-slate-900 font-bold text-xl">No reports found</p>
246
+ <p className="text-slate-500 mt-2 max-w-sm mx-auto">
247
+ Your report history will appear here once you submit an issue
248
+ through the app.
249
  </p>
250
  </div>
251
  ) : (
252
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
253
  {issues.map((issue) => (
254
+ <Link
255
  key={issue.id}
256
+ href={`/user/issues/${issue.id}`}
257
+ className="group bg-white hover:bg-slate-50 p-6 rounded-2xl border border-slate-200 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300 flex flex-col h-full"
258
  >
259
+ <div className="flex justify-between items-start mb-4">
260
  <div className="flex items-center gap-3">
261
  {getStateBadge(issue.state)}
262
+ <span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">
263
+ {issue.category || "General"}
 
 
264
  </span>
265
  </div>
266
+ <span className="text-xs text-slate-400 font-bold bg-slate-50 px-2 py-1 rounded-md">
267
  {new Date(issue.created_at).toLocaleDateString(undefined, {
268
+ month: "short",
269
  day: "numeric",
270
  year: "numeric",
271
  })}
272
  </span>
273
  </div>
274
 
275
+ <h3 className="text-lg font-bold text-slate-900 mb-3 line-clamp-2 min-h-[3.5rem] group-hover:text-urban-primary transition-colors">
276
  {issue.description || "No description provided"}
277
  </h3>
278
 
279
+ <div className="mt-auto pt-4 border-t border-slate-100 space-y-3">
280
+ <div className="flex items-center gap-2 text-sm text-slate-500">
281
+ <div className="w-8 h-8 rounded-lg bg-slate-50 flex items-center justify-center flex-shrink-0">
282
+ <MapPin className="w-4 h-4 text-urban-primary" />
283
+ </div>
284
+ <span className="line-clamp-1 font-medium">
285
+ {issue.full_address ||
286
+ [issue.locality, issue.city]
287
+ .filter(Boolean)
288
+ .join(", ") ||
289
+ "Location pending"}
290
+ </span>
291
+ </div>
292
+
293
+ {issue.image_urls?.[0] && (
294
+ <div className="relative aspect-video rounded-xl overflow-hidden border border-slate-100 bg-slate-50 group-hover:shadow-inner">
295
+ <img
296
+ src={issue.image_urls[0]}
297
+ alt="Issue"
298
+ className="absolute inset-0 h-full w-full object-cover group-hover:scale-105 transition-transform duration-500"
299
+ />
300
+ <div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent" />
301
+ </div>
302
+ )}
303
  </div>
304
+ </Link>
305
  ))}
306
  </div>
307
  )}
308
  </main>
309
 
310
+ <footer className="mt-20 py-12 bg-white/80 backdrop-blur-md border-t border-slate-200">
311
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center text-slate-500">
312
+ <p className="text-sm font-bold uppercase tracking-widest text-slate-400 mb-2">
313
+ Developed for CityTrack
314
+ </p>
315
+ <p className="text-xs font-medium">
316
+ Improving Urban Life with AI Governance
317
  </p>
318
  </div>
319
  </footer>
Frontend/app/worker/layout.tsx CHANGED
@@ -5,7 +5,11 @@ import { useAuth } from "@/components/AuthProvider";
5
  import DashboardSidebar from "@/components/DashboardSidebar";
6
  import DashboardHeader from "@/components/DashboardHeader";
7
 
8
- export default function WorkerLayout({ children }: { children: React.ReactNode }) {
 
 
 
 
9
  const { role, loading, signOut } = useAuth();
10
  const [mobileOpen, setMobileOpen] = useState(false);
11
  const [isSidebarOpen, setIsSidebarOpen] = useState(false);
@@ -20,20 +24,25 @@ export default function WorkerLayout({ children }: { children: React.ReactNode }
20
  if (loading) return null;
21
 
22
  return (
23
- <div className="flex min-h-screen bg-slate-50 font-sans text-slate-900 overflow-hidden">
24
- <DashboardSidebar
25
- role="worker"
26
- mobileOpen={mobileOpen}
27
- setMobileOpen={setMobileOpen}
28
  desktopOpen={isSidebarOpen}
29
- onLogout={signOut}
30
  />
31
-
32
- <div className="flex-1 flex flex-col min-h-screen transition-all duration-300 h-screen overflow-hidden">
33
- <DashboardHeader
34
- setMobileOpen={setMobileOpen}
35
- toggleSidebar={() => setIsSidebarOpen(!isSidebarOpen)}
36
- title="Field Worker Portal"
 
 
 
 
 
37
  />
38
  <main className="flex-1 overflow-x-hidden overflow-y-auto p-4 sm:p-6 lg:p-8">
39
  {children}
 
5
  import DashboardSidebar from "@/components/DashboardSidebar";
6
  import DashboardHeader from "@/components/DashboardHeader";
7
 
8
+ export default function WorkerLayout({
9
+ children,
10
+ }: {
11
+ children: React.ReactNode;
12
+ }) {
13
  const { role, loading, signOut } = useAuth();
14
  const [mobileOpen, setMobileOpen] = useState(false);
15
  const [isSidebarOpen, setIsSidebarOpen] = useState(false);
 
24
  if (loading) return null;
25
 
26
  return (
27
+ <div className="flex min-h-screen bg-urban-bg font-sans text-slate-900 overflow-hidden relative">
28
+ <DashboardSidebar
29
+ role="worker"
30
+ mobileOpen={mobileOpen}
31
+ setMobileOpen={setMobileOpen}
32
  desktopOpen={isSidebarOpen}
33
+ onLogout={signOut}
34
  />
35
+
36
+ <div className="absolute top-0 left-0 w-full h-full pointer-events-none z-0 overflow-hidden">
37
+ <div className="absolute top-[-10%] right-[-5%] w-[40%] h-[40%] bg-urban-primary/5 rounded-full blur-[100px]"></div>
38
+ <div className="absolute bottom-[-10%] left-[-5%] w-[30%] h-[30%] bg-amber-500/5 rounded-full blur-[80px]"></div>
39
+ </div>
40
+
41
+ <div className="flex-1 flex flex-col min-h-screen transition-all duration-300 relative z-10 h-screen overflow-hidden">
42
+ <DashboardHeader
43
+ setMobileOpen={setMobileOpen}
44
+ toggleSidebar={() => setIsSidebarOpen(!isSidebarOpen)}
45
+ title="Field Worker Portal"
46
  />
47
  <main className="flex-1 overflow-x-hidden overflow-y-auto p-4 sm:p-6 lg:p-8">
48
  {children}
Frontend/app/worker/page.tsx CHANGED
@@ -1,10 +1,7 @@
1
  "use client";
2
- import { useEffect, useState } from "react";
3
- // Removed duplicate imports
4
  import { useRouter } from "next/navigation";
5
  import Link from "next/link";
6
  import { useAuth } from "@/components/AuthProvider";
7
- import { apiGet } from "@/lib/api";
8
  import {
9
  Coffee,
10
  MapPin,
@@ -31,23 +28,17 @@ interface Task {
31
 
32
  import { useCachedFetch } from "@/hooks/useCachedFetch";
33
 
34
- // ... existing imports
35
-
36
  export default function WorkerDashboard() {
37
  const { user, role, loading: authLoading } = useAuth();
38
  const router = useRouter();
39
-
40
- // Use cached fetch for instant load + background update
41
  const { data: tasksData, loading: tasksLoading } = useCachedFetch<Task[]>(
42
- role === "worker" ? "/worker/tasks" : ""
43
  );
44
 
45
  const tasks = tasksData || [];
46
-
47
- // Combine loading states
48
  const isLoading = authLoading || (tasksLoading && tasks.length === 0);
49
 
50
-
51
  const getPriorityBadge = (priority: number) => {
52
  const badges: Record<number, { bg: string; text: string; border: string }> =
53
  {
@@ -79,7 +70,9 @@ export default function WorkerDashboard() {
79
  <span
80
  className={`px-2.5 py-1 rounded-md text-xs font-bold border ${badge.bg} ${badge.text} ${badge.border} flex items-center gap-1.5`}
81
  >
82
- <span className={`w-1.5 h-1.5 rounded-full ${priority === 1 ? 'bg-red-500' : priority === 2 ? 'bg-orange-500' : priority === 3 ? 'bg-amber-500' : 'bg-emerald-500'}`}></span>
 
 
83
  {labels[priority] || "Unknown"}
84
  </span>
85
  );
@@ -113,20 +106,22 @@ export default function WorkerDashboard() {
113
  }
114
 
115
  return (
116
- <div className="space-y-6">
117
  <div className="flex justify-between items-center">
118
  <div>
119
- <h2 className="text-2xl font-bold text-slate-900 tracking-tight">My Assignments</h2>
120
- <p className="text-sm text-slate-500">
 
 
121
  Tasks assigned to you for resolution.
122
  </p>
123
  </div>
124
  </div>
125
 
126
  <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
127
- <div className="bg-white/60 backdrop-blur-md p-6 rounded-2xl border border-slate-200/60 shadow-urban-sm hover:shadow-urban-md transition-all">
128
  <div className="flex items-center gap-3 mb-2">
129
- <div className="p-2.5 bg-blue-50 text-blue-600 rounded-xl shadow-sm">
130
  <AlertCircle className="w-5 h-5" />
131
  </div>
132
  <h3 className="text-slate-500 font-bold text-xs uppercase tracking-wider font-mono">
@@ -136,12 +131,12 @@ export default function WorkerDashboard() {
136
  <p className="text-4xl font-extrabold text-slate-900 mt-2 tracking-tighter">
137
  {
138
  tasks.filter((t) =>
139
- ["assigned", "in_progress", "rejected"].includes(t.state)
140
  ).length
141
  }
142
  </p>
143
  </div>
144
- <div className="bg-white/60 backdrop-blur-md p-6 rounded-2xl border border-slate-200/60 shadow-urban-sm hover:shadow-urban-md transition-all">
145
  <div className="flex items-center gap-3 mb-2">
146
  <div className="p-2.5 bg-amber-50 text-amber-600 rounded-xl shadow-sm">
147
  <Coffee className="w-5 h-5" />
@@ -153,24 +148,22 @@ export default function WorkerDashboard() {
153
  <p className="text-4xl font-extrabold text-slate-900 mt-2 tracking-tighter">
154
  {
155
  tasks.filter((t) =>
156
- ["pending_verification", "resolved"].includes(t.state)
157
  ).length
158
  }
159
  </p>
160
  </div>
161
  </div>
162
 
163
- <h3 className="text-lg font-bold text-slate-900 mb-4 flex items-center gap-2">
164
- <span className="w-1 h-5 bg-blue-600 rounded-full"></span>
165
  Current Assignments
166
  </h3>
167
 
168
  {tasks.length === 0 ? (
169
- <div className="text-center py-16 bg-white/40 backdrop-blur-sm rounded-2xl border border-slate-200/60 border-dashed">
170
  <Coffee className="w-12 h-12 mx-auto text-slate-300 mb-4" />
171
- <p className="text-slate-900 font-bold text-lg">
172
- All caught up!
173
- </p>
174
  <p className="text-slate-500 text-sm mt-1">
175
  Enjoy your break, no pending assignments.
176
  </p>
@@ -179,8 +172,8 @@ export default function WorkerDashboard() {
179
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
180
  {tasks.map((task) => (
181
  <Link key={task.id} href={`/worker/task/${task.id}`}>
182
- <div className="h-full p-6 bg-white/70 backdrop-blur-md rounded-2xl border border-slate-200/60 shadow-urban-sm hover:shadow-urban-md hover:-translate-y-1 hover:border-blue-300/50 transition-all cursor-pointer group flex flex-col justify-between relative overflow-hidden">
183
- <div className="absolute top-0 left-0 w-1 h-full bg-slate-200 group-hover:bg-blue-500 transition-colors"></div>
184
  <div>
185
  <div className="flex justify-between items-start mb-4 pl-2">
186
  <div className="flex items-center gap-2 flex-wrap">
@@ -190,8 +183,8 @@ export default function WorkerDashboard() {
190
  task.state === "pending_verification"
191
  ? "bg-orange-50 text-orange-700 border-orange-200"
192
  : task.state === "resolved"
193
- ? "bg-emerald-50 text-emerald-700 border-emerald-200"
194
- : "bg-slate-100 text-slate-600 border-slate-200"
195
  }`}
196
  >
197
  {task.state === "pending_verification"
@@ -201,14 +194,14 @@ export default function WorkerDashboard() {
201
  </div>
202
  </div>
203
 
204
- <h3 className="text-xl font-bold text-slate-900 mb-2 pl-2 group-hover:text-blue-700 transition-colors line-clamp-2 tracking-tight">
205
  {task.description || "Issue Report"}
206
  </h3>
207
 
208
  <div className="py-3 pl-2 space-y-2.5">
209
  <div className="flex items-center gap-2.5 text-sm text-slate-600">
210
  <div className="p-1.5 bg-slate-100 rounded-md text-slate-500">
211
- <MapPin className="w-3.5 h-3.5" />
212
  </div>
213
  <span className="truncate font-medium">
214
  {task.full_address || `${task.city}, ${task.locality}`}
@@ -230,7 +223,7 @@ export default function WorkerDashboard() {
230
  <span className="text-xs text-slate-400 font-mono font-medium bg-slate-100 px-2 py-1 rounded">
231
  ID: {task.id.slice(0, 8)}
232
  </span>
233
- <span className="text-blue-600 text-sm font-bold flex items-center gap-1.5 group-hover:gap-2.5 transition-all bg-blue-50/50 px-3 py-1.5 rounded-lg border border-blue-100/50 group-hover:bg-blue-100 group-hover:border-blue-200">
234
  Resolve <ArrowRight className="w-4 h-4" />
235
  </span>
236
  </div>
 
1
  "use client";
 
 
2
  import { useRouter } from "next/navigation";
3
  import Link from "next/link";
4
  import { useAuth } from "@/components/AuthProvider";
 
5
  import {
6
  Coffee,
7
  MapPin,
 
28
 
29
  import { useCachedFetch } from "@/hooks/useCachedFetch";
30
 
 
 
31
  export default function WorkerDashboard() {
32
  const { user, role, loading: authLoading } = useAuth();
33
  const router = useRouter();
34
+
 
35
  const { data: tasksData, loading: tasksLoading } = useCachedFetch<Task[]>(
36
+ role === "worker" ? "/worker/tasks" : "",
37
  );
38
 
39
  const tasks = tasksData || [];
 
 
40
  const isLoading = authLoading || (tasksLoading && tasks.length === 0);
41
 
 
42
  const getPriorityBadge = (priority: number) => {
43
  const badges: Record<number, { bg: string; text: string; border: string }> =
44
  {
 
70
  <span
71
  className={`px-2.5 py-1 rounded-md text-xs font-bold border ${badge.bg} ${badge.text} ${badge.border} flex items-center gap-1.5`}
72
  >
73
+ <span
74
+ className={`w-1.5 h-1.5 rounded-full ${priority === 1 ? "bg-red-500" : priority === 2 ? "bg-orange-500" : priority === 3 ? "bg-amber-500" : "bg-emerald-500"}`}
75
+ ></span>
76
  {labels[priority] || "Unknown"}
77
  </span>
78
  );
 
106
  }
107
 
108
  return (
109
+ <div className="space-y-8 max-w-7xl mx-auto">
110
  <div className="flex justify-between items-center">
111
  <div>
112
+ <h2 className="text-2xl font-black text-slate-900 tracking-tight">
113
+ My Assignments
114
+ </h2>
115
+ <p className="text-sm text-slate-500 font-medium">
116
  Tasks assigned to you for resolution.
117
  </p>
118
  </div>
119
  </div>
120
 
121
  <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
122
+ <div className="bg-white/70 backdrop-blur-md p-6 rounded-2xl border border-slate-200/70 shadow-urban-sm hover:shadow-urban-md transition-all">
123
  <div className="flex items-center gap-3 mb-2">
124
+ <div className="p-2.5 bg-urban-primary/10 text-urban-primary rounded-xl shadow-sm">
125
  <AlertCircle className="w-5 h-5" />
126
  </div>
127
  <h3 className="text-slate-500 font-bold text-xs uppercase tracking-wider font-mono">
 
131
  <p className="text-4xl font-extrabold text-slate-900 mt-2 tracking-tighter">
132
  {
133
  tasks.filter((t) =>
134
+ ["assigned", "in_progress", "rejected"].includes(t.state),
135
  ).length
136
  }
137
  </p>
138
  </div>
139
+ <div className="bg-white/70 backdrop-blur-md p-6 rounded-2xl border border-slate-200/70 shadow-urban-sm hover:shadow-urban-md transition-all">
140
  <div className="flex items-center gap-3 mb-2">
141
  <div className="p-2.5 bg-amber-50 text-amber-600 rounded-xl shadow-sm">
142
  <Coffee className="w-5 h-5" />
 
148
  <p className="text-4xl font-extrabold text-slate-900 mt-2 tracking-tighter">
149
  {
150
  tasks.filter((t) =>
151
+ ["pending_verification", "resolved"].includes(t.state),
152
  ).length
153
  }
154
  </p>
155
  </div>
156
  </div>
157
 
158
+ <h3 className="text-lg font-black text-slate-900 mb-4 flex items-center gap-2">
159
+ <span className="w-1.5 h-5 bg-urban-primary rounded-full"></span>
160
  Current Assignments
161
  </h3>
162
 
163
  {tasks.length === 0 ? (
164
+ <div className="text-center py-16 bg-white/60 backdrop-blur-sm rounded-2xl border border-slate-200/70 border-dashed">
165
  <Coffee className="w-12 h-12 mx-auto text-slate-300 mb-4" />
166
+ <p className="text-slate-900 font-bold text-lg">All caught up!</p>
 
 
167
  <p className="text-slate-500 text-sm mt-1">
168
  Enjoy your break, no pending assignments.
169
  </p>
 
172
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
173
  {tasks.map((task) => (
174
  <Link key={task.id} href={`/worker/task/${task.id}`}>
175
+ <div className="h-full p-6 bg-white/70 backdrop-blur-md rounded-2xl border border-slate-200/70 shadow-urban-sm hover:shadow-urban-md hover:-translate-y-1 hover:border-urban-primary/30 transition-all cursor-pointer group flex flex-col justify-between relative overflow-hidden">
176
+ <div className="absolute top-0 left-0 w-1 h-full bg-slate-200 group-hover:bg-urban-primary transition-colors"></div>
177
  <div>
178
  <div className="flex justify-between items-start mb-4 pl-2">
179
  <div className="flex items-center gap-2 flex-wrap">
 
183
  task.state === "pending_verification"
184
  ? "bg-orange-50 text-orange-700 border-orange-200"
185
  : task.state === "resolved"
186
+ ? "bg-emerald-50 text-emerald-700 border-emerald-200"
187
+ : "bg-slate-100 text-slate-600 border-slate-200"
188
  }`}
189
  >
190
  {task.state === "pending_verification"
 
194
  </div>
195
  </div>
196
 
197
+ <h3 className="text-xl font-bold text-slate-900 mb-2 pl-2 group-hover:text-urban-primary transition-colors line-clamp-2 tracking-tight">
198
  {task.description || "Issue Report"}
199
  </h3>
200
 
201
  <div className="py-3 pl-2 space-y-2.5">
202
  <div className="flex items-center gap-2.5 text-sm text-slate-600">
203
  <div className="p-1.5 bg-slate-100 rounded-md text-slate-500">
204
+ <MapPin className="w-3.5 h-3.5" />
205
  </div>
206
  <span className="truncate font-medium">
207
  {task.full_address || `${task.city}, ${task.locality}`}
 
223
  <span className="text-xs text-slate-400 font-mono font-medium bg-slate-100 px-2 py-1 rounded">
224
  ID: {task.id.slice(0, 8)}
225
  </span>
226
+ <span className="text-urban-primary text-sm font-bold flex items-center gap-1.5 group-hover:gap-2.5 transition-all bg-urban-primary/10 px-3 py-1.5 rounded-lg border border-urban-primary/20 group-hover:bg-urban-primary/20 group-hover:border-urban-primary/30">
227
  Resolve <ArrowRight className="w-4 h-4" />
228
  </span>
229
  </div>
Frontend/app/worker/task/[id]/page.tsx CHANGED
@@ -124,7 +124,9 @@ export default function TaskDetailPage() {
124
 
125
  if (loading) {
126
  return (
127
- <div className="text-slate-600 font-medium">Loading Task Details...</div>
 
 
128
  );
129
  }
130
 
@@ -134,7 +136,7 @@ export default function TaskDetailPage() {
134
  <p className="text-slate-500 text-lg">Task not found</p>
135
  <button
136
  onClick={() => router.back()}
137
- className="mt-4 text-blue-600 font-medium hover:underline"
138
  >
139
  Go Back
140
  </button>
@@ -143,7 +145,7 @@ export default function TaskDetailPage() {
143
  }
144
 
145
  return (
146
- <div className="space-y-6">
147
  <div className="flex items-center justify-between">
148
  <button
149
  onClick={() => router.back()}
@@ -155,7 +157,7 @@ export default function TaskDetailPage() {
155
  href={`https://www.google.com/maps?q=${task.latitude},${task.longitude}`}
156
  target="_blank"
157
  rel="noopener noreferrer"
158
- className="px-5 py-2.5 bg-blue-600 text-white text-sm font-bold rounded-xl hover:bg-blue-700 transition shadow-lg hover:shadow-blue-500/30 flex items-center gap-2 transform hover:-translate-y-0.5"
159
  >
160
  <Navigation className="w-4 h-4" /> Navigation
161
  </a>
@@ -163,7 +165,7 @@ export default function TaskDetailPage() {
163
 
164
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
165
  <div className="space-y-6">
166
- <div className="bg-white/60 backdrop-blur-md rounded-2xl border border-slate-200/60 shadow-urban-sm overflow-hidden">
167
  {task.annotated_url ? (
168
  <div className="relative h-72 bg-slate-100 group">
169
  <img
@@ -171,9 +173,9 @@ export default function TaskDetailPage() {
171
  alt="Issue"
172
  className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
173
  />
174
- <div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-60"></div>
175
  <div className="absolute bottom-4 left-4 text-white">
176
- <p className="font-bold text-lg text-shadow-sm">{task.category}</p>
177
  </div>
178
  </div>
179
  ) : (
@@ -184,15 +186,19 @@ export default function TaskDetailPage() {
184
 
185
  <div className="p-6">
186
  <div className="flex items-center gap-2 mb-4">
187
- <span className={`px-2.5 py-1 rounded-md text-xs font-bold uppercase tracking-wide border ${
188
- task.state === 'pending_verification' ? 'bg-orange-50 text-orange-700 border-orange-200' :
189
- task.state === 'resolved' ? 'bg-emerald-50 text-emerald-700 border-emerald-200' :
190
- 'bg-blue-50 text-blue-700 border-blue-200'
191
- }`}>
 
 
 
 
192
  {task.state.replace("_", " ")}
193
  </span>
194
  <span className="text-xs font-bold text-slate-400 uppercase tracking-wider ml-auto">
195
- ID: {task.id.slice(0, 8)}
196
  </span>
197
  </div>
198
 
@@ -203,7 +209,7 @@ export default function TaskDetailPage() {
203
  {task.full_address}
204
  </p>
205
 
206
- <div className="grid grid-cols-2 gap-6 py-6 border-t border-slate-200/60">
207
  <div>
208
  <p className="text-xs font-bold text-slate-400 uppercase tracking-wide mb-1">
209
  Reported On
@@ -227,9 +233,9 @@ export default function TaskDetailPage() {
227
  </div>
228
  </div>
229
 
230
- <div className="bg-white/60 backdrop-blur-md rounded-2xl border border-slate-200/60 shadow-urban-sm p-8 h-fit sticky top-6">
231
- <h2 className="text-xl font-bold text-slate-900 mb-6 border-b border-slate-200/60 pb-4 flex items-center gap-2">
232
- <span className="w-1.5 h-6 bg-blue-600 rounded-full"></span>
233
  Task Action
234
  </h2>
235
 
@@ -242,7 +248,7 @@ export default function TaskDetailPage() {
242
  </p>
243
  <button
244
  onClick={handleStart}
245
- className="w-full py-4 bg-blue-600 hover:bg-blue-700 text-white font-bold text-lg rounded-xl shadow-lg hover:shadow-blue-500/40 transition-all flex items-center justify-center gap-2 transform hover:-translate-y-1"
246
  >
247
  Start Task
248
  </button>
@@ -268,8 +274,8 @@ export default function TaskDetailPage() {
268
  onClick={() => fileRef.current?.click()}
269
  className={`w-full p-8 border-2 border-dashed rounded-2xl transition-all group ${
270
  previewUrl
271
- ? "border-blue-500 bg-blue-50/50"
272
- : "border-slate-300 hover:border-blue-400 hover:bg-slate-50"
273
  }`}
274
  >
275
  {previewUrl ? (
@@ -279,14 +285,14 @@ export default function TaskDetailPage() {
279
  alt="Proof"
280
  className="h-48 mx-auto rounded-xl shadow-md object-cover mb-4"
281
  />
282
- <span className="text-sm font-bold text-blue-600 group-hover:text-blue-700">
283
  Change Photo
284
  </span>
285
  </div>
286
  ) : (
287
- <div className="flex flex-col items-center text-slate-400 group-hover:text-blue-500 transition-colors">
288
- <div className="p-4 bg-slate-100 rounded-full mb-3 group-hover:bg-blue-100 transition-colors">
289
- <Camera className="w-8 h-8" />
290
  </div>
291
  <span className="font-bold">Tap to Upload Photo</span>
292
  </div>
@@ -301,7 +307,7 @@ export default function TaskDetailPage() {
301
  <textarea
302
  value={notes}
303
  onChange={(e) => setNotes(e.target.value)}
304
- className="w-full px-4 py-3 bg-white/50 border border-slate-200 rounded-xl text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all resize-none"
305
  rows={4}
306
  placeholder="Describe the repair work completed..."
307
  />
@@ -325,7 +331,7 @@ export default function TaskDetailPage() {
325
  ) : task.state === "pending_verification" ? (
326
  <div className="text-center py-10 bg-orange-50/50 rounded-2xl border border-orange-100">
327
  <div className="w-16 h-16 bg-orange-100 rounded-full flex items-center justify-center mx-auto mb-4">
328
- <Loader2 className="w-8 h-8 text-orange-500 animate-spin" />
329
  </div>
330
  <h3 className="text-xl font-bold text-orange-900 mb-2">
331
  Under Review
@@ -337,7 +343,9 @@ export default function TaskDetailPage() {
337
  ) : (
338
  <div className="text-center py-10 bg-emerald-50/50 rounded-2xl border border-emerald-100">
339
  <div className="w-16 h-16 bg-emerald-100 rounded-full flex items-center justify-center mx-auto mb-4">
340
- <div className="w-8 h-8 text-emerald-600 font-bold text-2xl">✓</div>
 
 
341
  </div>
342
  <h3 className="text-xl font-bold text-emerald-900 mb-2">
343
  Task Completed
 
124
 
125
  if (loading) {
126
  return (
127
+ <div className="text-slate-600 font-medium max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
128
+ Loading Task Details...
129
+ </div>
130
  );
131
  }
132
 
 
136
  <p className="text-slate-500 text-lg">Task not found</p>
137
  <button
138
  onClick={() => router.back()}
139
+ className="mt-4 text-urban-primary font-medium hover:underline"
140
  >
141
  Go Back
142
  </button>
 
145
  }
146
 
147
  return (
148
+ <div className="space-y-6 max-w-7xl mx-auto">
149
  <div className="flex items-center justify-between">
150
  <button
151
  onClick={() => router.back()}
 
157
  href={`https://www.google.com/maps?q=${task.latitude},${task.longitude}`}
158
  target="_blank"
159
  rel="noopener noreferrer"
160
+ className="px-5 py-2.5 bg-urban-primary text-white text-sm font-bold rounded-xl hover:bg-emerald-600 transition shadow-lg hover:shadow-emerald-500/30 flex items-center gap-2 transform hover:-translate-y-0.5"
161
  >
162
  <Navigation className="w-4 h-4" /> Navigation
163
  </a>
 
165
 
166
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
167
  <div className="space-y-6">
168
+ <div className="bg-white/70 backdrop-blur-md rounded-2xl border border-slate-200/70 shadow-urban-sm overflow-hidden">
169
  {task.annotated_url ? (
170
  <div className="relative h-72 bg-slate-100 group">
171
  <img
 
173
  alt="Issue"
174
  className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
175
  />
176
+ <div className="absolute inset-0 bg-linear-to-t from-black/50 to-transparent opacity-60"></div>
177
  <div className="absolute bottom-4 left-4 text-white">
178
+ <p className="font-bold text-lg">{task.category}</p>
179
  </div>
180
  </div>
181
  ) : (
 
186
 
187
  <div className="p-6">
188
  <div className="flex items-center gap-2 mb-4">
189
+ <span
190
+ className={`px-2.5 py-1 rounded-md text-xs font-bold uppercase tracking-wide border ${
191
+ task.state === "pending_verification"
192
+ ? "bg-orange-50 text-orange-700 border-orange-200"
193
+ : task.state === "resolved"
194
+ ? "bg-emerald-50 text-emerald-700 border-emerald-200"
195
+ : "bg-urban-primary/10 text-urban-primary border-urban-primary/20"
196
+ }`}
197
+ >
198
  {task.state.replace("_", " ")}
199
  </span>
200
  <span className="text-xs font-bold text-slate-400 uppercase tracking-wider ml-auto">
201
+ ID: {task.id.slice(0, 8)}
202
  </span>
203
  </div>
204
 
 
209
  {task.full_address}
210
  </p>
211
 
212
+ <div className="grid grid-cols-2 gap-6 py-6 border-t border-slate-200/70">
213
  <div>
214
  <p className="text-xs font-bold text-slate-400 uppercase tracking-wide mb-1">
215
  Reported On
 
233
  </div>
234
  </div>
235
 
236
+ <div className="bg-white/70 backdrop-blur-md rounded-2xl border border-slate-200/70 shadow-urban-sm p-8 h-fit sticky top-6">
237
+ <h2 className="text-xl font-black text-slate-900 mb-6 border-b border-slate-200/70 pb-4 flex items-center gap-2">
238
+ <span className="w-1.5 h-6 bg-urban-primary rounded-full"></span>
239
  Task Action
240
  </h2>
241
 
 
248
  </p>
249
  <button
250
  onClick={handleStart}
251
+ className="w-full py-4 bg-urban-primary hover:bg-emerald-600 text-white font-bold text-lg rounded-xl shadow-lg hover:shadow-emerald-500/40 transition-all flex items-center justify-center gap-2 transform hover:-translate-y-1"
252
  >
253
  Start Task
254
  </button>
 
274
  onClick={() => fileRef.current?.click()}
275
  className={`w-full p-8 border-2 border-dashed rounded-2xl transition-all group ${
276
  previewUrl
277
+ ? "border-urban-primary bg-urban-primary/10"
278
+ : "border-slate-300 hover:border-urban-primary/50 hover:bg-slate-50"
279
  }`}
280
  >
281
  {previewUrl ? (
 
285
  alt="Proof"
286
  className="h-48 mx-auto rounded-xl shadow-md object-cover mb-4"
287
  />
288
+ <span className="text-sm font-bold text-urban-primary group-hover:text-emerald-700">
289
  Change Photo
290
  </span>
291
  </div>
292
  ) : (
293
+ <div className="flex flex-col items-center text-slate-400 group-hover:text-urban-primary transition-colors">
294
+ <div className="p-4 bg-slate-100 rounded-full mb-3 group-hover:bg-urban-primary/10 transition-colors">
295
+ <Camera className="w-8 h-8" />
296
  </div>
297
  <span className="font-bold">Tap to Upload Photo</span>
298
  </div>
 
307
  <textarea
308
  value={notes}
309
  onChange={(e) => setNotes(e.target.value)}
310
+ className="w-full px-4 py-3 bg-white/50 border border-slate-200 rounded-xl text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-4 focus:ring-urban-primary/10 focus:border-urban-primary/40 transition-all resize-none"
311
  rows={4}
312
  placeholder="Describe the repair work completed..."
313
  />
 
331
  ) : task.state === "pending_verification" ? (
332
  <div className="text-center py-10 bg-orange-50/50 rounded-2xl border border-orange-100">
333
  <div className="w-16 h-16 bg-orange-100 rounded-full flex items-center justify-center mx-auto mb-4">
334
+ <Loader2 className="w-8 h-8 text-orange-500 animate-spin" />
335
  </div>
336
  <h3 className="text-xl font-bold text-orange-900 mb-2">
337
  Under Review
 
343
  ) : (
344
  <div className="text-center py-10 bg-emerald-50/50 rounded-2xl border border-emerald-100">
345
  <div className="w-16 h-16 bg-emerald-100 rounded-full flex items-center justify-center mx-auto mb-4">
346
+ <div className="w-8 h-8 text-emerald-600 font-bold text-2xl">
347
+
348
+ </div>
349
  </div>
350
  <h3 className="text-xl font-bold text-emerald-900 mb-2">
351
  Task Completed
Frontend/components/DashboardHeader.tsx CHANGED
@@ -1,50 +1,53 @@
1
-
2
-
3
  "use client";
4
- import { useState } from "react";
5
  import { Search, Bell, Menu, User } from "lucide-react";
6
 
7
  interface HeaderProps {
8
- setMobileOpen: (open: boolean) => void;
9
- toggleSidebar?: () => void;
10
- title?: string;
11
  }
12
 
13
- export default function DashboardHeader({ setMobileOpen, toggleSidebar, title = "Dashboard" }: HeaderProps) {
14
- return (
15
- <header className="sticky top-0 z-30 flex h-16 w-full items-center justify-between border-b border-slate-200/60 bg-white/60 backdrop-blur-md px-4 shadow-sm sm:px-6 lg:px-8 transition-all duration-300">
16
- <div className="flex items-center gap-4">
17
- <button
18
- onClick={() => {
19
- setMobileOpen(true); // Always open mobile menu
20
- toggleSidebar?.(); // Toggle desktop if function exists
21
- }}
22
- className="rounded-lg p-2 text-slate-500 hover:bg-slate-100/80 hover:text-slate-700 transition-colors"
23
- >
24
- <Menu className="h-6 w-6" />
25
- </button>
26
- <h1 className="text-xl font-bold text-slate-900 tracking-tight hidden sm:block">{title}</h1>
27
- </div>
 
 
 
 
 
 
28
 
29
- <div className="flex items-center gap-4">
30
- <div className="hidden md:flex relative group">
31
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400 group-focus-within:text-blue-500 transition-colors" />
32
- <input
33
- type="text"
34
- placeholder="Search issues, workers..."
35
- className="h-10 w-64 rounded-full border border-slate-200 bg-slate-50/50 pl-10 pr-4 text-sm outline-none transition-all placeholder:text-slate-400 focus:border-blue-500/50 focus:bg-white focus:ring-4 focus:ring-blue-500/10"
36
- />
37
- </div>
38
 
39
- <button className="relative rounded-full bg-white p-2.5 text-slate-500 shadow-sm ring-1 ring-slate-200 hover:text-blue-600 hover:ring-blue-200 transition-all">
40
- <Bell className="h-5 w-5" />
41
- <span className="absolute top-2 right-2.5 h-2 w-2 rounded-full bg-orange-500 ring-2 ring-white"></span>
42
- </button>
43
 
44
- <div className="h-9 w-9 rounded-full bg-blue-100 flex items-center justify-center text-blue-700 font-bold border border-white shadow-sm ring-2 ring-blue-50">
45
- <User className="h-5 w-5" />
46
- </div>
47
- </div>
48
- </header>
49
- );
50
  }
 
 
 
1
  "use client";
 
2
  import { Search, Bell, Menu, User } from "lucide-react";
3
 
4
  interface HeaderProps {
5
+ setMobileOpen: (open: boolean) => void;
6
+ toggleSidebar?: () => void;
7
+ title?: string;
8
  }
9
 
10
+ export default function DashboardHeader({
11
+ setMobileOpen,
12
+ toggleSidebar,
13
+ title = "Dashboard",
14
+ }: HeaderProps) {
15
+ return (
16
+ <header className="sticky top-0 z-30 flex h-16 w-full items-center justify-between border-b border-slate-200/60 bg-white/70 backdrop-blur-md px-4 shadow-sm sm:px-6 lg:px-8 transition-all duration-300">
17
+ <div className="flex items-center gap-4">
18
+ <button
19
+ onClick={() => {
20
+ setMobileOpen(true);
21
+ toggleSidebar?.();
22
+ }}
23
+ className="rounded-lg p-2 text-slate-500 hover:bg-slate-100/80 hover:text-slate-700 transition-colors"
24
+ >
25
+ <Menu className="h-6 w-6" />
26
+ </button>
27
+ <h1 className="text-xl font-black text-slate-900 tracking-tight hidden sm:block">
28
+ {title}
29
+ </h1>
30
+ </div>
31
 
32
+ <div className="flex items-center gap-4">
33
+ <div className="hidden md:flex relative group">
34
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400 group-focus-within:text-urban-primary transition-colors" />
35
+ <input
36
+ type="text"
37
+ placeholder="Search issues, workers..."
38
+ className="h-10 w-64 rounded-full border border-slate-200 bg-slate-50/60 pl-10 pr-4 text-sm outline-none transition-all placeholder:text-slate-400 focus:border-urban-primary/40 focus:bg-white focus:ring-4 focus:ring-urban-primary/10"
39
+ />
40
+ </div>
41
 
42
+ <button className="relative rounded-full bg-white p-2.5 text-slate-500 shadow-sm ring-1 ring-slate-200 hover:text-urban-primary hover:ring-urban-primary/20 transition-all">
43
+ <Bell className="h-5 w-5" />
44
+ <span className="absolute top-2 right-2.5 h-2 w-2 rounded-full bg-amber-500 ring-2 ring-white"></span>
45
+ </button>
46
 
47
+ <div className="h-9 w-9 rounded-full bg-urban-primary/10 flex items-center justify-center text-urban-primary font-bold border border-white shadow-sm ring-2 ring-urban-primary/20">
48
+ <User className="h-5 w-5" />
49
+ </div>
50
+ </div>
51
+ </header>
52
+ );
53
  }
Frontend/components/DashboardSidebar.tsx CHANGED
@@ -37,7 +37,6 @@ export default function DashboardSidebar({
37
  }: SidebarProps) {
38
  const pathname = usePathname();
39
 
40
- // ... (links definition skipped for brevity if not changing, but we are inside function)
41
  const adminLinks = [
42
  { href: "/admin", label: "Overview", icon: LayoutDashboard },
43
  { href: "/admin/issues", label: "Issues", icon: ClipboardList },
@@ -70,14 +69,14 @@ export default function DashboardSidebar({
70
  >
71
  <div className="flex h-16 items-center px-6 border-b border-slate-100/50">
72
  <div className="flex items-center gap-2">
73
- <div className="h-8 w-8 rounded-lg bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center shadow-lg shadow-blue-500/20">
74
- <span className="text-white font-bold font-mono">U</span>
75
  </div>
76
  <span className="text-xl font-bold tracking-tight text-slate-900">
77
  CityTracker
78
  </span>
79
  </div>
80
- <span className="ml-auto rounded-full bg-blue-50 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-blue-600 ring-1 ring-blue-100">
81
  {role}
82
  </span>
83
  </div>
@@ -95,18 +94,18 @@ export default function DashboardSidebar({
95
  className={cn(
96
  "flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition-all duration-200 group relative overflow-hidden",
97
  isActive
98
- ? "bg-blue-50 text-blue-700 shadow-sm ring-1 ring-blue-100"
99
  : "text-slate-500 hover:bg-slate-50 hover:text-slate-900",
100
  )}
101
  >
102
  {isActive && (
103
- <div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-blue-500 rounded-r-full" />
104
  )}
105
  <Icon
106
  className={cn(
107
  "h-5 w-5 transition-transform group-hover:scale-110",
108
  isActive
109
- ? "text-blue-600"
110
  : "text-slate-400 group-hover:text-slate-600",
111
  )}
112
  />
 
37
  }: SidebarProps) {
38
  const pathname = usePathname();
39
 
 
40
  const adminLinks = [
41
  { href: "/admin", label: "Overview", icon: LayoutDashboard },
42
  { href: "/admin/issues", label: "Issues", icon: ClipboardList },
 
69
  >
70
  <div className="flex h-16 items-center px-6 border-b border-slate-100/50">
71
  <div className="flex items-center gap-2">
72
+ <div className="h-8 w-8 rounded-lg bg-gradient-to-br from-urban-primary to-emerald-600 flex items-center justify-center shadow-lg shadow-emerald-500/20">
73
+ <span className="text-white font-bold font-mono">C</span>
74
  </div>
75
  <span className="text-xl font-bold tracking-tight text-slate-900">
76
  CityTracker
77
  </span>
78
  </div>
79
+ <span className="ml-auto rounded-full bg-urban-primary/10 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-urban-primary ring-1 ring-urban-primary/20">
80
  {role}
81
  </span>
82
  </div>
 
94
  className={cn(
95
  "flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition-all duration-200 group relative overflow-hidden",
96
  isActive
97
+ ? "bg-urban-primary/10 text-urban-primary shadow-sm ring-1 ring-urban-primary/20"
98
  : "text-slate-500 hover:bg-slate-50 hover:text-slate-900",
99
  )}
100
  >
101
  {isActive && (
102
+ <div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-urban-primary rounded-r-full" />
103
  )}
104
  <Icon
105
  className={cn(
106
  "h-5 w-5 transition-transform group-hover:scale-110",
107
  isActive
108
+ ? "text-urban-primary"
109
  : "text-slate-400 group-hover:text-slate-600",
110
  )}
111
  />