@fayz-ai/plugin-conversations 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -787,26 +787,32 @@ function ConversationList({ className }) {
787
787
  onClick: () => select(c.id),
788
788
  className: cn(
789
789
  "flex w-full items-start gap-3 border-b border-border/50 px-3 py-3 text-left transition-colors",
790
- // `text-accent-foreground` JUNTO com `bg-accent`: o par existe
791
- // para ser usado em par. Só o fundo trocava, e num tema em que
792
- // `accent` é quase preto o nome e a prévia continuavam
793
- // `text-foreground` escuro sobre escuro, ilegível na conversa
794
- // ABERTA, que é justamente a que se está lendo.
795
- active ? "bg-accent text-accent-foreground" : "hover:bg-muted/50"
790
+ // Um TOM da cor de marca, e não `bg-accent`.
791
+ //
792
+ // `bg-accent` resolve para quase preto neste tema, e a linha da
793
+ // conversa aberta ficava escura sobre escura. Trocar junto o
794
+ // `text-accent-foreground` deixava legível, mas pintava de
795
+ // preto a única linha que se está a ler — chamava mais atenção
796
+ // que a conversa.
797
+ //
798
+ // `bg-primary/10` é um véu: o texto continua com as cores dele,
799
+ // a seleção se lê como seleção e não como bloco, e funciona nos
800
+ // dois temas porque não presume nada sobre o fundo.
801
+ active ? "bg-primary/10" : "hover:bg-muted/50"
796
802
  ),
797
803
  children: [
798
804
  /* @__PURE__ */ jsx(Avatar, { name: c.contactName, accent: c.accent, channel: c.channel }),
799
805
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
800
806
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
801
- /* @__PURE__ */ jsx("span", { className: cn("truncate text-sm", active ? "" : "text-foreground", unread ? "font-semibold" : "font-medium"), children: c.contactName }),
802
- /* @__PURE__ */ jsx("span", { className: cn("shrink-0 text-[11px]", active ? "opacity-70" : unread ? "font-semibold text-primary" : "text-muted-foreground", active && unread && "font-semibold opacity-100"), children: relativeTime(c.lastMessageAt) })
807
+ /* @__PURE__ */ jsx("span", { className: cn("truncate text-sm text-foreground", unread ? "font-semibold" : "font-medium"), children: c.contactName }),
808
+ /* @__PURE__ */ jsx("span", { className: cn("shrink-0 text-[11px]", unread ? "font-semibold text-primary" : "text-muted-foreground"), children: relativeTime(c.lastMessageAt) })
803
809
  ] }),
804
810
  /* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-center gap-2", children: [
805
811
  /* @__PURE__ */ jsx(ChannelBadge, { channel: c.channel }),
806
- c.status !== "open" && /* @__PURE__ */ jsx("span", { className: cn("text-[10px] uppercase tracking-wide", active ? "opacity-70" : "text-muted-foreground/70"), children: t(`conversations.status.${c.status}`) })
812
+ c.status !== "open" && /* @__PURE__ */ jsx("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground/70", children: t(`conversations.status.${c.status}`) })
807
813
  ] }),
808
814
  /* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-center justify-between gap-2", children: [
809
- /* @__PURE__ */ jsx("span", { className: cn("truncate text-xs", active ? "opacity-80" : unread ? "text-foreground" : "text-muted-foreground"), children: c.lastMessagePreview }),
815
+ /* @__PURE__ */ jsx("span", { className: cn("truncate text-xs", unread ? "text-foreground" : "text-muted-foreground"), children: c.lastMessagePreview }),
810
816
  unread && /* @__PURE__ */ jsx("span", { className: "flex h-[18px] min-w-[18px] shrink-0 items-center justify-center rounded-full bg-primary px-1.5 text-[10px] font-semibold text-primary-foreground", children: c.unreadCount })
811
817
  ] })
812
818
  ] })
@@ -4633,12 +4639,177 @@ CREATE INDEX IF NOT EXISTS plg_conversation_messages_inbound_at
4633
4639
  ON public.plg_conversation_messages (conversation_id, at DESC)
4634
4640
  WHERE direction = 'inbound';
