const UiIconAdmin = ({ name, className = "w-5 h-5" }) => {
const icons = {
'dashboard': ,
'users': ,
'contracts': ,
'shield': ,
'file-text': ,
'bell': ,
'dollar': ,
'sun': ,
'x-circle': ,
'settings':
};
return icons[name] || null;
};
window.AdminDashboard = function({
currentUser,
globalUsers = [],
logs = [],
roles = [],
reminders = [],
vacations = [],
settings = {},
actions = [],
absences = [],
masterClients = [],
activeClientId,
setActiveClientId,
refreshData,
setView,
onLogout
}) {
const isMaster = window.SHIFT_CONFIG && window.SHIFT_CONFIG.isMaster;
// Inicia siempre en Auditoría ('logs') por defecto
const [tab, setTab] = React.useState(isMaster && activeClientId === 0 ? 'master' : 'logs');
const [toastMsg, ReactSetToastMsg] = React.useState('');
const [isPunching, setIsPunching] = React.useState(false);
// Identificar permisos de Sólo Lectura
const currentRoleObj = (roles || []).find(r => (r.name || '').toLowerCase() === (currentUser?.role || currentUser?.role_name || '').toLowerCase());
const isReadOnly = (currentUser?.accessLevel === 'readonly' || currentUser?.accessLevel === 'read_only' || currentRoleObj?.accessLevel === 'readonly' || currentRoleObj?.accessLevel === 'read_only');
const [confirmModal, setConfirmModal] = React.useState(null);
const [previewImage, setPreviewImage] = React.useState(null);
const [showClientModal, setShowClientModal] = React.useState(false);
const [editingClient, setEditingClient] = React.useState(null);
const [mapLogs, setMapLogs] = React.useState(null);
const mapContainerRef = React.useRef(null);
const mapInstanceRef = React.useRef(null);
const liveMarkersRef = React.useRef({});
const [editingUser, setEditingUser] = React.useState(null);
const [companyLogoUrl, setCompanyLogoUrl] = React.useState(settings?.logoUrl || '');
const [showCompanyLogo, setShowCompanyLogo] = React.useState(settings?.showCompanyLogo !== false);
const [inssRate, setInssRate] = React.useState(settings?.inssPatronalRate || 22.5);
const [lunchLimitMinutes, setLunchLimitMinutes] = React.useState(settings?.lunchLimitMinutes || 60);
const [gracePeriodMinutes, setGracePeriodMinutes] = React.useState(settings?.gracePeriodMinutes || 5);
const [maxTardinessAllowed, setMaxTardinessAllowed] = React.useState(settings?.maxTardinessAllowed || 3);
const [tardinessPenaltyAction, setTardinessPenaltyAction] = React.useState(settings?.tardinessPenaltyAction || 'deduct_minutes');
const [reasonsList, setReasonsList] = React.useState(settings?.exceptionReasons || ['Horas extras autorizadas', 'Solicitud de gerencia', 'Cobertura de turno especial']);
const [newReasonInput, setNewReasonInput] = React.useState('');
const [vacRate, setVacRate] = React.useState(settings?.vacationRate || 2.5);
const [selectedUserVacation, setSelectedUserVacation] = React.useState(null);
const showToast = (msg) => { ReactSetToastMsg(msg); setTimeout(() => ReactSetToastMsg(''), 3000); };
const confirmAction = (message, action) => {
if (isReadOnly) return alert("Modo supervisión: no tienes permisos para realizar esta acción.");
setConfirmModal({ message, action });
};
// Último estado de marcaje del Administrador hoy
const todayAdminLogs = React.useMemo(() => {
const todayStr = window.getNICDate ? window.getNICDate() : new Date().toISOString().split('T')[0];
return (logs || []).filter(l => Number(l.userId) === Number(currentUser?.id) && l.date === todayStr);
}, [logs, currentUser]);
const lastAdminLog = todayAdminLogs.length > 0 ? todayAdminLogs[0] : null;
const isCurrentlyCheckedIn = lastAdminLog && lastAdminLog.type !== 'salida';
// MARCAJE RÁPIDO PARA ADMINS/SUPERVISORES
const handleQuickPunch = async (punchType) => {
setIsPunching(true);
let coords = { lat: 0, lng: 0 };
try {
if (navigator.geolocation) {
const pos = await new Promise((resolve) => {
navigator.geolocation.getCurrentPosition(resolve, () => resolve(null), { timeout: 4000 });
});
if (pos && pos.coords) {
coords = { lat: pos.coords.latitude, lng: pos.coords.longitude };
}
}
} catch (eGeo) {}
const now = Date.now();
const dateStr = window.getNICDate ? window.getNICDate() : new Date().toISOString().split('T')[0];
try {
const res = await window.apiCall('save_log', {
userId: currentUser.id,
type: punchType,
timestamp: now,
date: dateStr,
location: coords,
client: 'Marcaje Rápido',
justification: null
});
if (res && res.success) {
showToast(`¡${punchType.toUpperCase()} registrada con éxito!`);
if (refreshData) await refreshData();
} else {
showToast("Error al registrar marcaje: " + (res?.error || ""));
}
} catch (err) {
showToast("Error de conexión al marcar.");
} finally {
setIsPunching(false);
}
};
React.useEffect(() => {
setCompanyLogoUrl(settings?.logoUrl || '');
setShowCompanyLogo(settings?.showCompanyLogo !== false);
setInssRate(settings?.inssPatronalRate || 22.5);
setLunchLimitMinutes(settings?.lunchLimitMinutes || 60);
setGracePeriodMinutes(settings?.gracePeriodMinutes || 5);
setMaxTardinessAllowed(settings?.maxTardinessAllowed || 3);
setTardinessPenaltyAction(settings?.tardinessPenaltyAction || 'deduct_minutes');
setReasonsList(settings?.exceptionReasons || ['Horas extras autorizadas', 'Solicitud de gerencia', 'Cobertura de turno especial']);
setVacRate(settings?.vacationRate || 2.5);
}, [settings]);
// MAPA GPS
React.useEffect(() => {
if (!mapLogs || mapLogs.length === 0 || !mapContainerRef.current) return;
if (mapInstanceRef.current) {
mapInstanceRef.current.remove();
mapInstanceRef.current = null;
}
liveMarkersRef.current = {};
const validCoords = mapLogs.filter(l => l.location && l.location.lat && l.location.lng);
if (validCoords.length === 0) {
showToast("Los registros seleccionados no contienen coordenadas GPS.");
setMapLogs(null);
return;
}
const initialPoint = [validCoords[0].location.lat, validCoords[0].location.lng];
const map = L.map(mapContainerRef.current).setView(initialPoint, 14);
mapInstanceRef.current = map;
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
}).addTo(map);
const latLngs = [];
const sortedPoints = [...validCoords].sort((a, b) => a.timestamp - b.timestamp);
sortedPoints.forEach((log, index) => {
const pos = [log.location.lat, log.location.lng];
latLngs.push(pos);
const isStart = log.type === 'entrada';
const isEnd = log.type === 'salida';
const color = isStart ? '#10b981' : isEnd ? '#ef4444' : '#6366f1';
const userInitial = (log.userName || 'U')[0].toUpperCase();
const avatarHtml = log.avatarUrl
? `

`
: `${userInitial}
`;
const customIcon = L.divIcon({
html: avatarHtml,
className: 'custom-avatar-marker',
iconSize: [38, 38],
iconAnchor: [19, 19]
});
const marker = L.marker(pos, { icon: customIcon }).addTo(map);
const timeStr = window.formatNICTimeFull ? window.formatNICTimeFull(log.timestamp) : new Date(log.timestamp).toLocaleTimeString();
marker.bindPopup(`
${log.userName || 'Colaborador'}
#${index + 1} • ${log.type.toUpperCase()}
Hora: ${timeStr}
`);
liveMarkersRef.current[log.id] = marker;
});
if (latLngs.length > 1) {
L.polyline(latLngs, { color: '#4f46e5', weight: 4, opacity: 0.75, dashArray: '8, 8' }).addTo(map);
map.fitBounds(L.latLngBounds(latLngs), { padding: [40, 40] });
}
setTimeout(() => {
if (mapInstanceRef.current) mapInstanceRef.current.invalidateSize();
}, 300);
}, [mapLogs]);
const handlePortalChange = (newId) => {
const idNum = Number(newId);
if (setActiveClientId) setActiveClientId(idNum);
if (idNum === 0) setTab('master');
else if (tab === 'master') setTab('logs');
};
const handleUserSubmit = async (e) => {
e.preventDefault();
if (isReadOnly) return showToast("Modo supervisión: no tienes permisos de edición.");
const f = new FormData(e.target);
const email = f.get('email').toLowerCase().trim();
const aliasCode = f.get('alias').trim();
if (!editingUser) {
const existingEmail = globalUsers.find(u => u.email === email);
if (existingEmail) return showToast(`ADVERTENCIA: Correo en uso por "${existingEmail.name}"`);
}
const data = {
id: editingUser ? editingUser.id : null,
name: f.get('name'),
alias: aliasCode,
email,
password: f.get('pass'),
role: f.get('role'),
status: f.get('status') || 'activo',
hiringDate: f.get('date') || window.getNICDate(),
vacationDaysUsed: parseFloat(f.get('vused')) || 0,
scheduleDays: JSON.parse(f.get('scheduleDays') || '["lun","mar","mie","jue","vie"]'),
scheduleStart: f.get('schedule_start') || '08:00',
scheduleEnd: f.get('schedule_end') || '17:00',
scheduleCustom: JSON.parse(f.get('scheduleCustom') || '{}'),
hasInss: f.get('hasInss') === '1' || f.get('hasInss') === 1,
inssNumber: f.get('inssNumber') || null,
baseSalary: parseFloat(f.get('base_salary')) || 0,
appliesInss: f.get('applies_inss') ? 1 : 0,
appliesIr: f.get('applies_ir') ? 1 : 0,
gpsTrackingEnabled: f.get('gps_tracking_enabled') ? 1 : 0,
avatar_base64: f.get('avatar_base64') || null,
idCard: f.get('id_card') || null,
civilStatus: f.get('civil_status') || 'Soltero(a)',
profession: f.get('profession') || 'Trabajador(a)',
homeAddress: f.get('home_address') || null,
positionTitle: f.get('position_title') || null,
paymentFrequency: f.get('payment_frequency') || 'Quincenal',
overrideCheckOut: editingUser ? (editingUser.overrideCheckOut || false) : false,
manualLock: editingUser ? (editingUser.manualLock || false) : false
};
try {
const res = await window.apiCall('save_user', data);
if(res.success) {
setEditingUser(null);
showToast("Usuario guardado exitosamente.");
if (refreshData) await refreshData();
} else {
showToast("Error al guardar: " + (res.error || "Desconocido"));
}
} catch (err) { showToast("Error de conexión al guardar usuario."); }
};
const deleteUser = async (id) => {
if (isReadOnly) return showToast("Modo supervisión: no tienes permisos para eliminar.");
await window.apiCall('delete_user', { id });
setConfirmModal(null); showToast("Usuario borrado.");
if (refreshData) await refreshData();
};
const toggleLock = async (u, lockStatus) => {
if (isReadOnly) return showToast("Modo supervisión: no tienes permisos de bloqueo.");
await window.apiCall('toggle_user_lock', { id: u.id, manualLock: lockStatus, overrideCheckOut: !lockStatus });
showToast(lockStatus ? "Bloqueado." : "Desbloqueado.");
if (refreshData) await refreshData();
};
const saveCompanySettings = async (extraPayload = {}) => {
if (isReadOnly) return showToast("Modo supervisión: no tienes permisos de configuración.");
const res = await window.apiCall('update_settings', {
vacation_rate: parseFloat(vacRate) || 2.5,
exception_reasons: reasonsList,
inss_patronal_rate: parseFloat(inssRate) || 22.5,
lunch_limit_minutes: parseInt(lunchLimitMinutes) || 60,
grace_period_minutes: parseInt(gracePeriodMinutes) || 5,
max_tardiness_allowed: parseInt(maxTardinessAllowed) || 3,
tardiness_penalty_action: tardinessPenaltyAction || 'deduct_minutes',
show_company_logo: showCompanyLogo ? 1 : 0,
...extraPayload
});
if (res.success && res.logo_url) {
setCompanyLogoUrl(res.logo_url);
}
showToast("Configuración de empresa actualizada.");
if (refreshData) await refreshData();
};
const exportPayrollReport = () => {
const headers = ["ID", "Empleado", "Correo", "Salario Base (C$)", "Faltas Deducidas", "Días Efectivos", "Salario Bruto", "INSS Laboral (7%)", "IR DGI", "Salario Neto", "INSS Patronal"];
const rows = globalUsers.map(u => {
const calc = window.calculatePayrollRow(u, 30, 0, settings?.inssPatronalRate || 22.5, absences);
return [u.id, u.name, u.email, u.baseSalary || 0, calc.deductedDays, calc.effectiveWorkedDays, calc.grossSalary, calc.inssLaboral, calc.irDGI, calc.netSalary, calc.inssPatronal];
});
window.exportToCSV(`Planilla_${window.SHIFT_CONFIG?.companyName || 'Empresa'}_${window.getNICDate()}`, headers, rows);
};
const setVacationStatus = async (vId, status) => {
if (isReadOnly) return showToast("Modo supervisión: no puedes cambiar estados.");
await window.apiCall('update_vacation_status', { id: vId, status });
showToast(`Actualizado a: ${status}`);
if (refreshData) await refreshData();
};
const revokeEntireVacation = async (vac) => {
if (isReadOnly) return showToast("Modo supervisión: no tienes permisos de revocación.");
confirmAction(`¿Revocar vacaciones de ${vac.userName}? Se reintegrarán sus días.`, async () => {
await window.apiCall('delete_vacation', { id: vac.id });
setConfirmModal(null);
showToast("Vacaciones revocadas con éxito.");
if (refreshData) await refreshData();
});
};
const calculateUsedDaysBreakdown = (user) => {
if (!user) return 0;
const manualUsed = parseFloat(user.vacationDaysUsed) || 0;
const systemApproved = (vacations || [])
.filter(v => v.userId === user.id && v.status === 'approved')
.reduce((acc, v) => {
if (v.type === 'paid') return acc + (v.paidDays || 0);
const totalReq = v.type === 'half' ? 0.5 : window.getDaysDiff(v.startDate, v.endDate || v.startDate);
const revoked = v.revokedDates ? v.revokedDates.length : 0;
return acc + Math.max(0, totalReq - revoked);
}, 0);
return (manualUsed + systemApproved).toFixed(2);
};
const renderNavItems = () => {
if (isMaster && activeClientId === 0) {
return (
);
}
return (
{!isReadOnly && (
)}
{!isReadOnly && (
)}
);
};
return (
{toastMsg &&
{toastMsg}
}
{previewImage && (
setPreviewImage(null)}>
e.stopPropagation()}>
)}
{mapLogs && (
setMapLogs(null)}>
e.stopPropagation()}>
Ruta y Marcajes GPS
{mapLogs.length} punto(s) registrado(s) • Monitoreo interactivo
)}
{confirmModal && (
setConfirmModal(null)}>
e.stopPropagation()}>
Confirmación
{confirmModal.message}
)}
{selectedUserVacation && (
setSelectedUserVacation(null)}>
e.stopPropagation()}>
{selectedUserVacation.name}
REPORTE VACACIONAL • INICIO: {window.formatDateToNIC(selectedUserVacation.hiringDate)}
Acumuladas Ley
{window.calculateVacationAccrued(selectedUserVacation, settings?.vacationRate||2.5).toFixed(2)}
Gozados / Aprobados
{calculateUsedDaysBreakdown(selectedUserVacation)}
Saldo Disponible
{window.calculateVacationBalance(selectedUserVacation, settings?.vacationRate||2.5, vacations)}
)}
{/* BARRA LATERAL CON MARCAJE RÁPIDO INTEGRADO */}
{tab === 'master' && isMaster && window.AdminSaaS && (
)}
{tab === 'logs' && window.AdminAuditoria && (
)}
{tab === 'users' && window.AdminPersonal && (
)}
{tab === 'contracts' && window.AdminContratos && (
)}
{tab === 'notices' && window.AdminAvisos && (
)}
{tab === 'payroll' && window.AdminPlanilla && (
)}
{tab === 'vacations' && window.AdminVacaciones && (
{ setVacRate(rate); saveCompanySettings(); }}
setVacationStatus={setVacationStatus} revokeEntireVacation={revokeEntireVacation} setSelectedUserVacation={setSelectedUserVacation}
isReadOnly={isReadOnly}
/>
)}
{tab === 'absences' && window.AdminAusencias && (
{}} confirmAction={confirmAction} isReadOnly={isReadOnly} showToast={showToast} />
)}
{!isReadOnly && tab === 'roles' && window.AdminRoles && (
)}
{!isReadOnly && tab === 'settings' && window.AdminAjustes && (
)}
{/* ASISTENTE INTELIGENTE COPILOT AI (WIDGET FLOTANTE MULTI-TENANT) */}
{window.AdminAICopilot && (
)}
);
};