feat: novo layout de boletim, suporte a refazer provas e nomenclatura unificada
This commit is contained in:
parent
2f50468cc5
commit
bf4ebd8b6b
|
|
@ -32,3 +32,6 @@
|
||||||
10. **Justification Logic**: Attendance justifications MUST include `fromStudentId` in notification metadata and support both `arquivo` and `arquivo_base64` keys for attachment compatibility. Notifications SHOULD include the motive text in the `message` field (JSON format: `{text, motivo}`) to allow previews in the manager bell.
|
10. **Justification Logic**: Attendance justifications MUST include `fromStudentId` in notification metadata and support both `arquivo` and `arquivo_base64` keys for attachment compatibility. Notifications SHOULD include the motive text in the `message` field (JSON format: `{text, motivo}`) to allow previews in the manager bell.
|
||||||
11. **Modal Standards**: System modals should utilize `bg-transparent` (no background darkening or blur) with `shadow-2xl` for depth, ensuring a clean and float-like aesthetic.
|
11. **Modal Standards**: System modals should utilize `bg-transparent` (no background darkening or blur) with `shadow-2xl` for depth, ensuring a clean and float-like aesthetic.
|
||||||
12. **Messages Automation**: Preventive reminders and overdue settings MUST be managed within their respective template modals (contextual logic), with independent manual triggers for each phase (Overdue vs. Upcoming).
|
12. **Messages Automation**: Preventive reminders and overdue settings MUST be managed within their respective template modals (contextual logic), with independent manual triggers for each phase (Overdue vs. Upcoming).
|
||||||
|
13. **Grading & Evaluation Standards**: Assessments are categorized as `exam` (violet/violet labels) or `activity` (sky/blue labels). The report card (Boletim) MUST support multiple evaluations per period, showing the individual breakdown (name and value). The module MUST be named **"Atividades e Provas"** for clarity.
|
||||||
|
14. **Asaas Safety**: All financial generation forms MUST implement a loading state (`isCreating`) to disable submit buttons and prevent duplicate charges.
|
||||||
|
15. **Retake Policy**: Students ARE allowed to retake activities and exams. The system MUST delete the previous submission and overwrite the grade in `school_data.json` to ensure only the latest attempt is valid.
|
||||||
|
|
|
||||||
11
MEMORY.md
11
MEMORY.md
|
|
@ -12,11 +12,12 @@
|
||||||
- [x] Correção das Imagens de Prova: Normalização das URLs nas questões de avaliações (Portal e Manager).
|
- [x] Correção das Imagens de Prova: Normalização das URLs nas questões de avaliações (Portal e Manager).
|
||||||
- [x] Estabilização de CI/CD: Transição para `runs-on: self-hosted` (ARM64 nativo) eliminando lentidão e crashes do QEMU.
|
- [x] Estabilização de CI/CD: Transição para `runs-on: self-hosted` (ARM64 nativo) eliminando lentidão e crashes do QEMU.
|
||||||
- [x] Correção do Sino de Notificações: Botões sempre visíveis, suporte a anexo via chave `arquivo` e exibição do **Motivo da Falta** direto na lista do sino.
|
- [x] Correção do Sino de Notificações: Botões sempre visíveis, suporte a anexo via chave `arquivo` e exibição do **Motivo da Falta** direto na lista do sino.
|
||||||
- [x] **Segurança Financeira:** Implementado estado de carregamento (`isCreating`) no botão de gerar cobranças para impedir disparos duplicados ao Asaas por cliques múltiplos.
|
- [x] **Segurança Financeira:** Implementada trava de segurança (`isCreating`) contra cliques múltiplos em formulários financeiros, resolvendo a duplicidade de cobranças no Asaas.
|
||||||
- [x] **Boletim & Avaliações:** Refatoração completa do sistema de notas para suportar múltiplas avaliações por período, integrando notas diretas e notas vindas de provas/atividades online.
|
- [x] **Boletim Detalhado (Manager):** Upgrade para layout de lista (Full-Width) com cores distintas: **Violeta (Provas)** e **Azul (Atividades)**.
|
||||||
- [x] **Sincronia Portal/Manager:** Ajustada a submissão de provas no Portal para calcular notas via `maxScore` e injetar automaticamente no boletim do Gerenciador via `examId`.
|
- [x] **Retake Logic:** Implementada possibilidade de refazer Provas/Atividades no Portal, com substituição automática da nota anterior e limpeza de submissão no banco.
|
||||||
- [x] **Padronização de Servidor:** Confirmado o uso de `server.selfhosted.js` em ambos os apps como ponto de entrada para garantir 100% de funcionalidades locais.
|
- [x] **Mapeamento de Períodos:** Corrigido o bug que exibia UUIDs (códigos) no boletim do aluno; agora exibe os nomes amigáveis (ex: 1º Bimestre).
|
||||||
- [ ] Próximo Passo: Monitorar o desempenho das submissões de provas simultâneas no Portal.
|
- [x] **Nomenclatura Unificada:** Alterado "Avaliações" para **"Atividades e Provas"** em todo o ecossistema (Portal e Manager).
|
||||||
|
- [ ] Próximo Passo: Analisar a necessidade de pesos diferenciados (médias ponderadas) entre Atividades e Provas no cálculo do boletim.
|
||||||
|
|
||||||
### 💳 Módulo Financeiro (Portal do Aluno)
|
### 💳 Módulo Financeiro (Portal do Aluno)
|
||||||
- **Funcionalidades Implementadas:**
|
- **Funcionalidades Implementadas:**
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ const AdminNotifications: React.FC<Props> = ({ data, updateData, setView, onNavi
|
||||||
<div className="absolute top-14 right-0 w-80 sm:w-96 bg-white rounded-2xl shadow-2xl border border-slate-200 overflow-hidden animate-in slide-in-from-top-4 fade-in duration-200 flex flex-col max-h-[80vh]">
|
<div className="absolute top-14 right-0 w-80 sm:w-96 bg-white rounded-2xl shadow-2xl border border-slate-200 overflow-hidden animate-in slide-in-from-top-4 fade-in duration-200 flex flex-col max-h-[80vh]">
|
||||||
<div className="p-4 bg-slate-50 border-b border-slate-200 flex items-center justify-between sticky top-0 z-10">
|
<div className="p-4 bg-slate-50 border-b border-slate-200 flex items-center justify-between sticky top-0 z-10">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-black text-slate-800 flex items-center gap-2">Avaliações Pendentes
|
<h3 className="font-black text-slate-800 flex items-center gap-2">Atividades/Provas Pendentes
|
||||||
{unreadCount > 0 && <span className="bg-indigo-100 text-indigo-700 px-2 py-0.5 rounded-full text-[10px] font-bold">{unreadCount}</span>}
|
{unreadCount > 0 && <span className="bg-indigo-100 text-indigo-700 px-2 py-0.5 rounded-full text-[10px] font-bold">{unreadCount}</span>}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -419,7 +419,7 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-3xl font-black text-slate-800 tracking-tight flex items-center gap-3">
|
<h2 className="text-3xl font-black text-slate-800 tracking-tight flex items-center gap-3">
|
||||||
<BookOpen className="text-indigo-600" size={32} /> Avaliações
|
<BookOpen className="text-indigo-600" size={32} /> Atividades e Provas
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-slate-500 mt-2 font-medium">Gerencie as provas e testes das turmas.</p>
|
<p className="text-slate-500 mt-2 font-medium">Gerencie as provas e testes das turmas.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -490,12 +490,27 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
|
||||||
<h4 className="font-black text-slate-800 uppercase tracking-wider text-sm">{subject.name}</h4>
|
<h4 className="font-black text-slate-800 uppercase tracking-wider text-sm">{subject.name}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{linkedExams.length > 0 && (
|
{(() => {
|
||||||
|
const linkedExams = (data.exams || []).filter(e => e.subjectId === subject.id && e.status === 'published');
|
||||||
|
const provasCount = linkedExams.filter(e => (e as any).evaluationType !== 'activity').length;
|
||||||
|
const atividadesCount = linkedExams.filter(e => (e as any).evaluationType === 'activity').length;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{provasCount > 0 && (
|
||||||
<div className="px-3 py-1 bg-violet-50 border border-violet-200 rounded-lg text-[10px] font-black text-violet-600 flex items-center gap-1">
|
<div className="px-3 py-1 bg-violet-50 border border-violet-200 rounded-lg text-[10px] font-black text-violet-600 flex items-center gap-1">
|
||||||
<FileText size={12} />
|
<FileText size={12} />
|
||||||
{linkedExams.length} {linkedExams.length === 1 ? 'Prova' : 'Provas'}
|
{provasCount} {provasCount === 1 ? 'Prova' : 'Provas'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{atividadesCount > 0 && (
|
||||||
|
<div className="px-3 py-1 bg-sky-50 border border-sky-200 rounded-lg text-[10px] font-black text-sky-600 flex items-center gap-1">
|
||||||
|
<FileText size={12} />
|
||||||
|
{atividadesCount} {atividadesCount === 1 ? 'Atividade' : 'Atividades'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
<div className="px-3 py-1 bg-white border border-slate-200 rounded-lg text-[10px] font-black text-slate-500">
|
<div className="px-3 py-1 bg-white border border-slate-200 rounded-lg text-[10px] font-black text-slate-500">
|
||||||
MÉDIA: {(() => {
|
MÉDIA: {(() => {
|
||||||
const subjectGrades = studentGrades[subject.id] || {};
|
const subjectGrades = studentGrades[subject.id] || {};
|
||||||
|
|
@ -506,7 +521,7 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{periods.map(period => {
|
{periods.map(period => {
|
||||||
const linkedExams = (data.exams || []).filter(e => e.subjectId === subject.id && e.periodId === period.id && e.status === 'published');
|
const linkedExams = (data.exams || []).filter(e => e.subjectId === subject.id && e.periodId === period.id && e.status === 'published');
|
||||||
const periodGrades = studentGrades[subject.id]?.[period.id] || {};
|
const periodGrades = studentGrades[subject.id]?.[period.id] || {};
|
||||||
|
|
@ -520,28 +535,35 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{linkedExams.length > 0 ? (
|
{linkedExams.length > 0 ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-4">
|
||||||
{linkedExams.map(exam => {
|
{linkedExams.map(exam => {
|
||||||
const isActivity = (exam as any).evaluationType === 'activity';
|
const isActivity = (exam as any).evaluationType === 'activity';
|
||||||
const maxScore = (exam as any).maxScore ?? 10;
|
const maxScore = (exam as any).maxScore ?? 10;
|
||||||
return (
|
return (
|
||||||
<div key={exam.id} className="space-y-1">
|
<div key={exam.id} className={`p-4 rounded-xl border flex flex-col md:flex-row md:items-center justify-between gap-4 ${isActivity ? 'bg-sky-50/50 border-sky-100' : 'bg-violet-50/50 border-violet-100'}`}>
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex-1">
|
||||||
<span className="text-[10px] font-bold text-slate-600 truncate pr-2" title={exam.title}>
|
<div className="flex flex-col">
|
||||||
<span className={`mr-1 px-1.5 py-0.5 rounded text-[8px] uppercase tracking-wider ${isActivity ? 'bg-sky-100 text-sky-700' : 'bg-violet-100 text-violet-700'}`}>
|
<div className="text-sm font-bold text-slate-800 leading-tight mb-1 flex items-center gap-2">
|
||||||
{isActivity ? 'Ativ' : 'Prova'}
|
<span className={`px-2 py-0.5 rounded text-[9px] uppercase tracking-wider font-black shrink-0 ${isActivity ? 'bg-sky-200 text-sky-800' : 'bg-violet-200 text-violet-800'}`}>
|
||||||
|
{isActivity ? 'Atividade' : 'Prova'}
|
||||||
</span>
|
</span>
|
||||||
{exam.title}
|
{exam.title}
|
||||||
</span>
|
|
||||||
<span className="text-[9px] text-slate-400 font-bold whitespace-nowrap shrink-0">Vale {maxScore}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
{exam.description && (
|
||||||
|
<p className="text-xs text-slate-500 leading-snug pr-2">{exam.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 shrink-0">
|
||||||
|
<span className="text-[10px] font-bold text-slate-500 uppercase">Nota (Máx {maxScore})</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
max={maxScore}
|
max={maxScore}
|
||||||
step="0.1"
|
step="0.1"
|
||||||
placeholder="—"
|
placeholder="—"
|
||||||
className="w-full px-2 py-1.5 bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all text-sm font-bold text-center"
|
className={`w-24 px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 transition-all text-sm font-black text-center ${isActivity ? 'bg-white border-sky-200 focus:ring-sky-500' : 'bg-white border-violet-200 focus:ring-violet-500'}`}
|
||||||
value={studentGrades[subject.id]?.[period.id]?.[exam.id] ?? ''}
|
value={studentGrades[subject.id]?.[period.id]?.[exam.id] ?? ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
let val = parseFloat(e.target.value);
|
let val = parseFloat(e.target.value);
|
||||||
|
|
@ -560,6 +582,7 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ const Sidebar: React.FC<SidebarProps> = ({ currentView, setView, user, logo, onL
|
||||||
{ id: View.Courses, icon: GraduationCap, label: 'Cursos' },
|
{ id: View.Courses, icon: GraduationCap, label: 'Cursos' },
|
||||||
{ id: View.Students, icon: Users, label: 'Alunos' },
|
{ id: View.Students, icon: Users, label: 'Alunos' },
|
||||||
{ id: View.Classes, icon: BookOpen, label: 'Turmas' },
|
{ id: View.Classes, icon: BookOpen, label: 'Turmas' },
|
||||||
{ id: View.Exams, icon: ClipboardList, label: 'Avaliações' },
|
{ id: View.Exams, icon: ClipboardList, label: 'Atividades e Provas' },
|
||||||
{ id: View.ReportCard, icon: FileText, label: 'Boletim Escolar' },
|
{ id: View.ReportCard, icon: FileText, label: 'Boletim Escolar' },
|
||||||
{ id: View.Finance, icon: CircleDollarSign, label: 'Financeiro' },
|
{ id: View.Finance, icon: CircleDollarSign, label: 'Financeiro' },
|
||||||
{ id: View.Contracts, icon: FileSignature, label: 'Contratos' },
|
{ id: View.Contracts, icon: FileSignature, label: 'Contratos' },
|
||||||
|
|
|
||||||
|
|
@ -251,15 +251,17 @@ app.get('/api/portal/notas', authMiddleware, async (req, res) => {
|
||||||
const enrichedGrades = grades.map((g) => {
|
const enrichedGrades = grades.map((g) => {
|
||||||
const subject = subjects.find((s) => s.id === g.subjectId);
|
const subject = subjects.find((s) => s.id === g.subjectId);
|
||||||
const exam = g.examId ? (schoolData.exams || []).find(e => e.id === g.examId) : null;
|
const exam = g.examId ? (schoolData.exams || []).find(e => e.id === g.examId) : null;
|
||||||
|
const periodObj = (schoolData.periods || []).find(p => p.id === g.period);
|
||||||
return {
|
return {
|
||||||
...g,
|
...g,
|
||||||
subjectName: subject?.name || 'Disciplina desconhecida',
|
subjectName: subject?.name || 'Disciplina desconhecida',
|
||||||
examTitle: exam?.title,
|
examTitle: exam?.title,
|
||||||
evaluationType: exam?.evaluationType || 'exam',
|
evaluationType: exam?.evaluationType || 'exam',
|
||||||
maxScore: exam?.maxScore
|
maxScore: exam?.maxScore,
|
||||||
|
periodName: periodObj ? periodObj.name : g.period
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const periods = [...new Set(grades.map((g) => g.period))];
|
const periods = [...new Set(enrichedGrades.map((g) => g.periodName))];
|
||||||
if (periods.length === 0) periods.push('1º Bimestre', '2º Bimestre', '3º Bimestre', '4º Bimestre');
|
if (periods.length === 0) periods.push('1º Bimestre', '2º Bimestre', '3º Bimestre', '4º Bimestre');
|
||||||
periods.sort();
|
periods.sort();
|
||||||
res.json({ grades: enrichedGrades, periods, allSubjects: courseSubjects });
|
res.json({ grades: enrichedGrades, periods, allSubjects: courseSubjects });
|
||||||
|
|
@ -519,12 +521,17 @@ app.post('/api/portal/avaliacoes/submeter', authMiddleware, async (req, res) =>
|
||||||
const { examId, answers } = req.body;
|
const { examId, answers } = req.body;
|
||||||
if (!examId || !answers) return res.status(400).json({ error: 'Dados obrigatórios' });
|
if (!examId || !answers) return res.status(400).json({ error: 'Dados obrigatórios' });
|
||||||
|
|
||||||
// Verificar se já submeteu
|
// Verificar se já submeteu e deletar a submissão anterior para permitir refazer
|
||||||
const { rows: existing } = await pool.query(
|
const { rows: existing } = await pool.query(
|
||||||
'SELECT id FROM provas_submissoes WHERE aluno_id = $1 AND prova_id = $2 LIMIT 1',
|
'SELECT * FROM provas_submissoes WHERE aluno_id = $1 AND prova_id = $2 LIMIT 1',
|
||||||
[req.user.studentId, examId]
|
[req.user.studentId, examId]
|
||||||
);
|
);
|
||||||
if (existing.length > 0) return res.status(409).json({ error: 'Você já realizou esta prova.' });
|
if (existing.length > 0) {
|
||||||
|
await pool.query(
|
||||||
|
'DELETE FROM provas_submissoes WHERE aluno_id = $1 AND prova_id = $2',
|
||||||
|
[req.user.studentId, examId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const schoolData = await getSchoolData();
|
const schoolData = await getSchoolData();
|
||||||
const exam = (schoolData.exams || []).find(e => e.id === examId);
|
const exam = (schoolData.exams || []).find(e => e.id === examId);
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ const navItems = [
|
||||||
{ path: '/minhas-aulas', label: 'Cronograma', icon: CalendarClock },
|
{ path: '/minhas-aulas', label: 'Cronograma', icon: CalendarClock },
|
||||||
{ path: '/financeiro', label: 'Financeiro', icon: CreditCard },
|
{ path: '/financeiro', label: 'Financeiro', icon: CreditCard },
|
||||||
{ path: '/notas', label: 'Notas', icon: BookOpen },
|
{ path: '/notas', label: 'Notas', icon: BookOpen },
|
||||||
{ path: '/avaliacoes', label: 'Avaliações', icon: ClipboardList },
|
{ path: '/avaliacoes', label: 'Atividades e Provas', icon: ClipboardList },
|
||||||
{ path: '/frequencia', label: 'Frequência', icon: CalendarCheck },
|
{ path: '/frequencia', label: 'Frequência', icon: CalendarCheck },
|
||||||
{ path: '/contratos', label: 'Contratos', icon: FileText },
|
{ path: '/contratos', label: 'Contratos', icon: FileText },
|
||||||
{ path: '/certificados', label: 'Certificados', icon: Award },
|
{ path: '/certificados', label: 'Certificados', icon: Award },
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export default function Avaliacoes() {
|
||||||
const [exams, setExams] = useState<Exam[]>([]);
|
const [exams, setExams] = useState<Exam[]>([]);
|
||||||
const [submissions, setSubmissions] = useState<ExamSubmission[]>([]);
|
const [submissions, setSubmissions] = useState<ExamSubmission[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [activeTab, setActiveTab] = useState<'exams' | 'activities'>('exams');
|
||||||
|
|
||||||
// Exam mode state
|
// Exam mode state
|
||||||
const [view, setView] = useState<ExamView>('listing');
|
const [view, setView] = useState<ExamView>('listing');
|
||||||
|
|
@ -598,7 +599,7 @@ export default function Avaliacoes() {
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
|
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={18} /> Voltar às Avaliações
|
<ArrowLeft size={18} /> Voltar às Atividades e Provas
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -608,14 +609,62 @@ export default function Avaliacoes() {
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// RENDER: Listing (Default)
|
// RENDER: Listing (Default)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
const filteredExams = exams.filter(e => {
|
||||||
|
const isActivity = (e as any).evaluationType === 'activity';
|
||||||
|
return activeTab === 'activities' ? isActivity : !isActivity;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-container">
|
<div className="page-container">
|
||||||
<div className="animate-fade-in" style={{ marginBottom: '1.5rem' }}>
|
<div className="animate-fade-in" style={{ marginBottom: '1.5rem' }}>
|
||||||
<h1 className="page-title">Avaliações</h1>
|
<h1 className="page-title">Atividades e Provas</h1>
|
||||||
<p className="page-subtitle">Provas e avaliações disponíveis para você</p>
|
<p className="page-subtitle">Provas e atividades disponíveis para você</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{exams.length === 0 ? (
|
{/* Tabs */}
|
||||||
|
<div className="animate-fade-in" style={{
|
||||||
|
display: 'flex', gap: '0.5rem', marginBottom: '2rem',
|
||||||
|
background: 'var(--color-surface-light)', padding: '0.5rem',
|
||||||
|
borderRadius: '12px', border: '1px solid var(--glass-border)',
|
||||||
|
width: 'fit-content'
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('exams')}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 1.5rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: 'none',
|
||||||
|
fontSize: '0.85rem',
|
||||||
|
fontWeight: 700,
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
background: activeTab === 'exams' ? 'var(--color-primary)' : 'transparent',
|
||||||
|
color: activeTab === 'exams' ? 'white' : 'var(--color-text-secondary)',
|
||||||
|
boxShadow: activeTab === 'exams' ? '0 2px 8px var(--bg-primary-alpha)' : 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Provas ({exams.filter(e => (e as any).evaluationType !== 'activity').length})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('activities')}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 1.5rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: 'none',
|
||||||
|
fontSize: '0.85rem',
|
||||||
|
fontWeight: 700,
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
background: activeTab === 'activities' ? 'var(--color-info, #0369a1)' : 'transparent',
|
||||||
|
color: activeTab === 'activities' ? 'white' : 'var(--color-text-secondary)',
|
||||||
|
boxShadow: activeTab === 'activities' ? '0 2px 8px rgba(3, 105, 161, 0.2)' : 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Atividades ({exams.filter(e => (e as any).evaluationType === 'activity').length})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredExams.length === 0 ? (
|
||||||
<div className="glass-card animate-fade-in" style={{
|
<div className="glass-card animate-fade-in" style={{
|
||||||
padding: '4rem 2rem', textAlign: 'center',
|
padding: '4rem 2rem', textAlign: 'center',
|
||||||
color: 'var(--color-text-secondary)',
|
color: 'var(--color-text-secondary)',
|
||||||
|
|
@ -630,7 +679,7 @@ export default function Avaliacoes() {
|
||||||
gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))',
|
gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))',
|
||||||
gap: '1rem',
|
gap: '1rem',
|
||||||
}} className="animate-fade-in stagger-children">
|
}} className="animate-fade-in stagger-children">
|
||||||
{exams.map(exam => {
|
{filteredExams.map(exam => {
|
||||||
const sub = getSubmission(exam.id);
|
const sub = getSubmission(exam.id);
|
||||||
const isDone = !!sub;
|
const isDone = !!sub;
|
||||||
|
|
||||||
|
|
@ -699,6 +748,7 @@ export default function Avaliacoes() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isDone ? (
|
{isDone ? (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||||
padding: '0.75rem 1rem', borderRadius: 10,
|
padding: '0.75rem 1rem', borderRadius: 10,
|
||||||
|
|
@ -717,6 +767,22 @@ export default function Avaliacoes() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
showAppConfirm('Deseja realmente refazer? Sua nota anterior será substituída.', () => startExam(exam));
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: '100%', padding: '0.65rem',
|
||||||
|
borderRadius: 10, border: '1px solid var(--color-primary-alpha)',
|
||||||
|
background: 'transparent', color: 'var(--color-primary)',
|
||||||
|
fontSize: '0.8rem', fontWeight: 700,
|
||||||
|
cursor: 'pointer', transition: 'all 0.2s',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Refazer {(exam as any).evaluationType === 'activity' ? 'Atividade' : 'Prova'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => startExam(exam)}
|
onClick={() => startExam(exam)}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ interface GradeWithSubject extends Grade {
|
||||||
examTitle?: string;
|
examTitle?: string;
|
||||||
evaluationType?: string;
|
evaluationType?: string;
|
||||||
maxScore?: number;
|
maxScore?: number;
|
||||||
|
periodName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Notas() {
|
export default function Notas() {
|
||||||
|
|
@ -139,7 +140,7 @@ export default function Notas() {
|
||||||
|
|
||||||
<div style={{ padding: '1.5rem' }}>
|
<div style={{ padding: '1.5rem' }}>
|
||||||
{periods.map(period => {
|
{periods.map(period => {
|
||||||
const periodGrades = subjectGrades.filter(g => g.period === period);
|
const periodGrades = subjectGrades.filter(g => (g.periodName || g.period) === period);
|
||||||
if (periodGrades.length === 0) return null;
|
if (periodGrades.length === 0) return null;
|
||||||
|
|
||||||
const periodTotal = periodGrades.reduce((sum, g) => sum + g.value, 0);
|
const periodTotal = periodGrades.reduce((sum, g) => sum + g.value, 0);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue