@fayz-ai/db 0.18.0 → 0.18.2
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.
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 134_todo_oficio_agrupa_os_seus_cadastros.sql — Cadastros deixa de ser sorte.
|
|
3
|
+
-- ---------------------------------------------------------------------------
|
|
4
|
+
-- A 117 pôs as telas de cadastro sob uma rubrica e a 132 lhes deu a ordem e o
|
|
5
|
+
-- vocabulário do V1. As duas mexeram em `beauty`, e só nele. O resultado é o
|
|
6
|
+
-- que um tenant vê hoje:
|
|
7
|
+
--
|
|
8
|
+
-- Espaço Facial (beauty) Cadastros ▸ Contatos · Equipamentos · …
|
|
9
|
+
-- Crema Club (school) Serviço · Categorias de Serviço · Locais de
|
|
10
|
+
-- Atendimento · Grupos de Locais · Profissionais ·
|
|
11
|
+
-- Contatos · Fornecedores · Parcerias ·
|
|
12
|
+
-- Equipamentos · Origens
|
|
13
|
+
--
|
|
14
|
+
-- Onze linhas planas, empurrando Análises e Financeiro para fora da primeira
|
|
15
|
+
-- tela. Não é escolha de ofício nenhum: é `createRegistryPlugin()` entrando em
|
|
16
|
+
-- todo aplicativo — o que a 117 queria — e só um dos rails tendo sido ensinado
|
|
17
|
+
-- a recolhê-las.
|
|
18
|
+
--
|
|
19
|
+
-- ── o que muda ──────────────────────────────────────────────────────────────
|
|
20
|
+
--
|
|
21
|
+
-- `school`, `shop` e `ticket` ganham a rubrica. `resto` já tinha uma com o
|
|
22
|
+
-- nome certo, mas recolhendo só `/company` e `/staff` — as dez telas de
|
|
23
|
+
-- `/registry/*` ficavam de fora dela, o que é pior que não ter: a casa lê
|
|
24
|
+
-- "Cadastros" no menu e os cadastros estão do lado de fora.
|
|
25
|
+
--
|
|
26
|
+
-- `beauty` não é tocado. Ele tem a lista longa que a 132 escreveu para dizer a
|
|
27
|
+
-- ORDEM do BeautySoft, e essa ordem é dele; reescrevê-la aqui seria trocar
|
|
28
|
+
-- sete anos de hábito por uma uniformização que ninguém pediu.
|
|
29
|
+
--
|
|
30
|
+
-- `full` também não. Ele já tem `under: ["/registry"]`, e um prefixo largo
|
|
31
|
+
-- sozinho é o pega-tudo — a mesma regra que a 132 documenta.
|
|
32
|
+
--
|
|
33
|
+
-- ── por que o prefixo curto basta ───────────────────────────────────────────
|
|
34
|
+
--
|
|
35
|
+
-- `under` casa por PREFIXO de rota: `/registry` serve `/registry/contacts` do
|
|
36
|
+
-- mesmo jeito que `/financial` serve `/financial/payables`. A lista longa do
|
|
37
|
+
-- salão existe para ordenar, não para alcançar. Onde não há ordem herdada para
|
|
38
|
+
-- honrar, o prefixo curto diz a mesma coisa com uma linha, e um cadastro novo
|
|
39
|
+
-- que um plugin publique amanhã entra na rubrica sozinho em vez de nascer solto
|
|
40
|
+
-- embaixo dela.
|
|
41
|
+
--
|
|
42
|
+
-- `/company` e `/staff` entram junto porque são cadastro pela mesma pergunta —
|
|
43
|
+
-- a ficha da casa e a de quem trabalha nela. É o que o `resto` já dizia.
|
|
44
|
+
--
|
|
45
|
+
-- ── posição ─────────────────────────────────────────────────────────────────
|
|
46
|
+
--
|
|
47
|
+
-- Antes de `reports`, como em `beauty` e em `resto`. Cadastro é o que se visita
|
|
48
|
+
-- de vez em quando para corrigir uma ficha; fica no fim, perto do que também se
|
|
49
|
+
-- abre por exceção — e longe de Painel, Conversas e do módulo do ofício, que
|
|
50
|
+
-- são a primeira tela do dia.
|
|
51
|
+
--
|
|
52
|
+
-- Uma rubrica sem filho ativo não desenha (ver `applyRailPreset`), então uma
|
|
53
|
+
-- conta que não instalou o registry não ganha um cabeçalho vazio.
|
|
54
|
+
-- ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
-- Insere a rubrica imediatamente antes de `reports`, preservando o resto do
|
|
57
|
+
-- rail na ordem em que está. Feito em SQL sobre o array e não com um rail novo
|
|
58
|
+
-- escrito à mão por aplicativo: o que muda é UMA entrada, e reescrever os
|
|
59
|
+
-- quatro rails inteiros aqui é como a 083 criou uma árvore paralela que
|
|
60
|
+
-- discordava dos plugins.
|
|
61
|
+
WITH alvo AS (
|
|
62
|
+
SELECT id,
|
|
63
|
+
preset->'rail' AS rail,
|
|
64
|
+
-- A rubrica deste ofício. `resto` já tem uma e ela é SUBSTITUÍDA pela
|
|
65
|
+
-- versão que alcança `/registry`; os outros três recebem uma nova.
|
|
66
|
+
jsonb_build_object(
|
|
67
|
+
'label', 'Cadastros',
|
|
68
|
+
'icon', 'ClipboardList',
|
|
69
|
+
'under', jsonb_build_array('/registry', '/company', '/staff')
|
|
70
|
+
) AS rubrica
|
|
71
|
+
FROM app.apps
|
|
72
|
+
WHERE id IN ('school', 'shop', 'ticket', 'resto')
|
|
73
|
+
AND preset ? 'rail'
|
|
74
|
+
),
|
|
75
|
+
limpo AS (
|
|
76
|
+
-- Fora a rubrica antiga, onde houver: manter as duas daria "Cadastros" duas
|
|
77
|
+
-- vezes no menu, e a primeira venceria a corrida pelas rotas.
|
|
78
|
+
SELECT a.id, a.rubrica,
|
|
79
|
+
COALESCE(
|
|
80
|
+
(SELECT jsonb_agg(e ORDER BY ord)
|
|
81
|
+
FROM jsonb_array_elements(a.rail) WITH ORDINALITY AS t(e, ord)
|
|
82
|
+
WHERE NOT (jsonb_typeof(e) = 'object' AND e->>'label' = 'Cadastros')),
|
|
83
|
+
'[]'::jsonb
|
|
84
|
+
) AS rail
|
|
85
|
+
FROM alvo a
|
|
86
|
+
),
|
|
87
|
+
posicao AS (
|
|
88
|
+
SELECT l.id, l.rail, l.rubrica,
|
|
89
|
+
-- Onde `reports` está. Sem ele, a rubrica vai para o fim.
|
|
90
|
+
COALESCE(
|
|
91
|
+
(SELECT MIN(ord) - 1
|
|
92
|
+
FROM jsonb_array_elements(l.rail) WITH ORDINALITY AS t(e, ord)
|
|
93
|
+
WHERE jsonb_typeof(e) = 'string' AND e #>> '{}' = 'reports'),
|
|
94
|
+
jsonb_array_length(l.rail)
|
|
95
|
+
) AS corte
|
|
96
|
+
FROM limpo l
|
|
97
|
+
)
|
|
98
|
+
UPDATE app.apps a
|
|
99
|
+
SET preset = jsonb_set(
|
|
100
|
+
a.preset, '{rail}',
|
|
101
|
+
(SELECT COALESCE(jsonb_agg(e ORDER BY ord), '[]'::jsonb)
|
|
102
|
+
FROM jsonb_array_elements(p.rail) WITH ORDINALITY AS t(e, ord)
|
|
103
|
+
WHERE ord <= p.corte)
|
|
104
|
+
|| jsonb_build_array(p.rubrica)
|
|
105
|
+
|| (SELECT COALESCE(jsonb_agg(e ORDER BY ord), '[]'::jsonb)
|
|
106
|
+
FROM jsonb_array_elements(p.rail) WITH ORDINALITY AS t(e, ord)
|
|
107
|
+
WHERE ord > p.corte)
|
|
108
|
+
),
|
|
109
|
+
updated_at = now()
|
|
110
|
+
FROM posicao p
|
|
111
|
+
WHERE a.id = p.id;
|
|
112
|
+
|
|
113
|
+
-- O módulo `registry` declarado, como a 132 fez para o salão: sem ele a conta
|
|
114
|
+
-- pode não montar o plugin, e a rubrica não desenha por não ter filho.
|
|
115
|
+
UPDATE app.apps
|
|
116
|
+
SET preset = jsonb_set(
|
|
117
|
+
preset, '{modules}',
|
|
118
|
+
(SELECT jsonb_agg(DISTINCT m)
|
|
119
|
+
FROM jsonb_array_elements(
|
|
120
|
+
COALESCE(preset->'modules', '[]'::jsonb) || '["registry"]'::jsonb) AS m)),
|
|
121
|
+
updated_at = now()
|
|
122
|
+
WHERE id IN ('school', 'shop', 'ticket', 'resto')
|
|
123
|
+
AND preset ? 'modules';
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
-- A supplier, customer or other non-login Person may be shared with selected
|
|
2
|
+
-- units. The grants live in app.*, so the browser gets a narrow public door
|
|
3
|
+
-- instead of direct access to the authorization schema.
|
|
4
|
+
|
|
5
|
+
-- The importer already preserved these V1 fields in metadata. Lift them into
|
|
6
|
+
-- the declared V2 fields without overwriting anything edited in V2.
|
|
7
|
+
UPDATE public.people p
|
|
8
|
+
SET custom_fields = jsonb_strip_nulls(jsonb_build_object(
|
|
9
|
+
'serviceProviderType', nullif(p.metadata #>> '{contact,service_provider_type}', ''),
|
|
10
|
+
'registrationNumber', nullif(p.metadata #>> '{legal,state_registration}', '')
|
|
11
|
+
)) || p.custom_fields
|
|
12
|
+
WHERE p.kind = 'supplier'
|
|
13
|
+
AND (
|
|
14
|
+
(NOT (p.custom_fields ? 'serviceProviderType') AND nullif(p.metadata #>> '{contact,service_provider_type}', '') IS NOT NULL)
|
|
15
|
+
OR
|
|
16
|
+
(NOT (p.custom_fields ? 'registrationNumber') AND nullif(p.metadata #>> '{legal,state_registration}', '') IS NOT NULL)
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
-- Suppliers imported before the normalized address book kept one legacy main
|
|
20
|
+
-- address on people. Copy complete addresses once; partial addresses remain on
|
|
21
|
+
-- the Person instead of inventing required postal/city/state values.
|
|
22
|
+
INSERT INTO public.addresses (
|
|
23
|
+
tenant_id, owner_type, owner_id, kind, label, recipient, phone, postal_code,
|
|
24
|
+
street, number, complement, district, city, state, country, is_default, metadata
|
|
25
|
+
)
|
|
26
|
+
SELECT p.tenant_id, 'person', p.id, 'both', 'Principal', p.name, p.phone,
|
|
27
|
+
p.postal_code, p.address,
|
|
28
|
+
nullif(p.metadata #>> '{address,number}', ''),
|
|
29
|
+
nullif(p.metadata #>> '{address,complement}', ''),
|
|
30
|
+
nullif(p.metadata #>> '{address,neighborhood}', ''),
|
|
31
|
+
p.city, p.state, coalesce(nullif(p.country, ''), 'BR'), true,
|
|
32
|
+
jsonb_build_object('source', 'people_legacy_address')
|
|
33
|
+
FROM public.people p
|
|
34
|
+
WHERE p.kind = 'supplier'
|
|
35
|
+
AND nullif(p.postal_code, '') IS NOT NULL
|
|
36
|
+
AND nullif(p.address, '') IS NOT NULL
|
|
37
|
+
AND nullif(p.city, '') IS NOT NULL
|
|
38
|
+
AND nullif(p.state, '') IS NOT NULL
|
|
39
|
+
AND NOT EXISTS (
|
|
40
|
+
SELECT 1 FROM public.addresses a
|
|
41
|
+
WHERE a.owner_type = 'person' AND a.owner_id = p.id
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE OR REPLACE FUNCTION public.people_available_unit_ids(p_person uuid)
|
|
45
|
+
RETURNS uuid[]
|
|
46
|
+
LANGUAGE plpgsql
|
|
47
|
+
STABLE
|
|
48
|
+
SECURITY DEFINER
|
|
49
|
+
SET search_path TO ''
|
|
50
|
+
AS $$
|
|
51
|
+
DECLARE
|
|
52
|
+
v_tenant uuid := public.people_assert_scope('people.manage');
|
|
53
|
+
v_units uuid[];
|
|
54
|
+
BEGIN
|
|
55
|
+
IF NOT EXISTS (
|
|
56
|
+
SELECT 1 FROM public.people p
|
|
57
|
+
WHERE p.tenant_id = v_tenant AND p.id = p_person AND p.merged_into_id IS NULL
|
|
58
|
+
) THEN
|
|
59
|
+
RAISE EXCEPTION 'people_available_unit_ids: person not found in this tenant'
|
|
60
|
+
USING ERRCODE = 'P0002';
|
|
61
|
+
END IF;
|
|
62
|
+
|
|
63
|
+
SELECT coalesce(array_agg(g.subject_id ORDER BY g.subject_id), '{}'::uuid[])
|
|
64
|
+
INTO v_units
|
|
65
|
+
FROM app.resource_grants g
|
|
66
|
+
WHERE g.tenant_id = v_tenant
|
|
67
|
+
AND g.resource_type = 'people.person'
|
|
68
|
+
AND g.resource_id = p_person
|
|
69
|
+
AND g.relation = 'available_in'
|
|
70
|
+
AND g.subject_type = 'unit';
|
|
71
|
+
|
|
72
|
+
RETURN v_units;
|
|
73
|
+
END $$;
|
|
74
|
+
|
|
75
|
+
CREATE OR REPLACE FUNCTION public.people_set_available_units(
|
|
76
|
+
p_person uuid,
|
|
77
|
+
p_unit_ids uuid[] DEFAULT '{}'::uuid[]
|
|
78
|
+
)
|
|
79
|
+
RETURNS uuid[]
|
|
80
|
+
LANGUAGE plpgsql
|
|
81
|
+
SECURITY DEFINER
|
|
82
|
+
SET search_path TO ''
|
|
83
|
+
AS $$
|
|
84
|
+
DECLARE
|
|
85
|
+
v_tenant uuid := public.people_assert_scope('people.manage');
|
|
86
|
+
v_unit_ids uuid[];
|
|
87
|
+
BEGIN
|
|
88
|
+
IF NOT EXISTS (
|
|
89
|
+
SELECT 1 FROM public.people p
|
|
90
|
+
WHERE p.tenant_id = v_tenant AND p.id = p_person AND p.merged_into_id IS NULL
|
|
91
|
+
) THEN
|
|
92
|
+
RAISE EXCEPTION 'people_set_available_units: person not found in this tenant'
|
|
93
|
+
USING ERRCODE = 'P0002';
|
|
94
|
+
END IF;
|
|
95
|
+
|
|
96
|
+
SELECT coalesce(array_agg(DISTINCT requested.unit_id ORDER BY requested.unit_id), '{}'::uuid[])
|
|
97
|
+
INTO v_unit_ids
|
|
98
|
+
FROM unnest(coalesce(p_unit_ids, '{}'::uuid[])) AS requested(unit_id);
|
|
99
|
+
|
|
100
|
+
IF EXISTS (
|
|
101
|
+
SELECT 1
|
|
102
|
+
FROM unnest(v_unit_ids) AS requested(unit_id)
|
|
103
|
+
LEFT JOIN app.units u
|
|
104
|
+
ON u.id = requested.unit_id AND u.tenant_id = v_tenant
|
|
105
|
+
WHERE u.id IS NULL OR NOT app.has_unit(requested.unit_id)
|
|
106
|
+
) THEN
|
|
107
|
+
RAISE EXCEPTION 'people_set_available_units: unit not found in this tenant'
|
|
108
|
+
USING ERRCODE = '42501';
|
|
109
|
+
END IF;
|
|
110
|
+
|
|
111
|
+
DELETE FROM app.resource_grants g
|
|
112
|
+
WHERE g.tenant_id = v_tenant
|
|
113
|
+
AND g.resource_type = 'people.person'
|
|
114
|
+
AND g.resource_id = p_person
|
|
115
|
+
AND g.relation = 'available_in'
|
|
116
|
+
AND g.subject_type = 'unit';
|
|
117
|
+
|
|
118
|
+
INSERT INTO app.resource_grants (
|
|
119
|
+
tenant_id, resource_type, resource_id, relation, subject_type, subject_id, created_by
|
|
120
|
+
)
|
|
121
|
+
SELECT v_tenant, 'people.person', p_person, 'available_in', 'unit', unit_id, auth.uid()
|
|
122
|
+
FROM unnest(v_unit_ids) AS requested(unit_id);
|
|
123
|
+
|
|
124
|
+
RETURN v_unit_ids;
|
|
125
|
+
END $$;
|
|
126
|
+
|
|
127
|
+
REVOKE ALL ON FUNCTION public.people_available_unit_ids(uuid) FROM PUBLIC;
|
|
128
|
+
REVOKE ALL ON FUNCTION public.people_set_available_units(uuid, uuid[]) FROM PUBLIC;
|
|
129
|
+
GRANT EXECUTE ON FUNCTION public.people_available_unit_ids(uuid) TO authenticated, service_role;
|
|
130
|
+
GRANT EXECUTE ON FUNCTION public.people_set_available_units(uuid, uuid[]) TO authenticated, service_role;
|
|
131
|
+
|
|
132
|
+
COMMENT ON FUNCTION public.people_available_unit_ids(uuid) IS
|
|
133
|
+
'Units where a Person record is available. Empty means organization-wide.';
|
|
134
|
+
COMMENT ON FUNCTION public.people_set_available_units(uuid, uuid[]) IS
|
|
135
|
+
'Replaces the unit availability of a Person. Empty means organization-wide.';
|
|
136
|
+
|
|
137
|
+
-- PostgREST may keep serving the previous function catalog after DDL. The
|
|
138
|
+
-- explicit notification makes the new RPCs available as soon as this migration
|
|
139
|
+
-- commits instead of waiting for a later cache refresh.
|
|
140
|
+
NOTIFY pgrst, 'reload schema';
|
package/package.json
CHANGED