@fayz-ai/plugin-workforce 0.1.1 → 0.1.3

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.
@@ -5,6 +5,7 @@ export declare const MIGRATION_003_O_PONTO_EM_ABERTO_PARA_DE_MENTIR_A_DURACAO =
5
5
  export declare const MIGRATION_004_A_JORNADA_IMPLAUSIVEL_NAO_SOMA = "-- ---------------------------------------------------------------------------\n-- 004_a_jornada_implausivel_nao_soma.sql \u2014 45h45 num dia.\n--\n-- Medido em 19/09/2026 no tenant Pertinho do C\u00E9u, na tela de Registros de Ponto:\n--\n-- Steffany Fidelis da Costa 15:41 \u2192 13:26 45h45\n-- CRISTIANE CARDOSO OLIVEIRA 17:14 \u2192 09:23 16h10\n-- jannyne gomes dos santos 09:07 \u2192 08:58 15h56\n--\n-- e o cabe\u00E7alho do dia somando **109h45** para oito pessoas.\n--\n-- Nenhum desses \u00E9 erro de conta: a 003 j\u00E1 consertou a soma, e esses dias est\u00E3o\n-- FECHADOS. Eles s\u00E3o a medi\u00E7\u00E3o correta de um par errado \u2014 algu\u00E9m entrou, foi\n-- embora sem bater a sa\u00EDda, e a sa\u00EDda caiu horas ou dias depois. O V1 grava\n-- `checkin_at` e `checkout_at` como um par s\u00F3, ent\u00E3o o buraco atravessou inteiro.\n--\n-- \u2500\u2500 Por que n\u00E3o 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\n--\n-- Trocar 45h45 por 8h faria o total parecer certo. \u00C9 o pior resultado poss\u00EDvel:\n--\n-- \u00B7 8h ali \u00E9 um n\u00FAmero que ningu\u00E9m trabalhou e ningu\u00E9m digitou \u2014 \u00E9 a regra\n-- \"nada de default inventado\" quebrada no lugar onde ela mais custa, que \u00E9\n-- base de c\u00E1lculo de folha;\n-- \u00B7 e o dia deixaria de parecer estranho, ent\u00E3o a batida furada nunca seria\n-- corrigida. O erro sai da tela e fica no dado, que \u00E9 o contr\u00E1rio de\n-- resolver.\n--\n-- \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\n--\n-- Estende a regra que a tela J\u00C1 tem. Ponto em aberto n\u00E3o soma, porque contar\n-- uma jornada que n\u00E3o terminou inventa hora. Um dia de 45h \u00E9 o mesmo problema\n-- com outra cara \u2014 um par em que n\u00E3o se pode confiar \u2014, ent\u00E3o ele tamb\u00E9m n\u00E3o\n-- soma. O valor medido continua \u00E0 vista, inteiro, com o motivo escrito ao lado.\n--\n-- `review_reason` nomeia por que o dia n\u00E3o entra na conta:\n--\n-- 'open' ainda n\u00E3o terminou\n-- 'span_too_long' fechou, e fechou com dura\u00E7\u00E3o que n\u00E3o \u00E9 uma jornada\n--\n-- NULL \u00E9 o dia que conta. Uma coluna s\u00F3, e quem soma filtra por ela \u2014 em vez de\n-- cada relat\u00F3rio redescobrir a regra e descobrir diferente.\n--\n-- \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\n--\n-- A jornada mais longa do vocabul\u00E1rio do V1 \u00E9 a escala 12x36: doze horas. Com\n-- duas de extra, quatorze. Dezesseis deixa margem para o turno que vira a noite\n-- e ainda separa com folga o caso real, que n\u00E3o \u00E9 uma jornada comprida e sim\n-- uma sa\u00EDda que ficou pendurada \u2014 os tr\u00EAs medidos acima t\u00EAm 16h, 16h e 46h.\n--\n-- \u00C9 o MESMO dezesseis que a tela usa para dizer \"sem sa\u00EDda h\u00E1 31h\" num ponto\n-- aberto, e \u00E9 de prop\u00F3sito: um n\u00FAmero, uma justificativa, os dois lados da\n-- mesma pergunta.\n--\n-- Idempotente e replay-safe.\n-- ---------------------------------------------------------------------------\n\nCREATE OR REPLACE FUNCTION public.workforce_max_shift_minutes()\nRETURNS integer LANGUAGE sql IMMUTABLE PARALLEL SAFE\nAS $$ SELECT 16 * 60 $$;\n\nCOMMENT ON FUNCTION public.workforce_max_shift_minutes() IS\n 'Acima disto uma jornada fechada deixa de ser jornada e vira sa\u00EDda n\u00E3o batida (004). 12x36 \u00E9 a mais longa do vocabul\u00E1rio do V1; dezesseis d\u00E1 margem ao turno que vira a noite. Fun\u00E7\u00E3o e n\u00E3o constante literal para que a tela, a view e um relat\u00F3rio futuro citem o mesmo n\u00FAmero.';\n\nCREATE OR REPLACE VIEW public.v_workforce_day\nWITH (security_invoker = 'true') AS\nWITH bruto AS (\n SELECT e.tenant_id,\n e.unit_id,\n e.person_id,\n e.work_date,\n min(e.effective_at) FILTER (WHERE e.kind = 'clock_in') AS first_in,\n max(e.effective_at) FILTER (WHERE e.kind = 'clock_out') AS last_out,\n count(*) FILTER (WHERE e.kind = 'break_start') AS break_count,\n count(*) FILTER (WHERE e.kind IN ('clock_in', 'break_end')) AS aberturas,\n count(*) FILTER (WHERE e.kind IN ('clock_out', 'break_start')) AS fechamentos,\n -- A \u00FAltima ABERTURA do dia. Quando as contagens n\u00E3o batem, \u00E9 ela que\n -- ficou sem par \u2014 os eventos de um dia chegam em ordem, e uma abertura\n -- anterior j\u00E1 teria sido fechada pela seguinte.\n max(e.effective_at) FILTER (WHERE e.kind IN ('clock_in', 'break_end')) AS ultima_abertura,\n sum(CASE\n WHEN e.kind IN ('clock_out', 'break_start') THEN extract(epoch FROM e.effective_at)\n WHEN e.kind IN ('clock_in', 'break_end') THEN -extract(epoch FROM e.effective_at)\n END) AS soma_com_sinal\n FROM public.v_workforce_time_entries e\n WHERE NOT e.voided\n GROUP BY e.tenant_id, e.unit_id, e.person_id, e.work_date\n),\nrealized AS (\n SELECT b.tenant_id,\n b.unit_id,\n b.person_id,\n b.work_date,\n b.first_in,\n b.last_out,\n b.break_count,\n (b.aberturas <> b.fechamentos) AS is_open,\n CASE WHEN b.aberturas <> b.fechamentos THEN b.ultima_abertura END AS open_since,\n -- Devolve \u00E0 soma a abertura sem par (003). Sem isto o que sobra \u00E9 o\n -- epoch dela, com sinal trocado.\n (b.soma_com_sinal\n + CASE WHEN b.aberturas <> b.fechamentos\n THEN extract(epoch FROM b.ultima_abertura)\n ELSE 0 END) AS worked_seconds\n FROM bruto b\n),\nplanned AS (\n SELECT a.tenant_id,\n a.unit_id,\n a.person_id,\n s.schedule_date AS work_date,\n a.id AS assignment_id,\n a.role,\n a.status AS assignment_status,\n a.shift_label,\n a.shift_start_time,\n a.shift_end_time,\n -- O turno que vira o dia \u00E9 normal aqui; sem o +24h a jornada da noite\n -- daria negativa.\n (extract(epoch FROM a.shift_end_time - a.shift_start_time)\n + CASE WHEN a.shift_end_time <= a.shift_start_time THEN 86400 ELSE 0 END) AS planned_seconds\n FROM public.plg_workforce_schedule_assignments a\n JOIN public.plg_workforce_schedules s ON s.id = a.schedule_id\n WHERE s.status IN ('published', 'closed')\n AND a.role = 'titular'\n AND a.shift_start_time IS NOT NULL\n AND a.shift_end_time IS NOT NULL\n)\nSELECT coalesce(p.tenant_id, r.tenant_id) AS tenant_id,\n coalesce(p.unit_id, r.unit_id) AS unit_id,\n coalesce(p.person_id, r.person_id) AS person_id,\n coalesce(p.work_date, r.work_date) AS work_date,\n p.assignment_id,\n p.shift_label,\n p.shift_start_time,\n p.shift_end_time,\n p.assignment_status,\n (p.planned_seconds / 60)::integer AS planned_minutes,\n -- O MEDIDO, sempre. N\u00E3o \u00E9 aqui que se decide se ele conta \u2014 a coluna\n -- mente se for corrigida, e um n\u00FAmero corrigido em sil\u00EAncio \u00E9 pior que\n -- um n\u00FAmero estranho.\n (r.worked_seconds / 60)::integer AS worked_minutes,\n ((r.worked_seconds - p.planned_seconds) / 60)::integer AS variance_minutes,\n r.first_in,\n r.last_out,\n r.break_count,\n coalesce(r.is_open, false) AS is_open,\n (r.person_id IS NULL) AS absent,\n (p.assignment_id IS NULL) AS outside_schedule,\n r.open_since,\n -- Por que este dia n\u00E3o entra na soma. NULL = entra.\n CASE\n WHEN r.person_id IS NULL THEN NULL -- escala sem ponto: n\u00E3o h\u00E1 o que somar\n WHEN r.is_open THEN 'open'\n WHEN (r.worked_seconds / 60) > public.workforce_max_shift_minutes()\n THEN 'span_too_long'\n END AS review_reason\n FROM planned p\n FULL OUTER JOIN realized r\n ON r.tenant_id = p.tenant_id\n AND r.person_id = p.person_id\n AND r.work_date = p.work_date;\n\nCOMMENT ON VIEW public.v_workforce_day IS\n 'Jornada prevista \u00D7 realizada por pessoa e dia. `worked_minutes` \u00E9 sempre o MEDIDO; `review_reason` diz por que um dia n\u00E3o entra na soma \u2014 `open` (n\u00E3o terminou) ou `span_too_long` (fechou com dura\u00E7\u00E3o que n\u00E3o \u00E9 jornada, 004). Quem soma filtra por ela.';\n\nCOMMENT ON COLUMN public.v_workforce_day.review_reason IS\n 'NULL = o dia conta. `open` = ponto em aberto. `span_too_long` = acima de workforce_max_shift_minutes(), que na pr\u00E1tica \u00E9 sa\u00EDda n\u00E3o batida e n\u00E3o jornada longa (004).';\n\nGRANT SELECT ON public.v_workforce_day TO authenticated, service_role;\n";
