@fayz-ai/db 0.18.2 → 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.
- package/migrations/136_a_baixa_do_espelho_e_do_porte_nao_do_v2.sql +204 -0
- package/migrations/137_a_retirada_e_um_canal_de_venda.sql +43 -0
- package/migrations/138_o_produto_tem_cor_e_a_conta_so_a_veste_se_pagar.sql +606 -0
- package/migrations/139_a_equipe_e_um_agrupamento_declarado.sql +50 -0
- package/migrations/140_a_unidade_se_escreve_pela_mesma_porta_que_se_le.sql +130 -0
- package/migrations/141_o_endereco_da_unidade_mora_na_tabela_de_enderecos.sql +203 -0
- package/migrations/142_a_coordenada_e_do_endereco_nao_da_unidade.sql +157 -0
- package/migrations/143_o_preset_para_de_nomear_telas_que_nao_existem.sql +29 -0
- package/migrations/144_marketing_e_um_botao_nao_uma_gaveta.sql +46 -0
- package/migrations/145_campanhas_e_templates_somem_em_toda_conta.sql +36 -0
- package/migrations/146_a_clinica_ve_as_proprias_analises.sql +31 -0
- package/migrations/147_tres_saloes_desligam_o_rh.sql +30 -0
- package/migrations/148_servicos_ganham_a_permissao_que_nunca_tiveram.sql +37 -0
- package/migrations/149_a_conta_do_fornecedor_nao_e_conta_da_casa.sql +131 -0
- package/migrations/150_todo_cadastro_tem_observacao_e_etiqueta.sql +62 -0
- package/migrations/151_a_parceria_e_uma_especie_de_pessoa.sql +35 -0
- package/migrations/152_a_profissao_e_a_especie_de_pet_sao_catalogos.sql +31 -0
- package/migrations/153_a_sala_nao_e_a_filial.sql +94 -0
- package/migrations/154_as_duas_tabelas_novas_dizem_de_quem_sao.sql +33 -0
- package/migrations/155_o_pacote_tem_observacao_e_etiqueta_e_diz_de_quem_e.sql +28 -0
- package/migrations/156_restaurante_nao_vende_servico.sql +44 -0
- package/migrations/157_o_que_a_porta_da_unidade_criou_tambem_se_revoga.sql +45 -0
- package/package.json +1 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 140 — a unidade se escreve pela mesma porta por onde se lê
|
|
3
|
+
--
|
|
4
|
+
-- `v_units` é a ponte pública de `app.units`: a tela de unidades LÊ tudo por
|
|
5
|
+
-- ela — nome, tipo, telefone, cidade, código, lat/lng — porque `app.units`
|
|
6
|
+
-- guarda o endereço em `metadata` e o PostgREST não alcança o schema `app`.
|
|
7
|
+
--
|
|
8
|
+
-- Escrever, não. A view é auto-atualizável só nas colunas que são referência
|
|
9
|
+
-- simples (name, parent_id, is_active, metadata); `kind` é um
|
|
10
|
+
-- `coalesce(metadata->>'legacy_kind', kind)` e `is_headquarters` é um
|
|
11
|
+
-- `kind = 'hq'` — expressões, e o Postgres recusa:
|
|
12
|
+
--
|
|
13
|
+
-- cannot update column "kind" of view "v_units"
|
|
14
|
+
--
|
|
15
|
+
-- O CRUD genérico manda o formulário inteiro num UPDATE só, então TROCAR O
|
|
16
|
+
-- NOME de uma unidade falhava por causa do campo ao lado. E o mesmo valia para
|
|
17
|
+
-- telefone, cidade, código e coordenadas: escrever qualquer um deles pedia uma
|
|
18
|
+
-- RPC que nunca existiu (só `unit_create`, e só para criar).
|
|
19
|
+
--
|
|
20
|
+
-- O gatilho INSTEAD OF fecha o círculo: a view passa a aceitar a MESMA forma
|
|
21
|
+
-- que devolve, e a tradução para `app.units` + `metadata` mora no banco, ao
|
|
22
|
+
-- lado da tradução de leitura, em vez de virar um caso especial no cliente.
|
|
23
|
+
--
|
|
24
|
+
-- SEM `security definer`, de propósito: a função roda como quem chamou, e a RLS
|
|
25
|
+
-- de `app.units` (units.manage) continua sendo a cerca. A view só deixou de
|
|
26
|
+
-- mentir sobre o que aceita; ela não ganhou permissão nenhuma.
|
|
27
|
+
--
|
|
28
|
+
-- `kind` tem duas casas porque `app.units.kind` só admite hq|branch: 'region'
|
|
29
|
+
-- — que agrupa unidades e onde ninguém trabalha — mora em
|
|
30
|
+
-- `metadata->>'legacy_kind'`, e é isso que a leitura já coalescia.
|
|
31
|
+
-- ============================================================================
|
|
32
|
+
|
|
33
|
+
CREATE OR REPLACE FUNCTION app.trg_v_units_write() RETURNS trigger
|
|
34
|
+
LANGUAGE plpgsql
|
|
35
|
+
SET search_path TO ''
|
|
36
|
+
AS $$
|
|
37
|
+
DECLARE
|
|
38
|
+
-- As chaves de `metadata` que ESTA view governa. Apagadas antes da mesclagem
|
|
39
|
+
-- para que limpar um campo na tela apague a chave, em vez de gravar "" —
|
|
40
|
+
-- `metadata->>'city'` devolvendo string vazia é pior que devolvendo NULL.
|
|
41
|
+
k_managed constant text[] := ARRAY['email','phone','address','city','state','country',
|
|
42
|
+
'postal_code','notes','code','latitude','longitude','created_by'];
|
|
43
|
+
v_meta jsonb;
|
|
44
|
+
v_overlay jsonb;
|
|
45
|
+
v_kind text;
|
|
46
|
+
v_legacy text;
|
|
47
|
+
v_tenant uuid;
|
|
48
|
+
v_id uuid;
|
|
49
|
+
BEGIN
|
|
50
|
+
IF TG_OP = 'DELETE' THEN
|
|
51
|
+
DELETE FROM app.units u WHERE u.id = OLD.id;
|
|
52
|
+
RETURN OLD;
|
|
53
|
+
END IF;
|
|
54
|
+
|
|
55
|
+
-- Num UPDATE parcial o Postgres preenche NEW com a linha antiga, então o que
|
|
56
|
+
-- ninguém tocou é reescrito igual. É por isso que a base é NEW.metadata: ela
|
|
57
|
+
-- já É a metadata antiga quando o cliente não mandou nenhuma.
|
|
58
|
+
v_meta := coalesce(NEW.metadata, '{}'::jsonb);
|
|
59
|
+
|
|
60
|
+
-- 'hq' não é um tipo que se escolhe na lista: é o interruptor "matriz". Um
|
|
61
|
+
-- valor fora de hq|branch (hoje 'region') não cabe na coluna e vai para
|
|
62
|
+
-- legacy_kind, que é de onde a leitura o tira.
|
|
63
|
+
v_legacy := nullif(btrim(coalesce(NEW.kind, '')), '');
|
|
64
|
+
IF coalesce(NEW.is_headquarters, false) OR v_legacy = 'hq' THEN
|
|
65
|
+
v_kind := 'hq';
|
|
66
|
+
ELSE
|
|
67
|
+
v_kind := 'branch';
|
|
68
|
+
END IF;
|
|
69
|
+
IF v_legacy IN ('hq', 'branch') OR v_legacy IS NULL THEN v_legacy := NULL; END IF;
|
|
70
|
+
|
|
71
|
+
v_overlay := jsonb_strip_nulls(jsonb_build_object(
|
|
72
|
+
'email', nullif(btrim(coalesce(NEW.email, '')), ''),
|
|
73
|
+
'phone', nullif(btrim(coalesce(NEW.phone, '')), ''),
|
|
74
|
+
'address', nullif(btrim(coalesce(NEW.address, '')), ''),
|
|
75
|
+
'city', nullif(btrim(coalesce(NEW.city, '')), ''),
|
|
76
|
+
'state', nullif(btrim(coalesce(NEW.state, '')), ''),
|
|
77
|
+
'country', nullif(btrim(coalesce(NEW.country, '')), ''),
|
|
78
|
+
'postal_code', nullif(btrim(coalesce(NEW.postal_code, '')), ''),
|
|
79
|
+
'notes', nullif(btrim(coalesce(NEW.notes, '')), ''),
|
|
80
|
+
'code', nullif(btrim(coalesce(NEW.code, '')), ''),
|
|
81
|
+
'latitude', CASE WHEN NEW.latitude IS NULL THEN NULL ELSE NEW.latitude::text END,
|
|
82
|
+
'longitude', CASE WHEN NEW.longitude IS NULL THEN NULL ELSE NEW.longitude::text END,
|
|
83
|
+
'created_by', CASE WHEN NEW.created_by IS NULL THEN NULL ELSE NEW.created_by::text END
|
|
84
|
+
));
|
|
85
|
+
|
|
86
|
+
v_meta := (v_meta - k_managed) || v_overlay;
|
|
87
|
+
|
|
88
|
+
IF v_legacy IS NULL THEN v_meta := v_meta - 'legacy_kind';
|
|
89
|
+
ELSE v_meta := jsonb_set(v_meta, '{legacy_kind}', to_jsonb(v_legacy)); END IF;
|
|
90
|
+
|
|
91
|
+
IF NEW.tags IS NULL OR cardinality(NEW.tags) = 0 THEN v_meta := v_meta - 'tags';
|
|
92
|
+
ELSE v_meta := jsonb_set(v_meta, '{tags}', to_jsonb(NEW.tags)); END IF;
|
|
93
|
+
|
|
94
|
+
IF NEW.custom_fields IS NULL OR NEW.custom_fields = '{}'::jsonb THEN v_meta := v_meta - 'custom_fields';
|
|
95
|
+
ELSE v_meta := jsonb_set(v_meta, '{custom_fields}', NEW.custom_fields); END IF;
|
|
96
|
+
|
|
97
|
+
IF TG_OP = 'INSERT' THEN
|
|
98
|
+
v_tenant := coalesce(NEW.tenant_id, app.current_tenant_id());
|
|
99
|
+
INSERT INTO app.units (id, tenant_id, kind, name, parent_unit_id, active, metadata)
|
|
100
|
+
VALUES (coalesce(NEW.id, gen_random_uuid()), v_tenant, v_kind, btrim(NEW.name),
|
|
101
|
+
NEW.parent_id, coalesce(NEW.is_active, true), v_meta)
|
|
102
|
+
RETURNING id INTO v_id;
|
|
103
|
+
ELSE
|
|
104
|
+
UPDATE app.units u
|
|
105
|
+
SET kind = v_kind,
|
|
106
|
+
name = btrim(NEW.name),
|
|
107
|
+
parent_unit_id = NEW.parent_id,
|
|
108
|
+
active = coalesce(NEW.is_active, true),
|
|
109
|
+
metadata = v_meta,
|
|
110
|
+
updated_at = now()
|
|
111
|
+
WHERE u.id = OLD.id
|
|
112
|
+
RETURNING u.id INTO v_id;
|
|
113
|
+
IF v_id IS NULL THEN
|
|
114
|
+
RAISE EXCEPTION 'v_units: unit % not found or not yours to change', OLD.id USING ERRCODE = '42501';
|
|
115
|
+
END IF;
|
|
116
|
+
END IF;
|
|
117
|
+
|
|
118
|
+
-- O RETURNING do cliente lê a VIEW de novo; devolver NEW cru daria a ele os
|
|
119
|
+
-- valores que ele mandou, e não os que o banco guardou.
|
|
120
|
+
SELECT * INTO NEW FROM public.v_units WHERE id = v_id;
|
|
121
|
+
RETURN NEW;
|
|
122
|
+
END $$;
|
|
123
|
+
|
|
124
|
+
COMMENT ON FUNCTION app.trg_v_units_write() IS
|
|
125
|
+
'Escreve v_units em app.units: kind fora de hq|branch vai para metadata.legacy_kind, o endereço vai para metadata (140).';
|
|
126
|
+
|
|
127
|
+
DROP TRIGGER IF EXISTS v_units_write ON public.v_units;
|
|
128
|
+
CREATE TRIGGER v_units_write
|
|
129
|
+
INSTEAD OF INSERT OR UPDATE OR DELETE ON public.v_units
|
|
130
|
+
FOR EACH ROW EXECUTE FUNCTION app.trg_v_units_write();
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 141 — o endereço da unidade mora na tabela de endereços
|
|
3
|
+
--
|
|
4
|
+
-- A unidade guardava o endereço em cinco chaves planas de `metadata`
|
|
5
|
+
-- (address, city, state, country, postal_code), e endereço já é uma TABELA:
|
|
6
|
+
-- `public.addresses`, a mesma da ficha da pessoa, que sempre aceitou
|
|
7
|
+
-- `owner_type = 'unit'` — havia inclusive linha gravada assim.
|
|
8
|
+
--
|
|
9
|
+
-- Duas verdades para a mesma coisa cobram o preço de sempre. Uma conta migrada
|
|
10
|
+
-- do V1 escreveu em `metadata.address` o objeto INTEIRO serializado, e a tela
|
|
11
|
+
-- de unidade mostrava, dentro de um input de uma linha:
|
|
12
|
+
--
|
|
13
|
+
-- {"city": "Rio de Janeiro", "state": "RJ", "number": "60", "street": ...}
|
|
14
|
+
--
|
|
15
|
+
-- Aqui a view passa a LER o endereço de `public.addresses` e o gatilho de
|
|
16
|
+
-- escrita (140) para de tocar naquelas cinco chaves. O formulário ganha o
|
|
17
|
+
-- mesmo bloco da pessoa — com CEP que consulta sozinho, número, complemento e
|
|
18
|
+
-- bairro —, limitado a UM: uma loja tem um endereço.
|
|
19
|
+
--
|
|
20
|
+
-- Uma unidade, um endereço: a linha nasce `kind = 'main'` e padrão, e
|
|
21
|
+
-- `addresses_one_default_idx` (UNIQUE em owner_type, owner_id, kind WHERE
|
|
22
|
+
-- is_default) é quem garante que não apareça um segundo.
|
|
23
|
+
-- ============================================================================
|
|
24
|
+
|
|
25
|
+
-- ── 1. o que estava em metadata vira linha ──────────────────────────────────
|
|
26
|
+
-- Dois formatos no mesmo campo: o objeto serializado que o porte do V1 deixou,
|
|
27
|
+
-- e a string simples de quem digitou na tela. `jsonb_typeof` separa os dois —
|
|
28
|
+
-- e um texto que não é JSON faria o cast estourar, então ele é tentado dentro
|
|
29
|
+
-- de um bloco que devolve NULL em vez de derrubar a migration.
|
|
30
|
+
CREATE OR REPLACE FUNCTION app.parse_legacy_address(p_raw text) RETURNS jsonb
|
|
31
|
+
LANGUAGE plpgsql IMMUTABLE
|
|
32
|
+
SET search_path TO ''
|
|
33
|
+
AS $$
|
|
34
|
+
DECLARE v jsonb;
|
|
35
|
+
BEGIN
|
|
36
|
+
IF p_raw IS NULL OR btrim(p_raw) = '' THEN RETURN NULL; END IF;
|
|
37
|
+
BEGIN v := p_raw::jsonb; EXCEPTION WHEN OTHERS THEN RETURN NULL; END;
|
|
38
|
+
IF jsonb_typeof(v) <> 'object' THEN RETURN NULL; END IF;
|
|
39
|
+
RETURN v;
|
|
40
|
+
END $$;
|
|
41
|
+
|
|
42
|
+
INSERT INTO public.addresses
|
|
43
|
+
(tenant_id, owner_type, owner_id, kind, postal_code, street, number, complement, district, city, state, country, is_default, metadata)
|
|
44
|
+
SELECT u.tenant_id, 'unit', u.id, 'main',
|
|
45
|
+
p.postal_code, p.street, p.number, p.complement, p.district, p.city, p.state,
|
|
46
|
+
coalesce(nullif(btrim(coalesce(u.metadata ->> 'country', '')), ''), 'BR'),
|
|
47
|
+
true,
|
|
48
|
+
jsonb_build_object('backfilled_from', 'app.units.metadata')
|
|
49
|
+
FROM app.units u
|
|
50
|
+
CROSS JOIN LATERAL (
|
|
51
|
+
SELECT j,
|
|
52
|
+
coalesce(nullif(btrim(coalesce(j ->> 'postal_code', u.metadata ->> 'postal_code', '')), ''), '') AS postal_code,
|
|
53
|
+
-- Sem objeto, o campo plano JÁ É a rua. Com objeto, a rua é a chave
|
|
54
|
+
-- `street` — e nunca o objeto inteiro, que é como 52 unidades
|
|
55
|
+
-- ganharam uma linha vazia na primeira volta desta migration.
|
|
56
|
+
coalesce(nullif(btrim(coalesce(j ->> 'street',
|
|
57
|
+
CASE WHEN j IS NULL THEN u.metadata ->> 'address' END, '')), ''), '') AS street,
|
|
58
|
+
nullif(btrim(coalesce(j ->> 'number', '')), '') AS number,
|
|
59
|
+
nullif(btrim(coalesce(j ->> 'complement', '')), '') AS complement,
|
|
60
|
+
nullif(btrim(coalesce(j ->> 'neighborhood', j ->> 'district', '')), '') AS district,
|
|
61
|
+
coalesce(nullif(btrim(coalesce(j ->> 'city', u.metadata ->> 'city', '')), ''), '') AS city,
|
|
62
|
+
coalesce(nullif(btrim(coalesce(j ->> 'state', u.metadata ->> 'state', '')), ''), '') AS state
|
|
63
|
+
FROM (SELECT app.parse_legacy_address(u.metadata ->> 'address') AS j) q
|
|
64
|
+
) p
|
|
65
|
+
WHERE NOT EXISTS (
|
|
66
|
+
SELECT 1 FROM public.addresses a WHERE a.owner_type = 'unit' AND a.owner_id = u.id)
|
|
67
|
+
-- Só quem tem o que migrar. O porte do V1 deixou objetos com TODAS as
|
|
68
|
+
-- chaves nulas; uma linha vazia para eles seria endereço inventado.
|
|
69
|
+
AND (p.street <> '' OR p.city <> '');
|
|
70
|
+
|
|
71
|
+
-- ── 2. a view lê o endereço de onde ele mora ────────────────────────────────
|
|
72
|
+
CREATE OR REPLACE VIEW public.v_units WITH (security_invoker='true') AS
|
|
73
|
+
SELECT u.id,
|
|
74
|
+
u.tenant_id,
|
|
75
|
+
coalesce(u.metadata ->> 'legacy_kind', u.kind) AS kind,
|
|
76
|
+
u.name,
|
|
77
|
+
u.metadata ->> 'email' AS email,
|
|
78
|
+
u.metadata ->> 'phone' AS phone,
|
|
79
|
+
-- A linha da rua como uma lista a mostra: "Rua X, 60". O número é
|
|
80
|
+
-- coluna própria em `addresses` e não se perde ao voltar para cá.
|
|
81
|
+
nullif(btrim(coalesce(a.street, '') ||
|
|
82
|
+
CASE WHEN nullif(btrim(coalesce(a.number, '')), '') IS NULL
|
|
83
|
+
THEN '' ELSE ', ' || btrim(a.number) END), '') AS address,
|
|
84
|
+
a.city,
|
|
85
|
+
a.state,
|
|
86
|
+
coalesce(a.country, u.metadata ->> 'country', 'BR') AS country,
|
|
87
|
+
a.postal_code,
|
|
88
|
+
(u.kind = 'hq') AS is_headquarters,
|
|
89
|
+
u.active AS is_active,
|
|
90
|
+
coalesce((SELECT array_agg(t) FROM jsonb_array_elements_text(
|
|
91
|
+
CASE WHEN jsonb_typeof(u.metadata -> 'tags') = 'array'
|
|
92
|
+
THEN u.metadata -> 'tags' ELSE '[]'::jsonb END) t), '{}'::text[]) AS tags,
|
|
93
|
+
u.metadata ->> 'notes' AS notes,
|
|
94
|
+
u.metadata,
|
|
95
|
+
CASE WHEN jsonb_typeof(u.metadata -> 'custom_fields') = 'object'
|
|
96
|
+
THEN u.metadata -> 'custom_fields' ELSE '{}'::jsonb END AS custom_fields,
|
|
97
|
+
CASE WHEN u.metadata ->> 'created_by' ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
98
|
+
THEN (u.metadata ->> 'created_by')::uuid END AS created_by,
|
|
99
|
+
u.created_at,
|
|
100
|
+
u.updated_at,
|
|
101
|
+
u.parent_unit_id AS parent_id,
|
|
102
|
+
u.metadata ->> 'code' AS code,
|
|
103
|
+
CASE WHEN u.metadata ->> 'latitude' ~ '^-?[0-9]+(\.[0-9]+)?$'
|
|
104
|
+
THEN (u.metadata ->> 'latitude')::numeric END AS latitude,
|
|
105
|
+
CASE WHEN u.metadata ->> 'longitude' ~ '^-?[0-9]+(\.[0-9]+)?$'
|
|
106
|
+
THEN (u.metadata ->> 'longitude')::numeric END AS longitude
|
|
107
|
+
FROM app.units u
|
|
108
|
+
-- LEFT: uma unidade sem endereço continua aparecendo na lista. Um JOIN
|
|
109
|
+
-- comum a faria sumir da tela por não ter CEP.
|
|
110
|
+
LEFT JOIN LATERAL (
|
|
111
|
+
SELECT x.* FROM public.addresses x
|
|
112
|
+
WHERE x.owner_type = 'unit' AND x.owner_id = u.id
|
|
113
|
+
ORDER BY x.is_default DESC, x.created_at
|
|
114
|
+
LIMIT 1
|
|
115
|
+
) a ON true;
|
|
116
|
+
|
|
117
|
+
COMMENT ON VIEW public.v_units IS
|
|
118
|
+
'Units in the shape the units screen reads (#214), com o endereço vindo de public.addresses (141).';
|
|
119
|
+
|
|
120
|
+
-- ── 3. quem escreve o endereço é a tabela de endereços ──────────────────────
|
|
121
|
+
CREATE OR REPLACE FUNCTION app.trg_v_units_write() RETURNS trigger
|
|
122
|
+
LANGUAGE plpgsql
|
|
123
|
+
SET search_path TO ''
|
|
124
|
+
AS $$
|
|
125
|
+
DECLARE
|
|
126
|
+
-- O endereço saiu desta lista (141): as colunas de endereço da view agora são
|
|
127
|
+
-- LEITURA de public.addresses, e aceitá-las aqui recriaria a segunda verdade.
|
|
128
|
+
-- Telefone, e-mail, código e coordenadas continuam sendo da unidade.
|
|
129
|
+
k_managed constant text[] := ARRAY['email','phone','notes','code','latitude','longitude','created_by'];
|
|
130
|
+
v_meta jsonb;
|
|
131
|
+
v_overlay jsonb;
|
|
132
|
+
v_kind text;
|
|
133
|
+
v_legacy text;
|
|
134
|
+
v_tenant uuid;
|
|
135
|
+
v_id uuid;
|
|
136
|
+
BEGIN
|
|
137
|
+
IF TG_OP = 'DELETE' THEN
|
|
138
|
+
DELETE FROM public.addresses a WHERE a.owner_type = 'unit' AND a.owner_id = OLD.id;
|
|
139
|
+
DELETE FROM app.units u WHERE u.id = OLD.id;
|
|
140
|
+
RETURN OLD;
|
|
141
|
+
END IF;
|
|
142
|
+
|
|
143
|
+
v_meta := coalesce(NEW.metadata, '{}'::jsonb);
|
|
144
|
+
|
|
145
|
+
v_legacy := nullif(btrim(coalesce(NEW.kind, '')), '');
|
|
146
|
+
IF coalesce(NEW.is_headquarters, false) OR v_legacy = 'hq' THEN
|
|
147
|
+
v_kind := 'hq';
|
|
148
|
+
ELSE
|
|
149
|
+
v_kind := 'branch';
|
|
150
|
+
END IF;
|
|
151
|
+
IF v_legacy IN ('hq', 'branch') OR v_legacy IS NULL THEN v_legacy := NULL; END IF;
|
|
152
|
+
|
|
153
|
+
v_overlay := jsonb_strip_nulls(jsonb_build_object(
|
|
154
|
+
'email', nullif(btrim(coalesce(NEW.email, '')), ''),
|
|
155
|
+
'phone', nullif(btrim(coalesce(NEW.phone, '')), ''),
|
|
156
|
+
'notes', nullif(btrim(coalesce(NEW.notes, '')), ''),
|
|
157
|
+
'code', nullif(btrim(coalesce(NEW.code, '')), ''),
|
|
158
|
+
'latitude', CASE WHEN NEW.latitude IS NULL THEN NULL ELSE NEW.latitude::text END,
|
|
159
|
+
'longitude', CASE WHEN NEW.longitude IS NULL THEN NULL ELSE NEW.longitude::text END,
|
|
160
|
+
'created_by', CASE WHEN NEW.created_by IS NULL THEN NULL ELSE NEW.created_by::text END
|
|
161
|
+
));
|
|
162
|
+
|
|
163
|
+
-- As cinco chaves velhas de endereço saem de vez: elas já viraram linha em
|
|
164
|
+
-- public.addresses, e deixá-las no metadata seria guardar uma cópia que
|
|
165
|
+
-- ninguém mais atualiza.
|
|
166
|
+
v_meta := (v_meta - k_managed - ARRAY['address','city','state','country','postal_code']) || v_overlay;
|
|
167
|
+
|
|
168
|
+
IF v_legacy IS NULL THEN v_meta := v_meta - 'legacy_kind';
|
|
169
|
+
ELSE v_meta := jsonb_set(v_meta, '{legacy_kind}', to_jsonb(v_legacy)); END IF;
|
|
170
|
+
|
|
171
|
+
IF NEW.tags IS NULL OR cardinality(NEW.tags) = 0 THEN v_meta := v_meta - 'tags';
|
|
172
|
+
ELSE v_meta := jsonb_set(v_meta, '{tags}', to_jsonb(NEW.tags)); END IF;
|
|
173
|
+
|
|
174
|
+
IF NEW.custom_fields IS NULL OR NEW.custom_fields = '{}'::jsonb THEN v_meta := v_meta - 'custom_fields';
|
|
175
|
+
ELSE v_meta := jsonb_set(v_meta, '{custom_fields}', NEW.custom_fields); END IF;
|
|
176
|
+
|
|
177
|
+
IF TG_OP = 'INSERT' THEN
|
|
178
|
+
v_tenant := coalesce(NEW.tenant_id, app.current_tenant_id());
|
|
179
|
+
INSERT INTO app.units (id, tenant_id, kind, name, parent_unit_id, active, metadata)
|
|
180
|
+
VALUES (coalesce(NEW.id, gen_random_uuid()), v_tenant, v_kind, btrim(NEW.name),
|
|
181
|
+
NEW.parent_id, coalesce(NEW.is_active, true), v_meta)
|
|
182
|
+
RETURNING id INTO v_id;
|
|
183
|
+
ELSE
|
|
184
|
+
UPDATE app.units u
|
|
185
|
+
SET kind = v_kind,
|
|
186
|
+
name = btrim(NEW.name),
|
|
187
|
+
parent_unit_id = NEW.parent_id,
|
|
188
|
+
active = coalesce(NEW.is_active, true),
|
|
189
|
+
metadata = v_meta,
|
|
190
|
+
updated_at = now()
|
|
191
|
+
WHERE u.id = OLD.id
|
|
192
|
+
RETURNING u.id INTO v_id;
|
|
193
|
+
IF v_id IS NULL THEN
|
|
194
|
+
RAISE EXCEPTION 'v_units: unit % not found or not yours to change', OLD.id USING ERRCODE = '42501';
|
|
195
|
+
END IF;
|
|
196
|
+
END IF;
|
|
197
|
+
|
|
198
|
+
SELECT * INTO NEW FROM public.v_units WHERE id = v_id;
|
|
199
|
+
RETURN NEW;
|
|
200
|
+
END $$;
|
|
201
|
+
|
|
202
|
+
COMMENT ON FUNCTION app.trg_v_units_write() IS
|
|
203
|
+
'Escreve v_units em app.units; o endereço é de public.addresses e não passa por aqui (141).';
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 142 — a coordenada é do endereço, não da unidade
|
|
3
|
+
--
|
|
4
|
+
-- Latitude e longitude ficaram no `metadata` da unidade quando o endereço saiu
|
|
5
|
+
-- de lá (141), e não é ali que elas moram: um par de coordenadas é a POSIÇÃO
|
|
6
|
+
-- DE UM ENDEREÇO. Separadas dele, a primeira mudança de rua deixa o ponteiro
|
|
7
|
+
-- apontando para a loja antiga — e nada no banco reclama.
|
|
8
|
+
--
|
|
9
|
+
-- E o mesmo par serve a quem não é unidade: o endereço de entrega de um
|
|
10
|
+
-- cliente quer coordenada pelo mesmo motivo que a loja quer.
|
|
11
|
+
--
|
|
12
|
+
-- A view continua publicando `latitude`/`longitude` — quem sugere "a unidade
|
|
13
|
+
-- mais perto de mim" lê de lá —, só que agora tirando-as da linha de endereço.
|
|
14
|
+
-- ============================================================================
|
|
15
|
+
|
|
16
|
+
ALTER TABLE public.addresses
|
|
17
|
+
ADD COLUMN IF NOT EXISTS latitude numeric,
|
|
18
|
+
ADD COLUMN IF NOT EXISTS longitude numeric;
|
|
19
|
+
|
|
20
|
+
COMMENT ON COLUMN public.addresses.latitude IS
|
|
21
|
+
'Opcional. Só junto de longitude — meia coordenada não localiza nada (142).';
|
|
22
|
+
|
|
23
|
+
-- O que estava na unidade vai para a linha de endereço dela. Cast guardado: o
|
|
24
|
+
-- valor veio de um campo de texto e uma string torta derrubaria a migration.
|
|
25
|
+
UPDATE public.addresses a
|
|
26
|
+
SET latitude = (u.metadata ->> 'latitude')::numeric,
|
|
27
|
+
longitude = (u.metadata ->> 'longitude')::numeric
|
|
28
|
+
FROM app.units u
|
|
29
|
+
WHERE a.owner_type = 'unit' AND a.owner_id = u.id
|
|
30
|
+
AND a.latitude IS NULL AND a.longitude IS NULL
|
|
31
|
+
AND u.metadata ->> 'latitude' ~ '^-?[0-9]+(\.[0-9]+)?$'
|
|
32
|
+
AND u.metadata ->> 'longitude' ~ '^-?[0-9]+(\.[0-9]+)?$';
|
|
33
|
+
|
|
34
|
+
UPDATE app.units u
|
|
35
|
+
SET metadata = u.metadata - 'latitude' - 'longitude'
|
|
36
|
+
WHERE u.metadata ? 'latitude' OR u.metadata ? 'longitude';
|
|
37
|
+
|
|
38
|
+
CREATE OR REPLACE VIEW public.v_units WITH (security_invoker='true') AS
|
|
39
|
+
SELECT u.id,
|
|
40
|
+
u.tenant_id,
|
|
41
|
+
coalesce(u.metadata ->> 'legacy_kind', u.kind) AS kind,
|
|
42
|
+
u.name,
|
|
43
|
+
u.metadata ->> 'email' AS email,
|
|
44
|
+
u.metadata ->> 'phone' AS phone,
|
|
45
|
+
nullif(btrim(coalesce(a.street, '') ||
|
|
46
|
+
CASE WHEN nullif(btrim(coalesce(a.number, '')), '') IS NULL
|
|
47
|
+
THEN '' ELSE ', ' || btrim(a.number) END), '') AS address,
|
|
48
|
+
a.city,
|
|
49
|
+
a.state,
|
|
50
|
+
coalesce(a.country, 'BR') AS country,
|
|
51
|
+
a.postal_code,
|
|
52
|
+
(u.kind = 'hq') AS is_headquarters,
|
|
53
|
+
u.active AS is_active,
|
|
54
|
+
coalesce((SELECT array_agg(t) FROM jsonb_array_elements_text(
|
|
55
|
+
CASE WHEN jsonb_typeof(u.metadata -> 'tags') = 'array'
|
|
56
|
+
THEN u.metadata -> 'tags' ELSE '[]'::jsonb END) t), '{}'::text[]) AS tags,
|
|
57
|
+
u.metadata ->> 'notes' AS notes,
|
|
58
|
+
u.metadata,
|
|
59
|
+
CASE WHEN jsonb_typeof(u.metadata -> 'custom_fields') = 'object'
|
|
60
|
+
THEN u.metadata -> 'custom_fields' ELSE '{}'::jsonb END AS custom_fields,
|
|
61
|
+
CASE WHEN u.metadata ->> 'created_by' ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
62
|
+
THEN (u.metadata ->> 'created_by')::uuid END AS created_by,
|
|
63
|
+
u.created_at,
|
|
64
|
+
u.updated_at,
|
|
65
|
+
u.parent_unit_id AS parent_id,
|
|
66
|
+
u.metadata ->> 'code' AS code,
|
|
67
|
+
a.latitude,
|
|
68
|
+
a.longitude
|
|
69
|
+
FROM app.units u
|
|
70
|
+
LEFT JOIN LATERAL (
|
|
71
|
+
SELECT x.* FROM public.addresses x
|
|
72
|
+
WHERE x.owner_type = 'unit' AND x.owner_id = u.id
|
|
73
|
+
ORDER BY x.is_default DESC, x.created_at
|
|
74
|
+
LIMIT 1
|
|
75
|
+
) a ON true;
|
|
76
|
+
|
|
77
|
+
COMMENT ON VIEW public.v_units IS
|
|
78
|
+
'Units in the shape the units screen reads (#214); endereço e coordenada vêm de public.addresses (141, 142).';
|
|
79
|
+
|
|
80
|
+
CREATE OR REPLACE FUNCTION app.trg_v_units_write() RETURNS trigger
|
|
81
|
+
LANGUAGE plpgsql
|
|
82
|
+
SET search_path TO ''
|
|
83
|
+
AS $$
|
|
84
|
+
DECLARE
|
|
85
|
+
-- Endereço (141) e coordenada (142) saíram daqui: são colunas de
|
|
86
|
+
-- public.addresses, e aceitá-las recriaria a segunda verdade.
|
|
87
|
+
k_managed constant text[] := ARRAY['email','phone','notes','code','created_by'];
|
|
88
|
+
k_moved constant text[] := ARRAY['address','city','state','country','postal_code','latitude','longitude'];
|
|
89
|
+
v_meta jsonb;
|
|
90
|
+
v_overlay jsonb;
|
|
91
|
+
v_kind text;
|
|
92
|
+
v_legacy text;
|
|
93
|
+
v_tenant uuid;
|
|
94
|
+
v_id uuid;
|
|
95
|
+
BEGIN
|
|
96
|
+
IF TG_OP = 'DELETE' THEN
|
|
97
|
+
DELETE FROM public.addresses a WHERE a.owner_type = 'unit' AND a.owner_id = OLD.id;
|
|
98
|
+
DELETE FROM app.units u WHERE u.id = OLD.id;
|
|
99
|
+
RETURN OLD;
|
|
100
|
+
END IF;
|
|
101
|
+
|
|
102
|
+
v_meta := coalesce(NEW.metadata, '{}'::jsonb);
|
|
103
|
+
|
|
104
|
+
v_legacy := nullif(btrim(coalesce(NEW.kind, '')), '');
|
|
105
|
+
IF coalesce(NEW.is_headquarters, false) OR v_legacy = 'hq' THEN
|
|
106
|
+
v_kind := 'hq';
|
|
107
|
+
ELSE
|
|
108
|
+
v_kind := 'branch';
|
|
109
|
+
END IF;
|
|
110
|
+
IF v_legacy IN ('hq', 'branch') OR v_legacy IS NULL THEN v_legacy := NULL; END IF;
|
|
111
|
+
|
|
112
|
+
v_overlay := jsonb_strip_nulls(jsonb_build_object(
|
|
113
|
+
'email', nullif(btrim(coalesce(NEW.email, '')), ''),
|
|
114
|
+
'phone', nullif(btrim(coalesce(NEW.phone, '')), ''),
|
|
115
|
+
'notes', nullif(btrim(coalesce(NEW.notes, '')), ''),
|
|
116
|
+
'code', nullif(btrim(coalesce(NEW.code, '')), ''),
|
|
117
|
+
'created_by', CASE WHEN NEW.created_by IS NULL THEN NULL ELSE NEW.created_by::text END
|
|
118
|
+
));
|
|
119
|
+
|
|
120
|
+
v_meta := (v_meta - k_managed - k_moved) || v_overlay;
|
|
121
|
+
|
|
122
|
+
IF v_legacy IS NULL THEN v_meta := v_meta - 'legacy_kind';
|
|
123
|
+
ELSE v_meta := jsonb_set(v_meta, '{legacy_kind}', to_jsonb(v_legacy)); END IF;
|
|
124
|
+
|
|
125
|
+
IF NEW.tags IS NULL OR cardinality(NEW.tags) = 0 THEN v_meta := v_meta - 'tags';
|
|
126
|
+
ELSE v_meta := jsonb_set(v_meta, '{tags}', to_jsonb(NEW.tags)); END IF;
|
|
127
|
+
|
|
128
|
+
IF NEW.custom_fields IS NULL OR NEW.custom_fields = '{}'::jsonb THEN v_meta := v_meta - 'custom_fields';
|
|
129
|
+
ELSE v_meta := jsonb_set(v_meta, '{custom_fields}', NEW.custom_fields); END IF;
|
|
130
|
+
|
|
131
|
+
IF TG_OP = 'INSERT' THEN
|
|
132
|
+
v_tenant := coalesce(NEW.tenant_id, app.current_tenant_id());
|
|
133
|
+
INSERT INTO app.units (id, tenant_id, kind, name, parent_unit_id, active, metadata)
|
|
134
|
+
VALUES (coalesce(NEW.id, gen_random_uuid()), v_tenant, v_kind, btrim(NEW.name),
|
|
135
|
+
NEW.parent_id, coalesce(NEW.is_active, true), v_meta)
|
|
136
|
+
RETURNING id INTO v_id;
|
|
137
|
+
ELSE
|
|
138
|
+
UPDATE app.units u
|
|
139
|
+
SET kind = v_kind,
|
|
140
|
+
name = btrim(NEW.name),
|
|
141
|
+
parent_unit_id = NEW.parent_id,
|
|
142
|
+
active = coalesce(NEW.is_active, true),
|
|
143
|
+
metadata = v_meta,
|
|
144
|
+
updated_at = now()
|
|
145
|
+
WHERE u.id = OLD.id
|
|
146
|
+
RETURNING u.id INTO v_id;
|
|
147
|
+
IF v_id IS NULL THEN
|
|
148
|
+
RAISE EXCEPTION 'v_units: unit % not found or not yours to change', OLD.id USING ERRCODE = '42501';
|
|
149
|
+
END IF;
|
|
150
|
+
END IF;
|
|
151
|
+
|
|
152
|
+
SELECT * INTO NEW FROM public.v_units WHERE id = v_id;
|
|
153
|
+
RETURN NEW;
|
|
154
|
+
END $$;
|
|
155
|
+
|
|
156
|
+
COMMENT ON FUNCTION app.trg_v_units_write() IS
|
|
157
|
+
'Escreve v_units em app.units; endereço e coordenada são de public.addresses (141, 142).';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 143 — o preset para de nomear telas que não existem
|
|
3
|
+
--
|
|
4
|
+
-- As telas de Campanhas e Templates do plugin de conversas foram removidas:
|
|
5
|
+
-- tomavam duas linhas de rail num plugin que declara que nem a própria caixa
|
|
6
|
+
-- de entrada merece uma, e apareciam DUAS VEZES no rail — uma reivindicada
|
|
7
|
+
-- pela entrada `conversations` do preset, outra na sobra do fim, porque o
|
|
8
|
+
-- `dedupeByRoute` não alcança as `pages` do aplicativo.
|
|
9
|
+
--
|
|
10
|
+
-- O preset do StudioControl ainda falava delas: escondia
|
|
11
|
+
-- `/conversations/campaigns` e renomeava `/conversations/templates` para
|
|
12
|
+
-- "Comunicação". Esconder e rotular uma rota que não existe mais não quebra
|
|
13
|
+
-- nada, e é exatamente por isso que fica: ninguém descobre por erro, só por
|
|
14
|
+
-- leitura — e quem ler daqui a um ano vai procurar a tela.
|
|
15
|
+
-- ============================================================================
|
|
16
|
+
|
|
17
|
+
UPDATE app.apps
|
|
18
|
+
SET preset = jsonb_set(
|
|
19
|
+
preset,
|
|
20
|
+
'{hide}',
|
|
21
|
+
coalesce((
|
|
22
|
+
SELECT jsonb_agg(h) FROM jsonb_array_elements(preset -> 'hide') h
|
|
23
|
+
WHERE h #>> '{}' <> '/conversations/campaigns'
|
|
24
|
+
), '[]'::jsonb))
|
|
25
|
+
WHERE preset -> 'hide' @> '["/conversations/campaigns"]'::jsonb;
|
|
26
|
+
|
|
27
|
+
UPDATE app.apps
|
|
28
|
+
SET preset = jsonb_set(preset, '{labels}', (preset -> 'labels') - '/conversations/templates')
|
|
29
|
+
WHERE preset -> 'labels' ? '/conversations/templates';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 144 — Marketing é um botão, não uma gaveta
|
|
3
|
+
--
|
|
4
|
+
-- No StoreControl a linha de Marketing era uma RUBRICA:
|
|
5
|
+
--
|
|
6
|
+
-- {"icon": "Megaphone", "label": "Marketing", "under": ["/marketing", "/conversations"]}
|
|
7
|
+
--
|
|
8
|
+
-- Uma rubrica é uma sanfona: clicar abre uma lista e o destino fica a dois
|
|
9
|
+
-- cliques. Mas `/marketing` já está em `whole` — ele abre INTEIRO, com as
|
|
10
|
+
-- próprias abas por dentro (é a regra "um assunto, uma linha"). Uma gaveta por
|
|
11
|
+
-- fora de uma tela que já se organiza por dentro é o mesmo menu desenhado duas
|
|
12
|
+
-- vezes, e a segunda vez em outra gramática.
|
|
13
|
+
--
|
|
14
|
+
-- E o que a rubrica agrupava não existe mais: `/conversations` perdeu as linhas
|
|
15
|
+
-- de Campanhas e Templates, então a gaveta ficou com um destino só de verdade —
|
|
16
|
+
-- o caso que o próprio rail já sabe colapsar ("uma rubrica com um destino só
|
|
17
|
+
-- não é uma rubrica"), mas colapsava mantendo o ícone e o rótulo da gaveta em
|
|
18
|
+
-- vez do da tela.
|
|
19
|
+
--
|
|
20
|
+
-- Todo aplicativo: os outros presets já traziam `"marketing"` como linha
|
|
21
|
+
-- simples, e esta migration deixa o StoreControl igual — sem depender de o
|
|
22
|
+
-- colapso acontecer por sorte de configuração.
|
|
23
|
+
-- ============================================================================
|
|
24
|
+
|
|
25
|
+
UPDATE app.apps
|
|
26
|
+
SET preset = jsonb_set(
|
|
27
|
+
preset,
|
|
28
|
+
'{rail}',
|
|
29
|
+
(SELECT jsonb_agg(
|
|
30
|
+
CASE WHEN jsonb_typeof(e) = 'object' AND e ->> 'label' = 'Marketing'
|
|
31
|
+
THEN '"marketing"'::jsonb
|
|
32
|
+
ELSE e END
|
|
33
|
+
ORDER BY ord)
|
|
34
|
+
FROM jsonb_array_elements(preset -> 'rail') WITH ORDINALITY AS t(e, ord)))
|
|
35
|
+
WHERE preset ? 'rail'
|
|
36
|
+
AND EXISTS (
|
|
37
|
+
SELECT 1 FROM jsonb_array_elements(preset -> 'rail') e
|
|
38
|
+
WHERE jsonb_typeof(e) = 'object' AND e ->> 'label' = 'Marketing');
|
|
39
|
+
|
|
40
|
+
-- `/marketing` abre inteiro em todo preset que tenha rail — é o que faz a
|
|
41
|
+
-- linha única levar direto à tela com as abas dentro, em vez de a uma sanfona.
|
|
42
|
+
UPDATE app.apps
|
|
43
|
+
SET preset = jsonb_set(preset, '{whole}',
|
|
44
|
+
coalesce(preset -> 'whole', '[]'::jsonb) || '["/marketing"]'::jsonb)
|
|
45
|
+
WHERE preset ? 'rail'
|
|
46
|
+
AND NOT coalesce(preset -> 'whole', '[]'::jsonb) @> '["/marketing"]'::jsonb;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 145 — Campanhas e Templates somem em toda conta
|
|
3
|
+
--
|
|
4
|
+
-- As duas telas foram REMOVIDAS do plugin de conversas, mas remover código só
|
|
5
|
+
-- vale para quem roda o código: cada aplicativo resolve `@fayz-ai/*` do npm na
|
|
6
|
+
-- versão que instalou, e os que estão no ar continuam desenhando as duas linhas
|
|
7
|
+
-- — às vezes DUAS VEZES, dentro de Agenda, de Marketing ou soltas no fim do
|
|
8
|
+
-- rail, conforme a rubrica de cada ofício. Esperar o release para parar de
|
|
9
|
+
-- mostrar uma tela que não deveria existir é esperar tempo demais.
|
|
10
|
+
--
|
|
11
|
+
-- O `hide` do preset não depende de versão: ele mora no banco, o rail o lê a
|
|
12
|
+
-- cada carga, e uma rota calada não é reivindicada por rubrica nenhuma nem
|
|
13
|
+
-- sobra para o fim (ver `applyRailPreset`). Vale para TODO preset, inclusive os
|
|
14
|
+
-- que não tinham `hide`.
|
|
15
|
+
--
|
|
16
|
+
-- E fica depois do release também. Esconder uma rota que não existe mais não
|
|
17
|
+
-- custa nada, e é a garantia de que um aplicativo parado numa versão velha —
|
|
18
|
+
-- ou um `pages` de app que declare a rota de novo por fora do `dedupeByRoute` —
|
|
19
|
+
-- não traga as duas linhas de volta sem ninguém perceber.
|
|
20
|
+
--
|
|
21
|
+
-- (Reverte a parte da 143 que tirou `/conversations/campaigns` do `hide` do
|
|
22
|
+
-- StudioControl: lá a leitura foi "a tela não existe mais, a linha do preset é
|
|
23
|
+
-- letra morta". Não era — era a única coisa segurando a tela nos apps no ar.)
|
|
24
|
+
-- ============================================================================
|
|
25
|
+
|
|
26
|
+
UPDATE app.apps
|
|
27
|
+
SET preset = jsonb_set(
|
|
28
|
+
coalesce(preset, '{}'::jsonb),
|
|
29
|
+
'{hide}',
|
|
30
|
+
(SELECT coalesce(jsonb_agg(DISTINCT h), '[]'::jsonb)
|
|
31
|
+
FROM jsonb_array_elements(
|
|
32
|
+
coalesce(preset -> 'hide', '[]'::jsonb)
|
|
33
|
+
|| '["/conversations/campaigns", "/conversations/templates"]'::jsonb) h))
|
|
34
|
+
WHERE preset IS NOT NULL
|
|
35
|
+
AND NOT (coalesce(preset -> 'hide', '[]'::jsonb)
|
|
36
|
+
@> '["/conversations/campaigns", "/conversations/templates"]'::jsonb);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 146 — a clínica vê as próprias análises
|
|
3
|
+
--
|
|
4
|
+
-- O preset do StudioControl (clínica e estética: Espaço Renova Rio, Clínica
|
|
5
|
+
-- Sol) dizia duas coisas que não podem ser verdade ao mesmo tempo:
|
|
6
|
+
--
|
|
7
|
+
-- rail: [… "financial", "reports", {Cadastros}]
|
|
8
|
+
-- hide: ["/analytics", …]
|
|
9
|
+
--
|
|
10
|
+
-- O rail nomeia a linha de Análises no lugar certo — entre Financeiro e
|
|
11
|
+
-- Cadastros — e o `hide` a silencia antes de qualquer rubrica poder
|
|
12
|
+
-- reivindicá-la (`applyRailPreset` poda primeiro). Ganha o `hide`, e a casa
|
|
13
|
+
-- que mais precisa olhar ocupação, ticket e retorno ficava sem a tela.
|
|
14
|
+
--
|
|
15
|
+
-- O `hide` veio de quando Análises era uma gaveta de relatórios crus; hoje é
|
|
16
|
+
-- uma tela só, com abas por dentro. Tirar a linha do `hide` é o suficiente:
|
|
17
|
+
-- o rail já sabe onde ela vai.
|
|
18
|
+
-- ============================================================================
|
|
19
|
+
|
|
20
|
+
UPDATE app.apps
|
|
21
|
+
SET preset = jsonb_set(
|
|
22
|
+
preset,
|
|
23
|
+
'{hide}',
|
|
24
|
+
coalesce((
|
|
25
|
+
SELECT jsonb_agg(h) FROM jsonb_array_elements(preset -> 'hide') h
|
|
26
|
+
WHERE h #>> '{}' <> '/analytics'
|
|
27
|
+
), '[]'::jsonb))
|
|
28
|
+
WHERE preset -> 'hide' @> '["/analytics"]'::jsonb
|
|
29
|
+
-- Só onde o rail JÁ diz onde a linha mora. Num preset sem `reports` no rail
|
|
30
|
+
-- ela cairia solta no fim, que é pior que não aparecer.
|
|
31
|
+
AND preset -> 'rail' @> '["reports"]'::jsonb;
|