novo agendamento
This commit is contained in:
parent
d6bd5b955a
commit
2647691ae2
6432
package-lock.json
generated
6432
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -5,6 +5,14 @@
|
||||
"dependencies": {
|
||||
"@ckeditor/ckeditor5-build-classic": "^41.4.2",
|
||||
"@ckeditor/ckeditor5-react": "^11.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||
"@fortawesome/react-fontawesome": "^3.1.0",
|
||||
"@fullcalendar/core": "^6.1.19",
|
||||
"@fullcalendar/daygrid": "^6.1.19",
|
||||
"@fullcalendar/interaction": "^6.1.19",
|
||||
"@fullcalendar/react": "^6.1.19",
|
||||
"@fullcalendar/timegrid": "^6.1.19",
|
||||
"@jitsi/react-sdk": "^1.4.0",
|
||||
"@sweetalert2/theme-dark": "^5.0.27",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
@ -20,7 +28,7 @@
|
||||
"bootstrap": "^5.3.8",
|
||||
"bootstrap-icons": "^1.13.1",
|
||||
"cors": "^2.8.5",
|
||||
"dayjs": "^1.11.18",
|
||||
"dayjs": "^1.11.19",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.1.0",
|
||||
"firebase": "^12.5.0",
|
||||
@ -44,6 +52,7 @@
|
||||
"react-quill": "^2.0.0",
|
||||
"react-router-dom": "^7.9.2",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-toastify": "^11.0.5",
|
||||
"recharts": "^3.1.2",
|
||||
"sweetalert2": "^11.22.4",
|
||||
"tiptap": "^1.32.2",
|
||||
@ -84,4 +93,4 @@
|
||||
"overrides": {
|
||||
"react": "$react"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,598 +1,364 @@
|
||||
import React from 'react'
|
||||
import "./style.css"
|
||||
import "../pages/style/TablePaciente.css"
|
||||
import CardConsultaPaciente from './CardConsultaPaciente'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useEffect, useState, useMemo } from 'react'
|
||||
import API_KEY from '../components/utils/apiKeys'
|
||||
import { useAuth } from '../components/utils/AuthProvider'
|
||||
import { GetPatientByID } from '../components/utils/Functions-Endpoints/Patient'
|
||||
import { GetDoctorByID } from '../components/utils/Functions-Endpoints/Doctor'
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import API_KEY from '../components/utils/apiKeys.js';
|
||||
import AgendamentoCadastroManager from '../pages/AgendamentoCadastroManager.jsx';
|
||||
import { GetPatientByID } from '../components/utils/Functions-Endpoints/Patient.js';
|
||||
import { GetAllDoctors, GetDoctorByID } from '../components/utils/Functions-Endpoints/Doctor.js';
|
||||
import { useAuth } from '../components/utils/AuthProvider.js';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/pt-br';
|
||||
import isBetween from 'dayjs/plugin/isBetween';
|
||||
import localeData from 'dayjs/plugin/localeData';
|
||||
import { ChevronLeft, ChevronRight, Edit, Trash2 } from 'lucide-react';
|
||||
import "../pages/style/Agendamento.css";
|
||||
import '../pages/style/FilaEspera.css';
|
||||
import Spinner from '../components/Spinner.jsx';
|
||||
|
||||
import { UserInfos } from '../components/utils/Functions-Endpoints/General'
|
||||
import dayjs from 'dayjs'
|
||||
import TabelaAgendamentoDia from "../components/AgendarConsulta/TabelaAgendamentoDia"
|
||||
|
||||
const ConsultasPaciente = ({ setDictInfo }) => {
|
||||
const { getAuthorizationHeader } = useAuth()
|
||||
const [agendamentosOrganizados, setAgendamentosOrganizados] = useState({})
|
||||
const [listaTodasConsultas, setListaTodasConsultas] = useState([])
|
||||
const [patientID, setPatientID] = useState("")
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
const [selectedID, setSelectedId] = useState("")
|
||||
let authHeader = getAuthorizationHeader()
|
||||
|
||||
const [motivoCancelamento, setMotivoCancelamento] = useState("")
|
||||
|
||||
const [consultas, setConsultas] = useState([])
|
||||
|
||||
const [consultasOrganizadas, setConsultasOrganizadas] = useState({})
|
||||
const [filaDeEspera, setFilaDeEspera] = useState([])
|
||||
const [viewFila, setViewFila] = useState(false)
|
||||
|
||||
const [listaConsultasID, setListaConsultaID] = useState([])
|
||||
const [coresConsultas,setCoresConsultas] = useState([])
|
||||
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
||||
dayjs.locale('pt-br');
|
||||
dayjs.extend(isBetween);
|
||||
dayjs.extend(localeData);
|
||||
|
||||
const Agendamento = ({ setDictInfo }) => {
|
||||
const navigate = useNavigate();
|
||||
const [listaTodosAgendamentos, setListaTodosAgendamentos] = useState([]);
|
||||
const [selectedID, setSelectedId] = useState('0');
|
||||
const [filaEsperaData, setFilaEsperaData] = useState([]);
|
||||
const [FiladeEspera, setFiladeEspera] = useState(false);
|
||||
const [PageNovaConsulta, setPageConsulta] = useState(false);
|
||||
const { getAuthorizationHeader } = useAuth();
|
||||
const [DictAgendamentosOrganizados, setAgendamentosOrganizados] = useState({});
|
||||
const [ListaDeMedicos, setListaDeMedicos] = useState([]);
|
||||
const [showSpinner, setShowSpinner] = useState(true);
|
||||
const [waitlistSearch, setWaitlistSearch] = useState('');
|
||||
const [waitSortKey, setWaitSortKey] = useState(null); // 'paciente' | 'medico' | 'data' | null
|
||||
const [waitSortDir, setWaitSortDir] = useState('asc'); // 'asc' | 'desc'
|
||||
const [waitSortKey, setWaitSortKey] = useState(null);
|
||||
const [waitSortDir, setWaitSortDir] = useState('asc');
|
||||
const [waitPage, setWaitPage] = useState(1);
|
||||
const [waitPerPage, setWaitPerPage] = useState(10);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
console.log(listaConsultasID, coresConsultas, "ojwhdofigewfey7few0fr74r")
|
||||
|
||||
}, [coresConsultas, listaConsultasID])
|
||||
|
||||
useMemo(() => {
|
||||
let conjuntoConsultas = {}
|
||||
let filaEspera = []
|
||||
|
||||
const fetchInfosConsultas = async (consulta) => {
|
||||
//console.log(doctor, "PACIENTE TRAZIDO PELO ")
|
||||
|
||||
//let consultaMelhorada = {...consulta, paciente_nome:paciente[0].full_name, medico_nome:doctor[0].full_name }
|
||||
|
||||
//console.log(consultaMelhorada,"ID DO MEDICO")
|
||||
|
||||
for(let i = 0; listaTodasConsultas.length > i; i++){
|
||||
|
||||
let consulta = listaTodasConsultas[i]
|
||||
|
||||
let doctor = await GetDoctorByID(consulta.doctor_id, authHeader)
|
||||
let paciente = await GetPatientByID(consulta.patient_id, authHeader)
|
||||
|
||||
consulta = {...consulta, medico_nome:doctor[0]?.full_name, paciente_nome:paciente[0]?.full_name}
|
||||
|
||||
|
||||
|
||||
|
||||
if(consulta.status === "requested"){
|
||||
|
||||
filaEspera.push(consulta)
|
||||
|
||||
}else{
|
||||
|
||||
let data = consulta.scheduled_at.split("T")[0]
|
||||
let chavesConsultas = Object.keys(conjuntoConsultas)
|
||||
|
||||
if(chavesConsultas.includes(data)){
|
||||
let lista = conjuntoConsultas[data]
|
||||
|
||||
lista.push(consulta)
|
||||
|
||||
conjuntoConsultas = {...conjuntoConsultas, [data]:lista}
|
||||
}else{
|
||||
conjuntoConsultas = {...conjuntoConsultas, [data]:[consulta] }
|
||||
}
|
||||
}
|
||||
}
|
||||
const authHeader = getAuthorizationHeader();
|
||||
const cacheMedicos = useMemo(() => ({}), []);
|
||||
const cachePacientes = useMemo(() => ({}), []);
|
||||
const [currentDate, setCurrentDate] = useState(dayjs());
|
||||
const [selectedDay, setSelectedDay] = useState(dayjs());
|
||||
|
||||
const [quickJump, setQuickJump] = useState({
|
||||
month: currentDate.month(),
|
||||
year: currentDate.year()
|
||||
});
|
||||
|
||||
setConsultasOrganizadas(conjuntoConsultas)
|
||||
setFilaDeEspera(filaEspera)
|
||||
|
||||
}
|
||||
|
||||
console.log("so muda")
|
||||
if(!listaTodasConsultas.length) return
|
||||
|
||||
console.log(filaEspera, "fila de espera")
|
||||
fetchInfosConsultas();
|
||||
|
||||
}, [listaTodasConsultas])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let userInfos = UserInfos(authHeader)
|
||||
|
||||
const fetchConsultas = async () => {
|
||||
try {
|
||||
const myHeaders = new Headers();
|
||||
myHeaders.append("Authorization", authHeader);
|
||||
myHeaders.append("apikey", API_KEY)
|
||||
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: myHeaders,
|
||||
redirect: 'follow'
|
||||
};
|
||||
|
||||
const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?patient_id=eq.${"6e7f8829-0574-42df-9290-8dbb70f75ada"}`, requestOptions);
|
||||
const result = await response.json();
|
||||
setListaTodasConsultas(result);
|
||||
} catch (error) {
|
||||
console.log('error', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchConsultas();
|
||||
}, []);
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
|
||||
const confirmConsulta = (selectedPatientId) => {
|
||||
var myHeaders = new Headers();
|
||||
myHeaders.append("Content-Type", "application/json");
|
||||
myHeaders.append('apikey', API_KEY)
|
||||
myHeaders.append("authorization", authHeader)
|
||||
|
||||
|
||||
var raw = JSON.stringify({ "status":"confirmed"
|
||||
});
|
||||
|
||||
|
||||
var requestOptions = {
|
||||
method: 'PATCH',
|
||||
headers: myHeaders,
|
||||
body: raw,
|
||||
redirect: 'follow'
|
||||
};
|
||||
|
||||
fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${selectedPatientId}`, requestOptions)
|
||||
.then(response => {if(response.status !== 200)(console.log(response))})
|
||||
.then(result => console.log(result))
|
||||
.catch(error => console.log('error', error));
|
||||
|
||||
}
|
||||
|
||||
const deleteConsulta = async (ID) => {
|
||||
const fetchAppointments = async () => {
|
||||
const myHeaders = new Headers(); myHeaders.append("Authorization", authHeader); myHeaders.append("apikey", API_KEY);
|
||||
const requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow' };
|
||||
try {
|
||||
const myHeaders = new Headers();
|
||||
myHeaders.append("Content-Type", "application/json");
|
||||
myHeaders.append('apikey', API_KEY);
|
||||
myHeaders.append("authorization", authHeader);
|
||||
|
||||
const raw = JSON.stringify({ "status": "cancelled", "cancellation_reason":motivoCancelamento });
|
||||
|
||||
const requestOptions = {
|
||||
method: 'PATCH',
|
||||
headers: myHeaders,
|
||||
body: raw,
|
||||
redirect: 'follow'
|
||||
};
|
||||
|
||||
const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${ID}`, requestOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Falha ao cancelar consulta: ${response.status} - ${errorText}`);
|
||||
}
|
||||
setConsultas(prevConsultas => prevConsultas.filter(consulta => consulta.id !== ID));
|
||||
|
||||
console.log("Consulta cancelada com sucesso!");
|
||||
alert("Consulta cancelada com sucesso!");
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro ao cancelar a consulta:', error);
|
||||
alert('Erro ao cancelar a consulta. Veja o console.');
|
||||
const res = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?select=*", requestOptions);
|
||||
const data = await res.json();
|
||||
setListaTodosAgendamentos(data);
|
||||
} catch (err) {
|
||||
console.error('Erro ao buscar agendamentos', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateAppointmentStatus = async (id, updates) => {
|
||||
const myHeaders = new Headers(); myHeaders.append("Authorization", authHeader); myHeaders.append("apikey", API_KEY); myHeaders.append("Content-Type", "application/json"); myHeaders.append("Prefer", "return=representation");
|
||||
const requestOptions = { method: 'PATCH', headers: myHeaders, body: JSON.stringify(updates) };
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${id}`, requestOptions);
|
||||
if (response.ok) {
|
||||
await fetchAppointments();
|
||||
return true;
|
||||
} else {
|
||||
console.error('Erro ao atualizar agendamento:', await response.text());
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro de rede/servidor:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const deleteConsulta = async (id) => {
|
||||
const confirmation = window.confirm("Tem certeza que deseja CANCELAR este agendamento? Esta ação é irreversível.");
|
||||
if (!confirmation) return;
|
||||
|
||||
setShowSpinner(true);
|
||||
const success = await updateAppointmentStatus(id, { status: "cancelled", cancellation_reason: "Cancelado pela Secretaria", updated_at: new Date().toISOString() });
|
||||
setShowSpinner(false);
|
||||
if (success) {
|
||||
alert("Consulta cancelada com sucesso!");
|
||||
} else {
|
||||
alert("Falha ao cancelar a consulta.");
|
||||
}
|
||||
};
|
||||
const confirmConsulta = async (id) => {
|
||||
const confirmation = window.confirm("Tem certeza que deseja CONFIRMAR/REVERTER o cancelamento deste agendamento para 'agendado'?");
|
||||
if (!confirmation) return;
|
||||
setShowSpinner(true);
|
||||
const success = await updateAppointmentStatus(id, { status: "agendado", cancellation_reason: null, updated_at: new Date().toISOString() });
|
||||
setShowSpinner(false);
|
||||
if (success) {
|
||||
alert("Agendamento confirmado/revertido para 'agendado' com sucesso!");
|
||||
} else {
|
||||
alert("Falha ao reverter o cancelamento.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickJumpChange = (type, value) => {
|
||||
setQuickJump(prev => ({ ...prev, [type]: Number(value) }));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setQuickJump({
|
||||
month: currentDate.month(),
|
||||
year: currentDate.year()
|
||||
});
|
||||
}, [currentDate]);
|
||||
|
||||
const applyQuickJump = () => {
|
||||
let newDate = dayjs().year(quickJump.year).month(quickJump.month).date(1);
|
||||
setCurrentDate(newDate);
|
||||
setSelectedDay(newDate);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!listaTodosAgendamentos.length) { setShowSpinner(false); return; }
|
||||
setShowSpinner(true);
|
||||
const fetchDados = async () => {
|
||||
const newDict = {}; const newFila = [];
|
||||
for (const agendamento of listaTodosAgendamentos) {
|
||||
if (!agendamento.doctor_id || !agendamento.patient_id) continue;
|
||||
if (!cacheMedicos[agendamento.doctor_id]) { cacheMedicos[agendamento.doctor_id] = (await GetDoctorByID(agendamento.doctor_id, authHeader))[0]; }
|
||||
if (!cachePacientes[agendamento.patient_id]) { cachePacientes[agendamento.patient_id] = (await GetPatientByID(agendamento.patient_id, authHeader))[0]; }
|
||||
const medico = cacheMedicos[agendamento.doctor_id];
|
||||
const paciente = cachePacientes[agendamento.patient_id];
|
||||
|
||||
const agendamentoMelhorado = { ...agendamento, medico_nome: medico?.full_name, paciente_nome: paciente?.full_name, paciente_cpf: paciente?.cpf };
|
||||
|
||||
if (agendamento.status === "requested") {
|
||||
newFila.push({ agendamento: agendamentoMelhorado, Infos: agendamentoMelhorado });
|
||||
}
|
||||
else {
|
||||
const DiaAgendamento = dayjs(agendamento.scheduled_at).format("YYYY-MM-DD");
|
||||
if (newDict[DiaAgendamento]) {
|
||||
newDict[DiaAgendamento].push(agendamentoMelhorado);
|
||||
}
|
||||
else {
|
||||
newDict[DiaAgendamento] = [agendamentoMelhorado];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const key in newDict) { newDict[key].sort((a, b) => a.scheduled_at.localeCompare(b.scheduled_at)); }
|
||||
|
||||
setAgendamentosOrganizados(newDict);
|
||||
setFilaEsperaData(newFila);
|
||||
setShowSpinner(false);
|
||||
};
|
||||
fetchDados();
|
||||
}, [listaTodosAgendamentos, authHeader, cacheMedicos, cachePacientes]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAppointments();
|
||||
GetAllDoctors(authHeader).then(docs => setListaDeMedicos(docs.map(d => ({ nomeMedico: d.full_name, idMedico: d.id }))));
|
||||
}, [authHeader]);
|
||||
|
||||
const filaEsperaFiltrada = useMemo(() => {
|
||||
if (!waitlistSearch.trim()) return filaDeEspera;
|
||||
if (!waitlistSearch.trim()) return filaEsperaData;
|
||||
const term = waitlistSearch.toLowerCase();
|
||||
return filaDeEspera.filter(item => {
|
||||
const paciente = item?.paciente_nome?.toLowerCase() || '';
|
||||
const medico = item?.medico_nome?.toLowerCase() || '';
|
||||
return paciente.includes(term) || medico.includes(term);
|
||||
});
|
||||
}, [waitlistSearch, filaDeEspera]);
|
||||
|
||||
|
||||
return filaEsperaData.filter(item => (item?.Infos?.paciente_nome?.toLowerCase() || '').includes(term) || (item?.Infos?.paciente_cpf?.toLowerCase() || '').includes(term) || (item?.Infos?.nome_medico?.toLowerCase() || '').includes(term));
|
||||
}, [waitlistSearch, filaEsperaData]);
|
||||
|
||||
const applySortingWaitlist = (arr) => {
|
||||
if (!Array.isArray(arr) || !waitSortKey) return arr;
|
||||
const copy = [...arr];
|
||||
if (waitSortKey === 'paciente') {
|
||||
copy.sort((a, b) => (a?.paciente_nome || '').localeCompare((b?.paciente_nome || ''), undefined, { sensitivity: 'base' }));
|
||||
} else if (waitSortKey === 'medico') {
|
||||
copy.sort((a, b) => (a?.medico_nome || '').localeCompare((b?.medico_nome || ''), undefined, { sensitivity: 'base' }));
|
||||
} else if (waitSortKey === 'data') {
|
||||
copy.sort((a, b) => new Date(a?.created_at || 0) - new Date(b?.created_at || 0));
|
||||
}
|
||||
if (waitSortKey === 'paciente') { copy.sort((a, b) => (a?.Infos?.paciente_nome || '').localeCompare((b?.Infos?.paciente_nome || ''))); }
|
||||
else if (waitSortKey === 'medico') { copy.sort((a, b) => (a?.Infos?.nome_medico || '').localeCompare((b?.Infos?.nome_medico || ''))); }
|
||||
else if (waitSortKey === 'data') { copy.sort((a, b) => new Date(a?.agendamento?.scheduled_at || 0) - new Date(b?.agendamento?.scheduled_at || 0)); }
|
||||
if (waitSortDir === 'desc') copy.reverse();
|
||||
return copy;
|
||||
};
|
||||
|
||||
const filaEsperaOrdenada = applySortingWaitlist(filaEsperaFiltrada);
|
||||
|
||||
|
||||
const waitTotalPages = Math.ceil(filaEsperaFiltrada.length / waitPerPage) || 1;
|
||||
const waitTotalPages = Math.ceil(filaEsperaOrdenada.length / waitPerPage) || 1;
|
||||
const waitIndiceInicial = (waitPage - 1) * waitPerPage;
|
||||
const waitIndiceFinal = waitIndiceInicial + waitPerPage;
|
||||
const filaEsperaPaginada = filaEsperaOrdenada.slice(waitIndiceInicial, waitIndiceFinal);
|
||||
|
||||
const gerarNumerosWaitPages = () => {
|
||||
const paginas = [];
|
||||
const paginasParaMostrar = 5;
|
||||
const paginas = []; const paginasParaMostrar = 5;
|
||||
let inicio = Math.max(1, waitPage - Math.floor(paginasParaMostrar / 2));
|
||||
let fim = Math.min(waitTotalPages, inicio + paginasParaMostrar - 1);
|
||||
inicio = Math.max(1, fim - paginasParaMostrar + 1);
|
||||
for (let i = inicio; i <= fim; i++) {
|
||||
paginas.push(i);
|
||||
}
|
||||
for (let i = inicio; i <= fim; i++) { paginas.push(i); }
|
||||
return paginas;
|
||||
};
|
||||
useEffect(() => { setWaitPage(1); }, [waitlistSearch, waitSortKey, waitSortDir]);
|
||||
|
||||
useEffect(() => {
|
||||
setWaitPage(1);
|
||||
}, [waitlistSearch, waitSortKey, waitSortDir]);
|
||||
// Funções do Calendário
|
||||
const generateDateGrid = () => {
|
||||
const grid = []; const startOfMonth = currentDate.startOf('month');
|
||||
let currentDay = startOfMonth.subtract(startOfMonth.day(), 'day');
|
||||
for (let i = 0; i < 42; i++) { grid.push(currentDay); currentDay = currentDay.add(1, 'day'); }
|
||||
return grid;
|
||||
};
|
||||
const dateGrid = useMemo(() => generateDateGrid(), [currentDate]);
|
||||
const weekDays = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'];
|
||||
const handleDateClick = (day) => setSelectedDay(day);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1> Gerencie suas consultas</h1>
|
||||
|
||||
<div className='form-container'>
|
||||
|
||||
<div className='btns-container'>
|
||||
<button className="btn btn-primary" onClick={() => { navigate("criar") }}>
|
||||
<h1>Minhas consultas</h1>
|
||||
{}
|
||||
<div className="btns-gerenciamento-e-consulta" style={{ display: 'flex', gap: '10px', marginBottom: '20px' }}>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => { setPageConsulta(true); setFiladeEspera(false); }}
|
||||
>
|
||||
<i className="bi bi-plus-circle"></i> Adicionar Consulta
|
||||
</button>
|
||||
{!viewFila ?
|
||||
<button onClick={() => setViewFila(true)} className="btn btn-primary">Ver fila de espera</button>
|
||||
:
|
||||
<button onClick={() => setViewFila(false)} className="btn btn-primary">Ver consultas </button>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{viewFila ?
|
||||
<div className="page-content table-paciente-container">
|
||||
<section className="row">
|
||||
<div className="col-12">
|
||||
<div className="card table-paciente-card">
|
||||
<div className="card-header">
|
||||
<h4 className="card-title mb-0">Fila de Espera</h4>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{/* Filtros */}
|
||||
<div className="card p-3 mb-3 table-paciente-filters">
|
||||
<h5 className="mb-3">
|
||||
<i className="bi bi-funnel-fill me-2 text-primary"></i> Filtros
|
||||
</h5>
|
||||
|
||||
<div className="mb-3">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Buscar por paciente ou médico..."
|
||||
value={waitlistSearch}
|
||||
onChange={(e) => setWaitlistSearch(e.target.value)}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
Digite o nome do paciente ou nome do médico
|
||||
</small>
|
||||
<button
|
||||
className={`btn ${FiladeEspera ? "btn-primary" : "btn-secondary"}`}
|
||||
onClick={() => { setFiladeEspera(!FiladeEspera); setPageConsulta(false); }}
|
||||
>
|
||||
<i className="bi bi-list-task me-1"></i> Fila de Espera ({filaEsperaData.length})
|
||||
</button>
|
||||
</div>
|
||||
{}
|
||||
|
||||
{!PageNovaConsulta ? (
|
||||
<div className='atendimento-eprocura'>
|
||||
<section className='calendario-ou-filaespera'>
|
||||
{FiladeEspera === false ? (
|
||||
<div className="calendar-wrapper">
|
||||
{}
|
||||
<div className="calendar-info-panel">
|
||||
<div className="info-date-display"><span>{selectedDay.format('MMM')}</span><strong>{selectedDay.format('DD')}</strong></div>
|
||||
<div className="info-details"><h3>{selectedDay.format('dddd')}</h3><p>{selectedDay.format('D [de] MMMM [de] YYYY')}</p></div>
|
||||
<div className="appointments-list">
|
||||
<h4>Consultas para {selectedDay.format('DD/MM')}</h4>
|
||||
{showSpinner ? <Spinner/> : (DictAgendamentosOrganizados[selectedDay.format('YYYY-MM-DD')]?.length > 0) ? (DictAgendamentosOrganizados[selectedDay.format('YYYY-MM-DD')].map(app => (
|
||||
<div key={app.id} className="appointment-item" data-status={app.status}>
|
||||
<div className="item-time">{dayjs(app.scheduled_at).format('HH:mm')}</div>
|
||||
<div className="item-details"><span>{app.paciente_nome}</span><small>Dr(a). {app.medico_nome}</small></div>
|
||||
{}
|
||||
</div>
|
||||
))) : (<div className="no-appointments-info"><p>Nenhuma consulta agendada.</p></div>)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-wrap align-items-center gap-2 mb-3">
|
||||
{/* Ordenação rápida */}
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<span className="text-muted small">Ordenar por:</span>
|
||||
{(() => {
|
||||
const sortValue = waitSortKey ? `${waitSortKey}-${waitSortDir}` : '';
|
||||
return (
|
||||
{}
|
||||
<div className="calendar-main">
|
||||
<div className="calendar-legend">
|
||||
<div className="legend-item" data-status="completed">Realizado</div><div className="legend-item" data-status="confirmed">Confirmado</div><div className="legend-item" data-status="agendado">Agendado</div><div className="legend-item" data-status="cancelled">Cancelado</div>
|
||||
</div>
|
||||
<div className="calendar-controls">
|
||||
<div className="date-indicator">
|
||||
<h2>{currentDate.format('MMMM [de] YYYY')}</h2>
|
||||
<div className="quick-jump-controls" style={{ display: 'flex', gap: '5px', marginTop: '10px' }}>
|
||||
<select
|
||||
className="form-select compact-select sort-select w-auto"
|
||||
value={sortValue}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
if (!v) { setWaitSortKey(null); setWaitSortDir('asc'); return; }
|
||||
const [k, d] = v.split('-');
|
||||
setWaitSortKey(k);
|
||||
setWaitSortDir(d);
|
||||
}}
|
||||
value={quickJump.month}
|
||||
onChange={(e) => handleQuickJumpChange('month', e.target.value)}
|
||||
className="form-select form-select-sm w-auto"
|
||||
>
|
||||
<option value="">Sem ordenação</option>
|
||||
<option value="paciente-asc">Paciente (A-Z)</option>
|
||||
<option value="paciente-desc">Paciente (Z-A)</option>
|
||||
<option value="medico-asc">Médico (A-Z)</option>
|
||||
<option value="medico-desc">Médico (Z-A)</option>
|
||||
<option value="data-asc">Data (mais antiga)</option>
|
||||
<option value="data-desc">Data (mais recente)</option>
|
||||
</select>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<div className="contador-pacientes">
|
||||
{filaEsperaFiltrada.length} DE {filaDeEspera.length} SOLICITAÇÕES ENCONTRADAS
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="table-responsive">
|
||||
<table className="table table-striped table-hover table-paciente-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nome do Paciente</th>
|
||||
<th>CPF</th>
|
||||
<th>Médico Solicitado</th>
|
||||
<th>Data da Solicitação</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filaEsperaPaginada.length > 0 ? (
|
||||
filaEsperaPaginada.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td>{item?.paciente_nome}</td>
|
||||
<td>{item?.paciente_cpf}</td>
|
||||
<td>{item?.medico_nome}</td>
|
||||
<td>{dayjs(item?.created_at).format('DD/MM/YYYY HH:mm')}</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-sm btn-delete"
|
||||
onClick={() => {
|
||||
setSelectedId(item.id)
|
||||
setShowDeleteModal(true);
|
||||
}}
|
||||
>
|
||||
<i className="bi bi-trash me-1"></i> Excluir
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="5" className="text-center py-4">
|
||||
<div className="text-muted">
|
||||
<i className="bi bi-inbox display-4"></i>
|
||||
<p className="mt-2">Nenhuma solicitação encontrada.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* Paginação */}
|
||||
{filaEsperaFiltrada.length > 0 && (
|
||||
<div className="d-flex justify-content-between align-items-center mt-3">
|
||||
<div className="d-flex align-items-center">
|
||||
<span className="me-2 text-muted">Itens por página:</span>
|
||||
<select
|
||||
className="form-select form-select-sm w-auto"
|
||||
value={waitPerPage}
|
||||
onChange={(e) => {
|
||||
setWaitPerPage(Number(e.target.value));
|
||||
setWaitPage(1);
|
||||
}}
|
||||
>
|
||||
<option value={5}>5</option>
|
||||
<option value={10}>10</option>
|
||||
<option value={25}>25</option>
|
||||
<option value={50}>50</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="d-flex align-items-center">
|
||||
<span className="me-3 text-muted">
|
||||
Página {waitPage} de {waitTotalPages} •
|
||||
Mostrando {waitIndiceInicial + 1}-{Math.min(waitIndiceFinal, filaEsperaFiltrada.length)} de {filaEsperaFiltrada.length}
|
||||
</span>
|
||||
|
||||
<nav>
|
||||
<ul className="pagination pagination-sm mb-0">
|
||||
<li className={`page-item ${waitPage === 1 ? 'disabled' : ''}`}>
|
||||
<button className="page-link" onClick={() => setWaitPage(p => Math.max(1, p - 1))}>
|
||||
<i className="bi bi-chevron-left"></i>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
{gerarNumerosWaitPages().map(pagina => (
|
||||
<li key={pagina} className={`page-item ${pagina === waitPage ? 'active' : ''}`}>
|
||||
<button className="page-link" onClick={() => setWaitPage(pagina)}>
|
||||
{pagina}
|
||||
</button>
|
||||
</li>
|
||||
{dayjs.months().map((month, index) => (
|
||||
<option key={index} value={index}>{month.charAt(0).toUpperCase() + month.slice(1)}</option>
|
||||
))}
|
||||
|
||||
<li className={`page-item ${waitPage === waitTotalPages ? 'disabled' : ''}`}>
|
||||
<button className="page-link" onClick={() => setWaitPage(p => Math.min(waitTotalPages, p + 1))}>
|
||||
<i className="bi bi-chevron-right"></i>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</select>
|
||||
<select
|
||||
value={quickJump.year}
|
||||
onChange={(e) => handleQuickJumpChange('year', e.target.value)}
|
||||
className="form-select form-select-sm w-auto"
|
||||
>
|
||||
{Array.from({ length: 11 }, (_, i) => dayjs().year() - 5 + i).map(year => (
|
||||
<option key={year} value={year}>{year}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
onClick={applyQuickJump}
|
||||
disabled={quickJump.month === currentDate.month() && quickJump.year === currentDate.year()}
|
||||
>
|
||||
Ir
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="nav-buttons">
|
||||
<button onClick={() => { setCurrentDate(currentDate.subtract(1, 'month')); setSelectedDay(currentDate.subtract(1, 'month')); }}><ChevronLeft size={20} /></button>
|
||||
<button onClick={() => { setCurrentDate(dayjs()); setSelectedDay(dayjs()); }}>Hoje</button>
|
||||
<button onClick={() => { setCurrentDate(currentDate.add(1, 'month')); setSelectedDay(currentDate.add(1, 'month')); }}><ChevronRight size={20} /></button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="calendar-grid">
|
||||
{weekDays.map(day => <div key={day} className="day-header">{day}</div>)}
|
||||
{dateGrid.map((day, index) => {
|
||||
const appointmentsOnDay = DictAgendamentosOrganizados[day.format('YYYY-MM-DD')] || [];
|
||||
const cellClasses = `day-cell ${day.isSame(currentDate, 'month') ? 'current-month' : 'other-month'} ${day.isSame(dayjs(), 'day') ? 'today' : ''} ${day.isSame(selectedDay, 'day') ? 'selected' : ''}`;
|
||||
return (
|
||||
<div key={index} className={cellClasses} onClick={() => handleDateClick(day)}>
|
||||
<span>{day.format('D')}</span>
|
||||
{appointmentsOnDay.length > 0 &&
|
||||
<div className="appointments-indicator">
|
||||
{appointmentsOnDay.length}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="page-content table-paciente-container">
|
||||
<section className="row">
|
||||
<div className="col-12">
|
||||
<div className="card table-paciente-card">
|
||||
<div className="card-header"><h4 className="card-title mb-0">Fila de Espera</h4></div>
|
||||
<div className="card-body">
|
||||
<div className="card p-3 mb-3 table-paciente-filters">
|
||||
<h5 className="mb-3"><i className="bi bi-funnel-fill me-2 text-primary"></i> Filtros</h5>
|
||||
<div className="mb-3"><input type="text" className="form-control" placeholder="Buscar por paciente, CPF ou médico..." value={waitlistSearch} onChange={(e) => setWaitlistSearch(e.target.value)} /><small className="text-muted">Digite o nome do paciente, CPF ou nome do médico</small></div>
|
||||
<div className="d-flex flex-wrap align-items-center gap-2 mb-3">
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<span className="me-2 text-muted small">Ordenar por:</span>
|
||||
{(() => { const sortValue = waitSortKey ? `${waitSortKey}-${waitSortDir}` : ''; return (<select className="form-select compact-select sort-select w-auto" value={sortValue} onChange={(e) => { const v = e.target.value; if (!v) { setWaitSortKey(null); setWaitSortDir('asc'); return; } const [k, d] = v.split('-'); setWaitSortKey(k); setWaitSortDir(d); }}>
|
||||
<option value="">Sem ordenação</option><option value="paciente-asc">Paciente (A-Z)</option><option value="paciente-desc">Paciente (Z-A)</option><option value="medico-asc">Médico (A-Z)</option><option value="medico-desc">Médico (Z-A)</option><option value="data-asc">Data (mais antiga)</option><option value="data-desc">Data (mais recente)</option>
|
||||
</select>);})()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3"><div className="contador-pacientes">{filaEsperaFiltrada.length} DE {filaEsperaData.length} SOLICITAÇÕES ENCONTRADAS</div></div>
|
||||
</div>
|
||||
<div className="table-responsive">
|
||||
<table className="table table-striped table-hover table-paciente-table">
|
||||
<thead><tr><th>Nome do Paciente</th><th>CPF</th><th>Médico Solicitado</th><th>Data da Solicitação</th><th>Ações</th></tr></thead>
|
||||
<tbody>
|
||||
{filaEsperaPaginada.length > 0 ? (filaEsperaPaginada.map((item, index) => (
|
||||
<tr key={index}><td>{item?.Infos?.paciente_nome}</td><td>{item?.Infos?.paciente_cpf}</td><td>{item?.Infos?.nome_medico}</td><td>{dayjs(item.agendamento.scheduled_at).format('DD/MM/YYYY')}</td>
|
||||
<td>
|
||||
{}
|
||||
<button className="btn btn-sm btn-delete" onClick={() => { deleteConsulta(item.agendamento.id); }}><i className="bi bi-trash me-1"></i> Excluir</button>
|
||||
</td>
|
||||
</tr>
|
||||
))) : (<tr><td colSpan="5" className="text-center py-4"><div className="text-muted">{showSpinner ? <Spinner /> : (<><i className="bi bi-inbox display-4"></i><p className="mt-2">Nenhuma solicitação encontrada.</p></>)}</div></td></tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
{filaEsperaFiltrada.length > 0 && (<div className="d-flex justify-content-between align-items-center mt-3">
|
||||
<div className="d-flex align-items-center"><span className="me-2 text-muted">Itens por página:</span><select className="form-select form-select-sm w-auto" value={waitPerPage} onChange={(e) => { setWaitPerPage(Number(e.target.value)); setWaitPage(1); }}><option value={5}>5</option><option value={10}>10</option><option value={25}>25</option><option value={50}>50</option></select></div>
|
||||
<div className="d-flex align-items-center"><span className="me-3 text-muted">Página {waitPage} de {waitTotalPages} • Mostrando {waitIndiceInicial + 1}-{Math.min(waitIndiceFinal, filaEsperaFiltrada.length)} de {filaEsperaFiltrada.length}</span><nav><ul className="pagination pagination-sm mb-0">
|
||||
<li className={`page-item ${waitPage === 1 ? 'disabled' : ''}`}><button className="page-link" onClick={() => setWaitPage(p => Math.max(1, p - 1))}><i className="bi bi-chevron-left"></i></button></li>
|
||||
{gerarNumerosWaitPages().map(pagina => (<li key={pagina} className={`page-item ${pagina === waitPage ? 'active' : ''}`}><button className="page-link" onClick={() => setWaitPage(pagina)}>{pagina}</button></li>))}
|
||||
<li className={`page-item ${waitPage === waitTotalPages ? 'disabled' : ''}`}><button className="page-link" onClick={() => setWaitPage(p => Math.min(waitTotalPages, p + 1))}><i className="bi bi-chevron-right"></i></button></li>
|
||||
</ul></nav></div>
|
||||
</div>)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
:
|
||||
<div>
|
||||
<h2 className='fila-titulo'>Suas proximas consultas</h2>
|
||||
|
||||
<TabelaAgendamentoDia agendamentos={consultasOrganizadas} setDictInfo={setDictInfo}
|
||||
selectedID={selectedID} setSelectedId={setSelectedId} setShowDeleteModal={setShowDeleteModal}
|
||||
coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID}
|
||||
listaConsultasID={listaConsultasID} setShowConfirmModal={setShowConfirmModal}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
{showDeleteModal && (
|
||||
<div
|
||||
className="modal fade show"
|
||||
style={{
|
||||
display: "block",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
}}
|
||||
tabIndex="-1"
|
||||
onClick={(e) =>
|
||||
e.target.classList.contains("modal") && setShowDeleteModal(false)
|
||||
}
|
||||
>
|
||||
<div className="modal-dialog modal-dialog-centered">
|
||||
<div className="modal-content">
|
||||
|
||||
<div className="modal-header bg-danger bg-opacity-25">
|
||||
<h5 className="modal-title text-danger">
|
||||
Confirmação de Cancelamento
|
||||
</h5>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close"
|
||||
onClick={() => setShowDeleteModal(false)}
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body">
|
||||
<p className="mb-0 fs-5">
|
||||
Qual o motivo do cancelamento?
|
||||
</p>
|
||||
<div className='campo-de-input'>
|
||||
|
||||
<textarea className='input-modal' value={motivoCancelamento} onChange={(e) => setMotivoCancelamento(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={() => {setShowDeleteModal(false);
|
||||
|
||||
}}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-danger"
|
||||
onClick={() => {
|
||||
|
||||
deleteConsulta(selectedID)
|
||||
setShowDeleteModal(false)
|
||||
let lista_cores = coresConsultas
|
||||
|
||||
let lista = listaConsultasID
|
||||
|
||||
lista.push(selectedID)
|
||||
lista_cores.push("cancelled")
|
||||
|
||||
setCoresConsultas(lista_cores)
|
||||
|
||||
setListaConsultaID(lista)
|
||||
|
||||
console.log("lista", lista)
|
||||
|
||||
}}
|
||||
|
||||
>
|
||||
<i className="bi bi-trash me-1"></i> Excluir
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>)}
|
||||
|
||||
{showConfirmModal &&(
|
||||
<div
|
||||
className="modal fade show"
|
||||
style={{
|
||||
display: "block",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
}}
|
||||
tabIndex="-1"
|
||||
onClick={(e) =>
|
||||
e.target.classList.contains("modal") && setShowDeleteModal(false)
|
||||
}
|
||||
>
|
||||
<div className="modal-dialog modal-dialog-centered">
|
||||
<div className="modal-content">
|
||||
|
||||
<div className="modal-header bg-success">
|
||||
<h5 className="modal-title">
|
||||
Confirmação de edição
|
||||
</h5>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="modal-body">
|
||||
<p className="mb-0 fs-5">
|
||||
Tem certeza que deseja retirar o cancelamento ?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={() => {setShowConfirmModal(false); setSelectedId("")}}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success"
|
||||
onClick={() => {confirmConsulta(selectedID);setShowConfirmModal(false)
|
||||
let lista_cores = coresConsultas
|
||||
|
||||
let lista = listaConsultasID
|
||||
|
||||
lista.push(selectedID)
|
||||
lista_cores.push("confirmed")
|
||||
|
||||
setCoresConsultas(lista_cores)
|
||||
|
||||
setListaConsultaID(lista)
|
||||
}}
|
||||
|
||||
>
|
||||
<i className="bi bi-trash me-1"></i> Confirmar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>)
|
||||
|
||||
|
||||
|
||||
}
|
||||
</div>
|
||||
|
||||
) : (
|
||||
<AgendamentoCadastroManager setPageConsulta={setPageConsulta} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConsultasPaciente;
|
||||
export default Agendamento;
|
||||
165
src/components/AgendarConsulta/CalendarComponent.jsx
Normal file
165
src/components/AgendarConsulta/CalendarComponent.jsx
Normal file
@ -0,0 +1,165 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { ChevronLeft, ChevronRight, Edit, Trash2 } from 'lucide-react';
|
||||
import Spinner from '../Spinner.jsx'; // Certifique-se de que o caminho está correto
|
||||
|
||||
const CalendarComponent = ({
|
||||
currentDate,
|
||||
setCurrentDate,
|
||||
selectedDay,
|
||||
setSelectedDay,
|
||||
DictAgendamentosOrganizados,
|
||||
showSpinner,
|
||||
setSelectedId,
|
||||
setShowDeleteModal,
|
||||
setShowConfirmModal,
|
||||
quickJump,
|
||||
handleQuickJumpChange,
|
||||
applyQuickJump
|
||||
}) => {
|
||||
|
||||
// Gera os 42 dias para o grid do calendário
|
||||
const generateDateGrid = () => {
|
||||
const grid = [];
|
||||
const startOfMonth = currentDate.startOf('month');
|
||||
// Começa no domingo da semana em que o mês começa (day() retorna 0 para domingo)
|
||||
let currentDay = startOfMonth.subtract(startOfMonth.day(), 'day');
|
||||
|
||||
// Gera 6 semanas (6*7 = 42 dias)
|
||||
for (let i = 0; i < 42; i++) {
|
||||
grid.push(currentDay);
|
||||
currentDay = currentDay.add(1, 'day');
|
||||
}
|
||||
return grid;
|
||||
};
|
||||
|
||||
const dateGrid = useMemo(() => generateDateGrid(), [currentDate]);
|
||||
const weekDays = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'];
|
||||
|
||||
const handleDateClick = (day) => {
|
||||
setSelectedDay(day);
|
||||
// Opcional: Adicionar lógica para abrir o agendamento do dia/semana/mês (sua primeira imagem)
|
||||
// Se você quiser que o clique abra a tela de agendamento de slots:
|
||||
// Exemplo: onSelectDate(day);
|
||||
};
|
||||
|
||||
// Função para obter o status de agendamentos para o indicador (ponto azul)
|
||||
const getAppointmentCount = (day) => {
|
||||
return DictAgendamentosOrganizados[day.format('YYYY-MM-DD')]?.length || 0;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="calendar-wrapper">
|
||||
{/* Painel lateral de informações */}
|
||||
<div className="calendar-info-panel">
|
||||
<div className="info-date-display">
|
||||
<span>{selectedDay.format('MMM')}</span>
|
||||
<strong>{selectedDay.format('DD')}</strong>
|
||||
</div>
|
||||
<div className="info-details">
|
||||
<h3>{selectedDay.format('dddd')}</h3>
|
||||
<p>{selectedDay.format('D [de] MMMM [de] YYYY')}</p>
|
||||
</div>
|
||||
<div className="appointments-list">
|
||||
<h4>Consultas para {selectedDay.format('DD/MM')}</h4>
|
||||
{showSpinner ? <Spinner/> : (DictAgendamentosOrganizados[selectedDay.format('YYYY-MM-DD')]?.length > 0) ? (
|
||||
DictAgendamentosOrganizados[selectedDay.format('YYYY-MM-DD')].map(app => (
|
||||
<div key={app.id} className="appointment-item" data-status={app.status}>
|
||||
<div className="item-time">{dayjs(app.scheduled_at).format('HH:mm')}</div>
|
||||
<div className="item-details">
|
||||
<span>{app.paciente_nome}</span>
|
||||
<small>Dr(a). {app.medico_nome}</small>
|
||||
</div>
|
||||
<div className="appointment-actions">
|
||||
{app.status === 'cancelled' ? (
|
||||
<button className="btn-action btn-edit" onClick={() => { setSelectedId(app.id); setShowConfirmModal(true); }}>
|
||||
<Edit size={16} />
|
||||
</button>
|
||||
) : (
|
||||
<button className="btn-action btn-delete" onClick={() => { setSelectedId(app.id); setShowDeleteModal(true); }}>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="no-appointments-info"><p>Nenhuma consulta agendada.</p></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Calendário Principal */}
|
||||
<div className="calendar-main">
|
||||
{/* Legenda */}
|
||||
<div className="calendar-legend">
|
||||
<div className="legend-item" data-status="completed">Realizado</div>
|
||||
<div className="legend-item" data-status="confirmed">Confirmado</div>
|
||||
<div className="legend-item" data-status="agendado">Agendado</div>
|
||||
<div className="legend-item" data-status="cancelled">Cancelado</div>
|
||||
</div>
|
||||
|
||||
{/* Controles de navegação e Jump */}
|
||||
<div className="calendar-controls">
|
||||
<div className="date-indicator">
|
||||
<h2>{currentDate.format('MMMM [de] YYYY')}</h2>
|
||||
<div className="quick-jump-controls" style={{ display: 'flex', gap: '5px', marginTop: '10px' }}>
|
||||
<select
|
||||
value={quickJump.month}
|
||||
onChange={(e) => handleQuickJumpChange('month', e.target.value)}
|
||||
className="form-select form-select-sm w-auto"
|
||||
>
|
||||
{dayjs.months().map((month, index) => (
|
||||
<option key={index} value={index}>{month.charAt(0).toUpperCase() + month.slice(1)}</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={quickJump.year}
|
||||
onChange={(e) => handleQuickJumpChange('year', e.target.value)}
|
||||
className="form-select form-select-sm w-auto"
|
||||
>
|
||||
{Array.from({ length: 11 }, (_, i) => dayjs().year() - 5 + i).map(year => (
|
||||
<option key={year} value={year}>{year}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
onClick={applyQuickJump}
|
||||
disabled={quickJump.month === currentDate.month() && quickJump.year === currentDate.year()}
|
||||
>
|
||||
Ir
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="nav-buttons">
|
||||
<button onClick={() => { setCurrentDate(currentDate.subtract(1, 'month')); setSelectedDay(currentDate.subtract(1, 'month')); }}><ChevronLeft size={20} /></button>
|
||||
<button onClick={() => { setCurrentDate(dayjs()); setSelectedDay(dayjs()); }}>Hoje</button>
|
||||
<button onClick={() => { setCurrentDate(currentDate.add(1, 'month')); setSelectedDay(currentDate.add(1, 'month')); }}><ChevronRight size={20} /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid dos dias */}
|
||||
<div className="calendar-grid">
|
||||
{weekDays.map(day => <div key={day} className="day-header">{day}</div>)}
|
||||
{dateGrid.map((day, index) => {
|
||||
const count = getAppointmentCount(day);
|
||||
const cellClasses = `day-cell ${day.isSame(currentDate, 'month') ? 'current-month' : 'other-month'} ${day.isSame(dayjs(), 'day') ? 'today' : ''} ${day.isSame(selectedDay, 'day') ? 'selected' : ''}`;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={cellClasses}
|
||||
onClick={() => handleDateClick(day)}
|
||||
>
|
||||
<span>{day.format('D')}</span>
|
||||
{count > 0 && <div className="appointments-indicator">{count}</div>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CalendarComponent;
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,82 +1,182 @@
|
||||
import React from 'react'
|
||||
import FormNovaConsulta from '../components/AgendarConsulta/FormNovaConsulta'
|
||||
import API_KEY from '../components/utils/apiKeys'
|
||||
import { useAuth } from '../components/utils/AuthProvider'
|
||||
import { useEffect,useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { UserInfos } from '../components/utils/Functions-Endpoints/General'
|
||||
const AgendamentoCadastroManager = ({setPageConsulta, Dict}) => {
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import FormNovaConsulta from '../components/AgendarConsulta/FormNovaConsulta';
|
||||
import API_KEY from '../components/utils/apiKeys';
|
||||
import { useAuth } from '../components/utils/AuthProvider';
|
||||
import dayjs from 'dayjs';
|
||||
import { UserInfos } from '../components/utils/Functions-Endpoints/General';
|
||||
import { toast } from 'react-toastify';
|
||||
const AgendamentoCadastroManager = ({ setPageConsulta, agendamentoInicial }) => {
|
||||
|
||||
const {getAuthorizationHeader} = useAuth()
|
||||
const [agendamento, setAgendamento] = useState({status:'confirmed'})
|
||||
const [idUsuario, setIDusuario] = useState('0')
|
||||
const { getAuthorizationHeader } = useAuth();
|
||||
|
||||
const [agendamento, setAgendamento] = useState({ status: 'agendado' });
|
||||
const [idUsuario, setIDusuario] = useState('0');
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
let authHeader = getAuthorizationHeader()
|
||||
let authHeader = getAuthorizationHeader();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
|
||||
if (agendamentoInicial && agendamentoInicial.id) {
|
||||
|
||||
const scheduled_at = dayjs(agendamentoInicial.scheduled_at);
|
||||
|
||||
useEffect(() => {
|
||||
setAgendamento({
|
||||
...agendamentoInicial,
|
||||
|
||||
dataAtendimento: scheduled_at.format('YYYY-MM-DD'),
|
||||
horarioInicio: scheduled_at.format('HH:mm'),
|
||||
|
||||
|
||||
tipo_consulta: agendamentoInicial.appointment_type || 'Presencial',
|
||||
convenio: agendamentoInicial.insurance_provider || 'Público',
|
||||
|
||||
|
||||
paciente_nome: agendamentoInicial.paciente_nome || '',
|
||||
paciente_cpf: agendamentoInicial.paciente_cpf || '',
|
||||
medico_nome: agendamentoInicial.medico_nome || '',
|
||||
|
||||
|
||||
patient_id: agendamentoInicial.patient_id,
|
||||
doctor_id: agendamentoInicial.doctor_id,
|
||||
status: agendamentoInicial.status,
|
||||
});
|
||||
setIsEditMode(true);
|
||||
|
||||
} else {
|
||||
|
||||
setAgendamento({
|
||||
status: 'agendado',
|
||||
patient_id: null,
|
||||
doctor_id: null,
|
||||
dataAtendimento: dayjs().format('YYYY-MM-DD'),
|
||||
horarioInicio: '',
|
||||
tipo_consulta: 'Presencial',
|
||||
convenio: 'Público',
|
||||
paciente_nome: '',
|
||||
paciente_cpf: '',
|
||||
medico_nome: ''
|
||||
});
|
||||
setIsEditMode(false);
|
||||
}
|
||||
|
||||
if(!Dict){setAgendamento({})}
|
||||
else{
|
||||
console.log(Dict)
|
||||
setAgendamento(Dict)
|
||||
}
|
||||
|
||||
const ColherInfoUsuario =async () => {
|
||||
const result = await UserInfos(authHeader)
|
||||
|
||||
setIDusuario(result?.profile?.id)
|
||||
|
||||
}
|
||||
ColherInfoUsuario()
|
||||
|
||||
|
||||
}, [])
|
||||
|
||||
|
||||
|
||||
const handleSave = (Dict) => {
|
||||
let DataAtual = dayjs()
|
||||
|
||||
const ColherInfoUsuario = async () => {
|
||||
const result = await UserInfos(authHeader);
|
||||
setIDusuario(result?.profile?.id);
|
||||
};
|
||||
ColherInfoUsuario();
|
||||
|
||||
|
||||
}, [agendamentoInicial, authHeader]);
|
||||
const handleSave = async (Dict) => {
|
||||
var myHeaders = new Headers();
|
||||
myHeaders.append("apikey", API_KEY);
|
||||
myHeaders.append("Authorization", authHeader);
|
||||
myHeaders.append("Content-Type", "application/json");
|
||||
myHeaders.append("Prefer", "return=representation");
|
||||
|
||||
var raw = JSON.stringify({
|
||||
"patient_id": Dict.patient_id,
|
||||
"doctor_id": Dict.doctor_id,
|
||||
"scheduled_at": `${Dict.dataAtendimento}T${Dict.horarioInicio}:00.000Z`,
|
||||
"duration_minutes": Dict.duration_minutes || 30,
|
||||
"appointment_type": Dict.tipo_consulta,
|
||||
"insurance_provider": Dict.convenio,
|
||||
"status": Dict.status || 'agendado',
|
||||
"created_by": idUsuario,
|
||||
"created_at": dayjs().toISOString(),
|
||||
|
||||
});
|
||||
|
||||
var requestOptions = {
|
||||
method: 'POST',
|
||||
headers: myHeaders,
|
||||
body: raw,
|
||||
redirect: 'follow'
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments", requestOptions);
|
||||
if (response.ok) {
|
||||
toast.success("Agendamento criado com sucesso! ✅");
|
||||
setPageConsulta(false);
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
console.error('Erro ao cadastrar agendamento:', errorText);
|
||||
toast.error(`Falha ao criar agendamento: ${JSON.parse(errorText)?.message || 'Erro desconhecido'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro de rede:', error);
|
||||
toast.error("Erro de rede ao salvar. ❌");
|
||||
}
|
||||
}
|
||||
const handleUpdate = async (Dict) => {
|
||||
const appointmentId = agendamentoInicial.id;
|
||||
|
||||
var myHeaders = new Headers();
|
||||
myHeaders.append("apikey", API_KEY);
|
||||
myHeaders.append("Authorization", authHeader);
|
||||
myHeaders.append("Content-Type", "application/json");
|
||||
myHeaders.append("Prefer", "return=representation");
|
||||
|
||||
var raw = JSON.stringify({
|
||||
|
||||
"patient_id": Dict.patient_id,
|
||||
"doctor_id": Dict.doctor_id,
|
||||
"scheduled_at": `${Dict.dataAtendimento}T${Dict.horarioInicio}:00.000Z`,
|
||||
"duration_minutes": 30,
|
||||
"appointment_type": Dict.tipo_consulta,
|
||||
|
||||
"patient_notes": "",
|
||||
"insurance_provider": Dict.convenio,
|
||||
"status": Dict.status,
|
||||
"created_by": idUsuario
|
||||
});
|
||||
|
||||
var requestOptions = {
|
||||
method: 'POST',
|
||||
headers: myHeaders,
|
||||
body: raw,
|
||||
redirect: 'follow'
|
||||
};
|
||||
|
||||
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments", requestOptions)
|
||||
.then(response => response.text())
|
||||
.then(result => console.log(result))
|
||||
.catch(error => console.log('error', error));
|
||||
|
||||
}
|
||||
var raw = JSON.stringify({
|
||||
"patient_id": Dict.patient_id,
|
||||
"doctor_id": Dict.doctor_id,
|
||||
"scheduled_at": `${Dict.dataAtendimento}T${Dict.horarioInicio}:00.000Z`,
|
||||
"duration_minutes": Dict.duration_minutes || 30,
|
||||
"appointment_type": Dict.tipo_consulta,
|
||||
"insurance_provider": Dict.convenio,
|
||||
"status": Dict.status,
|
||||
"updated_at": dayjs().toISOString(),
|
||||
"updated_by": idUsuario,
|
||||
});
|
||||
|
||||
var requestOptions = {
|
||||
method: 'PATCH',
|
||||
headers: myHeaders,
|
||||
body: raw,
|
||||
redirect: 'follow'
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${appointmentId}`, requestOptions);
|
||||
if (response.ok) {
|
||||
toast.success("Agendamento atualizado com sucesso! 📝");
|
||||
setPageConsulta(false);
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
console.error('Erro ao atualizar agendamento:', errorText);
|
||||
toast.error(`Falha ao atualizar agendamento: ${JSON.parse(errorText)?.message || 'Erro desconhecido'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro de rede:', error);
|
||||
toast.error("Erro de rede ao atualizar. ❌");
|
||||
}
|
||||
}
|
||||
const handleFormSubmit = (Dict) => {
|
||||
if (isEditMode) {
|
||||
|
||||
handleUpdate(Dict);
|
||||
} else {
|
||||
|
||||
handleSave(Dict);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
<FormNovaConsulta onSave={handleSave} agendamento={agendamento} setAgendamento={setAgendamento} onCancel={() => setPageConsulta(false)}/>
|
||||
return (
|
||||
<div>
|
||||
{}
|
||||
<FormNovaConsulta
|
||||
onSave={handleFormSubmit}
|
||||
agendamento={agendamento}
|
||||
setAgendamento={setAgendamento}
|
||||
onCancel={() => setPageConsulta(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgendamentoCadastroManager
|
||||
export default AgendamentoCadastroManager;
|
||||
139
src/pages/MedicoAgendamento.jsx
Normal file
139
src/pages/MedicoAgendamento.jsx
Normal file
@ -0,0 +1,139 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import CalendarComponent from '../components/AgendarConsulta/CalendarComponent.jsx';
|
||||
import { useAuth } from '../components/utils/AuthProvider.js';
|
||||
import TabelaAgendamentoDia from '../components/AgendarConsulta/TabelaAgendamentoDia';
|
||||
|
||||
dayjs.locale('pt-br');
|
||||
|
||||
const MedicoAgendamento = () => {
|
||||
const { getAuthorizationHeader, user } = useAuth();
|
||||
const [currentDate, setCurrentDate] = useState(dayjs());
|
||||
const [selectedDay, setSelectedDay] = useState(dayjs());
|
||||
const [DictAgendamentosOrganizados, setAgendamentosOrganizados] = useState({});
|
||||
const [showSpinner, setShowSpinner] = useState(true);
|
||||
const [modoVisualizacao, setModoVisualizacao] = useState('Dia');
|
||||
|
||||
|
||||
const [quickJump, setQuickJump] = useState({
|
||||
month: currentDate.month(),
|
||||
year: currentDate.year()
|
||||
});
|
||||
|
||||
const handleQuickJumpChange = (type, value) => {
|
||||
setQuickJump(prev => ({ ...prev, [type]: Number(value) }));
|
||||
};
|
||||
|
||||
const applyQuickJump = () => {
|
||||
let newDate = dayjs().year(quickJump.year).month(quickJump.month).date(1);
|
||||
setCurrentDate(newDate);
|
||||
setSelectedDay(newDate);
|
||||
};
|
||||
|
||||
|
||||
const [selectedID, setSelectedId] = useState('0');
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false); t
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const mockAgendamentos = {
|
||||
[dayjs().format('YYYY-MM-DD')]: [
|
||||
{ id: 1, scheduled_at: dayjs().set('hour', 10).set('minute', 0).toISOString(), paciente_nome: "Paciente Teste 1", medico_nome: "Dr. Mock", status: "agendado" },
|
||||
{ id: 2, scheduled_at: dayjs().set('hour', 11).set('minute', 30).toISOString(), paciente_nome: "Paciente Teste 2", medico_nome: "Dr. Mock", status: "confirmed" },
|
||||
],
|
||||
|
||||
'2025-10-27': [
|
||||
{ id: 3, scheduled_at: '2025-10-27T19:30:00Z', paciente_nome: 'Davi Andrade', medico_nome: 'Dr. João', status: 'agendado' },
|
||||
{ id: 4, scheduled_at: '2025-10-27T20:00:00Z', paciente_nome: 'Davi Andrade', medico_nome: 'Dr. João', status: 'agendado' },
|
||||
{ id: 5, scheduled_at: '2025-10-27T21:30:00Z', paciente_nome: 'Davi Andrade', medico_nome: 'Dr. João', status: 'agendado' },
|
||||
]
|
||||
};
|
||||
|
||||
const today = dayjs();
|
||||
const startOfMonth = today.startOf('month');
|
||||
const nov11 = startOfMonth.add(10, 'day').format('YYYY-MM-DD');
|
||||
|
||||
|
||||
mockAgendamentos[nov11] = [
|
||||
{ id: 6, scheduled_at: `${nov11}T10:30:00Z`, paciente_nome: 'Paciente C', medico_nome: 'Isaac Kauã', status: 'agendado' },
|
||||
{ id: 7, scheduled_at: `${nov11}T11:00:00Z`, paciente_nome: 'João Gustavo', medico_nome: 'João Gustavo', status: 'agendado' },
|
||||
{ id: 8, scheduled_at: `${nov11}T12:30:00Z`, paciente_nome: 'João Gustavo', medico_nome: 'João Gustavo', status: 'agendado' },
|
||||
{ id: 9, scheduled_at: `${nov11}T15:00:00Z`, paciente_nome: 'Pedro Abravanel', medico_nome: 'Fernando Prichowski', status: 'agendado' },
|
||||
];
|
||||
|
||||
|
||||
setAgendamentosOrganizados(mockAgendamentos);
|
||||
setShowSpinner(false);
|
||||
}, []);
|
||||
|
||||
|
||||
const handleSelectSlot = (timeSlot, doctorId) => {
|
||||
alert(`Abrir tela de Nova Consulta para o dia ${selectedDay.format('DD/MM/YYYY')} às ${timeSlot} com o Médico ID: ${doctorId}`);
|
||||
|
||||
};
|
||||
|
||||
const isMedico = true;
|
||||
const medicoLogadoID = user?.doctor_id || "ID_MEDICO_DEFAULT";
|
||||
|
||||
return (
|
||||
<div className='agendamento-medico-container'>
|
||||
<h1>Agenda do Médico: {user?.full_name || "Nome do Médico"}</h1>
|
||||
<div className="btns-gerenciamento-e-consulta" style={{ display: 'flex', gap: '10px', marginBottom: '20px' }}>
|
||||
<button className='manage-button btn' onClick={() => {}}><i className="bi bi-gear-fill me-1"></i> Mudar Disponibilidade</button>
|
||||
<button className="btn btn-primary" onClick={() => {}}><i className="bi bi-person-plus-fill"></i> Adicionar Paciente</button>
|
||||
</div>
|
||||
|
||||
<div className="tab-buttons" style={{ marginBottom: '20px' }}>
|
||||
<button className={`btn ${modoVisualizacao === 'Dia' ? 'btn-primary' : 'btn-outline-primary'}`} onClick={() => setModoVisualizacao('Dia')}>Dia</button>
|
||||
<button className={`btn ${modoVisualizacao === 'Semana' ? 'btn-primary' : 'btn-outline-primary'}`} onClick={() => setModoVisualizacao('Semana')}>Semana</button>
|
||||
<button className={`btn ${modoVisualizacao === 'Mês' ? 'btn-primary' : 'btn-outline-primary'}`} onClick={() => setModoVisualizacao('Mês')}>Mês</button>
|
||||
</div>
|
||||
|
||||
<div className='agenda-e-calendario-wrapper'>
|
||||
{}
|
||||
<div className='medico-calendar-column' style={{ flex: 1 }}>
|
||||
<CalendarComponent
|
||||
currentDate={currentDate}
|
||||
setCurrentDate={setCurrentDate}
|
||||
selectedDay={selectedDay}
|
||||
setSelectedDay={setSelectedDay}
|
||||
DictAgendamentosOrganizados={DictAgendamentosOrganizados}
|
||||
showSpinner={showSpinner}
|
||||
T
|
||||
setSelectedId={setSelectedId}
|
||||
setShowDeleteModal={setShowDeleteModal}
|
||||
setShowConfirmModal={setShowConfirmModal}
|
||||
quickJump={quickJump}
|
||||
handleQuickJumpChange={handleQuickJumpChange}
|
||||
applyQuickJump={applyQuickJump}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className='medico-schedule-column' style={{ flex: 2 }}>
|
||||
{modoVisualizacao === 'Dia' && (
|
||||
<TabelaAgendamentoDia
|
||||
selectedDay={selectedDay}
|
||||
agendamentosDoDia={DictAgendamentosOrganizados[selectedDay.format('YYYY-MM-DD')] || []}
|
||||
onSelectSlot={handleSelectSlot}
|
||||
isMedicoView={isMedico}
|
||||
medicoID={medicoLogadoID}
|
||||
horariosDeTrabalho={[
|
||||
{ hora: '19:30', medicoId: '123' },
|
||||
{ hora: '20:00', medicoId: '123' },
|
||||
{ hora: '21:30', medicoId: '123' }
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{}
|
||||
{}
|
||||
{}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MedicoAgendamento;
|
||||
132
src/pages/TabelaAgendamentoDia.jsx
Normal file
132
src/pages/TabelaAgendamentoDia.jsx
Normal file
@ -0,0 +1,132 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/pt-br';
|
||||
import { ChevronLeft, ChevronRight, Edit, Trash2, User, Stethoscope } from 'lucide-react';
|
||||
|
||||
|
||||
// Configura o Day.js para usar o idioma português do Brasil
|
||||
dayjs.locale('pt-br');
|
||||
|
||||
|
||||
const TabelaAgendamentoDia = ({
|
||||
agendamentos,
|
||||
setDictInfo,
|
||||
setShowDeleteModal,
|
||||
setSelectedId,
|
||||
setShowConfirmModal,
|
||||
listaConsultasID,
|
||||
coresConsultas
|
||||
}) => {
|
||||
const [currentDate, setCurrentDate] = useState(dayjs());
|
||||
const [appointmentsForDay, setAppointmentsForDay] = useState([]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const formattedDate = currentDate.format('YYYY-MM-DD');
|
||||
const dailyAppointments = agendamentos[formattedDate] || [];
|
||||
|
||||
|
||||
const appointmentsComStatusAtualizado = dailyAppointments.map(app => {
|
||||
const index = listaConsultasID.indexOf(app.id);
|
||||
if (index > -1) {
|
||||
return { ...app, status: coresConsultas[index] };
|
||||
}
|
||||
return app;
|
||||
});
|
||||
|
||||
|
||||
setAppointmentsForDay(appointmentsComStatusAtualizado);
|
||||
}, [currentDate, agendamentos, listaConsultasID, coresConsultas]);
|
||||
|
||||
|
||||
const handlePrevDay = () => {
|
||||
setCurrentDate(currentDate.subtract(1, 'day'));
|
||||
};
|
||||
|
||||
|
||||
const handleNextDay = () => {
|
||||
setCurrentDate(currentDate.add(1, 'day'));
|
||||
};
|
||||
|
||||
|
||||
const handleEdit = (agendamento) => {
|
||||
// Adapte para a sua lógica de edição, talvez abrindo um modal
|
||||
console.log("Editar:", agendamento);
|
||||
setDictInfo(agendamento);
|
||||
};
|
||||
|
||||
|
||||
const handleDelete = (id) => {
|
||||
setSelectedId(id);
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
|
||||
|
||||
// Gera os horários do dia (ex: 08:00 às 18:00)
|
||||
const renderTimeSlots = () => {
|
||||
const slots = [];
|
||||
for (let i = 8; i <= 18; i++) {
|
||||
const time = `${i.toString().padStart(2, '0')}:00`;
|
||||
const hourlyAppointments = appointmentsForDay.filter(app =>
|
||||
dayjs(app.scheduled_at).format('HH:mm') === time
|
||||
);
|
||||
|
||||
|
||||
slots.push(
|
||||
<div className="time-slot" key={time}>
|
||||
<div className="time-marker">{time}</div>
|
||||
<div className="appointments-container">
|
||||
{hourlyAppointments.length > 0 ? (
|
||||
hourlyAppointments.map(app => (
|
||||
<div key={app.id} className="appointment-card" data-status={app.status}>
|
||||
<div className="card-content">
|
||||
<div className="card-line">
|
||||
<Stethoscope size={16} className="card-icon" />
|
||||
<strong>Dr(a):</strong> {app.medico_nome}
|
||||
</div>
|
||||
<div className="card-line">
|
||||
<User size={16} className="card-icon" />
|
||||
<strong>Paciente:</strong> {app.paciente_nome}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-actions">
|
||||
<button className="btn-card-action" onClick={() => handleEdit(app)}>
|
||||
<Edit size={16} />
|
||||
</button>
|
||||
<button className="btn-card-action btn-delete" onClick={() => handleDelete(app.id)}>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="no-appointment-placeholder"></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return slots;
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="modern-daily-view">
|
||||
<div className="calendar-header">
|
||||
<button onClick={handlePrevDay} className="btn-nav">
|
||||
<ChevronLeft size={24} />
|
||||
</button>
|
||||
<h2>{currentDate.format('dddd, D [de] MMMM [de] YYYY')}</h2>
|
||||
<button onClick={handleNextDay} className="btn-nav">
|
||||
<ChevronRight size={24} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="timeline">
|
||||
{renderTimeSlots()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default TabelaAgendamentoDia;
|
||||
@ -1,3 +1,5 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
.filtros-container select,
|
||||
.filtros-container input {
|
||||
padding: 0.5rem;
|
||||
@ -14,667 +16,210 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.unidade-selecionarprofissional{
|
||||
background-color: #fdfdfdde;
|
||||
padding: 20px 10px;
|
||||
.unidade-selecionarprofissional {
|
||||
background-color: #ffffff;
|
||||
padding: 20px 20px;
|
||||
display: flex;
|
||||
border-radius:10px ;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.unidade-selecionarprofissional input, .unidade-selecionarprofissional select {
|
||||
margin-left: 8px;
|
||||
border-radius: 8px;
|
||||
padding: 5px;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.unidade-selecionarprofissional select{
|
||||
width: 7%;
|
||||
}
|
||||
|
||||
.busca-atendimento{
|
||||
.busca-atendimento {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin:10px;
|
||||
margin: 0;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.busca-atendimento select{
|
||||
padding:5px;
|
||||
border-radius:8px ;
|
||||
margin-left: 15px;
|
||||
background-color: #0078d7;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.busca-atendimento input{
|
||||
margin-left: 8px;
|
||||
.busca-atendimento input {
|
||||
border: 2px solid #000000;
|
||||
border-radius: 8px;
|
||||
padding: 5px;
|
||||
padding: 10px 15px;
|
||||
width: 100%;
|
||||
font-size: 1rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.btn-selecionar-tabeladia, .btn-selecionar-tabelasemana, .btn-selecionar-tabelames {
|
||||
background-color: rgba(231, 231, 231, 0.808);
|
||||
padding:8px 10px;
|
||||
font-size: larger;
|
||||
font-weight: bold;
|
||||
border-style: hidden;
|
||||
}
|
||||
|
||||
.btn-selecionar-tabeladia{
|
||||
border-radius: 10px 0px 0px 10px;
|
||||
}
|
||||
|
||||
.btn-selecionar-tabelames{
|
||||
border-radius: 0px 10px 10px 0px;
|
||||
}
|
||||
|
||||
.btn-selecionar-tabeladia.ativo, .btn-selecionar-tabelasemana.ativo, .btn-selecionar-tabelames.ativo{
|
||||
background-color: lightcyan;
|
||||
border-color: darkcyan;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.legenda-tabela{
|
||||
display: flex;
|
||||
|
||||
margin-top: 30px;
|
||||
margin-bottom: 10px;
|
||||
gap: 15px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.legenda-item-realizado{
|
||||
background-color: #2c5e37;
|
||||
}
|
||||
|
||||
.legenda-item-confirmed{
|
||||
background-color: #1e90ff;
|
||||
}
|
||||
.legenda-item-cancelado{
|
||||
background-color: #d9534f;
|
||||
}
|
||||
|
||||
.legenda-item-agendado{
|
||||
background-color: #f0ad4e;
|
||||
}
|
||||
|
||||
#status-card-consulta-completed, .legenda-item-realizado {
|
||||
background-color: #b7ffbd;
|
||||
border:3px solid #91d392;
|
||||
padding: 5px;
|
||||
font-weight: bold;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#status-card-consulta-cancelled, .legenda-item-cancelado {
|
||||
background-color: #ffb7cc;
|
||||
border:3px solid #ff6c84;
|
||||
padding: 5px;
|
||||
font-weight: bold;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#status-card-consulta-confirmed, .legenda-item-confirmed {
|
||||
background-color: #eef8fb;
|
||||
border:3px solid #d8dfe7;
|
||||
padding: 5px;
|
||||
font-weight: bold;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#status-card-consulta-agendado, .legenda-item-agendado {
|
||||
background-color: #f7f7c4;
|
||||
border:3px solid #f3ce67;
|
||||
padding: 5px;
|
||||
font-weight: bold;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.btns-e-legenda-container{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.calendario {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgb(255, 255, 255);
|
||||
border: 10px solid #ffffffc5;
|
||||
background-color: rgb(253, 253, 253);
|
||||
}
|
||||
|
||||
.calendario-ou-filaespera{
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.container-btns-agenda-fila_esepera{
|
||||
margin-top: 30px;
|
||||
.container-btns-agenda-fila_esepera {
|
||||
margin-top: 20px;
|
||||
margin-left: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 20px;
|
||||
margin-left:20px ;
|
||||
gap: 0;
|
||||
border-bottom: 2px solid #E2E8F0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn-fila-espera, .btn-agenda{
|
||||
.btn-fila-espera, .btn-agenda {
|
||||
background-color: transparent;
|
||||
border: 0px ;
|
||||
border-bottom: 3px solid rgb(253, 253, 253);
|
||||
padding: 8px;
|
||||
border-radius: 10px 10px 0px 0px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.opc-filaespera-ativo, .opc-agenda-ativo{
|
||||
color: white;
|
||||
background-color: #5980fd;
|
||||
}
|
||||
|
||||
html[data-bs-theme="dark"] {
|
||||
body {
|
||||
background-color: #121212;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.calendario {
|
||||
background-color: #1e1e1e;
|
||||
border: 10px solid #333;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.unidade-selecionarprofissional {
|
||||
background-color: #1e1e1e;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
|
||||
.busca-atendimento input {
|
||||
background-color: #2c2c2c;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
.btn-buscar,
|
||||
.btn-selecionar-tabeladia,
|
||||
.btn-selecionar-tabelasemana,
|
||||
.btn-selecionar-tabelames {
|
||||
background-color: #2c2c2c;
|
||||
color: #e0e0e0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-selecionar-tabeladia.ativo,
|
||||
.btn-selecionar-tabelasemana.ativo,
|
||||
.btn-selecionar-tabelames.ativo {
|
||||
background-color: #005a9e;
|
||||
border-color: #004578;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.legenda-item-realizado {
|
||||
background-color: #14532d;
|
||||
border-color: #166534;
|
||||
}
|
||||
|
||||
.legenda-item-confirmado {
|
||||
background-color: #1e3a8a;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
.legenda-item-cancelado {
|
||||
background-color: #7f1d1d;
|
||||
border-color: #dc2626;
|
||||
}
|
||||
|
||||
.legenda-item-agendado {
|
||||
background-color: #78350f;
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
|
||||
#status-card-consulta-realizado,
|
||||
.legenda-item-realizado {
|
||||
background-color: #14532d;
|
||||
border: 3px solid #166534;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
#status-card-consulta-cancelado,
|
||||
.legenda-item-cancelado {
|
||||
background-color: #7f1d1d;
|
||||
border: 3px solid #dc2626;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
#status-card-consulta-confirmado,
|
||||
.legenda-item-confirmado {
|
||||
background-color: #1e3a8a;
|
||||
border: 3px solid #2563eb;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
#status-card-consulta-agendado,
|
||||
.legenda-item-agendado {
|
||||
background-color: #78350f;
|
||||
border: 3px solid #f59e0b;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.btns-e-legenda-container {
|
||||
background-color: #181818;
|
||||
}
|
||||
|
||||
.container-btns-agenda-fila_esepera {
|
||||
background-color: #181818;
|
||||
}
|
||||
|
||||
.btn-fila-espera,
|
||||
.btn-agenda {
|
||||
background-color: #2c2c2c;
|
||||
color: #e0e0e0;
|
||||
border-bottom: 3px solid #333;
|
||||
}
|
||||
|
||||
.opc-filaespera-ativo,
|
||||
.opc-agenda-ativo {
|
||||
color: #fff;
|
||||
background-color: #005a9e;
|
||||
}
|
||||
}
|
||||
|
||||
/* Estilo para o botão de Editar */
|
||||
.btn-edit-custom {
|
||||
background-color: #FFF3CD;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
/* Estilo para o botão de Excluir (Deletar) */
|
||||
.btn-delete-custom {
|
||||
background-color: #F8D7DA;
|
||||
color: #721C24;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.cardconsulta{
|
||||
display:flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.container-botons{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#tabela-seletor-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||
|
||||
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto;
|
||||
width: fit-content;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#tabela-seletor-container p {
|
||||
margin: 0;
|
||||
font-size: 23px;
|
||||
font-weight: 500;
|
||||
color: #4085f6;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#tabela-seletor-container button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #555;
|
||||
font-size: 20px;
|
||||
border: 0;
|
||||
border-bottom: 2px solid transparent;
|
||||
padding: 12px 24px;
|
||||
border-radius: 0;
|
||||
font-weight: 600;
|
||||
color: #718096;
|
||||
cursor: pointer;
|
||||
padding: 4px 6px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
margin-bottom: -2px;
|
||||
transition: color 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
#tabela-seletor-container button:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
color: #000;
|
||||
.btn-fila-espera:hover, .btn-agenda:hover {
|
||||
color: #2B6CB0;
|
||||
}
|
||||
|
||||
#tabela-seletor-container i {
|
||||
pointer-events: none;
|
||||
.opc-filaespera-ativo, .opc-agenda-ativo {
|
||||
color: #4299E1;
|
||||
background-color: transparent;
|
||||
border-bottom: 2px solid #4299E1;
|
||||
}
|
||||
|
||||
|
||||
.input-e-dropdown-wrapper {
|
||||
position: relative;
|
||||
width: 350px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.busca-atendimento > div {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.busca-atendimento input {
|
||||
/* Garante que o input preencha a largura disponível dentro do seu contêiner */
|
||||
width: calc(100% - 40px); /* Exemplo: 100% menos a largura do ícone (aprox. 40px) */
|
||||
/* ... outros estilos de borda, padding, etc. do seu input ... */
|
||||
}
|
||||
|
||||
|
||||
/* 3. O Dropdown: Posicionamento e Estilização */
|
||||
.dropdown-medicos {
|
||||
/* POSICIONAMENTO: Faz o dropdown flutuar */
|
||||
position: absolute;
|
||||
|
||||
/* POSICIONAMENTO: Coloca o topo do dropdown logo abaixo do input */
|
||||
top: 100%;
|
||||
left: 0;
|
||||
|
||||
/* LARGURA: Essencial. Ocupa 100% do .input-e-dropdown-wrapper, limitando-se a ele. */
|
||||
width: 100%;
|
||||
|
||||
/* SOBREPOSIÇÃO: Garante que fique acima de outros elementos (como a Fila de Espera) */
|
||||
z-index: 1000;
|
||||
|
||||
/* ESTILIZAÇÃO: Aparência */
|
||||
background-color: #ffffff; /* Fundo branco para não vazar */
|
||||
border: 1px solid #ccc; /* Borda leve */
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* Sombra para profundidade */
|
||||
border-top: none; /* Deixa o visual mais integrado ao input */
|
||||
|
||||
/* COMPORTAMENTO: Limite de altura e scroll */
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
border-top: none;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.dropdown-item { padding: 10px 15px; cursor: pointer; }
|
||||
.dropdown-item:hover { background-color: #f0f0f0; }
|
||||
|
||||
/* 4. Estilização de cada item do dropdown */
|
||||
.dropdown-item {
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
.calendar-wrapper {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
font-family: 'Inter', sans-serif;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.calendar-info-panel { flex: 0 0 300px; border-right: 1px solid #E2E8F0; padding-right: 24px; display: flex; flex-direction: column; }
|
||||
.info-date-display { background-color: #EDF2F7; border-radius: 8px; padding: 12px; text-align: center; margin-bottom: 16px; }
|
||||
.info-date-display span { font-weight: 600; color: #718096; text-transform: uppercase; font-size: 0.9rem; }
|
||||
.info-date-display strong { display: block; font-size: 2.5rem; font-weight: 700; color: #2D3748; }
|
||||
.info-details { text-align: center; margin-bottom: 24px; }
|
||||
.info-details h3 { font-size: 1.25rem; font-weight: 600; color: #2D3748; margin: 0; text-transform: capitalize; }
|
||||
.info-details p { color: #718096; margin: 0; }
|
||||
.appointments-list { flex-grow: 1; overflow-y: auto; }
|
||||
.appointments-list h4 { font-size: 1rem; font-weight: 600; color: #4A5568; margin-bottom: 12px; position: sticky; top: 0; background-color: #fff; padding-bottom: 8px; }
|
||||
.appointment-item { display: flex; gap: 12px; padding: 10px; border-radius: 6px; border-left: 4px solid; margin-bottom: 8px; background-color: #F7FAFC; }
|
||||
.item-time { font-weight: 600; color: #2B6CB0; }
|
||||
.item-details { display: flex; flex-direction: column; }
|
||||
.item-details span { font-weight: 500; color: #2D3748; }
|
||||
.item-details small { color: #718096; }
|
||||
.no-appointments-info { text-align: center; padding: 20px; color: #A0AEC0; }
|
||||
.appointment-item[data-status="confirmed"] { border-color: #4299E1; }
|
||||
.appointment-item[data-status="completed"] { border-color: #48BB78; }
|
||||
.appointment-item[data-status="cancelled"] { border-color: #F56565; }
|
||||
.appointment-item[data-status="agendado"],
|
||||
.appointment-item[data-status="requested"] { border-color: #ED8936; }
|
||||
.calendar-main { flex-grow: 1; }
|
||||
.calendar-controls { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||||
.date-indicator h2 { font-size: 1.5rem; font-weight: 600; color: #2D3748; margin: 0; text-transform: capitalize; }
|
||||
.nav-buttons { display: flex; gap: 8px; }
|
||||
.nav-buttons button { padding: 8px 12px; border-radius: 6px; border: 1px solid #CBD5E0; background-color: #fff; font-weight: 600; cursor: pointer; transition: all 0.2s; }
|
||||
.nav-buttons button:hover { background-color: #EDF2F7; }
|
||||
.calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
|
||||
.day-header { font-weight: 600; color: #718096; text-align: center; padding: 8px 0; font-size: 0.875rem; }
|
||||
.day-cell { min-height: 110px; border-radius: 8px; border: 1px solid #E2E8F0; padding: 8px; transition: background-color 0.2s, border-color 0.2s; cursor: pointer; position: relative; }
|
||||
.day-cell span { font-weight: 600; color: #4A5568; }
|
||||
.day-cell:hover { background-color: #EDF2F7; border-color: #BEE3F8; }
|
||||
.day-cell.other-month { background-color: #F7FAFC; }
|
||||
.day-cell.other-month span { color: #A0AEC0; }
|
||||
.day-cell.today span { background-color: #4299E1; color: #fff; border-radius: 50%; padding: 2px 6px; display: inline-block; }
|
||||
.day-cell.selected { background-color: #BEE3F8; border-color: #4299E1; }
|
||||
.appointments-indicator { background-color: #4299E1; color: white; font-size: 0.7rem; font-weight: bold; border-radius: 50%; width: 18px; height: 18px; display: flex; justify-content: center; align-items: center; position: absolute; bottom: 8px; right: 8px; }
|
||||
|
||||
.calendar-legend {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
/* Evita que nomes muito longos quebrem ou saiam da caixa */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: #f0f0f0; /* Cor ao passar o mouse */
|
||||
color: #007bff;
|
||||
.legend-item[data-status="completed"] {
|
||||
background-color: #C6F6D5;
|
||||
border: 1px solid #9AE6B4;
|
||||
color: #2F855A;
|
||||
}
|
||||
.legend-item[data-status="confirmed"] {
|
||||
background-color: #EBF8FF;
|
||||
border: 1px solid #BEE3F8;
|
||||
color: #3182CE;
|
||||
}
|
||||
.legend-item[data-status="agendado"] {
|
||||
background-color: #FEFCBF;
|
||||
border: 1px solid #F6E05E;
|
||||
color: #B7791F;
|
||||
}
|
||||
.legend-item[data-status="cancelled"] {
|
||||
background-color: #FED7D7;
|
||||
border: 1px solid #FEB2B2;
|
||||
color: #C53030;
|
||||
}
|
||||
|
||||
|
||||
.input-modal{
|
||||
width: 80%;
|
||||
.appointment-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* Estilo para o botão Excluir nos modais de Agendamento - vermelho escuro */
|
||||
.modal-footer .btn-danger,
|
||||
button.btn.btn-danger {
|
||||
background-color: #c82333 !important;
|
||||
border-color: #bd2130 !important;
|
||||
color: #ffffff !important;
|
||||
.item-details {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.modal-footer .btn-danger:hover,
|
||||
.modal-footer .btn-danger:focus,
|
||||
.modal-footer .btn-danger:active,
|
||||
button.btn.btn-danger:hover,
|
||||
button.btn.btn-danger:focus,
|
||||
button.btn.btn-danger:active {
|
||||
background-color: #a71d2a !important;
|
||||
border-color: #9c1c28 !important;
|
||||
color: #ffffff !important;
|
||||
.appointment-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ===== ESTILOS REUTILIZADOS DA TABELA DE PACIENTES ===== */
|
||||
|
||||
/* Containers e Cards */
|
||||
.table-paciente-container {
|
||||
line-height: 2.5;
|
||||
}
|
||||
|
||||
.table-paciente-card {
|
||||
border: none;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.table-paciente-card .card-header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
/* Filtros */
|
||||
.table-paciente-filters {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.table-paciente-filters h5 {
|
||||
color: #495057;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Tabela */
|
||||
.table-paciente-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table-paciente-table th {
|
||||
background-color: #f8f9fa;
|
||||
color: #495057;
|
||||
font-weight: 600;
|
||||
padding: 15px 8px;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table-paciente-table td {
|
||||
padding: 15px 8px;
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.table-paciente-table tbody tr:hover {
|
||||
background-color: rgba(0, 0, 0, 0.025);
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
/* Badges e Contador */
|
||||
.contador-pacientes {
|
||||
background-color: #1e3a8a;
|
||||
color: white;
|
||||
padding: 0.5em 0.75em;
|
||||
font-size: 0.875em;
|
||||
font-weight: 500;
|
||||
border-radius: 0.375rem;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.filters-active .badge {
|
||||
font-size: 0.75em;
|
||||
padding: 0.4em 0.65em;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* Botões */
|
||||
.btn-view {
|
||||
background-color: #E6F2FF !important;
|
||||
color: #004085 !important;
|
||||
border: 1px solid #B8D4F0;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.btn-view:hover {
|
||||
background-color: #D1E7FF !important;
|
||||
border-color: #9EC5FE;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background-color: #FFF3CD !important;
|
||||
color: #856404 !important;
|
||||
border: 1px solid #FFEAA7;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.btn-edit:hover {
|
||||
background-color: #FFEEBA !important;
|
||||
border-color: #FFE087;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background-color: #F8D7DA !important;
|
||||
color: #721C24 !important;
|
||||
border: 1px solid #F5C6CB;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background-color: #F1B0B7 !important;
|
||||
border-color: #ED969E;
|
||||
}
|
||||
|
||||
/* Selects compactos */
|
||||
.compact-select {
|
||||
font-size: 1.0rem;
|
||||
padding: 0.45rem 0.5rem;
|
||||
}
|
||||
|
||||
.compact-select option {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Paginação */
|
||||
.pagination {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.page-link {
|
||||
color: #495057;
|
||||
border: 1px solid #dee2e6;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.page-link:hover {
|
||||
color: #1e3a8a;
|
||||
background-color: #e9ecef;
|
||||
border-color: #dee2e6;
|
||||
}
|
||||
|
||||
.page-item.active .page-link {
|
||||
background-color: #1e3a8a;
|
||||
border-color: #1e3a8a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.page-item.disabled .page-link {
|
||||
color: #6c757d;
|
||||
background-color: #f8f9fa;
|
||||
border-color: #dee2e6;
|
||||
}
|
||||
|
||||
.d-flex.justify-content-between.align-items-center {
|
||||
border-top: 1px solid #dee2e6;
|
||||
padding-top: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.text-center.py-4 .text-muted {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.text-center.py-4 .bi-inbox {
|
||||
font-size: 3rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.text-center.py-4 p {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.text-center.py-4 td {
|
||||
border-bottom: none;
|
||||
padding: 2rem !important;
|
||||
}
|
||||
|
||||
/* Responsividade */
|
||||
@media (max-width: 768px) {
|
||||
.table-paciente-table {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.table-paciente-table th,
|
||||
.table-paciente-table td {
|
||||
padding: 10px 6px;
|
||||
}
|
||||
|
||||
.btn-view,
|
||||
.btn-edit,
|
||||
.btn-delete {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.table-paciente-filters .d-flex {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.table-paciente-filters .form-select {
|
||||
min-width: 100% !important;
|
||||
}
|
||||
|
||||
.d-flex.justify-content-between.align-items-center {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
|
||||
.d-flex.justify-content-between.align-items-center > div {
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
flex-wrap: wrap;
|
||||
.btn-action {
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.me-3.text-muted {
|
||||
text-align: center;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.contador-pacientes {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.4em 0.6em;
|
||||
}
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-action.btn-edit {
|
||||
background-color: #FEFCBF;
|
||||
color: #B7791F;
|
||||
}
|
||||
.btn-action.btn-edit:hover {
|
||||
background-color: #F6E05E;
|
||||
}
|
||||
|
||||
.btn-action.btn-delete {
|
||||
background-color: #E53E3E;
|
||||
color: #fff;
|
||||
}
|
||||
.btn-action.btn-delete:hover {
|
||||
background-color: #C53030;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user