// fichaje.eu — Screen: Organización (admin: employees + permissions)
const ScreenOrganizacion = ({ role }) => {
const [view, setView] = React.useState('list');
const [createOpen, setCreateOpen] = React.useState(false);
const [editTarget, setEditTarget] = React.useState(null);
return (
e.stopPropagation()} style={{ background: '#fff', borderRadius: 'var(--r-xl)', width: 760, maxHeight: '92vh', display: 'flex', flexDirection: 'column', boxShadow: 'var(--shadow-lg)' }}>
Crear nuevo empleado
Se enviará un email de bienvenida con sus accesos cuando termines.
{[
{ n: 1, l: 'Datos básicos' },
{ n: 2, l: 'Contrato y horario' },
{ n: 3, l: 'Vacaciones' },
{ n: 4, l: 'Permisos' },
].map((s, i, arr) => (
= s.n ? 'var(--brand-600)' : 'var(--slate-150)',
color: step >= s.n ? '#fff' : 'var(--slate-500)',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
fontSize: 11, fontWeight: 700,
}}>{step > s.n ? : s.n}
{s.l}
{i < arr.length - 1 && s.n ? 'var(--brand-300)' : 'var(--slate-150)', alignSelf: 'center' }}/>}
))}
{step === 1 && }
{step === 2 && }
{step === 3 && }
{step === 4 && }
Paso {step} de 4
{err &&
{err}
}
Cancelar
{step > 1 && setStep(step - 1)} icon={} disabled={busy}>Atrás }
{step < 4 ? (
} onClick={() => setStep(step + 1)}>Siguiente
) : (
} onClick={submit} disabled={busy}>{busy ? 'Creando…' : 'Crear empleado'}
)}
);
};
const Step1Basics = ({ form, set }) => (
);
const Step2Contract = ({ form, set }) => (
);
const Step3Vacation = ({ form, set }) => {
const vacationDays = form.vacation_days;
const personalDays = form.personal_days;
const setVacationDays = (v) => set('vacation_days', v);
const setPersonalDays = (v) => set('personal_days', v);
const [proRata, setProRata] = React.useState(true);
return (
Por convenio Consultoría TIC , lo habitual son 23 días laborables de vacaciones + 2 días de asuntos propios al año. Aplicaremos el prorrateo según fecha de inicio.
}
tone="brand"
title="Vacaciones anuales"
subtitle="Días laborables que le pertenecen al año natural"
value={vacationDays}
onChange={setVacationDays}
unit="días"
presets={[22, 23, 25, 30]}
/>
}
tone="violet"
title="Asuntos propios"
subtitle="Días retribuidos para gestiones personales"
value={personalDays}
onChange={setPersonalDays}
unit="días"
presets={[0, 1, 2, 4]}
/>
Otros permisos retribuidos (configurables)
Prorratear si entra a mitad de año
Empieza el 1 jun → recibe {Math.round(vacationDays * 7 / 12)} días de vacaciones y {Math.round(personalDays * 7 / 12)} de asuntos propios para 2026.
Permitir arrastrar días al año siguiente
Hasta 5 días no consumidos pueden usarse antes del 31 marzo.
);
};
const Step4Permissions = ({ form, set }) => (
{[
{ r: 'Empleado', d: 'Acceso a sus propios datos y fichaje.', i:
},
{ r: 'Manager', d: 'Aprueba ausencias del equipo y ve registros.', i: },
{ r: 'RRHH', d: 'Gestiona personal, documentos y políticas.', i: },
{ r: 'Admin', d: 'Control total del workspace.', i: },
].map(o => {
const active = form.permission_role === o.r;
return (
set('permission_role', o.r)} style={{
padding: 14, textAlign: 'left',
border: `1px solid ${active ? 'var(--brand-500)' : 'var(--slate-200)'}`,
background: active ? 'var(--brand-50)' : '#fff',
borderRadius: 'var(--r-md)',
boxShadow: active ? 'var(--ring-brand)' : 'none',
display: 'flex', flexDirection: 'column', gap: 6, cursor: 'pointer',
}}>
{o.i}
{o.r}
{o.d}
);})}
Onboarding automático
{[
{ l: 'Crear cuenta corporativa', s: 'anna.boixader@iabusiness.es', on: true },
{ l: 'Firmar contrato base de Consultoría TIC', s: 'Plantilla "Indefinido · Jornada completa"', on: true },
{ l: 'Asignar equipo informático', s: 'Solicitud automática a IT', on: true },
{ l: 'Añadir a Slack: #general, #implementacion', s: '', on: true },
{ l: 'Notificar incorporación al equipo', s: 'Email al equipo de Implementación y tablón', on: false },
].map((x, i) => (
))}
);
const selectStyle = { width: '100%', padding: '10px 12px', fontSize: 14, border: '1px solid var(--slate-200)', borderRadius: 'var(--r-md)', background: '#fff' };
const PolicyCounter = ({ icon, tone, title, subtitle, value, onChange, unit, presets }) => {
const tones = {
brand: ['var(--brand-50)', 'var(--brand-600)', 'var(--brand-100)'],
violet: ['var(--violet-100)', '#6D28D9', 'var(--violet-100)'],
}[tone];
return (
onChange(Math.max(0, value - 1))} style={{ width: 32, height: 32, borderRadius: 'var(--r-md)', border: '1px solid var(--slate-200)', background: '#fff', color: 'var(--slate-700)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 18, fontWeight: 700 }}>−
{value}{unit}
onChange(value + 1)} style={{ width: 32, height: 32, borderRadius: 'var(--r-md)', border: '1px solid var(--slate-200)', background: '#fff', color: 'var(--slate-700)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 18, fontWeight: 700 }}>+
{presets.map(p => (
onChange(p)} style={{
flex: 1, padding: '6px 4px',
fontSize: 12, fontWeight: 600,
border: `1px solid ${value === p ? tones[1] : 'var(--slate-200)'}`,
background: value === p ? tones[0] : '#fff',
color: value === p ? tones[1] : 'var(--slate-600)',
borderRadius: 'var(--r-sm)',
}}>{p}
))}
);
};
const MiniField = ({ label, unit, defaultValue, disabled }) => (
);
const EditEmployeeModal = ({ employee, onClose }) => {
const [tab, setTab] = React.useState('datos');
const initialFirst = (employee.name || '').split(' ')[0] || '';
const initialLast = (employee.name || '').split(' ').slice(1).join(' ');
const [form, setForm] = React.useState({
first_name: initialFirst, last_name: initialLast,
email: employee.email || '', phone: '', location: 'Valencia, ES', birth: '',
role_title: employee.role || '', team: employee.team || 'Implementación',
contract: 'Indefinido · Jornada completa', start: '2024-03-04', manager: 'Núria Castells',
schedule: 'L–V · 09:00–18:00 (40h)',
});
const set = (k, v) => setForm(s => ({ ...s, [k]: v }));
const [busy, setBusy] = React.useState(false);
const save = async () => {
if (!employee.id || !window.api) return;
setBusy(true);
await window.api.updateEmployee(employee.id, {
full_name: `${form.first_name} ${form.last_name}`.trim(),
email: form.email, phone: form.phone, location: form.location, birthday: form.birth,
role: form.role_title, team: form.team, manager: form.manager,
contract: form.contract, start_date: form.start, schedule: form.schedule,
});
await window.api.reloadBootstrap();
window.location.reload();
};
const remove = async () => {
if (!employee.id || !window.api) return;
if (!window.confirm(`¿Dar de baja a ${employee.name}? Esta acción no se puede deshacer.`)) return;
setBusy(true);
await window.api.deleteEmployee(employee.id);
await window.api.reloadBootstrap();
window.location.reload();
};
return (
e.stopPropagation()} style={{ background: '#fff', borderRadius: 'var(--r-xl)', width: 780, maxHeight: '92vh', display: 'flex', flexDirection: 'column', boxShadow: 'var(--shadow-lg)' }}>
Editar {employee.name}
{employee.role} · {employee.team} · {employee.email}
}>Ver perfil
{[
{ v: 'datos', l: 'Datos' },
{ v: 'contrato', l: 'Contrato' },
{ v: 'vacaciones', l: 'Vacaciones y permisos' },
{ v: 'permisos', l: 'Rol y accesos' },
].map(t => (
setTab(t.v)} style={{
padding: '10px 14px',
fontSize: 13.5, fontWeight: 600,
color: tab === t.v ? 'var(--brand-600)' : 'var(--slate-600)',
borderBottom: `2px solid ${tab === t.v ? 'var(--brand-600)' : 'transparent'}`,
marginBottom: -1,
}}>{t.l}
))}
{tab === 'vacaciones' &&
}
{tab === 'datos' && (
set('first_name', e.target.value)}/>
set('last_name', e.target.value)}/>
set('email', e.target.value)} icon={}/>
set('phone', e.target.value)}/>
set('birth', e.target.value)}/>
set('location', e.target.value)}/>
)}
{tab === 'contrato' && (
set('role_title', e.target.value)}/>
set('team', e.target.value)}>
Implementación Plataforma Comercial Marketing RRHH Dirección
set('contract', e.target.value)}>
Indefinido · Jornada completa Indefinido · Media jornada Temporal · Jornada completa
set('start', e.target.value)}/>
set('manager', e.target.value)}/>
set('schedule', e.target.value)}>
L–V · 09:00–18:00 (40h) L–V · 08:00–15:00 (35h)
)}
{tab === 'permisos' && (
)}
} style={{ color: 'var(--danger-600)' }} onClick={remove} disabled={busy}>Dar de baja
Cancelar
} onClick={save} disabled={busy}>{busy ? 'Guardando…' : 'Guardar cambios'}
);
};
const EditVacationTab = ({ employee }) => {
const [vacationTotal, setVacationTotal] = React.useState(23);
const [personalTotal, setPersonalTotal] = React.useState(2);
const used = 7, usedPersonal = 1;
return (
Vacaciones — saldo actual 2026
{vacationTotal - used} / {vacationTotal} días
{used} usados · {vacationTotal - used} disponibles
Asuntos propios — saldo actual
{personalTotal - usedPersonal} / {personalTotal} días
{usedPersonal} usado · {personalTotal - usedPersonal} disponible
Asignación anual
} tone="brand" title="Vacaciones anuales"
subtitle="Días laborables por año natural"
value={vacationTotal} onChange={setVacationTotal} unit="días" presets={[22, 23, 25, 30]}/>
} tone="violet" title="Asuntos propios"
subtitle="Días retribuidos según convenio"
value={personalTotal} onChange={setPersonalTotal} unit="días" presets={[0, 1, 2, 4]}/>
Si cambias la asignación a mitad de año, el saldo actual se recalculará. Si reduces por debajo de los días ya consumidos ({used} usados), aparecerá saldo negativo y deberás justificar el ajuste.
Ajuste manual del saldo
}>Añadir ajuste
{[
{ d: '01 ene 2026', t: 'Saldo inicial 2026', v: '+23 días', by: 'Automático', tone: 'slate' },
{ d: '12 feb 2026', t: 'Arrastre días 2025', v: '+3 días', by: 'Helena Prats', tone: 'success' },
{ d: '20 abr 2026', t: 'Día compensación trabajo finde', v: '+1 día', by: 'Helena Prats', tone: 'success' },
{ d: 'mar–may', t: 'Vacaciones consumidas', v: '−7 días', by: 'Solicitudes aprobadas', tone: 'danger' },
].map((r, i, arr) => (
))}
);
};
const EmployeeAccessPanel = ({ employee }) => {
const [loading, setLoading] = React.useState(true);
const [hasUser, setHasUser] = React.useState(false);
const [username, setUsername] = React.useState('');
const [role, setRole] = React.useState('empleado');
const [password, setPassword] = React.useState('');
const [confirm, setConfirm] = React.useState('');
const [busy, setBusy] = React.useState(false);
const [msg, setMsg] = React.useState(null);
React.useEffect(() => {
let cancel = false;
(async () => {
if (!employee || !employee.id || !window.api) { setLoading(false); return; }
const r = await window.api.getEmployeeAccess(employee.id);
if (cancel) return;
if (r && !r.error) {
setHasUser(!!r.has_user);
setUsername(r.username || r.suggested_username || '');
setRole(r.role || 'empleado');
}
setLoading(false);
})();
return () => { cancel = true; };
}, [employee && employee.id]);
const save = async () => {
setMsg(null);
if (!password || password.length < 6) {
setMsg({ type: 'error', text: 'La contraseña debe tener al menos 6 caracteres.' });
return;
}
if (password !== confirm) {
setMsg({ type: 'error', text: 'Las contraseñas no coinciden.' });
return;
}
setBusy(true);
const r = await window.api.setEmployeeAccess(employee.id, { username, password, role });
setBusy(false);
if (r && r.error) {
setMsg({ type: 'error', text: r.error });
} else {
setHasUser(true);
setUsername(r.username || username);
setPassword(''); setConfirm('');
setMsg({ type: 'ok', text: r.created
? `Acceso creado. Login: empresa + «${r.username}» + nueva contraseña.`
: `Contraseña actualizada para «${r.username}».` });
}
};
if (loading) return
Cargando acceso…
;
return (
);
};
Object.assign(window, { ScreenOrganizacion });