@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/index.js CHANGED
@@ -1,154 +1,547 @@
1
- import React3 from 'react';
2
- import { startEventLog, getSupabaseClientOptional, getMessageDefaults, usePluginRuntimeOptional, resolveMessages, resolveMessage, renderMessageTemplate, loadEventRuns } from '@fayz-ai/core';
3
- import { resolveFayzAgentConnection, getFayzSupabaseClientOptional, useConnectors, adminNavigateTo, useOrganizationStore, connectorRoute, ConnectorMark, ConnectorDeliveries } from '@fayz-ai/admin';
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, MessageCircle, Mail, ChevronRight, RotateCcw, Clock, Monitor, RefreshCw, History, AlertTriangle, CircleCheck, CircleSlash } from 'lucide-react';
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
- // src/lib/connector-automations.ts
16
- function connectorAutomationRows(connectors) {
17
- const out = [];
18
- for (const connector of connectors) {
19
- if (connector.state !== "connected") continue;
20
- for (const auto of connector.definition?.managedAutomations ?? []) {
21
- out.push({
22
- connectorId: connector.id,
23
- connectorName: connector.name,
24
- hostPluginId: connector.hostPluginId,
25
- icon: connector.icon,
26
- logoUrl: connector.logoUrl,
27
- eventKey: auto.eventKey,
28
- label: auto.label,
29
- optional: auto.optional ?? false,
30
- ...connector.definition?.listDeliveries ? { listDeliveries: connector.definition.listDeliveries.bind(connector.definition) } : {}
31
- });
32
- }
33
- }
34
- return out;
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 groupConnectorAutomations(rows) {
37
- const groups = [];
38
- for (const row of rows) {
39
- let group = groups.find((g) => g.connectorId === row.connectorId);
40
- if (!group) {
41
- group = {
42
- connectorId: row.connectorId,
43
- connectorName: row.connectorName,
44
- hostPluginId: row.hostPluginId,
45
- icon: row.icon,
46
- logoUrl: row.logoUrl,
47
- items: []
48
- };
49
- groups.push(group);
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
- group.items.push(row);
125
+ return rows;
52
126
  }
53
- return groups;
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
- function ConnectorAutomations({ rows }) {
56
- if (rows.length === 0) return null;
57
- return /* @__PURE__ */ jsx(Fragment, { children: groupConnectorAutomations(rows).map((group) => {
58
- const href = connectorRoute({ hostPluginId: group.hostPluginId });
59
- return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
60
- /* @__PURE__ */ jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: group.connectorName }),
61
- group.items.map((row) => /* @__PURE__ */ jsx(
62
- AutomationCard,
63
- {
64
- row,
65
- group,
66
- href
67
- },
68
- `${group.connectorId}:${row.eventKey}`
69
- ))
70
- ] }, group.connectorId);
71
- }) });
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
- function AutomationCard({
74
- row,
75
- group,
76
- href
77
- }) {
78
- const [open, setOpen] = React3.useState(false);
79
- return /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
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
- "div",
256
+ "button",
82
257
  {
83
- role: "button",
84
- tabIndex: 0,
85
- onClick: () => adminNavigateTo(href),
86
- onKeyDown: (e) => {
87
- if (e.key === "Enter" || e.key === " ") {
88
- e.preventDefault();
89
- adminNavigateTo(href);
90
- }
91
- },
92
- className: "flex cursor-pointer items-start gap-4 p-4 transition-colors hover:bg-muted/30 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
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
- ConnectorMark,
96
- {
97
- connector: {
98
- id: group.connectorId,
99
- name: group.connectorName,
100
- icon: group.icon,
101
- logoUrl: group.logoUrl
102
- },
103
- size: "sm"
104
- }
105
- ),
106
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
107
- /* @__PURE__ */ jsx("p", { className: "font-medium", children: row.label }),
108
- /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
109
- "Quando ",
110
- /* @__PURE__ */ jsx("code", { className: "font-mono text-xs", children: row.eventKey }),
111
- " acontece aqui."
112
- ] })
113
- ] }),
114
- /* @__PURE__ */ jsx("span", { className: "shrink-0 pt-0.5 text-right text-xs text-muted-foreground", children: row.optional ? `Opcional \u2014 desligue em ${group.connectorName}` : `Ligada pela integra\xE7\xE3o \xB7 sai ao desconectar ${group.connectorName}` })
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
- row.listDeliveries && /* @__PURE__ */ jsxs(Fragment, { children: [
119
- /* @__PURE__ */ jsxs(
120
- "button",
121
- {
122
- type: "button",
123
- onClick: () => setOpen(!open),
124
- "aria-expanded": open,
125
- className: "flex w-full items-center gap-2 border-t border-border px-4 py-2 text-left transition-colors hover:bg-muted/40",
126
- children: [
127
- /* @__PURE__ */ jsx(ChevronRight, { className: `h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform ${open ? "rotate-90" : ""}` }),
128
- /* @__PURE__ */ jsxs("span", { className: "text-xs font-medium", children: [
129
- "O que j\xE1 foi para o ",
130
- group.connectorName
131
- ] })
132
- ]
133
- }
134
- ),
135
- open && /* @__PURE__ */ jsx("div", { className: "border-t border-border p-4", children: /* @__PURE__ */ jsx(
136
- ConnectorDeliveries,
137
- {
138
- load: (options) => row.listDeliveries({ ...options, eventKey: row.eventKey }),
139
- providerName: group.connectorName
140
- }
141
- ) })
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
- var CHANNEL_ICON = {
146
- email: Mail,
147
- whatsapp: MessageCircle,
148
- sms: Smartphone,
149
- push: MessageSquare,
150
- webchat: MessageSquare
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 KIND_TABS = [
160
- { value: "all", label: "Todos" },
161
- { value: "transactional", label: "Transacionais" },
162
- { value: "marketing", label: "Marketing" }
163
- ];
164
- var KIND_HINT = {
165
- all: "Tudo que o produto sabe enviar, por qualquer canal.",
166
- transactional: "Disparados por uma a\xE7\xE3o da pessoa. N\xE3o precisam de opt-in.",
167
- marketing: "Iniciativa da empresa. S\xF3 para quem aceitou receber."
552
+ var CHANNEL_ICON = {
553
+ email: Mail,
554
+ whatsapp: WhatsAppIcon,
555
+ sms: Smartphone,
556
+ push: MessageSquare,
557
+ webchat: MessageSquare
168
558
  };
169
- function iconFor(name) {
170
- if (!name) return Zap;
171
- const icons = LucideIcons;
172
- return icons[name] ?? Zap;
173
- }
174
- function AutomationsList({ store }) {
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 [messages, setMessages] = React3.useState(null);
178
- const [kind, setKind] = React3.useState("all");
179
- const { connectors } = useConnectors({ includeGrants: false, includeCatalog: false });
180
- const owned = React3.useMemo(() => connectorAutomationRows(connectors ?? []), [connectors]);
181
- const reload = React3.useCallback(async () => {
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 template overrides; showing defaults", err);
657
+ console.warn("[automations] could not load overrides; showing defaults", err);
187
658
  }
188
- setMessages(resolveMessages(overrides, runtime?.events ?? []));
189
- }, [store, tenantId, runtime]);
190
- React3.useEffect(() => {
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 toggle = async (m, enabled) => {
194
- setMessages((prev) => prev?.map((x) => x.event.name === m.event.name && x.channel === m.channel ? { ...x, enabled } : x) ?? null);
195
- try {
196
- await store.upsert({ eventKey: m.event.name, channel: m.channel, enabled }, tenantId);
197
- await reload();
198
- } catch (err) {
199
- console.warn("[automations] toggle failed", err);
200
- toast.error("N\xE3o foi poss\xEDvel salvar");
201
- await reload();
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
- const visible = (messages ?? []).filter((m) => kind === "all" || m.kind === kind);
205
- const ownedVisible = kind === "all" ? owned : [];
206
- const activeCount = (messages ?? []).filter((m) => m.enabled).length + owned.length;
207
- const totalCount = (messages?.length ?? 0) + owned.length;
208
- const groups = [];
209
- for (const m of visible) {
210
- let g = groups.find((x) => x.module === m.event.pluginId);
211
- if (!g) {
212
- g = { module: m.event.pluginId, label: m.event.pluginName, items: [] };
213
- groups.push(g);
214
- }
215
- g.items.push(m);
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
- return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
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
- PageTabs,
818
+ HeaderTabs,
220
819
  {
221
- className: "mb-0",
222
- tabs: KIND_TABS,
223
- value: kind,
224
- onChange: setKind,
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__ */ jsx("p", { className: "text-sm text-muted-foreground", children: KIND_HINT[kind] }),
235
- messages === null ? /* @__PURE__ */ jsx("div", { className: "space-y-3", children: Array.from({ length: 4 }).map((_, i) => /* @__PURE__ */ jsxs(Card, { className: "p-4", children: [
236
- /* @__PURE__ */ jsx("div", { className: "h-4 w-40 animate-pulse rounded bg-muted" }),
237
- /* @__PURE__ */ jsx("div", { className: "mt-2 h-3 w-64 animate-pulse rounded bg-muted" })
238
- ] }, i)) }) : groups.length === 0 && ownedVisible.length === 0 ? /* @__PURE__ */ jsxs(Card, { className: "flex flex-col items-center gap-1 p-12 text-center", children: [
239
- /* @__PURE__ */ jsx(Zap, { className: "mb-2 h-9 w-9 text-muted-foreground/40", strokeWidth: 1.5 }),
240
- /* @__PURE__ */ jsx("p", { className: "text-base font-semibold", children: "Nenhuma automa\xE7\xE3o nesta aba" }),
241
- /* @__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." })
242
- ] }) : groups.map((group) => /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
243
- /* @__PURE__ */ jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: group.label }),
244
- group.items.map((m) => {
245
- const Icon = iconFor(m.event.icon ?? m.event.pluginIcon);
246
- const ChannelIcon = CHANNEL_ICON[m.channel];
247
- const href = `/automations/${encodeURIComponent(m.event.name)}/${m.channel}`;
248
- return /* @__PURE__ */ jsx(
249
- Card,
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
- role: "button",
252
- tabIndex: 0,
253
- onClick: () => adminNavigateTo(href),
254
- onKeyDown: (e) => {
255
- if (e.key === "Enter" || e.key === " ") {
256
- e.preventDefault();
257
- adminNavigateTo(href);
258
- }
259
- },
260
- className: "cursor-pointer overflow-hidden transition-colors hover:border-primary/40 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
261
- children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-4 p-4", children: [
262
- /* @__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" }) }),
263
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
264
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
265
- /* @__PURE__ */ jsx("p", { className: "font-medium", children: m.override?.name ?? m.event.title ?? m.event.name }),
266
- /* @__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: [
267
- /* @__PURE__ */ jsx(ChannelIcon, { className: "h-3 w-3" }),
268
- CHANNEL_LABEL[m.channel]
269
- ] }),
270
- m.customized && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-primary/10 px-2 py-0.5 text-[11px] text-primary", children: "personalizado" })
271
- ] }),
272
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: m.event.description }),
273
- /* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground", children: [
274
- /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
275
- /* @__PURE__ */ jsx(Zap, { className: "h-3 w-3" }),
276
- m.event.trigger
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
- m.subject && /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
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__ */ jsxs("div", { className: "flex shrink-0 items-center gap-3", children: [
285
- /* @__PURE__ */ jsx(
286
- Switch,
287
- {
288
- checked: m.enabled,
289
- onCheckedChange: (next) => void toggle(m, next),
290
- label: m.enabled ? "Ativo" : "Inativo",
291
- "aria-label": `${m.event.title} \u2014 ${CHANNEL_LABEL[m.channel]}`
292
- }
293
- ),
294
- /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4 text-muted-foreground" })
295
- ] })
296
- ] })
297
- },
298
- `${m.event.name}:${m.channel}`
299
- );
300
- })
301
- ] }, group.module)),
302
- messages !== null && /* @__PURE__ */ jsx(ConnectorAutomations, { rows: ownedVisible })
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
- function toTemplate(automation) {
306
- const eventKey = automation.triggerConfig?.eventKey;
307
- if (!eventKey) return null;
308
- const action = automation.actions?.[0];
309
- return {
310
- id: automation.id,
311
- tenantId: automation.tenantId ?? "",
312
- eventKey,
313
- channel: automation.triggerConfig.sourceChannel ?? "email",
314
- enabled: automation.enabled,
315
- // Empty and absent mean the same — "no customisation", the catalogue default.
316
- subject: action?.config?.subject || null,
317
- body: action?.config?.body || null,
318
- // The automation name belongs to the platform, not a user rename: the
319
- // catalogue title still owns the screen.
320
- name: null,
321
- updatedAt: automation.updatedAt
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
- const LIST_TTL_MS = 3e4;
354
- let cache = null;
355
- function invalidate() {
356
- cache = null;
357
- }
358
- function load(tenantId) {
359
- if (cache && cache.tenantId === tenantId && Date.now() - cache.at < LIST_TTL_MS) return cache.promise;
360
- const promise = refresh(tenantId);
361
- promise.catch(() => {
362
- if (cache?.promise === promise) cache = null;
363
- });
364
- cache = { tenantId, at: Date.now(), promise };
365
- return promise;
366
- }
367
- async function requireToken() {
368
- const token = await getSessionToken();
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
- var cachedClient = null;
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
- var STATUS = {
455
- sent: { label: "Enviado", hint: "A mensagem saiu.", variant: "success", Icon: CircleCheck },
456
- recorded: {
457
- label: "Disparou",
458
- // Today's honest state: the trigger works, the send doesn't exist yet.
459
- hint: "O gatilho disparou e ficou registrado. O envio autom\xE1tico ainda n\xE3o est\xE1 ligado neste ambiente.",
460
- variant: "info",
461
- Icon: History
462
- },
463
- skipped: { label: "Ignorado", hint: "O gatilho disparou, mas nada foi enviado \u2014 automa\xE7\xE3o desligada ou sem canal.", variant: "warning", Icon: CircleSlash },
464
- failed: { label: "Falhou", hint: "A tentativa de envio quebrou. O erro est\xE1 na linha.", variant: "destructive", Icon: AlertTriangle }
465
- };
466
- function when(iso) {
467
- const then = new Date(iso).getTime();
468
- const diff = Date.now() - then;
469
- const min = Math.round(diff / 6e4);
470
- if (min < 1) return "agora";
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 RunRow({ run }) {
477
- const [open, setOpen] = React3.useState(false);
478
- const cfg = STATUS[run.status] ?? STATUS.recorded;
479
- const { Icon } = cfg;
480
- return /* @__PURE__ */ jsxs("div", { className: "border-b border-border last:border-b-0", children: [
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
- "button",
1073
+ "div",
483
1074
  {
484
- type: "button",
485
- onClick: () => setOpen(!open),
486
- className: "flex w-full items-center gap-3 px-4 py-2.5 text-left transition-colors hover:bg-muted/40",
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(ChevronRight, { className: `h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform ${open ? "rotate-90" : ""}` }),
489
- /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
490
- /* @__PURE__ */ jsx(Badge, { variant: cfg.variant, children: cfg.label }),
491
- /* @__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" }),
492
- /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: when(run.occurredAt) })
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
- open && /* @__PURE__ */ jsxs("div", { className: "space-y-3 bg-muted/20 px-4 pb-4 pt-1", children: [
497
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: cfg.hint }),
498
- /* @__PURE__ */ jsxs("div", { children: [
499
- /* @__PURE__ */ jsx("p", { className: "mb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: "Dados do disparo" }),
500
- /* @__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) })
501
- ] }),
502
- run.result && /* @__PURE__ */ jsxs("div", { children: [
503
- /* @__PURE__ */ jsx("p", { className: "mb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: "Resultado" }),
504
- /* @__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) })
505
- ] }),
506
- 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 })
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
- function RunLog({ eventKey, tenantId, enabled }) {
511
- const [runs, setRuns] = React3.useState(null);
512
- const [loading, setLoading] = React3.useState(false);
513
- const load = React3.useCallback(async () => {
514
- setLoading(true);
515
- setRuns(await loadEventRuns(eventKey, { tenantId, limit: 20 }));
516
- setLoading(false);
517
- }, [eventKey, tenantId]);
518
- React3.useEffect(() => {
519
- void load();
520
- }, [load]);
521
- return /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
522
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-border px-4 py-2.5", children: [
523
- /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
524
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Execu\xE7\xF5es recentes" }),
525
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Cada vez que este gatilho disparou nesta conta." })
526
- ] }),
527
- /* @__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" : ""}` }) })
528
- ] }),
529
- 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: [
530
- /* @__PURE__ */ jsx(History, { className: "h-7 w-7 text-muted-foreground/40", strokeWidth: 1.5 }),
531
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: "Nenhum disparo ainda" }),
532
- /* @__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." })
533
- ] }) : /* @__PURE__ */ jsx("div", { children: runs.map((run) => /* @__PURE__ */ jsx(RunRow, { run }, run.id)) })
534
- ] });
535
- }
536
- var EXEC_STATUS = {
537
- success: { label: "Executada", variant: "success", Icon: CircleCheck },
538
- error: { label: "Falhou", variant: "destructive", Icon: AlertTriangle }
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 SAMPLE = {
601
- store_name: "Sua Loja",
602
- company_name: "Sua Empresa",
603
- customer_name: "Maria Silva",
604
- contact_name: "Maria Silva",
605
- lead_name: "Maria Silva",
606
- order_number: "1042",
607
- order_total: "R$ 189,90",
608
- order_items: "1 \xD7 Camiseta Cl\xE1ssica \u2014 R$ 89,90\n2 \xD7 Caneca Esmaltada \u2014 R$ 100,00",
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 iconFor2(name) {
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 AutomationDetailPage({ eventKey, channel, store, backTo = "/automations" }) {
624
- const tenantId = useOrganizationStoreLike();
625
- const runtime = usePluginRuntimeOptional();
626
- const fayzConnection = React3.useMemo(() => resolveFayzAgentConnection(), []);
627
- const [message, setMessage] = React3.useState(void 0);
628
- const [tab, setTab] = React3.useState("preview");
629
- const [device, setDevice] = React3.useState("desktop");
630
- const [draft, setDraft] = React3.useState(null);
631
- const [saving, setSaving] = React3.useState(false);
632
- const back = React3.useCallback(() => adminNavigateTo(backTo), [backTo]);
633
- const reload = React3.useCallback(async () => {
634
- if (!eventKey || !channel) {
635
- setMessage(null);
636
- return;
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
- const found = resolveMessage(decodeURIComponent(eventKey), channel, overrides, runtime?.events ?? []);
645
- setMessage(found);
646
- if (found) setDraft({ subject: found.subject, body: found.body });
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 header = /* @__PURE__ */ jsx(
652
- SubpageHeader,
653
- {
654
- title: message === void 0 ? "" : message?.override?.name ?? message?.event.title ?? message?.event.name ?? "Automa\xE7\xE3o",
655
- parentLabel: "Automa\xE7\xF5es",
656
- onBack: back
657
- }
658
- );
659
- if (message === void 0) {
660
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
661
- header,
662
- /* @__PURE__ */ jsx("div", { className: "h-8 w-64 animate-pulse rounded bg-muted" }),
663
- /* @__PURE__ */ jsx(Card, { className: "h-96 animate-pulse" })
664
- ] });
665
- }
666
- if (!message) {
667
- return /* @__PURE__ */ jsxs(Fragment, { children: [
668
- header,
669
- /* @__PURE__ */ jsxs(Card, { className: "flex flex-col items-center gap-2 p-12 text-center", children: [
670
- /* @__PURE__ */ jsx(Mail, { className: "h-9 w-9 text-muted-foreground/40", strokeWidth: 1.5 }),
671
- /* @__PURE__ */ jsx("p", { className: "font-semibold", children: "Automa\xE7\xE3o n\xE3o encontrada" }),
672
- /* @__PURE__ */ jsxs("p", { className: "max-w-sm text-sm text-muted-foreground", children: [
673
- "O evento ",
674
- /* @__PURE__ */ jsx("code", { className: "font-mono", children: eventKey }),
675
- " n\xE3o est\xE1 no cat\xE1logo \u2014 o m\xF3dulo que o registra pode n\xE3o estar instalado nesta conta."
676
- ] }),
677
- /* @__PURE__ */ jsx(Button, { variant: "outline", className: "mt-2", onClick: back, children: "Voltar para Automa\xE7\xF5es" })
678
- ] })
679
- ] });
680
- }
681
- const event = message.event;
682
- const fallback = { subject: message.override ? void 0 : message.subject, body: message.body };
683
- const Icon = iconFor2(event.icon);
684
- const current = draft ?? { subject: message.subject, body: message.body };
685
- const dirty = current.subject !== message.subject || current.body !== message.body;
686
- const hasSubject = message.channel === "email";
687
- const patch = async (input) => {
688
- setSaving(true);
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(input, tenantId);
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] save failed", err);
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 previewSubject = renderMessageTemplate(current.subject, SAMPLE);
721
- const previewBody = renderMessageTemplate(current.body, SAMPLE);
722
- return /* @__PURE__ */ jsxs("div", { className: "space-y-5", children: [
723
- header,
724
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-start gap-3", children: [
725
- /* @__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" }) }),
726
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
727
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
728
- /* @__PURE__ */ jsx(Badge, { variant: "outline", children: CHANNEL_LABEL2[message.channel] }),
729
- /* @__PURE__ */ jsx(Badge, { variant: "outline", children: message.kind === "marketing" ? "Marketing" : "Transacional" }),
730
- message.customized && /* @__PURE__ */ jsx(Badge, { variant: "info", children: "Personalizado" })
731
- ] }),
732
- /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-sm text-muted-foreground", children: event.description })
733
- ] }),
734
- /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-wrap items-center gap-2", children: [
735
- /* @__PURE__ */ jsx(Hint, { label: "Descarta o texto personalizado e volta ao padr\xE3o do produto.", children: /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: () => void reset(), disabled: !message.customized || saving, children: [
736
- /* @__PURE__ */ jsx(RotateCcw, { className: "mr-2 h-4 w-4" }),
737
- "Restaurar padr\xE3o"
738
- ] }) }) }),
739
- /* @__PURE__ */ jsx(
740
- Switch,
741
- {
742
- size: "md",
743
- checked: message.enabled,
744
- onCheckedChange: (next) => void patch({ eventKey: event.name, channel: message.channel, enabled: next }),
745
- disabled: saving,
746
- label: message.enabled ? "Ativa" : "Inativa"
747
- }
748
- ),
749
- /* @__PURE__ */ jsx(Button, { onClick: save, disabled: !dirty || saving, children: saving ? "Salvando\u2026" : dirty ? "Salvar" : "Salvo" })
750
- ] })
751
- ] }),
752
- /* @__PURE__ */ jsx(HeaderTabs, { tabs: TABS, value: tab, onChange: setTab, "aria-label": "Visualiza\xE7\xE3o" }),
753
- /* @__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: [
754
- /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
755
- /* @__PURE__ */ jsx(Zap, { className: "h-3.5 w-3.5" }),
756
- "Gatilho:",
757
- /* @__PURE__ */ jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-[11px] text-foreground", children: event.name })
758
- ] }),
759
- /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
760
- /* @__PURE__ */ jsx(Clock, { className: "h-3.5 w-3.5" }),
761
- event.trigger
762
- ] }),
763
- hasSubject && current.subject && /* @__PURE__ */ jsxs("span", { className: "min-w-0 truncate", children: [
764
- "Assunto: ",
765
- current.subject
766
- ] })
767
- ] }),
768
- tab === "runs" ? (
769
- // Atrás de um projeto Fayz o histórico verdadeiro é o da plataforma —
770
- // é ela quem executa. Fora dele, o coletor local continua valendo.
771
- fayzConnection ? /* @__PURE__ */ jsx(FayzRunLog, { connection: fayzConnection, eventKey: event.name, tenantId, enabled: message.enabled }) : /* @__PURE__ */ jsx(RunLog, { eventKey: event.name, tenantId, enabled: message.enabled })
772
- ) : tab === "preview" ? /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
773
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-border px-4 py-2.5", children: [
774
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Pr\xE9via" }),
775
- /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: ["desktop", "mobile"].map((d) => /* @__PURE__ */ jsx(
776
- "button",
777
- {
778
- type: "button",
779
- onClick: () => setDevice(d),
780
- "aria-label": d === "desktop" ? "Computador" : "Celular",
781
- 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"}`,
782
- children: d === "desktop" ? /* @__PURE__ */ jsx(Monitor, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(Smartphone, { className: "h-4 w-4" })
783
- },
784
- d
785
- )) })
786
- ] }),
787
- /* @__PURE__ */ jsxs("div", { className: "bg-muted/30 p-6", children: [
788
- /* @__PURE__ */ jsx(
789
- "div",
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
- className: `mx-auto rounded-lg border border-border bg-card shadow-sm transition-[max-width] ${device === "mobile" ? "max-w-[380px]" : hasSubject ? "max-w-[600px]" : "max-w-[420px]"}`,
792
- children: hasSubject ? /* @__PURE__ */ jsxs(Fragment, { children: [
793
- /* @__PURE__ */ jsx("div", { className: "border-b border-border px-6 py-4 text-sm font-semibold", children: SAMPLE.store_name }),
794
- /* @__PURE__ */ jsxs("div", { className: "space-y-1 border-b border-border px-6 py-3", children: [
795
- /* @__PURE__ */ jsx("p", { className: "text-[11px] uppercase tracking-wide text-muted-foreground", children: "Assunto" }),
796
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: previewSubject })
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__ */ jsx("div", { className: "whitespace-pre-wrap px-6 py-5 text-sm leading-relaxed", children: previewBody })
799
- ] }) : /* @__PURE__ */ jsx("div", { className: "p-4", children: /* @__PURE__ */ jsx("div", { className: "whitespace-pre-wrap rounded-2xl rounded-tl-sm bg-[#dcf8c6] px-3 py-2 text-sm leading-relaxed text-[#111b21]", children: previewBody }) })
800
- }
801
- ),
802
- /* @__PURE__ */ jsx("p", { className: "mt-4 text-center text-xs text-muted-foreground", children: "Dados de exemplo \u2014 nenhum registro real foi usado." })
803
- ] })
804
- ] }) : /* @__PURE__ */ jsxs(Card, { className: "space-y-4 p-4", children: [
805
- hasSubject && /* @__PURE__ */ jsxs("div", { children: [
806
- /* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "Assunto" }),
807
- /* @__PURE__ */ jsx(
808
- "input",
809
- {
810
- value: current.subject,
811
- onChange: (e) => setDraft({ ...current, subject: e.target.value }),
812
- 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"
813
- }
814
- )
815
- ] }),
816
- /* @__PURE__ */ jsxs("div", { children: [
817
- /* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "Mensagem" }),
818
- /* @__PURE__ */ jsx(
819
- "textarea",
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
- rows: 14,
822
- value: current.body,
823
- onChange: (e) => setDraft({ ...current, body: e.target.value }),
824
- 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"
1460
+ eventKey: selected.eventKey,
1461
+ channel: selected.channel,
1462
+ store,
1463
+ backTo: basePath,
1464
+ embedded: true
825
1465
  }
826
- )
827
- ] }),
828
- /* @__PURE__ */ jsxs("div", { children: [
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 = () => React3.createElement(AutomationsList, { store });
1537
+ const ListPage = () => React4__default.createElement(AutomationsList, { store });
898
1538
  ListPage.displayName = "AutomationsList";
899
- const DetailPage = (props) => React3.createElement(AutomationDetailPage, {
900
- ...props,
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
- // "Back" must return you through the door you came in: opening the list
903
- // inside settings must not drop you on a loose page that app's menu does
904
- // not have.
905
- backTo: inSettings ? "/settings/automations" : "/automations"
1545
+ backTo: inSettings ? "/settings/automations" : "/automations",
1546
+ embedded: true,
1547
+ onDone: onClose
906
1548
  });
907
- DetailPage.displayName = "AutomationDetail";
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: ListPage, permission: READ }
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
- // One automation's detail. Off-menu: it is a row of the list. Its back
937
- // link comes from `backTo`.
938
- { path: "/automations/:eventKey/:channel", component: DetailPage, permission: READ }
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