6
6
  export declare const MIGRATION_005_A_FICHA_TRABALHISTA_TEM_ONDE_MORAR = "-- ---------------------------------------------------------------------------\n-- 005_a_ficha_trabalhista_tem_onde_morar.sql \u2014 o RH do V1 ganha destino.\n--\n-- Escrito a partir de `~/dev/beautyplace/docs/v1-guide/rh`, que documenta o\n-- m\u00F3dulo do V1 andando nele: a Vis\u00E3o Geral (oito cart\u00F5es, tr\u00EAs listas) e a Ficha\n-- de RH da pessoa (dezesseis vistas). O FLOW de cada um diz `v2: status:\n-- missing` \u2014 e dizia certo: n\u00E3o havia nada disso aqui.\n--\n-- \u2500\u2500 O que este arquivo N\u00C3O traz, e por qu\u00EA \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- N\u00E3o s\u00E3o as dezesseis vistas. Medido na origem em 19/09/2026, nas 886 pessoas\n-- dos tr\u00EAs tenants em porte:\n--\n-- as 12 tabelas de lista da Ficha 0 linhas \u2014 no V1 INTEIRO, n\u00E3o s\u00F3\n-- (f\u00E9rias, faltas, advert\u00EAncias, nesses tenants\n-- treinamentos, avalia\u00E7\u00F5es, exames,\n-- EPIs, pend\u00EAncias, dependentes,\n-- hist\u00F3rico salarial, 13\u00BA, benef\u00EDcios)\n-- admission_date, dismissal_date, 0 preenchidos\n-- job_title, pis_pasep, CTPS, eSocial\n-- base_salary 25 de 422\n-- contract_type 2\n--\n-- O pr\u00F3prio FLOW da Vis\u00E3o Geral avisa: *\"Num tenant rec\u00E9m-migrado ele mostra\n-- headcount cheio e todo o resto zerado \u2014 e isso n\u00E3o \u00E9 defeito, \u00E9 ficha vazia.\"*\n-- Aqui TODO tenant \u00E9 assim. Desenhar dezesseis vistas para portar zero linha\n-- seria construir um formul\u00E1rio de cada vez que algu\u00E9m leu um FLOW.\n--\n-- Ent\u00E3o vem o n\u00FAcleo que sustenta o conceito e j\u00E1 \u00E9 \u00FAtil no primeiro dia:\n--\n-- employment o v\u00EDnculo \u2014 \u00E9 dele que saem headcount, admiss\u00F5es,\n-- demiss\u00F5es, turnover e experi\u00EAncia vencendo. Sem ele\n-- nenhum n\u00FAmero de RH existe, e \u00E9 a vista 1 do V1\n-- vacations f\u00E9rias. Obriga\u00E7\u00E3o legal, recorrente, e a \u00FAnica lista do\n-- V1 que alimenta um cart\u00E3o\n-- absences faltas e afastamentos\n-- pending_items pend\u00EAncias \u2014 a outra lista que aparece no painel\n--\n-- As outras doze entram quando algu\u00E9m for us\u00E1-las. O que elas precisam j\u00E1 est\u00E1\n-- de p\u00E9: a forma da tabela, o escopo, a permiss\u00E3o e a view do painel.\n--\n-- \u2500\u2500 A permiss\u00E3o, 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\n--\n-- V1-069: no V1 **n\u00E3o h\u00E1 permiss\u00E3o separada para RH**. Quem edita cadastro v\u00EA\n-- sal\u00E1rio, CPF, dependentes e CID de atestado. O FLOW da Ficha fecha dizendo que\n-- portar s\u00F3 vale \"desde que a quest\u00E3o da permiss\u00E3o separada seja resolvida\n-- antes\" \u2014 ent\u00E3o ela \u00E9 resolvida aqui, e n\u00E3o depois.\n--\n-- `workforce_hr.*` \u00E9 uma fam\u00EDlia pr\u00F3pria: quem tem `workforce.read` v\u00EA escala e\n-- ponto e N\u00C3O v\u00EA sal\u00E1rio. S\u00E3o duas perguntas diferentes e agora t\u00EAm duas\n-- respostas.\n--\n-- \u2500\u2500 O que o V2 faz e o V1 n\u00E3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- O FLOW da vista 10 registra, como defeito: *\"N\u00E3o conversa com o ponto\n-- eletr\u00F4nico nem com a escala: uma falta lan\u00E7ada aqui n\u00E3o aparece l\u00E1.\"* No V1\n-- s\u00E3o dois sistemas sobre a mesma pessoa que n\u00E3o se olham \u2014 algu\u00E9m de atestado\n-- aparece como FALTA na folha de ponto, e quem confere precisa lembrar de cabe\u00E7a.\n--\n-- A \u00A77 conserta: `v_workforce_day` passa a saber do afastamento. Um dia coberto\n-- por atestado deixa de ser \"Falta\" e passa a dizer o que foi. \u00C9 a raz\u00E3o de o RH\n-- morar no mesmo plugin do ponto, e n\u00E3o ao lado dele.\n--\n-- Idempotente e replay-safe.\n-- ---------------------------------------------------------------------------\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 1. A PERMISS\u00C3O SEPARADA (V1-069)\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nINSERT INTO app.permissions (key, plugin, description) VALUES\n ('workforce_hr.read', 'workforce_hr', 'Ver a ficha trabalhista: v\u00EDnculo, sal\u00E1rio, f\u00E9rias, afastamentos e pend\u00EAncias.'),\n ('workforce_hr.create', 'workforce_hr', 'Lan\u00E7ar na ficha trabalhista.'),\n ('workforce_hr.edit', 'workforce_hr', 'Corrigir lan\u00E7amento da ficha trabalhista.'),\n ('workforce_hr.delete', 'workforce_hr', 'Excluir lan\u00E7amento da ficha trabalhista.')\nON CONFLICT (key) DO UPDATE SET plugin = EXCLUDED.plugin, description = EXCLUDED.description;\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 2. O V\u00CDNCULO\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n--\n-- Uma linha por pessoa, e n\u00E3o colunas em `public.people`. Tr\u00EAs raz\u00F5es, e a\n-- terceira \u00E9 a que decide:\n--\n-- \u00B7 `people` \u00E9 do core e atende cliente, fornecedor e profissional; quarenta\n-- colunas de CLT ali seriam nulas para a esmagadora maioria das linhas;\n-- \u00B7 o V1 tem DUAS tabelas (professionals, employees) com os mesmos campos, e\n-- o V2 tem uma pessoa s\u00F3 \u2014 a separa\u00E7\u00E3o \u00E9 papel, n\u00E3o cadastro;\n-- \u00B7 e \u00E9 a tabela que carrega o sal\u00E1rio. Ela precisa de uma policy PR\u00D3PRIA,\n-- e uma coluna n\u00E3o tem policy: RLS \u00E9 por tabela. Guardar sal\u00E1rio em\n-- `people` seria reproduzir o V1-069 no schema.\n\nCREATE TABLE IF NOT EXISTS public.plg_workforce_employment (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n unit_id uuid,\n person_id uuid NOT NULL REFERENCES public.people(id) ON DELETE CASCADE,\n\n -- \u2500\u2500 v\u00EDnculo (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\n -- Vocabul\u00E1rio exato do FLOW. Lista fechada porque \u00E9 ela que decide encargo, e\n -- \"CLT \" com espa\u00E7o vira um tipo de contrato novo em sil\u00EAncio.\n contract_kind text CHECK (contract_kind IN\n ('clt', 'pj', 'estagio', 'jovem_aprendiz', 'autonomo', 'socio', 'temporario', 'intermitente')),\n admission_date date,\n dismissal_date date,\n dismissal_reason text,\n -- O V1 tem os dois: o fim da PRIMEIRA experi\u00EAncia e o fim FINAL. \u00C9 o segundo\n -- que alimenta o cart\u00E3o \"Experi\u00EAncia vencendo\".\n probation_first_end date,\n probation_final_end date,\n job_title text,\n department text,\n schedule_regime text CHECK (schedule_regime IN\n ('mensalista', 'horista', 'diarista', 'escala_12x36', 'escala_6x1', 'escala_5x2')),\n weekly_hours numeric(5,2),\n monthly_hours numeric(6,2),\n esocial_registration text,\n\n -- \u2500\u2500 remunera\u00E7\u00E3o (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\n -- Valor de REFER\u00CANCIA, e o V1 diz isso com todas as letras: nada aqui vira\n -- conta a pagar, comiss\u00E3o ou desconto. O coment\u00E1rio fica porque a coluna\n -- chamada `base_salary` convida a supor o contr\u00E1rio.\n base_salary numeric(14,2),\n pay_frequency text CHECK (pay_frequency IN ('mensal', 'quinzenal', 'semanal', 'diario', 'por_hora')),\n -- Os quinze aux\u00EDlios e adicionais da vista 2 em jsonb, e n\u00E3o em quinze\n -- colunas. Medido: ZERO deles preenchido nas 886 pessoas. Quinze colunas\n -- nulas seriam quinze decis\u00F5es de schema tomadas sem um caso de uso; a chave\n -- que a casa usar aparece aqui e vira coluna quando pagar a passagem.\n allowances jsonb NOT NULL DEFAULT '{}'::jsonb,\n\n notes text,\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n -- Um v\u00EDnculo por pessoa. Recontrata\u00E7\u00E3o reabre o mesmo, com a data nova \u2014 o\n -- hist\u00F3rico de quem saiu e voltou \u00E9 uma vista de lista que ainda n\u00E3o existe,\n -- e inventar a segunda linha agora criaria dois headcounts para uma pessoa.\n CONSTRAINT plg_workforce_employment_person UNIQUE (tenant_id, person_id),\n -- Demiss\u00E3o antes da admiss\u00E3o n\u00E3o \u00E9 dado ruim, \u00E9 dado imposs\u00EDvel.\n CONSTRAINT plg_workforce_employment_dates\n CHECK (dismissal_date IS NULL OR admission_date IS NULL OR dismissal_date >= admission_date)\n);\n\nCOMMENT ON TABLE public.plg_workforce_employment IS\n 'O v\u00EDnculo trabalhista de uma pessoa (005). \u00C9 dele que saem headcount, admiss\u00F5es, demiss\u00F5es, turnover e experi\u00EAncia vencendo. Tabela pr\u00F3pria e n\u00E3o colunas em people: RLS \u00E9 por tabela, e \u00E9 o sal\u00E1rio que precisa da policy.';\nCOMMENT ON COLUMN public.plg_workforce_employment.base_salary IS\n 'Valor de REFER\u00CANCIA. N\u00E3o vira conta a pagar, comiss\u00E3o nem desconto \u2014 o V1 tamb\u00E9m n\u00E3o fazia isso, e o nome da coluna convida a supor que faz.';\nCOMMENT ON COLUMN public.plg_workforce_employment.allowances IS\n 'Aux\u00EDlios e adicionais (VT, VR, VA, plano, creche, noturno, insalubridade\u2026). Em jsonb porque os quinze campos da vista 2 do V1 est\u00E3o vazios nas 886 pessoas medidas: vira coluna quando algu\u00E9m pagar a passagem.';\n\nCREATE INDEX IF NOT EXISTS plg_workforce_employment_tenant_person\n ON public.plg_workforce_employment (tenant_id, person_id);\nCREATE INDEX IF NOT EXISTS plg_workforce_employment_ativo\n ON public.plg_workforce_employment (tenant_id) WHERE dismissal_date IS NULL;\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 3. F\u00C9RIAS (vista 8)\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nCREATE TABLE IF NOT EXISTS public.plg_workforce_vacations (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n unit_id uuid,\n person_id uuid NOT NULL REFERENCES public.people(id) ON DELETE CASCADE,\n -- O per\u00EDodo AQUISITIVO: o que a pessoa ganhou. \u00C9 o `accrual_end` que alimenta\n -- \"F\u00E9rias vencendo (60d)\".\n accrual_start date NOT NULL,\n accrual_end date NOT NULL,\n -- O per\u00EDodo de GOZO: quando ela tirou. Nulo \u00E9 o estado normal de quem ainda\n -- n\u00E3o programou.\n taken_start date,\n taken_end date,\n days_taken integer,\n days_sold integer,\n advance_13th boolean NOT NULL DEFAULT false,\n -- Digitado, como no V1. O sistema N\u00C3O calcula o ter\u00E7o \u2014 e escrever isso aqui\n -- \u00E9 o que impede algu\u00E9m de supor que a coluna vazia \u00E9 um bug.\n constitutional_third numeric(14,2),\n status text NOT NULL DEFAULT 'pendente'\n CHECK (status IN ('pendente', 'programada', 'em_gozo', 'concluida', 'vencida')),\n notes text,\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n CONSTRAINT plg_workforce_vacations_accrual CHECK (accrual_end >= accrual_start),\n CONSTRAINT plg_workforce_vacations_taken\n CHECK (taken_end IS NULL OR taken_start IS NULL OR taken_end >= taken_start)\n);\n\nCOMMENT ON TABLE public.plg_workforce_vacations IS\n 'Per\u00EDodo aquisitivo e de gozo de f\u00E9rias (005). Nada \u00E9 calculado, inclusive o ter\u00E7o constitucional \u2014 \u00E9 digitado, como no V1.';\n\nCREATE INDEX IF NOT EXISTS plg_workforce_vacations_vencendo\n ON public.plg_workforce_vacations (tenant_id, accrual_end) WHERE status <> 'concluida';\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 4. FALTAS E AFASTAMENTOS (vista 10)\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nCREATE TABLE IF NOT EXISTS public.plg_workforce_absences (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n unit_id uuid,\n person_id uuid NOT NULL REFERENCES public.people(id) ON DELETE CASCADE,\n kind text NOT NULL CHECK (kind IN\n ('falta_justificada', 'falta_injustificada', 'atestado', 'licenca_maternidade',\n 'licenca_paternidade', 'licenca_nao_remunerada', 'suspensao', 'ferias', 'afastamento_inss')),\n starts_on date NOT NULL,\n -- Nulo = afastamento em aberto, que \u00E9 um estado real (INSS sem alta marcada).\n ends_on date,\n days integer,\n paid boolean NOT NULL DEFAULT true,\n -- Dado m\u00E9dico. Mora aqui e n\u00E3o em `people` pelo mesmo motivo do sal\u00E1rio: \u00E9 a\n -- policy de `workforce_hr` que o protege, e V1-069 \u00E9 exatamente sobre isto.\n icd_code text,\n doctor text,\n notes text,\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n CONSTRAINT plg_workforce_absences_span CHECK (ends_on IS NULL OR ends_on >= starts_on)\n);\n\nCOMMENT ON TABLE public.plg_workforce_absences IS\n 'Falta e afastamento (005). Diferente do V1, CONVERSA com o ponto: v_workforce_day l\u00EA daqui, e um dia coberto por atestado deixa de aparecer como falta.';\nCOMMENT ON COLUMN public.plg_workforce_absences.icd_code IS\n 'CID do atestado. Dado de sa\u00FAde: \u00E9 a policy de workforce_hr que o separa de quem edita cadastro \u2014 o V1 n\u00E3o separava (V1-069).';\n\nCREATE INDEX IF NOT EXISTS plg_workforce_absences_periodo\n ON public.plg_workforce_absences (tenant_id, person_id, starts_on, ends_on);\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 5. PEND\u00CANCIAS (vista 16)\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nCREATE TABLE IF NOT EXISTS public.plg_workforce_pending_items (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n unit_id uuid,\n person_id uuid NOT NULL REFERENCES public.people(id) ON DELETE CASCADE,\n title text NOT NULL,\n category text CHECK (category IN ('documento', 'exame', 'epi', 'treinamento', 'assinatura', 'outro')),\n priority text NOT NULL DEFAULT 'normal' CHECK (priority IN ('baixa', 'normal', 'alta', 'urgente')),\n due_on date,\n resolved_at timestamptz,\n description text,\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nCOMMENT ON TABLE public.plg_workforce_pending_items IS\n 'O que falta na pasta de algu\u00E9m: documento, exame, EPI, assinatura (005). \u00C9 a lista que o painel cobra \u2014 \"Pend\u00EAncias cr\u00EDticas\" conta as n\u00E3o resolvidas de prioridade alta ou urgente.';\n\nCREATE INDEX IF NOT EXISTS plg_workforce_pending_abertas\n ON public.plg_workforce_pending_items (tenant_id, priority, due_on) WHERE resolved_at IS NULL;\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 6. ESCOPO, CARIMBO E PERMISS\u00C3O\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nDO $$\nDECLARE r record;\nBEGIN\n FOR r IN SELECT * FROM (VALUES\n ('plg_workforce_employment', 'workforce_hr.employment'),\n ('plg_workforce_vacations', 'workforce_hr.vacation'),\n ('plg_workforce_absences', 'workforce_hr.absence'),\n ('plg_workforce_pending_items', 'workforce_hr.pending_item')\n ) AS t(tbl, resource)\n LOOP\n EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', r.tbl);\n EXECUTE format('ALTER TABLE public.%I FORCE ROW LEVEL SECURITY', r.tbl);\n EXECUTE format('DROP TRIGGER IF EXISTS %I ON public.%I', r.tbl || '_updated_at', r.tbl);\n EXECUTE format('CREATE TRIGGER %I BEFORE UPDATE ON public.%I FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at()',\n r.tbl || '_updated_at', r.tbl);\n -- `workforce_hr` e n\u00E3o `workforce`: \u00E9 o nome do plugin que o scaffold usa\n -- para derivar `<plugin>.read/create/edit/delete`, ent\u00E3o passar outro nome\n -- aqui \u00C9 a separa\u00E7\u00E3o de permiss\u00E3o. Quem tem workforce.read v\u00EA escala e\n -- ponto e n\u00E3o v\u00EA sal\u00E1rio.\n PERFORM app.scaffold_table(format('public.%I', r.tbl)::regclass, r.resource, 'workforce_hr', false, 'enforce');\n END LOOP;\nEND $$;\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 7. O PONTO PASSA A SABER DO AFASTAMENTO\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n--\n-- O defeito que o FLOW da vista 10 registra: *\"N\u00E3o conversa com o ponto\n-- eletr\u00F4nico nem com a escala.\"* Duas telas sobre a mesma pessoa, no mesmo\n-- sistema, que n\u00E3o se olham \u2014 quem confere ponto v\u00EA FALTA em cima de um\n-- atestado que o RH lan\u00E7ou na semana passada, e precisa lembrar de cabe\u00E7a.\n--\n-- A view n\u00E3o muda de forma: ganha uma coluna no fim. `absent` continua sendo\n-- escala sem ponto, porque \u00E9 isso que ele \u00E9; o que muda \u00E9 que agora d\u00E1 para\n-- dizer POR QUE, quando algu\u00E9m sabe.\n\nCREATE OR REPLACE VIEW public.v_workforce_day\nWITH (security_invoker = 'true') AS\nWITH bruto AS (\n SELECT e.tenant_id, e.unit_id, e.person_id, e.work_date,\n min(e.effective_at) FILTER (WHERE e.kind = 'clock_in') AS first_in,\n max(e.effective_at) FILTER (WHERE e.kind = 'clock_out') AS last_out,\n count(*) FILTER (WHERE e.kind = 'break_start') AS break_count,\n count(*) FILTER (WHERE e.kind IN ('clock_in', 'break_end')) AS aberturas,\n count(*) FILTER (WHERE e.kind IN ('clock_out', 'break_start')) AS fechamentos,\n max(e.effective_at) FILTER (WHERE e.kind IN ('clock_in', 'break_end')) AS ultima_abertura,\n sum(CASE\n WHEN e.kind IN ('clock_out', 'break_start') THEN extract(epoch FROM e.effective_at)\n WHEN e.kind IN ('clock_in', 'break_end') THEN -extract(epoch FROM e.effective_at)\n END) AS soma_com_sinal\n FROM public.v_workforce_time_entries e\n WHERE NOT e.voided\n GROUP BY e.tenant_id, e.unit_id, e.person_id, e.work_date\n),\nrealized AS (\n SELECT b.tenant_id, b.unit_id, b.person_id, b.work_date,\n b.first_in, b.last_out, b.break_count,\n (b.aberturas <> b.fechamentos) AS is_open,\n CASE WHEN b.aberturas <> b.fechamentos THEN b.ultima_abertura END AS open_since,\n -- Devolve \u00E0 soma a abertura sem par (003).\n (b.soma_com_sinal\n + CASE WHEN b.aberturas <> b.fechamentos\n THEN extract(epoch FROM b.ultima_abertura)\n ELSE 0 END) AS worked_seconds\n FROM bruto b\n),\nplanned AS (\n SELECT a.tenant_id, a.unit_id, a.person_id,\n s.schedule_date AS work_date,\n a.id AS assignment_id, a.role, a.status AS assignment_status,\n a.shift_label, a.shift_start_time, a.shift_end_time,\n (extract(epoch FROM a.shift_end_time - a.shift_start_time)\n + CASE WHEN a.shift_end_time <= a.shift_start_time THEN 86400 ELSE 0 END) AS planned_seconds\n FROM public.plg_workforce_schedule_assignments a\n JOIN public.plg_workforce_schedules s ON s.id = a.schedule_id\n WHERE s.status IN ('published', 'closed')\n AND a.role = 'titular'\n AND a.shift_start_time IS NOT NULL\n AND a.shift_end_time IS NOT NULL\n)\nSELECT coalesce(p.tenant_id, r.tenant_id) AS tenant_id,\n coalesce(p.unit_id, r.unit_id) AS unit_id,\n coalesce(p.person_id, r.person_id) AS person_id,\n coalesce(p.work_date, r.work_date) AS work_date,\n p.assignment_id,\n p.shift_label,\n p.shift_start_time,\n p.shift_end_time,\n p.assignment_status,\n (p.planned_seconds / 60)::integer AS planned_minutes,\n (r.worked_seconds / 60)::integer AS worked_minutes,\n ((r.worked_seconds - p.planned_seconds) / 60)::integer AS variance_minutes,\n r.first_in,\n r.last_out,\n r.break_count,\n coalesce(r.is_open, false) AS is_open,\n (r.person_id IS NULL) AS absent,\n (p.assignment_id IS NULL) AS outside_schedule,\n r.open_since,\n CASE\n WHEN r.person_id IS NULL THEN NULL\n WHEN r.is_open THEN 'open'\n WHEN (r.worked_seconds / 60) > public.workforce_max_shift_minutes()\n THEN 'span_too_long'\n END AS review_reason,\n -- Coluna nova, no fim. `security_invoker` na view + a policy de\n -- `workforce_hr` na tabela: quem n\u00E3o tem a permiss\u00E3o simplesmente n\u00E3o\n -- enxerga o afastamento, e a coluna volta nula. A folha de ponto n\u00E3o\n -- vira a porta dos fundos do CID.\n (SELECT a.kind\n FROM public.plg_workforce_absences a\n WHERE a.tenant_id = coalesce(p.tenant_id, r.tenant_id)\n AND a.person_id = coalesce(p.person_id, r.person_id)\n AND coalesce(p.work_date, r.work_date) BETWEEN a.starts_on\n AND coalesce(a.ends_on, coalesce(p.work_date, r.work_date))\n ORDER BY a.starts_on DESC\n LIMIT 1) AS absence_kind\n FROM planned p\n FULL OUTER JOIN realized r\n ON r.tenant_id = p.tenant_id\n AND r.person_id = p.person_id\n AND r.work_date = p.work_date;\n\nCOMMENT ON COLUMN public.v_workforce_day.absence_kind IS\n 'O afastamento que cobre este dia, quando o RH lan\u00E7ou um (005). \u00C9 o conserto do defeito que o V1 tem: l\u00E1 a falta lan\u00E7ada no RH n\u00E3o aparece no ponto, e quem confere v\u00EA FALTA em cima de um atestado.';\n\nGRANT SELECT ON public.v_workforce_day TO authenticated, service_role;\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 8. A VIS\u00C3O GERAL \u2014 os oito n\u00FAmeros\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n--\n-- Os mesmos oito cart\u00F5es do V1, com tr\u00EAs diferen\u00E7as, e as tr\u00EAs s\u00E3o conserto de\n-- defeito registrado no FLOW:\n--\n-- V1-072 \"Experi\u00EAncia vencendo\" s\u00F3 olhava PROFISSIONAIS, porque o V1 tem\n-- duas tabelas. O V2 tem uma pessoa, ent\u00E3o olha todo mundo.\n-- V1-073 \"F\u00E9rias a vencer\" mostrava a data e N\u00C3O o nome. Aqui a view devolve\n-- person_id e nome, e a tela linka para a ficha.\n-- V1-074 aniversariante do dia 1\u00BA ca\u00EDa no m\u00EAs anterior, por compara\u00E7\u00E3o de\n-- data em UTC. `extract(month \u2026)` n\u00E3o tem esse problema.\n\nCREATE OR REPLACE VIEW public.v_workforce_hr_overview\nWITH (security_invoker = 'true') AS\nWITH base AS (\n SELECT e.tenant_id, e.person_id, e.admission_date, e.dismissal_date, e.probation_final_end,\n p.name, p.date_of_birth, p.is_active\n FROM public.plg_workforce_employment e\n JOIN public.people p ON p.id = e.person_id\n)\nSELECT b.tenant_id,\n count(*) FILTER (WHERE b.dismissal_date IS NULL AND coalesce(b.is_active, true)) AS headcount,\n count(*) FILTER (WHERE b.admission_date >= current_date - 30) AS admissions_30d,\n count(*) FILTER (WHERE b.dismissal_date >= current_date - 30) AS dismissals_30d,\n count(*) FILTER (WHERE b.probation_final_end BETWEEN current_date AND current_date + 60) AS probation_ending,\n count(*) FILTER (WHERE b.dismissal_date IS NULL\n AND extract(month FROM b.date_of_birth) = extract(month FROM current_date)) AS birthdays_this_month,\n (SELECT count(*) FROM public.plg_workforce_vacations v\n WHERE v.tenant_id = b.tenant_id AND v.status <> 'concluida'\n AND v.accrual_end BETWEEN current_date AND current_date + 60) AS vacations_due_60d,\n (SELECT count(*) FROM public.plg_workforce_pending_items i\n WHERE i.tenant_id = b.tenant_id AND i.resolved_at IS NULL\n AND i.priority IN ('alta', 'urgente')) AS critical_pending\n FROM base b\n GROUP BY b.tenant_id;\n\nCOMMENT ON VIEW public.v_workforce_hr_overview IS\n 'Os oito n\u00FAmeros do painel de RH do V1 (005). Turnover n\u00E3o est\u00E1 aqui de prop\u00F3sito: \u00E9 demiss\u00F5es \u00F7 headcount, e uma divis\u00E3o que a tela faz \u00E9 uma divis\u00E3o que a tela pode formatar \u2014 a view devolve as duas parcelas.';\n\n-- Quem vence, COM NOME \u2014 o V1-073 \u00E9 justamente a lista sem nome.\nCREATE OR REPLACE VIEW public.v_workforce_hr_upcoming\nWITH (security_invoker = 'true') AS\nSELECT v.tenant_id, 'vacation'::text AS kind, v.person_id, p.name AS person_name,\n v.accrual_end AS due_on, v.status AS detail\n FROM public.plg_workforce_vacations v\n JOIN public.people p ON p.id = v.person_id\n WHERE v.status <> 'concluida' AND v.accrual_end BETWEEN current_date AND current_date + 60\nUNION ALL\nSELECT e.tenant_id, 'probation', e.person_id, p.name,\n e.probation_final_end, e.job_title\n FROM public.plg_workforce_employment e\n JOIN public.people p ON p.id = e.person_id\n WHERE e.probation_final_end BETWEEN current_date AND current_date + 60\nUNION ALL\nSELECT i.tenant_id, 'pending', i.person_id, p.name, i.due_on, i.title\n FROM public.plg_workforce_pending_items i\n JOIN public.people p ON p.id = i.person_id\n WHERE i.resolved_at IS NULL AND i.priority IN ('alta', 'urgente');\n\nCOMMENT ON VIEW public.v_workforce_hr_upcoming IS\n 'O que vence nos pr\u00F3ximos 60 dias, com o NOME de quem (005). O V1 lista f\u00E9rias a vencer s\u00F3 com a data \u2014 V1-073 \u2014, o que obriga a procurar a pessoa em outra tela.';\n\nGRANT SELECT ON public.v_workforce_hr_overview TO authenticated, service_role;\nGRANT SELECT ON public.v_workforce_hr_upcoming TO authenticated, service_role;\n";
