@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
|
-
//
|
|
791
|
-
//
|
|
792
|
-
// `accent`
|
|
793
|
-
//
|
|
794
|
-
//
|
|
795
|
-
|
|
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
|
|
802
|
-
/* @__PURE__ */ jsx("span", { className: cn("shrink-0 text-[11px]",
|
|
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:
|
|
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",
|
|
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
|