Oleada UX 5: Servicios (duplicar, favoritos y orden para POS, mas vendidos) y Medicos (semana tipo con validacion en agenda, meta mensual con progreso)
This commit is contained in:
@@ -392,6 +392,21 @@ const Agenda: FC = () => {
|
||||
(b.all_day || (slot < b.time_to && slot + SLOT_MIN / 60 > b.time_from)))
|
||||
: undefined;
|
||||
|
||||
// Horario semanal del médico: celda fuera de su semana tipo.
|
||||
// Devuelve null si el médico no tiene horario configurado (sin restricción).
|
||||
const fueraDeHorario = (doctorId: number | null, slot: number): string | null => {
|
||||
if (!doctorId) return null;
|
||||
const doc = doctors.find((d) => d.id === doctorId);
|
||||
if (!doc?.tiene_horario || !doc.horario?.length) return null;
|
||||
const dow = String((new Date(`${date}T12:00:00`).getDay() + 6) % 7);
|
||||
const linea = doc.horario.find((h) => h.day_of_week === dow);
|
||||
if (!linea) return `Fuera de horario (no atiende ese día · ${doc.horario_resumen})`;
|
||||
if (slot < linea.hour_from - 1e-6 || slot >= linea.hour_to - 1e-6) {
|
||||
return `Fuera de horario (trabaja ${horaFloatStr(linea.hour_from)}-${horaFloatStr(linea.hour_to)})`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Drag & drop para mover citas (solo pendientes/confirmadas)
|
||||
const DRAGGABLE_STATES = ['pending', 'confirmed'];
|
||||
const handleDrop = async (doctorId: number | null, slot: number) => {
|
||||
@@ -1629,6 +1644,7 @@ const Agenda: FC = () => {
|
||||
{gridColumns.map((col) => {
|
||||
const cell = gridCellAppointments(col.doctorId, slot);
|
||||
const bloqueo = bloqueoEnCelda(col.doctorId, slot);
|
||||
const fuera = fueraDeHorario(col.doctorId, slot);
|
||||
const cellKey = `${col.key}-${slot}`;
|
||||
return (
|
||||
<td
|
||||
@@ -1636,7 +1652,8 @@ const Agenda: FC = () => {
|
||||
onDragOver={(e) => { if (dragApt && !bloqueo) { e.preventDefault(); setDropTarget(cellKey); } }}
|
||||
onDragLeave={() => setDropTarget((t) => (t === cellKey ? null : t))}
|
||||
onDrop={(e) => { e.preventDefault(); handleDrop(col.doctorId, slot); }}
|
||||
className={`p-1 align-top border-b border-l border-theme-border transition-colors ${dropTarget === cellKey ? 'bg-theme-accent-bg' : ''} ${isNowSlot ? 'border-t-2 border-t-theme-accent' : ''}`}
|
||||
className={`p-1 align-top border-b border-l border-theme-border transition-colors ${dropTarget === cellKey ? 'bg-theme-accent-bg' : ''} ${isNowSlot ? 'border-t-2 border-t-theme-accent' : ''} ${fuera ? 'opacity-50' : ''}`}
|
||||
title={fuera || undefined}
|
||||
>
|
||||
{bloqueo ? (
|
||||
<div
|
||||
@@ -1649,15 +1666,17 @@ const Agenda: FC = () => {
|
||||
</div>
|
||||
) : cell.length === 0 ? (
|
||||
<div
|
||||
onClick={(e) => clickCeldaLibre(e, date, slot, col.doctorId)}
|
||||
className={`cursor-pointer rounded-lg border py-1.5 text-center text-[10px] font-medium transition-colors ${
|
||||
quickFilter === 'libres'
|
||||
? 'bg-brand-mint border-emerald-700 text-theme-heading font-semibold'
|
||||
: 'border-dashed border-theme-border-strong text-theme-muted hover:bg-theme-accent-bg'
|
||||
onClick={(e) => { if (fuera) return; clickCeldaLibre(e, date, slot, col.doctorId); }}
|
||||
className={`rounded-lg border py-1.5 text-center text-[10px] font-medium transition-colors ${
|
||||
fuera
|
||||
? 'border-theme-border bg-theme-bg text-theme-muted cursor-not-allowed'
|
||||
: quickFilter === 'libres'
|
||||
? 'cursor-pointer bg-brand-mint border-emerald-700 text-theme-heading font-semibold'
|
||||
: 'cursor-pointer border-dashed border-theme-border-strong text-theme-muted hover:bg-theme-accent-bg'
|
||||
}`}
|
||||
title={`Nueva cita aquí — ${horaFloatStr(slot)}`}
|
||||
title={fuera || `Nueva cita aquí — ${horaFloatStr(slot)}`}
|
||||
>
|
||||
Libre
|
||||
{fuera ? 'Fuera de horario' : 'Libre'}
|
||||
</div>
|
||||
) : (
|
||||
cell.map((apt) => (
|
||||
@@ -1739,6 +1758,7 @@ const Agenda: FC = () => {
|
||||
{gridSlots.map((slot) => {
|
||||
const cell = gridCellAppointments(col.doctorId, slot);
|
||||
const bloqueo = bloqueoEnCelda(col.doctorId, slot);
|
||||
const fuera = fueraDeHorario(col.doctorId, slot);
|
||||
const cellKey = `${col.key}-${slot}`;
|
||||
const isNowSlot = nowSlot !== null && slot === nowSlot;
|
||||
return (
|
||||
@@ -1747,7 +1767,8 @@ const Agenda: FC = () => {
|
||||
onDragOver={(e) => { if (dragApt && !bloqueo) { e.preventDefault(); setDropTarget(cellKey); } }}
|
||||
onDragLeave={() => setDropTarget((t) => (t === cellKey ? null : t))}
|
||||
onDrop={(e) => { e.preventDefault(); handleDrop(col.doctorId, slot); }}
|
||||
className={`p-0.5 align-top border-b border-theme-border transition-colors ${isNowSlot ? 'border-l-2 border-l-theme-accent bg-theme-accent-bg/40' : 'border-l'} ${dropTarget === cellKey ? 'bg-theme-accent-bg' : ''}`}
|
||||
className={`p-0.5 align-top border-b border-theme-border transition-colors ${isNowSlot ? 'border-l-2 border-l-theme-accent bg-theme-accent-bg/40' : 'border-l'} ${dropTarget === cellKey ? 'bg-theme-accent-bg' : ''} ${fuera ? 'opacity-50' : ''}`}
|
||||
title={fuera || undefined}
|
||||
>
|
||||
{bloqueo ? (
|
||||
<div
|
||||
@@ -1759,13 +1780,15 @@ const Agenda: FC = () => {
|
||||
</div>
|
||||
) : cell.length === 0 ? (
|
||||
<div
|
||||
onClick={(e) => clickCeldaLibre(e, date, slot, col.doctorId)}
|
||||
className={`cursor-pointer rounded border py-1.5 transition-colors ${
|
||||
quickFilter === 'libres'
|
||||
? 'bg-brand-mint border-emerald-700'
|
||||
: 'border-dashed border-theme-border hover:bg-theme-accent-bg'
|
||||
onClick={(e) => { if (fuera) return; clickCeldaLibre(e, date, slot, col.doctorId); }}
|
||||
className={`rounded border py-1.5 transition-colors ${
|
||||
fuera
|
||||
? 'cursor-not-allowed border-theme-border bg-theme-bg'
|
||||
: quickFilter === 'libres'
|
||||
? 'cursor-pointer bg-brand-mint border-emerald-700'
|
||||
: 'cursor-pointer border-dashed border-theme-border hover:bg-theme-accent-bg'
|
||||
}`}
|
||||
title={`Libre ${horaFloatStr(slot)}`}
|
||||
title={fuera || `Libre ${horaFloatStr(slot)}`}
|
||||
/>
|
||||
) : (
|
||||
cell.map((apt) => (
|
||||
|
||||
Reference in New Issue
Block a user