7
7
  export declare const MIGRATION_006_O_CUSTO_DE_PESSOAL_E_UM_RAMO_DO_PLANO = "-- O custo de pessoal \u00E9 um RAMO DO PLANO, e quem o aponta \u00E9 a casa.\n--\n-- \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\n--\n-- A tela \"Encargos e honor\u00E1rios\" (RH \u2192 Custos) l\u00EA o raz\u00E3o do Financeiro e soma\n-- toda linha de conta a pagar que tenha conta cont\u00E1bil. Toda. Medido no cluster\n-- de Pertinho do C\u00E9u em 19/09/2026, o que ela chamava de custo de pessoal\n-- inclu\u00EDa:\n--\n-- 6.2.1 Aluguel R$ 21.061,99\n-- 6.2.3 IPTU R$ 26.613,73\n-- 6.5.5 ICMS R$ 30.964,92\n-- 6.4.1 Equipamentos R$ 82.595,87\n-- 6.3.1 Energia/Light R$ 27.185,62\n--\n-- \u2014 ao lado das que s\u00E3o, de fato, pessoal:\n--\n-- 5 DESPESAS COM PESSOAL (26 contas: Sal\u00E1rio, FGTS, INSS, Comiss\u00E3o,\n-- Hora Extra, Adiantamento, IRRF Folha, Vale Alimenta\u00E7\u00E3o, FREELANCER\n-- com oito filhas\u2026)\n--\n-- Um total que soma aluguel e ICMS sob o t\u00EDtulo \"custo de pessoal\" n\u00E3o est\u00E1\n-- impreciso: est\u00E1 errado, e erra para cima, que \u00E9 o lado que faz algu\u00E9m decidir\n-- contratar menos.\n--\n-- \u2500\u2500 Por que uma configura\u00E7\u00E3o, e n\u00E3o 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\n--\n-- N\u00E3o existe como o c\u00F3digo saber qual ramo \u00E9 pessoal. O plano de contas \u00E9 uma\n-- DECLARA\u00C7\u00C3O da casa (ver `040_o_demonstrativo_mostra_o_plano_inteiro`), e cada\n-- casa numera o dela: aqui pessoal \u00E9 o ramo `5`; o template que o SDK semeia\n-- para um tenant novo p\u00F5e folha em `4.03`. Deduzir pelo NOME da conta seria\n-- adivinhar em portugu\u00EAs \u2014 e `8.2.4 Rescis\u00E3o` e `8.2.6 Pro-labore`, que tamb\u00E9m\n-- s\u00E3o gente, moram fora do ramo 5 neste mesmo plano, por escolha de quem montou.\n--\n-- Ent\u00E3o a chave guarda a escolha, e a aus\u00EAncia dela \u00E9 uma aus\u00EAncia honesta: sem\n-- nada escolhido a tela mostra TODAS as despesas classificadas e diz que \u00E9 o que\n-- est\u00E1 fazendo. Um padr\u00E3o inventado ('5') faria a tela mentir com convic\u00E7\u00E3o no\n-- primeiro tenant cujo plano n\u00E3o usa esse n\u00FAmero.\n--\n-- \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\n--\n-- Um array de C\u00D3DIGOS, n\u00E3o de ids. O c\u00F3digo \u00E9 o que carrega a hierarquia: quem\n-- escolhe '5' escolhe o ramo inteiro \u2014 '5.4' e '5.4.1' entram com ele, e uma\n-- conta nova criada amanh\u00E3 dentro do ramo entra sozinha. Com id seria preciso\n-- re-marcar a configura\u00E7\u00E3o a cada conta nova, e ningu\u00E9m lembra.\n--\n-- `tenant_setting_set` cobra `settings.manage` e recusa chave n\u00E3o registrada \u2014\n-- \u00E9 por isso que este arquivo existe. Idempotente: `seed_setting_keys` faz\n-- upsert por chave.\n\nSELECT app.seed_setting_keys($$[\n {\n \"key\": \"workforce.payroll_account_codes\", \"type\": \"array\", \"default\": null,\n \"reader\": \"plugin:workforce\",\n \"description\": \"Os ramos do plano de contas que s\u00E3o custo de pessoal, por c\u00F3digo ('5', '8.2.4'). Um c\u00F3digo traz toda a descend\u00EAncia dele. Vazio ou nulo = a tela mostra todas as despesas classificadas e avisa que n\u00E3o h\u00E1 ramo escolhido.\"\n }\n]$$::jsonb);\n";
