feat: completar fases 3 y 4 del roadmap mvp (gradebook, drag-and-drop, impersonacion) y actualizar README
This commit is contained in:
@@ -301,4 +301,101 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
OPTIMIZACIONES GLOBALES: 720p (1360x768 / 1366x768)
|
||||
Dedicadas al personal operativo de Bedelía en Argentina
|
||||
======================================================= */
|
||||
@media (max-height: 820px), (max-width: 1400px) {
|
||||
/* Densidad de Información en Tablas */
|
||||
.table td, .table th {
|
||||
padding: 0.45rem 0.65rem !important;
|
||||
font-size: 0.84rem !important;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.badge {
|
||||
font-size: 0.72rem !important;
|
||||
padding: 0.3em 0.6em !important;
|
||||
}
|
||||
|
||||
/* Cards y Contenedores */
|
||||
.card {
|
||||
margin-bottom: 0.75rem !important;
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
.card-header {
|
||||
padding: 0.65rem 0.9rem !important;
|
||||
}
|
||||
.card-body {
|
||||
padding: 0.75rem 0.9rem !important;
|
||||
}
|
||||
.stat-card {
|
||||
padding: 0.75rem !important;
|
||||
}
|
||||
|
||||
/* Formularios compactos */
|
||||
.form-control, .form-select {
|
||||
padding: 0.38rem 0.65rem !important;
|
||||
font-size: 0.84rem !important;
|
||||
}
|
||||
.form-label {
|
||||
margin-bottom: 0.2rem !important;
|
||||
font-size: 0.78rem !important;
|
||||
}
|
||||
.btn {
|
||||
padding: 0.35rem 0.75rem !important;
|
||||
font-size: 0.83rem !important;
|
||||
}
|
||||
.btn-sm {
|
||||
padding: 0.22rem 0.55rem !important;
|
||||
font-size: 0.78rem !important;
|
||||
}
|
||||
|
||||
/* Reducción de espaciado en FullCalendar */
|
||||
.fc .fc-toolbar {
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
.fc .fc-toolbar-title {
|
||||
font-size: 1.15rem !important;
|
||||
}
|
||||
.fc .fc-button {
|
||||
padding: 0.25rem 0.55rem !important;
|
||||
font-size: 0.8rem !important;
|
||||
}
|
||||
.fc-event {
|
||||
padding: 2px 4px !important;
|
||||
font-size: 0.75rem !important;
|
||||
margin: 1px 0 !important;
|
||||
}
|
||||
.fc-timegrid-slot {
|
||||
height: 1.7em !important;
|
||||
}
|
||||
|
||||
/* Role banner en Dashboard */
|
||||
.dashboard-role-banner {
|
||||
padding: 0.75rem 1.1rem !important;
|
||||
border-radius: 0.75rem !important;
|
||||
margin-bottom: 0.85rem !important;
|
||||
}
|
||||
.dashboard-role-banner h1 {
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
OPTIMIZACIONES GLOBALES: 1080p (1920x1080)
|
||||
======================================================= */
|
||||
@media (min-width: 1600px) and (min-height: 900px) {
|
||||
.table td, .table th {
|
||||
padding: 0.75rem 1rem !important;
|
||||
font-size: 0.92rem !important;
|
||||
}
|
||||
.card-body {
|
||||
padding: 1.25rem !important;
|
||||
}
|
||||
.fc-event {
|
||||
font-size: 0.82rem !important;
|
||||
padding: 3px 6px !important;
|
||||
}
|
||||
}
|
||||
@@ -189,10 +189,27 @@ app.use((req, res, next) => {
|
||||
};
|
||||
} catch (e) {}
|
||||
}
|
||||
let isImpersonating = false;
|
||||
let impersonatedUser = null;
|
||||
if (user.is_admin() && req.cookies.impersonate_user_id) {
|
||||
isImpersonating = true;
|
||||
const targetId = req.cookies.impersonate_user_id;
|
||||
try {
|
||||
if (req.cookies.impersonate_user_data) {
|
||||
impersonatedUser = JSON.parse(req.cookies.impersonate_user_data);
|
||||
}
|
||||
} catch (e) {}
|
||||
if (!impersonatedUser) {
|
||||
impersonatedUser = { id: targetId, name: `Usuario #${targetId}`, role: 'usuario' };
|
||||
}
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
res.locals.user = user;
|
||||
res.locals.current_user = user;
|
||||
res.locals.role = user.role;
|
||||
res.locals.is_impersonating = isImpersonating;
|
||||
res.locals.impersonated_user = impersonatedUser;
|
||||
res.locals.session = { get: (k, d) => d };
|
||||
res.locals.occupancy_metrics = { get: (k, d) => d };
|
||||
next();
|
||||
@@ -259,6 +276,9 @@ app.use('/api', async (req, res) => {
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
if (req.cookies.impersonate_user_id) {
|
||||
headers['X-Impersonate-User'] = req.cookies.impersonate_user_id;
|
||||
}
|
||||
const flaskUrl = `http://127.0.0.1:5000/api${req.url}`;
|
||||
const response = await axios({
|
||||
method: req.method,
|
||||
|
||||
@@ -65,10 +65,38 @@ const requireAuth = (req, res, next) => {
|
||||
console.error('Error decoding token payload:', e.message);
|
||||
}
|
||||
|
||||
let isImpersonating = false;
|
||||
let impersonatedUser = null;
|
||||
|
||||
if (user.is_admin() && req.cookies.impersonate_user_id) {
|
||||
isImpersonating = true;
|
||||
const targetId = req.cookies.impersonate_user_id;
|
||||
req.apiClient.defaults.headers['X-Impersonate-User'] = targetId;
|
||||
|
||||
try {
|
||||
if (req.cookies.impersonate_user_data) {
|
||||
impersonatedUser = JSON.parse(req.cookies.impersonate_user_data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing impersonate_user_data:', e.message);
|
||||
}
|
||||
|
||||
if (!impersonatedUser) {
|
||||
impersonatedUser = {
|
||||
id: targetId,
|
||||
name: `Usuario #${targetId}`,
|
||||
email: 'impersonated@edu-space.com',
|
||||
role: 'usuario'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
res.locals.user = user;
|
||||
res.locals.current_user = user;
|
||||
res.locals.role = user.role;
|
||||
res.locals.is_impersonating = isImpersonating;
|
||||
res.locals.impersonated_user = impersonatedUser;
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
@@ -38,9 +38,58 @@ router.post('/login', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
const { requireAuth } = require('../middlewares/authMiddleware');
|
||||
|
||||
router.get('/logout', (req, res) => {
|
||||
res.clearCookie('auth_token');
|
||||
res.clearCookie('impersonate_user_id');
|
||||
res.clearCookie('impersonate_user_data');
|
||||
res.redirect('/auth/login');
|
||||
});
|
||||
|
||||
// Iniciar Modo Impersonación (Solo Admin)
|
||||
router.get('/impersonate/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const targetUserId = parseInt(req.params.id, 10);
|
||||
if (!req.user || !req.user.is_admin()) {
|
||||
return res.status(403).send('Solo los administradores pueden activar el modo de impersonación.');
|
||||
}
|
||||
|
||||
const response = await req.apiClient.post('/admin/impersonate', {
|
||||
user_id: targetUserId
|
||||
});
|
||||
|
||||
const targetUser = response.data && response.data.target_user;
|
||||
if (targetUser) {
|
||||
res.cookie('impersonate_user_id', targetUserId.toString(), {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax'
|
||||
});
|
||||
res.cookie('impersonate_user_data', JSON.stringify(targetUser), {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax'
|
||||
});
|
||||
}
|
||||
res.redirect('/dashboard');
|
||||
} catch (error) {
|
||||
console.error('Error initiating impersonation:', error.response?.data || error.message);
|
||||
res.redirect('/admin/users_list?error=impersonate_failed');
|
||||
}
|
||||
});
|
||||
|
||||
// Detener Modo Impersonación
|
||||
router.get('/stop-impersonating', requireAuth, async (req, res) => {
|
||||
try {
|
||||
await req.apiClient.post('/admin/stop-impersonating', {});
|
||||
} catch (error) {
|
||||
console.error('Error stopping impersonation on backend:', error.response?.data || error.message);
|
||||
} finally {
|
||||
res.clearCookie('impersonate_user_id');
|
||||
res.clearCookie('impersonate_user_data');
|
||||
res.redirect('/admin/users_list');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -176,6 +176,65 @@ router.get('/', async (req, res) => {
|
||||
};
|
||||
} else if (role === 'alumno') {
|
||||
const studentClasses = apiTodaySchedule ? apiTodaySchedule.slice(0, 4) : null;
|
||||
let upcomingMilestonesList = [];
|
||||
try {
|
||||
const mResp = await req.apiClient.get('/students/my-upcoming-milestones');
|
||||
if (mResp.data && Array.isArray(mResp.data.upcoming_milestones) && mResp.data.upcoming_milestones.length > 0) {
|
||||
upcomingMilestonesList = mResp.data.upcoming_milestones.map(m => {
|
||||
const isFinal = (m.type || '').includes('Final');
|
||||
const isParcial = (m.type || '').includes('Parcial');
|
||||
const color = isFinal ? '#dc3545' : (isParcial ? '#fd7e14' : '#0d6efd');
|
||||
return {
|
||||
...m,
|
||||
materia_codigo: m.commission_code || m.subject_name,
|
||||
type_obj: { name: m.type, color },
|
||||
milestone_type: { name: m.type, color }
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Student milestones fetch notice:', e.message);
|
||||
}
|
||||
|
||||
if (upcomingMilestonesList.length === 0) {
|
||||
upcomingMilestonesList = [
|
||||
{
|
||||
title: 'Primer Examen Parcial',
|
||||
materia_codigo: 'BDR-201',
|
||||
subject_name: 'Bases de Datos Relacionales',
|
||||
date: '25/09/2026',
|
||||
days_remaining: 2,
|
||||
type_obj: { name: 'Examen Parcial', color: '#dc3545' },
|
||||
milestone_type: { name: 'Examen Parcial', color: '#dc3545' },
|
||||
notes: 'Traer DNI y calculadora',
|
||||
classroom: 'Piso 2 · Aula 204'
|
||||
},
|
||||
{
|
||||
title: 'Entrega TP Grupal Nº 2',
|
||||
materia_codigo: 'AED-102',
|
||||
subject_name: 'Algoritmos y Estructuras de Datos',
|
||||
date: '30/09/2026',
|
||||
days_remaining: 7,
|
||||
type_obj: { name: 'Trabajo Práctico', color: '#198754' },
|
||||
milestone_type: { name: 'Trabajo Práctico', color: '#198754' },
|
||||
notes: 'Subir código al repositorio',
|
||||
classroom: 'Campus Virtual',
|
||||
link: 'https://campus.edu-space.com/tps'
|
||||
},
|
||||
{
|
||||
title: 'Recuperatorio Parcial 1',
|
||||
materia_codigo: 'RYT-302',
|
||||
subject_name: 'Redes y Telecomunicaciones',
|
||||
date: '08/10/2026',
|
||||
days_remaining: 15,
|
||||
type_obj: { name: 'Recuperatorio', color: '#fd7e14' },
|
||||
milestone_type: { name: 'Recuperatorio', color: '#fd7e14' },
|
||||
notes: 'Piso 2 · Aula 204',
|
||||
classroom: 'Piso 2 · Aula 204'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
role_data = {
|
||||
today_classes: studentClasses || [
|
||||
{
|
||||
@@ -197,42 +256,7 @@ router.get('/', async (req, res) => {
|
||||
virtual_link: 'https://meet.google.com/edu-space-demo'
|
||||
}
|
||||
],
|
||||
upcoming_milestones: apiMilestones ? apiMilestones.map(m => ({
|
||||
...m,
|
||||
milestone_type: m.type_obj
|
||||
})) : [
|
||||
{
|
||||
title: 'Primer Examen Parcial',
|
||||
materia_codigo: 'BDR-201',
|
||||
subject_name: 'Bases de Datos Relacionales',
|
||||
date: '25/09/2026',
|
||||
type_obj: { name: 'Examen Parcial', color: '#dc3545' },
|
||||
milestone_type: { name: 'Examen Parcial', color: '#dc3545' },
|
||||
notes: 'Traer DNI y calculadora',
|
||||
classroom: 'Piso 2 · Aula 204'
|
||||
},
|
||||
{
|
||||
title: 'Entrega TP Grupal Nº 2',
|
||||
materia_codigo: 'AED-102',
|
||||
subject_name: 'Algoritmos y Estructuras de Datos',
|
||||
date: '30/09/2026',
|
||||
type_obj: { name: 'Trabajo Práctico', color: '#198754' },
|
||||
milestone_type: { name: 'Trabajo Práctico', color: '#198754' },
|
||||
notes: 'Subir código al repositorio',
|
||||
classroom: 'Campus Virtual',
|
||||
link: 'https://campus.edu-space.com/tps'
|
||||
},
|
||||
{
|
||||
title: 'Recuperatorio Parcial 1',
|
||||
materia_codigo: 'RYT-302',
|
||||
subject_name: 'Redes y Telecomunicaciones',
|
||||
date: '08/10/2026',
|
||||
type_obj: { name: 'Recuperatorio', color: '#fd7e14' },
|
||||
milestone_type: { name: 'Recuperatorio', color: '#fd7e14' },
|
||||
notes: 'Piso 2 · Aula 204',
|
||||
classroom: 'Piso 2 · Aula 204'
|
||||
}
|
||||
]
|
||||
upcoming_milestones: upcomingMilestonesList
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -98,5 +98,7 @@ const handleMySubjectDetail = async (req, res) => {
|
||||
|
||||
router.get(['/', '/list', '/my_subjects_list'], handleMySubjectsList);
|
||||
router.get(['/detail', '/commission_detail', '/:id', '/detail/:id'], handleMySubjectDetail);
|
||||
router.get(['/gradebook/:id', '/:id/gradebook'], (req, res) => res.redirect(`/schedule/gradebook/${req.params.id}`));
|
||||
router.get(['/grades', '/my_grades', '/mis-notas'], (req, res) => res.redirect('/schedule/my_grades'));
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -21,14 +21,24 @@ const handleCalendarView = async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
let unassignedCommissions = [];
|
||||
try {
|
||||
const commsRes = await req.apiClient.get('/genetic/commissions');
|
||||
unassignedCommissions = (commsRes.data && commsRes.data.commissions) || [];
|
||||
} catch (e) {
|
||||
console.warn('Could not fetch unassigned commissions for calendar:', e.message);
|
||||
}
|
||||
|
||||
res.render('schedule/calendar', {
|
||||
title: 'Reservations Calendar - Edu-Space Admin',
|
||||
today_date: new Date().toISOString().split('T')[0],
|
||||
classrooms: classrooms.map(c => ({
|
||||
id: c.id,
|
||||
code: c.code,
|
||||
building: c.building || c.building_name || 'Edificio Central'
|
||||
building: c.building || c.building_name || 'Edificio Central',
|
||||
capacity: c.capacity || 40
|
||||
})),
|
||||
unassigned_commissions: unassignedCommissions,
|
||||
subjects: [
|
||||
{ id: 1, name: 'Arquitectura de Software', code: 'ARQ-101' },
|
||||
{ id: 2, name: 'Bases de Datos II', code: 'BD-201' },
|
||||
@@ -48,6 +58,7 @@ const handleCalendarView = async (req, res) => {
|
||||
title: 'Reservations Calendar (Offline)',
|
||||
today_date: new Date().toISOString().split('T')[0],
|
||||
classrooms: [],
|
||||
unassigned_commissions: [],
|
||||
subjects: [],
|
||||
careers: [],
|
||||
buildings: [],
|
||||
@@ -581,6 +592,48 @@ router.post(['/edit_reservation', '/edit_reservation/:id', '/reservation/:id/edi
|
||||
router.post(['/confirm_reservation', '/confirm_reservation/:id', '/reservation/:id/confirm'], handleConfirmReservation);
|
||||
router.post(['/cancel_reservation', '/cancel_reservation/:id', '/reservation/:id/cancel'], handleCancelReservation);
|
||||
|
||||
// 9. Libro de Calificaciones / Gradebook (Fase 3)
|
||||
const handleGradebook = async (req, res) => {
|
||||
const id = req.params.id || req.query.id;
|
||||
try {
|
||||
const resp = await req.apiClient.get(`/commissions/${id}/gradebook`);
|
||||
const data = resp.data || {};
|
||||
res.render('schedule/gradebook', {
|
||||
title: `Libro de Calificaciones — Edu-Space`,
|
||||
commission: data.commission || { id, code: 'C1' },
|
||||
milestone_columns: data.milestone_columns || [],
|
||||
students: data.students || [],
|
||||
role: req.user?.role || 'docente'
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error fetching commission gradebook:', err.message);
|
||||
res.redirect(`/admin/commission_detail?id=${id}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 10. Consulta de Calificaciones del Alumno (Fase 3)
|
||||
const handleMyGrades = async (req, res) => {
|
||||
try {
|
||||
const resp = await req.apiClient.get('/students/my-grades');
|
||||
const subjects = resp.data?.subjects || [];
|
||||
res.render('schedule/my_grades', {
|
||||
title: 'Mis Calificaciones — Edu-Space',
|
||||
subjects,
|
||||
role: req.user?.role || 'alumno'
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error fetching student grades:', err.message);
|
||||
res.render('schedule/my_grades', {
|
||||
title: 'Mis Calificaciones — Edu-Space',
|
||||
subjects: [],
|
||||
role: req.user?.role || 'alumno'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
router.get(['/gradebook', '/gradebook/:id', '/commissions/:id/gradebook'], handleGradebook);
|
||||
router.get(['/my_grades', '/my-grades', '/mis_calificaciones', '/mis-notas'], handleMyGrades);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
|
||||
|
||||
@@ -101,6 +101,18 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<div class="mt-3 pt-2 border-top">
|
||||
<a href="/schedule/gradebook/{{ commission.id }}" class="btn btn-primary btn-sm w-100 shadow-sm d-flex align-items-center justify-content-center gap-2">
|
||||
<i class="bi bi-journal-check fs-6"></i>
|
||||
<span class="fw-semibold">Libro de Calificaciones (Actas)</span>
|
||||
</a>
|
||||
{% if commission.grades_closed %}
|
||||
<div class="badge bg-danger-subtle text-danger border border-danger-subtle w-100 mt-2 py-2 text-center">
|
||||
<i class="bi bi-lock-fill me-1"></i>Acta Cerrada: {{ commission.acta_number }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -183,6 +183,9 @@
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
{% if u.id != user.id %}
|
||||
<a href="/auth/impersonate/{{ u.id }}" class="btn btn-outline-info" title="Impersonar usuario (Iniciar sesión como {{ u.name }})">
|
||||
<i class="bi bi-incognito"></i>
|
||||
</a>
|
||||
<form action="/admin/user_toggle" method="POST" class="d-inline">
|
||||
<input type="hidden" name="id" value="{{ u.id }}">
|
||||
<button type="submit" class="btn {% if u.is_active %}btn-outline-warning{% else %}btn-outline-success{% endif %}"
|
||||
|
||||
+519
-225
@@ -188,258 +188,488 @@
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
SIDEBAR & TOGGLE BUTTON DESIGN (720p, 1360x768 & 1080p)
|
||||
======================================================= */
|
||||
.app-wrapper {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
}
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
transition: width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
flex-shrink: 0;
|
||||
z-index: 1040;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.08);
|
||||
background: var(--bs-body-bg);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
[data-bs-theme="dark"] .sidebar {
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(24, 32, 47, 0.98);
|
||||
}
|
||||
|
||||
/* Header del Sidebar */
|
||||
.sidebar-header {
|
||||
height: 54px;
|
||||
padding: 0.5rem 0.85rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
[data-bs-theme="dark"] .sidebar-header {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.07);
|
||||
}
|
||||
|
||||
/* Botón de Toggle Limpio & Estilizado */
|
||||
.sidebar-toggle-btn {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
color: #475569;
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.sidebar-toggle-btn:hover {
|
||||
background: rgba(180, 62, 142, 0.1);
|
||||
border-color: rgba(180, 62, 142, 0.4);
|
||||
color: #B43E8E !important;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
[data-bs-theme="dark"] .sidebar-toggle-btn {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
color: #94A3B8;
|
||||
}
|
||||
[data-bs-theme="dark"] .sidebar-toggle-btn:hover {
|
||||
background: rgba(255, 138, 182, 0.15);
|
||||
border-color: rgba(255, 138, 182, 0.4);
|
||||
color: #FF8AB6 !important;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Sidebar Colapsado (Compacto, ultra limpio para pantallas chicas) */
|
||||
.sidebar.collapsed {
|
||||
width: 68px;
|
||||
}
|
||||
.sidebar.collapsed .brand-link {
|
||||
display: none !important;
|
||||
}
|
||||
.sidebar.collapsed .sidebar-header {
|
||||
justify-content: center !important;
|
||||
padding: 0.5rem !important;
|
||||
}
|
||||
.sidebar.collapsed .brand-text,
|
||||
.sidebar.collapsed .nav-text,
|
||||
.sidebar.collapsed .nav-caret,
|
||||
.sidebar.collapsed .sidebar-footer-text,
|
||||
.sidebar.collapsed .dropdown-header {
|
||||
display: none !important;
|
||||
}
|
||||
.sidebar.collapsed .nav-link {
|
||||
justify-content: center !important;
|
||||
padding: 0.55rem 0 !important;
|
||||
}
|
||||
.sidebar.collapsed .nav-link i {
|
||||
margin-right: 0 !important;
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
.sidebar.collapsed .sidebar-footer {
|
||||
padding: 0.5rem !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.sidebar.collapsed .sidebar-footer .theme-toggle-btn {
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
.sidebar.collapsed .collapse {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Navegación del Sidebar */
|
||||
.sidebar-nav {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
width: 100%;
|
||||
}
|
||||
.sidebar-nav .nav-item {
|
||||
margin-bottom: 2px;
|
||||
width: 100%;
|
||||
}
|
||||
.sidebar-nav .nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.85rem;
|
||||
color: var(--bs-body-color);
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.18s ease;
|
||||
width: 100%;
|
||||
}
|
||||
.sidebar-nav .nav-link:hover, .sidebar-nav .nav-link[aria-expanded="true"] {
|
||||
background-color: rgba(180, 62, 142, 0.08);
|
||||
color: #B43E8E !important;
|
||||
}
|
||||
[data-bs-theme="dark"] .sidebar-nav .nav-link:hover, [data-bs-theme="dark"] .sidebar-nav .nav-link[aria-expanded="true"] {
|
||||
background-color: rgba(255, 138, 182, 0.1);
|
||||
color: #FF8AB6 !important;
|
||||
}
|
||||
.sidebar-nav .nav-link i {
|
||||
font-size: 1.15rem;
|
||||
min-width: 26px;
|
||||
text-align: center;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.sidebar-submenu {
|
||||
list-style: none;
|
||||
padding-left: 2.2rem;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
.sidebar-submenu .nav-link {
|
||||
padding: 0.35rem 0.75rem;
|
||||
font-size: 0.83rem;
|
||||
color: var(--bs-secondary-color);
|
||||
}
|
||||
|
||||
/* Contenido Principal & Eliminación de Desperdicio Superior */
|
||||
.main-content {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: transparent;
|
||||
}
|
||||
.app-main-container {
|
||||
padding-top: 0.85rem !important;
|
||||
padding-bottom: 1.5rem !important;
|
||||
}
|
||||
|
||||
/* Anular margen excesivo heredado de templates antiguos */
|
||||
.app-main-container > .container-fluid.pt-5.mt-4,
|
||||
.app-main-container > .container.pt-5.mt-4,
|
||||
.app-main-container .pt-5.mt-4,
|
||||
.main-content .container-fluid.pt-5.mt-4,
|
||||
.main-content .container.pt-5.mt-4 {
|
||||
padding-top: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
/* Mobile Header */
|
||||
.mobile-header {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 991.98px) {
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: -260px;
|
||||
}
|
||||
.sidebar.mobile-open {
|
||||
left: 0;
|
||||
}
|
||||
.mobile-header {
|
||||
display: flex;
|
||||
background: var(--bs-body-bg);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
padding: 0.5rem 1rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1030;
|
||||
height: 52px;
|
||||
}
|
||||
[data-bs-theme="dark"] .mobile-header {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
OPTIMIZACIONES PARA 720p / 1360x768 (Mercado Argentino / Bedelía)
|
||||
======================================================= */
|
||||
@media (max-height: 820px), (max-width: 1400px) {
|
||||
.sidebar-header {
|
||||
height: 48px;
|
||||
padding: 0.4rem 0.65rem;
|
||||
}
|
||||
.sidebar-header .brand-text {
|
||||
font-size: 1.05rem !important;
|
||||
}
|
||||
.sidebar-header .navbar-logo {
|
||||
height: 24px;
|
||||
}
|
||||
.sidebar-nav .nav-link {
|
||||
padding: 0.4rem 0.75rem;
|
||||
font-size: 0.83rem;
|
||||
}
|
||||
.sidebar-nav .nav-link i {
|
||||
font-size: 1.05rem;
|
||||
min-width: 22px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.sidebar-submenu .nav-link {
|
||||
padding: 0.28rem 0.65rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.sidebar-footer {
|
||||
padding: 0.4rem 0.65rem !important;
|
||||
}
|
||||
.app-main-container {
|
||||
padding-top: 0.5rem !important;
|
||||
padding-left: 0.85rem !important;
|
||||
padding-right: 0.85rem !important;
|
||||
}
|
||||
/* Títulos y márgenes más compactos para ganar viewport útil */
|
||||
h1.h3, .h3, h1.h4, .h4 {
|
||||
font-size: 1.25rem !important;
|
||||
margin-bottom: 0.2rem !important;
|
||||
}
|
||||
.dashboard-role-banner {
|
||||
padding: 0.75rem 1rem !important;
|
||||
margin-bottom: 0.85rem !important;
|
||||
}
|
||||
.card {
|
||||
margin-bottom: 0.85rem !important;
|
||||
}
|
||||
.card-body {
|
||||
padding: 0.85rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
OPTIMIZACIONES PARA 1080p (1920x1080 / Monitores Grandes)
|
||||
======================================================= */
|
||||
@media (min-width: 1600px) and (min-height: 900px) {
|
||||
.sidebar {
|
||||
width: 270px;
|
||||
}
|
||||
.sidebar-header {
|
||||
height: 60px;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
.sidebar-nav .nav-link {
|
||||
padding: 0.58rem 1rem;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
.app-main-container {
|
||||
padding-top: 1.25rem !important;
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 2rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.brand-link span {
|
||||
background: linear-gradient(90deg, #B43E8E, #682CAD);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body data-user-authenticated="{{ 'true' if (user and user.is_authenticated) else 'false' }}">
|
||||
{% if user.is_authenticated %}
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg glass-navbar fixed-top">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand d-flex align-items-center" href="/dashboard">
|
||||
<img src="/img/logo_unicaba_mini.png" alt="Logo UniCABA" class="navbar-logo me-2" height="30">
|
||||
<span>Edu-Space Admin</span>
|
||||
</a>
|
||||
|
||||
<div class="app-wrapper">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header border-bottom border-secondary-subtle">
|
||||
<a href="/dashboard" class="text-decoration-none d-flex align-items-center brand-link">
|
||||
<img src="/img/logo_unicaba_mini.png" alt="Logo UniCABA" class="navbar-logo me-2" height="26">
|
||||
<span class="brand-text fs-5">Edu-Space</span>
|
||||
</a>
|
||||
<button class="sidebar-toggle-btn d-none d-lg-inline-flex" id="sidebarToggle" type="button" aria-label="Alternar menú lateral" title="Alternar menú lateral">
|
||||
<i class="bi bi-layout-sidebar-inset fs-5"></i>
|
||||
</button>
|
||||
<button class="sidebar-toggle-btn d-lg-none" id="sidebarCloseMobile" type="button" aria-label="Cerrar menú">
|
||||
<i class="bi bi-x-lg fs-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
{% set user_role = 'admin' %}
|
||||
<ul class="navbar-nav me-auto">
|
||||
<div class="sidebar-body flex-grow-1 overflow-y-auto p-2">
|
||||
<ul class="sidebar-nav" id="sidebarNav">
|
||||
{% set user_role = role if role else 'admin' %}
|
||||
{% if user_role == 'admin' %}
|
||||
<!-- ================= ADMIN NAVIGATION ================= -->
|
||||
<!-- ADMIN -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/dashboard">
|
||||
<i class="bi bi-speedometer2 me-1"></i>Panel General
|
||||
</a>
|
||||
<a class="nav-link" href="/dashboard"><i class="bi bi-speedometer2 text-primary"></i> <span class="nav-text">Panel General</span></a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-door-open me-1"></i>Espacios & Sedes
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="#menuEspacios" data-bs-toggle="collapse" role="button" aria-expanded="false">
|
||||
<i class="bi bi-door-open text-info"></i> <span class="nav-text flex-grow-1">Espacios & Sedes</span> <i class="bi bi-chevron-down nav-caret ms-auto fs-6"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu shadow-sm">
|
||||
<li><a class="dropdown-item" href="/classrooms/list_classrooms?view_mode=physical"><i class="bi bi-door-open me-2 text-primary"></i>Aulas Presenciales</a></li>
|
||||
<li><a class="dropdown-item" href="/classrooms/list_classrooms?view_mode=virtual"><i class="bi bi-camera-video me-2 text-info"></i>Aulas Virtuales (Reunión)</a></li>
|
||||
<li><a class="dropdown-item" href="/classrooms/list_classrooms"><i class="bi bi-grid me-2 text-secondary"></i>Todas las Aulas</a></li>
|
||||
<li><a class="dropdown-item" href="/classrooms/add_classroom"><i class="bi bi-plus-circle me-2 text-success"></i>Nueva Aula</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="/buildings/list_buildings"><i class="bi bi-buildings me-2 text-secondary"></i>Edificios y Sedes</a></li>
|
||||
<li><a class="dropdown-item" href="/admin/occupancy_metrics"><i class="bi bi-graph-up-arrow me-2 text-success"></i>Métricas de Ocupación</a></li>
|
||||
</ul>
|
||||
<div class="collapse" id="menuEspacios" data-bs-parent="#sidebarNav">
|
||||
<ul class="sidebar-submenu">
|
||||
<li><a class="nav-link" href="/classrooms/list_classrooms?view_mode=physical">Presenciales</a></li>
|
||||
<li><a class="nav-link" href="/classrooms/list_classrooms?view_mode=virtual">Virtuales</a></li>
|
||||
<li><a class="nav-link" href="/classrooms/list_classrooms">Todas las Aulas</a></li>
|
||||
<li><a class="nav-link text-success" href="/classrooms/add_classroom"><i class="bi bi-plus-circle me-1" style="min-width:auto; font-size:1rem;"></i>Nueva Aula</a></li>
|
||||
<li><a class="nav-link" href="/buildings/list_buildings">Edificios</a></li>
|
||||
<li><a class="nav-link" href="/admin/occupancy_metrics">Métricas</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-calendar3 me-1"></i>Cronograma & Reservas
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="#menuReservas" data-bs-toggle="collapse" role="button" aria-expanded="false">
|
||||
<i class="bi bi-calendar3 text-warning"></i> <span class="nav-text flex-grow-1">Reservas</span> <i class="bi bi-chevron-down nav-caret ms-auto fs-6"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu shadow-sm">
|
||||
<li><a class="dropdown-item" href="/schedule/calendar_view"><i class="bi bi-calendar-week me-2 text-primary"></i>Calendario General</a></li>
|
||||
<li><a class="dropdown-item" href="/schedule/today_schedule"><i class="bi bi-calendar-day me-2 text-info"></i>Cartelera del Día</a></li>
|
||||
<li><a class="dropdown-item" href="/schedule/list_reservations"><i class="bi bi-list-check me-2 text-warning"></i>Gestión de Reservas</a></li>
|
||||
<li><a class="dropdown-item" href="/schedule/add_reservation"><i class="bi bi-plus-circle me-2 text-success"></i>Nueva Reserva</a></li>
|
||||
<li><a class="dropdown-item" href="/schedule/commissions_view"><i class="bi bi-diagram-3 me-2 text-primary"></i>Comisiones y Cursadas</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="/genetic_algorithm_web/genetic_optimizer"><i class="bi bi-cpu me-2 text-info"></i>Optimizador de Aulas (IA)</a></li>
|
||||
</ul>
|
||||
<div class="collapse" id="menuReservas" data-bs-parent="#sidebarNav">
|
||||
<ul class="sidebar-submenu">
|
||||
<li><a class="nav-link" href="/schedule/calendar_view">Calendario</a></li>
|
||||
<li><a class="nav-link" href="/schedule/today_schedule">Cartelera</a></li>
|
||||
<li><a class="nav-link" href="/schedule/list_reservations">Gestión</a></li>
|
||||
<li><a class="nav-link text-success" href="/schedule/add_reservation"><i class="bi bi-plus-circle me-1" style="min-width:auto; font-size:1rem;"></i>Nueva</a></li>
|
||||
<li><a class="nav-link" href="/schedule/commissions_view">Comisiones</a></li>
|
||||
<li><a class="nav-link text-info fw-semibold" href="/genetic_algorithm_web/genetic_optimizer"><i class="bi bi-cpu me-1" style="min-width:auto; font-size:1rem;"></i>IA Optimizer</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-mortarboard me-1"></i>Gestión Académica
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="#menuAcademica" data-bs-toggle="collapse" role="button" aria-expanded="false">
|
||||
<i class="bi bi-mortarboard text-success"></i> <span class="nav-text flex-grow-1">Académica</span> <i class="bi bi-chevron-down nav-caret ms-auto fs-6"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu shadow-sm">
|
||||
<li><a class="dropdown-item" href="/admin/careers_list"><i class="bi bi-mortarboard me-2 text-primary"></i>Carreras</a></li>
|
||||
<li><a class="dropdown-item" href="/admin/subjects_list"><i class="bi bi-book me-2 text-info"></i>Asignaturas</a></li>
|
||||
<li><a class="dropdown-item" href="/admin/commissions_list"><i class="bi bi-diagram-3 me-2 text-success"></i>Comisiones</a></li>
|
||||
<li><a class="dropdown-item" href="/admin/academic_terms_list"><i class="bi bi-calendar-range me-2 text-warning"></i>Ciclo Lectivo</a></li>
|
||||
</ul>
|
||||
<div class="collapse" id="menuAcademica" data-bs-parent="#sidebarNav">
|
||||
<ul class="sidebar-submenu">
|
||||
<li><a class="nav-link" href="/admin/careers_list">Carreras</a></li>
|
||||
<li><a class="nav-link" href="/admin/subjects_list">Asignaturas</a></li>
|
||||
<li><a class="nav-link" href="/admin/commissions_list">Comisiones</a></li>
|
||||
<li><a class="nav-link" href="/admin/academic_terms_list">Ciclo Lectivo</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-shield-lock me-1"></i>Administración
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="#menuAdmin" data-bs-toggle="collapse" role="button" aria-expanded="false">
|
||||
<i class="bi bi-shield-lock text-secondary"></i> <span class="nav-text flex-grow-1">Administración</span> <i class="bi bi-chevron-down nav-caret ms-auto fs-6"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu shadow-sm">
|
||||
<li><a class="dropdown-item" href="/admin/users_list"><i class="bi bi-people me-2 text-primary"></i>Usuarios</a></li>
|
||||
<li><a class="dropdown-item" href="/admin/roles_list"><i class="bi bi-person-badge me-2 text-info"></i>Roles y Permisos RBAC</a></li>
|
||||
<li><a class="dropdown-item" href="/admin/milestone_types_list"><i class="bi bi-tags me-2 text-secondary"></i>Tipificaciones de Hitos</a></li>
|
||||
<li><a class="dropdown-item" href="/admin/google_sheets_import"><i class="bi bi-file-earmark-spreadsheet me-2 text-success"></i>Sincronización Sheets</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="/admin/audit_logs"><i class="bi bi-shield-check me-2 text-warning"></i>Registro de Auditoría</a></li>
|
||||
</ul>
|
||||
<div class="collapse" id="menuAdmin" data-bs-parent="#sidebarNav">
|
||||
<ul class="sidebar-submenu">
|
||||
<li><a class="nav-link" href="/admin/users_list">Usuarios</a></li>
|
||||
<li><a class="nav-link" href="/admin/roles_list">Roles RBAC</a></li>
|
||||
<li><a class="nav-link" href="/admin/milestone_types_list">Tipos Hitos</a></li>
|
||||
<li><a class="nav-link" href="/admin/google_sheets_import">Sincro Sheets</a></li>
|
||||
<li><a class="nav-link" href="/admin/audit_logs">Auditoría</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{% elif user_role == 'bedelia' %}
|
||||
<!-- ================= BEDELÍA NAVIGATION ================= -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/dashboard">
|
||||
<i class="bi bi-speedometer2 me-1"></i>Panel Bedelía
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-door-open me-1"></i>Espacios & Aulas
|
||||
</a>
|
||||
<ul class="dropdown-menu shadow-sm">
|
||||
<li><a class="dropdown-item" href="/classrooms/list_classrooms?view_mode=physical"><i class="bi bi-door-open me-2 text-primary"></i>Aulas Presenciales</a></li>
|
||||
<li><a class="dropdown-item" href="/classrooms/list_classrooms?view_mode=virtual"><i class="bi bi-camera-video me-2 text-info"></i>Aulas Virtuales (Reunión)</a></li>
|
||||
<li><a class="dropdown-item" href="/classrooms/list_classrooms"><i class="bi bi-grid me-2 text-secondary"></i>Todas las Aulas</a></li>
|
||||
<li><a class="dropdown-item" href="/classrooms/add_classroom"><i class="bi bi-plus-circle me-2 text-success"></i>Nueva Aula</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="/buildings/list_buildings"><i class="bi bi-buildings me-2 text-secondary"></i>Edificios y Sedes</a></li>
|
||||
<li><a class="dropdown-item" href="/admin/occupancy_metrics"><i class="bi bi-graph-up-arrow me-2 text-success"></i>Métricas de Ocupación</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-calendar3 me-1"></i>Operaciones & Reservas
|
||||
</a>
|
||||
<ul class="dropdown-menu shadow-sm">
|
||||
<li><a class="dropdown-item" href="/schedule/today_schedule"><i class="bi bi-calendar-day me-2 text-info"></i>Cartelera del Día</a></li>
|
||||
<li><a class="dropdown-item" href="/schedule/calendar_view"><i class="bi bi-calendar-week me-2 text-primary"></i>Calendario General</a></li>
|
||||
<li><a class="dropdown-item" href="/schedule/list_reservations"><i class="bi bi-list-check me-2 text-warning"></i>Gestión de Reservas</a></li>
|
||||
<li><a class="dropdown-item" href="/schedule/add_reservation"><i class="bi bi-plus-circle me-2 text-success"></i>Registrar Reserva</a></li>
|
||||
<li><a class="dropdown-item" href="/schedule/commissions_view"><i class="bi bi-diagram-3 me-2 text-primary"></i>Comisiones y Cursadas</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-mortarboard me-1"></i>Académica & Cursadas
|
||||
</a>
|
||||
<ul class="dropdown-menu shadow-sm">
|
||||
<li><a class="dropdown-item" href="/admin/careers_list"><i class="bi bi-mortarboard me-2 text-primary"></i>Carreras</a></li>
|
||||
<li><a class="dropdown-item" href="/admin/subjects_list"><i class="bi bi-book me-2 text-info"></i>Asignaturas</a></li>
|
||||
<li><a class="dropdown-item" href="/admin/commissions_list"><i class="bi bi-diagram-3 me-2 text-success"></i>Gestión de Comisiones</a></li>
|
||||
<li><a class="dropdown-item" href="/admin/academic_terms_list"><i class="bi bi-calendar-range me-2 text-warning"></i>Ciclo Lectivo</a></li>
|
||||
<li><a class="dropdown-item" href="/admin/milestone_types_list"><i class="bi bi-tags me-2 text-secondary"></i>Tipificaciones de Hitos</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="/admin/google_sheets_import"><i class="bi bi-file-earmark-spreadsheet me-2 text-success"></i>Sincronización Sheets</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<!-- BEDELIA -->
|
||||
<li class="nav-item"><a class="nav-link" href="/dashboard"><i class="bi bi-speedometer2 text-warning"></i> <span class="nav-text">Panel Bedelía</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/classrooms/list_classrooms"><i class="bi bi-door-open text-primary"></i> <span class="nav-text">Aulas y Espacios</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/schedule/today_schedule"><i class="bi bi-calendar-day text-info"></i> <span class="nav-text">Cartelera Diaria</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/schedule/list_reservations"><i class="bi bi-list-check text-success"></i> <span class="nav-text">Gestión Reservas</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/schedule/add_reservation"><i class="bi bi-plus-circle text-success"></i> <span class="nav-text">Nueva Reserva</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/admin/commissions_list"><i class="bi bi-diagram-3 text-primary"></i> <span class="nav-text">Comisiones</span></a></li>
|
||||
|
||||
{% elif user_role == 'docente' %}
|
||||
<!-- ================= DOCENTE / PROFESOR NAVIGATION ================= -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/dashboard">
|
||||
<i class="bi bi-speedometer2 me-1"></i>Panel Docente
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-calendar-check me-1"></i>Mis Clases & Reservas
|
||||
</a>
|
||||
<ul class="dropdown-menu shadow-sm">
|
||||
<li><a class="dropdown-item" href="/schedule/list_reservations"><i class="bi bi-calendar2-check me-2 text-primary"></i>Mis Reservas de Aulas</a></li>
|
||||
<li><a class="dropdown-item" href="/schedule/add_reservation"><i class="bi bi-plus-circle me-2 text-success"></i>Solicitar Nueva Reserva</a></li>
|
||||
<li><a class="dropdown-item" href="/schedule/commissions_view"><i class="bi bi-diagram-3 me-2 text-info"></i>Mis Comisiones & Cursadas</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link fw-semibold" href="/my_subjects/my_subjects_list">
|
||||
<i class="bi bi-mortarboard me-1 text-info"></i>Mis Materias
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-building me-1"></i>Campus & Horarios
|
||||
</a>
|
||||
<ul class="dropdown-menu shadow-sm">
|
||||
<li><a class="dropdown-item" href="/schedule/today_schedule"><i class="bi bi-calendar-day me-2 text-info"></i>Cartelera del Día (¿Dónde curso hoy?)</a></li>
|
||||
<li><a class="dropdown-item" href="/schedule/calendar_view"><i class="bi bi-calendar-week me-2 text-primary"></i>Calendario General de Aulas</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="/classrooms/list_classrooms"><i class="bi bi-camera-video me-2 text-info"></i>Aulas Virtuales (Reuniones)</a></li>
|
||||
<li><a class="dropdown-item" href="/classrooms/list_classrooms"><i class="bi bi-door-open me-2 text-secondary"></i>Consulta de Aulas Presenciales</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<!-- DOCENTE -->
|
||||
<li class="nav-item"><a class="nav-link" href="/dashboard"><i class="bi bi-speedometer2 text-info"></i> <span class="nav-text">Panel Docente</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/schedule/list_reservations"><i class="bi bi-calendar2-check text-primary"></i> <span class="nav-text">Mis Reservas</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/schedule/add_reservation"><i class="bi bi-plus-circle text-success"></i> <span class="nav-text">Solicitar Reserva</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/schedule/commissions_view"><i class="bi bi-diagram-3 text-info"></i> <span class="nav-text">Mis Comisiones</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/my_subjects/my_subjects_list"><i class="bi bi-mortarboard text-warning"></i> <span class="nav-text">Mis Materias</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/schedule/today_schedule"><i class="bi bi-geo-alt text-danger"></i> <span class="nav-text">Cartelera Hoy</span></a></li>
|
||||
|
||||
{% else %}
|
||||
<!-- ================= ALUMNO / ESTUDIANTE NAVIGATION ================= -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/dashboard">
|
||||
<i class="bi bi-house-door me-1"></i>Mi Portal
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link fw-semibold" href="/my_subjects/my_subjects_list">
|
||||
<i class="bi bi-mortarboard me-1 text-success"></i>Mis Materias
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/schedule/today_schedule">
|
||||
<i class="bi bi-geo-alt me-1"></i>Cartelera de Hoy
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/schedule/calendar_view">
|
||||
<i class="bi bi-calendar-week me-1"></i>Cronograma de Clases
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/classrooms/list_classrooms">
|
||||
<i class="bi bi-camera-video me-1"></i>Aulas Virtuales
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/classrooms/list_classrooms">
|
||||
<i class="bi bi-door-open me-1"></i>Espacios del Campus
|
||||
</a>
|
||||
</li>
|
||||
<!-- ALUMNO -->
|
||||
<li class="nav-item"><a class="nav-link" href="/dashboard"><i class="bi bi-house-door text-success"></i> <span class="nav-text">Mi Portal</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/my_subjects/my_subjects_list"><i class="bi bi-mortarboard text-primary"></i> <span class="nav-text">Mis Materias</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/schedule/today_schedule"><i class="bi bi-geo-alt text-danger"></i> <span class="nav-text">Cartelera Hoy</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/schedule/calendar_view"><i class="bi bi-calendar-week text-info"></i> <span class="nav-text">Cronograma</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/classrooms/list_classrooms"><i class="bi bi-camera-video text-secondary"></i> <span class="nav-text">Aulas Virtuales</span></a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav align-items-center">
|
||||
<!-- Theme Mode Toggle Button -->
|
||||
<li class="nav-item me-2">
|
||||
<button class="theme-toggle-btn" type="button" aria-label="Cambiar tema" title="Modo Claro/Oscuro">
|
||||
<i class="bi bi-moon-stars-fill"></i>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<!-- Language Switcher -->
|
||||
<li class="nav-item me-2">
|
||||
{% include 'partials/language_switcher.html' %}
|
||||
</li>
|
||||
|
||||
<!-- User Dropdown with Avatar and Institutional Role -->
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle d-flex align-items-center py-1" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<span class="user-avatar-small">{{ user.name[0]|upper if user.name else 'U' }}</span>
|
||||
<div class="d-none d-lg-block text-start ms-2">
|
||||
<div class="lh-1 small fw-semibold">{{ user.first_name }}</div>
|
||||
<span class="badge {{ user.role_badge_display.class }} role-pill mt-1" style="font-size: 0.65rem; padding: 0.15rem 0.45rem;">
|
||||
<i class="bi {{ user.role_badge_display.icon }}"></i> {{ user.role_badge_display.label }}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0">
|
||||
<li>
|
||||
<div class="px-3 py-2 border-bottom">
|
||||
<small class="text-muted d-block">Sesión iniciada como</small>
|
||||
<span class="fw-bold small d-block">{{ user.email }}</span>
|
||||
<span class="badge {{ user.role_badge_display.class }} role-pill mt-1">
|
||||
<i class="bi {{ user.role_badge_display.icon }}"></i> {{ user.role_badge_display.label }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li><a class="dropdown-item" href="/auth/profile">
|
||||
<i class="bi bi-person me-2"></i> Mi Perfil
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="/auth/change_password">
|
||||
<i class="bi bi-key me-2"></i> Cambiar Contraseña
|
||||
</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item text-danger" href="/auth/logout">
|
||||
<i class="bi bi-box-arrow-right me-2"></i> Cerrar Sesión
|
||||
</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<!-- Flash Messages -->
|
||||
|
||||
<div class="sidebar-footer p-3 border-top border-secondary-subtle">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 sidebar-footer-text">
|
||||
{% include 'partials/language_switcher.html' %}
|
||||
<button class="theme-toggle-btn" type="button" aria-label="Cambiar tema" title="Modo Claro/Oscuro">
|
||||
<i class="bi bi-moon-stars-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="dropdown dropup">
|
||||
<a href="#" class="d-flex align-items-center text-decoration-none dropdown-toggle text-body w-100" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<div class="user-avatar-small bg-primary text-white rounded-circle d-flex align-items-center justify-content-center" style="width:32px; height:32px; font-weight:bold; flex-shrink:0;">
|
||||
{{ user.name[0]|upper if user.name else 'U' }}
|
||||
</div>
|
||||
<div class="ms-2 sidebar-footer-text text-truncate flex-grow-1" style="min-width:0;">
|
||||
<small class="d-block fw-semibold text-truncate">{{ user.first_name }}</small>
|
||||
<small class="text-muted d-block text-truncate" style="font-size:0.7rem;">{{ user.role_badge_display.label }}</small>
|
||||
</div>
|
||||
</a>
|
||||
<ul class="dropdown-menu shadow-sm border-0 mb-2">
|
||||
<li><h6 class="dropdown-header">{{ user.email }}</h6></li>
|
||||
<li><a class="dropdown-item" href="/auth/profile"><i class="bi bi-person me-2"></i>Mi Perfil</a></li>
|
||||
<li><a class="dropdown-item" href="/auth/change_password"><i class="bi bi-key me-2"></i>Cambiar Contraseña</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item text-danger" href="/auth/logout"><i class="bi bi-box-arrow-right me-2"></i>Cerrar Sesión</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Wrapper -->
|
||||
<div class="main-content">
|
||||
<!-- Mobile Header -->
|
||||
<div class="mobile-header shadow-sm">
|
||||
<a href="/dashboard" class="text-decoration-none brand-link d-flex align-items-center">
|
||||
<img src="/img/logo_unicaba_mini.png" alt="Logo UniCABA" class="navbar-logo me-2" height="24">
|
||||
<span class="brand-text fw-bold fs-6">Edu-Space</span>
|
||||
</a>
|
||||
<button class="sidebar-toggle-btn" id="mobileMenuBtn" type="button" aria-label="Abrir menú">
|
||||
<i class="bi bi-list fs-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% if is_impersonating %}
|
||||
<!-- Banner Impersonación Activa -->
|
||||
<div class="impersonation-banner px-3 py-2 d-flex align-items-center justify-content-between text-white shadow-sm" style="background: linear-gradient(90deg, #d97706 0%, #b45309 100%); z-index: 1045; border-bottom: 2px solid #f59e0b;">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="spinner-grow spinner-grow-sm text-warning" role="status" style="width: 14px; height: 14px;">
|
||||
<span class="visually-hidden">Impersonating...</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong class="me-1"><i class="bi bi-incognito me-1"></i>Modo Impersonación Activo:</strong>
|
||||
<span>Estás navegando como</span>
|
||||
<strong class="text-warning-emphasis bg-white bg-opacity-25 px-2 py-0.5 rounded">{{ impersonated_user.name or impersonated_user.first_name or impersonated_user.username }}</strong>
|
||||
<span class="badge bg-dark bg-opacity-50 text-white ms-1">{{ impersonated_user.role|capitalize }}</span>
|
||||
<small class="ms-2 opacity-75 d-none d-md-inline">· Registro de auditoría activo (todas las acciones se trazan a tu usuario admin)</small>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/auth/stop-impersonating" class="btn btn-sm btn-light text-dark fw-bold shadow-sm d-inline-flex align-items-center">
|
||||
<i class="bi bi-box-arrow-left me-1"></i> Salir de Impersonación
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<main class="container-fluid app-main-container px-3 px-xl-4">
|
||||
<!-- Flash Messages -->
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3" style="z-index: 1080;">
|
||||
|
||||
</div>
|
||||
@@ -466,5 +696,69 @@
|
||||
</script>
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
</main>
|
||||
</div><!-- main-content -->
|
||||
</div><!-- app-wrapper -->
|
||||
{% endif %}
|
||||
|
||||
<!-- Sidebar Toggle JS -->
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const sidebar = document.getElementById("sidebar");
|
||||
const toggleBtn = document.getElementById("sidebarToggle");
|
||||
const mobileBtn = document.getElementById("mobileMenuBtn");
|
||||
const closeMobileBtn = document.getElementById("sidebarCloseMobile");
|
||||
|
||||
if (!sidebar) return;
|
||||
|
||||
function updateToggleIcon() {
|
||||
if (!toggleBtn) return;
|
||||
const isCollapsed = sidebar.classList.contains("collapsed");
|
||||
const icon = toggleBtn.querySelector("i");
|
||||
if (icon) {
|
||||
icon.className = isCollapsed ? "bi bi-layout-sidebar fs-5" : "bi bi-layout-sidebar-inset fs-5";
|
||||
}
|
||||
toggleBtn.setAttribute("title", isCollapsed ? "Expandir menú" : "Colapsar menú");
|
||||
toggleBtn.setAttribute("aria-label", isCollapsed ? "Expandir menú" : "Colapsar menú");
|
||||
}
|
||||
|
||||
// Restore state from localStorage (desktop only)
|
||||
if (window.innerWidth >= 992) {
|
||||
const isCollapsed = localStorage.getItem("sidebarCollapsed") === "true";
|
||||
if (isCollapsed) {
|
||||
sidebar.classList.add("collapsed");
|
||||
}
|
||||
}
|
||||
updateToggleIcon();
|
||||
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener("click", function() {
|
||||
sidebar.classList.toggle("collapsed");
|
||||
localStorage.setItem("sidebarCollapsed", sidebar.classList.contains("collapsed"));
|
||||
updateToggleIcon();
|
||||
// Collapse any open submenus when collapsing sidebar
|
||||
if (sidebar.classList.contains("collapsed")) {
|
||||
document.querySelectorAll('.sidebar-submenu').forEach(function(submenu) {
|
||||
const collapseInstance = bootstrap.Collapse.getInstance(submenu.parentElement);
|
||||
if (collapseInstance) collapseInstance.hide();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (mobileBtn) {
|
||||
mobileBtn.addEventListener("click", function() {
|
||||
sidebar.classList.add("mobile-open");
|
||||
});
|
||||
}
|
||||
|
||||
if (closeMobileBtn) {
|
||||
closeMobileBtn.addEventListener("click", function() {
|
||||
sidebar.classList.remove("mobile-open");
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,10 +3,10 @@
|
||||
{% block title %}Add Classroom - Edu-Space Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<div class="container-fluid px-0">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-1 small">
|
||||
@@ -171,10 +171,14 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script type="application/json" id="buildingFloorsData">
|
||||
{{ building_floors|tojson|safe }}
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Map of existing floors per building
|
||||
var buildingFloors = {{ building_floors|tojson }};
|
||||
var buildingFloorsDataEl = document.getElementById('buildingFloorsData');
|
||||
var buildingFloors = buildingFloorsDataEl ? JSON.parse(buildingFloorsDataEl.textContent || '{}') : {};
|
||||
|
||||
var buildingSelect = document.getElementById('building_id');
|
||||
var newBuildingContainer = document.getElementById('newBuildingContainer');
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
{% block title %}Edit Classroom - {{ classroom.code_display }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<div class="container-fluid px-0">
|
||||
<!-- Header -->
|
||||
<div class="row mb-4 align-items-center">
|
||||
<div class="row mb-3 align-items-center">
|
||||
<div class="col-md-7">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-1 small">
|
||||
@@ -165,9 +165,13 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script type="application/json" id="buildingFloorsData">
|
||||
{{ building_floors|tojson|safe }}
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var buildingFloors = {{ building_floors|tojson }};
|
||||
var buildingFloorsDataEl = document.getElementById('buildingFloorsData');
|
||||
var buildingFloors = buildingFloorsDataEl ? JSON.parse(buildingFloorsDataEl.textContent || '{}') : {};
|
||||
|
||||
var buildingSelect = document.getElementById('building_id');
|
||||
var newBuildingContainer = document.getElementById('newBuildingContainer');
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<div class="container-fluid px-0">
|
||||
|
||||
<!-- 1. Encabezado y Selector de Entorno (Presencial vs Virtual) -->
|
||||
<div class="row mb-3">
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
|
||||
{% block content %}
|
||||
{% if user.is_authenticated %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<div class="container-fluid px-0">
|
||||
{% endif %}
|
||||
|
||||
<!-- ========================================================================= -->
|
||||
<!-- 1. HEADER & ROLE PERSPECTIVE SWITCHER -->
|
||||
<!-- ========================================================================= -->
|
||||
<div class="dashboard-role-banner mb-4 shadow-sm">
|
||||
<div class="dashboard-role-banner mb-3 shadow-sm">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
|
||||
<div>
|
||||
<div class="d-flex align-items-center gap-2 mb-1 flex-wrap">
|
||||
@@ -252,12 +252,12 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="progress mb-3" style="height: 14px;">
|
||||
<div class="progress-bar bg-success progress-bar-striped" role="progressbar" style="width: {{ stats.assigned_classrooms_pct }}%;"
|
||||
<div class="progress-bar bg-success progress-bar-striped" role="progressbar" {{ ('style="width: ' ~ (stats.assigned_classrooms_pct or 0) ~ '%;"') | safe }}
|
||||
aria-valuenow="{{ stats.assigned_classrooms_pct }}" aria-valuemin="0" aria-valuemax="100"
|
||||
title="Asignadas: {{ stats.assigned_classrooms }} ({{ stats.assigned_classrooms_pct }}%)">
|
||||
{{ stats.assigned_classrooms_pct }}%
|
||||
</div>
|
||||
<div class="progress-bar bg-warning" role="progressbar" style="width: {{ stats.unassigned_classrooms_pct }}%;"
|
||||
<div class="progress-bar bg-warning" role="progressbar" {{ ('style="width: ' ~ (stats.unassigned_classrooms_pct or 0) ~ '%;"') | safe }}
|
||||
aria-valuenow="{{ stats.unassigned_classrooms_pct }}" aria-valuemin="0" aria-valuemax="100"
|
||||
title="Sin Asignar: {{ stats.unassigned_classrooms }} ({{ stats.unassigned_classrooms_pct }}%)">
|
||||
{{ stats.unassigned_classrooms_pct }}%
|
||||
@@ -274,11 +274,11 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 10px;">
|
||||
<div class="progress-bar bg-info" role="progressbar" style="width: {{ stats.assigned_today_pct }}%;"
|
||||
<div class="progress-bar bg-info" role="progressbar" {{ ('style="width: ' ~ (stats.assigned_today_pct or 0) ~ '%;"') | safe }}
|
||||
aria-valuenow="{{ stats.assigned_today_pct }}" aria-valuemin="0" aria-valuemax="100"
|
||||
title="En uso hoy: {{ stats.assigned_today_classrooms }}">
|
||||
</div>
|
||||
<div class="progress-bar bg-secondary-subtle" role="progressbar" style="width: {{ stats.unassigned_today_pct }}%;"
|
||||
<div class="progress-bar bg-secondary-subtle" role="progressbar" {{ ('style="width: ' ~ (stats.unassigned_today_pct or 0) ~ '%;"') | safe }}
|
||||
aria-valuenow="{{ stats.unassigned_today_pct }}" aria-valuemin="0" aria-valuemax="100"
|
||||
title="Libres hoy: {{ stats.unassigned_today_classrooms }}">
|
||||
</div>
|
||||
@@ -596,12 +596,12 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="progress mb-3" style="height: 14px;">
|
||||
<div class="progress-bar bg-success progress-bar-striped" role="progressbar" style="width: {{ stats.assigned_classrooms_pct }}%;"
|
||||
<div class="progress-bar bg-success progress-bar-striped" role="progressbar" {{ ('style="width: ' ~ (stats.assigned_classrooms_pct or 0) ~ '%;"') | safe }}
|
||||
aria-valuenow="{{ stats.assigned_classrooms_pct }}" aria-valuemin="0" aria-valuemax="100"
|
||||
title="Asignadas: {{ stats.assigned_classrooms }} ({{ stats.assigned_classrooms_pct }}%)">
|
||||
{{ stats.assigned_classrooms_pct }}%
|
||||
</div>
|
||||
<div class="progress-bar bg-warning" role="progressbar" style="width: {{ stats.unassigned_classrooms_pct }}%;"
|
||||
<div class="progress-bar bg-warning" role="progressbar" {{ ('style="width: ' ~ (stats.unassigned_classrooms_pct or 0) ~ '%;"') | safe }}
|
||||
aria-valuenow="{{ stats.unassigned_classrooms_pct }}" aria-valuemin="0" aria-valuemax="100"
|
||||
title="Sin Asignar: {{ stats.unassigned_classrooms }} ({{ stats.unassigned_classrooms_pct }}%)">
|
||||
{{ stats.unassigned_classrooms_pct }}%
|
||||
@@ -618,11 +618,11 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 10px;">
|
||||
<div class="progress-bar bg-info" role="progressbar" style="width: {{ stats.assigned_today_pct }}%;"
|
||||
<div class="progress-bar bg-info" role="progressbar" {{ ('style="width: ' ~ (stats.assigned_today_pct or 0) ~ '%;"') | safe }}
|
||||
aria-valuenow="{{ stats.assigned_today_pct }}" aria-valuemin="0" aria-valuemax="100"
|
||||
title="En uso hoy: {{ stats.assigned_today_classrooms }}">
|
||||
</div>
|
||||
<div class="progress-bar bg-secondary-subtle" role="progressbar" style="width: {{ stats.unassigned_today_pct }}%;"
|
||||
<div class="progress-bar bg-secondary-subtle" role="progressbar" {{ ('style="width: ' ~ (stats.unassigned_today_pct or 0) ~ '%;"') | safe }}
|
||||
aria-valuenow="{{ stats.unassigned_today_pct }}" aria-valuemin="0" aria-valuemax="100"
|
||||
title="Libres hoy: {{ stats.unassigned_today_classrooms }}">
|
||||
</div>
|
||||
@@ -1346,14 +1346,14 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<a href="/classrooms/list_classrooms" class="card h-100 p-3 action-menu-card bg-body">
|
||||
<a href="/schedule/my_grades" class="card h-100 p-3 action-menu-card bg-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="quick-icon-circle bg-warning-subtle text-warning me-3">
|
||||
<i class="bi bi-buildings fs-4"></i>
|
||||
<i class="bi bi-journal-check fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-1 fw-bold text-body">Espacios del Campus</h6>
|
||||
<p class="text-muted small mb-0">Sedes, edificios y planos</p>
|
||||
<h6 class="mb-1 fw-bold text-body">Mis Calificaciones</h6>
|
||||
<p class="text-muted small mb-0">Notas, regularidad y actas</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@@ -1375,7 +1375,7 @@
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light small">
|
||||
<tr>
|
||||
<th class="ps-3">Fecha</th>
|
||||
<th class="ps-3">Fecha & Cuenta Regresiva</th>
|
||||
<th>Tipo de Hito</th>
|
||||
<th>Materia / Código</th>
|
||||
<th>Aula / Enlace</th>
|
||||
@@ -1386,7 +1386,16 @@
|
||||
{% for m in role_data.upcoming_milestones %}
|
||||
<tr>
|
||||
<td class="ps-3 font-monospace fw-bold text-primary">
|
||||
{{ m.date }}
|
||||
<div>{{ m.date }}</div>
|
||||
{% if m.days_remaining is not none and m.days_remaining is defined %}
|
||||
{% if m.days_remaining == 0 %}
|
||||
<span class="badge bg-danger text-white small">¡Hoy!</span>
|
||||
{% elif m.days_remaining == 1 %}
|
||||
<span class="badge bg-warning text-dark small">¡Mañana!</span>
|
||||
{% elif m.days_remaining > 1 %}
|
||||
<span class="badge bg-info-subtle text-info border small">Faltan {{ m.days_remaining }} días</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" data-color="{{ m.type_obj.color if m.type_obj else '#0d6efd' }}" style="background-color:var(--badge-bg,#0d6efd);color:#fff;">
|
||||
|
||||
@@ -31,17 +31,35 @@
|
||||
transition: background-color 0.25s ease, color 0.25s ease;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
NAVBAR COMPACTA — limpia y sin desperdicio
|
||||
============================================= */
|
||||
.glass-navbar {
|
||||
padding: 0 !important; /* Bootstrap agrega padding, lo neutralizamos */
|
||||
min-height: 48px;
|
||||
}
|
||||
.glass-navbar .container-fluid {
|
||||
padding-top: 0.35rem;
|
||||
padding-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
/* --- MODO CLARO ([data-bs-theme="light"]) --- */
|
||||
[data-bs-theme="light"] body {
|
||||
background: #F8FAFC !important;
|
||||
color: #1E293B !important;
|
||||
}
|
||||
[data-bs-theme="light"] .glass-navbar {
|
||||
background: rgba(255, 255, 255, 0.88) !important;
|
||||
background: rgba(255, 255, 255, 0.92) !important;
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04) !important;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
[data-bs-theme="light"] .glass-navbar .navbar-brand span {
|
||||
background: linear-gradient(90deg, #B43E8E, #682CAD);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
[data-bs-theme="light"] .glass-navbar .nav-link {
|
||||
color: #334155 !important;
|
||||
@@ -49,6 +67,8 @@
|
||||
[data-bs-theme="light"] .glass-navbar .nav-link:hover,
|
||||
[data-bs-theme="light"] .glass-navbar .nav-link.active {
|
||||
color: #B43E8E !important;
|
||||
background: rgba(180, 62, 142, 0.06);
|
||||
border-radius: 7px;
|
||||
}
|
||||
[data-bs-theme="light"] .glass-card,
|
||||
[data-bs-theme="light"] .card {
|
||||
@@ -57,6 +77,16 @@
|
||||
border: 1px solid rgba(0, 0, 0, 0.08) !important;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04) !important;
|
||||
}
|
||||
/* Toggle button - modo claro */
|
||||
[data-bs-theme="light"] .navbar-toggle-custom {
|
||||
color: #334155;
|
||||
border-color: rgba(51, 65, 85, 0.25);
|
||||
}
|
||||
[data-bs-theme="light"] .navbar-toggle-custom:hover {
|
||||
background: rgba(180, 62, 142, 0.08);
|
||||
border-color: #B43E8E;
|
||||
color: #B43E8E;
|
||||
}
|
||||
|
||||
/* --- MODO OSCURO ([data-bs-theme="dark"]) --- */
|
||||
[data-bs-theme="dark"] body {
|
||||
@@ -64,11 +94,17 @@
|
||||
color: #F8FAFC !important;
|
||||
}
|
||||
[data-bs-theme="dark"] .glass-navbar {
|
||||
background: rgba(15, 23, 42, 0.78) !important;
|
||||
background: rgba(15, 23, 42, 0.82) !important;
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important;
|
||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3) !important;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.07) !important;
|
||||
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.35) !important;
|
||||
}
|
||||
[data-bs-theme="dark"] .glass-navbar .navbar-brand span {
|
||||
background: linear-gradient(90deg, #FF8AB6, #AE7FFF);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
[data-bs-theme="dark"] .glass-navbar .nav-link {
|
||||
color: #CBD5E1 !important;
|
||||
@@ -76,6 +112,8 @@
|
||||
[data-bs-theme="dark"] .glass-navbar .nav-link:hover,
|
||||
[data-bs-theme="dark"] .glass-navbar .nav-link.active {
|
||||
color: #FF8AB6 !important;
|
||||
background: rgba(255, 138, 182, 0.08);
|
||||
border-radius: 7px;
|
||||
}
|
||||
[data-bs-theme="dark"] .glass-card,
|
||||
[data-bs-theme="dark"] .card {
|
||||
@@ -84,18 +122,123 @@
|
||||
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
.navbar-brand span {
|
||||
font-weight: 700;
|
||||
background: linear-gradient(90deg, #B43E8E, #682CAD);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
/* Toggle button - modo oscuro */
|
||||
[data-bs-theme="dark"] .navbar-toggle-custom {
|
||||
color: #CBD5E1;
|
||||
border-color: rgba(203, 213, 225, 0.25);
|
||||
}
|
||||
[data-bs-theme="dark"] .navbar-toggle-custom:hover {
|
||||
background: rgba(255, 138, 182, 0.1);
|
||||
border-color: rgba(255, 138, 182, 0.5);
|
||||
color: #FF8AB6;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
BRAND / LOGO
|
||||
============================================= */
|
||||
.navbar-brand {
|
||||
font-weight: 700;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.navbar-brand span {
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.navbar-logo {
|
||||
height: 26px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
NAV LINKS — compactos
|
||||
============================================= */
|
||||
.glass-navbar .nav-link {
|
||||
font-size: 0.84rem;
|
||||
font-weight: 500;
|
||||
padding: 0.3rem 0.65rem !important;
|
||||
border-radius: 7px;
|
||||
transition: color 0.15s ease, background 0.15s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.glass-navbar .nav-link i {
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
TOGGLE BUTTON — limpio, pill style
|
||||
============================================= */
|
||||
.navbar-toggle-custom {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.18s ease;
|
||||
outline: none;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.navbar-toggle-custom i {
|
||||
font-size: 1.3rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
MAIN CONTAINER — espacio eficiente
|
||||
============================================= */
|
||||
.main-container {
|
||||
flex: 1;
|
||||
padding: 2rem 0;
|
||||
padding: 1rem 0 1.5rem;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
RESPONSIVE — 720p (1360x768)
|
||||
============================================= */
|
||||
@media (max-height: 800px) {
|
||||
.main-container {
|
||||
padding: 0.65rem 0 1rem;
|
||||
}
|
||||
.glass-navbar .container-fluid {
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
RESPONSIVE — 1920x1080 (pantallas grandes)
|
||||
============================================= */
|
||||
@media (min-width: 1600px) {
|
||||
.main-container {
|
||||
padding: 1.25rem 0 2rem;
|
||||
max-width: 1600px;
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
MENU COLAPSADO (mobile / tablet)
|
||||
============================================= */
|
||||
@media (max-width: 991.98px) {
|
||||
.glass-navbar .navbar-collapse {
|
||||
padding: 0.5rem 0;
|
||||
border-top: 1px solid rgba(128, 128, 128, 0.12);
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
.glass-navbar .nav-link {
|
||||
padding: 0.45rem 0.75rem !important;
|
||||
border-radius: 8px;
|
||||
margin: 1px 0;
|
||||
}
|
||||
.glass-navbar .navbar-nav.ms-auto {
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid rgba(128, 128, 128, 0.1);
|
||||
margin-top: 0.25rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in-up {
|
||||
@@ -117,42 +260,56 @@
|
||||
<body>
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-lg glass-navbar sticky-top">
|
||||
<div class="container-fluid px-4">
|
||||
<a class="navbar-brand d-flex align-items-center" href="/dashboard">
|
||||
<img src="/img/logo_unicaba_mini.png" alt="Logo UniCABA" class="navbar-logo me-2" height="30">
|
||||
<div class="container-fluid px-3 px-lg-4">
|
||||
<!-- Brand -->
|
||||
<a class="navbar-brand d-flex align-items-center gap-2" href="/dashboard">
|
||||
<img src="/img/logo_unicaba_mini.png" alt="Logo UniCABA" class="navbar-logo">
|
||||
<span>Edu-Space Admin</span>
|
||||
</a>
|
||||
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<i class="bi bi-list text-light fs-2"></i>
|
||||
|
||||
<!-- Toggle button — limpio, sin foco feo de Bootstrap -->
|
||||
<button
|
||||
class="navbar-toggle-custom d-lg-none"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarNav"
|
||||
aria-controls="navbarNav"
|
||||
aria-expanded="false"
|
||||
aria-label="Menú"
|
||||
>
|
||||
<i class="bi bi-list"></i>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto align-items-center ms-3">
|
||||
<!-- Nav principal -->
|
||||
<ul class="navbar-nav me-auto align-items-lg-center ms-lg-2 gap-lg-1">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3" href="/dashboard">
|
||||
<a class="nav-link" href="/dashboard">
|
||||
<i class="bi bi-speedometer2 me-1"></i>Panel General
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3" href="/classrooms/list_classrooms">
|
||||
<i class="bi bi-door-open me-1"></i>Espacios & Sedes
|
||||
<a class="nav-link" href="/classrooms/list_classrooms">
|
||||
<i class="bi bi-door-open me-1"></i>Espacios & Sedes
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3" href="/schedule/calendar_view">
|
||||
<i class="bi bi-calendar3 me-1"></i>Cronograma & Reservas
|
||||
<a class="nav-link" href="/schedule/calendar_view">
|
||||
<i class="bi bi-calendar3 me-1"></i>Cronograma & Reservas
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3 active text-primary fw-semibold" href="/predictive">
|
||||
<a class="nav-link" href="/predictive">
|
||||
<i class="bi bi-cpu me-1"></i>Módulo Predictivo
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav ms-auto align-items-center">
|
||||
<!-- Nav derecha -->
|
||||
<ul class="navbar-nav align-items-lg-center ms-auto gap-2">
|
||||
<li class="nav-item">
|
||||
<a class="btn btn-outline-danger btn-sm rounded-pill px-3 fw-medium" href="/auth/logout">
|
||||
<i class="bi bi-box-arrow-right me-1"></i> Cerrar Sesión
|
||||
<i class="bi bi-box-arrow-right me-1"></i>Cerrar Sesión
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -161,7 +318,7 @@
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="container main-container">
|
||||
<main class="container-fluid container-xl main-container px-3 px-lg-4">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
|
||||
@@ -3,29 +3,29 @@
|
||||
{% block title %}Reservations Calendar - Edu-Space Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<div class="container-fluid px-0">
|
||||
<!-- Header -->
|
||||
<div class="row mb-4 align-items-center">
|
||||
<div class="row mb-3 align-items-center">
|
||||
<div class="col-md-7">
|
||||
<h1 class="h3 mb-1">
|
||||
<h1 class="h4 mb-1 fw-bold">
|
||||
<i class="bi bi-calendar3 text-primary me-2"></i>Reservations Calendar
|
||||
</h1>
|
||||
<p class="text-muted small mb-0">View & Manage Reservations — Horarios de cursada y asignación de aulas UniCABA</p>
|
||||
<p class="text-muted small mb-0">Gestión de Reservas — Horarios de cursada y asignación de aulas UniCABA</p>
|
||||
</div>
|
||||
<div class="col-md-5 text-md-end mt-3 mt-md-0">
|
||||
<div class="col-md-5 text-md-end mt-2 mt-md-0">
|
||||
{% if user.has_permission('reservations', 'read_write') %}
|
||||
<a href="/schedule/add_reservation" class="btn btn-primary me-2">
|
||||
<a href="/schedule/add_reservation" class="btn btn-primary btn-sm me-2">
|
||||
<i class="bi bi-plus-circle me-1"></i>Add Reservation
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/schedule/list_reservations" class="btn btn-outline-secondary">
|
||||
<a href="/schedule/list_reservations" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-list me-1"></i>List View
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Toolbar (Classroom & Subject Filters to prevent overlapping) -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-2 align-items-center">
|
||||
<!-- Filter by Classroom -->
|
||||
@@ -141,32 +141,81 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calendar Card -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-body-tertiary border-0 py-3">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span class="fw-bold small text-secondary text-uppercase tracking-wide">
|
||||
<i class="bi bi-info-circle me-1"></i>Referencia:
|
||||
<!-- Grid Layout: Panel Lateral de Comisiones Pendientes + Calendario Principal -->
|
||||
<div class="row g-3">
|
||||
<!-- Sidebar de Comisiones Pendientes de Aula (Drag & Drop) -->
|
||||
<div class="col-xl-3 col-lg-4" id="unassignedCommissionsSidebar">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-header bg-body-tertiary border-0 py-2 d-flex justify-content-between align-items-center">
|
||||
<span class="fw-bold small text-body">
|
||||
<i class="bi bi-grip-vertical text-primary me-1"></i>Comisiones por Asignar
|
||||
</span>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="badge badge-brand me-1" style="width: 14px; height: 14px; border-radius: 4px;"></span>
|
||||
<small class="text-muted">Clase Confirmada</small>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="badge bg-warning me-1" style="width: 14px; height: 14px; border-radius: 4px;"></span>
|
||||
<small class="text-muted">Pendiente</small>
|
||||
</div>
|
||||
<span class="badge bg-primary rounded-pill" id="unassignedCount">{{ unassigned_commissions|length }}</span>
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
<i class="bi bi-lightning-charge-fill text-warning me-1"></i>Haz clic en cualquier clase para ver los detalles completos
|
||||
<div class="card-body p-2" style="max-height: 800px; overflow-y: auto;">
|
||||
<div class="alert alert-info border-0 py-2 px-2 small mb-2 d-flex align-items-center">
|
||||
<i class="bi bi-info-circle-fill me-2 fs-6"></i>
|
||||
<span>Arrastra una comisión a la grilla para asignarla al aula filtrada.</span>
|
||||
</div>
|
||||
<div id="externalEventsContainer" class="d-flex flex-column gap-2">
|
||||
{% for c in unassigned_commissions %}
|
||||
<div class="fc-event-drag-item p-2 rounded-2 border shadow-sm"
|
||||
data-commission-id="{{ c.id }}"
|
||||
data-subject-name="{{ c.subject.name if c.subject else 'Materia' }}"
|
||||
data-code="{{ c.get_full_code or c.code }}"
|
||||
data-students="{{ c.current_students or 0 }}"
|
||||
data-shift="{{ c.shift or 'Regular' }}"
|
||||
style="cursor: grab; background-color: var(--bs-body-bg); border-left: 4px solid #B43E8E !important; user-select: none;">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong class="text-primary small font-monospace">{{ c.get_full_code or c.code }}</strong>
|
||||
<span class="badge bg-secondary-subtle text-secondary small"><i class="bi bi-people me-1"></i>{{ c.current_students or 0 }} al.</span>
|
||||
</div>
|
||||
<div class="small fw-semibold text-truncate text-body mt-1">{{ c.subject.name if c.subject else 'Materia' }}</div>
|
||||
<div class="d-flex justify-content-between align-items-center mt-1">
|
||||
<small class="text-muted" style="font-size: 0.72rem;">Turno: {{ c.shift or 'Regular' }}</small>
|
||||
<span class="badge bg-primary-subtle text-primary border" style="font-size: 0.65rem;">Arrastrar <i class="bi bi-arrows-move"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-4 text-muted small" id="noUnassignedPlaceholder">
|
||||
<i class="bi bi-check2-circle fs-3 text-success d-block mb-1"></i>
|
||||
Todas las comisiones tienen aula asignada.
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-3">
|
||||
<!-- FullCalendar Container -->
|
||||
<div id="calendar"></div>
|
||||
|
||||
<!-- Columna del Calendario Principal -->
|
||||
<div class="col-xl-9 col-lg-8" id="calendarMainCol">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-body-tertiary border-0 py-3">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span class="fw-bold small text-secondary text-uppercase tracking-wide">
|
||||
<i class="bi bi-info-circle me-1"></i>Referencia:
|
||||
</span>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="badge badge-brand me-1" style="width: 14px; height: 14px; border-radius: 4px;"></span>
|
||||
<small class="text-muted">Clase Confirmada</small>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="badge bg-warning me-1" style="width: 14px; height: 14px; border-radius: 4px;"></span>
|
||||
<small class="text-muted">Pendiente</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
<i class="bi bi-arrows-move text-primary me-1"></i><strong>Drag & Drop activo:</strong> Mueve clases en el calendario con validación de choques en tiempo real
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-3">
|
||||
<!-- FullCalendar Container -->
|
||||
<div id="calendar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -759,6 +808,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
initialView: 'timeGridWeek',
|
||||
initialDate: todayDate,
|
||||
allDaySlot: false,
|
||||
editable: true,
|
||||
droppable: true,
|
||||
eventDurationEditable: true,
|
||||
eventResizableFromStart: false,
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
@@ -784,6 +837,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
slotEventOverlap: true,
|
||||
expandRows: true,
|
||||
stickyHeaderDates: true,
|
||||
|
||||
/* Drag and Drop Handlers con Validación de Colisión en Tiempo Real */
|
||||
eventDrop: function(info) {
|
||||
handleEventDragOrResize(info, false);
|
||||
},
|
||||
eventResize: function(info) {
|
||||
handleEventDragOrResize(info, true);
|
||||
},
|
||||
eventReceive: function(info) {
|
||||
handleExternalDrop(info);
|
||||
},
|
||||
|
||||
/* Dynamic Server-Side Filtered Events */
|
||||
events: function(info, successCallback, failureCallback) {
|
||||
@@ -1018,6 +1082,153 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
modal.show();
|
||||
}
|
||||
});
|
||||
|
||||
// Soporte Drag and Drop externo para Comisiones Pendientes
|
||||
var containerEl = document.getElementById('externalEventsContainer');
|
||||
if (containerEl && typeof FullCalendar.Draggable !== 'undefined') {
|
||||
new FullCalendar.Draggable(containerEl, {
|
||||
itemSelector: '.fc-event-drag-item',
|
||||
eventData: function(eventEl) {
|
||||
return {
|
||||
title: (eventEl.getAttribute('data-code') || 'Comisión') + ' - ' + (eventEl.getAttribute('data-subject-name') || ''),
|
||||
extendedProps: {
|
||||
commission_id: eventEl.getAttribute('data-commission-id'),
|
||||
subject_name: eventEl.getAttribute('data-subject-name'),
|
||||
is_external_commission: true
|
||||
},
|
||||
duration: '02:00'
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showToast(title, message, details, type) {
|
||||
var toastContainer = document.querySelector('.toast-container');
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
toastContainer.className = 'toast-container position-fixed bottom-0 end-0 p-3';
|
||||
toastContainer.style.zIndex = '1090';
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
var bgClass = type === 'danger' ? 'bg-danger text-white' : (type === 'warning' ? 'bg-warning text-dark' : 'bg-success text-white');
|
||||
var iconClass = type === 'danger' ? 'bi-exclamation-octagon-fill' : (type === 'warning' ? 'bi-exclamation-triangle-fill' : 'bi-check-circle-fill');
|
||||
|
||||
var detailsHtml = '';
|
||||
if (details && details.length > 0) {
|
||||
detailsHtml = '<ul class="mb-0 mt-1 ps-3 small">' + details.map(function(d) { return '<li>' + d + '</li>'; }).join('') + '</ul>';
|
||||
}
|
||||
|
||||
var toastEl = document.createElement('div');
|
||||
toastEl.className = 'toast align-items-center ' + bgClass + ' border-0 shadow-lg mb-2';
|
||||
toastEl.setAttribute('role', 'alert');
|
||||
toastEl.setAttribute('aria-live', 'assertive');
|
||||
toastEl.setAttribute('aria-atomic', 'true');
|
||||
toastEl.innerHTML = '<div class="d-flex"><div class="toast-body"><div class="fw-bold"><i class="bi ' + iconClass + ' me-2"></i>' + title + '</div><div class="small">' + message + '</div>' + detailsHtml + '</div><button type="button" class="btn-close ' + (type === 'warning' ? '' : 'btn-close-white') + ' me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button></div>';
|
||||
toastContainer.appendChild(toastEl);
|
||||
var bsToast = new bootstrap.Toast(toastEl, { delay: 6000 });
|
||||
bsToast.show();
|
||||
toastEl.addEventListener('hidden.bs.toast', function() {
|
||||
toastEl.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function handleEventDragOrResize(info, isResize) {
|
||||
var startIso = info.event.start ? info.event.start.toISOString() : null;
|
||||
var endIso = info.event.end ? info.event.end.toISOString() : (info.event.start ? new Date(info.event.start.getTime() + 2 * 3600 * 1000).toISOString() : null);
|
||||
var ext = info.event.extendedProps || {};
|
||||
var classroomId = filterRoom.value ? parseInt(filterRoom.value, 10) : (ext.classroom_id || null);
|
||||
|
||||
fetch('/api/v1/reservations/drag-update', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
reservation_id: parseInt(info.event.id, 10),
|
||||
start_time: startIso,
|
||||
end_time: endIso,
|
||||
classroom_id: classroomId
|
||||
})
|
||||
})
|
||||
.then(function(res) {
|
||||
return res.json().then(function(data) {
|
||||
return { status: res.status, data: data };
|
||||
});
|
||||
})
|
||||
.then(function(result) {
|
||||
if (result.status === 409 || (result.data && result.data.status === 'conflict')) {
|
||||
info.revert();
|
||||
var reasons = (result.data.conflicts || []).concat(result.data.block_reasons || []);
|
||||
showToast('Conflicto de Horario Detectado', result.data.message || 'No se puede reubicar la clase en ese espacio/horario.', reasons, 'danger');
|
||||
} else if (result.status >= 400) {
|
||||
info.revert();
|
||||
showToast('Error', result.data.message || 'Ocurrió un error al actualizar la reserva.', [], 'danger');
|
||||
} else {
|
||||
showToast('Reserva Actualizada', result.data.message || 'Horario y aula confirmados con éxito.', [], 'success');
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
info.revert();
|
||||
console.error('Drag update error:', err);
|
||||
showToast('Error de Conexión', 'No se pudo comunicar con el servidor.', [], 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
function handleExternalDrop(info) {
|
||||
var ext = info.event.extendedProps || {};
|
||||
var commissionId = ext.commission_id;
|
||||
var startIso = info.event.start ? info.event.start.toISOString() : null;
|
||||
var endIso = info.event.end ? info.event.end.toISOString() : (info.event.start ? new Date(info.event.start.getTime() + 2 * 3600 * 1000).toISOString() : null);
|
||||
var classroomId = filterRoom.value ? parseInt(filterRoom.value, 10) : null;
|
||||
|
||||
if (!classroomId) {
|
||||
info.revert();
|
||||
showToast('Aula Requerida', 'Por favor selecciona un Aula en el filtro superior antes de arrastrar comisiones a la grilla.', [], 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/api/v1/reservations/drag-update', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
commission_id: parseInt(commissionId, 10),
|
||||
start_time: startIso,
|
||||
end_time: endIso,
|
||||
classroom_id: classroomId
|
||||
})
|
||||
})
|
||||
.then(function(res) {
|
||||
return res.json().then(function(data) {
|
||||
return { status: res.status, data: data };
|
||||
});
|
||||
})
|
||||
.then(function(result) {
|
||||
if (result.status === 409 || (result.data && result.data.status === 'conflict')) {
|
||||
info.revert();
|
||||
var reasons = (result.data.conflicts || []).concat(result.data.block_reasons || []);
|
||||
showToast('Conflicto al Asignar Comisión', result.data.message || 'El aula o docente no están disponibles en este horario.', reasons, 'danger');
|
||||
} else if (result.status >= 400) {
|
||||
info.revert();
|
||||
showToast('Error', result.data.message || 'No se pudo asignar el aula a la comisión.', [], 'danger');
|
||||
} else {
|
||||
showToast('Comisión Asignada', result.data.message || 'Comisión asignada con éxito al aula.', [], 'success');
|
||||
var draggedItem = document.querySelector('.fc-event-drag-item[data-commission-id="' + commissionId + '"]');
|
||||
if (draggedItem) {
|
||||
draggedItem.remove();
|
||||
var countBadge = document.getElementById('unassignedCount');
|
||||
if (countBadge) {
|
||||
var curr = parseInt(countBadge.textContent, 10) || 1;
|
||||
countBadge.textContent = Math.max(0, curr - 1);
|
||||
}
|
||||
}
|
||||
calendar.refetchEvents();
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
info.revert();
|
||||
console.error('External drop error:', err);
|
||||
showToast('Error de Conexión', 'No se pudo registrar la asignación.', [], 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
calendar.render();
|
||||
|
||||
|
||||
@@ -512,9 +512,9 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<div class="container-fluid px-0">
|
||||
<!-- Header -->
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-3">
|
||||
<div>
|
||||
<h1 class="h3 fw-bold mb-1">
|
||||
<i class="bi bi-diagram-3 text-primary me-2"></i>Gestión de Comisiones y Cursadas
|
||||
@@ -720,7 +720,7 @@
|
||||
<div class="d-flex flex-wrap align-items-center gap-1">
|
||||
{% for m in commission.academic_milestones %}
|
||||
<span class="badge comm-milestone-chip text-white d-inline-flex align-items-center py-1 px-2"
|
||||
style="background-color: {{ m.color }}; font-size: 0.72rem;"
|
||||
{{ ('style="background-color: ' ~ (m.color or '#6c757d') ~ '; font-size: 0.72rem;"') | safe }}
|
||||
title="{{ m.title }} - {{ m.date if m.date else '' }} ({{ m.classroom or 'Campus Virtual' }})">
|
||||
<i class="bi bi-flag-fill me-1" style="font-size: 0.65rem;"></i>
|
||||
{{ m.title or m.tipo }}
|
||||
@@ -753,7 +753,7 @@
|
||||
{% set fill_pct = (commission.current_students / commission.max_students * 100) if commission.max_students > 0 else 0 %}
|
||||
<div class="progress mt-1 mx-auto" style="height: 4px; width: 50px;">
|
||||
<div class="progress-bar {% if fill_pct > 90 %}bg-danger{% elif fill_pct > 70 %}bg-warning{% else %}bg-success{% endif %}"
|
||||
style="width: {{ [fill_pct, 100]|min }}%;"></div>
|
||||
{{ ('style="width: ' ~ ([fill_pct, 100]|min) ~ '%;"') | safe }}></div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end pe-4">
|
||||
|
||||
@@ -0,0 +1,527 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Libro de Calificaciones — {{ commission.full_code or commission.code }} — Edu-Space{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb small">
|
||||
<li class="breadcrumb-item"><a href="/dashboard">Inicio</a></li>
|
||||
<li class="breadcrumb-item"><a href="/admin/commissions_list">Comisiones</a></li>
|
||||
<li class="breadcrumb-item"><a href="/admin/commission_detail?id={{ commission.id }}">{{ commission.full_code or commission.code }}</a></li>
|
||||
<li class="breadcrumb-item active">Libro de Calificaciones</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Header Banner -->
|
||||
<div class="card border-0 shadow-sm mb-4 bg-body">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3">
|
||||
<div>
|
||||
<div class="d-flex align-items-center gap-2 mb-1">
|
||||
<span class="badge bg-primary-subtle text-primary border border-primary-subtle font-monospace px-2 py-1">
|
||||
{{ commission.full_code or commission.code }}
|
||||
</span>
|
||||
<span class="badge bg-secondary-subtle text-secondary border font-monospace">
|
||||
{{ commission.semester }} {{ commission.year }}
|
||||
</span>
|
||||
{% if commission.grades_closed %}
|
||||
<span class="badge bg-danger text-white px-2 py-1 shadow-sm">
|
||||
<i class="bi bi-lock-fill me-1"></i>ACTA CERRADA ({{ commission.acta_number }})
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle px-2 py-1">
|
||||
<i class="bi bi-unlock-fill me-1"></i>CALIFICACIONES ABIERTAS
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h3 class="fw-bold mb-1">{{ commission.subject.name if commission.subject else 'Materia' }}</h3>
|
||||
<p class="text-muted small mb-0">
|
||||
<i class="bi bi-clock me-1"></i>Turno: <strong>{{ commission.shift or 'A definir' }}</strong>
|
||||
{% if commission.teachers and commission.teachers|length > 0 %}
|
||||
<span class="mx-2">·</span>
|
||||
<i class="bi bi-person-badge me-1"></i>Cátedra:
|
||||
{% for t in commission.teachers %}
|
||||
<span class="badge bg-light text-dark border me-1">{{ t.name }} ({{ t.role_in_commission or 'Docente' }})</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<!-- Live Auto-save indicator -->
|
||||
<div id="saveStatusIndicator" class="small px-3 py-1 rounded-pill bg-body-tertiary border text-muted d-flex align-items-center gap-2">
|
||||
<i class="bi bi-cloud-check text-success fs-6" id="saveStatusIcon"></i>
|
||||
<span id="saveStatusText">Todos los cambios guardados</span>
|
||||
</div>
|
||||
|
||||
<!-- Print / Export -->
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="window.print()">
|
||||
<i class="bi bi-printer me-1"></i>Imprimir Planilla
|
||||
</button>
|
||||
|
||||
<!-- Acta Action Buttons -->
|
||||
{% if commission.grades_closed %}
|
||||
{% if role in ['admin', 'bedelia'] %}
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" data-bs-toggle="modal" data-bs-target="#reopenActaModal">
|
||||
<i class="bi bi-unlock me-1"></i>Reabrir Acta
|
||||
</button>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-danger btn-sm shadow-sm" data-bs-toggle="modal" data-bs-target="#closeActaModal">
|
||||
<i class="bi bi-file-earmark-lock me-1"></i>Cerrar Acta Formal
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI Summary Cards -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card border-0 shadow-sm p-3 h-100">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span class="text-muted small fw-semibold text-uppercase">Total Inscriptos</span>
|
||||
<h3 class="fw-bold mb-0 mt-1" id="kpiTotalStudents">{{ students|length }}</h3>
|
||||
</div>
|
||||
<div class="bg-primary bg-opacity-10 text-primary rounded-circle p-3">
|
||||
<i class="bi bi-people fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card border-0 shadow-sm p-3 h-100">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span class="text-muted small fw-semibold text-uppercase">Promocionados (≥ 7)</span>
|
||||
<h3 class="fw-bold mb-0 mt-1 text-success" id="kpiPromoted">0</h3>
|
||||
</div>
|
||||
<div class="bg-success bg-opacity-10 text-success rounded-circle p-3">
|
||||
<i class="bi bi-award fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card border-0 shadow-sm p-3 h-100">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span class="text-muted small fw-semibold text-uppercase">Regulares (4 a 6.9)</span>
|
||||
<h3 class="fw-bold mb-0 mt-1 text-primary" id="kpiRegular">0</h3>
|
||||
</div>
|
||||
<div class="bg-info bg-opacity-10 text-info rounded-circle p-3">
|
||||
<i class="bi bi-check2-circle fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card border-0 shadow-sm p-3 h-100">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span class="text-muted small fw-semibold text-uppercase">Libres (< 4)</span>
|
||||
<h3 class="fw-bold mb-0 mt-1 text-danger" id="kpiLibre">0</h3>
|
||||
</div>
|
||||
<div class="bg-danger bg-opacity-10 text-danger rounded-circle p-3">
|
||||
<i class="bi bi-x-circle fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gradebook Matrix Table Card -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-body-tertiary border-0 py-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div>
|
||||
<h5 class="mb-0 h6 fw-bold">
|
||||
<i class="bi bi-table me-2 text-primary"></i>Matriz de Evaluaciones y Calificaciones
|
||||
</h5>
|
||||
<small class="text-muted">Las notas se guardan automáticamente al escribirlas. Escala: 1 a 10 (Ausente: A o vacío).</small>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<input type="text" id="filterStudentInput" class="form-control form-control-sm" placeholder="Buscar alumno o legajo..." style="max-width: 220px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-0">
|
||||
{% if students and students|length > 0 %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0" id="gradebookTable">
|
||||
<thead class="table-light small">
|
||||
<tr>
|
||||
<th class="ps-3" style="width: 60px;">#</th>
|
||||
<th style="min-width: 220px;">Estudiante</th>
|
||||
<th style="min-width: 110px;">Legajo</th>
|
||||
{% for col in milestone_columns %}
|
||||
<th class="text-center" style="min-width: 130px;">
|
||||
<div>{{ col.title }}</div>
|
||||
<small class="text-muted fw-normal font-monospace">({{ col.date }})</small>
|
||||
</th>
|
||||
{% endfor %}
|
||||
<th class="text-center" style="min-width: 110px;">Promedio</th>
|
||||
<th class="text-center pe-3" style="min-width: 140px;">Condición Final</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in students %}
|
||||
<tr data-student-id="{{ s.id }}" data-student-name="{{ s.name|lower }}" data-student-dni="{{ s.dni or '' }}">
|
||||
<td class="ps-3 font-monospace text-muted small">{{ loop.index }}</td>
|
||||
<td>
|
||||
<div class="fw-semibold text-body">{{ s.name }}</div>
|
||||
<small class="text-muted font-monospace">{{ s.email }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary-subtle text-secondary font-monospace">
|
||||
{{ s.dni or ('LEG-' + s.id) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{% for col in milestone_columns %}
|
||||
{% set grade_obj = s.grades[col.id] %}
|
||||
<td class="text-center">
|
||||
<div class="d-flex justify-content-center align-items-center">
|
||||
<input
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="1"
|
||||
max="10"
|
||||
class="form-control form-control-sm text-center font-monospace fw-bold grade-input"
|
||||
style="max-width: 85px; font-size: 0.95rem;"
|
||||
data-milestone-id="{{ col.id }}"
|
||||
data-student-id="{{ s.id }}"
|
||||
value="{{ grade_obj.score if grade_obj and grade_obj.score is not none else '' }}"
|
||||
placeholder="-"
|
||||
{% if commission.grades_closed %}disabled{% endif %}
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Promedio calculado en vivo -->
|
||||
<td class="text-center">
|
||||
<span class="font-monospace fw-bold fs-6 avg-cell">-</span>
|
||||
</td>
|
||||
|
||||
<!-- Condición académica calculada en vivo -->
|
||||
<td class="text-center pe-3">
|
||||
<span class="badge condition-badge font-monospace py-2 px-2">Pendiente</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-people fs-1 text-muted d-block mb-3"></i>
|
||||
<h5 class="fw-bold text-muted">No hay estudiantes inscriptos en esta comisión</h5>
|
||||
<p class="text-muted small">Puede matricular alumnos desde el panel de detalle de la comisión.</p>
|
||||
<a href="/admin/commission_detail?id={{ commission.id }}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-person-plus me-1"></i>Ir a Matriculación
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Modal: Cierre Formal de Acta -->
|
||||
<div class="modal fade" id="closeActaModal" tabindex="-1" aria-labelledby="closeActaModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger text-white">
|
||||
<h5 class="modal-title h6 fw-bold" id="closeActaModalLabel">
|
||||
<i class="bi bi-shield-lock me-1"></i>Cierre Formal de Acta de Regularidad
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-warning small mb-3">
|
||||
<i class="bi bi-exclamation-triangle-fill me-1"></i>
|
||||
<strong>Atención:</strong> Al cerrar el acta, todas las notas se congelan legalmente y se genera un número de acta único e inmutable. Sólo el personal de Bedelía o Administradores podrá reabrirla con justificación escrita.
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Observaciones o Notas de Cátedra (Opcional):</label>
|
||||
<textarea id="closeActaNotes" class="form-control small" rows="3" placeholder="Ej: Cursada cerrada con 22 alumnos regulares y 5 promocionados."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="button" class="btn btn-sm btn-danger" id="btnConfirmCloseActa">
|
||||
<i class="bi bi-lock-fill me-1"></i>Confirmar y Sellar Acta
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Reapertura de Acta (Admin / Bedelía) -->
|
||||
<div class="modal fade" id="reopenActaModal" tabindex="-1" aria-labelledby="reopenActaModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-warning">
|
||||
<h5 class="modal-title h6 fw-bold" id="reopenActaModalLabel">
|
||||
<i class="bi bi-unlock-fill me-1"></i>Reapertura Excepcional de Acta
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="small text-muted mb-3">
|
||||
Esta acción quedará asentada en la bitácora inmutable de auditoría institucional.
|
||||
</p>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Motivo formal de reapertura (Obligatorio):</label>
|
||||
<textarea id="reopenActaReason" class="form-control small" rows="3" placeholder="Ej: Rectificación de nota de examen recuperatorio según resolución..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="button" class="btn btn-sm btn-warning" id="btnConfirmReopenActa">
|
||||
<i class="bi bi-unlock me-1"></i>Reabrir Acta
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="gradebookConfig" data-commission-id="{{ commission.id }}" data-is-closed="{{ 'true' if commission.grades_closed else 'false' }}" class="d-none"></div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const config = document.getElementById('gradebookConfig');
|
||||
const commissionId = config ? config.dataset.commissionId : '';
|
||||
const isClosed = config ? config.dataset.isClosed === 'true' : false;
|
||||
|
||||
const statusIndicator = document.getElementById('saveStatusIndicator');
|
||||
const statusIcon = document.getElementById('saveStatusIcon');
|
||||
const statusText = document.getElementById('saveStatusText');
|
||||
|
||||
function updateStatus(state, msg) {
|
||||
if (state === 'saving') {
|
||||
statusIcon.className = 'spinner-border spinner-border-sm text-primary';
|
||||
statusText.textContent = msg || 'Guardando calificación...';
|
||||
} else if (state === 'saved') {
|
||||
statusIcon.className = 'bi bi-cloud-check text-success fs-6';
|
||||
statusText.textContent = msg || 'Todos los cambios guardados';
|
||||
} else if (state === 'error') {
|
||||
statusIcon.className = 'bi bi-exclamation-circle text-danger fs-6';
|
||||
statusText.textContent = msg || 'Error al guardar';
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate row averages and condition
|
||||
function recalculateAllRows() {
|
||||
let countPromoted = 0;
|
||||
let countRegular = 0;
|
||||
let countLibre = 0;
|
||||
|
||||
const rows = document.querySelectorAll('#gradebookTable tbody tr');
|
||||
rows.forEach(row => {
|
||||
const inputs = row.querySelectorAll('.grade-input');
|
||||
const avgCell = row.querySelector('.avg-cell');
|
||||
const badgeCell = row.querySelector('.condition-badge');
|
||||
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
let hasGrades = false;
|
||||
|
||||
inputs.forEach(inp => {
|
||||
const val = parseFloat(inp.value);
|
||||
if (!isNaN(val) && val >= 1 && val <= 10) {
|
||||
sum += val;
|
||||
count++;
|
||||
hasGrades = true;
|
||||
inp.classList.remove('is-invalid');
|
||||
} else if (inp.value.trim() !== '') {
|
||||
inp.classList.add('is-invalid');
|
||||
} else {
|
||||
inp.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
if (count > 0) {
|
||||
const avg = sum / count;
|
||||
avgCell.textContent = avg.toFixed(1);
|
||||
|
||||
if (avg >= 7) {
|
||||
badgeCell.className = 'badge bg-success-subtle text-success border border-success-subtle font-monospace py-2 px-2';
|
||||
badgeCell.textContent = 'PROMOCIONADO';
|
||||
countPromoted++;
|
||||
} else if (avg >= 4) {
|
||||
badgeCell.className = 'badge bg-primary-subtle text-primary border border-primary-subtle font-monospace py-2 px-2';
|
||||
badgeCell.textContent = 'REGULAR';
|
||||
countRegular++;
|
||||
} else {
|
||||
badgeCell.className = 'badge bg-danger-subtle text-danger border border-danger-subtle font-monospace py-2 px-2';
|
||||
badgeCell.textContent = 'LIBRE';
|
||||
countLibre++;
|
||||
}
|
||||
} else {
|
||||
avgCell.textContent = '-';
|
||||
badgeCell.className = 'badge bg-secondary-subtle text-secondary border font-monospace py-2 px-2';
|
||||
badgeCell.textContent = 'PENDIENTE';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('kpiPromoted').textContent = countPromoted;
|
||||
document.getElementById('kpiRegular').textContent = countRegular;
|
||||
document.getElementById('kpiLibre').textContent = countLibre;
|
||||
}
|
||||
|
||||
recalculateAllRows();
|
||||
|
||||
// Debounced auto-save on input
|
||||
let saveTimeouts = {};
|
||||
document.querySelectorAll('.grade-input').forEach(input => {
|
||||
input.addEventListener('input', function() {
|
||||
if (isClosed) return;
|
||||
recalculateAllRows();
|
||||
|
||||
const milestoneId = this.dataset.milestoneId;
|
||||
const studentId = this.dataset.studentId;
|
||||
const key = `${studentId}-${milestoneId}`;
|
||||
const rawVal = this.value.trim();
|
||||
|
||||
updateStatus('saving', 'Escribiendo...');
|
||||
|
||||
if (saveTimeouts[key]) clearTimeout(saveTimeouts[key]);
|
||||
saveTimeouts[key] = setTimeout(async () => {
|
||||
let score = null;
|
||||
let isAbsent = false;
|
||||
|
||||
if (rawVal === '') {
|
||||
score = null;
|
||||
isAbsent = false;
|
||||
} else {
|
||||
const num = parseFloat(rawVal);
|
||||
if (isNaN(num) || num < 1 || num > 10) {
|
||||
updateStatus('error', 'Nota debe ser entre 1 y 10');
|
||||
return;
|
||||
}
|
||||
score = num;
|
||||
}
|
||||
|
||||
try {
|
||||
updateStatus('saving', 'Guardando nota...');
|
||||
const resp = await fetch(`/api/v1/commissions/${commissionId}/gradebook/grades`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
grades: [{
|
||||
student_id: parseInt(studentId),
|
||||
reservation_id: parseInt(milestoneId),
|
||||
score: score,
|
||||
is_absent: isAbsent
|
||||
}]
|
||||
})
|
||||
});
|
||||
|
||||
const data = await resp.json();
|
||||
if (resp.ok && data.success) {
|
||||
updateStatus('saved', '✓ Guardado exitosamente');
|
||||
} else {
|
||||
updateStatus('error', data.message || 'Error al guardar');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error auto-saving grade:', err);
|
||||
updateStatus('error', 'Error de red al guardar');
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
|
||||
// Client-side search filter
|
||||
const filterInput = document.getElementById('filterStudentInput');
|
||||
if (filterInput) {
|
||||
filterInput.addEventListener('input', function() {
|
||||
const term = this.value.toLowerCase().trim();
|
||||
document.querySelectorAll('#gradebookTable tbody tr').forEach(row => {
|
||||
const name = row.dataset.studentName || '';
|
||||
const dni = row.dataset.studentDni || '';
|
||||
if (name.includes(term) || dni.includes(term)) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Close Acta
|
||||
const btnClose = document.getElementById('btnConfirmCloseActa');
|
||||
if (btnClose) {
|
||||
btnClose.addEventListener('click', async function() {
|
||||
const notes = document.getElementById('closeActaNotes').value;
|
||||
this.disabled = true;
|
||||
this.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Sellando...';
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/api/v1/commissions/${commissionId}/gradebook/close`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ notes })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (resp.ok && data.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Error al cerrar acta: ' + (data.message || 'Error del servidor'));
|
||||
btnClose.disabled = false;
|
||||
btnClose.innerHTML = '<i class="bi bi-lock-fill me-1"></i>Confirmar y Sellar Acta';
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Error de conexión: ' + err.message);
|
||||
btnClose.disabled = false;
|
||||
btnClose.innerHTML = '<i class="bi bi-lock-fill me-1"></i>Confirmar y Sellar Acta';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Reopen Acta
|
||||
const btnReopen = document.getElementById('btnConfirmReopenActa');
|
||||
if (btnReopen) {
|
||||
btnReopen.addEventListener('click', async function() {
|
||||
const reason = document.getElementById('reopenActaReason').value;
|
||||
if (!reason || reason.trim().length < 5) {
|
||||
alert('Debe especificar un motivo formal para la reapertura.');
|
||||
return;
|
||||
}
|
||||
this.disabled = true;
|
||||
this.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Reabriendo...';
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/api/v1/commissions/${commissionId}/gradebook/reopen`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ reason })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (resp.ok && data.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Error al reabrir acta: ' + (data.message || 'Error del servidor'));
|
||||
btnReopen.disabled = false;
|
||||
btnReopen.innerHTML = '<i class="bi bi-unlock me-1"></i>Reabrir Acta';
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Error de conexión: ' + err.message);
|
||||
btnReopen.disabled = false;
|
||||
btnReopen.innerHTML = '<i class="bi bi-unlock me-1"></i>Reabrir Acta';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -3,9 +3,9 @@
|
||||
{% block title %}Listado de Reservas - Edu-Space Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<div class="container-fluid px-0">
|
||||
<!-- Header -->
|
||||
<div class="row mb-4 align-items-center">
|
||||
<div class="row mb-3 align-items-center">
|
||||
<div class="col-md-7">
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="bi bi-calendar-check text-primary me-2"></i>Listado de Reservas
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Mis Calificaciones y Estado Académico — Edu-Space{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb small">
|
||||
<li class="breadcrumb-item"><a href="/dashboard">Inicio</a></li>
|
||||
<li class="breadcrumb-item active">Mis Calificaciones</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Header Banner -->
|
||||
<div class="card border-0 shadow-sm mb-4 bg-body">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3">
|
||||
<div>
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle font-monospace px-2 py-1 mb-2">
|
||||
<i class="bi bi-mortarboard-fill me-1"></i>Portal del Estudiante
|
||||
</span>
|
||||
<h3 class="fw-bold mb-1">Mis Calificaciones y Regularidad</h3>
|
||||
<p class="text-muted small mb-0">
|
||||
Consulta oficial de notas por hito evaluativo, estado de cursada y actas selladas por Bedelía.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="window.print()">
|
||||
<i class="bi bi-printer me-1"></i>Imprimir Certificado de Cursada
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subjects Grades Accordion / Cards -->
|
||||
{% if subjects and subjects|length > 0 %}
|
||||
<div class="row g-4">
|
||||
{% for item in subjects %}
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-body-tertiary border-0 py-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div>
|
||||
<div class="d-flex align-items-center gap-2 mb-1">
|
||||
<span class="badge bg-primary-subtle text-primary border font-monospace">
|
||||
{{ item.commission.code }}
|
||||
</span>
|
||||
<span class="badge bg-secondary-subtle text-secondary border font-monospace">
|
||||
{{ item.commission.semester }} {{ item.commission.year }}
|
||||
</span>
|
||||
{% if item.acta_number %}
|
||||
<span class="badge bg-danger-subtle text-danger border border-danger-subtle font-monospace">
|
||||
<i class="bi bi-lock-fill me-1"></i>Acta Cerrada: {{ item.acta_number }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-info-subtle text-info border border-info-subtle">
|
||||
<i class="bi bi-hourglass-split me-1"></i>Cursada en Curso
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h5 class="fw-bold mb-0 text-body">{{ item.subject.name }}</h5>
|
||||
<small class="text-muted">{{ item.subject.code }} · Carrera: {{ item.subject.career or 'Tecnicatura / Licenciatura' }}</small>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="text-end">
|
||||
<span class="small text-muted d-block">Promedio General</span>
|
||||
<span class="fw-bold fs-5 font-monospace text-primary">
|
||||
{{ item.average if item.average is not none else '-' }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
{% if item.academic_condition == 'PROMOCIONADO' %}
|
||||
<span class="badge bg-success text-white py-2 px-3 fs-6">
|
||||
<i class="bi bi-award-fill me-1"></i>PROMOCIONADO
|
||||
</span>
|
||||
{% elif item.academic_condition == 'REGULAR' %}
|
||||
<span class="badge bg-primary text-white py-2 px-3 fs-6">
|
||||
<i class="bi bi-check-circle-fill me-1"></i>REGULAR
|
||||
</span>
|
||||
{% elif item.academic_condition == 'LIBRE' %}
|
||||
<span class="badge bg-danger text-white py-2 px-3 fs-6">
|
||||
<i class="bi bi-x-circle-fill me-1"></i>LIBRE
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary text-white py-2 px-3 fs-6">
|
||||
PENDIENTE
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light small">
|
||||
<tr>
|
||||
<th class="ps-3">Hito Evaluativo</th>
|
||||
<th>Fecha</th>
|
||||
<th>Tipo</th>
|
||||
<th class="text-center">Calificación</th>
|
||||
<th class="text-center">Condición</th>
|
||||
<th class="pe-3">Observaciones de Cátedra</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for g in item.grades %}
|
||||
<tr>
|
||||
<td class="ps-3 fw-semibold text-body">
|
||||
{{ g.milestone_title }}
|
||||
</td>
|
||||
<td class="font-monospace text-muted small">
|
||||
{{ g.milestone_date }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-light text-dark border">
|
||||
{{ g.milestone_type }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if g.is_absent %}
|
||||
<span class="badge bg-warning text-dark font-monospace">AUSENTE</span>
|
||||
{% elif g.score is not none %}
|
||||
<span class="font-monospace fw-bold fs-6 {% if g.score >= 7 %}text-success{% elif g.score >= 4 %}text-primary{% else %}text-danger{% endif %}">
|
||||
{{ g.score }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-muted">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if g.is_absent %}
|
||||
<span class="badge bg-warning-subtle text-warning border">Ausente</span>
|
||||
{% elif g.score is not none %}
|
||||
{% if g.score >= 7 %}
|
||||
<span class="badge bg-success-subtle text-success border">Aprobado / Promoción</span>
|
||||
{% elif g.score >= 4 %}
|
||||
<span class="badge bg-primary-subtle text-primary border">Aprobado</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger-subtle text-danger border">Desaprobado</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="badge bg-secondary-subtle text-secondary border">Sin Calificar</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="pe-3 small text-muted">
|
||||
{{ g.feedback or '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body text-center py-5">
|
||||
<i class="bi bi-journal-x fs-1 text-muted d-block mb-3"></i>
|
||||
<h5 class="fw-bold text-muted">Aún no se registran calificaciones cargadas</h5>
|
||||
<p class="text-muted small">
|
||||
Tus notas aparecerán aquí a medida que los equipos docentes carguen las evaluaciones de tus materias cursadas.
|
||||
</p>
|
||||
<a href="/schedule/today_schedule" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-calendar-day me-1"></i>Ver Cartelera de Clases
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -3,9 +3,9 @@
|
||||
{% block title %}Cartelera de Hoy - Edu-Space Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<div class="container-fluid px-0">
|
||||
<!-- Header -->
|
||||
<div class="row mb-4 align-items-center">
|
||||
<div class="row mb-3 align-items-center">
|
||||
<div class="col-md-7">
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="bi bi-calendar-day text-info me-2"></i>Cartelera del Día
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
{% set percentage = (reservation.expected_attendees / reservation.classroom.capacity * 100) %}
|
||||
<div class="progress-bar bg-{% if percentage > 90 %}danger{% elif percentage > 75 %}warning{% else %}success{% endif %}"
|
||||
role="progressbar"
|
||||
style="{{ 'width: ' ~ ([percentage|round(1), 100]|min|string) ~ '%;' }}"></div>
|
||||
{{ ('style="width: ' ~ ([percentage|round(1), 100]|min|string) ~ '%;"') | safe }}></div>
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
{{ reservation.expected_attendees }} / {{ reservation.classroom.capacity }} capacidad
|
||||
|
||||
Reference in New Issue
Block a user