Merge claude/youthful-mendel-0c569f: import-legacy + server-side pacientes search
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -201,18 +201,18 @@ export const HybridBackend = {
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
getPacientes: async (termo: string = ''): Promise<Paciente[]> => {
|
getPacientes: async (termo: string = ''): Promise<{ data: Paciente[]; total: number }> => {
|
||||||
const res = await apiFetch(`${API_URL}/pacientes`);
|
const url = termo.trim() ? `${API_URL}/pacientes?q=${encodeURIComponent(termo)}` : `${API_URL}/pacientes`;
|
||||||
const data = await res.json();
|
const res = await apiFetch(url);
|
||||||
if (!termo) return data;
|
return res.json();
|
||||||
const t = termo.toUpperCase();
|
|
||||||
return data.filter((p: Paciente) => p.nome.includes(t) || p.cpf.includes(t));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
checkCpfExists: async (cpf: string): Promise<boolean> => {
|
checkCpfExists: async (cpf: string): Promise<boolean> => {
|
||||||
const pacientes = await HybridBackend.getPacientes();
|
|
||||||
const cleanCpf = cpf.replace(/\D/g, '');
|
const cleanCpf = cpf.replace(/\D/g, '');
|
||||||
if (!cleanCpf) return false;
|
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);
|
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 = () => {
|
export const SyncView: React.FC = () => {
|
||||||
const [logs, setLogs] = useState<string[]>([]);
|
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 [status, setStatus] = useState<any>(null);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
@@ -871,15 +871,25 @@ export const SyncView: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => { loadStatus(); }, []);
|
useEffect(() => { loadStatus(); }, []);
|
||||||
|
|
||||||
const runSync = async (dir: 'push' | 'pull') => {
|
const runSync = async (dir: 'push' | 'pull' | 'import-legacy') => {
|
||||||
setOperation(dir);
|
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 {
|
try {
|
||||||
const res = await fetch(`/api/sync/${dir}`, { method: 'POST', headers: authHeaders });
|
const res = await fetch(`/api/sync/${dir}`, { method: 'POST', headers: authHeaders });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setLogs(data.logs || []);
|
setLogs(data.logs || []);
|
||||||
if (data.success) {
|
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();
|
loadStatus();
|
||||||
} else {
|
} else {
|
||||||
toast.error('ERRO NA SINCRONIZAÇÃO. VERIFIQUE OS LOGS.');
|
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' : ''} />
|
<Database size={15} className={operation === 'pull' ? 'animate-spin' : ''} />
|
||||||
{operation === 'pull' ? 'IMPORTANDO...' : 'RESTAURAR DO BACKUP'}
|
{operation === 'pull' ? 'IMPORTANDO...' : 'RESTAURAR DO BACKUP'}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -380,7 +380,9 @@ export const PatientsView: React.FC = () => {
|
|||||||
const debouncedBusca = useDebounce(busca, 500);
|
const debouncedBusca = useDebounce(busca, 500);
|
||||||
|
|
||||||
const fetcher = useCallback(() => HybridBackend.getPacientes(debouncedBusca), [debouncedBusca]);
|
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();
|
const toast = useToast();
|
||||||
|
|
||||||
@@ -430,7 +432,7 @@ export const PatientsView: React.FC = () => {
|
|||||||
closeModal();
|
closeModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const displayedPacientes = busca ? pacientes : pacientes?.slice(-3).reverse();
|
const displayedPacientes = pacientes;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col relative">
|
<div className="h-full flex flex-col relative">
|
||||||
@@ -454,13 +456,14 @@ export const PatientsView: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!busca && (
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-sm font-bold text-gray-500 uppercase">
|
<h3 className="text-sm font-bold text-gray-500 uppercase">
|
||||||
ÚLTIMOS CADASTRADOS ({pacientes?.length ?? 0})
|
{busca
|
||||||
|
? `RESULTADOS PARA "${busca}" (${pacientes.length})`
|
||||||
|
: `PACIENTES — exibindo ${pacientes.length} de ${totalPacientes} cadastrados`
|
||||||
|
}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 overflow-y-auto pb-10">
|
<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>}
|
{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