1
This commit is contained in:
parent
fa2ac8f712
commit
3d0b3ab8f6
@ -76,8 +76,20 @@ async def login(req: LoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserOut)
|
||||
async def get_me(user: User = Depends(get_current_user)):
|
||||
return UserOut.model_validate(user)
|
||||
async def get_me(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
user_out = UserOut.model_validate(user)
|
||||
|
||||
# Attach enabled_modules from user's class
|
||||
if user.class_id:
|
||||
from app.db.models import Class_
|
||||
from sqlalchemy import select
|
||||
|
||||
result = await db.execute(select(Class_).where(Class_.id == user.class_id))
|
||||
class_ = result.scalar_one_or_none()
|
||||
if class_:
|
||||
user_out.enabled_modules = class_.get_enabled_modules()
|
||||
|
||||
return user_out
|
||||
|
||||
|
||||
@router.put("/change-password")
|
||||
|
||||
@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.core.deps import require_role
|
||||
from app.db.database import get_db
|
||||
from app.db.models import User
|
||||
from app.schemas.class_ import ClassCreate, ClassUpdate, ClassOut
|
||||
from app.schemas.class_ import ClassCreate, ClassUpdate, ClassOut, ModuleUpdate
|
||||
from app.schemas.user import UserListItem
|
||||
from app.schemas.roster import RosterOut, RosterImportRequest
|
||||
from app.schemas.common import PageResponse
|
||||
@ -268,3 +268,55 @@ async def regenerate_invite(
|
||||
if not code:
|
||||
raise HTTPException(status_code=404, detail="Class not found")
|
||||
return {"invite_code": code}
|
||||
|
||||
|
||||
# --- Module management ---
|
||||
|
||||
|
||||
@router.get("/{class_id}/modules")
|
||||
async def get_class_modules(
|
||||
class_id: int,
|
||||
admin: User = Depends(require_role("super_admin", "class_admin")),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
if admin.role == "class_admin" and admin.class_id != class_id:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
class_ = await get_class_by_id(db, class_id)
|
||||
if class_ is None:
|
||||
raise HTTPException(status_code=404, detail="Class not found")
|
||||
|
||||
return {
|
||||
"class_id": class_id,
|
||||
"enabled_modules": class_.get_enabled_modules(),
|
||||
"available_modules": class_.ALL_MODULES,
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{class_id}/modules")
|
||||
async def update_class_modules(
|
||||
class_id: int,
|
||||
data: ModuleUpdate,
|
||||
admin: User = Depends(require_role("super_admin", "class_admin")),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
if admin.role == "class_admin" and admin.class_id != class_id:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
class_ = await get_class_by_id(db, class_id)
|
||||
if class_ is None:
|
||||
raise HTTPException(status_code=404, detail="Class not found")
|
||||
|
||||
# Validate module keys
|
||||
valid_keys = set(class_.ALL_MODULES)
|
||||
for m in data.enabled_modules:
|
||||
if m not in valid_keys:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid module: {m}")
|
||||
|
||||
class_.set_enabled_modules(data.enabled_modules)
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"class_id": class_id,
|
||||
"enabled_modules": class_.get_enabled_modules(),
|
||||
}
|
||||
|
||||
@ -15,11 +15,26 @@ class Class_(Base):
|
||||
cohort_year: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
invite_code: Mapped[str | None] = mapped_column(String(20), unique=True, nullable=True)
|
||||
enabled_modules: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON array of module keys
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
# All available modules
|
||||
ALL_MODULES = ["announcements", "directory", "timeline", "assignments", "votes", "schedule", "resources"]
|
||||
|
||||
def get_enabled_modules(self) -> list[str]:
|
||||
if not self.enabled_modules:
|
||||
return list(self.ALL_MODULES)
|
||||
try:
|
||||
return json.loads(self.enabled_modules)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return list(self.ALL_MODULES)
|
||||
|
||||
def set_enabled_modules(self, modules: list[str]):
|
||||
self.enabled_modules = json.dumps(modules, ensure_ascii=False) if modules else None
|
||||
|
||||
members: Mapped[list["User"]] = relationship("User", back_populates="class_")
|
||||
timelines: Mapped[list["Timeline"]] = relationship(
|
||||
"Timeline", back_populates="class_", cascade="all, delete-orphan"
|
||||
|
||||
@ -39,7 +39,7 @@ async def ensure_super_admin():
|
||||
|
||||
async def ensure_sample_class():
|
||||
"""Seed a sample class if none exists."""
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy import select, func, text
|
||||
from app.db.database import async_session
|
||||
from app.db.models import Class_
|
||||
|
||||
@ -57,9 +57,23 @@ async def ensure_sample_class():
|
||||
logger.info("Sample class seeded")
|
||||
|
||||
|
||||
async def migrate_add_enabled_modules():
|
||||
"""Add enabled_modules column to classes table if not exists."""
|
||||
from sqlalchemy import text
|
||||
from app.db.database import engine
|
||||
|
||||
async with engine.begin() as conn:
|
||||
result = await conn.execute(text("PRAGMA table_info(classes)"))
|
||||
columns = [row[1] for row in result.fetchall()]
|
||||
if "enabled_modules" not in columns:
|
||||
await conn.execute(text("ALTER TABLE classes ADD COLUMN enabled_modules TEXT"))
|
||||
logger.info("Migration: added enabled_modules column to classes table")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
await create_tables()
|
||||
await migrate_add_enabled_modules()
|
||||
await ensure_super_admin()
|
||||
await ensure_sample_class()
|
||||
yield
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, field_validator
|
||||
|
||||
|
||||
class ClassCreate(BaseModel):
|
||||
@ -22,6 +23,21 @@ class ClassOut(BaseModel):
|
||||
description: str | None
|
||||
invite_code: str | None = None
|
||||
member_count: int = 0
|
||||
enabled_modules: list[str] | None = None
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
@field_validator("enabled_modules", mode="before")
|
||||
@classmethod
|
||||
def parse_enabled_modules(cls, v):
|
||||
if isinstance(v, str):
|
||||
try:
|
||||
return json.loads(v)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return None
|
||||
return v
|
||||
|
||||
|
||||
class ModuleUpdate(BaseModel):
|
||||
enabled_modules: list[str]
|
||||
|
||||
@ -22,6 +22,7 @@ class UserOut(BaseModel):
|
||||
avatar_url: str | None
|
||||
bio: str | None
|
||||
created_at: datetime
|
||||
enabled_modules: list[str] | None = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
132
frontend/src/app/(app)/admin/modules/page.tsx
Normal file
132
frontend/src/app/(app)/admin/modules/page.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useActiveClass } from "@/hooks/use-active-class";
|
||||
import { fetchAPI, putAPI } from "@/lib/api";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { ErrorState } from "@/components/error-state";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const ALL_MODULES = [
|
||||
{ key: "announcements", label: "公告", desc: "发布和管理班级公告" },
|
||||
{ key: "directory", label: "花名册", desc: "查看班级成员花名册" },
|
||||
{ key: "timeline", label: "班级动态", desc: "分享班级动态和互动" },
|
||||
{ key: "assignments", label: "作业", desc: "发布和提交课程作业" },
|
||||
{ key: "votes", label: "投票", desc: "发起班级投票活动" },
|
||||
{ key: "schedule", label: "排期表", desc: "查看课程和活动排期" },
|
||||
{ key: "resources", label: "资源库", desc: "上传和下载学习资源" },
|
||||
];
|
||||
|
||||
interface ModuleConfig {
|
||||
class_id: number;
|
||||
enabled_modules: string[];
|
||||
available_modules: string[];
|
||||
}
|
||||
|
||||
export default function ModulesPage() {
|
||||
const { activeClassId, activeClassName } = useActiveClass();
|
||||
const [config, setConfig] = useState<ModuleConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadModules = async () => {
|
||||
if (!activeClassId) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetchAPI<ModuleConfig>(`/api/classes/${activeClassId}/modules`);
|
||||
setConfig(res);
|
||||
} catch (err: any) {
|
||||
setError(err.message || "加载失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadModules();
|
||||
}, [activeClassId]);
|
||||
|
||||
const handleToggle = async (moduleKey: string, enabled: boolean) => {
|
||||
if (!config || !activeClassId) return;
|
||||
|
||||
const updated = enabled
|
||||
? [...config.enabled_modules, moduleKey]
|
||||
: config.enabled_modules.filter((m) => m !== moduleKey);
|
||||
|
||||
// Optimistic update
|
||||
setConfig({ ...config, enabled_modules: updated });
|
||||
|
||||
try {
|
||||
await putAPI(`/api/classes/${activeClassId}/modules`, {
|
||||
enabled_modules: updated,
|
||||
});
|
||||
toast.success(
|
||||
enabled
|
||||
? `已启用「${ALL_MODULES.find((m) => m.key === moduleKey)?.label}」`
|
||||
: `已禁用「${ALL_MODULES.find((m) => m.key === moduleKey)?.label}」`
|
||||
);
|
||||
} catch (err: any) {
|
||||
// Rollback
|
||||
setConfig(config);
|
||||
toast.error(err.message || "操作失败");
|
||||
}
|
||||
};
|
||||
|
||||
if (!activeClassId) {
|
||||
return (
|
||||
<div className="text-center py-12 text-gray-400">
|
||||
请先选择一个班级
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">模块管理</h1>
|
||||
<p className="text-gray-500 mt-1">
|
||||
班级: {activeClassName}
|
||||
</p>
|
||||
<p className="text-gray-400 text-sm mt-1">
|
||||
控制班级侧边栏中显示的功能模块
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="animate-pulse space-y-4">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<Card key={i}>
|
||||
<CardContent className="p-4">
|
||||
<div className="h-16 bg-gray-200 rounded" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : error ? (
|
||||
<ErrorState message={error} onRetry={loadModules} />
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{ALL_MODULES.map((module) => (
|
||||
<Card key={module.key}>
|
||||
<CardContent className="p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">{module.label}</p>
|
||||
<p className="text-sm text-gray-500">{module.desc}</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config?.enabled_modules.includes(module.key) ?? false}
|
||||
onCheckedChange={(checked) => handleToggle(module.key, checked)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -43,6 +43,19 @@ export default function AdminPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
<Link href="/admin/modules">
|
||||
<Card className="hover:shadow-md transition-shadow cursor-pointer">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">模块管理</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-500 text-sm">
|
||||
控制班级功能模块的显示
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -3,35 +3,50 @@
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useSidebar } from "@/hooks/use-sidebar";
|
||||
import { useActiveClass } from "@/hooks/use-active-class";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { UserRole } from "@/lib/types";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
|
||||
// Module keys that can be toggled
|
||||
const TOGGLEABLE_MODULES = ["announcements", "directory", "timeline", "assignments", "votes", "schedule", "resources"];
|
||||
|
||||
const navItems = [
|
||||
{ href: "/dashboard", label: "首页", icon: "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" },
|
||||
{ href: "/announcements", label: "公告", icon: "M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z" },
|
||||
{ href: "/directory", label: "花名册", icon: "M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" },
|
||||
{ href: "/timeline", label: "班级动态", icon: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" },
|
||||
{ href: "/assignments", label: "作业", icon: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" },
|
||||
{ href: "/votes", label: "投票", icon: "M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" },
|
||||
{ href: "/schedule", label: "排期表", icon: "M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" },
|
||||
{ href: "/resources", label: "资源库", icon: "M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" },
|
||||
{ href: "/profile", label: "个人资料", icon: "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" },
|
||||
{ href: "/dashboard", label: "首页", icon: "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6", moduleKey: undefined },
|
||||
{ href: "/announcements", label: "公告", icon: "M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z", moduleKey: "announcements" },
|
||||
{ href: "/directory", label: "花名册", icon: "M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z", moduleKey: "directory" },
|
||||
{ href: "/timeline", label: "班级动态", icon: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z", moduleKey: "timeline" },
|
||||
{ href: "/assignments", label: "作业", icon: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z", moduleKey: "assignments" },
|
||||
{ href: "/votes", label: "投票", icon: "M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4", moduleKey: "votes" },
|
||||
{ href: "/schedule", label: "排期表", icon: "M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z", moduleKey: "schedule" },
|
||||
{ href: "/resources", label: "资源库", icon: "M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z", moduleKey: "resources" },
|
||||
{ href: "/profile", label: "个人资料", icon: "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z", moduleKey: undefined },
|
||||
];
|
||||
|
||||
const adminItems = [
|
||||
{ href: "/admin/members", label: "成员管理", icon: "M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z", roles: ["super_admin", "class_admin"] as UserRole[] },
|
||||
{ href: "/admin/classes", label: "班级管理", icon: "M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4", roles: ["super_admin"] as UserRole[] },
|
||||
{ href: "/admin/modules", label: "模块管理", icon: "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z", roles: ["super_admin", "class_admin"] as UserRole[] },
|
||||
];
|
||||
|
||||
export function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
const { isOpen, close } = useSidebar();
|
||||
const { user } = useAuth();
|
||||
const { enabledModules } = useActiveClass();
|
||||
const visibleAdminItems = user
|
||||
? adminItems.filter((item) => item.roles.includes(user.role))
|
||||
: [];
|
||||
|
||||
// Default to all modules enabled if not loaded yet
|
||||
const defaultModules = ["announcements", "directory", "timeline", "assignments", "votes", "schedule", "resources"];
|
||||
const enabledSet = new Set(enabledModules ?? defaultModules);
|
||||
|
||||
// Filter navItems based on enabled modules (items without moduleKey are always visible)
|
||||
const visibleNavItems = navItems.filter(
|
||||
(item) => !item.moduleKey || enabledSet.has(item.moduleKey)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile backdrop */}
|
||||
@ -56,7 +71,7 @@ export function Sidebar() {
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4 space-y-1">
|
||||
{navItems.map((item) => (
|
||||
{visibleNavItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
|
||||
@ -23,6 +23,8 @@ interface ActiveClassContextValue {
|
||||
availableClasses: ClassInfo[];
|
||||
/** Switch active class (super admin only) */
|
||||
setActiveClassId: (id: number) => void;
|
||||
/** Enabled modules for the active class */
|
||||
enabledModules: string[] | null;
|
||||
}
|
||||
|
||||
const ActiveClassContext = createContext<ActiveClassContextValue>({
|
||||
@ -31,6 +33,7 @@ const ActiveClassContext = createContext<ActiveClassContextValue>({
|
||||
canSwitchClass: false,
|
||||
availableClasses: [],
|
||||
setActiveClassId: () => {},
|
||||
enabledModules: null,
|
||||
});
|
||||
|
||||
export function ActiveClassProvider({ children }: { children: ReactNode }) {
|
||||
@ -49,6 +52,18 @@ export function ActiveClassProvider({ children }: { children: ReactNode }) {
|
||||
: null;
|
||||
const activeClassName = isSuperAdmin ? superAdminClassName : userClassName;
|
||||
|
||||
// Derive enabled modules based on active class
|
||||
const enabledModules = (() => {
|
||||
if (!activeClassId) return null;
|
||||
// For super admin, get from availableClasses
|
||||
if (isSuperAdmin) {
|
||||
const cls = availableClasses.find((c) => c.id === activeClassId);
|
||||
return cls?.enabled_modules ?? null;
|
||||
}
|
||||
// For others, get from user (which comes from /api/auth/me)
|
||||
return user?.enabled_modules ?? null;
|
||||
})();
|
||||
|
||||
// Super admin: load all classes and auto-select first
|
||||
useEffect(() => {
|
||||
if (!isSuperAdmin) return;
|
||||
@ -90,6 +105,7 @@ export function ActiveClassProvider({ children }: { children: ReactNode }) {
|
||||
canSwitchClass: isSuperAdmin,
|
||||
availableClasses,
|
||||
setActiveClassId,
|
||||
enabledModules,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -20,6 +20,7 @@ export interface AuthUser {
|
||||
avatar_url: string | null;
|
||||
bio: string | null;
|
||||
created_at: string;
|
||||
enabled_modules: string[] | null;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
@ -34,6 +35,7 @@ export interface ClassInfo {
|
||||
description: string | null;
|
||||
invite_code: string | null;
|
||||
member_count: number;
|
||||
enabled_modules: string[] | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user