feat(sync): add import-legacy route for Apps Script patients; fix pacientes API to use server-side search
- POST /api/sync/import-legacy: reads PACIENTES-CONSULTT-CLINIC tab (1755 patients),
maps Nome+SobreNome→nome, CPF, Credenciada→convenio, DataNasci, TefefonePrincipal→telefone
- GET /api/pacientes: now supports ?q= server-side search, returns {data, total} — avoids loading all 1755 rows in client
- PatientsView: uses {data, total} response, shows count of all patients in header
- SyncView: adds "Importar Planilha Legada" button (amber)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -201,18 +201,18 @@ export const HybridBackend = {
|
||||
return true;
|
||||
},
|
||||
|
||||
getPacientes: async (termo: string = ''): Promise<Paciente[]> => {
|
||||
const res = await apiFetch(`${API_URL}/pacientes`);
|
||||
const data = await res.json();
|
||||
if (!termo) return data;
|
||||
const t = termo.toUpperCase();
|
||||
return data.filter((p: Paciente) => p.nome.includes(t) || p.cpf.includes(t));
|
||||
getPacientes: async (termo: string = ''): Promise<{ data: Paciente[]; total: number }> => {
|
||||
const url = termo.trim() ? `${API_URL}/pacientes?q=${encodeURIComponent(termo)}` : `${API_URL}/pacientes`;
|
||||
const res = await apiFetch(url);
|
||||
return res.json();
|
||||
},
|
||||
|
||||
checkCpfExists: async (cpf: string): Promise<boolean> => {
|
||||
const pacientes = await HybridBackend.getPacientes();
|
||||
const cleanCpf = cpf.replace(/\D/g, '');
|
||||
if (!cleanCpf) return false;
|
||||
const res = await apiFetch(`${API_URL}/pacientes?q=${encodeURIComponent(cpf)}`);
|
||||
const { data } = await res.json();
|
||||
const pacientes: Paciente[] = data || [];
|
||||
return pacientes.some(p => p.cpf.replace(/\D/g, '') === cleanCpf);
|
||||
},
|
||||
|
||||
|
||||
@@ -853,7 +853,7 @@ const SYNC_TABS = ['pacientes', 'agendamentos', 'financeiro', 'leads', 'guias'];
|
||||
|
||||
export const SyncView: React.FC = () => {
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
const [operation, setOperation] = useState<'push' | 'pull' | null>(null);
|
||||
const [operation, setOperation] = useState<'push' | 'pull' | 'import-legacy' | null>(null);
|
||||
const [status, setStatus] = useState<any>(null);
|
||||
const toast = useToast();
|
||||
|
||||
@@ -871,15 +871,25 @@ export const SyncView: React.FC = () => {
|
||||
|
||||
useEffect(() => { loadStatus(); }, []);
|
||||
|
||||
const runSync = async (dir: 'push' | 'pull') => {
|
||||
const runSync = async (dir: 'push' | 'pull' | 'import-legacy') => {
|
||||
setOperation(dir);
|
||||
setLogs([`>>> ${dir === 'push' ? 'BACKUP PostgreSQL → Google Sheets' : 'RESTAURAR Google Sheets → PostgreSQL'}...`]);
|
||||
const labels: Record<string, string> = {
|
||||
push: 'BACKUP PostgreSQL → Google Sheets',
|
||||
pull: 'RESTAURAR Google Sheets → PostgreSQL',
|
||||
'import-legacy': 'IMPORTAR PACIENTES (aba PACIENTES-CONSULTT-CLINIC)...'
|
||||
};
|
||||
setLogs([`>>> ${labels[dir]}...`]);
|
||||
try {
|
||||
const res = await fetch(`/api/sync/${dir}`, { method: 'POST', headers: authHeaders });
|
||||
const data = await res.json();
|
||||
setLogs(data.logs || []);
|
||||
if (data.success) {
|
||||
toast.success(dir === 'push' ? 'BACKUP REALIZADO!' : 'DADOS RESTAURADOS!');
|
||||
const msgs: Record<string, string> = {
|
||||
push: 'BACKUP REALIZADO!',
|
||||
pull: 'DADOS RESTAURADOS!',
|
||||
'import-legacy': `${data.imported ?? 0} PACIENTES IMPORTADOS!`
|
||||
};
|
||||
toast.success(msgs[dir]);
|
||||
loadStatus();
|
||||
} else {
|
||||
toast.error('ERRO NA SINCRONIZAÇÃO. VERIFIQUE OS LOGS.');
|
||||
@@ -919,6 +929,15 @@ export const SyncView: React.FC = () => {
|
||||
<Database size={15} className={operation === 'pull' ? 'animate-spin' : ''} />
|
||||
{operation === 'pull' ? 'IMPORTANDO...' : 'RESTAURAR DO BACKUP'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => runSync('import-legacy')}
|
||||
disabled={!!operation}
|
||||
title="Importa pacientes da aba PACIENTES-CONSULTT-CLINIC (Apps Script)"
|
||||
className={`px-4 py-2 rounded-xl flex items-center gap-2 text-xs font-black uppercase transition-all shadow-sm ${operation ? 'bg-gray-200 text-gray-400 cursor-not-allowed' : 'bg-amber-500 hover:bg-amber-600 text-white shadow-amber-100'}`}
|
||||
>
|
||||
<Database size={15} className={operation === 'import-legacy' ? 'animate-spin' : ''} />
|
||||
{operation === 'import-legacy' ? 'IMPORTANDO...' : 'IMPORTAR PLANILHA LEGADA'}
|
||||
</button>
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
|
||||
@@ -380,7 +380,9 @@ export const PatientsView: React.FC = () => {
|
||||
const debouncedBusca = useDebounce(busca, 500);
|
||||
|
||||
const fetcher = useCallback(() => HybridBackend.getPacientes(debouncedBusca), [debouncedBusca]);
|
||||
const { data: pacientes, isLoading, error, refresh } = useHybridBackend<Paciente[]>(fetcher, [debouncedBusca]);
|
||||
const { data: result, isLoading, error, refresh } = useHybridBackend<{ data: Paciente[]; total: number }>(fetcher, [debouncedBusca]);
|
||||
const pacientes = result?.data ?? [];
|
||||
const totalPacientes = result?.total ?? 0;
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
@@ -430,7 +432,7 @@ export const PatientsView: React.FC = () => {
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const displayedPacientes = busca ? pacientes : pacientes?.slice(-3).reverse();
|
||||
const displayedPacientes = pacientes;
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col relative">
|
||||
@@ -454,13 +456,14 @@ export const PatientsView: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!busca && (
|
||||
<div className="mb-4">
|
||||
<h3 className="text-sm font-bold text-gray-500 uppercase">
|
||||
ÚLTIMOS CADASTRADOS ({pacientes?.length ?? 0})
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-sm font-bold text-gray-500 uppercase">
|
||||
{busca
|
||||
? `RESULTADOS PARA "${busca}" (${pacientes.length})`
|
||||
: `PACIENTES — exibindo ${pacientes.length} de ${totalPacientes} cadastrados`
|
||||
}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 overflow-y-auto pb-10">
|
||||
{isLoading && <div className="col-span-3 text-center p-10 text-gray-500 uppercase font-bold flex justify-center items-center gap-2"><Loader2 className="animate-spin" /> CARREGANDO DADOS DO POSTGRESQL...</div>}
|
||||
|
||||
Reference in New Issue
Block a user