feat: fixing certificate and block
This commit is contained in:
@@ -9,6 +9,8 @@ import { useAuth } from "@/context/AuthContext";
|
||||
import DiscussionBoard from "@/components/DiscussionBoard";
|
||||
import { AnnouncementsList } from "@/components/AnnouncementsList";
|
||||
import AboutCourse from "@/components/AboutCourse";
|
||||
import CertificateModal from "@/components/CertificateModal";
|
||||
import { CertificateResponse } from "@/lib/api";
|
||||
|
||||
export default function CourseOutlinePage({ params }: { params: { id: string } }) {
|
||||
const { user } = useAuth();
|
||||
@@ -22,6 +24,11 @@ export default function CourseOutlinePage({ params }: { params: { id: string } }
|
||||
const [instructors, setInstructors] = useState<any[]>([]);
|
||||
const [meetings, setMeetings] = useState<Meeting[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<'outline' | 'about'>('outline');
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [certificate, setCertificate] = useState<CertificateResponse | null>(null);
|
||||
const [showCertificateModal, setShowCertificateModal] = useState(false);
|
||||
const [loadingCertificate, setLoadingCertificate] = useState(false);
|
||||
const [orgSettings, setOrgSettings] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
@@ -37,11 +44,15 @@ export default function CourseOutlinePage({ params }: { params: { id: string } }
|
||||
setUserGrades(grades);
|
||||
|
||||
const enrollmentData = await lmsApi.getEnrollments(user.id);
|
||||
const enrolled = enrollmentData.some(e => e.course_id === params.id);
|
||||
|
||||
const enrollment = enrollmentData.find(e => e.course_id === params.id);
|
||||
|
||||
// Allow preview token to override enrollment status
|
||||
const isPreview = typeof window !== 'undefined' && !!sessionStorage.getItem('preview_token');
|
||||
setIsEnrolled(enrolled || isPreview);
|
||||
setIsEnrolled(!!enrollment || isPreview);
|
||||
|
||||
if (enrollment) {
|
||||
setProgress(enrollment.progress * 100);
|
||||
}
|
||||
} else {
|
||||
// Even if not logged in, if there's a preview token, consider "enrolled" for UI
|
||||
const isPreview = typeof window !== 'undefined' && !!sessionStorage.getItem('preview_token');
|
||||
@@ -65,6 +76,11 @@ export default function CourseOutlinePage({ params }: { params: { id: string } }
|
||||
lmsApi.getMeetings(params.id)
|
||||
.then(setMeetings)
|
||||
.catch(console.error);
|
||||
|
||||
// Load organization settings (branding includes the certificates_enabled flag)
|
||||
lmsApi.getBranding()
|
||||
.then(res => setOrgSettings(res.organization))
|
||||
.catch(console.error);
|
||||
}, [params.id, user]);
|
||||
|
||||
const handleEnrollOrBuy = async () => {
|
||||
@@ -120,6 +136,38 @@ export default function CourseOutlinePage({ params }: { params: { id: string } }
|
||||
});
|
||||
};
|
||||
|
||||
const handleViewCertificate = async () => {
|
||||
if (certificate) {
|
||||
setShowCertificateModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingCertificate(true);
|
||||
try {
|
||||
const data = await lmsApi.getCertificate(params.id);
|
||||
setCertificate(data);
|
||||
setShowCertificateModal(true);
|
||||
} catch (error: any) {
|
||||
console.error("No se pudo obtener el certificado", error);
|
||||
if (error.status === 404 || error.status === 403) {
|
||||
// Try to issue it if completed but not issued
|
||||
if (progress >= 100) {
|
||||
try {
|
||||
const issued = await lmsApi.issueCertificate(params.id);
|
||||
setCertificate(issued);
|
||||
setShowCertificateModal(true);
|
||||
} catch (issueError) {
|
||||
alert("No se pudo generar el certificado. Asegúrate de haber aprobado todas las lecciones.");
|
||||
}
|
||||
} else {
|
||||
alert("Aún no has completado este curso.");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setLoadingCertificate(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (lessonId: string, isGraded: boolean, allowRetry: boolean) => {
|
||||
if (isLessonLocked(lessonId)) {
|
||||
return <Lock size={18} className="text-gray-600" />;
|
||||
@@ -274,6 +322,17 @@ export default function CourseOutlinePage({ params }: { params: { id: string } }
|
||||
📊 Progreso
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
{isEnrolled && orgSettings?.certificates_enabled !== false && progress >= 100 && (
|
||||
<button
|
||||
onClick={handleViewCertificate}
|
||||
disabled={loadingCertificate}
|
||||
className="btn-premium px-8 py-3 !bg-emerald-600 !text-white shadow-lg shadow-emerald-500/20 active:scale-95 flex items-center gap-2"
|
||||
>
|
||||
<Award size={16} />
|
||||
{loadingCertificate ? "Preparando..." : "Mi Certificado"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -485,6 +544,13 @@ export default function CourseOutlinePage({ params }: { params: { id: string } }
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showCertificateModal && certificate && (
|
||||
<CertificateModal
|
||||
certificate={certificate}
|
||||
onClose={() => setShowCertificateModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,9 +40,7 @@ export default function MyLearningPage() {
|
||||
try {
|
||||
const { course, modules } = await lmsApi.getCourseOutline(enrollment.course_id);
|
||||
|
||||
// TODO: Implement actual progress tracking
|
||||
// For now, show 0% progress for all courses
|
||||
const progress = 0;
|
||||
const progress = enrollment.progress || 0;
|
||||
|
||||
enrichedEnrollments.push({
|
||||
course: { ...course, modules },
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import { X, Printer, Download, Award, ShieldCheck } from "lucide-react";
|
||||
import { CertificateResponse } from "@/lib/api";
|
||||
|
||||
interface CertificateModalProps {
|
||||
certificate: CertificateResponse;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function CertificateModal({ certificate, onClose }: CertificateModalProps) {
|
||||
const handlePrint = () => {
|
||||
window.print();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/80 backdrop-blur-md animate-in fade-in duration-300">
|
||||
<div className="relative w-full max-w-5xl md:h-[90vh] bg-white dark:bg-slate-900 rounded-[2rem] overflow-hidden flex flex-col shadow-2xl border border-white/10">
|
||||
|
||||
{/* Header / Toolbar */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-slate-100 dark:border-white/5 bg-slate-50 dark:bg-black/20 no-print">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-blue-600 flex items-center justify-center text-white shadow-lg shadow-blue-500/20">
|
||||
<Award size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-sm font-bold text-slate-900 dark:text-white">Certificado Oficial</h2>
|
||||
<p className="text-[10px] font-black uppercase tracking-widest text-blue-600 dark:text-blue-500">Emitido el {new Date(certificate.issued_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handlePrint}
|
||||
className="p-3 rounded-xl hover:bg-slate-200 dark:hover:bg-white/10 text-slate-600 dark:text-gray-400 transition-all flex items-center gap-2 text-xs font-bold"
|
||||
>
|
||||
<Printer size={18} />
|
||||
<span className="hidden sm:inline">Imprimir / PDF</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-3 rounded-xl hover:bg-red-500/10 text-slate-400 hover:text-red-500 transition-all"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Certificate Content Wrapper */}
|
||||
<div className="flex-1 overflow-y-auto p-4 md:p-12 flex items-center justify-center bg-slate-100 dark:bg-black/40">
|
||||
<div className="certificate-container shadow-2xl ring-1 ring-black/5">
|
||||
{/*
|
||||
We inject the HTML directly.
|
||||
Note: The backend must sanitize this OR we trust our own generated template.
|
||||
*/}
|
||||
<div
|
||||
className="bg-white"
|
||||
dangerouslySetInnerHTML={{ __html: certificate.certificate_html }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Info */}
|
||||
<div className="p-6 bg-slate-50 dark:bg-black/20 border-t border-slate-100 dark:border-white/5 flex flex-col md:flex-row items-center justify-between gap-4 no-print">
|
||||
<div className="flex items-center gap-3 text-emerald-600 dark:text-emerald-500">
|
||||
<ShieldCheck size={18} />
|
||||
<span className="text-[10px] font-black uppercase tracking-widest">Este certificado es auténtico y verificable</span>
|
||||
</div>
|
||||
<div className="text-[10px] font-bold text-slate-400 dark:text-gray-500 flex items-center gap-2">
|
||||
Código: <code className="bg-slate-200 dark:bg-white/5 px-2 py-1 rounded text-blue-600 dark:text-blue-400 font-mono">{certificate.verification_code}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx global>{`
|
||||
.certificate-container {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
background: white;
|
||||
transform-origin: center;
|
||||
}
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
body { background: white !important; margin: 0 !important; padding: 0 !important; }
|
||||
.certificate-container {
|
||||
box-shadow: none !important;
|
||||
ring: none !important;
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -142,6 +142,30 @@ export interface PaymentPreferenceResponse {
|
||||
init_point: string;
|
||||
}
|
||||
|
||||
export interface CertificateResponse {
|
||||
id: string;
|
||||
user_id: string;
|
||||
course_id: string;
|
||||
course_title: string;
|
||||
student_name: string;
|
||||
certificate_html: string;
|
||||
issued_at: string;
|
||||
verification_code: string;
|
||||
metadata: any;
|
||||
}
|
||||
|
||||
export interface CertificateResponse {
|
||||
id: string;
|
||||
user_id: string;
|
||||
course_id: string;
|
||||
course_title: string;
|
||||
student_name: string;
|
||||
certificate_html: string;
|
||||
issued_at: string;
|
||||
verification_code: string;
|
||||
metadata: any;
|
||||
}
|
||||
|
||||
export interface QuizQuestion {
|
||||
id: string;
|
||||
question: string;
|
||||
@@ -371,6 +395,7 @@ export interface Enrollment {
|
||||
id: string;
|
||||
user_id: string;
|
||||
course_id: string;
|
||||
progress: number;
|
||||
enrolled_at: string;
|
||||
}
|
||||
|
||||
@@ -862,5 +887,19 @@ export const lmsApi = {
|
||||
|
||||
async getMyBadges(): Promise<Badge[]> {
|
||||
return apiFetch(`/my/badges`, {}, false);
|
||||
},
|
||||
|
||||
// Certificates
|
||||
async getCertificate(courseId: string): Promise<CertificateResponse> {
|
||||
return apiFetch(`/courses/${courseId}/certificate`);
|
||||
},
|
||||
async issueCertificate(courseId: string, forceReissue = false): Promise<CertificateResponse> {
|
||||
return apiFetch(`/courses/${courseId}/certificate/issue`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ force_reissue: forceReissue })
|
||||
});
|
||||
},
|
||||
async verifyCertificate(code: string): Promise<any> {
|
||||
return apiFetch(`/certificates/verify/${code}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { cmsApi } from "@/lib/api";
|
||||
import { RefreshCw, Database, Users, CheckCircle2, AlertCircle, Loader2 } from "lucide-react";
|
||||
|
||||
interface IntegrationsSectionProps {
|
||||
courseId: string;
|
||||
}
|
||||
|
||||
export default function IntegrationsSection({ courseId }: IntegrationsSectionProps) {
|
||||
const [syncingAll, setSyncingAll] = useState(false);
|
||||
const [syncingStudents, setSyncingStudents] = useState(false);
|
||||
const [syncingAssignments, setSyncingAssignments] = useState(false);
|
||||
const [status, setStatus] = useState<{ type: 'success' | 'error', message: string } | null>(null);
|
||||
|
||||
const handleSyncAll = async () => {
|
||||
setSyncingAll(true);
|
||||
setStatus(null);
|
||||
try {
|
||||
const result = await cmsApi.syncSamAll();
|
||||
setStatus({
|
||||
type: 'success',
|
||||
message: `Sincronización completa finalizada. Estudiantes: ${result.students_synced}, Asignaciones: ${result.assignments_synced}`
|
||||
});
|
||||
} catch (err: any) {
|
||||
setStatus({ type: 'error', message: err.message || "Error al sincronizar con SAM" });
|
||||
} finally {
|
||||
setSyncingAll(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSyncStudents = async () => {
|
||||
setSyncingStudents(true);
|
||||
setStatus(null);
|
||||
try {
|
||||
const result = await cmsApi.syncSamStudents();
|
||||
setStatus({
|
||||
type: 'success',
|
||||
message: `Estudiantes sincronizados: ${result.students_synced}`
|
||||
});
|
||||
} catch (err: any) {
|
||||
setStatus({ type: 'error', message: err.message || "Error al sincronizar estudiantes" });
|
||||
} finally {
|
||||
setSyncingStudents(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSyncAssignments = async () => {
|
||||
setSyncingAssignments(true);
|
||||
setStatus(null);
|
||||
try {
|
||||
const result = await cmsApi.syncSamAssignments();
|
||||
setStatus({
|
||||
type: 'success',
|
||||
message: `Asignaciones sincronizadas: ${result.assignments_synced}`
|
||||
});
|
||||
} catch (err: any) {
|
||||
setStatus({ type: 'error', message: err.message || "Error al sincronizar asignaciones" });
|
||||
} finally {
|
||||
setSyncingAssignments(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-3xl p-8 space-y-8 shadow-sm">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-2xl bg-indigo-600/10 flex items-center justify-center text-indigo-600 dark:text-indigo-400">
|
||||
<RefreshCw size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="section-title">Integraciones y Sincronización</h2>
|
||||
<p className="text-sm text-slate-500 dark:text-gray-400">Gestiona la conexión con sistemas externos (SAM y MySQL Legacy)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* SAM Integration Card */}
|
||||
<div className="bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl p-6 shadow-sm space-y-6">
|
||||
<div className="flex items-center gap-2 text-indigo-600 dark:text-indigo-400">
|
||||
<Database size={20} />
|
||||
<h3 className="font-bold uppercase tracking-wider text-sm">SAM (Sistema Académico)</h3>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-500 dark:text-gray-400">
|
||||
Sincroniza la base de datos de estudiantes y las inscripciones a cursos desde el sistema SAM v3.
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={handleSyncAll}
|
||||
disabled={syncingAll || syncingStudents || syncingAssignments}
|
||||
className="w-full flex items-center justify-between p-4 bg-slate-50 dark:bg-black/20 hover:bg-indigo-50 dark:hover:bg-indigo-500/10 border border-slate-200 dark:border-white/10 rounded-xl transition-all group"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RefreshCw className={`w-5 h-5 text-indigo-500 ${syncingAll ? 'animate-spin' : 'group-hover:rotate-180 transition-transform duration-500'}`} />
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-bold text-slate-900 dark:text-white">Sincronización Total</div>
|
||||
<div className="text-[10px] text-slate-500 dark:text-gray-500">Estudiantes + Inscripciones</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleSyncStudents}
|
||||
disabled={syncingAll || syncingStudents}
|
||||
className="flex-1 flex items-center gap-2 p-3 bg-white dark:bg-black/10 hover:bg-slate-50 dark:hover:bg-white/5 border border-slate-200 dark:border-white/10 rounded-xl text-xs font-bold text-slate-600 dark:text-gray-300 transition-all"
|
||||
>
|
||||
<Users size={14} />
|
||||
{syncingStudents ? "Sincronizando..." : "Solo Estudiantes"}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSyncAssignments}
|
||||
disabled={syncingAll || syncingAssignments}
|
||||
className="flex-1 flex items-center gap-2 p-3 bg-white dark:bg-black/10 hover:bg-slate-50 dark:hover:bg-white/5 border border-slate-200 dark:border-white/10 rounded-xl text-xs font-bold text-slate-600 dark:text-gray-300 transition-all"
|
||||
>
|
||||
<RefreshCw size={14} className={syncingAssignments ? 'animate-spin' : ''} />
|
||||
{syncingAssignments ? "Sincronizando..." : "Solo Inscripciones"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MySQL Legacy Sync Card */}
|
||||
<div className="bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl p-6 shadow-sm space-y-6 opacity-75 grayscale hover:grayscale-0 hover:opacity-100 transition-all">
|
||||
<div className="flex items-center gap-2 text-slate-600 dark:text-gray-400">
|
||||
<AlertCircle size={20} />
|
||||
<h3 className="font-bold uppercase tracking-wider text-sm">MySQL Legacy Sync</h3>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-500 dark:text-gray-400">
|
||||
Importación manual de contenidos y estructuras de cursos desde el sistema antiguo.
|
||||
<span className="block mt-1 text-amber-500 font-bold">⚠️ Use con precaución: puede duplicar contenidos.</span>
|
||||
</p>
|
||||
|
||||
<button
|
||||
disabled={true}
|
||||
className="w-full flex items-center justify-center p-4 bg-slate-100 dark:bg-black/40 border border-dashed border-slate-300 dark:border-white/10 rounded-xl text-slate-400 text-xs font-bold"
|
||||
>
|
||||
Próximamente para este curso específico
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{status && (
|
||||
<div className={`p-4 rounded-2xl flex items-center gap-3 animate-in fade-in slide-in-from-bottom-2 ${status.type === 'success' ? 'bg-green-500/10 text-green-600 dark:text-green-400 border border-green-500/20' : 'bg-red-500/10 text-red-600 dark:text-red-400 border border-red-500/20'}`}>
|
||||
{status.type === 'success' ? <CheckCircle2 size={20} /> : <AlertCircle size={20} />}
|
||||
<p className="text-sm font-bold">{status.message}</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,7 @@ const DEFAULT_CERTIFICATE_TEMPLATE = `
|
||||
|
||||
import CourseEditorLayout from "@/components/CourseEditorLayout";
|
||||
import TeamManagementSection from "./TeamManagementSection";
|
||||
import IntegrationsSection from "./IntegrationsSection";
|
||||
|
||||
export default function CourseSettingsPage() {
|
||||
const { id } = useParams() as { id: string };
|
||||
@@ -147,6 +148,7 @@ export default function CourseSettingsPage() {
|
||||
>
|
||||
<div className="space-y-8">
|
||||
<TeamManagementSection courseId={id} />
|
||||
<IntegrationsSection courseId={id} />
|
||||
|
||||
{/* Passing Percentage Section */}
|
||||
<section className="bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-3xl p-8 shadow-sm">
|
||||
|
||||
@@ -43,6 +43,11 @@ const featureCards: Array<{
|
||||
title: "Code Lab",
|
||||
description: "Permite laboratorios de código generados o editados manualmente.",
|
||||
},
|
||||
{
|
||||
key: "certificates_enabled",
|
||||
title: "Generación de Certificados",
|
||||
description: "Habilita la emisión automática de certificados al completar cursos.",
|
||||
},
|
||||
];
|
||||
|
||||
const defaultSettings: OrganizationExerciseSettings = {
|
||||
@@ -54,6 +59,7 @@ const defaultSettings: OrganizationExerciseSettings = {
|
||||
role_playing_enabled: true,
|
||||
mermaid_enabled: false,
|
||||
code_lab_enabled: true,
|
||||
certificates_enabled: true,
|
||||
};
|
||||
|
||||
export default function ExerciseFeatureSettings() {
|
||||
@@ -94,6 +100,7 @@ export default function ExerciseFeatureSettings() {
|
||||
role_playing_enabled: settings.role_playing_enabled,
|
||||
mermaid_enabled: settings.mermaid_enabled,
|
||||
code_lab_enabled: settings.code_lab_enabled,
|
||||
certificates_enabled: settings.certificates_enabled,
|
||||
};
|
||||
const updated = await cmsApi.updateOrganizationExerciseSettings(payload);
|
||||
setSettings(updated);
|
||||
|
||||
@@ -278,6 +278,7 @@ export interface OrganizationExerciseSettings {
|
||||
role_playing_enabled: boolean;
|
||||
mermaid_enabled: boolean;
|
||||
code_lab_enabled: boolean;
|
||||
certificates_enabled: boolean;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
@@ -433,6 +434,12 @@ export interface GlobalAiUsageResponse {
|
||||
student_chat_usage: StudentChatUsage[];
|
||||
}
|
||||
|
||||
export interface SamSyncResponse {
|
||||
students_synced: number;
|
||||
assignments_synced: number;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
// ==================== Grading ====================
|
||||
|
||||
export interface GradingCategory {
|
||||
@@ -899,6 +906,11 @@ export const cmsApi = {
|
||||
getCourseTeam: (courseId: string): Promise<CourseInstructor[]> => apiFetch(`/courses/${courseId}/team`),
|
||||
addTeamMember: (courseId: string, email: string, role: string): Promise<CourseInstructor> => apiFetch(`/courses/${courseId}/team`, { method: 'POST', body: JSON.stringify({ email, role }) }),
|
||||
removeTeamMember: (courseId: string, userId: string): Promise<void> => apiFetch(`/courses/${courseId}/team/${userId}`, { method: 'DELETE' }),
|
||||
|
||||
// SAM Integration
|
||||
syncSamStudents: (): Promise<SamSyncResponse> => apiFetch('/sam/sync-students', { method: 'POST' }),
|
||||
syncSamAssignments: (): Promise<SamSyncResponse> => apiFetch('/sam/sync-assignments', { method: 'POST' }),
|
||||
syncSamAll: (): Promise<SamSyncResponse> => apiFetch('/sam/sync-all', { method: 'POST' }),
|
||||
getUsers: (): Promise<User[]> => apiFetch('/users'),
|
||||
|
||||
// Modules & Lessons
|
||||
|
||||
Reference in New Issue
Block a user