Compare commits
2 Commits
2c39f404d8
...
6dba55684a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dba55684a | ||
|
|
edb717050b |
@ -4,7 +4,7 @@ import { useRouter, useSearchParams } from "next/navigation";
|
|||||||
import { CalendarRegistrationForm } from "@/components/forms/calendar-registration-form";
|
import { CalendarRegistrationForm } from "@/components/forms/calendar-registration-form";
|
||||||
import HeaderAgenda from "@/components/agenda/HeaderAgenda";
|
import HeaderAgenda from "@/components/agenda/HeaderAgenda";
|
||||||
import FooterAgenda from "@/components/agenda/FooterAgenda";
|
import FooterAgenda from "@/components/agenda/FooterAgenda";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { criarAgendamento } from '@/lib/api';
|
import { criarAgendamento } from '@/lib/api';
|
||||||
import { toast } from '@/hooks/use-toast';
|
import { toast } from '@/hooks/use-toast';
|
||||||
|
|
||||||
@ -40,6 +40,26 @@ export default function NovoAgendamentoPage() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const [formData, setFormData] = useState<FormData>({});
|
const [formData, setFormData] = useState<FormData>({});
|
||||||
|
// Prefill from search params if provided (origin=consultas)
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const origin = searchParams?.get?.('origin')
|
||||||
|
if (!origin) return
|
||||||
|
const next: FormData = { ...formData }
|
||||||
|
const pId = searchParams.get('patientId')
|
||||||
|
const tipo = searchParams.get('tipo')
|
||||||
|
const especialidade = searchParams.get('especialidade')
|
||||||
|
const local = searchParams.get('local')
|
||||||
|
if (pId) next.patientId = pId
|
||||||
|
if (tipo) next.appointmentType = tipo
|
||||||
|
if (especialidade) next.chief_complaint = `Especialidade: ${especialidade}`
|
||||||
|
if (local) next.unit = local
|
||||||
|
setFormData(next)
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleFormChange = (data: FormData) => {
|
const handleFormChange = (data: FormData) => {
|
||||||
setFormData(data);
|
setFormData(data);
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import Link from 'next/link'
|
|||||||
import ProtectedRoute from '@/components/ProtectedRoute'
|
import ProtectedRoute from '@/components/ProtectedRoute'
|
||||||
import { useAuth } from '@/hooks/useAuth'
|
import { useAuth } from '@/hooks/useAuth'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { buscarPacientes, buscarPacientePorUserId, getUserInfo, listarMensagensPorPaciente } from '@/lib/api'
|
import { buscarPacientes, buscarPacientePorUserId, getUserInfo, listarMensagensPorPaciente, listarAgendamentos, buscarMedicosPorIds } from '@/lib/api'
|
||||||
import { useReports } from '@/hooks/useReports'
|
import { useReports } from '@/hooks/useReports'
|
||||||
// Simulação de internacionalização básica
|
// Simulação de internacionalização básica
|
||||||
const strings = {
|
const strings = {
|
||||||
@ -265,37 +265,11 @@ export default function PacientePage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consultas fictícias
|
// Consultas (fetched from server for the logged-in patient)
|
||||||
const [currentDate, setCurrentDate] = useState(new Date())
|
const [currentDate, setCurrentDate] = useState(new Date())
|
||||||
const consultasFicticias = [
|
const [consultas, setConsultas] = useState<any[]>([])
|
||||||
{
|
const [consultasLoading, setConsultasLoading] = useState(false)
|
||||||
id: 1,
|
const [consultasError, setConsultasError] = useState<string | null>(null)
|
||||||
medico: "Dr. Carlos Andrade",
|
|
||||||
especialidade: "Cardiologia",
|
|
||||||
local: "Clínica Coração Feliz",
|
|
||||||
data: new Date().toISOString().split('T')[0],
|
|
||||||
hora: "09:00",
|
|
||||||
status: "Confirmada"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
medico: "Dra. Fernanda Lima",
|
|
||||||
especialidade: "Dermatologia",
|
|
||||||
local: "Clínica Pele Viva",
|
|
||||||
data: new Date().toISOString().split('T')[0],
|
|
||||||
hora: "14:30",
|
|
||||||
status: "Pendente"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
medico: "Dr. João Silva",
|
|
||||||
especialidade: "Ortopedia",
|
|
||||||
local: "Hospital Ortopédico",
|
|
||||||
data: (() => { let d = new Date(); d.setDate(d.getDate()+1); return d.toISOString().split('T')[0] })(),
|
|
||||||
hora: "11:00",
|
|
||||||
status: "Cancelada"
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function formatDatePt(date: Date) {
|
function formatDatePt(date: Date) {
|
||||||
return date.toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
|
return date.toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
|
||||||
@ -311,7 +285,17 @@ export default function PacientePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const todayStr = currentDate.toISOString().split('T')[0];
|
const todayStr = currentDate.toISOString().split('T')[0];
|
||||||
const consultasDoDia = consultasFicticias.filter(c => c.data === todayStr);
|
// compute appointments for the selected day (normalize scheduled_at to local YYYY-MM-DD)
|
||||||
|
const consultasDoDia = consultas.filter(c => {
|
||||||
|
try {
|
||||||
|
const scheduled = c.scheduled_at || c.time || c.data || c.date || null
|
||||||
|
if (!scheduled) return false
|
||||||
|
const d = new Date(scheduled)
|
||||||
|
if (isNaN(d.getTime())) return false
|
||||||
|
const y = d.getFullYear(); const m = String(d.getMonth()+1).padStart(2,'0'); const day = String(d.getDate()).padStart(2,'0')
|
||||||
|
return `${y}-${m}-${day}` === todayStr
|
||||||
|
} catch (e) { return false }
|
||||||
|
})
|
||||||
|
|
||||||
function Consultas() {
|
function Consultas() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -333,7 +317,77 @@ export default function PacientePage() {
|
|||||||
especialidade,
|
especialidade,
|
||||||
local: localizacao
|
local: localizacao
|
||||||
})
|
})
|
||||||
router.push(`/resultados?${params.toString()}`)
|
// if we have a linked patient id, include it so the agenda page can prefill
|
||||||
|
try { if (patientId) params.append('patientId', String(patientId)) } catch(e){}
|
||||||
|
router.push(`/agenda?origin=consultas&${params.toString()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper: fetch raw appointment data and enrich with doctor objects (returns array)
|
||||||
|
async function fetchConsultasData(): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
const qs = `?select=*&patient_id=eq.${encodeURIComponent(String(patientId))}&order=scheduled_at.asc&limit=200`
|
||||||
|
const appts = await listarAgendamentos(qs).catch(() => [])
|
||||||
|
|
||||||
|
const doctorIds = Array.from(new Set((appts || []).map((a:any) => String(a.doctor_id || a.doctor || a.requested_by || '').trim()).filter(Boolean)))
|
||||||
|
let doctorMap = new Map<string, any>()
|
||||||
|
if (doctorIds.length) {
|
||||||
|
try {
|
||||||
|
const docs = await buscarMedicosPorIds(doctorIds).catch(() => [])
|
||||||
|
for (const d of docs || []) if (d && d.id) doctorMap.set(String(d.id), d)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[PacientePage] falha ao buscar medicos para agendamentos', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = (appts || []).map((a:any) => ({ ...a, doctor: a.doctor || doctorMap.get(String(a.doctor_id || a.doctor || a.requested_by || '')) || undefined }))
|
||||||
|
return normalized
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[PacientePage] fetchConsultasData error', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapper that updates component state (safe to call from button or useEffect)
|
||||||
|
async function loadAndSetConsultas() {
|
||||||
|
if (!patientId) { setConsultas([]); return }
|
||||||
|
setConsultasLoading(true)
|
||||||
|
setConsultasError(null)
|
||||||
|
try {
|
||||||
|
const data = await fetchConsultasData()
|
||||||
|
setConsultas(data)
|
||||||
|
} catch (err:any) {
|
||||||
|
setConsultasError('Falha ao carregar consultas.')
|
||||||
|
} finally {
|
||||||
|
setConsultasLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let mounted = true
|
||||||
|
if (!mostrarAgendadas) return
|
||||||
|
// only load when dialog is requested
|
||||||
|
loadAndSetConsultas()
|
||||||
|
return () => { mounted = false }
|
||||||
|
}, [mostrarAgendadas, patientId])
|
||||||
|
|
||||||
|
// click handler for the "Ver consultas agendadas" button
|
||||||
|
const [dbgClicks, setDbgClicks] = useState(0)
|
||||||
|
const [dbgLastAt, setDbgLastAt] = useState<string | null>(null)
|
||||||
|
async function handleOpenAgendadas() {
|
||||||
|
try {
|
||||||
|
// quick debug log so we can see the click happened
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.debug('[PacientePage] abrir dialog de consultas')
|
||||||
|
setMostrarAgendadas(true)
|
||||||
|
// record a visible debug pill so it's obvious this ran even if console is closed
|
||||||
|
setDbgClicks(c => c + 1)
|
||||||
|
setDbgLastAt(new Date().toLocaleTimeString())
|
||||||
|
// load data but don't block UI
|
||||||
|
loadAndSetConsultas().catch(() => {})
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn('[PacientePage] handleOpenAgendadas failed', e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -411,7 +465,12 @@ export default function PacientePage() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="transition duration-200 bg-white text-[#1e293b] border border-black/10 rounded-md shadow-[0_2px_6px_rgba(0,0,0,0.03)] hover:bg-[#2563eb] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563eb] dark:bg-slate-800 dark:text-slate-100 dark:border-white/10 dark:hover:bg-[#2563eb] dark:hover:text-white"
|
className="transition duration-200 bg-white text-[#1e293b] border border-black/10 rounded-md shadow-[0_2px_6px_rgba(0,0,0,0.03)] hover:bg-[#2563eb] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563eb] dark:bg-slate-800 dark:text-slate-100 dark:border-white/10 dark:hover:bg-[#2563eb] dark:hover:text-white"
|
||||||
onClick={() => setMostrarAgendadas(true)}
|
id="ver-consultas-btn"
|
||||||
|
onClick={() => handleOpenAgendadas()}
|
||||||
|
onPointerDown={() => handleOpenAgendadas()}
|
||||||
|
onMouseDown={() => handleOpenAgendadas()}
|
||||||
|
onTouchStart={() => handleOpenAgendadas()}
|
||||||
|
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleOpenAgendadas() }}
|
||||||
>
|
>
|
||||||
Ver consultas agendadas
|
Ver consultas agendadas
|
||||||
</Button>
|
</Button>
|
||||||
@ -419,12 +478,117 @@ export default function PacientePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={mostrarAgendadas} onOpenChange={open => setMostrarAgendadas(open)}>
|
<Dialog open={mostrarAgendadas} onOpenChange={open => setMostrarAgendadas(open)}>
|
||||||
<DialogContent className="max-w-3xl space-y-6 sm:max-h-[85vh] overflow-hidden">
|
<DialogContent className="max-w-3xl space-y-6 sm:max-h-[85vh] overflow-hidden">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-2xl font-semibold text-foreground">Consultas agendadas</DialogTitle>
|
<DialogTitle className="text-2xl font-semibold text-foreground">Consultas agendadas</DialogTitle>
|
||||||
<DialogDescription>Gerencie suas consultas confirmadas, pendentes ou canceladas.</DialogDescription>
|
<DialogDescription>Gerencie suas consultas confirmadas, pendentes ou canceladas.</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* visible debug pill to show handler ran (appears after first click) */}
|
||||||
|
{dbgClicks > 0 && (
|
||||||
|
<div className="fixed top-16 right-4 z-[9999] bg-primary text-primary-foreground px-3 py-1 rounded shadow">
|
||||||
|
Clicks: {dbgClicks} • último: {dbgLastAt}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 rounded-lg border border-border bg-muted/40 p-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => navigateDate('prev')}
|
||||||
|
aria-label="Dia anterior"
|
||||||
|
className={`group shadow-sm ${hoverPrimaryIconClass}`}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4 transition group-hover:text-white" />
|
||||||
|
</Button>
|
||||||
|
<span className="text-lg font-medium text-foreground">{formatDatePt(currentDate)}</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => navigateDate('next')}
|
||||||
|
aria-label="Próximo dia"
|
||||||
|
className={`group shadow-sm ${hoverPrimaryIconClass}`}
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4 transition group-hover:text-white" />
|
||||||
|
</Button>
|
||||||
|
{isSelectedDateToday && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={goToToday}
|
||||||
|
className="border border-border text-foreground focus-visible:ring-2 focus-visible:ring-[#2563eb]/60 active:scale-[0.97] hover:bg-transparent hover:text-foreground"
|
||||||
|
>
|
||||||
|
Hoje
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{consultasDoDia.length} consulta{consultasDoDia.length !== 1 ? 's' : ''} agendada{consultasDoDia.length !== 1 ? 's' : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 overflow-y-auto max-h-[70vh] pr-1 sm:pr-2">
|
||||||
|
{consultasLoading ? (
|
||||||
|
<div className="text-center py-10 text-muted-foreground">Carregando consultas...</div>
|
||||||
|
) : consultasError ? (
|
||||||
|
<div className="text-center py-10 text-red-600">{consultasError}</div>
|
||||||
|
) : consultasDoDia.length === 0 ? (
|
||||||
|
<div className="text-center py-10 text-muted-foreground">
|
||||||
|
<Calendar className="h-12 w-12 mx-auto mb-4 opacity-60" />
|
||||||
|
<p className="text-lg font-medium">Nenhuma consulta agendada para este dia</p>
|
||||||
|
<p className="text-sm">Use a busca para marcar uma nova consulta.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
consultasDoDia.map(consulta => {
|
||||||
|
// normalize fields for display
|
||||||
|
const doctorName = consulta.doctor_name || consulta.medico || consulta.medico_name || (consulta.doctor && (consulta.doctor.full_name || consulta.doctor.name)) || 'Médico'
|
||||||
|
const specialty = consulta.specialty || consulta.especialidade || consulta.especialidade || consulta.type || ''
|
||||||
|
const location = consulta.unit || consulta.location || consulta.local || ''
|
||||||
|
const scheduled = consulta.scheduled_at || consulta.time || consulta.date || consulta.data || ''
|
||||||
|
let timeDisplay = ''
|
||||||
|
try { const dt = new Date(scheduled); if (!isNaN(dt.getTime())) timeDisplay = `${String(dt.getHours()).padStart(2,'0')}:${String(dt.getMinutes()).padStart(2,'0')}` } catch {}
|
||||||
|
const status = (consulta.status || consulta.state || consulta.status_label || '').toString() || 'Confirmada'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={consulta.id || JSON.stringify(consulta)} className="rounded-xl border border-black/5 dark:border-white/10 bg-card shadow-[0_4px_12px_rgba(0,0,0,0.05)] dark:shadow-none p-5">
|
||||||
|
<div className="grid gap-4 md:grid-cols-[minmax(0,2fr)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1.4fr)] items-start">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<span className="mt-1 h-3 w-3 flex-shrink-0 rounded-full" style={{ backgroundColor: status.toLowerCase().includes('confirm') ? '#22c55e' : status.toLowerCase().includes('pend') ? '#fbbf24' : '#ef4444' }} />
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="font-medium flex items-center gap-2 text-foreground">
|
||||||
|
<Stethoscope className="h-4 w-4 text-muted-foreground" />
|
||||||
|
{doctorName}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground break-words">{specialty} • {location}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-foreground">
|
||||||
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="font-medium">{timeDisplay}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className={`px-3 py-1 rounded-full text-sm font-medium text-white ${status.toLowerCase().includes('confirm') ? 'bg-green-600' : status.toLowerCase().includes('pend') ? 'bg-yellow-500' : 'bg-red-600'}`}>{status}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||||
|
<Button type="button" variant="outline" size="sm" className="border border-[#2563eb]/40 text-[#2563eb] hover:bg-transparent hover:text-[#2563eb] focus-visible:ring-2 focus-visible:ring-[#2563eb]/40 active:scale-[0.97]">Detalhes</Button>
|
||||||
|
{status.toLowerCase() !== 'cancelada' && (<Button type="button" variant="secondary" size="sm" className={hoverPrimaryClass}>Reagendar</Button>)}
|
||||||
|
{status.toLowerCase() !== 'cancelada' && (<Button type="button" variant="destructive" size="sm" className="transition duration-200 hover:bg-[#dc2626] focus-visible:ring-2 focus-visible:ring-[#dc2626]/60 active:scale-[0.97]">Cancelar</Button>)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 rounded-lg border border-border bg-muted/40 p-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-4 rounded-lg border border-border bg-muted/40 p-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
31
susconecta/dev_user_info.json
Normal file
31
susconecta/dev_user_info.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"id": "8c02cbac-16f8-4437-b362-342f5a6ea6df",
|
||||||
|
"email": "yinohos278@nrlord.com",
|
||||||
|
"email_confirmed_at": "2025-10-22T21:14:13.540678Z",
|
||||||
|
"created_at": "2025-10-22T21:14:13.536407Z",
|
||||||
|
"last_sign_in_at": "2025-10-23T19:35:45.222772Z"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"id": "8c02cbac-16f8-4437-b362-342f5a6ea6df",
|
||||||
|
"full_name": "João Gustavo",
|
||||||
|
"avatar_url": null,
|
||||||
|
"created_at": "2025-10-22T21:14:13.536083+00:00",
|
||||||
|
"updated_at": "2025-10-22T21:14:13.581474+00:00",
|
||||||
|
"email": "yinohos278@nrlord.com",
|
||||||
|
"disabled": false,
|
||||||
|
"phone": "(79) 99161-9988"
|
||||||
|
},
|
||||||
|
"roles": [
|
||||||
|
"paciente"
|
||||||
|
],
|
||||||
|
"permissions": {
|
||||||
|
"isAdmin": false,
|
||||||
|
"isManager": false,
|
||||||
|
"isDoctor": false,
|
||||||
|
"isSecretary": false,
|
||||||
|
"isPatient": true,
|
||||||
|
"canManageUsers": false,
|
||||||
|
"isAdminOrManager": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user