(null);
const [authLoading, setAuthLoading] = useState(false);
@@ -129,7 +132,7 @@ export default function App() {
const res = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ name: regName, email: regEmail, password: regPassword })
+ body: JSON.stringify({ name: regName, email: regEmail, password: regPassword, birthDate: regBirthDate, whatsapp: regWhatsapp })
});
const data = await res.json();
@@ -250,6 +253,14 @@ export default function App() {
/>
)}
+ {/* RENDER VIEW: CERTIFICATES */}
+ {activeView === 'certificates' && (
+
+ )}
+
{/* Unlock Modal */}
@@ -326,6 +337,29 @@ export default function App() {
/>
+
+
+ setRegBirthDate(e.target.value)}
+ className="w-full glass-input rounded-lg p-3 text-xs text-white focus:outline-none"
+ />
+
+
+
+
+ setRegWhatsapp(e.target.value)}
+ placeholder="(11) 99999-9999"
+ className="w-full glass-input rounded-lg p-3 text-xs text-white focus:outline-none"
+ />
+
+
(null);
const [courseForm, setCourseForm] = useState({
- title: '', description: '', thumbnail: '', category: '', isLocked: false, price: 0
+ title: '', description: '', thumbnail: '', category: '', isLocked: false, price: 0, certificateMode: 'FULL_COURSE' as 'FULL_COURSE' | 'PER_MODULE'
});
const [showModuleForm, setShowModuleForm] = useState(false);
@@ -62,6 +62,10 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
title: '', videoUrl: '', videoType: 'youtube' as 'youtube' | 'direct', order: 1, content: ''
});
+ const [showActivityModal, setShowActivityModal] = useState(false);
+ const [activeModuleForActivity, setActiveModuleForActivity] = useState(null);
+ const [activityQuestions, setActivityQuestions] = useState<{question: string, options: string[], correctIndex: number}[]>([]);
+
useEffect(() => {
fetchCourses();
}, []);
@@ -125,7 +129,7 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
await fetchCourses();
setShowCourseForm(false);
setEditingCourse(null);
- setCourseForm({ title: '', description: '', thumbnail: '', category: '', isLocked: false, price: 0 });
+ setCourseForm({ title: '', description: '', thumbnail: '', category: '', isLocked: false, price: 0, certificateMode: 'FULL_COURSE' });
} else {
const errData = await res.json();
alert(`Erro ao salvar curso: ${errData.error || 'Falha na resposta do servidor'}`);
@@ -144,7 +148,8 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
thumbnail: course.thumbnail,
category: course.category,
isLocked: course.isLocked || false,
- price: course.price || 0
+ price: course.price || 0,
+ certificateMode: course.certificateMode || 'FULL_COURSE'
});
setShowCourseForm(true);
setTimeout(() => {
@@ -399,14 +404,26 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
setCourseForm({ ...courseForm, price: Number(e.target.value) })}
- placeholder="Ex: 89.90"
- className="w-full glass-input rounded-lg p-3 text-sm text-white focus:outline-none font-mono"
+ className="w-full glass-input rounded-lg p-3 text-sm text-white focus:outline-none"
/>
-
+
+
+
+
+
+
)}
+
+ {/* --- ACTIVITY MODAL --- */}
+ {showActivityModal && (
+
+
+
Gerenciar Atividades do Módulo
+
+
+ {activityQuestions.map((q, qIndex) => (
+
+
+
Pergunta {qIndex + 1}
+
+
+
{
+ const newQ = [...activityQuestions];
+ newQ[qIndex].question = e.target.value;
+ setActivityQuestions(newQ);
+ }}
+ placeholder="Digite a pergunta..."
+ className="w-full glass-input rounded-lg p-3 text-sm text-white focus:outline-none"
+ />
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ )}
);
}
diff --git a/src/components/AdminDashboard.tsx b/src/components/AdminDashboard.tsx
index 8a8a849..05eac23 100644
--- a/src/components/AdminDashboard.tsx
+++ b/src/components/AdminDashboard.tsx
@@ -21,6 +21,12 @@ interface Student {
createdAt: string;
totalPaid: number;
unlockedCourses?: string[];
+ moduleGrades?: { id: string, activityId: string, score: number, passed: boolean }[];
+ certificates?: { id: string, courseId: string | null, moduleId: string | null, certificateType: string, unlockedAt: string, finalGrade: number | null, courseName: string, moduleName: string | null }[];
+ birthDate?: string;
+ whatsapp?: string;
+ trialEndsAt?: string | null;
+ payments?: PaymentLog[];
}
interface PaymentLog {
@@ -58,16 +64,59 @@ export default function AdminDashboard({ token }: AdminDashboardProps) {
const [expandedStudentId, setExpandedStudentId] = useState(null);
const [searchQuery, setSearchQuery] = useState('');
- const [activeTab, setActiveTab] = useState<'finance' | 'students' | 'webhooks'>('finance');
+ const [activeTab, setActiveTab] = useState<'finance' | 'students' | 'webhooks' | 'settings'>('finance');
const [isLoading, setIsLoading] = useState(true);
const [actionLoading, setActionLoading] = useState(null);
+ // Settings State
+ const [settings, setSettings] = useState(null);
+ const [isSavingSettings, setIsSavingSettings] = useState(false);
+
useEffect(() => {
fetchDashboardData();
fetchStudents();
fetchCourses();
+ fetchSettings();
}, []);
+ const fetchSettings = async () => {
+ try {
+ const res = await fetch('/api/admin/settings', {
+ headers: { 'Authorization': `Bearer ${token}` }
+ });
+ if (res.ok) {
+ const data = await res.json();
+ setSettings(data);
+ }
+ } catch (err) {
+ console.error('Error fetching settings:', err);
+ }
+ };
+
+ const handleSaveSettings = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsSavingSettings(true);
+ try {
+ const res = await fetch('/api/admin/settings', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`
+ },
+ body: JSON.stringify(settings)
+ });
+ if (res.ok) {
+ alert('Configurações salvas com sucesso!');
+ } else {
+ alert('Erro ao salvar configurações.');
+ }
+ } catch (err) {
+ console.error('Error saving settings:', err);
+ } finally {
+ setIsSavingSettings(false);
+ }
+ };
+
const fetchCourses = async () => {
try {
const res = await fetch('/api/courses', {
@@ -160,6 +209,30 @@ export default function AdminDashboard({ token }: AdminDashboardProps) {
}
};
+ const handleUnlockCertificate = async (studentId: string, type: 'COURSE' | 'MODULE', targetId: string, courseName: string, moduleName?: string) => {
+ setActionLoading(`cert_${studentId}_${targetId}`);
+ try {
+ const res = await fetch(`/api/admin/students/${studentId}/unlock-certificate`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`
+ },
+ body: JSON.stringify({ type, targetId, courseName, moduleName })
+ });
+ if (res.ok) {
+ await fetchStudents();
+ alert('Certificado liberado manualmente!');
+ } else {
+ alert('Erro ao liberar certificado.');
+ }
+ } catch (err) {
+ console.error('Error unlocking certificate:', err);
+ } finally {
+ setActionLoading(null);
+ }
+ };
+
const getStatusTag = (status: SubscriptionStatus) => {
switch (status) {
case 'ACTIVE':
@@ -288,23 +361,34 @@ export default function AdminDashboard({ token }: AdminDashboardProps) {
{/* TABS CONTAINER */}
-
-
-
@@ -415,16 +499,14 @@ export default function AdminDashboard({ token }: AdminDashboardProps) {
filteredStudents.map(student => (
- |
- {student.name}
- {student.email}
- {student.unlockedCourses && student.unlockedCourses.length > 0 && (
-
-
- {student.unlockedCourses.length} {student.unlockedCourses.length === 1 ? 'Curso Avulso' : 'Cursos Avulsos'}
-
+
+
+
+ {student.name}
+ {student.email}
+ {student.whatsapp && WA: {student.whatsapp} }
- )}
+
|
{student.asaasCustomerId || Sem vínculo}
@@ -531,6 +613,55 @@ export default function AdminDashboard({ token }: AdminDashboardProps) {
})}
)}
+
+
+ {/* --- CERTIFICATE MANAGEMENT --- */}
+
+
+
+ Certificados & Desempenho
+
+
+
+
+ {/* Grades Table */}
+
+ Notas do Aluno
+ {(!student.moduleGrades || student.moduleGrades.length === 0) ? (
+ Nenhuma avaliação realizada ainda.
+ ) : (
+
+ {student.moduleGrades.map((grade, idx) => (
+
+ Avaliação {grade.activityId.substring(0, 8)}
+
+ Nota: {grade.score.toFixed(1)}
+
+
+ ))}
+
+ )}
+
+
+ {/* Certificates Manager */}
+
+ Desbloqueio de Certificados
+ {courses.map(course => (
+
+
+ {course.title}
+ handleUnlockCertificate(student.id, 'COURSE', course.id, course.title)}
+ className="bg-zinc-800 hover:bg-[#E50914] text-white px-2 py-1 rounded transition-colors"
+ >
+ Liberar (Curso)
+
+
+
+ ))}
+
+
+
|
|
@@ -607,6 +738,104 @@ export default function AdminDashboard({ token }: AdminDashboardProps) {
)}
+ {/* SETTINGS TAB */}
+ {activeTab === 'settings' && settings && (
+
+
+
+
Configurações do Sistema & Asaas (Sandbox)
+
+
+
+
+ )}
);
diff --git a/src/components/CertificateView.tsx b/src/components/CertificateView.tsx
new file mode 100644
index 0000000..d6d2774
--- /dev/null
+++ b/src/components/CertificateView.tsx
@@ -0,0 +1,121 @@
+import React, { useRef } from 'react';
+import { Download, Award, ShieldCheck } from 'lucide-react';
+// @ts-ignore
+// html2pdf is loaded globally via CDN in index.html
+
+interface CertificateViewProps {
+ certificate: {
+ id: string;
+ courseName: string;
+ moduleName?: string | null;
+ certificateType: 'COURSE' | 'MODULE';
+ unlockedAt: string;
+ finalGrade: number | null;
+ };
+ studentName: string;
+}
+
+export default function CertificateView({ certificate, studentName }: CertificateViewProps) {
+ const certRef = useRef(null);
+
+ const handleDownload = () => {
+ if (certRef.current) {
+ const opt = {
+ margin: 0,
+ filename: `Certificado_${certificate.courseName.replace(/\s+/g, '_')}.pdf`,
+ image: { type: 'jpeg', quality: 1 },
+ html2canvas: { scale: 2, useCORS: true },
+ jsPDF: { unit: 'in', format: 'letter', orientation: 'landscape' }
+ };
+
+ // @ts-ignore
+ html2pdf().set(opt).from(certRef.current).save();
+ }
+ };
+
+ return (
+
+
+
+
+ Seu Certificado
+
+
+
+ Baixar PDF
+
+
+
+
+ {/* Certificate Container (Will be converted to PDF) */}
+
+ {/* Decorative Borders */}
+
+
+
+
+
+ {/* Content */}
+
+
+
+
+
+
+
+ Certificado de Conclusão
+
+
+ MicrotecFlix - Plataforma de Estudos
+
+
+
Certificamos que
+
+
+ {studentName}
+
+
+
+ concluiu com êxito o {certificate.certificateType === 'COURSE' ? 'curso' : 'módulo'}:
+
+
+
+ {certificate.certificateType === 'COURSE'
+ ? certificate.courseName
+ : `${certificate.moduleName} (${certificate.courseName})`}
+
+
+
+
+
Data de Conclusão
+
{new Date(certificate.unlockedAt).toLocaleDateString('pt-BR')}
+
+
+ {certificate.finalGrade !== null && (
+
+
Nota Final
+
{certificate.finalGrade.toFixed(1)} / 10
+
+ )}
+
+
+
Código de Validação
+
{certificate.id.toUpperCase()}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/CertificatesTab.tsx b/src/components/CertificatesTab.tsx
new file mode 100644
index 0000000..bc912da
--- /dev/null
+++ b/src/components/CertificatesTab.tsx
@@ -0,0 +1,75 @@
+import React, { useState, useEffect } from 'react';
+import CertificateView from './CertificateView';
+import { Award, RefreshCw } from 'lucide-react';
+
+interface CertificatesTabProps {
+ token: string | null;
+ studentName: string;
+}
+
+export default function CertificatesTab({ token, studentName }: CertificatesTabProps) {
+ const [certificates, setCertificates] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ fetchCertificates();
+ }, []);
+
+ const fetchCertificates = async () => {
+ setIsLoading(true);
+ try {
+ const res = await fetch('/api/certificates', {
+ headers: { 'Authorization': `Bearer ${token}` }
+ });
+ if (res.ok) {
+ const data = await res.json();
+ setCertificates(data);
+ }
+ } catch (err) {
+ console.error('Error fetching certificates:', err);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+
+
Carregando seus certificados...
+
+ );
+ }
+
+ if (certificates.length === 0) {
+ return (
+
+
+
Nenhum certificado ainda
+
+ Complete os cursos e as avaliações dos módulos para desbloquear seus certificados.
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ Meus Certificados
+
+
+ Você possui {certificates.length} {certificates.length === 1 ? 'certificado' : 'certificados'} desbloqueados.
+
+
+
+
+ {certificates.map(cert => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/CourseActivity.tsx b/src/components/CourseActivity.tsx
new file mode 100644
index 0000000..bc2f702
--- /dev/null
+++ b/src/components/CourseActivity.tsx
@@ -0,0 +1,201 @@
+import React, { useState, useEffect } from 'react';
+import { ArrowLeft, CheckCircle, XCircle, Award } from 'lucide-react';
+
+interface CourseActivityProps {
+ token: string | null;
+ moduleId: string;
+ onBack: () => void;
+ onCompleted: (passed: boolean) => void;
+}
+
+export default function CourseActivity({ token, moduleId, onBack, onCompleted }: CourseActivityProps) {
+ const [activity, setActivity] = useState(null);
+ const [answers, setAnswers] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [result, setResult] = useState<{ score: number, passed: boolean } | null>(null);
+
+ useEffect(() => {
+ fetchActivity();
+ }, [moduleId]);
+
+ const fetchActivity = async () => {
+ setIsLoading(true);
+ try {
+ const res = await fetch(`/api/modules/${moduleId}/activity`, {
+ headers: { 'Authorization': `Bearer ${token}` }
+ });
+ if (res.ok) {
+ const data = await res.json();
+ setActivity(data);
+ setAnswers(new Array(data.questions.length).fill(-1));
+ } else {
+ setActivity(null);
+ }
+ } catch (err) {
+ console.error('Error fetching activity:', err);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleSelectAnswer = (qIndex: number, optIndex: number) => {
+ const newAnswers = [...answers];
+ newAnswers[qIndex] = optIndex;
+ setAnswers(newAnswers);
+ };
+
+ const handleSubmit = async () => {
+ if (answers.includes(-1)) {
+ alert('Por favor, responda todas as perguntas.');
+ return;
+ }
+
+ setIsLoading(true);
+ try {
+ const res = await fetch(`/api/modules/${moduleId}/submit-activity`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`
+ },
+ body: JSON.stringify({ answers })
+ });
+
+ if (res.ok) {
+ const data = await res.json();
+ setResult({ score: data.score, passed: data.passed });
+ if (data.passed) {
+ onCompleted(true);
+ }
+ } else {
+ alert('Erro ao enviar atividade.');
+ }
+ } catch (err) {
+ console.error('Error submitting activity:', err);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+
+
Carregando atividade...
+
+ );
+ }
+
+ if (!activity || !activity.questions || activity.questions.length === 0) {
+ return (
+
+
+
Nenhuma avaliação disponível
+
Este módulo ainda não possui uma atividade cadastrada.
+
+ Voltar para as Aulas
+
+
+ );
+ }
+
+ if (result) {
+ return (
+
+
+
+
+ {result.passed ? (
+
+
+
+
+
Parabéns!
+
Você foi aprovado nesta atividade.
+
+
Sua Nota: {result.score.toFixed(1)} / 10
+
+
+ ) : (
+
+
+
+
+
Não foi dessa vez.
+
Sua nota não atingiu a média mínima de 6.0.
+
+
Sua Nota: {result.score.toFixed(1)} / 10
+
+
Revise o conteúdo e tente novamente.
+
+ )}
+
+
+
+ Voltar para as Aulas
+
+ {!result.passed && (
+ { setResult(null); setAnswers(new Array(activity.questions.length).fill(-1)); }} className="glass-btn-primary px-6 py-3 rounded-xl text-white font-bold text-sm">
+ Tentar Novamente
+
+ )}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
Avaliação do Módulo
+
+
+
+ {activity.questions.map((q: any, qIndex: number) => (
+
+
+ {qIndex + 1}.
+ {q.question}
+
+
+ {q.options.map((opt: string, optIndex: number) => (
+
handleSelectAnswer(qIndex, optIndex)}
+ className={`w-full text-left p-4 rounded-xl border transition-all duration-200 flex items-center space-x-3 ${
+ answers[qIndex] === optIndex
+ ? 'bg-zinc-800 border-[#E50914] shadow-[0_0_15px_rgba(229,9,20,0.15)]'
+ : 'bg-zinc-900/50 border-white/5 hover:bg-zinc-800 hover:border-white/10'
+ }`}
+ >
+
+ {answers[qIndex] === optIndex &&
}
+
+
+ {opt}
+
+
+ ))}
+
+
+ ))}
+
+
+
+ Finalizar Avaliação
+
+
+
+
+ );
+}
diff --git a/src/components/CourseView.tsx b/src/components/CourseView.tsx
index 3c54f78..c413296 100644
--- a/src/components/CourseView.tsx
+++ b/src/components/CourseView.tsx
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
-import { ArrowLeft, BookOpen, ChevronRight, FileText, CheckCircle2, Circle, MessageSquare, AlertCircle } from 'lucide-react';
+import { ArrowLeft, BookOpen, ChevronRight, FileText, CheckCircle2, Circle, MessageSquare, AlertCircle, Award } from 'lucide-react';
import VideoPlayer from './VideoPlayer';
+import CourseActivity from './CourseActivity';
interface LessonProgress {
watchedPercentage: number;
@@ -49,6 +50,7 @@ export default function CourseView({ courseId, token, onBack }: CourseViewProps)
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
+ const [activeActivityModuleId, setActiveActivityModuleId] = useState(null);
useEffect(() => {
fetchCourseDetails();
@@ -209,6 +211,22 @@ export default function CourseView({ courseId, token, onBack }: CourseViewProps)
);
}
+ if (activeActivityModuleId) {
+ return (
+ setActiveActivityModuleId(null)}
+ onCompleted={(passed) => {
+ if (passed) {
+ // Re-fetch course details to update any unlocked certificates if applicable
+ fetchCourseDetails();
+ }
+ }}
+ />
+ );
+ }
+
return (
{/* Back Header */}
@@ -367,6 +385,19 @@ export default function CourseView({ courseId, token, onBack }: CourseViewProps)
);
})
)}
+
+ {/* Module Activity Button */}
+ {mod.lessons.length > 0 && (
+
+
setActiveActivityModuleId(mod.id)}
+ className="w-full flex items-center justify-center space-x-2 bg-zinc-800 hover:bg-[#E50914] hover:text-white text-zinc-300 transition-colors py-2.5 rounded-lg text-xs font-bold uppercase tracking-wider group"
+ >
+
+ Avaliação do Módulo
+
+
+ )}
))}
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 9948278..d39d46e 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -62,10 +62,18 @@ export default function Navbar({ user, activeView, setView, onLogout }: NavbarPr
Cursos
setView('subscription')}
- className={`font-medium transition-colors hover:text-[#E50914] ${activeView === 'subscription' ? 'text-white' : 'text-zinc-400'}`}
+ onClick={() => setView('certificates')}
+ className={`font-medium transition-colors hover:text-[#E50914] flex items-center space-x-1 ${activeView === 'certificates' ? 'text-white' : 'text-zinc-400'}`}
>
- Minha Assinatura
+
+ Meu Certificado
+
+ setView('subscription')}
+ className={`font-medium transition-colors hover:text-[#E50914] flex items-center space-x-1 ${activeView === 'subscription' ? 'text-white' : 'text-zinc-400'}`}
+ >
+
+ Minha Assinatura
diff --git a/src/db/db.ts b/src/db/db.ts
index 413d47d..c040878 100644
--- a/src/db/db.ts
+++ b/src/db/db.ts
@@ -14,6 +14,10 @@ export interface DatabaseSchema {
progress: LessonProgress[];
webhookLogs: WebhookLog[];
payments: SubscriptionPayment[];
+ activities: any[];
+ moduleGrades: any[];
+ certificates: any[];
+ settings: any; // SystemSettings
}
const DEFAULT_DB: DatabaseSchema = {
@@ -24,7 +28,17 @@ const DEFAULT_DB: DatabaseSchema = {
notes: [],
progress: [],
webhookLogs: [],
- payments: []
+ payments: [],
+ activities: [],
+ moduleGrades: [],
+ certificates: [],
+ settings: {
+ trialDurationDays: 7,
+ asaasEnvironment: 'sandbox',
+ asaasApiKey: '',
+ asaasWebhookUrl: '',
+ asaasWebhookSecret: ''
+ }
};
// Helper to load db
@@ -395,6 +409,16 @@ function createInitialDb(): DatabaseSchema {
notes,
progress,
webhookLogs,
- payments
+ payments,
+ activities: [],
+ moduleGrades: [],
+ certificates: [],
+ settings: {
+ trialDurationDays: 7,
+ asaasEnvironment: 'sandbox',
+ asaasApiKey: '',
+ asaasWebhookUrl: '',
+ asaasWebhookSecret: ''
+ }
};
}
diff --git a/src/services/asaas.ts b/src/services/asaas.ts
index a1b50ac..0542597 100644
--- a/src/services/asaas.ts
+++ b/src/services/asaas.ts
@@ -1,15 +1,16 @@
import { SubscriptionPayment } from '../types';
+import { loadDb } from '../db/db';
export class AsaasService {
private static getApiKey(): string | null {
- return process.env.ASAAS_API_KEY || null;
+ const db = loadDb();
+ return db.settings?.asaasApiKey || process.env.ASAAS_API_KEY || null;
}
private static getApiUrl(): string {
- // If we have an API key, check if it looks like a production or sandbox one.
- // Standard sandbox keys often start with '$' or are long hashes, let's allow configuring via env or default to sandbox
- const isProd = process.env.ASAAS_ENV === 'production';
- return isProd ? 'https://api.asaas.com/v3' : 'https://sandbox.asaas.com/v3';
+ const db = loadDb();
+ const env = db.settings?.asaasEnvironment || 'sandbox';
+ return env === 'production' ? 'https://api.asaas.com/v3' : 'https://sandbox.asaas.com/v3';
}
private static getHeaders() {
diff --git a/src/types.ts b/src/types.ts
index 5132c3e..e158fe3 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -10,15 +10,27 @@ export interface User {
subscriptionStatus: SubscriptionStatus;
asaasCustomerId: string | null;
unlockedCourses?: string[]; // IDs of courses unlocked individually
+ birthDate?: string;
+ whatsapp?: string;
+ trialEndsAt?: string | null;
createdAt: string;
}
+export interface SystemSettings {
+ trialDurationDays: number;
+ asaasEnvironment: 'sandbox' | 'production';
+ asaasApiKey: string;
+ asaasWebhookUrl: string;
+ asaasWebhookSecret: string;
+}
+
export interface Course {
id: string;
title: string;
description: string;
thumbnail: string;
category: string;
+ certificateMode?: 'PER_MODULE' | 'FULL_COURSE';
isLocked?: boolean; // If true, requires payment/unlock
price?: number; // Cost to unlock if locked (e.g. 89.90)
createdAt: string;
@@ -85,3 +97,31 @@ export interface AdminDashboardStats {
overdueSubscribers: number;
canceledSubscribers: number;
}
+
+export interface ModuleActivity {
+ id: string;
+ moduleId: string;
+ questions: any[];
+ createdAt: string;
+}
+
+export interface ModuleGrade {
+ id: string;
+ userId: string;
+ activityId: string;
+ score: number;
+ passed: boolean;
+ createdAt: string;
+}
+
+export interface Certificate {
+ id: string;
+ userId: string;
+ courseId: string | null;
+ moduleId: string | null;
+ certificateType: 'COURSE' | 'MODULE';
+ unlockedAt: string;
+ finalGrade: number | null;
+ courseName: string;
+ moduleName: string | null;
+}