@fayz-ai/db 0.14.0 → 0.16.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.
Files changed (68) hide show
  1. package/migrations/011_leadcontrol_joins_the_family.sql +5 -1
  2. package/migrations/029_o_administrador_enxerga_o_que_existe.sql +167 -0
  3. package/migrations/031_ticketcontrol_joins_the_family.sql +109 -0
  4. package/migrations/032_a_marca_e_um_conjunto_de_tokens.sql +291 -0
  5. package/migrations/041_a_entrega_tem_onde_guardar_a_politica.sql +53 -0
  6. package/migrations/042_o_token_de_marca_fecha_a_porta_anon.sql +17 -0
  7. package/migrations/043_fiscalcontrol_joins_the_family.sql +75 -0
  8. package/migrations/044_o_teste_gratis_dura_trinta_dias.sql +15 -0
  9. package/migrations/045_o_espelho_manda_enquanto_o_v2_nao_escreveu.sql +519 -0
  10. package/migrations/046_the_tick_advances_the_flows.sql +77 -0
  11. package/migrations/047_the_tick_sweeps_the_offers.sql +74 -0
  12. package/migrations/052_fullcontrol_joins_the_family.sql +213 -0
  13. package/migrations/053_o_erp_para_de_exigir_o_que_nao_desenha.sql +37 -0
  14. package/migrations/054_o_arquivo_migrado_ganha_bytes.sql +439 -0
  15. package/migrations/061_a_linha_sem_data_pode_ser_estrutura.sql +253 -0
  16. package/migrations/063_o_anexo_ja_registrado_recebe_os_bytes.sql +303 -0
  17. package/migrations/064_um_arquivo_anexado_a_dezenove_contas.sql +178 -0
  18. package/migrations/065_apagar_o_sobrevivente_nao_apaga_o_tenant.sql +59 -0
  19. package/migrations/066_the_port_machinery_closes_its_doors.sql +51 -0
  20. package/migrations/067_the_erp_keeps_the_front_door.sql +56 -0
  21. package/migrations/068_o_ledger_encontra_a_linha_pelo_caminho_que_o_writer_usa.sql +55 -0
  22. package/migrations/069_a_sondagem_de_irma_usa_indice.sql +293 -0
  23. package/migrations/071_o_sdr_trabalha_o_funil_e_mais_nada.sql +110 -0
  24. package/migrations/072_o_porte_nao_avisa_a_plataforma_de_uma_venda_antiga.sql +91 -0
  25. package/migrations/073_a_exclusao_e_contrato_do_cliente_nao_do_cluster.sql +300 -0
  26. package/migrations/074_a_classificacao_quebrada_nao_leva_o_dinheiro_junto.sql +332 -0
  27. package/migrations/075_o_apelido_da_referencia_nao_disputa_com_a_variavel.sql +276 -0
  28. package/migrations/076_o_desligado_tambem_precisa_atravessar.sql +110 -0
  29. package/migrations/077_a_unidade_escolhida_chega_ao_banco.sql +128 -0
  30. package/migrations/078_onde_o_profissional_atende_atravessa.sql +71 -0
  31. package/migrations/079_quem_aparece_na_agenda_e_quem_atende.sql +210 -0
  32. package/migrations/080_o_contexto_da_requisicao_se_calcula_uma_vez.sql +240 -0
  33. package/migrations/081_a_permissao_por_unidade_se_resolve_uma_vez_por_unidade.sql +88 -0
  34. package/migrations/082_a_cerca_tambem_se_lembra.sql +110 -0
  35. package/migrations/083_quem_e_profissional_continua_agendavel.sql +32 -0
  36. package/migrations/084_quem_atende_em_varias_nao_cabe_numa_coluna.sql +60 -0
  37. package/migrations/085_a_unidade_do_profissional_vem_de_onde_ele_atende.sql +47 -0
  38. package/migrations/086_a_ponte_de_clientes_volta_a_existir.sql +89 -0
  39. package/migrations/087_a_liberacao_pergunta_ao_indice_antes_da_funcao.sql +79 -0
  40. package/migrations/088_quem_pode_executar_o_que_o_porte_criou.sql +50 -0
  41. package/migrations/089_a_memoria_de_transacao_nao_serve_para_isto.sql +86 -0
  42. package/migrations/090_a_marca_tambem_conta_como_mudanca_de_configuracao.sql +75 -0
  43. package/migrations/091_o_shell_tambem_e_configuracao_do_tenant.sql +71 -0
  44. package/migrations/092_a_marca_tem_uma_grafia_so_e_uma_porta_so.sql +159 -0
  45. package/migrations/093_a_marca_carrega_o_tema_inteiro.sql +179 -0
  46. package/migrations/094_a_porta_do_dominio_aprende_a_grafia_nova.sql +55 -0
  47. package/migrations/095_o_aplicativo_tem_um_preset_que_a_conta_estende.sql +98 -0
  48. package/migrations/096_o_sublinhado_tambem_e_nome_de_modulo.sql +100 -0
  49. package/migrations/097_o_oficio_diz_o_que_oferece_e_a_conta_o_que_usa.sql +137 -0
  50. package/migrations/098_a_arvore_do_oficio_nomeia_quem_ainda_desenha.sql +22 -0
  51. package/migrations/099_o_estoque_mora_dentro_de_produtos.sql +48 -0
  52. package/migrations/100_o_chefcontrol_desenha_abas_de_modulo.sql +24 -0
  53. package/migrations/101_o_oficio_agrupa_o_plugin_nomeia.sql +93 -0
  54. package/migrations/102_tres_marcas_ganham_fundo_e_contraste.sql +73 -0
  55. package/migrations/103_great_djs_e_iam_club_entram_na_frota.sql +75 -0
  56. package/migrations/104_o_curso_e_a_bilheteria_ganham_preset.sql +29 -0
  57. package/migrations/105_o_que_o_oficio_exige_ele_tambem_oferece.sql +78 -0
  58. package/migrations/106_a_comunidade_se_gere_como_escola.sql +84 -0
  59. package/migrations/107_o_logo_da_coluna_e_projecao_do_documento.sql +53 -0
  60. package/migrations/108_o_modulo_do_oficio_vizinho_tem_onde_ser_visto.sql +78 -0
  61. package/migrations/109_a_conta_diz_o_que_nao_usa_e_a_marca_pinta_a_casa.sql +67 -0
  62. package/migrations/110_a_tabela_de_ramos_aprende_o_vocabulario_fechado.sql +60 -0
  63. package/migrations/111_o_erp_generico_nao_vende_por_pedido.sql +50 -0
  64. package/migrations/112_o_cache_do_contexto_sabe_de_quem_ele_e.sql +327 -0
  65. package/migrations/113_o_ramo_volta_a_saber_que_papeis_a_conta_nasce_tendo.sql +124 -0
  66. package/migrations/114_a_resposta_inicial_volta_a_dizer_o_que_a_pessoa_pode.sql +93 -0
  67. package/migrations/115_o_rail_e_de_quem_tem_um.sql +108 -0
  68. package/package.json +1 -1