4635
4641
  `;
4642
+ var MIGRATION_005_A_CONVERSA_CONTA_AO_FUNIL_QUE_HOUVE_CONTATO = `-- ---------------------------------------------------------------------------
4643
+ -- 005_a_conversa_conta_ao_funil_que_houve_contato.sql \u2014 a ponte que faltava.
4644
+ --
4645
+ -- \u2500\u2500 o buraco, medido \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4646
+ --
4647
+ -- No cluster: 8 conversas, 42 mensagens trocadas \u2014 incluindo respostas de
4648
+ -- gente de verdade \u2014 e 315 neg\xF3cios TODOS parados em "Novo (feira)", com
4649
+ -- \`contact_status = 'not_contacted'\`. Zero atividades vindas da caixa.
4650
+ --
4651
+ -- N\xE3o era defeito: n\xE3o existia liga\xE7\xE3o nenhuma. Nenhum \`INSERT\` em
4652
+ -- \`plg_crm_activities\` fora do plugin-crm, nenhuma fonte de linha do tempo
4653
+ -- registrada. A caixa de entrada e o funil olhavam para as mesmas pessoas e
4654
+ -- n\xE3o se falavam.
4655
+ --
4656
+ -- O maquin\xE1rio do CRM j\xE1 estava pronto e correto: \`crm_recompute_deal_status\`
4657
+ -- (crm/020) l\xEA as atividades, pega o \xDALTIMO \`outcome\` e deriva o status; o
4658
+ -- gatilho \`crm_activity_restatus\` dispara a cada atividade inserida. Faltava
4659
+ -- algu\xE9m inserir.
4660
+ --
4661
+ -- \u2500\u2500 por que o \`outcome\` \xE9 o que \xE9 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4662
+ --
4663
+ -- mensagem RECEBIDA \u2192 \`replied\` o lead respondeu. \xC9 o fato mais forte
4664
+ -- que a caixa produz, e o que mais move
4665
+ -- o status ("Respondeu").
4666
+ -- mensagem ENVIADA \u2192 \`left_message\` deixamos recado. Mandar N\xC3O \xE9 ser
4667
+ -- lido: tratar envio como contato feito
4668
+ -- encheria o funil de "conectado" sem
4669
+ -- ningu\xE9m do outro lado. A 020 mapeia
4670
+ -- este valor para "N\xE3o atendeu", que \xE9
4671
+ -- literalmente o estado \u2014 ainda sem
4672
+ -- resposta.
4673
+ --
4674
+ -- O vocabul\xE1rio \xE9 FECHADO por CHECK (crm/020:49): \`connected, no_answer,
4675
+ -- left_message, replied, scheduled\`. Conferido antes de escrever \u2014 a primeira
4676
+ -- vers\xE3o desta migration usava \`attempted\`, que a regra da 020 nomeia mas a
4677
+ -- restri\xE7\xE3o da coluna N\xC3O aceita, e teria falhado em toda mensagem enviada.
4678
+ --
4679
+ -- \u2500\u2500 e o \`deal_id\` n\xE3o \xE9 opcional \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4680
+ --
4681
+ -- \`crm_recompute_deal_status\` agrega \`WHERE a.deal_id = p_deal_id\`. Uma
4682
+ -- atividade s\xF3 com \`lead_id\` n\xE3o entra na conta e n\xE3o move status nenhum \u2014
4683
+ -- por isso o gatilho procura o neg\xF3cio do lead e o carimba.
4684
+ --
4685
+ -- \u2500\u2500 uma atividade por CONVERSA E POR DIA, n\xE3o por mensagem \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4686
+ --
4687
+ -- Uma conversa de trinta mensagens n\xE3o s\xE3o trinta contatos: \xE9 um. Registrar
4688
+ -- cada mensagem encheria a linha do tempo de ru\xEDdo e afogaria o que importa \u2014
4689
+ -- a liga\xE7\xE3o, a visita, a proposta. Ent\xE3o a primeira mensagem do dia abre a
4690
+ -- atividade e as seguintes ATUALIZAM a mesma linha (o corpo mais recente, e o
4691
+ -- \`outcome\` mais forte do dia).
4692
+ --
4693
+ -- Atualizar tamb\xE9m dispara o \`crm_activity_restatus\` (o gatilho \xE9 INSERT OR
4694
+ -- UPDATE), ent\xE3o uma resposta que chega depois de um envio move o status no
4695
+ -- mesmo dia, que \xE9 o comportamento que se quer.
4696
+ --
4697
+ -- \u2500\u2500 e se o contato n\xE3o for um lead \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4698
+ --
4699
+ -- \`contact_person_id\` \xE9 nulo enquanto ningu\xE9m casou o telefone com uma pessoa.
4700
+ -- A\xED n\xE3o h\xE1 a quem pendurar o fato e a fun\xE7\xE3o sai calada. N\xE3o inventa lead: a
4701
+ -- caixa recebe mensagem de entregador, de banco e de engano, e cada um deles
4702
+ -- viraria um card no funil de algu\xE9m.
4703
+ -- ---------------------------------------------------------------------------
4704
+
4705
+ CREATE OR REPLACE FUNCTION public.conversations_log_crm_activity() RETURNS trigger
4706
+ LANGUAGE plpgsql SECURITY DEFINER
4707
+ SET search_path TO ''
4708
+ AS $$
4709
+ DECLARE
4710
+ v_tenant uuid;
4711
+ v_person uuid;
4712
+ v_canal text;
4713
+ v_deal uuid;
4714
+ v_outcome text;
4715
+ v_titulo text;
4716
+ v_corpo text;
4717
+ v_dia date := (NEW.at AT TIME ZONE 'UTC')::date;
4718
+ v_ativ uuid;
4719
+ BEGIN
4720
+ -- Sem CRM montado n\xE3o h\xE1 o que escrever. \`to_regclass\` em vez de refer\xEAncia
4721
+ -- direta porque conversas \xE9 plugin OPCIONAL e pode viver num cluster que n\xE3o
4722
+ -- tem funil nenhum.
4723
+ IF to_regclass('public.plg_crm_activities') IS NULL THEN RETURN NULL; END IF;
4724
+
4725
+ SELECT c.tenant_id, c.contact_person_id, c.channel
4726
+ INTO v_tenant, v_person, v_canal
4727
+ FROM public.plg_conversations c WHERE c.id = NEW.conversation_id;
4728
+
4729
+ IF v_person IS NULL THEN RETURN NULL; END IF;
4730
+
4731
+ SELECT d.id INTO v_deal
4732
+ FROM public.plg_crm_deals d
4733
+ WHERE d.tenant_id = v_tenant AND d.lead_id = v_person
4734
+ ORDER BY d.created_at DESC LIMIT 1;
4735
+
4736
+ IF NEW.direction = 'inbound' THEN
4737
+ v_outcome := 'replied';
4738
+ v_titulo := 'Respondeu no ' || coalesce(v_canal, 'canal');
4739
+ ELSE
4740
+ v_outcome := 'left_message';
4741
+ v_titulo := 'Mensagem enviada no ' || coalesce(v_canal, 'canal');
4742
+ END IF;
4743
+
4744
+ v_corpo := left(coalesce(NEW.body, ''), 2000);
4745
+
4746
+ -- A atividade do dia, se j\xE1 houver.
4747
+ SELECT a.id INTO v_ativ
4748
+ FROM public.plg_crm_activities a
4749
+ WHERE a.tenant_id = v_tenant
4750
+ AND a.lead_id = v_person
4751
+ AND a.activity_type = 'whatsapp'
4752
+ AND (a.completed_at AT TIME ZONE 'UTC')::date = v_dia
4753
+ ORDER BY a.completed_at DESC LIMIT 1;
4754
+
4755
+ IF v_ativ IS NULL THEN
4756
+ INSERT INTO public.plg_crm_activities (
4757
+ tenant_id, deal_id, lead_id, contact_id, activity_type,
4758
+ title, description, outcome, completed_at, created_by_name
4759
+ ) VALUES (
4760
+ v_tenant, v_deal, v_person, v_person, 'whatsapp',
4761
+ v_titulo, v_corpo, v_outcome, NEW.at,
4762
+ -- Quem fez foi a caixa, n\xE3o uma pessoa: o autor em uuid seria mentira,
4763
+ -- e "(sistema)" \xE9 a resposta honesta para quem l\xEA a linha do tempo.
4764
+ 'Caixa de entrada'
4765
+ );
4766
+ ELSE
4767
+ UPDATE public.plg_crm_activities a SET
4768
+ -- \`replied\` NUNCA \xE9 rebaixado por um envio posterior: o lead ter
4769
+ -- respondido \xE9 o fato do dia, e mandar outra mensagem depois n\xE3o o
4770
+ -- desfaz.
4771
+ outcome = CASE WHEN a.outcome = 'replied' THEN 'replied' ELSE v_outcome END,
4772
+ title = CASE WHEN a.outcome = 'replied' THEN a.title ELSE v_titulo END,
4773
+ description = v_corpo,
4774
+ completed_at = NEW.at,
4775
+ deal_id = coalesce(a.deal_id, v_deal)
4776
+ WHERE a.id = v_ativ;
4777
+ END IF;
4778
+
4779
+ RETURN NULL;
4780
+ END $$;
4781
+
4782
+ REVOKE ALL ON FUNCTION public.conversations_log_crm_activity() FROM PUBLIC;
4783
+ GRANT EXECUTE ON FUNCTION public.conversations_log_crm_activity() TO authenticated, service_role;
4784
+
4785
+ COMMENT ON FUNCTION public.conversations_log_crm_activity() IS
4786
+ 'Uma mensagem vira contato na linha do tempo do lead e do neg\xF3cio, e o status do card deriva disso pelo gatilho da crm/020. Uma atividade por conversa por dia (005).';
4787
+
4788
+ -- AFTER: falhar ao registrar no CRM n\xE3o pode recusar uma mensagem que j\xE1
4789
+ -- chegou do provedor. A caixa \xE9 a fonte; o funil \xE9 quem escuta.
4790
+ DROP TRIGGER IF EXISTS conversations_log_crm_activity ON public.plg_conversation_messages;
4791
+ CREATE TRIGGER conversations_log_crm_activity
4792
+ AFTER INSERT ON public.plg_conversation_messages
4793
+ FOR EACH ROW EXECUTE FUNCTION public.conversations_log_crm_activity();
4794
+
4795
+ -- O \xEDndice que a busca da atividade do dia usa. Sem ele, cada mensagem varre
4796
+ -- as atividades do inquilino inteiro \u2014 e mensagem \xE9 o que mais chega aqui.
4797
+ DO $$
4798
+ BEGIN
4799
+ -- Guarda pela mesma raz\xE3o da fun\xE7\xE3o: conversas \xE9 opcional e pode viver sem
4800
+ -- CRM. Um \xEDndice sobre tabela que n\xE3o existe recusa o arquivo inteiro.
4801
+ IF to_regclass('public.plg_crm_activities') IS NULL THEN RETURN; END IF;
4802
+ CREATE INDEX IF NOT EXISTS plg_crm_activities_lead_day
4803
+ ON public.plg_crm_activities (tenant_id, lead_id, activity_type, completed_at DESC);
4804
+ END $$;
4805
+ `;
4636
4806
  var MIGRATIONS = [
4637
4807
  { id: "000_baseline", sql: MIGRATION_000_BASELINE },
4638
4808
  { id: "001_a_caixa_sabe_quem_falou_por_ultimo", sql: MIGRATION_001_A_CAIXA_SABE_QUEM_FALOU_POR_ULTIMO },
4639
4809
  { id: "002_a_casa_escolhe_por_onde_o_whatsapp_sai", sql: MIGRATION_002_A_CASA_ESCOLHE_POR_ONDE_O_WHATSAPP_SAI },
4640
4810
  { id: "003_a_campanha_sai_no_ritmo_que_o_numero_aguenta", sql: MIGRATION_003_A_CAMPANHA_SAI_NO_RITMO_QUE_O_NUMERO_AGUENTA },
4641
- { id: "004_a_tela_sabe_se_pode_falar_antes_de_deixar_escrever", sql: MIGRATION_004_A_TELA_SABE_SE_PODE_FALAR_ANTES_DE_DEIXAR_ESCREVER }
4811
+ { id: "004_a_tela_sabe_se_pode_falar_antes_de_deixar_escrever", sql: MIGRATION_004_A_TELA_SABE_SE_PODE_FALAR_ANTES_DE_DEIXAR_ESCREVER },
4812
+ { id: "005_a_conversa_conta_ao_funil_que_houve_contato", sql: MIGRATION_005_A_CONVERSA_CONTA_AO_FUNIL_QUE_HOUVE_CONTATO }
4642
4813
  ];
4643
4814
 
4644
4815
  // src/index.ts