backup/agendamento #60
@ -11,6 +11,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select";
|
||||
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
|
||||
import {
|
||||
Table,
|
||||
@ -20,13 +21,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { User, FolderOpen, X, Users, MessageSquare, ClipboardList, Plus, Edit, Trash2, ChevronLeft, ChevronRight, Clock, FileCheck, Upload, Download, Eye, History, Stethoscope, Pill, Activity, Search } from "lucide-react"
|
||||
import { Calendar as CalendarIcon, FileText, Settings } from "lucide-react";
|
||||
@ -39,6 +34,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";
|
||||
@ -82,6 +78,20 @@ const colorsByType = {
|
||||
return p?.idade ?? p?.age ?? '';
|
||||
};
|
||||
|
||||
// Normaliza número de telefone para E.164 básico (prioriza +55 quando aplicável)
|
||||
const normalizePhoneNumber = (raw?: string) => {
|
||||
if (!raw || typeof raw !== 'string') return '';
|
||||
// Remover tudo que não for dígito
|
||||
const digits = raw.replace(/\D+/g, '');
|
||||
if (!digits) return '';
|
||||
// Já tem código de país (começa com 55)
|
||||
if (digits.startsWith('55') && digits.length >= 11) return '+' + digits;
|
||||
// Se tiver 10 ou 11 dígitos (DDD + número), assume Brasil e prefixa +55
|
||||
if (digits.length === 10 || digits.length === 11) return '+55' + digits;
|
||||
// Se tiver outros formatos pequenos, apenas prefixa +
|
||||
return '+' + digits;
|
||||
};
|
||||
|
||||
// Helpers para normalizar campos do laudo/relatório
|
||||
const getReportPatientName = (r: any) => r?.paciente?.full_name ?? r?.paciente?.nome ?? r?.patient?.full_name ?? r?.patient?.nome ?? r?.patient_name ?? r?.patient_full_name ?? '';
|
||||
const getReportPatientId = (r: any) => r?.paciente?.id ?? r?.patient?.id ?? r?.patient_id ?? r?.patientId ?? r?.patient_id_raw ?? r?.patient_id ?? r?.id ?? '';
|
||||
@ -101,7 +111,7 @@ const colorsByType = {
|
||||
};
|
||||
|
||||
const ProfissionalPage = () => {
|
||||
const { logout, user } = useAuth();
|
||||
const { logout, user, token } = useAuth();
|
||||
const [activeSection, setActiveSection] = useState('calendario');
|
||||
const [pacienteSelecionado, setPacienteSelecionado] = useState<any>(null);
|
||||
|
||||
@ -374,10 +384,98 @@ const ProfissionalPage = () => {
|
||||
const [selectedEvent, setSelectedEvent] = useState<any>(null);
|
||||
const [currentCalendarDate, setCurrentCalendarDate] = useState(new Date());
|
||||
|
||||
const handleSave = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const [commPhoneNumber, setCommPhoneNumber] = useState('');
|
||||
const [commMessage, setCommMessage] = useState('');
|
||||
const [commPatientId, setCommPatientId] = useState<string | null>(null);
|
||||
const [smsSending, setSmsSending] = useState(false);
|
||||
|
||||
const handleSave = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
console.log("Laudo salvo!");
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
setSmsSending(true);
|
||||
try {
|
||||
// Validate required fields
|
||||
if (!commPhoneNumber || !commPhoneNumber.trim()) throw new Error('O campo phone_number é obrigatório');
|
||||
if (!commMessage || !commMessage.trim()) throw new Error('O campo message é obrigatório');
|
||||
|
||||
const payload: any = { phone_number: commPhoneNumber.trim(), message: commMessage.trim() };
|
||||
if (commPatientId) payload.patient_id = commPatientId;
|
||||
|
||||
const headers: Record<string,string> = { 'Content-Type': 'application/json' };
|
||||
// include any default headers from ENV_CONFIG if present (e.g. apikey)
|
||||
if ((ENV_CONFIG as any)?.DEFAULT_HEADERS) Object.assign(headers, (ENV_CONFIG as any).DEFAULT_HEADERS);
|
||||
// include Authorization if we have a token (user session)
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
// Ensure apikey is present (frontend only has ANON key in this project)
|
||||
if (!headers.apikey && (ENV_CONFIG as any)?.SUPABASE_ANON_KEY) {
|
||||
headers.apikey = (ENV_CONFIG as any).SUPABASE_ANON_KEY;
|
||||
}
|
||||
// Ensure Accept header
|
||||
headers['Accept'] = 'application/json';
|
||||
|
||||
// Normalizar número antes de enviar (E.164 básico)
|
||||
const normalized = normalizePhoneNumber(commPhoneNumber);
|
||||
if (!normalized) throw new Error('Número inválido após normalização');
|
||||
payload.phone_number = normalized;
|
||||
|
||||
// Debug: log payload and headers with secrets masked to help diagnose issues
|
||||
try {
|
||||
const masked = { ...headers } as Record<string, any>;
|
||||
if (masked.apikey && typeof masked.apikey === 'string') masked.apikey = `${masked.apikey.slice(0,4)}...${masked.apikey.slice(-4)}`;
|
||||
if (masked.Authorization) masked.Authorization = 'Bearer <<token-present>>';
|
||||
console.debug('[ProfissionalPage] Enviando SMS -> url:', `${(ENV_CONFIG as any).SUPABASE_URL}/functions/v1/send-sms`, 'payload:', payload, 'headers(masked):', masked);
|
||||
} catch (e) {
|
||||
// ignore logging errors
|
||||
}
|
||||
|
||||
const res = await fetch(`${(ENV_CONFIG as any).SUPABASE_URL}/functions/v1/send-sms`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const body = await res.json().catch(() => null);
|
||||
if (!res.ok) {
|
||||
// If server returned 5xx and we sent a patient_id, try a single retry without patient_id
|
||||
if (res.status >= 500 && payload.patient_id) {
|
||||
try {
|
||||
const fallback = { phone_number: payload.phone_number, message: payload.message };
|
||||
console.debug('[ProfissionalPage] 5xx ao enviar com patient_id — tentando reenviar sem patient_id', { fallback });
|
||||
const retryRes = await fetch(`${(ENV_CONFIG as any).SUPABASE_URL}/functions/v1/send-sms`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(fallback),
|
||||
});
|
||||
const retryBody = await retryRes.json().catch(() => null);
|
||||
if (retryRes.ok) {
|
||||
alert('SMS enviado com sucesso (sem patient_id)');
|
||||
setCommPhoneNumber('');
|
||||
setCommMessage('');
|
||||
setCommPatientId(null);
|
||||
return;
|
||||
} else {
|
||||
throw new Error(retryBody?.message || retryBody?.error || `Erro ao enviar SMS (retry ${retryRes.status})`);
|
||||
}
|
||||
} catch (retryErr) {
|
||||
console.warn('[ProfissionalPage] Reenvio sem patient_id falhou', retryErr);
|
||||
throw new Error(body?.message || body?.error || `Erro ao enviar SMS (${res.status})`);
|
||||
}
|
||||
}
|
||||
throw new Error(body?.message || body?.error || `Erro ao enviar SMS (${res.status})`);
|
||||
}
|
||||
|
||||
// success feedback
|
||||
alert('SMS enviado com sucesso');
|
||||
// clear fields
|
||||
setCommPhoneNumber('');
|
||||
setCommMessage('');
|
||||
setCommPatientId(null);
|
||||
} catch (err: any) {
|
||||
alert(String(err?.message || err || 'Falha ao enviar SMS'));
|
||||
} finally {
|
||||
setSmsSending(false);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -2480,60 +2578,64 @@ const ProfissionalPage = () => {
|
||||
<div className="bg-card shadow-md rounded-lg p-6">
|
||||
<h2 className="text-2xl font-bold mb-4 text-foreground">Comunicação com o Paciente</h2>
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="destinatario">Destinatário</Label>
|
||||
<Select>
|
||||
<SelectTrigger id="destinatario" className="hover:border-primary focus:border-primary cursor-pointer">
|
||||
<SelectValue placeholder="Selecione o paciente" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-popover border">
|
||||
{pacientes.map((paciente) => (
|
||||
<SelectItem
|
||||
key={paciente.cpf}
|
||||
value={paciente.nome}
|
||||
className="hover:bg-blue-50 focus:bg-blue-50 cursor-pointer dark:hover:bg-primary dark:hover:text-primary-foreground dark:focus:bg-primary dark:focus:text-primary-foreground"
|
||||
<Label htmlFor="patientSelect">Paciente *</Label>
|
||||
<Select
|
||||
value={commPatientId ?? ''}
|
||||
onValueChange={(val: string) => {
|
||||
// Radix Select does not allow an Item with empty string as value.
|
||||
// Use a sentinel value "__none" for the "-- nenhum --" choice and map it to null here.
|
||||
const v = val === "__none" ? null : (val || null);
|
||||
setCommPatientId(v);
|
||||
if (!v) {
|
||||
setCommPhoneNumber('');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const found = (pacientes || []).find((p: any) => String(p.id ?? p.uuid ?? p.email ?? '') === String(v));
|
||||
if (found) {
|
||||
setCommPhoneNumber(
|
||||
found.phone_mobile ?? found.celular ?? found.telefone ?? found.phone ?? found.mobile ?? found.phone_number ?? ''
|
||||
);
|
||||
} else {
|
||||
setCommPhoneNumber('');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[ProfissionalPage] erro ao preencher telefone do paciente selecionado', e);
|
||||
setCommPhoneNumber('');
|
||||
}
|
||||
}}
|
||||
>
|
||||
{paciente.nome} - {paciente.cpf}
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="-- nenhum --" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none">-- nenhum --</SelectItem>
|
||||
{pacientes && pacientes.map((p:any) => (
|
||||
<SelectItem key={String(p.id || p.uuid || p.cpf || p.email)} value={String(p.id ?? p.uuid ?? p.email ?? '')}>
|
||||
{p.full_name ?? p.nome ?? p.name ?? p.email ?? String(p.id ?? p.cpf ?? '')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tipoMensagem">Tipo de mensagem</Label>
|
||||
<Select>
|
||||
<SelectTrigger id="tipoMensagem" className="hover:border-primary focus:border-primary cursor-pointer">
|
||||
<SelectValue placeholder="Selecione o tipo" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-popover border">
|
||||
<SelectItem value="lembrete" className="hover:bg-blue-50 focus:bg-blue-50 cursor-pointer dark:hover:bg-primary dark:hover:text-primary-foreground dark:focus:bg-primary dark:focus:text-primary-foreground">Lembrete de Consulta</SelectItem>
|
||||
<SelectItem value="resultado" className="hover:bg-blue-50 focus:bg-blue-50 cursor-pointer dark:hover:bg-primary dark:hover:text-primary-foreground dark:focus:bg-primary dark:focus:text-primary-foreground">Resultado de Exame</SelectItem>
|
||||
<SelectItem value="instrucao" className="hover:bg-blue-50 focus:bg-blue-50 cursor-pointer dark:hover:bg-primary dark:hover:text-primary-foreground dark:focus:bg-primary dark:focus:text-primary-foreground">Instruções Pós-Consulta</SelectItem>
|
||||
<SelectItem value="outro" className="hover:bg-blue-50 focus:bg-blue-50 cursor-pointer dark:hover:bg-primary dark:hover:text-primary-foreground dark:focus:bg-primary dark:focus:text-primary-foreground">Outro</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label htmlFor="dataEnvio">Data de envio</Label>
|
||||
<p id="dataEnvio" className="text-sm text-muted-foreground">03/09/2025</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="statusEntrega">Status da entrega</Label>
|
||||
<p id="statusEntrega" className="text-sm text-muted-foreground">Pendente</p>
|
||||
</div>
|
||||
<Label htmlFor="phoneNumber">Número (phone_number)</Label>
|
||||
<Input id="phoneNumber" placeholder="+5511999999999" value={commPhoneNumber} readOnly disabled className="bg-muted/50" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Resposta do paciente</Label>
|
||||
<div className="border rounded-md p-3 bg-muted/40 space-y-2">
|
||||
<p className="text-sm">"Ok, obrigado pelo lembrete!"</p>
|
||||
<p className="text-xs text-muted-foreground">03/09/2025 14:30</p>
|
||||
</div>
|
||||
<Label htmlFor="message">Mensagem (message)</Label>
|
||||
<textarea id="message" className="w-full p-2 border rounded" rows={5} value={commMessage} onChange={(e) => setCommMessage(e.target.value)} />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-6">
|
||||
<Button onClick={handleSave}>Registrar Comunicação</Button>
|
||||
<Button onClick={handleSave} disabled={smsSending}>
|
||||
{smsSending ? 'Enviando...' : 'Enviar SMS'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user