name: supabase-json-meetings-import
description: Import meetings from an external JSON file (Painel do Corretor) into Supabase tasks table, creating missing team members as needed. Handles dedup, column mapping, and bulk inserts via Edge Function.
category: data-science
Importar Reuniões do Painel do Corretor para Supabase
Contexto
Arquivo JSON baixado do Painel do Corretor com 105 reuniões (últimos 7 dias). Estrutura:
`json
{
"meetings": [
{
"id": "abc123",
"title": " Reunião com Cliente — João Silva",
"status": "in_progress",
"channel": "whatsapp",
"due_date": "2026-06-03T14:00:00Z",
"start_date": "2026-06-03T14:00:00Z",
"responsible_name": "Pietra",
"responsible_phone": "(51) 99999-9999",
"responsible_email": "pietra@email.com",
"link": "https://...",
"priority": "high",
"progress": 50,
"created_by": "user-uuid"
}
]
}
`
Problemas Encontrados
1. Colunas inexistentes na tabela tasks — source, responsible_email, link, start_date, priority, progress, created_by NÃO existem no schema do PostgREST (mesmo existindo no DB)
2. PostgREST schema cache — colunas novas não aparecem no cache até refresh
3. Dedup por external_ref — a maioria dos tasks existentes tem external_ref = null, então dedup precisa ser por title|due_date
4. team_members incompleto — só Ilano existia; membros das reuniões precisavam ser criados antes de referenciar responsible_id
5. Telefone duplicado nos membros — Normalizar phones com replace(/\D/g, "")
Passo a Passo
1. Criar Edge Function temporária para inspecionar o banco
`typescript
// inspect-db/index.ts
Deno.serve(async (req) => {
const url = Deno.env.get("SUPABASE_URL")!;
const key = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const [tasksRes, membersRes] = await Promise.all([
fetch(${url}/rest/v1/tasks?deleted_at=is.null&order=created_at.desc&limit=100, {
headers: { Authorization: Bearer ${key}, apikey: key },
}),
fetch(${url}/rest/v1/team_members?order=name, {
headers: { Authorization: Bearer ${key}, apikey: key },
}),
]);
const tasks = await tasksRes.json();
const members = await membersRes.json();
return new Response(JSON.stringify({ tasks, members }, null, 2), {
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
});
});
`
Deploy: SUPABASE_ACCESS_TOKEN="sbp_..." supabase functions deploy inspect-db --no-verify-jwt
2. Preparar payload JSON
`python
import json
with open('reunioes.json') as f:
data = json.load(f)
meetings = data['meetings']
payload = {"meetings": meetings}
with open('/tmp/compare_payload.json', 'w') as f:
json.dump(payload, f, ensure_ascii=False)
`
3. Criar função de sync
COLUNAS CONFIRMADAS em tasks: id, title, status, channel, due_date, external_ref, responsible_id, project_id, created_by
NÃO USAR (sem confirmar): source, responsible_email, link, start_date, priority, progress, responsible_name, responsible_phone
`typescript
// sync-meetings/index.ts
Deno.serve(async (req) => {
const url = Deno.env.get("SUPABASE_URL")!;
const key = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const { meetings } = await req.json();
// 1. Buscar membros existentes
const membersRes = await fetch(${url}/rest/v1/team_members?select=id,name,phone,email, {
headers: { Authorization: Bearer ${key}, apikey: key },
});
const members: Array
const memberByPhone = new Map
const memberByEmail = new Map
members.forEach(m => {
if (m.phone) memberByPhone.set(m.phone.replace(/\D/g, ""), m);
if (m.email) memberByEmail.set((m.email || "").toLowerCase(), m);
});
// 2. Buscar tasks existentes (14 dias) para dedup
const cutoff = new Date(Date.now() - 14 24 60 60 1000).toISOString();
const existingRes = await fetch(${url}/rest/v1/tasks?deleted_at=is.null&created_at=gte.${cutoff}&select=id,external_ref,title,due_date, {
headers: { Authorization: Bearer ${key}, apikey: key },
});
const rawExisting = await existingRes.json();
const existing: Array
// Dedup: external_ref OU title|due_date
const existingRefs = new Set(existing.map(t => t.external_ref).filter(Boolean));
const existingKeys = new Set(existing.map(t => ${t.title || ""}|${t.due_date || ""}.toLowerCase()));
// 3. Criar membros faltantes
const nameToInfo = new Map
meetings.forEach((m: Record
const name = (m.responsible_name || "").trim();
if (!name) return;
const nl = name.toLowerCase();
if (!nameToInfo.has(nl)) nameToInfo.set(nl, { phone: m.responsible_phone || "", email: m.responsible_email || "" });
});
const newMembers: Array<{ name: string; phone: string; email: string }> = [];
const existingNames = new Set(members.map(m => m.name.toLowerCase()));
for (const [name, info] of nameToInfo.entries()) {
if (!existingNames.has(name)) {
newMembers.push({ name: name.replace(/\b\w/g, c => c.toUpperCase()), phone: info.phone, email: info.email });
}
}
// Inserir membros novos
if (newMembers.length > 0) {
await fetch(${url}/rest/v1/team_members, {
method: "POST",
headers: { Authorization: Bearer ${key}, apikey: key, "Content-Type: "application/json", Prefer: "return=minimal" },
body: JSON.stringify(newMembers),
});
// Refetch membros para ter IDs
const refetch = await fetch(${url}/rest/v1/team_members?select=id,name,phone,email, {
headers: { Authorization: Bearer ${key}, apikey: key },
});
const updated: Array
updated.forEach(m => {
if (m.phone) memberByPhone.set(m.phone.replace(/\D/g, ""), m);
if (m.email) memberByEmail.set((m.email || "").toLowerCase(), m);
});
}
// 4. Preparar tasks
const toInsert: Array
for (const m of meetings) {
const ref = m.id as string;
const key2 = ${(m.title as string || "").toLowerCase()}|${m.due_date || ""};
if ((ref && existingRefs.has(ref)) || existingKeys.has(key2)) continue;
const phone = (m.responsible_phone as string || "").replace(/\D/g, "");
const email = (m.responsible_email as string || "").toLowerCase();
let responsible_id: string | null = null;
if (phone && memberByPhone.has(phone)) responsible_id = memberByPhone.get(phone)!.id;
else if (email && memberByEmail.has(email)) responsible_id = memberByEmail.get(email)!.id;
// SÓ colunas confirmadas!
toInsert.push({
external_ref: ref || null,
title: (m.title as string) || "",
status: m.status === "completed" ? "completed" : m.status === "in_progress" ? "in_progress" : "todo",
channel: m.channel || "mensagem",
due_date: m.due_date || null,
responsible_id,
});
}
// 5. Bulk insert
let inserted = 0;
for (let i = 0; i < toInsert.length; i += 100) {
const chunk = toInsert.slice(i, i + 100);
const insRes = await fetch(${url}/rest/v1/tasks, {
method: "POST",
headers: { Authorization: Bearer ${key}, apikey: key, "Content-Type": "application/json", Prefer: "return=minimal" },
body: JSON.stringify(chunk),
});
if (insRes.ok) inserted += chunk.length;
}
return new Response(JSON.stringify({ success: true, total: meetings.length, inserted }), {
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
});
});
`
4. Deployar e rodar
`bash
cd /var/www/comercialrs
SUPABASE_ACCESS_TOKEN="sbp_..." supabase functions deploy sync-meetings --no-verify-jwt
SERVICE_KEY="4513a6f..." # service role key
ANON_KEY="eyJhbGci..." # anon key
SB_URL="https://dauftiqcvgaydddoxqhh.supabase.co"
curl -s --max-time 60 -X POST "$SB_URL/functions/v1/sync-meetings" \
-H "Authorization: Bearer $SERVICE_KEY" \
-H "apikey: $ANON_KEY" \
-H "Content-Type: application/json" \
-d @/tmp/compare_payload.json | python3 -m json.tool
`
5. Limpar funções temporárias após uso
`bash
Deletar via Dashboard ou deixar as Edge Functions temporárias
Funções temporárias criadas: inspect-db, compare-meetings, sync-meetings,
fix-team-duplicates, check-phones, check-member-phones, diagnose-sync,
reset-admin-password, read-config
`
Armadilhas
- -
sourcenão existe emtasks— se insetir com esse campo, TODO insert falha silenciosamente - -
responsible_namenão existe — usarresponsible_id(UUID do team_member) - - Dedup duplo — usar
external_ref(string única do sistema externo) Etitle|due_datecomo fallback - - Telefones normalizados — sempre usar
.replace(/\D/g, "")para comparar phones - - Membros criados em batch — buscar IDs após criação para usar em tasks