feat: version 4.7.0 -- Agenda medica

Modulo clinic (habilitado por usuario): citas de pacientes para medicos con choques
bloqueantes (medico, paciente con cualquier medico, quirofano/bloqueos recurrentes y
horario de atencion), validados en transaccion con bloqueo de filas para que dos
recepcionistas no tomen el mismo hueco. Recepcion (alcance todos) y medico (alcance
propio). Vista Dia por columnas de medico, Semana y Lista; pacientes con historial;
bloqueos; configuracion de personal, horarios y tipos solo admin/superadmin. Capa de
citas en el calendario personal del medico. Migracion 036.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-28 12:57:43 -05:00
parent bdc62403f4
commit 1e25022630
29 changed files with 1649 additions and 34 deletions
@@ -0,0 +1,10 @@
import { DatePicker } from './DatePicker.jsx';
import { ComboBox } from './ComboBox.jsx';
const hours = Array.from({length:24},(_,i) => ({value:String(i).padStart(2,'0'),label:String(i).padStart(2,'0')}));
const minutes = Array.from({length:12},(_,i) => ({value:String(i*5).padStart(2,'0'),label:String(i*5).padStart(2,'0')}));
export function DateTimeEditor({ value, onChange, allDay, label, isEnd = false }) {
const date = value.slice(0,10), hour = value.slice(11,13), minute = value.slice(14,16);
const setDate = (next) => onChange(`${next} ${allDay ? (isEnd ? '23:59' : '00:00') : `${hour}:${minute}`}:00`);
const setTime = (h,m) => onChange(`${date} ${h}:${m}:00`);
return <div className="meeting-datetime"><DatePicker value={date} onChange={setDate} label={label} />{!allDay && <><ComboBox options={hours} value={hour} onChange={(h) => setTime(h,minute)} placeholder={`${label} HH`} /><span>:</span><ComboBox options={minutes} value={minute} onChange={(m) => setTime(hour,m)} placeholder={`${label} MM`} /></>}</div>;
}
+2 -1
View File
@@ -1,7 +1,7 @@
import { NavLink } from 'react-router-dom';
import { useUserDrafts } from '../hooks/useDraftForm.js';
import { DraftMark } from './NavigationGuard.jsx';
import { LayoutDashboard, FolderKanban, Building2, Settings, Repeat, StickyNote, KeyRound, Monitor, CalendarDays } from 'lucide-react';
import { LayoutDashboard, FolderKanban, Building2, Settings, Repeat, StickyNote, KeyRound, Monitor, CalendarDays, Stethoscope } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useUi } from '../context/ui-context.jsx';
import { useAuth } from '../context/auth-context.jsx';
@@ -15,6 +15,7 @@ const ITEMS = [
{ to: '/', icon: LayoutDashboard, labelKey: 'nav_dashboard', end: true },
{ to: '/proyectos', module: 'projects', icon: FolderKanban, labelKey: 'nav_projects' },
{ to: '/calendario', module: 'calendar', icon: CalendarDays, labelKey: 'nav_calendar' },
{ to: '/agenda-medica', module: 'clinic', icon: Stethoscope, labelKey: 'nav_clinic' },
{ to: '/tareas-recurrentes', module: 'recurring_tasks', icon: Repeat, labelKey: 'nav_recurring_tasks' },
{ to: '/notas', module: 'notes', icon: StickyNote, labelKey: 'nav_notes' },
{ to: '/contrasenas', module: 'vault', icon: KeyRound, labelKey: 'nav_passwords' },
@@ -0,0 +1,55 @@
import { useUi } from '../context/ui-context.jsx';
import { Field, FieldRow } from './Field.jsx';
import { ComboBox } from './ComboBox.jsx';
import { DatePicker } from './DatePicker.jsx';
// Reuniones y bloqueos clínicos comparten las mismas reglas de recurrencia del backend.
export function RecurrenceEditor({
value,
start,
onChange
}) {
const {
t
} = useUi();
const weekday = (new Date(`${start.slice(0, 10)}T00:00:00Z`).getUTCDay() + 6) % 7;
const labels = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
const patch = fields => onChange({
...value,
...fields
});
return <>
<Field label={t('calendar_recurrence')}>
<ComboBox options={['none', 'daily', 'weekly', 'monthly'].map(x => ({
value: x,
label: t(`calendar_${x}`)
}))} value={value?.frequency || 'none'} onChange={x => onChange(x === 'none' ? null : {
frequency: x,
interval: 1,
weekdays: x === 'weekly' ? [weekday] : [],
count: 10
})} />
</Field>
{value && <>
<FieldRow>
<Field label={t('calendar_interval')}><input className="input" type="number" min="1" max="365" value={value.interval} onChange={e => patch({
interval: Number(e.target.value)
})} /></Field>
<Field label={t('calendar_count')}><input className="input" type="number" min="1" max="365" value={value.count || ''} onChange={e => patch({
count: e.target.value ? Number(e.target.value) : null,
until: null
})} /></Field>
<Field label={t('calendar_until')}><DatePicker value={value.until || ''} onChange={x => patch({
until: x || null,
count: null
})} label={t('calendar_until')} /></Field>
</FieldRow>
{value.frequency === 'weekly' && <div className="calendar-weekdays">{labels.map((name, index) => <label key={name}>
<input type="checkbox" checked={value.weekdays.includes(index)} onChange={() => patch({
weekdays: value.weekdays.includes(index) ? value.weekdays.filter(x => x !== index) : [...value.weekdays, index]
})} />
{' '}{t(`calendar_${name}`)}
</label>)}</div>}
</>}
</>;
}