bloque 4: campanas por segmentos con medicion (modulo skeen_campanas: segmentos reutilizables con conteo verificado, campanas por WhatsApp y correo con programacion, prueba previa, limite por minuto, pausa por errores, consentimiento por canal y atribucion de citas) + encuesta al salir con NPS (campo propio, envio automatico y recordatorio, reporte con tendencia y desglose); apagado por defecto
This commit is contained in:
131
frontend/src/components/EncuestasPanel.tsx
Normal file
131
frontend/src/components/EncuestasPanel.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import type { FC } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Save, Smile } from 'lucide-react';
|
||||
import { Button, Input, TextArea, toast } from './ui';
|
||||
import {
|
||||
odooApi, ENCUESTAS_DEFAULT, type ClinicSettings, type EncuestasSettings,
|
||||
} from '../services/odoo';
|
||||
|
||||
interface Props {
|
||||
settings: ClinicSettings;
|
||||
onSaved: (settings: ClinicSettings) => void;
|
||||
}
|
||||
|
||||
const Toggle: FC<{ label: string; checked: boolean; onChange: (v: boolean) => void; ayuda?: string }> = ({ label, checked, onChange, ayuda }) => (
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
onClick={() => onChange(!checked)}
|
||||
className={`mt-0.5 relative h-6 w-11 shrink-0 rounded-full transition-colors ${checked ? 'bg-theme-accent' : 'bg-theme-border-strong'}`}
|
||||
>
|
||||
<span className={`absolute top-0.5 h-5 w-5 rounded-full bg-white transition-transform ${checked ? 'translate-x-5' : 'translate-x-0.5'}`} />
|
||||
</button>
|
||||
<span>
|
||||
<span className="block text-sm font-medium text-theme-heading">{label}</span>
|
||||
{ayuda && <span className="block text-xs text-theme-muted">{ayuda}</span>}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
|
||||
// Configuración → Encuestas: bloque `encuestas`. Apagado (default) no se envía
|
||||
// ninguna encuesta tras completar una cita.
|
||||
const EncuestasPanel: FC<Props> = ({ settings, onSaved }) => {
|
||||
const [form, setForm] = useState<EncuestasSettings>(settings.encuestas || ENCUESTAS_DEFAULT);
|
||||
const [plantillas, setPlantillas] = useState<{ id: number; name: string; activo: boolean; tiene_nps: boolean }[]>([]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => setForm(settings.encuestas || ENCUESTAS_DEFAULT), [settings.encuestas]);
|
||||
useEffect(() => {
|
||||
odooApi.getEncuestasConfig()
|
||||
.then((res) => { if (res.status === 'success') setPlantillas(res.plantillas || []); })
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const set = <K extends keyof EncuestasSettings>(k: K, v: EncuestasSettings[K]) =>
|
||||
setForm((prev) => ({ ...prev, [k]: v }));
|
||||
|
||||
const guardar = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await odooApi.putClinicSettings({
|
||||
encuestas: { ...form }, motivo: 'Configuración de encuestas',
|
||||
});
|
||||
if (res.status === 'success') {
|
||||
onSaved(res.settings);
|
||||
toast.success('Configuración de encuestas guardada');
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
|
||||
toast.error(msg || 'No se pudo guardar la configuración de encuestas');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="font-heading text-lg text-theme-heading flex items-center gap-2">
|
||||
<Smile size={18} /> Encuestas de salida (NPS)
|
||||
</h3>
|
||||
<p className="text-sm text-theme-muted mt-1">
|
||||
Apagado (default) no se envía ninguna encuesta al completar una cita. Crea la plantilla
|
||||
(tipo Encuesta) con una pregunta de recomendación (0-10) en Formularios.
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={guardar} loading={saving}><Save size={16} className="mr-1.5" />Guardar</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Toggle label="Activar encuestas de salida" checked={form.activo} onChange={(v) => set('activo', v)}
|
||||
ayuda="Envía la encuesta tras completarse una cita y habilita el reporte de Satisfacción." />
|
||||
<Toggle label="Enviar solo a la primera visita" checked={form.solo_primera_visita}
|
||||
onChange={(v) => set('solo_primera_visita', v)}
|
||||
ayuda="Solo se envía a quien tiene una única visita completada." />
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-theme-heading mb-1.5">Plantilla de encuesta</label>
|
||||
<select
|
||||
value={form.plantilla_id == null ? '' : String(form.plantilla_id)}
|
||||
onChange={(e) => set('plantilla_id', e.target.value ? Number(e.target.value) : null)}
|
||||
className="w-full rounded-lg border border-theme-border-strong bg-theme-surface px-3 py-2 text-sm text-theme-heading"
|
||||
>
|
||||
<option value="">— Ninguna —</option>
|
||||
{plantillas.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}{p.tiene_nps ? ' · NPS' : ''}{p.activo ? '' : ' (inactiva)'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-theme-heading mb-1.5">Canal</label>
|
||||
<select value={form.canal} onChange={(e) => set('canal', e.target.value as EncuestasSettings['canal'])}
|
||||
className="w-full rounded-lg border border-theme-border-strong bg-theme-surface px-3 py-2 text-sm text-theme-heading">
|
||||
<option value="whatsapp">WhatsApp</option>
|
||||
<option value="email">Correo</option>
|
||||
<option value="ambos">Ambos</option>
|
||||
</select>
|
||||
</div>
|
||||
<Input type="number" min={0} max={720} label="Enviar tras (horas de completada la cita)"
|
||||
value={form.enviar_tras_horas}
|
||||
onChange={(e) => set('enviar_tras_horas', Number(e.target.value) || 0)} />
|
||||
<Input type="number" min={0} max={720} label="Recordatorio si no responde (horas, 0 = sin recordatorio)"
|
||||
value={form.recordatorio_horas}
|
||||
onChange={(e) => set('recordatorio_horas', Number(e.target.value) || 0)} />
|
||||
</div>
|
||||
|
||||
<TextArea rows={3} label="Agradecimiento (se muestra al terminar la encuesta)"
|
||||
value={form.agradecimiento}
|
||||
onChange={(e) => set('agradecimiento', e.target.value)}
|
||||
placeholder="¡Gracias por tu opinión!" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EncuestasPanel;
|
||||
Reference in New Issue
Block a user