feature/add-appointments-endpoint #52
@ -76,6 +76,7 @@ const capitalize = (s: string) => {
|
|||||||
|
|
||||||
export default function ConsultasPage() {
|
export default function ConsultasPage() {
|
||||||
const [appointments, setAppointments] = useState<any[]>([]);
|
const [appointments, setAppointments] = useState<any[]>([]);
|
||||||
|
const [originalAppointments, setOriginalAppointments] = useState<any[]>([]);
|
||||||
const [searchValue, setSearchValue] = useState<string>('');
|
const [searchValue, setSearchValue] = useState<string>('');
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
@ -292,6 +293,7 @@ export default function ConsultasPage() {
|
|||||||
const mapped = await fetchAndMapAppointments();
|
const mapped = await fetchAndMapAppointments();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setAppointments(mapped);
|
setAppointments(mapped);
|
||||||
|
setOriginalAppointments(mapped || []);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[ConsultasPage] Falha ao carregar agendamentos, usando mocks", 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
|
// 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();
|
const trimmed = String(val || '').trim();
|
||||||
if (!trimmed) return;
|
if (!trimmed) {
|
||||||
setIsLoading(true);
|
setAppointments(originalAppointments || []);
|
||||||
try {
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await performSearch(searchValue);
|
// keep behavior consistent: perform a local filter immediately
|
||||||
|
performSearch(searchValue);
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === 'Escape') {
|
||||||
setSearchValue('');
|
setSearchValue('');
|
||||||
setIsLoading(true);
|
setAppointments(originalAppointments || []);
|
||||||
try {
|
|
||||||
const mapped = await fetchAndMapAppointments();
|
|
||||||
setAppointments(mapped);
|
|
||||||
} catch (err) {
|
|
||||||
setAppointments([]);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -378,8 +347,8 @@ export default function ConsultasPage() {
|
|||||||
setSearchValue('');
|
setSearchValue('');
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const mapped = await fetchAndMapAppointments();
|
// Reset to the original cached list without refetching from server
|
||||||
setAppointments(mapped);
|
setAppointments(originalAppointments || []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setAppointments([]);
|
setAppointments([]);
|
||||||
} finally {
|
} 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
|
// Keep localForm synchronized with editingAppointment
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showForm && editingAppointment) {
|
if (showForm && editingAppointment) {
|
||||||
@ -451,27 +428,12 @@ export default function ConsultasPage() {
|
|||||||
<Input
|
<Input
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="Buscar por..."
|
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}
|
value={searchValue}
|
||||||
onChange={(e) => setSearchValue(e.target.value)}
|
onChange={(e) => setSearchValue(e.target.value)}
|
||||||
onKeyDown={handleSearchKeyDown}
|
onKeyDown={handleSearchKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<Select>
|
<Select>
|
||||||
<SelectTrigger className="w-[180px]">
|
<SelectTrigger className="w-[180px]">
|
||||||
|
|||||||
@ -200,7 +200,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
'apikey': ENV_CONFIG.SUPABASE_ANON_KEY,
|
'apikey': ENV_CONFIG.SUPABASE_ANON_KEY,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (infoRes.ok) {
|
if (infoRes.ok) {
|
||||||
const info = await infoRes.json().catch(() => null)
|
const info = await infoRes.json().catch(() => null)
|
||||||
const roles: string[] = Array.isArray(info?.roles) ? info.roles : (info?.roles ? [info.roles] : [])
|
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
|
response.user.userType = derived
|
||||||
console.log('[AUTH] userType reconciled from roles ->', 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 {
|
} else {
|
||||||
console.warn('[AUTH] Falha ao obter user-info para reconciliar roles:', infoRes.status)
|
console.warn('[AUTH] Falha ao obter user-info para reconciliar roles:', infoRes.status)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -724,10 +724,29 @@ async function parse<T>(res: Response): Promise<T> {
|
|||||||
rawText = '';
|
rawText = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.error('[API ERROR]', res.url, res.status, json, 'raw:', rawText);
|
|
||||||
const code = (json && (json.error?.code || json.code)) ?? res.status;
|
const code = (json && (json.error?.code || json.code)) ?? res.status;
|
||||||
const msg = (json && (json.error?.message || json.message || json.error)) ?? res.statusText;
|
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
|
// Mensagens amigáveis para erros comuns
|
||||||
let friendlyMessage = msg;
|
let friendlyMessage = msg;
|
||||||
|
|
||||||
@ -741,28 +760,24 @@ async function parse<T>(res: Response): Promise<T> {
|
|||||||
friendlyMessage = 'Tipo de acesso inválido.';
|
friendlyMessage = 'Tipo de acesso inválido.';
|
||||||
} else if (msg?.includes('Missing required fields')) {
|
} else if (msg?.includes('Missing required fields')) {
|
||||||
friendlyMessage = 'Campos obrigatórios não preenchidos.';
|
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) {
|
} else if (res.status === 500) {
|
||||||
friendlyMessage = 'Erro no servidor ao criar usuário. Entre em contato com o suporte.';
|
friendlyMessage = 'Erro no servidor ao criar usuário. Entre em contato com o suporte.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Erro de CPF duplicado
|
// 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.';
|
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)
|
// 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.';
|
friendlyMessage = 'Já existe um paciente cadastrado com este email. Por favor, use um email diferente.';
|
||||||
}
|
}
|
||||||
// Erro de CRM duplicado (médico)
|
// 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.';
|
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)
|
// 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.';
|
friendlyMessage = 'Já existe um médico cadastrado com este email. Por favor, use um email diferente.';
|
||||||
}
|
}
|
||||||
// Outros erros de constraint unique
|
// 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[]> {
|
export async function listarAgendamentos(query?: string): Promise<Appointment[]> {
|
||||||
const qs = query && String(query).trim() ? (String(query).startsWith('?') ? query : `?${query}`) : '';
|
const qs = query && String(query).trim() ? (String(query).startsWith('?') ? query : `?${query}`) : '';
|
||||||
const url = `${REST}/appointments${qs}`;
|
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);
|
return await parse<Appointment[]>(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1720,11 +1744,23 @@ export async function getCurrentUser(): Promise<CurrentUser> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserInfo(): Promise<UserInfo> {
|
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 url = `${API_BASE}/functions/v1/user-info`;
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: baseHeaders(),
|
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);
|
return await parse<UserInfo>(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user