develop #83

Merged
M-Gabrielly merged 426 commits from develop into main 2025-12-04 04:13:15 +00:00
3 changed files with 91 additions and 91 deletions
Showing only changes of commit ee3b855f8a - Show all commits

View File

@ -76,6 +76,7 @@ const capitalize = (s: string) => {
export default function ConsultasPage() {
const [appointments, setAppointments] = useState<any[]>([]);
const [originalAppointments, setOriginalAppointments] = useState<any[]>([]);
const [searchValue, setSearchValue] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(true);
const [showForm, setShowForm] = useState(false);
@ -292,6 +293,7 @@ export default function ConsultasPage() {
const mapped = await fetchAndMapAppointments();
if (!mounted) return;
setAppointments(mapped);
setOriginalAppointments(mapped || []);
setIsLoading(false);
} catch (err) {
console.warn("[ConsultasPage] Falha ao carregar agendamentos, usando mocks", err);
@ -304,73 +306,40 @@ export default function ConsultasPage() {
}, []);
// Search box: allow fetching a single appointment by ID when pressing Enter
const performSearch = async (val: string) => {
// Perform a local-only search against the already-loaded appointments.
// This intentionally does not call the server — it filters the cached list.
const performSearch = (val: string) => {
const trimmed = String(val || '').trim();
if (!trimmed) return;
setIsLoading(true);
try {
const ap = await buscarAgendamentoPorId(trimmed, '*');
// resolve patient and doctor names if possible
let patient = ap.patient_id || '';
let professional = ap.doctor_id || '';
try {
if (ap.patient_id) {
const list = await buscarPacientesPorIds([ap.patient_id]);
if (list && list.length) patient = list[0].full_name || String(ap.patient_id);
}
} catch (e) {
// ignore
}
try {
if (ap.doctor_id) {
const list = await buscarMedicosPorIds([ap.doctor_id]);
if (list && list.length) professional = list[0].full_name || String(ap.doctor_id);
}
} catch (e) {}
const mappedSingle = [{
id: ap.id,
patient,
patient_id: ap.patient_id,
scheduled_at: ap.scheduled_at,
duration_minutes: ap.duration_minutes ?? null,
appointment_type: ap.appointment_type ?? null,
status: ap.status ?? 'requested',
professional,
notes: ap.notes ?? ap.patient_notes ?? '',
chief_complaint: ap.chief_complaint ?? null,
patient_notes: ap.patient_notes ?? null,
insurance_provider: ap.insurance_provider ?? null,
checked_in_at: ap.checked_in_at ?? null,
completed_at: ap.completed_at ?? null,
cancelled_at: ap.cancelled_at ?? null,
cancellation_reason: ap.cancellation_reason ?? null,
}];
setAppointments(mappedSingle as any[]);
} catch (err) {
console.warn('[ConsultasPage] buscarAgendamentoPorId falhou:', err);
alert('Agendamento não encontrado');
} finally {
setIsLoading(false);
if (!trimmed) {
setAppointments(originalAppointments || []);
return;
}
const q = trimmed.toLowerCase();
const localMatches = (originalAppointments || []).filter((a) => {
const patient = String(a.patient || '').toLowerCase();
const professional = String(a.professional || '').toLowerCase();
const pid = String(a.patient_id || '').toLowerCase();
const aid = String(a.id || '').toLowerCase();
return (
patient.includes(q) ||
professional.includes(q) ||
pid === q ||
aid === q
);
});
setAppointments(localMatches as any[]);
};
const handleSearchKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
const handleSearchKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
await performSearch(searchValue);
// keep behavior consistent: perform a local filter immediately
performSearch(searchValue);
} else if (e.key === 'Escape') {
setSearchValue('');
setIsLoading(true);
try {
const mapped = await fetchAndMapAppointments();
setAppointments(mapped);
} catch (err) {
setAppointments([]);
} finally {
setIsLoading(false);
}
setAppointments(originalAppointments || []);
}
};
@ -378,8 +347,8 @@ export default function ConsultasPage() {
setSearchValue('');
setIsLoading(true);
try {
const mapped = await fetchAndMapAppointments();
setAppointments(mapped);
// Reset to the original cached list without refetching from server
setAppointments(originalAppointments || []);
} catch (err) {
setAppointments([]);
} finally {
@ -387,6 +356,14 @@ export default function ConsultasPage() {
}
};
// Debounce live filtering as the user types. Operates only on the cached originalAppointments.
useEffect(() => {
const t = setTimeout(() => {
performSearch(searchValue);
}, 250);
return () => clearTimeout(t);
}, [searchValue, originalAppointments]);
// Keep localForm synchronized with editingAppointment
useEffect(() => {
if (showForm && editingAppointment) {
@ -451,27 +428,12 @@ export default function ConsultasPage() {
<Input
type="search"
placeholder="Buscar por..."
className="pl-8 pr-4 w-full shadow-sm border border-border bg-transparent mr-2"
className="pl-8 pr-4 w-full shadow-sm border border-border bg-transparent"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onKeyDown={handleSearchKeyDown}
/>
</div>
<div className="flex-shrink-0 flex items-center gap-2">
<Button
size="sm"
variant="ghost"
className="h-8 px-3 rounded-md bg-muted/10 hover:bg-muted/20 border border-border shadow-sm"
onClick={() => performSearch(searchValue)}
aria-label="Buscar agendamento"
>
<Search className="h-4 w-4 mr-1" />
<span className="hidden sm:inline">Buscar</span>
</Button>
<Button size="sm" variant="outline" className="h-8 px-3" onClick={handleClearSearch}>
Limpar
</Button>
</div>
</div>
<Select>
<SelectTrigger className="w-[180px]">

View File

@ -200,7 +200,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
'apikey': ENV_CONFIG.SUPABASE_ANON_KEY,
}
})
if (infoRes.ok) {
const info = await infoRes.json().catch(() => null)
const roles: string[] = Array.isArray(info?.roles) ? info.roles : (info?.roles ? [info.roles] : [])
@ -218,6 +217,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
response.user.userType = derived
console.log('[AUTH] userType reconciled from roles ->', derived)
}
} else if (infoRes.status === 401 || infoRes.status === 403) {
// Authentication/permission issue: don't spam the console with raw response
console.warn('[AUTH] user-info returned', infoRes.status, '- skipping role reconciliation');
} else {
console.warn('[AUTH] Falha ao obter user-info para reconciliar roles:', infoRes.status)
}

View File

@ -724,10 +724,29 @@ async function parse<T>(res: Response): Promise<T> {
rawText = '';
}
}
console.error('[API ERROR]', res.url, res.status, json, 'raw:', rawText);
const code = (json && (json.error?.code || json.code)) ?? res.status;
const msg = (json && (json.error?.message || json.message || json.error)) ?? res.statusText;
// Special-case authentication/authorization errors to reduce noisy logs
if (res.status === 401) {
// If the server returned an empty body, avoid dumping raw text to console.error
if (!rawText && !json) {
console.warn('[API AUTH] 401 Unauthorized for', res.url, '- no auth token or token expired.');
} else {
console.warn('[API AUTH] 401 Unauthorized for', res.url, 'response:', json ?? rawText);
}
throw new Error('Você não está autenticado. Faça login novamente.');
}
if (res.status === 403) {
console.warn('[API AUTH] 403 Forbidden for', res.url, (json ?? rawText) ? 'response: ' + (json ?? rawText) : '');
throw new Error('Você não tem permissão para executar esta ação.');
}
// For other errors, log a concise error and try to produce a friendly message
console.error('[API ERROR]', res.url, res.status, json ? json : 'no-json', rawText ? 'raw body present' : 'no raw body');
// Mensagens amigáveis para erros comuns
let friendlyMessage = msg;
@ -741,28 +760,24 @@ async function parse<T>(res: Response): Promise<T> {
friendlyMessage = 'Tipo de acesso inválido.';
} else if (msg?.includes('Missing required fields')) {
friendlyMessage = 'Campos obrigatórios não preenchidos.';
} else if (res.status === 401) {
friendlyMessage = 'Você não está autenticado. Faça login novamente.';
} else if (res.status === 403) {
friendlyMessage = 'Você não tem permissão para criar usuários.';
} else if (res.status === 500) {
friendlyMessage = 'Erro no servidor ao criar usuário. Entre em contato com o suporte.';
}
}
// Erro de CPF duplicado
else if (code === '23505' && msg.includes('patients_cpf_key')) {
else if (code === '23505' && msg && msg.includes('patients_cpf_key')) {
friendlyMessage = 'Já existe um paciente cadastrado com este CPF. Por favor, verifique se o paciente já está registrado no sistema ou use um CPF diferente.';
}
// Erro de email duplicado (paciente)
else if (code === '23505' && msg.includes('patients_email_key')) {
else if (code === '23505' && msg && msg.includes('patients_email_key')) {
friendlyMessage = 'Já existe um paciente cadastrado com este email. Por favor, use um email diferente.';
}
// Erro de CRM duplicado (médico)
else if (code === '23505' && msg.includes('doctors_crm')) {
else if (code === '23505' && msg && msg.includes('doctors_crm')) {
friendlyMessage = 'Já existe um médico cadastrado com este CRM. Por favor, verifique se o médico já está registrado no sistema.';
}
// Erro de email duplicado (médico)
else if (code === '23505' && msg.includes('doctors_email_key')) {
else if (code === '23505' && msg && msg.includes('doctors_email_key')) {
friendlyMessage = 'Já existe um médico cadastrado com este email. Por favor, use um email diferente.';
}
// Outros erros de constraint unique
@ -1005,7 +1020,16 @@ export async function atualizarAgendamento(id: string | number, input: Appointme
export async function listarAgendamentos(query?: string): Promise<Appointment[]> {
const qs = query && String(query).trim() ? (String(query).startsWith('?') ? query : `?${query}`) : '';
const url = `${REST}/appointments${qs}`;
const res = await fetch(url, { method: 'GET', headers: baseHeaders() });
const headers = baseHeaders();
// If there is no auth token, avoid calling the endpoint which requires auth and return friendly error
const jwt = getAuthToken();
if (!jwt) {
throw new Error('Não autenticado. Faça login para listar agendamentos.');
}
const res = await fetch(url, { method: 'GET', headers });
if (!res.ok && res.status === 401) {
throw new Error('Não autenticado. Token ausente ou expirado. Faça login novamente.');
}
return await parse<Appointment[]>(res);
}
@ -1720,11 +1744,23 @@ export async function getCurrentUser(): Promise<CurrentUser> {
}
export async function getUserInfo(): Promise<UserInfo> {
const jwt = getAuthToken();
if (!jwt) {
// No token available — avoid calling the protected function and throw a friendly error
throw new Error('Você não está autenticado. Faça login para acessar informações do usuário.');
}
const url = `${API_BASE}/functions/v1/user-info`;
const res = await fetch(url, {
method: "GET",
headers: baseHeaders(),
});
// Avoid calling parse() for auth errors to prevent noisy console dumps
if (!res.ok && res.status === 401) {
throw new Error('Você não está autenticado. Faça login novamente.');
}
return await parse<UserInfo>(res);
}