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