@@ -0,0 +1,137 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 079_o_oficio_diz_o_que_oferece_e_a_conta_o_que_usa.sql
3
+ -- ---------------------------------------------------------------------------
4
+ -- Uma clínica abria mostrando módulo de restaurante, de loja e de escola ao
5
+ -- mesmo tempo. Não era dado sujo: era a regra.
6
+ --
7
+ -- ── Por que o aplicativo central mostra tudo ────────────────────────────────
8
+ -- `resolvePluginRuntime` é ADITIVO: uma linha `active` liga, uma linha
9
+ -- `disabled` desliga, e o silêncio cai no padrão do manifesto. Hoje 24 dos 29
10
+ -- plugins publicados nascem `defaultEnabled: true`.
11
+ --
12
+ -- Num aplicativo de OFÍCIO isso está certo: o resto-saas importa doze pacotes,
13
+ -- e a curadoria já aconteceu no `import`. O aplicativo central importa os vinte
14
+ -- e cinco — e ali o mesmo padrão quer dizer "toda conta recebe todo ofício,
15
+ -- a menos que escreva uma linha `disabled` para cada um que não quer".
16
+ --
17
+ -- A Clinica de Teste tem catorze linhas, todas `active`, nenhuma `disabled`.
18
+ -- Ela nunca disse que não era um restaurante. Ninguém nunca disse.
19
+ --
20
+ -- ── O que faltava: a OFERTA ────────────────────────────────────────────────
21
+ -- A 077 deu ao aplicativo um `preset` com `shell` e `nav` — como DESENHAR. Não
22
+ -- havia onde dizer o que ele OFERECE. Sem essa lista, o catálogo do aplicativo
23
+ -- central é a união de tudo que foi compilado, que não é catálogo nenhum.
24
+ --
25
+ -- app.apps.preset.modules o ofício — o que este ofício oferece
26
+ -- app.tenant_plugins a conta — o que esta casa ligou ou desligou
27
+ -- app.roles o papel — quem, dentro dela, enxerga
28
+ --
29
+ -- Com `modules`, o silêncio deixa de significar "ligado": um plugin que o
30
+ -- ofício não oferece está FORA, mesmo nascendo `defaultEnabled: true`. E um que
31
+ -- ele oferece continua obedecendo a conta — que é como o Artorius, sendo food,
32
+ -- não tem Balcão: o ChefControl OFERECE o PDV, e a conta dele diz que não usa.
33
+ --
34
+ -- O ofício propõe; a conta dispõe.
35
+ --
36
+ -- ── E o ramo (`vertical_id`)? Ele era quatro vocabulários ──────────────────
37
+ -- Quatro lugares diziam o ramo de uma conta, e nenhum concordava com o outro:
38
+ --
39
+ -- tenants.vertical_id food, restaurant, beauty, salon, clinic,
40
+ -- commerce, ecommerce, retail, null — 9 grafias
41
+ -- app.vertical_defaults agency, ecommerce, restaurant, salon, school
42
+ -- — 5 grafias
43
+ -- app.tenant_apps.app_id resto, beauty, shop, full, fiscal, school
44
+ -- PluginManifest.verticalId nunca preenchido (é `options?.verticalId`, e
45
+ -- o aplicativo central não passa nada)
46
+ --
47
+ -- A interseção das duas primeiras é de TRÊS nomes. Pertinho do Céu é `food`; a
48
+ -- tabela de defaults só conhece `restaurant`. Catorze das vinte e duas contas
49
+ -- têm um ramo que a tabela de ramos nunca ouviu falar — e é por isso que nada
50
+ -- específico de ofício jamais disparou.
51
+ --
52
+ -- A saída não é uma quinta grafia. É reconhecer qual das quatro tem dono:
53
+ -- `tenant_apps` é escrita pelo provisionamento, tem chave estrangeira para
54
+ -- `app.apps` e já viaja no `app_config()`. O RAMO DE UMA CONTA É O OFÍCIO DO
55
+ -- APLICATIVO QUE ELA RODA. `tenants.vertical_id` passa a ser consequência —
56
+ -- rótulo para relatório e para o onboarding, nunca portão.
57
+ -- ---------------------------------------------------------------------------
58
+
59
+ -- ── 1. O aplicativo declara seu ofício, uma vez ────────────────────────────
60
+ ALTER TABLE app.apps ADD COLUMN IF NOT EXISTS vertical_id text;
61
+
62
+ COMMENT ON COLUMN app.apps.vertical_id IS
63
+ 'O ofício deste aplicativo, em UM vocabulário: food, beauty, commerce, education, events, accounting, generic. NULL = não é ofício (control-admin). `tenants.vertical_id` é derivada daqui — ver 079.';
64
+
65
+ UPDATE app.apps SET vertical_id = v.vertical FROM (VALUES
66
+ ('resto', 'food'),
67
+ ('beauty', 'beauty'), -- salão E clínica: os dois rodam o StudioControl
68
+ ('shop', 'commerce'),
69
+ ('school', 'education'),
70
+ ('ticket', 'events'),
71
+ ('fiscal', 'accounting'),
72
+ ('full', 'generic'),
73
+ ('crm', 'generic')
74
+ ) AS v(id, vertical) WHERE app.apps.id = v.id;
75
+
76
+ -- ── 2. O ofício declara o que oferece ──────────────────────────────────────
77
+ -- Extraído do que cada aplicativo REALMENTE compõe hoje (o array `plugins` de
78
+ -- `src/config/app.tsx` de cada repositório), não de uma lista desejada.
79
+ -- `::jsonb` e nao so `m.modules`: num VALUES o literal `$$[...]$$` e TEXTO, e
80
+ -- `jsonb_build_object` com texto guarda uma STRING, nao um array. A diferenca
81
+ -- so aparece tres migrations adiante, quando a invariante da 101 chama
82
+ -- `jsonb_array_length` e recebe um escalar.
83
+ UPDATE app.apps SET preset = preset || jsonb_build_object('modules', m.modules::jsonb) FROM (VALUES
84
+ ('resto', $$["dashboard","admin","notifications","menu","orders","tables","reservations","kitchen",
85
+ "resto-counter","resto-module-toggles","resto-printing","agenda","inventory","crm",
86
+ "financial","fiscal-br","custom_forms","marketing","automations","conversations",
87
+ "tasks","workforce","reports","openbanking"]$$),
88
+ ('beauty', $$["dashboard","admin","notifications","agenda","crm","financial","inventory","marketing",
89
+ "automations","custom_forms","tasks","scribe","workforce","reports","openbanking"]$$),
90
+ ('shop', $$["dashboard","admin","notifications","shop","orders","inventory","crm","marketing",
91
+ "financial","automations","reports","blog","sites"]$$),
92
+ ('full', $$["dashboard","admin","notifications","crm","conversations","financial","inventory",
93
+ "marketing","automations","reports","tasks","custom_forms"]$$),
94
+ ('school', $$["dashboard","admin","notifications","courses","agenda","crm","financial","marketing",
95
+ "automations","reports","custom_forms"]$$),
96
+ ('fiscal', $$["dashboard","admin","notifications","financial","fiscal-br","reports","openbanking"]$$),
97
+ ('ticket', $$["dashboard","admin","notifications","orders","crm","financial","marketing",
98
+ "automations","reports"]$$)
99
+ ) AS m(id, modules) WHERE app.apps.id = m.id;
100
+
101
+ -- ── 3. A conta que rodava cinco ofícios volta a rodar o seu ────────────────
102
+ -- Resíduo do backfill que deu todos os aplicativos a todas as contas. Já
103
+ -- corrigido para os restaurantes; a clínica ficou para trás.
104
+ DELETE FROM app.tenant_apps ta
105
+ USING public.tenants t
106
+ WHERE ta.tenant_id = t.id
107
+ AND t.name = 'Clinica de Teste'
108
+ AND ta.app_id <> 'beauty';
109
+
110
+ -- ── 4. O ramo passa a ser consequência do ofício ───────────────────────────
111
+ -- Onde a conta roda um ofício, a coluna repete o que `tenant_apps` já diz. Onde
112
+ -- ela roda mais de um (Papa Léguas: resto + shop), vence o mais específico —
113
+ -- `fiscal` é retaguarda e nunca é o produto principal; `full` é o ERP sem ofício.
114
+ UPDATE public.tenants t SET vertical_id = d.vertical
115
+ FROM (
116
+ SELECT ta.tenant_id,
117
+ (array_agg(a.vertical_id ORDER BY (a.vertical_id = 'accounting'), (a.vertical_id = 'generic'), a.id))[1] AS vertical
118
+ FROM app.tenant_apps ta
119
+ JOIN app.apps a ON a.id = ta.app_id
120
+ WHERE ta.status = 'active' AND a.vertical_id IS NOT NULL
121
+ GROUP BY ta.tenant_id
122
+ ) d
123
+ WHERE d.tenant_id = t.id AND t.vertical_id IS DISTINCT FROM d.vertical;
124
+
125
+ -- Sem aplicativo nenhum não há ofício a derivar. `generic` é a resposta honesta:
126
+ -- a conta existe e ainda não disse o que faz.
127
+ UPDATE public.tenants t SET vertical_id = 'generic'
128
+ WHERE NOT EXISTS (SELECT 1 FROM app.tenant_apps ta WHERE ta.tenant_id = t.id AND ta.status = 'active');
129
+
130
+ -- Fecha o vocabulário. Sem isto a próxima conta criada à mão reabre o problema.
131
+ ALTER TABLE public.tenants DROP CONSTRAINT IF EXISTS tenants_vertical_id_check;
132
+ ALTER TABLE public.tenants ADD CONSTRAINT tenants_vertical_id_check
133
+ CHECK (vertical_id IS NULL OR vertical_id IN
134
+ ('food','beauty','commerce','education','events','accounting','generic'));
135
+
136
+ COMMENT ON COLUMN public.tenants.vertical_id IS
137
+ 'O ramo desta conta, DERIVADO do ofício do aplicativo que ela roda (079). Rótulo para relatório e onboarding — quem decide o que aparece é app.apps.preset.modules cruzado com app.tenant_plugins, nunca esta coluna.';
@@ -0,0 +1,22 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 080_a_arvore_do_oficio_nomeia_quem_ainda_desenha.sql
3
+ -- ---------------------------------------------------------------------------
4
+ -- Quando o ofício traz árvore curada (`preset.nav`), quem mais pode pôr linha
5
+ -- no rail?
6
+ --
7
+ -- "Ninguém" foi a primeira resposta, e custou o Painel e as Conversas: a árvore
8
+ -- do ChefControl nunca teve essas duas linhas porque, no resto-saas, elas vêm
9
+ -- da nav dos PRÓPRIOS plugins — lá só os de ofício levam `hideNav()`.
10
+ --
11
+ -- "Todos" é o erro oposto, e foi o que apareceu na tela seguinte: a Agenda
12
+ -- desenhou "Agenda" ao lado de "Operação" sendo que, dentro dela, ela já é a
13
+ -- linha "Reservas". Um módulo em dois lugares do mesmo menu.
14
+ --
15
+ -- `navExtras` é a exceção nomeada — o equivalente declarativo do `hideNav()`
16
+ -- que o aplicativo de ofício escreve em código. Só vale onde há `nav`: um
17
+ -- ofício que apenas ORDENA plugins (beauty, shop, full) não silencia ninguém, e
18
+ -- para eles a lista não existe.
19
+ -- ---------------------------------------------------------------------------
20
+
21
+ UPDATE app.apps SET preset = preset || '{"navExtras":["dashboard","conversations"]}'::jsonb
22
+ WHERE id = 'resto';
@@ -0,0 +1,48 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 081_o_estoque_mora_dentro_de_produtos.sql — o StoreControl ganha árvore.
3
+ -- ---------------------------------------------------------------------------
4
+ -- PULSE e Artorius mostravam "Produtos" e "Estoque" como duas linhas irmãs do
5
+ -- rail. Duas entradas para a mesma coisa na cabeça de quem opera uma loja — e
6
+ -- não é que uma esteja errada: o catálogo (o que vendo, preço, foto) e a
7
+ -- posição (quanto tem, onde, a que custo) são fatos diferentes sobre o MESMO
8
+ -- produto. O que estava errado era pedir que a pessoa escolhesse entre eles no
9
+ -- menu, como se fossem assuntos separados.
10
+ --
11
+ -- O ChefControl já tinha resolvido isso: "Produtos ▸ Cardápio · Estoque". Aqui
12
+ -- é a mesma forma, com o vocabulário da loja — e é a segunda vez que a árvore
13
+ -- curada de um ofício responde uma pergunta que o rail montado de plugins
14
+ -- soltos não sabia responder.
15
+ --
16
+ -- ── Por que a árvore precisa nomear TUDO ───────────────────────────────────
17
+ -- Onde há `nav`, só quem está em `navExtras` ainda desenha linha própria (080).
18
+ -- Então um módulo ausente da árvore desaparece do menu — Marketing, Financeiro
19
+ -- e Relatórios entram aqui não por decoração, mas porque sem eles sumiriam.
20
+ -- ---------------------------------------------------------------------------
21
+
22
+ UPDATE app.apps SET preset = preset || $$
23
+ {
24
+ "navExtras": ["dashboard", "conversations"],
25
+ "nav": [
26
+ { "route": "/orders", "label": "Pedidos", "icon": "ShoppingBag", "position": 10, "children": [
27
+ { "route": "/orders", "label": "Pedidos", "icon": "ShoppingBag" },
28
+ { "route": "/orders/customers", "label": "Clientes", "icon": "Users" }
29
+ ]},
30
+ { "route": "/products", "label": "Produtos", "icon": "Package", "position": 20, "children": [
31
+ { "route": "/products", "label": "Catálogo", "icon": "Package" },
32
+ { "route": "/products/collections", "label": "Coleções", "icon": "Layers" },
33
+ { "route": "/products/categories", "label": "Categorias", "icon": "FolderTree" },
34
+ { "route": "/inventory", "label": "Estoque", "icon": "Boxes" }
35
+ ]},
36
+ { "route": "/discounts", "label": "Descontos", "icon": "Tag", "position": 25, "children": [
37
+ { "route": "/discounts", "label": "Descontos", "icon": "Tag" },
38
+ { "route": "/discounts/gift-cards", "label": "Gift Cards", "icon": "CreditCard" }
39
+ ]},
40
+ { "route": "/content", "label": "Conteúdo", "icon": "Image", "position": 28 },
41
+ { "route": "/sales", "label": "CRM", "icon": "Filter", "position": 30 },
42
+ { "route": "/marketing", "label": "Marketing", "icon": "Megaphone", "position": 35 },
43
+ { "route": "/financial", "label": "Financeiro", "icon": "DollarSign", "position": 40 },
44
+ { "route": "/automations", "label": "Automações", "icon": "Zap", "position": 45 },
45
+ { "route": "/analytics", "label": "Relatórios", "icon": "BarChart3", "position": 50 }
46
+ ]
47
+ }
48
+ $$::jsonb WHERE id = 'shop';
@@ -0,0 +1,24 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 082_o_chefcontrol_desenha_abas_de_modulo.sql
3
+ -- ---------------------------------------------------------------------------
4
+ -- O Estoque abria sem Produtos, Movimentações, Contagens e Receitas.
5
+ --
6
+ -- `ModulePage` com variante `sidebar` não desenha sub-navegação nenhuma — e a
7
+ -- razão é boa: nessa variante o RAIL é que lista as sub-telas do módulo como
8
+ -- filhas da linha dele. Só que aqui o rail não lista: as filhas de "Produtos"
9
+ -- são Cardápio e Estoque, que são MÓDULOS irmãos, não as abas de dentro do
10
+ -- Estoque. A promessa não era cumprida por ninguém, e as abas deixavam de
11
+ -- existir.
12
+ --
13
+ -- A 077 semeou `moduleNav: "sidebar"` no preset do ChefControl. Foi engano:
14
+ -- essa é a escolha do FullControl, que a declara em `src/config/app.tsx`. Os
15
+ -- três aplicativos de referência — resto-saas, beauty-saas e marketplace-saas —
16
+ -- não declaram `moduleNav` nenhum e caem no padrão do shell para
17
+ -- `layout: 'sidebar'`, que é `tabs`.
18
+ --
19
+ -- O preset volta a dizer o que o aplicativo de referência faz.
20
+ -- ---------------------------------------------------------------------------
21
+
22
+ UPDATE app.apps
23
+ SET preset = jsonb_set(preset, '{shell,moduleNav}', '"tabs"'::jsonb)
24
+ WHERE id = 'resto';
@@ -0,0 +1,93 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 083_o_oficio_agrupa_o_plugin_nomeia.sql — o preset para de redesenhar o rail.
3
+ -- ---------------------------------------------------------------------------
4
+ -- A 077 deu ao ofício uma ÁRVORE: cada linha com rota, rótulo, ícone e posição,
5
+ -- escrita à mão em SQL. Mas o plugin já diz tudo isso — e duas fontes para o
6
+ -- mesmo fato brigam. Brigaram quatro vezes, e todas as quatro chegaram como
7
+ -- defeito de tela:
8
+ --
9
+ -- o Painel sumiu a árvore não o listava, e ela calava os plugins
10
+ -- a Agenda duplicou a árvore a chamava "Reservas"; o plugin, "Agenda"
11
+ -- "Pedidos" saiu 2x a rubrica e o primeiro filho tinham o mesmo rótulo
12
+ -- o Cardápio ficou solto o `menu` não estava na árvore da loja
13
+ --
14
+ -- Nenhum era erro de renderização. Todos eram a árvore discordando do catálogo.
15
+ --
16
+ -- ── A forma nova ───────────────────────────────────────────────────────────
17
+ -- O preset passa a declarar só o AGRUPAMENTO, numa lista única que se lê de
18
+ -- cima para baixo como o próprio rail:
19
+ --
20
+ -- "rail": [
21
+ -- "dashboard", ← as linhas deste plugin
22
+ -- { "label": "Produtos", "icon": "Package",
23
+ -- "under": ["/menu", "/inventory"] }, ← uma rubrica
24
+ -- "financial"
25
+ -- ]
26
+ --
27
+ -- Uma string é o plugin, com os rótulos DELE. Um objeto é a rubrica, e `under`
28
+ -- são prefixos de rota — o mesmo casamento por prefixo que o rail já usa para
29
+ -- aninhar. O preset nunca diz como uma linha se chama, então não tem como
30
+ -- discordar.
31
+ --
32
+ -- Quem não foi nomeado vai para o FIM, não some: é a regra do ByDesign — uma
33
+ -- pergunta que ninguém respondeu cai no padrão, não em "não". O contrário faria
34
+ -- do dia em que um aplicativo publica um plugin novo o dia em que ele
35
+ -- desaparece de toda conta, em silêncio.
36
+ --
37
+ -- `nav` e `navExtras` saem junto. `order` também: a ordem agora é a da lista.
38
+ -- ---------------------------------------------------------------------------
39
+
40
+ -- ── ChefControl ────────────────────────────────────────────────────────────
41
+ -- As sete rubricas do resto-saas, agora sem repetir o nome de nenhuma tela.
42
+ UPDATE app.apps SET preset = (preset - 'nav' - 'navExtras' - 'order') || $$
43
+ {
44
+ "rail": [
45
+ "dashboard",
46
+ "conversations",
47
+ { "label": "Operação", "icon": "Utensils",
48
+ "under": ["/orders", "/kitchen", "/clients", "/reservations", "/tables"] },
49
+ { "label": "Delivery", "icon": "Bike", "under": ["/delivery"] },
50
+ { "label": "Produtos", "icon": "Package", "under": ["/menu", "/inventory"] },
51
+ "financial",
52
+ "workforce",
53
+ { "label": "Cadastros", "icon": "Database", "under": ["/company", "/staff"] },
54
+ "reports"
55
+ ]
56
+ }
57
+ $$::jsonb WHERE id = 'resto';
58
+
59
+ -- ── StoreControl ───────────────────────────────────────────────────────────
60
+ -- "Produtos" junta o catálogo da loja, o estoque e — quando a casa tiver — o
61
+ -- cardápio. É a resposta à redundância Produtos × Estoque, agora sem inventar
62
+ -- rótulo nenhum.
63
+ UPDATE app.apps SET preset = (preset - 'nav' - 'navExtras' - 'order') || $$
64
+ {
65
+ "rail": [
66
+ "dashboard",
67
+ "conversations",
68
+ { "label": "Vendas", "icon": "ShoppingBag", "under": ["/orders"] },
69
+ { "label": "Produtos", "icon": "Package",
70
+ "under": ["/products", "/inventory", "/menu"] },
71
+ { "label": "Descontos", "icon": "Tag", "under": ["/discounts"] },
72
+ "shop",
73
+ "crm",
74
+ "marketing",
75
+ "financial",
76
+ "automations",
77
+ "reports"
78
+ ]
79
+ }
80
+ $$::jsonb WHERE id = 'shop';
81
+
82
+ -- ── StudioControl e FullControl ────────────────────────────────────────────
83
+ -- Eles nunca desenharam rail à mão: ordenam os plugins, e é só isso que o
84
+ -- preset guarda. A lista vira `rail` para que exista UM vocabulário.
85
+ UPDATE app.apps SET preset = (preset - 'order') || $$
86
+ { "rail": ["dashboard", "conversations", "agenda", "crm", "financial",
87
+ "inventory", "marketing", "workforce", "reports"] }
88
+ $$::jsonb WHERE id = 'beauty';
89
+
90
+ UPDATE app.apps SET preset = (preset - 'order') || $$
91
+ { "rail": ["dashboard", "conversations", "crm", "financial", "inventory",
92
+ "marketing", "automations", "reports"] }
93
+ $$::jsonb WHERE id = 'full';
@@ -0,0 +1,73 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 084_tres_marcas_ganham_fundo_e_contraste.sql
3
+ -- ---------------------------------------------------------------------------
4
+ -- Três contas passam a pintar a casa inteira, não só o rail. Os valores são os
5
+ -- que o founder passou; o que este arquivo guarda é a decisão de onde cada um
6
+ -- entra.
7
+ --
8
+ -- ── Espaço Renova Rio ──────────────────────────────────────────────────────
9
+ -- O fundo vira o marrom escuro da própria marca — antes a página era clara e só
10
+ -- a trilha era escura, o que lia como duas casas. O botão vai para uma pele
11
+ -- clara: sobre marrom escuro, um botão marrom não é um botão.
12
+ -- O rail fica um degrau ABAIXO da página (#100C0A contra #17120E) para ler como
13
+ -- recuo, não como uma segunda página encostada.
14
+ --
15
+ -- ── Clínica Sol ────────────────────────────────────────────────────────────
16
+ -- O laranja #FF7B05 da marca contra os azuis que ela já tinha. É o único par de
17
+ -- complementares aqui, e por isso o rail guarda o laranja só para o item ativo.
18
+ --
19
+ -- ── PULSE ──────────────────────────────────────────────────────────────────
20
+ -- Verde #83CA17 sobre preto — no RAIL. A página segue clara.
21
+ --
22
+ -- A primeira versão pintou a casa inteira de preto e o resultado foi ilegível:
23
+ -- carta #151515 sobre fundo #0B0B0B é contraste de 1.2:1, e todo número do
24
+ -- painel sumiu. "Preto" era a cor da trilha, não do sistema — e a diferença
25
+ -- entre uma marca escura e um tema escuro é que a segunda decisão precisa de
26
+ -- uma paleta inteira, não de duas cores.
27
+ --
28
+ -- ── O que isto expôs no código ─────────────────────────────────────────────
29
+ -- O vocabulário da marca sabe dizer a cor do BOTÃO e não a do TEXTO dentro
30
+ -- dele: `primaryForeground` cai em branco quando ninguém diz, e ninguém pode
31
+ -- dizer. Serviu enquanto toda marca era escura; verde-limão e bege com texto
32
+ -- branco são ilegíveis. A correção não é um campo novo — é derivar por
33
+ -- luminância (ver `readableOn` em packages/admin/src/app/admin-app.tsx), que
34
+ -- não tem como ser preenchido errado e já vale para a marca que ainda não
35
+ -- existe.
36
+ -- ---------------------------------------------------------------------------
37
+
38
+ UPDATE public.tenants SET settings = jsonb_set(settings, '{branding,tokens}', $$
39
+ {
40
+ "color": { "primary": "#E3B591", "accent": "#E8A46A" },
41
+ "surface": { "background": "#17120E", "card": "#211A15", "foreground": "#F5F1ED" },
42
+ "rail": { "background": "#100C0A", "foreground": "#F5F1ED", "border": "#2A2018",
43
+ "accent": "#2A2018", "accentForeground": "#E3B591", "muted": "#9B8B7C" },
44
+ "radius": "0.75rem"
45
+ }
46
+ $$::jsonb), updated_at = now()
47
+ WHERE name = 'Espaco Renova Rio';
48
+
49
+ UPDATE public.tenants SET settings = jsonb_set(settings, '{branding,tokens}', $$
50
+ {
51
+ "color": { "primary": "#FF7B05", "accent": "#FF9633" },
52
+ "surface": { "background": "#F6F8FC", "card": "#FFFFFF", "foreground": "#111620" },
53
+ "rail": { "background": "#121826", "foreground": "#EEF1F7", "border": "#1F2A40",
54
+ "accent": "#26324C", "accentForeground": "#FF9633", "muted": "#8A96AC" },
55
+ "radius": "0.75rem"
56
+ }
57
+ $$::jsonb), updated_at = now()
58
+ WHERE name = 'Clínica Sol - Espaço Terapêutico';
59
+
60
+ -- `jsonb_set` exige que o caminho pai exista, e a PULSE não tinha `tokens`:
61
+ -- por isso a mesclagem sobre `branding` em vez do caminho fundo.
62
+ UPDATE public.tenants
63
+ SET settings = jsonb_set(coalesce(settings,'{}'::jsonb), '{branding}',
64
+ coalesce(settings->'branding','{}'::jsonb) || $$
65
+ { "tokens": {
66
+ "color": { "primary": "#83CA17", "accent": "#6FAE12" },
67
+ "surface": { "background": "#F7F8F5", "card": "#FFFFFF", "foreground": "#14160F" },
68
+ "rail": { "background": "#0B0B0B", "foreground": "#F2F4F0", "border": "#1E1E1E",
69
+ "accent": "#1C2612", "accentForeground": "#9BDB3A", "muted": "#8A8A8A" },
70
+ "radius": "0.5rem" } }
71
+ $$::jsonb, true),
72
+ updated_at = now()
73
+ WHERE name = 'PULSE';
@@ -0,0 +1,75 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 085_great_djs_e_iam_club_entram_na_frota.sql
3
+ -- ---------------------------------------------------------------------------
4
+ -- As duas existiam como APLICATIVOS com backend próprio — Great DJs no projeto
5
+ -- `fayz-calendar`, IAM no `iam-community-app` — e não como CONTA no cluster do
6
+ -- Control, que é o único que o aplicativo central lê. Sem linha aqui elas não
7
+ -- apareciam no seletor, por mais que o produto existisse.
8
+ --
9
+ -- Great DJs → CourseControl (education). O produto dela é aula de DJ, e a
10
+ -- agenda de booking é consequência disso, não o contrário.
11
+ -- IAM Club → TicketControl (events). Clube é porta, lista e ingresso.
12
+ --
13
+ -- Os ids são os que `platform_tenant_create` sorteou; este arquivo reencena o
14
+ -- estado, não o recria — daí o `WHERE slug` em vez de `INSERT`.
15
+ --
16
+ -- ── Sobre os ladrilhos ─────────────────────────────────────────────────────
17
+ -- Os dois logos vieram com fundo branco chapado. Recortar por limiar comeria o
18
+ -- brilho de neon do IAM junto — ele é lavanda clarinho perto das letras. A
19
+ -- chave é tratar branco como TRANSPARÊNCIA e não como cor: alfa = 1 − brancura,
20
+ -- e a cor restante renormalizada. O que era halo sobre branco vira halo sobre
21
+ -- preto, que é como um neon se comporta de verdade.
22
+ --
23
+ -- Caminho `<tenant>/branding/tile.png` e não `branding/<tenant>/...` como as
24
+ -- marcas antigas: a política do bucket exige o id do tenant na PRIMEIRA pasta.
25
+ -- As antigas entraram por service_role, contornando a política — este caminho
26
+ -- é o que o próprio dono da conta consegue escrever.
27
+ -- ---------------------------------------------------------------------------
28
+
29
+ UPDATE public.tenants t SET settings = jsonb_set(coalesce(t.settings,'{}'::jsonb), '{branding}',
30
+ coalesce(t.settings->'branding','{}'::jsonb) || $$
31
+ {
32
+ "identity": { "name": "IAM Club",
33
+ "logoUrl": "https://klvfxzreepavcpyjiwla.supabase.co/storage/v1/object/public/avatars/d6ba2252-7807-4d25-895c-b6945a2a8171/branding/tile.png",
34
+ "markUrl": "https://klvfxzreepavcpyjiwla.supabase.co/storage/v1/object/public/avatars/d6ba2252-7807-4d25-895c-b6945a2a8171/branding/tile.png" },
35
+ "tokens": {
36
+ "color": { "primary": "#8F6CF5", "accent": "#A387F7" },
37
+ "surface": { "background": "#F8F7FD", "card": "#FFFFFF", "foreground": "#15121F" },
38
+ "rail": { "background": "#0B0912", "foreground": "#F1EEFB", "border": "#241E3A",
39
+ "accent": "#241E3A", "accentForeground": "#B6A0FA", "muted": "#8B84A6" },
40
+ "radius": "0.75rem" }
41
+ }
42
+ $$::jsonb, true), updated_at = now()
43
+ WHERE t.slug = 'iam-club';
44
+
45
+ UPDATE public.tenants t SET settings = jsonb_set(coalesce(t.settings,'{}'::jsonb), '{branding}',
46
+ coalesce(t.settings->'branding','{}'::jsonb) || $$
47
+ {
48
+ "identity": { "name": "Great DJs",
49
+ "logoUrl": "https://klvfxzreepavcpyjiwla.supabase.co/storage/v1/object/public/avatars/973e8465-09f8-41a9-93fb-a906d9667a1f/branding/tile.png",
50
+ "markUrl": "https://klvfxzreepavcpyjiwla.supabase.co/storage/v1/object/public/avatars/973e8465-09f8-41a9-93fb-a906d9667a1f/branding/tile.png" },
51
+ "tokens": {
52
+ "color": { "primary": "#F39A21", "accent": "#FFB44D" },
53
+ "surface": { "background": "#FAF8F4", "card": "#FFFFFF", "foreground": "#1A1611" },
54
+ "rail": { "background": "#0E0C09", "foreground": "#F7F3EC", "border": "#2A2114",
55
+ "accent": "#2A2114", "accentForeground": "#FFB44D", "muted": "#968B79" },
56
+ "radius": "0.75rem" }
57
+ }
58
+ $$::jsonb, true), updated_at = now()
59
+ WHERE t.slug = 'great-djs';
60
+
61
+ -- ── Correção de duas marcas da rodada anterior ─────────────────────────────
62
+ -- PULSE e Espaço Renova receberam a casa INTEIRA escura, e as duas ficaram
63
+ -- ilegíveis: carta sobre fundo quase da mesma cor. "Preto" e "marrom escuro"
64
+ -- eram a cor da TRILHA. Uma marca escura e um tema escuro são decisões
65
+ -- diferentes — a segunda precisa de uma paleta inteira, não de duas cores.
66
+ UPDATE public.tenants SET settings = jsonb_set(settings, '{branding,tokens}', $$
67
+ {
68
+ "color": { "primary": "#B4632A", "accent": "#C97B3C" },
69
+ "surface": { "background": "#FBF8F5", "card": "#FFFFFF", "foreground": "#1A1512" },
70
+ "rail": { "background": "#171310", "foreground": "#F5F1ED", "border": "#2A2018",
71
+ "accent": "#2A2018", "accentForeground": "#E3B591", "muted": "#9B8B7C" },
72
+ "radius": "0.75rem"
73
+ }
74
+ $$::jsonb), updated_at = now()
75
+ WHERE name = 'Espaco Renova Rio';
@@ -0,0 +1,29 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 086_o_curso_e_a_bilheteria_ganham_preset.sql
3
+ -- ---------------------------------------------------------------------------
4
+ -- CourseControl e TicketControl tinham `modules` (079) e nada mais. Sem `shell`
5
+ -- eles herdavam o `moduleNav: 'sidebar'` do pacote central — e nessa variante
6
+ -- `ModulePage` não desenha sub-navegação, na promessa de que o rail lista as
7
+ -- sub-telas do módulo. O rail não lista. Mesmo defeito que tirou as abas do
8
+ -- Estoque (082), só que estas duas contas nasceram hoje e já nasceriam assim.
9
+ --
10
+ -- O `rail` é curto de propósito: nenhum dos dois tem aplicativo de referência
11
+ -- com rail curado, então o preset ordena e não agrupa. Agrupar é uma decisão
12
+ -- que se toma olhando a tela cheia de dados, e elas ainda estão vazias.
13
+ -- ---------------------------------------------------------------------------
14
+
15
+ UPDATE app.apps SET preset = preset || $$
16
+ {
17
+ "shell": { "layout": "sidebar", "moduleNav": "tabs", "contentFrame": true },
18
+ "rail": ["dashboard", "conversations", "courses", "agenda", "crm",
19
+ "marketing", "financial", "automations", "reports"]
20
+ }
21
+ $$::jsonb WHERE id = 'school';
22
+
23
+ UPDATE app.apps SET preset = preset || $$
24
+ {
25
+ "shell": { "layout": "sidebar", "moduleNav": "tabs", "contentFrame": true },
26
+ "rail": ["dashboard", "conversations", "orders", "crm",
27
+ "marketing", "financial", "automations", "reports"]
28
+ }
29
+ $$::jsonb WHERE id = 'ticket';
@@ -0,0 +1,78 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 087_o_que_o_ofício_exige, ele também oferece.
3
+ -- ---------------------------------------------------------------------------
4
+ -- O IAM Club abriu com "Estoque" e "Cardápio" no rail de uma bilheteria. Não
5
+ -- foi a oferta falhando: foi `app.apps.requires` e `app.apps.preset.modules`
6
+ -- dizendo coisas diferentes sobre o mesmo aplicativo.
7
+ --
8
+ -- requires → menu, inventory, orders, financial, conversations
9
+ -- modules → ...orders, crm, financial, marketing, automations, reports
10
+ --
11
+ -- `platform_tenant_create` semeia linhas `active` a partir de `requires`, e uma
12
+ -- linha explícita vence a oferta — corretamente, é assim que o módulo vendido à
13
+ -- parte funciona. Só que aqui não havia venda nenhuma: havia duas listas
14
+ -- discordando, e a provisão obedeceu a que estava errada.
15
+ --
16
+ -- ── Qual das duas estava errada ────────────────────────────────────────────
17
+ -- Nenhuma, e é essa a parte interessante: um clube TEM bar. `requires` estava
18
+ -- certo sobre o negócio e `modules` é que estava incompleto — eu escrevi a
19
+ -- lista da 079 olhando os plugins que o app-shell do TicketControl importa, e
20
+ -- não o que a operação de um clube precisa.
21
+ --
22
+ -- Então a oferta cresce, e o rail ganha a rubrica que faltava: "Bar".
23
+ --
24
+ -- ── A invariante ───────────────────────────────────────────────────────────
25
+ -- `requires ⊆ modules`, sempre. Um aplicativo não pode EXIGIR o que não
26
+ -- OFERECE — seria pedir que a conta ligue algo que o catálogo dela não tem. A
27
+ -- checagem no fim deste arquivo é o que impede a próxima divergência de chegar
28
+ -- na tela em vez de na aplicação da migration.
29
+ -- ---------------------------------------------------------------------------
30
+
31
+ UPDATE app.apps SET preset = preset || $$
32
+ {
33
+ "modules": ["dashboard","admin","notifications","conversations","orders","menu","inventory",
34
+ "crm","financial","marketing","automations","reports"],
35
+ "rail": ["dashboard", "conversations",
36
+ { "label": "Bilheteria", "icon": "Ticket", "under": ["/orders"] },
37
+ { "label": "Bar", "icon": "Beer", "under": ["/menu", "/inventory"] },
38
+ "crm", "marketing", "financial", "automations", "reports"]
39
+ }
40
+ $$::jsonb WHERE id = 'ticket';
41
+
42
+ -- O `requires` do CourseControl cabe na oferta dele; só falta `conversations`,
43
+ -- que a oferta esqueceu e o rail já nomeia.
44
+ UPDATE app.apps SET preset = jsonb_set(preset, '{modules}',
45
+ (preset->'modules') || '["conversations"]'::jsonb)
46
+ WHERE id = 'school' AND NOT (preset->'modules' ? 'conversations');
47
+
48
+ -- Mesma falta no ChefControl e no StoreControl: `requires` não os inclui, mas
49
+ -- o rail deles nomeia `conversations` — e um nome no rail que não está na
50
+ -- oferta nunca desenha.
51
+ UPDATE app.apps SET preset = jsonb_set(preset, '{modules}',
52
+ (preset->'modules') || '["conversations"]'::jsonb)
53
+ WHERE id IN ('shop','full','fiscal') AND NOT (preset->'modules' ? 'conversations');
54
+
55
+ -- A checagem abaixo pegou esta linha enquanto eu a escrevia: a lista nova do
56
+ -- TicketControl tinha derrubado `conversations`, que o `requires` dele exige. É
57
+ -- o tipo de omissão que só aparece na tela três telas depois.
58
+
59
+ -- ── A invariante, verificada ───────────────────────────────────────────────
60
+ DO $$
61
+ DECLARE v_bad text;
62
+ BEGIN
63
+ SELECT string_agg(a.id || ' exige ' || r.m || ' e não oferece', '; ')
64
+ INTO v_bad
65
+ FROM app.apps a
66
+ CROSS JOIN LATERAL unnest(a.requires) AS r(m)
67
+ WHERE a.active
68
+ -- `jsonb_typeof` antes de `jsonb_array_length`: uma verificação que
69
+ -- ESTOURA numa forma inesperada é pior que a divergência que ela procura —
70
+ -- ela derruba a cadeia inteira em vez de nomear a linha errada. Um ofício
71
+ -- sem oferta declarada simplesmente não é verificado.
72
+ AND jsonb_typeof(a.preset->'modules') = 'array'
73
+ AND jsonb_array_length(a.preset->'modules') > 0
74
+ AND NOT (a.preset->'modules' ? r.m);
75
+ IF v_bad IS NOT NULL THEN
76
+ RAISE EXCEPTION 'requires fora da oferta: %', v_bad;
77
+ END IF;
78
+ END $$;