const { useState, useEffect, useRef, useMemo } = React;
const AdminIcon = ({ name, className = "w-6 h-6" }) => {
const icons = {
'map-pin': ,
'x': ,
'chevron-left': ,
'chevron-right': ,
'edit': ,
'plus': ,
'switch':
};
return icons[name] || null;
};
function RouteMapModal({ logs, onClose }) {
const mapRef = useRef(null);
const mapInstance = useRef(null);
useEffect(() => {
const loadLeaflet = async () => {
if (!window.L) {
const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'; document.head.appendChild(link);
const script = document.createElement('script'); script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'; document.head.appendChild(script);
await new Promise(r => script.onload = r);
}
initMap();
};
loadLeaflet();
return () => { if (mapInstance.current) { mapInstance.current.remove(); mapInstance.current = null; } };
}, []);
const initMap = () => {
if (!mapRef.current || !window.L) return;
const validLogs = logs.filter(l => l.location && l.location.lat).sort((a,b) => a.timestamp - b.timestamp);
if (validLogs.length === 0) {
mapRef.current.innerHTML = '
El usuario no registró GPS en este(os) marcaje(s).
';
return;
}
mapInstance.current = window.L.map(mapRef.current).setView([validLogs[0].location.lat, validLogs[0].location.lng], 15);
window.L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap' }).addTo(mapInstance.current);
const latlngs = [];
validLogs.forEach((l) => {
const pt = [l.location.lat, l.location.lng];
latlngs.push(pt);
const time = window.formatNICTimeFull ? window.formatNICTimeFull(l.timestamp).split(', ')[1] : new Date(l.timestamp).toLocaleTimeString();
const color = l.type.includes('entrada') ? '#10b981' : l.type.includes('salida') ? '#ef4444' : '#6366f1';
const iconHtml = `
`;
const customIcon = window.L.divIcon({ html: iconHtml, className: '', iconSize: [16,16], iconAnchor: [8,8] });
window.L.marker(pt, {icon: customIcon}).addTo(mapInstance.current)
.bindPopup(`${l.type.replace('_',' ')} ${time} ${l.client?` Visita a: ${l.client}`:''}
`);
});
if (latlngs.length > 1) {
const polyline = window.L.polyline(latlngs, {color: '#4f46e5', weight: 4, dashArray: '5, 10'}).addTo(mapInstance.current);
mapInstance.current.fitBounds(polyline.getBounds(), { padding: [50, 50] });
}
};
return (
e.stopPropagation()}>
Auditoría Geográfica
{logs.length > 1 ? 'Ruta trazada durante el día' : 'Punto de marcaje exacto'}
);
}
window.AdminDashboard = function({ globalUsers, logs, roles, reminders = [], vacations = [], settings, actions = [], absences = [], masterClients = [], activeClientId, setActiveClientId, refreshData, setView, onLogout }) {
const isMaster = window.SHIFT_CONFIG && window.SHIFT_CONFIG.isMaster;
const [tab, setTab] = useState(isMaster ? 'master' : 'logs');
const [toastMsg, setToastMsg] = useState('');
// SaaS Maestro State
const [showClientModal, setShowClientModal] = useState(false);
const [editingClient, setEditingClient] = useState(null);
// UI Auditoría
const [searchName, setSearchName] = useState('');
const [searchDate, setSearchDate] = useState(window.getNICDate());
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [mapLogs, setMapLogs] = useState(null);
// UI Gestión General
const [editingUser, setEditingUser] = useState(null);
const [userToDelete, setUserToDelete] = useState(null);
const [previewImage, setPreviewImage] = useState(null);
const [confirmModal, setConfirmModal] = useState(null);
// Acciones y Botones
const [editingActionId, setEditingActionId] = useState(null);
const [newActionLabel, setNewActionLabel] = useState('');
// Roles & Avisos
const [editingRoleName, setEditingRoleName] = useState(null);
const [newRoleName, setNewRoleName] = useState('');
const [selectedPermissions, setSelectedPermissions] = useState([]);
const [selectedCameraActions, setSelectedCameraActions] = useState([]);
const [editingReminderId, setEditingReminderId] = useState(null);
const [notifTitle, setNotifTitle] = useState('');
const [notifBody, setNotifBody] = useState('');
const [targetUsers, setTargetUsers] = useState([]);
// Vacaciones
const [vacRate, setVacRate] = useState(settings?.vacationRate || 2.5);
const [selectedUserVacation, setSelectedUserVacation] = useState(null);
const [managingVacation, setManagingVacation] = useState(null);
const [tempRevokedDates, setTempRevokedDates] = useState([]);
const [directVacType, setDirectVacType] = useState('full');
const [directVacStart, setDirectVacStart] = useState('');
const [directVacEnd, setDirectVacEnd] = useState('');
const [directPaidDays, setDirectPaidDays] = useState(1);
const [vacPage, setVacPage] = useState(0);
// Ausencias
const [absenceUser, setAbsenceUser] = useState('');
const [absenceDate, setAbsenceDate] = useState(window.getNICDate());
const [absenceType, setAbsenceType] = useState('injustificada');
const [absenceNotes, setAbsenceNotes] = useState('');
const showToast = (msg) => { setToastMsg(msg); setTimeout(() => setToastMsg(''), 3000); };
const confirmAction = (message, action) => setConfirmModal({ message, action });
const currentCompanyObj = useMemo(() => {
if (!masterClients || masterClients.length === 0) return null;
return masterClients.find(c => c.id === activeClientId);
}, [masterClients, activeClientId]);
const handleSaveClient = async (e) => {
e.preventDefault();
const f = new FormData(e.target);
const payload = {
id: editingClient ? editingClient.id : null,
company_name: f.get('company_name'),
shift_subdomain: f.get('shift_subdomain'),
contact_name: f.get('contact_name'),
contact_email: f.get('contact_email'),
contact_phone: f.get('contact_phone'),
module_shift: f.get('module_shift') ? 1 : 0,
admin_password: f.get('admin_password') || ''
};
const res = await window.apiCall('save_master_client', payload);
if (res.success) {
setShowClientModal(false); setEditingClient(null);
if (refreshData) refreshData();
showToast("Empresa guardada exitosamente.");
} else showToast(res.error || "Error al guardar empresa.");
};
const toggleModule = async (c) => {
await window.apiCall('toggle_client_module', { id: c.id, module_shift: c.module_shift ? 0 : 1 });
if (refreshData) refreshData();
showToast("Estado de módulo actualizado.");
};
const filteredLogs = useMemo(() => {
return logs.filter(l => {
const matchName = (l.userName || '').toLowerCase().includes(searchName.toLowerCase());
let matchDate = true;
if (searchDate) { matchDate = l.date === searchDate; }
else if (startDate && endDate) { matchDate = l.date >= startDate && l.date <= endDate; }
return matchName && matchDate;
}).sort((a,b) => b.timestamp - a.timestamp);
}, [logs, searchName, searchDate, startDate, endDate]);
const handleUserSubmit = async (e) => {
e.preventDefault();
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'),
hiringDate: f.get('date'), vacationDaysUsed: parseFloat(f.get('vused')) || 0,
overrideCheckOut: editingUser ? (editingUser.overrideCheckOut || false) : false,
manualLock: editingUser ? (editingUser.manualLock || false) : false
};
try {
await window.apiCall('save_user', data);
setEditingUser(null); e.target.reset(); showToast("Datos guardados.");
if (refreshData) refreshData();
} catch (err) { showToast("Error de red al guardar usuario."); }
};
const deleteUser = async (id) => {
await window.apiCall('delete_user', { id });
setUserToDelete(null); setConfirmModal(null); showToast("Usuario borrado.");
if (refreshData) refreshData();
};
const isUserLockedToday = (user) => {
const todayLogs = logs.filter(l => l.userId === user.id && l.date === window.getNICDate());
const hasSalida = todayLogs.some(l => l.type === 'salida');
return user.manualLock || (hasSalida && !user.overrideCheckOut);
};
const toggleLock = async (u, lockStatus) => {
await window.apiCall('toggle_user_lock', { id: u.id, manualLock: lockStatus, overrideCheckOut: !lockStatus });
showToast(lockStatus ? "Bloqueado." : "Desbloqueado.");
if (refreshData) refreshData();
};
const saveCustomAction = async () => {
if (!newActionLabel.trim()) return showToast("Escribe un nombre para el botón.");
const newId = editingActionId || newActionLabel.toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, '') + '_' + Date.now().toString().slice(-4);
await window.apiCall('save_action', { action_id: newId, label: newActionLabel });
setEditingActionId(null); setNewActionLabel(''); showToast("Botón guardado exitosamente.");
if (refreshData) refreshData();
};
const deleteCustomAction = async (act) => {
confirmAction(`¿Borrar el botón "${act.label}" de todo el sistema?`, async () => {
await window.apiCall('delete_action', { action_id: act.id });
setConfirmModal(null); showToast("Botón eliminado.");
if (refreshData) refreshData();
});
};
const updateVacationRate = async () => {
await window.apiCall('update_settings', { vacation_rate: parseFloat(vacRate) || 2.5 });
showToast("Tasa mensual actualizada.");
if (refreshData) refreshData();
};
const setVacationStatus = async (vId, status) => {
await window.apiCall('update_vacation_status', { id: vId, status });
showToast(`Actualizado a: ${status}`);
if (refreshData) refreshData();
};
const handleDirectAssignment = async (e) => {
e.preventDefault();
if (!selectedUserVacation) return;
let payload = {
userId: selectedUserVacation.id, userName: selectedUserVacation.name, type: directVacType,
status: 'approved', requestedAt: Date.now(), seenByUser: false, revokedDates: []
};
if (directVacType === 'paid') {
if (!directPaidDays || directPaidDays <= 0) return showToast("Cantidad de días inválida.");
payload.paidDays = Number(directPaidDays);
} else {
if (!directVacStart || (directVacType === 'range' && !directVacEnd)) return showToast("Faltan fechas.");
const end = directVacType === 'range' ? directVacEnd : directVacStart;
if (directVacType === 'range' && end < directVacStart) return showToast("Fecha de fin debe ser mayor.");
payload.startDate = directVacStart;
payload.endDate = end;
}
await window.apiCall('add_vacation', payload);
setDirectVacStart(''); setDirectVacEnd(''); setDirectVacType('full'); setDirectPaidDays(1);
showToast("Acción guardada exitosamente.");
if (refreshData) refreshData();
};
const toggleRevokeDay = (dateStr) => {
setTempRevokedDates(prev => prev.includes(dateStr) ? prev.filter(d => d !== dateStr) : [...prev, dateStr]);
};
const confirmRevokedDates = async () => {
if (!managingVacation) return;
await window.apiCall('update_revoked_dates', { id: managingVacation.id, revokedDates: tempRevokedDates });
setManagingVacation(null); showToast("Días actualizados en el saldo del usuario.");
if (refreshData) refreshData();
};
const saveAbsence = async (e) => {
e.preventDefault();
if (!absenceUser || !absenceDate) return showToast("Falta usuario o fecha.");
const userObj = globalUsers.find(u => u.id == absenceUser);
await window.apiCall('save_absence', {
userId: userObj.id, userName: userObj.name, date: absenceDate,
type: absenceType, notes: absenceNotes, createdAt: Date.now()
});
if (absenceType === 'descontada') {
await window.apiCall('add_vacation', {
userId: userObj.id, userName: userObj.name, type: 'full',
startDate: absenceDate, endDate: absenceDate, status: 'approved',
requestedAt: Date.now(), seenByUser: true, revokedDates: [], notes: 'Ausencia Convertida a Vacación'
});
}
setAbsenceUser(''); setAbsenceNotes(''); showToast("Ausencia Registrada.");
if (refreshData) refreshData();
};
const saveRole = async () => {
if (!newRoleName) return showToast("Escribe un nombre.");
await window.apiCall('save_role', {
name: newRoleName.toLowerCase().trim(), oldName: editingRoleName,
permissions: selectedPermissions, cameraActions: selectedCameraActions
});
setEditingRoleName(null); setNewRoleName(''); setSelectedPermissions([]); setSelectedCameraActions([]); showToast("Rol guardado.");
if (refreshData) refreshData();
};
const saveNotification = async () => {
if (!notifTitle || targetUsers.length === 0) return showToast("Faltan datos en el aviso.");
await window.apiCall('save_reminder', {
id: editingReminderId, title: notifTitle, message: notifBody, targetUsers: targetUsers, createdAt: Date.now()
});
setEditingReminderId(null); setNotifTitle(''); setNotifBody(''); setTargetUsers([]); showToast("Aviso enviado.");
if (refreshData) refreshData();
};
const approvedVacsHistory = useMemo(() => vacations.filter(v => v.status === 'approved').sort((a,b)=>new Date(b.startDate)-new Date(a.startDate)), [vacations]);
const totalVacPages = Math.ceil(approvedVacsHistory.length / 5);
const paginatedVacs = approvedVacsHistory.slice(vacPage * 5, (vacPage + 1) * 5);
return (
{toastMsg &&
{toastMsg}
}
{/* MODAL CREAR / EDITAR EMPRESA SAAS */}
{showClientModal && (
setShowClientModal(false)}>
)}
{mapLogs &&
setMapLogs(null)} />}
{previewImage && (
setPreviewImage(null)}>
e.stopPropagation()}>
setPreviewImage(null)} className="absolute top-4 right-4 bg-slate-900/50 text-white p-2 rounded-full hover:bg-slate-900 transition-colors">
)}
{confirmModal && (
setConfirmModal(null)}>
e.stopPropagation()}>
Confirmación
{confirmModal.message}
setConfirmModal(null)} className="flex-1 py-4 bg-slate-100 text-slate-600 rounded-2xl font-black uppercase text-[10px] tracking-widest active:scale-95 transition-all">Cancelar
Proceder
)}
{selectedUserVacation && (
setSelectedUserVacation(null)}>
e.stopPropagation()}>
{selectedUserVacation.name}
REPORTE VACACIONAL • SALDO: {window.calculateVacationBalance(selectedUserVacation, settings?.vacationRate||2.5, vacations)} DÍAS
setSelectedUserVacation(null)} className="p-3 bg-slate-50 text-slate-400 hover:text-slate-600 rounded-full transition-colors">
HISTORIAL Y SOLICITUDES DEL USUARIO
{vacations.filter(v=>v.userId === selectedUserVacation.id).length === 0 ?
Sin registros en el sistema.
:
vacations.filter(v=>v.userId === selectedUserVacation.id).sort((a,b)=>new Date(b.startDate)-new Date(a.startDate)).map(v => (
{v.type === 'half' ? 'MEDIO DÍA (0.5)' : v.type === 'paid' ? 'PAGO VACACIONES' : 'RANGO/DÍA COMPLETO'}
{v.type === 'paid' ?
DÍAS LIQUIDADOS: {v.paidDays}
:
FECHAS: {window.formatDateToNIC ? window.formatDateToNIC(v.startDate) : v.startDate} {v.type==='range'?` AL ${window.formatDateToNIC ? window.formatDateToNIC(v.endDate) : v.endDate}`:''}
}
{v.status === 'approved' ? 'APROBADO' : v.status === 'pending' ? 'PENDIENTE DE REVISIÓN' : 'DENEGADO / INACTIVO'}
{v.status === 'pending' &&
setVacationStatus(v.id, 'approved')} className="px-6 py-3 bg-slate-900 text-white rounded-xl font-black text-[10px] uppercase tracking-widest">Aprobar setVacationStatus(v.id, 'denied')} className="px-6 py-3 bg-white border border-slate-200 text-slate-600 rounded-xl font-black text-[10px] uppercase tracking-widest">Denegar
}
{v.status === 'approved' && v.type !== 'paid' &&
{setManagingVacation(v); setTempRevokedDates(v.revokedDates || []);}} className="px-6 py-3 bg-slate-900 text-white rounded-xl font-black text-[10px] uppercase tracking-widest active:scale-95 transition-all">GESTIONAR / REVOCAR }
confirmAction("Borrar el registro por completo de la base de datos.", async ()=>{ await window.apiCall('delete_vacation', {id: v.id}); setConfirmModal(null); showToast('Registro borrado.'); })} className="text-slate-400 font-bold text-[9px] uppercase tracking-widest underline hover:text-red-500 transition-colors">ELIMINAR FILA
))
}
)}
{managingVacation && (
{setManagingVacation(null); setTempRevokedDates([]);}}>
e.stopPropagation()}>
Edición Quirúrgica
{setManagingVacation(null); setTempRevokedDates([]);}} className="p-2 bg-slate-100 text-slate-400 rounded-full hover:bg-slate-200 transition-colors">
DESMARCA LOS DÍAS ESPECÍFICOS QUE EL USUARIO SÍ TRABAJARÁ (SE LE DEVOLVERÁN A SU SALDO).
{window.getDatesInRange(managingVacation.startDate, managingVacation.endDate || managingVacation.startDate).map(dateStr => {
const isRevoked = tempRevokedDates.includes(dateStr);
return (
{window.formatDateToNIC ? window.formatDateToNIC(dateStr) : dateStr}
);
})}
{setManagingVacation(null); setTempRevokedDates([]);}} className="flex-1 py-4 bg-slate-100 text-slate-600 font-black rounded-2xl uppercase text-[10px] tracking-widest">Cancelar
Aplicar Cambios
)}
Shift Central
{isMaster && masterClients && masterClients.length > 0 && (
Empresa:
setActiveClientId && setActiveClientId(Number(e.target.value))}
className="bg-transparent font-black text-indigo-900 text-xs outline-none cursor-pointer uppercase"
>
{masterClients.map(c => (
{c.company_name} (@{c.shift_subdomain})
))}
)}
setView('dashboard')} className="px-5 py-2.5 bg-indigo-50 text-indigo-600 rounded-2xl font-black uppercase text-[10px] active:scale-95 transition-all">Vista App
Salir
{isMaster && (
setTab('master')} className={`px-6 py-3 rounded-xl font-black uppercase text-[10px] whitespace-nowrap transition-all ${tab==='master'?'bg-indigo-600 text-white shadow-md':''}`}>Empresas SaaS
)}
setTab('logs')} className={`px-6 py-3 rounded-xl font-black uppercase text-[10px] whitespace-nowrap transition-all ${tab==='logs'?'bg-white text-indigo-600 shadow-sm':''}`}>Auditoría
setTab('users')} className={`px-6 py-3 rounded-xl font-black uppercase text-[10px] whitespace-nowrap transition-all ${tab==='users'?'bg-white text-indigo-600 shadow-sm':''}`}>Personal
setTab('vacations')} className={`px-6 py-3 rounded-xl font-black uppercase text-[10px] whitespace-nowrap transition-all ${tab==='vacations'?'bg-white text-emerald-600 shadow-sm':''}`}>Vacaciones
setTab('absences')} className={`px-6 py-3 rounded-xl font-black uppercase text-[10px] whitespace-nowrap transition-all ${tab==='absences'?'bg-white text-red-600 shadow-sm':''}`}>Ausencias
setTab('roles')} className={`px-6 py-3 rounded-xl font-black uppercase text-[10px] whitespace-nowrap transition-all ${tab==='roles'?'bg-white text-indigo-600 shadow-sm':''}`}>Roles y Botones
setTab('notif')} className={`px-6 py-3 rounded-xl font-black uppercase text-[10px] whitespace-nowrap transition-all ${tab==='notif'?'bg-white text-indigo-600 shadow-sm':''}`}>Avisos
{tab === 'master' && isMaster && (
Gestión de Portales Corporativos
Crea subdominios y asigna administradores a cada cliente
{setEditingClient(null); setShowClientModal(true);}} className="px-6 py-4 bg-indigo-600 text-white font-black rounded-2xl uppercase tracking-widest flex items-center gap-2 shadow-lg active:scale-95 transition-all">
Nueva Empresa
{masterClients.map(c => (
{c.module_shift == 1 ? 'MÓDULO ACTIVO' : 'INACTIVO'}
{setEditingClient(c); setShowClientModal(true);}} className="p-2 bg-slate-50 text-slate-500 rounded-xl hover:bg-slate-100">
{c.company_name}
{c.shift_subdomain}.shift.marcasnicaragua.com
Contacto: {c.contact_name || 'N/A'}
Correo: {c.contact_email}
setActiveClientId && setActiveClientId(c.id)} className={`flex-1 py-3 rounded-2xl font-black uppercase text-[10px] transition-all ${activeClientId === c.id ? 'bg-indigo-600 text-white shadow-md' : 'bg-slate-100 text-slate-600'}`}>
{activeClientId === c.id ? 'Administrando Portal' : 'Inspeccionar Portal'}
toggleModule(c)} className={`px-4 py-3 rounded-2xl font-black uppercase text-[10px] ${c.module_shift == 1 ? 'bg-red-50 text-red-600' : 'bg-emerald-50 text-emerald-600'}`}>
{c.module_shift == 1 ? 'Pausar' : 'Activar'}
))}
)}
{tab === 'logs' && (
Usuario Marca Fecha/Hora Acciones
{filteredLogs.map(l => {
const actionObj = actions.find(a => a.id === l.type);
const displayType = actionObj ? actionObj.label : l.type.replace('_',' ');
return (
{l.userName}
{displayType}
{window.formatNICTimeFull ? window.formatNICTimeFull(l.timestamp) : new Date(l.timestamp).toLocaleString()}
{l.location && l.location.lat ? (
setMapLogs([l])} className="px-4 py-2 bg-indigo-50 text-indigo-600 hover:bg-indigo-600 hover:text-white rounded-xl font-black text-[9px] uppercase tracking-widest flex items-center justify-center gap-2 active:scale-95 transition-all">
MAPA
) : Sin GPS }
{l.photo && setPreviewImage(l.photo)} className="w-10 h-10 rounded-xl object-cover cursor-pointer hover:scale-110 transition-transform border border-slate-200 shadow-sm" />}
)
})}
)}
{tab === 'users' && (
{globalUsers.map(u => {
const isLocked = isUserLockedToday(u);
return (
{u.name}
CÓDIGO/ALIAS: @{u.alias} • ID #{u.id} • {u.role} • {u.status}
{isLocked ? (
toggleLock(u, false)} className="px-4 py-3 bg-emerald-50 text-emerald-600 rounded-xl font-black uppercase text-[10px] active:scale-95 transition-all">Desbloquear
) : (
toggleLock(u, true)} className="px-4 py-3 bg-orange-50 text-orange-600 rounded-xl font-black uppercase text-[10px] active:scale-95 transition-all">Bloquear
)}
setEditingUser(u)} className="px-4 py-3 bg-slate-50 text-slate-600 rounded-xl font-black uppercase text-[10px] active:scale-95 transition-all">Editar
confirmAction("Borrar por completo al usuario de la base de datos.", ()=>deleteUser(u.id))} className="px-4 py-3 bg-red-50 text-red-600 rounded-xl font-black uppercase text-[10px] active:scale-95 transition-all">Borrar
);
})}
)}
{tab === 'vacations' && (
Solicitudes Entrantes
{vacations.filter(v => v.status === 'pending').length === 0 ? (
) : (
vacations.filter(v => v.status === 'pending').map(v => (
{v.userName}
Pide: {v.type === 'half' ? 'MEDIO DÍA (0.5)' : `${window.getDaysDiff(v.startDate, v.endDate || v.startDate)} DÍA(S)`}
Del {window.formatDateToNIC?window.formatDateToNIC(v.startDate):v.startDate} {v.type==='range' ? `al ${window.formatDateToNIC?window.formatDateToNIC(v.endDate):v.endDate}`:''}
setVacationStatus(v.id, 'approved')} className="px-6 py-4 bg-emerald-500 text-white rounded-2xl font-black uppercase text-[10px] shadow-lg active:scale-95 transition-all">Aprobar
setVacationStatus(v.id, 'denied')} className="px-6 py-4 bg-white text-red-600 rounded-2xl font-black uppercase text-[10px] active:scale-95 transition-all border border-red-200">Denegar
))
)}
Saldos
TOCA EL NOMBRE PARA GESTIONAR
Consultor (Perfil) Disponible Contrato
{globalUsers.map(u => (
setSelectedUserVacation(u)}>
{u.name}
{window.calculateVacationBalance(u, settings?.vacationRate||2.5, vacations)}
{window.formatDateToNIC?window.formatDateToNIC(u.hiringDate):u.hiringDate}
))}
Aprobadas General
setVacPage(p=>p-1)} className={`p-2 rounded-full transition-colors ${vacPage===0?'opacity-30':'hover:bg-slate-100 text-slate-600'}`}>
{vacPage+1} / {totalVacPages||1}
=(totalVacPages-1)} onClick={()=>setVacPage(p=>p+1)} className={`p-2 rounded-full transition-colors ${vacPage>=(totalVacPages-1)?'opacity-30':'hover:bg-slate-100 text-slate-600'}`}>
{paginatedVacs.length === 0 ?
Sin registros históricos.
:
paginatedVacs.map(v => (
{v.userName}
FECHAS: {window.formatDateToNIC?window.formatDateToNIC(v.startDate):v.startDate} {v.type==='range'?` AL ${window.formatDateToNIC?window.formatDateToNIC(v.endDate):v.endDate}`:''}
{v.revokedDates && v.revokedDates.length > 0 &&
*{v.revokedDates.length} DÍAS REVOCADOS
}
{v.type === 'paid' &&
*PAGO EN EFECTIVO ({v.paidDays} DÍAS)
}
))
}
)}
{tab === 'absences' && (
Historial de Ausencias
{absences.length === 0 ?
Sin registros de ausencias.
:
absences.sort((a,b)=>b.createdAt - a.createdAt).map(a => (
{a.userName}
Fecha de falta: {window.formatDateToNIC?window.formatDateToNIC(a.date):a.date}
{a.type.replace('_', ' ')}
{a.notes &&
"{a.notes}"
}
confirmAction("¿Borrar este registro de ausencia?", async ()=>{ await window.apiCall('delete_absence', {id: a.id}); setConfirmModal(null); showToast("Eliminado."); })} className="text-slate-400 font-black text-[9px] uppercase tracking-widest underline hover:text-red-500 transition-colors">Borrar Registro
))
}
)}
{tab === 'roles' && (
Configuración de Botones (Marcajes)
Crea o renombra los botones que ven los usuarios en la aplicación. Usa el ícono de edición para modificar su nombre visible sin romper el historial.
setNewActionLabel(e.target.value)} placeholder="Nombre del nuevo botón (Ej: Permiso Médico)..." className="flex-1 p-4 bg-slate-50 rounded-2xl font-bold uppercase outline-none focus:ring-2 focus:ring-emerald-100" />
{editingActionId ? 'Guardar Cambios' : 'Crear Botón'}
{editingActionId && {setEditingActionId(null); setNewActionLabel('');}} className="py-4 px-8 bg-slate-100 text-slate-500 font-black rounded-2xl uppercase tracking-widest text-[10px] active:scale-95 transition-all">Cancelar }
{actions.map(a => (
{a.label}
{setEditingActionId(a.id); setNewActionLabel(a.label);}} className="p-2 bg-white text-indigo-500 rounded-xl shadow-sm border border-slate-200 active:scale-95">
deleteCustomAction(a)} className="p-2 bg-white text-red-500 rounded-xl shadow-sm border border-slate-200 active:scale-95">
))}
{editingRoleName ? "Editar Rol" : "Nuevo Rol"}
setNewRoleName(e.target.value)} placeholder="Nombre del Rol (Ej: VENTAS)" className="w-full p-4 bg-slate-50 rounded-2xl font-bold uppercase outline-none focus:ring-2" />
Permisos (Mostrar Botones)
{actions.map(a => (
{setSelectedPermissions(prev => prev.includes(a.id) ? prev.filter(p => p !== a.id) : [...prev, a.id]);}} className={`p-4 rounded-2xl font-black text-[10px] uppercase border active:scale-95 transition-all ${selectedPermissions.includes(a.id)?'bg-indigo-600 text-white border-indigo-600 shadow-md':'bg-slate-50 text-slate-500 hover:bg-slate-100'}`}>{a.label}
))}
Exigir Foto al Marcar
{[{id:'entrada', label:'Entrada'}, {id:'salida', label:'Salida'}, ...actions].map(a => (
{setSelectedCameraActions(prev => prev.includes(a.id) ? prev.filter(p => p !== a.id) : [...prev, a.id]);}} className={`p-4 rounded-2xl font-black text-[10px] uppercase border active:scale-95 transition-all flex items-center justify-center gap-1 ${selectedCameraActions.includes(a.id)?'bg-emerald-500 text-white border-emerald-500 shadow-md':'bg-slate-50 text-slate-500 hover:bg-slate-100'}`}>
{a.label}
))}
{editingRoleName && {setEditingRoleName(null); setNewRoleName(''); setSelectedPermissions([]); setSelectedCameraActions([]);}} className="w-1/3 py-4 bg-slate-100 text-slate-500 font-black rounded-2xl uppercase">X }
Guardar Perfil
{roles.map(r => {
const rName = r.name || r; const rPerms = r.permissions || []; const rCam = r.cameraActions || ['entrada', 'salida', 'visita_in'];
return (
{rName}
Permisos: {rPerms.join(' • ')}
{setEditingRoleName(rName); setNewRoleName(rName); setSelectedPermissions(rPerms); setSelectedCameraActions(rCam);}} className="px-6 py-3 bg-slate-50 text-indigo-600 rounded-xl font-black uppercase text-[10px] active:scale-95 transition-all shadow-sm">Editar
);
})}
)}
{tab === 'notif' && (
{editingReminderId ? "Editar Aviso" : "Nuevo Aviso General"}
setNotifTitle(e.target.value)} placeholder="Título Impactante" className="w-full p-4 bg-slate-50 rounded-2xl font-bold outline-none focus:ring-2" />
setNotifBody(e.target.value)} placeholder="El mensaje que leerán..." className="w-full p-4 bg-slate-50 rounded-2xl h-28 outline-none focus:ring-2 resize-none" />
{globalUsers.map(u => (
setTargetUsers(p=>p.includes(u.id)?p.filter(id=>id!==u.id):[...p, u.id])} className={`w-full p-3 rounded-xl text-left font-bold text-[11px] transition-colors ${targetUsers.includes(u.id)?'bg-indigo-600 text-white shadow-md':'hover:bg-slate-200 text-slate-600'}`}>{u.name}
))}
{editingReminderId && {setEditingReminderId(null); setNotifTitle(''); setNotifBody(''); setTargetUsers([]);}} className="w-1/3 py-4 bg-slate-100 text-slate-500 font-black rounded-2xl uppercase">X }
Difundir Aviso
{reminders.map(r => (
{setEditingReminderId(r.id); setNotifTitle(r.title); setNotifBody(r.message); setTargetUsers(r.targetUsers||[]);}} className="px-5 py-3 bg-slate-50 text-slate-600 rounded-xl font-black uppercase text-[10px] active:scale-95 transition-all">Editar
confirmAction("Eliminar aviso de forma permanente de todos los usuarios.", async ()=>{ await window.apiCall('delete_reminder', {id: r.id}); setConfirmModal(null); showToast('Eliminado.'); })} className="px-5 py-3 bg-red-50 text-red-600 rounded-xl font-black uppercase text-[10px] active:scale-95 transition-all">Borrar
))}
)}
);
};