Files
encoach_frontend_new_v2/src/pages/admin/CredentialDashboard.tsx
Yamen Ahmad 11a7265460 feat(v3): restructure project + add complete frontend
- Restructure: move backend from new_project/ to backend/
- Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks)
- Add docs/ with SRS specs, user stories, and workflow documentation
- Update .gitignore for new directory layout

Workflows implemented:
  WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration,
  WF4 General English Exam, WF5 Course Generation,
  WF6 Entity Student Onboarding, AI Course Generation,
  Adaptive Learning Engine UI, White-Label Branding, Score Release

Made-with: Cursor
2026-04-10 17:26:42 +04:00

207 lines
8.1 KiB
TypeScript

import { useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
useCredentialStatuses,
useResendAllPendingCredentials,
useResendCredential,
} from "@/hooks/queries/useEntityOnboarding";
import type { CredentialStatus } from "@/types";
import { toast } from "sonner";
export default function CredentialDashboard() {
const [accountFilter, setAccountFilter] = useState<string>("all");
const [emailSentFilter, setEmailSentFilter] = useState<string>("all");
const [placementFilter, setPlacementFilter] = useState<string>("all");
const [dateFrom, setDateFrom] = useState("");
const [dateTo, setDateTo] = useState("");
const [selected, setSelected] = useState<Set<number>>(new Set());
const filters = useMemo(
() => ({
account_status: accountFilter === "all" ? undefined : accountFilter,
email_sent:
emailSentFilter === "all" ? undefined : emailSentFilter === "yes" ? true : emailSentFilter === "no" ? false : undefined,
placement_status: placementFilter === "all" ? undefined : placementFilter,
date_from: dateFrom || undefined,
date_to: dateTo || undefined,
}),
[accountFilter, emailSentFilter, placementFilter, dateFrom, dateTo],
);
const { data, isLoading } = useCredentialStatuses(filters);
const resendOne = useResendCredential();
const resendAll = useResendAllPendingCredentials();
const rows: CredentialStatus[] = data?.data ?? [];
const toggle = (id: number) => {
setSelected((prev) => {
const n = new Set(prev);
if (n.has(id)) n.delete(id);
else n.add(id);
return n;
});
};
const toggleAll = (checked: boolean) => {
if (!checked) setSelected(new Set());
else setSelected(new Set(rows.map((r) => r.student_id)));
};
return (
<div className="space-y-6 p-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Credential dashboard</h1>
<p className="text-muted-foreground text-sm mt-1">Track onboarding emails and placement</p>
</div>
<Card>
<CardHeader>
<CardTitle className="text-base">Filters</CardTitle>
</CardHeader>
<CardContent className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<div className="space-y-2">
<Label>Account status</Label>
<Select value={accountFilter} onValueChange={setAccountFilter}>
<SelectTrigger>
<SelectValue placeholder="All" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All</SelectItem>
<SelectItem value="pending_first_login">Pending first login</SelectItem>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="password_reset_required">Password reset required</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Email sent</Label>
<Select value={emailSentFilter} onValueChange={setEmailSentFilter}>
<SelectTrigger>
<SelectValue placeholder="All" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All</SelectItem>
<SelectItem value="yes">Yes</SelectItem>
<SelectItem value="no">No</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Placement status</Label>
<Select value={placementFilter} onValueChange={setPlacementFilter}>
<SelectTrigger>
<SelectValue placeholder="All" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All</SelectItem>
<SelectItem value="not_started">Not started</SelectItem>
<SelectItem value="in_progress">In progress</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
<SelectItem value="bypassed">Bypassed</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2 md:col-span-2 lg:col-span-1">
<Label>Date range</Label>
<div className="flex gap-2">
<Input type="date" value={dateFrom} onChange={(e) => setDateFrom(e.target.value)} />
<Input type="date" value={dateTo} onChange={(e) => setDateTo(e.target.value)} />
</div>
</div>
</CardContent>
</Card>
<div className="flex justify-end">
<Button
variant="secondary"
onClick={() =>
resendAll.mutate(undefined, {
onSuccess: () => toast.success("Bulk resend started."),
onError: (e) => toast.error(e instanceof Error ? e.message : "Failed"),
})
}
disabled={resendAll.isPending}
>
Resend All Pending
</Button>
</div>
<Card>
<CardContent className="pt-6">
{isLoading ? (
<div className="flex justify-center py-12 text-muted-foreground">Loading</div>
) : (
<div className="rounded-md border overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">
<Checkbox
checked={rows.length > 0 && selected.size === rows.length}
onCheckedChange={(c) => toggleAll(!!c)}
/>
</TableHead>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Account Status</TableHead>
<TableHead>Email Sent</TableHead>
<TableHead>First Login</TableHead>
<TableHead>Placement Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rows.map((r) => (
<TableRow key={r.student_id}>
<TableCell>
<Checkbox
checked={selected.has(r.student_id)}
onCheckedChange={() => toggle(r.student_id)}
/>
</TableCell>
<TableCell className="font-medium">{r.name}</TableCell>
<TableCell>{r.email}</TableCell>
<TableCell>{r.account_status}</TableCell>
<TableCell>{r.email_sent ? "Yes" : "No"}</TableCell>
<TableCell>{r.first_login_at ? new Date(r.first_login_at).toLocaleString() : "—"}</TableCell>
<TableCell>{r.placement_status}</TableCell>
<TableCell className="text-right">
<Button
size="sm"
variant="outline"
onClick={() =>
resendOne.mutate(r.student_id, {
onSuccess: () => toast.success("Email resent."),
onError: (e) => toast.error(e instanceof Error ? e.message : "Failed"),
})
}
disabled={resendOne.isPending}
>
Resend Email
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
</div>
);
}