@fayz-ai/plugin-workforce 0.3.14 → 0.3.15
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/dist/WorkforcePage-A64KM6IC.js +2133 -0
- package/dist/WorkforcePage-A64KM6IC.js.map +1 -0
- package/dist/chunk-6GWD6GQK.js +177 -0
- package/dist/chunk-6GWD6GQK.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -5440
- package/dist/index.js.map +1 -1
- package/dist/migrations/main.browser.js +14 -0
- package/dist/migrations/main.browser.js.map +1 -0
- package/dist/migrations/main.js +3141 -0
- package/dist/migrations/main.js.map +1 -0
- package/package.json +10 -4
|
@@ -0,0 +1,3141 @@
|
|
|
1
|
+
// src/migrations/index.ts
|
|
2
|
+
var MIGRATION_000_BASELINE = `-- O RH ganha ponto e escala.
|
|
3
|
+
--
|
|
4
|
+
-- D9 \xB7 Gente & Jornada de trabalho \xE9 o \xFAltimo dom\xEDnio inteiro do V1 sem NENHUMA
|
|
5
|
+
-- contraparte no V2. O registro de features do app diz isso feature por feature:
|
|
6
|
+
-- \`sch.templates\` P0 missing, \`sch.calendar\` P0 missing ("Nenhum m\xF3dulo do SDK \xE9
|
|
7
|
+
-- dono disso hoje \u2014 precisa de uma decis\xE3o antes de c\xF3digo"), \`sch.clock\` P1
|
|
8
|
+
-- missing ("O que existe \xE9 o hor\xE1rio PREVISTO de trabalho. A batida realizada,
|
|
9
|
+
-- com ajuste auditado, n\xE3o existe"), \`sch.clock.adjust\` P1 missing. O host
|
|
10
|
+
-- previsto para os quatro \xE9 o mesmo: **plugin-workforce**. Este arquivo \xE9 a
|
|
11
|
+
-- camada de dados dele.
|
|
12
|
+
--
|
|
13
|
+
-- \u2500\u2500 O que foi medido na origem (Pertinho do C\xE9u, licen\xE7a 8, 07/09/2026) \u2500\u2500\u2500\u2500\u2500\u2500
|
|
14
|
+
--
|
|
15
|
+
-- Da documenta\xE7\xE3o do V1 (\`docs/v1-guide/restaurante/escalas/FLOW.md\` e
|
|
16
|
+
-- \`docs/v1-guide/agenda/ponto-eletronico/FLOW.md\`), tudo verificado na tela:
|
|
17
|
+
--
|
|
18
|
+
-- 325 registros de ponto no per\xEDodo \u2014 e TODOS os 325 marcados
|
|
19
|
+
-- "Fora da escala" (V1-043). A coluna existe justamente para cruzar
|
|
20
|
+
-- ponto \xD7 escala e nunca varia: o ponto est\xE1 ativo em \`PC Servi\xE7os\`
|
|
21
|
+
-- enquanto a opera\xE7\xE3o e as escalas s\xE3o de \`Pertinho do C\xE9u\`.
|
|
22
|
+
-- 20 batimentos EM ABERTO, incluindo 5 de 5 hoje e 8 de 16 ontem; um
|
|
23
|
+
-- deles com entrada \xE0s 23:29 e "13h07 (parcial)".
|
|
24
|
+
-- R$ 47.747 o valor do per\xEDodo \u2014 SUBESTIMADO, porque o V1 s\xF3 calcula valor no
|
|
25
|
+
-- fechamento do par, e 20 pares nunca fecharam.
|
|
26
|
+
-- 103 profissionais habilitados para arrastar numa vaga de template.
|
|
27
|
+
-- 7 templates de escala no tenant.
|
|
28
|
+
--
|
|
29
|
+
-- Tr\xEAs dos n\xFAmeros acima s\xE3o sintoma do MESMO defeito de modelo, e \xE9 ele que
|
|
30
|
+
-- este arquivo existe para n\xE3o repetir: no V1 a batida \xE9 um par entrada/sa\xEDda
|
|
31
|
+
-- numa \xFAnica linha, com o ajuste como colunas \`*_adjusted_at\` por cima. Um par
|
|
32
|
+
-- que nunca fecha n\xE3o tem dura\xE7\xE3o, n\xE3o tem valor e n\xE3o tem como ser conferido
|
|
33
|
+
-- contra nada.
|
|
34
|
+
--
|
|
35
|
+
-- \u2500\u2500 Duas decis\xF5es de arquitetura que o reposit\xF3rio j\xE1 tomou \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
36
|
+
--
|
|
37
|
+
-- 1. *"Ponto \xE9 raz\xE3o append-only, como todo saldo \u2014 nunca uma coluna que algu\xE9m
|
|
38
|
+
-- edita."* (\`docs/migration/how-to-port.json\`, mv.sch.clock: *"Batida \xE9
|
|
39
|
+
-- append-only e ajuste \xE9 registro separado com autor e motivo \u2014 apagar
|
|
40
|
+
-- batida \xE9 o que a lei n\xE3o permite."*)
|
|
41
|
+
--
|
|
42
|
+
-- Por isso \`plg_workforce_time_entries\` \xE9 append-only de verdade \u2014 trigger
|
|
43
|
+
-- que recusa UPDATE e DELETE, e as policies de UPDATE/DELETE derrubadas
|
|
44
|
+
-- depois do scaffold \u2014 e o ajuste do V1 (\`checkin_adjusted_at\`,
|
|
45
|
+
-- \`checkout_adjusted_at\`, \`adjustment_reason\`, \`adjusted_by_*\`) vira LINHA em
|
|
46
|
+
-- \`plg_workforce_time_adjustments\`, com autor, motivo e carimbo. A pr\xF3pria
|
|
47
|
+
-- tela do V1 j\xE1 enuncia a regra que o V2 passa a garantir no banco: *"Os
|
|
48
|
+
-- hor\xE1rios reais dos batimentos s\xE3o preservados."*
|
|
49
|
+
--
|
|
50
|
+
-- 2. *"Escala de funcion\xE1rio N\xC3O mora na agenda \u2014 quem oferta hor\xE1rio \xE9 o RH, a
|
|
51
|
+
-- agenda s\xF3 consome."* (registro de features, dom\xEDnio D3.)
|
|
52
|
+
--
|
|
53
|
+
-- Por isso a escala \xE9 deste plugin, e a ponte com a agenda \xE9 UMA coluna:
|
|
54
|
+
-- \`plg_workforce_schedule_assignments.schedule_row_id\`, que aponta para a
|
|
55
|
+
-- linha de \`public.schedules\` que a publica\xE7\xE3o materializa.
|
|
56
|
+
--
|
|
57
|
+
-- \u2500\u2500 O que este arquivo N\xC3O cria, porque j\xE1 existe \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
58
|
+
--
|
|
59
|
+
-- O PREVISTO j\xE1 est\xE1 no V2. \`public.schedules\` (spine) tem exatamente a forma
|
|
60
|
+
-- de uma oferta de hor\xE1rio \u2014 \`kind\`, \`assignee_id\` \u2192 \`public.people\`,
|
|
61
|
+
-- \`day_of_week\`, \`specific_date\`, \`starts_at\`, \`ends_at\`, \`unit_id\` \u2014 e
|
|
62
|
+
-- \`public.schedule_kinds\` \xE9 o registro de esp\xE9cies dela (hoje: working_hours,
|
|
63
|
+
-- exception, block, unclassified). Criar uma tabela de escala prevista aqui
|
|
64
|
+
-- seria a segunda tabela de hor\xE1rio do cluster, que diverge da primeira na
|
|
65
|
+
-- pr\xF3xima mudan\xE7a. Em vez disso este arquivo **semeia a esp\xE9cie \`work_shift\`** e
|
|
66
|
+
-- usa \`public.schedules\`.
|
|
67
|
+
--
|
|
68
|
+
-- \`public.people\` tamb\xE9m j\xE1 resolve sozinha o pior defeito do modelo do V1: l\xE1 o
|
|
69
|
+
-- ponto aponta para \`person_type ENUM('professional','employee')\` + \`person_id\`,
|
|
70
|
+
-- uma FK polim\xF3rfica para duas tabelas de cadastro que a pr\xF3pria tela mistura
|
|
71
|
+
-- ("A aba mistura Profissional e Funcion\xE1rio no mesmo filtro e na mesma lista, e
|
|
72
|
+
-- a diferen\xE7a entre os dois cadastros n\xE3o \xE9 explicada em lugar nenhum"). No V2
|
|
73
|
+
-- os dois j\xE1 s\xE3o \`public.people\` com papel em \`public.person_roles\`, ent\xE3o o par
|
|
74
|
+
-- vira UMA FK \`person_id uuid \u2192 public.people(id)\`.
|
|
75
|
+
--
|
|
76
|
+
-- N\xC3O porta \`schedule_grids\` / \`schedule_grid_blocks\`: apesar do nome s\xE3o grade
|
|
77
|
+
-- de disponibilidade de AGENDAMENTO, n\xE3o escala de trabalho. Ficam registradas
|
|
78
|
+
-- em \`migration.excluded_tables\` com o motivo, que \xE9 mais barato que descobrir
|
|
79
|
+
-- isso de novo daqui a seis meses.
|
|
80
|
+
--
|
|
81
|
+
-- \u2500\u2500 As tr\xEAs lacunas do V1 que o V2 resolve aqui \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
82
|
+
--
|
|
83
|
+
-- (a) FECHAMENTO DE COMPET\xCANCIA. O V1 n\xE3o tem nenhum. Qualquer ajuste pode
|
|
84
|
+
-- mudar, em sil\xEAncio, um m\xEAs de horas j\xE1 entregue ao contador e j\xE1 pago \u2014
|
|
85
|
+
-- e a \xFAnica trava que existe \xE9 de tela: \`payable_invoice_id\` preenchido
|
|
86
|
+
-- esconde o bot\xE3o "Ajustar". Uma trava de tela \xE9 uma trava at\xE9 a primeira
|
|
87
|
+
-- chamada de API.
|
|
88
|
+
-- \u2192 \`plg_workforce_periods\`, no MESMO padr\xE3o que o financeiro j\xE1 provou em
|
|
89
|
+
-- \`plugin-financial/src/migrations/006\`: compet\xEAncia \xE9 o M\xCAS (data no dia
|
|
90
|
+
-- 1\xBA, por constraint), \xE9 do TENANT e n\xE3o da unidade (um m\xEAs meio fechado \xE9
|
|
91
|
+
-- a pior resposta poss\xEDvel para quem precisa dizer que acabou), a escrita
|
|
92
|
+
-- \xE9 s\xF3 por RPC (\`workforce_period_close\` / \`workforce_period_reopen\`, sem
|
|
93
|
+
-- policy de INSERT/UPDATE \u2014 uma policy de UPDATE seria a porta dos fundos
|
|
94
|
+
-- da trava), reabrir exige motivo, e a permiss\xE3o de fechar \xE9 pr\xF3pria
|
|
95
|
+
-- (\`workforce.close_period\`): quem bate ponto n\xE3o \xE9 quem fecha o m\xEAs.
|
|
96
|
+
-- A trava vira trigger sobre a batida E sobre o ajuste, e o ajuste ganha
|
|
97
|
+
-- a segunda trava do V1, agora no banco: batida j\xE1 vinculada a um t\xEDtulo
|
|
98
|
+
-- a pagar n\xE3o aceita ajuste.
|
|
99
|
+
--
|
|
100
|
+
-- (b) JORNADA PREVISTA \xD7 REALIZADA. No V1 elas nunca se encontram: a coluna
|
|
101
|
+
-- "Escala" da lista de ponto existe para isso e marcou 325 de 325 registros
|
|
102
|
+
-- como "Fora da escala".
|
|
103
|
+
-- \u2192 O PREVISTO mora em \`public.schedules\` com \`kind = 'work_shift'\` (n\xE3o em
|
|
104
|
+
-- tabela nova \u2014 ver acima); o REALIZADO mora em
|
|
105
|
+
-- \`plg_workforce_time_entries\`; e o cruzamento \xE9 a view
|
|
106
|
+
-- \`public.v_workforce_day\`, um FULL OUTER JOIN de prop\xF3sito, para que as
|
|
107
|
+
-- duas assimetrias apare\xE7am em vez de sumirem: previsto sem realizado \xE9
|
|
108
|
+
-- FALTA, realizado sem previsto \xE9 FORA DA ESCALA. A publica\xE7\xE3o da escala
|
|
109
|
+
-- materializa a linha de \`public.schedules\` e guarda o id em
|
|
110
|
+
-- \`plg_workforce_schedule_assignments.schedule_row_id\`, que \xE9 o que torna
|
|
111
|
+
-- o cruzamento uma chave e n\xE3o uma heur\xEDstica de nome e hor\xE1rio.
|
|
112
|
+
--
|
|
113
|
+
-- (c) INTERVALO / ALMO\xC7O. O V1 tem UM par entrada/sa\xEDda por ciclo. Quem sai para
|
|
114
|
+
-- almo\xE7ar e volta ou n\xE3o registra o intervalo, ou bate a sa\xEDda e a entrada e
|
|
115
|
+
-- o dia vira dois registros que ningu\xE9m soma. Num restaurante com turno
|
|
116
|
+
-- partido isso \xE9 a jornada inteira.
|
|
117
|
+
-- \u2192 A batida deixa de ser um PAR numa linha e passa a ser um EVENTO:
|
|
118
|
+
-- \`kind \u2208 (clock_in, break_start, break_end, clock_out)\`, N por dia,
|
|
119
|
+
-- agrupados por \`(person_id, work_date)\` e ordenados por \`seq\`. A mesma
|
|
120
|
+
-- decis\xE3o que satisfaz o append-only resolve o intervalo de gra\xE7a, e ainda
|
|
121
|
+
-- responde "quais pontos est\xE3o em aberto?" sem coluna derivada: em aberto
|
|
122
|
+
-- \xE9 o dia cujos eventos de abertura (clock_in, break_end) n\xE3o empatam com
|
|
123
|
+
-- os de fechamento (clock_out, break_start).
|
|
124
|
+
-- \`work_date\` \xE9 coluna, e n\xE3o \`punched_at::date\`, porque o V1 j\xE1 convive
|
|
125
|
+
-- com o turno que cruza a meia-noite \u2014 a pr\xF3pria caixa de ajuste avisa:
|
|
126
|
+
-- *"\xFAtil quando o funcion\xE1rio esqueceu de bater a sa\xEDda e o registro
|
|
127
|
+
-- cruzou para o dia seguinte"*. A jornada da noite de s\xE1bado \xE9 de s\xE1bado.
|
|
128
|
+
--
|
|
129
|
+
-- \u2500\u2500 Uma decis\xE3o a mais, tomada na fronteira da migra\xE7\xE3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
130
|
+
--
|
|
131
|
+
-- No molde do V1, titular vs stand-by \xE9 POSICIONAL: s\xE3o titulares os primeiros
|
|
132
|
+
-- \`required_count\` de \`schedule_template_assignments\` por \`priority\`, e o resto
|
|
133
|
+
-- \xE9 reserva. Ningu\xE9m grava isso; \xE9 uma regra que vive no c\xF3digo da tela. Na
|
|
134
|
+
-- inst\xE2ncia o V1 j\xE1 tem \`role\` como coluna de verdade. Aqui o molde tamb\xE9m tem:
|
|
135
|
+
-- \`plg_workforce_schedule_template_assignments.role\` \xE9 coluna, e o writer da
|
|
136
|
+
-- migra\xE7\xE3o CALCULA a posi\xE7\xE3o uma vez, contra o \`required_count\` da fun\xE7\xE3o,
|
|
137
|
+
-- em vez de deixar a regra escondida em cada leitor que vier depois.
|
|
138
|
+
--
|
|
139
|
+
-- Idempotente. Nada aqui apaga dado.
|
|
140
|
+
|
|
141
|
+
-- A RPC de fechamento consulta a view de cruzamento, que nasce tr\xEAs se\xE7\xF5es
|
|
142
|
+
-- depois dela \u2014 as duas moram onde o assunto est\xE1, n\xE3o onde o carregador
|
|
143
|
+
-- preferiria. Sess\xE3o, e \xE9 o que os baselines gerados do cluster j\xE1 fazem.
|
|
144
|
+
SET check_function_bodies = false;
|
|
145
|
+
|
|
146
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
147
|
+
-- 0. O plugin existe: as permiss\xF5es
|
|
148
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
149
|
+
--
|
|
150
|
+
-- O scaffold desenha as policies em cima de \`<plugin>.read/create/edit/delete\`
|
|
151
|
+
-- e \`<plugin>.read_all\`. Sem essas chaves no cat\xE1logo, a policy compila e nega
|
|
152
|
+
-- tudo para todo mundo \u2014 uma tabela protegida por n\xE3o existir para ningu\xE9m.
|
|
153
|
+
--
|
|
154
|
+
-- Duas chaves s\xE3o separadas de prop\xF3sito, e as duas s\xE3o segrega\xE7\xE3o de fun\xE7\xE3o:
|
|
155
|
+
-- \`workforce.adjust_time\` porque corrigir a batida de outra pessoa \xE9 outra
|
|
156
|
+
-- ordem de decis\xE3o que editar uma escala, e \`workforce.close_period\` porque
|
|
157
|
+
-- quem aponta n\xE3o \xE9 quem fecha o m\xEAs.
|
|
158
|
+
SELECT app.seed_permissions('[
|
|
159
|
+
{"key": "workforce.read", "plugin": "workforce", "description": "Ver escala, ponto e compet\xEAncia."},
|
|
160
|
+
{"key": "workforce.create", "plugin": "workforce", "description": "Criar escala, template e batida."},
|
|
161
|
+
{"key": "workforce.edit", "plugin": "workforce", "description": "Editar escala e template."},
|
|
162
|
+
{"key": "workforce.delete", "plugin": "workforce", "description": "Excluir escala e template."},
|
|
163
|
+
{"key": "workforce.manage", "plugin": "workforce", "description": "Administrar o m\xF3dulo de jornada."},
|
|
164
|
+
{"key": "workforce.read_all", "plugin": "workforce", "description": "Ver a jornada de todo mundo, e n\xE3o s\xF3 a pr\xF3pria."},
|
|
165
|
+
{"key": "workforce.adjust_time", "plugin": "workforce", "description": "Ajustar batida de ponto. Separada de workforce.edit: corrigir a hora de outra pessoa \xE9 prova, n\xE3o edi\xE7\xE3o."},
|
|
166
|
+
{"key": "workforce.close_period", "plugin": "workforce", "description": "Fechar e reabrir a compet\xEAncia de jornada. Separada de workforce.edit: quem aponta n\xE3o \xE9 quem fecha."}
|
|
167
|
+
]'::jsonb);
|
|
168
|
+
|
|
169
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
170
|
+
-- 1. O PREVISTO j\xE1 existe \u2014 s\xF3 falta a esp\xE9cie
|
|
171
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
172
|
+
--
|
|
173
|
+
-- \`working_hours\` \xE9 a semana recorrente do PROFISSIONAL, para a agenda saber que
|
|
174
|
+
-- um hor\xE1rio pode existir. \`work_shift\` \xE9 outra coisa: \xE9 o turno de trabalho que
|
|
175
|
+
-- o RH ofertou para uma pessoa num dia, e \xE9 contra ele que o ponto \xE9 conferido.
|
|
176
|
+
-- Reaproveitar \`working_hours\` misturaria "posso ser agendado" com "estou
|
|
177
|
+
-- escalado", que \xE9 justamente a confus\xE3o que o D3 mandou n\xE3o fazer.
|
|
178
|
+
INSERT INTO public.schedule_kinds (kind, plugin, description, status)
|
|
179
|
+
VALUES ('work_shift', 'workforce',
|
|
180
|
+
'O turno de trabalho que o RH escalou para uma pessoa \u2014 o PREVISTO contra o qual o ponto \xE9 conferido. Distinto de working_hours, que diz apenas quando um hor\xE1rio PODE existir na agenda.',
|
|
181
|
+
'declared')
|
|
182
|
+
ON CONFLICT (kind) DO UPDATE
|
|
183
|
+
SET plugin = EXCLUDED.plugin, description = EXCLUDED.description, updated_at = now();
|
|
184
|
+
|
|
185
|
+
-- \`public.schedules\` foi scaffoldada em modo \`shadow\` (065): as policies de
|
|
186
|
+
-- autoriza\xE7\xE3o existem, mas as anteriores nunca foram derrubadas, e RLS soma
|
|
187
|
+
-- policies em OR \u2014 uma policy legada sobrevivente alarga tudo silenciosamente.
|
|
188
|
+
-- A partir do momento em que a escala do RH mora aqui dentro, sombra n\xE3o serve:
|
|
189
|
+
-- \`scaffold_set_mode\` reemite com os MESMOS resource_type/plugin j\xE1 registrados
|
|
190
|
+
-- (\`agenda.schedule\` / \`agenda\`) e derruba o que sobrou. N\xE3o \xE9 \`scaffold_table\`:
|
|
191
|
+
-- repetir o scaffold aqui reclassificaria uma tabela do spine para dentro deste
|
|
192
|
+
-- plugin, e a agenda perderia a pr\xF3pria mesa.
|
|
193
|
+
DO $$
|
|
194
|
+
BEGIN
|
|
195
|
+
IF to_regclass('public.schedules') IS NOT NULL
|
|
196
|
+
AND EXISTS (SELECT 1 FROM app.scaffold_registry
|
|
197
|
+
WHERE table_schema = 'public' AND table_name = 'schedules' AND mode = 'shadow') THEN
|
|
198
|
+
PERFORM app.scaffold_set_mode('public.schedules'::regclass, 'enforce');
|
|
199
|
+
END IF;
|
|
200
|
+
END $$;
|
|
201
|
+
|
|
202
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
203
|
+
-- 2. O REALIZADO: a batida, append-only
|
|
204
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
205
|
+
|
|
206
|
+
CREATE TABLE IF NOT EXISTS public.plg_workforce_time_entries (
|
|
207
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
208
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
209
|
+
unit_id uuid REFERENCES app.units(id),
|
|
210
|
+
-- UMA fk, onde o V1 tem person_type + person_id. \`public.people\` j\xE1 \xE9 o
|
|
211
|
+
-- cadastro \xFAnico; profissional e funcion\xE1rio s\xE3o pap\xE9is em person_roles.
|
|
212
|
+
person_id uuid NOT NULL REFERENCES public.people(id) ON DELETE RESTRICT,
|
|
213
|
+
-- O nome COMO ESTAVA na hora da batida. N\xE3o \xE9 desnormaliza\xE7\xE3o por pregui\xE7a: a
|
|
214
|
+
-- folha de ponto \xE9 prova, e prova n\xE3o pode mudar porque algu\xE9m corrigiu um
|
|
215
|
+
-- cadastro dois anos depois.
|
|
216
|
+
person_name text,
|
|
217
|
+
-- O evento, n\xE3o o par. \xC9 esta coluna que resolve o intervalo (c): o almo\xE7o \xE9
|
|
218
|
+
-- break_start + break_end, e o dia aceita quantos pares de intervalo houver.
|
|
219
|
+
kind text NOT NULL
|
|
220
|
+
CHECK (kind IN ('clock_in', 'break_start', 'break_end', 'clock_out')),
|
|
221
|
+
punched_at timestamptz NOT NULL,
|
|
222
|
+
-- O DIA DA JORNADA, que n\xE3o \xE9 \`punched_at::date\`. O turno da noite de s\xE1bado
|
|
223
|
+
-- que sai \xE0s 02:10 \xE9 jornada de s\xE1bado; o V1 j\xE1 convive com isso e a pr\xF3pria
|
|
224
|
+
-- caixa de ajuste dele avisa que o registro "cruzou para o dia seguinte".
|
|
225
|
+
work_date date NOT NULL,
|
|
226
|
+
-- Ordena os eventos do dia quando dois caem no mesmo instante (totem com
|
|
227
|
+
-- clique duplo, importa\xE7\xE3o em lote). Sem ele o "\xFAltimo evento do dia" \xE9
|
|
228
|
+
-- indeterminado, e \xE9 ele que diz se o ponto est\xE1 em aberto.
|
|
229
|
+
seq integer NOT NULL DEFAULT 1,
|
|
230
|
+
origin text NOT NULL DEFAULT 'totem'
|
|
231
|
+
CHECK (origin IN ('totem', 'device', 'manual', 'import', 'migration')),
|
|
232
|
+
-- A prova que o totem produz: foto, coordenada com precis\xE3o, semelhan\xE7a
|
|
233
|
+
-- facial, IP e aparelho. Fica em jsonb e n\xE3o em dez colunas porque \xE9 um
|
|
234
|
+
-- conjunto de evid\xEAncia de UM canal de captura \u2014 o totem com AWS Rekognition
|
|
235
|
+
-- do V1 \u2014 e o pr\xF3ximo canal (crach\xE1, biometria local) traz outro conjunto.
|
|
236
|
+
-- O que n\xE3o pode \xE9 perder: no V1 s\xE3o \`photo_path\`, \`lat\`, \`lng\`, \`accuracy_m\`,
|
|
237
|
+
-- \`ip\`, \`user_agent\` e \`face_score\`, medidos em campo como
|
|
238
|
+
-- "-22.89342, -43.39140 \xB7 \xB1 14m" e "Match facial 100.0%".
|
|
239
|
+
evidence jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
240
|
+
-- O turno que esta batida cumpre, quando h\xE1 um. Nulo \xE9 uma resposta leg\xEDtima
|
|
241
|
+
-- e frequente \u2014 \xE9 literalmente 325 de 325 no tenant medido \u2014 e o V2 prefere
|
|
242
|
+
-- o nulo honesto a um v\xEDnculo inventado.
|
|
243
|
+
assignment_id uuid,
|
|
244
|
+
-- Onde esta hora custa. O turno traz o padr\xE3o (ver plg_workforce_schedule_shifts);
|
|
245
|
+
-- a batida carrega o que de fato foi classificado, porque a hora do cozinheiro
|
|
246
|
+
-- e a do gar\xE7om caem em centros de custo diferentes no mesmo dia.
|
|
247
|
+
-- Sem FK inline: as tr\xEAs colunas abaixo apontam para tabelas de OUTRO plugin,
|
|
248
|
+
-- e a jornada tem de poder ser instalada num cluster que n\xE3o ligou o
|
|
249
|
+
-- financeiro. A FK \xE9 declarada no fim do arquivo, condicionada \xE0 exist\xEAncia do
|
|
250
|
+
-- destino \u2014 presente vira integridade, ausente vira s\xF3 um uuid guardado.
|
|
251
|
+
chart_account_id uuid,
|
|
252
|
+
cost_center_id uuid,
|
|
253
|
+
-- O \`payable_invoice_id\` do V1. No V1 ele \xE9 uma trava DE TELA (preenchido,
|
|
254
|
+
-- o bot\xE3o "Ajustar" some); aqui ele \xE9 uma trava de banco, pela trigger da
|
|
255
|
+
-- se\xE7\xE3o 4. Hora j\xE1 paga n\xE3o se corrige sem reabrir a compet\xEAncia.
|
|
256
|
+
payable_invoice_id uuid,
|
|
257
|
+
notes text,
|
|
258
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
259
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
260
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
261
|
+
-- A mesma pessoa n\xE3o bate o mesmo evento duas vezes no mesmo instante. \xC9 o
|
|
262
|
+
-- que torna o replay da migra\xE7\xE3o e o clique duplo no totem inofensivos.
|
|
263
|
+
UNIQUE (tenant_id, person_id, work_date, kind, punched_at, seq)
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
COMMENT ON TABLE public.plg_workforce_time_entries IS
|
|
267
|
+
'A batida, append-only: um EVENTO (entrada, in\xEDcio/fim de intervalo, sa\xEDda), nunca um par edit\xE1vel. '
|
|
268
|
+
'UPDATE e DELETE s\xE3o recusados por trigger; a corre\xE7\xE3o \xE9 uma linha em plg_workforce_time_adjustments.';
|
|
269
|
+
|
|
270
|
+
-- "O que foi batido neste per\xEDodo, nesta unidade?" \u2014 a pergunta da aba Registros
|
|
271
|
+
-- de Ponto, que o V1 responde com filtros De/At\xE9/Unidade/Pessoa.
|
|
272
|
+
CREATE INDEX IF NOT EXISTS plg_workforce_time_entries_period_idx
|
|
273
|
+
ON public.plg_workforce_time_entries (tenant_id, unit_id, work_date, punched_at);
|
|
274
|
+
|
|
275
|
+
-- "Quais pontos est\xE3o em aberto?" \u2014 20 no tenant medido. Em aberto n\xE3o \xE9 coluna
|
|
276
|
+
-- (seria estado derivado numa tabela append-only): \xE9 o dia em que os eventos de
|
|
277
|
+
-- ABERTURA n\xE3o empatam com os de fechamento. Este \xEDndice serve a metade que a
|
|
278
|
+
-- view varre, e responde sozinho "quem entrou hoje e onde".
|
|
279
|
+
CREATE INDEX IF NOT EXISTS plg_workforce_time_entries_open_idx
|
|
280
|
+
ON public.plg_workforce_time_entries (tenant_id, person_id, work_date, seq)
|
|
281
|
+
WHERE kind IN ('clock_in', 'break_end');
|
|
282
|
+
|
|
283
|
+
-- "O que esta pessoa bateu?" \u2014 a folha individual, e o que a folha de pagamento
|
|
284
|
+
-- soma no fim do m\xEAs.
|
|
285
|
+
CREATE INDEX IF NOT EXISTS plg_workforce_time_entries_person_idx
|
|
286
|
+
ON public.plg_workforce_time_entries (tenant_id, person_id, work_date DESC);
|
|
287
|
+
|
|
288
|
+
DROP TRIGGER IF EXISTS plg_workforce_time_entries_updated_at ON public.plg_workforce_time_entries;
|
|
289
|
+
CREATE TRIGGER plg_workforce_time_entries_updated_at
|
|
290
|
+
BEFORE UPDATE ON public.plg_workforce_time_entries
|
|
291
|
+
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
292
|
+
|
|
293
|
+
GRANT SELECT, INSERT ON public.plg_workforce_time_entries TO authenticated;
|
|
294
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_workforce_time_entries TO service_role;
|
|
295
|
+
|
|
296
|
+
-- O scaffold \xE9 quem sabe desenhar a pol\xEDtica deste cluster (tenant, unidade,
|
|
297
|
+
-- permiss\xE3o do plugin, escotilhas de compartilhamento). Escrever a policy \xE0 m\xE3o
|
|
298
|
+
-- criaria a segunda regra de autoriza\xE7\xE3o da jornada, que diverge da primeira na
|
|
299
|
+
-- pr\xF3xima mudan\xE7a do scaffold.
|
|
300
|
+
--
|
|
301
|
+
-- \`p_owner_scoping = false\`: a batida n\xE3o tem dono no sentido de "criada por
|
|
302
|
+
-- mim". Quem enxerga ponto de quem \xE9 \`workforce.read\` por unidade.
|
|
303
|
+
SELECT app.scaffold_table('public.plg_workforce_time_entries'::regclass,
|
|
304
|
+
'workforce.time_entry', 'workforce', false, 'enforce');
|
|
305
|
+
|
|
306
|
+
-- \u2500\u2500 append-only, das duas formas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
307
|
+
--
|
|
308
|
+
-- O scaffold acabou de criar policies de UPDATE e DELETE, porque \xE9 isso que ele
|
|
309
|
+
-- faz em toda tabela. Aqui elas n\xE3o deveriam existir: apagar batida \xE9 o que a
|
|
310
|
+
-- lei n\xE3o permite. As duas defesas s\xE3o deliberadamente redundantes \u2014 a policy
|
|
311
|
+
-- some para quem entra por \`authenticated\`, e a trigger pega tamb\xE9m service_role
|
|
312
|
+
-- e qualquer SECURITY DEFINER esquecido.
|
|
313
|
+
DROP POLICY IF EXISTS plg_workforce_time_entries_authz_update ON public.plg_workforce_time_entries;
|
|
314
|
+
DROP POLICY IF EXISTS plg_workforce_time_entries_authz_delete ON public.plg_workforce_time_entries;
|
|
315
|
+
|
|
316
|
+
CREATE OR REPLACE FUNCTION public.plg_workforce_time_entries_append_only()
|
|
317
|
+
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
|
|
318
|
+
AS $$
|
|
319
|
+
BEGIN
|
|
320
|
+
-- A escotilha \xE9 a mesma que o resto do cluster usa: durante uma onda de
|
|
321
|
+
-- migra\xE7\xE3o o motor marca a sess\xE3o, e s\xF3 ela pode reescrever a linha que ela
|
|
322
|
+
-- mesma acabou de trazer. Fora disso n\xE3o h\xE1 caminho.
|
|
323
|
+
IF coalesce(current_setting('migration.writer', true), '') = 'on' THEN
|
|
324
|
+
RETURN CASE TG_OP WHEN 'DELETE' THEN OLD ELSE NEW END;
|
|
325
|
+
END IF;
|
|
326
|
+
RAISE EXCEPTION 'plg_workforce_time_entries \xE9 append-only (% recusado) \u2014 corrija pelo ajuste, que \xE9 uma linha com autor e motivo', TG_OP
|
|
327
|
+
USING ERRCODE = '42501',
|
|
328
|
+
HINT = 'INSERT em plg_workforce_time_adjustments (action correct|insert|void) exige workforce.adjust_time.';
|
|
329
|
+
END $$;
|
|
330
|
+
|
|
331
|
+
DROP TRIGGER IF EXISTS plg_workforce_time_entries_append_only ON public.plg_workforce_time_entries;
|
|
332
|
+
CREATE TRIGGER plg_workforce_time_entries_append_only
|
|
333
|
+
BEFORE UPDATE OR DELETE ON public.plg_workforce_time_entries
|
|
334
|
+
FOR EACH ROW EXECUTE FUNCTION public.plg_workforce_time_entries_append_only();
|
|
335
|
+
|
|
336
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
337
|
+
-- 3. O AJUSTE \xE9 uma linha, n\xE3o uma coluna
|
|
338
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
339
|
+
--
|
|
340
|
+
-- O V1 guarda o ajuste como quatro colunas na pr\xF3pria batida
|
|
341
|
+
-- (\`checkin_adjusted_at\`, \`checkout_adjusted_at\`, \`adjustment_reason\`,
|
|
342
|
+
-- \`adjusted_by_*\`) \u2014 o que j\xE1 \xE9 melhor que sobrescrever, e \xE9 a parte boa do
|
|
343
|
+
-- modelo dele: *"Os hor\xE1rios reais dos batimentos s\xE3o preservados."* O que ele
|
|
344
|
+
-- n\xE3o tem \xE9 HIST\xD3RICO: o segundo ajuste apaga o primeiro, e ningu\xE9m consegue
|
|
345
|
+
-- dizer quantas vezes aquela hora mudou nem por quem.
|
|
346
|
+
--
|
|
347
|
+
-- Tr\xEAs a\xE7\xF5es, porque o V1 faz as tr\xEAs e chama todas de "ajustar":
|
|
348
|
+
-- correct \u2014 a hora \xE9 outra (o caso comum)
|
|
349
|
+
-- insert \u2014 faltou a batida (o "esqueceu de bater a sa\xEDda" do V1; a batida
|
|
350
|
+
-- nova entra em plg_workforce_time_entries com origin 'manual' e
|
|
351
|
+
-- esta linha \xE9 quem carrega o autor e o motivo dela)
|
|
352
|
+
-- void \u2014 esta batida n\xE3o conta (o clique duplo, o totem testado). N\xE3o \xE9
|
|
353
|
+
-- DELETE: a linha fica, marcada, porque sumir com prova \xE9 o que o
|
|
354
|
+
-- append-only existe para impedir.
|
|
355
|
+
CREATE TABLE IF NOT EXISTS public.plg_workforce_time_adjustments (
|
|
356
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
357
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
358
|
+
unit_id uuid REFERENCES app.units(id),
|
|
359
|
+
time_entry_id uuid NOT NULL
|
|
360
|
+
REFERENCES public.plg_workforce_time_entries(id) ON DELETE CASCADE,
|
|
361
|
+
action text NOT NULL CHECK (action IN ('correct', 'insert', 'void')),
|
|
362
|
+
-- A hora que passa a valer. Obrigat\xF3ria em correct/insert e sem sentido em
|
|
363
|
+
-- void \u2014 a constraint diz isso em vez de deixar para a tela lembrar.
|
|
364
|
+
effective_at timestamptz,
|
|
365
|
+
-- No V1 o motivo \xE9 OPCIONAL ("Motivo (opcional)"), e \xE9 por isso que ningu\xE9m
|
|
366
|
+
-- sabe por que 20 pontos foram mexidos. Aqui \xE9 NOT NULL: um ajuste sem motivo
|
|
367
|
+
-- registrado \xE9 indistingu\xEDvel de uma fraude bem-educada.
|
|
368
|
+
reason text NOT NULL,
|
|
369
|
+
adjusted_by_user_id uuid,
|
|
370
|
+
-- O nome de quem ajustou, congelado \u2014 mesma raz\xE3o de \`person_name\`.
|
|
371
|
+
adjusted_by_name text,
|
|
372
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
373
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
374
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
375
|
+
CONSTRAINT plg_workforce_time_adjustments_effective_check
|
|
376
|
+
CHECK ((action = 'void' AND effective_at IS NULL) OR (action <> 'void' AND effective_at IS NOT NULL)),
|
|
377
|
+
CONSTRAINT plg_workforce_time_adjustments_reason_check
|
|
378
|
+
CHECK (btrim(reason) <> '')
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
COMMENT ON TABLE public.plg_workforce_time_adjustments IS
|
|
382
|
+
'O ajuste de ponto como LINHA, com autor e motivo obrigat\xF3rio. O V1 guarda isso em colunas da pr\xF3pria batida e perde o hist\xF3rico no segundo ajuste; aqui cada corre\xE7\xE3o fica, e a hora que vale \xE9 a da \xFAltima n\xE3o anulada.';
|
|
383
|
+
|
|
384
|
+
-- "Qual a hora que vale nesta batida?" \u2014 a view da se\xE7\xE3o 7 pergunta isso uma vez
|
|
385
|
+
-- por batida, e \xE9 o caminho quente da folha de ponto inteira.
|
|
386
|
+
CREATE INDEX IF NOT EXISTS plg_workforce_time_adjustments_entry_idx
|
|
387
|
+
ON public.plg_workforce_time_adjustments (time_entry_id, created_at DESC);
|
|
388
|
+
|
|
389
|
+
DROP TRIGGER IF EXISTS plg_workforce_time_adjustments_updated_at ON public.plg_workforce_time_adjustments;
|
|
390
|
+
CREATE TRIGGER plg_workforce_time_adjustments_updated_at
|
|
391
|
+
BEFORE UPDATE ON public.plg_workforce_time_adjustments
|
|
392
|
+
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
393
|
+
|
|
394
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_workforce_time_adjustments TO authenticated, service_role;
|
|
395
|
+
|
|
396
|
+
SELECT app.scaffold_table('public.plg_workforce_time_adjustments'::regclass,
|
|
397
|
+
'workforce.time_adjustment', 'workforce', false, 'enforce');
|
|
398
|
+
|
|
399
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
400
|
+
-- 4. A COMPET\xCANCIA (lacuna a)
|
|
401
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
402
|
+
--
|
|
403
|
+
-- Mesmo desenho de \`plg_financial_periods\` (plugin-financial/006), de
|
|
404
|
+
-- prop\xF3sito: a empresa fecha UM m\xEAs, e ter dois calend\xE1rios de fechamento com
|
|
405
|
+
-- regras diferentes \u2014 um do dinheiro e outro das horas \u2014 \xE9 como n\xE3o ter
|
|
406
|
+
-- fechamento nenhum, porque a pergunta "fevereiro est\xE1 fechado?" passa a ter
|
|
407
|
+
-- duas respostas.
|
|
408
|
+
CREATE TABLE IF NOT EXISTS public.plg_workforce_periods (
|
|
409
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
410
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
411
|
+
-- A compet\xEAncia \xE9 do TENANT, n\xE3o da unidade: fechar o m\xEAs \xE9 um ato de
|
|
412
|
+
-- empresa, e um m\xEAs meio fechado \xE9 a pior resposta para quem precisa dizer ao
|
|
413
|
+
-- contador que acabou. A coluna existe porque o scaffold precisa dela.
|
|
414
|
+
unit_id uuid REFERENCES app.units(id),
|
|
415
|
+
-- Sempre o dia 1\xBA, por constraint e n\xE3o por conven\xE7\xE3o de quem escreve.
|
|
416
|
+
competence_month date NOT NULL,
|
|
417
|
+
status text NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'closed')),
|
|
418
|
+
closed_at timestamptz,
|
|
419
|
+
closed_by_user_id uuid,
|
|
420
|
+
reopened_at timestamptz,
|
|
421
|
+
reopened_by_user_id uuid,
|
|
422
|
+
-- Reabrir sem dizer por qu\xEA \xE9 como n\xE3o ter fechado. Obrigat\xF3rio na RPC; a
|
|
423
|
+
-- coluna aceita nulo porque um per\xEDodo nunca reaberto n\xE3o tem um.
|
|
424
|
+
reopen_reason text,
|
|
425
|
+
-- O que o fechamento virou no financeiro, quando virou. A folha \xE9 uma conta a
|
|
426
|
+
-- pagar como outra qualquer, e a classifica\xE7\xE3o dela mora aqui porque \xE9 o
|
|
427
|
+
-- fechamento \u2014 n\xE3o a batida \u2014 que produz o lan\xE7amento.
|
|
428
|
+
payable_invoice_id uuid,
|
|
429
|
+
chart_account_id uuid,
|
|
430
|
+
cost_center_id uuid,
|
|
431
|
+
notes text,
|
|
432
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
433
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
434
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
435
|
+
CONSTRAINT plg_workforce_periods_first_of_month_check
|
|
436
|
+
CHECK (competence_month = date_trunc('month', competence_month)::date),
|
|
437
|
+
CONSTRAINT plg_workforce_periods_tenant_month_key UNIQUE (tenant_id, competence_month)
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
COMMENT ON TABLE public.plg_workforce_periods IS
|
|
441
|
+
'Compet\xEAncia mensal da jornada e seu estado. Fechada = nenhuma batida nem ajuste entra com data naquele m\xEAs. Escrita s\xF3 por workforce_period_close/reopen.';
|
|
442
|
+
|
|
443
|
+
-- A trava consulta "este m\xEAs est\xE1 fechado?" a cada batida: \xEDndice parcial.
|
|
444
|
+
CREATE INDEX IF NOT EXISTS plg_workforce_periods_closed_idx
|
|
445
|
+
ON public.plg_workforce_periods (tenant_id, competence_month) WHERE status = 'closed';
|
|
446
|
+
|
|
447
|
+
DROP TRIGGER IF EXISTS plg_workforce_periods_updated_at ON public.plg_workforce_periods;
|
|
448
|
+
CREATE TRIGGER plg_workforce_periods_updated_at
|
|
449
|
+
BEFORE UPDATE ON public.plg_workforce_periods
|
|
450
|
+
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
451
|
+
|
|
452
|
+
GRANT SELECT ON public.plg_workforce_periods TO authenticated;
|
|
453
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_workforce_periods TO service_role;
|
|
454
|
+
|
|
455
|
+
SELECT app.scaffold_table('public.plg_workforce_periods'::regclass,
|
|
456
|
+
'workforce.period', 'workforce', false, 'enforce');
|
|
457
|
+
|
|
458
|
+
-- ESCREVER \xC9 S\xD3 PELA RPC. Fechar e reabrir s\xE3o ATOS, n\xE3o edi\xE7\xF5es de linha, e o
|
|
459
|
+
-- ato \xE9 onde a permiss\xE3o e o registro de QUEM moram. Uma policy de UPDATE aqui
|
|
460
|
+
-- seria a porta dos fundos da trava \u2014 \xE9 a mesma decis\xE3o do financeiro/006.
|
|
461
|
+
DROP POLICY IF EXISTS plg_workforce_periods_authz_insert ON public.plg_workforce_periods;
|
|
462
|
+
DROP POLICY IF EXISTS plg_workforce_periods_authz_update ON public.plg_workforce_periods;
|
|
463
|
+
DROP POLICY IF EXISTS plg_workforce_periods_authz_delete ON public.plg_workforce_periods;
|
|
464
|
+
|
|
465
|
+
CREATE OR REPLACE FUNCTION public.plg_workforce_period_is_closed(p_tenant uuid, p_date date)
|
|
466
|
+
RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO ''
|
|
467
|
+
AS $$
|
|
468
|
+
SELECT EXISTS (
|
|
469
|
+
SELECT 1 FROM public.plg_workforce_periods p
|
|
470
|
+
WHERE p.tenant_id = p_tenant
|
|
471
|
+
AND p.status = 'closed'
|
|
472
|
+
AND p.competence_month = date_trunc('month', p_date)::date);
|
|
473
|
+
$$;
|
|
474
|
+
|
|
475
|
+
COMMENT ON FUNCTION public.plg_workforce_period_is_closed(uuid, date) IS
|
|
476
|
+
'O m\xEAs desta data est\xE1 fechado para a jornada deste tenant?';
|
|
477
|
+
|
|
478
|
+
-- \u2500\u2500 a trava sobre a batida \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
479
|
+
CREATE OR REPLACE FUNCTION public.trg_workforce_refuse_closed_period()
|
|
480
|
+
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
|
|
481
|
+
AS $$
|
|
482
|
+
DECLARE
|
|
483
|
+
v_date date;
|
|
484
|
+
BEGIN
|
|
485
|
+
-- Uma onda de migra\xE7\xE3o traz o passado inteiro, e o passado inteiro est\xE1 em
|
|
486
|
+
-- meses que o V1 j\xE1 tratou como encerrados. Fechar por cima do que ainda est\xE1
|
|
487
|
+
-- chegando seria fechar antes de existir.
|
|
488
|
+
IF coalesce(current_setting('migration.writer', true), '') = 'on' THEN
|
|
489
|
+
RETURN NEW;
|
|
490
|
+
END IF;
|
|
491
|
+
|
|
492
|
+
-- A data que conta \xE9 a da JORNADA, n\xE3o a de cria\xE7\xE3o da linha.
|
|
493
|
+
IF TG_TABLE_NAME = 'plg_workforce_time_entries' THEN
|
|
494
|
+
v_date := NEW.work_date;
|
|
495
|
+
ELSE
|
|
496
|
+
SELECT e.work_date INTO v_date
|
|
497
|
+
FROM public.plg_workforce_time_entries e WHERE e.id = NEW.time_entry_id;
|
|
498
|
+
END IF;
|
|
499
|
+
|
|
500
|
+
IF v_date IS NOT NULL AND public.plg_workforce_period_is_closed(NEW.tenant_id, v_date) THEN
|
|
501
|
+
RAISE EXCEPTION 'a compet\xEAncia de % est\xE1 fechada \u2014 reabra o per\xEDodo para lan\xE7ar nela', to_char(v_date, 'MM/YYYY')
|
|
502
|
+
USING ERRCODE = '23514',
|
|
503
|
+
HINT = 'workforce_period_reopen(date_trunc(''month'', data)::date, ''motivo'') exige workforce.close_period.';
|
|
504
|
+
END IF;
|
|
505
|
+
|
|
506
|
+
RETURN NEW;
|
|
507
|
+
END $$;
|
|
508
|
+
|
|
509
|
+
DROP TRIGGER IF EXISTS plg_workforce_time_entries_refuse_closed_period ON public.plg_workforce_time_entries;
|
|
510
|
+
CREATE TRIGGER plg_workforce_time_entries_refuse_closed_period
|
|
511
|
+
BEFORE INSERT ON public.plg_workforce_time_entries
|
|
512
|
+
FOR EACH ROW EXECUTE FUNCTION public.trg_workforce_refuse_closed_period();
|
|
513
|
+
|
|
514
|
+
DROP TRIGGER IF EXISTS plg_workforce_time_adjustments_refuse_closed_period ON public.plg_workforce_time_adjustments;
|
|
515
|
+
CREATE TRIGGER plg_workforce_time_adjustments_refuse_closed_period
|
|
516
|
+
BEFORE INSERT OR UPDATE ON public.plg_workforce_time_adjustments
|
|
517
|
+
FOR EACH ROW EXECUTE FUNCTION public.trg_workforce_refuse_closed_period();
|
|
518
|
+
|
|
519
|
+
-- \u2500\u2500 a segunda trava do V1, agora no banco \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
520
|
+
--
|
|
521
|
+
-- No V1, batida com \`payable_invoice_id\` preenchido n\xE3o pode ser ajustada \u2014 e a
|
|
522
|
+
-- regra vive no componente que esconde o bot\xE3o. Aqui ela vive onde n\xE3o d\xE1 para
|
|
523
|
+
-- contornar. A linha migrada \xE9 coberta pela mesma trava: o V1 aponta para uma
|
|
524
|
+
-- tabela de t\xEDtulos cuja contraparte no V2 n\xE3o \xE9 a mesma coisa (ver a nota do
|
|
525
|
+
-- de-para), ent\xE3o a refer\xEAncia crua fica em \`metadata.v1.payable_invoice_id\` \u2014 e
|
|
526
|
+
-- basta ela existir para a hora estar travada.
|
|
527
|
+
CREATE OR REPLACE FUNCTION public.trg_workforce_refuse_settled_adjustment()
|
|
528
|
+
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
|
|
529
|
+
AS $$
|
|
530
|
+
DECLARE
|
|
531
|
+
v_entry public.plg_workforce_time_entries%ROWTYPE;
|
|
532
|
+
BEGIN
|
|
533
|
+
IF coalesce(current_setting('migration.writer', true), '') = 'on' THEN
|
|
534
|
+
RETURN NEW;
|
|
535
|
+
END IF;
|
|
536
|
+
SELECT * INTO v_entry FROM public.plg_workforce_time_entries WHERE id = NEW.time_entry_id;
|
|
537
|
+
IF v_entry.payable_invoice_id IS NOT NULL
|
|
538
|
+
OR (v_entry.metadata #>> '{v1,payable_invoice_id}') IS NOT NULL THEN
|
|
539
|
+
RAISE EXCEPTION 'esta batida j\xE1 virou t\xEDtulo a pagar \u2014 a hora paga n\xE3o se corrige sem estornar o t\xEDtulo'
|
|
540
|
+
USING ERRCODE = '23514';
|
|
541
|
+
END IF;
|
|
542
|
+
IF NOT app.has_permission('workforce.adjust_time', v_entry.unit_id) THEN
|
|
543
|
+
RAISE EXCEPTION 'ajustar ponto exige workforce.adjust_time' USING ERRCODE = '42501';
|
|
544
|
+
END IF;
|
|
545
|
+
RETURN NEW;
|
|
546
|
+
END $$;
|
|
547
|
+
|
|
548
|
+
DROP TRIGGER IF EXISTS plg_workforce_time_adjustments_refuse_settled ON public.plg_workforce_time_adjustments;
|
|
549
|
+
CREATE TRIGGER plg_workforce_time_adjustments_refuse_settled
|
|
550
|
+
BEFORE INSERT ON public.plg_workforce_time_adjustments
|
|
551
|
+
FOR EACH ROW EXECUTE FUNCTION public.trg_workforce_refuse_settled_adjustment();
|
|
552
|
+
|
|
553
|
+
-- \u2500\u2500 fechar e reabrir \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
554
|
+
CREATE OR REPLACE FUNCTION public.workforce_period_close(p_month date, p_notes text DEFAULT NULL)
|
|
555
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
|
|
556
|
+
AS $$
|
|
557
|
+
DECLARE
|
|
558
|
+
v_tenant uuid := app.current_tenant_id();
|
|
559
|
+
v_month date := date_trunc('month', p_month)::date;
|
|
560
|
+
v_open bigint;
|
|
561
|
+
v_row public.plg_workforce_periods%ROWTYPE;
|
|
562
|
+
BEGIN
|
|
563
|
+
IF v_tenant IS NULL THEN
|
|
564
|
+
RAISE EXCEPTION 'workforce_period_close: sem tenant na sess\xE3o' USING ERRCODE = '42501';
|
|
565
|
+
END IF;
|
|
566
|
+
IF NOT app.has_permission('workforce.close_period', NULL) THEN
|
|
567
|
+
RAISE EXCEPTION 'workforce_period_close: exige workforce.close_period' USING ERRCODE = '42501';
|
|
568
|
+
END IF;
|
|
569
|
+
IF v_month > date_trunc('month', current_date)::date THEN
|
|
570
|
+
RAISE EXCEPTION 'workforce_period_close: % ainda n\xE3o terminou', to_char(v_month, 'MM/YYYY')
|
|
571
|
+
USING ERRCODE = '22023';
|
|
572
|
+
END IF;
|
|
573
|
+
|
|
574
|
+
-- Ponto em aberto \xE9 jornada que ainda vai mudar. Fechar por cima produz um
|
|
575
|
+
-- total de horas que muda depois de fechado, que \xE9 exatamente o que a trava
|
|
576
|
+
-- existe para impedir \u2014 e \xE9 o defeito medido no V1: 20 batimentos em aberto
|
|
577
|
+
-- fazem o valor do per\xEDodo (R$ 47.747) ser um n\xFAmero menor que o real.
|
|
578
|
+
SELECT count(*) INTO v_open
|
|
579
|
+
FROM public.v_workforce_day d
|
|
580
|
+
WHERE d.tenant_id = v_tenant
|
|
581
|
+
AND d.is_open
|
|
582
|
+
AND date_trunc('month', d.work_date)::date <= v_month;
|
|
583
|
+
IF v_open > 0 THEN
|
|
584
|
+
RAISE EXCEPTION 'workforce_period_close: % ponto(s) em aberto em % ou antes \u2014 feche ou ajuste antes', v_open, to_char(v_month, 'MM/YYYY')
|
|
585
|
+
USING ERRCODE = '23514',
|
|
586
|
+
HINT = 'SELECT * FROM public.v_workforce_day WHERE is_open;';
|
|
587
|
+
END IF;
|
|
588
|
+
|
|
589
|
+
INSERT INTO public.plg_workforce_periods (tenant_id, competence_month, status, closed_at, closed_by_user_id, notes)
|
|
590
|
+
VALUES (v_tenant, v_month, 'closed', now(), auth.uid(), p_notes)
|
|
591
|
+
ON CONFLICT (tenant_id, competence_month) DO UPDATE
|
|
592
|
+
SET status = 'closed', closed_at = now(), closed_by_user_id = auth.uid(),
|
|
593
|
+
notes = coalesce(EXCLUDED.notes, public.plg_workforce_periods.notes), updated_at = now()
|
|
594
|
+
RETURNING * INTO v_row;
|
|
595
|
+
|
|
596
|
+
RETURN jsonb_build_object('period_id', v_row.id, 'competence_month', v_month,
|
|
597
|
+
'status', 'closed', 'closed_at', v_row.closed_at);
|
|
598
|
+
END $$;
|
|
599
|
+
|
|
600
|
+
CREATE OR REPLACE FUNCTION public.workforce_period_reopen(p_month date, p_reason text)
|
|
601
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
|
|
602
|
+
AS $$
|
|
603
|
+
DECLARE
|
|
604
|
+
v_tenant uuid := app.current_tenant_id();
|
|
605
|
+
v_month date := date_trunc('month', p_month)::date;
|
|
606
|
+
v_row public.plg_workforce_periods%ROWTYPE;
|
|
607
|
+
BEGIN
|
|
608
|
+
IF v_tenant IS NULL THEN
|
|
609
|
+
RAISE EXCEPTION 'workforce_period_reopen: sem tenant na sess\xE3o' USING ERRCODE = '42501';
|
|
610
|
+
END IF;
|
|
611
|
+
IF NOT app.has_permission('workforce.close_period', NULL) THEN
|
|
612
|
+
RAISE EXCEPTION 'workforce_period_reopen: exige workforce.close_period' USING ERRCODE = '42501';
|
|
613
|
+
END IF;
|
|
614
|
+
IF nullif(btrim(coalesce(p_reason, '')), '') IS NULL THEN
|
|
615
|
+
RAISE EXCEPTION 'workforce_period_reopen: o motivo \xE9 obrigat\xF3rio' USING ERRCODE = '22023';
|
|
616
|
+
END IF;
|
|
617
|
+
|
|
618
|
+
UPDATE public.plg_workforce_periods SET
|
|
619
|
+
status = 'open', reopened_at = now(), reopened_by_user_id = auth.uid(),
|
|
620
|
+
reopen_reason = p_reason, updated_at = now()
|
|
621
|
+
WHERE tenant_id = v_tenant AND competence_month = v_month
|
|
622
|
+
RETURNING * INTO v_row;
|
|
623
|
+
|
|
624
|
+
IF v_row.id IS NULL THEN
|
|
625
|
+
RAISE EXCEPTION 'workforce_period_reopen: % nunca foi fechado', to_char(v_month, 'MM/YYYY')
|
|
626
|
+
USING ERRCODE = 'P0002';
|
|
627
|
+
END IF;
|
|
628
|
+
|
|
629
|
+
RETURN jsonb_build_object('period_id', v_row.id, 'competence_month', v_month,
|
|
630
|
+
'status', 'open', 'reopened_at', v_row.reopened_at, 'reason', p_reason);
|
|
631
|
+
END $$;
|
|
632
|
+
|
|
633
|
+
REVOKE ALL ON FUNCTION public.workforce_period_close(date, text) FROM PUBLIC;
|
|
634
|
+
REVOKE ALL ON FUNCTION public.workforce_period_reopen(date, text) FROM PUBLIC;
|
|
635
|
+
GRANT EXECUTE ON FUNCTION public.workforce_period_close(date, text) TO authenticated;
|
|
636
|
+
GRANT EXECUTE ON FUNCTION public.workforce_period_reopen(date, text) TO authenticated;
|
|
637
|
+
|
|
638
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
639
|
+
-- 5. A ESCALA: o molde
|
|
640
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
641
|
+
|
|
642
|
+
CREATE TABLE IF NOT EXISTS public.plg_workforce_schedule_templates (
|
|
643
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
644
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
645
|
+
-- No V1 o template \xE9 POR UNIDADE (o editor abre com um seletor de Unidade), e
|
|
646
|
+
-- aqui tamb\xE9m: "Almo\xE7o Seg. a Sex." da matriz n\xE3o \xE9 o da filial.
|
|
647
|
+
unit_id uuid REFERENCES app.units(id),
|
|
648
|
+
name text NOT NULL,
|
|
649
|
+
is_active boolean NOT NULL DEFAULT true,
|
|
650
|
+
notes text,
|
|
651
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
652
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
653
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
COMMENT ON TABLE public.plg_workforce_schedule_templates IS
|
|
657
|
+
'O molde da escala. Pr\xE9-requisito duro no V1: sem template n\xE3o se cria escala nenhuma \u2014 "Selecione um template de escala acima para come\xE7ar."';
|
|
658
|
+
|
|
659
|
+
CREATE TABLE IF NOT EXISTS public.plg_workforce_schedule_shifts (
|
|
660
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
661
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
662
|
+
unit_id uuid REFERENCES app.units(id),
|
|
663
|
+
template_id uuid NOT NULL
|
|
664
|
+
REFERENCES public.plg_workforce_schedule_templates(id) ON DELETE CASCADE,
|
|
665
|
+
label text NOT NULL,
|
|
666
|
+
start_time time NOT NULL,
|
|
667
|
+
end_time time NOT NULL,
|
|
668
|
+
-- 0 = domingo, como \`extract(dow)\` e como \`public.schedules.day_of_week\`.
|
|
669
|
+
-- A constraint existe porque um 7 solto num array \xE9 um turno que nunca
|
|
670
|
+
-- acontece e que ningu\xE9m percebe faltando.
|
|
671
|
+
weekdays smallint[] NOT NULL DEFAULT '{}'::smallint[],
|
|
672
|
+
-- Turno que vira o dia (16:00\u201302:00) \xE9 normal em restaurante, e \xE9 a raz\xE3o de
|
|
673
|
+
-- \`work_date\` ser coluna na batida. Guardar a travessia aqui evita que cada
|
|
674
|
+
-- leitor redescubra \`end_time < start_time\` \xE0 sua maneira.
|
|
675
|
+
crosses_midnight boolean NOT NULL GENERATED ALWAYS AS (end_time <= start_time) STORED,
|
|
676
|
+
sort_order integer NOT NULL DEFAULT 0,
|
|
677
|
+
-- O padr\xE3o de onde a hora deste turno custa. A batida copia e pode divergir.
|
|
678
|
+
-- FK condicional no fim do arquivo, como nas outras duas tabelas.
|
|
679
|
+
chart_account_id uuid,
|
|
680
|
+
cost_center_id uuid,
|
|
681
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
682
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
683
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
684
|
+
CONSTRAINT plg_workforce_schedule_shifts_weekdays_check
|
|
685
|
+
CHECK (weekdays <@ ARRAY[0,1,2,3,4,5,6]::smallint[])
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
CREATE TABLE IF NOT EXISTS public.plg_workforce_schedule_requirements (
|
|
689
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
690
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
691
|
+
unit_id uuid REFERENCES app.units(id),
|
|
692
|
+
shift_id uuid NOT NULL
|
|
693
|
+
REFERENCES public.plg_workforce_schedule_shifts(id) ON DELETE CASCADE,
|
|
694
|
+
-- Texto, e n\xE3o FK para um cadastro de profiss\xE3o: no V1 isto \xE9 o nome livre que
|
|
695
|
+
-- a tela mostra ("Gar\xE7om (1/1)", "Cozinha (0/3)"), e o V2 ainda n\xE3o tem o
|
|
696
|
+
-- cadastro de profiss\xE3o que seria o destino honesto. Inventar a FK agora seria
|
|
697
|
+
-- criar um cadastro a partir de dado sujo; o texto atravessa e fica audit\xE1vel.
|
|
698
|
+
profession_name text NOT NULL,
|
|
699
|
+
required_count integer NOT NULL DEFAULT 1 CHECK (required_count > 0),
|
|
700
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
701
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
702
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
703
|
+
UNIQUE (shift_id, profession_name)
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
COMMENT ON COLUMN public.plg_workforce_schedule_requirements.required_count IS
|
|
707
|
+
'A lota\xE7\xE3o da fun\xE7\xE3o no turno \u2014 o denominador do "Gar\xE7om (1/1)" da tela. \xC9 contra ele que a posi\xE7\xE3o por prioridade vira titular ou stand-by.';
|
|
708
|
+
|
|
709
|
+
CREATE TABLE IF NOT EXISTS public.plg_workforce_schedule_template_assignments (
|
|
710
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
711
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
712
|
+
unit_id uuid REFERENCES app.units(id),
|
|
713
|
+
template_id uuid NOT NULL
|
|
714
|
+
REFERENCES public.plg_workforce_schedule_templates(id) ON DELETE CASCADE,
|
|
715
|
+
shift_id uuid NOT NULL
|
|
716
|
+
REFERENCES public.plg_workforce_schedule_shifts(id) ON DELETE CASCADE,
|
|
717
|
+
profession_name text NOT NULL,
|
|
718
|
+
person_id uuid NOT NULL REFERENCES public.people(id) ON DELETE CASCADE,
|
|
719
|
+
-- COLUNA, e n\xE3o posi\xE7\xE3o. No V1 titular vs stand-by no molde \xE9 impl\xEDcito \u2014 s\xE3o
|
|
720
|
+
-- titulares os primeiros \`required_count\` por \`priority\` \u2014 e essa regra s\xF3
|
|
721
|
+
-- existe no c\xF3digo da tela. Aqui ela \xE9 calculada uma vez, na entrada, e fica
|
|
722
|
+
-- escrita: a mesma verdade que a inst\xE2ncia j\xE1 grava.
|
|
723
|
+
role text NOT NULL DEFAULT 'titular' CHECK (role IN ('titular', 'standby')),
|
|
724
|
+
priority integer NOT NULL DEFAULT 0,
|
|
725
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
726
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
727
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
728
|
+
UNIQUE (shift_id, profession_name, person_id)
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
732
|
+
-- 6. A ESCALA: a inst\xE2ncia do dia
|
|
733
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
734
|
+
|
|
735
|
+
CREATE TABLE IF NOT EXISTS public.plg_workforce_schedules (
|
|
736
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
737
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
738
|
+
unit_id uuid REFERENCES app.units(id),
|
|
739
|
+
schedule_date date NOT NULL,
|
|
740
|
+
-- SET NULL e n\xE3o CASCADE: apagar o molde n\xE3o pode apagar a escala de ter\xE7a
|
|
741
|
+
-- passada, que \xE9 registro de quem trabalhou. A c\xF3pia do turno j\xE1 est\xE1 no
|
|
742
|
+
-- snapshot da atribui\xE7\xE3o, ent\xE3o a escala sobrevive sozinha ao molde.
|
|
743
|
+
template_id uuid REFERENCES public.plg_workforce_schedule_templates(id) ON DELETE SET NULL,
|
|
744
|
+
status text NOT NULL DEFAULT 'draft'
|
|
745
|
+
CHECK (status IN ('draft', 'published', 'closed')),
|
|
746
|
+
published_at timestamptz,
|
|
747
|
+
published_by_user_id uuid,
|
|
748
|
+
is_special_day boolean NOT NULL DEFAULT false,
|
|
749
|
+
special_day_label text,
|
|
750
|
+
notes text,
|
|
751
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
752
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
753
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
754
|
+
-- Publicada sem carimbo de quando \xE9 uma escala que ningu\xE9m consegue provar
|
|
755
|
+
-- ter avisado \u2014 e avisar \xE9 a \xFAnica coisa que a publica\xE7\xE3o faz.
|
|
756
|
+
CONSTRAINT plg_workforce_schedules_published_check
|
|
757
|
+
CHECK (status <> 'published' OR published_at IS NOT NULL)
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
-- "Qual a escala deste dia?" \u2014 \xE9 o clique do calend\xE1rio, e \xE9 a consulta que a
|
|
761
|
+
-- tela faz primeiro em toda sess\xE3o.
|
|
762
|
+
CREATE INDEX IF NOT EXISTS plg_workforce_schedules_day_idx
|
|
763
|
+
ON public.plg_workforce_schedules (tenant_id, unit_id, schedule_date DESC, status);
|
|
764
|
+
|
|
765
|
+
CREATE TABLE IF NOT EXISTS public.plg_workforce_schedule_assignments (
|
|
766
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
767
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
768
|
+
unit_id uuid REFERENCES app.units(id),
|
|
769
|
+
schedule_id uuid NOT NULL
|
|
770
|
+
REFERENCES public.plg_workforce_schedules(id) ON DELETE CASCADE,
|
|
771
|
+
-- A refer\xEAncia ao turno do molde \xE9 conveni\xEAncia, n\xE3o depend\xEAncia: o snapshot
|
|
772
|
+
-- abaixo \xE9 quem manda. Por isso SET NULL.
|
|
773
|
+
shift_id uuid REFERENCES public.plg_workforce_schedule_shifts(id) ON DELETE SET NULL,
|
|
774
|
+
-- SNAPSHOT, copiado na cria\xE7\xE3o da escala, exatamente como o V1 faz. Mudar o
|
|
775
|
+
-- molde n\xE3o pode reescrever a escala de anteontem: o que a pessoa foi chamada
|
|
776
|
+
-- para cumprir \xE9 um fato daquele dia, e \xE9 contra ESTE hor\xE1rio que o ponto dela
|
|
777
|
+
-- \xE9 conferido.
|
|
778
|
+
shift_label text,
|
|
779
|
+
shift_start_time time,
|
|
780
|
+
shift_end_time time,
|
|
781
|
+
profession_name text,
|
|
782
|
+
person_id uuid NOT NULL REFERENCES public.people(id) ON DELETE RESTRICT,
|
|
783
|
+
role text NOT NULL DEFAULT 'titular' CHECK (role IN ('titular', 'standby')),
|
|
784
|
+
priority integer NOT NULL DEFAULT 0,
|
|
785
|
+
status text NOT NULL DEFAULT 'pending'
|
|
786
|
+
CHECK (status IN ('pending', 'notified', 'confirmed', 'declined', 'realized')),
|
|
787
|
+
-- O convite que sai por link, sem sess\xE3o. \xDAnico no tenant porque \xE9 uma
|
|
788
|
+
-- credencial: dois convites com o mesmo token s\xE3o duas pessoas confirmando
|
|
789
|
+
-- pelo mesmo endere\xE7o.
|
|
790
|
+
response_token text,
|
|
791
|
+
response_channel text,
|
|
792
|
+
notified_at timestamptz,
|
|
793
|
+
responded_at timestamptz,
|
|
794
|
+
-- A PONTE COM A AGENDA, e a raz\xE3o de a escala n\xE3o morar l\xE1. O RH oferta; ao
|
|
795
|
+
-- publicar, a atribui\xE7\xE3o materializa uma linha de \`public.schedules\` com
|
|
796
|
+
-- kind 'work_shift' e a agenda consome. A coluna \xE9 o que torna o cruzamento
|
|
797
|
+
-- previsto \xD7 realizado uma chave em vez de um casamento por nome e hora.
|
|
798
|
+
schedule_row_id uuid REFERENCES public.schedules(id) ON DELETE SET NULL,
|
|
799
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
800
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
801
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
802
|
+
-- A mesma pessoa n\xE3o ocupa a mesma vaga duas vezes no mesmo dia. No V1 nada
|
|
803
|
+
-- impede, e o resultado \xE9 a lota\xE7\xE3o "2/1" que a tela n\xE3o sabe desenhar.
|
|
804
|
+
UNIQUE (schedule_id, shift_id, profession_name, person_id)
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
CREATE UNIQUE INDEX IF NOT EXISTS plg_workforce_schedule_assignments_token_uq
|
|
808
|
+
ON public.plg_workforce_schedule_assignments (tenant_id, response_token)
|
|
809
|
+
WHERE response_token IS NOT NULL;
|
|
810
|
+
|
|
811
|
+
-- "Quem est\xE1 escalado hoje, e quem ainda n\xE3o respondeu?" \u2014 os contadores
|
|
812
|
+
-- "N titulares \xB7 N pendentes" do cart\xE3o da escala.
|
|
813
|
+
CREATE INDEX IF NOT EXISTS plg_workforce_schedule_assignments_schedule_idx
|
|
814
|
+
ON public.plg_workforce_schedule_assignments (schedule_id, role, status);
|
|
815
|
+
|
|
816
|
+
-- "O que ESTA pessoa tem pela frente?" \u2014 a folha individual e o convite.
|
|
817
|
+
CREATE INDEX IF NOT EXISTS plg_workforce_schedule_assignments_person_idx
|
|
818
|
+
ON public.plg_workforce_schedule_assignments (tenant_id, person_id, status);
|
|
819
|
+
|
|
820
|
+
-- O log da atribui\xE7\xE3o: quem mudou o qu\xEA, por qual canal. No V1 \xE9
|
|
821
|
+
-- \`schedule_assignment_logs\`, e \xE9 a \xFAnica parte do modelo de escala dele que j\xE1
|
|
822
|
+
-- nasceu append-only.
|
|
823
|
+
CREATE TABLE IF NOT EXISTS public.plg_workforce_assignment_logs (
|
|
824
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
825
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
826
|
+
unit_id uuid REFERENCES app.units(id),
|
|
827
|
+
assignment_id uuid NOT NULL
|
|
828
|
+
REFERENCES public.plg_workforce_schedule_assignments(id) ON DELETE CASCADE,
|
|
829
|
+
from_status text,
|
|
830
|
+
to_status text NOT NULL,
|
|
831
|
+
channel text,
|
|
832
|
+
note text,
|
|
833
|
+
changed_by_user_id uuid,
|
|
834
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
835
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
836
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
CREATE INDEX IF NOT EXISTS plg_workforce_assignment_logs_assignment_idx
|
|
840
|
+
ON public.plg_workforce_assignment_logs (assignment_id, created_at DESC);
|
|
841
|
+
|
|
842
|
+
-- \u2500\u2500 triggers, grants e scaffold das seis tabelas de escala \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
843
|
+
--
|
|
844
|
+
-- Em la\xE7o porque s\xE3o seis vezes a mesma disciplina: o gatilho de updated_at, o
|
|
845
|
+
-- GRANT para os dois pap\xE9is que existem, e o scaffold desenhando a policy. Seis
|
|
846
|
+
-- blocos copiados divergiriam no primeiro dia em que algu\xE9m editasse um s\xF3.
|
|
847
|
+
DO $$
|
|
848
|
+
DECLARE
|
|
849
|
+
r record;
|
|
850
|
+
BEGIN
|
|
851
|
+
FOR r IN
|
|
852
|
+
SELECT * FROM (VALUES
|
|
853
|
+
('plg_workforce_schedule_templates', 'workforce.schedule_template'),
|
|
854
|
+
('plg_workforce_schedule_shifts', 'workforce.schedule_shift'),
|
|
855
|
+
('plg_workforce_schedule_requirements', 'workforce.schedule_requirement'),
|
|
856
|
+
('plg_workforce_schedule_template_assignments', 'workforce.schedule_template_assignment'),
|
|
857
|
+
('plg_workforce_schedules', 'workforce.schedule'),
|
|
858
|
+
('plg_workforce_schedule_assignments', 'workforce.schedule_assignment'),
|
|
859
|
+
('plg_workforce_assignment_logs', 'workforce.assignment_log')
|
|
860
|
+
) AS t(tbl, resource)
|
|
861
|
+
LOOP
|
|
862
|
+
EXECUTE format('DROP TRIGGER IF EXISTS %I ON public.%I', r.tbl || '_updated_at', r.tbl);
|
|
863
|
+
EXECUTE format('CREATE TRIGGER %I BEFORE UPDATE ON public.%I FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at()',
|
|
864
|
+
r.tbl || '_updated_at', r.tbl);
|
|
865
|
+
EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON public.%I TO authenticated, service_role', r.tbl);
|
|
866
|
+
PERFORM app.scaffold_table(format('public.%I', r.tbl)::regclass, r.resource, 'workforce', false, 'enforce');
|
|
867
|
+
END LOOP;
|
|
868
|
+
END $$;
|
|
869
|
+
|
|
870
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
871
|
+
-- 7. O CRUZAMENTO previsto \xD7 realizado (lacuna b)
|
|
872
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
873
|
+
|
|
874
|
+
-- A hora que VALE em cada batida: a \xFAltima corre\xE7\xE3o n\xE3o anulada, ou o hor\xE1rio
|
|
875
|
+
-- real. \xC9 a leitura que substitui as colunas \`*_adjusted_at\` do V1 sem perder a
|
|
876
|
+
-- propriedade que o V1 acertou \u2014 o hor\xE1rio real continua ali, intocado.
|
|
877
|
+
--
|
|
878
|
+
-- \`security_invoker\` em todas as views desta se\xE7\xE3o: uma folha de ponto que
|
|
879
|
+
-- ignora escopo \xE9 um vazamento com totais.
|
|
880
|
+
-- REPLAY: \`DROP\` e depois \`CREATE\`, e n\xE3o \`CREATE OR REPLACE\`.
|
|
881
|
+
--
|
|
882
|
+
-- As migrations 003/004/005 ACRESCENTAM colunas a esta view. Acrescentar,
|
|
883
|
+
-- \`CREATE OR REPLACE\` faz; TIRAR, n\xE3o faz \u2014 e \xE9 o que a baseline faria numa
|
|
884
|
+
-- segunda passada, porque a vers\xE3o dela \xE9 a estreita. O cluster devolve
|
|
885
|
+
-- \`42P16: cannot drop columns from view\` e a corrente para aqui.
|
|
886
|
+
--
|
|
887
|
+
-- Conferido em \`pg_depend\` antes de derrubar: nada pende destas duas views,
|
|
888
|
+
-- ent\xE3o o \`DROP\` \xE9 sem \`CASCADE\` \u2014 de prop\xF3sito. Sem ele, o dia em que
|
|
889
|
+
-- algu\xE9m construir sobre elas o \`DROP\` falha alto em vez de levar junto.
|
|
890
|
+
-- Na ordem da depend\xEAncia: \`v_workforce_day\` \xE9 constru\xEDda SOBRE esta, ent\xE3o
|
|
891
|
+
-- sai primeiro. Sem isso o \`DROP\` de baixo recusa \u2014 e recusar \xE9 o certo,
|
|
892
|
+
-- foi assim que esta depend\xEAncia apareceu.
|
|
893
|
+
DROP VIEW IF EXISTS public.v_workforce_day;
|
|
894
|
+
DROP VIEW IF EXISTS public.v_workforce_time_entries;
|
|
895
|
+
CREATE VIEW public.v_workforce_time_entries
|
|
896
|
+
WITH (security_invoker = 'true') AS
|
|
897
|
+
SELECT e.id,
|
|
898
|
+
e.tenant_id,
|
|
899
|
+
e.unit_id,
|
|
900
|
+
e.person_id,
|
|
901
|
+
e.person_name,
|
|
902
|
+
e.kind,
|
|
903
|
+
e.work_date,
|
|
904
|
+
e.seq,
|
|
905
|
+
e.origin,
|
|
906
|
+
e.punched_at,
|
|
907
|
+
coalesce(c.effective_at, e.punched_at) AS effective_at,
|
|
908
|
+
(c.id IS NOT NULL) AS adjusted,
|
|
909
|
+
(v.id IS NOT NULL) AS voided,
|
|
910
|
+
c.reason AS adjustment_reason,
|
|
911
|
+
c.adjusted_by_name,
|
|
912
|
+
e.assignment_id,
|
|
913
|
+
e.payable_invoice_id,
|
|
914
|
+
e.evidence,
|
|
915
|
+
e.metadata
|
|
916
|
+
FROM public.plg_workforce_time_entries e
|
|
917
|
+
LEFT JOIN LATERAL (
|
|
918
|
+
SELECT a.id, a.effective_at, a.reason, a.adjusted_by_name
|
|
919
|
+
FROM public.plg_workforce_time_adjustments a
|
|
920
|
+
WHERE a.time_entry_id = e.id AND a.action IN ('correct', 'insert')
|
|
921
|
+
ORDER BY a.created_at DESC, a.id DESC
|
|
922
|
+
LIMIT 1) c ON true
|
|
923
|
+
LEFT JOIN LATERAL (
|
|
924
|
+
SELECT a.id FROM public.plg_workforce_time_adjustments a
|
|
925
|
+
WHERE a.time_entry_id = e.id AND a.action = 'void'
|
|
926
|
+
LIMIT 1) v ON true;
|
|
927
|
+
|
|
928
|
+
COMMENT ON VIEW public.v_workforce_time_entries IS
|
|
929
|
+
'A batida com a hora que vale: a \xFAltima corre\xE7\xE3o n\xE3o anulada, ou o hor\xE1rio real. O real (punched_at) nunca some \u2014 \xE9 o que torna o ponto audit\xE1vel.';
|
|
930
|
+
|
|
931
|
+
-- O dia de trabalho de uma pessoa, previsto ao lado de realizado.
|
|
932
|
+
--
|
|
933
|
+
-- FULL OUTER JOIN de prop\xF3sito. As duas assimetrias s\xE3o exatamente o que o V1
|
|
934
|
+
-- n\xE3o consegue mostrar, e s\xE3o as duas perguntas do RH: previsto sem realizado \xE9
|
|
935
|
+
-- FALTA; realizado sem previsto \xE9 FORA DA ESCALA (que no tenant medido \xE9 o
|
|
936
|
+
-- estado de 325 de 325 registros \u2014 n\xE3o porque as pessoas trabalhem fora da
|
|
937
|
+
-- escala, mas porque o ponto est\xE1 ligado numa unidade e a escala em outra).
|
|
938
|
+
--
|
|
939
|
+
-- As horas trabalhadas saem de uma soma com sinal: clock_out e break_start
|
|
940
|
+
-- FECHAM um trecho, clock_in e break_end ABREM. Quantos intervalos houver no
|
|
941
|
+
-- dia, a conta \xE9 a mesma \u2014 \xE9 isso que o par \xFAnico do V1 n\xE3o consegue fazer.
|
|
942
|
+
-- REPLAY: \`DROP\` e depois \`CREATE\`, e n\xE3o \`CREATE OR REPLACE\`.
|
|
943
|
+
--
|
|
944
|
+
-- As migrations 003/004/005 ACRESCENTAM colunas a esta view. Acrescentar,
|
|
945
|
+
-- \`CREATE OR REPLACE\` faz; TIRAR, n\xE3o faz \u2014 e \xE9 o que a baseline faria numa
|
|
946
|
+
-- segunda passada, porque a vers\xE3o dela \xE9 a estreita. O cluster devolve
|
|
947
|
+
-- \`42P16: cannot drop columns from view\` e a corrente para aqui.
|
|
948
|
+
--
|
|
949
|
+
-- Conferido em \`pg_depend\` antes de derrubar: nada pende destas duas views,
|
|
950
|
+
-- ent\xE3o o \`DROP\` \xE9 sem \`CASCADE\` \u2014 de prop\xF3sito. Sem ele, o dia em que
|
|
951
|
+
-- algu\xE9m construir sobre elas o \`DROP\` falha alto em vez de levar junto.
|
|
952
|
+
DROP VIEW IF EXISTS public.v_workforce_day;
|
|
953
|
+
CREATE VIEW public.v_workforce_day
|
|
954
|
+
WITH (security_invoker = 'true') AS
|
|
955
|
+
WITH realized AS (
|
|
956
|
+
SELECT e.tenant_id,
|
|
957
|
+
e.unit_id,
|
|
958
|
+
e.person_id,
|
|
959
|
+
e.work_date,
|
|
960
|
+
min(e.effective_at) FILTER (WHERE e.kind = 'clock_in') AS first_in,
|
|
961
|
+
max(e.effective_at) FILTER (WHERE e.kind = 'clock_out') AS last_out,
|
|
962
|
+
count(*) FILTER (WHERE e.kind = 'break_start') AS break_count,
|
|
963
|
+
count(*) FILTER (WHERE e.kind IN ('clock_in', 'break_end'))
|
|
964
|
+
<> count(*) FILTER (WHERE e.kind IN ('clock_out', 'break_start')) AS is_open,
|
|
965
|
+
sum(CASE
|
|
966
|
+
WHEN e.kind IN ('clock_out', 'break_start') THEN extract(epoch FROM e.effective_at)
|
|
967
|
+
WHEN e.kind IN ('clock_in', 'break_end') THEN -extract(epoch FROM e.effective_at)
|
|
968
|
+
END) AS worked_seconds
|
|
969
|
+
FROM public.v_workforce_time_entries e
|
|
970
|
+
WHERE NOT e.voided
|
|
971
|
+
GROUP BY e.tenant_id, e.unit_id, e.person_id, e.work_date
|
|
972
|
+
),
|
|
973
|
+
planned AS (
|
|
974
|
+
SELECT a.tenant_id,
|
|
975
|
+
a.unit_id,
|
|
976
|
+
a.person_id,
|
|
977
|
+
s.schedule_date AS work_date,
|
|
978
|
+
a.id AS assignment_id,
|
|
979
|
+
a.role,
|
|
980
|
+
a.status AS assignment_status,
|
|
981
|
+
a.shift_label,
|
|
982
|
+
a.shift_start_time,
|
|
983
|
+
a.shift_end_time,
|
|
984
|
+
-- O turno que vira o dia \xE9 normal aqui; sem o +24h a jornada da noite
|
|
985
|
+
-- daria negativa.
|
|
986
|
+
(extract(epoch FROM a.shift_end_time - a.shift_start_time)
|
|
987
|
+
+ CASE WHEN a.shift_end_time <= a.shift_start_time THEN 86400 ELSE 0 END) AS planned_seconds
|
|
988
|
+
FROM public.plg_workforce_schedule_assignments a
|
|
989
|
+
JOIN public.plg_workforce_schedules s ON s.id = a.schedule_id
|
|
990
|
+
WHERE s.status IN ('published', 'closed')
|
|
991
|
+
AND a.role = 'titular'
|
|
992
|
+
AND a.shift_start_time IS NOT NULL
|
|
993
|
+
AND a.shift_end_time IS NOT NULL
|
|
994
|
+
)
|
|
995
|
+
SELECT coalesce(p.tenant_id, r.tenant_id) AS tenant_id,
|
|
996
|
+
coalesce(p.unit_id, r.unit_id) AS unit_id,
|
|
997
|
+
coalesce(p.person_id, r.person_id) AS person_id,
|
|
998
|
+
coalesce(p.work_date, r.work_date) AS work_date,
|
|
999
|
+
p.assignment_id,
|
|
1000
|
+
p.shift_label,
|
|
1001
|
+
p.shift_start_time,
|
|
1002
|
+
p.shift_end_time,
|
|
1003
|
+
p.assignment_status,
|
|
1004
|
+
(p.planned_seconds / 60)::integer AS planned_minutes,
|
|
1005
|
+
(r.worked_seconds / 60)::integer AS worked_minutes,
|
|
1006
|
+
((r.worked_seconds - p.planned_seconds) / 60)::integer AS variance_minutes,
|
|
1007
|
+
r.first_in,
|
|
1008
|
+
r.last_out,
|
|
1009
|
+
r.break_count,
|
|
1010
|
+
coalesce(r.is_open, false) AS is_open,
|
|
1011
|
+
-- As duas assimetrias, nomeadas. S\xE3o as perguntas do RH, e nenhuma delas
|
|
1012
|
+
-- tem resposta no V1.
|
|
1013
|
+
(r.person_id IS NULL) AS absent,
|
|
1014
|
+
(p.assignment_id IS NULL) AS outside_schedule
|
|
1015
|
+
FROM planned p
|
|
1016
|
+
FULL OUTER JOIN realized r
|
|
1017
|
+
ON r.tenant_id = p.tenant_id
|
|
1018
|
+
AND r.person_id = p.person_id
|
|
1019
|
+
AND r.work_date = p.work_date;
|
|
1020
|
+
|
|
1021
|
+
COMMENT ON VIEW public.v_workforce_day IS
|
|
1022
|
+
'Jornada prevista \xD7 realizada por pessoa e dia. Previsto vem da escala publicada, realizado das batidas com ajuste aplicado. FULL OUTER JOIN: \`absent\` \xE9 escala sem ponto, \`outside_schedule\` \xE9 ponto sem escala \u2014 as duas perguntas que o V1 n\xE3o responde.';
|
|
1023
|
+
|
|
1024
|
+
GRANT SELECT ON public.v_workforce_time_entries TO authenticated, service_role;
|
|
1025
|
+
GRANT SELECT ON public.v_workforce_day TO authenticated, service_role;
|
|
1026
|
+
|
|
1027
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1028
|
+
-- 8. OS WRITERS da migra\xE7\xE3o
|
|
1029
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1030
|
+
|
|
1031
|
+
-- O vocabul\xE1rio do de-para \xE9 lista fechada de prop\xF3sito: uma linha que nomeia um
|
|
1032
|
+
-- writer inexistente falha na hora, n\xE3o na terceira hora da onda.
|
|
1033
|
+
--
|
|
1034
|
+
-- ACR\xC9SCIMO, e n\xE3o reemiss\xE3o. A vers\xE3o anterior reescrevia a lista INTEIRA \u2014
|
|
1035
|
+
-- "a lista at\xE9 aqui (046) mais os oito da jornada". Uma lista escrita por
|
|
1036
|
+
-- extenso \xE9 um retrato: ela apaga todo writer que outro passo registrou e que
|
|
1037
|
+
-- o retrato n\xE3o viu. Medido no cluster vivo, re-aplicar esta baseline dava
|
|
1038
|
+
--
|
|
1039
|
+
-- 23514: check constraint "allowlist_writer_check" is violated by some row
|
|
1040
|
+
--
|
|
1041
|
+
-- porque l\xE1 j\xE1 havia writers que esta lista n\xE3o nomeia. \xC9 o mesmo defeito que
|
|
1042
|
+
-- \`agenda/008\` e \`financial/072\` j\xE1 pagaram, e a forma abaixo \xE9 a de
|
|
1043
|
+
-- \`inventory/004\`: leia o que existe, acrescente o que falta, nunca reemita.
|
|
1044
|
+
DO $$
|
|
1045
|
+
DECLARE
|
|
1046
|
+
v_def text;
|
|
1047
|
+
v_falta text[];
|
|
1048
|
+
v_meus text[] := ARRAY[
|
|
1049
|
+
'wf_time_entry', 'wf_schedule_template', 'wf_schedule_shift',
|
|
1050
|
+
'wf_schedule_requirement', 'wf_schedule_template_assignment',
|
|
1051
|
+
'wf_schedule', 'wf_schedule_assignment', 'wf_assignment_log'];
|
|
1052
|
+
BEGIN
|
|
1053
|
+
SELECT pg_get_constraintdef(oid) INTO v_def
|
|
1054
|
+
FROM pg_constraint WHERE conname = 'allowlist_writer_check';
|
|
1055
|
+
|
|
1056
|
+
IF v_def IS NULL THEN
|
|
1057
|
+
-- Ningu\xE9m escreveu a regra ainda: aqui ela nasce, com os writers desta
|
|
1058
|
+
-- jornada e os que a corrente j\xE1 registrou at\xE9 este ponto.
|
|
1059
|
+
ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check
|
|
1060
|
+
CHECK (writer IS NULL OR writer = ANY (ARRAY[
|
|
1061
|
+
'person', 'unit', 'category', 'product', 'order', 'invoice', 'price_table',
|
|
1062
|
+
'price_table_item', 'package', 'package_item', 'field_rule', 'holiday', 'facet',
|
|
1063
|
+
'fin_account', 'fin_payment_method_type', 'fin_payment_method', 'fin_card_brand',
|
|
1064
|
+
'fin_chart_account', 'fin_cost_center', 'inv_measurement_unit', 'inv_unit_conversion',
|
|
1065
|
+
'inv_location', 'inv_movement', 'order_item', 'fin_movement', 'authz_role',
|
|
1066
|
+
'authz_role_permission', 'person_registration',
|
|
1067
|
+
'fin_recurring_bill', 'fin_recurring_bill_item', 'fin_recurring_bill_occurrence',
|
|
1068
|
+
'wf_time_entry', 'wf_schedule_template', 'wf_schedule_shift', 'wf_schedule_requirement',
|
|
1069
|
+
'wf_schedule_template_assignment', 'wf_schedule', 'wf_schedule_assignment',
|
|
1070
|
+
'wf_assignment_log']));
|
|
1071
|
+
RETURN;
|
|
1072
|
+
END IF;
|
|
1073
|
+
|
|
1074
|
+
SELECT array_agg(w) INTO v_falta
|
|
1075
|
+
FROM unnest(v_meus) AS w
|
|
1076
|
+
WHERE strpos(v_def, '''' || w || '''') = 0;
|
|
1077
|
+
|
|
1078
|
+
IF v_falta IS NULL THEN RETURN; END IF;
|
|
1079
|
+
|
|
1080
|
+
-- Entram na frente do primeiro elemento: a posi\xE7\xE3o n\xE3o importa para um
|
|
1081
|
+
-- \`= ANY\`, e ancorar em \`ARRAY[\` dispensa conhecer qualquer valor da lista
|
|
1082
|
+
-- \u2014 que \xE9 justamente o que n\xE3o se pode presumir.
|
|
1083
|
+
ALTER TABLE migration.allowlist DROP CONSTRAINT allowlist_writer_check;
|
|
1084
|
+
EXECUTE 'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check '
|
|
1085
|
+
|| replace(v_def, 'ARRAY[',
|
|
1086
|
+
'ARRAY[' || (SELECT string_agg(quote_literal(w) || '::text, ', '')
|
|
1087
|
+
FROM unnest(v_falta) AS w));
|
|
1088
|
+
END $$;
|
|
1089
|
+
|
|
1090
|
+
-- \u2500\u2500 o de-para da pessoa polim\xF3rfica \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1091
|
+
--
|
|
1092
|
+
-- Este helper \xE9 a tradu\xE7\xE3o de \`person_type\` + \`person_id\` para a FK \xFAnica do V2,
|
|
1093
|
+
-- e existe uma vez porque quatro writers precisam dela. O ledger \xE9 a \xFAnica fonte
|
|
1094
|
+
-- que sabe o de-para, e ele guarda a TABELA de origem \u2014 ent\xE3o 'professional' e
|
|
1095
|
+
-- 'employee' resolvem pelo mesmo caminho, e o writer n\xE3o precisa conhecer
|
|
1096
|
+
-- nenhuma das duas.
|
|
1097
|
+
CREATE OR REPLACE FUNCTION migration._wf_person(p_tenant uuid, p_meta jsonb)
|
|
1098
|
+
RETURNS uuid LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO ''
|
|
1099
|
+
AS $$
|
|
1100
|
+
SELECT l.target_id
|
|
1101
|
+
FROM migration.ledger l
|
|
1102
|
+
WHERE l.tenant_id = p_tenant
|
|
1103
|
+
AND l.source_table = CASE WHEN (p_meta #>> '{v1,person_type}') = 'professional'
|
|
1104
|
+
THEN 'professionals' ELSE 'employees' END
|
|
1105
|
+
AND l.source_numeric_id = nullif(p_meta #>> '{v1,person_id}', '')::bigint
|
|
1106
|
+
AND l.target_table = 'public.people'
|
|
1107
|
+
AND l.status = 'migrated'
|
|
1108
|
+
LIMIT 1;
|
|
1109
|
+
$$;
|
|
1110
|
+
|
|
1111
|
+
COMMENT ON FUNCTION migration._wf_person(uuid, jsonb) IS
|
|
1112
|
+
'person_type + person_id do V1 \u2192 o uuid de public.people. Duas tabelas de cadastro no V1, um cadastro s\xF3 no V2.';
|
|
1113
|
+
|
|
1114
|
+
-- Helper de migra\xE7\xE3o l\xEA o ledger inteiro do tenant: n\xE3o \xE9 fun\xE7\xE3o de aplica\xE7\xE3o.
|
|
1115
|
+
REVOKE ALL ON FUNCTION migration._wf_person(uuid, jsonb) FROM PUBLIC, anon, authenticated;
|
|
1116
|
+
|
|
1117
|
+
-- \u2500\u2500 a batida \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1118
|
+
--
|
|
1119
|
+
-- UMA linha do V1 vira AT\xC9 QUATRO linhas do V2: a entrada, a sa\xEDda (quando
|
|
1120
|
+
-- existe), e o ajuste de cada uma (quando \`*_adjusted_at\` est\xE1 preenchido). \xC9 a
|
|
1121
|
+
-- tradu\xE7\xE3o do par-com-colunas-de-ajuste para eventos-com-ajuste-em-linha.
|
|
1122
|
+
--
|
|
1123
|
+
-- Os ids derivados s\xE3o determin\xEDsticos (md5 do id de origem + o sufixo do papel)
|
|
1124
|
+
-- para que reoferecer a mesma linha numa segunda passada ATUALIZE em vez de
|
|
1125
|
+
-- duplicar o dia de trabalho de algu\xE9m. \`target_id\` devolvido \xE9 o da ENTRADA:
|
|
1126
|
+
-- \xE9 a linha do V1 que ela representa.
|
|
1127
|
+
CREATE OR REPLACE FUNCTION migration.upsert_wf_time_entry(
|
|
1128
|
+
p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb
|
|
1129
|
+
) RETURNS jsonb
|
|
1130
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
|
|
1131
|
+
AS $function$
|
|
1132
|
+
DECLARE
|
|
1133
|
+
v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
|
|
1134
|
+
v_person uuid;
|
|
1135
|
+
v_unit uuid := (p_row ->> 'unit_id')::uuid;
|
|
1136
|
+
v_in timestamptz := (p_row ->> 'checkin_at')::timestamptz;
|
|
1137
|
+
v_out timestamptz := (p_row ->> 'checkout_at')::timestamptz;
|
|
1138
|
+
v_in_adj timestamptz := (p_row ->> 'checkin_adjusted_at')::timestamptz;
|
|
1139
|
+
v_out_adj timestamptz := (p_row ->> 'checkout_adjusted_at')::timestamptz;
|
|
1140
|
+
v_work_date date;
|
|
1141
|
+
v_out_id uuid;
|
|
1142
|
+
v_reason text := coalesce(nullif(btrim(coalesce(p_row ->> 'adjustment_reason', '')), ''),
|
|
1143
|
+
'ajuste importado do V1 sem motivo registrado');
|
|
1144
|
+
v_evidence jsonb;
|
|
1145
|
+
v_created boolean := false;
|
|
1146
|
+
v_standalone boolean := migration._standalone();
|
|
1147
|
+
BEGIN
|
|
1148
|
+
IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
|
|
1149
|
+
|
|
1150
|
+
IF p_id IS NULL OR v_in IS NULL THEN
|
|
1151
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',
|
|
1152
|
+
'reason', 'uma batida precisa de id e de hora de entrada');
|
|
1153
|
+
END IF;
|
|
1154
|
+
|
|
1155
|
+
-- Recusar \xE9 o certo, e \xE9 tempor\xE1rio: a pessoa entra na mesma onda, e a segunda
|
|
1156
|
+
-- passada oferece a batida de novo. Inventar a pessoa aqui seria pendurar
|
|
1157
|
+
-- horas de trabalho \u2014 e portanto dinheiro \u2014 em quem n\xE3o existe.
|
|
1158
|
+
v_person := migration._wf_person(p_tenant, v_meta);
|
|
1159
|
+
IF v_person IS NULL THEN
|
|
1160
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',
|
|
1161
|
+
'reason', format('batida de %s #%s cuja pessoa ainda n\xE3o migrou',
|
|
1162
|
+
coalesce(v_meta #>> '{v1,person_type}', '<sem tipo>'),
|
|
1163
|
+
coalesce(v_meta #>> '{v1,person_id}', '<sem id>')));
|
|
1164
|
+
END IF;
|
|
1165
|
+
|
|
1166
|
+
-- A jornada \xE9 do dia da ENTRADA, sempre \u2014 mesmo quando a sa\xEDda \xE9 do dia
|
|
1167
|
+
-- seguinte. \xC9 a regra que o V1 admite na caixa de ajuste e nunca grava.
|
|
1168
|
+
v_work_date := coalesce((p_row ->> 'work_date')::date, v_in::date);
|
|
1169
|
+
v_out_id := md5(p_id::text || ':clock_out')::uuid;
|
|
1170
|
+
|
|
1171
|
+
-- Todo o conjunto de prova do totem numa chave s\xF3. Perder qualquer um destes
|
|
1172
|
+
-- campos \xE9 perder a defesa de uma batida contestada.
|
|
1173
|
+
v_evidence := jsonb_strip_nulls(jsonb_build_object(
|
|
1174
|
+
'photo_path', p_row ->> 'checkin_photo_path',
|
|
1175
|
+
'lat', (p_row ->> 'checkin_lat')::numeric,
|
|
1176
|
+
'lng', (p_row ->> 'checkin_lng')::numeric,
|
|
1177
|
+
'accuracy_m', (p_row ->> 'checkin_accuracy_m')::numeric,
|
|
1178
|
+
'ip', p_row ->> 'checkin_ip',
|
|
1179
|
+
'user_agent', p_row ->> 'checkin_user_agent',
|
|
1180
|
+
'face_score', (p_row ->> 'checkin_face_score')::numeric));
|
|
1181
|
+
|
|
1182
|
+
IF NOT EXISTS (SELECT 1 FROM public.plg_workforce_time_entries WHERE id = p_id) THEN
|
|
1183
|
+
INSERT INTO public.plg_workforce_time_entries
|
|
1184
|
+
(id, tenant_id, unit_id, person_id, person_name, kind, punched_at, work_date, seq,
|
|
1185
|
+
origin, evidence, assignment_id, notes, metadata)
|
|
1186
|
+
VALUES (p_id, p_tenant, v_unit, v_person, p_row ->> 'person_name', 'clock_in', v_in,
|
|
1187
|
+
v_work_date, 1, 'migration', v_evidence,
|
|
1188
|
+
(p_row ->> 'assignment_id')::uuid, p_row ->> 'notes', v_meta);
|
|
1189
|
+
v_created := true;
|
|
1190
|
+
ELSE
|
|
1191
|
+
UPDATE public.plg_workforce_time_entries SET
|
|
1192
|
+
unit_id = coalesce(v_unit, unit_id),
|
|
1193
|
+
person_id = v_person,
|
|
1194
|
+
person_name = coalesce(p_row ->> 'person_name', person_name),
|
|
1195
|
+
punched_at = v_in,
|
|
1196
|
+
work_date = v_work_date,
|
|
1197
|
+
evidence = migration._merge_meta(evidence, v_evidence),
|
|
1198
|
+
assignment_id = coalesce((p_row ->> 'assignment_id')::uuid, assignment_id),
|
|
1199
|
+
notes = CASE WHEN p_row ? 'notes' THEN p_row ->> 'notes' ELSE notes END,
|
|
1200
|
+
metadata = migration._merge_meta(metadata, v_meta)
|
|
1201
|
+
WHERE id = p_id;
|
|
1202
|
+
END IF;
|
|
1203
|
+
|
|
1204
|
+
-- A sa\xEDda s\xF3 existe quando existe. \`checkout_at IS NULL\` \xE9 o "ponto em aberto"
|
|
1205
|
+
-- do V1 \u2014 20 deles no tenant medido \u2014 e ele atravessa como aus\xEAncia de evento,
|
|
1206
|
+
-- que \xE9 o que ele \xE9. Fabricar uma sa\xEDda para "fechar" o registro inventaria
|
|
1207
|
+
-- horas trabalhadas.
|
|
1208
|
+
IF v_out IS NOT NULL THEN
|
|
1209
|
+
IF NOT EXISTS (SELECT 1 FROM public.plg_workforce_time_entries WHERE id = v_out_id) THEN
|
|
1210
|
+
INSERT INTO public.plg_workforce_time_entries
|
|
1211
|
+
(id, tenant_id, unit_id, person_id, person_name, kind, punched_at, work_date, seq,
|
|
1212
|
+
origin, evidence, assignment_id, metadata)
|
|
1213
|
+
VALUES (v_out_id, p_tenant, v_unit, v_person, p_row ->> 'person_name', 'clock_out', v_out,
|
|
1214
|
+
v_work_date, 2, 'migration',
|
|
1215
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
1216
|
+
'photo_path', p_row ->> 'checkout_photo_path',
|
|
1217
|
+
'lat', (p_row ->> 'checkout_lat')::numeric,
|
|
1218
|
+
'lng', (p_row ->> 'checkout_lng')::numeric,
|
|
1219
|
+
'accuracy_m', (p_row ->> 'checkout_accuracy_m')::numeric,
|
|
1220
|
+
'ip', p_row ->> 'checkout_ip',
|
|
1221
|
+
'user_agent', p_row ->> 'checkout_user_agent',
|
|
1222
|
+
'face_score', (p_row ->> 'checkout_face_score')::numeric)),
|
|
1223
|
+
(p_row ->> 'assignment_id')::uuid,
|
|
1224
|
+
migration._deep_set(v_meta, ARRAY['migration', 'derived_from'], to_jsonb(p_id::text)));
|
|
1225
|
+
ELSE
|
|
1226
|
+
UPDATE public.plg_workforce_time_entries SET
|
|
1227
|
+
punched_at = v_out, work_date = v_work_date, person_id = v_person
|
|
1228
|
+
WHERE id = v_out_id;
|
|
1229
|
+
END IF;
|
|
1230
|
+
END IF;
|
|
1231
|
+
|
|
1232
|
+
-- O ajuste vira LINHA. No V1 ele \xE9 um par de colunas por cima da batida, e o
|
|
1233
|
+
-- hist\xF3rico morre no segundo ajuste; aqui cada corre\xE7\xE3o fica, com autor e
|
|
1234
|
+
-- motivo. O motivo \xE9 opcional no V1 e obrigat\xF3rio aqui \u2014 quando vem vazio, o
|
|
1235
|
+
-- texto diz exatamente isso, em vez de fingir que algu\xE9m justificou.
|
|
1236
|
+
IF v_in_adj IS NOT NULL THEN
|
|
1237
|
+
INSERT INTO public.plg_workforce_time_adjustments
|
|
1238
|
+
(id, tenant_id, unit_id, time_entry_id, action, effective_at, reason,
|
|
1239
|
+
adjusted_by_user_id, adjusted_by_name, metadata)
|
|
1240
|
+
VALUES (md5(p_id::text || ':adjust_in')::uuid, p_tenant, v_unit, p_id, 'correct',
|
|
1241
|
+
v_in_adj, v_reason,
|
|
1242
|
+
(p_row ->> 'adjusted_by_user_id')::uuid, p_row ->> 'adjusted_by_name',
|
|
1243
|
+
migration._deep_set(v_meta, ARRAY['migration', 'from'], to_jsonb('checkin_adjusted_at'::text)))
|
|
1244
|
+
ON CONFLICT (id) DO UPDATE
|
|
1245
|
+
SET effective_at = EXCLUDED.effective_at, reason = EXCLUDED.reason,
|
|
1246
|
+
adjusted_by_name = EXCLUDED.adjusted_by_name, updated_at = now();
|
|
1247
|
+
END IF;
|
|
1248
|
+
|
|
1249
|
+
IF v_out_adj IS NOT NULL AND v_out IS NOT NULL THEN
|
|
1250
|
+
INSERT INTO public.plg_workforce_time_adjustments
|
|
1251
|
+
(id, tenant_id, unit_id, time_entry_id, action, effective_at, reason,
|
|
1252
|
+
adjusted_by_user_id, adjusted_by_name, metadata)
|
|
1253
|
+
VALUES (md5(p_id::text || ':adjust_out')::uuid, p_tenant, v_unit, v_out_id, 'correct',
|
|
1254
|
+
v_out_adj, v_reason,
|
|
1255
|
+
(p_row ->> 'adjusted_by_user_id')::uuid, p_row ->> 'adjusted_by_name',
|
|
1256
|
+
migration._deep_set(v_meta, ARRAY['migration', 'from'], to_jsonb('checkout_adjusted_at'::text)))
|
|
1257
|
+
ON CONFLICT (id) DO UPDATE
|
|
1258
|
+
SET effective_at = EXCLUDED.effective_at, reason = EXCLUDED.reason,
|
|
1259
|
+
adjusted_by_name = EXCLUDED.adjusted_by_name, updated_at = now();
|
|
1260
|
+
END IF;
|
|
1261
|
+
|
|
1262
|
+
IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_workforce_time_entries', p_id, '{}'::jsonb); END IF;
|
|
1263
|
+
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,
|
|
1264
|
+
'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END,
|
|
1265
|
+
'entries', CASE WHEN v_out IS NULL THEN 1 ELSE 2 END,
|
|
1266
|
+
'open', v_out IS NULL);
|
|
1267
|
+
END $function$;
|
|
1268
|
+
|
|
1269
|
+
-- \u2500\u2500 o molde \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1270
|
+
CREATE OR REPLACE FUNCTION migration.upsert_wf_schedule_template(
|
|
1271
|
+
p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb
|
|
1272
|
+
) RETURNS jsonb
|
|
1273
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
|
|
1274
|
+
AS $function$
|
|
1275
|
+
DECLARE
|
|
1276
|
+
v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
|
|
1277
|
+
v_name text := nullif(btrim(coalesce(p_row ->> 'name', '')), '');
|
|
1278
|
+
v_created boolean := false;
|
|
1279
|
+
v_standalone boolean := migration._standalone();
|
|
1280
|
+
BEGIN
|
|
1281
|
+
IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
|
|
1282
|
+
IF p_id IS NULL OR v_name IS NULL THEN
|
|
1283
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',
|
|
1284
|
+
'reason', 'um template de escala precisa de id e de nome \u2014 \xE9 por ele que a tela o oferece');
|
|
1285
|
+
END IF;
|
|
1286
|
+
|
|
1287
|
+
IF NOT EXISTS (SELECT 1 FROM public.plg_workforce_schedule_templates WHERE id = p_id) THEN
|
|
1288
|
+
INSERT INTO public.plg_workforce_schedule_templates (id, tenant_id, unit_id, name, is_active, notes, metadata)
|
|
1289
|
+
VALUES (p_id, p_tenant, (p_row ->> 'unit_id')::uuid, v_name,
|
|
1290
|
+
coalesce((p_row ->> 'is_active')::boolean, true), p_row ->> 'notes', v_meta);
|
|
1291
|
+
v_created := true;
|
|
1292
|
+
ELSE
|
|
1293
|
+
UPDATE public.plg_workforce_schedule_templates SET
|
|
1294
|
+
unit_id = CASE WHEN p_row ? 'unit_id' THEN (p_row ->> 'unit_id')::uuid ELSE unit_id END,
|
|
1295
|
+
name = v_name,
|
|
1296
|
+
is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END,
|
|
1297
|
+
notes = CASE WHEN p_row ? 'notes' THEN p_row ->> 'notes' ELSE notes END,
|
|
1298
|
+
metadata = migration._merge_meta(metadata, v_meta)
|
|
1299
|
+
WHERE id = p_id;
|
|
1300
|
+
END IF;
|
|
1301
|
+
|
|
1302
|
+
IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_workforce_schedule_templates', p_id, '{}'::jsonb); END IF;
|
|
1303
|
+
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,
|
|
1304
|
+
'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);
|
|
1305
|
+
END $function$;
|
|
1306
|
+
|
|
1307
|
+
CREATE OR REPLACE FUNCTION migration.upsert_wf_schedule_shift(
|
|
1308
|
+
p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb
|
|
1309
|
+
) RETURNS jsonb
|
|
1310
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
|
|
1311
|
+
AS $function$
|
|
1312
|
+
DECLARE
|
|
1313
|
+
v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
|
|
1314
|
+
v_template uuid := (p_row ->> 'template_id')::uuid;
|
|
1315
|
+
v_start time := (p_row ->> 'start_time')::time;
|
|
1316
|
+
v_end time := (p_row ->> 'end_time')::time;
|
|
1317
|
+
v_days smallint[];
|
|
1318
|
+
v_bad smallint[];
|
|
1319
|
+
v_unit uuid;
|
|
1320
|
+
v_created boolean := false;
|
|
1321
|
+
v_standalone boolean := migration._standalone();
|
|
1322
|
+
BEGIN
|
|
1323
|
+
IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
|
|
1324
|
+
IF p_id IS NULL OR v_template IS NULL OR v_start IS NULL OR v_end IS NULL THEN
|
|
1325
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',
|
|
1326
|
+
'reason', 'um turno precisa de id, do template, de in\xEDcio e de fim');
|
|
1327
|
+
END IF;
|
|
1328
|
+
SELECT unit_id INTO v_unit FROM public.plg_workforce_schedule_templates
|
|
1329
|
+
WHERE id = v_template AND tenant_id = p_tenant;
|
|
1330
|
+
IF NOT FOUND THEN
|
|
1331
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',
|
|
1332
|
+
'reason', format('o template %s n\xE3o existe neste tenant', v_template));
|
|
1333
|
+
END IF;
|
|
1334
|
+
|
|
1335
|
+
-- Sem corre\xE7\xE3o silenciosa nos dias da semana. Um 7 solto (ou um 1..7 em vez de
|
|
1336
|
+
-- 0..6) \xE9 um turno que nunca acontece, e um turno que nunca acontece \xE9 uma
|
|
1337
|
+
-- pessoa que ningu\xE9m chamou \u2014 some sem erro nenhum.
|
|
1338
|
+
SELECT coalesce(array_agg(x::smallint ORDER BY x), '{}'::smallint[])
|
|
1339
|
+
INTO v_days
|
|
1340
|
+
FROM jsonb_array_elements_text(coalesce(p_row -> 'weekdays', '[]'::jsonb)) AS t(x);
|
|
1341
|
+
SELECT coalesce(array_agg(d), '{}'::smallint[]) INTO v_bad
|
|
1342
|
+
FROM unnest(v_days) AS d WHERE d < 0 OR d > 6;
|
|
1343
|
+
IF array_length(v_bad, 1) > 0 THEN
|
|
1344
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',
|
|
1345
|
+
'reason', format('dia da semana fora de 0..6 (domingo..s\xE1bado): %s', v_bad::text));
|
|
1346
|
+
END IF;
|
|
1347
|
+
|
|
1348
|
+
IF NOT EXISTS (SELECT 1 FROM public.plg_workforce_schedule_shifts WHERE id = p_id) THEN
|
|
1349
|
+
INSERT INTO public.plg_workforce_schedule_shifts
|
|
1350
|
+
(id, tenant_id, unit_id, template_id, label, start_time, end_time, weekdays, sort_order, metadata)
|
|
1351
|
+
VALUES (p_id, p_tenant, v_unit, v_template,
|
|
1352
|
+
coalesce(nullif(btrim(coalesce(p_row ->> 'label', '')), ''), 'Turno'),
|
|
1353
|
+
v_start, v_end, v_days, coalesce((p_row ->> 'sort_order')::integer, 0), v_meta);
|
|
1354
|
+
v_created := true;
|
|
1355
|
+
ELSE
|
|
1356
|
+
UPDATE public.plg_workforce_schedule_shifts SET
|
|
1357
|
+
unit_id = v_unit,
|
|
1358
|
+
label = coalesce(nullif(btrim(coalesce(p_row ->> 'label', '')), ''), label),
|
|
1359
|
+
start_time = v_start,
|
|
1360
|
+
end_time = v_end,
|
|
1361
|
+
weekdays = v_days,
|
|
1362
|
+
sort_order = CASE WHEN p_row ? 'sort_order' THEN coalesce((p_row ->> 'sort_order')::integer, sort_order) ELSE sort_order END,
|
|
1363
|
+
metadata = migration._merge_meta(metadata, v_meta)
|
|
1364
|
+
WHERE id = p_id;
|
|
1365
|
+
END IF;
|
|
1366
|
+
|
|
1367
|
+
IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_workforce_schedule_shifts', p_id, '{}'::jsonb); END IF;
|
|
1368
|
+
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,
|
|
1369
|
+
'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END,
|
|
1370
|
+
'crosses_midnight', v_end <= v_start);
|
|
1371
|
+
END $function$;
|
|
1372
|
+
|
|
1373
|
+
CREATE OR REPLACE FUNCTION migration.upsert_wf_schedule_requirement(
|
|
1374
|
+
p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb
|
|
1375
|
+
) RETURNS jsonb
|
|
1376
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
|
|
1377
|
+
AS $function$
|
|
1378
|
+
DECLARE
|
|
1379
|
+
v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
|
|
1380
|
+
v_shift uuid := (p_row ->> 'shift_id')::uuid;
|
|
1381
|
+
v_prof text := nullif(btrim(coalesce(p_row ->> 'profession_name', '')), '');
|
|
1382
|
+
v_count integer := coalesce((p_row ->> 'required_count')::integer, 1);
|
|
1383
|
+
v_unit uuid;
|
|
1384
|
+
v_id uuid;
|
|
1385
|
+
v_standalone boolean := migration._standalone();
|
|
1386
|
+
BEGIN
|
|
1387
|
+
IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
|
|
1388
|
+
IF p_id IS NULL OR v_shift IS NULL OR v_prof IS NULL THEN
|
|
1389
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',
|
|
1390
|
+
'reason', 'uma vaga precisa de id, do turno e do nome da fun\xE7\xE3o');
|
|
1391
|
+
END IF;
|
|
1392
|
+
SELECT unit_id INTO v_unit FROM public.plg_workforce_schedule_shifts
|
|
1393
|
+
WHERE id = v_shift AND tenant_id = p_tenant;
|
|
1394
|
+
IF NOT FOUND THEN
|
|
1395
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',
|
|
1396
|
+
'reason', format('o turno %s n\xE3o existe neste tenant', v_shift));
|
|
1397
|
+
END IF;
|
|
1398
|
+
-- Lota\xE7\xE3o zero ou negativa \xE9 vaga que n\xE3o existe; e a constraint recusaria a
|
|
1399
|
+
-- linha inteira mais tarde, sem dizer qual era.
|
|
1400
|
+
IF v_count <= 0 THEN
|
|
1401
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',
|
|
1402
|
+
'reason', format('lota\xE7\xE3o de %L \xE9 %s \u2014 uma vaga precisa de pelo menos uma pessoa', v_prof, v_count));
|
|
1403
|
+
END IF;
|
|
1404
|
+
|
|
1405
|
+
-- Devolve o id da linha que de fato ficou: quando duas vagas do V1 nomeiam a
|
|
1406
|
+
-- mesma fun\xE7\xE3o no mesmo turno, a segunda funde na primeira, e o ledger tem de
|
|
1407
|
+
-- apontar para onde o dado foi parar \u2014 n\xE3o para o id que ela trazia.
|
|
1408
|
+
INSERT INTO public.plg_workforce_schedule_requirements
|
|
1409
|
+
(id, tenant_id, unit_id, shift_id, profession_name, required_count, metadata)
|
|
1410
|
+
VALUES (p_id, p_tenant, v_unit, v_shift, v_prof, v_count, v_meta)
|
|
1411
|
+
ON CONFLICT (shift_id, profession_name) DO UPDATE
|
|
1412
|
+
SET required_count = EXCLUDED.required_count,
|
|
1413
|
+
metadata = migration._merge_meta(public.plg_workforce_schedule_requirements.metadata, EXCLUDED.metadata),
|
|
1414
|
+
updated_at = now()
|
|
1415
|
+
RETURNING id INTO v_id;
|
|
1416
|
+
|
|
1417
|
+
IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_workforce_schedule_requirements', v_id, '{}'::jsonb); END IF;
|
|
1418
|
+
RETURN jsonb_build_object('status', 'migrated', 'target_id', v_id,
|
|
1419
|
+
'reason', CASE WHEN v_id = p_id THEN 'upserted' ELSE 'fundida na vaga que j\xE1 existia para esta fun\xE7\xE3o neste turno' END);
|
|
1420
|
+
END $function$;
|
|
1421
|
+
|
|
1422
|
+
-- No molde do V1, titular vs stand-by \xE9 POSICIONAL \u2014 os primeiros
|
|
1423
|
+
-- \`required_count\` por \`priority\`. Este writer faz essa conta UMA vez, na
|
|
1424
|
+
-- entrada, e grava o resultado em \`role\`. Deixar a regra impl\xEDcita no V2 seria
|
|
1425
|
+
-- repor um comportamento que s\xF3 existe no c\xF3digo de uma tela, e que a pr\xF3xima
|
|
1426
|
+
-- tela a ler a mesma tabela erraria em sil\xEAncio.
|
|
1427
|
+
CREATE OR REPLACE FUNCTION migration.upsert_wf_schedule_template_assignment(
|
|
1428
|
+
p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb
|
|
1429
|
+
) RETURNS jsonb
|
|
1430
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
|
|
1431
|
+
AS $function$
|
|
1432
|
+
DECLARE
|
|
1433
|
+
v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
|
|
1434
|
+
v_shift uuid := (p_row ->> 'shift_id')::uuid;
|
|
1435
|
+
v_prof text := nullif(btrim(coalesce(p_row ->> 'profession_name', '')), '');
|
|
1436
|
+
v_priority integer := coalesce((p_row ->> 'priority')::integer, 0);
|
|
1437
|
+
v_person uuid;
|
|
1438
|
+
v_template uuid;
|
|
1439
|
+
v_unit uuid;
|
|
1440
|
+
v_required integer;
|
|
1441
|
+
v_rank integer;
|
|
1442
|
+
v_role text;
|
|
1443
|
+
v_standalone boolean := migration._standalone();
|
|
1444
|
+
BEGIN
|
|
1445
|
+
IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
|
|
1446
|
+
IF p_id IS NULL OR v_shift IS NULL THEN
|
|
1447
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',
|
|
1448
|
+
'reason', 'uma aloca\xE7\xE3o de molde precisa de id e do turno');
|
|
1449
|
+
END IF;
|
|
1450
|
+
|
|
1451
|
+
-- No molde a pessoa \xE9 sempre \`professionals\` \u2014 o V1 n\xE3o escala funcion\xE1rio
|
|
1452
|
+
-- pelo template \u2014 mas o helper resolve pelo que o metadata disser, e n\xE3o pelo
|
|
1453
|
+
-- que este writer sup\xF5e.
|
|
1454
|
+
v_person := migration._wf_person(p_tenant,
|
|
1455
|
+
CASE WHEN (v_meta #>> '{v1,person_type}') IS NULL
|
|
1456
|
+
THEN migration._deep_set(v_meta, ARRAY['v1', 'person_type'], to_jsonb('professional'::text))
|
|
1457
|
+
ELSE v_meta END);
|
|
1458
|
+
IF v_person IS NULL THEN
|
|
1459
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',
|
|
1460
|
+
'reason', format('aloca\xE7\xE3o de molde cuja pessoa ainda n\xE3o migrou (%s #%s)',
|
|
1461
|
+
coalesce(v_meta #>> '{v1,person_type}', 'professional'),
|
|
1462
|
+
coalesce(v_meta #>> '{v1,person_id}', '<sem id>')));
|
|
1463
|
+
END IF;
|
|
1464
|
+
|
|
1465
|
+
SELECT s.template_id, s.unit_id INTO v_template, v_unit
|
|
1466
|
+
FROM public.plg_workforce_schedule_shifts s
|
|
1467
|
+
WHERE s.id = v_shift AND s.tenant_id = p_tenant;
|
|
1468
|
+
IF NOT FOUND THEN
|
|
1469
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',
|
|
1470
|
+
'reason', format('o turno %s n\xE3o existe neste tenant', v_shift));
|
|
1471
|
+
END IF;
|
|
1472
|
+
|
|
1473
|
+
INSERT INTO public.plg_workforce_schedule_template_assignments
|
|
1474
|
+
(id, tenant_id, unit_id, template_id, shift_id, profession_name, person_id, role, priority, metadata)
|
|
1475
|
+
VALUES (p_id, p_tenant, v_unit, v_template, v_shift, coalesce(v_prof, '\u2014'), v_person,
|
|
1476
|
+
'titular', v_priority, v_meta)
|
|
1477
|
+
ON CONFLICT (shift_id, profession_name, person_id) DO UPDATE
|
|
1478
|
+
SET priority = EXCLUDED.priority,
|
|
1479
|
+
metadata = migration._merge_meta(public.plg_workforce_schedule_template_assignments.metadata, EXCLUDED.metadata),
|
|
1480
|
+
updated_at = now();
|
|
1481
|
+
|
|
1482
|
+
-- A POSI\xC7\xC3O vira PAPEL. Recalculada a cada linha porque a ordem muda quando a
|
|
1483
|
+
-- pr\xF3xima chega: a quarta pessoa de uma vaga de tr\xEAs rebaixa ningu\xE9m, mas
|
|
1484
|
+
-- entra como reserva, e a s\xE9tima tamb\xE9m.
|
|
1485
|
+
SELECT r.required_count INTO v_required
|
|
1486
|
+
FROM public.plg_workforce_schedule_requirements r
|
|
1487
|
+
WHERE r.shift_id = v_shift AND r.profession_name = coalesce(v_prof, '\u2014');
|
|
1488
|
+
|
|
1489
|
+
SELECT rank INTO v_rank FROM (
|
|
1490
|
+
SELECT a.id, row_number() OVER (ORDER BY a.priority, a.created_at, a.id) AS rank
|
|
1491
|
+
FROM public.plg_workforce_schedule_template_assignments a
|
|
1492
|
+
WHERE a.shift_id = v_shift AND a.profession_name = coalesce(v_prof, '\u2014')) s
|
|
1493
|
+
WHERE s.id = p_id;
|
|
1494
|
+
|
|
1495
|
+
-- Sem vaga declarada n\xE3o h\xE1 denominador, e chutar um faria a pessoa virar
|
|
1496
|
+
-- titular ou reserva por acaso. \`titular\` \xE9 o que o V1 mostra nesse caso.
|
|
1497
|
+
v_role := CASE WHEN v_required IS NULL THEN 'titular'
|
|
1498
|
+
WHEN v_rank <= v_required THEN 'titular'
|
|
1499
|
+
ELSE 'standby' END;
|
|
1500
|
+
UPDATE public.plg_workforce_schedule_template_assignments SET role = v_role WHERE id = p_id;
|
|
1501
|
+
|
|
1502
|
+
IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_workforce_schedule_template_assignments', p_id, '{}'::jsonb); END IF;
|
|
1503
|
+
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'role', v_role,
|
|
1504
|
+
'reason', format('posi\xE7\xE3o %s de %s vaga(s) \u2192 %s', v_rank, coalesce(v_required::text, 'sem lota\xE7\xE3o declarada'), v_role));
|
|
1505
|
+
END $function$;
|
|
1506
|
+
|
|
1507
|
+
-- \u2500\u2500 a inst\xE2ncia \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1508
|
+
CREATE OR REPLACE FUNCTION migration.upsert_wf_schedule(
|
|
1509
|
+
p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb
|
|
1510
|
+
) RETURNS jsonb
|
|
1511
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
|
|
1512
|
+
AS $function$
|
|
1513
|
+
DECLARE
|
|
1514
|
+
v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
|
|
1515
|
+
v_date date := (p_row ->> 'schedule_date')::date;
|
|
1516
|
+
v_status_v1 text := nullif(btrim(lower(coalesce(p_row ->> 'status', ''))), '');
|
|
1517
|
+
v_status text;
|
|
1518
|
+
v_published timestamptz := (p_row ->> 'published_at')::timestamptz;
|
|
1519
|
+
v_created boolean := false;
|
|
1520
|
+
v_standalone boolean := migration._standalone();
|
|
1521
|
+
BEGIN
|
|
1522
|
+
IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
|
|
1523
|
+
IF p_id IS NULL OR v_date IS NULL THEN
|
|
1524
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',
|
|
1525
|
+
'reason', 'uma escala precisa de id e do dia a que pertence');
|
|
1526
|
+
END IF;
|
|
1527
|
+
|
|
1528
|
+
v_status := CASE v_status_v1
|
|
1529
|
+
WHEN 'draft' THEN 'draft'
|
|
1530
|
+
WHEN 'published' THEN 'published'
|
|
1531
|
+
WHEN 'closed' THEN 'closed'
|
|
1532
|
+
ELSE NULL END;
|
|
1533
|
+
IF v_status IS NULL THEN
|
|
1534
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',
|
|
1535
|
+
'reason', format('estado de escala desconhecido: %L (o V2 conhece draft, published e closed)', v_status_v1));
|
|
1536
|
+
END IF;
|
|
1537
|
+
|
|
1538
|
+
-- Publicada sem carimbo \xE9 o rascunho do V1 que algu\xE9m marcou como publicado
|
|
1539
|
+
-- sem a a\xE7\xE3o que grava a data. A constraint recusaria; aqui a data de
|
|
1540
|
+
-- publica\xE7\xE3o vira a do pr\xF3prio dia da escala, e o remendo fica escrito.
|
|
1541
|
+
IF v_status <> 'draft' AND v_published IS NULL THEN
|
|
1542
|
+
v_published := v_date::timestamptz;
|
|
1543
|
+
v_meta := migration._deep_set(v_meta, ARRAY['migration', 'published_at_inferred'], to_jsonb(true));
|
|
1544
|
+
END IF;
|
|
1545
|
+
|
|
1546
|
+
IF NOT EXISTS (SELECT 1 FROM public.plg_workforce_schedules WHERE id = p_id) THEN
|
|
1547
|
+
INSERT INTO public.plg_workforce_schedules
|
|
1548
|
+
(id, tenant_id, unit_id, schedule_date, template_id, status, published_at,
|
|
1549
|
+
is_special_day, special_day_label, notes, metadata)
|
|
1550
|
+
VALUES (p_id, p_tenant, (p_row ->> 'unit_id')::uuid, v_date,
|
|
1551
|
+
(p_row ->> 'template_id')::uuid, v_status, v_published,
|
|
1552
|
+
coalesce((p_row ->> 'is_special_day')::boolean, false),
|
|
1553
|
+
p_row ->> 'special_day_label', p_row ->> 'notes', v_meta);
|
|
1554
|
+
v_created := true;
|
|
1555
|
+
ELSE
|
|
1556
|
+
UPDATE public.plg_workforce_schedules SET
|
|
1557
|
+
unit_id = CASE WHEN p_row ? 'unit_id' THEN (p_row ->> 'unit_id')::uuid ELSE unit_id END,
|
|
1558
|
+
schedule_date = v_date,
|
|
1559
|
+
template_id = CASE WHEN p_row ? 'template_id' THEN (p_row ->> 'template_id')::uuid ELSE template_id END,
|
|
1560
|
+
status = v_status,
|
|
1561
|
+
published_at = v_published,
|
|
1562
|
+
is_special_day = CASE WHEN p_row ? 'is_special_day' THEN coalesce((p_row ->> 'is_special_day')::boolean, false) ELSE is_special_day END,
|
|
1563
|
+
special_day_label = CASE WHEN p_row ? 'special_day_label' THEN p_row ->> 'special_day_label' ELSE special_day_label END,
|
|
1564
|
+
notes = CASE WHEN p_row ? 'notes' THEN p_row ->> 'notes' ELSE notes END,
|
|
1565
|
+
metadata = migration._merge_meta(metadata, v_meta)
|
|
1566
|
+
WHERE id = p_id;
|
|
1567
|
+
END IF;
|
|
1568
|
+
|
|
1569
|
+
IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_workforce_schedules', p_id, '{}'::jsonb); END IF;
|
|
1570
|
+
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,
|
|
1571
|
+
'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);
|
|
1572
|
+
END $function$;
|
|
1573
|
+
|
|
1574
|
+
CREATE OR REPLACE FUNCTION migration.upsert_wf_schedule_assignment(
|
|
1575
|
+
p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb
|
|
1576
|
+
) RETURNS jsonb
|
|
1577
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
|
|
1578
|
+
AS $function$
|
|
1579
|
+
DECLARE
|
|
1580
|
+
v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
|
|
1581
|
+
v_schedule uuid := (p_row ->> 'schedule_id')::uuid;
|
|
1582
|
+
v_role_v1 text := nullif(btrim(lower(coalesce(p_row ->> 'role', ''))), '');
|
|
1583
|
+
v_status_v1 text := nullif(btrim(lower(coalesce(p_row ->> 'status', ''))), '');
|
|
1584
|
+
v_role text;
|
|
1585
|
+
v_status text;
|
|
1586
|
+
v_person uuid;
|
|
1587
|
+
v_unit uuid;
|
|
1588
|
+
v_start time := (p_row ->> 'shift_start_time')::time;
|
|
1589
|
+
v_end time := (p_row ->> 'shift_end_time')::time;
|
|
1590
|
+
v_created boolean := false;
|
|
1591
|
+
v_standalone boolean := migration._standalone();
|
|
1592
|
+
BEGIN
|
|
1593
|
+
IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
|
|
1594
|
+
IF p_id IS NULL OR v_schedule IS NULL THEN
|
|
1595
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',
|
|
1596
|
+
'reason', 'uma atribui\xE7\xE3o precisa de id e da escala a que pertence');
|
|
1597
|
+
END IF;
|
|
1598
|
+
SELECT unit_id INTO v_unit FROM public.plg_workforce_schedules
|
|
1599
|
+
WHERE id = v_schedule AND tenant_id = p_tenant;
|
|
1600
|
+
IF NOT FOUND THEN
|
|
1601
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',
|
|
1602
|
+
'reason', format('a escala %s n\xE3o existe neste tenant', v_schedule));
|
|
1603
|
+
END IF;
|
|
1604
|
+
|
|
1605
|
+
v_person := migration._wf_person(p_tenant,
|
|
1606
|
+
CASE WHEN (v_meta #>> '{v1,person_type}') IS NULL
|
|
1607
|
+
THEN migration._deep_set(v_meta, ARRAY['v1', 'person_type'], to_jsonb('professional'::text))
|
|
1608
|
+
ELSE v_meta END);
|
|
1609
|
+
IF v_person IS NULL THEN
|
|
1610
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',
|
|
1611
|
+
'reason', format('atribui\xE7\xE3o de escala cuja pessoa ainda n\xE3o migrou (%s #%s)',
|
|
1612
|
+
coalesce(v_meta #>> '{v1,person_type}', 'professional'),
|
|
1613
|
+
coalesce(v_meta #>> '{v1,person_id}', '<sem id>')));
|
|
1614
|
+
END IF;
|
|
1615
|
+
|
|
1616
|
+
-- Stand-by N\xC3O \xE9 folga: \xE9 o substituto acion\xE1vel, e a pr\xF3pria tela do V1 diz
|
|
1617
|
+
-- ("Substitutos em caso de falta"). Confundir os dois na tradu\xE7\xE3o apagaria a
|
|
1618
|
+
-- fila de quem cobre a falta.
|
|
1619
|
+
v_role := CASE v_role_v1
|
|
1620
|
+
WHEN 'titular' THEN 'titular'
|
|
1621
|
+
WHEN 'standby' THEN 'standby'
|
|
1622
|
+
WHEN 'stand_by' THEN 'standby'
|
|
1623
|
+
WHEN 'stand-by' THEN 'standby'
|
|
1624
|
+
ELSE NULL END;
|
|
1625
|
+
IF v_role IS NULL THEN
|
|
1626
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',
|
|
1627
|
+
'reason', format('papel na vaga desconhecido: %L (o V2 conhece titular e standby)', v_role_v1));
|
|
1628
|
+
END IF;
|
|
1629
|
+
|
|
1630
|
+
v_status := CASE v_status_v1
|
|
1631
|
+
WHEN 'pending' THEN 'pending'
|
|
1632
|
+
WHEN 'notified' THEN 'notified'
|
|
1633
|
+
WHEN 'confirmed' THEN 'confirmed'
|
|
1634
|
+
WHEN 'declined' THEN 'declined'
|
|
1635
|
+
WHEN 'realized' THEN 'realized'
|
|
1636
|
+
ELSE NULL END;
|
|
1637
|
+
IF v_status IS NULL THEN
|
|
1638
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',
|
|
1639
|
+
'reason', format('estado de atribui\xE7\xE3o desconhecido: %L', v_status_v1));
|
|
1640
|
+
END IF;
|
|
1641
|
+
|
|
1642
|
+
IF NOT EXISTS (SELECT 1 FROM public.plg_workforce_schedule_assignments WHERE id = p_id) THEN
|
|
1643
|
+
INSERT INTO public.plg_workforce_schedule_assignments
|
|
1644
|
+
(id, tenant_id, unit_id, schedule_id, shift_id, shift_label, shift_start_time, shift_end_time,
|
|
1645
|
+
profession_name, person_id, role, priority, status, response_token, response_channel,
|
|
1646
|
+
notified_at, responded_at, metadata)
|
|
1647
|
+
VALUES (p_id, p_tenant, v_unit, v_schedule, (p_row ->> 'shift_id')::uuid,
|
|
1648
|
+
p_row ->> 'shift_label', v_start, v_end,
|
|
1649
|
+
p_row ->> 'profession_name', v_person, v_role,
|
|
1650
|
+
coalesce((p_row ->> 'priority')::integer, 0), v_status,
|
|
1651
|
+
-- O token do V1 \xE9 uma credencial viva de um canal que o V2 ainda n\xE3o
|
|
1652
|
+
-- opera. Ele atravessa para n\xE3o perder o rastro de quem foi chamado
|
|
1653
|
+
-- por link, mas quem emitir o pr\xF3ximo convite emite outro.
|
|
1654
|
+
p_row ->> 'response_token', p_row ->> 'response_channel',
|
|
1655
|
+
(p_row ->> 'notified_at')::timestamptz, (p_row ->> 'responded_at')::timestamptz,
|
|
1656
|
+
v_meta);
|
|
1657
|
+
v_created := true;
|
|
1658
|
+
ELSE
|
|
1659
|
+
UPDATE public.plg_workforce_schedule_assignments SET
|
|
1660
|
+
unit_id = v_unit,
|
|
1661
|
+
shift_id = CASE WHEN p_row ? 'shift_id' THEN (p_row ->> 'shift_id')::uuid ELSE shift_id END,
|
|
1662
|
+
shift_label = CASE WHEN p_row ? 'shift_label' THEN p_row ->> 'shift_label' ELSE shift_label END,
|
|
1663
|
+
shift_start_time = coalesce(v_start, shift_start_time),
|
|
1664
|
+
shift_end_time = coalesce(v_end, shift_end_time),
|
|
1665
|
+
profession_name = CASE WHEN p_row ? 'profession_name' THEN p_row ->> 'profession_name' ELSE profession_name END,
|
|
1666
|
+
person_id = v_person,
|
|
1667
|
+
role = v_role,
|
|
1668
|
+
priority = CASE WHEN p_row ? 'priority' THEN coalesce((p_row ->> 'priority')::integer, priority) ELSE priority END,
|
|
1669
|
+
status = v_status,
|
|
1670
|
+
response_token = CASE WHEN p_row ? 'response_token' THEN p_row ->> 'response_token' ELSE response_token END,
|
|
1671
|
+
response_channel = CASE WHEN p_row ? 'response_channel' THEN p_row ->> 'response_channel' ELSE response_channel END,
|
|
1672
|
+
notified_at = CASE WHEN p_row ? 'notified_at' THEN (p_row ->> 'notified_at')::timestamptz ELSE notified_at END,
|
|
1673
|
+
responded_at = CASE WHEN p_row ? 'responded_at' THEN (p_row ->> 'responded_at')::timestamptz ELSE responded_at END,
|
|
1674
|
+
metadata = migration._merge_meta(metadata, v_meta)
|
|
1675
|
+
WHERE id = p_id;
|
|
1676
|
+
END IF;
|
|
1677
|
+
|
|
1678
|
+
IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_workforce_schedule_assignments', p_id, '{}'::jsonb); END IF;
|
|
1679
|
+
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'role', v_role,
|
|
1680
|
+
'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);
|
|
1681
|
+
END $function$;
|
|
1682
|
+
|
|
1683
|
+
CREATE OR REPLACE FUNCTION migration.upsert_wf_assignment_log(
|
|
1684
|
+
p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb
|
|
1685
|
+
) RETURNS jsonb
|
|
1686
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
|
|
1687
|
+
AS $function$
|
|
1688
|
+
DECLARE
|
|
1689
|
+
v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
|
|
1690
|
+
v_assignment uuid := (p_row ->> 'assignment_id')::uuid;
|
|
1691
|
+
v_to text := nullif(btrim(lower(coalesce(p_row ->> 'to_status', ''))), '');
|
|
1692
|
+
v_unit uuid;
|
|
1693
|
+
v_standalone boolean := migration._standalone();
|
|
1694
|
+
BEGIN
|
|
1695
|
+
IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
|
|
1696
|
+
IF p_id IS NULL OR v_assignment IS NULL OR v_to IS NULL THEN
|
|
1697
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',
|
|
1698
|
+
'reason', 'uma linha de hist\xF3rico precisa de id, da atribui\xE7\xE3o e do estado para o qual foi');
|
|
1699
|
+
END IF;
|
|
1700
|
+
SELECT unit_id INTO v_unit FROM public.plg_workforce_schedule_assignments
|
|
1701
|
+
WHERE id = v_assignment AND tenant_id = p_tenant;
|
|
1702
|
+
IF NOT FOUND THEN
|
|
1703
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',
|
|
1704
|
+
'reason', format('a atribui\xE7\xE3o %s n\xE3o existe neste tenant', v_assignment));
|
|
1705
|
+
END IF;
|
|
1706
|
+
|
|
1707
|
+
-- O log \xE9 hist\xF3rico: reoferecer a mesma linha \xE9 replay, e replay n\xE3o reescreve
|
|
1708
|
+
-- o que j\xE1 foi registrado.
|
|
1709
|
+
IF EXISTS (SELECT 1 FROM public.plg_workforce_assignment_logs WHERE id = p_id) THEN
|
|
1710
|
+
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,
|
|
1711
|
+
'reason', 'already in the ledger (append-only)');
|
|
1712
|
+
END IF;
|
|
1713
|
+
|
|
1714
|
+
INSERT INTO public.plg_workforce_assignment_logs
|
|
1715
|
+
(id, tenant_id, unit_id, assignment_id, from_status, to_status, channel, note,
|
|
1716
|
+
changed_by_user_id, metadata, created_at)
|
|
1717
|
+
VALUES (p_id, p_tenant, v_unit, v_assignment,
|
|
1718
|
+
nullif(btrim(lower(coalesce(p_row ->> 'from_status', ''))), ''), v_to,
|
|
1719
|
+
p_row ->> 'channel', p_row ->> 'note',
|
|
1720
|
+
(p_row ->> 'changed_by_user_id')::uuid, v_meta,
|
|
1721
|
+
coalesce((p_row ->> 'created_at')::timestamptz, now()));
|
|
1722
|
+
|
|
1723
|
+
IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_workforce_assignment_logs', p_id, '{}'::jsonb); END IF;
|
|
1724
|
+
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'created');
|
|
1725
|
+
END $function$;
|
|
1726
|
+
|
|
1727
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1728
|
+
-- 9. O DE-PARA
|
|
1729
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1730
|
+
|
|
1731
|
+
-- \`_allow\` \xE9 o construtor oficial: a allowlist tem gatilho de congelamento, e um
|
|
1732
|
+
-- INSERT direto por cima de uma linha \`frozen\` seria recusado. S\xE3o oito
|
|
1733
|
+
-- argumentos \u2014 (source, target, domain, writer, writer_args, financial[],
|
|
1734
|
+
-- columns, notes) \u2014 e \`layer\` \xE9 fixo em 'core-A' l\xE1 dentro.
|
|
1735
|
+
|
|
1736
|
+
-- A allowlist \xE9 CONGELADA (\`trg_allowlist_frozen\`): mudan\xE7a de core-A \xE9
|
|
1737
|
+
-- arquivo de migration, n\xE3o escrita solta \u2014 e este \xE9 o arquivo. Sem a
|
|
1738
|
+
-- escotilha, a PRIMEIRA passada passa (a linha ainda n\xE3o existia) e a
|
|
1739
|
+
-- SEGUNDA falha: \xE9 assim que uma migration deixa de ser replay-safe sem
|
|
1740
|
+
-- ningu\xE9m ver. \`true\` no terceiro argumento: vale s\xF3 nesta transa\xE7\xE3o.
|
|
1741
|
+
SELECT set_config('migration.allowlist_unlock', 'on', true);
|
|
1742
|
+
|
|
1743
|
+
SELECT migration._allow(
|
|
1744
|
+
'time_clock_entries', 'public.plg_workforce_time_entries', 'workforce',
|
|
1745
|
+
'wf_time_entry', '{}'::jsonb, ARRAY[]::text[],
|
|
1746
|
+
jsonb_build_object(
|
|
1747
|
+
'uuid', 'id',
|
|
1748
|
+
'unit_id', 'unit_id@companies:app.units',
|
|
1749
|
+
-- O par polim\xF3rfico. Nenhuma das duas vira coluna: juntas elas viram
|
|
1750
|
+
-- \`person_id\`, resolvido pelo ledger em migration._wf_person.
|
|
1751
|
+
'person_type', 'jsonb:v1.person_type',
|
|
1752
|
+
'person_id', 'jsonb:v1.person_id',
|
|
1753
|
+
'person_name', 'person_name',
|
|
1754
|
+
'checkin_at', 'checkin_at',
|
|
1755
|
+
'checkout_at', 'checkout_at',
|
|
1756
|
+
'checkin_photo_path', 'checkin_photo_path',
|
|
1757
|
+
'checkin_lat', 'checkin_lat',
|
|
1758
|
+
'checkin_lng', 'checkin_lng',
|
|
1759
|
+
'checkin_accuracy_m', 'checkin_accuracy_m',
|
|
1760
|
+
'checkin_ip', 'checkin_ip',
|
|
1761
|
+
'checkin_user_agent', 'checkin_user_agent',
|
|
1762
|
+
'checkin_face_score', 'checkin_face_score',
|
|
1763
|
+
'checkout_photo_path', 'checkout_photo_path',
|
|
1764
|
+
'checkout_lat', 'checkout_lat',
|
|
1765
|
+
'checkout_lng', 'checkout_lng',
|
|
1766
|
+
'checkout_accuracy_m', 'checkout_accuracy_m',
|
|
1767
|
+
'checkout_ip', 'checkout_ip',
|
|
1768
|
+
'checkout_user_agent', 'checkout_user_agent',
|
|
1769
|
+
'checkout_face_score', 'checkout_face_score',
|
|
1770
|
+
'schedule_assignment_id','assignment_id@schedule_assignments',
|
|
1771
|
+
-- O ajuste do V1 n\xE3o vira coluna: vira LINHA em
|
|
1772
|
+
-- plg_workforce_time_adjustments, com autor e motivo.
|
|
1773
|
+
'checkin_adjusted_at', 'checkin_adjusted_at',
|
|
1774
|
+
'checkout_adjusted_at', 'checkout_adjusted_at',
|
|
1775
|
+
'adjustment_reason', 'adjustment_reason',
|
|
1776
|
+
'adjusted_by_user_id', 'adjusted_by_user_id',
|
|
1777
|
+
'adjusted_by_name', 'adjusted_by_name',
|
|
1778
|
+
-- Derivadas e n\xE3o portadas: \`manually_adjusted\` \xE9 a exist\xEAncia de um ajuste,
|
|
1779
|
+
-- \`adjusted_by\`/\`adjusted_at\` s\xE3o redundantes com a linha do ajuste, e
|
|
1780
|
+
-- \`outside_schedule\` \xE9 o resultado do cruzamento \u2014 que no V1 \xE9 \`true\` em 325
|
|
1781
|
+
-- de 325 registros por um defeito de configura\xE7\xE3o, e que no V2 \xE9 a coluna
|
|
1782
|
+
-- \`outside_schedule\` da view v_workforce_day, calculada e n\xE3o guardada.
|
|
1783
|
+
'manually_adjusted', '-',
|
|
1784
|
+
'adjusted_by', '-',
|
|
1785
|
+
'adjusted_at', '-',
|
|
1786
|
+
'outside_schedule', 'jsonb:v1.outside_schedule',
|
|
1787
|
+
'notes', 'notes',
|
|
1788
|
+
-- N\xC3O vira FK: no V1 aponta para a tabela de t\xEDtulos do V1, cuja contraparte
|
|
1789
|
+
-- no V2 n\xE3o \xE9 a mesma coisa (mesma recusa da 046). A refer\xEAncia crua \xE9 o que
|
|
1790
|
+
-- TRAVA o ajuste da linha migrada \u2014 a trigger da se\xE7\xE3o 4 l\xEA exatamente aqui.
|
|
1791
|
+
'payable_invoice_id', 'jsonb:v1.payable_invoice_id'
|
|
1792
|
+
),
|
|
1793
|
+
'A batida. Uma linha do V1 (par entrada/sa\xEDda + colunas de ajuste) vira at\xE9 4 do V2: dois eventos append-only e duas linhas de ajuste. checkout_at nulo atravessa como aus\xEAncia de evento \u2014 \xE9 o ponto em aberto, 20 deles no tenant medido.'
|
|
1794
|
+
);
|
|
1795
|
+
|
|
1796
|
+
SELECT migration._allow(
|
|
1797
|
+
'schedule_templates', 'public.plg_workforce_schedule_templates', 'workforce',
|
|
1798
|
+
'wf_schedule_template', '{}'::jsonb, ARRAY[]::text[],
|
|
1799
|
+
jsonb_build_object(
|
|
1800
|
+
'uuid', 'id',
|
|
1801
|
+
'unit_id', 'unit_id@companies:app.units',
|
|
1802
|
+
'name', 'name',
|
|
1803
|
+
'active', 'is_active'
|
|
1804
|
+
),
|
|
1805
|
+
'O molde da escala. Pr\xE9-requisito duro: sem template o V1 n\xE3o deixa criar escala nenhuma.'
|
|
1806
|
+
);
|
|
1807
|
+
|
|
1808
|
+
SELECT migration._allow(
|
|
1809
|
+
'schedule_template_shifts', 'public.plg_workforce_schedule_shifts', 'workforce',
|
|
1810
|
+
'wf_schedule_shift', '{}'::jsonb, ARRAY[]::text[],
|
|
1811
|
+
jsonb_build_object(
|
|
1812
|
+
'uuid', 'id',
|
|
1813
|
+
'template_id', 'template_id@schedule_templates',
|
|
1814
|
+
'label', 'label',
|
|
1815
|
+
'start_time', 'start_time',
|
|
1816
|
+
'end_time', 'end_time',
|
|
1817
|
+
'weekdays', 'weekdays',
|
|
1818
|
+
'sort_order', 'sort_order'
|
|
1819
|
+
),
|
|
1820
|
+
'O turno do molde. weekdays \xE9 0..6 (domingo..s\xE1bado); qualquer valor fora disso \xE9 quarentena, porque um turno que nunca acontece some sem erro.'
|
|
1821
|
+
);
|
|
1822
|
+
|
|
1823
|
+
SELECT migration._allow(
|
|
1824
|
+
'schedule_template_requirements', 'public.plg_workforce_schedule_requirements', 'workforce',
|
|
1825
|
+
'wf_schedule_requirement', '{}'::jsonb, ARRAY[]::text[],
|
|
1826
|
+
jsonb_build_object(
|
|
1827
|
+
'uuid', 'id',
|
|
1828
|
+
'shift_id', 'shift_id@schedule_template_shifts',
|
|
1829
|
+
'profession_name', 'profession_name',
|
|
1830
|
+
'required_count', 'required_count'
|
|
1831
|
+
),
|
|
1832
|
+
'A lota\xE7\xE3o da fun\xE7\xE3o no turno \u2014 o "(1/1)" da tela, e o denominador que transforma posi\xE7\xE3o em titular/stand-by.'
|
|
1833
|
+
);
|
|
1834
|
+
|
|
1835
|
+
SELECT migration._allow(
|
|
1836
|
+
'schedule_template_assignments', 'public.plg_workforce_schedule_template_assignments', 'workforce',
|
|
1837
|
+
'wf_schedule_template_assignment', '{}'::jsonb, ARRAY[]::text[],
|
|
1838
|
+
jsonb_build_object(
|
|
1839
|
+
'uuid', 'id',
|
|
1840
|
+
'template_id', 'template_id@schedule_templates',
|
|
1841
|
+
'shift_id', 'shift_id@schedule_template_shifts',
|
|
1842
|
+
'profession_name', 'profession_name',
|
|
1843
|
+
'professional_id', 'jsonb:v1.person_id',
|
|
1844
|
+
'priority', 'priority'
|
|
1845
|
+
),
|
|
1846
|
+
'A aloca\xE7\xE3o do molde. No V1 titular \xD7 stand-by \xE9 POSICIONAL (os primeiros required_count por priority) e vive s\xF3 no c\xF3digo da tela; o writer faz a conta uma vez e grava em \`role\`.'
|
|
1847
|
+
);
|
|
1848
|
+
|
|
1849
|
+
SELECT migration._allow(
|
|
1850
|
+
'schedules', 'public.plg_workforce_schedules', 'workforce',
|
|
1851
|
+
'wf_schedule', '{}'::jsonb, ARRAY[]::text[],
|
|
1852
|
+
jsonb_build_object(
|
|
1853
|
+
'uuid', 'id',
|
|
1854
|
+
'unit_id', 'unit_id@companies:app.units',
|
|
1855
|
+
'schedule_date', 'schedule_date',
|
|
1856
|
+
'template_id', 'template_id@schedule_templates',
|
|
1857
|
+
'status', 'status',
|
|
1858
|
+
'published_at', 'published_at',
|
|
1859
|
+
'is_special_day', 'is_special_day',
|
|
1860
|
+
'special_day_label', 'special_day_label',
|
|
1861
|
+
'notes', 'notes'
|
|
1862
|
+
),
|
|
1863
|
+
'A escala do dia. Hom\xF4nima da \`public.schedules\` do V2, que \xE9 OUTRA coisa (a oferta de hor\xE1rio do spine) \u2014 \xE9 a raz\xE3o do prefixo plg_workforce_.'
|
|
1864
|
+
);
|
|
1865
|
+
|
|
1866
|
+
SELECT migration._allow(
|
|
1867
|
+
'schedule_assignments', 'public.plg_workforce_schedule_assignments', 'workforce',
|
|
1868
|
+
'wf_schedule_assignment', '{}'::jsonb, ARRAY[]::text[],
|
|
1869
|
+
jsonb_build_object(
|
|
1870
|
+
'uuid', 'id',
|
|
1871
|
+
'schedule_id', 'schedule_id@schedules',
|
|
1872
|
+
'shift_id', 'shift_id@schedule_template_shifts',
|
|
1873
|
+
-- O SNAPSHOT do V1 atravessa como snapshot. Reconstru\xED-lo a partir do turno
|
|
1874
|
+
-- atual do molde reescreveria a escala de anteontem quando o molde mudasse.
|
|
1875
|
+
'shift_label', 'shift_label',
|
|
1876
|
+
'shift_start_time', 'shift_start_time',
|
|
1877
|
+
'shift_end_time', 'shift_end_time',
|
|
1878
|
+
'profession_name', 'profession_name',
|
|
1879
|
+
'professional_id', 'jsonb:v1.person_id',
|
|
1880
|
+
'role', 'role',
|
|
1881
|
+
'priority', 'priority',
|
|
1882
|
+
'status', 'status',
|
|
1883
|
+
'response_token', 'response_token',
|
|
1884
|
+
'response_channel', 'response_channel',
|
|
1885
|
+
'notified_at', 'notified_at',
|
|
1886
|
+
'responded_at', 'responded_at'
|
|
1887
|
+
),
|
|
1888
|
+
'A atribui\xE7\xE3o do dia. Stand-by n\xE3o \xE9 folga: \xE9 o substituto acion\xE1vel ("Substitutos em caso de falta"), e confundir os dois apagaria a fila de quem cobre a falta.'
|
|
1889
|
+
);
|
|
1890
|
+
|
|
1891
|
+
SELECT migration._allow(
|
|
1892
|
+
'schedule_assignment_logs', 'public.plg_workforce_assignment_logs', 'workforce',
|
|
1893
|
+
'wf_assignment_log', '{}'::jsonb, ARRAY[]::text[],
|
|
1894
|
+
jsonb_build_object(
|
|
1895
|
+
'uuid', 'id',
|
|
1896
|
+
'assignment_id', 'assignment_id@schedule_assignments',
|
|
1897
|
+
'from_status', 'from_status',
|
|
1898
|
+
'to_status', 'to_status',
|
|
1899
|
+
'channel', 'channel',
|
|
1900
|
+
'note', 'note',
|
|
1901
|
+
'changed_by_user_id','changed_by_user_id',
|
|
1902
|
+
'created_at', 'created_at'
|
|
1903
|
+
),
|
|
1904
|
+
'O hist\xF3rico da atribui\xE7\xE3o: a \xFAnica parte do modelo de escala do V1 que j\xE1 nasceu append-only, e que atravessa como \xE9.'
|
|
1905
|
+
);
|
|
1906
|
+
|
|
1907
|
+
-- As duas que N\xC3O atravessam, e a raz\xE3o escrita onde a pr\xF3xima onda vai
|
|
1908
|
+
-- procurar. "Grid" aqui \xE9 grade de disponibilidade de AGENDAMENTO \u2014 quando um
|
|
1909
|
+
-- hor\xE1rio pode ser marcado \u2014, n\xE3o escala de trabalho. Port\xE1-las para dentro do
|
|
1910
|
+
-- RH criaria a terceira tabela de hor\xE1rio do cluster e colocaria a agenda dentro
|
|
1911
|
+
-- do plugin que o D3 mandou justamente separar dela.
|
|
1912
|
+
-- \`WHERE tenant_id IS NULL\` no \xE1rbitro: a 073 da espinha trocou a PK de
|
|
1913
|
+
-- \`migration.excluded_tables\` por dois \xEDndices \xFAnicos PARCIAIS \u2014 um global e
|
|
1914
|
+
-- um por inquilino \u2014, porque "n\xE3o trazer esta tabela" passou a ser decis\xE3o de
|
|
1915
|
+
-- cliente e n\xE3o do cluster. \xCDndice parcial s\xF3 serve de \xE1rbitro de ON CONFLICT
|
|
1916
|
+
-- quando o comando repete o mesmo recorte. Esta decis\xE3o \xE9 global.
|
|
1917
|
+
INSERT INTO migration.excluded_tables (source_table, reason) VALUES
|
|
1918
|
+
('schedule_grids',
|
|
1919
|
+
'Apesar do nome, \xE9 grade de disponibilidade de AGENDAMENTO (quando um hor\xE1rio pode ser marcado), n\xE3o escala de trabalho. O destino natural \xE9 a agenda (public.schedules, kind working_hours), n\xE3o o RH.'),
|
|
1920
|
+
('schedule_grid_blocks',
|
|
1921
|
+
'Bloco da grade de agendamento \u2014 mesma raz\xE3o de schedule_grids. Escala de trabalho \xE9 plg_workforce_schedule_*.')
|
|
1922
|
+
ON CONFLICT (source_table) WHERE tenant_id IS NULL DO UPDATE SET reason = EXCLUDED.reason;
|
|
1923
|
+
|
|
1924
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1925
|
+
-- 10. A amarra\xE7\xE3o com o financeiro, quando o financeiro existe
|
|
1926
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1927
|
+
--
|
|
1928
|
+
-- Hora de trabalho vira dinheiro, e o V2 j\xE1 tem onde: \`plg_financial_invoices\`
|
|
1929
|
+
-- para o t\xEDtulo, \`plg_financial_chart_of_accounts\` e \`plg_financial_cost_centers\`
|
|
1930
|
+
-- para a classifica\xE7\xE3o. O que a jornada N\xC3O pode fazer \xE9 exigir o financeiro:
|
|
1931
|
+
-- o plugin \xE9 instal\xE1vel sozinho, e uma FK inline recusaria o arquivo inteiro num
|
|
1932
|
+
-- cluster que n\xE3o ligou o m\xF3dulo. Declarar a FK aqui, condicionada ao destino,
|
|
1933
|
+
-- \xE9 o que d\xE1 integridade onde ela \xE9 poss\xEDvel sem transformar prefer\xEAncia em
|
|
1934
|
+
-- depend\xEAncia.
|
|
1935
|
+
--
|
|
1936
|
+
-- Todas ON DELETE SET NULL: apagar um centro de custo n\xE3o pode apagar a batida
|
|
1937
|
+
-- de ningu\xE9m. O v\xEDnculo some, a hora fica.
|
|
1938
|
+
DO $$
|
|
1939
|
+
DECLARE
|
|
1940
|
+
r record;
|
|
1941
|
+
BEGIN
|
|
1942
|
+
FOR r IN
|
|
1943
|
+
SELECT * FROM (VALUES
|
|
1944
|
+
('plg_workforce_time_entries', 'chart_account_id', 'plg_financial_chart_of_accounts'),
|
|
1945
|
+
('plg_workforce_time_entries', 'cost_center_id', 'plg_financial_cost_centers'),
|
|
1946
|
+
('plg_workforce_time_entries', 'payable_invoice_id', 'plg_financial_invoices'),
|
|
1947
|
+
('plg_workforce_schedule_shifts', 'chart_account_id', 'plg_financial_chart_of_accounts'),
|
|
1948
|
+
('plg_workforce_schedule_shifts', 'cost_center_id', 'plg_financial_cost_centers'),
|
|
1949
|
+
('plg_workforce_periods', 'chart_account_id', 'plg_financial_chart_of_accounts'),
|
|
1950
|
+
('plg_workforce_periods', 'cost_center_id', 'plg_financial_cost_centers'),
|
|
1951
|
+
('plg_workforce_periods', 'payable_invoice_id', 'plg_financial_invoices')
|
|
1952
|
+
) AS t(tbl, col, target)
|
|
1953
|
+
LOOP
|
|
1954
|
+
CONTINUE WHEN to_regclass('public.' || r.target) IS NULL;
|
|
1955
|
+
CONTINUE WHEN EXISTS (SELECT 1 FROM pg_constraint
|
|
1956
|
+
WHERE conname = r.tbl || '_' || r.col || '_fkey'
|
|
1957
|
+
AND conrelid = ('public.' || r.tbl)::regclass);
|
|
1958
|
+
EXECUTE format(
|
|
1959
|
+
'ALTER TABLE public.%I ADD CONSTRAINT %I FOREIGN KEY (%I) REFERENCES public.%I(id) ON DELETE SET NULL',
|
|
1960
|
+
r.tbl, r.tbl || '_' || r.col || '_fkey', r.col, r.target);
|
|
1961
|
+
END LOOP;
|
|
1962
|
+
END $$;
|
|
1963
|
+
|
|
1964
|
+
SELECT set_config('migration.allowlist_unlock', 'off', true);
|
|
1965
|
+
`;
|
|
1966
|
+
var MIGRATION_001_A_PROVA_DA_BATIDA_TEM_BYTES = `-- ---------------------------------------------------------------------------
|
|
1967
|
+
-- 001_a_prova_da_batida_tem_bytes.sql \u2014 a foto do ponto atravessa, e n\xE3o s\xF3 o
|
|
1968
|
+
-- caminho dela.
|
|
1969
|
+
--
|
|
1970
|
+
-- A 000 escreveu, no coment\xE1rio de \`evidence\`:
|
|
1971
|
+
--
|
|
1972
|
+
-- "O que n\xE3o pode \xE9 perder: no V1 s\xE3o \`photo_path\`, \`lat\`, \`lng\`,
|
|
1973
|
+
-- \`accuracy_m\`, \`ip\`, \`user_agent\` e \`face_score\`."
|
|
1974
|
+
--
|
|
1975
|
+
-- Sete campos. Seis atravessaram. O s\xE9timo \u2014 o \xFAnico que uma pessoa reconhece \u2014
|
|
1976
|
+
-- atravessou como TEXTO: \`8/2026-07-05/professional-15-1783278419303.jpg\`,
|
|
1977
|
+
-- que \xE9 um objeto num bucket PRIVADO de outro projeto Supabase, o do V1.
|
|
1978
|
+
--
|
|
1979
|
+
-- Medido em 18/09/2026 no tenant Pertinho do C\xE9u: 1.320 das 1.361 batidas t\xEAm
|
|
1980
|
+
-- esse caminho preenchido, e nenhuma das 1.320 imagens \xE9 alcan\xE7\xE1vel daqui. No
|
|
1981
|
+
-- dia em que o V1 sair do ar elas deixam de existir em qualquer lugar.
|
|
1982
|
+
--
|
|
1983
|
+
-- \xC9 pior que uma coluna vazia, porque parece cheia. \`evidence.photo_path\` diz
|
|
1984
|
+
-- que houve captura e sugere que a captura est\xE1 guardada. A tela de Registros de
|
|
1985
|
+
-- Ponto, por isso mesmo, escondia a se\xE7\xE3o de evid\xEAncia inteira \u2014 o coment\xE1rio
|
|
1986
|
+
-- dela dizia que a evid\xEAncia estava "hoje vazia", o que j\xE1 n\xE3o era verdade.
|
|
1987
|
+
--
|
|
1988
|
+
-- Este arquivo declara para o motor de arquivos da 054 (\`migration.asset_kinds\`)
|
|
1989
|
+
-- que a foto de batida \xE9 uma esp\xE9cie com bytes para transportar: de que bucket
|
|
1990
|
+
-- ela sai, para qual vai, o que falta atravessar, e quem reescreve o ponteiro.
|
|
1991
|
+
-- Mover os bytes \xE9 do fayz-etl (\`fayz_etl/blobs.py\`); a regra \xE9 daqui.
|
|
1992
|
+
--
|
|
1993
|
+
-- Idempotente e replay-safe.
|
|
1994
|
+
-- ---------------------------------------------------------------------------
|
|
1995
|
+
|
|
1996
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1997
|
+
-- \xA71 O bucket, PRIVADO
|
|
1998
|
+
--
|
|
1999
|
+
-- N\xE3o \xE9 escolha de estilo: \xE9 biometria facial de um funcion\xE1rio, com hora e
|
|
2000
|
+
-- coordenada ao lado. O V1 j\xE1 a guarda em bucket privado e serve por URL
|
|
2001
|
+
-- assinada de curta dura\xE7\xE3o (edge function \`time-clock-photo-url\`), e o V2 n\xE3o
|
|
2002
|
+
-- pode rebaixar isso \u2014 um bucket p\xFAblico transformaria "a cara e o hor\xE1rio de
|
|
2003
|
+
-- quem trabalha aqui" em algo que basta um caminho para ver.
|
|
2004
|
+
--
|
|
2005
|
+
-- A leitura \xE9 por pasta de tenant, o mesmo escopo que o resto da plataforma usa.
|
|
2006
|
+
-- Com ele, \`createSignedUrl\` do lado do app funciona para quem \xE9 do tenant e
|
|
2007
|
+
-- falha para quem n\xE3o \xE9, sem RPC no meio.
|
|
2008
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2009
|
+
|
|
2010
|
+
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
|
2011
|
+
VALUES ('workforce-punch-photos', 'workforce-punch-photos', false, 5242880,
|
|
2012
|
+
ARRAY['image/jpeg', 'image/png', 'image/webp'])
|
|
2013
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
2014
|
+
public = EXCLUDED.public,
|
|
2015
|
+
file_size_limit = EXCLUDED.file_size_limit,
|
|
2016
|
+
allowed_mime_types = EXCLUDED.allowed_mime_types;
|
|
2017
|
+
|
|
2018
|
+
DROP POLICY IF EXISTS plg_workforce_punch_photos_read ON storage.objects;
|
|
2019
|
+
DROP POLICY IF EXISTS plg_workforce_punch_photos_insert ON storage.objects;
|
|
2020
|
+
DROP POLICY IF EXISTS plg_workforce_punch_photos_update ON storage.objects;
|
|
2021
|
+
DROP POLICY IF EXISTS plg_workforce_punch_photos_delete ON storage.objects;
|
|
2022
|
+
|
|
2023
|
+
-- Sem \`anon\`. A prova de uma jornada n\xE3o se l\xEA deslogado.
|
|
2024
|
+
CREATE POLICY plg_workforce_punch_photos_read ON storage.objects
|
|
2025
|
+
FOR SELECT TO authenticated
|
|
2026
|
+
USING (
|
|
2027
|
+
bucket_id = 'workforce-punch-photos'
|
|
2028
|
+
AND (storage.foldername(name))[1] IN (SELECT public.user_tenant_ids()::text)
|
|
2029
|
+
);
|
|
2030
|
+
|
|
2031
|
+
CREATE POLICY plg_workforce_punch_photos_insert ON storage.objects
|
|
2032
|
+
FOR INSERT TO authenticated
|
|
2033
|
+
WITH CHECK (
|
|
2034
|
+
bucket_id = 'workforce-punch-photos'
|
|
2035
|
+
AND (storage.foldername(name))[1] IN (SELECT public.user_tenant_ids()::text)
|
|
2036
|
+
);
|
|
2037
|
+
|
|
2038
|
+
CREATE POLICY plg_workforce_punch_photos_update ON storage.objects
|
|
2039
|
+
FOR UPDATE TO authenticated
|
|
2040
|
+
USING (
|
|
2041
|
+
bucket_id = 'workforce-punch-photos'
|
|
2042
|
+
AND (storage.foldername(name))[1] IN (SELECT public.user_tenant_ids()::text)
|
|
2043
|
+
)
|
|
2044
|
+
WITH CHECK (
|
|
2045
|
+
bucket_id = 'workforce-punch-photos'
|
|
2046
|
+
AND (storage.foldername(name))[1] IN (SELECT public.user_tenant_ids()::text)
|
|
2047
|
+
);
|
|
2048
|
+
|
|
2049
|
+
-- DELETE fica de fora de prop\xF3sito: apagar a foto de uma batida \xE9 apagar prova.
|
|
2050
|
+
-- Quem precisar (pedido de exclus\xE3o LGPD) passa pelo \`service_role\`, que \xE9 um
|
|
2051
|
+
-- ato deliberado e registrado, n\xE3o um clique.
|
|
2052
|
+
|
|
2053
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2054
|
+
-- \xA72 A esp\xE9cie
|
|
2055
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2056
|
+
|
|
2057
|
+
SELECT public.register_kind('document', 'punch_photo', 'workforce', ARRAY['active'], NULL,
|
|
2058
|
+
'A foto que o totem capturou na batida \u2014 prova de quem bateu o ponto, junto com GPS e score facial');
|
|
2059
|
+
|
|
2060
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2061
|
+
-- \xA73 O que falta, e o ponteiro
|
|
2062
|
+
--
|
|
2063
|
+
-- Todo o resto deste arquivo depende da 054 do base. Num pool que ainda n\xE3o a
|
|
2064
|
+
-- aplicou, o plugin instala sem o transporte de bytes em vez de recusar a
|
|
2065
|
+
-- instala\xE7\xE3o: o ponto funciona sem foto, e a foto sem o ponto n\xE3o \xE9 nada.
|
|
2066
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2067
|
+
|
|
2068
|
+
DO $do$
|
|
2069
|
+
BEGIN
|
|
2070
|
+
IF to_regproc('migration.register_asset_kind(text,text,text,text,text,text)') IS NULL THEN
|
|
2071
|
+
RAISE NOTICE 'plugin-workforce 001: migration.register_asset_kind ausente (base < 054); a foto da batida fica sem transporte';
|
|
2072
|
+
RETURN;
|
|
2073
|
+
END IF;
|
|
2074
|
+
|
|
2075
|
+
CREATE OR REPLACE VIEW migration.v_asset_backlog_punch_photo AS
|
|
2076
|
+
SELECT e.tenant_id,
|
|
2077
|
+
'time_entry'::text AS subject_type,
|
|
2078
|
+
e.id AS subject_id,
|
|
2079
|
+
e.person_id,
|
|
2080
|
+
e.evidence ->> 'photo_path' AS source_path,
|
|
2081
|
+
-- O bucket do V1 \xE9 privado: n\xE3o h\xE1 URL para baixar sem chave, e o
|
|
2082
|
+
-- transporte usa a chave de servi\xE7o da origem.
|
|
2083
|
+
NULL::text AS source_url,
|
|
2084
|
+
-- Uma foto por batida, nomeada pela batida. O caminho do V1 tem epoch no
|
|
2085
|
+
-- nome e n\xE3o \xE9 reconstru\xEDvel a partir do destino; o do V2 \xE9 deriv\xE1vel de
|
|
2086
|
+
-- qualquer lado, o que \xE9 o que faz o rerun sobrescrever em vez de dobrar.
|
|
2087
|
+
e.tenant_id::text || '/' || e.id::text || '.jpg' AS target_path,
|
|
2088
|
+
coalesce(e.person_name, 'sem nome') || ' \xB7 '
|
|
2089
|
+
|| to_char(e.punched_at, 'DD/MM/YYYY HH24:MI') AS title
|
|
2090
|
+
FROM public.plg_workforce_time_entries e
|
|
2091
|
+
-- \`photo_path\` \xE9 a proced\xEAncia (o caminho no V1, que fica); \`photo\` \xE9 o
|
|
2092
|
+
-- ponteiro do V2. Ter o primeiro e n\xE3o o segundo \xC9 a defini\xE7\xE3o de "falta
|
|
2093
|
+
-- atravessar", e escrever o segundo tira a linha desta lista.
|
|
2094
|
+
WHERE e.evidence ? 'photo_path'
|
|
2095
|
+
AND NOT (e.evidence ? 'photo');
|
|
2096
|
+
|
|
2097
|
+
COMMENT ON VIEW migration.v_asset_backlog_punch_photo IS
|
|
2098
|
+
'Batidas cuja foto ainda s\xF3 existe no bucket privado do V1 (001). A condi\xE7\xE3o \xC9 o ponteiro: escrever evidence.photo apaga a linha desta lista.';
|
|
2099
|
+
|
|
2100
|
+
CREATE OR REPLACE FUNCTION migration._point_punch_photo(p_tenant uuid, p_document uuid, p_row jsonb)
|
|
2101
|
+
RETURNS void
|
|
2102
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
|
|
2103
|
+
AS $fn$
|
|
2104
|
+
DECLARE v_entry uuid := (p_row ->> 'subject_id')::uuid;
|
|
2105
|
+
BEGIN
|
|
2106
|
+
IF v_entry IS NULL THEN
|
|
2107
|
+
RAISE EXCEPTION '_point_punch_photo: preciso de subject_id (a batida)' USING ERRCODE = '22023';
|
|
2108
|
+
END IF;
|
|
2109
|
+
-- UPDATE numa tabela append-only: passa porque \`attach_asset\` liga
|
|
2110
|
+
-- \`migration.writer\`, que \xE9 a mesma escotilha que o writer da batida usa.
|
|
2111
|
+
-- \`photo_path\` N\xC3O \xE9 tocado \u2014 ele \xE9 de onde a imagem veio, e continua sendo.
|
|
2112
|
+
UPDATE public.plg_workforce_time_entries
|
|
2113
|
+
SET evidence = migration._deep_set(coalesce(evidence, '{}'::jsonb), ARRAY['photo'],
|
|
2114
|
+
jsonb_build_object(
|
|
2115
|
+
'bucket', p_row ->> 'bucket',
|
|
2116
|
+
'path', p_row ->> 'target_path',
|
|
2117
|
+
'document_id', p_document)),
|
|
2118
|
+
updated_at = now()
|
|
2119
|
+
WHERE id = v_entry AND tenant_id = p_tenant;
|
|
2120
|
+
END $fn$;
|
|
2121
|
+
|
|
2122
|
+
PERFORM migration.register_asset_kind(
|
|
2123
|
+
'punch_photo', 'time-clock-photos', 'workforce-punch-photos',
|
|
2124
|
+
'v_asset_backlog_punch_photo', 'migration._point_punch_photo',
|
|
2125
|
+
'A foto do totem. Bucket privado nas duas pontas: \xE9 biometria facial com hora e coordenada ao lado, e o V1 j\xE1 a tratava assim.');
|
|
2126
|
+
END $do$;
|
|
2127
|
+
`;
|
|
2128
|
+
var MIGRATION_002_O_GUARDA_DA_FOTO_OLHAVA_O_NOME_ERRADO = `-- ---------------------------------------------------------------------------
|
|
2129
|
+
-- 002_o_guarda_da_foto_olhava_o_nome_errado.sql \u2014 o \xA73 da 001 nunca rodou.
|
|
2130
|
+
--
|
|
2131
|
+
-- A 001 protege o pr\xF3prio corpo com um guard, porque tudo nela depois do \xA72
|
|
2132
|
+
-- depende da 054 do base:
|
|
2133
|
+
--
|
|
2134
|
+
-- IF to_regproc('migration.register_asset_kind(text,text,text,text,text,text)')
|
|
2135
|
+
-- IS NULL THEN RETURN; END IF;
|
|
2136
|
+
--
|
|
2137
|
+
-- **\`to_regproc\` recebe um NOME, n\xE3o uma assinatura.** Com a lista de tipos
|
|
2138
|
+
-- entre par\xEAnteses ela devolve NULL \u2014 e n\xE3o levanta erro, que \xE9 exatamente o que
|
|
2139
|
+
-- torna isso invis\xEDvel: \`to_regproc\` foi feita para responder "n\xE3o existe" em vez
|
|
2140
|
+
-- de estourar. Quem l\xEA a linha l\xEA uma checagem; o Postgres l\xEA uma pergunta
|
|
2141
|
+
-- malformada e responde "n\xE3o existe" \xE0s duas.
|
|
2142
|
+
--
|
|
2143
|
+
-- Resultado medido no cluster em 19/09/2026, logo depois de aplicar a 001: o
|
|
2144
|
+
-- bucket \`workforce-punch-photos\` existe (\xA71, fora do bloco), a esp\xE9cie
|
|
2145
|
+
-- \`punch_photo\` est\xE1 registrada (\xA72, fora do bloco), e do \xA73 n\xE3o existe NADA \u2014
|
|
2146
|
+
-- nem a view de backlog, nem \`_point_punch_photo\`, nem a linha em
|
|
2147
|
+
-- \`migration.asset_kinds\`. A migration "passou". A foto continuou sem
|
|
2148
|
+
-- transporte, e o \`mig-assets\` teria relatado s\xF3 os 63 avatares como se fosse o
|
|
2149
|
+
-- backlog inteiro.
|
|
2150
|
+
--
|
|
2151
|
+
-- Quem quer assinatura \xE9 \`to_regprocedure\`. \xC9 a \xFAnica diferen\xE7a deste arquivo.
|
|
2152
|
+
--
|
|
2153
|
+
-- \u2500\u2500 Por que um arquivo novo, e n\xE3o uma corre\xE7\xE3o na 001 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2154
|
+
--
|
|
2155
|
+
-- A 001 j\xE1 est\xE1 no ledger deste cluster. \`fayz db apply\` trata checksum
|
|
2156
|
+
-- diferente num arquivo aplicado como PARADA \u2014 e est\xE1 certo: um app que j\xE1 rodou
|
|
2157
|
+
-- a 001 n\xE3o pode descobrir que os bytes dela mudaram. Corrigir para frente custa
|
|
2158
|
+
-- este arquivo; editar a 001 custaria o apply de todo app que j\xE1 a tomou.
|
|
2159
|
+
--
|
|
2160
|
+
-- Idempotente e replay-safe.
|
|
2161
|
+
-- ---------------------------------------------------------------------------
|
|
2162
|
+
|
|
2163
|
+
DO $do$
|
|
2164
|
+
BEGIN
|
|
2165
|
+
-- \`to_regprocedure\`, que \xE9 quem entende \`(text,text,\u2026)\`. A mesma armadilha est\xE1
|
|
2166
|
+
-- na 152 do base (\`to_regproc('app.scaffold_table(text,text,text,boolean,text)')\`),
|
|
2167
|
+
-- e \xE9 por isso que a checagem aqui \xE9 pelo NOME quando basta o nome: um guard que
|
|
2168
|
+
-- mente \xE9 pior que nenhum guard, porque o arquivo sai dizendo que aplicou.
|
|
2169
|
+
IF to_regprocedure('migration.register_asset_kind(text,text,text,text,text,text)') IS NULL THEN
|
|
2170
|
+
RAISE NOTICE 'plugin-workforce 002: migration.register_asset_kind ausente (base < 054); a foto da batida fica sem transporte';
|
|
2171
|
+
RETURN;
|
|
2172
|
+
END IF;
|
|
2173
|
+
|
|
2174
|
+
CREATE OR REPLACE VIEW migration.v_asset_backlog_punch_photo AS
|
|
2175
|
+
SELECT e.tenant_id,
|
|
2176
|
+
'time_entry'::text AS subject_type,
|
|
2177
|
+
e.id AS subject_id,
|
|
2178
|
+
e.person_id,
|
|
2179
|
+
e.evidence ->> 'photo_path' AS source_path,
|
|
2180
|
+
-- O bucket do V1 \xE9 privado: n\xE3o h\xE1 URL para baixar sem chave, e o
|
|
2181
|
+
-- transporte usa a chave de servi\xE7o da origem.
|
|
2182
|
+
NULL::text AS source_url,
|
|
2183
|
+
-- Uma foto por batida, nomeada pela batida. O caminho do V1 tem epoch no
|
|
2184
|
+
-- nome e n\xE3o \xE9 reconstru\xEDvel a partir do destino; o do V2 \xE9 deriv\xE1vel de
|
|
2185
|
+
-- qualquer lado, o que \xE9 o que faz o rerun sobrescrever em vez de dobrar.
|
|
2186
|
+
e.tenant_id::text || '/' || e.id::text || '.jpg' AS target_path,
|
|
2187
|
+
coalesce(e.person_name, 'sem nome') || ' \xB7 '
|
|
2188
|
+
|| to_char(e.punched_at, 'DD/MM/YYYY HH24:MI') AS title
|
|
2189
|
+
FROM public.plg_workforce_time_entries e
|
|
2190
|
+
-- \`photo_path\` \xE9 a proced\xEAncia (o caminho no V1, que fica); \`photo\` \xE9 o
|
|
2191
|
+
-- ponteiro do V2. Ter o primeiro e n\xE3o o segundo \xC9 a defini\xE7\xE3o de "falta
|
|
2192
|
+
-- atravessar", e escrever o segundo tira a linha desta lista.
|
|
2193
|
+
WHERE e.evidence ? 'photo_path'
|
|
2194
|
+
AND NOT (e.evidence ? 'photo');
|
|
2195
|
+
|
|
2196
|
+
COMMENT ON VIEW migration.v_asset_backlog_punch_photo IS
|
|
2197
|
+
'Batidas cuja foto ainda s\xF3 existe no bucket privado do V1 (001/002). A condi\xE7\xE3o \xC9 o ponteiro: escrever evidence.photo apaga a linha desta lista.';
|
|
2198
|
+
|
|
2199
|
+
CREATE OR REPLACE FUNCTION migration._point_punch_photo(p_tenant uuid, p_document uuid, p_row jsonb)
|
|
2200
|
+
RETURNS void
|
|
2201
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
|
|
2202
|
+
AS $fn$
|
|
2203
|
+
DECLARE v_entry uuid := (p_row ->> 'subject_id')::uuid;
|
|
2204
|
+
BEGIN
|
|
2205
|
+
IF v_entry IS NULL THEN
|
|
2206
|
+
RAISE EXCEPTION '_point_punch_photo: preciso de subject_id (a batida)' USING ERRCODE = '22023';
|
|
2207
|
+
END IF;
|
|
2208
|
+
-- UPDATE numa tabela append-only: passa porque \`attach_asset\` liga
|
|
2209
|
+
-- \`migration.writer\`, que \xE9 a mesma escotilha que o writer da batida usa.
|
|
2210
|
+
-- \`photo_path\` N\xC3O \xE9 tocado \u2014 ele \xE9 de onde a imagem veio, e continua sendo.
|
|
2211
|
+
UPDATE public.plg_workforce_time_entries
|
|
2212
|
+
SET evidence = migration._deep_set(coalesce(evidence, '{}'::jsonb), ARRAY['photo'],
|
|
2213
|
+
jsonb_build_object(
|
|
2214
|
+
'bucket', p_row ->> 'bucket',
|
|
2215
|
+
'path', p_row ->> 'target_path',
|
|
2216
|
+
'document_id', p_document)),
|
|
2217
|
+
updated_at = now()
|
|
2218
|
+
WHERE id = v_entry AND tenant_id = p_tenant;
|
|
2219
|
+
END $fn$;
|
|
2220
|
+
|
|
2221
|
+
PERFORM migration.register_asset_kind(
|
|
2222
|
+
'punch_photo', 'time-clock-photos', 'workforce-punch-photos',
|
|
2223
|
+
'v_asset_backlog_punch_photo', 'migration._point_punch_photo',
|
|
2224
|
+
'A foto do totem. Bucket privado nas duas pontas: \xE9 biometria facial com hora e coordenada ao lado, e o V1 j\xE1 a tratava assim.');
|
|
2225
|
+
END $do$;
|
|
2226
|
+
`;
|
|
2227
|
+
var MIGRATION_003_O_PONTO_EM_ABERTO_PARA_DE_MENTIR_A_DURACAO = `-- ---------------------------------------------------------------------------
|
|
2228
|
+
-- 003_o_ponto_em_aberto_para_de_mentir_a_duracao.sql \u2014 \`-497155h02\`.
|
|
2229
|
+
--
|
|
2230
|
+
-- A tela de Registros de Ponto mostra isso na coluna Dura\xE7\xE3o de todo ponto em
|
|
2231
|
+
-- aberto. Medido em 19/09/2026 no tenant Pertinho do C\xE9u: **19 dias negativos,
|
|
2232
|
+
-- e os 19 est\xE3o abertos** \u2014 nenhum dia fechado d\xE1 negativo. O n\xFAmero n\xE3o \xE9
|
|
2233
|
+
-- aleat\xF3rio e n\xE3o \xE9 overflow.
|
|
2234
|
+
--
|
|
2235
|
+
-- \u2500\u2500 De onde ele sai \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2236
|
+
--
|
|
2237
|
+
-- A 000 soma as horas com sinal, e o desenho \xE9 bom: \`clock_out\` e \`break_start\`
|
|
2238
|
+
-- FECHAM um trecho e entram positivos, \`clock_in\` e \`break_end\` ABREM e entram
|
|
2239
|
+
-- negativos. Quantos intervalos o dia tiver, a conta \xE9 a mesma \u2014 \xE9 isso que o
|
|
2240
|
+
-- par \xFAnico do V1 n\xE3o consegue fazer.
|
|
2241
|
+
--
|
|
2242
|
+
-- Ela s\xF3 assume uma coisa que o dia em aberto n\xE3o cumpre: que toda abertura tem
|
|
2243
|
+
-- o seu fechamento. Um dia com uma batida s\xF3 \u2014
|
|
2244
|
+
--
|
|
2245
|
+
-- clock_in 18:09
|
|
2246
|
+
--
|
|
2247
|
+
-- \u2014 soma \`-extract(epoch FROM '2026-09-18 18:09')\`, que \xE9 \u22121.789.790. \u2060 segundos
|
|
2248
|
+
-- desde 1970. Dividido por 60 vira \u221229.829.302 minutos, e o formatador de horas
|
|
2249
|
+
-- faz o resto: \`-497155h02\`. O epoch vazou para a tela.
|
|
2250
|
+
--
|
|
2251
|
+
-- \u2500\u2500 O conserto da conta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2252
|
+
--
|
|
2253
|
+
-- Devolver \xE0 soma a abertura que ficou sem par. O que sobra \xE9 exatamente a
|
|
2254
|
+
-- soma dos trechos FECHADOS, que \xE9 a \xFAnica coisa que se pode afirmar de um dia
|
|
2255
|
+
-- que ainda n\xE3o terminou:
|
|
2256
|
+
--
|
|
2257
|
+
-- entrada 09:00, sa\xEDda 12:00, entrada 13:00 \u2192 3h00 fechadas, aberto desde 13:00
|
|
2258
|
+
-- entrada 18:09 \u2192 0h00 fechadas, aberto desde 18:09
|
|
2259
|
+
--
|
|
2260
|
+
-- Zero n\xE3o \xE9 um defeito aqui: \xE9 a resposta certa para "quanto j\xE1 fechou?" antes
|
|
2261
|
+
-- de qualquer fechamento. O que faltava para a tela era a OUTRA metade da
|
|
2262
|
+
-- pergunta, e \xE9 a coluna nova.
|
|
2263
|
+
--
|
|
2264
|
+
-- \u2500\u2500 \`open_since\`, e por que uma coluna e n\xE3o uma conta na tela \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2265
|
+
--
|
|
2266
|
+
-- Quem confere ponto quer saber h\xE1 quanto tempo a pessoa est\xE1 dentro. A tela
|
|
2267
|
+
-- n\xE3o consegue descobrir isso sozinha: ela recebe o dia agregado, n\xE3o as
|
|
2268
|
+
-- batidas, e "a \xFAltima abertura sem par" \xE9 justamente o que a agrega\xE7\xE3o sabe e
|
|
2269
|
+
-- ela n\xE3o. Sem a coluna, a alternativa seria a tela buscar as batidas de cada
|
|
2270
|
+
-- linha \u2014 299 consultas para desenhar uma tabela.
|
|
2271
|
+
--
|
|
2272
|
+
-- O rel\xF3gio corre do lado de quem olha, n\xE3o do banco: a coluna \xE9 o INSTANTE, e
|
|
2273
|
+
-- o "6h58 em curso" \xE9 calculado no render. Um intervalo calculado aqui
|
|
2274
|
+
-- envelheceria entre a consulta e a pintura, e envelheceria de novo a cada
|
|
2275
|
+
-- segundo que a aba ficasse aberta.
|
|
2276
|
+
--
|
|
2277
|
+
-- \u2500\u2500 O que N\xC3O muda \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2278
|
+
--
|
|
2279
|
+
-- Dia em aberto continua **fora** do total do dia e do per\xEDodo. A regra \xE9 a do
|
|
2280
|
+
-- V1 e est\xE1 certa: contar uma jornada que n\xE3o terminou inventa hora que ningu\xE9m
|
|
2281
|
+
-- trabalhou. O que muda \xE9 que agora o n\xFAmero que ele n\xE3o soma tamb\xE9m n\xE3o \xE9
|
|
2282
|
+
-- absurdo quando algu\xE9m olha a linha.
|
|
2283
|
+
--
|
|
2284
|
+
-- Idempotente e replay-safe.
|
|
2285
|
+
-- ---------------------------------------------------------------------------
|
|
2286
|
+
|
|
2287
|
+
CREATE OR REPLACE VIEW public.v_workforce_day
|
|
2288
|
+
WITH (security_invoker = 'true') AS
|
|
2289
|
+
WITH bruto AS (
|
|
2290
|
+
SELECT e.tenant_id,
|
|
2291
|
+
e.unit_id,
|
|
2292
|
+
e.person_id,
|
|
2293
|
+
e.work_date,
|
|
2294
|
+
min(e.effective_at) FILTER (WHERE e.kind = 'clock_in') AS first_in,
|
|
2295
|
+
max(e.effective_at) FILTER (WHERE e.kind = 'clock_out') AS last_out,
|
|
2296
|
+
count(*) FILTER (WHERE e.kind = 'break_start') AS break_count,
|
|
2297
|
+
count(*) FILTER (WHERE e.kind IN ('clock_in', 'break_end')) AS aberturas,
|
|
2298
|
+
count(*) FILTER (WHERE e.kind IN ('clock_out', 'break_start')) AS fechamentos,
|
|
2299
|
+
-- A \xFAltima ABERTURA do dia. Quando as contagens n\xE3o batem, \xE9 ela que
|
|
2300
|
+
-- ficou sem par \u2014 os eventos de um dia chegam em ordem, e uma abertura
|
|
2301
|
+
-- anterior j\xE1 teria sido fechada pela seguinte.
|
|
2302
|
+
max(e.effective_at) FILTER (WHERE e.kind IN ('clock_in', 'break_end')) AS ultima_abertura,
|
|
2303
|
+
sum(CASE
|
|
2304
|
+
WHEN e.kind IN ('clock_out', 'break_start') THEN extract(epoch FROM e.effective_at)
|
|
2305
|
+
WHEN e.kind IN ('clock_in', 'break_end') THEN -extract(epoch FROM e.effective_at)
|
|
2306
|
+
END) AS soma_com_sinal
|
|
2307
|
+
FROM public.v_workforce_time_entries e
|
|
2308
|
+
WHERE NOT e.voided
|
|
2309
|
+
GROUP BY e.tenant_id, e.unit_id, e.person_id, e.work_date
|
|
2310
|
+
),
|
|
2311
|
+
realized AS (
|
|
2312
|
+
SELECT b.tenant_id,
|
|
2313
|
+
b.unit_id,
|
|
2314
|
+
b.person_id,
|
|
2315
|
+
b.work_date,
|
|
2316
|
+
b.first_in,
|
|
2317
|
+
b.last_out,
|
|
2318
|
+
b.break_count,
|
|
2319
|
+
(b.aberturas <> b.fechamentos) AS is_open,
|
|
2320
|
+
CASE WHEN b.aberturas <> b.fechamentos THEN b.ultima_abertura END AS open_since,
|
|
2321
|
+
-- Devolve \xE0 soma a abertura sem par. Sem isto o que sobra \xE9 o epoch
|
|
2322
|
+
-- dela, com sinal trocado, e foi o que a tela vinha mostrando.
|
|
2323
|
+
(b.soma_com_sinal
|
|
2324
|
+
+ CASE WHEN b.aberturas <> b.fechamentos
|
|
2325
|
+
THEN extract(epoch FROM b.ultima_abertura)
|
|
2326
|
+
ELSE 0 END) AS worked_seconds
|
|
2327
|
+
FROM bruto b
|
|
2328
|
+
),
|
|
2329
|
+
planned AS (
|
|
2330
|
+
SELECT a.tenant_id,
|
|
2331
|
+
a.unit_id,
|
|
2332
|
+
a.person_id,
|
|
2333
|
+
s.schedule_date AS work_date,
|
|
2334
|
+
a.id AS assignment_id,
|
|
2335
|
+
a.role,
|
|
2336
|
+
a.status AS assignment_status,
|
|
2337
|
+
a.shift_label,
|
|
2338
|
+
a.shift_start_time,
|
|
2339
|
+
a.shift_end_time,
|
|
2340
|
+
-- O turno que vira o dia \xE9 normal aqui; sem o +24h a jornada da noite
|
|
2341
|
+
-- daria negativa.
|
|
2342
|
+
(extract(epoch FROM a.shift_end_time - a.shift_start_time)
|
|
2343
|
+
+ CASE WHEN a.shift_end_time <= a.shift_start_time THEN 86400 ELSE 0 END) AS planned_seconds
|
|
2344
|
+
FROM public.plg_workforce_schedule_assignments a
|
|
2345
|
+
JOIN public.plg_workforce_schedules s ON s.id = a.schedule_id
|
|
2346
|
+
WHERE s.status IN ('published', 'closed')
|
|
2347
|
+
AND a.role = 'titular'
|
|
2348
|
+
AND a.shift_start_time IS NOT NULL
|
|
2349
|
+
AND a.shift_end_time IS NOT NULL
|
|
2350
|
+
)
|
|
2351
|
+
SELECT coalesce(p.tenant_id, r.tenant_id) AS tenant_id,
|
|
2352
|
+
coalesce(p.unit_id, r.unit_id) AS unit_id,
|
|
2353
|
+
coalesce(p.person_id, r.person_id) AS person_id,
|
|
2354
|
+
coalesce(p.work_date, r.work_date) AS work_date,
|
|
2355
|
+
p.assignment_id,
|
|
2356
|
+
p.shift_label,
|
|
2357
|
+
p.shift_start_time,
|
|
2358
|
+
p.shift_end_time,
|
|
2359
|
+
p.assignment_status,
|
|
2360
|
+
(p.planned_seconds / 60)::integer AS planned_minutes,
|
|
2361
|
+
(r.worked_seconds / 60)::integer AS worked_minutes,
|
|
2362
|
+
((r.worked_seconds - p.planned_seconds) / 60)::integer AS variance_minutes,
|
|
2363
|
+
r.first_in,
|
|
2364
|
+
r.last_out,
|
|
2365
|
+
r.break_count,
|
|
2366
|
+
coalesce(r.is_open, false) AS is_open,
|
|
2367
|
+
(r.person_id IS NULL) AS absent,
|
|
2368
|
+
(p.assignment_id IS NULL) AS outside_schedule,
|
|
2369
|
+
-- Coluna nova, no fim: \`CREATE OR REPLACE VIEW\` s\xF3 aceita acrescentar
|
|
2370
|
+
-- aqui, e \xE9 de prop\xF3sito \u2014 quem l\xEA por posi\xE7\xE3o n\xE3o se move.
|
|
2371
|
+
r.open_since
|
|
2372
|
+
FROM planned p
|
|
2373
|
+
FULL OUTER JOIN realized r
|
|
2374
|
+
ON r.tenant_id = p.tenant_id
|
|
2375
|
+
AND r.person_id = p.person_id
|
|
2376
|
+
AND r.work_date = p.work_date;
|
|
2377
|
+
|
|
2378
|
+
COMMENT ON VIEW public.v_workforce_day IS
|
|
2379
|
+
'Jornada prevista \xD7 realizada por pessoa e dia. Previsto vem da escala publicada, realizado das batidas com ajuste aplicado. FULL OUTER JOIN: \`absent\` \xE9 escala sem ponto, \`outside_schedule\` \xE9 ponto sem escala. \`worked_minutes\` conta s\xF3 os trechos FECHADOS; \`open_since\` \xE9 a abertura que ainda n\xE3o tem par (003).';
|
|
2380
|
+
|
|
2381
|
+
COMMENT ON COLUMN public.v_workforce_day.open_since IS
|
|
2382
|
+
'O instante da abertura sem par, quando o ponto est\xE1 aberto (003). \xC9 o INSTANTE e n\xE3o a dura\xE7\xE3o: o rel\xF3gio corre do lado de quem olha a tela, e um intervalo calculado aqui envelheceria entre a consulta e a pintura.';
|
|
2383
|
+
|
|
2384
|
+
GRANT SELECT ON public.v_workforce_day TO authenticated, service_role;
|
|
2385
|
+
`;
|
|
2386
|
+
var MIGRATION_004_A_JORNADA_IMPLAUSIVEL_NAO_SOMA = `-- ---------------------------------------------------------------------------
|
|
2387
|
+
-- 004_a_jornada_implausivel_nao_soma.sql \u2014 45h45 num dia.
|
|
2388
|
+
--
|
|
2389
|
+
-- Medido em 19/09/2026 no tenant Pertinho do C\xE9u, na tela de Registros de Ponto:
|
|
2390
|
+
--
|
|
2391
|
+
-- Steffany Fidelis da Costa 15:41 \u2192 13:26 45h45
|
|
2392
|
+
-- CRISTIANE CARDOSO OLIVEIRA 17:14 \u2192 09:23 16h10
|
|
2393
|
+
-- jannyne gomes dos santos 09:07 \u2192 08:58 15h56
|
|
2394
|
+
--
|
|
2395
|
+
-- e o cabe\xE7alho do dia somando **109h45** para oito pessoas.
|
|
2396
|
+
--
|
|
2397
|
+
-- Nenhum desses \xE9 erro de conta: a 003 j\xE1 consertou a soma, e esses dias est\xE3o
|
|
2398
|
+
-- FECHADOS. Eles s\xE3o a medi\xE7\xE3o correta de um par errado \u2014 algu\xE9m entrou, foi
|
|
2399
|
+
-- embora sem bater a sa\xEDda, e a sa\xEDda caiu horas ou dias depois. O V1 grava
|
|
2400
|
+
-- \`checkin_at\` e \`checkout_at\` como um par s\xF3, ent\xE3o o buraco atravessou inteiro.
|
|
2401
|
+
--
|
|
2402
|
+
-- \u2500\u2500 Por que n\xE3o um teto de 8h \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2403
|
+
--
|
|
2404
|
+
-- Trocar 45h45 por 8h faria o total parecer certo. \xC9 o pior resultado poss\xEDvel:
|
|
2405
|
+
--
|
|
2406
|
+
-- \xB7 8h ali \xE9 um n\xFAmero que ningu\xE9m trabalhou e ningu\xE9m digitou \u2014 \xE9 a regra
|
|
2407
|
+
-- "nada de default inventado" quebrada no lugar onde ela mais custa, que \xE9
|
|
2408
|
+
-- base de c\xE1lculo de folha;
|
|
2409
|
+
-- \xB7 e o dia deixaria de parecer estranho, ent\xE3o a batida furada nunca seria
|
|
2410
|
+
-- corrigida. O erro sai da tela e fica no dado, que \xE9 o contr\xE1rio de
|
|
2411
|
+
-- resolver.
|
|
2412
|
+
--
|
|
2413
|
+
-- \u2500\u2500 O que esta migration faz \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2414
|
+
--
|
|
2415
|
+
-- Estende a regra que a tela J\xC1 tem. Ponto em aberto n\xE3o soma, porque contar
|
|
2416
|
+
-- uma jornada que n\xE3o terminou inventa hora. Um dia de 45h \xE9 o mesmo problema
|
|
2417
|
+
-- com outra cara \u2014 um par em que n\xE3o se pode confiar \u2014, ent\xE3o ele tamb\xE9m n\xE3o
|
|
2418
|
+
-- soma. O valor medido continua \xE0 vista, inteiro, com o motivo escrito ao lado.
|
|
2419
|
+
--
|
|
2420
|
+
-- \`review_reason\` nomeia por que o dia n\xE3o entra na conta:
|
|
2421
|
+
--
|
|
2422
|
+
-- 'open' ainda n\xE3o terminou
|
|
2423
|
+
-- 'span_too_long' fechou, e fechou com dura\xE7\xE3o que n\xE3o \xE9 uma jornada
|
|
2424
|
+
--
|
|
2425
|
+
-- NULL \xE9 o dia que conta. Uma coluna s\xF3, e quem soma filtra por ela \u2014 em vez de
|
|
2426
|
+
-- cada relat\xF3rio redescobrir a regra e descobrir diferente.
|
|
2427
|
+
--
|
|
2428
|
+
-- \u2500\u2500 O limite, e por que dezesseis horas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2429
|
+
--
|
|
2430
|
+
-- A jornada mais longa do vocabul\xE1rio do V1 \xE9 a escala 12x36: doze horas. Com
|
|
2431
|
+
-- duas de extra, quatorze. Dezesseis deixa margem para o turno que vira a noite
|
|
2432
|
+
-- e ainda separa com folga o caso real, que n\xE3o \xE9 uma jornada comprida e sim
|
|
2433
|
+
-- uma sa\xEDda que ficou pendurada \u2014 os tr\xEAs medidos acima t\xEAm 16h, 16h e 46h.
|
|
2434
|
+
--
|
|
2435
|
+
-- \xC9 o MESMO dezesseis que a tela usa para dizer "sem sa\xEDda h\xE1 31h" num ponto
|
|
2436
|
+
-- aberto, e \xE9 de prop\xF3sito: um n\xFAmero, uma justificativa, os dois lados da
|
|
2437
|
+
-- mesma pergunta.
|
|
2438
|
+
--
|
|
2439
|
+
-- Idempotente e replay-safe.
|
|
2440
|
+
-- ---------------------------------------------------------------------------
|
|
2441
|
+
|
|
2442
|
+
CREATE OR REPLACE FUNCTION public.workforce_max_shift_minutes()
|
|
2443
|
+
RETURNS integer LANGUAGE sql IMMUTABLE PARALLEL SAFE
|
|
2444
|
+
AS $$ SELECT 16 * 60 $$;
|
|
2445
|
+
|
|
2446
|
+
COMMENT ON FUNCTION public.workforce_max_shift_minutes() IS
|
|
2447
|
+
'Acima disto uma jornada fechada deixa de ser jornada e vira sa\xEDda n\xE3o batida (004). 12x36 \xE9 a mais longa do vocabul\xE1rio do V1; dezesseis d\xE1 margem ao turno que vira a noite. Fun\xE7\xE3o e n\xE3o constante literal para que a tela, a view e um relat\xF3rio futuro citem o mesmo n\xFAmero.';
|
|
2448
|
+
|
|
2449
|
+
CREATE OR REPLACE VIEW public.v_workforce_day
|
|
2450
|
+
WITH (security_invoker = 'true') AS
|
|
2451
|
+
WITH bruto AS (
|
|
2452
|
+
SELECT e.tenant_id,
|
|
2453
|
+
e.unit_id,
|
|
2454
|
+
e.person_id,
|
|
2455
|
+
e.work_date,
|
|
2456
|
+
min(e.effective_at) FILTER (WHERE e.kind = 'clock_in') AS first_in,
|
|
2457
|
+
max(e.effective_at) FILTER (WHERE e.kind = 'clock_out') AS last_out,
|
|
2458
|
+
count(*) FILTER (WHERE e.kind = 'break_start') AS break_count,
|
|
2459
|
+
count(*) FILTER (WHERE e.kind IN ('clock_in', 'break_end')) AS aberturas,
|
|
2460
|
+
count(*) FILTER (WHERE e.kind IN ('clock_out', 'break_start')) AS fechamentos,
|
|
2461
|
+
-- A \xFAltima ABERTURA do dia. Quando as contagens n\xE3o batem, \xE9 ela que
|
|
2462
|
+
-- ficou sem par \u2014 os eventos de um dia chegam em ordem, e uma abertura
|
|
2463
|
+
-- anterior j\xE1 teria sido fechada pela seguinte.
|
|
2464
|
+
max(e.effective_at) FILTER (WHERE e.kind IN ('clock_in', 'break_end')) AS ultima_abertura,
|
|
2465
|
+
sum(CASE
|
|
2466
|
+
WHEN e.kind IN ('clock_out', 'break_start') THEN extract(epoch FROM e.effective_at)
|
|
2467
|
+
WHEN e.kind IN ('clock_in', 'break_end') THEN -extract(epoch FROM e.effective_at)
|
|
2468
|
+
END) AS soma_com_sinal
|
|
2469
|
+
FROM public.v_workforce_time_entries e
|
|
2470
|
+
WHERE NOT e.voided
|
|
2471
|
+
GROUP BY e.tenant_id, e.unit_id, e.person_id, e.work_date
|
|
2472
|
+
),
|
|
2473
|
+
realized AS (
|
|
2474
|
+
SELECT b.tenant_id,
|
|
2475
|
+
b.unit_id,
|
|
2476
|
+
b.person_id,
|
|
2477
|
+
b.work_date,
|
|
2478
|
+
b.first_in,
|
|
2479
|
+
b.last_out,
|
|
2480
|
+
b.break_count,
|
|
2481
|
+
(b.aberturas <> b.fechamentos) AS is_open,
|
|
2482
|
+
CASE WHEN b.aberturas <> b.fechamentos THEN b.ultima_abertura END AS open_since,
|
|
2483
|
+
-- Devolve \xE0 soma a abertura sem par (003). Sem isto o que sobra \xE9 o
|
|
2484
|
+
-- epoch dela, com sinal trocado.
|
|
2485
|
+
(b.soma_com_sinal
|
|
2486
|
+
+ CASE WHEN b.aberturas <> b.fechamentos
|
|
2487
|
+
THEN extract(epoch FROM b.ultima_abertura)
|
|
2488
|
+
ELSE 0 END) AS worked_seconds
|
|
2489
|
+
FROM bruto b
|
|
2490
|
+
),
|
|
2491
|
+
planned AS (
|
|
2492
|
+
SELECT a.tenant_id,
|
|
2493
|
+
a.unit_id,
|
|
2494
|
+
a.person_id,
|
|
2495
|
+
s.schedule_date AS work_date,
|
|
2496
|
+
a.id AS assignment_id,
|
|
2497
|
+
a.role,
|
|
2498
|
+
a.status AS assignment_status,
|
|
2499
|
+
a.shift_label,
|
|
2500
|
+
a.shift_start_time,
|
|
2501
|
+
a.shift_end_time,
|
|
2502
|
+
-- O turno que vira o dia \xE9 normal aqui; sem o +24h a jornada da noite
|
|
2503
|
+
-- daria negativa.
|
|
2504
|
+
(extract(epoch FROM a.shift_end_time - a.shift_start_time)
|
|
2505
|
+
+ CASE WHEN a.shift_end_time <= a.shift_start_time THEN 86400 ELSE 0 END) AS planned_seconds
|
|
2506
|
+
FROM public.plg_workforce_schedule_assignments a
|
|
2507
|
+
JOIN public.plg_workforce_schedules s ON s.id = a.schedule_id
|
|
2508
|
+
WHERE s.status IN ('published', 'closed')
|
|
2509
|
+
AND a.role = 'titular'
|
|
2510
|
+
AND a.shift_start_time IS NOT NULL
|
|
2511
|
+
AND a.shift_end_time IS NOT NULL
|
|
2512
|
+
)
|
|
2513
|
+
SELECT coalesce(p.tenant_id, r.tenant_id) AS tenant_id,
|
|
2514
|
+
coalesce(p.unit_id, r.unit_id) AS unit_id,
|
|
2515
|
+
coalesce(p.person_id, r.person_id) AS person_id,
|
|
2516
|
+
coalesce(p.work_date, r.work_date) AS work_date,
|
|
2517
|
+
p.assignment_id,
|
|
2518
|
+
p.shift_label,
|
|
2519
|
+
p.shift_start_time,
|
|
2520
|
+
p.shift_end_time,
|
|
2521
|
+
p.assignment_status,
|
|
2522
|
+
(p.planned_seconds / 60)::integer AS planned_minutes,
|
|
2523
|
+
-- O MEDIDO, sempre. N\xE3o \xE9 aqui que se decide se ele conta \u2014 a coluna
|
|
2524
|
+
-- mente se for corrigida, e um n\xFAmero corrigido em sil\xEAncio \xE9 pior que
|
|
2525
|
+
-- um n\xFAmero estranho.
|
|
2526
|
+
(r.worked_seconds / 60)::integer AS worked_minutes,
|
|
2527
|
+
((r.worked_seconds - p.planned_seconds) / 60)::integer AS variance_minutes,
|
|
2528
|
+
r.first_in,
|
|
2529
|
+
r.last_out,
|
|
2530
|
+
r.break_count,
|
|
2531
|
+
coalesce(r.is_open, false) AS is_open,
|
|
2532
|
+
(r.person_id IS NULL) AS absent,
|
|
2533
|
+
(p.assignment_id IS NULL) AS outside_schedule,
|
|
2534
|
+
r.open_since,
|
|
2535
|
+
-- Por que este dia n\xE3o entra na soma. NULL = entra.
|
|
2536
|
+
CASE
|
|
2537
|
+
WHEN r.person_id IS NULL THEN NULL -- escala sem ponto: n\xE3o h\xE1 o que somar
|
|
2538
|
+
WHEN r.is_open THEN 'open'
|
|
2539
|
+
WHEN (r.worked_seconds / 60) > public.workforce_max_shift_minutes()
|
|
2540
|
+
THEN 'span_too_long'
|
|
2541
|
+
END AS review_reason
|
|
2542
|
+
FROM planned p
|
|
2543
|
+
FULL OUTER JOIN realized r
|
|
2544
|
+
ON r.tenant_id = p.tenant_id
|
|
2545
|
+
AND r.person_id = p.person_id
|
|
2546
|
+
AND r.work_date = p.work_date;
|
|
2547
|
+
|
|
2548
|
+
COMMENT ON VIEW public.v_workforce_day IS
|
|
2549
|
+
'Jornada prevista \xD7 realizada por pessoa e dia. \`worked_minutes\` \xE9 sempre o MEDIDO; \`review_reason\` diz por que um dia n\xE3o entra na soma \u2014 \`open\` (n\xE3o terminou) ou \`span_too_long\` (fechou com dura\xE7\xE3o que n\xE3o \xE9 jornada, 004). Quem soma filtra por ela.';
|
|
2550
|
+
|
|
2551
|
+
COMMENT ON COLUMN public.v_workforce_day.review_reason IS
|
|
2552
|
+
'NULL = o dia conta. \`open\` = ponto em aberto. \`span_too_long\` = acima de workforce_max_shift_minutes(), que na pr\xE1tica \xE9 sa\xEDda n\xE3o batida e n\xE3o jornada longa (004).';
|
|
2553
|
+
|
|
2554
|
+
GRANT SELECT ON public.v_workforce_day TO authenticated, service_role;
|
|
2555
|
+
`;
|
|
2556
|
+
var MIGRATION_005_A_FICHA_TRABALHISTA_TEM_ONDE_MORAR = `-- ---------------------------------------------------------------------------
|
|
2557
|
+
-- 005_a_ficha_trabalhista_tem_onde_morar.sql \u2014 o RH do V1 ganha destino.
|
|
2558
|
+
--
|
|
2559
|
+
-- Escrito a partir de \`~/dev/beautyplace/docs/v1-guide/rh\`, que documenta o
|
|
2560
|
+
-- m\xF3dulo do V1 andando nele: a Vis\xE3o Geral (oito cart\xF5es, tr\xEAs listas) e a Ficha
|
|
2561
|
+
-- de RH da pessoa (dezesseis vistas). O FLOW de cada um diz \`v2: status:
|
|
2562
|
+
-- missing\` \u2014 e dizia certo: n\xE3o havia nada disso aqui.
|
|
2563
|
+
--
|
|
2564
|
+
-- \u2500\u2500 O que este arquivo N\xC3O traz, e por qu\xEA \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2565
|
+
--
|
|
2566
|
+
-- N\xE3o s\xE3o as dezesseis vistas. Medido na origem em 19/09/2026, nas 886 pessoas
|
|
2567
|
+
-- dos tr\xEAs tenants em porte:
|
|
2568
|
+
--
|
|
2569
|
+
-- as 12 tabelas de lista da Ficha 0 linhas \u2014 no V1 INTEIRO, n\xE3o s\xF3
|
|
2570
|
+
-- (f\xE9rias, faltas, advert\xEAncias, nesses tenants
|
|
2571
|
+
-- treinamentos, avalia\xE7\xF5es, exames,
|
|
2572
|
+
-- EPIs, pend\xEAncias, dependentes,
|
|
2573
|
+
-- hist\xF3rico salarial, 13\xBA, benef\xEDcios)
|
|
2574
|
+
-- admission_date, dismissal_date, 0 preenchidos
|
|
2575
|
+
-- job_title, pis_pasep, CTPS, eSocial
|
|
2576
|
+
-- base_salary 25 de 422
|
|
2577
|
+
-- contract_type 2
|
|
2578
|
+
--
|
|
2579
|
+
-- O pr\xF3prio FLOW da Vis\xE3o Geral avisa: *"Num tenant rec\xE9m-migrado ele mostra
|
|
2580
|
+
-- headcount cheio e todo o resto zerado \u2014 e isso n\xE3o \xE9 defeito, \xE9 ficha vazia."*
|
|
2581
|
+
-- Aqui TODO tenant \xE9 assim. Desenhar dezesseis vistas para portar zero linha
|
|
2582
|
+
-- seria construir um formul\xE1rio de cada vez que algu\xE9m leu um FLOW.
|
|
2583
|
+
--
|
|
2584
|
+
-- Ent\xE3o vem o n\xFAcleo que sustenta o conceito e j\xE1 \xE9 \xFAtil no primeiro dia:
|
|
2585
|
+
--
|
|
2586
|
+
-- employment o v\xEDnculo \u2014 \xE9 dele que saem headcount, admiss\xF5es,
|
|
2587
|
+
-- demiss\xF5es, turnover e experi\xEAncia vencendo. Sem ele
|
|
2588
|
+
-- nenhum n\xFAmero de RH existe, e \xE9 a vista 1 do V1
|
|
2589
|
+
-- vacations f\xE9rias. Obriga\xE7\xE3o legal, recorrente, e a \xFAnica lista do
|
|
2590
|
+
-- V1 que alimenta um cart\xE3o
|
|
2591
|
+
-- absences faltas e afastamentos
|
|
2592
|
+
-- pending_items pend\xEAncias \u2014 a outra lista que aparece no painel
|
|
2593
|
+
--
|
|
2594
|
+
-- As outras doze entram quando algu\xE9m for us\xE1-las. O que elas precisam j\xE1 est\xE1
|
|
2595
|
+
-- de p\xE9: a forma da tabela, o escopo, a permiss\xE3o e a view do painel.
|
|
2596
|
+
--
|
|
2597
|
+
-- \u2500\u2500 A permiss\xE3o, que o FLOW manda resolver ANTES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2598
|
+
--
|
|
2599
|
+
-- V1-069: no V1 **n\xE3o h\xE1 permiss\xE3o separada para RH**. Quem edita cadastro v\xEA
|
|
2600
|
+
-- sal\xE1rio, CPF, dependentes e CID de atestado. O FLOW da Ficha fecha dizendo que
|
|
2601
|
+
-- portar s\xF3 vale "desde que a quest\xE3o da permiss\xE3o separada seja resolvida
|
|
2602
|
+
-- antes" \u2014 ent\xE3o ela \xE9 resolvida aqui, e n\xE3o depois.
|
|
2603
|
+
--
|
|
2604
|
+
-- \`workforce_hr.*\` \xE9 uma fam\xEDlia pr\xF3pria: quem tem \`workforce.read\` v\xEA escala e
|
|
2605
|
+
-- ponto e N\xC3O v\xEA sal\xE1rio. S\xE3o duas perguntas diferentes e agora t\xEAm duas
|
|
2606
|
+
-- respostas.
|
|
2607
|
+
--
|
|
2608
|
+
-- \u2500\u2500 O que o V2 faz e o V1 n\xE3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2609
|
+
--
|
|
2610
|
+
-- O FLOW da vista 10 registra, como defeito: *"N\xE3o conversa com o ponto
|
|
2611
|
+
-- eletr\xF4nico nem com a escala: uma falta lan\xE7ada aqui n\xE3o aparece l\xE1."* No V1
|
|
2612
|
+
-- s\xE3o dois sistemas sobre a mesma pessoa que n\xE3o se olham \u2014 algu\xE9m de atestado
|
|
2613
|
+
-- aparece como FALTA na folha de ponto, e quem confere precisa lembrar de cabe\xE7a.
|
|
2614
|
+
--
|
|
2615
|
+
-- A \xA77 conserta: \`v_workforce_day\` passa a saber do afastamento. Um dia coberto
|
|
2616
|
+
-- por atestado deixa de ser "Falta" e passa a dizer o que foi. \xC9 a raz\xE3o de o RH
|
|
2617
|
+
-- morar no mesmo plugin do ponto, e n\xE3o ao lado dele.
|
|
2618
|
+
--
|
|
2619
|
+
-- Idempotente e replay-safe.
|
|
2620
|
+
-- ---------------------------------------------------------------------------
|
|
2621
|
+
|
|
2622
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2623
|
+
-- 1. A PERMISS\xC3O SEPARADA (V1-069)
|
|
2624
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2625
|
+
|
|
2626
|
+
INSERT INTO app.permissions (key, plugin, description) VALUES
|
|
2627
|
+
('workforce_hr.read', 'workforce_hr', 'Ver a ficha trabalhista: v\xEDnculo, sal\xE1rio, f\xE9rias, afastamentos e pend\xEAncias.'),
|
|
2628
|
+
('workforce_hr.create', 'workforce_hr', 'Lan\xE7ar na ficha trabalhista.'),
|
|
2629
|
+
('workforce_hr.edit', 'workforce_hr', 'Corrigir lan\xE7amento da ficha trabalhista.'),
|
|
2630
|
+
('workforce_hr.delete', 'workforce_hr', 'Excluir lan\xE7amento da ficha trabalhista.')
|
|
2631
|
+
ON CONFLICT (key) DO UPDATE SET plugin = EXCLUDED.plugin, description = EXCLUDED.description;
|
|
2632
|
+
|
|
2633
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2634
|
+
-- 2. O V\xCDNCULO
|
|
2635
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2636
|
+
--
|
|
2637
|
+
-- Uma linha por pessoa, e n\xE3o colunas em \`public.people\`. Tr\xEAs raz\xF5es, e a
|
|
2638
|
+
-- terceira \xE9 a que decide:
|
|
2639
|
+
--
|
|
2640
|
+
-- \xB7 \`people\` \xE9 do core e atende cliente, fornecedor e profissional; quarenta
|
|
2641
|
+
-- colunas de CLT ali seriam nulas para a esmagadora maioria das linhas;
|
|
2642
|
+
-- \xB7 o V1 tem DUAS tabelas (professionals, employees) com os mesmos campos, e
|
|
2643
|
+
-- o V2 tem uma pessoa s\xF3 \u2014 a separa\xE7\xE3o \xE9 papel, n\xE3o cadastro;
|
|
2644
|
+
-- \xB7 e \xE9 a tabela que carrega o sal\xE1rio. Ela precisa de uma policy PR\xD3PRIA,
|
|
2645
|
+
-- e uma coluna n\xE3o tem policy: RLS \xE9 por tabela. Guardar sal\xE1rio em
|
|
2646
|
+
-- \`people\` seria reproduzir o V1-069 no schema.
|
|
2647
|
+
|
|
2648
|
+
CREATE TABLE IF NOT EXISTS public.plg_workforce_employment (
|
|
2649
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
2650
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
2651
|
+
unit_id uuid,
|
|
2652
|
+
person_id uuid NOT NULL REFERENCES public.people(id) ON DELETE CASCADE,
|
|
2653
|
+
|
|
2654
|
+
-- \u2500\u2500 v\xEDnculo (vista 1 do V1: Contrato) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2655
|
+
-- Vocabul\xE1rio exato do FLOW. Lista fechada porque \xE9 ela que decide encargo, e
|
|
2656
|
+
-- "CLT " com espa\xE7o vira um tipo de contrato novo em sil\xEAncio.
|
|
2657
|
+
contract_kind text CHECK (contract_kind IN
|
|
2658
|
+
('clt', 'pj', 'estagio', 'jovem_aprendiz', 'autonomo', 'socio', 'temporario', 'intermitente')),
|
|
2659
|
+
admission_date date,
|
|
2660
|
+
dismissal_date date,
|
|
2661
|
+
dismissal_reason text,
|
|
2662
|
+
-- O V1 tem os dois: o fim da PRIMEIRA experi\xEAncia e o fim FINAL. \xC9 o segundo
|
|
2663
|
+
-- que alimenta o cart\xE3o "Experi\xEAncia vencendo".
|
|
2664
|
+
probation_first_end date,
|
|
2665
|
+
probation_final_end date,
|
|
2666
|
+
job_title text,
|
|
2667
|
+
department text,
|
|
2668
|
+
schedule_regime text CHECK (schedule_regime IN
|
|
2669
|
+
('mensalista', 'horista', 'diarista', 'escala_12x36', 'escala_6x1', 'escala_5x2')),
|
|
2670
|
+
weekly_hours numeric(5,2),
|
|
2671
|
+
monthly_hours numeric(6,2),
|
|
2672
|
+
esocial_registration text,
|
|
2673
|
+
|
|
2674
|
+
-- \u2500\u2500 remunera\xE7\xE3o (vista 2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2675
|
+
-- Valor de REFER\xCANCIA, e o V1 diz isso com todas as letras: nada aqui vira
|
|
2676
|
+
-- conta a pagar, comiss\xE3o ou desconto. O coment\xE1rio fica porque a coluna
|
|
2677
|
+
-- chamada \`base_salary\` convida a supor o contr\xE1rio.
|
|
2678
|
+
base_salary numeric(14,2),
|
|
2679
|
+
pay_frequency text CHECK (pay_frequency IN ('mensal', 'quinzenal', 'semanal', 'diario', 'por_hora')),
|
|
2680
|
+
-- Os quinze aux\xEDlios e adicionais da vista 2 em jsonb, e n\xE3o em quinze
|
|
2681
|
+
-- colunas. Medido: ZERO deles preenchido nas 886 pessoas. Quinze colunas
|
|
2682
|
+
-- nulas seriam quinze decis\xF5es de schema tomadas sem um caso de uso; a chave
|
|
2683
|
+
-- que a casa usar aparece aqui e vira coluna quando pagar a passagem.
|
|
2684
|
+
allowances jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
2685
|
+
|
|
2686
|
+
notes text,
|
|
2687
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
2688
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
2689
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
2690
|
+
-- Um v\xEDnculo por pessoa. Recontrata\xE7\xE3o reabre o mesmo, com a data nova \u2014 o
|
|
2691
|
+
-- hist\xF3rico de quem saiu e voltou \xE9 uma vista de lista que ainda n\xE3o existe,
|
|
2692
|
+
-- e inventar a segunda linha agora criaria dois headcounts para uma pessoa.
|
|
2693
|
+
CONSTRAINT plg_workforce_employment_person UNIQUE (tenant_id, person_id),
|
|
2694
|
+
-- Demiss\xE3o antes da admiss\xE3o n\xE3o \xE9 dado ruim, \xE9 dado imposs\xEDvel.
|
|
2695
|
+
CONSTRAINT plg_workforce_employment_dates
|
|
2696
|
+
CHECK (dismissal_date IS NULL OR admission_date IS NULL OR dismissal_date >= admission_date)
|
|
2697
|
+
);
|
|
2698
|
+
|
|
2699
|
+
COMMENT ON TABLE public.plg_workforce_employment IS
|
|
2700
|
+
'O v\xEDnculo trabalhista de uma pessoa (005). \xC9 dele que saem headcount, admiss\xF5es, demiss\xF5es, turnover e experi\xEAncia vencendo. Tabela pr\xF3pria e n\xE3o colunas em people: RLS \xE9 por tabela, e \xE9 o sal\xE1rio que precisa da policy.';
|
|
2701
|
+
COMMENT ON COLUMN public.plg_workforce_employment.base_salary IS
|
|
2702
|
+
'Valor de REFER\xCANCIA. N\xE3o vira conta a pagar, comiss\xE3o nem desconto \u2014 o V1 tamb\xE9m n\xE3o fazia isso, e o nome da coluna convida a supor que faz.';
|
|
2703
|
+
COMMENT ON COLUMN public.plg_workforce_employment.allowances IS
|
|
2704
|
+
'Aux\xEDlios e adicionais (VT, VR, VA, plano, creche, noturno, insalubridade\u2026). Em jsonb porque os quinze campos da vista 2 do V1 est\xE3o vazios nas 886 pessoas medidas: vira coluna quando algu\xE9m pagar a passagem.';
|
|
2705
|
+
|
|
2706
|
+
CREATE INDEX IF NOT EXISTS plg_workforce_employment_tenant_person
|
|
2707
|
+
ON public.plg_workforce_employment (tenant_id, person_id);
|
|
2708
|
+
CREATE INDEX IF NOT EXISTS plg_workforce_employment_ativo
|
|
2709
|
+
ON public.plg_workforce_employment (tenant_id) WHERE dismissal_date IS NULL;
|
|
2710
|
+
|
|
2711
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2712
|
+
-- 3. F\xC9RIAS (vista 8)
|
|
2713
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2714
|
+
|
|
2715
|
+
CREATE TABLE IF NOT EXISTS public.plg_workforce_vacations (
|
|
2716
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
2717
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
2718
|
+
unit_id uuid,
|
|
2719
|
+
person_id uuid NOT NULL REFERENCES public.people(id) ON DELETE CASCADE,
|
|
2720
|
+
-- O per\xEDodo AQUISITIVO: o que a pessoa ganhou. \xC9 o \`accrual_end\` que alimenta
|
|
2721
|
+
-- "F\xE9rias vencendo (60d)".
|
|
2722
|
+
accrual_start date NOT NULL,
|
|
2723
|
+
accrual_end date NOT NULL,
|
|
2724
|
+
-- O per\xEDodo de GOZO: quando ela tirou. Nulo \xE9 o estado normal de quem ainda
|
|
2725
|
+
-- n\xE3o programou.
|
|
2726
|
+
taken_start date,
|
|
2727
|
+
taken_end date,
|
|
2728
|
+
days_taken integer,
|
|
2729
|
+
days_sold integer,
|
|
2730
|
+
advance_13th boolean NOT NULL DEFAULT false,
|
|
2731
|
+
-- Digitado, como no V1. O sistema N\xC3O calcula o ter\xE7o \u2014 e escrever isso aqui
|
|
2732
|
+
-- \xE9 o que impede algu\xE9m de supor que a coluna vazia \xE9 um bug.
|
|
2733
|
+
constitutional_third numeric(14,2),
|
|
2734
|
+
status text NOT NULL DEFAULT 'pendente'
|
|
2735
|
+
CHECK (status IN ('pendente', 'programada', 'em_gozo', 'concluida', 'vencida')),
|
|
2736
|
+
notes text,
|
|
2737
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
2738
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
2739
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
2740
|
+
CONSTRAINT plg_workforce_vacations_accrual CHECK (accrual_end >= accrual_start),
|
|
2741
|
+
CONSTRAINT plg_workforce_vacations_taken
|
|
2742
|
+
CHECK (taken_end IS NULL OR taken_start IS NULL OR taken_end >= taken_start)
|
|
2743
|
+
);
|
|
2744
|
+
|
|
2745
|
+
COMMENT ON TABLE public.plg_workforce_vacations IS
|
|
2746
|
+
'Per\xEDodo aquisitivo e de gozo de f\xE9rias (005). Nada \xE9 calculado, inclusive o ter\xE7o constitucional \u2014 \xE9 digitado, como no V1.';
|
|
2747
|
+
|
|
2748
|
+
CREATE INDEX IF NOT EXISTS plg_workforce_vacations_vencendo
|
|
2749
|
+
ON public.plg_workforce_vacations (tenant_id, accrual_end) WHERE status <> 'concluida';
|
|
2750
|
+
|
|
2751
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2752
|
+
-- 4. FALTAS E AFASTAMENTOS (vista 10)
|
|
2753
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2754
|
+
|
|
2755
|
+
CREATE TABLE IF NOT EXISTS public.plg_workforce_absences (
|
|
2756
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
2757
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
2758
|
+
unit_id uuid,
|
|
2759
|
+
person_id uuid NOT NULL REFERENCES public.people(id) ON DELETE CASCADE,
|
|
2760
|
+
kind text NOT NULL CHECK (kind IN
|
|
2761
|
+
('falta_justificada', 'falta_injustificada', 'atestado', 'licenca_maternidade',
|
|
2762
|
+
'licenca_paternidade', 'licenca_nao_remunerada', 'suspensao', 'ferias', 'afastamento_inss')),
|
|
2763
|
+
starts_on date NOT NULL,
|
|
2764
|
+
-- Nulo = afastamento em aberto, que \xE9 um estado real (INSS sem alta marcada).
|
|
2765
|
+
ends_on date,
|
|
2766
|
+
days integer,
|
|
2767
|
+
paid boolean NOT NULL DEFAULT true,
|
|
2768
|
+
-- Dado m\xE9dico. Mora aqui e n\xE3o em \`people\` pelo mesmo motivo do sal\xE1rio: \xE9 a
|
|
2769
|
+
-- policy de \`workforce_hr\` que o protege, e V1-069 \xE9 exatamente sobre isto.
|
|
2770
|
+
icd_code text,
|
|
2771
|
+
doctor text,
|
|
2772
|
+
notes text,
|
|
2773
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
2774
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
2775
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
2776
|
+
CONSTRAINT plg_workforce_absences_span CHECK (ends_on IS NULL OR ends_on >= starts_on)
|
|
2777
|
+
);
|
|
2778
|
+
|
|
2779
|
+
COMMENT ON TABLE public.plg_workforce_absences IS
|
|
2780
|
+
'Falta e afastamento (005). Diferente do V1, CONVERSA com o ponto: v_workforce_day l\xEA daqui, e um dia coberto por atestado deixa de aparecer como falta.';
|
|
2781
|
+
COMMENT ON COLUMN public.plg_workforce_absences.icd_code IS
|
|
2782
|
+
'CID do atestado. Dado de sa\xFAde: \xE9 a policy de workforce_hr que o separa de quem edita cadastro \u2014 o V1 n\xE3o separava (V1-069).';
|
|
2783
|
+
|
|
2784
|
+
CREATE INDEX IF NOT EXISTS plg_workforce_absences_periodo
|
|
2785
|
+
ON public.plg_workforce_absences (tenant_id, person_id, starts_on, ends_on);
|
|
2786
|
+
|
|
2787
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2788
|
+
-- 5. PEND\xCANCIAS (vista 16)
|
|
2789
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2790
|
+
|
|
2791
|
+
CREATE TABLE IF NOT EXISTS public.plg_workforce_pending_items (
|
|
2792
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
2793
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
2794
|
+
unit_id uuid,
|
|
2795
|
+
person_id uuid NOT NULL REFERENCES public.people(id) ON DELETE CASCADE,
|
|
2796
|
+
title text NOT NULL,
|
|
2797
|
+
category text CHECK (category IN ('documento', 'exame', 'epi', 'treinamento', 'assinatura', 'outro')),
|
|
2798
|
+
priority text NOT NULL DEFAULT 'normal' CHECK (priority IN ('baixa', 'normal', 'alta', 'urgente')),
|
|
2799
|
+
due_on date,
|
|
2800
|
+
resolved_at timestamptz,
|
|
2801
|
+
description text,
|
|
2802
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
2803
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
2804
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
2805
|
+
);
|
|
2806
|
+
|
|
2807
|
+
COMMENT ON TABLE public.plg_workforce_pending_items IS
|
|
2808
|
+
'O que falta na pasta de algu\xE9m: documento, exame, EPI, assinatura (005). \xC9 a lista que o painel cobra \u2014 "Pend\xEAncias cr\xEDticas" conta as n\xE3o resolvidas de prioridade alta ou urgente.';
|
|
2809
|
+
|
|
2810
|
+
CREATE INDEX IF NOT EXISTS plg_workforce_pending_abertas
|
|
2811
|
+
ON public.plg_workforce_pending_items (tenant_id, priority, due_on) WHERE resolved_at IS NULL;
|
|
2812
|
+
|
|
2813
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2814
|
+
-- 6. ESCOPO, CARIMBO E PERMISS\xC3O
|
|
2815
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2816
|
+
|
|
2817
|
+
DO $$
|
|
2818
|
+
DECLARE r record;
|
|
2819
|
+
BEGIN
|
|
2820
|
+
FOR r IN SELECT * FROM (VALUES
|
|
2821
|
+
('plg_workforce_employment', 'workforce_hr.employment'),
|
|
2822
|
+
('plg_workforce_vacations', 'workforce_hr.vacation'),
|
|
2823
|
+
('plg_workforce_absences', 'workforce_hr.absence'),
|
|
2824
|
+
('plg_workforce_pending_items', 'workforce_hr.pending_item')
|
|
2825
|
+
) AS t(tbl, resource)
|
|
2826
|
+
LOOP
|
|
2827
|
+
EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', r.tbl);
|
|
2828
|
+
EXECUTE format('ALTER TABLE public.%I FORCE ROW LEVEL SECURITY', r.tbl);
|
|
2829
|
+
EXECUTE format('DROP TRIGGER IF EXISTS %I ON public.%I', r.tbl || '_updated_at', r.tbl);
|
|
2830
|
+
EXECUTE format('CREATE TRIGGER %I BEFORE UPDATE ON public.%I FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at()',
|
|
2831
|
+
r.tbl || '_updated_at', r.tbl);
|
|
2832
|
+
-- \`workforce_hr\` e n\xE3o \`workforce\`: \xE9 o nome do plugin que o scaffold usa
|
|
2833
|
+
-- para derivar \`<plugin>.read/create/edit/delete\`, ent\xE3o passar outro nome
|
|
2834
|
+
-- aqui \xC9 a separa\xE7\xE3o de permiss\xE3o. Quem tem workforce.read v\xEA escala e
|
|
2835
|
+
-- ponto e n\xE3o v\xEA sal\xE1rio.
|
|
2836
|
+
PERFORM app.scaffold_table(format('public.%I', r.tbl)::regclass, r.resource, 'workforce_hr', false, 'enforce');
|
|
2837
|
+
END LOOP;
|
|
2838
|
+
END $$;
|
|
2839
|
+
|
|
2840
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2841
|
+
-- 7. O PONTO PASSA A SABER DO AFASTAMENTO
|
|
2842
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2843
|
+
--
|
|
2844
|
+
-- O defeito que o FLOW da vista 10 registra: *"N\xE3o conversa com o ponto
|
|
2845
|
+
-- eletr\xF4nico nem com a escala."* Duas telas sobre a mesma pessoa, no mesmo
|
|
2846
|
+
-- sistema, que n\xE3o se olham \u2014 quem confere ponto v\xEA FALTA em cima de um
|
|
2847
|
+
-- atestado que o RH lan\xE7ou na semana passada, e precisa lembrar de cabe\xE7a.
|
|
2848
|
+
--
|
|
2849
|
+
-- A view n\xE3o muda de forma: ganha uma coluna no fim. \`absent\` continua sendo
|
|
2850
|
+
-- escala sem ponto, porque \xE9 isso que ele \xE9; o que muda \xE9 que agora d\xE1 para
|
|
2851
|
+
-- dizer POR QUE, quando algu\xE9m sabe.
|
|
2852
|
+
|
|
2853
|
+
CREATE OR REPLACE VIEW public.v_workforce_day
|
|
2854
|
+
WITH (security_invoker = 'true') AS
|
|
2855
|
+
WITH bruto AS (
|
|
2856
|
+
SELECT e.tenant_id, e.unit_id, e.person_id, e.work_date,
|
|
2857
|
+
min(e.effective_at) FILTER (WHERE e.kind = 'clock_in') AS first_in,
|
|
2858
|
+
max(e.effective_at) FILTER (WHERE e.kind = 'clock_out') AS last_out,
|
|
2859
|
+
count(*) FILTER (WHERE e.kind = 'break_start') AS break_count,
|
|
2860
|
+
count(*) FILTER (WHERE e.kind IN ('clock_in', 'break_end')) AS aberturas,
|
|
2861
|
+
count(*) FILTER (WHERE e.kind IN ('clock_out', 'break_start')) AS fechamentos,
|
|
2862
|
+
max(e.effective_at) FILTER (WHERE e.kind IN ('clock_in', 'break_end')) AS ultima_abertura,
|
|
2863
|
+
sum(CASE
|
|
2864
|
+
WHEN e.kind IN ('clock_out', 'break_start') THEN extract(epoch FROM e.effective_at)
|
|
2865
|
+
WHEN e.kind IN ('clock_in', 'break_end') THEN -extract(epoch FROM e.effective_at)
|
|
2866
|
+
END) AS soma_com_sinal
|
|
2867
|
+
FROM public.v_workforce_time_entries e
|
|
2868
|
+
WHERE NOT e.voided
|
|
2869
|
+
GROUP BY e.tenant_id, e.unit_id, e.person_id, e.work_date
|
|
2870
|
+
),
|
|
2871
|
+
realized AS (
|
|
2872
|
+
SELECT b.tenant_id, b.unit_id, b.person_id, b.work_date,
|
|
2873
|
+
b.first_in, b.last_out, b.break_count,
|
|
2874
|
+
(b.aberturas <> b.fechamentos) AS is_open,
|
|
2875
|
+
CASE WHEN b.aberturas <> b.fechamentos THEN b.ultima_abertura END AS open_since,
|
|
2876
|
+
-- Devolve \xE0 soma a abertura sem par (003).
|
|
2877
|
+
(b.soma_com_sinal
|
|
2878
|
+
+ CASE WHEN b.aberturas <> b.fechamentos
|
|
2879
|
+
THEN extract(epoch FROM b.ultima_abertura)
|
|
2880
|
+
ELSE 0 END) AS worked_seconds
|
|
2881
|
+
FROM bruto b
|
|
2882
|
+
),
|
|
2883
|
+
planned AS (
|
|
2884
|
+
SELECT a.tenant_id, a.unit_id, a.person_id,
|
|
2885
|
+
s.schedule_date AS work_date,
|
|
2886
|
+
a.id AS assignment_id, a.role, a.status AS assignment_status,
|
|
2887
|
+
a.shift_label, a.shift_start_time, a.shift_end_time,
|
|
2888
|
+
(extract(epoch FROM a.shift_end_time - a.shift_start_time)
|
|
2889
|
+
+ CASE WHEN a.shift_end_time <= a.shift_start_time THEN 86400 ELSE 0 END) AS planned_seconds
|
|
2890
|
+
FROM public.plg_workforce_schedule_assignments a
|
|
2891
|
+
JOIN public.plg_workforce_schedules s ON s.id = a.schedule_id
|
|
2892
|
+
WHERE s.status IN ('published', 'closed')
|
|
2893
|
+
AND a.role = 'titular'
|
|
2894
|
+
AND a.shift_start_time IS NOT NULL
|
|
2895
|
+
AND a.shift_end_time IS NOT NULL
|
|
2896
|
+
)
|
|
2897
|
+
SELECT coalesce(p.tenant_id, r.tenant_id) AS tenant_id,
|
|
2898
|
+
coalesce(p.unit_id, r.unit_id) AS unit_id,
|
|
2899
|
+
coalesce(p.person_id, r.person_id) AS person_id,
|
|
2900
|
+
coalesce(p.work_date, r.work_date) AS work_date,
|
|
2901
|
+
p.assignment_id,
|
|
2902
|
+
p.shift_label,
|
|
2903
|
+
p.shift_start_time,
|
|
2904
|
+
p.shift_end_time,
|
|
2905
|
+
p.assignment_status,
|
|
2906
|
+
(p.planned_seconds / 60)::integer AS planned_minutes,
|
|
2907
|
+
(r.worked_seconds / 60)::integer AS worked_minutes,
|
|
2908
|
+
((r.worked_seconds - p.planned_seconds) / 60)::integer AS variance_minutes,
|
|
2909
|
+
r.first_in,
|
|
2910
|
+
r.last_out,
|
|
2911
|
+
r.break_count,
|
|
2912
|
+
coalesce(r.is_open, false) AS is_open,
|
|
2913
|
+
(r.person_id IS NULL) AS absent,
|
|
2914
|
+
(p.assignment_id IS NULL) AS outside_schedule,
|
|
2915
|
+
r.open_since,
|
|
2916
|
+
CASE
|
|
2917
|
+
WHEN r.person_id IS NULL THEN NULL
|
|
2918
|
+
WHEN r.is_open THEN 'open'
|
|
2919
|
+
WHEN (r.worked_seconds / 60) > public.workforce_max_shift_minutes()
|
|
2920
|
+
THEN 'span_too_long'
|
|
2921
|
+
END AS review_reason,
|
|
2922
|
+
-- Coluna nova, no fim. \`security_invoker\` na view + a policy de
|
|
2923
|
+
-- \`workforce_hr\` na tabela: quem n\xE3o tem a permiss\xE3o simplesmente n\xE3o
|
|
2924
|
+
-- enxerga o afastamento, e a coluna volta nula. A folha de ponto n\xE3o
|
|
2925
|
+
-- vira a porta dos fundos do CID.
|
|
2926
|
+
(SELECT a.kind
|
|
2927
|
+
FROM public.plg_workforce_absences a
|
|
2928
|
+
WHERE a.tenant_id = coalesce(p.tenant_id, r.tenant_id)
|
|
2929
|
+
AND a.person_id = coalesce(p.person_id, r.person_id)
|
|
2930
|
+
AND coalesce(p.work_date, r.work_date) BETWEEN a.starts_on
|
|
2931
|
+
AND coalesce(a.ends_on, coalesce(p.work_date, r.work_date))
|
|
2932
|
+
ORDER BY a.starts_on DESC
|
|
2933
|
+
LIMIT 1) AS absence_kind
|
|
2934
|
+
FROM planned p
|
|
2935
|
+
FULL OUTER JOIN realized r
|
|
2936
|
+
ON r.tenant_id = p.tenant_id
|
|
2937
|
+
AND r.person_id = p.person_id
|
|
2938
|
+
AND r.work_date = p.work_date;
|
|
2939
|
+
|
|
2940
|
+
COMMENT ON COLUMN public.v_workforce_day.absence_kind IS
|
|
2941
|
+
'O afastamento que cobre este dia, quando o RH lan\xE7ou um (005). \xC9 o conserto do defeito que o V1 tem: l\xE1 a falta lan\xE7ada no RH n\xE3o aparece no ponto, e quem confere v\xEA FALTA em cima de um atestado.';
|
|
2942
|
+
|
|
2943
|
+
GRANT SELECT ON public.v_workforce_day TO authenticated, service_role;
|
|
2944
|
+
|
|
2945
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2946
|
+
-- 8. A VIS\xC3O GERAL \u2014 os oito n\xFAmeros
|
|
2947
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2948
|
+
--
|
|
2949
|
+
-- Os mesmos oito cart\xF5es do V1, com tr\xEAs diferen\xE7as, e as tr\xEAs s\xE3o conserto de
|
|
2950
|
+
-- defeito registrado no FLOW:
|
|
2951
|
+
--
|
|
2952
|
+
-- V1-072 "Experi\xEAncia vencendo" s\xF3 olhava PROFISSIONAIS, porque o V1 tem
|
|
2953
|
+
-- duas tabelas. O V2 tem uma pessoa, ent\xE3o olha todo mundo.
|
|
2954
|
+
-- V1-073 "F\xE9rias a vencer" mostrava a data e N\xC3O o nome. Aqui a view devolve
|
|
2955
|
+
-- person_id e nome, e a tela linka para a ficha.
|
|
2956
|
+
-- V1-074 aniversariante do dia 1\xBA ca\xEDa no m\xEAs anterior, por compara\xE7\xE3o de
|
|
2957
|
+
-- data em UTC. \`extract(month \u2026)\` n\xE3o tem esse problema.
|
|
2958
|
+
|
|
2959
|
+
CREATE OR REPLACE VIEW public.v_workforce_hr_overview
|
|
2960
|
+
WITH (security_invoker = 'true') AS
|
|
2961
|
+
WITH base AS (
|
|
2962
|
+
SELECT e.tenant_id, e.person_id, e.admission_date, e.dismissal_date, e.probation_final_end,
|
|
2963
|
+
p.name, p.date_of_birth, p.is_active
|
|
2964
|
+
FROM public.plg_workforce_employment e
|
|
2965
|
+
JOIN public.people p ON p.id = e.person_id
|
|
2966
|
+
)
|
|
2967
|
+
SELECT b.tenant_id,
|
|
2968
|
+
count(*) FILTER (WHERE b.dismissal_date IS NULL AND coalesce(b.is_active, true)) AS headcount,
|
|
2969
|
+
count(*) FILTER (WHERE b.admission_date >= current_date - 30) AS admissions_30d,
|
|
2970
|
+
count(*) FILTER (WHERE b.dismissal_date >= current_date - 30) AS dismissals_30d,
|
|
2971
|
+
count(*) FILTER (WHERE b.probation_final_end BETWEEN current_date AND current_date + 60) AS probation_ending,
|
|
2972
|
+
count(*) FILTER (WHERE b.dismissal_date IS NULL
|
|
2973
|
+
AND extract(month FROM b.date_of_birth) = extract(month FROM current_date)) AS birthdays_this_month,
|
|
2974
|
+
(SELECT count(*) FROM public.plg_workforce_vacations v
|
|
2975
|
+
WHERE v.tenant_id = b.tenant_id AND v.status <> 'concluida'
|
|
2976
|
+
AND v.accrual_end BETWEEN current_date AND current_date + 60) AS vacations_due_60d,
|
|
2977
|
+
(SELECT count(*) FROM public.plg_workforce_pending_items i
|
|
2978
|
+
WHERE i.tenant_id = b.tenant_id AND i.resolved_at IS NULL
|
|
2979
|
+
AND i.priority IN ('alta', 'urgente')) AS critical_pending
|
|
2980
|
+
FROM base b
|
|
2981
|
+
GROUP BY b.tenant_id;
|
|
2982
|
+
|
|
2983
|
+
COMMENT ON VIEW public.v_workforce_hr_overview IS
|
|
2984
|
+
'Os oito n\xFAmeros do painel de RH do V1 (005). Turnover n\xE3o est\xE1 aqui de prop\xF3sito: \xE9 demiss\xF5es \xF7 headcount, e uma divis\xE3o que a tela faz \xE9 uma divis\xE3o que a tela pode formatar \u2014 a view devolve as duas parcelas.';
|
|
2985
|
+
|
|
2986
|
+
-- Quem vence, COM NOME \u2014 o V1-073 \xE9 justamente a lista sem nome.
|
|
2987
|
+
CREATE OR REPLACE VIEW public.v_workforce_hr_upcoming
|
|
2988
|
+
WITH (security_invoker = 'true') AS
|
|
2989
|
+
SELECT v.tenant_id, 'vacation'::text AS kind, v.person_id, p.name AS person_name,
|
|
2990
|
+
v.accrual_end AS due_on, v.status AS detail
|
|
2991
|
+
FROM public.plg_workforce_vacations v
|
|
2992
|
+
JOIN public.people p ON p.id = v.person_id
|
|
2993
|
+
WHERE v.status <> 'concluida' AND v.accrual_end BETWEEN current_date AND current_date + 60
|
|
2994
|
+
UNION ALL
|
|
2995
|
+
SELECT e.tenant_id, 'probation', e.person_id, p.name,
|
|
2996
|
+
e.probation_final_end, e.job_title
|
|
2997
|
+
FROM public.plg_workforce_employment e
|
|
2998
|
+
JOIN public.people p ON p.id = e.person_id
|
|
2999
|
+
WHERE e.probation_final_end BETWEEN current_date AND current_date + 60
|
|
3000
|
+
UNION ALL
|
|
3001
|
+
SELECT i.tenant_id, 'pending', i.person_id, p.name, i.due_on, i.title
|
|
3002
|
+
FROM public.plg_workforce_pending_items i
|
|
3003
|
+
JOIN public.people p ON p.id = i.person_id
|
|
3004
|
+
WHERE i.resolved_at IS NULL AND i.priority IN ('alta', 'urgente');
|
|
3005
|
+
|
|
3006
|
+
COMMENT ON VIEW public.v_workforce_hr_upcoming IS
|
|
3007
|
+
'O que vence nos pr\xF3ximos 60 dias, com o NOME de quem (005). O V1 lista f\xE9rias a vencer s\xF3 com a data \u2014 V1-073 \u2014, o que obriga a procurar a pessoa em outra tela.';
|
|
3008
|
+
|
|
3009
|
+
GRANT SELECT ON public.v_workforce_hr_overview TO authenticated, service_role;
|
|
3010
|
+
GRANT SELECT ON public.v_workforce_hr_upcoming TO authenticated, service_role;
|
|
3011
|
+
`;
|
|
3012
|
+
var MIGRATION_006_O_CUSTO_DE_PESSOAL_E_UM_RAMO_DO_PLANO = `-- O custo de pessoal \xE9 um RAMO DO PLANO, e quem o aponta \xE9 a casa.
|
|
3013
|
+
--
|
|
3014
|
+
-- \u2500\u2500 O defeito, medido \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3015
|
+
--
|
|
3016
|
+
-- A tela "Encargos e honor\xE1rios" (RH \u2192 Custos) l\xEA o raz\xE3o do Financeiro e soma
|
|
3017
|
+
-- toda linha de conta a pagar que tenha conta cont\xE1bil. Toda. Medido no cluster
|
|
3018
|
+
-- de Pertinho do C\xE9u em 19/09/2026, o que ela chamava de custo de pessoal
|
|
3019
|
+
-- inclu\xEDa:
|
|
3020
|
+
--
|
|
3021
|
+
-- 6.2.1 Aluguel R$ 21.061,99
|
|
3022
|
+
-- 6.2.3 IPTU R$ 26.613,73
|
|
3023
|
+
-- 6.5.5 ICMS R$ 30.964,92
|
|
3024
|
+
-- 6.4.1 Equipamentos R$ 82.595,87
|
|
3025
|
+
-- 6.3.1 Energia/Light R$ 27.185,62
|
|
3026
|
+
--
|
|
3027
|
+
-- \u2014 ao lado das que s\xE3o, de fato, pessoal:
|
|
3028
|
+
--
|
|
3029
|
+
-- 5 DESPESAS COM PESSOAL (26 contas: Sal\xE1rio, FGTS, INSS, Comiss\xE3o,
|
|
3030
|
+
-- Hora Extra, Adiantamento, IRRF Folha, Vale Alimenta\xE7\xE3o, FREELANCER
|
|
3031
|
+
-- com oito filhas\u2026)
|
|
3032
|
+
--
|
|
3033
|
+
-- Um total que soma aluguel e ICMS sob o t\xEDtulo "custo de pessoal" n\xE3o est\xE1
|
|
3034
|
+
-- impreciso: est\xE1 errado, e erra para cima, que \xE9 o lado que faz algu\xE9m decidir
|
|
3035
|
+
-- contratar menos.
|
|
3036
|
+
--
|
|
3037
|
+
-- \u2500\u2500 Por que uma configura\xE7\xE3o, e n\xE3o uma regra \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3038
|
+
--
|
|
3039
|
+
-- N\xE3o existe como o c\xF3digo saber qual ramo \xE9 pessoal. O plano de contas \xE9 uma
|
|
3040
|
+
-- DECLARA\xC7\xC3O da casa (ver \`040_o_demonstrativo_mostra_o_plano_inteiro\`), e cada
|
|
3041
|
+
-- casa numera o dela: aqui pessoal \xE9 o ramo \`5\`; o template que o SDK semeia
|
|
3042
|
+
-- para um tenant novo p\xF5e folha em \`4.03\`. Deduzir pelo NOME da conta seria
|
|
3043
|
+
-- adivinhar em portugu\xEAs \u2014 e \`8.2.4 Rescis\xE3o\` e \`8.2.6 Pro-labore\`, que tamb\xE9m
|
|
3044
|
+
-- s\xE3o gente, moram fora do ramo 5 neste mesmo plano, por escolha de quem montou.
|
|
3045
|
+
--
|
|
3046
|
+
-- Ent\xE3o a chave guarda a escolha, e a aus\xEAncia dela \xE9 uma aus\xEAncia honesta: sem
|
|
3047
|
+
-- nada escolhido a tela mostra TODAS as despesas classificadas e diz que \xE9 o que
|
|
3048
|
+
-- est\xE1 fazendo. Um padr\xE3o inventado ('5') faria a tela mentir com convic\xE7\xE3o no
|
|
3049
|
+
-- primeiro tenant cujo plano n\xE3o usa esse n\xFAmero.
|
|
3050
|
+
--
|
|
3051
|
+
-- \u2500\u2500 A forma \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3052
|
+
--
|
|
3053
|
+
-- Um array de C\xD3DIGOS, n\xE3o de ids. O c\xF3digo \xE9 o que carrega a hierarquia: quem
|
|
3054
|
+
-- escolhe '5' escolhe o ramo inteiro \u2014 '5.4' e '5.4.1' entram com ele, e uma
|
|
3055
|
+
-- conta nova criada amanh\xE3 dentro do ramo entra sozinha. Com id seria preciso
|
|
3056
|
+
-- re-marcar a configura\xE7\xE3o a cada conta nova, e ningu\xE9m lembra.
|
|
3057
|
+
--
|
|
3058
|
+
-- \`tenant_setting_set\` cobra \`settings.manage\` e recusa chave n\xE3o registrada \u2014
|
|
3059
|
+
-- \xE9 por isso que este arquivo existe. Idempotente: \`seed_setting_keys\` faz
|
|
3060
|
+
-- upsert por chave.
|
|
3061
|
+
|
|
3062
|
+
SELECT app.seed_setting_keys($$[
|
|
3063
|
+
{
|
|
3064
|
+
"key": "workforce.payroll_account_codes", "type": "array", "default": null,
|
|
3065
|
+
"reader": "plugin:workforce",
|
|
3066
|
+
"description": "Os ramos do plano de contas que s\xE3o custo de pessoal, por c\xF3digo ('5', '8.2.4'). Um c\xF3digo traz toda a descend\xEAncia dele. Vazio ou nulo = a tela mostra todas as despesas classificadas e avisa que n\xE3o h\xE1 ramo escolhido."
|
|
3067
|
+
}
|
|
3068
|
+
]$$::jsonb);
|
|
3069
|
+
`;
|
|
3070
|
+
var MIGRATION_007_A_PORTA_DE_SERVICO_DO_PONTO_FECHA_TAMBEM = `-- ---------------------------------------------------------------------------
|
|
3071
|
+
-- 007_a_porta_de_servico_do_ponto_fecha_tambem.sql
|
|
3072
|
+
--
|
|
3073
|
+
-- \u2500\u2500 o buraco, e por que ele estava invis\xEDvel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3074
|
+
--
|
|
3075
|
+
-- As fun\xE7\xF5es que este plugin cria em \`migration\` nasceram execut\xE1veis por
|
|
3076
|
+
-- PUBLIC \u2014 \xE9 o padr\xE3o do Postgres, e ningu\xE9m revogou. Elas s\xE3o
|
|
3077
|
+
-- \`SECURITY DEFINER\` e escrevem dados de QUALQUER inquilino: uma delas
|
|
3078
|
+
-- alcan\xE7\xE1vel pela chave an\xF4nima \xE9 a base inteira alcan\xE7\xE1vel pela chave
|
|
3079
|
+
-- an\xF4nima.
|
|
3080
|
+
--
|
|
3081
|
+
-- Isto n\xE3o aparecia porque a \`000_baseline\` falhava e levava junto os outros
|
|
3082
|
+
-- seis arquivos deste plugin \u2014 as fun\xE7\xF5es nunca chegavam a existir na bancada.
|
|
3083
|
+
-- Com a baseline consertada (#443) o passo inteiro passou a aplicar, e as duas
|
|
3084
|
+
-- su\xEDtes que guardam esta fronteira ficaram vermelhas na hora:
|
|
3085
|
+
--
|
|
3086
|
+
-- 090_migration_ledger/001 "no migration function is executable by anon
|
|
3087
|
+
-- or authenticated" \u2014 have 9, want 0
|
|
3088
|
+
-- crm/009 "nenhuma fun\xE7\xE3o do schema migration \xE9 cham\xE1vel
|
|
3089
|
+
-- por anon" \u2014 have 9, want 0
|
|
3090
|
+
--
|
|
3091
|
+
-- Ou seja: a regra sempre esteve escrita e medida; o que faltava era o plugin
|
|
3092
|
+
-- chegar ao banco para ser medido. \xC9 o argumento do #338 de novo \u2014 um plugin
|
|
3093
|
+
-- fora da corrente \xE9 um plugin cujas tabelas (e cujas portas) s\xE3o coincid\xEAncia.
|
|
3094
|
+
--
|
|
3095
|
+
-- \u2500\u2500 por extenso, e n\xE3o varrendo o cat\xE1logo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3096
|
+
--
|
|
3097
|
+
-- A tenta\xE7\xE3o \xE9 \`REVOKE\` num la\xE7o sobre \`pg_proc\`. J\xE1 foi tentado neste repo e
|
|
3098
|
+
-- derrubou 35 su\xEDtes: varrer revoga de fun\xE7\xF5es que outros passos criaram e
|
|
3099
|
+
-- precisam de execu\xE7\xE3o. Revogar em massa \xE9 f\xE1cil de escrever e imposs\xEDvel de
|
|
3100
|
+
-- revisar. A lista abaixo \xE9 a das fun\xE7\xF5es DESTE plugin, nomeada uma a uma.
|
|
3101
|
+
-- ---------------------------------------------------------------------------
|
|
3102
|
+
|
|
3103
|
+
DO $$
|
|
3104
|
+
DECLARE
|
|
3105
|
+
v_fn text;
|
|
3106
|
+
v_args text;
|
|
3107
|
+
v_n integer := 0;
|
|
3108
|
+
BEGIN
|
|
3109
|
+
-- Cada writer do de-para, com a assinatura que a corrente lhe deu. Nomear em
|
|
3110
|
+
-- vez de varrer \xE9 o que torna esta lista revis\xE1vel.
|
|
3111
|
+
FOR v_fn, v_args IN
|
|
3112
|
+
SELECT p.proname, pg_get_function_identity_arguments(p.oid)
|
|
3113
|
+
FROM pg_proc p
|
|
3114
|
+
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
3115
|
+
WHERE n.nspname = 'migration'
|
|
3116
|
+
AND (p.proname LIKE 'upsert_wf\\_%' OR p.proname IN ('_wf_person', '_point_punch_photo'))
|
|
3117
|
+
LOOP
|
|
3118
|
+
EXECUTE format('REVOKE ALL ON FUNCTION migration.%I(%s) FROM PUBLIC, anon, authenticated', v_fn, v_args);
|
|
3119
|
+
-- \`service_role\` fica: \xE9 quem a importa\xE7\xE3o usa, e \xE9 a \xFAnica identidade que
|
|
3120
|
+
-- tem raz\xE3o para atravessar inquilinos.
|
|
3121
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION migration.%I(%s) TO service_role', v_fn, v_args);
|
|
3122
|
+
v_n := v_n + 1;
|
|
3123
|
+
END LOOP;
|
|
3124
|
+
|
|
3125
|
+
RAISE NOTICE 'workforce/007: % fun\xE7\xE3o(\xF5es) de migration fechadas a anon e authenticated', v_n;
|
|
3126
|
+
END $$;
|
|
3127
|
+
`;
|
|
3128
|
+
var MIGRATIONS = [
|
|
3129
|
+
{ id: "000_baseline", sql: MIGRATION_000_BASELINE },
|
|
3130
|
+
{ id: "001_a_prova_da_batida_tem_bytes", sql: MIGRATION_001_A_PROVA_DA_BATIDA_TEM_BYTES },
|
|
3131
|
+
{ id: "002_o_guarda_da_foto_olhava_o_nome_errado", sql: MIGRATION_002_O_GUARDA_DA_FOTO_OLHAVA_O_NOME_ERRADO },
|
|
3132
|
+
{ id: "003_o_ponto_em_aberto_para_de_mentir_a_duracao", sql: MIGRATION_003_O_PONTO_EM_ABERTO_PARA_DE_MENTIR_A_DURACAO },
|
|
3133
|
+
{ id: "004_a_jornada_implausivel_nao_soma", sql: MIGRATION_004_A_JORNADA_IMPLAUSIVEL_NAO_SOMA },
|
|
3134
|
+
{ id: "005_a_ficha_trabalhista_tem_onde_morar", sql: MIGRATION_005_A_FICHA_TRABALHISTA_TEM_ONDE_MORAR },
|
|
3135
|
+
{ id: "006_o_custo_de_pessoal_e_um_ramo_do_plano", sql: MIGRATION_006_O_CUSTO_DE_PESSOAL_E_UM_RAMO_DO_PLANO },
|
|
3136
|
+
{ id: "007_a_porta_de_servico_do_ponto_fecha_tambem", sql: MIGRATION_007_A_PORTA_DE_SERVICO_DO_PONTO_FECHA_TAMBEM }
|
|
3137
|
+
];
|
|
3138
|
+
|
|
3139
|
+
export { MIGRATIONS, MIGRATION_000_BASELINE, MIGRATION_001_A_PROVA_DA_BATIDA_TEM_BYTES, MIGRATION_002_O_GUARDA_DA_FOTO_OLHAVA_O_NOME_ERRADO, MIGRATION_003_O_PONTO_EM_ABERTO_PARA_DE_MENTIR_A_DURACAO, MIGRATION_004_A_JORNADA_IMPLAUSIVEL_NAO_SOMA, MIGRATION_005_A_FICHA_TRABALHISTA_TEM_ONDE_MORAR, MIGRATION_006_O_CUSTO_DE_PESSOAL_E_UM_RAMO_DO_PLANO, MIGRATION_007_A_PORTA_DE_SERVICO_DO_PONTO_FECHA_TAMBEM };
|
|
3140
|
+
//# sourceMappingURL=main.js.map
|
|
3141
|
+
//# sourceMappingURL=main.js.map
|