Files
riseup_squad_03/src/hooks/useAgenda.js
letvb20-dot 94dab58d85 new file: public/favicon.svg
deleted:    src/assets/hero.png
modified:   src/components/AppShell.jsx
modified:   src/components/calendar/AgendaDailyView.jsx
modified:   src/components/calendar/AgendaMonthlyView.jsx
modified:   src/components/calendar/AgendaWeeklyView.jsx
modified:   src/hooks/useAgenda.js
modified:   src/index.css
modified:   src/mappers/appointmentMapper.js
modified:   src/mappers/reportMapper.js
modified:   src/pages/AgendaPage.jsx
modified:   src/pages/AuthPages.jsx
modified:   src/pages/HomePage.jsx
modified:   src/pages/MessagesPage.jsx
modified:   src/pages/PatientsPage.jsx
modified:   src/pages/ProfilePage.jsx
modified:   src/pages/ReportsPage.jsx
modified:   src/pages/SettingsPage.jsx
modified:   src/repositories/appointmentRepository.js
modified:   src/repositories/settingsRepository.js
2026-05-08 01:32:46 -03:00

430 lines
13 KiB
JavaScript

import { useEffect, useMemo, useState } from 'react'
import { isSameDay } from 'date-fns'
import { appointmentRepository } from '../repositories/appointmentRepository.js'
import { availabilityRepository } from '../repositories/availabilityRepository.js'
import { patientRepository } from '../repositories/patientRepository.js'
import { professionalRepository } from '../repositories/professionalRepository.js'
import { profileRepository } from '../repositories/profileRepository.js'
import { formatLocalDateInput, parseLocalDate, sortAppointmentsByTime } from '../utils/agendaDate.js'
const initialForm = {
patientId: '',
professionalId: '',
type: 'Retorno',
time: '15:30',
mode: 'Teleconsulta',
status: 'Aguardando',
notes: '',
}
export function useAgenda() {
const [patients, setPatients] = useState([])
const [professionals, setProfessionals] = useState([])
const [currentProfessional, setCurrentProfessional] = useState(null)
const [viewerProfile, setViewerProfile] = useState(null)
const [localAppointments, setLocalAppointments] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [availableSlots, setAvailableSlots] = useState([])
const [slotsLoading, setSlotsLoading] = useState(false)
const [slotsError, setSlotsError] = useState('')
const [activeView, setActiveView] = useState('Dia')
const [baseDate, setBaseDate] = useState(new Date())
const [status, setStatus] = useState('Todos')
const [doctorFilter, setDoctorFilter] = useState('Todos')
const [doctorSearch, setDoctorSearch] = useState('')
const [unitFilter, setUnitFilter] = useState('')
const [modalOpen, setModalOpen] = useState(false)
const [editingAppointment, setEditingAppointment] = useState(null)
const [form, setForm] = useState(initialForm)
const agendaScope = viewerProfile?.isDoctor ? 'doctor' : 'global'
const canCreateAppointment = agendaScope === 'doctor'
? Boolean(currentProfessional?.id)
: professionals.length > 0
useEffect(() => {
let active = true
async function loadAgendaContext() {
try {
setError('')
const [patientsData, professionalsData, currentProfile] = await Promise.all([
patientRepository.getAll(),
professionalRepository.getAll(),
profileRepository.getCurrentUserProfile(),
])
if (!active) return
const currentScope = currentProfile?.isDoctor ? 'doctor' : 'global'
const resolvedProfessional = professionalRepository.resolveCurrentProfessional(currentProfile, professionalsData)
const initialProfessionalId =
currentScope === 'doctor'
? resolvedProfessional?.id || ''
: professionalsData?.[0]?.id || ''
setViewerProfile(currentProfile)
setPatients(patientsData || [])
setCurrentProfessional(resolvedProfessional)
setProfessionals(professionalsData || [])
setForm((current) => ({
...current,
patientId: patientsData?.length ? patientsData[0].id : '',
professionalId: initialProfessionalId,
}))
if (currentScope === 'doctor' && !resolvedProfessional) {
setLocalAppointments([])
setError('Não foi possível vincular o médico logado a um profissional da base.')
return
}
const appointmentsData = await appointmentRepository.getAll({
doctorId: currentScope === 'doctor' ? resolvedProfessional?.id : undefined,
})
if (!active) return
setLocalAppointments(
currentScope === 'doctor' && resolvedProfessional
? filterAppointmentsByProfessional(appointmentsData || [], resolvedProfessional.id)
: sortAppointmentsByTime(appointmentsData || []),
)
} catch (loadError) {
if (!active) return
console.error(loadError)
setError(loadError.message || 'Erro ao carregar agenda.')
} finally {
if (active) setLoading(false)
}
}
loadAgendaContext()
return () => {
active = false
}
}, [])
useEffect(() => {
if (!modalOpen || editingAppointment) return
const targetProfessionalId = agendaScope === 'doctor'
? currentProfessional?.id
: form.professionalId
let active = true
async function loadAvailableSlots() {
if (!targetProfessionalId) {
setAvailableSlots([])
setSlotsError('')
return
}
setSlotsLoading(true)
setSlotsError('')
try {
const slots = await availabilityRepository.getAvailableSlots({
doctorId: targetProfessionalId,
date: formatLocalDateInput(baseDate),
appointmentType: form.mode,
})
if (!active) return
const activeSlots = slots.filter((slot) => slot.available)
setAvailableSlots(activeSlots)
if (activeSlots.length) {
setForm((current) =>
activeSlots.some((slot) => slot.time === current.time)
? current
: { ...current, time: activeSlots[0].time },
)
}
} catch (loadError) {
if (!active) return
setAvailableSlots([])
setSlotsError(loadError.message || 'Não foi possível calcular horários disponíveis.')
} finally {
if (active) setSlotsLoading(false)
}
}
loadAvailableSlots()
return () => {
active = false
}
}, [agendaScope, baseDate, currentProfessional?.id, editingAppointment, form.mode, form.professionalId, modalOpen])
const visibleAppointments = useMemo(() => {
let filtered = localAppointments
if (status !== 'Todos') {
filtered = filtered.filter((appointment) => appointment.status === status)
}
if (agendaScope !== 'doctor' && doctorFilter !== 'Todos') {
filtered = filterAppointmentsByProfessional(filtered, doctorFilter)
}
if (agendaScope !== 'doctor') {
const normalizedDoctorSearch = normalizeValue(doctorSearch)
const normalizedUnit = normalizeValue(unitFilter)
if (normalizedDoctorSearch || normalizedUnit) {
filtered = filtered.filter((appointment) => {
const professional = professionals.find(
(item) => normalizeValue(item.id) === normalizeValue(appointment.professionalId),
)
const professionalName = normalizeValue(professional?.name || appointment.professional)
const professionalUnit = normalizeValue(professional?.unit || appointment.unit)
return (
(!normalizedDoctorSearch || professionalName.includes(normalizedDoctorSearch)) &&
(!normalizedUnit || professionalUnit === normalizedUnit)
)
})
}
}
if (activeView === 'Dia') {
filtered = filtered.filter((appointment) => {
if (!appointment.date) return false
const appointmentDate = parseLocalDate(appointment.date)
if (!appointmentDate) return false
return isSameDay(appointmentDate, baseDate)
})
}
return sortAppointmentsByTime(filtered)
}, [activeView, agendaScope, baseDate, doctorFilter, doctorSearch, localAppointments, professionals, status, unitFilter])
function updateForm(field, value) {
setForm((current) => ({ ...current, [field]: value }))
}
function openCreateModal({ date, time } = {}) {
if (date) {
const parsedDate = parseLocalDate(date)
if (parsedDate) setBaseDate(parsedDate)
}
setEditingAppointment(null)
setAvailableSlots([])
setSlotsError('')
setForm((current) => ({
...initialForm,
patientId: current.patientId || patients[0]?.id || '',
professionalId:
agendaScope === 'doctor'
? currentProfessional?.id || ''
: current.professionalId || professionals[0]?.id || '',
time: time || current.time || initialForm.time,
}))
setModalOpen(true)
}
function openAppointmentModal(appointment) {
const parsedDate = parseLocalDate(appointment.date)
if (parsedDate) setBaseDate(parsedDate)
setEditingAppointment(appointment)
setAvailableSlots([])
setSlotsError('')
setForm({
patientId: appointment.patientId || '',
professionalId: appointment.professionalId || '',
type: appointment.type || initialForm.type,
time: appointment.time || initialForm.time,
mode: appointment.mode || initialForm.mode,
status: appointment.status || initialForm.status,
notes: appointment.notes || '',
})
setModalOpen(true)
}
function closeAppointmentModal() {
setModalOpen(false)
setEditingAppointment(null)
}
async function handleSubmitAppointment(event) {
event.preventDefault()
if (editingAppointment) {
await updateAppointment()
return
}
await createAppointment()
}
async function createAppointment() {
const payload = buildPayload()
if (!payload) return
try {
const created = await appointmentRepository.create(payload)
setLocalAppointments((current) => sortAppointmentsByTime([...current, enrichAppointment(created, payload, patients, professionals)]))
closeAppointmentModal()
} catch (createError) {
alert(createError.message || 'Erro ao criar agendamento.')
}
}
async function updateAppointment() {
if (!editingAppointment) return
const payload = buildPayload()
if (!payload) return
try {
const updated = await appointmentRepository.update(editingAppointment.id, payload)
setLocalAppointments((current) =>
sortAppointmentsByTime(
current.map((appointment) =>
appointment.id === editingAppointment.id
? enrichAppointment(updated, payload, patients, professionals)
: appointment,
),
),
)
closeAppointmentModal()
} catch (updateError) {
alert(updateError.message || 'Erro ao atualizar agendamento.')
}
}
async function handleCancelAppointment() {
if (!editingAppointment) return
if (!window.confirm('Tem certeza que deseja cancelar este agendamento?')) return
const payload = buildPayload({ status: 'Cancelada' })
if (!payload) return
try {
const cancelled = await appointmentRepository.cancel(editingAppointment.id, payload)
setLocalAppointments((current) =>
sortAppointmentsByTime(
current.map((appointment) =>
appointment.id === editingAppointment.id
? enrichAppointment(cancelled, payload, patients, professionals)
: appointment,
),
),
)
closeAppointmentModal()
} catch (cancelError) {
alert(cancelError.message || 'Erro ao cancelar agendamento.')
}
}
function buildPayload(overrides = {}) {
if (!form.patientId) {
alert('Selecione um paciente para salvar o agendamento.')
return null
}
const targetProfessionalId = agendaScope === 'doctor'
? currentProfessional?.id
: form.professionalId
if (!targetProfessionalId) {
alert('Não foi possível identificar o profissional da consulta.')
return null
}
return {
patientId: form.patientId,
date: formatLocalDateInput(baseDate),
time: form.time,
type: form.type,
mode: form.mode,
status: form.status,
notes: form.notes,
room: form.mode === 'Teleconsulta' ? 'Virtual' : 'Consultório 1',
professionalId: targetProfessionalId,
...overrides,
}
}
return {
patients,
professionals,
currentProfessional,
viewerProfile,
agendaScope,
loading,
error,
canCreateAppointment,
activeView,
setActiveView,
baseDate,
setBaseDate,
status,
setStatus,
doctorFilter,
setDoctorFilter,
doctorSearch,
setDoctorSearch,
unitFilter,
setUnitFilter,
modalOpen,
editingAppointment,
form,
updateForm,
openCreateModal,
openAppointmentModal,
closeAppointmentModal,
handleSubmitAppointment,
handleCancelAppointment,
visibleAppointments,
availableSlots,
slotsLoading,
slotsError,
}
}
function filterAppointmentsByProfessional(appointments, professionalId) {
const normalizedProfessionalId = normalizeValue(professionalId)
return sortAppointmentsByTime(
appointments.filter((appointment) => normalizeValue(appointment.professionalId) === normalizedProfessionalId),
)
}
function enrichAppointment(appointment, payload, patients, professionals) {
const patient = patients.find((item) => String(item.id) === String(payload.patientId))
const professional = professionals.find((item) => String(item.id) === String(payload.professionalId))
return {
...appointment,
patientId: payload.patientId,
professionalId: payload.professionalId,
patient: patient?.name || patient?.full_name || patient?.nome || appointment.patient,
professional: professional?.name || professional?.full_name || professional?.nome || appointment.professional,
date: payload.date,
time: payload.time,
type: payload.type,
mode: payload.mode,
status: payload.status,
notes: payload.notes,
room: payload.room,
}
}
function normalizeValue(value) {
return String(value || '').trim().toLowerCase()
}