@adatechnology/notification-ui 0.1.0-rc.2 → 0.1.0-rc.4

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.
@@ -5,7 +5,8 @@ import { createContext, useContext, useMemo } from "react";
5
5
  var pt_BR_default = {
6
6
  "bell.label": "Notifica\xE7\xF5es",
7
7
  "bell.unreadSuffix": "n\xE3o lidas",
8
- "list.title": "Notifica\xE7\xF5es",
8
+ "list.title": "Caixa de entrada",
9
+ "list.unread": "n\xE3o lidas",
9
10
  "list.empty": "Nenhuma notifica\xE7\xE3o por aqui.",
10
11
  "list.loading": "Carregando\u2026",
11
12
  "list.error": "N\xE3o foi poss\xEDvel carregar suas notifica\xE7\xF5es.",
@@ -47,14 +48,34 @@ var pt_BR_default = {
47
48
  "settings.saving": "Salvando\u2026",
48
49
  "settings.unchanged": "Nada mudou ainda.",
49
50
  "settings.close": "Fechar",
50
- "settings.version": "v"
51
+ "settings.version": "v",
52
+ "settings.newTemplate": "Nova mensagem",
53
+ "settings.remove": "Remover",
54
+ "settings.key": "Chave da notifica\xE7\xE3o",
55
+ "settings.keyHint": "N\xE3o muda depois de criada \u2014 \xE9 por ela que o envio acha a mensagem.",
56
+ "settings.channel": "Canal",
57
+ "settings.locale": "Idioma",
58
+ "settings.variablesTitle": "Vari\xE1veis",
59
+ "settings.variablesHint": "Clique para inserir no ponto onde o cursor estava.",
60
+ "settings.unknownVariables": "Vari\xE1veis que n\xE3o existem nesta notifica\xE7\xE3o:",
61
+ "settings.viewport.desktop": "Computador",
62
+ "settings.viewport.mobile": "Celular",
63
+ "settings.constraint.title": "Assunto passa do limite do canal:",
64
+ "settings.constraint.body": "Texto passa do limite do canal:",
65
+ "settings.tab.messages": "Mensagens",
66
+ "settings.tab.routing": "Roteamento",
67
+ "settings.policiesTitle": "Canais permitidos por assunto",
68
+ "settings.policiesHint": "Decis\xE3o da empresa. Canal desligado aqui n\xE3o sai, mesmo que o destinat\xE1rio tenha pedido.",
69
+ "settings.savePolicies": "Salvar roteamento",
70
+ "settings.discard": "Descartar"
51
71
  };
52
72
 
53
73
  // src/locales/en.json
54
74
  var en_default = {
55
75
  "bell.label": "Notifications",
56
76
  "bell.unreadSuffix": "unread",
57
- "list.title": "Notifications",
77
+ "list.title": "Inbox",
78
+ "list.unread": "unread",
58
79
  "list.empty": "Nothing here yet.",
59
80
  "list.loading": "Loading\u2026",
60
81
  "list.error": "We couldn't load your notifications.",
@@ -96,7 +117,26 @@ var en_default = {
96
117
  "settings.saving": "Saving\u2026",
97
118
  "settings.unchanged": "Nothing changed yet.",
98
119
  "settings.close": "Close",
99
- "settings.version": "v"
120
+ "settings.version": "v",
121
+ "settings.newTemplate": "New message",
122
+ "settings.remove": "Remove",
123
+ "settings.key": "Notification key",
124
+ "settings.keyHint": "Fixed once created \u2014 sending looks the message up by it.",
125
+ "settings.channel": "Channel",
126
+ "settings.locale": "Language",
127
+ "settings.variablesTitle": "Variables",
128
+ "settings.variablesHint": "Click to insert where the cursor was.",
129
+ "settings.unknownVariables": "Variables this notification does not have:",
130
+ "settings.viewport.desktop": "Desktop",
131
+ "settings.viewport.mobile": "Mobile",
132
+ "settings.constraint.title": "Subject exceeds the channel limit:",
133
+ "settings.constraint.body": "Body exceeds the channel limit:",
134
+ "settings.tab.messages": "Messages",
135
+ "settings.tab.routing": "Routing",
136
+ "settings.policiesTitle": "Allowed channels per topic",
137
+ "settings.policiesHint": "Company decision. A channel turned off here never sends, even if the recipient opted in.",
138
+ "settings.savePolicies": "Save routing",
139
+ "settings.discard": "Discard"
100
140
  };
101
141
 
102
142
  // src/locales/index.ts
@@ -123,6 +163,7 @@ function NotificationProvider({
123
163
  const value = useMemo(
124
164
  () => ({
125
165
  client,
166
+ locale,
126
167
  messages: { ...resolveMessages(locale), ...messageOverrides },
127
168
  theme,
128
169
  pollIntervalSeconds
@@ -146,7 +187,9 @@ var NOTIFICATION_QUERY_KEYS = {
146
187
  list: (filters) => ["notifications", "list", filters],
147
188
  unreadCount: () => ["notifications", "unread-count"],
148
189
  preferences: () => ["notifications", "preferences"],
149
- templates: () => ["notifications", "templates"]
190
+ templates: () => ["notifications", "templates"],
191
+ templateVariables: () => ["notifications", "template-variables"],
192
+ categoryPolicies: () => ["notifications", "category-policies"]
150
193
  };
151
194
 
152
195
  // src/hooks/useNotifications.ts
@@ -253,19 +296,83 @@ function useNotificationStream(options = {}) {
253
296
  }, [client, queryClient, enabled, onEvent]);
254
297
  }
255
298
 
256
- // src/hooks/usePreferences.ts
299
+ // src/hooks/useCategoryPolicies.ts
300
+ import { useMemo as useMemo2, useState } from "react";
257
301
  import { useMutation as useMutation2, useQuery as useQuery2, useQueryClient as useQueryClient3 } from "@tanstack/react-query";
302
+ function identityOf(params) {
303
+ return `${params.category}:${params.channel}`;
304
+ }
305
+ function useCategoryPolicies() {
306
+ const { client } = useNotificationContext();
307
+ const queryClient = useQueryClient3();
308
+ const policiesQuery = useQuery2({
309
+ queryKey: NOTIFICATION_QUERY_KEYS.categoryPolicies(),
310
+ queryFn: () => client.getCategoryPolicies()
311
+ });
312
+ const mutation = useMutation2({
313
+ mutationFn: (policies) => client.updateCategoryPolicies(policies),
314
+ onSuccess() {
315
+ void queryClient.invalidateQueries({ queryKey: NOTIFICATION_QUERY_KEYS.categoryPolicies() });
316
+ }
317
+ });
318
+ const [touched, setTouched] = useState({});
319
+ const saved = useMemo2(() => {
320
+ const byIdentity = /* @__PURE__ */ new Map();
321
+ for (const policy of policiesQuery.data ?? []) byIdentity.set(identityOf(policy), policy.enabled);
322
+ return byIdentity;
323
+ }, [policiesQuery.data]);
324
+ function isAllowed(params) {
325
+ const identity = identityOf(params);
326
+ return touched[identity] ?? saved.get(identity) ?? true;
327
+ }
328
+ return {
329
+ isLoading: policiesQuery.isLoading,
330
+ isSaving: mutation.isPending,
331
+ error: mutation.error?.message,
332
+ isDirty: Object.keys(touched).length > 0,
333
+ isAllowed,
334
+ toggle(params) {
335
+ const identity = identityOf(params);
336
+ setTouched((current) => ({ ...current, [identity]: !isAllowed(params) }));
337
+ },
338
+ save() {
339
+ const categories = new Set(Object.keys(touched).map((identity) => identity.split(":")[0] ?? ""));
340
+ const policies = [];
341
+ for (const category of categories) {
342
+ const channels = /* @__PURE__ */ new Set([
343
+ ...[...saved.keys()].filter((identity) => identity.startsWith(`${category}:`)),
344
+ ...Object.keys(touched).filter((identity) => identity.startsWith(`${category}:`))
345
+ ]);
346
+ for (const identity of channels) {
347
+ const channel = identity.slice(category.length + 1);
348
+ policies.push({
349
+ category,
350
+ channel,
351
+ enabled: isAllowed({ category, channel })
352
+ });
353
+ }
354
+ }
355
+ mutation.mutate(policies, { onSuccess: () => setTouched({}) });
356
+ },
357
+ reset() {
358
+ setTouched({});
359
+ }
360
+ };
361
+ }
362
+
363
+ // src/hooks/usePreferences.ts
364
+ import { useMutation as useMutation3, useQuery as useQuery3, useQueryClient as useQueryClient4 } from "@tanstack/react-query";
258
365
  function usePreferences() {
259
366
  const { client } = useNotificationContext();
260
- return useQuery2({
367
+ return useQuery3({
261
368
  queryKey: NOTIFICATION_QUERY_KEYS.preferences(),
262
369
  queryFn: () => client.getPreferences()
263
370
  });
264
371
  }
265
372
  function useUpdatePreferences() {
266
373
  const { client } = useNotificationContext();
267
- const queryClient = useQueryClient3();
268
- return useMutation2({
374
+ const queryClient = useQueryClient4();
375
+ return useMutation3({
269
376
  mutationFn: (preferences) => client.updatePreferences(preferences),
270
377
  onSuccess(saved) {
271
378
  queryClient.setQueryData(NOTIFICATION_QUERY_KEYS.preferences(), saved);
@@ -274,25 +381,77 @@ function useUpdatePreferences() {
274
381
  }
275
382
  function useTemplates() {
276
383
  const { client } = useNotificationContext();
277
- return useQuery2({
384
+ return useQuery3({
278
385
  queryKey: NOTIFICATION_QUERY_KEYS.templates(),
279
386
  queryFn: () => client.listTemplates()
280
387
  });
281
388
  }
282
389
  function useUpsertTemplate() {
283
390
  const { client } = useNotificationContext();
284
- const queryClient = useQueryClient3();
285
- return useMutation2({
391
+ const queryClient = useQueryClient4();
392
+ return useMutation3({
286
393
  mutationFn: (input) => client.upsertTemplate(input),
287
394
  onSuccess() {
288
395
  void queryClient.invalidateQueries({ queryKey: NOTIFICATION_QUERY_KEYS.templates() });
289
396
  }
290
397
  });
291
398
  }
399
+ function useTemplateVariables() {
400
+ const { client } = useNotificationContext();
401
+ return useQuery3({
402
+ queryKey: NOTIFICATION_QUERY_KEYS.templateVariables(),
403
+ queryFn: () => client.listTemplateVariables(),
404
+ staleTime: Number.POSITIVE_INFINITY
405
+ });
406
+ }
407
+ function useDeactivateTemplate() {
408
+ const { client } = useNotificationContext();
409
+ const queryClient = useQueryClient4();
410
+ return useMutation3({
411
+ mutationFn: (id) => client.deactivateTemplate(id),
412
+ onSuccess() {
413
+ void queryClient.invalidateQueries({ queryKey: NOTIFICATION_QUERY_KEYS.templates() });
414
+ }
415
+ });
416
+ }
417
+
418
+ // src/format/notificationTime.ts
419
+ var SECOND = 1e3;
420
+ var MINUTE = 60 * SECOND;
421
+ var HOUR = 60 * MINUTE;
422
+ var DAY = 24 * HOUR;
423
+ var RELATIVE_HORIZON = 7 * DAY;
424
+ function parseInstant(isoTimestamp) {
425
+ const instant = new Date(isoTimestamp);
426
+ return Number.isNaN(instant.getTime()) ? void 0 : instant;
427
+ }
428
+ function formatNotificationTime(isoTimestamp, locale, now) {
429
+ const instant = parseInstant(isoTimestamp);
430
+ if (!instant) return "";
431
+ const elapsed = Math.max(0, now.getTime() - instant.getTime());
432
+ if (elapsed >= RELATIVE_HORIZON) {
433
+ return new Intl.DateTimeFormat(locale, { day: "2-digit", month: "2-digit", year: "numeric" }).format(instant);
434
+ }
435
+ const relative = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
436
+ if (elapsed < MINUTE) return relative.format(0, "second");
437
+ if (elapsed < HOUR) return relative.format(-Math.floor(elapsed / MINUTE), "minute");
438
+ if (elapsed < DAY) return relative.format(-Math.floor(elapsed / HOUR), "hour");
439
+ return relative.format(-Math.floor(elapsed / DAY), "day");
440
+ }
441
+ function formatNotificationTimestamp(isoTimestamp, locale) {
442
+ const instant = parseInstant(isoTimestamp);
443
+ if (!instant) return "";
444
+ return new Intl.DateTimeFormat(locale, { dateStyle: "short", timeStyle: "short" }).format(instant);
445
+ }
292
446
 
293
447
  // src/hooks/useTemplateEditor.ts
294
- import { useMemo as useMemo2, useState } from "react";
295
- import { renderTemplate } from "@adatechnology/notification-contracts";
448
+ import { useMemo as useMemo3, useState as useState2 } from "react";
449
+ import {
450
+ PREVIEW_VIEWPORT_BY_CHANNEL,
451
+ buildPreviewPayload,
452
+ diffTemplateVariables,
453
+ renderTemplate
454
+ } from "@adatechnology/notification-contracts";
296
455
  function toDraft(template) {
297
456
  return {
298
457
  key: template.key,
@@ -303,12 +462,18 @@ function toDraft(template) {
303
462
  whatsappTemplateName: template.whatsappTemplateName ?? ""
304
463
  };
305
464
  }
465
+ function buildEmptyDraft(params) {
466
+ return { key: "", channel: params.channel, locale: params.locale, subject: "", body: "", whatsappTemplateName: "" };
467
+ }
306
468
  function useTemplateEditor(params = {}) {
307
469
  const templatesQuery = useTemplates();
470
+ const variablesQuery = useTemplateVariables();
308
471
  const upsert = useUpsertTemplate();
309
- const [selectedId, setSelectedId] = useState(void 0);
310
- const [draft, setDraft] = useState(void 0);
311
- const templates = useMemo2(() => {
472
+ const deactivateMutation = useDeactivateTemplate();
473
+ const [selectedId, setSelectedId] = useState2(void 0);
474
+ const [draft, setDraft] = useState2(void 0);
475
+ const [isNew, setIsNew] = useState2(false);
476
+ const templates = useMemo3(() => {
312
477
  const activeByIdentity = /* @__PURE__ */ new Map();
313
478
  for (const template of templatesQuery.data ?? []) {
314
479
  if (!template.active) continue;
@@ -320,37 +485,75 @@ function useTemplateEditor(params = {}) {
320
485
  (left, right) => left.key.localeCompare(right.key) || left.channel.localeCompare(right.channel)
321
486
  );
322
487
  }, [templatesQuery.data]);
323
- const selected = useMemo2(() => templates.find((template) => template.id === selectedId), [templates, selectedId]);
324
- const preview = useMemo2(() => {
325
- if (!draft) return void 0;
326
- return renderTemplate({
488
+ const selected = useMemo3(() => templates.find((template) => template.id === selectedId), [templates, selectedId]);
489
+ const variables = useMemo3(() => draft ? variablesQuery.data?.[draft.key] ?? [] : [], [variablesQuery.data, draft]);
490
+ const variableDiff = useMemo3(
491
+ () => diffTemplateVariables({
492
+ body: draft?.body ?? "",
493
+ subject: draft?.subject || void 0,
494
+ // Chave sem entrada no catálogo é catálogo não declarado, não catálogo vazio — o servidor
495
+ // aceita qualquer variável nesse caso, e a tela não pode acusar o que a rota vai gravar.
496
+ variables: draft && variablesQuery.data?.[draft.key] ? variables : void 0
497
+ }),
498
+ [draft, variables, variablesQuery.data]
499
+ );
500
+ const previews = useMemo3(() => {
501
+ if (!draft) return [];
502
+ const payload = { ...buildPreviewPayload(variables), ...params.previewPayload ?? {} };
503
+ const rendered = renderTemplate({
327
504
  channel: draft.channel,
328
505
  subject: draft.subject || void 0,
329
506
  body: draft.body,
330
- payload: params.previewPayload ?? {}
507
+ payload
331
508
  });
332
- }, [draft, params.previewPayload]);
333
- const isDirty = useMemo2(() => {
334
- if (!draft || !selected) return false;
509
+ const viewports = PREVIEW_VIEWPORT_BY_CHANNEL[draft.channel] ?? [];
510
+ return viewports.map((viewport) => ({ ...viewport, rendered }));
511
+ }, [draft, variables, params.previewPayload]);
512
+ const isDirty = useMemo3(() => {
513
+ if (!draft) return false;
514
+ if (isNew) return draft.key.length > 0 || draft.body.length > 0;
515
+ if (!selected) return false;
335
516
  const original = toDraft(selected);
336
517
  return Object.keys(original).some((field) => original[field] !== draft[field]);
337
- }, [draft, selected]);
518
+ }, [draft, selected, isNew]);
338
519
  return {
339
520
  templates,
340
521
  isLoading: templatesQuery.isLoading,
341
522
  isSaving: upsert.isPending,
342
- error: upsert.error?.message,
523
+ isDeactivating: deactivateMutation.isPending,
524
+ error: upsert.error?.message ?? deactivateMutation.error?.message,
343
525
  selected,
344
526
  draft,
345
- preview,
527
+ isNew,
528
+ isIdentityLocked: !isNew,
529
+ previews,
530
+ variables,
531
+ variableDiff,
346
532
  isDirty,
533
+ // A variável desconhecida é recusada pelo servidor com 400; bloquear o botão evita a ida
534
+ // perdida, sem virar a única defesa — a validação continua sendo a da rota.
535
+ canSave: Boolean(draft?.key && draft.body) && variableDiff.unknown.length === 0,
347
536
  select(template) {
348
537
  setSelectedId(template.id);
349
538
  setDraft(toDraft(template));
539
+ setIsNew(false);
540
+ },
541
+ startNew() {
542
+ setSelectedId(void 0);
543
+ setIsNew(true);
544
+ setDraft(buildEmptyDraft({ channel: params.defaultChannel ?? "email", locale: params.defaultLocale ?? "pt-BR" }));
350
545
  },
351
546
  update(patch) {
352
547
  setDraft((current) => current ? { ...current, ...patch } : current);
353
548
  },
549
+ insertVariable({ name, field, cursorIndex }) {
550
+ setDraft((current) => {
551
+ if (!current) return current;
552
+ const text = current[field];
553
+ const at = Math.min(Math.max(cursorIndex, 0), text.length);
554
+ return { ...current, [field]: `${text.slice(0, at)}{{${name}}}${text.slice(at)}` };
555
+ });
556
+ },
354
557
  save() {
355
558
  if (!draft) return;
356
559
  const body = {
@@ -359,15 +562,30 @@ function useTemplateEditor(params = {}) {
359
562
  locale: draft.locale,
360
563
  active: true,
361
564
  body: draft.body,
362
- // Decisão 3: chave omitida quando vazia.
363
565
  ...draft.subject ? { subject: draft.subject } : {},
364
566
  ...draft.whatsappTemplateName ? { whatsappTemplateName: draft.whatsappTemplateName } : {}
365
567
  };
366
- upsert.mutate(body, { onSuccess: () => setSelectedId(void 0) });
568
+ upsert.mutate(body, {
569
+ onSuccess: () => {
570
+ setSelectedId(void 0);
571
+ setDraft(void 0);
572
+ setIsNew(false);
573
+ }
574
+ });
575
+ },
576
+ deactivate(id) {
577
+ deactivateMutation.mutate(id, {
578
+ onSuccess: () => {
579
+ if (selectedId !== id) return;
580
+ setSelectedId(void 0);
581
+ setDraft(void 0);
582
+ }
583
+ });
367
584
  },
368
585
  clear() {
369
586
  setSelectedId(void 0);
370
587
  setDraft(void 0);
588
+ setIsNew(false);
371
589
  }
372
590
  };
373
591
  }
@@ -385,10 +603,15 @@ export {
385
603
  useDeleteNotification,
386
604
  flattenNotificationPages,
387
605
  useNotificationStream,
606
+ useCategoryPolicies,
388
607
  usePreferences,
389
608
  useUpdatePreferences,
390
609
  useTemplates,
391
610
  useUpsertTemplate,
611
+ useTemplateVariables,
612
+ useDeactivateTemplate,
613
+ formatNotificationTime,
614
+ formatNotificationTimestamp,
392
615
  useTemplateEditor
393
616
  };
394
- //# sourceMappingURL=chunk-FNUYTJ36.js.map
617
+ //# sourceMappingURL=chunk-QBQ62VZ3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/NotificationProvider.tsx","../src/locales/pt-BR.json","../src/locales/en.json","../src/locales/index.ts","../src/hooks/useNotifications.ts","../src/hooks/queryKeys.ts","../src/hooks/useNotificationStream.ts","../src/hooks/useCategoryPolicies.ts","../src/hooks/usePreferences.ts","../src/format/notificationTime.ts","../src/hooks/useTemplateEditor.ts"],"sourcesContent":["/**\n * Copyright (c) 2026 Ada Technology. MIT License.\n *\n * Provider do pacote. **Não instancia `QueryClient`** — usa o do host (regra de módulos\n * plugáveis §4.2). Instanciar o próprio criaria dois caches na mesma página: a inbox invalidaria\n * um, e a tela do produto continuaria lendo o outro.\n */\n\nimport { createContext, useContext, useMemo, type ReactNode } from 'react'\nimport type { NotificationClient } from '@adatechnology/notification-client'\n\nimport { DEFAULT_LOCALE, resolveMessages, type NotificationLocale, type NotificationMessages } from './locales'\n\nexport type NotificationTheme = {\n /** Classe aplicada na raiz de cada componente — é por onde o host injeta os tokens dele. */\n readonly rootClassName?: string\n readonly components?: Partial<Record<string, string>>\n}\n\nexport type NotificationContextValue = {\n readonly client: NotificationClient\n /**\n * O idioma resolvido, e não só os textos dele: data e número não vivem no arquivo de locale —\n * quem os formata é `Intl`, e `Intl` precisa da tag do idioma. Sem isto o item da lista teria de\n * chutar um locale fixo, e a inbox mostraria \"5 minutes ago\" numa tela em português.\n */\n readonly locale: NotificationLocale\n readonly messages: NotificationMessages\n readonly theme: NotificationTheme\n /** Polling de fallback quando o SSE não está disponível (segundos); `0` desliga. */\n readonly pollIntervalSeconds: number\n}\n\nconst NotificationContext = createContext<NotificationContextValue | null>(null)\n\nexport type NotificationProviderProps = {\n readonly client: NotificationClient\n readonly locale?: NotificationLocale\n /** Sobrescreve textos pontuais sem trocar o locale inteiro. */\n readonly messageOverrides?: Partial<NotificationMessages>\n readonly theme?: NotificationTheme\n readonly pollIntervalSeconds?: number\n readonly children: ReactNode\n}\n\nexport function NotificationProvider({\n client,\n locale = DEFAULT_LOCALE,\n messageOverrides,\n theme = {},\n pollIntervalSeconds = 60,\n children,\n}: NotificationProviderProps) {\n const value = useMemo<NotificationContextValue>(\n () => ({\n client,\n locale,\n messages: { ...resolveMessages(locale), ...messageOverrides },\n theme,\n pollIntervalSeconds,\n }),\n [client, locale, messageOverrides, theme, pollIntervalSeconds],\n )\n\n return <NotificationContext.Provider value={value}>{children}</NotificationContext.Provider>\n}\n\nexport function useNotificationContext(): NotificationContextValue {\n const context = useContext(NotificationContext)\n if (!context) throw new Error('notification-ui: componente usado fora de <NotificationProvider>')\n return context\n}\n","{\n \"bell.label\": \"Notificações\",\n \"bell.unreadSuffix\": \"não lidas\",\n \"list.title\": \"Caixa de entrada\",\n \"list.unread\": \"não lidas\",\n \"list.empty\": \"Nenhuma notificação por aqui.\",\n \"list.loading\": \"Carregando…\",\n \"list.error\": \"Não foi possível carregar suas notificações.\",\n \"list.retry\": \"Tentar de novo\",\n \"list.loadMore\": \"Carregar mais\",\n \"list.markAllAsRead\": \"Marcar todas como lidas\",\n \"item.markAsRead\": \"Marcar como lida\",\n \"item.delete\": \"Excluir\",\n \"item.unread\": \"Não lida\",\n \"preferences.title\": \"Preferências de notificação\",\n \"preferences.description\": \"Escolha por onde você quer ser avisado em cada assunto.\",\n \"preferences.save\": \"Salvar preferências\",\n \"preferences.saved\": \"Preferências salvas.\",\n \"preferences.error\": \"Não foi possível salvar. Tente de novo.\",\n \"preferences.empty\": \"Nenhuma preferência configurada ainda.\",\n \"preferences.quietHours\": \"Silêncio entre\",\n \"preferences.quietHoursTo\": \"e\",\n \"channel.inbox\": \"No app\",\n \"channel.push\": \"Push\",\n \"channel.email\": \"E-mail\",\n \"channel.whatsapp\": \"WhatsApp\",\n \"channel.sms\": \"SMS\",\n \"workspace.title\": \"Notificações\",\n \"workspace.description\": \"O que foi avisado, e por quais canais.\",\n \"workspace.settingsLink\": \"Configurações\",\n \"settings.title\": \"Configurações de notificação\",\n \"settings.description\": \"Por onde avisar, e o texto exato que chega.\",\n \"settings.channelsTitle\": \"Canais\",\n \"settings.channelsHint\": \"Desligar um canal não apaga o histórico — o aviso deixa de sair por ali daqui em diante.\",\n \"settings.templatesTitle\": \"Mensagens\",\n \"settings.templatesHint\": \"Uma versão nova é criada a cada gravação; a anterior fica no histórico.\",\n \"settings.templatesEmpty\": \"Nenhuma mensagem cadastrada.\",\n \"settings.pickTemplate\": \"Escolha uma mensagem para editar.\",\n \"settings.subject\": \"Título\",\n \"settings.subjectHint\": \"O que aparece na lista. Vazio, o título é derivado da primeira linha do texto.\",\n \"settings.body\": \"Texto\",\n \"settings.previewTitle\": \"Como o destinatário vê\",\n \"settings.save\": \"Salvar nova versão\",\n \"settings.saving\": \"Salvando…\",\n \"settings.unchanged\": \"Nada mudou ainda.\",\n \"settings.close\": \"Fechar\",\n \"settings.version\": \"v\",\n \"settings.newTemplate\": \"Nova mensagem\",\n \"settings.remove\": \"Remover\",\n \"settings.key\": \"Chave da notificação\",\n \"settings.keyHint\": \"Não muda depois de criada — é por ela que o envio acha a mensagem.\",\n \"settings.channel\": \"Canal\",\n \"settings.locale\": \"Idioma\",\n \"settings.variablesTitle\": \"Variáveis\",\n \"settings.variablesHint\": \"Clique para inserir no ponto onde o cursor estava.\",\n \"settings.unknownVariables\": \"Variáveis que não existem nesta notificação:\",\n \"settings.viewport.desktop\": \"Computador\",\n \"settings.viewport.mobile\": \"Celular\",\n \"settings.constraint.title\": \"Assunto passa do limite do canal:\",\n \"settings.constraint.body\": \"Texto passa do limite do canal:\",\n \"settings.tab.messages\": \"Mensagens\",\n \"settings.tab.routing\": \"Roteamento\",\n \"settings.policiesTitle\": \"Canais permitidos por assunto\",\n \"settings.policiesHint\": \"Decisão da empresa. Canal desligado aqui não sai, mesmo que o destinatário tenha pedido.\",\n \"settings.savePolicies\": \"Salvar roteamento\",\n \"settings.discard\": \"Descartar\"\n}\n","{\n \"bell.label\": \"Notifications\",\n \"bell.unreadSuffix\": \"unread\",\n \"list.title\": \"Inbox\",\n \"list.unread\": \"unread\",\n \"list.empty\": \"Nothing here yet.\",\n \"list.loading\": \"Loading…\",\n \"list.error\": \"We couldn't load your notifications.\",\n \"list.retry\": \"Try again\",\n \"list.loadMore\": \"Load more\",\n \"list.markAllAsRead\": \"Mark all as read\",\n \"item.markAsRead\": \"Mark as read\",\n \"item.delete\": \"Delete\",\n \"item.unread\": \"Unread\",\n \"preferences.title\": \"Notification preferences\",\n \"preferences.description\": \"Choose how you want to be notified about each topic.\",\n \"preferences.save\": \"Save preferences\",\n \"preferences.saved\": \"Preferences saved.\",\n \"preferences.error\": \"Couldn't save. Try again.\",\n \"preferences.empty\": \"No preferences configured yet.\",\n \"preferences.quietHours\": \"Quiet between\",\n \"preferences.quietHoursTo\": \"and\",\n \"channel.inbox\": \"In app\",\n \"channel.push\": \"Push\",\n \"channel.email\": \"Email\",\n \"channel.whatsapp\": \"WhatsApp\",\n \"channel.sms\": \"SMS\",\n \"workspace.title\": \"Notifications\",\n \"workspace.description\": \"What was sent, and through which channels.\",\n \"workspace.settingsLink\": \"Settings\",\n \"settings.title\": \"Notification settings\",\n \"settings.description\": \"Where to notify, and the exact text that arrives.\",\n \"settings.channelsTitle\": \"Channels\",\n \"settings.channelsHint\": \"Turning a channel off does not erase history — notices stop going out through it from now on.\",\n \"settings.templatesTitle\": \"Messages\",\n \"settings.templatesHint\": \"Each save creates a new version; the previous one stays in history.\",\n \"settings.templatesEmpty\": \"No messages registered.\",\n \"settings.pickTemplate\": \"Pick a message to edit.\",\n \"settings.subject\": \"Title\",\n \"settings.subjectHint\": \"Shown in the list. Empty, the title is derived from the first line of the body.\",\n \"settings.body\": \"Body\",\n \"settings.previewTitle\": \"How the recipient sees it\",\n \"settings.save\": \"Save new version\",\n \"settings.saving\": \"Saving…\",\n \"settings.unchanged\": \"Nothing changed yet.\",\n \"settings.close\": \"Close\",\n \"settings.version\": \"v\",\n \"settings.newTemplate\": \"New message\",\n \"settings.remove\": \"Remove\",\n \"settings.key\": \"Notification key\",\n \"settings.keyHint\": \"Fixed once created — sending looks the message up by it.\",\n \"settings.channel\": \"Channel\",\n \"settings.locale\": \"Language\",\n \"settings.variablesTitle\": \"Variables\",\n \"settings.variablesHint\": \"Click to insert where the cursor was.\",\n \"settings.unknownVariables\": \"Variables this notification does not have:\",\n \"settings.viewport.desktop\": \"Desktop\",\n \"settings.viewport.mobile\": \"Mobile\",\n \"settings.constraint.title\": \"Subject exceeds the channel limit:\",\n \"settings.constraint.body\": \"Body exceeds the channel limit:\",\n \"settings.tab.messages\": \"Messages\",\n \"settings.tab.routing\": \"Routing\",\n \"settings.policiesTitle\": \"Allowed channels per topic\",\n \"settings.policiesHint\": \"Company decision. A channel turned off here never sends, even if the recipient opted in.\",\n \"settings.savePolicies\": \"Save routing\",\n \"settings.discard\": \"Discard\"\n}\n","/**\n * Copyright (c) 2026 Ada Technology. MIT License.\n *\n * Nenhum texto visível vive em tag ou prop (`web.md` §6). O host sobrescreve chaves pontuais via\n * `messageOverrides` sem precisar fornecer um locale inteiro.\n */\n\nimport ptBR from './pt-BR.json'\nimport en from './en.json'\n\nexport type NotificationMessages = typeof ptBR\nexport type NotificationLocale = 'pt-BR' | 'en'\n\nexport const DEFAULT_LOCALE: NotificationLocale = 'pt-BR'\n\nconst MESSAGES_BY_LOCALE: Record<NotificationLocale, NotificationMessages> = {\n 'pt-BR': ptBR,\n en: en as NotificationMessages,\n}\n\nexport function resolveMessages(locale: NotificationLocale): NotificationMessages {\n return MESSAGES_BY_LOCALE[locale] ?? MESSAGES_BY_LOCALE[DEFAULT_LOCALE]\n}\n","/**\n * Copyright (c) 2026 Ada Technology. MIT License.\n *\n * Camada headless — a válvula de escape do item 4 da §4 da regra de módulos plugáveis: quando a\n * tela default não serve, o produto monta a própria sobre estes mesmos hooks e continua herdando\n * upgrades de lógica.\n */\n\nimport { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { NotificationSummary } from '@adatechnology/notification-contracts'\n\nimport { useNotificationContext } from '../NotificationProvider'\nimport { NOTIFICATION_QUERY_KEYS } from './queryKeys'\n\nexport type UseNotificationsOptions = {\n readonly category?: string\n readonly read?: boolean\n readonly perPage?: number\n}\n\nexport function useNotifications(options: UseNotificationsOptions = {}) {\n const { client } = useNotificationContext()\n\n return useInfiniteQuery({\n queryKey: NOTIFICATION_QUERY_KEYS.list({ category: options.category, read: options.read }),\n initialPageParam: undefined as string | undefined,\n queryFn: ({ pageParam }) =>\n client.listNotifications({\n category: options.category,\n read: options.read,\n cursor: pageParam,\n perPage: options.perPage,\n }),\n getNextPageParam: (lastPage) => lastPage.nextCursor,\n })\n}\n\nexport function useUnreadCount() {\n const { client, pollIntervalSeconds } = useNotificationContext()\n\n return useQuery({\n queryKey: NOTIFICATION_QUERY_KEYS.unreadCount(),\n queryFn: () => client.countUnread(),\n // Polling é o fallback de quem não ligou o SSE. Com o stream ativo,\n // `useNotificationStream` invalida esta chave e o refetch acontece na hora.\n refetchInterval: pollIntervalSeconds > 0 ? pollIntervalSeconds * 1000 : false,\n })\n}\n\nexport function useMarkAsRead() {\n const { client } = useNotificationContext()\n const queryClient = useQueryClient()\n\n return useMutation({\n mutationFn: (id: string) => client.markAsRead(id),\n async onMutate(id: string) {\n // Atualização otimista: marcar como lida é ação de um clique, e esperar o round-trip para\n // o badge cair faz a interface parecer travada.\n await queryClient.cancelQueries({ queryKey: NOTIFICATION_QUERY_KEYS.all })\n const previousCount = queryClient.getQueryData<number>(NOTIFICATION_QUERY_KEYS.unreadCount())\n\n queryClient.setQueryData<number>(NOTIFICATION_QUERY_KEYS.unreadCount(), (current) =>\n current === undefined ? current : Math.max(0, current - 1),\n )\n return { previousCount, id }\n },\n onError(_error, _id, context) {\n // Rollback: sem isto o badge ficaria mentindo até o próximo refetch.\n if (context?.previousCount !== undefined) {\n queryClient.setQueryData(NOTIFICATION_QUERY_KEYS.unreadCount(), context.previousCount)\n }\n },\n onSettled() {\n void queryClient.invalidateQueries({ queryKey: NOTIFICATION_QUERY_KEYS.all })\n },\n })\n}\n\nexport function useMarkAllAsRead() {\n const { client } = useNotificationContext()\n const queryClient = useQueryClient()\n\n return useMutation({\n mutationFn: () => client.markAllAsRead(),\n onSettled() {\n void queryClient.invalidateQueries({ queryKey: NOTIFICATION_QUERY_KEYS.all })\n },\n })\n}\n\nexport function useDeleteNotification() {\n const { client } = useNotificationContext()\n const queryClient = useQueryClient()\n\n return useMutation({\n mutationFn: (id: string) => client.deleteNotification(id),\n onSettled() {\n void queryClient.invalidateQueries({ queryKey: NOTIFICATION_QUERY_KEYS.all })\n },\n })\n}\n\n/** Achata as páginas do infinite query — a lista quase sempre quer o array direto. */\nexport function flattenNotificationPages(\n pages: readonly { data: readonly NotificationSummary[] }[] | undefined,\n): readonly NotificationSummary[] {\n return pages?.flatMap((page) => page.data) ?? []\n}\n","/**\n * Copyright (c) 2026 Ada Technology. MIT License.\n *\n * Chaves centralizadas: o sino e a lista leem o mesmo `unreadCount`, e uma invalidação precisa\n * atingir os dois. Chave montada inline em cada hook é como badge e lista passam a mostrar\n * números diferentes.\n */\n\nexport const NOTIFICATION_QUERY_KEYS = {\n all: ['notifications'] as const,\n list: (filters: { category?: string; read?: boolean }) => ['notifications', 'list', filters] as const,\n unreadCount: () => ['notifications', 'unread-count'] as const,\n preferences: () => ['notifications', 'preferences'] as const,\n templates: () => ['notifications', 'templates'] as const,\n templateVariables: () => ['notifications', 'template-variables'] as const,\n categoryPolicies: () => ['notifications', 'category-policies'] as const,\n} as const\n","/**\n * Copyright (c) 2026 Ada Technology. MIT License.\n */\n\nimport { useEffect } from 'react'\nimport { useQueryClient } from '@tanstack/react-query'\nimport { subscribeToNotificationStream } from '@adatechnology/notification-client'\n\nimport { useNotificationContext } from '../NotificationProvider'\nimport { NOTIFICATION_QUERY_KEYS } from './queryKeys'\n\nexport type UseNotificationStreamOptions = {\n /** Desligado por omissão: abrir SSE é decisão do host, que sabe se o backend expõe a rota. */\n readonly enabled?: boolean\n readonly onEvent?: (event: { event: string; data: unknown }) => void\n}\n\n/**\n * Liga o SSE do inbox ao cache do TanStack: cada evento invalida as chaves, e o badge e a lista\n * atualizam juntos. `useEffect` aqui é o uso correto do hook — sincronizar com um sistema externo\n * (`react.md`, \"useEffect só para sincronizar com sistemas externos\"), não transformar dado.\n */\nexport function useNotificationStream(options: UseNotificationStreamOptions = {}): void {\n const { client } = useNotificationContext()\n const queryClient = useQueryClient()\n const enabled = options.enabled ?? false\n const onEvent = options.onEvent\n\n useEffect(() => {\n if (!enabled) return\n\n let subscription: { close(): void } | undefined\n let cancelled = false\n\n void client.resolveStreamRequest().then(({ url, headers }) => {\n // A resolução do token é assíncrona; se o componente desmontou nesse meio-tempo, não abre\n // conexão nenhuma — senão sobraria um stream órfão a cada navegação rápida.\n if (cancelled) return\n\n subscription = subscribeToNotificationStream({\n url,\n headers,\n onEvent: (event) => {\n onEvent?.(event)\n void queryClient.invalidateQueries({ queryKey: NOTIFICATION_QUERY_KEYS.all })\n },\n })\n })\n\n return () => {\n cancelled = true\n subscription?.close()\n }\n }, [client, queryClient, enabled, onEvent])\n}\n","/**\n * Copyright (c) 2026 Ada Technology. MIT License.\n *\n * O roteamento: por quais canais cada categoria PODE sair, decidido pela empresa.\n *\n * É a camada que faltava. O chamador já podia enumerar canais em cada `sendNotification`, e quem\n * recebe já tinha preferência — mas \"cobrança nunca sai por SMS\" não cabia em nenhum dos dois:\n * o primeiro espalha a decisão por todo o código, o segundo deixa ela na mão do destinatário.\n *\n * O modelo aqui é de teto, não de piso: desligar barra o canal para todo mundo; ligar apenas\n * permite, e a preferência de quem recebe continua valendo por cima.\n */\n\nimport { useMemo, useState } from 'react'\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { NotificationCategoryPolicy } from '@adatechnology/notification-contracts'\n\nimport { useNotificationContext } from '../NotificationProvider'\nimport { NOTIFICATION_QUERY_KEYS } from './queryKeys'\n\nexport type UseCategoryPoliciesResult = {\n readonly isLoading: boolean\n readonly isSaving: boolean\n readonly error: string | undefined\n readonly isDirty: boolean\n /** Linha ausente é permitido — o mesmo default do servidor, para a tela não mentir. */\n isAllowed: (params: { category: string; channel: string }) => boolean\n toggle: (params: { category: string; channel: string }) => void\n save: () => void\n reset: () => void\n}\n\nfunction identityOf(params: { category: string; channel: string }): string {\n return `${params.category}:${params.channel}`\n}\n\nexport function useCategoryPolicies(): UseCategoryPoliciesResult {\n const { client } = useNotificationContext()\n const queryClient = useQueryClient()\n\n const policiesQuery = useQuery({\n queryKey: NOTIFICATION_QUERY_KEYS.categoryPolicies(),\n queryFn: () => client.getCategoryPolicies(),\n })\n\n const mutation = useMutation({\n mutationFn: (policies: readonly NotificationCategoryPolicy[]) => client.updateCategoryPolicies(policies),\n onSuccess() {\n void queryClient.invalidateQueries({ queryKey: NOTIFICATION_QUERY_KEYS.categoryPolicies() })\n },\n })\n\n /**\n * O rascunho guarda só o que foi TOCADO, não uma cópia do conjunto inteiro. Com a cópia, salvar\n * gravaria linha para toda combinação exibida na tela — e a ausência de linha, que é o default\n * \"permitido\", viraria centenas de registros de `enabled: true` sem significado.\n */\n const [touched, setTouched] = useState<Record<string, boolean>>({})\n\n const saved = useMemo(() => {\n const byIdentity = new Map<string, boolean>()\n for (const policy of policiesQuery.data ?? []) byIdentity.set(identityOf(policy), policy.enabled)\n return byIdentity\n }, [policiesQuery.data])\n\n function isAllowed(params: { category: string; channel: string }): boolean {\n const identity = identityOf(params)\n return touched[identity] ?? saved.get(identity) ?? true\n }\n\n return {\n isLoading: policiesQuery.isLoading,\n isSaving: mutation.isPending,\n error: mutation.error?.message,\n isDirty: Object.keys(touched).length > 0,\n isAllowed,\n\n toggle(params) {\n const identity = identityOf(params)\n setTouched((current) => ({ ...current, [identity]: !isAllowed(params) }))\n },\n\n save() {\n // Só as categorias tocadas vão no corpo: o servidor substitui categoria a categoria, e\n // mandar o conjunto inteiro apagaria e reescreveria política que ninguém editou.\n const categories = new Set(Object.keys(touched).map((identity) => identity.split(':')[0] ?? ''))\n const policies: NotificationCategoryPolicy[] = []\n for (const category of categories) {\n const channels = new Set([\n ...[...saved.keys()].filter((identity) => identity.startsWith(`${category}:`)),\n ...Object.keys(touched).filter((identity) => identity.startsWith(`${category}:`)),\n ])\n for (const identity of channels) {\n const channel = identity.slice(category.length + 1)\n policies.push({\n category,\n channel: channel as NotificationCategoryPolicy['channel'],\n enabled: isAllowed({ category, channel }),\n })\n }\n }\n mutation.mutate(policies, { onSuccess: () => setTouched({}) })\n },\n\n reset() {\n setTouched({})\n },\n }\n}\n","/**\n * Copyright (c) 2026 Ada Technology. MIT License.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { NotificationPreference, UpsertTemplateBody } from '@adatechnology/notification-contracts'\n\nimport { useNotificationContext } from '../NotificationProvider'\nimport { NOTIFICATION_QUERY_KEYS } from './queryKeys'\n\nexport function usePreferences() {\n const { client } = useNotificationContext()\n\n return useQuery({\n queryKey: NOTIFICATION_QUERY_KEYS.preferences(),\n queryFn: () => client.getPreferences(),\n })\n}\n\nexport function useUpdatePreferences() {\n const { client } = useNotificationContext()\n const queryClient = useQueryClient()\n\n return useMutation({\n mutationFn: (preferences: readonly NotificationPreference[]) => client.updatePreferences(preferences),\n onSuccess(saved) {\n // A resposta já é o estado persistido — usar direto evita um refetch e o piscar do painel\n // entre o valor otimista e o confirmado.\n queryClient.setQueryData(NOTIFICATION_QUERY_KEYS.preferences(), saved)\n },\n })\n}\n\nexport function useTemplates() {\n const { client } = useNotificationContext()\n\n return useQuery({\n queryKey: NOTIFICATION_QUERY_KEYS.templates(),\n queryFn: () => client.listTemplates(),\n })\n}\n\n/**\n * Salva uma VERSÃO nova do template. É o que a tela de configuração usa para o lojista mudar a copy\n * sem deploy.\n *\n * Invalida em vez de escrever no cache: o servidor decide o número da versão, então o objeto salvo\n * traz um campo que o otimista não teria como prever — e mostrar \"versão 3\" quando o banco gravou 4\n * confunde exatamente quem está tentando conferir o que salvou.\n */\nexport function useUpsertTemplate() {\n const { client } = useNotificationContext()\n const queryClient = useQueryClient()\n\n return useMutation({\n mutationFn: (input: UpsertTemplateBody) => client.upsertTemplate(input),\n onSuccess() {\n void queryClient.invalidateQueries({ queryKey: NOTIFICATION_QUERY_KEYS.templates() })\n },\n })\n}\n\n/**\n * O catálogo é config do host: muda com deploy, não com uso. `staleTime: Infinity` evita refetch a\n * cada foco de janela por um dado que não muda durante a sessão.\n */\nexport function useTemplateVariables() {\n const { client } = useNotificationContext()\n\n return useQuery({\n queryKey: NOTIFICATION_QUERY_KEYS.templateVariables(),\n queryFn: () => client.listTemplateVariables(),\n staleTime: Number.POSITIVE_INFINITY,\n })\n}\n\n/**\n * Desativa o template. Invalida a lista pelo mesmo motivo do upsert: o servidor derruba TODAS as\n * versões ativas da identidade, e um cache otimista que removesse só a linha clicada mostraria a\n * versão anterior de volta na tela.\n */\nexport function useDeactivateTemplate() {\n const { client } = useNotificationContext()\n const queryClient = useQueryClient()\n\n return useMutation({\n mutationFn: (id: string) => client.deactivateTemplate(id),\n onSuccess() {\n void queryClient.invalidateQueries({ queryKey: NOTIFICATION_QUERY_KEYS.templates() })\n },\n })\n}\n","/**\n * Copyright (c) 2026 Ada Technology. MIT License.\n *\n * Quando o aviso chegou, dito do jeito que se diz em voz alta.\n *\n * `Intl` faz o trabalho de idioma — o pacote não guarda \"há\", \"ago\" nem nome de mês em lugar nenhum,\n * o que manteria `web.md` §6 de pé e ainda assim quebraria no primeiro locale novo. O que é decisão\n * nossa são as faixas: segundo, minuto, hora, dia, e a virada para data absoluta.\n */\n\nconst SECOND = 1_000\nconst MINUTE = 60 * SECOND\nconst HOUR = 60 * MINUTE\nconst DAY = 24 * HOUR\n\n/**\n * Passada uma semana, o relativo para de informar e passa a exigir conta: \"há 43 dias\" é um convite\n * a abrir o calendário. A data, o leitor lê.\n */\nconst RELATIVE_HORIZON = 7 * DAY\n\nfunction parseInstant(isoTimestamp: string): Date | undefined {\n const instant = new Date(isoTimestamp)\n return Number.isNaN(instant.getTime()) ? undefined : instant\n}\n\nexport function formatNotificationTime(isoTimestamp: string, locale: string, now: Date): string {\n const instant = parseInstant(isoTimestamp)\n if (!instant) return ''\n\n // Relógio do navegador adiantado é rotina; anunciar \"daqui a 2 minutos\" para algo que já chegou\n // não é. O piso em zero transforma o desvio em \"agora\", que é o que de fato aconteceu.\n const elapsed = Math.max(0, now.getTime() - instant.getTime())\n\n if (elapsed >= RELATIVE_HORIZON) {\n return new Intl.DateTimeFormat(locale, { day: '2-digit', month: '2-digit', year: 'numeric' }).format(instant)\n }\n\n // `numeric: 'auto'` é o que troca \"há 1 dia\" por \"ontem\" e \"há 0 segundos\" por \"agora\".\n const relative = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })\n\n if (elapsed < MINUTE) return relative.format(0, 'second')\n if (elapsed < HOUR) return relative.format(-Math.floor(elapsed / MINUTE), 'minute')\n if (elapsed < DAY) return relative.format(-Math.floor(elapsed / HOUR), 'hour')\n return relative.format(-Math.floor(elapsed / DAY), 'day')\n}\n\n/** O instante inteiro, para o `title` — é a hora que o texto relativo esconde. */\nexport function formatNotificationTimestamp(isoTimestamp: string, locale: string): string {\n const instant = parseInstant(isoTimestamp)\n if (!instant) return ''\n\n return new Intl.DateTimeFormat(locale, { dateStyle: 'short', timeStyle: 'short' }).format(instant)\n}\n","/**\n * Copyright (c) 2026 Ada Technology. MIT License.\n *\n * A lógica do editor de template, que estava reimplementada no host.\n *\n * Ela veio de `quickcart/modules/notifications/hooks/useNotificationSettings.hook.ts` — 207 linhas\n * que o segundo produto reescreveria, com as mesmas quatro decisões sutis para redescobrir. Elas\n * são o motivo de isto ser pacote e não exemplo:\n *\n * 1. **Versão ativa mais alta.** A rota devolve o histórico inteiro, correto para auditoria; na tela\n * isso faz a mesma mensagem aparecer três vezes e ninguém sabe qual está no ar.\n * 2. **Seleção pelo id, não pelo objeto.** Salvar cria versão nova no servidor; segurando o objeto,\n * o editor mostra a versão que já não é a ativa e a gravação seguinte parte de um texto velho.\n * 3. **Campo vazio é ausência.** Gravar `subject: ''` faz o renderer tratar como subject existente e\n * derivar título vazio, em vez de cair no fallback que usa o corpo.\n * 4. **Preview pelo `renderTemplate` do contracts**, o mesmo do envio. Reimplementar a interpolação\n * dá um preview que confere hoje e mente quando o renderer mudar.\n *\n * A criação de template novo trouxe uma quinta: **a identidade é imutável depois de criada**.\n * `key`+`channel`+`locale` é o que o `sendNotification` procura; deixar editar num template\n * existente não seria edição, seria criar outro e deixar o antigo órfão no ar.\n */\n\nimport { useMemo, useState } from 'react'\nimport {\n PREVIEW_VIEWPORT_BY_CHANNEL,\n buildPreviewPayload,\n diffTemplateVariables,\n renderTemplate,\n} from '@adatechnology/notification-contracts'\nimport type {\n NotificationChannel,\n NotificationTemplate,\n PreviewViewportSpec,\n RenderedTemplatePreview,\n TemplateVariableDefinition,\n TemplateVariableDiff,\n UpsertTemplateBody,\n} from '@adatechnology/notification-contracts'\n\nimport { useDeactivateTemplate, useTemplateVariables, useTemplates, useUpsertTemplate } from './usePreferences'\n\nexport type TemplateDraft = {\n readonly key: string\n readonly channel: string\n readonly locale: string\n readonly subject: string\n readonly body: string\n readonly whatsappTemplateName: string\n}\n\nexport type TemplateDraftField = 'subject' | 'body'\n\nexport type TemplatePreviewFrame = PreviewViewportSpec & {\n readonly rendered: RenderedTemplatePreview\n}\n\nfunction toDraft(template: NotificationTemplate): TemplateDraft {\n return {\n key: template.key,\n channel: template.channel,\n locale: template.locale,\n subject: template.subject ?? '',\n body: template.body,\n whatsappTemplateName: template.whatsappTemplateName ?? '',\n }\n}\n\nfunction buildEmptyDraft(params: { channel: string; locale: string }): TemplateDraft {\n return { key: '', channel: params.channel, locale: params.locale, subject: '', body: '', whatsappTemplateName: '' }\n}\n\nexport type UseTemplateEditorParams = {\n /**\n * Valores de exemplo do preview. Hoje o padrão vem do catálogo de variáveis do servidor; isto\n * continua existindo para o produto sobrescrever um exemplo pontual sem mexer no catálogo.\n */\n readonly previewPayload?: Readonly<Record<string, unknown>>\n /** Canal e locale de um template novo, antes de o operador escolher. */\n readonly defaultChannel?: string\n readonly defaultLocale?: string\n}\n\nexport type UseTemplateEditorResult = {\n readonly templates: readonly NotificationTemplate[]\n readonly isLoading: boolean\n readonly isSaving: boolean\n readonly isDeactivating: boolean\n readonly error: string | undefined\n readonly selected: NotificationTemplate | undefined\n readonly draft: TemplateDraft | undefined\n /** `true` enquanto o rascunho é um template que ainda não existe no servidor. */\n readonly isNew: boolean\n /** Identidade travada — `false` só durante a criação. */\n readonly isIdentityLocked: boolean\n /** Os dois quadros do preview, na ordem em que a tela desenha. */\n readonly previews: readonly TemplatePreviewFrame[]\n /** As variáveis que esta `key` declara. Vazio quando o catálogo não conhece a chave. */\n readonly variables: readonly TemplateVariableDefinition[]\n readonly variableDiff: TemplateVariableDiff\n readonly isDirty: boolean\n readonly canSave: boolean\n select: (template: NotificationTemplate) => void\n startNew: () => void\n update: (patch: Partial<TemplateDraft>) => void\n /** Insere `{{nome}}` na posição do cursor. O operador nunca digita o nome à mão. */\n insertVariable: (params: { name: string; field: TemplateDraftField; cursorIndex: number }) => void\n save: () => void\n deactivate: (id: string) => void\n clear: () => void\n}\n\nexport function useTemplateEditor(params: UseTemplateEditorParams = {}): UseTemplateEditorResult {\n const templatesQuery = useTemplates()\n const variablesQuery = useTemplateVariables()\n const upsert = useUpsertTemplate()\n const deactivateMutation = useDeactivateTemplate()\n\n const [selectedId, setSelectedId] = useState<string | undefined>(undefined)\n const [draft, setDraft] = useState<TemplateDraft | undefined>(undefined)\n const [isNew, setIsNew] = useState(false)\n\n /** Decisão 1: só a ativa mais alta por `key`+`channel`+`locale` — a mesma que o envio faz. */\n const templates = useMemo(() => {\n const activeByIdentity = new Map<string, NotificationTemplate>()\n for (const template of templatesQuery.data ?? []) {\n if (!template.active) continue\n const identity = `${template.key}:${template.channel}:${template.locale}`\n const current = activeByIdentity.get(identity)\n if (!current || template.version > current.version) activeByIdentity.set(identity, template)\n }\n return [...activeByIdentity.values()].sort(\n (left, right) => left.key.localeCompare(right.key) || left.channel.localeCompare(right.channel),\n )\n }, [templatesQuery.data])\n\n /** Decisão 2: derivado do id. */\n const selected = useMemo(() => templates.find((template) => template.id === selectedId), [templates, selectedId])\n\n const variables = useMemo(() => (draft ? (variablesQuery.data?.[draft.key] ?? []) : []), [variablesQuery.data, draft])\n\n const variableDiff = useMemo(\n () =>\n diffTemplateVariables({\n body: draft?.body ?? '',\n subject: draft?.subject || undefined,\n // Chave sem entrada no catálogo é catálogo não declarado, não catálogo vazio — o servidor\n // aceita qualquer variável nesse caso, e a tela não pode acusar o que a rota vai gravar.\n variables: draft && variablesQuery.data?.[draft.key] ? variables : undefined,\n }),\n [draft, variables, variablesQuery.data],\n )\n\n /** Decisão 4: o mesmo render do envio, uma vez por viewport do canal. */\n const previews = useMemo<readonly TemplatePreviewFrame[]>(() => {\n if (!draft) return []\n const payload = { ...buildPreviewPayload(variables), ...(params.previewPayload ?? {}) }\n const rendered = renderTemplate({\n channel: draft.channel,\n subject: draft.subject || undefined,\n body: draft.body,\n payload,\n })\n const viewports = PREVIEW_VIEWPORT_BY_CHANNEL[draft.channel as NotificationChannel] ?? []\n return viewports.map((viewport) => ({ ...viewport, rendered }))\n }, [draft, variables, params.previewPayload])\n\n const isDirty = useMemo(() => {\n if (!draft) return false\n if (isNew) return draft.key.length > 0 || draft.body.length > 0\n if (!selected) return false\n const original = toDraft(selected)\n return (Object.keys(original) as (keyof TemplateDraft)[]).some((field) => original[field] !== draft[field])\n }, [draft, selected, isNew])\n\n return {\n templates,\n isLoading: templatesQuery.isLoading,\n isSaving: upsert.isPending,\n isDeactivating: deactivateMutation.isPending,\n error: upsert.error?.message ?? deactivateMutation.error?.message,\n selected,\n draft,\n isNew,\n isIdentityLocked: !isNew,\n previews,\n variables,\n variableDiff,\n isDirty,\n // A variável desconhecida é recusada pelo servidor com 400; bloquear o botão evita a ida\n // perdida, sem virar a única defesa — a validação continua sendo a da rota.\n canSave: Boolean(draft?.key && draft.body) && variableDiff.unknown.length === 0,\n\n select(template) {\n setSelectedId(template.id)\n setDraft(toDraft(template))\n setIsNew(false)\n },\n\n startNew() {\n setSelectedId(undefined)\n setIsNew(true)\n setDraft(buildEmptyDraft({ channel: params.defaultChannel ?? 'email', locale: params.defaultLocale ?? 'pt-BR' }))\n },\n\n update(patch) {\n setDraft((current) => (current ? { ...current, ...patch } : current))\n },\n\n insertVariable({ name, field, cursorIndex }) {\n setDraft((current) => {\n if (!current) return current\n const text = current[field]\n const at = Math.min(Math.max(cursorIndex, 0), text.length)\n return { ...current, [field]: `${text.slice(0, at)}{{${name}}}${text.slice(at)}` }\n })\n },\n\n save() {\n if (!draft) return\n const body: UpsertTemplateBody = {\n key: draft.key,\n channel: draft.channel as UpsertTemplateBody['channel'],\n locale: draft.locale,\n active: true,\n body: draft.body,\n ...(draft.subject ? { subject: draft.subject } : {}),\n ...(draft.whatsappTemplateName ? { whatsappTemplateName: draft.whatsappTemplateName } : {}),\n }\n upsert.mutate(body, {\n onSuccess: () => {\n setSelectedId(undefined)\n setDraft(undefined)\n setIsNew(false)\n },\n })\n },\n\n deactivate(id) {\n deactivateMutation.mutate(id, {\n onSuccess: () => {\n if (selectedId !== id) return\n setSelectedId(undefined)\n setDraft(undefined)\n },\n })\n },\n\n clear() {\n setSelectedId(undefined)\n setDraft(undefined)\n setIsNew(false)\n },\n }\n}\n"],"mappings":"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","names":["useQueryClient","useQueryClient","useMemo","useMutation","useQuery","useQueryClient","useQueryClient","useQuery","useMutation","useMemo","useMutation","useQuery","useQueryClient","useQuery","useQueryClient","useMutation","useMemo","useState","useState","useMemo"]}