Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4787a0cd66 |
6302
package-lock.json
generated
6302
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,6 @@ import API_KEY from "./utils/apiKeys";
|
|||||||
import "./AgendarConsulta/style/formagendamentos.css";
|
import "./AgendarConsulta/style/formagendamentos.css";
|
||||||
import { GetAllDoctors } from './utils/Functions-Endpoints/Doctor';
|
import { GetAllDoctors } from './utils/Functions-Endpoints/Doctor';
|
||||||
|
|
||||||
|
|
||||||
const ENDPOINT_CRIAR_EXCECAO = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_exceptions";
|
const ENDPOINT_CRIAR_EXCECAO = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_exceptions";
|
||||||
|
|
||||||
const FormCriarExcecao = ({ onCancel, doctorID }) => {
|
const FormCriarExcecao = ({ onCancel, doctorID }) => {
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import API_KEY from "../apiKeys";
|
import API_KEY from "../apiKeys";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const GetPatientByID = async (ID,authHeader) => {
|
const GetPatientByID = async (ID,authHeader) => {
|
||||||
|
|
||||||
var myHeaders = new Headers();
|
var myHeaders = new Headers();
|
||||||
|
|||||||
@ -4,6 +4,11 @@
|
|||||||
"icon": "calendar",
|
"icon": "calendar",
|
||||||
"url": "/medico/agendamento"
|
"url": "/medico/agendamento"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Exceções de Disponibilidade",
|
||||||
|
"icon": "calendar-x-fill",
|
||||||
|
"url": "/medico/excecoes-disponibilidade"
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "Relatórios",
|
"name": "Relatórios",
|
||||||
@ -17,5 +22,4 @@
|
|||||||
"url": "/medico/chat"
|
"url": "/medico/chat"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|||||||
@ -8,11 +8,14 @@ import "./style/Agendamento.css";
|
|||||||
import './style/FilaEspera.css';
|
import './style/FilaEspera.css';
|
||||||
import { useAuth } from '../components/utils/AuthProvider';
|
import { useAuth } from '../components/utils/AuthProvider';
|
||||||
import API_KEY from '../components/utils/apiKeys';
|
import API_KEY from '../components/utils/apiKeys';
|
||||||
|
import { GetAllDoctors } from '../components/utils/Functions-Endpoints/Doctor';
|
||||||
|
import { UserInfos } from '../components/utils/Functions-Endpoints/General';
|
||||||
|
|
||||||
dayjs.extend(weekday);
|
dayjs.extend(weekday);
|
||||||
dayjs.locale('pt-br');
|
dayjs.locale('pt-br');
|
||||||
|
|
||||||
const ENDPOINT_BASE = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_exceptions";
|
const ENDPOINT_BASE = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_exceptions";
|
||||||
|
const API_ROOT = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1";
|
||||||
|
|
||||||
const getDateRange = (date, view) => {
|
const getDateRange = (date, view) => {
|
||||||
const startDayjs = dayjs(date);
|
const startDayjs = dayjs(date);
|
||||||
@ -99,7 +102,52 @@ const ExcecoesDisponibilidade = () => {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (response.ok && Array.isArray(result)) {
|
if (response.ok && Array.isArray(result)) {
|
||||||
setExcecoes(result);
|
// usar função existente para obter lista de médicos
|
||||||
|
const doctorIds = Array.from(new Set(result.map(r => r.doctor_id).filter(Boolean)));
|
||||||
|
let doctors = [];
|
||||||
|
try {
|
||||||
|
// GetAllDoctors já retorna id e full_name (ver Doctor.js)
|
||||||
|
doctors = await GetAllDoctors(authHeader);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Falha ao obter lista de médicos via GetAllDoctors:', err);
|
||||||
|
doctors = [];
|
||||||
|
}
|
||||||
|
const doctorMap = {};
|
||||||
|
if (Array.isArray(doctors)) {
|
||||||
|
doctors.forEach(d => {
|
||||||
|
if (d && d.id != null) doctorMap[d.id] = d.full_name || d.name || d.display_name || d.id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// buscar nomes de quem criou (profiles) — usa REST direto (não há função pronta)
|
||||||
|
const createdByIds = Array.from(new Set(result.map(r => r.created_by).filter(Boolean)));
|
||||||
|
const creatorMap = {};
|
||||||
|
if (createdByIds.length > 0) {
|
||||||
|
const inList = createdByIds.map(id => (isNaN(Number(id)) ? `"${String(id).replace(/"/g,'\\"')}"` : id)).join(',');
|
||||||
|
const profilesUrl = `${API_ROOT}/profiles?select=*&id=in.(${inList})`;
|
||||||
|
try {
|
||||||
|
const resProfiles = await fetch(profilesUrl, { method: 'GET', headers: myHeaders, redirect: 'follow' });
|
||||||
|
if (resProfiles.ok) {
|
||||||
|
const profiles = await resProfiles.json();
|
||||||
|
profiles.forEach(p => {
|
||||||
|
creatorMap[p.id] = p.full_name || p.name || p.username || p.email || p.id;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('Não foi possível buscar profiles:', resProfiles.status);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Erro ao buscar profiles:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// anexa nomes às exceções (fallback para ids caso não encontre nome)
|
||||||
|
const enriched = result.map(r => ({
|
||||||
|
...r,
|
||||||
|
doctor_name: doctorMap[r.doctor_id] || r.doctor_name || r.doctor_id,
|
||||||
|
created_by_name: creatorMap[r.created_by] || r.created_by_name || r.created_by
|
||||||
|
}));
|
||||||
|
|
||||||
|
setExcecoes(enriched);
|
||||||
} else {
|
} else {
|
||||||
setExcecoes([]);
|
setExcecoes([]);
|
||||||
console.error("Erro ao listar exceções (Status:", response.status, "):", result);
|
console.error("Erro ao listar exceções (Status:", response.status, "):", result);
|
||||||
@ -232,7 +280,7 @@ const ExcecoesDisponibilidade = () => {
|
|||||||
<table className="fila-tabela">
|
<table className="fila-tabela">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Médico (ID)</th>
|
<th>Médico (Nome)</th>
|
||||||
<th>Data</th>
|
<th>Data</th>
|
||||||
<th>Início</th>
|
<th>Início</th>
|
||||||
<th>Término</th>
|
<th>Término</th>
|
||||||
@ -244,12 +292,12 @@ const ExcecoesDisponibilidade = () => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{excecoes.map((exc) => (
|
{excecoes.map((exc) => (
|
||||||
<tr key={exc.id}>
|
<tr key={exc.id}>
|
||||||
<td><p>{exc.doctor_id}</p></td>
|
<td><p>{exc.doctor_name || exc.doctor_id}</p></td>
|
||||||
<td>{dayjs(exc.date).format('DD/MM/YYYY')}</td>
|
<td>{dayjs(exc.date).format('DD/MM/YYYY')}</td>
|
||||||
<td>{exc.start_time ? dayjs(exc.start_time, 'HH:mm:ss').format('HH:mm') : '—'}</td>
|
<td>{exc.start_time ? dayjs(exc.start_time, 'HH:mm:ss').format('HH:mm') : '—'}</td>
|
||||||
<td>{exc.end_time ? dayjs(exc.end_time, 'HH:mm:ss').format('HH:mm') : '—'}</td>
|
<td>{exc.end_time ? dayjs(exc.end_time, 'HH:mm:ss').format('HH:mm') : '—'}</td>
|
||||||
<td><p>{exc.reason}</p></td>
|
<td><p>{exc.reason}</p></td>
|
||||||
<td>{exc.created_by || '—'}</td>
|
<td>{exc.created_by_name || exc.created_by || '—'}</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="d-flex gap-2">
|
<div className="d-flex gap-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
249
src/pages/ExcecoesDisponibilidadeDoctor.jsx
Normal file
249
src/pages/ExcecoesDisponibilidadeDoctor.jsx
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import weekday from 'dayjs/plugin/weekday';
|
||||||
|
import 'dayjs/locale/pt-br';
|
||||||
|
import { useAuth } from '../components/utils/AuthProvider'
|
||||||
|
import FormCriarExcecao from '../components/FormCriarExcecao';
|
||||||
|
|
||||||
|
dayjs.extend(weekday);
|
||||||
|
dayjs.locale('pt-br');
|
||||||
|
|
||||||
|
const ENDPOINT_BASE = 'https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_exceptions';
|
||||||
|
|
||||||
|
const getDateRange = (date, view) => {
|
||||||
|
const base = dayjs(date);
|
||||||
|
let fromDate, toDate, titleRange;
|
||||||
|
|
||||||
|
if (view === 'diario') {
|
||||||
|
fromDate = base.startOf('day').format('YYYY-MM-DD');
|
||||||
|
toDate = base.endOf('day').format('YYYY-MM-DD');
|
||||||
|
titleRange = base.format('DD/MM/YYYY');
|
||||||
|
} else if (view === 'semanal') {
|
||||||
|
fromDate = base.startOf('week').format('YYYY-MM-DD');
|
||||||
|
toDate = base.endOf('week').format('YYYY-MM-DD');
|
||||||
|
titleRange = `${base.startOf('week').format('DD/MM')} - ${base.endOf('week').format('DD/MM')}`;
|
||||||
|
} else { // mensal
|
||||||
|
fromDate = base.startOf('month').format('YYYY-MM-DD');
|
||||||
|
toDate = base.endOf('month').format('YYYY-MM-DD');
|
||||||
|
titleRange = base.format('MMMM YYYY');
|
||||||
|
}
|
||||||
|
return { fromDate, toDate, titleRange };
|
||||||
|
};
|
||||||
|
|
||||||
|
const ExcecoesDisponibilidadeDoctor = () => {
|
||||||
|
const { getAuthorizationHeader, user } = useAuth();
|
||||||
|
const [excecoes, setExcecoes] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [visualizacao, setVisualizacao] = useState('diario');
|
||||||
|
const [dataFiltro, setDataFiltro] = useState(dayjs().format('YYYY-MM-DD'));
|
||||||
|
const [mostrarForm, setMostrarForm] = useState(false);
|
||||||
|
const [erro, setErro] = useState('');
|
||||||
|
const [buscaTexto, setBuscaTexto] = useState('');
|
||||||
|
|
||||||
|
const doctorID = user?.doctor_id || user?.id; // ajuste conforme estrutura real
|
||||||
|
|
||||||
|
const { fromDate, toDate, titleRange } = useMemo(
|
||||||
|
() => getDateRange(dataFiltro, visualizacao),
|
||||||
|
[dataFiltro, visualizacao]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchExcecoes = useCallback(async () => {
|
||||||
|
if (!doctorID) return;
|
||||||
|
setLoading(true);
|
||||||
|
setErro('');
|
||||||
|
try {
|
||||||
|
// trata getAuthorizationHeader() que pode retornar objeto ou string
|
||||||
|
const maybeAuth = getAuthorizationHeader();
|
||||||
|
const headers = {};
|
||||||
|
if (typeof maybeAuth === 'string') {
|
||||||
|
headers['Authorization'] = maybeAuth;
|
||||||
|
} else if (maybeAuth && typeof maybeAuth === 'object') {
|
||||||
|
Object.assign(headers, maybeAuth);
|
||||||
|
}
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
headers['Prefer'] = 'count=exact';
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append('doctor_id', `eq.${doctorID}`);
|
||||||
|
params.append('date', `gte.${fromDate}`);
|
||||||
|
params.append('date', `lte.${toDate}`);
|
||||||
|
params.append('order', 'date.asc');
|
||||||
|
|
||||||
|
const res = await fetch(`${ENDPOINT_BASE}?${params.toString()}`, {
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('Falha ao carregar exceções');
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
setExcecoes(Array.isArray(data) ? data : []);
|
||||||
|
} catch (e) {
|
||||||
|
setErro(e.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [doctorID, fromDate, toDate, getAuthorizationHeader]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchExcecoes();
|
||||||
|
}, [fetchExcecoes]);
|
||||||
|
|
||||||
|
const handleDelete = async (id) => {
|
||||||
|
if (!window.confirm('Remover esta exceção?')) return;
|
||||||
|
try {
|
||||||
|
const maybeAuth = getAuthorizationHeader();
|
||||||
|
const headers = {};
|
||||||
|
if (typeof maybeAuth === 'string') {
|
||||||
|
headers['Authorization'] = maybeAuth;
|
||||||
|
} else if (maybeAuth && typeof maybeAuth === 'object') {
|
||||||
|
Object.assign(headers, maybeAuth);
|
||||||
|
}
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
|
||||||
|
const res = await fetch(`${ENDPOINT_BASE}?id=eq.${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Erro ao deletar');
|
||||||
|
setExcecoes(prev => prev.filter(e => e.id !== id));
|
||||||
|
} catch (e) {
|
||||||
|
alert(e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const excecoesFiltradas = useMemo(() => {
|
||||||
|
if (!buscaTexto.trim()) return excecoes;
|
||||||
|
const txt = buscaTexto.toLowerCase();
|
||||||
|
return excecoes.filter(e =>
|
||||||
|
(e.reason || '').toLowerCase().includes(txt) ||
|
||||||
|
(e.kind || '').toLowerCase().includes(txt)
|
||||||
|
);
|
||||||
|
}, [buscaTexto, excecoes]);
|
||||||
|
|
||||||
|
const mudarData = (delta) => {
|
||||||
|
const base = dayjs(dataFiltro);
|
||||||
|
let nova;
|
||||||
|
if (visualizacao === 'diario') nova = base.add(delta, 'day');
|
||||||
|
else if (visualizacao === 'semanal') nova = base.add(delta, 'week');
|
||||||
|
else nova = base.add(delta, 'month');
|
||||||
|
setDataFiltro(nova.format('YYYY-MM-DD'));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mostrarForm) {
|
||||||
|
return (
|
||||||
|
<div className="container mt-3">
|
||||||
|
<h4>Nova Exceção</h4>
|
||||||
|
<FormCriarExcecao
|
||||||
|
doctorID={doctorID}
|
||||||
|
onCancel={(reload) => {
|
||||||
|
setMostrarForm(false);
|
||||||
|
if (reload) fetchExcecoes();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mt-3">
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4>Exceções de Disponibilidade</h4>
|
||||||
|
<button className="btn btn-primary" onClick={() => setMostrarForm(true)}>
|
||||||
|
Criar Exceção
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="d-flex gap-2 flex-wrap mb-3">
|
||||||
|
<div className="btn-group">
|
||||||
|
<button
|
||||||
|
className={`btn btn-sm ${visualizacao === 'diario' ? 'btn-secondary' : 'btn-outline-secondary'}`}
|
||||||
|
onClick={() => setVisualizacao('diario')}
|
||||||
|
>
|
||||||
|
Diário
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`btn btn-sm ${visualizacao === 'semanal' ? 'btn-secondary' : 'btn-outline-secondary'}`}
|
||||||
|
onClick={() => setVisualizacao('semanal')}
|
||||||
|
>
|
||||||
|
Semanal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`btn btn-sm ${visualizacao === 'mensal' ? 'btn-secondary' : 'btn-outline-secondary'}`}
|
||||||
|
onClick={() => setVisualizacao('mensal')}
|
||||||
|
>
|
||||||
|
Mensal
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="d-flex align-items-center gap-2">
|
||||||
|
<button className="btn btn-outline-dark btn-sm" onClick={() => mudarData(-1)}><</button>
|
||||||
|
<strong>{titleRange}</strong>
|
||||||
|
<button className="btn btn-outline-dark btn-sm" onClick={() => mudarData(1)}>></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
value={dataFiltro}
|
||||||
|
onChange={e => setDataFiltro(e.target.value)}
|
||||||
|
style={{ maxWidth: 160 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Buscar (motivo / tipo)"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
value={buscaTexto}
|
||||||
|
onChange={e => setBuscaTexto(e.target.value)}
|
||||||
|
style={{ flex: 1, minWidth: 180 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{erro && <div className="alert alert-danger py-1">{erro}</div>}
|
||||||
|
{loading && <div>Carregando...</div>}
|
||||||
|
|
||||||
|
{!loading && excecoesFiltradas.length === 0 && (
|
||||||
|
<div className="alert alert-info py-1">Nenhuma exceção neste intervalo.</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && excecoesFiltradas.length > 0 && (
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table table-sm table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Data</th>
|
||||||
|
<th>Início</th>
|
||||||
|
<th>Término</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Motivo</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{excecoesFiltradas.map(ex => (
|
||||||
|
<tr key={ex.id}>
|
||||||
|
<td>{ex.date ? dayjs(ex.date).format('DD/MM/YYYY') : '-'}</td>
|
||||||
|
<td>{ex.start_time || ex.inicio || '-'}</td>
|
||||||
|
<td>{ex.end_time || ex.termino || '-'}</td>
|
||||||
|
<td>{ex.kind || ex.tipoAtendimento || '-'}</td>
|
||||||
|
<td>{ex.reason || ex.motivo || '-'}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-danger btn-sm"
|
||||||
|
onClick={() => handleDelete(ex.id)}
|
||||||
|
>
|
||||||
|
Remover
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExcecoesDisponibilidadeDoctor;
|
||||||
@ -11,6 +11,8 @@ import FormNovoRelatorio from "../../PagesMedico/FormNovoRelatorio";
|
|||||||
import EditPageRelatorio from "../../PagesMedico/EditPageRelatorio";
|
import EditPageRelatorio from "../../PagesMedico/EditPageRelatorio";
|
||||||
import BotaoVideoChamada from '../../components/BotaoVideoChamada';
|
import BotaoVideoChamada from '../../components/BotaoVideoChamada';
|
||||||
|
|
||||||
|
import ExcecoesDisponibilidadeDoctor from "../../pages/ExcecoesDisponibilidadeDoctor.jsx";
|
||||||
|
|
||||||
import DoctorAgendamentoEditPage from "../../PagesMedico/DoctorAgendamentoEditPage";
|
import DoctorAgendamentoEditPage from "../../PagesMedico/DoctorAgendamentoEditPage";
|
||||||
|
|
||||||
function PerfilMedico() {
|
function PerfilMedico() {
|
||||||
@ -31,6 +33,7 @@ function PerfilMedico() {
|
|||||||
<Route path="/relatorios" element={<DoctorRelatorioManager />} />
|
<Route path="/relatorios" element={<DoctorRelatorioManager />} />
|
||||||
<Route path="/agendamento" element={<DoctorAgendamentoManager setDictInfo={setDictInfo}/>} />
|
<Route path="/agendamento" element={<DoctorAgendamentoManager setDictInfo={setDictInfo}/>} />
|
||||||
<Route path="/agendamento/edit" element={<DoctorAgendamentoEditPage DictInfo={dictInfo} setDictInfo={setDictInfo}/>} />
|
<Route path="/agendamento/edit" element={<DoctorAgendamentoEditPage DictInfo={dictInfo} setDictInfo={setDictInfo}/>} />
|
||||||
|
<Route path="/excecoes-disponibilidade" element={<ExcecoesDisponibilidadeDoctor />} />
|
||||||
<Route path="/chat" element={<Chat />} />
|
<Route path="/chat" element={<Chat />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user