@fayz-ai/db 0.18.1 → 0.20.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 (24) hide show
  1. package/migrations/135_a_pessoa_diz_em_quais_unidades_esta_disponivel.sql +140 -0
  2. package/migrations/136_a_baixa_do_espelho_e_do_porte_nao_do_v2.sql +204 -0
  3. package/migrations/137_a_retirada_e_um_canal_de_venda.sql +43 -0
  4. package/migrations/138_o_produto_tem_cor_e_a_conta_so_a_veste_se_pagar.sql +606 -0
  5. package/migrations/139_a_equipe_e_um_agrupamento_declarado.sql +50 -0
  6. package/migrations/140_a_unidade_se_escreve_pela_mesma_porta_que_se_le.sql +130 -0
  7. package/migrations/141_o_endereco_da_unidade_mora_na_tabela_de_enderecos.sql +203 -0
  8. package/migrations/142_a_coordenada_e_do_endereco_nao_da_unidade.sql +157 -0
  9. package/migrations/143_o_preset_para_de_nomear_telas_que_nao_existem.sql +29 -0
  10. package/migrations/144_marketing_e_um_botao_nao_uma_gaveta.sql +46 -0
  11. package/migrations/145_campanhas_e_templates_somem_em_toda_conta.sql +36 -0
  12. package/migrations/146_a_clinica_ve_as_proprias_analises.sql +31 -0
  13. package/migrations/147_tres_saloes_desligam_o_rh.sql +30 -0
  14. package/migrations/148_servicos_ganham_a_permissao_que_nunca_tiveram.sql +37 -0
  15. package/migrations/149_a_conta_do_fornecedor_nao_e_conta_da_casa.sql +131 -0
  16. package/migrations/150_todo_cadastro_tem_observacao_e_etiqueta.sql +62 -0
  17. package/migrations/151_a_parceria_e_uma_especie_de_pessoa.sql +35 -0
  18. package/migrations/152_a_profissao_e_a_especie_de_pet_sao_catalogos.sql +31 -0
  19. package/migrations/153_a_sala_nao_e_a_filial.sql +94 -0
  20. package/migrations/154_as_duas_tabelas_novas_dizem_de_quem_sao.sql +33 -0
  21. package/migrations/155_o_pacote_tem_observacao_e_etiqueta_e_diz_de_quem_e.sql +28 -0
  22. package/migrations/156_restaurante_nao_vende_servico.sql +44 -0
  23. package/migrations/157_o_que_a_porta_da_unidade_criou_tambem_se_revoga.sql +45 -0
  24. package/package.json +1 -1
