Admin fixes, minio texts removed, added is_active to User model

This commit is contained in:
2026-09-25 13:01:48 +05:30
parent 46b44b5a14
commit 3773d0cc51
7 changed files with 153 additions and 43 deletions

View File

@@ -307,6 +307,16 @@ def bulk_delete_students(payload: DeleteUsersRequest, session: Session = Depends
session.commit()
return {"success": True}
@app.put("/api/admin/students/{user_id}/toggle", tags=["Admin"])
def toggle_student_active(user_id: UUID, session: Session = Depends(get_session)):
user = session.get(models.User, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
user.is_active = not getattr(user, 'is_active', True)
session.add(user)
session.commit()
return {"success": True, "is_active": user.is_active}
@app.post("/api/auth/register", tags=["Authentication"])
def register(payload: RegisterPayload, session: Session = Depends(get_session)):
import bcrypt

View File

@@ -65,6 +65,7 @@ class User(SQLModel, table=True):
created_at: datetime = Field(default_factory=datetime.utcnow)
last_active: datetime = Field(default_factory=datetime.utcnow)
active_session_token: Optional[str] = None
is_active: bool = Field(default=True)
class Parent(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True)

View File

@@ -28,8 +28,6 @@ export default function AdminLayout({ children }: { children: ReactNode }) {
<nav className="flex-1 px-4 space-y-1.5 mt-6">
<NavItem href="/admin" icon={<LayoutDashboard size={18} />} label="Command Center" active={pathname === "/admin"} />
<NavItem href="/admin/tests" icon={<FileText size={18} />} label="Tests & DPPs" active={pathname.startsWith("/admin/tests")} />
<NavItem href="/admin/questions" icon={<Database size={18} />} label="Question Bank" active={pathname.startsWith("/admin/questions")} />
<NavItem href="/admin/agents" icon={<Terminal size={18} />} label="AI Agents" active={pathname.startsWith("/admin/agents")} />
<NavItem href="/admin/students" icon={<Users size={18} />} label="Students" active={pathname.startsWith("/admin/students")} />
<NavItem href="/admin/coupons" icon={<Tag size={18} />} label="Coupons" active={pathname.startsWith("/admin/coupons")} />
</nav>

View File

@@ -1,4 +1,4 @@
"use client";
"use client";
import { fetchWithAuth } from "@/lib/apiClient";
import { useState, useEffect } from "react";
@@ -65,24 +65,6 @@ export default function SettingsPage() {
</div>
<div className="space-y-6">
<Card className="border-border bg-secondary/10">
<CardHeader>
<CardTitle>AI Inference Configuration</CardTitle>
<CardDescription>Manage the underlying LLM models and API keys for the AI agents.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-bold">Primary Model Name</label>
<input type="text" defaultValue="gemini-1.5-pro-experimental" className="w-full bg-background border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[var(--cyan-ai)]" />
</div>
<div className="space-y-2">
<label className="text-sm font-bold">OpenAI API Key (Fallback)</label>
<input type="password" defaultValue="sk-........................" className="w-full bg-background border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[var(--cyan-ai)]" />
</div>
<Button onClick={() => alert("Configuration Saved via Admin Panel")} className="bg-[var(--cyan-ai)] text-background hover:bg-[var(--cyan-ai-80)] font-bold">Save Configuration</Button>
</CardContent>
</Card>
<Card className="border-border bg-secondary/10">
<CardHeader>
<CardTitle>Trial Management</CardTitle>

View File

@@ -25,6 +25,24 @@ export default function StudentsPage() {
const [students, setStudents] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [search, setSearch] = useState("")
const [tierFilter, setTierFilter] = useState<string>("All")
const [courseFilter, setCourseFilter] = useState<string>("All")
const handleToggleActive = async (studentId: string) => {
try {
const res = await fetchWithAuth(/api/admin/students/\/toggle, {
method: "PUT"
})
if (res.ok) {
const fresh = await fetchWithAuth("/api/admin/students").then(r => r.json())
setStudents(fresh)
}
} catch (err) {
console.error("Failed to toggle student:", err)
}
}
const [selectedIds, setSelectedIds] = useState<string[]>([])
const [deleting, setDeleting] = useState(false)
const [isUploading, setIsUploading] = useState(false)
@@ -118,14 +136,18 @@ export default function StudentsPage() {
.finally(() => setLoading(false))
}, [])
const filtered = students.filter(s =>
s.name.toLowerCase().includes(search.toLowerCase()) ||
s.email.toLowerCase().includes(search.toLowerCase()) ||
s.phone.includes(search)
)
const formatDate = (iso: string) => {
try { return new Date(iso).toLocaleDateString("en-IN", { day: "numeric", month: "short", year: "numeric" }) }
const filtered = students.filter(s => {
const searchMatch = (s.name || "").toLowerCase().includes(search.toLowerCase()) ||
(s.email || "").toLowerCase().includes(search.toLowerCase()) ||
(s.phone || "").includes(search)
const tierMatch = tierFilter === "All" || s.tier === tierFilter
const courseMatch = courseFilter === "All" || s.target_exam === courseFilter
return searchMatch && tierMatch && courseMatch
})
}
catch { return iso }
}
@@ -159,8 +181,9 @@ export default function StudentsPage() {
</div>
<div className="bg-secondary/10 border border-border rounded-xl overflow-hidden backdrop-blur-sm">
<div className="p-4 border-b border-border flex items-center gap-4">
<div className="relative flex-1 max-w-md">
<div className="p-4 border-b border-border flex flex-col md:flex-row items-center gap-4">
<div className="relative flex-1 w-full max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" size={16} />
<input
type="text"
@@ -170,8 +193,30 @@ export default function StudentsPage() {
className="w-full bg-background border border-border rounded-lg pl-10 pr-4 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary"
/>
</div>
<select
className="w-full md:w-auto bg-background border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary"
value={tierFilter}
onChange={e => setTierFilter(e.target.value)}
>
<option value="All">All Plans</option>
<option value="AI Coach">AI Coach</option>
<option value="Pro">Pro</option>
<option value="Free">Free</option>
</select>
<select
className="w-full md:w-auto bg-background border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary"
value={courseFilter}
onChange={e => setCourseFilter(e.target.value)}
>
<option value="All">All Courses</option>
<option value="JEE_Main">JEE Main</option>
<option value="JEE_Advanced">JEE Advanced</option>
<option value="NEET">NEET</option>
<option value="JEE_NEET">JEE + NEET</option>
</select>
</div>
<div className="overflow-x-auto">
{loading ? (
<div className="flex items-center justify-center py-20 text-muted-foreground gap-3">
@@ -192,13 +237,16 @@ export default function StudentsPage() {
}}
/>
</th>
<th className="px-6 py-4 font-semibold">Student</th>
<th className="px-6 py-4 font-semibold">Plan</th>
<th className="px-6 py-4 font-semibold">Target</th>
<th className="px-6 py-4 font-semibold">Tests</th>
<th className="px-6 py-4 font-semibold">Accuracy</th>
<th className="px-6 py-4 font-semibold text-center">Streak</th>
<th className="px-6 py-4 font-semibold">Last Active</th>
<th className="px-6 py-4 font-semibold">Dates</th>
<th className="px-6 py-4 font-semibold">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
@@ -255,7 +303,24 @@ export default function StudentsPage() {
{s.streak_count}
</span>
</td>
<td className="px-6 py-4 text-muted-foreground text-xs">{formatDate(s.last_active)}</td>
<td className="px-6 py-4 text-sm text-muted-foreground">
<div className="flex flex-col gap-1">
<span className="text-xs">Joined: {formatDate(s.created_at)}</span>
<span className="text-xs">Active: {formatDate(s.last_active)}</span>
</div>
</td>
<td className="px-6 py-4" onClick={(e) => e.stopPropagation()}>
<div className="flex flex-col items-center gap-2">
<span className="text-xs font-semibold">{s.is_active !== false ? 'Enabled' : 'Disabled'}</span>
<div
onClick={() => handleToggleActive(s.id)}
className={h-5 w-9 rounded-full relative cursor-pointer transition-colors }>
<div className={bsolute top-0.5 size-4 rounded-full transition-all }></div>
</div>
</div>
</td>
</tr>
))}
</tbody>

View File

@@ -172,6 +172,45 @@ export default function AdminTestsPage() {
const handleUpload = async (e: React.FormEvent) => {
e.preventDefault()
if (testType === 'DPP_PDF') {
const file = fileInputRef.current?.files?.[0]
if (!file) {
alert("Please select a PDF file to upload to Minio.")
return
}
if (!userId) return
setUploading(true)
try {
const formData = new FormData()
formData.append("file", file)
formData.append("title", title)
formData.append("exam_pattern", examPattern)
formData.append("time_limit", timeLimit)
formData.append("admin_id", userId as string)
if (scheduledDate) formData.append("scheduled_date", scheduledDate.toISOString())
const res = await fetchWithAuth("/api/admin/tests/upload", {
method: "POST",
body: formData
})
if (res.ok) {
alert("PDF uploaded successfully to Minio and connected!")
setTitle("")
removeFile()
fetchTests()
} else {
const err = await res.json()
alert(`Upload failed: ${JSON.stringify(err)}`)
}
} catch (err) {
alert("Upload failed")
} finally {
setUploading(false)
}
return
}
if (!validQuestions || validQuestions.length === 0) {
alert("Please upload a valid JSON file with questions first.")
return
@@ -251,13 +290,14 @@ export default function AdminTestsPage() {
value={testType}
onChange={(e) => setTestType(e.target.value)}
>
<option value="CBT">🎯 CBT Mock Test — appears in Mock Tests section</option>
<option value="DPS">📋 DPP (Daily Practice) — appears in PrepPilot AI section</option>
<option value="CBT">🎯 CBT Test (JSON) — appears in Mock Tests section</option>
<option value="PREP_PILOT">🤖 Prep Pilot Test (as CBT JSON) — appears in AI Coach</option>
<option value="DPP_PDF">📋 DPP PDF (Minio Upload) — Class/Plan aligned</option>
</select>
<p className="text-[11px] text-muted-foreground">
{testType === 'CBT'
? '✅ This will appear under Dashboard → Mock Tests (CBT) for students to attempt.'
: '✅ This will appear under AI Coach → PrepPilot AI as a DPP with full CBT interface.'}
{testType === 'CBT' && '✅ Purely for CBT. Appears under Dashboard → Mock Tests.'}
{testType === 'PREP_PILOT' && '✅ Purely for CBT. Appears under AI Coach → PrepPilot AI.'}
{testType === 'DPP_PDF' && '✅ Uploads a PDF to Minio storage for Class/Plan aligned DPP.'}
</p>
</div>
@@ -313,7 +353,21 @@ export default function AdminTestsPage() {
<div className="space-y-2 pt-2">
<label className="text-sm font-medium">Questions JSON File</label>
{validQuestions ? (
{testType === 'DPP_PDF' ? (
<div className="space-y-2 pt-2">
<label className="text-sm font-medium">PDF File (Minio Upload)</label>
<input
type="file"
accept=".pdf"
ref={fileInputRef}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background cursor-pointer file:border-0 file:bg-primary file:text-primary-foreground file:h-full file:px-4 file:mr-4 file:font-semibold hover:file:bg-primary/90"
onChange={(e) => {
const file = e.target.files?.[0]
if(file) setValidationErrors([])
}}
/>
</div>
) : validQuestions ? (
<div className="flex items-center justify-between p-3 border border-green-500/50 bg-green-500/10 rounded-md">
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
<CheckCircle2 className="w-5 h-5" />
@@ -364,9 +418,9 @@ export default function AdminTestsPage() {
)}
</div>
<Button type="submit" className="w-full" disabled={uploading || !validQuestions}>
<Button type="submit" className="w-full" disabled={uploading || (!validQuestions && testType !== 'DPP_PDF')}>
{uploading ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <FileText className="w-4 h-4 mr-2" />}
{uploading ? "Creating Test..." : "Create Test from JSON"}
{uploading ? "Creating Test..." : (testType === 'DPP_PDF' ? "Upload PDF to Minio" : "Create Test from JSON")}
</Button>
</form>
</CardContent>

View File

@@ -60,7 +60,7 @@ export default function SettingsPage() {
const data = await res.json();
setAvatarUrl(data.url);
localStorage.setItem(`user_avatar_${userId}`, data.url);
alert("Profile picture uploaded successfully to Minio!");
alert("Profile picture uploaded successfully!");
} catch (err) {
console.error(err);
alert("Failed to upload profile picture.");
@@ -127,7 +127,7 @@ export default function SettingsPage() {
<Card className="border-border bg-background shadow-sm md:col-span-1 h-fit">
<CardHeader>
<CardTitle>Profile Photo</CardTitle>
<CardDescription>Upload photo to Minio storage.</CardDescription>
<CardDescription>Upload your profile photo.</CardDescription>
</CardHeader>
<CardContent className="flex flex-col items-center space-y-4">
<div className="relative size-32 rounded-full border border-border bg-secondary/50 flex items-center justify-center overflow-hidden">