8
+ export declare const MIGRATION_007_A_PORTA_DE_SERVICO_DO_PONTO_FECHA_TAMBEM = "-- ---------------------------------------------------------------------------\n-- 007_a_porta_de_servico_do_ponto_fecha_tambem.sql\n--\n-- \u2500\u2500 o buraco, e por que ele estava invis\u00EDvel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- As fun\u00E7\u00F5es que este plugin cria em `migration` nasceram execut\u00E1veis por\n-- PUBLIC \u2014 \u00E9 o padr\u00E3o do Postgres, e ningu\u00E9m revogou. Elas s\u00E3o\n-- `SECURITY DEFINER` e escrevem dados de QUALQUER inquilino: uma delas\n-- alcan\u00E7\u00E1vel pela chave an\u00F4nima \u00E9 a base inteira alcan\u00E7\u00E1vel pela chave\n-- an\u00F4nima.\n--\n-- Isto n\u00E3o aparecia porque a `000_baseline` falhava e levava junto os outros\n-- seis arquivos deste plugin \u2014 as fun\u00E7\u00F5es nunca chegavam a existir na bancada.\n-- Com a baseline consertada (#443) o passo inteiro passou a aplicar, e as duas\n-- su\u00EDtes que guardam esta fronteira ficaram vermelhas na hora:\n--\n-- 090_migration_ledger/001 \"no migration function is executable by anon\n-- or authenticated\" \u2014 have 9, want 0\n-- crm/009 \"nenhuma fun\u00E7\u00E3o do schema migration \u00E9 cham\u00E1vel\n-- por anon\" \u2014 have 9, want 0\n--\n-- Ou seja: a regra sempre esteve escrita e medida; o que faltava era o plugin\n-- chegar ao banco para ser medido. \u00C9 o argumento do #338 de novo \u2014 um plugin\n-- fora da corrente \u00E9 um plugin cujas tabelas (e cujas portas) s\u00E3o coincid\u00EAncia.\n--\n-- \u2500\u2500 por extenso, e n\u00E3o varrendo o cat\u00E1logo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- A tenta\u00E7\u00E3o \u00E9 `REVOKE` num la\u00E7o sobre `pg_proc`. J\u00E1 foi tentado neste repo e\n-- derrubou 35 su\u00EDtes: varrer revoga de fun\u00E7\u00F5es que outros passos criaram e\n-- precisam de execu\u00E7\u00E3o. Revogar em massa \u00E9 f\u00E1cil de escrever e imposs\u00EDvel de\n-- revisar. A lista abaixo \u00E9 a das fun\u00E7\u00F5es DESTE plugin, nomeada uma a uma.\n-- ---------------------------------------------------------------------------\n\nDO $$\nDECLARE\n v_fn text;\n v_args text;\n v_n integer := 0;\nBEGIN\n -- Cada writer do de-para, com a assinatura que a corrente lhe deu. Nomear em\n -- vez de varrer \u00E9 o que torna esta lista revis\u00E1vel.\n FOR v_fn, v_args IN\n SELECT p.proname, pg_get_function_identity_arguments(p.oid)\n FROM pg_proc p\n JOIN pg_namespace n ON n.oid = p.pronamespace\n WHERE n.nspname = 'migration'\n AND (p.proname LIKE 'upsert_wf\\_%' OR p.proname IN ('_wf_person', '_point_punch_photo'))\n LOOP\n EXECUTE format('REVOKE ALL ON FUNCTION migration.%I(%s) FROM PUBLIC, anon, authenticated', v_fn, v_args);\n -- `service_role` fica: \u00E9 quem a importa\u00E7\u00E3o usa, e \u00E9 a \u00FAnica identidade que\n -- tem raz\u00E3o para atravessar inquilinos.\n EXECUTE format('GRANT EXECUTE ON FUNCTION migration.%I(%s) TO service_role', v_fn, v_args);\n v_n := v_n + 1;\n END LOOP;\n\n RAISE NOTICE 'workforce/007: % fun\u00E7\u00E3o(\u00F5es) de migration fechadas a anon e authenticated', v_n;\nEND $$;\n";
8
9
  export declare const MIGRATIONS: Array<{
9
10
  id: string;
10
11
  sql: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,s2xHA26DlC,CAAA;AAED,eAAO,MAAM,yCAAyC,ggVAiKrD,CAAA;AAED,eAAO,MAAM,mDAAmD,6mLAkG/D,CAAA;AAED,eAAO,MAAM,wDAAwD,klSA8JpE,CAAA;AAED,eAAO,MAAM,4CAA4C,o/SAyKxD,CAAA;AAED,eAAO,MAAM,gDAAgD,yshCAuc5D,CAAA;AAED,eAAO,MAAM,mDAAmD,2iIAyD/D,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAQzD,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,s2xHA26DlC,CAAA;AAED,eAAO,MAAM,yCAAyC,ggVAiKrD,CAAA;AAED,eAAO,MAAM,mDAAmD,6mLAkG/D,CAAA;AAED,eAAO,MAAM,wDAAwD,klSA8JpE,CAAA;AAED,eAAO,MAAM,4CAA4C,o/SAyKxD,CAAA;AAED,eAAO,MAAM,gDAAgD,yshCAuc5D,CAAA;AAED,eAAO,MAAM,mDAAmD,2iIAyD/D,CAAA;AAED,eAAO,MAAM,sDAAsD,w4GAyDlE,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CASzD,CAAA"}
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "status": "beta",
5
5
  "dependencies": []
6
6
  },
7
- "version": "0.1.1",
7
+ "version": "0.1.3",
8
8
  "description": "Fayz SDK — plugin-workforce plugin",
9
9
  "type": "module",
10
10
  "sideEffects": false,
@@ -27,9 +27,9 @@
27
27
  "zustand": "^4.5.0 || ^5.0.0"
28
28
  },
29
29
  "dependencies": {
30
- "@fayz-ai/admin": "^0.23.0",
30
+ "@fayz-ai/admin": "^0.25.0",
31
31
  "@fayz-ai/core": "^0.23.0",
32
- "@fayz-ai/ui": "^0.23.0"
32
+ "@fayz-ai/ui": "^0.25.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "^20.0.0",