@fayz-ai/plugin-automations 0.9.0 → 0.10.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/README.md +4 -4
- package/dist/data/fayz.d.ts +11 -0
- package/dist/data/fayz.d.ts.map +1 -0
- package/dist/data/fayz.test.d.ts +2 -0
- package/dist/data/fayz.test.d.ts.map +1 -0
- package/dist/data/templates.d.ts +9 -0
- package/dist/data/templates.d.ts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +902 -107
- package/dist/index.js.map +1 -1
- package/dist/lib/connector-automations.d.ts +32 -0
- package/dist/lib/connector-automations.d.ts.map +1 -0
- package/dist/lib/runtime.d.ts +4 -0
- package/dist/lib/runtime.d.ts.map +1 -0
- package/dist/views/AutomationDetailPage.d.ts +12 -0
- package/dist/views/AutomationDetailPage.d.ts.map +1 -0
- package/dist/views/AutomationsList.d.ts +6 -0
- package/dist/views/AutomationsList.d.ts.map +1 -0
- package/dist/views/ConnectorAutomations.d.ts +6 -0
- package/dist/views/ConnectorAutomations.d.ts.map +1 -0
- package/dist/views/RunLog.d.ts +16 -0
- package/dist/views/RunLog.d.ts.map +1 -0
- package/package.json +8 -6
- package/dist/views/AutomationsHome.d.ts +0 -3
- package/dist/views/AutomationsHome.d.ts.map +0 -1
- package/src/index.ts +0 -52
- package/src/views/AutomationsHome.tsx +0 -112
package/dist/index.js
CHANGED
|
@@ -1,144 +1,939 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
1
|
+
import React3 from 'react';
|
|
2
|
+
import { startEventLog, getSupabaseClientOptional, getMessageDefaults, usePluginRuntimeOptional, resolveMessages, resolveMessage, renderMessageTemplate, loadEventRuns } from '@fayz-ai/core';
|
|
3
|
+
import { resolveFayzAgentConnection, getFayzSupabaseClientOptional, useConnectors, adminNavigateTo, useOrganizationStore, connectorRoute, ConnectorMark, ConnectorDeliveries } from '@fayz-ai/admin';
|
|
4
|
+
import * as LucideIcons from 'lucide-react';
|
|
5
|
+
import { Zap, MessageSquare, Smartphone, MessageCircle, Mail, ChevronRight, RotateCcw, Clock, Monitor, RefreshCw, History, AlertTriangle, CircleCheck, CircleSlash } from 'lucide-react';
|
|
6
|
+
import { HeaderTabs, Card, Switch, SubpageHeader, Button, Badge, Hint, toast } from '@fayz-ai/ui';
|
|
7
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
8
|
+
import { createFayzAutomationsClient, FayzAutomationsError } from '@fayz-ai/sdk/automations';
|
|
5
9
|
|
|
6
10
|
// src/index.ts
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
{
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
{ icon: Tag, label: "Create task" }
|
|
30
|
-
]
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
id: "3",
|
|
34
|
-
name: "Appointment reminder",
|
|
35
|
-
trigger: "Appointment booked",
|
|
36
|
-
enrolled: 87,
|
|
37
|
-
active: true,
|
|
38
|
-
steps: [
|
|
39
|
-
{ icon: Clock, label: "Wait until 24h before" },
|
|
40
|
-
{ icon: MessageSquare, label: "WhatsApp reminder" }
|
|
41
|
-
]
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: "4",
|
|
45
|
-
name: "Won deal \u2192 review request",
|
|
46
|
-
trigger: "Deal marked Won",
|
|
47
|
-
enrolled: 41,
|
|
48
|
-
active: false,
|
|
49
|
-
steps: [
|
|
50
|
-
{ icon: Clock, label: "Wait 2 days" },
|
|
51
|
-
{ icon: Mail, label: "Ask for review" }
|
|
52
|
-
]
|
|
11
|
+
function useOrganizationStoreLike() {
|
|
12
|
+
return useOrganizationStore((s) => s.currentOrg?.id);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/lib/connector-automations.ts
|
|
16
|
+
function connectorAutomationRows(connectors) {
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const connector of connectors) {
|
|
19
|
+
if (connector.state !== "connected") continue;
|
|
20
|
+
for (const auto of connector.definition?.managedAutomations ?? []) {
|
|
21
|
+
out.push({
|
|
22
|
+
connectorId: connector.id,
|
|
23
|
+
connectorName: connector.name,
|
|
24
|
+
hostPluginId: connector.hostPluginId,
|
|
25
|
+
icon: connector.icon,
|
|
26
|
+
logoUrl: connector.logoUrl,
|
|
27
|
+
eventKey: auto.eventKey,
|
|
28
|
+
label: auto.label,
|
|
29
|
+
optional: auto.optional ?? false,
|
|
30
|
+
...connector.definition?.listDeliveries ? { listDeliveries: connector.definition.listDeliveries.bind(connector.definition) } : {}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
53
33
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
function groupConnectorAutomations(rows) {
|
|
37
|
+
const groups = [];
|
|
38
|
+
for (const row of rows) {
|
|
39
|
+
let group = groups.find((g) => g.connectorId === row.connectorId);
|
|
40
|
+
if (!group) {
|
|
41
|
+
group = {
|
|
42
|
+
connectorId: row.connectorId,
|
|
43
|
+
connectorName: row.connectorName,
|
|
44
|
+
hostPluginId: row.hostPluginId,
|
|
45
|
+
icon: row.icon,
|
|
46
|
+
logoUrl: row.logoUrl,
|
|
47
|
+
items: []
|
|
48
|
+
};
|
|
49
|
+
groups.push(group);
|
|
50
|
+
}
|
|
51
|
+
group.items.push(row);
|
|
52
|
+
}
|
|
53
|
+
return groups;
|
|
54
|
+
}
|
|
55
|
+
function ConnectorAutomations({ rows }) {
|
|
56
|
+
if (rows.length === 0) return null;
|
|
57
|
+
return /* @__PURE__ */ jsx(Fragment, { children: groupConnectorAutomations(rows).map((group) => {
|
|
58
|
+
const href = connectorRoute({ hostPluginId: group.hostPluginId });
|
|
59
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
60
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: group.connectorName }),
|
|
61
|
+
group.items.map((row) => /* @__PURE__ */ jsx(
|
|
62
|
+
AutomationCard,
|
|
63
|
+
{
|
|
64
|
+
row,
|
|
65
|
+
group,
|
|
66
|
+
href
|
|
67
|
+
},
|
|
68
|
+
`${group.connectorId}:${row.eventKey}`
|
|
69
|
+
))
|
|
70
|
+
] }, group.connectorId);
|
|
71
|
+
}) });
|
|
72
|
+
}
|
|
73
|
+
function AutomationCard({
|
|
74
|
+
row,
|
|
75
|
+
group,
|
|
76
|
+
href
|
|
77
|
+
}) {
|
|
78
|
+
const [open, setOpen] = React3.useState(false);
|
|
79
|
+
return /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
|
|
80
|
+
/* @__PURE__ */ jsxs(
|
|
81
|
+
"div",
|
|
82
|
+
{
|
|
83
|
+
role: "button",
|
|
84
|
+
tabIndex: 0,
|
|
85
|
+
onClick: () => adminNavigateTo(href),
|
|
86
|
+
onKeyDown: (e) => {
|
|
87
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
adminNavigateTo(href);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
className: "flex cursor-pointer items-start gap-4 p-4 transition-colors hover:bg-muted/30 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
93
|
+
children: [
|
|
94
|
+
/* @__PURE__ */ jsx(
|
|
95
|
+
ConnectorMark,
|
|
96
|
+
{
|
|
97
|
+
connector: {
|
|
98
|
+
id: group.connectorId,
|
|
99
|
+
name: group.connectorName,
|
|
100
|
+
icon: group.icon,
|
|
101
|
+
logoUrl: group.logoUrl
|
|
102
|
+
},
|
|
103
|
+
size: "sm"
|
|
104
|
+
}
|
|
105
|
+
),
|
|
106
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
107
|
+
/* @__PURE__ */ jsx("p", { className: "font-medium", children: row.label }),
|
|
108
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
|
|
109
|
+
"Quando ",
|
|
110
|
+
/* @__PURE__ */ jsx("code", { className: "font-mono text-xs", children: row.eventKey }),
|
|
111
|
+
" acontece aqui."
|
|
112
|
+
] })
|
|
113
|
+
] }),
|
|
114
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 pt-0.5 text-right text-xs text-muted-foreground", children: row.optional ? `Opcional \u2014 desligue em ${group.connectorName}` : `Ligada pela integra\xE7\xE3o \xB7 sai ao desconectar ${group.connectorName}` })
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
),
|
|
118
|
+
row.listDeliveries && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
119
|
+
/* @__PURE__ */ jsxs(
|
|
120
|
+
"button",
|
|
121
|
+
{
|
|
122
|
+
type: "button",
|
|
123
|
+
onClick: () => setOpen(!open),
|
|
124
|
+
"aria-expanded": open,
|
|
125
|
+
className: "flex w-full items-center gap-2 border-t border-border px-4 py-2 text-left transition-colors hover:bg-muted/40",
|
|
126
|
+
children: [
|
|
127
|
+
/* @__PURE__ */ jsx(ChevronRight, { className: `h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform ${open ? "rotate-90" : ""}` }),
|
|
128
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs font-medium", children: [
|
|
129
|
+
"O que j\xE1 foi para o ",
|
|
130
|
+
group.connectorName
|
|
131
|
+
] })
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
),
|
|
135
|
+
open && /* @__PURE__ */ jsx("div", { className: "border-t border-border p-4", children: /* @__PURE__ */ jsx(
|
|
136
|
+
ConnectorDeliveries,
|
|
137
|
+
{
|
|
138
|
+
load: (options) => row.listDeliveries({ ...options, eventKey: row.eventKey }),
|
|
139
|
+
providerName: group.connectorName
|
|
140
|
+
}
|
|
141
|
+
) })
|
|
142
|
+
] })
|
|
59
143
|
] });
|
|
60
144
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
145
|
+
var CHANNEL_ICON = {
|
|
146
|
+
email: Mail,
|
|
147
|
+
whatsapp: MessageCircle,
|
|
148
|
+
sms: Smartphone,
|
|
149
|
+
push: MessageSquare,
|
|
150
|
+
webchat: MessageSquare
|
|
151
|
+
};
|
|
152
|
+
var CHANNEL_LABEL = {
|
|
153
|
+
email: "E-mail",
|
|
154
|
+
whatsapp: "WhatsApp",
|
|
155
|
+
sms: "SMS",
|
|
156
|
+
push: "Push",
|
|
157
|
+
webchat: "Chat"
|
|
158
|
+
};
|
|
159
|
+
var KIND_TABS = [
|
|
160
|
+
{ value: "all", label: "Todos" },
|
|
161
|
+
{ value: "transactional", label: "Transacionais" },
|
|
162
|
+
{ value: "marketing", label: "Marketing" }
|
|
163
|
+
];
|
|
164
|
+
var KIND_HINT = {
|
|
165
|
+
all: "Tudo que o produto sabe enviar, por qualquer canal.",
|
|
166
|
+
transactional: "Disparados por uma a\xE7\xE3o da pessoa. N\xE3o precisam de opt-in.",
|
|
167
|
+
marketing: "Iniciativa da empresa. S\xF3 para quem aceitou receber."
|
|
168
|
+
};
|
|
169
|
+
function iconFor(name) {
|
|
170
|
+
if (!name) return Zap;
|
|
171
|
+
const icons = LucideIcons;
|
|
172
|
+
return icons[name] ?? Zap;
|
|
173
|
+
}
|
|
174
|
+
function AutomationsList({ store }) {
|
|
175
|
+
const tenantId = useOrganizationStoreLike();
|
|
176
|
+
const runtime = usePluginRuntimeOptional();
|
|
177
|
+
const [messages, setMessages] = React3.useState(null);
|
|
178
|
+
const [kind, setKind] = React3.useState("all");
|
|
179
|
+
const { connectors } = useConnectors({ includeGrants: false, includeCatalog: false });
|
|
180
|
+
const owned = React3.useMemo(() => connectorAutomationRows(connectors ?? []), [connectors]);
|
|
181
|
+
const reload = React3.useCallback(async () => {
|
|
182
|
+
let overrides = [];
|
|
183
|
+
try {
|
|
184
|
+
overrides = await store.list(tenantId);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.warn("[automations] could not load template overrides; showing defaults", err);
|
|
187
|
+
}
|
|
188
|
+
setMessages(resolveMessages(overrides, runtime?.events ?? []));
|
|
189
|
+
}, [store, tenantId, runtime]);
|
|
190
|
+
React3.useEffect(() => {
|
|
191
|
+
void reload();
|
|
192
|
+
}, [reload]);
|
|
193
|
+
const toggle = async (m, enabled) => {
|
|
194
|
+
setMessages((prev) => prev?.map((x) => x.event.name === m.event.name && x.channel === m.channel ? { ...x, enabled } : x) ?? null);
|
|
195
|
+
try {
|
|
196
|
+
await store.upsert({ eventKey: m.event.name, channel: m.channel, enabled }, tenantId);
|
|
197
|
+
await reload();
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.warn("[automations] toggle failed", err);
|
|
200
|
+
toast.error("N\xE3o foi poss\xEDvel salvar");
|
|
201
|
+
await reload();
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const visible = (messages ?? []).filter((m) => kind === "all" || m.kind === kind);
|
|
205
|
+
const ownedVisible = kind === "all" ? owned : [];
|
|
206
|
+
const activeCount = (messages ?? []).filter((m) => m.enabled).length + owned.length;
|
|
207
|
+
const totalCount = (messages?.length ?? 0) + owned.length;
|
|
208
|
+
const groups = [];
|
|
209
|
+
for (const m of visible) {
|
|
210
|
+
let g = groups.find((x) => x.module === m.event.pluginId);
|
|
211
|
+
if (!g) {
|
|
212
|
+
g = { module: m.event.pluginId, label: m.event.pluginName, items: [] };
|
|
213
|
+
groups.push(g);
|
|
214
|
+
}
|
|
215
|
+
g.items.push(m);
|
|
216
|
+
}
|
|
217
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
218
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
|
|
219
|
+
/* @__PURE__ */ jsx(HeaderTabs, { tabs: KIND_TABS, value: kind, onChange: setKind, "aria-label": "Automa\xE7\xF5es" }),
|
|
220
|
+
messages !== null && /* @__PURE__ */ jsxs("span", { className: "text-sm text-muted-foreground", children: [
|
|
221
|
+
activeCount,
|
|
222
|
+
" de ",
|
|
223
|
+
totalCount,
|
|
224
|
+
" automa\xE7\xF5es ativas"
|
|
225
|
+
] })
|
|
72
226
|
] }),
|
|
73
|
-
/* @__PURE__ */ jsx("
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
227
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: KIND_HINT[kind] }),
|
|
228
|
+
messages === null ? /* @__PURE__ */ jsx("div", { className: "space-y-3", children: Array.from({ length: 4 }).map((_, i) => /* @__PURE__ */ jsxs(Card, { className: "p-4", children: [
|
|
229
|
+
/* @__PURE__ */ jsx("div", { className: "h-4 w-40 animate-pulse rounded bg-muted" }),
|
|
230
|
+
/* @__PURE__ */ jsx("div", { className: "mt-2 h-3 w-64 animate-pulse rounded bg-muted" })
|
|
231
|
+
] }, i)) }) : groups.length === 0 && ownedVisible.length === 0 ? /* @__PURE__ */ jsxs(Card, { className: "flex flex-col items-center gap-1 p-12 text-center", children: [
|
|
232
|
+
/* @__PURE__ */ jsx(Zap, { className: "mb-2 h-9 w-9 text-muted-foreground/40", strokeWidth: 1.5 }),
|
|
233
|
+
/* @__PURE__ */ jsx("p", { className: "text-base font-semibold", children: "Nenhuma automa\xE7\xE3o nesta aba" }),
|
|
234
|
+
/* @__PURE__ */ jsx("p", { className: "max-w-sm text-sm text-muted-foreground", children: "Os eventos v\xEAm dos m\xF3dulos instalados \u2014 a loja traz os do pedido, o CRM os do funil." })
|
|
235
|
+
] }) : groups.map((group) => /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
236
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: group.label }),
|
|
237
|
+
group.items.map((m) => {
|
|
238
|
+
const Icon = iconFor(m.event.icon ?? m.event.pluginIcon);
|
|
239
|
+
const ChannelIcon = CHANNEL_ICON[m.channel];
|
|
240
|
+
const href = `/automations/${encodeURIComponent(m.event.name)}/${m.channel}`;
|
|
241
|
+
return /* @__PURE__ */ jsx(
|
|
242
|
+
Card,
|
|
243
|
+
{
|
|
244
|
+
role: "button",
|
|
245
|
+
tabIndex: 0,
|
|
246
|
+
onClick: () => adminNavigateTo(href),
|
|
247
|
+
onKeyDown: (e) => {
|
|
248
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
249
|
+
e.preventDefault();
|
|
250
|
+
adminNavigateTo(href);
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
className: "cursor-pointer overflow-hidden transition-colors hover:border-primary/40 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
254
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-4 p-4", children: [
|
|
255
|
+
/* @__PURE__ */ jsx("span", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-border text-muted-foreground", children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" }) }),
|
|
256
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
257
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
258
|
+
/* @__PURE__ */ jsx("p", { className: "font-medium", children: m.override?.name ?? m.event.title ?? m.event.name }),
|
|
259
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground", children: [
|
|
260
|
+
/* @__PURE__ */ jsx(ChannelIcon, { className: "h-3 w-3" }),
|
|
261
|
+
CHANNEL_LABEL[m.channel]
|
|
262
|
+
] }),
|
|
263
|
+
m.customized && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-primary/10 px-2 py-0.5 text-[11px] text-primary", children: "personalizado" })
|
|
264
|
+
] }),
|
|
265
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: m.event.description }),
|
|
266
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground", children: [
|
|
267
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
|
|
268
|
+
/* @__PURE__ */ jsx(Zap, { className: "h-3 w-3" }),
|
|
269
|
+
m.event.trigger
|
|
270
|
+
] }),
|
|
271
|
+
m.subject && /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
|
|
272
|
+
"Assunto: ",
|
|
273
|
+
m.subject
|
|
274
|
+
] })
|
|
275
|
+
] })
|
|
276
|
+
] }),
|
|
277
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-3", children: [
|
|
278
|
+
/* @__PURE__ */ jsx(
|
|
279
|
+
Switch,
|
|
280
|
+
{
|
|
281
|
+
checked: m.enabled,
|
|
282
|
+
onCheckedChange: (next) => void toggle(m, next),
|
|
283
|
+
label: m.enabled ? "Ativo" : "Inativo",
|
|
284
|
+
"aria-label": `${m.event.title} \u2014 ${CHANNEL_LABEL[m.channel]}`
|
|
285
|
+
}
|
|
286
|
+
),
|
|
287
|
+
/* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4 text-muted-foreground" })
|
|
288
|
+
] })
|
|
86
289
|
] })
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
290
|
+
},
|
|
291
|
+
`${m.event.name}:${m.channel}`
|
|
292
|
+
);
|
|
293
|
+
})
|
|
294
|
+
] }, group.module)),
|
|
295
|
+
messages !== null && /* @__PURE__ */ jsx(ConnectorAutomations, { rows: ownedVisible })
|
|
296
|
+
] });
|
|
297
|
+
}
|
|
298
|
+
function toTemplate(automation) {
|
|
299
|
+
const eventKey = automation.triggerConfig?.eventKey;
|
|
300
|
+
if (!eventKey) return null;
|
|
301
|
+
const action = automation.actions?.[0];
|
|
302
|
+
return {
|
|
303
|
+
id: automation.id,
|
|
304
|
+
tenantId: automation.tenantId ?? "",
|
|
305
|
+
eventKey,
|
|
306
|
+
channel: automation.triggerConfig.sourceChannel ?? "email",
|
|
307
|
+
enabled: automation.enabled,
|
|
308
|
+
// Empty and absent mean the same — "no customisation", the catalogue default.
|
|
309
|
+
subject: action?.config?.subject || null,
|
|
310
|
+
body: action?.config?.body || null,
|
|
311
|
+
// The automation name belongs to the platform, not a user rename: the
|
|
312
|
+
// catalogue title still owns the screen.
|
|
313
|
+
name: null,
|
|
314
|
+
updatedAt: automation.updatedAt
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function catalogDefaultEnabled(eventKey, channel) {
|
|
318
|
+
const def = getMessageDefaults().find((d) => d.eventKey === eventKey);
|
|
319
|
+
return def?.defaultEnabled?.[channel] ?? false;
|
|
320
|
+
}
|
|
321
|
+
async function defaultSessionToken() {
|
|
322
|
+
const session = (await getFayzSupabaseClientOptional()?.auth.getSession())?.data.session;
|
|
323
|
+
return session?.access_token ?? null;
|
|
324
|
+
}
|
|
325
|
+
function createFayzTemplateStore(connection, deps = {}) {
|
|
326
|
+
const client = deps.client ?? createFayzAutomationsClient({
|
|
327
|
+
baseUrl: connection.apiBaseUrl,
|
|
328
|
+
projectId: connection.projectId,
|
|
329
|
+
publishableKey: connection.publishableKey
|
|
330
|
+
});
|
|
331
|
+
const getSessionToken = deps.getSessionToken ?? defaultSessionToken;
|
|
332
|
+
const ids = /* @__PURE__ */ new Map();
|
|
333
|
+
const keyOf = (eventKey, channel) => `${eventKey}:${channel}`;
|
|
334
|
+
async function refresh(tenantId) {
|
|
335
|
+
const automations = await client.list({ tenantId });
|
|
336
|
+
ids.clear();
|
|
337
|
+
const rows = [];
|
|
338
|
+
for (const automation of automations) {
|
|
339
|
+
const row = toTemplate(automation);
|
|
340
|
+
if (!row) continue;
|
|
341
|
+
ids.set(keyOf(row.eventKey, row.channel), automation.id);
|
|
342
|
+
rows.push(row);
|
|
343
|
+
}
|
|
344
|
+
return rows;
|
|
345
|
+
}
|
|
346
|
+
const LIST_TTL_MS = 3e4;
|
|
347
|
+
let cache = null;
|
|
348
|
+
function invalidate() {
|
|
349
|
+
cache = null;
|
|
350
|
+
}
|
|
351
|
+
function load(tenantId) {
|
|
352
|
+
if (cache && cache.tenantId === tenantId && Date.now() - cache.at < LIST_TTL_MS) return cache.promise;
|
|
353
|
+
const promise = refresh(tenantId);
|
|
354
|
+
promise.catch(() => {
|
|
355
|
+
if (cache?.promise === promise) cache = null;
|
|
356
|
+
});
|
|
357
|
+
cache = { tenantId, at: Date.now(), promise };
|
|
358
|
+
return promise;
|
|
359
|
+
}
|
|
360
|
+
async function requireToken() {
|
|
361
|
+
const token = await getSessionToken();
|
|
362
|
+
if (!token) throw new Error("Sess\xE3o expirada \u2014 entre de novo.");
|
|
363
|
+
return token;
|
|
364
|
+
}
|
|
365
|
+
async function resolveId(eventKey, channel, tenantId) {
|
|
366
|
+
const key = keyOf(eventKey, channel);
|
|
367
|
+
if (!ids.has(key)) await load(tenantId);
|
|
368
|
+
return ids.get(key);
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
async list(tenantId) {
|
|
372
|
+
if (!tenantId) return [];
|
|
373
|
+
return load(tenantId);
|
|
374
|
+
},
|
|
375
|
+
async upsert(input, tenantId) {
|
|
376
|
+
if (!tenantId) throw new Error("@fayz-ai/plugin-automations: tenantId is required");
|
|
377
|
+
const appSessionToken = await requireToken();
|
|
378
|
+
const key = keyOf(input.eventKey, input.channel);
|
|
379
|
+
let id = await resolveId(input.eventKey, input.channel, tenantId);
|
|
380
|
+
if (!id) {
|
|
381
|
+
const action = {};
|
|
382
|
+
if (input.subject != null) action.subject = input.subject;
|
|
383
|
+
if (input.body != null) action.body = input.body;
|
|
384
|
+
try {
|
|
385
|
+
const created = await client.create({
|
|
386
|
+
tenantId,
|
|
387
|
+
appSessionToken,
|
|
388
|
+
eventKey: input.eventKey,
|
|
389
|
+
sourceChannel: input.channel,
|
|
390
|
+
// The platform has no "inherit from catalogue": materialise the default.
|
|
391
|
+
enabled: input.enabled ?? catalogDefaultEnabled(input.eventKey, input.channel),
|
|
392
|
+
...Object.keys(action).length ? { action } : {}
|
|
393
|
+
});
|
|
394
|
+
ids.set(key, created.id);
|
|
395
|
+
invalidate();
|
|
396
|
+
const row2 = toTemplate(created);
|
|
397
|
+
if (!row2) throw new Error("@fayz-ai/plugin-automations: platform did not round-trip eventKey");
|
|
398
|
+
return row2;
|
|
399
|
+
} catch (err) {
|
|
400
|
+
if (!(err instanceof FayzAutomationsError) || err.status !== 409) throw err;
|
|
401
|
+
invalidate();
|
|
402
|
+
await refresh(tenantId);
|
|
403
|
+
id = ids.get(key);
|
|
404
|
+
if (!id) throw err;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const updated = await client.update({
|
|
408
|
+
id,
|
|
409
|
+
tenantId,
|
|
410
|
+
appSessionToken,
|
|
411
|
+
// Only what was sent: omitting has to mean "leave it alone", as in the local store.
|
|
412
|
+
...typeof input.enabled === "boolean" ? { enabled: input.enabled } : {},
|
|
413
|
+
...input.subject !== void 0 || input.body !== void 0 ? {
|
|
414
|
+
action: {
|
|
415
|
+
...input.subject !== void 0 ? { subject: input.subject } : {},
|
|
416
|
+
...input.body !== void 0 ? { body: input.body } : {}
|
|
417
|
+
}
|
|
418
|
+
} : {}
|
|
419
|
+
});
|
|
420
|
+
invalidate();
|
|
421
|
+
const row = toTemplate(updated);
|
|
422
|
+
if (!row) throw new Error("@fayz-ai/plugin-automations: platform did not round-trip eventKey");
|
|
423
|
+
return row;
|
|
424
|
+
},
|
|
425
|
+
async reset(eventKey, channel, tenantId) {
|
|
426
|
+
if (!tenantId) return;
|
|
427
|
+
const id = await resolveId(eventKey, channel, tenantId);
|
|
428
|
+
if (!id) return;
|
|
429
|
+
const appSessionToken = await requireToken();
|
|
430
|
+
await client.update({ id, tenantId, appSessionToken, action: { subject: null, body: null } });
|
|
431
|
+
invalidate();
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
var cachedClient = null;
|
|
436
|
+
function getFayzAutomationsClient(connection) {
|
|
437
|
+
const key = `${connection.apiBaseUrl}|${connection.projectId}|${connection.publishableKey}`;
|
|
438
|
+
if (cachedClient?.key === key) return cachedClient.client;
|
|
439
|
+
const client = createFayzAutomationsClient({
|
|
440
|
+
baseUrl: connection.apiBaseUrl,
|
|
441
|
+
projectId: connection.projectId,
|
|
442
|
+
publishableKey: connection.publishableKey
|
|
443
|
+
});
|
|
444
|
+
cachedClient = { key, client };
|
|
445
|
+
return client;
|
|
446
|
+
}
|
|
447
|
+
var STATUS = {
|
|
448
|
+
sent: { label: "Enviado", hint: "A mensagem saiu.", variant: "success", Icon: CircleCheck },
|
|
449
|
+
recorded: {
|
|
450
|
+
label: "Disparou",
|
|
451
|
+
// Today's honest state: the trigger works, the send doesn't exist yet.
|
|
452
|
+
hint: "O gatilho disparou e ficou registrado. O envio autom\xE1tico ainda n\xE3o est\xE1 ligado neste ambiente.",
|
|
453
|
+
variant: "info",
|
|
454
|
+
Icon: History
|
|
455
|
+
},
|
|
456
|
+
skipped: { label: "Ignorado", hint: "O gatilho disparou, mas nada foi enviado \u2014 automa\xE7\xE3o desligada ou sem canal.", variant: "warning", Icon: CircleSlash },
|
|
457
|
+
failed: { label: "Falhou", hint: "A tentativa de envio quebrou. O erro est\xE1 na linha.", variant: "destructive", Icon: AlertTriangle }
|
|
458
|
+
};
|
|
459
|
+
function when(iso) {
|
|
460
|
+
const then = new Date(iso).getTime();
|
|
461
|
+
const diff = Date.now() - then;
|
|
462
|
+
const min = Math.round(diff / 6e4);
|
|
463
|
+
if (min < 1) return "agora";
|
|
464
|
+
if (min < 60) return `h\xE1 ${min} min`;
|
|
465
|
+
const h = Math.round(min / 60);
|
|
466
|
+
if (h < 24) return `h\xE1 ${h} h`;
|
|
467
|
+
return new Date(iso).toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
|
|
468
|
+
}
|
|
469
|
+
function RunRow({ run }) {
|
|
470
|
+
const [open, setOpen] = React3.useState(false);
|
|
471
|
+
const cfg = STATUS[run.status] ?? STATUS.recorded;
|
|
472
|
+
const { Icon } = cfg;
|
|
473
|
+
return /* @__PURE__ */ jsxs("div", { className: "border-b border-border last:border-b-0", children: [
|
|
474
|
+
/* @__PURE__ */ jsxs(
|
|
475
|
+
"button",
|
|
476
|
+
{
|
|
477
|
+
type: "button",
|
|
478
|
+
onClick: () => setOpen(!open),
|
|
479
|
+
className: "flex w-full items-center gap-3 px-4 py-2.5 text-left transition-colors hover:bg-muted/40",
|
|
480
|
+
children: [
|
|
481
|
+
/* @__PURE__ */ jsx(ChevronRight, { className: `h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform ${open ? "rotate-90" : ""}` }),
|
|
482
|
+
/* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
|
|
483
|
+
/* @__PURE__ */ jsx(Badge, { variant: cfg.variant, children: cfg.label }),
|
|
484
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm text-muted-foreground", children: typeof run.payload.record_name === "string" ? run.payload.record_name : typeof run.payload.customer_name === "string" ? run.payload.customer_name : typeof run.payload.contact_name === "string" ? run.payload.contact_name : run.subjectId ?? "\u2014" }),
|
|
485
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: when(run.occurredAt) })
|
|
486
|
+
]
|
|
487
|
+
}
|
|
488
|
+
),
|
|
489
|
+
open && /* @__PURE__ */ jsxs("div", { className: "space-y-3 bg-muted/20 px-4 pb-4 pt-1", children: [
|
|
490
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: cfg.hint }),
|
|
491
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
492
|
+
/* @__PURE__ */ jsx("p", { className: "mb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: "Dados do disparo" }),
|
|
493
|
+
/* @__PURE__ */ jsx("pre", { className: "overflow-x-auto rounded-md border border-border bg-card p-3 font-mono text-[11px] leading-relaxed", children: JSON.stringify(run.payload, null, 2) })
|
|
494
|
+
] }),
|
|
495
|
+
run.result && /* @__PURE__ */ jsxs("div", { children: [
|
|
496
|
+
/* @__PURE__ */ jsx("p", { className: "mb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: "Resultado" }),
|
|
497
|
+
/* @__PURE__ */ jsx("pre", { className: "overflow-x-auto rounded-md border border-border bg-card p-3 font-mono text-[11px] leading-relaxed", children: JSON.stringify(run.result, null, 2) })
|
|
498
|
+
] }),
|
|
499
|
+
run.error && /* @__PURE__ */ jsx("p", { className: "rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 font-mono text-[11px] text-destructive", children: run.error })
|
|
500
|
+
] })
|
|
501
|
+
] });
|
|
502
|
+
}
|
|
503
|
+
function RunLog({ eventKey, tenantId, enabled }) {
|
|
504
|
+
const [runs, setRuns] = React3.useState(null);
|
|
505
|
+
const [loading, setLoading] = React3.useState(false);
|
|
506
|
+
const load = React3.useCallback(async () => {
|
|
507
|
+
setLoading(true);
|
|
508
|
+
setRuns(await loadEventRuns(eventKey, { tenantId, limit: 20 }));
|
|
509
|
+
setLoading(false);
|
|
510
|
+
}, [eventKey, tenantId]);
|
|
511
|
+
React3.useEffect(() => {
|
|
512
|
+
void load();
|
|
513
|
+
}, [load]);
|
|
514
|
+
return /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
|
|
515
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-border px-4 py-2.5", children: [
|
|
516
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
517
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Execu\xE7\xF5es recentes" }),
|
|
518
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Cada vez que este gatilho disparou nesta conta." })
|
|
519
|
+
] }),
|
|
520
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => void load(), disabled: loading, children: /* @__PURE__ */ jsx(RefreshCw, { className: `h-3.5 w-3.5 ${loading ? "animate-spin" : ""}` }) })
|
|
521
|
+
] }),
|
|
522
|
+
runs === null ? /* @__PURE__ */ jsx("div", { className: "space-y-2 p-4", children: [1, 2, 3].map((i) => /* @__PURE__ */ jsx("div", { className: "h-8 animate-pulse rounded bg-muted/40" }, i)) }) : runs.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1 px-4 py-10 text-center", children: [
|
|
523
|
+
/* @__PURE__ */ jsx(History, { className: "h-7 w-7 text-muted-foreground/40", strokeWidth: 1.5 }),
|
|
524
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: "Nenhum disparo ainda" }),
|
|
525
|
+
/* @__PURE__ */ jsx("p", { className: "max-w-sm text-xs text-muted-foreground", children: enabled ? "Assim que este evento acontecer, ele aparece aqui \u2014 mesmo antes de existir envio." : "A automa\xE7\xE3o est\xE1 desligada. O gatilho continua sendo registrado aqui quando o evento acontecer." })
|
|
526
|
+
] }) : /* @__PURE__ */ jsx("div", { children: runs.map((run) => /* @__PURE__ */ jsx(RunRow, { run }, run.id)) })
|
|
527
|
+
] });
|
|
528
|
+
}
|
|
529
|
+
var EXEC_STATUS = {
|
|
530
|
+
success: { label: "Executada", variant: "success", Icon: CircleCheck },
|
|
531
|
+
error: { label: "Falhou", variant: "destructive", Icon: AlertTriangle }
|
|
532
|
+
};
|
|
533
|
+
function ExecutionRow({ execution }) {
|
|
534
|
+
const cfg = EXEC_STATUS[execution.status] ?? { label: execution.status, variant: "info", Icon: History };
|
|
535
|
+
const { Icon } = cfg;
|
|
536
|
+
return /* @__PURE__ */ jsxs("div", { className: "border-b border-border px-4 py-2.5 last:border-b-0", children: [
|
|
537
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
538
|
+
/* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
|
|
539
|
+
/* @__PURE__ */ jsx(Badge, { variant: cfg.variant, children: cfg.label }),
|
|
540
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm text-muted-foreground", children: execution.automationName }),
|
|
541
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: when(execution.executedAt) })
|
|
542
|
+
] }),
|
|
543
|
+
execution.error && /* @__PURE__ */ jsx("p", { className: "ml-6 mt-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 font-mono text-[11px] text-destructive", children: execution.error })
|
|
544
|
+
] });
|
|
545
|
+
}
|
|
546
|
+
function FayzRunLog({ connection, eventKey, tenantId, enabled }) {
|
|
547
|
+
const [executions, setExecutions] = React3.useState(null);
|
|
548
|
+
const [loading, setLoading] = React3.useState(false);
|
|
549
|
+
const load = React3.useCallback(async () => {
|
|
550
|
+
if (!tenantId) return;
|
|
551
|
+
setLoading(true);
|
|
552
|
+
try {
|
|
553
|
+
setExecutions(await getFayzAutomationsClient(connection).listExecutions({ tenantId, eventKey, limit: 20 }));
|
|
554
|
+
} catch (err) {
|
|
555
|
+
console.warn("[automations] could not load platform executions", err);
|
|
556
|
+
setExecutions([]);
|
|
557
|
+
} finally {
|
|
558
|
+
setLoading(false);
|
|
559
|
+
}
|
|
560
|
+
}, [connection, eventKey, tenantId]);
|
|
561
|
+
React3.useEffect(() => {
|
|
562
|
+
void load();
|
|
563
|
+
}, [load]);
|
|
564
|
+
return /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
|
|
565
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-border px-4 py-2.5", children: [
|
|
566
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
567
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Execu\xE7\xF5es recentes" }),
|
|
568
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "O que a plataforma Fayz executou para este gatilho nesta conta." })
|
|
93
569
|
] }),
|
|
94
|
-
/* @__PURE__ */ jsx(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
570
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => void load(), disabled: loading, children: /* @__PURE__ */ jsx(RefreshCw, { className: `h-3.5 w-3.5 ${loading ? "animate-spin" : ""}` }) })
|
|
571
|
+
] }),
|
|
572
|
+
executions === null ? /* @__PURE__ */ jsx("div", { className: "space-y-2 p-4", children: [1, 2, 3].map((i) => /* @__PURE__ */ jsx("div", { className: "h-8 animate-pulse rounded bg-muted/40" }, i)) }) : executions.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1 px-4 py-10 text-center", children: [
|
|
573
|
+
/* @__PURE__ */ jsx(History, { className: "h-7 w-7 text-muted-foreground/40", strokeWidth: 1.5 }),
|
|
574
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: "Nenhuma execu\xE7\xE3o ainda" }),
|
|
575
|
+
/* @__PURE__ */ jsx("p", { className: "max-w-sm text-xs text-muted-foreground", children: enabled ? "Assim que este evento acontecer, a plataforma executa a automa\xE7\xE3o e o resultado aparece aqui." : "A automa\xE7\xE3o est\xE1 desligada \u2014 a plataforma n\xE3o executa nada para este gatilho." })
|
|
576
|
+
] }) : /* @__PURE__ */ jsx("div", { children: executions.map((e) => /* @__PURE__ */ jsx(ExecutionRow, { execution: e }, e.id)) })
|
|
577
|
+
] });
|
|
578
|
+
}
|
|
579
|
+
var TABS = [
|
|
580
|
+
{ value: "preview", label: "Pr\xE9via" },
|
|
581
|
+
{ value: "code", label: "C\xF3digo" },
|
|
582
|
+
// O que já aconteceu. Fica ao lado do texto, e não numa tela à parte, porque
|
|
583
|
+
// é a mesma pergunta em dois tempos: o que vai sair, e o que saiu.
|
|
584
|
+
{ value: "runs", label: "Execu\xE7\xF5es" }
|
|
585
|
+
];
|
|
586
|
+
var CHANNEL_LABEL2 = {
|
|
587
|
+
email: "E-mail",
|
|
588
|
+
whatsapp: "WhatsApp",
|
|
589
|
+
sms: "SMS",
|
|
590
|
+
push: "Push",
|
|
591
|
+
webchat: "Chat"
|
|
592
|
+
};
|
|
593
|
+
var SAMPLE = {
|
|
594
|
+
store_name: "Sua Loja",
|
|
595
|
+
company_name: "Sua Empresa",
|
|
596
|
+
customer_name: "Maria Silva",
|
|
597
|
+
contact_name: "Maria Silva",
|
|
598
|
+
lead_name: "Maria Silva",
|
|
599
|
+
order_number: "1042",
|
|
600
|
+
order_total: "R$ 189,90",
|
|
601
|
+
order_items: "1 \xD7 Camiseta Cl\xE1ssica \u2014 R$ 89,90\n2 \xD7 Caneca Esmaltada \u2014 R$ 100,00",
|
|
602
|
+
tracking_code: "BR123456789BR",
|
|
603
|
+
cart_url: "https://sualoja.com.br/carrinho",
|
|
604
|
+
deal_name: "Proposta \u2014 Reforma da fachada",
|
|
605
|
+
deal_value: "R$ 12.400,00",
|
|
606
|
+
stage_name: "Negocia\xE7\xE3o",
|
|
607
|
+
owner_name: "Jo\xE3o Pereira",
|
|
608
|
+
appointment_date: "12/08/2026",
|
|
609
|
+
appointment_time: "14:30"
|
|
610
|
+
};
|
|
611
|
+
function iconFor2(name) {
|
|
612
|
+
if (!name) return Zap;
|
|
613
|
+
const icons = LucideIcons;
|
|
614
|
+
return icons[name] ?? Zap;
|
|
615
|
+
}
|
|
616
|
+
function AutomationDetailPage({ eventKey, channel, store, backTo = "/automations" }) {
|
|
617
|
+
const tenantId = useOrganizationStoreLike();
|
|
618
|
+
const runtime = usePluginRuntimeOptional();
|
|
619
|
+
const fayzConnection = React3.useMemo(() => resolveFayzAgentConnection(), []);
|
|
620
|
+
const [message, setMessage] = React3.useState(void 0);
|
|
621
|
+
const [tab, setTab] = React3.useState("preview");
|
|
622
|
+
const [device, setDevice] = React3.useState("desktop");
|
|
623
|
+
const [draft, setDraft] = React3.useState(null);
|
|
624
|
+
const [saving, setSaving] = React3.useState(false);
|
|
625
|
+
const back = React3.useCallback(() => adminNavigateTo(backTo), [backTo]);
|
|
626
|
+
const reload = React3.useCallback(async () => {
|
|
627
|
+
if (!eventKey || !channel) {
|
|
628
|
+
setMessage(null);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
let overrides = [];
|
|
632
|
+
try {
|
|
633
|
+
overrides = await store.list(tenantId);
|
|
634
|
+
} catch (err) {
|
|
635
|
+
console.warn("[automations] could not load overrides; showing defaults", err);
|
|
636
|
+
}
|
|
637
|
+
const found = resolveMessage(decodeURIComponent(eventKey), channel, overrides, runtime?.events ?? []);
|
|
638
|
+
setMessage(found);
|
|
639
|
+
if (found) setDraft({ subject: found.subject, body: found.body });
|
|
640
|
+
}, [eventKey, channel, store, tenantId, runtime]);
|
|
641
|
+
React3.useEffect(() => {
|
|
642
|
+
void reload();
|
|
643
|
+
}, [reload]);
|
|
644
|
+
const header = /* @__PURE__ */ jsx(
|
|
645
|
+
SubpageHeader,
|
|
646
|
+
{
|
|
647
|
+
title: message === void 0 ? "" : message?.override?.name ?? message?.event.title ?? message?.event.name ?? "Automa\xE7\xE3o",
|
|
648
|
+
parentLabel: "Automa\xE7\xF5es",
|
|
649
|
+
onBack: back
|
|
650
|
+
}
|
|
651
|
+
);
|
|
652
|
+
if (message === void 0) {
|
|
653
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
654
|
+
header,
|
|
655
|
+
/* @__PURE__ */ jsx("div", { className: "h-8 w-64 animate-pulse rounded bg-muted" }),
|
|
656
|
+
/* @__PURE__ */ jsx(Card, { className: "h-96 animate-pulse" })
|
|
657
|
+
] });
|
|
658
|
+
}
|
|
659
|
+
if (!message) {
|
|
660
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
661
|
+
header,
|
|
662
|
+
/* @__PURE__ */ jsxs(Card, { className: "flex flex-col items-center gap-2 p-12 text-center", children: [
|
|
663
|
+
/* @__PURE__ */ jsx(Mail, { className: "h-9 w-9 text-muted-foreground/40", strokeWidth: 1.5 }),
|
|
664
|
+
/* @__PURE__ */ jsx("p", { className: "font-semibold", children: "Automa\xE7\xE3o n\xE3o encontrada" }),
|
|
665
|
+
/* @__PURE__ */ jsxs("p", { className: "max-w-sm text-sm text-muted-foreground", children: [
|
|
666
|
+
"O evento ",
|
|
667
|
+
/* @__PURE__ */ jsx("code", { className: "font-mono", children: eventKey }),
|
|
668
|
+
" n\xE3o est\xE1 no cat\xE1logo \u2014 o m\xF3dulo que o registra pode n\xE3o estar instalado nesta conta."
|
|
99
669
|
] }),
|
|
100
|
-
|
|
101
|
-
] }
|
|
102
|
-
] }
|
|
103
|
-
|
|
670
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", className: "mt-2", onClick: back, children: "Voltar para Automa\xE7\xF5es" })
|
|
671
|
+
] })
|
|
672
|
+
] });
|
|
673
|
+
}
|
|
674
|
+
const event = message.event;
|
|
675
|
+
const fallback = { subject: message.override ? void 0 : message.subject, body: message.body };
|
|
676
|
+
const Icon = iconFor2(event.icon);
|
|
677
|
+
const current = draft ?? { subject: message.subject, body: message.body };
|
|
678
|
+
const dirty = current.subject !== message.subject || current.body !== message.body;
|
|
679
|
+
const hasSubject = message.channel === "email";
|
|
680
|
+
const patch = async (input) => {
|
|
681
|
+
setSaving(true);
|
|
682
|
+
try {
|
|
683
|
+
await store.upsert(input, tenantId);
|
|
684
|
+
await reload();
|
|
685
|
+
} catch (err) {
|
|
686
|
+
console.warn("[automations] save failed", err);
|
|
687
|
+
toast.error("N\xE3o foi poss\xEDvel salvar");
|
|
688
|
+
} finally {
|
|
689
|
+
setSaving(false);
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
const save = () => void patch({
|
|
693
|
+
eventKey: event.name,
|
|
694
|
+
channel: message.channel,
|
|
695
|
+
// Igual ao padrão volta a ser "sem personalização", não uma cópia.
|
|
696
|
+
subject: current.subject.trim() === (fallback?.subject ?? "") ? null : current.subject.trim() || null,
|
|
697
|
+
body: current.body.trim() === fallback?.body ? null : current.body.trim() || null
|
|
698
|
+
});
|
|
699
|
+
const reset = async () => {
|
|
700
|
+
setDraft({ subject: fallback?.subject ?? "", body: fallback?.body ?? "" });
|
|
701
|
+
setSaving(true);
|
|
702
|
+
try {
|
|
703
|
+
await store.reset(event.name, message.channel, tenantId);
|
|
704
|
+
await reload();
|
|
705
|
+
toast.success("Texto restaurado");
|
|
706
|
+
} catch (err) {
|
|
707
|
+
console.warn("[automations] reset failed", err);
|
|
708
|
+
toast.error("N\xE3o foi poss\xEDvel restaurar");
|
|
709
|
+
} finally {
|
|
710
|
+
setSaving(false);
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
const previewSubject = renderMessageTemplate(current.subject, SAMPLE);
|
|
714
|
+
const previewBody = renderMessageTemplate(current.body, SAMPLE);
|
|
715
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-5", children: [
|
|
716
|
+
header,
|
|
717
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-start gap-3", children: [
|
|
718
|
+
/* @__PURE__ */ jsx("span", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-border text-muted-foreground", children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" }) }),
|
|
719
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
720
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
721
|
+
/* @__PURE__ */ jsx(Badge, { variant: "outline", children: CHANNEL_LABEL2[message.channel] }),
|
|
722
|
+
/* @__PURE__ */ jsx(Badge, { variant: "outline", children: message.kind === "marketing" ? "Marketing" : "Transacional" }),
|
|
723
|
+
message.customized && /* @__PURE__ */ jsx(Badge, { variant: "info", children: "Personalizado" })
|
|
724
|
+
] }),
|
|
725
|
+
/* @__PURE__ */ jsx("p", { className: "mt-0.5 text-sm text-muted-foreground", children: event.description })
|
|
726
|
+
] }),
|
|
727
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-wrap items-center gap-2", children: [
|
|
728
|
+
/* @__PURE__ */ jsx(Hint, { label: "Descarta o texto personalizado e volta ao padr\xE3o do produto.", children: /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: () => void reset(), disabled: !message.customized || saving, children: [
|
|
729
|
+
/* @__PURE__ */ jsx(RotateCcw, { className: "mr-2 h-4 w-4" }),
|
|
730
|
+
"Restaurar padr\xE3o"
|
|
731
|
+
] }) }) }),
|
|
732
|
+
/* @__PURE__ */ jsx(
|
|
733
|
+
Switch,
|
|
734
|
+
{
|
|
735
|
+
size: "md",
|
|
736
|
+
checked: message.enabled,
|
|
737
|
+
onCheckedChange: (next) => void patch({ eventKey: event.name, channel: message.channel, enabled: next }),
|
|
738
|
+
disabled: saving,
|
|
739
|
+
label: message.enabled ? "Ativa" : "Inativa"
|
|
740
|
+
}
|
|
741
|
+
),
|
|
742
|
+
/* @__PURE__ */ jsx(Button, { onClick: save, disabled: !dirty || saving, children: saving ? "Salvando\u2026" : dirty ? "Salvar" : "Salvo" })
|
|
743
|
+
] })
|
|
744
|
+
] }),
|
|
745
|
+
/* @__PURE__ */ jsx(HeaderTabs, { tabs: TABS, value: tab, onChange: setTab, "aria-label": "Visualiza\xE7\xE3o" }),
|
|
746
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-6 gap-y-2 border-b border-border pb-4 text-xs text-muted-foreground", children: [
|
|
747
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
|
|
748
|
+
/* @__PURE__ */ jsx(Zap, { className: "h-3.5 w-3.5" }),
|
|
749
|
+
"Gatilho:",
|
|
750
|
+
/* @__PURE__ */ jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-[11px] text-foreground", children: event.name })
|
|
751
|
+
] }),
|
|
752
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
|
|
753
|
+
/* @__PURE__ */ jsx(Clock, { className: "h-3.5 w-3.5" }),
|
|
754
|
+
event.trigger
|
|
755
|
+
] }),
|
|
756
|
+
hasSubject && current.subject && /* @__PURE__ */ jsxs("span", { className: "min-w-0 truncate", children: [
|
|
757
|
+
"Assunto: ",
|
|
758
|
+
current.subject
|
|
759
|
+
] })
|
|
760
|
+
] }),
|
|
761
|
+
tab === "runs" ? (
|
|
762
|
+
// Atrás de um projeto Fayz o histórico verdadeiro é o da plataforma —
|
|
763
|
+
// é ela quem executa. Fora dele, o coletor local continua valendo.
|
|
764
|
+
fayzConnection ? /* @__PURE__ */ jsx(FayzRunLog, { connection: fayzConnection, eventKey: event.name, tenantId, enabled: message.enabled }) : /* @__PURE__ */ jsx(RunLog, { eventKey: event.name, tenantId, enabled: message.enabled })
|
|
765
|
+
) : tab === "preview" ? /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
|
|
766
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-border px-4 py-2.5", children: [
|
|
767
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Pr\xE9via" }),
|
|
768
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: ["desktop", "mobile"].map((d) => /* @__PURE__ */ jsx(
|
|
769
|
+
"button",
|
|
770
|
+
{
|
|
771
|
+
type: "button",
|
|
772
|
+
onClick: () => setDevice(d),
|
|
773
|
+
"aria-label": d === "desktop" ? "Computador" : "Celular",
|
|
774
|
+
className: `flex h-7 w-7 items-center justify-center rounded-md transition-colors ${device === d ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted/60"}`,
|
|
775
|
+
children: d === "desktop" ? /* @__PURE__ */ jsx(Monitor, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(Smartphone, { className: "h-4 w-4" })
|
|
776
|
+
},
|
|
777
|
+
d
|
|
778
|
+
)) })
|
|
779
|
+
] }),
|
|
780
|
+
/* @__PURE__ */ jsxs("div", { className: "bg-muted/30 p-6", children: [
|
|
781
|
+
/* @__PURE__ */ jsx(
|
|
782
|
+
"div",
|
|
783
|
+
{
|
|
784
|
+
className: `mx-auto rounded-lg border border-border bg-card shadow-sm transition-[max-width] ${device === "mobile" ? "max-w-[380px]" : hasSubject ? "max-w-[600px]" : "max-w-[420px]"}`,
|
|
785
|
+
children: hasSubject ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
786
|
+
/* @__PURE__ */ jsx("div", { className: "border-b border-border px-6 py-4 text-sm font-semibold", children: SAMPLE.store_name }),
|
|
787
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1 border-b border-border px-6 py-3", children: [
|
|
788
|
+
/* @__PURE__ */ jsx("p", { className: "text-[11px] uppercase tracking-wide text-muted-foreground", children: "Assunto" }),
|
|
789
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: previewSubject })
|
|
790
|
+
] }),
|
|
791
|
+
/* @__PURE__ */ jsx("div", { className: "whitespace-pre-wrap px-6 py-5 text-sm leading-relaxed", children: previewBody })
|
|
792
|
+
] }) : /* @__PURE__ */ jsx("div", { className: "p-4", children: /* @__PURE__ */ jsx("div", { className: "whitespace-pre-wrap rounded-2xl rounded-tl-sm bg-[#dcf8c6] px-3 py-2 text-sm leading-relaxed text-[#111b21]", children: previewBody }) })
|
|
793
|
+
}
|
|
794
|
+
),
|
|
795
|
+
/* @__PURE__ */ jsx("p", { className: "mt-4 text-center text-xs text-muted-foreground", children: "Dados de exemplo \u2014 nenhum registro real foi usado." })
|
|
796
|
+
] })
|
|
797
|
+
] }) : /* @__PURE__ */ jsxs(Card, { className: "space-y-4 p-4", children: [
|
|
798
|
+
hasSubject && /* @__PURE__ */ jsxs("div", { children: [
|
|
799
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "Assunto" }),
|
|
800
|
+
/* @__PURE__ */ jsx(
|
|
801
|
+
"input",
|
|
802
|
+
{
|
|
803
|
+
value: current.subject,
|
|
804
|
+
onChange: (e) => setDraft({ ...current, subject: e.target.value }),
|
|
805
|
+
className: "w-full rounded-input border border-input bg-card px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
806
|
+
}
|
|
807
|
+
)
|
|
808
|
+
] }),
|
|
809
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
810
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "Mensagem" }),
|
|
811
|
+
/* @__PURE__ */ jsx(
|
|
812
|
+
"textarea",
|
|
813
|
+
{
|
|
814
|
+
rows: 14,
|
|
815
|
+
value: current.body,
|
|
816
|
+
onChange: (e) => setDraft({ ...current, body: e.target.value }),
|
|
817
|
+
className: "w-full rounded-input border border-input bg-card px-3 py-2 font-mono text-xs leading-relaxed focus:outline-none focus:ring-2 focus:ring-ring"
|
|
818
|
+
}
|
|
819
|
+
)
|
|
820
|
+
] }),
|
|
821
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
822
|
+
/* @__PURE__ */ jsx("p", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: "Campos deste evento \u2014 o envio substitui pelo dado real" }),
|
|
823
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: message.placeholders.map((p) => /* @__PURE__ */ jsx("span", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground", children: p }, p)) })
|
|
824
|
+
] })
|
|
825
|
+
] })
|
|
104
826
|
] });
|
|
105
827
|
}
|
|
828
|
+
var TABLE = "plg_automations_templates";
|
|
829
|
+
function rowToTemplate(row) {
|
|
830
|
+
return {
|
|
831
|
+
id: row.id,
|
|
832
|
+
tenantId: row.tenant_id,
|
|
833
|
+
eventKey: row.event_key,
|
|
834
|
+
channel: row.channel ?? "email",
|
|
835
|
+
// NULL fica NULL: quem decide o padrão é o catálogo, não este mapeamento.
|
|
836
|
+
// Colapsar para `false` aqui desligaria em silêncio todo modelo cuja linha
|
|
837
|
+
// só existe porque o texto foi editado.
|
|
838
|
+
enabled: row.enabled ?? null,
|
|
839
|
+
// String vazia e NULL significam a mesma coisa — "sem personalização" —,
|
|
840
|
+
// senão um assunto apagado sairia em branco em vez de voltar ao padrão.
|
|
841
|
+
subject: row.subject || null,
|
|
842
|
+
body: row.body || null,
|
|
843
|
+
name: row.name || null,
|
|
844
|
+
updatedAt: row.updated_at ?? row.created_at
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function createSupabaseTemplateStore() {
|
|
848
|
+
const db = () => {
|
|
849
|
+
const client = getSupabaseClientOptional();
|
|
850
|
+
if (!client) throw new Error("@fayz-ai/plugin-automations: Supabase client not initialized");
|
|
851
|
+
return client;
|
|
852
|
+
};
|
|
853
|
+
return {
|
|
854
|
+
async list(tenantId) {
|
|
855
|
+
let q = db().from(TABLE).select("*");
|
|
856
|
+
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
857
|
+
const { data, error } = await q;
|
|
858
|
+
if (error) throw error;
|
|
859
|
+
return (data ?? []).map(rowToTemplate);
|
|
860
|
+
},
|
|
861
|
+
async upsert(input, tenantId) {
|
|
862
|
+
const payload = {
|
|
863
|
+
tenant_id: tenantId,
|
|
864
|
+
event_key: input.eventKey,
|
|
865
|
+
channel: input.channel
|
|
866
|
+
};
|
|
867
|
+
if (input.enabled !== void 0) payload.enabled = input.enabled;
|
|
868
|
+
if (input.subject !== void 0) payload.subject = input.subject;
|
|
869
|
+
if (input.body !== void 0) payload.body = input.body;
|
|
870
|
+
if (input.name !== void 0) payload.name = input.name;
|
|
871
|
+
const { data, error } = await db().from(TABLE).upsert(payload, { onConflict: "tenant_id,event_key,channel" }).select().single();
|
|
872
|
+
if (error) throw error;
|
|
873
|
+
return rowToTemplate(data);
|
|
874
|
+
},
|
|
875
|
+
async reset(eventKey, channel, tenantId) {
|
|
876
|
+
let q = db().from(TABLE).delete().eq("event_key", eventKey).eq("channel", channel);
|
|
877
|
+
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
878
|
+
const { error } = await q;
|
|
879
|
+
if (error) throw error;
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
}
|
|
106
883
|
|
|
107
884
|
// src/index.ts
|
|
108
885
|
function createAutomationsPlugin(options) {
|
|
109
|
-
const
|
|
110
|
-
|
|
886
|
+
const connection = resolveFayzAgentConnection();
|
|
887
|
+
const store = options?.store ?? (connection ? createFayzTemplateStore(connection) : createSupabaseTemplateStore());
|
|
888
|
+
startEventLog();
|
|
889
|
+
const inSettings = options?.placement === "settings";
|
|
890
|
+
const ListPage = () => React3.createElement(AutomationsList, { store });
|
|
891
|
+
ListPage.displayName = "AutomationsList";
|
|
892
|
+
const DetailPage = (props) => React3.createElement(AutomationDetailPage, {
|
|
893
|
+
...props,
|
|
894
|
+
store,
|
|
895
|
+
// "Back" must return you through the door you came in: opening the list
|
|
896
|
+
// inside settings must not drop you on a loose page that app's menu does
|
|
897
|
+
// not have.
|
|
898
|
+
backTo: inSettings ? "/settings/automations" : "/automations"
|
|
899
|
+
});
|
|
900
|
+
DetailPage.displayName = "AutomationDetail";
|
|
901
|
+
const READ = { feature: "automations", action: "read" };
|
|
111
902
|
return {
|
|
112
903
|
id: "automations",
|
|
113
|
-
name: options?.navLabel ?? "
|
|
904
|
+
name: options?.navLabel ?? "Automa\xE7\xF5es",
|
|
114
905
|
icon: "Zap",
|
|
115
|
-
version: "
|
|
906
|
+
version: "2.0.0",
|
|
116
907
|
scope: options?.scope ?? "universal",
|
|
117
908
|
verticalId: options?.verticalId,
|
|
118
909
|
defaultEnabled: true,
|
|
119
910
|
dependencies: [],
|
|
120
911
|
declaredFeatures: [{ id: "automations", label: "Automations", group: "Automate" }],
|
|
121
|
-
navigation: [
|
|
912
|
+
navigation: inSettings ? [] : [
|
|
122
913
|
{
|
|
123
914
|
section: options?.navSection ?? "main",
|
|
124
915
|
position: options?.navPosition ?? 6,
|
|
125
|
-
label: options?.navLabel ?? "
|
|
916
|
+
label: options?.navLabel ?? "Automa\xE7\xF5es",
|
|
126
917
|
route: "/automations",
|
|
127
918
|
icon: "Zap",
|
|
128
|
-
permission:
|
|
919
|
+
permission: READ,
|
|
920
|
+
indexChild: false
|
|
129
921
|
}
|
|
130
922
|
],
|
|
923
|
+
// The SAME list, served inside settings — not a second screen.
|
|
924
|
+
settings: inSettings ? [
|
|
925
|
+
{ id: "automations", label: options?.navLabel ?? "Automa\xE7\xF5es", icon: "Zap", component: ListPage, permission: READ }
|
|
926
|
+
] : [],
|
|
131
927
|
routes: [
|
|
132
|
-
{
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
928
|
+
{ path: "/automations", component: ListPage, permission: READ },
|
|
929
|
+
// One automation's detail. Off-menu: it is a row of the list. Its back
|
|
930
|
+
// link comes from `backTo`.
|
|
931
|
+
{ path: "/automations/:eventKey/:channel", component: DetailPage, permission: READ }
|
|
137
932
|
],
|
|
138
933
|
widgets: []
|
|
139
934
|
};
|
|
140
935
|
}
|
|
141
936
|
|
|
142
|
-
export { createAutomationsPlugin };
|
|
937
|
+
export { createAutomationsPlugin, createFayzTemplateStore, createSupabaseTemplateStore };
|
|
143
938
|
//# sourceMappingURL=index.js.map
|
|
144
939
|
//# sourceMappingURL=index.js.map
|