forked from RiseUP/riseup-squad20
fix-search-appoinments-endpoint
This commit is contained in:
parent
6e33d6406e
commit
ee3b855f8a
@ -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]">
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -724,13 +724,32 @@ 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;
|
||||
|
||||
|
||||
// Erros de criação de usuário
|
||||
if (res.url?.includes('create-user')) {
|
||||
if (msg?.includes('Failed to assign user role')) {
|
||||
@ -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
|
||||
@ -778,7 +793,7 @@ async function parse<T>(res: Response): Promise<T> {
|
||||
friendlyMessage = 'Registro referenciado em outra tabela. Remova referências dependentes antes de tentar novamente.';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
throw new Error(friendlyMessage);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user