@@ -0,0 +1,606 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 138_o_produto_tem_cor_e_a_conta_so_a_veste_se_pagar.sql
3
+ --
4
+ -- O tema de cada Control deixa de ser código e vira dado — e a marca da conta
5
+ -- passa a ser uma CAMADA sobre ele, não um substituto dele.
6
+ --
7
+ -- ── O que a tela mostrou ───────────────────────────────────────────────────
8
+ -- Medido no cluster: `Pertinho do Céu` é um RESTAURANTE (vertical food, roda o
9
+ -- `resto`) pintado de azul-marinho #304878. Ninguém escolheu esse azul: o
10
+ -- `onboarding/submit.ts` grava `primaryColor`/`accentColor` no cadastro da
11
+ -- conta, e daí em diante o `ThemeInitializer` deixava a conta sobrescrever o
12
+ -- aplicativo SEMPRE, sem perguntar nada a ninguém. Vinte das trinta contas do
13
+ -- cluster têm um tema inteiro (rail + surface + color) que nunca foi decisão de
14
+ -- ninguém.
15
+ --
16
+ -- O resultado é um produto que não tem cara. ChefControl é ouro #FBAC00 — é o
17
+ -- único que o brand book v4 lhe dá —, e o restaurante abria azul.
18
+ --
19
+ -- ── Onde o tema do produto morava ──────────────────────────────────────────
20
+ -- Em `src/config/theme.ts`, DENTRO do bundle de cada aplicativo. Isso funcionou
21
+ -- enquanto cada Control era um deploy. Agora um único `control-app` atende
22
+ -- restaurante, clínica e escola na mesma sessão — e um tema compilado só sabe
23
+ -- ser um. `app.apps` guardava doze colunas sobre um aplicativo e NENHUMA sobre
24
+ -- como ele se apresenta: `accent_color` pinta o ladrilho do switcher e mais
25
+ -- nada.
26
+ --
27
+ -- Então o tema desce para a linha. Mesmo movimento que a 077 fez com o rail: o
28
+ -- que é conhecimento de produto vira dado, e trocar a cor de um Control passa a
29
+ -- ser um UPDATE em vez de um release.
30
+ --
31
+ -- ── As três camadas, e quem arbitra ────────────────────────────────────────
32
+ --
33
+ -- 1. PRODUTO app.apps.theme ouro no Chef, ameixa no Studio
34
+ -- 2. CONTA tenants.settings.branding logo e nome sempre; cor, só se…
35
+ -- 3. DECISÃO plan.entitlements …o plano disser que sim
36
+ --
37
+ -- A regra em uma frase: IDENTIDADE (logo, nome, favicon) é de toda conta —
38
+ -- ninguém abre um sistema sem a própria marca no alto. PELE (cor, rail, papel)
39
+ -- é de quem paga por ela. Um restaurante no plano `pro` abre ouro com a logo
40
+ -- dele; o Espaço Facial, no `business`, continua preto com bege.
41
+ --
42
+ -- O ponto 3 fica no cliente, junto de todos os outros tetos de plano
43
+ -- (`config.billing.plans`), e não aqui: é cosmético, e duplicar a tabela de
44
+ -- planos no banco só para pintar um botão criaria uma segunda fonte da verdade
45
+ -- para a mesma pergunta. O banco ENTREGA as duas camadas; quem escolhe qual
46
+ -- pintar é o `ThemeInitializer`.
47
+ --
48
+ -- ── O formato ──────────────────────────────────────────────────────────────
49
+ -- `theme` é um `SaasTheme` — exatamente o objeto que o shell já consome, com
50
+ -- as cores em triplas HSL sem `hsl()` porque o SDK as compõe como
51
+ -- `hsl(var(--x) / <alpha>)`. O que o banco grava é o que a casca já sabe
52
+ -- pintar, sem tradutor no meio. Mesma disciplina da 093, que trouxe o tema do
53
+ -- inquilino no vocabulário do `SaasTheme.sidebar`.
54
+ --
55
+ -- Os sete temas abaixo são cópia fiel do que cada aplicativo tinha compilado —
56
+ -- esta migração não redesenha nada, ela só muda o tema de lugar. `crm` e
57
+ -- `control-admin` ficam sem tema: o primeiro está inativo, o segundo é a mesa
58
+ -- da casa e herda o do bundle, que é o certo para uma ferramenta interna.
59
+ --
60
+ -- Idempotente: uma coluna com IF NOT EXISTS, sete UPDATEs por id e uma função
61
+ -- reemitida.
62
+ -- ---------------------------------------------------------------------------
63
+
64
+ -- ── §1 a coluna ───────────────────────────────────────────────────────────
65
+
66
+ ALTER TABLE app.apps ADD COLUMN IF NOT EXISTS theme jsonb;
67
+
68
+ COMMENT ON COLUMN app.apps.theme IS
69
+ 'O tema do PRODUTO (138): um SaasTheme completo — brand, sidebar, colors, darkColors, perception —, no mesmo vocabulário que o shell consome. A conta ESTENDE isto com tenants.settings.branding, e só até onde o plano dela permitir. NULL = este aplicativo não reivindica cor e herda a do bundle.';
70
+
71
+ -- ── §2 os sete produtos ───────────────────────────────────────────────────
72
+
73
+ UPDATE app.apps SET theme = $theme$
74
+ {
75
+ "name": "ChefControl",
76
+ "__kind": "saas-theme",
77
+ "brand": "41 100% 49.2%",
78
+ "colors": {
79
+ "background": "42 41.9% 93.9%",
80
+ "content": "42 41.9% 93.9%",
81
+ "card": "43 100% 98.6%",
82
+ "popover": "43 100% 98.6%",
83
+ "muted": "44 24.4% 91.2%",
84
+ "foreground": "80 6.7% 8.8%",
85
+ "cardForeground": "80 6.7% 8.8%",
86
+ "popoverForeground": "80 6.7% 8.8%",
87
+ "mutedForeground": "68 4% 39.6%",
88
+ "primary": "41 100% 49.2%",
89
+ "primaryForeground": "80 6.7% 8.8%",
90
+ "secondary": "44 24.4% 91.2%",
91
+ "secondaryForeground": "75 4.3% 18.4%",
92
+ "accent": "44 24.4% 91.2%",
93
+ "accentForeground": "72 3.9% 24.9%",
94
+ "border": "39 17.9% 81.4%",
95
+ "input": "39 17.9% 81.4%",
96
+ "ring": "208 70.8% 44.3%",
97
+ "destructive": "4 76.4% 39.8%",
98
+ "destructiveForeground": "0 0% 100%",
99
+ "success": "151 59.5% 30%",
100
+ "successForeground": "0 0% 100%",
101
+ "warning": "41 70.7% 55.9%",
102
+ "warningForeground": "80 6.7% 8.8%",
103
+ "sidebar": "80 6.7% 8.8%",
104
+ "sidebarForeground": "42 41.9% 93.9%",
105
+ "sidebarBorder": "60 3.5% 22.4%",
106
+ "sidebarAccent": "44 53.3% 15.1%",
107
+ "sidebarAccentForeground": "42 73.1% 69.4%",
108
+ "sidebarMuted": "60 4.3% 63.9%"
109
+ },
110
+ "darkColors": {
111
+ "primary": "41 100% 49.2%",
112
+ "primaryForeground": "80 6.7% 8.8%",
113
+ "ring": "208 70.8% 44.3%",
114
+ "accent": "72 5.6% 17.5%",
115
+ "accentForeground": "42 41.9% 93.9%",
116
+ "background": "80 6.7% 8.8%",
117
+ "content": "80 6.7% 8.8%",
118
+ "card": "80 4.3% 13.5%",
119
+ "popover": "80 4.3% 13.5%",
120
+ "muted": "72 5.6% 17.5%",
121
+ "mutedForeground": "60 4.3% 63.9%",
122
+ "foreground": "42 41.9% 93.9%",
123
+ "cardForeground": "42 41.9% 93.9%",
124
+ "popoverForeground": "42 41.9% 93.9%",
125
+ "border": "60 3.5% 22.4%",
126
+ "input": "48 2.9% 34.3%",
127
+ "destructive": "4 76.4% 39.8%",
128
+ "destructiveForeground": "0 0% 100%",
129
+ "success": "151 59.5% 30%",
130
+ "successForeground": "0 0% 100%",
131
+ "warning": "41 70.7% 55.9%",
132
+ "warningForeground": "80 6.7% 8.8%",
133
+ "sidebar": "80 6.7% 8.8%",
134
+ "sidebarForeground": "42 41.9% 93.9%",
135
+ "sidebarBorder": "60 3.5% 22.4%",
136
+ "sidebarAccent": "44 53.3% 15.1%",
137
+ "sidebarAccentForeground": "42 73.1% 69.4%",
138
+ "sidebarMuted": "60 4.3% 63.9%"
139
+ },
140
+ "perception": {
141
+ "buttonRadius": "0.5rem",
142
+ "inputRadius": "0.5rem",
143
+ "cardRadius": "0.75rem",
144
+ "modalRadius": "1rem",
145
+ "fontFamily": "\"Hanken Grotesk\", ui-sans-serif, system-ui, -apple-system, sans-serif",
146
+ "fontFamilyMono": "\"JetBrains Mono\", ui-monospace, SFMono-Regular, monospace",
147
+ "shadowSm": "0 1px 2px rgba(23, 24, 21, 0.06), 0 1px 1px rgba(23, 24, 21, 0.04)",
148
+ "shadowMd": "0 2px 4px rgba(23, 24, 21, 0.07), 0 4px 12px rgba(23, 24, 21, 0.06)",
149
+ "shadowLg": "0 8px 24px rgba(23, 24, 21, 0.1), 0 2px 6px rgba(23, 24, 21, 0.06)"
150
+ }
151
+ }
152
+ $theme$::jsonb WHERE id = 'resto';
153
+
154
+ UPDATE app.apps SET theme = $theme$
155
+ {
156
+ "__kind": "saas-theme",
157
+ "name": "StudioControl",
158
+ "preset": "classic_admin",
159
+ "brand": "260 34% 47%",
160
+ "shadow": "subtle",
161
+ "font": "dm-sans",
162
+ "sidebar": {
163
+ "background": "258 33% 10%",
164
+ "foreground": "260 20% 90%",
165
+ "border": "0 0% 100% / 0.1",
166
+ "accent": "257 57% 76% / 0.16",
167
+ "accentForeground": "257 57% 76%",
168
+ "muted": "0 0% 100% / 0.55"
169
+ },
170
+ "content": {
171
+ "background": "60 12% 95%"
172
+ },
173
+ "colors": {
174
+ "background": "60 12% 95%",
175
+ "foreground": "156 12% 8%",
176
+ "card": "0 0% 100%",
177
+ "cardForeground": "156 12% 8%",
178
+ "popover": "0 0% 100%",
179
+ "popoverForeground": "156 12% 8%",
180
+ "secondary": "60 11% 91%",
181
+ "secondaryForeground": "156 12% 8%",
182
+ "muted": "60 11% 91%",
183
+ "mutedForeground": "60 3% 35%",
184
+ "border": "60 11% 91%",
185
+ "input": "60 8% 84%",
186
+ "ring": "260 34% 47%",
187
+ "accent": "260 56% 95%",
188
+ "accentForeground": "260 34% 47%"
189
+ },
190
+ "perception": {
191
+ "buttonRadius": "12px",
192
+ "cardRadius": "16px",
193
+ "inputRadius": "12px",
194
+ "modalRadius": "20px",
195
+ "fontFamily": "'DM Sans', system-ui, sans-serif",
196
+ "fontFamilyMono": "'JetBrains Mono', ui-monospace, monospace",
197
+ "shadowSm": "0 1px 3px rgba(11,15,14,0.06), 0 1px 2px rgba(11,15,14,0.04)",
198
+ "shadowMd": "0 4px 12px rgba(11,15,14,0.06), 0 2px 4px rgba(11,15,14,0.04)",
199
+ "shadowLg": "0 12px 32px rgba(11,15,14,0.08), 0 4px 12px rgba(11,15,14,0.04)"
200
+ }
201
+ }
202
+ $theme$::jsonb WHERE id = 'beauty';
203
+
204
+ UPDATE app.apps SET theme = $theme$
205
+ {
206
+ "__kind": "saas-theme",
207
+ "name": "StoreControl",
208
+ "brand": "130 80% 34%",
209
+ "font": "dm-sans",
210
+ "sidebar": {
211
+ "background": "151 16% 7%",
212
+ "foreground": "0 0% 100%",
213
+ "border": "0 0% 100% / 0.09",
214
+ "accent": "130 72% 53% / 0.14",
215
+ "accentForeground": "130 72% 53%",
216
+ "muted": "0 0% 100% / 0.6"
217
+ },
218
+ "content": {
219
+ "background": "60 12% 95%"
220
+ },
221
+ "colors": {
222
+ "background": "60 12% 95%",
223
+ "foreground": "155 11% 8%",
224
+ "card": "0 0% 100%",
225
+ "cardForeground": "155 11% 8%",
226
+ "popover": "0 0% 100%",
227
+ "popoverForeground": "155 11% 8%",
228
+ "border": "60 11% 91%",
229
+ "input": "60 8% 84%",
230
+ "muted": "60 11% 91%",
231
+ "mutedForeground": "60 3% 35%",
232
+ "secondary": "60 11% 91%",
233
+ "secondaryForeground": "155 11% 8%",
234
+ "accent": "129 78% 93%",
235
+ "accentForeground": "130 80% 34%",
236
+ "ring": "130 80% 34%",
237
+ "success": "130 80% 34%",
238
+ "successForeground": "0 0% 100%",
239
+ "destructive": "6 100% 68%",
240
+ "destructiveForeground": "0 0% 100%",
241
+ "warning": "54 90% 50%",
242
+ "warningForeground": "155 11% 8%"
243
+ },
244
+ "perception": {
245
+ "buttonRadius": "12px",
246
+ "cardRadius": "16px",
247
+ "inputRadius": "12px",
248
+ "modalRadius": "18px",
249
+ "fontFamily": "\"DM Sans\", ui-sans-serif, system-ui, -apple-system, sans-serif",
250
+ "fontFamilyMono": "\"JetBrains Mono\", ui-monospace, SFMono-Regular, monospace",
251
+ "shadowSm": "0 1px 3px rgba(11,15,14,0.06), 0 1px 2px rgba(11,15,14,0.04)",
252
+ "shadowMd": "0 4px 12px rgba(11,15,14,0.06), 0 2px 4px rgba(11,15,14,0.04)",
253
+ "shadowLg": "0 12px 32px rgba(11,15,14,0.08), 0 4px 12px rgba(11,15,14,0.04)"
254
+ }
255
+ }
256
+ $theme$::jsonb WHERE id = 'shop';
257
+
258
+ UPDATE app.apps SET theme = $theme$
259
+ {
260
+ "__kind": "saas-theme",
261
+ "name": "CourseControl",
262
+ "brand": "175 84% 32%",
263
+ "font": "dm-sans",
264
+ "sidebar": {
265
+ "background": "185 30% 8%",
266
+ "foreground": "0 0% 100%",
267
+ "border": "0 0% 100% / 0.09",
268
+ "accent": "175 77% 41% / 0.14",
269
+ "accentForeground": "175 77% 41%",
270
+ "muted": "0 0% 100% / 0.6"
271
+ },
272
+ "content": {
273
+ "background": "60 12% 95%"
274
+ },
275
+ "colors": {
276
+ "background": "60 12% 95%",
277
+ "foreground": "155 11% 8%",
278
+ "card": "0 0% 100%",
279
+ "cardForeground": "155 11% 8%",
280
+ "popover": "0 0% 100%",
281
+ "popoverForeground": "155 11% 8%",
282
+ "border": "60 11% 91%",
283
+ "input": "60 8% 84%",
284
+ "muted": "60 11% 91%",
285
+ "mutedForeground": "60 3% 35%",
286
+ "secondary": "60 11% 91%",
287
+ "secondaryForeground": "155 11% 8%",
288
+ "accent": "172 60% 93%",
289
+ "accentForeground": "175 84% 32%",
290
+ "ring": "175 84% 32%",
291
+ "success": "130 80% 34%",
292
+ "successForeground": "0 0% 100%",
293
+ "destructive": "6 100% 68%",
294
+ "destructiveForeground": "0 0% 100%",
295
+ "warning": "54 90% 50%",
296
+ "warningForeground": "155 11% 8%"
297
+ },
298
+ "perception": {
299
+ "buttonRadius": "12px",
300
+ "cardRadius": "16px",
301
+ "inputRadius": "12px",
302
+ "modalRadius": "18px",
303
+ "fontFamily": "\"DM Sans\", ui-sans-serif, system-ui, -apple-system, sans-serif",
304
+ "fontFamilyMono": "\"JetBrains Mono\", ui-monospace, SFMono-Regular, monospace",
305
+ "shadowSm": "0 1px 3px rgba(11,15,14,0.06), 0 1px 2px rgba(11,15,14,0.04)",
306
+ "shadowMd": "0 4px 12px rgba(11,15,14,0.06), 0 2px 4px rgba(11,15,14,0.04)",
307
+ "shadowLg": "0 12px 32px rgba(11,15,14,0.08), 0 4px 12px rgba(11,15,14,0.04)"
308
+ }
309
+ }
310
+ $theme$::jsonb WHERE id = 'school';
311
+
312
+ UPDATE app.apps SET theme = $theme$
313
+ {
314
+ "__kind": "saas-theme",
315
+ "name": "TicketControl",
316
+ "brand": "276 91% 43%",
317
+ "font": "dm-sans",
318
+ "sidebar": {
319
+ "background": "266 30% 8%",
320
+ "foreground": "0 0% 100%",
321
+ "border": "0 0% 100% / 0.09",
322
+ "accent": "276 91% 55% / 0.20",
323
+ "accentForeground": "276 90% 72%",
324
+ "muted": "0 0% 100% / 0.6"
325
+ },
326
+ "content": {
327
+ "background": "270 20% 96%"
328
+ },
329
+ "colors": {
330
+ "background": "270 20% 96%",
331
+ "foreground": "266 25% 10%",
332
+ "card": "0 0% 100%",
333
+ "cardForeground": "266 25% 10%",
334
+ "popover": "0 0% 100%",
335
+ "popoverForeground": "266 25% 10%",
336
+ "border": "261 22% 92%",
337
+ "input": "263 18% 86%",
338
+ "muted": "261 22% 92%",
339
+ "mutedForeground": "263 12% 40%",
340
+ "secondary": "261 22% 92%",
341
+ "secondaryForeground": "266 25% 10%",
342
+ "accent": "276 90% 95%",
343
+ "accentForeground": "276 91% 43%",
344
+ "ring": "276 91% 43%",
345
+ "primary": "276 91% 43%",
346
+ "primaryForeground": "0 0% 100%",
347
+ "success": "152 76% 32%",
348
+ "successForeground": "0 0% 100%",
349
+ "destructive": "2 80% 55%",
350
+ "destructiveForeground": "0 0% 100%",
351
+ "warning": "38 92% 50%",
352
+ "warningForeground": "266 25% 10%"
353
+ },
354
+ "perception": {
355
+ "buttonRadius": "12px",
356
+ "cardRadius": "16px",
357
+ "inputRadius": "12px",
358
+ "modalRadius": "18px",
359
+ "fontFamily": "\"DM Sans\", ui-sans-serif, system-ui, -apple-system, sans-serif",
360
+ "fontFamilyMono": "\"JetBrains Mono\", ui-monospace, SFMono-Regular, monospace",
361
+ "shadowSm": "0 1px 3px rgba(23,17,32,0.06), 0 1px 2px rgba(23,17,32,0.04)",
362
+ "shadowMd": "0 4px 12px rgba(23,17,32,0.06), 0 2px 4px rgba(23,17,32,0.04)",
363
+ "shadowLg": "0 12px 32px rgba(23,17,32,0.08), 0 4px 12px rgba(23,17,32,0.04)"
364
+ }
365
+ }
366
+ $theme$::jsonb WHERE id = 'ticket';
367
+
368
+ UPDATE app.apps SET theme = $theme$
369
+ {
370
+ "__kind": "saas-theme",
371
+ "name": "FiscalControl",
372
+ "brand": "215 25% 27%",
373
+ "font": "dm-sans",
374
+ "sidebar": {
375
+ "background": "222 47% 11%",
376
+ "foreground": "0 0% 100%",
377
+ "border": "0 0% 100% / 0.09",
378
+ "accent": "213 27% 70% / 0.18",
379
+ "accentForeground": "213 27% 84%",
380
+ "muted": "0 0% 100% / 0.6"
381
+ },
382
+ "content": {
383
+ "background": "210 30% 97%"
384
+ },
385
+ "colors": {
386
+ "background": "210 30% 97%",
387
+ "foreground": "222 47% 11%",
388
+ "card": "0 0% 100%",
389
+ "cardForeground": "222 47% 11%",
390
+ "popover": "0 0% 100%",
391
+ "popoverForeground": "222 47% 11%",
392
+ "border": "214 32% 91%",
393
+ "input": "213 27% 84%",
394
+ "muted": "214 32% 91%",
395
+ "mutedForeground": "215 16% 47%",
396
+ "secondary": "214 32% 91%",
397
+ "secondaryForeground": "222 47% 11%",
398
+ "accent": "210 40% 96%",
399
+ "accentForeground": "215 25% 27%",
400
+ "ring": "215 25% 27%",
401
+ "primary": "215 25% 27%",
402
+ "primaryForeground": "0 0% 100%",
403
+ "success": "152 76% 32%",
404
+ "successForeground": "0 0% 100%",
405
+ "destructive": "2 80% 55%",
406
+ "destructiveForeground": "0 0% 100%",
407
+ "warning": "38 92% 50%",
408
+ "warningForeground": "222 47% 11%"
409
+ },
410
+ "perception": {
411
+ "buttonRadius": "12px",
412
+ "cardRadius": "16px",
413
+ "inputRadius": "12px",
414
+ "modalRadius": "18px",
415
+ "fontFamily": "\"DM Sans\", ui-sans-serif, system-ui, -apple-system, sans-serif",
416
+ "fontFamilyMono": "\"JetBrains Mono\", ui-monospace, SFMono-Regular, monospace",
417
+ "shadowSm": "0 1px 3px rgba(15,23,42,0.06), 0 1px 2px rgba(15,23,42,0.04)",
418
+ "shadowMd": "0 4px 12px rgba(15,23,42,0.06), 0 2px 4px rgba(15,23,42,0.04)",
419
+ "shadowLg": "0 12px 32px rgba(15,23,42,0.08), 0 4px 12px rgba(15,23,42,0.04)"
420
+ }
421
+ }
422
+ $theme$::jsonb WHERE id = 'fiscal';
423
+
424
+ UPDATE app.apps SET theme = $theme$
425
+ {
426
+ "__kind": "saas-theme",
427
+ "name": "FullControl",
428
+ "brand": "224 64% 33%",
429
+ "font": "dm-sans",
430
+ "sidebar": {
431
+ "background": "222 30% 8%",
432
+ "foreground": "0 0% 100%",
433
+ "border": "0 0% 100% / 0.09",
434
+ "accent": "213 94% 68% / 0.16",
435
+ "accentForeground": "213 94% 68%",
436
+ "muted": "0 0% 100% / 0.6"
437
+ },
438
+ "content": {
439
+ "background": "210 20% 96%"
440
+ },
441
+ "colors": {
442
+ "background": "210 20% 96%",
443
+ "foreground": "222 25% 10%",
444
+ "card": "0 0% 100%",
445
+ "cardForeground": "222 25% 10%",
446
+ "popover": "0 0% 100%",
447
+ "popoverForeground": "222 25% 10%",
448
+ "border": "214 18% 92%",
449
+ "input": "214 15% 85%",
450
+ "muted": "214 18% 92%",
451
+ "mutedForeground": "215 14% 35%",
452
+ "secondary": "214 18% 92%",
453
+ "secondaryForeground": "222 25% 10%",
454
+ "accent": "214 95% 93%",
455
+ "accentForeground": "224 64% 33%",
456
+ "ring": "221 83% 53%",
457
+ "success": "152 76% 32%",
458
+ "successForeground": "0 0% 100%",
459
+ "destructive": "6 100% 68%",
460
+ "destructiveForeground": "0 0% 100%",
461
+ "warning": "38 92% 50%",
462
+ "warningForeground": "222 25% 10%"
463
+ },
464
+ "perception": {
465
+ "buttonRadius": "12px",
466
+ "cardRadius": "16px",
467
+ "inputRadius": "12px",
468
+ "modalRadius": "18px",
469
+ "fontFamily": "\"DM Sans\", ui-sans-serif, system-ui, -apple-system, sans-serif",
470
+ "fontFamilyMono": "\"JetBrains Mono\", ui-monospace, SFMono-Regular, monospace",
471
+ "shadowSm": "0 1px 3px rgba(13,17,26,0.06), 0 1px 2px rgba(13,17,26,0.04)",
472
+ "shadowMd": "0 4px 12px rgba(13,17,26,0.06), 0 2px 4px rgba(13,17,26,0.04)",
473
+ "shadowLg": "0 12px 32px rgba(13,17,26,0.08), 0 4px 12px rgba(13,17,26,0.04)"
474
+ }
475
+ }
476
+ $theme$::jsonb WHERE id = 'full';
477
+
478
+ -- ── §3 app_config() passa a entregar o tema ───────────────────────────────
479
+ --
480
+ -- Reemitida a partir do ARQUIVO da 115, e não da definição viva do cluster: o
481
+ -- comentário dela conta que reemitir a função copiando do banco já perdeu uma
482
+ -- chave TRÊS vezes. A única mudança é `'theme', a.theme` nos dois ramos da
483
+ -- união.
484
+
485
+ CREATE OR REPLACE FUNCTION public.app_config()
486
+ RETURNS jsonb
487
+ LANGUAGE plpgsql
488
+ STABLE SECURITY DEFINER
489
+ SET search_path TO ''
490
+ AS $function$
491
+ DECLARE
492
+ v_tenant uuid := app.current_tenant_id();
493
+ v_staff text;
494
+ BEGIN
495
+ -- `platform` é do CLUSTER, não da cadeia canônica: um banco montado do zero
496
+ -- (o db-from-zero, um pool novo) não tem esse schema, e uma chamada direta
497
+ -- estoura na inicialização da variável — antes de qualquer linha do corpo.
498
+ IF pg_catalog.to_regproc('platform.staff_role(uuid)') IS NOT NULL THEN
499
+ EXECUTE 'SELECT platform.staff_role($1)' INTO v_staff USING (SELECT auth.uid());
500
+ END IF;
501
+
502
+ IF v_tenant IS NULL THEN
503
+ RETURN jsonb_build_object('tenant_id', NULL, 'plugins', '[]'::jsonb, 'nav', '[]'::jsonb,
504
+ 'apps', '[]'::jsonb, 'permissions', '[]'::jsonb, 'generated_at', now());
505
+ END IF;
506
+
507
+ RETURN jsonb_build_object(
508
+ 'tenant_id', v_tenant,
509
+ 'branding', coalesce(app.brand_of(v_tenant), '{}'::jsonb),
510
+ 'settings', public.tenant_settings(),
511
+ 'plugins', coalesce((
512
+ SELECT jsonb_agg(jsonb_build_object('plugin', t.plugin_id, 'facet', nullif(t.facet, ''),
513
+ 'status', CASE WHEN t.expires_at IS NOT NULL AND t.expires_at <= now() THEN 'disabled' ELSE t.status END,
514
+ 'source', t.source, 'config', t.config, 'expires_at', t.expires_at)
515
+ ORDER BY t.plugin_id, t.facet)
516
+ FROM app.tenant_plugins t WHERE t.tenant_id = v_tenant), '[]'::jsonb),
517
+ -- `trade` ordena a lista: quem roda dois aplicativos tem UM ofício, e o
518
+ -- outro é retaguarda (084). Entre dois SEM ofício, quem declara
519
+ -- `preset.rail` vence quem não declara (115).
520
+ --
521
+ -- O PRIMEIRO desta lista é quem empresta o rail (`useAppPreset`) e agora
522
+ -- também quem empresta a COR (`useAppTheme`) — as duas leituras têm de ser
523
+ -- o mesmo aplicativo, senão a casa abre com o menu de um produto e a cara
524
+ -- de outro.
525
+ 'apps', coalesce((
526
+ SELECT jsonb_agg(x ORDER BY x ->> 'house',
527
+ (x ->> 'trade')::boolean DESC,
528
+ (x -> 'preset' -> 'rail' IS NOT NULL) DESC,
529
+ x ->> 'name') FROM (
530
+ SELECT jsonb_build_object('id', a.id, 'name', a.name, 'icon', a.icon, 'accent_color', a.accent_color,
531
+ 'url', a.url, 'house', false, 'preset', a.preset, 'theme', a.theme, 'vertical_id', a.vertical_id,
532
+ 'trade', a.vertical_id IS NOT NULL AND a.vertical_id NOT IN ('accounting','generic')) AS x
533
+ FROM app.tenant_apps ta JOIN app.apps a ON a.id = ta.app_id
534
+ WHERE ta.tenant_id = v_tenant AND ta.status = 'active' AND a.active
535
+ UNION ALL
536
+ SELECT jsonb_build_object('id', a.id, 'name', a.name, 'icon', a.icon, 'accent_color', a.accent_color,
537
+ 'url', a.url, 'house', true, 'preset', a.preset, 'theme', a.theme, 'vertical_id', a.vertical_id,
538
+ 'trade', false) AS x
539
+ FROM app.apps a WHERE v_staff IS NOT NULL AND a.id = 'control-admin' AND a.active AND a.url IS NOT NULL) apps), '[]'::jsonb),
540
+ 'nav', coalesce((
541
+ SELECT jsonb_agg(jsonb_build_object('key', n.entry_key, 'hidden', n.hidden, 'label', n.label,
542
+ 'section', n.section, 'position', n.position) ORDER BY n.entry_key)
543
+ FROM app.tenant_nav n WHERE n.tenant_id = v_tenant), '[]'::jsonb),
544
+ 'permissions', coalesce((
545
+ SELECT jsonb_agg(DISTINCT rp.permission)
546
+ FROM app.memberships m
547
+ JOIN app.role_permissions rp ON rp.role_id = m.role_id AND rp.tenant_id = m.tenant_id
548
+ WHERE m.tenant_id = v_tenant AND m.user_id = (SELECT auth.uid()) AND m.active
549
+ ), '[]'::jsonb),
550
+ 'generated_at', now());
551
+ END $function$;
552
+
553
+ -- ── §4 a porta ANÔNIMA: quem é este aplicativo, antes de haver sessão ─────
554
+ --
555
+ -- A tela de login acontece antes de existir tenant, e `app.apps` é legível só
556
+ -- para `authenticated` (001). Sem isto o `.env` do bundle não teria como ler a
557
+ -- linha que ele reivindica, e a tela de entrada do ChefControl continuaria
558
+ -- azul até alguém digitar a senha.
559
+ --
560
+ -- Devolve só o que uma porta de entrada mostra — nome, marca, cor. Não devolve
561
+ -- `preset` (a árvore do menu não é assunto de quem ainda não entrou) nem `url`
562
+ -- (a lista de origens do handoff de sessão continua atrás do login).
563
+ CREATE OR REPLACE FUNCTION public.app_identity(p_app text)
564
+ RETURNS jsonb
565
+ LANGUAGE sql
566
+ STABLE SECURITY DEFINER
567
+ SET search_path TO ''
568
+ AS $function$
569
+ SELECT jsonb_build_object('id', a.id, 'name', a.name, 'icon', a.icon,
570
+ 'accent_color', a.accent_color, 'theme', a.theme)
571
+ FROM app.apps a WHERE a.id = p_app AND a.active;
572
+ $function$;
573
+
574
+ REVOKE ALL ON FUNCTION public.app_identity(text) FROM PUBLIC;
575
+ GRANT EXECUTE ON FUNCTION public.app_identity(text) TO anon, authenticated;
576
+
577
+ COMMENT ON FUNCTION public.app_identity(text) IS
578
+ 'Como este aplicativo se apresenta antes de haver sessão (138): nome, ícone, cor e tema. A tela de login lê por aqui, a partir do VITE_APP_ID do bundle. Deliberadamente SEM preset e SEM url — o menu e a lista de origens do handoff continuam atrás do login.';
579
+
580
+ -- ── §5 a pele que ninguém escolheu sai da frente ──────────────────────────
581
+ --
582
+ -- `Espaço Renova Rio` está no `business`, então o teto de plano NÃO o segura —
583
+ -- e é isso que torna este UPDATE necessário em vez de automático. O marrom
584
+ -- #935427 que ele veste não foi decisão da casa: é `onboarding/submit.ts`
585
+ -- gravando uma paleta no cadastro. A casa quer o StudioControl como ele é.
586
+ --
587
+ -- Sai a PELE, fica a IDENTIDADE: nome e logo continuam, porque quem abre o
588
+ -- sistema abre o sistema DELES. É a mesma linha que a camada 2 desenha — e
589
+ -- aqui ela é traçada à mão porque não há como perguntar retroativamente a uma
590
+ -- conta se a cor dela foi escolha ou sobra de formulário.
591
+ --
592
+ -- As outras contas `business` do cluster (Artorius, Clínica Sol, Espaço Facial)
593
+ -- ficam como estão: são marcas de verdade, e a 093 existe por causa delas.
594
+ -- As contas `free` e `pro` não precisam de UPDATE nenhum — o teto de plano já
595
+ -- deixa de ler os tokens delas, e o dado fica no lugar para o dia em que
596
+ -- assinarem.
597
+ UPDATE public.tenants
598
+ SET settings = jsonb_set(
599
+ settings,
600
+ '{branding,tokens}',
601
+ '{}'::jsonb,
602
+ true
603
+ )
604
+ WHERE lower(name) = 'espaco renova rio'
605
+ AND settings #> '{branding,tokens}' IS NOT NULL
606
+ AND settings #>> '{branding,tokens,color,primary}' = '#935427';
@@ -0,0 +1,50 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 139_a_equipe_e_um_agrupamento_declarado.sql — a equipe entra, e três
3
+ -- agrupamentos que já estavam na tela param de ser indeclarados.
4
+ -- ---------------------------------------------------------------------------
5
+ -- Os Cadastros agrupam pelo arquétipo `category`: categoria de serviço, grupo
6
+ -- de locais, origem. Desde a 147 a espécie de uma categoria é uma LINHA em
7
+ -- `category_kinds`, e `categories.kind` tem chave estrangeira para ela
8
+ -- (`categories_kind_registered`). Só que as três espécies que o
9
+ -- `plugin-registry` escreve nunca foram declaradas por ninguém: o seed traz
10
+ -- `service`, `good`, `product`, `shop`, `collection`, `menu_category` e
11
+ -- `unclassified`, e mais nada.
12
+ --
13
+ -- O efeito é o pior tipo de defeito: a tela existe, o formulário abre, e o
14
+ -- salvar volta 23503 — violação de chave estrangeira — numa tela de cadastro,
15
+ -- que é onde ninguém procura por erro de integridade. Quem viu isso concluiu
16
+ -- "os Cadastros não funcionam", e não estava errado.
17
+ --
18
+ -- ── a equipe ────────────────────────────────────────────────────────────────
19
+ --
20
+ -- `staff_team` é a espécie nova: um agrupamento de FICHAS da equipe (people
21
+ -- kind='staff'), não de logins. A distinção importa e é a mesma que o
22
+ -- `v_team_members` já confunde por nome — aquilo são os membros do tenant
23
+ -- (app.memberships), quem entra no sistema. Uma casa tem recepção, estética e
24
+ -- vendas muito antes de ter três logins, e é a ficha que responde por turno,
25
+ -- comissão e escala.
26
+ --
27
+ -- O vínculo mora na ficha (`people.custom_fields->>'teamId'`), como a profissão
28
+ -- e a comissão: é uma escolha do cadastro, não uma terceira tabela de ligação
29
+ -- para uma pessoa que está em uma equipe.
30
+ --
31
+ -- As tabelas `public.teams` e `public.team_members` do baseline NÃO são isto e
32
+ -- não são tocadas aqui: nenhuma linha de código as lê, `team_members` aponta
33
+ -- para `user_id`, e reaproveitá-las por terem o nome certo seria herdar um
34
+ -- modelo que ninguém escolheu.
35
+ --
36
+ -- Idempotente: `register_kind` é upsert por espécie.
37
+ -- ---------------------------------------------------------------------------
38
+
39
+ SELECT public.register_kind('category', 'staff_team', 'registry', NULL, NULL,
40
+ 'Uma equipe da casa — o agrupamento das fichas da equipe (recepção, estética, vendas).');
41
+
42
+ -- As três que a tela já escrevia sem declarar.
43
+ SELECT public.register_kind('category', 'service_category', 'registry', NULL, NULL,
44
+ 'Categoria de serviço — como o catálogo de serviços se divide para quem marca.');
45
+
46
+ SELECT public.register_kind('category', 'location_group', 'registry', NULL, NULL,
47
+ 'Grupo de locais — a região ou o franqueado que recolhe várias filiais.');
48
+
49
+ SELECT public.register_kind('category', 'origin', 'registry', NULL, NULL,
50
+ 'Origem — por onde o cliente chegou até a casa.');