Añade empresas desde M1 o M3 para ver el portal.
';
return;
}
sel.innerHTML = emps.map(e => `Cargando informe...
';
// Cargar datos en paralelo
const hoy = new Date();
const primerDiaMes = new Date(hoy.getFullYear(), hoy.getMonth(), 1).toISOString().split('T')[0];
const ultimoDiaMes = new Date(hoy.getFullYear(), hoy.getMonth()+1, 0).toISOString().split('T')[0];
const [sesRes, trabRes, asistRes, histRes] = await Promise.all([
db.from('sesiones').select('*').eq('empresa_id', empresaId).order('fecha', {ascending:false}),
db.from('trabajadores').select('*').eq('empresa_id', empresaId).eq('activo', true).order('nombre'),
db.from('asistencia').select('*, sesiones(fecha,tipo,empresa_id), trabajadores(nombre)').eq('asistio', true),
db.from('historial_empresa').select('*').eq('empresa_id', empresaId).order('created_at', {ascending:false}).limit(10)
]);
const sesiones = sesRes.data || [];
const trabajadores = trabRes.data || [];
const asistencia = (asistRes.data || []).filter(a => a.sesiones?.empresa_id === empresaId);
window._m4._historial = histRes.data || [];
// Métricas del mes actual
const sesionesMes = sesiones.filter(s => s.fecha >= primerDiaMes && s.fecha <= ultimoDiaMes && s.estado === 'realizada');
const sesionesTotal = sesiones.filter(s => s.estado === 'realizada').length;
const sesionesProg = Math.round((emp.sesiones_semana || 2) * 4.33);
// Asistencia media por sesión
const asistPorSesion = {};
asistencia.forEach(a => {
if (!asistPorSesion[a.sesion_id]) asistPorSesion[a.sesion_id] = 0;
asistPorSesion[a.sesion_id]++;
});
const asistVals = Object.values(asistPorSesion);
const asistMedia = asistVals.length > 0 ? Math.round(asistVals.reduce((s,v)=>s+v,0)/asistVals.length) : 0;
const asistPct = trabajadores.length > 0 && asistMedia > 0 ? Math.round(asistMedia/trabajadores.length*100) : 0;
// Asistencia por trabajador
const asistPorTrab = {};
asistencia.forEach(a => {
const tid = a.trabajador_id;
if (!asistPorTrab[tid]) asistPorTrab[tid] = 0;
asistPorTrab[tid]++;
});
const trabConAsist = trabajadores.map(t => ({
...t,
sesiones: asistPorTrab[t.id] || 0,
pct: sesionesTotal > 0 ? Math.min(100, Math.round((asistPorTrab[t.id]||0)/sesionesTotal*100)) : 0
})).sort((a,b) => b.pct - a.pct);
// Sesiones por tipo
const porTipo = {};
sesiones.filter(s=>s.estado==='realizada').forEach(s => {
porTipo[s.tipo] = (porTipo[s.tipo]||0) + 1;
});
// Datos para gráfica — últimos 6 meses
const meses = [];
const sesionesPorMes = [];
const asistPorMes = [];
for (let i = 5; i >= 0; i--) {
const d = new Date(hoy.getFullYear(), hoy.getMonth()-i, 1);
const inicio = d.toISOString().split('T')[0];
const fin = new Date(d.getFullYear(), d.getMonth()+1, 0).toISOString().split('T')[0];
meses.push(d.toLocaleDateString('es-ES',{month:'short'}));
const sesMes = sesiones.filter(s => s.fecha >= inicio && s.fecha <= fin && s.estado === 'realizada');
sesionesPorMes.push(sesMes.length);
// Asistencia % ese mes
const asistMes = asistencia.filter(a => a.sesiones?.fecha >= inicio && a.sesiones?.fecha <= fin);
const asistMesPorSes = {};
asistMes.forEach(a => { asistMesPorSes[a.sesion_id] = (asistMesPorSes[a.sesion_id]||0)+1; });
const vals = Object.values(asistMesPorSes);
const pctMes = vals.length > 0 && trabajadores.length > 0
? Math.round((vals.reduce((s,v)=>s+v,0)/vals.length)/trabajadores.length*100)
: 0;
asistPorMes.push(pctMes);
}
document.getElementById('m4-content').innerHTML = `
Próximas sesiones
${(() => {
const hoyStr = new Date().toISOString().split('T')[0];
const proxSes = sesiones.filter(s=>s.fecha>=hoyStr&&s.empresa_id===empresaId).sort((a,b)=>a.fecha.localeCompare(b.fecha)||a.hora.localeCompare(b.hora)).slice(0,4);
if (proxSes.length===0) return `
Sin sesiones programadas próximamente.
`;
return proxSes.map(s=>{
const f=new Date(s.fecha+'T12:00:00').toLocaleDateString('es-ES',{weekday:'short',day:'numeric',month:'short'});
const tipoLbl=s.tipo?s.tipo.charAt(0).toUpperCase()+s.tipo.slice(1):'Sesión';
return `
${f} · ${s.hora?.slice(0,5)||''}
${tipoLbl} · ${s.duracion_min||60}min
Programada `;
}).join('');
})()}
Asistencia por empleado
${trabConAsist.length === 0
? `
Sin trabajadores registrados. Añádelos desde M1.
`
: trabConAsist.slice(0,8).map(t => `
${t.nombre}
${t.email||'Sin email'}
`).join('')
}
Sesiones por tipo
${Object.keys(porTipo).length === 0
? `
Sin sesiones registradas todavía.
`
: Object.entries(porTipo).sort((a,b)=>b[1]-a[1]).map(([tipo, n]) => {
const tipoLbl = tipo.charAt(0).toUpperCase()+tipo.slice(1);
return `
${tipoLbl}
${n} sesion${n!==1?'es':''}
Realizada${n!==1?'s':''}
`;
}).join('')
}
${sesionesTotal > 0 ? `
Nota: ${sesionesTotal} sesiones realizadas desde el inicio del contrato. ${asistPct >= 70 ? 'Excelente nivel de participación.' : 'Hay margen de mejora en la asistencia.'}
` : ''}
Ingresos automáticos — contratos activos
`;
window._init_m7 = async function() {
window._m7 = { chart: null };
document.getElementById('m7-g-fecha').value = new Date().toISOString().split('T')[0];
document.getElementById('m7-i-fecha').value = new Date().toISOString().split('T')[0];
await m7Load();
};
async function m7Load() {
const [gastosRes, extrasRes, empRes] = await Promise.all([
db.from('gastos').select('*').order('fecha', {ascending:false}),
db.from('ingresos_extra').select('*').order('fecha', {ascending:false}),
db.from('empresas').select('nombre,importe_total,paquete,estado_pago,fecha_inicio').eq('activa',true)
]);
const gastos = gastosRes.data||[];
const extras = extrasRes.data||[];
const empresas = empRes.data||[];
// Ingresos automáticos = suma de contratos
const ingresosAuto = empresas.reduce((s,e)=>s+(e.importe_total||0),0);
const ingresosExtra = extras.reduce((s,i)=>s+(i.importe||0),0);
const totalIngresos = ingresosAuto + ingresosExtra;
const totalGastos = gastos.reduce((s,g)=>s+(g.importe||0),0);
const margen = totalIngresos - totalGastos;
// Métricas
const metricsEl = document.getElementById('m7-metrics');
if (metricsEl) metricsEl.innerHTML = `
Sin empresas activas — los ingresos de contratos aparecen aquí automáticamente.
';
} else {
autoEl.innerHTML = `
Sin gastos registrados todavía.
'; return; }
const catLabel = {material_deportivo:'Material',desplazamientos:'Desplazamientos',software:'Software',formacion:'Formación',gestoria:'Gestoría',marketing:'Marketing',administrativo:'Administrativo',otro:'Otro'};
// Agrupar por mes
const porMes = {};
gastos.forEach(g=>{
const mes=g.fecha.slice(0,7);
if(!porMes[mes])porMes[mes]=[];
porMes[mes].push(g);
});
el.innerHTML = Object.entries(porMes).map(([mes,items])=>{
const total=items.reduce((s,g)=>s+(g.importe||0),0);
const fechaMes=new Date(mes+'-01').toLocaleDateString('es-ES',{month:'long',year:'numeric'});
return `Sin ingresos manuales registrados.
'; return; }
const catLabel={subvencion:'Subvención',formacion:'Formación',consultoria:'Consultoría',otro:'Otro'};
el.innerHTML = extras.map(i=>`Cargando...
';
let query = db.from('documentos').select('*').order('created_at', {ascending:false});
if (empresaId) query = query.eq('empresa_id', empresaId);
else query = query.is('empresa_id', null);
const {data:docs} = await query;
if (!docs||docs.length===0) {
el.innerHTML = `${docs.map(d=>{
const ico = iconos[d.tipo_mime]||'📎';
const tamaño = d.tamaño_bytes ? (d.tamaño_bytes>1024*1024?(d.tamaño_bytes/1024/1024).toFixed(1)+'MB':(d.tamaño_bytes/1024).toFixed(0)+'KB') : '—';
const fecha = new Date(d.created_at).toLocaleDateString('es-ES',{day:'numeric',month:'short',year:'numeric'});
return `
${ico}
${d.nombre}
${catLabel[d.categoria]||'—'} · ${tamaño} · ${fecha} · ${d.subido_por||'FitCorp'}
${d.visible_rrhh?'Visible RRHH':'Privado'}
Descargar
Eliminar
`;
}).join('')}
`;
}
function m8HandleFiles(files) {
if (!files||files.length===0) return;
window._m8.pendingFiles = files;
const panel = document.getElementById('m8-upload-panel');
const pending = document.getElementById('m8-files-pending');
if (panel) panel.style.display='block';
if (pending) pending.textContent = `${files.length} archivo${files.length!==1?'s':''} listo${files.length!==1?'s':''} para subir: ${Array.from(files).map(f=>f.name).join(', ')}`;
}
function m8CancelUpload() {
window._m8.pendingFiles = null;
const panel = document.getElementById('m8-upload-panel');
if (panel) panel.style.display='none';
document.getElementById('m8-fileinput').value='';
}
async function m8Upload() {
const files = window._m8.pendingFiles;
if (!files||files.length===0) return;
const categoria = document.getElementById('m8-categoria').value;
const visibleRrhh = document.getElementById('m8-visible-rrhh').checked;
const email = window.STATE?.currentUser?.email||'jaime@fitcorp.es';
const progress = document.getElementById('m8-progress');
const fill = document.getElementById('m8-progressfill');
if (progress) progress.style.display='block';
let subidos=0;
for (const file of Array.from(files)) {
const ext = file.name.split('.').pop();
const path = `${window._m8.currentEmpresaId||'general'}/${Date.now()}_${file.name.replace(/[^a-zA-Z0-9._-]/g,'_')}`;
// Subir a Supabase Storage
const {error:upErr} = await db.storage.from('documentos').upload(path, file, {contentType:file.type});
if (!upErr) {
// Guardar metadatos en tabla documentos
await db.from('documentos').insert({
nombre: file.name,
storage_path: path,
tipo_mime: file.type,
tamaño_bytes: file.size,
empresa_id: window._m8.currentEmpresaId||null,
categoria,
visible_rrhh: visibleRrhh,
subido_por: email
});
}
subidos++;
if (fill) fill.style.width=Math.round(subidos/files.length*100)+'%';
}
m8Toast(`✓ ${subidos} archivo${subidos!==1?'s':''} subido${subidos!==1?'s':''}`, 'ok');
m8CancelUpload();
await m8LoadDocs(window._m8.currentEmpresaId);
}
async function m8Download(path, nombre) {
const {data, error} = await db.storage.from('documentos').download(path);
if (error||!data) { m8Toast('Error al descargar el archivo','err'); return; }
const url = URL.createObjectURL(data);
const a = document.createElement('a');
a.href=url; a.download=nombre; a.click();
URL.revokeObjectURL(url);
}
async function m8Delete(id, path) {
await db.storage.from('documentos').remove([path]);
await db.from('documentos').delete().eq('id',id);
m8Toast('Archivo eliminado','ok');
await m8LoadDocs(window._m8.currentEmpresaId);
}
function m8Toast(msg,type){
const t=document.getElementById('m8-toast');
if(!t)return;
t.textContent=msg;t.className='m8-toast '+type;t.style.display='block';
setTimeout(()=>{t.style.display='none';},3500);
}
// ── INIT ──
document.addEventListener('DOMContentLoaded', async () => {
document.getElementById('screen-login').style.display = 'flex';
document.getElementById('module-container').style.display = 'none';
testConnection();
// Comprobar si ya hay sesión activa (token guardado en el navegador)
const { data: { session } } = await db.auth.getSession();
if (session?.user) {
// Hay sesión activa — buscar rol y entrar directamente
const { data: userData } = await db.from('usuarios').select('*').eq('email', session.user.email).single();
if (userData) {
const roleLabels = { admin:'Administrador', trainer:'Entrenador', company:'RRHH Empresa', worker:'Empleado' };
const user = {
id: userData.id,
auth_id: session.user.id,
email: userData.email,
name: userData.nombre,
avatar: userData.nombre.split(' ').slice(0,2).map(w=>w[0]).join('').toUpperCase(),
role: userData.rol,
roleLabel: roleLabels[userData.rol] || userData.rol,
empresa_id: userData.empresa_id || null
};
enterApp(user);
return;
}
}
// Sin sesión — mostrar login
document.getElementById('screen-login').style.display = 'flex';
});
window.fitcorpNavigate = loadModule;
window.sendPrompt = (msg) => {
if (msg.includes('sesión') || msg.includes('registrar')) loadModule('m5');
else if (msg.includes('empresa')) loadModule('m1');
};