feat: inbox window + master data settings + contacts backend (camadas 1-3)#493
Open
x1strategyltda-art wants to merge 30 commits into
Open
feat: inbox window + master data settings + contacts backend (camadas 1-3)#493x1strategyltda-art wants to merge 30 commits into
x1strategyltda-art wants to merge 30 commits into
Conversation
…perfect respond.io)
- WhatsApp delivery tracking + reactions + bug 24h fixado - Inbox card 84px + player audio bubble + leading-none avatares - Workspace contact field visibility (parcial recovery) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ssignedId nos schemas Recovery Fase 1 da Camada 2 Dados Mestres (sessão 2fb51603 perdida por lefthook auto-stash em 2026-05-26). action.ts: createContactRequest extende com assigneeUserId (responsável), tagIds (etiquetas iniciais) e customFields (record opcional). gender agora opcional para casar com tela "Novo Contato" pixel-perfect. query.ts: listContactsRequest extende com lifecycleStageIds, blocked (sidebar Bloqueados) e assignedId (filter Caixas/Times igual Inbox). listContactsItem ganha lifecycleStage via createSelectSchema.
Recovery Fase 2 da Camada 2 Dados Mestres. Nova ordem definida pelo Pedro 2026-05-26: Análises → Inbox → Contatos → Agentes de IA → Transmissão → Fluxos → Configurações. Antes Fluxos estava em terceiro lugar (logo após Inbox). Move pra posição 6, e Contatos sobe pra 3 — assim a sequência reflete a priorização operacional (atender → cadastrar → automatizar).
…cycle + bloqueados) Recovery Fase 3 da Camada 2 Dados Mestres. ContactsAreaSidebar reescrita pra ficar pixel-perfect ao InboxAreaSidebar (Pedro: "literalmente o mesmo"): filtros top Todos/Minhas/Não atribuídas, seção Times via inboxTeams, Ciclo de vida com toggle global compartilhado (chave chatbotx.lifecycle.visible), rodapé Contatos bloqueados. Comportamento: navega via URL search params (assignedId, lifecycleStageId, blocked) em vez de mutar chatStore. Active state vem de useSearchParams(). Botão collapse compartilha use-inbox-sidebar-collapsed com Inbox. Layout absorve as novas props necessárias: currentUserId via getCurrentUserId, inboxTeams via listInboxTeams, lifecycleCounts + assignmentCounts + unreadByStage + unreadByAssignment via queries lifecycle-stages. Envolve tudo em ChatStore/InboxStore/User/CustomField/Tag providers (mesma stack do /inbox/layout) pra que o drawer "Detalhes do contato" reuse o ContactDetail FULL.
…ields + responsável
Recovery Fase 4 da Camada 2 Dados Mestres.
Form reescrito pixel-perfect Respond.io: First Name → Last Name → Phone* →
Email → Responsável (Combobox de workspace members) → Tags (popover com
checkboxes e chips) → Custom Fields (accordion). Footer sticky Cancel +
Create no rodapé do Sheet.
Action createContact ganha suporte a 3 campos opcionais novos vindos do
schema: assigneeUserId (vira conversation.assignedUserId), tagIds (INSERT
em contactsToTagsModel) e customFields (record que vira rows em
contactCustomFieldModel) — tudo na mesma transaction Drizzle.
Renomeia onSubmmited → onSubmitted no form e no dialog (typo).
i18n: adiciona 8 chaves nos 3 locales (pt-BR/en/vi):
contacts.placeholders.{addFirstName,addLastName,addEmail,selectAssignee},
tags.{addTag,searchPlaceholder,emptyState}, customFields.addValue (com
interpolação {field}) e actions.remove.
… na conversation
Recovery Fase 5 da Camada 2 Dados Mestres.
Drizzle relational query API não aceita filter aninhado cross-table no
where (conversation: { assignedUserId: X } retorna "Unknown relational
filter field"). Solução: helper resolveContactIdsByAssignment pré-busca
os contactIds via SELECT direto em conversationModel pelo tipo de
assignedId vindo do sidebar:
• "unassigned" → assignedUserId IS NULL AND assignedInboxTeamId IS NULL
• "u_<userId>" → assignedUserId = <userId>
• "t_<teamId>" → assignedInboxTeamId = <teamId>
listContacts curto-circuita com data vazia quando o lookup retorna zero
IDs (evita query desnecessária e mantém render consistente). generateWhere
recebe os IDs como segundo parâmetro e aplica id IN (...) no findMany.
…l pixel-perfect
Recovery Fase 6 (final) da Camada 2 Dados Mestres.
handleRowClick: alimenta chatStore com a conversation do contato
(prependConversation + setActiveConversationId) antes de abrir o drawer.
Assim o <ContactDetail> FULL — mesmo componente do Inbox — reusa a mesma
fonte de verdade (useChatStore.conversations) pra renderizar assignedUser,
tags reais, etc. Fecha o drawer limpa o activeConversationId no store.
Cell da coluna Nome refatorado pra <ContactNameCell> (componente novo no
fim do arquivo): seed do avatar = contact.id (snowflake imutável) +
useAvatarUrl pra imagem própria quando existe — mesmo padrão do
<ConversationItem> do Inbox (reference_avatares_consistentes), garantindo
visual idêntico em qualquer lugar que renderiza o contato.
Tabela:
• Tags em UMA linha sem wrap — 3 chips + "+N" pro restante, evita row
de Francisca alargar quando tem muitas tags.
• Wrapper relative + drawer overlay (sem encolher tabela).
• tableClassName: separator white/[0.06] sutil + tipografia
text-text-secondary (#CFD3D8) Inter 14px weight 400 — pixel-perfect
Respond.io (reference_fontes_respond_io).
Em algum commit anterior o item foi removido do navMain mas a rota /manage/integrations/page.tsx continua existindo e renderizando ManageOrganizationSettings (credenciais OAuth da org). Volta no topo do navMain com Grid2x2PlusIcon (visual original). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(auth) trustedOrigins localhost Paridade Respond.io Camada 1 (Fundamentos) — gap ChatbotXIO#7 catalogado na auditoria 2026-05-27. Schema: User.activityStatus (text default 'available' notNull) + User.lastActiveAt (timestamptz nullable). Migration 20260527124958_add_user_activity_status. Better-Auth: additionalFields registra ambos campos pra que authClient.updateUser({ activityStatus }) funcione sem rota custom. trustedOrigins agora aceita http://localhost:3123 além do BETTER_AUTH_URL (que aponta pra ngrok quando rodando WhatsApp E2E) — sem isso sign-in do localhost falhava com "Invalid origin". utils.ts e middlewares/auth.ts: coerce default `?? "available"` no spread de sessionData.user porque Better-Auth tipa additionalFields como `string | null | undefined` mas UserModel exige `string` notNull. UI: novo features/account/activity-status.tsx exporta <ActivityStatusSelector> (dropdown 3 opções, chama authClient.updateUser) e <ActivityStatusDot> (dot colorido reutilizável). profile-form troca pill hardcoded "Available" por selector. nav-user (sidebar bottom) ganha dot colorido metade-fora no avatar (estilo Slack) + seção "Meu status" no dropdown pra trocar de qualquer lugar. SidebarMenuButton tem overflow-hidden hardcoded — override com overflow-visible! no nosso uso pra dot não ser cortado. i18n 3 locales: namespace personalSettings.activityStatus com sectionLabel/available/busy/offline/updateSuccess/updateError. Remove key obsoleta statusAvailable. MVP: sem auto-offline por inatividade (precisa heartbeat client + worker cron) e sem broadcast realtime via PartySocket (status muda local mas outros usuários só veem após refresh). Anotado como follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Paridade Respond.io Camada 1 (gap ChatbotXIO#9 catalogado na auditoria 2026-05-27). O grupo agrupa Admins (Members) + Inbox Teams — ambos configs do workspace. "Workspace" é mais conciso e alinha com o termo "Workspace Settings" usado no menu de configurações Respond.io. Label igual nos 3 locales (pt-BR/en/vi) — termo internacional. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…XIO#13+ChatbotXIO#14) Camada 2 Dados Mestres — 2 gaps cosméticos catalogados na auditoria 2026-05-27: ChatbotXIO#13 Tags delete confirmação numérica: delete-tag-dialog ganha Input field quando totalContacts > 0 (soma contactsCount das tags selecionadas). Botão Delete habilita só quando user digita o número exato. Sem uso = dialog simples (skip confirm). Bate Respond.io ("introduza o número de contactos atribuídos a esta etiqueta"). ChatbotXIO#14 Lifecycle limite 20 stages: constante MAX_LIFECYCLE_STAGES = 20 em schema/index.ts + refine() bloqueia save acima do limite. lifecycle-stages-editor: addStage check + toast erro + botão "Adicionar etapa" desabilitado com tooltip quando atinge limite (active+lost combinados conforme workspace-settings.md doc). i18n 3 locales: tags.delete.{usageDescription,confirmInputLabel} (plural ICU) + lifecycle.maxStagesReached. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hatbotXIO#10 Fase A+B) Paridade Respond.io Camada 2 (gap ChatbotXIO#10 catalogado 2026-05-27). Backend completo; UI Add/Edit dialog (Fase C) e drawer Inbox render (Fase D) seguem em commits separados. Schema: customFieldTypes enum ganha "time", "url", "list" (Respond.io tem todos). customFieldVisibilities enum novo (alwaysShow/alwaysHide/hideWhenEmpty). CustomField.showInInbox boolean substituído por CustomField.visibility text (default "alwaysShow"). Coluna CustomField.values (jsonb) pra opções pré-definidas do tipo list. Migration 20260527133314_custom_field_visibility_types_values: manual (Drizzle não consegue ADD VALUE em pgEnum sem prompt). ALTER TYPE ADD VALUE x3 + ADD COLUMN visibility + UPDATE backfill showInInbox→visibility + DROP COLUMN showInInbox + ADD COLUMN values. Cascade: - custom-field-hook.ts customFieldIconsMap ganha 3 ícones lucide (Clock/Link/List) - create-custom-field.action remove showInInbox: true (default schema cobre) - custom-field-table cell visibility renderiza 3 estados via Record top-level VISIBILITY_LABEL_KEY (evita nested ternary) Próximo: Fase C — Add/Edit dialog renderiza chips de values pra type list + Customize Visibility 3-state. Fase D — drawer Inbox renderiza input específico por tipo + respeita hideWhenEmpty. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…botXIO#10 Fase C) Paridade Respond.io Camada 2 (gap ChatbotXIO#10, parte C). Fases A+B (backend + provider) entregues em 5a3acda. Schemas action: createCustomFieldRequest e updateCustomFieldRequest ganham visibility (enum) e values (string[] max 100) opcionais. customFieldTypes.options já tinha os 3 novos tipos (commit anterior). UI: - custom-field-label.tsx: switch case → Record TYPE_LABEL_KEY com 11 entradas (paridade Respond.io: shortText/longText/list/boolean/email/ phoneNumber/number/url/date/datetime/time). - create-custom-field.tsx (Add dialog): 11 opções no select Type, novo campo Visibilidade (alwaysShow/alwaysHide/hideWhenEmpty), conditional ValuesEditor quando type="list". - update-custom-field-dialog.tsx: idem para visibility/values. Edit dialog só mostra ValuesEditor pra custom fields existentes do tipo list. - NOVO components/values-editor.tsx: Input + chips Badge removíveis, Enter adiciona, dedup automático. Reutilizável. i18n 3 locales (pt-BR/en/vi): fields.time.label, fields.list.label, customFields.valuesLabel, customFields.valuesPlaceholder. Visibility labels já existiam. Falta Fase D — Inbox drawer renderiza inputs específicos por tipo (list multi-select, url link, time picker) + respeita hideWhenEmpty (esconde row quando contact não tem value preenchido). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(gap ChatbotXIO#10 Fase D) Paridade Respond.io Camada 2 (gap ChatbotXIO#10 — final). Fases A+B (backend) + C (UI settings) entregues em 5a3acda e fa1c5e4. Schema: ContactEditableField ganha visibility?: CustomFieldVisibility + options?: string[] (popula values do customField pra type="list"). contact-detail.tsx (drawer Inbox): - Loop customFields populate visibility + options. - visibleFields filter: exclui alwaysHide (vai pro accordion oculto) e hideWhenEmpty quando value vazio (paridade Respond.io — campo só aparece se preenchido). - hiddenFields agrupa alwaysHide com o sistema legado de workspace-contact-field-visibility (override por usuário). InlineContactField router: - type="list" → delega pra ListContactField (popover com checkboxes). - type="url" preenchido e não editando → link clicável azul + ExternalLinkIcon + botão "Editar" pra voltar pro Input. - resolveInputType olha field.type primeiro (url/time/date/datetime/ email/phoneNumber/number), fallback pra key dos defaults. NOVO list-contact-field.tsx: - Popover com Checkboxes das `options` (definidas pelo admin em /settings/contact-fields). - Display: chips Badge dos selecionados (CSV no value armazenado). - Save imediato a cada toggle via updateContactFieldAction (mesma action dos defaults). - empty state quando admin não configurou opções. i18n 3 locales: customFields.emptyOptions. ChatbotXIO#10 FECHADO. Fases A+B+C+D entregues. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…atbotXIO#11) Paridade Respond.io Camada 2 (gap ChatbotXIO#11 catalogado 2026-05-27). Fecha Custom Fields 100% junto com ChatbotXIO#10. Parte 1 — fieldId slug: - Schema CustomField ganha coluna fieldId (text NOT NULL) + unique index (workspaceId, fieldId). Migration manual com backfill via regexp_replace(name, '[^a-zA-Z0-9]+', '_'). 2 fields existentes no DB de dev convertidos pra link_do_pedido + teste. - Helper @chatbotx.io/utils/slug exporta slugify() e slugifyUnique() (sufixa _2, _3 quando há colisão). NFD + diacríticos removidos pra ASCII-only. - create-custom-field action consulta slugs existentes do workspace e gera fieldId único antes do INSERT. Field renomeado mantém o fieldId original (imutável — APIs/templates externos dependem dele). - Tabela /settings/contact-fields coluna "ID do campo" mostra fieldId em vez do Snowflake id. Acessor mudou de "id" pra "fieldId". Parte 2 — Default Contact Fields read-only: - NOVO components/default-fields-list.tsx renderiza 7 rows fixas no topo da tabela /settings/contact-fields: firstName, lastName, phoneNumber, email, country, locale, avatar. Bate Respond.io ("Standard Contact Fields" — não editáveis nem deletáveis). - Cada row: ícone tipo + label i18n + fieldId code + tipo label + lock icon com tooltip "Campo padrão, não editável". - Visual border-dashed pra diferenciar de custom fields da grid abaixo (que são editáveis). i18n 3 locales: customFields.defaultsTitle/defaultsTooltip/ defaultsLockTooltip. Labels dos 7 defaults reaproveitam keys existentes (fields.firstName/lastName/email/phoneNumber/country/ locale/avatar.label). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…otXIO#12) Paridade Respond.io Camada 2 (gap ChatbotXIO#12 catalogado 2026-05-27). Snippets saem do mínimo (shortcut + text) pra estrutura completa. Schema SavedReply ganha: - name (text nullable) — campo descritivo separado do shortcut. Visível na tabela de gestão pra usuário identificar snippet sem decifrar o comando curto. - topics (jsonb string[]) — tags pra organizar (máx 10/snippet). Filtro futuro no popover do composer. - files (jsonb SavedReplyFile[]) — anexos (máx 5/snippet). Cada arquivo expande junto da mensagem ao usar o snippet. Migration manual com ADD COLUMN IF NOT EXISTS pros 3 campos. Action createSavedReply enforça limite 5000 snippets/workspace via SELECT COUNT antes do INSERT — paridade Respond.io documentada em workspace-settings.md ("cada espaço de trabalho só pode ter até 5.000 snippets"). editSavedReply propaga os 3 novos campos no UPDATE. Schema action createSavedReplyRequest estende com name/topics/files opcionais + constantes MAX_SNIPPETS_PER_WORKSPACE (5000), MAX_TOPICS_PER_SNIPPET (10), MAX_FILES_PER_SNIPPET (5). snippetFileSchema valida formato dos anexos. UI snippet-form-dialog refatorado com 5 campos: - Nome (input) — opcional - Atalho (input) — obrigatório - Tópicos — novo componente TopicsInput inline (input + chips removíveis, Enter adiciona, dedup, max enforcement) - Mensagem (textarea) — obrigatório - Anexos — novo componente FilesList inline (file input + lista com remove + tamanho formatado). MVP com blob URL local; upload S3 presigned via DirectUploadButton fica pra commit separado. Snippets-table-columns reordenado: - Coluna Nome (com fallback pro shortcut quando vazio) - Nova coluna Atalho (código /shortcut em font mono) - Nova coluna Tópicos (3 chips visíveis + "+N") - Nova coluna Anexos (paperclip icon + count) - Mensagem/Adicionado/Editado mantidos i18n 3 locales: snippets.shortcutLabel/shortcutPlaceholder/topicsLabel/ topicsPlaceholder/topicsLimitReached/filesLabel/addFile/filesLimitReached. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ChatbotXIO#15 Fase A) Paridade Respond.io Camada 2 (gap ChatbotXIO#15 catalogado 2026-05-27). Backend completo; Fases B/C/D seguem em commits separados. Schema novo `ConversationCategory` — admin CRUD em /settings/closing-notes: - name + description + position + workspaceId - unique (workspaceId, name) - cascade ON DELETE workspace Schema novo `ConversationClosingNote` — 1-1 com Conversation: - conversationId (unique → 1-1) - categoryId nullable + categoria SET NULL ao deletar - summary text nullable - closedByUserId nullable + user SET NULL ao deletar - cascade ON DELETE conversation/workspace Workspace ganha coluna `closingNotesMode` text default "disabled": - "disabled" (atual — fecha conversa direto, sem dialog) - "optional" (abre dialog, pode pular) - "mandatoryDialog" (categoria obrigatória, summary opcional) - "mandatoryBoth" (categoria + summary obrigatórios) Partial `closingNotesModes` Zod enum em packages/database/src/partials/conversation.ts (exporta ClosingNotesMode type). Relations seguindo pattern defineRelationsPart (Drizzle v2, não relations() legado): conversationCategory.workspace+closingNotes / conversationClosingNote.workspace+conversation+category+closedByUser. Registrado em relations/index.ts (TWO edits — import + spread no relations object — invariant AGENTS.md). Audit log keys novas em packages/business/src/audit-log/types.ts (5): - CONVERSATION_CATEGORY_CREATED/UPDATED/DELETED - CLOSING_NOTES_MODE_UPDATED - CONVERSATION_CLOSED_WITH_NOTE Migration `20260527143406_closing_notes_categories_and_mode` manual via drizzle-kit generate --custom. ALTER TABLE Workspace + 2x CREATE TABLE + 2x CREATE UNIQUE INDEX. Aplicada via docker exec psql + journal sync via db:migrate. Próximo: Fase B — /settings/closing-notes UI (CRUD categorias + radio 4 modos). Fase C — modal close conversation no Inbox. Fase D — render closing note no contact activity timeline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Paridade Respond.io Camada 2 (gap ChatbotXIO#15 Fase B). Fase A backend entregue em bcb27d6. Nova rota /settings/closing-notes (page.tsx busca workspace.closingNotesMode + lista categorias) renderiza ClosingNotesSettings: - Bloco "Modo de operação" com RadioGroup 4 opções (disabled/optional/mandatoryDialog/mandatoryBoth). Cada opção tem label + descrição explicando comportamento. Save imediato via updateClosingNotesModeAction ao trocar radio. - Bloco "Categorias de conversa" com lista de cards e botão "Adicionar categoria". Cada card: nome + descrição + botões editar/deletar. Empty state quando 0 categorias. - CategoryFormDialog reaproveita pra create E edit. Form: Nome (required 1-100) + Descrição (opcional max 500). Validação Zod, toast no sucesso/erro, revalidatePath após mudança. - AlertDialog de confirmação ao deletar (substitui window.confirm proibido pelo Biome). Pattern useState deletingCategory + dialog controlled. Item novo no sidebar /settings (grupo "Caixa de entrada", após Snippets) com ícone Note iconsax. Actions: - create-category — auditLog CONVERSATION_CATEGORY_CREATED - update-category — auditLog CONVERSATION_CATEGORY_UPDATED - delete-category — auditLog CONVERSATION_CATEGORY_DELETED (sem inputSchema, só bindArgs workspaceId+id; chamada direta async fora do useAction hook porque hook não tipa multi-bindArgs) - update-mode — auditLog CLOSING_NOTES_MODE_UPDATED Query listConversationCategories (RSC) orderBy(position, name). Await explícito no return (Biome require-await rule). i18n 3 locales: 20+ keys closingNotes.* (title/subtitle/modeTitle/ modeSubtitle/mode.{disabled,optional,mandatoryDialog,mandatoryBoth} com label+desc/categoriesTitle/addCategory/editCategoryTitle/ namePlaceholder/descriptionPlaceholder/emptyCategories/ deleteCategoryTitle/confirmDelete + toasts). Próximo: Fase C — modal close conversation no Inbox (respeita modo + valida campos). Fase D — render closing note na timeline do contato. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rkspace (Fase C) ChatbotXIO#15 Fase C — conecta backend + settings UI das Fases A/B com o fluxo do atendente. Botão "Fechar" no header da conversa agora: - mode=disabled → arquiva direto (comportamento atual) - mode=optional → abre modal, pode "Pular e fechar" - mode=mandatoryDialog → exige categoria - mode=mandatoryBoth → exige categoria + resumo Server (closeConversationWithNoteAction) re-valida a obrigatoriedade contextual pra evitar bypass via DevTools. Cria ConversationClosingNote (1-1 via unique conversationId) + arquiva + emite os mesmos eventos do archiveConversationAction pra triggers/analytics não regredirem. Prop drilling: inbox/page.tsx → ChatLayout → MessageHead → CloseConversationButton. Config fetchada server-side em getClosingNotesConfig (mode + categorias ordenadas por position+name). i18n: closingNotes.dialog.* em en, pt-BR e vi. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ato (Fase D) ChatbotXIO#15 Fase D — fecha o loop. Quando atendente fecha conversa com categoria ou resumo via CloseConversationDialog, o evento agora aparece no histórico de atividades do contato (drawer do Inbox). Mudanças: - contactEventTypes: adiciona CONVERSATION_CLOSED_WITH_NOTE (string "contact.conversation.closedWithNote", padrão dot.notation existente). - closeConversationWithNoteAction: chama recordContactEvent passando meta = { conversationId, categoryName, summary, closedByUserName }. Fire-and-forget (não bloqueia o archive — recordContactEvent já tem try/catch interno). - contact-activity-log.tsx: novo renderer com CheckCircle2Icon verde + badge da categoria + box com summary em itálico. Mantém o estilo hardcoded pt-br dos outros renderers do arquivo. Nenhuma migração de schema necessária — ContactEvent já é flexível (eventType: text, meta: jsonb). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ntre arquivos ChatbotXIO#15 hotfix. Build standalone quebrou com: Error: relations -> conversationCategoryModel: { closingNotes: r.many.conversationClosingNoteModel(...) }: not enough data provided to build the relation - "from"/"to" are not defined, and no reverse relations of table "conversationClosingNoteModel" were found defineRelationsPart só infere reverse relations DENTRO da mesma chamada (mesmo arquivo). Como conversationCategoryModel mora em relations/conversation-category.ts e o inverso (category) mora em relations/conversation-closing-note.ts, o auto-link falha. Fix: from/to explícitos no .many do lado da categoria. Padrão consistente com workspace.savedReplies que faz a mesma coisa. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hatbotXIO#20.3) Quick wins da auditoria ChatbotXIO#20 da Camada 3 — 2 features marginais do Respond.io que faltavam: ChatbotXIO#20.2 — DeleteContactDialog ganha confirmação numérica Quando o(s) contato(s) selecionado(s) têm tags ou conversa vinculadas, exige digitar o número total (tags + conversas) pra liberar Excluir — mesmo padrão do DeleteTagsDialog (ChatbotXIO#13). Contatos vazios mantém o flow rápido "Tem certeza?" anterior. - Nova prop opcional usageCounts: { tags, conversations } - Caller calcula no client (já tem dados no schema) - contacts-list-action.tsx (bulk delete na /contacts) soma de cada row - conversation-action.tsx (delete do Inbox) passa { tags: 0, conversations: 1 } porque contactResource não carrega tags lá — comentário explica - i18n: contacts.delete.* em en, pt-BR e vi ChatbotXIO#20.3 — Columns picker /contacts persistido em localStorage Botão "Colunas" no toolbar abre Popover com checkbox por coluna. TanStack column.toggleVisibility() nativo + meta.label em todas as hideable. 3 colunas fixas (select, nome, criado em) com enableHiding: false não aparecem no picker. - ContactsColumnsPicker reutilizável (recebe table) - localStorage key chatbotx.contacts.columnVisibility (SSR-safe via lazy useState + effect on change) - Atalho "Mostrar/Ocultar todas" no header - meta.label em 8 colunas que faltavam (channels, lifecycleStage, email, phoneNumber, tags, country, locale, conversationStatus, lastMessage) - i18n: contacts.columnsPicker.* em en, pt-BR e vi Camada 3: 1/5 (ChatbotXIO#20 fechado). Falta ChatbotXIO#16, ChatbotXIO#17, ChatbotXIO#18, ChatbotXIO#19. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oft-delete)
Estratégia A escolhida pelo Pedro: soft-delete via mergedIntoId em vez
de DELETE físico. Reverter é trivial (UPDATE 3 campos pra NULL), histórico
preservado, auditável.
Schema (Contact)
- mergedIntoId bigint → Contact.id (self-ref via AnyPgColumn pra
quebrar circularidade de tipos; ON DELETE SET NULL)
- mergedAt timestamp with timezone
- mergedByUserId bigint → User.id (ON DELETE SET NULL)
- INDEX Contact_mergedIntoId_idx pra acelerar lookups "fundidos em X"
- Migration custom: 20260527155004_unmerge_contact_fields
mergeContacts() refator
- Etapa 5 (era DELETE FROM Contact) virou UPDATE SET mergedIntoId,
mergedAt, mergedByUserId. Conversation/tag/customField/etc continuam
sendo MOVIDOS no momento do merge (não snapshot) — unmerge restaura
o contato MAS NÃO reverte movimentos. UI avisa explicitamente.
- Docstring atualizada explicando soft-delete
Filtros mergedIntoId IS NULL
- list-contacts.queries.ts → generateWhere adiciona mergedIntoId:isNull
- count-blocked-contacts.ts → isNull no AND do count
- listConversations NÃO precisa filtrar (conversation foi movida no
merge; duplicate fica sem conversation, naturalmente não aparece)
- Bulk actions (add tag, custom field, etc) ficam como debt list —
o pior cenário hoje é "bulk action atinge contato fundido", não
visível na UI mas ainda no banco
unmergeContactsAction
- bindArgs workspaceId + inputSchema { primaryId, duplicateIds[] }
- Valida: todos os duplicates têm mergedIntoId = primaryId (evita
unmerge cruzado entre primaries diferentes)
- UPDATE SET 3 campos pra NULL
- logAudit CONTACT_UNMERGED + recordContactEvent UNMERGED no PRIMARY
UI
- MergedContactsPanel — lazy fetch via listContactsMergedInto;
banner amarelo no drawer do contato primary; lista com botão
"Desfazer fusão" por linha; warning sobre conversas/tags
- Plugado em ContactDetailDrawer antes do "Ver no Inbox"
- i18n contacts.unmerge.* em en, pt-BR, vi
Camada 3: 2/5 (ChatbotXIO#17 + ChatbotXIO#20 fechados). Falta ChatbotXIO#16, ChatbotXIO#18, ChatbotXIO#19.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Entrega cumulativa de 3 camadas da pirâmide ChatbotX (clone open-source Respond.io):
/settings/tags), Custom Fields (/settings/contact-fields), Snippets (/settings/snippets), Bot Fields (/settings/bot-fields). Cores tag via tripleta HSL (bg L=18% / text L=93% / outline L=24%). Redirects 301 das URLs antigas.12 audit log keys novas: TAG/CUSTOM_FIELD/BOT_FIELD/SNIPPET + CONTACT_TAG_ADDED/REMOVED/FIELD_UPDATED/MERGED/IMPORTED.
i18n em 3 locales (pt-BR, en, vi) em todas as features novas.
Worker fix: cast em
flow.tspra acomodar TriggerNode norunStepsAndQuickReplies.Test plan
/settings/tags— confirmar audit log + ContactEvent/settings/contact-fields— confirmar 8 colunas (Nome | ID | Descrição | Tipo | Visibilidade | Data | Ações)/settings/snippets/settings/bot-fields/tags→/settings/tags,/custom-fields→/settings/contact-fields,/bot-fields→/settings/bot-fieldsSELECT action FROM \"AuditLog\" WHERE \"workspaceId\" = ... ORDER BY \"createdAt\" DESC🤖 Generated with Claude Code