@fayz-ai/plugin-automations 0.10.2 → 0.12.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/data/fayz.d.ts.map +1 -1
- package/dist/data/payment-preflight.d.ts +23 -0
- package/dist/data/payment-preflight.d.ts.map +1 -0
- package/dist/data/provider-templates.d.ts +34 -0
- package/dist/data/provider-templates.d.ts.map +1 -0
- package/dist/data/templates.d.ts.map +1 -1
- package/dist/data/test-send.d.ts +18 -0
- package/dist/data/test-send.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1396 -749
- package/dist/index.js.map +1 -1
- package/dist/views/AutomationDetailPage.d.ts +26 -1
- package/dist/views/AutomationDetailPage.d.ts.map +1 -1
- package/dist/views/AutomationsList.d.ts +21 -1
- package/dist/views/AutomationsList.d.ts.map +1 -1
- package/dist/views/ProviderTemplateStatus.d.ts +35 -0
- package/dist/views/ProviderTemplateStatus.d.ts.map +1 -0
- package/dist/views/whatsapp-preview-bg.d.ts +2 -0
- package/dist/views/whatsapp-preview-bg.d.ts.map +1 -0
- package/package.json +7 -6
package/dist/index.js
CHANGED
|
@@ -1,154 +1,547 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import * as React4 from 'react';
|
|
2
|
+
import React4__default from 'react';
|
|
3
|
+
import { getSupabaseClientOptional, startEventLog, getMessageDefaults, normalizeMessageActions, usePluginRuntimeOptional, resolveMessages, resolveMessage, renderMessageTemplate, loadEventRuns, getActiveTenantId } from '@fayz-ai/core';
|
|
4
|
+
import { resolveFayzAgentConnection, registerRailPage, getFayzSupabaseClientOptional, useConnectors, useAgentSurface, PageActions, askAI, SettingsTabs, adminNavigateTo, RightRailPage, AskAIButton, useOrganizationStore, connectorRoute, ConnectorMark, ConnectorDeliveries } from '@fayz-ai/admin';
|
|
4
5
|
import * as LucideIcons from 'lucide-react';
|
|
5
|
-
import { Zap, MessageSquare, Smartphone,
|
|
6
|
-
import { PageTabs, Card, Switch, SubpageHeader, Button, Badge, Hint, HeaderTabs, toast } from '@fayz-ai/ui';
|
|
7
|
-
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
|
+
import { WandSparkles, Zap, Receipt, Megaphone, MessageSquare, Smartphone, Mail, ChevronRight, RotateCcw, Check, Clock, Monitor, RefreshCw, History, Send, AlertTriangle, CircleCheck, CircleSlash } from 'lucide-react';
|
|
7
|
+
import { PageHeaderActions, PageTabs, Card, WhatsAppIcon, Switch, SubpageHeader, Button, Badge, Hint, HeaderTabs, toast, PhoneField } from '@fayz-ai/ui';
|
|
8
8
|
import { createFayzAutomationsClient, FayzAutomationsError } from '@fayz-ai/sdk/automations';
|
|
9
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
9
10
|
|
|
10
11
|
// src/index.ts
|
|
12
|
+
var PREFLIGHT_FUNCTION = "tyxter-payment-preflight";
|
|
13
|
+
async function readReadiness() {
|
|
14
|
+
const supabase = getSupabaseClientOptional();
|
|
15
|
+
const tenantId = getActiveTenantId();
|
|
16
|
+
if (!supabase || !tenantId) return null;
|
|
17
|
+
const { data, error } = await supabase.functions.invoke(PREFLIGHT_FUNCTION, {
|
|
18
|
+
body: { action: "status", tenantId }
|
|
19
|
+
});
|
|
20
|
+
if (error) return null;
|
|
21
|
+
const payments = data?.payments;
|
|
22
|
+
return payments ?? null;
|
|
23
|
+
}
|
|
24
|
+
function canOfferPayment(readiness) {
|
|
25
|
+
return Boolean(readiness?.ready) && readiness?.mode !== null && readiness?.mode !== void 0;
|
|
26
|
+
}
|
|
27
|
+
function usePaymentReadiness() {
|
|
28
|
+
const [readiness, setReadiness] = React4.useState(null);
|
|
29
|
+
const [loading, setLoading] = React4.useState(true);
|
|
30
|
+
React4.useEffect(() => {
|
|
31
|
+
let cancelled = false;
|
|
32
|
+
void readReadiness().then((value) => {
|
|
33
|
+
if (!cancelled) setReadiness(value);
|
|
34
|
+
}).catch(() => {
|
|
35
|
+
}).finally(() => {
|
|
36
|
+
if (!cancelled) setLoading(false);
|
|
37
|
+
});
|
|
38
|
+
return () => {
|
|
39
|
+
cancelled = true;
|
|
40
|
+
};
|
|
41
|
+
}, []);
|
|
42
|
+
return { readiness, loading };
|
|
43
|
+
}
|
|
44
|
+
var SEND_FUNCTION = "messaging-send";
|
|
45
|
+
function readFunctionError(error, data) {
|
|
46
|
+
const payload = data ?? null;
|
|
47
|
+
if (payload?.error) return { ok: false, error: payload.error, message: payload.message };
|
|
48
|
+
const context = error?.context;
|
|
49
|
+
if (context?.error) return { ok: false, error: context.error, message: context.message };
|
|
50
|
+
return { ok: false, error: "send_failed", message: error instanceof Error ? error.message : void 0 };
|
|
51
|
+
}
|
|
52
|
+
async function sendTestMessage(input) {
|
|
53
|
+
const supabase = getSupabaseClientOptional();
|
|
54
|
+
const tenantId = getActiveTenantId();
|
|
55
|
+
if (!supabase || !tenantId) return { ok: false, error: "not_available" };
|
|
56
|
+
const { data, error } = await supabase.functions.invoke(SEND_FUNCTION, {
|
|
57
|
+
body: {
|
|
58
|
+
test: true,
|
|
59
|
+
tenantId,
|
|
60
|
+
channel: input.channel,
|
|
61
|
+
eventKey: input.eventKey,
|
|
62
|
+
to: input.to,
|
|
63
|
+
variables: input.variables,
|
|
64
|
+
subject: { type: "test", id: crypto.randomUUID() },
|
|
65
|
+
...input.template ? { template: input.template } : {}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
if (error || data?.error) return readFunctionError(error, data);
|
|
69
|
+
return { ok: true };
|
|
70
|
+
}
|
|
11
71
|
function useOrganizationStoreLike() {
|
|
12
72
|
return useOrganizationStore((s) => s.currentOrg?.id);
|
|
13
73
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
74
|
+
function toTemplate(automation) {
|
|
75
|
+
const eventKey = automation.triggerConfig?.eventKey;
|
|
76
|
+
if (!eventKey) return null;
|
|
77
|
+
const action = automation.actions?.[0];
|
|
78
|
+
return {
|
|
79
|
+
id: automation.id,
|
|
80
|
+
tenantId: automation.tenantId ?? "",
|
|
81
|
+
eventKey,
|
|
82
|
+
channel: automation.triggerConfig.sourceChannel ?? "email",
|
|
83
|
+
enabled: automation.enabled,
|
|
84
|
+
// Empty and absent mean the same — "no customisation", the catalogue default.
|
|
85
|
+
subject: action?.config?.subject || null,
|
|
86
|
+
body: action?.config?.body || null,
|
|
87
|
+
// The automation name belongs to the platform, not a user rename: the
|
|
88
|
+
// catalogue title still owns the screen.
|
|
89
|
+
name: null,
|
|
90
|
+
// The platform's automation action carries a subject and a body and nothing
|
|
91
|
+
// else, so there is nowhere for a customised button to live yet. NULL, not
|
|
92
|
+
// [], because the catalogue default is what still applies — an empty list
|
|
93
|
+
// would read as "this tenant removed the buttons".
|
|
94
|
+
actions: null,
|
|
95
|
+
updatedAt: automation.updatedAt
|
|
96
|
+
};
|
|
35
97
|
}
|
|
36
|
-
function
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
98
|
+
function catalogDefaultEnabled(eventKey, channel) {
|
|
99
|
+
const def = getMessageDefaults().find((d) => d.eventKey === eventKey);
|
|
100
|
+
return def?.defaultEnabled?.[channel] ?? false;
|
|
101
|
+
}
|
|
102
|
+
async function defaultSessionToken() {
|
|
103
|
+
const session = (await getFayzSupabaseClientOptional()?.auth.getSession())?.data.session;
|
|
104
|
+
return session?.access_token ?? null;
|
|
105
|
+
}
|
|
106
|
+
function createFayzTemplateStore(connection, deps = {}) {
|
|
107
|
+
const client = deps.client ?? createFayzAutomationsClient({
|
|
108
|
+
baseUrl: connection.apiBaseUrl,
|
|
109
|
+
projectId: connection.projectId,
|
|
110
|
+
publishableKey: connection.publishableKey
|
|
111
|
+
});
|
|
112
|
+
const getSessionToken = deps.getSessionToken ?? defaultSessionToken;
|
|
113
|
+
const ids = /* @__PURE__ */ new Map();
|
|
114
|
+
const keyOf = (eventKey, channel) => `${eventKey}:${channel}`;
|
|
115
|
+
async function refresh(tenantId) {
|
|
116
|
+
const automations = await client.list({ tenantId });
|
|
117
|
+
ids.clear();
|
|
118
|
+
const rows = [];
|
|
119
|
+
for (const automation of automations) {
|
|
120
|
+
const row = toTemplate(automation);
|
|
121
|
+
if (!row) continue;
|
|
122
|
+
ids.set(keyOf(row.eventKey, row.channel), automation.id);
|
|
123
|
+
rows.push(row);
|
|
50
124
|
}
|
|
51
|
-
|
|
125
|
+
return rows;
|
|
52
126
|
}
|
|
53
|
-
|
|
127
|
+
const LIST_TTL_MS = 3e4;
|
|
128
|
+
let cache = null;
|
|
129
|
+
function invalidate() {
|
|
130
|
+
cache = null;
|
|
131
|
+
}
|
|
132
|
+
function load(tenantId) {
|
|
133
|
+
if (cache && cache.tenantId === tenantId && Date.now() - cache.at < LIST_TTL_MS) return cache.promise;
|
|
134
|
+
const promise = refresh(tenantId);
|
|
135
|
+
promise.catch(() => {
|
|
136
|
+
if (cache?.promise === promise) cache = null;
|
|
137
|
+
});
|
|
138
|
+
cache = { tenantId, at: Date.now(), promise };
|
|
139
|
+
return promise;
|
|
140
|
+
}
|
|
141
|
+
async function requireToken() {
|
|
142
|
+
const token = await getSessionToken();
|
|
143
|
+
if (!token) throw new Error("Sess\xE3o expirada \u2014 entre de novo.");
|
|
144
|
+
return token;
|
|
145
|
+
}
|
|
146
|
+
async function resolveId(eventKey, channel, tenantId) {
|
|
147
|
+
const key = keyOf(eventKey, channel);
|
|
148
|
+
if (!ids.has(key)) await load(tenantId);
|
|
149
|
+
return ids.get(key);
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
async list(tenantId) {
|
|
153
|
+
if (!tenantId) return [];
|
|
154
|
+
return load(tenantId);
|
|
155
|
+
},
|
|
156
|
+
async upsert(input, tenantId) {
|
|
157
|
+
if (!tenantId) throw new Error("@fayz-ai/plugin-automations: tenantId is required");
|
|
158
|
+
const appSessionToken = await requireToken();
|
|
159
|
+
const key = keyOf(input.eventKey, input.channel);
|
|
160
|
+
let id = await resolveId(input.eventKey, input.channel, tenantId);
|
|
161
|
+
if (!id) {
|
|
162
|
+
const action = {};
|
|
163
|
+
if (input.subject != null) action.subject = input.subject;
|
|
164
|
+
if (input.body != null) action.body = input.body;
|
|
165
|
+
try {
|
|
166
|
+
const created = await client.create({
|
|
167
|
+
tenantId,
|
|
168
|
+
appSessionToken,
|
|
169
|
+
eventKey: input.eventKey,
|
|
170
|
+
sourceChannel: input.channel,
|
|
171
|
+
// The platform has no "inherit from catalogue": materialise the default.
|
|
172
|
+
enabled: input.enabled ?? catalogDefaultEnabled(input.eventKey, input.channel),
|
|
173
|
+
...Object.keys(action).length ? { action } : {}
|
|
174
|
+
});
|
|
175
|
+
ids.set(key, created.id);
|
|
176
|
+
invalidate();
|
|
177
|
+
const row2 = toTemplate(created);
|
|
178
|
+
if (!row2) throw new Error("@fayz-ai/plugin-automations: platform did not round-trip eventKey");
|
|
179
|
+
return row2;
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (!(err instanceof FayzAutomationsError) || err.status !== 409) throw err;
|
|
182
|
+
invalidate();
|
|
183
|
+
await refresh(tenantId);
|
|
184
|
+
id = ids.get(key);
|
|
185
|
+
if (!id) throw err;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const updated = await client.update({
|
|
189
|
+
id,
|
|
190
|
+
tenantId,
|
|
191
|
+
appSessionToken,
|
|
192
|
+
// Only what was sent: omitting has to mean "leave it alone", as in the local store.
|
|
193
|
+
...typeof input.enabled === "boolean" ? { enabled: input.enabled } : {},
|
|
194
|
+
...input.subject !== void 0 || input.body !== void 0 ? {
|
|
195
|
+
action: {
|
|
196
|
+
...input.subject !== void 0 ? { subject: input.subject } : {},
|
|
197
|
+
...input.body !== void 0 ? { body: input.body } : {}
|
|
198
|
+
}
|
|
199
|
+
} : {}
|
|
200
|
+
});
|
|
201
|
+
invalidate();
|
|
202
|
+
const row = toTemplate(updated);
|
|
203
|
+
if (!row) throw new Error("@fayz-ai/plugin-automations: platform did not round-trip eventKey");
|
|
204
|
+
return row;
|
|
205
|
+
},
|
|
206
|
+
async reset(eventKey, channel, tenantId) {
|
|
207
|
+
if (!tenantId) return;
|
|
208
|
+
const id = await resolveId(eventKey, channel, tenantId);
|
|
209
|
+
if (!id) return;
|
|
210
|
+
const appSessionToken = await requireToken();
|
|
211
|
+
await client.update({ id, tenantId, appSessionToken, action: { subject: null, body: null } });
|
|
212
|
+
invalidate();
|
|
213
|
+
}
|
|
214
|
+
};
|
|
54
215
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
href
|
|
67
|
-
},
|
|
68
|
-
`${group.connectorId}:${row.eventKey}`
|
|
69
|
-
))
|
|
70
|
-
] }, group.connectorId);
|
|
71
|
-
}) });
|
|
216
|
+
var cachedClient = null;
|
|
217
|
+
function getFayzAutomationsClient(connection) {
|
|
218
|
+
const key = `${connection.apiBaseUrl}|${connection.projectId}|${connection.publishableKey}`;
|
|
219
|
+
if (cachedClient?.key === key) return cachedClient.client;
|
|
220
|
+
const client = createFayzAutomationsClient({
|
|
221
|
+
baseUrl: connection.apiBaseUrl,
|
|
222
|
+
projectId: connection.projectId,
|
|
223
|
+
publishableKey: connection.publishableKey
|
|
224
|
+
});
|
|
225
|
+
cachedClient = { key, client };
|
|
226
|
+
return client;
|
|
72
227
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
228
|
+
var STATUS = {
|
|
229
|
+
sent: { label: "Enviado", hint: "A mensagem saiu.", variant: "success", Icon: CircleCheck },
|
|
230
|
+
recorded: {
|
|
231
|
+
label: "Disparou",
|
|
232
|
+
// Today's honest state: the trigger works, the send doesn't exist yet.
|
|
233
|
+
hint: "O gatilho disparou e ficou registrado. O envio autom\xE1tico ainda n\xE3o est\xE1 ligado neste ambiente.",
|
|
234
|
+
variant: "info",
|
|
235
|
+
Icon: History
|
|
236
|
+
},
|
|
237
|
+
skipped: { label: "Ignorado", hint: "O gatilho disparou, mas nada foi enviado \u2014 automa\xE7\xE3o desligada ou sem canal.", variant: "warning", Icon: CircleSlash },
|
|
238
|
+
failed: { label: "Falhou", hint: "A tentativa de envio quebrou. O erro est\xE1 na linha.", variant: "destructive", Icon: AlertTriangle }
|
|
239
|
+
};
|
|
240
|
+
function when(iso) {
|
|
241
|
+
const then = new Date(iso).getTime();
|
|
242
|
+
const diff = Date.now() - then;
|
|
243
|
+
const min = Math.round(diff / 6e4);
|
|
244
|
+
if (min < 1) return "agora";
|
|
245
|
+
if (min < 60) return `h\xE1 ${min} min`;
|
|
246
|
+
const h = Math.round(min / 60);
|
|
247
|
+
if (h < 24) return `h\xE1 ${h} h`;
|
|
248
|
+
return new Date(iso).toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
|
|
249
|
+
}
|
|
250
|
+
function RunRow({ run }) {
|
|
251
|
+
const [open, setOpen] = React4__default.useState(false);
|
|
252
|
+
const cfg = STATUS[run.status] ?? STATUS.recorded;
|
|
253
|
+
const { Icon } = cfg;
|
|
254
|
+
return /* @__PURE__ */ jsxs("div", { className: "border-b border-border last:border-b-0", children: [
|
|
80
255
|
/* @__PURE__ */ jsxs(
|
|
81
|
-
"
|
|
256
|
+
"button",
|
|
82
257
|
{
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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",
|
|
258
|
+
type: "button",
|
|
259
|
+
onClick: () => setOpen(!open),
|
|
260
|
+
className: "flex w-full items-center gap-3 px-4 py-2.5 text-left transition-colors hover:bg-muted/40",
|
|
93
261
|
children: [
|
|
94
|
-
/* @__PURE__ */ jsx(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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}` })
|
|
262
|
+
/* @__PURE__ */ jsx(ChevronRight, { className: `h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform ${open ? "rotate-90" : ""}` }),
|
|
263
|
+
/* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
|
|
264
|
+
/* @__PURE__ */ jsx(Badge, { variant: cfg.variant, children: cfg.label }),
|
|
265
|
+
/* @__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" }),
|
|
266
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: when(run.occurredAt) })
|
|
115
267
|
]
|
|
116
268
|
}
|
|
117
269
|
),
|
|
118
|
-
|
|
119
|
-
/* @__PURE__ */
|
|
120
|
-
|
|
121
|
-
{
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
) })
|
|
270
|
+
open && /* @__PURE__ */ jsxs("div", { className: "space-y-3 bg-muted/20 px-4 pb-4 pt-1", children: [
|
|
271
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: cfg.hint }),
|
|
272
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
273
|
+
/* @__PURE__ */ jsx("p", { className: "mb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: "Dados do disparo" }),
|
|
274
|
+
/* @__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) })
|
|
275
|
+
] }),
|
|
276
|
+
run.result && /* @__PURE__ */ jsxs("div", { children: [
|
|
277
|
+
/* @__PURE__ */ jsx("p", { className: "mb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: "Resultado" }),
|
|
278
|
+
/* @__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) })
|
|
279
|
+
] }),
|
|
280
|
+
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 })
|
|
142
281
|
] })
|
|
143
282
|
] });
|
|
144
283
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
284
|
+
function RunLog({ eventKey, tenantId, enabled }) {
|
|
285
|
+
const [runs, setRuns] = React4__default.useState(null);
|
|
286
|
+
const [loading, setLoading] = React4__default.useState(false);
|
|
287
|
+
const load = React4__default.useCallback(async () => {
|
|
288
|
+
setLoading(true);
|
|
289
|
+
setRuns(await loadEventRuns(eventKey, { tenantId, limit: 20 }));
|
|
290
|
+
setLoading(false);
|
|
291
|
+
}, [eventKey, tenantId]);
|
|
292
|
+
React4__default.useEffect(() => {
|
|
293
|
+
void load();
|
|
294
|
+
}, [load]);
|
|
295
|
+
return /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
|
|
296
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-border px-4 py-2.5", children: [
|
|
297
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
298
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Execu\xE7\xF5es recentes" }),
|
|
299
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Cada vez que este gatilho disparou nesta conta." })
|
|
300
|
+
] }),
|
|
301
|
+
/* @__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" : ""}` }) })
|
|
302
|
+
] }),
|
|
303
|
+
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: [
|
|
304
|
+
/* @__PURE__ */ jsx(History, { className: "h-7 w-7 text-muted-foreground/40", strokeWidth: 1.5 }),
|
|
305
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: "Nenhum disparo ainda" }),
|
|
306
|
+
/* @__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." })
|
|
307
|
+
] }) : /* @__PURE__ */ jsx("div", { children: runs.map((run) => /* @__PURE__ */ jsx(RunRow, { run }, run.id)) })
|
|
308
|
+
] });
|
|
309
|
+
}
|
|
310
|
+
var EXEC_STATUS = {
|
|
311
|
+
success: { label: "Executada", variant: "success", Icon: CircleCheck },
|
|
312
|
+
error: { label: "Falhou", variant: "destructive", Icon: AlertTriangle }
|
|
313
|
+
};
|
|
314
|
+
function ExecutionRow({ execution }) {
|
|
315
|
+
const cfg = EXEC_STATUS[execution.status] ?? { label: execution.status, variant: "info", Icon: History };
|
|
316
|
+
const { Icon } = cfg;
|
|
317
|
+
return /* @__PURE__ */ jsxs("div", { className: "border-b border-border px-4 py-2.5 last:border-b-0", children: [
|
|
318
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
319
|
+
/* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
|
|
320
|
+
/* @__PURE__ */ jsx(Badge, { variant: cfg.variant, children: cfg.label }),
|
|
321
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm text-muted-foreground", children: execution.automationName }),
|
|
322
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: when(execution.executedAt) })
|
|
323
|
+
] }),
|
|
324
|
+
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 })
|
|
325
|
+
] });
|
|
326
|
+
}
|
|
327
|
+
function FayzRunLog({ connection, eventKey, tenantId, enabled }) {
|
|
328
|
+
const [executions, setExecutions] = React4__default.useState(null);
|
|
329
|
+
const [loading, setLoading] = React4__default.useState(false);
|
|
330
|
+
const load = React4__default.useCallback(async () => {
|
|
331
|
+
if (!tenantId) return;
|
|
332
|
+
setLoading(true);
|
|
333
|
+
try {
|
|
334
|
+
setExecutions(await getFayzAutomationsClient(connection).listExecutions({ tenantId, eventKey, limit: 20 }));
|
|
335
|
+
} catch (err) {
|
|
336
|
+
console.warn("[automations] could not load platform executions", err);
|
|
337
|
+
setExecutions([]);
|
|
338
|
+
} finally {
|
|
339
|
+
setLoading(false);
|
|
340
|
+
}
|
|
341
|
+
}, [connection, eventKey, tenantId]);
|
|
342
|
+
React4__default.useEffect(() => {
|
|
343
|
+
void load();
|
|
344
|
+
}, [load]);
|
|
345
|
+
return /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
|
|
346
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-border px-4 py-2.5", children: [
|
|
347
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
348
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Execu\xE7\xF5es recentes" }),
|
|
349
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "O que a plataforma Fayz executou para este gatilho nesta conta." })
|
|
350
|
+
] }),
|
|
351
|
+
/* @__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" : ""}` }) })
|
|
352
|
+
] }),
|
|
353
|
+
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: [
|
|
354
|
+
/* @__PURE__ */ jsx(History, { className: "h-7 w-7 text-muted-foreground/40", strokeWidth: 1.5 }),
|
|
355
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: "Nenhuma execu\xE7\xE3o ainda" }),
|
|
356
|
+
/* @__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." })
|
|
357
|
+
] }) : /* @__PURE__ */ jsx("div", { children: executions.map((e) => /* @__PURE__ */ jsx(ExecutionRow, { execution: e }, e.id)) })
|
|
358
|
+
] });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/views/whatsapp-preview-bg.ts
|
|
362
|
+
var WHATSAPP_PREVIEW_BG = "https://assets.fayz.ai/connectors/whatsapp-background.jpg";
|
|
363
|
+
var PROVIDER_TEMPLATES_TABLE = "plg_automations_provider_templates";
|
|
364
|
+
function rowToProviderTemplate(row) {
|
|
365
|
+
return {
|
|
366
|
+
id: row.id,
|
|
367
|
+
tenantId: row.tenant_id ?? null,
|
|
368
|
+
eventKey: row.event_key,
|
|
369
|
+
channel: row.channel ?? "whatsapp",
|
|
370
|
+
provider: row.provider ?? "tyxter",
|
|
371
|
+
templateName: row.template_name ?? "",
|
|
372
|
+
status: row.status ?? "draft",
|
|
373
|
+
rejectionReason: row.rejection_reason || null,
|
|
374
|
+
updatedAt: row.updated_at ?? row.created_at ?? null
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
async function loadProviderTemplates() {
|
|
378
|
+
const client = getSupabaseClientOptional();
|
|
379
|
+
if (!client) return [];
|
|
380
|
+
try {
|
|
381
|
+
const { data, error } = await client.from(PROVIDER_TEMPLATES_TABLE).select("*");
|
|
382
|
+
if (error) throw error;
|
|
383
|
+
return (data ?? []).map(rowToProviderTemplate);
|
|
384
|
+
} catch (err) {
|
|
385
|
+
console.warn("[automations] could not load provider template status", err);
|
|
386
|
+
return [];
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function providerTemplateFor(rows, eventKey, channel) {
|
|
390
|
+
const matching = rows.filter((r) => r.eventKey === eventKey && r.channel === channel);
|
|
391
|
+
return {
|
|
392
|
+
own: matching.find((r) => r.tenantId !== null) ?? null,
|
|
393
|
+
fallback: matching.find((r) => r.tenantId === null) ?? null
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function providerApprovalFor(rows, eventKey, channel) {
|
|
397
|
+
if (channel !== "whatsapp") return null;
|
|
398
|
+
const { own, fallback } = providerTemplateFor(rows, eventKey, channel);
|
|
399
|
+
const row = own ?? fallback;
|
|
400
|
+
if (!row) return null;
|
|
401
|
+
const fallbackApproved = fallback?.status === "approved";
|
|
402
|
+
switch (row.status) {
|
|
403
|
+
case "approved":
|
|
404
|
+
return { tone: "ok", label: "Aprovado pela Meta" };
|
|
405
|
+
case "rejected":
|
|
406
|
+
return {
|
|
407
|
+
tone: fallbackApproved && own ? "waiting" : "error",
|
|
408
|
+
label: fallbackApproved && own ? "Seu texto foi recusado" : "Recusado pela Meta",
|
|
409
|
+
detail: row.rejectionReason ? `Motivo: ${row.rejectionReason}` : "A Meta n\xE3o disse o motivo."
|
|
410
|
+
};
|
|
411
|
+
case "paused":
|
|
412
|
+
return {
|
|
413
|
+
tone: "error",
|
|
414
|
+
label: "Pausado pela Meta",
|
|
415
|
+
detail: row.rejectionReason ?? "Costuma ser excesso de marca\xE7\xF5es de spam pelos clientes."
|
|
416
|
+
};
|
|
417
|
+
case "disabled":
|
|
418
|
+
return { tone: "error", label: "Desativado pela Meta", detail: row.rejectionReason ?? void 0 };
|
|
419
|
+
case "submitted":
|
|
420
|
+
return {
|
|
421
|
+
tone: "waiting",
|
|
422
|
+
label: "Aguardando aprova\xE7\xE3o da Meta",
|
|
423
|
+
detail: fallbackApproved && own ? "At\xE9 l\xE1 a mensagem sai com o texto padr\xE3o." : "A revis\xE3o costuma levar algumas horas."
|
|
424
|
+
};
|
|
425
|
+
default:
|
|
426
|
+
return { tone: "waiting", label: "Ainda n\xE3o enviado para aprova\xE7\xE3o" };
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
var TONE_VARIANT = {
|
|
430
|
+
ok: "success",
|
|
431
|
+
waiting: "warning",
|
|
432
|
+
error: "destructive"
|
|
433
|
+
};
|
|
434
|
+
function ProviderTemplateBadge({ approval, showDetail }) {
|
|
435
|
+
if (!approval) return null;
|
|
436
|
+
if (approval.tone === "ok" && !showDetail) return null;
|
|
437
|
+
return /* @__PURE__ */ jsxs("span", { className: "inline-flex flex-wrap items-center gap-1.5", children: [
|
|
438
|
+
/* @__PURE__ */ jsx(Badge, { variant: TONE_VARIANT[approval.tone], children: approval.label }),
|
|
439
|
+
showDetail && approval.detail && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: approval.detail })
|
|
440
|
+
] });
|
|
441
|
+
}
|
|
442
|
+
function useProviderTemplates() {
|
|
443
|
+
const [rows, setRows] = React4__default.useState([]);
|
|
444
|
+
React4__default.useEffect(() => {
|
|
445
|
+
let cancelled = false;
|
|
446
|
+
void loadProviderTemplates().then((data) => {
|
|
447
|
+
if (!cancelled) setRows(data);
|
|
448
|
+
});
|
|
449
|
+
return () => {
|
|
450
|
+
cancelled = true;
|
|
451
|
+
};
|
|
452
|
+
}, []);
|
|
453
|
+
return rows;
|
|
454
|
+
}
|
|
455
|
+
var ACTION_CATALOG = [
|
|
456
|
+
{ id: "confirm", label: "Confirma", kind: "reply", hint: "O cliente confirma o hor\xE1rio no toque." },
|
|
457
|
+
{ id: "cancel", label: "Cancela", kind: "reply", hint: "O cliente desmarca sem precisar ligar." },
|
|
458
|
+
{ id: "pay", label: "Pagar", kind: "payment", hint: "O cliente recebe o Pix na conversa." }
|
|
459
|
+
];
|
|
460
|
+
function nextActions(current, id, on) {
|
|
461
|
+
const kept = new Set(current.filter((a) => a.id !== id).map((a) => a.id));
|
|
462
|
+
if (on) kept.add(id);
|
|
463
|
+
const labels = new Map(current.map((a) => [a.id, a.label]));
|
|
464
|
+
return ACTION_CATALOG.filter((a) => kept.has(a.id)).map((a) => ({ id: a.id, label: labels.get(a.id) ?? a.label, kind: a.kind }));
|
|
465
|
+
}
|
|
466
|
+
var TEST_ERROR = {
|
|
467
|
+
template_not_approved: "O modelo ainda n\xE3o foi aprovado pela Meta \u2014 sincronize os modelos e tente de novo.",
|
|
468
|
+
no_sender: "Este app ainda n\xE3o tem um n\xFAmero de WhatsApp ativo. Veja Conversas \u2192 Tyxter.",
|
|
469
|
+
not_connected: "O WhatsApp ainda n\xE3o est\xE1 conectado neste app. Veja Conversas \u2192 Tyxter.",
|
|
470
|
+
contact_opted_out: "Este contato pediu para n\xE3o receber mensagens.",
|
|
471
|
+
invalid_recipient: "Esse destino n\xE3o parece v\xE1lido.",
|
|
472
|
+
not_a_member: "A sua sess\xE3o n\xE3o pertence a este neg\xF3cio.",
|
|
473
|
+
not_available: "O envio de teste n\xE3o est\xE1 dispon\xEDvel neste ambiente.",
|
|
474
|
+
email_not_configured: "O envio de e-mail n\xE3o est\xE1 configurado neste app.",
|
|
475
|
+
template_not_found: "Escreva a mensagem antes de testar.",
|
|
476
|
+
email_rejected: "O provedor de e-mail recusou a mensagem.",
|
|
477
|
+
email_credential_rejected: "A chave de e-mail do app foi recusada pelo provedor.",
|
|
478
|
+
email_rate_limited: "O provedor pediu uma pausa \u2014 tente de novo em instantes."
|
|
151
479
|
};
|
|
480
|
+
function TestSendStrip({ eventKey, channel, placeholders, template }) {
|
|
481
|
+
const isEmail = channel === "email";
|
|
482
|
+
const [recipient, setRecipient] = React4__default.useState("");
|
|
483
|
+
const [sending, setSending] = React4__default.useState(false);
|
|
484
|
+
const valid = isEmail ? /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(recipient.trim()) : recipient.replace(/\D/g, "").length >= 10;
|
|
485
|
+
const send = async () => {
|
|
486
|
+
setSending(true);
|
|
487
|
+
try {
|
|
488
|
+
const variables = Object.fromEntries(placeholders.map((p) => {
|
|
489
|
+
const key = p.replace(/[{}]/g, "").trim();
|
|
490
|
+
return [key, SAMPLE[key] ?? key];
|
|
491
|
+
}));
|
|
492
|
+
const result = await sendTestMessage({ eventKey, channel, to: recipient.trim(), variables, template });
|
|
493
|
+
if (result.ok) {
|
|
494
|
+
toast.success(isEmail ? "Teste enviado \u2014 confira a caixa de entrada desse e-mail" : "Teste enviado \u2014 confira o WhatsApp desse n\xFAmero");
|
|
495
|
+
} else {
|
|
496
|
+
toast.error(TEST_ERROR[result.error ?? ""] ?? result.message ?? "N\xE3o foi poss\xEDvel enviar o teste");
|
|
497
|
+
}
|
|
498
|
+
} finally {
|
|
499
|
+
setSending(false);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
return (
|
|
503
|
+
// Sem moldura própria: o strip vive dentro do Card da aba Testar, e uma
|
|
504
|
+
// borda aqui era o anel duplicado de sempre.
|
|
505
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap items-end gap-2", children: [
|
|
506
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
507
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: isEmail ? "E-mail que recebe o teste" : "N\xFAmero que recebe o teste" }),
|
|
508
|
+
isEmail ? /* @__PURE__ */ jsx(
|
|
509
|
+
"input",
|
|
510
|
+
{
|
|
511
|
+
type: "email",
|
|
512
|
+
value: recipient,
|
|
513
|
+
onChange: (e) => setRecipient(e.target.value),
|
|
514
|
+
placeholder: "voce@exemplo.com",
|
|
515
|
+
disabled: sending,
|
|
516
|
+
"aria-label": "E-mail que recebe o teste",
|
|
517
|
+
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"
|
|
518
|
+
}
|
|
519
|
+
) : /* @__PURE__ */ jsx(
|
|
520
|
+
PhoneField,
|
|
521
|
+
{
|
|
522
|
+
value: recipient,
|
|
523
|
+
onChange: setRecipient,
|
|
524
|
+
placeholder: "(21) 99999-9999",
|
|
525
|
+
disabled: sending,
|
|
526
|
+
"aria-label": "N\xFAmero que recebe o teste"
|
|
527
|
+
}
|
|
528
|
+
)
|
|
529
|
+
] }),
|
|
530
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: () => void send(), disabled: !valid || sending, children: [
|
|
531
|
+
/* @__PURE__ */ jsx(Send, { className: "mr-2 h-3.5 w-3.5" }),
|
|
532
|
+
sending ? "Enviando\u2026" : "Enviar teste"
|
|
533
|
+
] })
|
|
534
|
+
] })
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
var TABS = [
|
|
538
|
+
{ value: "preview", label: "Pr\xE9via" },
|
|
539
|
+
{ value: "code", label: "C\xF3digo" },
|
|
540
|
+
// O que já aconteceu. Fica ao lado do texto, e não numa tela à parte, porque
|
|
541
|
+
// é a mesma pergunta em dois tempos: o que vai sair, e o que saiu.
|
|
542
|
+
{ value: "runs", label: "Execu\xE7\xF5es" }
|
|
543
|
+
];
|
|
544
|
+
var TEST_TAB = { value: "test", label: "Testar" };
|
|
152
545
|
var CHANNEL_LABEL = {
|
|
153
546
|
email: "E-mail",
|
|
154
547
|
whatsapp: "WhatsApp",
|
|
@@ -156,440 +549,635 @@ var CHANNEL_LABEL = {
|
|
|
156
549
|
push: "Push",
|
|
157
550
|
webchat: "Chat"
|
|
158
551
|
};
|
|
159
|
-
var
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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."
|
|
552
|
+
var CHANNEL_ICON = {
|
|
553
|
+
email: Mail,
|
|
554
|
+
whatsapp: WhatsAppIcon,
|
|
555
|
+
sms: Smartphone,
|
|
556
|
+
push: MessageSquare,
|
|
557
|
+
webchat: MessageSquare
|
|
168
558
|
};
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
559
|
+
var WRITE_MESSAGE_TOOL = {
|
|
560
|
+
id: "automations.writeMessage",
|
|
561
|
+
name: "writeAutomationMessage",
|
|
562
|
+
description: "Writes the subject and body of the automation message the user has open, into the editor (it does NOT save \u2014 the user does). Use ONLY the placeholders listed in the surface state, spelled exactly, including the braces: an invented one renders as literal text in a message a customer reads. Match the channel: e-mail takes a subject and paragraphs; WhatsApp and SMS take no subject and few lines.",
|
|
563
|
+
icon: "MessageSquare",
|
|
564
|
+
mode: "read",
|
|
565
|
+
execution: { plane: "client" },
|
|
566
|
+
category: "Automations",
|
|
567
|
+
parameters: {
|
|
568
|
+
type: "object",
|
|
569
|
+
properties: {
|
|
570
|
+
subject: { type: "string", description: "Subject line. Leave empty for channels that have none." },
|
|
571
|
+
body: { type: "string", description: "Message body, with placeholders where they belong." }
|
|
572
|
+
},
|
|
573
|
+
required: ["body"]
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
var SAMPLE = {
|
|
577
|
+
store_name: "Sua Loja",
|
|
578
|
+
company_name: "Sua Empresa",
|
|
579
|
+
customer_name: "Maria Silva",
|
|
580
|
+
contact_name: "Maria Silva",
|
|
581
|
+
lead_name: "Maria Silva",
|
|
582
|
+
order_number: "1042",
|
|
583
|
+
order_total: "R$ 189,90",
|
|
584
|
+
order_items: "1 \xD7 Camiseta Cl\xE1ssica \u2014 R$ 89,90\n2 \xD7 Caneca Esmaltada \u2014 R$ 100,00",
|
|
585
|
+
tracking_code: "BR123456789BR",
|
|
586
|
+
cart_url: "https://sualoja.com.br/carrinho",
|
|
587
|
+
deal_name: "Proposta \u2014 Reforma da fachada",
|
|
588
|
+
deal_value: "R$ 12.400,00",
|
|
589
|
+
stage_name: "Negocia\xE7\xE3o",
|
|
590
|
+
owner_name: "Jo\xE3o Pereira",
|
|
591
|
+
appointment_date: "12/08/2026",
|
|
592
|
+
appointment_time: "14:30"
|
|
593
|
+
};
|
|
594
|
+
function AutomationDetailPage({ eventKey, channel, store, backTo = "/automations", embedded, onDone }) {
|
|
175
595
|
const tenantId = useOrganizationStoreLike();
|
|
176
596
|
const runtime = usePluginRuntimeOptional();
|
|
177
|
-
const
|
|
178
|
-
const [
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
const
|
|
597
|
+
const fayzConnection = React4__default.useMemo(() => resolveFayzAgentConnection(), []);
|
|
598
|
+
const [message, setMessage] = React4__default.useState(void 0);
|
|
599
|
+
const [tab, setTab] = React4__default.useState("preview");
|
|
600
|
+
const [device, setDevice] = React4__default.useState("desktop");
|
|
601
|
+
const [draft, setDraft] = React4__default.useState(null);
|
|
602
|
+
const [labelDrafts, setLabelDrafts] = React4__default.useState({});
|
|
603
|
+
const [saving, setSaving] = React4__default.useState(false);
|
|
604
|
+
const providerTemplates = useProviderTemplates();
|
|
605
|
+
const { readiness, loading: readinessLoading } = usePaymentReadiness();
|
|
606
|
+
const back = React4__default.useCallback(
|
|
607
|
+
() => onDone ? onDone() : adminNavigateTo(backTo),
|
|
608
|
+
[onDone, backTo]
|
|
609
|
+
);
|
|
610
|
+
const surfaceRef = React4__default.useRef({ message, draft });
|
|
611
|
+
surfaceRef.current = { message, draft };
|
|
612
|
+
const agentSurface = React4__default.useMemo(() => ({
|
|
613
|
+
id: "automations.message",
|
|
614
|
+
pluginId: "automations",
|
|
615
|
+
instructions: "The user is editing one automation message. `placeholders` in the surface state are the ONLY variables that resolve \u2014 copy them verbatim, braces included. Call writeAutomationMessage once with the whole text; the editor fills and the user reviews before saving.",
|
|
616
|
+
describe: () => {
|
|
617
|
+
const o = surfaceRef.current;
|
|
618
|
+
if (!o.message) return { title: "Automa\xE7\xE3o" };
|
|
619
|
+
const current2 = o.draft ?? { subject: o.message.subject, body: o.message.body };
|
|
620
|
+
return {
|
|
621
|
+
title: `Automa\xE7\xE3o \u2014 ${o.message.event?.title ?? o.message.event?.name ?? ""}`,
|
|
622
|
+
state: {
|
|
623
|
+
event: o.message.event?.name ?? null,
|
|
624
|
+
channel: o.message.channel,
|
|
625
|
+
hasSubject: o.message.channel === "email",
|
|
626
|
+
placeholders: Object.keys(SAMPLE).map((key) => `{{${key}}}`),
|
|
627
|
+
currentSubject: current2.subject,
|
|
628
|
+
currentBody: current2.body
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
},
|
|
632
|
+
tools: [{
|
|
633
|
+
tool: WRITE_MESSAGE_TOOL,
|
|
634
|
+
execute: (args) => {
|
|
635
|
+
const o = surfaceRef.current;
|
|
636
|
+
if (!o.message) return { ok: false, error: "no_message_open" };
|
|
637
|
+
const current2 = o.draft ?? { subject: o.message.subject, body: o.message.body };
|
|
638
|
+
const body = String(args.body ?? "").trim();
|
|
639
|
+
if (!body) return { ok: false, error: "empty_body" };
|
|
640
|
+
const subject = args.subject === void 0 ? current2.subject : String(args.subject);
|
|
641
|
+
setDraft({ subject, body });
|
|
642
|
+
setTab("code");
|
|
643
|
+
return { ok: true, filled: true, saved: false, note: "Draft filled \u2014 the user still has to press Save." };
|
|
644
|
+
}
|
|
645
|
+
}]
|
|
646
|
+
}), []);
|
|
647
|
+
useAgentSurface(agentSurface);
|
|
648
|
+
const reload = React4__default.useCallback(async () => {
|
|
649
|
+
if (!eventKey || !channel) {
|
|
650
|
+
setMessage(null);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
182
653
|
let overrides = [];
|
|
183
654
|
try {
|
|
184
655
|
overrides = await store.list(tenantId);
|
|
185
656
|
} catch (err) {
|
|
186
|
-
console.warn("[automations] could not load
|
|
657
|
+
console.warn("[automations] could not load overrides; showing defaults", err);
|
|
187
658
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
659
|
+
const found = resolveMessage(decodeURIComponent(eventKey), channel, overrides, runtime?.events ?? []);
|
|
660
|
+
setMessage(found);
|
|
661
|
+
if (found) setDraft({ subject: found.subject, body: found.body });
|
|
662
|
+
}, [eventKey, channel, store, tenantId, runtime]);
|
|
663
|
+
React4__default.useEffect(() => {
|
|
191
664
|
void reload();
|
|
192
665
|
}, [reload]);
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
console.warn("[automations] toggle failed", err);
|
|
200
|
-
toast.error("N\xE3o foi poss\xEDvel salvar");
|
|
201
|
-
await reload();
|
|
666
|
+
const header = embedded ? null : /* @__PURE__ */ jsx(
|
|
667
|
+
SubpageHeader,
|
|
668
|
+
{
|
|
669
|
+
title: message === void 0 ? "" : message?.override?.name ?? message?.event.title ?? message?.event.name ?? "Automa\xE7\xE3o",
|
|
670
|
+
parentLabel: "Automa\xE7\xF5es",
|
|
671
|
+
onBack: back
|
|
202
672
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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);
|
|
673
|
+
);
|
|
674
|
+
if (message === void 0) {
|
|
675
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
676
|
+
header,
|
|
677
|
+
/* @__PURE__ */ jsx("div", { className: "h-8 w-64 animate-pulse rounded bg-muted" }),
|
|
678
|
+
/* @__PURE__ */ jsx(Card, { className: "h-96 animate-pulse" })
|
|
679
|
+
] });
|
|
216
680
|
}
|
|
217
|
-
|
|
681
|
+
if (!message) {
|
|
682
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
683
|
+
header,
|
|
684
|
+
/* @__PURE__ */ jsxs(Card, { className: "flex flex-col items-center gap-2 p-12 text-center", children: [
|
|
685
|
+
/* @__PURE__ */ jsx(Mail, { className: "h-9 w-9 text-muted-foreground/40", strokeWidth: 1.5 }),
|
|
686
|
+
/* @__PURE__ */ jsx("p", { className: "font-semibold", children: "Automa\xE7\xE3o n\xE3o encontrada" }),
|
|
687
|
+
/* @__PURE__ */ jsxs("p", { className: "max-w-sm text-sm text-muted-foreground", children: [
|
|
688
|
+
"O evento ",
|
|
689
|
+
/* @__PURE__ */ jsx("code", { className: "font-mono", children: eventKey }),
|
|
690
|
+
" n\xE3o est\xE1 no cat\xE1logo \u2014 o m\xF3dulo que o registra pode n\xE3o estar instalado nesta conta."
|
|
691
|
+
] }),
|
|
692
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", className: "mt-2", onClick: back, children: "Voltar para Automa\xE7\xF5es" })
|
|
693
|
+
] })
|
|
694
|
+
] });
|
|
695
|
+
}
|
|
696
|
+
const event = message.event;
|
|
697
|
+
const fallback = { subject: message.override ? void 0 : message.subject, body: message.body };
|
|
698
|
+
const ChannelIcon = CHANNEL_ICON[message.channel] ?? Zap;
|
|
699
|
+
const isWhatsapp = message.channel === "whatsapp";
|
|
700
|
+
const current = draft ?? { subject: message.subject, body: message.body };
|
|
701
|
+
const dirty = current.subject !== message.subject || current.body !== message.body;
|
|
702
|
+
const hasSubject = message.channel === "email";
|
|
703
|
+
const patch = async (input) => {
|
|
704
|
+
setSaving(true);
|
|
705
|
+
try {
|
|
706
|
+
await store.upsert(input, tenantId);
|
|
707
|
+
await reload();
|
|
708
|
+
} catch (err) {
|
|
709
|
+
console.warn("[automations] save failed", err);
|
|
710
|
+
toast.error("N\xE3o foi poss\xEDvel salvar");
|
|
711
|
+
} finally {
|
|
712
|
+
setSaving(false);
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
const save = () => void patch({
|
|
716
|
+
eventKey: event.name,
|
|
717
|
+
channel: message.channel,
|
|
718
|
+
// Igual ao padrão volta a ser "sem personalização", não uma cópia.
|
|
719
|
+
subject: current.subject.trim() === (fallback?.subject ?? "") ? null : current.subject.trim() || null,
|
|
720
|
+
body: current.body.trim() === fallback?.body ? null : current.body.trim() || null
|
|
721
|
+
});
|
|
722
|
+
const reset = async () => {
|
|
723
|
+
setDraft({ subject: fallback?.subject ?? "", body: fallback?.body ?? "" });
|
|
724
|
+
setSaving(true);
|
|
725
|
+
try {
|
|
726
|
+
await store.reset(event.name, message.channel, tenantId);
|
|
727
|
+
await reload();
|
|
728
|
+
toast.success("Texto restaurado");
|
|
729
|
+
} catch (err) {
|
|
730
|
+
console.warn("[automations] reset failed", err);
|
|
731
|
+
toast.error("N\xE3o foi poss\xEDvel restaurar");
|
|
732
|
+
} finally {
|
|
733
|
+
setSaving(false);
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
const previewSubject = renderMessageTemplate(current.subject, SAMPLE);
|
|
737
|
+
const previewBody = renderMessageTemplate(current.body, SAMPLE);
|
|
738
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-5", children: [
|
|
739
|
+
header,
|
|
740
|
+
/* @__PURE__ */ jsxs("div", { className: embedded ? "space-y-3" : "flex flex-wrap items-start gap-3", children: [
|
|
741
|
+
/* @__PURE__ */ jsxs("div", { className: embedded ? "flex items-start gap-3" : "contents", children: [
|
|
742
|
+
/* @__PURE__ */ jsx(
|
|
743
|
+
"span",
|
|
744
|
+
{
|
|
745
|
+
className: `flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border ${isWhatsapp ? "border-[#25D366]/30 bg-[#25D366]/10 text-[#1faf55]" : "border-border text-muted-foreground"}`,
|
|
746
|
+
children: /* @__PURE__ */ jsx(ChannelIcon, { className: "h-4 w-4" })
|
|
747
|
+
}
|
|
748
|
+
),
|
|
749
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
750
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
751
|
+
/* @__PURE__ */ jsx(Badge, { variant: "outline", children: CHANNEL_LABEL[message.channel] }),
|
|
752
|
+
/* @__PURE__ */ jsx(Badge, { variant: "outline", children: message.kind === "marketing" ? "Marketing" : "Transacional" }),
|
|
753
|
+
message.customized && /* @__PURE__ */ jsx(Badge, { variant: "info", children: "Personalizado" })
|
|
754
|
+
] }),
|
|
755
|
+
/* @__PURE__ */ jsx("p", { className: "mt-0.5 text-sm text-muted-foreground", children: event.description }),
|
|
756
|
+
/* @__PURE__ */ jsx("div", { className: "mt-1.5", children: /* @__PURE__ */ jsx(
|
|
757
|
+
ProviderTemplateBadge,
|
|
758
|
+
{
|
|
759
|
+
approval: providerApprovalFor(providerTemplates, event.name, message.channel),
|
|
760
|
+
showDetail: true
|
|
761
|
+
}
|
|
762
|
+
) })
|
|
763
|
+
] })
|
|
764
|
+
] }),
|
|
765
|
+
/* @__PURE__ */ jsxs("div", { className: embedded ? "flex items-center gap-2" : "flex shrink-0 flex-wrap items-center gap-2", children: [
|
|
766
|
+
/* @__PURE__ */ jsx(
|
|
767
|
+
Switch,
|
|
768
|
+
{
|
|
769
|
+
size: "md",
|
|
770
|
+
checked: message.enabled,
|
|
771
|
+
onCheckedChange: (next) => void patch({ eventKey: event.name, channel: message.channel, enabled: next }),
|
|
772
|
+
disabled: saving,
|
|
773
|
+
label: message.enabled ? "Ativa" : "Inativa"
|
|
774
|
+
}
|
|
775
|
+
),
|
|
776
|
+
/* @__PURE__ */ jsxs("div", { className: embedded ? "ml-auto flex items-center gap-2" : "contents", children: [
|
|
777
|
+
/* @__PURE__ */ jsx(Hint, { label: "Descarta o texto personalizado e volta ao padr\xE3o do produto.", children: /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsxs(
|
|
778
|
+
Button,
|
|
779
|
+
{
|
|
780
|
+
variant: embedded ? "ghost" : "outline",
|
|
781
|
+
size: embedded ? "icon" : void 0,
|
|
782
|
+
onClick: () => void reset(),
|
|
783
|
+
disabled: !message.customized || saving,
|
|
784
|
+
"aria-label": embedded ? "Restaurar padr\xE3o" : void 0,
|
|
785
|
+
children: [
|
|
786
|
+
/* @__PURE__ */ jsx(RotateCcw, { className: embedded ? "h-4 w-4" : "mr-2 h-4 w-4" }),
|
|
787
|
+
!embedded && "Restaurar padr\xE3o"
|
|
788
|
+
]
|
|
789
|
+
}
|
|
790
|
+
) }) }),
|
|
791
|
+
/* @__PURE__ */ jsx(
|
|
792
|
+
AskAIButton,
|
|
793
|
+
{
|
|
794
|
+
variant: embedded ? "ghost" : "outline",
|
|
795
|
+
label: embedded ? "" : void 0,
|
|
796
|
+
hint: "Descreva o tom da mensagem\u2026",
|
|
797
|
+
intro: {
|
|
798
|
+
role: "setup",
|
|
799
|
+
persona: "Redator das automa\xE7\xF5es",
|
|
800
|
+
pitch: "Diga como esta mensagem deve soar.",
|
|
801
|
+
description: "Escrevo o assunto e o corpo desta automa\xE7\xE3o usando as vari\xE1veis que ela tem \xE0 disposi\xE7\xE3o. O texto cai no editor ao lado para voc\xEA revisar \u2014 nada \xE9 salvo sem voc\xEA.",
|
|
802
|
+
suggestions: [
|
|
803
|
+
{ label: "Mais curta e direta" },
|
|
804
|
+
{ label: "Mais calorosa, com o nome do cliente" },
|
|
805
|
+
{ label: "Formal, para um p\xFAblico corporativo" }
|
|
806
|
+
]
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
),
|
|
810
|
+
dirty || saving ? /* @__PURE__ */ jsx(Button, { onClick: save, disabled: saving, children: saving ? "Salvando\u2026" : "Salvar" }) : /* @__PURE__ */ jsxs("span", { className: "inline-flex h-9 items-center gap-1 px-2 text-xs font-medium text-muted-foreground", children: [
|
|
811
|
+
/* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }),
|
|
812
|
+
"Salvo"
|
|
813
|
+
] })
|
|
814
|
+
] })
|
|
815
|
+
] })
|
|
816
|
+
] }),
|
|
218
817
|
/* @__PURE__ */ jsx(
|
|
219
|
-
|
|
818
|
+
HeaderTabs,
|
|
220
819
|
{
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
"aria-label": "Automa\xE7\xF5es",
|
|
226
|
-
actions: messages !== null && /* @__PURE__ */ jsxs("span", { className: "text-sm text-muted-foreground", children: [
|
|
227
|
-
activeCount,
|
|
228
|
-
" de ",
|
|
229
|
-
totalCount,
|
|
230
|
-
" automa\xE7\xF5es ativas"
|
|
231
|
-
] })
|
|
820
|
+
tabs: isWhatsapp || message.channel === "email" ? [...TABS, TEST_TAB] : TABS,
|
|
821
|
+
value: tab,
|
|
822
|
+
onChange: setTab,
|
|
823
|
+
"aria-label": "Visualiza\xE7\xE3o"
|
|
232
824
|
}
|
|
233
825
|
),
|
|
234
|
-
/* @__PURE__ */
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
/* @__PURE__ */
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
826
|
+
/* @__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: [
|
|
827
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
|
|
828
|
+
/* @__PURE__ */ jsx(Zap, { className: "h-3.5 w-3.5" }),
|
|
829
|
+
"Gatilho:",
|
|
830
|
+
/* @__PURE__ */ jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-[11px] text-foreground", children: event.name })
|
|
831
|
+
] }),
|
|
832
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
|
|
833
|
+
/* @__PURE__ */ jsx(Clock, { className: "h-3.5 w-3.5" }),
|
|
834
|
+
event.trigger
|
|
835
|
+
] }),
|
|
836
|
+
hasSubject && current.subject && /* @__PURE__ */ jsxs("span", { className: "min-w-0 truncate", children: [
|
|
837
|
+
"Assunto: ",
|
|
838
|
+
current.subject
|
|
839
|
+
] })
|
|
840
|
+
] }),
|
|
841
|
+
tab === "runs" ? (
|
|
842
|
+
// Atrás de um projeto Fayz o histórico verdadeiro é o da plataforma —
|
|
843
|
+
// é ela quem executa. Fora dele, o coletor local continua valendo.
|
|
844
|
+
fayzConnection ? /* @__PURE__ */ jsx(FayzRunLog, { connection: fayzConnection, eventKey: event.name, tenantId, enabled: message.enabled }) : /* @__PURE__ */ jsx(RunLog, { eventKey: event.name, tenantId, enabled: message.enabled })
|
|
845
|
+
) : tab === "preview" ? /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
|
|
846
|
+
!embedded && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-border px-4 py-2.5", children: [
|
|
847
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Pr\xE9via" }),
|
|
848
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: ["desktop", "mobile"].map((d) => /* @__PURE__ */ jsx(
|
|
849
|
+
"button",
|
|
250
850
|
{
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
/* @__PURE__ */ jsx("p", { className: "text-
|
|
273
|
-
/* @__PURE__ */ jsxs("div", { className: "mt-
|
|
274
|
-
/* @__PURE__ */
|
|
275
|
-
|
|
276
|
-
|
|
851
|
+
type: "button",
|
|
852
|
+
onClick: () => setDevice(d),
|
|
853
|
+
"aria-label": d === "desktop" ? "Computador" : "Celular",
|
|
854
|
+
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"}`,
|
|
855
|
+
children: d === "desktop" ? /* @__PURE__ */ jsx(Monitor, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(Smartphone, { className: "h-4 w-4" })
|
|
856
|
+
},
|
|
857
|
+
d
|
|
858
|
+
)) })
|
|
859
|
+
] }),
|
|
860
|
+
/* @__PURE__ */ jsxs("div", { className: embedded ? "bg-muted/30 p-4" : "bg-muted/30 p-6", children: [
|
|
861
|
+
/* @__PURE__ */ jsx(
|
|
862
|
+
"div",
|
|
863
|
+
{
|
|
864
|
+
className: `mx-auto overflow-hidden rounded-card bg-card shadow-sm transition-[max-width] ${device === "mobile" ? "max-w-[380px]" : hasSubject ? "max-w-[600px]" : "max-w-[420px]"}`,
|
|
865
|
+
children: hasSubject ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
866
|
+
/* @__PURE__ */ jsxs("div", { "aria-hidden": true, className: "flex items-center gap-1.5 border-b border-border bg-muted/40 px-4 py-2.5", children: [
|
|
867
|
+
/* @__PURE__ */ jsx("span", { className: "h-3 w-3 rounded-full bg-[#ff5f57]" }),
|
|
868
|
+
/* @__PURE__ */ jsx("span", { className: "h-3 w-3 rounded-full bg-[#febc2e]" }),
|
|
869
|
+
/* @__PURE__ */ jsx("span", { className: "h-3 w-3 rounded-full bg-[#28c840]" })
|
|
870
|
+
] }),
|
|
871
|
+
/* @__PURE__ */ jsxs("div", { className: "px-6 pb-4 pt-5", children: [
|
|
872
|
+
/* @__PURE__ */ jsx("p", { className: "text-base font-semibold leading-snug", children: previewSubject }),
|
|
873
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-4 flex items-center gap-3", children: [
|
|
874
|
+
/* @__PURE__ */ jsx("span", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/10 text-sm font-semibold text-primary", children: (SAMPLE.store_name ?? "S").charAt(0) }),
|
|
875
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
876
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-semibold", children: SAMPLE.store_name }),
|
|
877
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "para mim" })
|
|
277
878
|
] }),
|
|
278
|
-
|
|
279
|
-
"Assunto: ",
|
|
280
|
-
m.subject
|
|
281
|
-
] })
|
|
879
|
+
/* @__PURE__ */ jsx("span", { className: "ml-auto shrink-0 text-xs text-muted-foreground", children: "09:41" })
|
|
282
880
|
] })
|
|
283
881
|
] }),
|
|
284
|
-
/* @__PURE__ */
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
882
|
+
/* @__PURE__ */ jsx("div", { className: "whitespace-pre-wrap border-t border-border px-6 py-5 text-sm leading-relaxed", children: previewBody })
|
|
883
|
+
] }) : /* @__PURE__ */ jsxs(
|
|
884
|
+
"div",
|
|
885
|
+
{
|
|
886
|
+
className: "p-4",
|
|
887
|
+
style: isWhatsapp ? { backgroundImage: `url(${WHATSAPP_PREVIEW_BG})`, backgroundSize: "cover", backgroundPosition: "center" } : void 0,
|
|
888
|
+
children: [
|
|
889
|
+
/* @__PURE__ */ jsx("div", { className: "whitespace-pre-wrap rounded-2xl rounded-tl-sm bg-[#dcf8c6] px-3 py-2 text-sm leading-relaxed text-[#111b21] shadow-sm", children: previewBody }),
|
|
890
|
+
message.actions.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-1 space-y-1", children: message.actions.map((action) => /* @__PURE__ */ jsx(
|
|
891
|
+
"div",
|
|
892
|
+
{
|
|
893
|
+
className: "rounded-lg bg-white px-3 py-1.5 text-center text-sm font-medium text-[#1e7fd6] shadow-sm",
|
|
894
|
+
children: action.label
|
|
895
|
+
},
|
|
896
|
+
action.id
|
|
897
|
+
)) })
|
|
898
|
+
]
|
|
899
|
+
}
|
|
900
|
+
)
|
|
901
|
+
}
|
|
902
|
+
),
|
|
903
|
+
/* @__PURE__ */ jsx("p", { className: "mt-4 text-center text-xs text-muted-foreground", children: "Dados de exemplo \u2014 nenhum registro real foi usado." })
|
|
904
|
+
] })
|
|
905
|
+
] }) : tab === "test" ? /* @__PURE__ */ jsxs(Card, { className: "p-4", children: [
|
|
906
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: isWhatsapp ? "Envio real, pelo mesmo caminho da automa\xE7\xE3o \u2014 modelo aprovado, n\xFAmero ativo e entrega de verdade, com os dados de exemplo." : "Envio real, pelo mesmo caminho da automa\xE7\xE3o \u2014 vai o texto que est\xE1 na tela, salvo ou n\xE3o, com os dados de exemplo." }),
|
|
907
|
+
/* @__PURE__ */ jsx(
|
|
908
|
+
TestSendStrip,
|
|
909
|
+
{
|
|
910
|
+
eventKey: event.name,
|
|
911
|
+
channel: message.channel,
|
|
912
|
+
placeholders: message.placeholders,
|
|
913
|
+
template: message.channel === "email" ? { subject: current.subject, body: current.body } : void 0
|
|
914
|
+
}
|
|
915
|
+
)
|
|
916
|
+
] }) : /* @__PURE__ */ jsxs(Card, { className: "space-y-4 p-4", children: [
|
|
917
|
+
hasSubject && /* @__PURE__ */ jsxs("div", { children: [
|
|
918
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "Assunto" }),
|
|
919
|
+
/* @__PURE__ */ jsx(
|
|
920
|
+
"input",
|
|
921
|
+
{
|
|
922
|
+
value: current.subject,
|
|
923
|
+
onChange: (e) => setDraft({ ...current, subject: e.target.value }),
|
|
924
|
+
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"
|
|
925
|
+
}
|
|
926
|
+
)
|
|
927
|
+
] }),
|
|
928
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
929
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "Mensagem" }),
|
|
930
|
+
/* @__PURE__ */ jsx(
|
|
931
|
+
"textarea",
|
|
932
|
+
{
|
|
933
|
+
rows: 14,
|
|
934
|
+
value: current.body,
|
|
935
|
+
onChange: (e) => setDraft({ ...current, body: e.target.value }),
|
|
936
|
+
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"
|
|
937
|
+
}
|
|
938
|
+
)
|
|
939
|
+
] }),
|
|
940
|
+
message.channel === "whatsapp" && /* @__PURE__ */ jsxs("div", { children: [
|
|
941
|
+
/* @__PURE__ */ jsx("p", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: "Bot\xF5es \u2014 o cliente responde no toque, sem digitar" }),
|
|
942
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-2", children: ACTION_CATALOG.map((action) => {
|
|
943
|
+
const active = message.actions.find((a) => a.id === action.id);
|
|
944
|
+
const on = Boolean(active);
|
|
945
|
+
const gated = action.id === "pay" && !canOfferPayment(readiness);
|
|
946
|
+
const blocked = gated && !readinessLoading;
|
|
947
|
+
const labelValue = labelDrafts[action.id] ?? active?.label ?? action.label;
|
|
948
|
+
const commitLabel = () => {
|
|
949
|
+
const next = labelValue.trim();
|
|
950
|
+
setLabelDrafts((d) => {
|
|
951
|
+
const { [action.id]: _, ...rest } = d;
|
|
952
|
+
return rest;
|
|
953
|
+
});
|
|
954
|
+
if (!active || !next || next === active.label) return;
|
|
955
|
+
void patch({
|
|
956
|
+
eventKey: event.name,
|
|
957
|
+
channel: message.channel,
|
|
958
|
+
actions: message.actions.map((a) => a.id === action.id ? { ...a, label: next } : a)
|
|
959
|
+
});
|
|
960
|
+
};
|
|
961
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1", children: [
|
|
962
|
+
/* @__PURE__ */ jsx(
|
|
963
|
+
Switch,
|
|
964
|
+
{
|
|
965
|
+
size: "md",
|
|
966
|
+
checked: on,
|
|
967
|
+
disabled: saving || gated,
|
|
968
|
+
onCheckedChange: (next) => void patch({
|
|
969
|
+
eventKey: event.name,
|
|
970
|
+
channel: message.channel,
|
|
971
|
+
actions: nextActions(message.actions, action.id, next)
|
|
972
|
+
}),
|
|
973
|
+
label: on ? "" : action.label
|
|
974
|
+
}
|
|
975
|
+
),
|
|
976
|
+
on && // O texto do botão como a cliente vê. 25 caracteres é o
|
|
977
|
+
// teto da Meta para QUICK_REPLY; mudar o texto de um
|
|
978
|
+
// modelo já aprovado dispara nova aprovação — a
|
|
979
|
+
// pastilha de status acima conta essa história.
|
|
980
|
+
/* @__PURE__ */ jsx(
|
|
981
|
+
"input",
|
|
982
|
+
{
|
|
983
|
+
value: labelValue,
|
|
984
|
+
maxLength: 25,
|
|
985
|
+
disabled: saving,
|
|
986
|
+
"aria-label": `Texto do bot\xE3o ${action.label}`,
|
|
987
|
+
onChange: (e) => setLabelDrafts((d) => ({ ...d, [action.id]: e.target.value })),
|
|
988
|
+
onBlur: commitLabel,
|
|
989
|
+
onKeyDown: (e) => {
|
|
990
|
+
if (e.key === "Enter") e.target.blur();
|
|
991
|
+
},
|
|
992
|
+
className: "w-36 rounded-input border border-input bg-card px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
993
|
+
}
|
|
994
|
+
),
|
|
995
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: blocked ? "Para cobrar por aqui, configure a conta de pagamento em Conversas \u2192 Tyxter." : action.hint })
|
|
996
|
+
] }, action.id);
|
|
997
|
+
}) })
|
|
998
|
+
] }),
|
|
999
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1000
|
+
/* @__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" }),
|
|
1001
|
+
/* @__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)) })
|
|
1002
|
+
] })
|
|
1003
|
+
] })
|
|
303
1004
|
] });
|
|
304
1005
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
function catalogDefaultEnabled(eventKey, channel) {
|
|
325
|
-
const def = getMessageDefaults().find((d) => d.eventKey === eventKey);
|
|
326
|
-
return def?.defaultEnabled?.[channel] ?? false;
|
|
327
|
-
}
|
|
328
|
-
async function defaultSessionToken() {
|
|
329
|
-
const session = (await getFayzSupabaseClientOptional()?.auth.getSession())?.data.session;
|
|
330
|
-
return session?.access_token ?? null;
|
|
331
|
-
}
|
|
332
|
-
function createFayzTemplateStore(connection, deps = {}) {
|
|
333
|
-
const client = deps.client ?? createFayzAutomationsClient({
|
|
334
|
-
baseUrl: connection.apiBaseUrl,
|
|
335
|
-
projectId: connection.projectId,
|
|
336
|
-
publishableKey: connection.publishableKey
|
|
337
|
-
});
|
|
338
|
-
const getSessionToken = deps.getSessionToken ?? defaultSessionToken;
|
|
339
|
-
const ids = /* @__PURE__ */ new Map();
|
|
340
|
-
const keyOf = (eventKey, channel) => `${eventKey}:${channel}`;
|
|
341
|
-
async function refresh(tenantId) {
|
|
342
|
-
const automations = await client.list({ tenantId });
|
|
343
|
-
ids.clear();
|
|
344
|
-
const rows = [];
|
|
345
|
-
for (const automation of automations) {
|
|
346
|
-
const row = toTemplate(automation);
|
|
347
|
-
if (!row) continue;
|
|
348
|
-
ids.set(keyOf(row.eventKey, row.channel), automation.id);
|
|
349
|
-
rows.push(row);
|
|
1006
|
+
|
|
1007
|
+
// src/lib/connector-automations.ts
|
|
1008
|
+
function connectorAutomationRows(connectors) {
|
|
1009
|
+
const out = [];
|
|
1010
|
+
for (const connector of connectors) {
|
|
1011
|
+
if (connector.state !== "connected") continue;
|
|
1012
|
+
for (const auto of connector.definition?.managedAutomations ?? []) {
|
|
1013
|
+
out.push({
|
|
1014
|
+
connectorId: connector.id,
|
|
1015
|
+
connectorName: connector.name,
|
|
1016
|
+
hostPluginId: connector.hostPluginId,
|
|
1017
|
+
icon: connector.icon,
|
|
1018
|
+
logoUrl: connector.logoUrl,
|
|
1019
|
+
eventKey: auto.eventKey,
|
|
1020
|
+
label: auto.label,
|
|
1021
|
+
optional: auto.optional ?? false,
|
|
1022
|
+
...connector.definition?.listDeliveries ? { listDeliveries: connector.definition.listDeliveries.bind(connector.definition) } : {}
|
|
1023
|
+
});
|
|
350
1024
|
}
|
|
351
|
-
return rows;
|
|
352
1025
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (!token) throw new Error("Sess\xE3o expirada \u2014 entre de novo.");
|
|
370
|
-
return token;
|
|
371
|
-
}
|
|
372
|
-
async function resolveId(eventKey, channel, tenantId) {
|
|
373
|
-
const key = keyOf(eventKey, channel);
|
|
374
|
-
if (!ids.has(key)) await load(tenantId);
|
|
375
|
-
return ids.get(key);
|
|
376
|
-
}
|
|
377
|
-
return {
|
|
378
|
-
async list(tenantId) {
|
|
379
|
-
if (!tenantId) return [];
|
|
380
|
-
return load(tenantId);
|
|
381
|
-
},
|
|
382
|
-
async upsert(input, tenantId) {
|
|
383
|
-
if (!tenantId) throw new Error("@fayz-ai/plugin-automations: tenantId is required");
|
|
384
|
-
const appSessionToken = await requireToken();
|
|
385
|
-
const key = keyOf(input.eventKey, input.channel);
|
|
386
|
-
let id = await resolveId(input.eventKey, input.channel, tenantId);
|
|
387
|
-
if (!id) {
|
|
388
|
-
const action = {};
|
|
389
|
-
if (input.subject != null) action.subject = input.subject;
|
|
390
|
-
if (input.body != null) action.body = input.body;
|
|
391
|
-
try {
|
|
392
|
-
const created = await client.create({
|
|
393
|
-
tenantId,
|
|
394
|
-
appSessionToken,
|
|
395
|
-
eventKey: input.eventKey,
|
|
396
|
-
sourceChannel: input.channel,
|
|
397
|
-
// The platform has no "inherit from catalogue": materialise the default.
|
|
398
|
-
enabled: input.enabled ?? catalogDefaultEnabled(input.eventKey, input.channel),
|
|
399
|
-
...Object.keys(action).length ? { action } : {}
|
|
400
|
-
});
|
|
401
|
-
ids.set(key, created.id);
|
|
402
|
-
invalidate();
|
|
403
|
-
const row2 = toTemplate(created);
|
|
404
|
-
if (!row2) throw new Error("@fayz-ai/plugin-automations: platform did not round-trip eventKey");
|
|
405
|
-
return row2;
|
|
406
|
-
} catch (err) {
|
|
407
|
-
if (!(err instanceof FayzAutomationsError) || err.status !== 409) throw err;
|
|
408
|
-
invalidate();
|
|
409
|
-
await refresh(tenantId);
|
|
410
|
-
id = ids.get(key);
|
|
411
|
-
if (!id) throw err;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
const updated = await client.update({
|
|
415
|
-
id,
|
|
416
|
-
tenantId,
|
|
417
|
-
appSessionToken,
|
|
418
|
-
// Only what was sent: omitting has to mean "leave it alone", as in the local store.
|
|
419
|
-
...typeof input.enabled === "boolean" ? { enabled: input.enabled } : {},
|
|
420
|
-
...input.subject !== void 0 || input.body !== void 0 ? {
|
|
421
|
-
action: {
|
|
422
|
-
...input.subject !== void 0 ? { subject: input.subject } : {},
|
|
423
|
-
...input.body !== void 0 ? { body: input.body } : {}
|
|
424
|
-
}
|
|
425
|
-
} : {}
|
|
426
|
-
});
|
|
427
|
-
invalidate();
|
|
428
|
-
const row = toTemplate(updated);
|
|
429
|
-
if (!row) throw new Error("@fayz-ai/plugin-automations: platform did not round-trip eventKey");
|
|
430
|
-
return row;
|
|
431
|
-
},
|
|
432
|
-
async reset(eventKey, channel, tenantId) {
|
|
433
|
-
if (!tenantId) return;
|
|
434
|
-
const id = await resolveId(eventKey, channel, tenantId);
|
|
435
|
-
if (!id) return;
|
|
436
|
-
const appSessionToken = await requireToken();
|
|
437
|
-
await client.update({ id, tenantId, appSessionToken, action: { subject: null, body: null } });
|
|
438
|
-
invalidate();
|
|
1026
|
+
return out;
|
|
1027
|
+
}
|
|
1028
|
+
function groupConnectorAutomations(rows) {
|
|
1029
|
+
const groups = [];
|
|
1030
|
+
for (const row of rows) {
|
|
1031
|
+
let group = groups.find((g) => g.connectorId === row.connectorId);
|
|
1032
|
+
if (!group) {
|
|
1033
|
+
group = {
|
|
1034
|
+
connectorId: row.connectorId,
|
|
1035
|
+
connectorName: row.connectorName,
|
|
1036
|
+
hostPluginId: row.hostPluginId,
|
|
1037
|
+
icon: row.icon,
|
|
1038
|
+
logoUrl: row.logoUrl,
|
|
1039
|
+
items: []
|
|
1040
|
+
};
|
|
1041
|
+
groups.push(group);
|
|
439
1042
|
}
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function getFayzAutomationsClient(connection) {
|
|
444
|
-
const key = `${connection.apiBaseUrl}|${connection.projectId}|${connection.publishableKey}`;
|
|
445
|
-
if (cachedClient?.key === key) return cachedClient.client;
|
|
446
|
-
const client = createFayzAutomationsClient({
|
|
447
|
-
baseUrl: connection.apiBaseUrl,
|
|
448
|
-
projectId: connection.projectId,
|
|
449
|
-
publishableKey: connection.publishableKey
|
|
450
|
-
});
|
|
451
|
-
cachedClient = { key, client };
|
|
452
|
-
return client;
|
|
1043
|
+
group.items.push(row);
|
|
1044
|
+
}
|
|
1045
|
+
return groups;
|
|
453
1046
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
if (min < 60) return `h\xE1 ${min} min`;
|
|
472
|
-
const h = Math.round(min / 60);
|
|
473
|
-
if (h < 24) return `h\xE1 ${h} h`;
|
|
474
|
-
return new Date(iso).toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
|
|
1047
|
+
function ConnectorAutomations({ rows }) {
|
|
1048
|
+
if (rows.length === 0) return null;
|
|
1049
|
+
return /* @__PURE__ */ jsx(Fragment, { children: groupConnectorAutomations(rows).map((group) => {
|
|
1050
|
+
const href = connectorRoute({ hostPluginId: group.hostPluginId });
|
|
1051
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
1052
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: group.connectorName }),
|
|
1053
|
+
group.items.map((row) => /* @__PURE__ */ jsx(
|
|
1054
|
+
AutomationCard,
|
|
1055
|
+
{
|
|
1056
|
+
row,
|
|
1057
|
+
group,
|
|
1058
|
+
href
|
|
1059
|
+
},
|
|
1060
|
+
`${group.connectorId}:${row.eventKey}`
|
|
1061
|
+
))
|
|
1062
|
+
] }, group.connectorId);
|
|
1063
|
+
}) });
|
|
475
1064
|
}
|
|
476
|
-
function
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
1065
|
+
function AutomationCard({
|
|
1066
|
+
row,
|
|
1067
|
+
group,
|
|
1068
|
+
href
|
|
1069
|
+
}) {
|
|
1070
|
+
const [open, setOpen] = React4__default.useState(false);
|
|
1071
|
+
return /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
|
|
481
1072
|
/* @__PURE__ */ jsxs(
|
|
482
|
-
"
|
|
1073
|
+
"div",
|
|
483
1074
|
{
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
1075
|
+
role: "button",
|
|
1076
|
+
tabIndex: 0,
|
|
1077
|
+
onClick: () => adminNavigateTo(href),
|
|
1078
|
+
onKeyDown: (e) => {
|
|
1079
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1080
|
+
e.preventDefault();
|
|
1081
|
+
adminNavigateTo(href);
|
|
1082
|
+
}
|
|
1083
|
+
},
|
|
1084
|
+
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",
|
|
487
1085
|
children: [
|
|
488
|
-
/* @__PURE__ */ jsx(
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
1086
|
+
/* @__PURE__ */ jsx(
|
|
1087
|
+
ConnectorMark,
|
|
1088
|
+
{
|
|
1089
|
+
connector: {
|
|
1090
|
+
id: group.connectorId,
|
|
1091
|
+
name: group.connectorName,
|
|
1092
|
+
icon: group.icon,
|
|
1093
|
+
logoUrl: group.logoUrl
|
|
1094
|
+
},
|
|
1095
|
+
size: "sm"
|
|
1096
|
+
}
|
|
1097
|
+
),
|
|
1098
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
1099
|
+
/* @__PURE__ */ jsx("p", { className: "font-medium", children: row.label }),
|
|
1100
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
|
|
1101
|
+
"Quando ",
|
|
1102
|
+
/* @__PURE__ */ jsx("code", { className: "font-mono text-xs", children: row.eventKey }),
|
|
1103
|
+
" acontece aqui."
|
|
1104
|
+
] })
|
|
1105
|
+
] }),
|
|
1106
|
+
/* @__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}` })
|
|
493
1107
|
]
|
|
494
1108
|
}
|
|
495
1109
|
),
|
|
496
|
-
|
|
497
|
-
/* @__PURE__ */
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
1110
|
+
row.listDeliveries && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1111
|
+
/* @__PURE__ */ jsxs(
|
|
1112
|
+
"button",
|
|
1113
|
+
{
|
|
1114
|
+
type: "button",
|
|
1115
|
+
onClick: () => setOpen(!open),
|
|
1116
|
+
"aria-expanded": open,
|
|
1117
|
+
className: "flex w-full items-center gap-2 border-t border-border px-4 py-2 text-left transition-colors hover:bg-muted/40",
|
|
1118
|
+
children: [
|
|
1119
|
+
/* @__PURE__ */ jsx(ChevronRight, { className: `h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform ${open ? "rotate-90" : ""}` }),
|
|
1120
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs font-medium", children: [
|
|
1121
|
+
"O que j\xE1 foi para o ",
|
|
1122
|
+
group.connectorName
|
|
1123
|
+
] })
|
|
1124
|
+
]
|
|
1125
|
+
}
|
|
1126
|
+
),
|
|
1127
|
+
open && /* @__PURE__ */ jsx("div", { className: "border-t border-border p-4", children: /* @__PURE__ */ jsx(
|
|
1128
|
+
ConnectorDeliveries,
|
|
1129
|
+
{
|
|
1130
|
+
load: (options) => row.listDeliveries({ ...options, eventKey: row.eventKey }),
|
|
1131
|
+
providerName: group.connectorName
|
|
1132
|
+
}
|
|
1133
|
+
) })
|
|
507
1134
|
] })
|
|
508
1135
|
] });
|
|
509
1136
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
1137
|
+
var CONFIGURE_TOOL = {
|
|
1138
|
+
id: "automations.configure",
|
|
1139
|
+
name: "configureAutomation",
|
|
1140
|
+
description: "Turns an automation on or off, and/or rewrites its message. Use the `event` and `channel` values exactly as they appear in the surface state. Placeholders must come from that automation's own list \u2014 an invented one renders as literal text in a message a customer reads. To review several, call once per automation.",
|
|
1141
|
+
icon: "Zap",
|
|
1142
|
+
mode: "persist",
|
|
1143
|
+
execution: { plane: "client" },
|
|
1144
|
+
category: "Automations",
|
|
1145
|
+
parameters: {
|
|
1146
|
+
type: "object",
|
|
1147
|
+
properties: {
|
|
1148
|
+
event: { type: "string", description: "Event key from the surface state." },
|
|
1149
|
+
channel: { type: "string", description: "Channel of that automation (email | whatsapp | sms | in_app)." },
|
|
1150
|
+
enabled: { type: "boolean", description: "Turn it on or off. Omit to leave as is." },
|
|
1151
|
+
subject: { type: "string", description: "New subject. Email only." },
|
|
1152
|
+
body: { type: "string", description: "New message body." }
|
|
1153
|
+
},
|
|
1154
|
+
required: ["event", "channel"]
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
var ASK_AI_ACTION = {
|
|
1158
|
+
label: "Criar com IA",
|
|
1159
|
+
hint: "O que revisar nas automa\xE7\xF5es\u2026",
|
|
1160
|
+
intro: {
|
|
1161
|
+
role: "setup",
|
|
1162
|
+
persona: "Redator das automa\xE7\xF5es",
|
|
1163
|
+
pitch: "Diga o que revisar nas automa\xE7\xF5es.",
|
|
1164
|
+
description: "Vejo todas as automa\xE7\xF5es deste app, quais est\xE3o ligadas e o texto de cada uma. Posso ligar, desligar e reescrever \u2014 em uma ou em v\xE1rias de uma vez. Voc\xEA confirma cada mudan\xE7a.",
|
|
1165
|
+
suggestions: [
|
|
1166
|
+
{ label: "Revise as mensagens de pedido no tom da minha marca" },
|
|
1167
|
+
{ label: "Ligue as automa\xE7\xF5es de entrega" },
|
|
1168
|
+
{ label: "Deixe as mensagens de WhatsApp mais curtas" }
|
|
1169
|
+
]
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
var CHANNEL_ICON2 = {
|
|
1173
|
+
email: Mail,
|
|
1174
|
+
// The brand glyph, not a generic bubble: in a list of channels, WhatsApp
|
|
1175
|
+
// is recognized by its mark before its label.
|
|
1176
|
+
whatsapp: WhatsAppIcon,
|
|
1177
|
+
sms: Smartphone,
|
|
1178
|
+
push: MessageSquare,
|
|
1179
|
+
webchat: MessageSquare
|
|
539
1180
|
};
|
|
540
|
-
function ExecutionRow({ execution }) {
|
|
541
|
-
const cfg = EXEC_STATUS[execution.status] ?? { label: execution.status, variant: "info", Icon: History };
|
|
542
|
-
const { Icon } = cfg;
|
|
543
|
-
return /* @__PURE__ */ jsxs("div", { className: "border-b border-border px-4 py-2.5 last:border-b-0", children: [
|
|
544
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
545
|
-
/* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
|
|
546
|
-
/* @__PURE__ */ jsx(Badge, { variant: cfg.variant, children: cfg.label }),
|
|
547
|
-
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm text-muted-foreground", children: execution.automationName }),
|
|
548
|
-
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: when(execution.executedAt) })
|
|
549
|
-
] }),
|
|
550
|
-
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 })
|
|
551
|
-
] });
|
|
552
|
-
}
|
|
553
|
-
function FayzRunLog({ connection, eventKey, tenantId, enabled }) {
|
|
554
|
-
const [executions, setExecutions] = React3.useState(null);
|
|
555
|
-
const [loading, setLoading] = React3.useState(false);
|
|
556
|
-
const load = React3.useCallback(async () => {
|
|
557
|
-
if (!tenantId) return;
|
|
558
|
-
setLoading(true);
|
|
559
|
-
try {
|
|
560
|
-
setExecutions(await getFayzAutomationsClient(connection).listExecutions({ tenantId, eventKey, limit: 20 }));
|
|
561
|
-
} catch (err) {
|
|
562
|
-
console.warn("[automations] could not load platform executions", err);
|
|
563
|
-
setExecutions([]);
|
|
564
|
-
} finally {
|
|
565
|
-
setLoading(false);
|
|
566
|
-
}
|
|
567
|
-
}, [connection, eventKey, tenantId]);
|
|
568
|
-
React3.useEffect(() => {
|
|
569
|
-
void load();
|
|
570
|
-
}, [load]);
|
|
571
|
-
return /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
|
|
572
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-border px-4 py-2.5", children: [
|
|
573
|
-
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
574
|
-
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Execu\xE7\xF5es recentes" }),
|
|
575
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "O que a plataforma Fayz executou para este gatilho nesta conta." })
|
|
576
|
-
] }),
|
|
577
|
-
/* @__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" : ""}` }) })
|
|
578
|
-
] }),
|
|
579
|
-
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: [
|
|
580
|
-
/* @__PURE__ */ jsx(History, { className: "h-7 w-7 text-muted-foreground/40", strokeWidth: 1.5 }),
|
|
581
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: "Nenhuma execu\xE7\xE3o ainda" }),
|
|
582
|
-
/* @__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." })
|
|
583
|
-
] }) : /* @__PURE__ */ jsx("div", { children: executions.map((e) => /* @__PURE__ */ jsx(ExecutionRow, { execution: e }, e.id)) })
|
|
584
|
-
] });
|
|
585
|
-
}
|
|
586
|
-
var TABS = [
|
|
587
|
-
{ value: "preview", label: "Pr\xE9via" },
|
|
588
|
-
{ value: "code", label: "C\xF3digo" },
|
|
589
|
-
// O que já aconteceu. Fica ao lado do texto, e não numa tela à parte, porque
|
|
590
|
-
// é a mesma pergunta em dois tempos: o que vai sair, e o que saiu.
|
|
591
|
-
{ value: "runs", label: "Execu\xE7\xF5es" }
|
|
592
|
-
];
|
|
593
1181
|
var CHANNEL_LABEL2 = {
|
|
594
1182
|
email: "E-mail",
|
|
595
1183
|
whatsapp: "WhatsApp",
|
|
@@ -597,239 +1185,287 @@ var CHANNEL_LABEL2 = {
|
|
|
597
1185
|
push: "Push",
|
|
598
1186
|
webchat: "Chat"
|
|
599
1187
|
};
|
|
600
|
-
var
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
tracking_code: "BR123456789BR",
|
|
610
|
-
cart_url: "https://sualoja.com.br/carrinho",
|
|
611
|
-
deal_name: "Proposta \u2014 Reforma da fachada",
|
|
612
|
-
deal_value: "R$ 12.400,00",
|
|
613
|
-
stage_name: "Negocia\xE7\xE3o",
|
|
614
|
-
owner_name: "Jo\xE3o Pereira",
|
|
615
|
-
appointment_date: "12/08/2026",
|
|
616
|
-
appointment_time: "14:30"
|
|
1188
|
+
var KIND_TABS = [
|
|
1189
|
+
{ value: "all", label: "Todos", icon: Zap },
|
|
1190
|
+
{ value: "transactional", label: "Transacionais", icon: Receipt },
|
|
1191
|
+
{ value: "marketing", label: "Marketing", icon: Megaphone }
|
|
1192
|
+
];
|
|
1193
|
+
var KIND_HINT = {
|
|
1194
|
+
all: "Tudo que o produto sabe enviar, por qualquer canal.",
|
|
1195
|
+
transactional: "Disparados por uma a\xE7\xE3o da pessoa. N\xE3o precisam de opt-in.",
|
|
1196
|
+
marketing: "Iniciativa da empresa. S\xF3 para quem aceitou receber."
|
|
617
1197
|
};
|
|
618
|
-
function
|
|
1198
|
+
function iconFor(name) {
|
|
619
1199
|
if (!name) return Zap;
|
|
620
1200
|
const icons = LucideIcons;
|
|
621
1201
|
return icons[name] ?? Zap;
|
|
622
1202
|
}
|
|
623
|
-
function
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const
|
|
632
|
-
const
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
1203
|
+
function readSelection(basePath) {
|
|
1204
|
+
const hash = window.location.hash.slice(1) || "/";
|
|
1205
|
+
if (!hash.startsWith(basePath + "/")) return null;
|
|
1206
|
+
const [eventKey, channel] = hash.slice(basePath.length + 1).split("/");
|
|
1207
|
+
if (!eventKey || !channel) return null;
|
|
1208
|
+
return { eventKey, channel };
|
|
1209
|
+
}
|
|
1210
|
+
function AutomationsList({ store, variant = "page", basePath = "/automations" }) {
|
|
1211
|
+
const tenantId = useOrganizationStoreLike();
|
|
1212
|
+
const runtime = usePluginRuntimeOptional();
|
|
1213
|
+
const [messages, setMessages] = React4__default.useState(null);
|
|
1214
|
+
const [kind, setKind] = React4__default.useState("all");
|
|
1215
|
+
const { connectors } = useConnectors({ includeGrants: false, includeCatalog: false });
|
|
1216
|
+
const owned = React4__default.useMemo(() => connectorAutomationRows(connectors ?? []), [connectors]);
|
|
1217
|
+
const providerTemplates = useProviderTemplates();
|
|
1218
|
+
const [selected, setSelected] = React4__default.useState(() => readSelection(basePath));
|
|
1219
|
+
React4__default.useEffect(() => {
|
|
1220
|
+
const sync = () => setSelected(readSelection(basePath));
|
|
1221
|
+
sync();
|
|
1222
|
+
window.addEventListener("hashchange", sync);
|
|
1223
|
+
return () => window.removeEventListener("hashchange", sync);
|
|
1224
|
+
}, [basePath]);
|
|
1225
|
+
const reload = React4__default.useCallback(async () => {
|
|
638
1226
|
let overrides = [];
|
|
639
1227
|
try {
|
|
640
1228
|
overrides = await store.list(tenantId);
|
|
641
1229
|
} catch (err) {
|
|
642
|
-
console.warn("[automations] could not load overrides; showing defaults", err);
|
|
1230
|
+
console.warn("[automations] could not load template overrides; showing defaults", err);
|
|
643
1231
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
}, [eventKey, channel, store, tenantId, runtime]);
|
|
648
|
-
React3.useEffect(() => {
|
|
1232
|
+
setMessages(resolveMessages(overrides, runtime?.events ?? []));
|
|
1233
|
+
}, [store, tenantId, runtime]);
|
|
1234
|
+
React4__default.useEffect(() => {
|
|
649
1235
|
void reload();
|
|
650
1236
|
}, [reload]);
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
1237
|
+
const surfaceRef = React4__default.useRef({ messages, store, tenantId, reload, basePath });
|
|
1238
|
+
surfaceRef.current = { messages, store, tenantId, reload, basePath };
|
|
1239
|
+
const agentSurface = React4__default.useMemo(() => ({
|
|
1240
|
+
id: "automations.list",
|
|
1241
|
+
pluginId: "automations",
|
|
1242
|
+
instructions: "The user is on the automations list. `automations` in the surface state is the complete set \u2014 use each entry's `event` and `channel` verbatim, and each entry's own `placeholders` when rewriting its text. Call configureAutomation once per automation you change; say afterwards which ones you touched.",
|
|
1243
|
+
describe: () => {
|
|
1244
|
+
const o = surfaceRef.current;
|
|
1245
|
+
return {
|
|
1246
|
+
title: "Automa\xE7\xF5es",
|
|
1247
|
+
state: {
|
|
1248
|
+
automations: (o.messages ?? []).map((m) => ({
|
|
1249
|
+
event: m.event.name,
|
|
1250
|
+
title: m.event.title,
|
|
1251
|
+
channel: m.channel,
|
|
1252
|
+
enabled: m.enabled,
|
|
1253
|
+
customized: Boolean(m.customized),
|
|
1254
|
+
subject: m.subject,
|
|
1255
|
+
body: m.body,
|
|
1256
|
+
// `fields` é o contrato do evento — os mesmos descritores que viram
|
|
1257
|
+
// as merge fields do editor. É de lá que saem os placeholders.
|
|
1258
|
+
placeholders: (m.event.fields ?? []).map((f) => `{{${f.name}}}`)
|
|
1259
|
+
}))
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
},
|
|
1263
|
+
tools: [{
|
|
1264
|
+
tool: CONFIGURE_TOOL,
|
|
1265
|
+
execute: async (args) => {
|
|
1266
|
+
const o = surfaceRef.current;
|
|
1267
|
+
const found = (o.messages ?? []).find(
|
|
1268
|
+
(m) => m.event.name === args.event && m.channel === args.channel
|
|
1269
|
+
);
|
|
1270
|
+
if (!found) {
|
|
1271
|
+
return {
|
|
1272
|
+
ok: false,
|
|
1273
|
+
error: "unknown_automation",
|
|
1274
|
+
available: (o.messages ?? []).map((m) => `${m.event.name}/${m.channel}`)
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
try {
|
|
1278
|
+
await o.store.upsert({
|
|
1279
|
+
eventKey: found.event.name,
|
|
1280
|
+
channel: found.channel,
|
|
1281
|
+
...args.enabled === void 0 ? {} : { enabled: Boolean(args.enabled) },
|
|
1282
|
+
...args.subject === void 0 ? {} : { subject: String(args.subject) },
|
|
1283
|
+
...args.body === void 0 ? {} : { body: String(args.body) }
|
|
1284
|
+
}, o.tenantId);
|
|
1285
|
+
await o.reload();
|
|
1286
|
+
return {
|
|
1287
|
+
ok: true,
|
|
1288
|
+
event: found.event.name,
|
|
1289
|
+
channel: found.channel,
|
|
1290
|
+
ref: {
|
|
1291
|
+
id: `${found.event.name}/${found.channel}`,
|
|
1292
|
+
label: `${found.event.title ?? found.event.name} \u2014 ${CHANNEL_LABEL2[found.channel] ?? found.channel}`,
|
|
1293
|
+
path: `${o.basePath}/${encodeURIComponent(found.event.name)}/${found.channel}`
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
} catch (e) {
|
|
1297
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}]
|
|
1301
|
+
}), []);
|
|
1302
|
+
useAgentSurface(agentSurface);
|
|
1303
|
+
const toggle = async (m, enabled) => {
|
|
1304
|
+
setMessages((prev) => prev?.map((x) => x.event.name === m.event.name && x.channel === m.channel ? { ...x, enabled } : x) ?? null);
|
|
689
1305
|
try {
|
|
690
|
-
await store.upsert(
|
|
1306
|
+
await store.upsert({ eventKey: m.event.name, channel: m.channel, enabled }, tenantId);
|
|
691
1307
|
await reload();
|
|
692
1308
|
} catch (err) {
|
|
693
|
-
console.warn("[automations]
|
|
1309
|
+
console.warn("[automations] toggle failed", err);
|
|
694
1310
|
toast.error("N\xE3o foi poss\xEDvel salvar");
|
|
695
|
-
} finally {
|
|
696
|
-
setSaving(false);
|
|
697
|
-
}
|
|
698
|
-
};
|
|
699
|
-
const save = () => void patch({
|
|
700
|
-
eventKey: event.name,
|
|
701
|
-
channel: message.channel,
|
|
702
|
-
// Igual ao padrão volta a ser "sem personalização", não uma cópia.
|
|
703
|
-
subject: current.subject.trim() === (fallback?.subject ?? "") ? null : current.subject.trim() || null,
|
|
704
|
-
body: current.body.trim() === fallback?.body ? null : current.body.trim() || null
|
|
705
|
-
});
|
|
706
|
-
const reset = async () => {
|
|
707
|
-
setDraft({ subject: fallback?.subject ?? "", body: fallback?.body ?? "" });
|
|
708
|
-
setSaving(true);
|
|
709
|
-
try {
|
|
710
|
-
await store.reset(event.name, message.channel, tenantId);
|
|
711
1311
|
await reload();
|
|
712
|
-
toast.success("Texto restaurado");
|
|
713
|
-
} catch (err) {
|
|
714
|
-
console.warn("[automations] reset failed", err);
|
|
715
|
-
toast.error("N\xE3o foi poss\xEDvel restaurar");
|
|
716
|
-
} finally {
|
|
717
|
-
setSaving(false);
|
|
718
1312
|
}
|
|
719
1313
|
};
|
|
720
|
-
const
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1314
|
+
const visible = (messages ?? []).filter((m) => kind === "all" || m.kind === kind);
|
|
1315
|
+
const ownedVisible = kind === "all" ? owned : [];
|
|
1316
|
+
const activeCount = (messages ?? []).filter((m) => m.enabled).length + owned.length;
|
|
1317
|
+
const totalCount = (messages?.length ?? 0) + owned.length;
|
|
1318
|
+
const groups = [];
|
|
1319
|
+
for (const m of visible) {
|
|
1320
|
+
let g = groups.find((x) => x.module === m.event.pluginId);
|
|
1321
|
+
if (!g) {
|
|
1322
|
+
g = { module: m.event.pluginId, label: m.event.pluginName, items: [] };
|
|
1323
|
+
groups.push(g);
|
|
1324
|
+
}
|
|
1325
|
+
g.items.push(m);
|
|
1326
|
+
}
|
|
1327
|
+
const counter = messages !== null && /* @__PURE__ */ jsxs("span", { className: "text-sm text-muted-foreground", children: [
|
|
1328
|
+
activeCount,
|
|
1329
|
+
" de ",
|
|
1330
|
+
totalCount,
|
|
1331
|
+
" automa\xE7\xF5es ativas"
|
|
1332
|
+
] });
|
|
1333
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
1334
|
+
variant !== "panel" ? /* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsx(
|
|
1335
|
+
PageActions,
|
|
1336
|
+
{
|
|
1337
|
+
actions: [{
|
|
1338
|
+
id: "automations-ai",
|
|
1339
|
+
label: ASK_AI_ACTION.label,
|
|
1340
|
+
icon: WandSparkles,
|
|
1341
|
+
intent: "primary",
|
|
1342
|
+
creates: true,
|
|
1343
|
+
onSelect: () => askAI({ hint: ASK_AI_ACTION.hint, intro: ASK_AI_ACTION.intro })
|
|
1344
|
+
}]
|
|
1345
|
+
}
|
|
1346
|
+
) }) : null,
|
|
1347
|
+
variant === "panel" ? /* @__PURE__ */ jsx(
|
|
1348
|
+
SettingsTabs,
|
|
1349
|
+
{
|
|
1350
|
+
tabs: KIND_TABS.map((t) => ({ id: t.value, label: t.label, IconComp: t.icon })),
|
|
1351
|
+
value: kind,
|
|
1352
|
+
onChange: (id) => setKind(id),
|
|
1353
|
+
"aria-label": "Automa\xE7\xF5es",
|
|
1354
|
+
actions: counter
|
|
1355
|
+
}
|
|
1356
|
+
) : (
|
|
1357
|
+
/* Mesma faixa das listas e da sub-nav de módulo. */
|
|
1358
|
+
/* @__PURE__ */ jsx(
|
|
1359
|
+
PageTabs,
|
|
1360
|
+
{
|
|
1361
|
+
className: "mb-0",
|
|
1362
|
+
tabs: KIND_TABS.map((t) => ({ value: t.value, label: t.label, icon: t.icon })),
|
|
1363
|
+
value: kind,
|
|
1364
|
+
onChange: setKind,
|
|
1365
|
+
"aria-label": "Automa\xE7\xF5es",
|
|
1366
|
+
actions: counter
|
|
1367
|
+
}
|
|
1368
|
+
)
|
|
1369
|
+
),
|
|
1370
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: KIND_HINT[kind] }),
|
|
1371
|
+
messages === null ? /* @__PURE__ */ jsx("div", { className: "space-y-3", children: Array.from({ length: 4 }).map((_, i) => /* @__PURE__ */ jsxs(Card, { className: "p-4", children: [
|
|
1372
|
+
/* @__PURE__ */ jsx("div", { className: "h-4 w-40 animate-pulse rounded bg-muted" }),
|
|
1373
|
+
/* @__PURE__ */ jsx("div", { className: "mt-2 h-3 w-64 animate-pulse rounded bg-muted" })
|
|
1374
|
+
] }, i)) }) : groups.length === 0 && ownedVisible.length === 0 ? /* @__PURE__ */ jsxs(Card, { className: "flex flex-col items-center gap-1 p-12 text-center", children: [
|
|
1375
|
+
/* @__PURE__ */ jsx(Zap, { className: "mb-2 h-9 w-9 text-muted-foreground/40", strokeWidth: 1.5 }),
|
|
1376
|
+
/* @__PURE__ */ jsx("p", { className: "text-base font-semibold", children: "Nenhuma automa\xE7\xE3o nesta aba" }),
|
|
1377
|
+
/* @__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." })
|
|
1378
|
+
] }) : groups.map((group) => /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
1379
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: group.label }),
|
|
1380
|
+
group.items.map((m) => {
|
|
1381
|
+
const Icon = iconFor(m.event.icon ?? m.event.pluginIcon);
|
|
1382
|
+
const ChannelIcon = CHANNEL_ICON2[m.channel];
|
|
1383
|
+
const href = `${basePath}/${encodeURIComponent(m.event.name)}/${m.channel}`;
|
|
1384
|
+
const isOpen = selected != null && decodeURIComponent(selected.eventKey) === m.event.name && selected.channel === m.channel;
|
|
1385
|
+
return /* @__PURE__ */ jsx(
|
|
1386
|
+
Card,
|
|
790
1387
|
{
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1388
|
+
role: "button",
|
|
1389
|
+
tabIndex: 0,
|
|
1390
|
+
"aria-current": isOpen || void 0,
|
|
1391
|
+
onClick: () => adminNavigateTo(href),
|
|
1392
|
+
onKeyDown: (e) => {
|
|
1393
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1394
|
+
e.preventDefault();
|
|
1395
|
+
adminNavigateTo(href);
|
|
1396
|
+
}
|
|
1397
|
+
},
|
|
1398
|
+
style: isOpen ? { outline: "2px solid hsl(var(--primary) / 0.65)", outlineOffset: "-2px" } : void 0,
|
|
1399
|
+
className: `cursor-pointer overflow-hidden transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring ${isOpen ? "" : "hover:border-primary/40"}`,
|
|
1400
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-4 p-4", children: [
|
|
1401
|
+
/* @__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" }) }),
|
|
1402
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
1403
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
1404
|
+
/* @__PURE__ */ jsx("p", { className: "font-medium", children: m.override?.name ?? m.event.title ?? m.event.name }),
|
|
1405
|
+
/* @__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: [
|
|
1406
|
+
/* @__PURE__ */ jsx(ChannelIcon, { className: "h-3 w-3" }),
|
|
1407
|
+
CHANNEL_LABEL2[m.channel]
|
|
1408
|
+
] }),
|
|
1409
|
+
m.customized && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-primary/10 px-2 py-0.5 text-[11px] text-primary", children: "personalizado" }),
|
|
1410
|
+
/* @__PURE__ */ jsx(
|
|
1411
|
+
ProviderTemplateBadge,
|
|
1412
|
+
{
|
|
1413
|
+
approval: providerApprovalFor(providerTemplates, m.event.name, m.channel)
|
|
1414
|
+
}
|
|
1415
|
+
)
|
|
1416
|
+
] }),
|
|
1417
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: m.event.description }),
|
|
1418
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground", children: [
|
|
1419
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
|
|
1420
|
+
/* @__PURE__ */ jsx(Zap, { className: "h-3 w-3" }),
|
|
1421
|
+
m.event.trigger
|
|
1422
|
+
] }),
|
|
1423
|
+
m.subject && /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
|
|
1424
|
+
"Assunto: ",
|
|
1425
|
+
m.subject
|
|
1426
|
+
] })
|
|
1427
|
+
] })
|
|
797
1428
|
] }),
|
|
798
|
-
/* @__PURE__ */
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1429
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-3", children: [
|
|
1430
|
+
/* @__PURE__ */ jsx(
|
|
1431
|
+
Switch,
|
|
1432
|
+
{
|
|
1433
|
+
checked: m.enabled,
|
|
1434
|
+
onCheckedChange: (next) => void toggle(m, next),
|
|
1435
|
+
label: m.enabled ? "Ativo" : "Inativo",
|
|
1436
|
+
"aria-label": `${m.event.title} \u2014 ${CHANNEL_LABEL2[m.channel]}`
|
|
1437
|
+
}
|
|
1438
|
+
),
|
|
1439
|
+
/* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4 text-muted-foreground" })
|
|
1440
|
+
] })
|
|
1441
|
+
] })
|
|
1442
|
+
},
|
|
1443
|
+
`${m.event.name}:${m.channel}`
|
|
1444
|
+
);
|
|
1445
|
+
})
|
|
1446
|
+
] }, group.module)),
|
|
1447
|
+
messages !== null && /* @__PURE__ */ jsx(ConnectorAutomations, { rows: ownedVisible }),
|
|
1448
|
+
selected && /* @__PURE__ */ jsx(
|
|
1449
|
+
RightRailPage,
|
|
1450
|
+
{
|
|
1451
|
+
title: (messages ?? []).find(
|
|
1452
|
+
(m) => m.event.name === decodeURIComponent(selected.eventKey) && m.channel === selected.channel
|
|
1453
|
+
)?.event.title ?? "Automa\xE7\xE3o",
|
|
1454
|
+
open: true,
|
|
1455
|
+
onClose: () => adminNavigateTo(basePath),
|
|
1456
|
+
width: "max-w-2xl",
|
|
1457
|
+
children: /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-4", children: /* @__PURE__ */ jsx(
|
|
1458
|
+
AutomationDetailPage,
|
|
820
1459
|
{
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1460
|
+
eventKey: selected.eventKey,
|
|
1461
|
+
channel: selected.channel,
|
|
1462
|
+
store,
|
|
1463
|
+
backTo: basePath,
|
|
1464
|
+
embedded: true
|
|
825
1465
|
}
|
|
826
|
-
)
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
/* @__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" }),
|
|
830
|
-
/* @__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)) })
|
|
831
|
-
] })
|
|
832
|
-
] })
|
|
1466
|
+
) })
|
|
1467
|
+
}
|
|
1468
|
+
)
|
|
833
1469
|
] });
|
|
834
1470
|
}
|
|
835
1471
|
var TABLE = "plg_automations_templates";
|
|
@@ -848,6 +1484,9 @@ function rowToTemplate(row) {
|
|
|
848
1484
|
subject: row.subject || null,
|
|
849
1485
|
body: row.body || null,
|
|
850
1486
|
name: row.name || null,
|
|
1487
|
+
// NULL fica NULL pelo mesmo motivo do `enabled`; lista vazia é uma escolha
|
|
1488
|
+
// ("sem botões"), não a ausência de uma.
|
|
1489
|
+
actions: normalizeMessageActions(row.actions),
|
|
851
1490
|
updatedAt: row.updated_at ?? row.created_at
|
|
852
1491
|
};
|
|
853
1492
|
}
|
|
@@ -875,6 +1514,7 @@ function createSupabaseTemplateStore() {
|
|
|
875
1514
|
if (input.subject !== void 0) payload.subject = input.subject;
|
|
876
1515
|
if (input.body !== void 0) payload.body = input.body;
|
|
877
1516
|
if (input.name !== void 0) payload.name = input.name;
|
|
1517
|
+
if (input.actions !== void 0) payload.actions = input.actions;
|
|
878
1518
|
const { data, error } = await db().from(TABLE).upsert(payload, { onConflict: "tenant_id,event_key,channel" }).select().single();
|
|
879
1519
|
if (error) throw error;
|
|
880
1520
|
return rowToTemplate(data);
|
|
@@ -894,20 +1534,24 @@ function createAutomationsPlugin(options) {
|
|
|
894
1534
|
const store = options?.store ?? (connection ? createFayzTemplateStore(connection) : createSupabaseTemplateStore());
|
|
895
1535
|
startEventLog();
|
|
896
1536
|
const inSettings = options?.placement === "settings";
|
|
897
|
-
const ListPage = () =>
|
|
1537
|
+
const ListPage = () => React4__default.createElement(AutomationsList, { store });
|
|
898
1538
|
ListPage.displayName = "AutomationsList";
|
|
899
|
-
const
|
|
900
|
-
|
|
1539
|
+
const SettingsListPage = () => React4__default.createElement(AutomationsList, { store, variant: "panel", basePath: "/settings/automations" });
|
|
1540
|
+
SettingsListPage.displayName = "AutomationsSettings";
|
|
1541
|
+
const RailDetail = ({ params, onClose }) => React4__default.createElement(AutomationDetailPage, {
|
|
1542
|
+
eventKey: params.eventKey,
|
|
1543
|
+
channel: params.channel,
|
|
901
1544
|
store,
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
backTo: inSettings ? "/settings/automations" : "/automations"
|
|
1545
|
+
backTo: inSettings ? "/settings/automations" : "/automations",
|
|
1546
|
+
embedded: true,
|
|
1547
|
+
onDone: onClose
|
|
906
1548
|
});
|
|
907
|
-
|
|
1549
|
+
RailDetail.displayName = "AutomationRailDetail";
|
|
1550
|
+
registerRailPage("automations.detail", RailDetail);
|
|
908
1551
|
const READ = { feature: "automations", action: "read" };
|
|
909
1552
|
return {
|
|
910
1553
|
id: "automations",
|
|
1554
|
+
defaultAgentRole: "setup",
|
|
911
1555
|
name: options?.navLabel ?? "Automa\xE7\xF5es",
|
|
912
1556
|
icon: "Zap",
|
|
913
1557
|
version: "2.0.0",
|
|
@@ -929,18 +1573,21 @@ function createAutomationsPlugin(options) {
|
|
|
929
1573
|
],
|
|
930
1574
|
// The SAME list, served inside settings — not a second screen.
|
|
931
1575
|
settings: inSettings ? [
|
|
932
|
-
{ id: "automations", label: options?.navLabel ?? "Automa\xE7\xF5es", icon: "Zap", component:
|
|
1576
|
+
{ id: "automations", label: options?.navLabel ?? "Automa\xE7\xF5es", icon: "Zap", component: SettingsListPage, permission: READ }
|
|
933
1577
|
] : [],
|
|
934
1578
|
routes: [
|
|
935
1579
|
{ path: "/automations", component: ListPage, permission: READ },
|
|
936
|
-
//
|
|
937
|
-
//
|
|
938
|
-
|
|
1580
|
+
// O detalhe de uma automação é a MESMA tela com a coluna da direita
|
|
1581
|
+
// aberta — a lista continua atrás dela. Por isso o curinga aponta para o
|
|
1582
|
+
// mesmo componente: a shell colapsa '/x' e '/x/*' na mesma chave, então a
|
|
1583
|
+
// página não remonta ao abrir o painel (ver AdminShell.routeKey). Um
|
|
1584
|
+
// segundo componente aqui traria de volta a troca de tela.
|
|
1585
|
+
{ path: "/automations/*", component: ListPage, permission: READ }
|
|
939
1586
|
],
|
|
940
1587
|
widgets: []
|
|
941
1588
|
};
|
|
942
1589
|
}
|
|
943
1590
|
|
|
944
|
-
export { createAutomationsPlugin, createFayzTemplateStore, createSupabaseTemplateStore };
|
|
1591
|
+
export { PROVIDER_TEMPLATES_TABLE, createAutomationsPlugin, createFayzTemplateStore, createSupabaseTemplateStore, loadProviderTemplates, providerApprovalFor, providerTemplateFor };
|
|
945
1592
|
//# sourceMappingURL=index.js.map
|
|
946
1593
|
//# sourceMappingURL=index.js.map
|