diff --git a/app/admin/forms/volunteer/page.tsx b/app/admin/forms/volunteer/page.tsx new file mode 100644 index 00000000..d2b7fd17 --- /dev/null +++ b/app/admin/forms/volunteer/page.tsx @@ -0,0 +1,485 @@ +"use client"; + +import { useState, useEffect, useMemo } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { HandHeart, Search, MoreHorizontal, Eye, Download } from "lucide-react"; +import { toast } from "sonner"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +const INTEREST_LABELS: Record = { + "event-planning": "Event Planning & Organization", + "technical-support": "Technical Support (Coding & Mentoring)", + "community-management": "Community Management", + "marketing": "Marketing & Promotion", + "content-creation": "Content Creation", + "graphic-designing": "Graphic Designing", +}; + +const STATUS_COLORS: Record = { + pending: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200", + approved: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200", + rejected: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200", +}; + +// Define VolunteerApplication type +interface VolunteerApplication { + id: string; + first_name: string; + last_name: string; + email: string; + phone: string; + location: string; + occupation: string; + company: string; + experience: string; + skills: string; + interests: string[]; + motivation: string; + previous_volunteer: string; + status: string; + created_at: string; +} + +export default function AdminVolunteerPage() { + const [applications, setApplications] = useState([]); + const [searchTerm, setSearchTerm] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + const [loading, setLoading] = useState(true); + // Dialog state + const [viewDialogOpen, setViewDialogOpen] = useState(false); + const [selectedApp, setSelectedApp] = useState(null); + + // Refetch volunteers + const fetchVolunteers = async () => { + setLoading(true); + try { + const res = await fetch("/api/admin-volunteers"); + const json = await res.json(); + if (json.volunteers) setApplications(json.volunteers); + } catch { + // Optionally handle error + } + setLoading(false); + }; + + useEffect(() => { + fetchVolunteers(); + }, []); + + // Delete handler + const handleDelete = async (id: string) => { + if (!window.confirm("Are you sure you want to delete this application?")) return; + try { + const res = await fetch("/api/admin-volunteers", { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id }), + }); + const json = await res.json(); + if (json.success) { + toast.success("Application deleted successfully"); + fetchVolunteers(); + } else { + toast.error(json.error || "Failed to delete application"); + } + } catch { + toast.error("Failed to delete application"); + } + }; + + const filteredApplications = useMemo(() => { + return applications.filter((app) => { + const matchesSearch = + app.first_name?.toLowerCase().includes(searchTerm.toLowerCase()) || + app.last_name?.toLowerCase().includes(searchTerm.toLowerCase()) || + app.email?.toLowerCase().includes(searchTerm.toLowerCase()) || + app.phone?.toLowerCase().includes(searchTerm.toLowerCase()) || + app.location?.toLowerCase().includes(searchTerm.toLowerCase()) || + app.occupation?.toLowerCase().includes(searchTerm.toLowerCase()) || + app.company?.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesStatus = statusFilter === "all" || app.status === statusFilter; + return matchesSearch && matchesStatus; + }); + }, [applications, searchTerm, statusFilter]); + + // Stats + const total = applications.length; + const pending = applications.filter((a) => a.status === "pending").length; + const approved = applications.filter((a) => a.status === "approved").length; + const rejected = applications.filter((a) => a.status === "rejected").length; + const thisMonth = new Date().getMonth(); + const thisYear = new Date().getFullYear(); + const newThisMonth = applications.filter((a) => { + const d = new Date(a.created_at); + return d.getMonth() === thisMonth && d.getFullYear() === thisYear; + }).length; + + const stats = [ + { + title: "Total Applications", + value: total, + icon: HandHeart, + color: "text-pink-600", + bgColor: "bg-pink-50 dark:bg-pink-950/20", + }, + { + title: "Pending", + value: pending, + icon: HandHeart, + color: "text-yellow-600", + bgColor: "bg-yellow-50 dark:bg-yellow-950/20", + }, + { + title: "Approved", + value: approved, + icon: HandHeart, + color: "text-green-600", + bgColor: "bg-green-50 dark:bg-green-950/20", + }, + { + title: "Rejected", + value: rejected, + icon: HandHeart, + color: "text-red-600", + bgColor: "bg-red-50 dark:bg-red-950/20", + }, + { + title: "New This Month", + value: newThisMonth, + icon: HandHeart, + color: "text-rose-600", + bgColor: "bg-rose-50 dark:bg-rose-950/20", + }, + ]; + + const getStatusBadge = (status: string) => ( + {status.charAt(0).toUpperCase() + status.slice(1)} + ); + + return ( +
+
+ + + + + + + + + +
+
+ +
+

+ Volunteer Applications +

+

View and manage all volunteer applications

+
+
+ + {/* Stats Cards */} +
+ {stats.map((stat) => ( + + + {stat.title} +
+ +
+
+ +
{stat.value}
+
+
+ ))} +
+ + {/* Filters */} +
+
+ +
+
+
+ + setSearchTerm(e.target.value)} + className="pl-10 text-sm" + /> +
+ +
+
+ + {/* Table */} + + + + + Volunteer Applications + + + Search and filter through all volunteer applications + + + + {loading ? ( +
+ +
+ ) : filteredApplications.length === 0 ? ( +
+ +

No applications found

+

Try adjusting your search criteria

+
+ ) : ( +
+ + + + Name + Email + Phone + Status + Company + Interests + Applied + Actions + + + + {filteredApplications.map((app) => ( + + +
+ {app.first_name} {app.last_name} +
+
+ {app.email} + {app.phone} + {getStatusBadge(app.status)} + {app.company} + + {Array.isArray(app.interests) + ? app.interests.map((i: string) => ( + + {INTEREST_LABELS[i] || i} + + )) + : null} + + + {app.created_at ? new Date(app.created_at).toLocaleDateString() : "-"} + + + + + + + + Actions + { + setSelectedApp(app); + setViewDialogOpen(true); + }} + > + + View Application + + handleDelete(app.id)} + > + + Delete Application + + + + +
+ ))} +
+
+
+ )} +
+
+ + {/* View Application Dialog (outside the map) */} + + + + Volunteer Application Details + + Full details for {selectedApp?.first_name} {selectedApp?.last_name} + + + {selectedApp && ( +
+
+
+ +
{selectedApp.first_name}
+
+
+ +
{selectedApp.last_name}
+
+
+ +
{selectedApp.email}
+
+
+ +
{selectedApp.phone}
+
+
+ +
{selectedApp.location}
+
+
+ +
{selectedApp.occupation}
+
+
+ +
{selectedApp.company}
+
+
+
+ +
{selectedApp.experience}
+
+
+ +
{selectedApp.skills}
+
+
+ +
+ {Array.isArray(selectedApp.interests) + ? selectedApp.interests.map((i: string) => ( + + {INTEREST_LABELS[i] || i} + + )) + : null} +
+
+
+ +
{selectedApp.motivation}
+
+
+ +
{selectedApp.previous_volunteer}
+
+
+ +
{getStatusBadge(selectedApp.status)}
+
+
+ +
{selectedApp.created_at ? new Date(selectedApp.created_at).toLocaleString() : "-"}
+
+
+ )} + + {/* Approve/Reject buttons for admin */} + {selectedApp && ( +
+ + +
+ )} + {/* Close button removed, X icon in header is functional */} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/api/admin-volunteers/route.ts b/app/api/admin-volunteers/route.ts new file mode 100644 index 00000000..a631de6a --- /dev/null +++ b/app/api/admin-volunteers/route.ts @@ -0,0 +1,72 @@ +import { NextResponse } from "next/server"; +import { createClient } from "@supabase/supabase-js"; + +// Setup Supabase client with service role key (bypasses RLS) +const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! +); + +// GET: List all volunteer applications +export async function GET() { + const { data, error } = await supabase + .from("volunteer_applications") + .select("*") + .order("created_at", { ascending: false }); + + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } + return NextResponse.json({ volunteers: data }); +} + +// POST: Create a new volunteer application +export async function POST(req: Request) { + const body = await req.json(); + const { data, error } = await supabase + .from("volunteer_applications") + .insert([body]) + .select(); + + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } + return NextResponse.json({ volunteer: data[0] }); +} + +// PATCH: Update a volunteer application (expects { id, ...fields }) +export async function PATCH(req: Request) { + const body = await req.json(); + const { id, ...fields } = body; + if (!id) { + return NextResponse.json({ error: "Missing id" }, { status: 400 }); + } + const { data, error } = await supabase + .from("volunteer_applications") + .update(fields) + .eq("id", id) + .select(); + + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } + return NextResponse.json({ volunteer: data[0] }); +} + +// DELETE: Delete a volunteer application (expects { id }) +export async function DELETE(req: Request) { + const body = await req.json(); + const { id } = body; + if (!id) { + return NextResponse.json({ error: "Missing id" }, { status: 400 }); + } + const { error } = await supabase + .from("volunteer_applications") + .delete() + .eq("id", id); + + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } + return NextResponse.json({ success: true }); +} \ No newline at end of file