feat: version 4.12.0 -- Conexiones externas: pacientes de SAGA, empleados de nomina y medicos

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-29 17:06:10 -05:00
parent fabe5b530d
commit 0ee9ed65bf
43 changed files with 1402 additions and 69 deletions
+20 -2
View File
@@ -6,6 +6,7 @@ import { useAuth } from '../../context/auth-context.jsx';
import { useAsync, useAction } from '../../hooks/useAsync.js';
import { listUsers, listRoles, createUser, updateUser, setUserActive, approveUser, rejectUser, resendUserEmail, listUserSessions, revokeUserSession, revokeAllUserSessions } from '../../services/users.js';
import { listDepartments } from '../../services/departments.js';
import { listEmployees } from '../../services/integrations.js';
import { ListView } from '../../components/ListView.jsx';
import { SearchInput } from '../../components/SearchInput.jsx';
import { Button } from '../../components/Button.jsx';
@@ -27,7 +28,10 @@ function flatten(nodes, acc = []) {
}
const STATUS_COLOR = { pending_verification: 'muted', pending_approval: 'warning', active: 'success', invited: 'muted' };
const EMPTY_FORM = { name: '', email: '', roleId: '', departmentId: '', position: '', password: '' };
const EMPTY_FORM = { name: '', email: '', roleId: '', departmentId: '', position: '', password: '', employeeId: '' };
// Nómina viene en MAYÚSCULAS: "RIERA CAJAMARCA" → "Riera Cajamarca".
const titleCase = (s) => String(s || '').toLowerCase().replace(/(^|\s)\p{L}/gu, (m) => m.toUpperCase()).trim();
const fold = (s) => String(s || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').trim().toUpperCase();
export default function UsersPanel() {
const { t, pushToast, openDialog } = useUi();
@@ -47,6 +51,7 @@ export default function UsersPanel() {
const revokeAllSessions = useCallback(() => revokeAllUserSessions(sessionsUser.id), [sessionsUser]);
const [search, setSearch] = useState('');
const [employees, setEmployees] = useState([]);
const [editing, setEditing] = useState(false);
const [selected, setSelected] = useState(null);
const [approving, setApproving] = useState(null);
@@ -79,6 +84,14 @@ export default function UsersPanel() {
function startNew() {
setSelected(null);
setEditing(true);
// Empleados de nómina que aún no tienen usuario (si hay fuente de nómina configurada).
listEmployees(true).then(setEmployees).catch(() => setEmployees([]));
}
function pickEmployee(id) {
const e = employees.find((x) => x.id === Number(id));
if (!e) return setForm((f) => ({ ...f, employeeId: '' }));
const dept = deptOptions.find((d) => fold(d.label) === fold(e.department_name));
setForm((f) => ({ ...f, employeeId: e.id, name: titleCase(`${e.first_names} ${e.last_names}`), email: e.email || f.email, position: e.position ? titleCase(e.position) : f.position, departmentId: dept ? dept.value : f.departmentId }));
}
function startEdit(user) {
@@ -94,7 +107,7 @@ export default function UsersPanel() {
if (selected) {
await updateUser(selected.id, { name: form.name.trim(), roleId: form.roleId, departmentId: form.departmentId || null, position: form.position || null, editVersion: form.editVersion });
} else {
await createUser({ name: form.name.trim(), email: form.email.trim(), roleId: form.roleId, departmentId: form.departmentId || null, position: form.position || null, password: form.password });
await createUser({ name: form.name.trim(), email: form.email.trim(), roleId: form.roleId, departmentId: form.departmentId || null, position: form.position || null, password: form.password, employeeId: form.employeeId || undefined });
}
draft.clear();
await reload();
@@ -208,6 +221,11 @@ export default function UsersPanel() {
<DetailPopup open={editing} title={selected ? t('common_edit') : t('user_new')} onClose={() => guardedClose(draft, () => setEditing(false))} width={560} height={500}>
<DraftNotice draft={draft} />
{!selected && employees.length > 0 && (
<Field label={t('user_from_payroll')} dirty={draft.isFieldDirty('employeeId')}>
<ComboBox options={[{ value: '', label: '—' }, ...employees.map((e) => ({ value: e.id, label: `${e.last_names} ${e.first_names} · ${e.identification}${e.position ? ` · ${e.position}` : ''}` }))]} value={form.employeeId} onChange={pickEmployee} />
</Field>
)}
<FieldRow>
<Field label={t('common_name')} dirty={draft.isFieldDirty('name')}>
<input className="input" value={form.name} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} autoFocus />