Compare commits
No commits in common. "4344ccedca64e5f99943fc50fa1740101153f686" and "5f902e0899dfd3f7cbbc7c3de31f2d8cc7e06603" have entirely different histories.
4344ccedca
...
5f902e0899
@ -520,30 +520,28 @@ export default function RelatoriosPage() {
|
||||
|
||||
{/* Performance por médico */}
|
||||
<div className="bg-card border border-border rounded-lg shadow p-6">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-2 md:gap-0 mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><TrendingUp className="w-5 h-5" /> Performance por Médico</h2>
|
||||
<Button size="sm" variant="outline" className="hover:bg-primary! hover:text-white! transition-colors w-full md:w-auto" onClick={() => exportPDF("Performance por Médico", "Resumo da performance por médico.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
<Button size="sm" variant="outline" className="hover:bg-primary! hover:text-white! transition-colors" onClick={() => exportPDF("Performance por Médico", "Resumo da performance por médico.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-muted-foreground border-b border-border">
|
||||
<th className="text-left font-medium py-3 px-2 md:px-0">Médico</th>
|
||||
<th className="text-center font-medium py-3 px-2 md:px-0">Consultas</th>
|
||||
<th className="text-center font-medium py-3 px-2 md:px-0">Absenteísmo (%)</th>
|
||||
<table className="w-full text-sm mt-4">
|
||||
<thead>
|
||||
<tr className="text-muted-foreground">
|
||||
<th className="text-left font-medium">Médico</th>
|
||||
<th className="text-left font-medium">Consultas</th>
|
||||
<th className="text-left font-medium">Absenteísmo (%)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(loading ? performancePorMedico : medicosPerformance).map((m) => (
|
||||
<tr key={m.nome}>
|
||||
<td className="py-1">{m.nome}</td>
|
||||
<td className="py-1">{m.consultas}</td>
|
||||
<td className="py-1">{m.absenteismo}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(loading ? performancePorMedico : medicosPerformance).map((m) => (
|
||||
<tr key={m.nome} className="border-b border-border/50 hover:bg-muted/30 transition-colors">
|
||||
<td className="py-3 px-2 md:px-0">{m.nome}</td>
|
||||
<td className="py-3 px-2 md:px-0 text-center font-medium">{m.consultas}</td>
|
||||
<td className="py-3 px-2 md:px-0 text-center text-blue-500 font-medium">{m.absenteismo}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -244,12 +244,8 @@ export default function ResultadosClient() {
|
||||
}
|
||||
|
||||
const onlyAvail = (res?.slots || []).filter((s: any) => s.available)
|
||||
const nowMs = Date.now()
|
||||
for (const s of onlyAvail) {
|
||||
const dt = new Date(s.datetime)
|
||||
const dtMs = dt.getTime()
|
||||
// Filtrar: só mostrar horários que são posteriores ao horário atual
|
||||
if (dtMs < nowMs) continue
|
||||
const key = dt.toISOString().split('T')[0]
|
||||
const bucket = days.find(d => d.dateKey === key)
|
||||
if (!bucket) continue
|
||||
@ -264,6 +260,7 @@ export default function ResultadosClient() {
|
||||
|
||||
// compute nearest slot (earliest available in the returned window, but after now)
|
||||
let nearest: { iso: string; label: string } | null = null
|
||||
const nowMs = Date.now()
|
||||
const allSlots = days.flatMap(d => d.horarios || [])
|
||||
const futureSorted = allSlots
|
||||
.map(s => ({ ...s, ms: new Date(s.iso).getTime() }))
|
||||
@ -585,24 +582,17 @@ export default function ResultadosClient() {
|
||||
})
|
||||
|
||||
const merged = Array.from(mergedMap.values()).sort((a:any,b:any) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime())
|
||||
const nowMs = Date.now()
|
||||
// Filtrar: só mostrar horários que são posteriores ao horário atual
|
||||
const futureOnly = merged.filter((s: any) => new Date(s.datetime).getTime() >= nowMs)
|
||||
const formatted = (futureOnly || []).map((s:any) => ({ iso: s.datetime, label: new Date(s.datetime).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) }))
|
||||
const formatted = (merged || []).map((s:any) => ({ iso: s.datetime, label: new Date(s.datetime).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) }))
|
||||
setMoreTimesSlots(formatted)
|
||||
return formatted
|
||||
} else {
|
||||
const nowMs = Date.now()
|
||||
// Filtrar: só mostrar horários que são posteriores ao horário atual
|
||||
const slots = (av.slots || []).filter((s:any) => new Date(s.datetime).getTime() >= nowMs).map((s:any) => ({ iso: s.datetime, label: new Date(s.datetime).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) }))
|
||||
const slots = (av.slots || []).map((s:any) => ({ iso: s.datetime, label: new Date(s.datetime).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) }))
|
||||
setMoreTimesSlots(slots)
|
||||
return slots
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[ResultadosClient] erro ao filtrar por disponibilidades', e)
|
||||
const nowMs = Date.now()
|
||||
// Filtrar: só mostrar horários que são posteriores ao horário atual
|
||||
const slots = (av.slots || []).filter((s:any) => new Date(s.datetime).getTime() >= nowMs).map((s:any) => ({ iso: s.datetime, label: new Date(s.datetime).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) }))
|
||||
const slots = (av.slots || []).map((s:any) => ({ iso: s.datetime, label: new Date(s.datetime).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) }))
|
||||
setMoreTimesSlots(slots)
|
||||
return slots
|
||||
}
|
||||
|
||||
@ -7,7 +7,6 @@ import ProtectedRoute from "@/components/shared/ProtectedRoute";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { buscarPacientes, listarPacientes, buscarPacientePorId, buscarPacientesPorIds, buscarMedicoPorId, buscarMedicosPorIds, buscarMedicos, listarAgendamentos, type Paciente, buscarRelatorioPorId, atualizarMedico } from "@/lib/api";
|
||||
import { ENV_CONFIG } from '@/lib/env-config';
|
||||
import { useReports } from "@/hooks/useReports";
|
||||
import { CreateReportData } from "@/types/report-types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@ -37,6 +36,7 @@ import {
|
||||
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { ENV_CONFIG } from '@/lib/env-config';
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
@ -182,7 +182,7 @@ const ProfissionalPage = () => {
|
||||
const q = `doctor_id=eq.${encodeURIComponent(String(resolvedDoctorId))}&select=patient_id&limit=200`;
|
||||
const appts = await listarAgendamentos(q).catch(() => []);
|
||||
for (const a of (appts || [])) {
|
||||
const pid = (a as any).patient_id ?? null;
|
||||
const pid = a.patient_id ?? a.patient ?? a.patient_id_raw ?? null;
|
||||
if (pid) patientIdSet.add(String(pid));
|
||||
}
|
||||
} catch (e) {
|
||||
@ -211,7 +211,6 @@ const ProfissionalPage = () => {
|
||||
})();
|
||||
return () => { mounted = false; };
|
||||
// Re-run when user id becomes available so patients assigned to the logged-in doctor are loaded
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [user?.id]);
|
||||
|
||||
// Carregar perfil do médico correspondente ao usuário logado
|
||||
@ -430,9 +429,6 @@ const ProfissionalPage = () => {
|
||||
const [commPhoneNumber, setCommPhoneNumber] = useState('');
|
||||
const [commMessage, setCommMessage] = useState('');
|
||||
const [commPatientId, setCommPatientId] = useState<string | null>(null);
|
||||
const [commResponses, setCommResponses] = useState<any[]>([]);
|
||||
const [commResponsesLoading, setCommResponsesLoading] = useState(false);
|
||||
const [commResponsesError, setCommResponsesError] = useState<string | null>(null);
|
||||
const [smsSending, setSmsSending] = useState(false);
|
||||
|
||||
const handleSave = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
@ -524,68 +520,6 @@ const ProfissionalPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadCommResponses = async (patientId?: string) => {
|
||||
const pid = patientId ?? commPatientId;
|
||||
if (!pid) {
|
||||
setCommResponses([]);
|
||||
setCommResponsesError('Selecione um paciente para ver respostas');
|
||||
return;
|
||||
}
|
||||
setCommResponsesLoading(true);
|
||||
setCommResponsesError(null);
|
||||
try {
|
||||
// 1) tentar buscar por patient_id (o comportamento ideal)
|
||||
const qs = new URLSearchParams();
|
||||
qs.set('patient_id', `eq.${String(pid)}`);
|
||||
qs.set('order', 'created_at.desc');
|
||||
const url = `${(ENV_CONFIG as any).REST}/messages?${qs.toString()}`;
|
||||
const headers: Record<string,string> = { 'Accept': 'application/json' };
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
if ((ENV_CONFIG as any)?.SUPABASE_ANON_KEY) headers['apikey'] = (ENV_CONFIG as any).SUPABASE_ANON_KEY;
|
||||
const r = await fetch(url, { method: 'GET', headers });
|
||||
let data = await r.json().catch(() => []);
|
||||
data = Array.isArray(data) ? data : [];
|
||||
|
||||
// 2) Se não houver mensagens por patient_id, tentar buscar por número (from/to)
|
||||
if ((!data || data.length === 0) && commPhoneNumber) {
|
||||
try {
|
||||
const norm = normalizePhoneNumber(commPhoneNumber);
|
||||
if (norm) {
|
||||
// Primeiro tenta buscar mensagens onde `from` é o número
|
||||
const qsFrom = new URLSearchParams();
|
||||
qsFrom.set('from', `eq.${String(norm)}`);
|
||||
qsFrom.set('order', 'created_at.desc');
|
||||
const urlFrom = `${(ENV_CONFIG as any).REST}/messages?${qsFrom.toString()}`;
|
||||
const rf = await fetch(urlFrom, { method: 'GET', headers });
|
||||
const dataFrom = await rf.json().catch(() => []);
|
||||
if (Array.isArray(dataFrom) && dataFrom.length) {
|
||||
data = dataFrom;
|
||||
} else {
|
||||
// se nada, tenta `to` (caso o provedor grave a direção inversa)
|
||||
const qsTo = new URLSearchParams();
|
||||
qsTo.set('to', `eq.${String(norm)}`);
|
||||
qsTo.set('order', 'created_at.desc');
|
||||
const urlTo = `${(ENV_CONFIG as any).REST}/messages?${qsTo.toString()}`;
|
||||
const rt = await fetch(urlTo, { method: 'GET', headers });
|
||||
const dataTo = await rt.json().catch(() => []);
|
||||
if (Array.isArray(dataTo) && dataTo.length) data = dataTo;
|
||||
}
|
||||
}
|
||||
} catch (phoneErr) {
|
||||
// não bloqueara o fluxo principal; apenas log
|
||||
console.warn('[ProfissionalPage] fallback por telefone falhou', phoneErr);
|
||||
}
|
||||
}
|
||||
|
||||
setCommResponses(Array.isArray(data) ? data : []);
|
||||
} catch (e: any) {
|
||||
setCommResponsesError(String(e?.message || e || 'Falha ao buscar respostas'));
|
||||
setCommResponses([]);
|
||||
} finally {
|
||||
setCommResponsesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleEditarLaudo = (paciente: any) => {
|
||||
@ -2704,9 +2638,6 @@ const ProfissionalPage = () => {
|
||||
// Use a sentinel value "__none" for the "-- nenhum --" choice and map it to null here.
|
||||
const v = val === "__none" ? null : (val || null);
|
||||
setCommPatientId(v);
|
||||
// clear previous responses when changing selection
|
||||
setCommResponses([]);
|
||||
setCommResponsesError(null);
|
||||
if (!v) {
|
||||
setCommPhoneNumber('');
|
||||
return;
|
||||
@ -2724,8 +2655,6 @@ const ProfissionalPage = () => {
|
||||
console.warn('[ProfissionalPage] erro ao preencher telefone do paciente selecionado', e);
|
||||
setCommPhoneNumber('');
|
||||
}
|
||||
// carregar respostas do paciente selecionado
|
||||
void loadCommResponses(String(v));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
@ -2757,35 +2686,6 @@ const ProfissionalPage = () => {
|
||||
{smsSending ? 'Enviando...' : 'Enviar SMS'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Respostas do paciente */}
|
||||
<div className="mt-6 border-t border-border pt-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-lg font-semibold">Últimas respostas do paciente</h3>
|
||||
<div>
|
||||
<Button size="sm" variant="outline" onClick={() => void loadCommResponses()} disabled={!commPatientId || commResponsesLoading}>
|
||||
{commResponsesLoading ? 'Atualizando...' : 'Atualizar respostas'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{commResponsesLoading ? (
|
||||
<div className="text-sm text-muted-foreground">Carregando respostas...</div>
|
||||
) : commResponsesError ? (
|
||||
<div className="text-sm text-red-500">{commResponsesError}</div>
|
||||
) : (commResponses && commResponses.length) ? (
|
||||
<div className="space-y-2">
|
||||
{commResponses.map((m:any) => (
|
||||
<div key={m.id} className="p-3 rounded border border-border bg-muted/10">
|
||||
<div className="text-xs text-muted-foreground">{m.created_at ? new Date(m.created_at).toLocaleString() : ''}</div>
|
||||
<div className="mt-1 whitespace-pre-wrap">{m.body ?? m.content ?? m.message ?? '-'}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground">Nenhuma resposta encontrada para o paciente selecionado.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -3041,8 +2941,8 @@ const ProfissionalPage = () => {
|
||||
);
|
||||
case 'laudos':
|
||||
return renderLaudosSection();
|
||||
case 'comunicacao':
|
||||
return renderComunicacaoSection();
|
||||
// case 'comunicacao':
|
||||
// return renderComunicacaoSection();
|
||||
case 'perfil':
|
||||
return renderPerfilSection();
|
||||
default:
|
||||
@ -3113,14 +3013,15 @@ const ProfissionalPage = () => {
|
||||
<FileText className="mr-2 h-4 w-4" />
|
||||
Laudos
|
||||
</Button>
|
||||
<Button
|
||||
{/* Comunicação removida - campos embaixo do calendário */}
|
||||
{/* <Button
|
||||
variant={activeSection === 'comunicacao' ? 'default' : 'ghost'}
|
||||
className="w-full justify-start transition-colors hover:bg-primary! hover:text-white! cursor-pointer"
|
||||
onClick={() => setActiveSection('comunicacao')}
|
||||
>
|
||||
<MessageSquare className="mr-2 h-4 w-4" />
|
||||
SMS
|
||||
</Button>
|
||||
Comunicação
|
||||
</Button> */}
|
||||
<Button
|
||||
variant={activeSection === 'perfil' ? 'default' : 'ghost'}
|
||||
className="w-full justify-start transition-colors hover:bg-primary! hover:text-white! cursor-pointer"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user