Permisos: marketing (export completo, agenda/citas, cumpleanos, visitas, WACRM), gerencia gestiona usuarios y roles, recepcion edita expedientes, cosmetologas ven agenda, fix del sidebar que no dejaba seleccionar Leads
This commit is contained in:
@@ -105,9 +105,14 @@ const Agenda: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const { openPatient } = usePatientDrawer();
|
||||
// Rol médico: agenda restringida a su propio hr.employee (filtro también en servidor)
|
||||
// Rol médico: agenda restringida a su propio hr.employee (filtro también en servidor).
|
||||
// Cosmetología ve la agenda completa (es personal clínico, sin la privacidad
|
||||
// restringida del médico), pero arranca en su propia columna si tiene
|
||||
// empleado vinculado.
|
||||
const isMedico = user?.role === 'medico';
|
||||
const isCosmetologa = user?.role === 'cosmetologia';
|
||||
const medicoEmployeeId = isMedico && user?.employee_id ? user.employee_id : null;
|
||||
const propiaColumnaId = isCosmetologa && user?.employee_id ? user.employee_id : null;
|
||||
const [appointments, setAppointments] = useState<Appointment[]>([]);
|
||||
const [patients, setPatients] = useState<Patient[]>([]);
|
||||
const [services, setServices] = useState<Service[]>([]);
|
||||
@@ -120,6 +125,7 @@ const Agenda: FC = () => {
|
||||
const [stateFilter, setStateFilter] = useState('');
|
||||
const [doctorFilter, setDoctorFilter] = useState(() => {
|
||||
if (medicoEmployeeId) return String(medicoEmployeeId);
|
||||
if (propiaColumnaId) return String(propiaColumnaId);
|
||||
const saved = readAgendaPrefs().doctorFilter;
|
||||
return typeof saved === 'string' ? saved : '';
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FC } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Phone, MapPin, Mail, Globe, Save, Building2, ArrowRightLeft, Percent, Check, Calculator } from 'lucide-react';
|
||||
import Layout from '../components/Layout';
|
||||
@@ -11,6 +11,7 @@ import AuditoriaPanel from '../components/AuditoriaPanel';
|
||||
import ImpactoModal from '../components/ImpactoModal';
|
||||
import { Card, Button, Input, PageHeader, toast } from '../components/ui';
|
||||
import { odooApi, type Doctor } from '../services/odoo';
|
||||
import { useAuth } from '../lib/auth';
|
||||
|
||||
const TABS = [
|
||||
{ key: 'clinica', label: 'Clínica' },
|
||||
@@ -45,8 +46,17 @@ const defaultSettings: ClinicSettings = {
|
||||
const STORAGE_KEY = 'skeen_clinic_settings';
|
||||
|
||||
const Configuracion: FC = () => {
|
||||
const { user } = useAuth();
|
||||
// Gerencia gestiona usuarios y roles; la sección "Clínica" (tipo de cambio y
|
||||
// comisiones) queda para administración del sistema.
|
||||
const esAdmin = user?.role === 'admin';
|
||||
const tabsVisibles = useMemo(
|
||||
() => (esAdmin ? TABS : TABS.filter((t) => t.key === 'usuarios' || t.key === 'roles')),
|
||||
[esAdmin],
|
||||
);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const tab = searchParams.get('tab') || 'clinica';
|
||||
const tabPedida = searchParams.get('tab') || 'clinica';
|
||||
const tab = tabsVisibles.some((t) => t.key === tabPedida) ? tabPedida : tabsVisibles[0].key;
|
||||
const setTab = (t: string) => setSearchParams({ tab: t });
|
||||
const [subcatalogo, setSubcatalogo] = useState<string>(() => {
|
||||
const sub = searchParams.get('sub');
|
||||
@@ -85,6 +95,7 @@ const Configuracion: FC = () => {
|
||||
const [tcSaving, setTcSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!esAdmin) return;
|
||||
odooApi.getExchangeRate()
|
||||
.then((res) => {
|
||||
if (res.current) {
|
||||
@@ -123,6 +134,7 @@ const Configuracion: FC = () => {
|
||||
const [impactoOpen, setImpactoOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!esAdmin) return;
|
||||
odooApi.getCommissionDefault()
|
||||
.then((res) => setCommissionDefault(String(res.commission_pct ?? 0)))
|
||||
.catch(() => {/* odoo no disponible, ignorar */});
|
||||
@@ -184,7 +196,7 @@ const Configuracion: FC = () => {
|
||||
|
||||
{/* Tabs de secciones */}
|
||||
<div className="flex flex-wrap gap-1.5 mb-6">
|
||||
{TABS.map((t) => (
|
||||
{tabsVisibles.map((t) => (
|
||||
<button
|
||||
key={t.key}
|
||||
type="button"
|
||||
|
||||
@@ -133,6 +133,7 @@ const Pacientes: FC = () => {
|
||||
const [entryDetail, setEntryDetail] = useState<
|
||||
{ kind: 'cita'; item: Appointment } | { kind: 'venta'; item: Sale } | { kind: 'visita'; item: Visita } | null
|
||||
>(null);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [onlyIncomplete, setOnlyIncomplete] = useState(false);
|
||||
const [onlyVip, setOnlyVip] = useState(false);
|
||||
const [onlyRecent, setOnlyRecent] = useState(false);
|
||||
@@ -415,13 +416,30 @@ const Pacientes: FC = () => {
|
||||
].sort((a, b) => b.date.localeCompare(a.date))
|
||||
: [];
|
||||
|
||||
// Exportación COMPLETA: pide al servidor todos los pacientes del filtro
|
||||
// (no sólo la página visible). Hoy son ~7.2k filas.
|
||||
const exportPatients = async () => {
|
||||
try {
|
||||
setExporting(true);
|
||||
const res = await odooApi.exportPatients({
|
||||
search: search || undefined,
|
||||
incomplete: onlyIncomplete ? 1 : undefined,
|
||||
vip: onlyVip ? 1 : undefined,
|
||||
recent: onlyRecent ? 1 : (onlyStale ? 0 : undefined),
|
||||
doctor_id: doctorFilter ? parseInt(doctorFilter, 10) : undefined,
|
||||
});
|
||||
if (res.status !== 'success') {
|
||||
toast.error('Error al exportar');
|
||||
return;
|
||||
}
|
||||
const todos = res.patients;
|
||||
await exportToExcel({
|
||||
filename: `pacientes-skeen-${new Date().toISOString().split('T')[0]}`,
|
||||
sheetName: 'Pacientes',
|
||||
title: 'Pacientes SKEEN',
|
||||
subtitle: `${patients.length} pacientes de la vista actual`,
|
||||
subtitle: res.truncado
|
||||
? `${res.total} de ${res.total_disponible} pacientes (límite ${res.limite})`
|
||||
: `${res.total} pacientes (exportación completa)`,
|
||||
columns: [
|
||||
{ header: 'Expediente', key: 'patient_id' },
|
||||
{ header: 'Nombre', key: 'name' },
|
||||
@@ -436,24 +454,26 @@ const Pacientes: FC = () => {
|
||||
{ header: 'Fuente', key: 'source' },
|
||||
{ header: 'VIP', key: 'vip' },
|
||||
],
|
||||
rows: patients.map((p) => ({
|
||||
rows: todos.map((p) => ({
|
||||
patient_id: p.patient_id, name: p.name, phone: p.phone, email: p.email,
|
||||
birth_date: p.birth_date, gender: p.gender || '', last_visit: p.last_visit,
|
||||
total_visits: p.total_visits, wallet_points: p.wallet_points,
|
||||
total_spent: p.total_spent, source: p.source, vip: p.is_vip ? 'Sí' : 'No',
|
||||
})),
|
||||
});
|
||||
toast.success(`Excel descargado (${patients.length} pacientes de la vista actual)`);
|
||||
toast.success(`Excel descargado (${res.total} pacientes)`);
|
||||
} catch (err) {
|
||||
toast.error('Error al exportar');
|
||||
console.error(err);
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout title="Pacientes" subtitle="Directorio de pacientes">
|
||||
<PageHeader title="Pacientes" subtitle="Busca, crea y gestiona pacientes">
|
||||
<Button variant="outline" onClick={exportPatients} disabled={patients.length === 0}>
|
||||
<Button variant="outline" onClick={exportPatients} loading={exporting} disabled={patients.length === 0}>
|
||||
<Download size={16} className="mr-2" />
|
||||
Exportar CSV
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user