feat(sync): add academic data reset tool, configurable sheets URL and fix sync timeout

This commit is contained in:
2026-09-19 11:13:24 -03:00
parent 42b31ec44e
commit 5087e22988
10 changed files with 835 additions and 101 deletions
+114 -26
View File
@@ -16,27 +16,57 @@ const MOCK_RECENT_SUBJECTS = [
// 1. Google Sheets Import
const handleGoogleSheetsImport = async (req, res, justSynced = false) => {
try {
let totalClassrooms = 31;
let totalReservations = 19;
let totalClassrooms = 0;
let totalReservations = 0;
let totalCareers = 0;
let totalSubjects = 0;
let totalCommissions = 0;
let recentSubjects = [];
let sheetsConfig = {
active_url: 'https://docs.google.com/spreadsheets/d/e/2PACX-1vSc_T_BQjbn3uPelioCgx52UM5Py-qNhJN0TYPd1kmsN5jdb3Q8rAaIvNMF_2ZTzQt6bH--yIWQKrKR/pub?single=true&output=csv',
default_url: 'https://docs.google.com/spreadsheets/d/e/2PACX-1vSc_T_BQjbn3uPelioCgx52UM5Py-qNhJN0TYPd1kmsN5jdb3Q8rAaIvNMF_2ZTzQt6bH--yIWQKrKR/pub?single=true&output=csv',
source: 'default'
};
try {
const classRes = await req.apiClient.get('/classrooms');
if (classRes.data && classRes.data.classrooms) {
totalClassrooms = classRes.data.classrooms.length;
const [classRes, resRes, carRes, subjRes, commRes, cfgRes] = await Promise.allSettled([
req.apiClient.get('/classrooms'),
req.apiClient.get('/reservations'),
req.apiClient.get('/careers'),
req.apiClient.get('/subjects'),
req.apiClient.get('/commissions'),
req.apiClient.get('/admin/sheets-config')
]);
if (classRes.status === 'fulfilled' && classRes.value.data?.classrooms) {
totalClassrooms = classRes.value.data.classrooms.length;
}
const resRes = await req.apiClient.get('/reservations');
if (resRes.data && resRes.data.reservations) {
totalReservations = resRes.data.reservations.length;
if (resRes.status === 'fulfilled' && resRes.value.data?.reservations) {
totalReservations = resRes.value.data.reservations.length;
}
if (carRes.status === 'fulfilled' && carRes.value.data?.careers) {
totalCareers = carRes.value.data.careers.length;
}
if (subjRes.status === 'fulfilled' && subjRes.value.data?.subjects) {
const subjs = subjRes.value.data.subjects;
totalSubjects = subjs.length;
recentSubjects = subjs.slice(0, 15);
}
if (commRes.status === 'fulfilled' && commRes.value.data?.commissions) {
totalCommissions = commRes.value.data.commissions.length;
}
if (cfgRes.status === 'fulfilled' && cfgRes.value.data?.config) {
sheetsConfig = cfgRes.value.data.config;
}
} catch (apiErr) {
console.warn('Google Sheets import live stats fallback:', apiErr.message);
}
const metrics = {
total_careers: 5,
total_subjects: 18,
total_careers: totalCareers,
total_subjects: totalSubjects,
total_classrooms: totalClassrooms,
total_commissions: 24,
total_commissions: totalCommissions,
total_reservations: totalReservations,
sheets: [
{ name: 'LUNES' },
@@ -49,34 +79,53 @@ const handleGoogleSheetsImport = async (req, res, justSynced = false) => {
};
let stats = null;
let syncError = null;
if (justSynced) {
try {
const syncRes = await req.apiClient.post('/dashboard/sync-sheets');
const syncPayload = {};
if (req.body && req.body.sheets_url) {
syncPayload.sheets_url = req.body.sheets_url;
}
const syncRes = await req.apiClient.post('/dashboard/sync-sheets', syncPayload, { timeout: 90000 });
if (syncRes.data && syncRes.data.stats) {
stats = syncRes.data.stats;
}
// Refresh counts after sync
try {
const [cR, rR, crR, sR, cmR] = await Promise.allSettled([
req.apiClient.get('/classrooms'),
req.apiClient.get('/reservations'),
req.apiClient.get('/careers'),
req.apiClient.get('/subjects'),
req.apiClient.get('/commissions')
]);
if (cR.status === 'fulfilled') metrics.total_classrooms = cR.value.data?.classrooms?.length || 0;
if (rR.status === 'fulfilled') metrics.total_reservations = rR.value.data?.reservations?.length || 0;
if (crR.status === 'fulfilled') metrics.total_careers = crR.value.data?.careers?.length || 0;
if (sR.status === 'fulfilled') {
metrics.total_subjects = sR.value.data?.subjects?.length || 0;
recentSubjects = (sR.value.data?.subjects || []).slice(0, 15);
}
if (cmR.status === 'fulfilled') metrics.total_commissions = cmR.value.data?.commissions?.length || 0;
} catch (e) {}
} catch (err) {
console.warn('Backend sync-sheets API notice:', err.message);
stats = {
total_rows_processed: 239,
careers_created: 0,
classrooms_created: 0,
subjects_created: 0,
commissions_created: 0,
reservations_created: 0,
errors: []
};
}
if (stats && (!stats.errors || stats.errors.length === 0)) {
stats.errors = null;
console.error('Backend sync-sheets API error:', err.response?.data || err.message);
syncError = err.response?.data?.message || err.message;
}
}
const flashMsg = req.query.msg || null;
const flashType = req.query.type || 'info';
res.render('admin/import', {
title: 'Import from Google Sheets - Edu-Space Admin',
metrics,
stats,
recent_subjects: MOCK_RECENT_SUBJECTS
sync_error: syncError,
sheets_config: sheetsConfig,
recent_subjects: recentSubjects.length > 0 ? recentSubjects : MOCK_RECENT_SUBJECTS,
flash_msg: flashMsg,
flash_type: flashType
});
} catch (error) {
console.error('Error rendering google sheets import:', error.message);
@@ -87,6 +136,45 @@ const handleGoogleSheetsImport = async (req, res, justSynced = false) => {
router.get(['/google_sheets_import', '/import'], (req, res) => handleGoogleSheetsImport(req, res, false));
router.post(['/google_sheets_import', '/import'], (req, res) => handleGoogleSheetsImport(req, res, true));
// Guardar configuración del enlace de Google Sheets
router.post('/google_sheets_config', async (req, res) => {
try {
const url = (req.body.google_sheets_url || req.body.url || '').trim();
if (!url) {
return res.redirect('/admin/google_sheets_import?msg=La+URL+no+puede+estar+vac%C3%ADa&type=danger');
}
await req.apiClient.post('/admin/sheets-config', { url });
res.redirect('/admin/google_sheets_import?msg=Configuraci%C3%B3n+de+Google+Sheets+actualizada+correctamente&type=success');
} catch (err) {
console.error('Error updating sheets config:', err.message);
const errMsg = encodeURIComponent(err.response?.data?.message || 'Error al guardar la configuración');
res.redirect(`/admin/google_sheets_import?msg=${errMsg}&type=danger`);
}
});
// Purga / Reinicio de datos académicos
router.post('/reset_academic_data', async (req, res) => {
try {
const payload = {
all: req.body.reset_all === 'true' || req.body.reset_all === 'on',
reservations: req.body.reset_reservations === 'true' || req.body.reset_reservations === 'on',
commissions: req.body.reset_commissions === 'true' || req.body.reset_commissions === 'on',
subjects: req.body.reset_subjects === 'true' || req.body.reset_subjects === 'on',
careers: req.body.reset_careers === 'true' || req.body.reset_careers === 'on',
classrooms: req.body.reset_classrooms === 'true' || req.body.reset_classrooms === 'on'
};
const purgeRes = await req.apiClient.post('/admin/reset-academic-data', payload);
const del = purgeRes.data?.deleted || {};
const summary = encodeURIComponent(`Datos reiniciados: ${del.reservations || 0} reservas, ${del.commissions || 0} comisiones, ${del.subjects || 0} materias, ${del.careers || 0} carreras, ${del.classrooms || 0} aulas.`);
res.redirect(`/admin/google_sheets_import?msg=${summary}&type=warning`);
} catch (err) {
console.error('Error resetting academic data:', err.message);
const errMsg = encodeURIComponent(err.response?.data?.message || 'Error al purgar los datos');
res.redirect(`/admin/google_sheets_import?msg=${errMsg}&type=danger`);
}
});
// 2. Occupancy Metrics
const handleOccupancyMetrics = async (req, res) => {
try {
+1 -1
View File
@@ -3,7 +3,7 @@ const axios = require('axios');
// Configuración base de Axios conectando hacia Flask
const apiClient = axios.create({
baseURL: process.env.FLASK_API_URL || 'http://localhost:5000/api/v1',
timeout: 5000,
timeout: 25000,
headers: {
'Content-Type': 'application/json'
}