@fayz-ai/plugin-agenda 0.22.0 → 0.23.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"turn-queue-plugin.d.ts","sourceRoot":"","sources":["../../src/lib/turn-queue-plugin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AASnD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAA;AAE5E,wBAAgB,2BAA2B,CAAC,OAAO,GAAE,4BAAiC,GAAG,cAAc,CA8DtG"}
1
+ {"version":3,"file":"turn-queue-plugin.d.ts","sourceRoot":"","sources":["../../src/lib/turn-queue-plugin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAQnD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAA;AAE5E,wBAAgB,2BAA2B,CAAC,OAAO,GAAE,4BAAiC,GAAG,cAAc,CAgEtG"}
@@ -19,6 +19,8 @@ export declare const MIGRATION_017_A_EQUIPE_DO_DIA_APARECE = "-- The board must
19
19
  export declare const MIGRATION_018_O_AGENDAMENTO_QUE_SE_REPETE = "-- O agendamento que se repete.\n--\n-- \"Repetir\" no agendamento \u2014 o que o Google Calendar chama de s\u00E9rie. At\u00E9 aqui\n-- s\u00F3 o BLOQUEIO se repetia (`schedules` kind='block'); o compromisso de cliente\n-- era sempre avulso, e marcar o retorno de quinze em quinze dias por seis meses\n-- era doze aberturas do mesmo formul\u00E1rio.\n--\n-- \u2500\u2500 Por que aqui a ocorr\u00EAncia \u00E9 LINHA, e no bloqueio n\u00E3o \u00E9 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- A 010 escolheu o contr\u00E1rio, e escolheu certo para o que ela trata: o bloqueio\n-- recorrente \u00E9 UMA linha por dia da semana com vig\u00EAncia e paridade, expandida\n-- na LEITURA (`agenda_busy_blocks` no banco, `lib/schedule-engine.ts` na tela).\n-- O motivo est\u00E1 escrito l\u00E1: um almo\u00E7o de segunda a sexta at\u00E9 o fim do ano s\u00E3o\n-- 1.300 ocorr\u00EAncias de uma coisa que n\u00E3o tem identidade pr\u00F3pria \u2014 e a grade\n-- ETERNA (248 das 482 do V1 t\u00EAm `valid_to` nulo) n\u00E3o se expande de jeito nenhum.\n--\n-- Um compromisso de cliente \u00E9 o caso oposto, e por tr\u00EAs raz\u00F5es concretas que\n-- vivem neste schema:\n--\n-- 1. CADA OCORR\u00CANCIA \u00C9 UM FATO SEPARADO. Ela tem status pr\u00F3prio (a de 12/11\n-- foi falta, a de 26/11 foi atendida), check-in pr\u00F3prio (`checked_in_at`),\n-- e pode ter PEDIDO pr\u00F3prio \u2014 `order_id`, com fatura e recebimento. Nada\n-- disso cabe numa regra: a regra n\u00E3o pode estar \"paga\".\n-- 2. TODO LEITOR J\u00C1 L\u00CA LINHA. A grade da semana, `checkConflict`, a fila da\n-- vez (`plg_agenda_turn_entries.appointment_id`), as confirma\u00E7\u00F5es, o\n-- hist\u00F3rico da ficha. S\u00E9rie virtual obrigaria cada um deles a aprender a\n-- expandir uma regra \u2014 seis lugares, a mesma regra, divergindo em seis.\n-- 3. A JANELA \u00C9 FINITA POR NATUREZA. Ningu\u00E9m marca \"retorno toda semana para\n-- sempre\": marca at\u00E9 uma data ou um n\u00FAmero de vezes. O que n\u00E3o se expande\n-- \u00E9 a grade eterna, e esta n\u00E3o \u00E9 uma.\n--\n-- Por isso: ocorr\u00EAncia = linha em `public.appointments`, com `series_id` REAL.\n-- N\u00E3o por casamento de campos \u2014 a identidade do bloqueio \u00E9 reconstru\u00EDda\n-- comparando t\u00EDtulo + hor\u00E1rio + profissional (`views/CalendarView.tsx`), e \u00E9\n-- justamente a parte que n\u00E3o se deve copiar: dois almo\u00E7os de t\u00EDtulos iguais em\n-- hor\u00E1rios iguais s\u00E3o o mesmo bloqueio para aquela heur\u00EDstica.\n--\n-- \u2500\u2500 A regra tamb\u00E9m fica guardada \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-- Materializar n\u00E3o dispensa a regra. Sem ela a tela n\u00E3o sabe dizer \"a cada 2\n-- semanas, 10 vezes\" ao reabrir uma ocorr\u00EAncia, e estender a s\u00E9rie depois seria\n-- adivinhar o intervalo pela dist\u00E2ncia entre duas linhas. O precedente \u00E9 o\n-- Financeiro: `RecurringBill` guarda a regra E materializa as parcelas.\n--\n-- \u2500\u2500 O que N\u00C3O est\u00E1 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\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-- RRULE livre (v\u00E1rios dias da semana, \"primeira ter\u00E7a do m\u00EAs\" escolhida \u00E0 m\u00E3o).\n-- `freq` \u00E9 uma lista fechada de tr\u00EAs; crescer \u00E9 acrescentar valor ao CHECK e um\n-- caso no expansor de `lib/recurrence.ts`, n\u00E3o mudar o modelo.\n--\n-- \"Este e os seguintes\" tamb\u00E9m n\u00E3o: ele exige PARTIR a regra em duas (a antiga\n-- ganha `until_date`, a nova nasce do dia editado). Esta migration n\u00E3o impede \u2014\n-- \u00E9 um INSERT a mais no mesmo formato.\n--\n-- `v_appointments` N\u00C3O \u00E9 alargada. O provider j\u00E1 l\u00EA `public.appointments`\n-- direto e funde com a view (`data/supabase.ts`), justamente porque a view n\u00E3o\n-- exp\u00F5e `resource_id` nem `unit_id`; `series_id` entra pelo mesmo caminho e n\u00E3o\n-- custa uma redefini\u00E7\u00E3o de view que a 007 teve de reescrever inteira.\n--\n-- Idempotente.\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 REGRA\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_agenda_recurrences (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n -- A cerca da franquia, herdada da primeira ocorr\u00EAncia. Nula \u00E9 leg\u00EDtima: a\n -- conta de uma unidade s\u00F3 n\u00E3o escolhe unidade, e a 004 estampa `unit_id` no\n -- agendamento por trigger \u2014 a regra nasce antes de a trigger ter rodado.\n unit_id uuid,\n -- `daily` | `weekly` | `monthly_weekday`. Mensal \u00E9 \"mesmo dia da semana\"\n -- (a 2\u00AA ter\u00E7a), n\u00E3o \"mesmo dia do m\u00EAs\": \u00E9 o que o retorno de sal\u00E3o e de\n -- cl\u00EDnica significa, e \u00E9 o que o Google oferece como preset.\n freq text NOT NULL,\n -- A cada N per\u00EDodos. 2 + weekly = \"a cada 2 semanas\".\n interval_count integer DEFAULT 1 NOT NULL,\n -- O dia da PRIMEIRA ocorr\u00EAncia, que \u00E9 a \u00E2ncora de tudo: o weekday da\n -- semanal e a ordinal do m\u00EAs da mensal saem dele.\n starts_on date NOT NULL,\n -- `HH:MM` da s\u00E9rie. Guardado para a edi\u00E7\u00E3o \"todos\" saber que hor\u00E1rio\n -- aplicar sem reler uma ocorr\u00EAncia qualquer e torcer para ser a certa.\n time_of_day time without time zone,\n -- O fim, por UMA ponta s\u00F3. Ver o CHECK abaixo.\n until_date date,\n occurrence_count integer,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT plg_agenda_recurrences_pkey PRIMARY KEY (id),\n CONSTRAINT plg_agenda_recurrences_freq_chk\n CHECK (freq IN ('daily', 'weekly', 'monthly_weekday')),\n CONSTRAINT plg_agenda_recurrences_interval_chk\n CHECK (interval_count BETWEEN 1 AND 12),\n -- UMA ponta, n\u00E3o duas. Data E contagem juntas dariam duas respostas para\n -- \"quando acaba\", e quem lesse a regra depois n\u00E3o saberia qual valeu.\n CONSTRAINT plg_agenda_recurrences_end_chk CHECK (\n (until_date IS NOT NULL AND occurrence_count IS NULL)\n OR (until_date IS NULL AND occurrence_count IS NOT NULL)\n ),\n -- O MESMO TETO DA TELA, no banco. `RECURRENCE_MAX_OCCURRENCES` em\n -- `lib/recurrence.ts` \u00E9 60; um teto que s\u00F3 existe no navegador n\u00E3o \u00E9 teto.\n CONSTRAINT plg_agenda_recurrences_count_chk\n CHECK (occurrence_count IS NULL OR occurrence_count BETWEEN 1 AND 60)\n);\n\nDO $$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_agenda_recurrences_tenant_fk') THEN\n ALTER TABLE public.plg_agenda_recurrences\n ADD CONSTRAINT plg_agenda_recurrences_tenant_fk\n FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_agenda_recurrences_unit_fk') THEN\n ALTER TABLE public.plg_agenda_recurrences\n ADD CONSTRAINT plg_agenda_recurrences_unit_fk\n FOREIGN KEY (unit_id) REFERENCES app.units(id) ON DELETE SET NULL;\n END IF;\nEND $$;\n\nCREATE INDEX IF NOT EXISTS plg_agenda_recurrences_tenant_idx\n ON public.plg_agenda_recurrences (tenant_id, starts_on DESC);\n\nCOMMENT ON TABLE public.plg_agenda_recurrences IS\n 'A regra de uma s\u00E9rie de agendamentos (\"Repetir\"). As ocorr\u00EAncias s\u00E3o linhas reais em public.appointments, ligadas por appointments.series_id \u2014 ver o cabe\u00E7alho da migration 018 para por que aqui se materializa e no bloqueio n\u00E3o.';\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. A OCORR\u00CANCIA APONTA PARA A REGRA\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\nALTER TABLE public.appointments ADD COLUMN IF NOT EXISTS series_id uuid;\n\nCOMMENT ON COLUMN public.appointments.series_id IS\n 'A s\u00E9rie de \"Repetir\" a que esta ocorr\u00EAncia pertence. NULL = agendamento avulso, que \u00E9 a esmagadora maioria. ON DELETE SET NULL: apagar a regra solta as ocorr\u00EAncias, nunca as apaga.';\n\nDO $$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'appointments_series_fk') THEN\n -- SET NULL, e n\u00E3o CASCADE. A 007 j\u00E1 custou uma agenda inteira a um CASCADE\n -- (\"o cascade levou a agenda junto\"): a regra \u00E9 METADADO da s\u00E9rie, e perder\n -- o metadado n\u00E3o pode apagar atendimentos que j\u00E1 aconteceram e j\u00E1 foram\n -- cobrados. Soltar a ocorr\u00EAncia \u00E9 a perda honesta.\n ALTER TABLE public.appointments\n ADD CONSTRAINT appointments_series_fk\n FOREIGN KEY (series_id) REFERENCES public.plg_agenda_recurrences(id) ON DELETE SET NULL;\n END IF;\nEND $$;\n\n-- A pergunta da tela: quais s\u00E3o as outras ocorr\u00EAncias desta s\u00E9rie, em ordem.\nCREATE INDEX IF NOT EXISTS idx_appointments_series\n ON public.appointments USING btree (tenant_id, series_id, starts_at)\n WHERE series_id IS NOT 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. updated_at\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\nDROP TRIGGER IF EXISTS plg_agenda_recurrences_updated_at ON public.plg_agenda_recurrences;\nCREATE TRIGGER plg_agenda_recurrences_updated_at\n BEFORE UPDATE ON public.plg_agenda_recurrences\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\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. Quem enxerga\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-- A mesma cerca das ocorr\u00EAncias: quem pode ler a agenda da unidade pode ler a\n-- regra das s\u00E9ries dela. Regra sem unidade (conta de uma unidade s\u00F3) fica no\n-- alcance de tenant \u2014 ela n\u00E3o guarda cliente nem valor, s\u00F3 freq e datas.\n\nALTER TABLE public.plg_agenda_recurrences ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_agenda_recurrences FORCE ROW LEVEL SECURITY;\nGRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.plg_agenda_recurrences TO authenticated;\nGRANT ALL ON TABLE public.plg_agenda_recurrences TO service_role;\n\nDROP POLICY IF EXISTS plg_agenda_recurrences_authz_select ON public.plg_agenda_recurrences;\nCREATE POLICY plg_agenda_recurrences_authz_select ON public.plg_agenda_recurrences\n FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (plg_agenda_recurrences.unit_id IS NULL OR (SELECT app.has_unit(plg_agenda_recurrences.unit_id)))\n AND (SELECT app.has_permission('agenda.read', plg_agenda_recurrences.unit_id)));\n\nDROP POLICY IF EXISTS plg_agenda_recurrences_authz_insert ON public.plg_agenda_recurrences;\nCREATE POLICY plg_agenda_recurrences_authz_insert ON public.plg_agenda_recurrences\n FOR INSERT TO authenticated WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (plg_agenda_recurrences.unit_id IS NULL OR (SELECT app.has_unit(plg_agenda_recurrences.unit_id)))\n AND (SELECT app.has_permission('agenda.create', plg_agenda_recurrences.unit_id)));\n\nDROP POLICY IF EXISTS plg_agenda_recurrences_authz_update ON public.plg_agenda_recurrences;\nCREATE POLICY plg_agenda_recurrences_authz_update ON public.plg_agenda_recurrences\n FOR UPDATE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (plg_agenda_recurrences.unit_id IS NULL OR (SELECT app.has_unit(plg_agenda_recurrences.unit_id)))\n AND (SELECT app.has_permission('agenda.edit', plg_agenda_recurrences.unit_id)))\n WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (plg_agenda_recurrences.unit_id IS NULL OR (SELECT app.has_unit(plg_agenda_recurrences.unit_id)))\n AND (SELECT app.has_permission('agenda.edit', plg_agenda_recurrences.unit_id)));\n\nDROP POLICY IF EXISTS plg_agenda_recurrences_authz_delete ON public.plg_agenda_recurrences;\nCREATE POLICY plg_agenda_recurrences_authz_delete ON public.plg_agenda_recurrences\n FOR DELETE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (plg_agenda_recurrences.unit_id IS NULL OR (SELECT app.has_unit(plg_agenda_recurrences.unit_id)))\n AND (SELECT app.has_permission('agenda.delete', plg_agenda_recurrences.unit_id)));\n\n-- N\u00C3O entra em `scoped_resources`, pela mesma raz\u00E3o da 013: aquele registro\n-- pede `owner_id` porque responde \"de quem \u00E9 esta linha\". Uma regra de\n-- recorr\u00EAncia n\u00E3o tem dono \u2014 ela descreve as ocorr\u00EAncias, e a cerca das\n-- ocorr\u00EAncias j\u00E1 est\u00E1 em `appointments`.\n";
20
20
  export declare const MIGRATION_019_A_SERIE_NASCE_NUMA_VIAGEM_SO = "-- 019_a_serie_nasce_numa_viagem_so.sql \u2014 a s\u00E9rie inteira em uma ida ao banco.\n--\n-- A 018 materializou a s\u00E9rie, e o cliente a escrevia no la\u00E7o: uma chamada por\n-- ocorr\u00EAncia, em s\u00E9rie, e cada ocorr\u00EAncia custava QUATRO requisi\u00E7\u00F5es \u2014 o RPC de\n-- cria\u00E7\u00E3o, o update da sala, a sincroniza\u00E7\u00E3o de convidados e o carimbo do\n-- `series_id`. Uma s\u00E9rie de doze ter\u00E7as era ~48 viagens, uma depois da outra,\n-- com a aba aberta o tempo todo. Sessenta (o teto) seriam 240.\n--\n-- O custo n\u00E3o \u00E9 s\u00F3 espera. Entre a primeira e a \u00FAltima n\u00E3o h\u00E1 transa\u00E7\u00E3o: fechar\n-- a aba no meio deixa metade da s\u00E9rie criada, e metade de uma s\u00E9rie \u00E9 pior que\n-- nenhuma \u2014 o cliente recebeu doze datas por telefone.\n--\n-- Ent\u00E3o o la\u00E7o desce para o banco. Uma chamada, uma transa\u00E7\u00E3o, tudo ou nada.\n--\n-- N\u00C3O \u00C9 UM SEGUNDO CAMINHO DE ESCRITA. Esta fun\u00E7\u00E3o n\u00E3o insere em\n-- `appointments`: ela chama `agenda_booking_create`, que \u00E9 a mesma porta que o\n-- cliente usava, dentro do la\u00E7o. Pedido, linhas, dura\u00E7\u00E3o e idempot\u00EAncia\n-- continuam decididos num lugar s\u00F3 \u2014 o que muda \u00E9 de onde o la\u00E7o roda. A sala\n-- e os convidados, que o cliente estampava depois porque a assinatura do RPC\n-- est\u00E1 congelada, entram aqui pelo mesmo motivo e na mesma transa\u00E7\u00E3o.\n\nSET check_function_bodies = false;\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-- A s\u00E9rie, de uma vez\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-- `p_days` s\u00E3o DIAS locais (`YYYY-MM-DD`) j\u00E1 expandidos por\n-- `lib/recurrence.expandRecurrence`. A expans\u00E3o fica no TypeScript de\n-- prop\u00F3sito: \u00E9 a parte que erra se algo errar, e teste de aritm\u00E9tica de datas\n-- n\u00E3o deve precisar de banco. O que o banco faz \u00E9 escrever.\n--\n-- `p_time_of_day` chega como texto `HH:MM` e o instante \u00E9 montado AQUI, dia a\n-- dia, no fuso da sess\u00E3o. Mandar 12 timestamps prontos daria no mesmo at\u00E9 a\n-- s\u00E9rie cruzar um hor\u00E1rio de ver\u00E3o \u2014 e a\u00ED a oitava ocorr\u00EAncia cairia uma hora\n-- fora sem ningu\u00E9m ter mexido nela.\nCREATE OR REPLACE FUNCTION public.agenda_series_create(\n p_days date[],\n p_time_of_day text,\n p_rule jsonb,\n p_lines jsonb DEFAULT '[]'::jsonb,\n p_party_id uuid DEFAULT NULL,\n p_assignee_id uuid DEFAULT NULL,\n p_unit_id uuid DEFAULT NULL,\n p_location_id uuid DEFAULT NULL,\n p_resource_id uuid DEFAULT NULL,\n p_kind text DEFAULT 'appointment',\n p_notes text DEFAULT NULL,\n p_metadata jsonb DEFAULT '{}'::jsonb,\n p_attendee_ids uuid[] DEFAULT NULL\n) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_series uuid;\n v_day date;\n v_starts timestamptz;\n v_res jsonb;\n v_booking uuid;\n v_ids uuid[] := '{}';\n v_failed jsonb := '[]'::jsonb;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'agenda_series_create: no active tenant' USING ERRCODE = '42501';\n END IF;\n -- A MESMA permiss\u00E3o de uma ocorr\u00EAncia. Criar doze n\u00E3o \u00E9 um poder novo; \u00E9 o\n -- mesmo gesto doze vezes, e `agenda_booking_create` vai conferir de novo em\n -- cada uma. Conferir aqui \u00E9 para a s\u00E9rie falhar ANTES de escrever a regra.\n IF NOT (select app.has_permission('agenda.create', p_unit_id)) THEN\n RAISE EXCEPTION 'agenda_series_create: agenda.create is required' USING ERRCODE = '42501';\n END IF;\n IF p_days IS NULL OR array_length(p_days, 1) IS NULL THEN\n RAISE EXCEPTION 'agenda_series_create: a s\u00E9rie n\u00E3o tem nenhuma data' USING ERRCODE = '22023';\n END IF;\n -- O MESMO teto do expansor (`RECURRENCE_MAX_OCCURRENCES`). Aqui porque o\n -- cliente n\u00E3o \u00E9 a \u00FAnica porta: um agente ou um script chamam este RPC direto,\n -- e um `p_days` de mil datas \u00E9 uma transa\u00E7\u00E3o que segura a tabela.\n IF array_length(p_days, 1) > 60 THEN\n RAISE EXCEPTION 'agenda_series_create: uma s\u00E9rie tem no m\u00E1ximo 60 ocorr\u00EAncias' USING ERRCODE = '22023';\n END IF;\n\n INSERT INTO public.plg_agenda_recurrences (\n tenant_id, unit_id, freq, interval_count, starts_on, time_of_day, until_date, occurrence_count\n ) VALUES (\n v_tenant, p_unit_id,\n p_rule ->> 'freq',\n coalesce((p_rule ->> 'interval')::integer, 1),\n p_days[1],\n -- `time_of_day` \u00E9 `time`; o par\u00E2metro chega como `HH:MM` de texto porque \u00E9\n -- assim que a tela o tem. O cast \u00E9 expl\u00EDcito para o erro ser aqui, e n\u00E3o\n -- uma s\u00E9rie recusada inteira por causa de dois pontos.\n p_time_of_day::time,\n nullif(p_rule ->> 'until', '')::date,\n CASE WHEN nullif(p_rule ->> 'until', '') IS NULL\n THEN coalesce((p_rule ->> 'count')::integer, array_length(p_days, 1))\n ELSE NULL END\n )\n RETURNING id INTO v_series;\n\n FOREACH v_day IN ARRAY p_days LOOP\n v_starts := (v_day::text || ' ' || p_time_of_day)::timestamp AT TIME ZONE current_setting('TimeZone');\n\n BEGIN\n v_res := public.agenda_booking_create(\n p_starts_at => v_starts,\n p_ends_at => NULL,\n p_lines => p_lines,\n p_party_id => p_party_id,\n p_assignee_id => p_assignee_id,\n p_unit_id => p_unit_id,\n p_location_id => p_location_id,\n p_kind => p_kind,\n p_notes => p_notes,\n p_metadata => p_metadata);\n\n v_booking := (v_res ->> 'appointment_id')::uuid;\n\n -- S\u00E9rie e sala no mesmo UPDATE: as duas s\u00E3o colunas que a porta\n -- congelada de `agenda_booking_create` n\u00E3o escreve, e separ\u00E1-las seria\n -- escrever a mesma linha duas vezes.\n UPDATE public.appointments\n SET series_id = v_series,\n resource_id = coalesce(p_resource_id, resource_id)\n WHERE id = v_booking;\n\n IF p_attendee_ids IS NOT NULL AND array_length(p_attendee_ids, 1) > 0 THEN\n INSERT INTO public.appointment_attendees (tenant_id, booking_id, person_id, role)\n SELECT v_tenant, v_booking, a, 'required'\n FROM unnest(p_attendee_ids) a\n ON CONFLICT DO NOTHING;\n END IF;\n\n v_ids := v_ids || v_booking;\n EXCEPTION WHEN OTHERS THEN\n -- UMA DATA QUE N\u00C3O ENTROU N\u00C3O DERRUBA AS OUTRAS, e tamb\u00E9m n\u00E3o some: o\n -- bloco devolve o motivo e a tela conta quantas nasceram. \u00C9 a mesma\n -- regra do conflito \u2014 avisa, n\u00E3o impede \u2014, e o `EXCEPTION` aninhado\n -- desfaz s\u00F3 esta itera\u00E7\u00E3o, n\u00E3o a s\u00E9rie.\n v_failed := v_failed || jsonb_build_object('date', v_day::text, 'message', SQLERRM);\n END;\n END LOOP;\n\n RETURN jsonb_build_object(\n 'series_id', v_series,\n 'booking_ids', to_jsonb(v_ids),\n 'failed', v_failed);\nEND $$;\n\nREVOKE ALL ON FUNCTION public.agenda_series_create(date[], text, jsonb, jsonb, uuid, uuid, uuid, uuid, uuid, text, text, jsonb, uuid[]) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.agenda_series_create(date[], text, jsonb, jsonb, uuid, uuid, uuid, uuid, uuid, text, text, jsonb, uuid[]) TO authenticated, service_role;\n\nCOMMENT ON FUNCTION public.agenda_series_create(date[], text, jsonb, jsonb, uuid, uuid, uuid, uuid, uuid, text, text, jsonb, uuid[]) IS\n 'Cria a regra de recorr\u00EAncia e TODAS as ocorr\u00EAncias numa transa\u00E7\u00E3o, chamando agenda_booking_create por dia. Substitui o la\u00E7o de ~4 requisi\u00E7\u00F5es por ocorr\u00EAncia que o cliente fazia.';\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-- Mudar a s\u00E9rie inteira\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-- S\u00F3 o que \u00E9 DA S\u00C9RIE: o hor\u00E1rio, o profissional, a sala, o local, a nota e o\n-- status. O DIA de cada ocorr\u00EAncia \u00E9 dela \u2014 mandar um `starts_at` \u00FAnico para\n-- todas empilharia a s\u00E9rie num dia s\u00F3, que \u00E9 o defeito que `planSeriesEdit`\n-- existe para n\u00E3o deixar acontecer no cliente. Aqui \u00E9 a mesma regra, do lado\n-- do banco, para quem chamar direto.\n--\n-- AS LINHAS (servi\u00E7os) N\u00C3O ENTRAM. Reescrev\u00EA-las pede saber se o agendamento\n-- tem pedido ou n\u00E3o \u2014 a regra que `updateBooking` carrega no TypeScript \u2014 e\n-- duplic\u00E1-la aqui seria o segundo caminho de escrita que esta migration\n-- recusa. Quem muda servi\u00E7o numa s\u00E9rie inteira continua indo pelo cliente.\nCREATE OR REPLACE FUNCTION public.agenda_series_update(\n p_series_id uuid,\n p_time_of_day text,\n p_assignee_id uuid DEFAULT NULL,\n p_location_id uuid DEFAULT NULL,\n p_resource_id uuid DEFAULT NULL,\n p_notes text DEFAULT NULL,\n p_status text DEFAULT NULL,\n p_touch text[] DEFAULT '{}'\n) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_unit uuid;\n v_count integer;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'agenda_series_update: no active tenant' USING ERRCODE = '42501';\n END IF;\n\n SELECT r.unit_id INTO v_unit FROM public.plg_agenda_recurrences r\n WHERE r.id = p_series_id AND r.tenant_id = v_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'agenda_series_update: s\u00E9rie % n\u00E3o encontrada', p_series_id USING ERRCODE = 'P0002';\n END IF;\n IF NOT (select app.has_permission('agenda.edit', v_unit)) THEN\n RAISE EXCEPTION 'agenda_series_update: agenda.edit is required' USING ERRCODE = '42501';\n END IF;\n\n -- `p_touch` diz QUAIS campos a tela mexeu. Sem ele, um NULL seria amb\u00EDguo\n -- entre \"n\u00E3o mexi\" e \"esvaziei\" \u2014 e desatribuir o profissional de uma s\u00E9rie\n -- \u00E9 uma decis\u00E3o t\u00E3o leg\u00EDtima quanto troc\u00E1-lo.\n UPDATE public.appointments a\n SET starts_at = ((a.starts_at AT TIME ZONE current_setting('TimeZone'))::date::text\n || ' ' || p_time_of_day)::timestamp AT TIME ZONE current_setting('TimeZone'),\n ends_at = CASE WHEN a.ends_at IS NULL THEN NULL ELSE\n ((a.starts_at AT TIME ZONE current_setting('TimeZone'))::date::text\n || ' ' || p_time_of_day)::timestamp AT TIME ZONE current_setting('TimeZone')\n + (a.ends_at - a.starts_at) END,\n assignee_id = CASE WHEN 'assigneeId' = ANY(p_touch) THEN p_assignee_id ELSE a.assignee_id END,\n location_id = CASE WHEN 'locationId' = ANY(p_touch) THEN p_location_id ELSE a.location_id END,\n resource_id = CASE WHEN 'resourceId' = ANY(p_touch) THEN p_resource_id ELSE a.resource_id END,\n notes = CASE WHEN 'notes' = ANY(p_touch) THEN p_notes ELSE a.notes END,\n status = CASE WHEN 'status' = ANY(p_touch) THEN p_status ELSE a.status END,\n updated_at = now()\n WHERE a.series_id = p_series_id\n AND a.tenant_id = v_tenant;\n GET DIAGNOSTICS v_count = ROW_COUNT;\n\n UPDATE public.plg_agenda_recurrences\n SET time_of_day = p_time_of_day::time, updated_at = now()\n WHERE id = p_series_id;\n\n RETURN jsonb_build_object('updated', v_count);\nEND $$;\n\nREVOKE ALL ON FUNCTION public.agenda_series_update(uuid, text, uuid, uuid, uuid, text, text, text[]) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.agenda_series_update(uuid, text, uuid, uuid, uuid, text, text, text[]) TO authenticated, service_role;\n\nCOMMENT ON FUNCTION public.agenda_series_update(uuid, text, uuid, uuid, uuid, text, text, text[]) IS\n 'Aplica a mesma mudan\u00E7a a todas as ocorr\u00EAncias da s\u00E9rie num UPDATE s\u00F3, preservando o DIA de cada uma. N\u00E3o toca nas linhas de servi\u00E7o \u2014 essa regra vive no cliente.';\n";
21
21
  export declare const MIGRATION_020_A_LISTA_DA_VEZ_SE_LIGA_NO_CADASTRO_DA_UNIDADE = "-- A lista da vez se liga no cadastro da unidade.\n--\n-- No V1 o interruptor \"Ativar lista da vez nesta unidade\" e o raio da cerca\n-- moram no cadastro da empresa (V1: companies.turn_queue_enabled,\n-- turn_queue_radius_m). Em 016 a fila nasceu sem tela de configura\u00E7\u00E3o: a linha\n-- surge sozinha, LIGADA, no primeiro \"estou dispon\u00EDvel\", e o app s\u00F3 pode l\u00EA-la.\n--\n-- Esta RPC \u00E9 a porta que faltava \u2014 a aba \"Lista da vez\" da unidade grava por\n-- ela. Quem configura \u00E9 quem administra a filial (settings.manage NA unidade).\n-- A coordenada continua sendo a do endere\u00E7o da unidade (142); n\u00E3o h\u00E1 segunda\n-- latitude aqui.\n--\n-- Chave que conecta: plg_agenda_turn_queues.unit_id = app.units.id\n-- (UNIQUE tenant_id, unit_id, queue_key). Idempotente.\n\nCREATE OR REPLACE FUNCTION public.agenda_turn_queue_configure(\n p_unit_id uuid,\n p_enabled boolean,\n p_geofence_required boolean DEFAULT NULL,\n p_radius_meters integer DEFAULT NULL\n)\nRETURNS public.plg_agenda_turn_queues\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $function$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_row public.plg_agenda_turn_queues;\nBEGIN\n IF v_tenant IS NULL OR auth.uid() IS NULL THEN\n RAISE EXCEPTION 'agenda_turn_queue_configure: no session or no active tenant' USING ERRCODE = '42501';\n END IF;\n IF NOT EXISTS (SELECT 1 FROM app.units u WHERE u.id = p_unit_id AND u.tenant_id = v_tenant) THEN\n RAISE EXCEPTION 'agenda_turn_queue_configure: unit does not belong to this tenant' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('settings.manage', p_unit_id) THEN\n RAISE EXCEPTION 'agenda_turn_queue_configure: settings.manage is required on this unit' USING ERRCODE = '42501';\n END IF;\n IF p_radius_meters IS NOT NULL AND (p_radius_meters < 10 OR p_radius_meters > 1000) THEN\n RAISE EXCEPTION 'agenda_turn_queue_configure: radius must be between 10 and 1000 meters' USING ERRCODE = '22023';\n END IF;\n\n INSERT INTO public.plg_agenda_turn_queues AS q (tenant_id, unit_id, is_active, geofence_required, geofence_radius_meters)\n VALUES (v_tenant, p_unit_id, coalesce(p_enabled, false), coalesce(p_geofence_required, true), coalesce(p_radius_meters, 80))\n ON CONFLICT (tenant_id, unit_id, queue_key) DO UPDATE\n SET is_active = coalesce(p_enabled, q.is_active),\n geofence_required = coalesce(p_geofence_required, q.geofence_required),\n geofence_radius_meters = coalesce(p_radius_meters, q.geofence_radius_meters),\n updated_at = now()\n RETURNING * INTO v_row;\n RETURN v_row;\nEND $function$;\n\nREVOKE ALL ON FUNCTION public.agenda_turn_queue_configure(uuid, boolean, boolean, integer) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.agenda_turn_queue_configure(uuid, boolean, boolean, integer) TO authenticated, service_role;\n";
22
+ export declare const MIGRATION_021_A_VEZ_POR_AREA = "-- A lista da vez do V1 (BeautySoft, 20261114100000 + 20261114110000): uma fila\n-- por \u00C1REA dentro da unidade, aus\u00EAncia curta que guarda o lugar, a recep\u00E7\u00E3o\n-- conduzindo a fila (chamar, come\u00E7ou, terminou, mover, tirar), servi\u00E7o r\u00E1pido\n-- que n\u00E3o tira da vez e a fila recome\u00E7ando todo dia.\n--\n-- \u00C1reas, \u00E1rea fixada e servi\u00E7os r\u00E1pidos s\u00E3o da REDE (como no V1); o resto \u00E9 da\n-- unidade e mora na linha de plg_agenda_turn_queues desde 016/020.\n-- Aditivo e idempotente: nada some, 016/017/020 continuam respondendo.\n\n-- 1) Configura\u00E7\u00E3o da rede -------------------------------------------------------\nCREATE TABLE IF NOT EXISTS public.plg_agenda_turn_areas (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n key text NOT NULL CHECK (key ~ '^[a-z0-9_]{1,30}$'),\n name text NOT NULL CHECK (length(btrim(name)) BETWEEN 1 AND 40),\n color text NOT NULL DEFAULT 'slate'\n CHECK (color IN ('amber', 'green', 'blue', 'pink', 'purple', 'orange', 'teal', 'slate')),\n keywords text[] NOT NULL DEFAULT '{}',\n sort_order integer NOT NULL DEFAULT 0,\n is_active boolean NOT NULL DEFAULT true,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE (tenant_id, key)\n);\n\nCREATE TABLE IF NOT EXISTS public.plg_agenda_turn_professional_areas (\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n professional_id uuid NOT NULL REFERENCES public.people(id) ON DELETE CASCADE,\n area_key text NOT NULL,\n updated_at timestamptz NOT NULL DEFAULT now(),\n PRIMARY KEY (tenant_id, professional_id)\n);\n\nCREATE TABLE IF NOT EXISTS public.plg_agenda_turn_quick_services (\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n product_id uuid NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,\n created_at timestamptz NOT NULL DEFAULT now(),\n PRIMARY KEY (tenant_id, product_id)\n);\n\nALTER TABLE public.plg_agenda_turn_areas ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_agenda_turn_areas FORCE ROW LEVEL SECURITY;\nALTER TABLE public.plg_agenda_turn_professional_areas ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_agenda_turn_professional_areas FORCE ROW LEVEL SECURITY;\nALTER TABLE public.plg_agenda_turn_quick_services ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_agenda_turn_quick_services FORCE ROW LEVEL SECURITY;\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'plg_agenda_turn_areas' AND policyname = 'plg_agenda_turn_areas_read') THEN\n CREATE POLICY plg_agenda_turn_areas_read ON public.plg_agenda_turn_areas\n FOR SELECT TO authenticated USING (tenant_id = app.current_tenant_id());\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'plg_agenda_turn_professional_areas' AND policyname = 'plg_agenda_turn_professional_areas_read') THEN\n CREATE POLICY plg_agenda_turn_professional_areas_read ON public.plg_agenda_turn_professional_areas\n FOR SELECT TO authenticated USING (tenant_id = app.current_tenant_id());\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'plg_agenda_turn_quick_services' AND policyname = 'plg_agenda_turn_quick_services_read') THEN\n CREATE POLICY plg_agenda_turn_quick_services_read ON public.plg_agenda_turn_quick_services\n FOR SELECT TO authenticated USING (tenant_id = app.current_tenant_id());\n END IF;\nEND $$;\n\nREVOKE ALL ON public.plg_agenda_turn_areas, public.plg_agenda_turn_professional_areas, public.plg_agenda_turn_quick_services FROM PUBLIC, anon, authenticated;\nGRANT SELECT ON public.plg_agenda_turn_areas, public.plg_agenda_turn_professional_areas, public.plg_agenda_turn_quick_services TO authenticated;\n\n-- 2) Unidade, entradas e trilha -------------------------------------------------\nALTER TABLE public.plg_agenda_turn_queues\n ADD COLUMN IF NOT EXISTS follows_appointments boolean NOT NULL DEFAULT true,\n ADD COLUMN IF NOT EXISTS self_checkin boolean NOT NULL DEFAULT true;\n\nALTER TABLE public.plg_agenda_turn_entries\n ADD COLUMN IF NOT EXISTS area_key text,\n ADD COLUMN IF NOT EXISTS away_reason text,\n ADD COLUMN IF NOT EXISTS joined_at timestamptz;\n\nALTER TABLE public.plg_agenda_turn_events\n ADD COLUMN IF NOT EXISTS area_key text;\n\n-- 'away' = saiu e guarda o lugar. 'unavailable' segue sendo \"fora da fila\".\nALTER TABLE public.plg_agenda_turn_entries DROP CONSTRAINT IF EXISTS plg_agenda_turn_entries_status_check;\nALTER TABLE public.plg_agenda_turn_entries ADD CONSTRAINT plg_agenda_turn_entries_status_check\n CHECK (status IN ('available', 'away', 'serving', 'unavailable'));\n\nUPDATE public.plg_agenda_turn_entries\n SET joined_at = coalesce(available_at, state_changed_at)\n WHERE joined_at IS NULL AND status <> 'unavailable';\n\n-- 3) Helpers ----------------------------------------------------------------------\nCREATE OR REPLACE FUNCTION public.agenda_turn__norm(p text)\nRETURNS text LANGUAGE sql IMMUTABLE SET search_path = '' AS $$\n SELECT translate(lower(coalesce(p, '')),\n '\u00E1\u00E0\u00E2\u00E3\u00E4\u00E9\u00E8\u00EA\u00EB\u00ED\u00EC\u00EE\u00EF\u00F3\u00F2\u00F4\u00F5\u00F6\u00FA\u00F9\u00FB\u00FC\u00E7\u00F1', 'aaaaaeeeeiiiiooooouuuucn');\n$$;\n\nCREATE OR REPLACE FUNCTION public.agenda_turn__area_defs(p_tenant uuid)\nRETURNS TABLE(key text, name text, color text, keywords text[], sort_order integer)\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n WITH cad AS (\n SELECT a.key, a.name, a.color, a.keywords, a.sort_order\n FROM public.plg_agenda_turn_areas a\n WHERE a.tenant_id = p_tenant AND a.is_active\n ), padrao(key, name, color, keywords, sort_order) AS (\n VALUES\n ('cabelo', 'Cabelo', 'amber', ARRAY['cabel', 'color', 'escov', 'penteado', 'barb'], 1),\n ('unhas', 'Unhas', 'green', ARRAY['manicur', 'pedicur', 'unha'], 2),\n ('estetica', 'Est\u00E9tica', 'blue', ARRAY['estet', 'depil', 'sobrancel', 'massag', 'cili', 'maqui'], 3)\n )\n SELECT * FROM cad\n UNION ALL\n SELECT * FROM padrao WHERE NOT EXISTS (SELECT 1 FROM cad)\n UNION ALL\n SELECT 'geral', 'Outros', 'slate', ARRAY[]::text[], 99\n WHERE NOT EXISTS (SELECT 1 FROM cad WHERE cad.key = 'geral');\n$$;\n\n-- The specialty arrives as a JSON array string ('[\"Cabelereiro\",\"Colorista\"]')\n-- from the V1 ETL, or as plain text typed in the V2 form.\nCREATE OR REPLACE FUNCTION public.agenda_turn__auto_area_of(p_tenant uuid, p_person uuid)\nRETURNS text LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_spec text;\n v_tokens text[];\n v_key text;\nBEGIN\n SELECT btrim(coalesce(nullif(p.metadata->'professional'->>'specialty', ''),\n nullif(p.metadata->'professional'->>'job_title', ''),\n nullif(p.custom_fields->>'profession', ''), ''))\n INTO v_spec FROM public.people p WHERE p.id = p_person AND p.tenant_id = p_tenant;\n IF coalesce(v_spec, '') = '' THEN RETURN 'geral'; END IF;\n BEGIN\n IF left(v_spec, 1) = '[' THEN\n SELECT array_agg(public.agenda_turn__norm(x)) INTO v_tokens FROM jsonb_array_elements_text(v_spec::jsonb) x;\n END IF;\n EXCEPTION WHEN others THEN v_tokens := NULL;\n END;\n IF v_tokens IS NULL THEN v_tokens := ARRAY[public.agenda_turn__norm(v_spec)]; END IF;\n\n SELECT d.key INTO v_key FROM public.agenda_turn__area_defs(p_tenant) d\n WHERE EXISTS (SELECT 1 FROM unnest(d.keywords) pat, unnest(v_tokens) tok\n WHERE pat <> '' AND tok LIKE '%' || public.agenda_turn__norm(pat) || '%')\n ORDER BY d.sort_order, d.key LIMIT 1;\n RETURN coalesce(v_key, 'geral');\nEND $$;\n\n-- A pinned area wins over the specialty, as long as that area still exists.\nCREATE OR REPLACE FUNCTION public.agenda_turn__area_of(p_tenant uuid, p_person uuid)\nRETURNS text LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT coalesce(\n (SELECT pa.area_key FROM public.plg_agenda_turn_professional_areas pa\n WHERE pa.tenant_id = p_tenant AND pa.professional_id = p_person\n AND EXISTS (SELECT 1 FROM public.agenda_turn__area_defs(p_tenant) d WHERE d.key = pa.area_key)),\n public.agenda_turn__auto_area_of(p_tenant, p_person));\n$$;\n\nCREATE OR REPLACE FUNCTION public.agenda_turn__tz(p_tenant uuid, p_unit uuid)\nRETURNS text LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT coalesce(\n (SELECT nullif(u.timezone, '') FROM public.v_units u WHERE u.id = p_unit),\n (SELECT nullif(t.settings->>'timezone', '') FROM public.tenants t WHERE t.id = p_tenant),\n 'America/Sao_Paulo');\n$$;\n\n-- Yesterday's queue does not survive the night: entries from a previous local\n-- day go out of the queue and leave a 'reset' in the trail.\nCREATE OR REPLACE FUNCTION public.agenda_turn__purge_stale(p_queue uuid, p_tz text)\nRETURNS void LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n WITH velhas AS (\n UPDATE public.plg_agenda_turn_entries e\n SET status = 'unavailable', away_reason = NULL, appointment_id = NULL,\n state_changed_at = now(), updated_at = now()\n WHERE e.queue_id = p_queue AND e.status <> 'unavailable'\n AND (coalesce(e.joined_at, e.available_at, e.state_changed_at) AT TIME ZONE p_tz)::date\n < (now() AT TIME ZONE p_tz)::date\n RETURNING e.tenant_id, e.unit_id, e.professional_id, e.area_key, e.position\n )\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, area_key, payload)\n SELECT v.tenant_id, p_queue, v.unit_id, v.professional_id, 'reset', 'automatic', v.area_key,\n jsonb_build_object('from_position', v.position)\n FROM velhas v;\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.agenda_turn__renumber(p_queue uuid, p_area text)\nRETURNS void LANGUAGE sql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\n UPDATE public.plg_agenda_turn_entries e SET position = o.rn\n FROM (SELECT id, row_number() OVER (ORDER BY position, joined_at, id) AS rn\n FROM public.plg_agenda_turn_entries\n WHERE queue_id = p_queue AND area_key IS NOT DISTINCT FROM p_area\n AND status IN ('available', 'away')) o\n WHERE e.id = o.id AND e.position IS DISTINCT FROM o.rn;\n$$;\n\nCREATE OR REPLACE FUNCTION public.agenda_turn__next_position(p_queue uuid, p_area text)\nRETURNS bigint LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT coalesce(max(position), 0) + 1 FROM public.plg_agenda_turn_entries\n WHERE queue_id = p_queue AND area_key IS NOT DISTINCT FROM p_area AND status IN ('available', 'away');\n$$;\n\n-- Bookable people of this unit (or of the whole network), as the agenda shows them.\nCREATE OR REPLACE FUNCTION public.agenda_turn__in_unit(p_tenant uuid, p_unit uuid, p_person uuid)\nRETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT EXISTS (\n SELECT 1 FROM public.v_bookable_people b\n WHERE b.id = p_person AND b.tenant_id = p_tenant AND b.is_active AND b.is_bookable\n AND b.kind <> 'customer'\n AND (b.unit_ids IS NULL OR p_unit = ANY (b.unit_ids)));\n$$;\n\nCREATE OR REPLACE FUNCTION public.agenda_turn__queue(p_tenant uuid, p_unit uuid)\nRETURNS public.plg_agenda_turn_queues LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT * FROM public.plg_agenda_turn_queues q\n WHERE q.tenant_id = p_tenant AND q.unit_id = p_unit AND q.queue_key = 'walk_in';\n$$;\n\n-- 4) Quadro -------------------------------------------------------------------------\nCREATE OR REPLACE FUNCTION public.agenda_turn_queue_board(p_unit_id uuid)\nRETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_queue public.plg_agenda_turn_queues;\n v_tz text;\n v_day date;\n v_lat numeric;\n v_lng numeric;\n v_result jsonb;\nBEGIN\n IF v_tenant IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;\n IF p_unit_id IS NULL THEN RAISE EXCEPTION 'unit_required' USING ERRCODE = '22023'; END IF;\n IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.read', p_unit_id) THEN\n RAISE EXCEPTION 'agenda.turn_queue.read is required' USING ERRCODE = '42501';\n END IF;\n v_queue := public.agenda_turn__queue(v_tenant, p_unit_id);\n v_tz := public.agenda_turn__tz(v_tenant, p_unit_id);\n v_day := (now() AT TIME ZONE v_tz)::date;\n SELECT u.latitude, u.longitude INTO v_lat, v_lng FROM public.v_units u WHERE u.id = p_unit_id AND u.tenant_id = v_tenant;\n\n WITH ent AS (\n SELECT e.*, row_number() OVER (PARTITION BY e.area_key, (e.status IN ('available', 'away'))\n ORDER BY e.position, e.joined_at, e.id) AS rnk\n FROM public.plg_agenda_turn_entries e\n WHERE e.queue_id = v_queue.id AND e.status IN ('available', 'away', 'serving')\n AND (coalesce(e.joined_at, e.available_at, e.state_changed_at) AT TIME ZONE v_tz)::date = v_day\n ), appts AS (\n SELECT a.assignee_id AS professional_id, count(*)::int AS n,\n min(a.starts_at) FILTER (WHERE a.starts_at >= now() AND a.status NOT IN ('completed', 'cancelled', 'no_show')) AS proximo\n FROM public.appointments a\n WHERE a.tenant_id = v_tenant AND a.assignee_id IS NOT NULL\n AND coalesce(a.unit_id, a.location_id) = p_unit_id\n AND (a.starts_at AT TIME ZONE v_tz)::date = v_day\n AND a.status <> 'cancelled'\n GROUP BY 1\n ), roster AS (\n SELECT p.id, p.name, p.avatar_url, public.agenda_turn__area_of(v_tenant, p.id) AS area\n FROM public.people p\n WHERE p.tenant_id = v_tenant AND p.is_active\n AND (public.agenda_turn__in_unit(v_tenant, p_unit_id, p.id)\n OR EXISTS (SELECT 1 FROM appts t WHERE t.professional_id = p.id))\n ), served AS (\n SELECT ev.professional_id,\n count(*) FILTER (WHERE ev.event_type IN ('started', 'advanced_to_service'))::int AS atendimentos\n FROM public.plg_agenda_turn_events ev\n WHERE ev.queue_id = v_queue.id AND (ev.occurred_at AT TIME ZONE v_tz)::date = v_day\n GROUP BY 1\n )\n SELECT jsonb_build_object(\n 'unitId', p_unit_id,\n 'today', v_day,\n 'enabled', coalesce(v_queue.is_active, false),\n 'selfCheckin', coalesce(v_queue.self_checkin, true),\n 'auto', coalesce(v_queue.follows_appointments, true),\n 'requireLocation', coalesce(v_queue.geofence_required, false),\n 'locationConfigured', v_lat IS NOT NULL AND v_lng IS NOT NULL,\n 'areas', coalesce((SELECT jsonb_agg(jsonb_build_object('key', d.key, 'name', d.name, 'color', d.color) ORDER BY d.sort_order, d.key)\n FROM public.agenda_turn__area_defs(v_tenant) d), '[]'::jsonb),\n 'entries', coalesce((\n SELECT jsonb_agg(jsonb_build_object(\n 'professionalId', e.professional_id, 'name', p.name, 'avatarUrl', p.avatar_url,\n 'area', coalesce(e.area_key, 'geral'), 'status', e.status,\n 'rank', CASE WHEN e.status IN ('available', 'away') THEN e.rnk END,\n 'awayReason', e.away_reason, 'since', e.state_changed_at, 'joinedAt', e.joined_at,\n 'appointmentId', e.appointment_id,\n 'servedToday', coalesce(s.atendimentos, 0)\n ) ORDER BY e.area_key, (e.status = 'serving'), e.rnk)\n FROM ent e JOIN public.people p ON p.id = e.professional_id\n LEFT JOIN served s ON s.professional_id = e.professional_id), '[]'::jsonb),\n 'roster', coalesce((\n SELECT jsonb_agg(jsonb_build_object(\n 'professionalId', r.id, 'name', r.name, 'avatarUrl', r.avatar_url, 'area', r.area,\n 'appointmentsToday', coalesce(t.n, 0), 'nextAppointmentAt', t.proximo,\n 'servedToday', coalesce(s.atendimentos, 0)) ORDER BY r.name)\n FROM roster r\n LEFT JOIN appts t ON t.professional_id = r.id\n LEFT JOIN served s ON s.professional_id = r.id\n WHERE NOT EXISTS (SELECT 1 FROM ent e WHERE e.professional_id = r.id)), '[]'::jsonb),\n 'events', coalesce((\n SELECT jsonb_agg(x.item ORDER BY x.at DESC) FROM (\n SELECT jsonb_build_object('id', ev.id, 'professionalId', ev.professional_id, 'name', ep.name,\n 'type', ev.event_type, 'origin', ev.origin, 'area', ev.area_key,\n 'reason', coalesce(ev.reason, ev.payload->>'reason'), 'createdAt', ev.occurred_at) AS item,\n ev.occurred_at AS at\n FROM public.plg_agenda_turn_events ev\n JOIN public.people ep ON ep.id = ev.professional_id\n WHERE ev.queue_id = v_queue.id AND (ev.occurred_at AT TIME ZONE v_tz)::date = v_day\n ORDER BY ev.occurred_at DESC LIMIT 60) x), '[]'::jsonb)\n ) INTO v_result;\n RETURN v_result;\nEND $$;\n\n-- 5) A\u00E7\u00F5es da recep\u00E7\u00E3o --------------------------------------------------------------\n-- join \u2192 create \u00B7 leave \u2192 delete \u00B7 away/back/move/start/finish/skip/call \u2192 edit\nCREATE OR REPLACE FUNCTION public.agenda_turn_queue_act(\n p_unit_id uuid,\n p_professional_id uuid,\n p_action text,\n p_position integer DEFAULT NULL,\n p_reason text DEFAULT NULL\n) RETURNS jsonb LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_queue public.plg_agenda_turn_queues;\n v_entry public.plg_agenda_turn_entries;\n v_need text := CASE p_action WHEN 'join' THEN 'create' WHEN 'leave' THEN 'delete' ELSE 'edit' END;\n v_reason text := nullif(btrim(coalesce(p_reason, '')), '');\n v_area text;\n v_pos bigint;\n v_ids uuid[];\n v_users uuid[];\nBEGIN\n IF v_tenant IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;\n IF p_action NOT IN ('join', 'leave', 'away', 'back', 'move', 'start', 'finish', 'skip', 'call') THEN\n RAISE EXCEPTION 'turn_queue_invalid_action:%', p_action USING ERRCODE = '22023';\n END IF;\n IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.' || v_need, p_unit_id) THEN\n RAISE EXCEPTION 'agenda.turn_queue.% is required', v_need USING ERRCODE = '42501';\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.people p WHERE p.id = p_professional_id AND p.tenant_id = v_tenant AND p.is_active) THEN\n RAISE EXCEPTION 'turn_queue_invalid_professional' USING ERRCODE = '22023';\n END IF;\n\n SELECT * INTO v_queue FROM public.plg_agenda_turn_queues q\n WHERE q.tenant_id = v_tenant AND q.unit_id = p_unit_id AND q.queue_key = 'walk_in' FOR UPDATE;\n IF v_queue.id IS NULL OR NOT v_queue.is_active THEN\n RAISE EXCEPTION 'turn_queue_disabled' USING ERRCODE = '22023';\n END IF;\n PERFORM public.agenda_turn__purge_stale(v_queue.id, public.agenda_turn__tz(v_tenant, p_unit_id));\n\n SELECT * INTO v_entry FROM public.plg_agenda_turn_entries e\n WHERE e.queue_id = v_queue.id AND e.professional_id = p_professional_id;\n IF v_entry.id IS NOT NULL AND v_entry.status = 'unavailable' THEN v_entry := NULL; END IF;\n\n IF p_action = 'join' THEN\n IF v_entry.id IS NOT NULL THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;\n v_area := public.agenda_turn__area_of(v_tenant, p_professional_id);\n v_pos := public.agenda_turn__next_position(v_queue.id, v_area);\n INSERT INTO public.plg_agenda_turn_entries\n (tenant_id, queue_id, unit_id, professional_id, status, position, area_key, joined_at, available_at, state_changed_at, appointment_id, away_reason, updated_at)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'available', v_pos, v_area, now(), now(), now(), NULL, NULL, now())\n ON CONFLICT (queue_id, professional_id) DO UPDATE SET\n status = 'available', position = v_pos, area_key = v_area, joined_at = now(), available_at = now(),\n state_changed_at = now(), appointment_id = NULL, away_reason = NULL, updated_at = now();\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'joined', 'frontdesk', auth.uid(), v_area, jsonb_build_object('to_position', v_pos));\n RETURN jsonb_build_object('ok', true, 'changed', true, 'area', v_area, 'position', v_pos);\n END IF;\n\n IF p_action = 'call' THEN\n SELECT array_agg(DISTINCT m.user_id) INTO v_users FROM app.memberships m\n WHERE m.tenant_id = v_tenant AND m.person_id = p_professional_id AND m.active;\n IF v_users IS NOT NULL THEN\n INSERT INTO public.notifications (tenant_id, user_id, title, body, type, link, subject_type, subject_id)\n SELECT v_tenant, u, 'Cliente para voc\u00EA na recep\u00E7\u00E3o', coalesce(v_reason, 'Voc\u00EA \u00E9 o pr\u00F3ximo da vez.'),\n 'turn_queue_call', '#/agenda', 'person', p_professional_id\n FROM unnest(v_users) u;\n END IF;\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, reason, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'called', 'frontdesk', auth.uid(),\n coalesce(v_entry.area_key, public.agenda_turn__area_of(v_tenant, p_professional_id)), v_reason,\n jsonb_build_object('notified', v_users IS NOT NULL));\n RETURN jsonb_build_object('ok', true, 'changed', true, 'notified', v_users IS NOT NULL);\n END IF;\n\n IF v_entry.id IS NULL THEN RAISE EXCEPTION 'turn_queue_not_in_queue' USING ERRCODE = '22023'; END IF;\n\n IF p_action = 'leave' THEN\n UPDATE public.plg_agenda_turn_entries SET status = 'unavailable', away_reason = NULL, appointment_id = NULL,\n state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n PERFORM public.agenda_turn__renumber(v_queue.id, v_entry.area_key);\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'left', 'frontdesk', auth.uid(), v_entry.area_key, jsonb_build_object('from_position', v_entry.position));\n\n ELSIF p_action = 'away' THEN\n IF v_entry.status = 'serving' THEN RAISE EXCEPTION 'turn_queue_in_service' USING ERRCODE = '22023'; END IF;\n UPDATE public.plg_agenda_turn_entries SET status = 'away', away_reason = coalesce(v_reason, 'Ausente'),\n state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, reason, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'away', 'frontdesk', auth.uid(), v_entry.area_key, coalesce(v_reason, 'Ausente'),\n jsonb_build_object('from_position', v_entry.position));\n\n ELSIF p_action = 'back' THEN\n IF v_entry.status <> 'away' THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;\n UPDATE public.plg_agenda_turn_entries SET status = 'available', away_reason = NULL,\n state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'back', 'frontdesk', auth.uid(), v_entry.area_key, jsonb_build_object('to_position', v_entry.position));\n\n ELSIF p_action = 'move' THEN\n IF v_entry.status = 'serving' THEN RAISE EXCEPTION 'turn_queue_in_service' USING ERRCODE = '22023'; END IF;\n IF p_position IS NULL OR p_position < 1 THEN RAISE EXCEPTION 'turn_queue_invalid_position' USING ERRCODE = '22023'; END IF;\n SELECT array_agg(id ORDER BY position, joined_at, id) INTO v_ids FROM public.plg_agenda_turn_entries\n WHERE queue_id = v_queue.id AND area_key IS NOT DISTINCT FROM v_entry.area_key\n AND status IN ('available', 'away') AND id <> v_entry.id;\n v_ids := coalesce(v_ids, ARRAY[]::uuid[]);\n v_pos := least(p_position, coalesce(array_length(v_ids, 1), 0) + 1);\n v_ids := v_ids[1:v_pos - 1] || v_entry.id || v_ids[v_pos:];\n UPDATE public.plg_agenda_turn_entries e SET position = o.ord, updated_at = now()\n FROM unnest(v_ids) WITH ORDINALITY o(id, ord) WHERE e.id = o.id;\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'moved', 'frontdesk', auth.uid(), v_entry.area_key,\n jsonb_build_object('from_position', v_entry.position, 'to_position', v_pos));\n\n ELSIF p_action = 'start' THEN\n IF v_entry.status = 'serving' THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;\n UPDATE public.plg_agenda_turn_entries SET status = 'serving', away_reason = NULL, previous_position = position,\n state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n PERFORM public.agenda_turn__renumber(v_queue.id, v_entry.area_key);\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'started', 'frontdesk', auth.uid(), v_entry.area_key, jsonb_build_object('from_position', v_entry.position));\n\n ELSIF p_action = 'finish' THEN\n v_pos := public.agenda_turn__next_position(v_queue.id, v_entry.area_key);\n UPDATE public.plg_agenda_turn_entries SET status = 'available', away_reason = NULL, appointment_id = NULL,\n position = v_pos, available_at = now(), state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n PERFORM public.agenda_turn__renumber(v_queue.id, v_entry.area_key);\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'finished', 'frontdesk', auth.uid(), v_entry.area_key, jsonb_build_object('to_position', v_pos));\n\n ELSIF p_action = 'skip' THEN\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, reason, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'skipped', 'frontdesk', auth.uid(), v_entry.area_key, v_reason,\n jsonb_build_object('from_position', v_entry.position));\n END IF;\n\n RETURN jsonb_build_object('ok', true, 'changed', true);\nEND $$;\n\n-- 6) O pr\u00F3prio profissional (\"Cheguei\") -------------------------------------------\nCREATE OR REPLACE FUNCTION public.agenda_turn_queue_my_state(p_unit_id uuid)\nRETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_person uuid;\n v_queue public.plg_agenda_turn_queues;\n v_entry public.plg_agenda_turn_entries;\n v_tz text;\n v_rank bigint;\nBEGIN\n IF v_tenant IS NULL OR auth.uid() IS NULL OR p_unit_id IS NULL THEN RETURN NULL; END IF;\n SELECT m.person_id INTO v_person FROM app.memberships m\n WHERE m.tenant_id = v_tenant AND m.user_id = auth.uid() AND m.person_id IS NOT NULL LIMIT 1;\n IF v_person IS NULL OR NOT public.agenda_turn__in_unit(v_tenant, p_unit_id, v_person) THEN RETURN NULL; END IF;\n v_queue := public.agenda_turn__queue(v_tenant, p_unit_id);\n IF v_queue.id IS NULL OR NOT v_queue.is_active THEN RETURN NULL; END IF;\n v_tz := public.agenda_turn__tz(v_tenant, p_unit_id);\n\n SELECT * INTO v_entry FROM public.plg_agenda_turn_entries e\n WHERE e.queue_id = v_queue.id AND e.professional_id = v_person AND e.status <> 'unavailable'\n AND (coalesce(e.joined_at, e.available_at, e.state_changed_at) AT TIME ZONE v_tz)::date = (now() AT TIME ZONE v_tz)::date;\n IF v_entry.id IS NOT NULL AND v_entry.status IN ('available', 'away') THEN\n SELECT count(*) + 1 INTO v_rank FROM public.plg_agenda_turn_entries o\n WHERE o.queue_id = v_queue.id AND o.area_key IS NOT DISTINCT FROM v_entry.area_key\n AND o.status IN ('available', 'away') AND o.id <> v_entry.id\n AND (o.position, o.joined_at, o.id) < (v_entry.position, v_entry.joined_at, v_entry.id);\n END IF;\n RETURN jsonb_build_object(\n 'professionalId', v_person,\n 'selfCheckin', v_queue.self_checkin,\n 'requireLocation', v_queue.geofence_required,\n 'inQueue', v_entry.id IS NOT NULL,\n 'area', coalesce(v_entry.area_key, public.agenda_turn__area_of(v_tenant, v_person)),\n 'status', v_entry.status,\n 'awayReason', v_entry.away_reason,\n 'rank', v_rank);\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.agenda_turn_queue_my_action(\n p_unit_id uuid,\n p_action text,\n p_latitude double precision DEFAULT NULL,\n p_longitude double precision DEFAULT NULL,\n p_accuracy_m double precision DEFAULT NULL,\n p_reason text DEFAULT NULL\n) RETURNS jsonb LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_person uuid;\n v_queue public.plg_agenda_turn_queues;\n v_entry public.plg_agenda_turn_entries;\n v_reason text := nullif(btrim(coalesce(p_reason, '')), '');\n v_lat numeric;\n v_lng numeric;\n v_distance double precision;\n v_area text;\n v_pos bigint;\nBEGIN\n IF v_tenant IS NULL OR auth.uid() IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;\n IF p_action NOT IN ('join', 'leave', 'away', 'back', 'finish') THEN\n RAISE EXCEPTION 'turn_queue_invalid_action:%', p_action USING ERRCODE = '22023';\n END IF;\n SELECT m.person_id INTO v_person FROM app.memberships m\n WHERE m.tenant_id = v_tenant AND m.user_id = auth.uid() AND m.person_id IS NOT NULL LIMIT 1;\n IF v_person IS NULL THEN RAISE EXCEPTION 'turn_queue_professional_not_linked' USING ERRCODE = '42501'; END IF;\n IF NOT public.agenda_turn__in_unit(v_tenant, p_unit_id, v_person) THEN\n RAISE EXCEPTION 'turn_queue_professional_outside_unit' USING ERRCODE = '42501';\n END IF;\n SELECT * INTO v_queue FROM public.plg_agenda_turn_queues q\n WHERE q.tenant_id = v_tenant AND q.unit_id = p_unit_id AND q.queue_key = 'walk_in' FOR UPDATE;\n IF v_queue.id IS NULL OR NOT v_queue.is_active THEN RAISE EXCEPTION 'turn_queue_disabled' USING ERRCODE = '22023'; END IF;\n IF NOT v_queue.self_checkin THEN RAISE EXCEPTION 'turn_queue_checkin_by_frontdesk' USING ERRCODE = '22023'; END IF;\n PERFORM public.agenda_turn__purge_stale(v_queue.id, public.agenda_turn__tz(v_tenant, p_unit_id));\n\n SELECT * INTO v_entry FROM public.plg_agenda_turn_entries e\n WHERE e.queue_id = v_queue.id AND e.professional_id = v_person AND e.status <> 'unavailable';\n\n IF p_action = 'join' THEN\n IF v_entry.id IS NOT NULL THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;\n IF v_queue.geofence_required THEN\n SELECT u.latitude, u.longitude INTO v_lat, v_lng FROM public.v_units u WHERE u.id = p_unit_id AND u.tenant_id = v_tenant;\n IF v_lat IS NULL OR v_lng IS NULL THEN RAISE EXCEPTION 'unit_has_no_coordinates' USING ERRCODE = '22023'; END IF;\n IF p_latitude IS NULL OR p_longitude IS NULL THEN RAISE EXCEPTION 'location_required' USING ERRCODE = '22023'; END IF;\n v_distance := 6371000 * acos(least(1, greatest(-1,\n cos(radians(v_lat::double precision)) * cos(radians(p_latitude)) * cos(radians(p_longitude) - radians(v_lng::double precision))\n + sin(radians(v_lat::double precision)) * sin(radians(p_latitude)))));\n IF v_distance > v_queue.geofence_radius_meters + least(coalesce(p_accuracy_m, 0), 100) THEN\n RAISE EXCEPTION 'outside_geofence' USING ERRCODE = '42501';\n END IF;\n END IF;\n v_area := public.agenda_turn__area_of(v_tenant, v_person);\n v_pos := public.agenda_turn__next_position(v_queue.id, v_area);\n INSERT INTO public.plg_agenda_turn_entries\n (tenant_id, queue_id, unit_id, professional_id, status, position, area_key, joined_at, available_at, state_changed_at, updated_at)\n VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'available', v_pos, v_area, now(), now(), now(), now())\n ON CONFLICT (queue_id, professional_id) DO UPDATE SET\n status = 'available', position = v_pos, area_key = v_area, joined_at = now(), available_at = now(),\n state_changed_at = now(), appointment_id = NULL, away_reason = NULL, updated_at = now();\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key,\n inside_geofence, location_accuracy_meters, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'joined', 'app', auth.uid(), v_area,\n CASE WHEN v_queue.geofence_required THEN true END, p_accuracy_m, jsonb_build_object('to_position', v_pos));\n RETURN jsonb_build_object('ok', true, 'changed', true, 'area', v_area);\n END IF;\n\n IF v_entry.id IS NULL THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;\n\n IF p_action = 'leave' THEN\n UPDATE public.plg_agenda_turn_entries SET status = 'unavailable', away_reason = NULL, appointment_id = NULL,\n state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n PERFORM public.agenda_turn__renumber(v_queue.id, v_entry.area_key);\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key)\n VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'left', 'app', auth.uid(), v_entry.area_key);\n ELSIF p_action = 'away' THEN\n IF v_entry.status <> 'available' THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;\n UPDATE public.plg_agenda_turn_entries SET status = 'away', away_reason = coalesce(v_reason, 'Ausente'),\n state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, reason)\n VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'away', 'app', auth.uid(), v_entry.area_key, coalesce(v_reason, 'Ausente'));\n ELSIF p_action = 'back' THEN\n IF v_entry.status <> 'away' THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;\n UPDATE public.plg_agenda_turn_entries SET status = 'available', away_reason = NULL,\n state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key)\n VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'back', 'app', auth.uid(), v_entry.area_key);\n ELSIF p_action = 'finish' THEN\n IF v_entry.status <> 'serving' THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;\n v_pos := public.agenda_turn__next_position(v_queue.id, v_entry.area_key);\n UPDATE public.plg_agenda_turn_entries SET status = 'available', appointment_id = NULL, position = v_pos,\n available_at = now(), state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'finished', 'app', auth.uid(), v_entry.area_key, jsonb_build_object('to_position', v_pos));\n END IF;\n RETURN jsonb_build_object('ok', true, 'changed', true);\nEND $$;\n\n-- 7) Configurar ---------------------------------------------------------------------\nCREATE OR REPLACE FUNCTION public.agenda_turn_queue_config(p_unit_id uuid)\nRETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_queue public.plg_agenda_turn_queues;\nBEGIN\n IF v_tenant IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;\n IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.read', p_unit_id) THEN\n RAISE EXCEPTION 'agenda.turn_queue.read is required' USING ERRCODE = '42501';\n END IF;\n v_queue := public.agenda_turn__queue(v_tenant, p_unit_id);\n RETURN jsonb_build_object(\n 'unit', jsonb_build_object('auto', coalesce(v_queue.follows_appointments, true),\n 'selfCheckin', coalesce(v_queue.self_checkin, true),\n 'requireLocation', coalesce(v_queue.geofence_required, false)),\n 'areas', coalesce((SELECT jsonb_agg(jsonb_build_object('key', d.key, 'name', d.name, 'color', d.color, 'keywords', to_jsonb(d.keywords)) ORDER BY d.sort_order, d.key)\n FROM public.agenda_turn__area_defs(v_tenant) d WHERE d.key <> 'geral'), '[]'::jsonb),\n 'professionals', coalesce((\n SELECT jsonb_agg(jsonb_build_object(\n 'professionalId', p.id, 'name', p.name,\n 'manualArea', pa.area_key,\n 'autoArea', public.agenda_turn__auto_area_of(v_tenant, p.id)\n ) ORDER BY p.name)\n FROM public.people p\n LEFT JOIN public.plg_agenda_turn_professional_areas pa ON pa.tenant_id = v_tenant AND pa.professional_id = p.id\n WHERE p.tenant_id = v_tenant AND p.is_active AND public.agenda_turn__in_unit(v_tenant, p_unit_id, p.id)), '[]'::jsonb),\n 'quickServiceIds', coalesce((SELECT jsonb_agg(q.product_id) FROM public.plg_agenda_turn_quick_services q WHERE q.tenant_id = v_tenant), '[]'::jsonb),\n 'services', coalesce((SELECT jsonb_agg(jsonb_build_object('id', s.id, 'name', s.name) ORDER BY s.name)\n FROM public.products s\n WHERE s.tenant_id = v_tenant AND s.kind = 'service' AND coalesce(s.is_active, true)), '[]'::jsonb)\n );\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.agenda_turn_queue_save_config(p_unit_id uuid, p_config jsonb)\nRETURNS jsonb LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_area jsonb;\n v_i int := 0;\nBEGIN\n IF v_tenant IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;\n IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.edit', p_unit_id) THEN\n RAISE EXCEPTION 'agenda.turn_queue.edit is required' USING ERRCODE = '42501';\n END IF;\n\n IF jsonb_typeof(p_config->'unit') = 'object' THEN\n INSERT INTO public.plg_agenda_turn_queues AS q (tenant_id, unit_id, is_active, follows_appointments, self_checkin, geofence_required)\n VALUES (v_tenant, p_unit_id, false,\n coalesce((p_config->'unit'->>'auto')::boolean, true),\n coalesce((p_config->'unit'->>'selfCheckin')::boolean, true),\n coalesce((p_config->'unit'->>'requireLocation')::boolean, false))\n ON CONFLICT (tenant_id, unit_id, queue_key) DO UPDATE SET\n follows_appointments = coalesce((p_config->'unit'->>'auto')::boolean, q.follows_appointments),\n self_checkin = coalesce((p_config->'unit'->>'selfCheckin')::boolean, q.self_checkin),\n geofence_required = coalesce((p_config->'unit'->>'requireLocation')::boolean, q.geofence_required),\n updated_at = now();\n END IF;\n\n -- The list sent replaces the network's list; a missing area is deactivated.\n IF jsonb_typeof(p_config->'areas') = 'array' THEN\n UPDATE public.plg_agenda_turn_areas SET is_active = false, updated_at = now()\n WHERE tenant_id = v_tenant AND key NOT IN (SELECT x->>'key' FROM jsonb_array_elements(p_config->'areas') x);\n FOR v_area IN SELECT * FROM jsonb_array_elements(p_config->'areas') LOOP\n v_i := v_i + 1;\n INSERT INTO public.plg_agenda_turn_areas (tenant_id, key, name, color, keywords, sort_order, is_active)\n VALUES (v_tenant, v_area->>'key', btrim(v_area->>'name'), coalesce(v_area->>'color', 'slate'),\n coalesce(ARRAY(SELECT btrim(k) FROM jsonb_array_elements_text(coalesce(v_area->'keywords', '[]'::jsonb)) k WHERE btrim(k) <> ''), ARRAY[]::text[]),\n v_i, true)\n ON CONFLICT (tenant_id, key) DO UPDATE SET\n name = excluded.name, color = excluded.color, keywords = excluded.keywords,\n sort_order = excluded.sort_order, is_active = true, updated_at = now();\n END LOOP;\n END IF;\n\n -- {\"<person uuid>\": \"<area key>\" | null}; null = back to Autom\u00E1tico.\n IF jsonb_typeof(p_config->'professionalAreas') = 'object' THEN\n DELETE FROM public.plg_agenda_turn_professional_areas pa\n WHERE pa.tenant_id = v_tenant\n AND pa.professional_id::text IN (SELECT jsonb_object_keys(p_config->'professionalAreas'));\n INSERT INTO public.plg_agenda_turn_professional_areas (tenant_id, professional_id, area_key)\n SELECT v_tenant, p.id, p_config->'professionalAreas'->>p.id::text\n FROM public.people p\n WHERE p.tenant_id = v_tenant\n AND p.id::text IN (SELECT jsonb_object_keys(p_config->'professionalAreas'))\n AND nullif(p_config->'professionalAreas'->>p.id::text, '') IS NOT NULL;\n END IF;\n\n IF jsonb_typeof(p_config->'quickServiceIds') = 'array' THEN\n DELETE FROM public.plg_agenda_turn_quick_services WHERE tenant_id = v_tenant;\n INSERT INTO public.plg_agenda_turn_quick_services (tenant_id, product_id)\n SELECT v_tenant, s.id FROM public.products s\n WHERE s.tenant_id = v_tenant\n AND s.id::text IN (SELECT x #>> '{}' FROM jsonb_array_elements(p_config->'quickServiceIds') x)\n ON CONFLICT DO NOTHING;\n END IF;\n\n RETURN jsonb_build_object('ok', true);\nEND $$;\n\n-- 8) A fila anda sozinha pelo atendimento ---------------------------------------\n-- Replaces 016's trigger body: start leaves the queue unless every service is\n-- quick; leaving in_progress (any status) puts the person back at the END.\n-- Never blocks the agenda.\nCREATE OR REPLACE FUNCTION public.agenda_turn_queue_on_appointment_status()\nRETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_unit uuid := coalesce(NEW.unit_id, NEW.location_id);\n v_queue public.plg_agenda_turn_queues;\n v_entry public.plg_agenda_turn_entries;\n v_tz text;\n v_pos bigint;\n v_quick boolean;\nBEGIN\n IF NEW.assignee_id IS NULL OR v_unit IS NULL OR NEW.status IS NOT DISTINCT FROM OLD.status THEN RETURN NEW; END IF;\n IF NEW.status <> 'in_progress' AND OLD.status IS DISTINCT FROM 'in_progress' THEN RETURN NEW; END IF;\n\n SELECT * INTO v_queue FROM public.plg_agenda_turn_queues q\n WHERE q.tenant_id = NEW.tenant_id AND q.unit_id = v_unit AND q.queue_key = 'walk_in'\n AND q.is_active AND q.follows_appointments\n FOR UPDATE;\n IF v_queue.id IS NULL THEN RETURN NEW; END IF;\n v_tz := public.agenda_turn__tz(NEW.tenant_id, v_unit);\n\n SELECT * INTO v_entry FROM public.plg_agenda_turn_entries e\n WHERE e.queue_id = v_queue.id AND e.professional_id = NEW.assignee_id AND e.status <> 'unavailable'\n AND (coalesce(e.joined_at, e.available_at, e.state_changed_at) AT TIME ZONE v_tz)::date = (now() AT TIME ZONE v_tz)::date;\n IF v_entry.id IS NULL THEN RETURN NEW; END IF;\n\n IF NEW.status = 'in_progress' THEN\n IF v_entry.status = 'serving' THEN RETURN NEW; END IF;\n SELECT bool_and(q.product_id IS NOT NULL) INTO v_quick\n FROM public.appointment_items ai\n LEFT JOIN public.plg_agenda_turn_quick_services q ON q.tenant_id = NEW.tenant_id AND q.product_id = ai.service_id\n WHERE ai.booking_id = NEW.id;\n IF coalesce(v_quick, false) THEN\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, appointment_id, area_key)\n VALUES (NEW.tenant_id, v_queue.id, v_unit, NEW.assignee_id, 'quick_service', 'automatic', NEW.id, v_entry.area_key);\n RETURN NEW;\n END IF;\n UPDATE public.plg_agenda_turn_entries SET status = 'serving', away_reason = NULL, appointment_id = NEW.id,\n previous_position = position, state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n PERFORM public.agenda_turn__renumber(v_queue.id, v_entry.area_key);\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, appointment_id, area_key)\n VALUES (NEW.tenant_id, v_queue.id, v_unit, NEW.assignee_id, 'started', 'automatic', NEW.id, v_entry.area_key);\n ELSIF v_entry.status = 'serving' AND v_entry.appointment_id = NEW.id THEN\n v_pos := public.agenda_turn__next_position(v_queue.id, v_entry.area_key);\n UPDATE public.plg_agenda_turn_entries SET status = 'available', appointment_id = NULL, position = v_pos,\n available_at = now(), state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, appointment_id, area_key, payload)\n VALUES (NEW.tenant_id, v_queue.id, v_unit, NEW.assignee_id, 'finished', 'automatic', NEW.id, v_entry.area_key,\n jsonb_build_object('status', NEW.status, 'to_position', v_pos));\n END IF;\n RETURN NEW;\nEXCEPTION WHEN others THEN\n RAISE WARNING 'agenda_turn_queue_on_appointment_status: %', SQLERRM;\n RETURN NEW;\nEND $$;\n\n-- 9) Relat\u00F3rio: a justi\u00E7a da fila no per\u00EDodo --------------------------------------\nCREATE OR REPLACE FUNCTION public.agenda_turn_queue_report(p_unit_ids uuid[], p_from date, p_to date)\nRETURNS TABLE(unit_id uuid, unit_name text, professional_id uuid, professional_name text, area text,\n entradas bigint, atendimentos bigint, servicos_rapidos bigint, chamados bigint,\n pulos bigint, ausencias bigint, minutos_ausente numeric)\nLANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\nBEGIN\n IF v_tenant IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;\n RETURN QUERY\n WITH units_ok AS MATERIALIZED (\n SELECT u.id, u.name FROM public.v_units u\n WHERE u.tenant_id = v_tenant AND (p_unit_ids IS NULL OR u.id = ANY (p_unit_ids))\n AND app.has_unit(u.id) AND app.has_permission('agenda.turn_queue.read', u.id)\n ), ev AS MATERIALIZED (\n SELECT e.*, lead(e.occurred_at) OVER (PARTITION BY e.unit_id, e.professional_id ORDER BY e.occurred_at, e.id) AS proximo_em\n FROM public.plg_agenda_turn_events e JOIN units_ok u ON u.id = e.unit_id\n WHERE e.tenant_id = v_tenant\n AND (e.occurred_at AT TIME ZONE public.agenda_turn__tz(v_tenant, e.unit_id))::date BETWEEN p_from AND p_to\n )\n SELECT ev.unit_id, max(u.name), ev.professional_id, max(p.name), max(ev.area_key),\n count(*) FILTER (WHERE ev.event_type IN ('joined', 'available', 'override_available')),\n count(*) FILTER (WHERE ev.event_type IN ('started', 'advanced_to_service')),\n count(*) FILTER (WHERE ev.event_type = 'quick_service'),\n count(*) FILTER (WHERE ev.event_type = 'called'),\n count(*) FILTER (WHERE ev.event_type = 'skipped'),\n count(*) FILTER (WHERE ev.event_type = 'away'),\n round(coalesce(sum(extract(epoch FROM (ev.proximo_em - ev.occurred_at)) / 60)\n FILTER (WHERE ev.event_type = 'away' AND ev.proximo_em IS NOT NULL), 0)::numeric, 0)\n FROM ev\n JOIN units_ok u ON u.id = ev.unit_id\n JOIN public.people p ON p.id = ev.professional_id\n GROUP BY ev.unit_id, ev.professional_id\n ORDER BY 1, 7 DESC, 4;\nEND $$;\n\n-- 10) Grants e tempo real -----------------------------------------------------------\nREVOKE ALL ON FUNCTION public.agenda_turn__norm(text) FROM PUBLIC, anon;\nREVOKE ALL ON FUNCTION public.agenda_turn__area_defs(uuid) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.agenda_turn__auto_area_of(uuid, uuid) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.agenda_turn__area_of(uuid, uuid) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.agenda_turn__tz(uuid, uuid) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.agenda_turn__purge_stale(uuid, text) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.agenda_turn__renumber(uuid, text) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.agenda_turn__next_position(uuid, text) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.agenda_turn__in_unit(uuid, uuid, uuid) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.agenda_turn__queue(uuid, uuid) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.agenda_turn_queue_board(uuid) FROM PUBLIC, anon;\nREVOKE ALL ON FUNCTION public.agenda_turn_queue_act(uuid, uuid, text, integer, text) FROM PUBLIC, anon;\nREVOKE ALL ON FUNCTION public.agenda_turn_queue_my_state(uuid) FROM PUBLIC, anon;\nREVOKE ALL ON FUNCTION public.agenda_turn_queue_my_action(uuid, text, double precision, double precision, double precision, text) FROM PUBLIC, anon;\nREVOKE ALL ON FUNCTION public.agenda_turn_queue_config(uuid) FROM PUBLIC, anon;\nREVOKE ALL ON FUNCTION public.agenda_turn_queue_save_config(uuid, jsonb) FROM PUBLIC, anon;\nREVOKE ALL ON FUNCTION public.agenda_turn_queue_report(uuid[], date, date) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.agenda_turn_queue_board(uuid) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.agenda_turn_queue_act(uuid, uuid, text, integer, text) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.agenda_turn_queue_my_state(uuid) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.agenda_turn_queue_my_action(uuid, text, double precision, double precision, double precision, text) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.agenda_turn_queue_config(uuid) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.agenda_turn_queue_save_config(uuid, jsonb) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.agenda_turn_queue_report(uuid[], date, date) TO authenticated;\n\nDO $$\nBEGIN\n IF EXISTS (SELECT 1 FROM pg_publication WHERE pubname = 'supabase_realtime') THEN\n IF NOT EXISTS (SELECT 1 FROM pg_publication_tables WHERE pubname = 'supabase_realtime' AND schemaname = 'public' AND tablename = 'plg_agenda_turn_entries') THEN\n ALTER PUBLICATION supabase_realtime ADD TABLE public.plg_agenda_turn_entries;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_publication_tables WHERE pubname = 'supabase_realtime' AND schemaname = 'public' AND tablename = 'plg_agenda_turn_events') THEN\n ALTER PUBLICATION supabase_realtime ADD TABLE public.plg_agenda_turn_events;\n END IF;\n END IF;\nEND $$;\n";
23
+ export declare const MIGRATION_022_A_CHAVE_DA_FILA_E_CONFIGURACAO_DA_UNIDADE = "-- A chave da fila \u00E9 configura\u00E7\u00E3o da unidade, n\u00E3o coluna.\n--\n-- 016/020/021 guardaram \"ligada\", \"exigir loja\", raio, \"anda sozinha\" e\n-- \"check-in pelo profissional\" como colunas de plg_agenda_turn_queues. O SDK j\u00E1\n-- tem casa para isso (127/168): chave registrada em app.setting_keys com\n-- scope 'unit', valor em app.units.settings, lido em cascata\n-- unidade \u2192 casa \u2192 padr\u00E3o. Um toggle novo vira uma linha de registro, n\u00E3o uma\n-- migration de coluna, e a rede ganha \"padr\u00E3o da casa\" sem 18 escritas.\n--\n-- plg_agenda_turn_queues continua: \u00E9 a FILA (entidade, dona das entradas e da\n-- trilha). S\u00F3 a configura\u00E7\u00E3o sai dela. As colunas antigas ficam paradas e s\u00E3o\n-- removidas numa limpeza \u00E0 parte (apps publicados ainda leem is_active).\n-- Idempotente: o espelho s\u00F3 copia para unidade que ainda n\u00E3o tem a chave.\n\nSELECT app.seed_setting_keys($$[\n { \"key\": \"agenda.turn_queue.enabled\", \"type\": \"boolean\", \"default\": false, \"scope\": \"unit\", \"reader\": \"plugin:agenda\",\n \"description\": \"Usar a lista da vez nesta unidade (V1: companies.turn_queue_enabled).\" },\n { \"key\": \"agenda.turn_queue.follows_appointments\", \"type\": \"boolean\", \"default\": true, \"scope\": \"unit\", \"reader\": \"plugin:agenda\",\n \"description\": \"A fila anda sozinha pelo atendimento: iniciar tira da vez, concluir devolve ao fim (V1: companies.turn_queue_auto).\" },\n { \"key\": \"agenda.turn_queue.self_checkin\", \"type\": \"boolean\", \"default\": true, \"scope\": \"unit\", \"reader\": \"plugin:agenda\",\n \"description\": \"O profissional faz o pr\u00F3prio check-in pelo bot\u00E3o Cheguei (V1: companies.turn_queue_self_checkin).\" },\n { \"key\": \"agenda.turn_queue.require_location\", \"type\": \"boolean\", \"default\": false, \"scope\": \"unit\", \"reader\": \"plugin:agenda\",\n \"description\": \"O Cheguei s\u00F3 vale dentro do raio da loja (V1: companies.turn_queue_require_location).\" },\n { \"key\": \"agenda.turn_queue.radius_m\", \"type\": \"integer\", \"default\": 80, \"scope\": \"unit\", \"reader\": \"plugin:agenda\",\n \"description\": \"Raio da loja em metros, de 10 a 1000 (V1: companies.turn_queue_radius_m).\" }\n]$$::jsonb);\n\n-- Leitura pela linha da fila: nula quando n\u00E3o h\u00E1 fila, para o chamador decidir.\nCREATE OR REPLACE FUNCTION public.agenda_turn__on(p_queue public.plg_agenda_turn_queues, p_key text)\nRETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT CASE WHEN p_queue.id IS NULL THEN NULL\n ELSE (app.unit_setting_value(p_queue.tenant_id, p_queue.unit_id, 'agenda.turn_queue.' || p_key) #>> '{}')::boolean END;\n$$;\n\nCREATE OR REPLACE FUNCTION public.agenda_turn__radius(p_queue public.plg_agenda_turn_queues)\nRETURNS integer LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT coalesce((app.unit_setting_value(p_queue.tenant_id, p_queue.unit_id, 'agenda.turn_queue.radius_m') #>> '{}')::integer, 80);\n$$;\n\nCREATE OR REPLACE FUNCTION public.agenda_turn__unit_cfg(p_tenant uuid, p_unit uuid)\nRETURNS jsonb LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT jsonb_build_object(\n 'enabled', coalesce((app.unit_setting_value(p_tenant, p_unit, 'agenda.turn_queue.enabled') #>> '{}')::boolean, false),\n 'auto', coalesce((app.unit_setting_value(p_tenant, p_unit, 'agenda.turn_queue.follows_appointments') #>> '{}')::boolean, true),\n 'selfCheckin', coalesce((app.unit_setting_value(p_tenant, p_unit, 'agenda.turn_queue.self_checkin') #>> '{}')::boolean, true),\n 'requireLocation', coalesce((app.unit_setting_value(p_tenant, p_unit, 'agenda.turn_queue.require_location') #>> '{}')::boolean, false),\n 'radiusM', coalesce((app.unit_setting_value(p_tenant, p_unit, 'agenda.turn_queue.radius_m') #>> '{}')::integer, 80));\n$$;\n\n-- Grava s\u00F3 a diferen\u00E7a para o padr\u00E3o da casa; igual ao padr\u00E3o apaga a exce\u00E7\u00E3o.\n-- Compara pelo tenant da unidade, n\u00E3o da sess\u00E3o: dentro de migration n\u00E3o h\u00E1 sess\u00E3o.\nCREATE OR REPLACE FUNCTION public.agenda_turn__put(p_unit uuid, p_key text, p_value jsonb)\nRETURNS void LANGUAGE sql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\n UPDATE app.units u\n SET settings = app.jsonb_set_path(coalesce(u.settings, '{}'::jsonb), string_to_array('agenda.turn_queue.' || p_key, '.'),\n CASE WHEN app.unit_setting_value(u.tenant_id, NULL, 'agenda.turn_queue.' || p_key) IS NOT DISTINCT FROM p_value THEN NULL ELSE p_value END)\n WHERE u.id = p_unit AND p_value IS NOT NULL AND p_value <> 'null'::jsonb;\n$$;\n\n-- Espelho \u00FAnico do que as colunas diziam.\nDO $$\nDECLARE r record;\nBEGIN\n FOR r IN\n SELECT q.* FROM public.plg_agenda_turn_queues q JOIN app.units u ON u.id = q.unit_id\n WHERE q.queue_key = 'walk_in' AND u.settings #> '{agenda,turn_queue}' IS NULL\n LOOP\n PERFORM public.agenda_turn__put(r.unit_id, 'enabled', to_jsonb(r.is_active));\n PERFORM public.agenda_turn__put(r.unit_id, 'follows_appointments', to_jsonb(r.follows_appointments));\n PERFORM public.agenda_turn__put(r.unit_id, 'self_checkin', to_jsonb(r.self_checkin));\n PERFORM public.agenda_turn__put(r.unit_id, 'require_location', to_jsonb(r.geofence_required));\n PERFORM public.agenda_turn__put(r.unit_id, 'radius_m', to_jsonb(r.geofence_radius_meters));\n END LOOP;\n -- Re-pass what is already stored so values equal to the house default stop\n -- being exceptions (a first replay wrote them all).\n FOR r IN SELECT u.id, k.key, k.value FROM app.units u, jsonb_each(u.settings #> '{agenda,turn_queue}') k\n WHERE jsonb_typeof(u.settings #> '{agenda,turn_queue}') = 'object' LOOP\n PERFORM public.agenda_turn__put(r.id, r.key, r.value);\n END LOOP;\nEND $$;\n\nCOMMENT ON COLUMN public.plg_agenda_turn_queues.is_active IS 'Superada por app.units.settings agenda.turn_queue.enabled (022). N\u00E3o ler.';\nCOMMENT ON COLUMN public.plg_agenda_turn_queues.geofence_required IS 'Superada por agenda.turn_queue.require_location (022). N\u00E3o ler.';\nCOMMENT ON COLUMN public.plg_agenda_turn_queues.geofence_radius_meters IS 'Superada por agenda.turn_queue.radius_m (022). N\u00E3o ler.';\nCOMMENT ON COLUMN public.plg_agenda_turn_queues.follows_appointments IS 'Superada por agenda.turn_queue.follows_appointments (022). N\u00E3o ler.';\nCOMMENT ON COLUMN public.plg_agenda_turn_queues.self_checkin IS 'Superada por agenda.turn_queue.self_checkin (022). N\u00E3o ler.';\n\n-- A aba Lista da vez da unidade (020): mesma assinatura, agora escrevendo a configura\u00E7\u00E3o.\nCREATE OR REPLACE FUNCTION public.agenda_turn_queue_configure(\n p_unit_id uuid,\n p_enabled boolean,\n p_geofence_required boolean DEFAULT NULL,\n p_radius_meters integer DEFAULT NULL\n)\nRETURNS public.plg_agenda_turn_queues\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $function$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_row public.plg_agenda_turn_queues;\n v_cfg jsonb;\nBEGIN\n IF v_tenant IS NULL OR auth.uid() IS NULL THEN\n RAISE EXCEPTION 'agenda_turn_queue_configure: no session or no active tenant' USING ERRCODE = '42501';\n END IF;\n IF NOT EXISTS (SELECT 1 FROM app.units u WHERE u.id = p_unit_id AND u.tenant_id = v_tenant) THEN\n RAISE EXCEPTION 'agenda_turn_queue_configure: unit does not belong to this tenant' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('settings.manage', p_unit_id) THEN\n RAISE EXCEPTION 'agenda_turn_queue_configure: settings.manage is required on this unit' USING ERRCODE = '42501';\n END IF;\n IF p_radius_meters IS NOT NULL AND (p_radius_meters < 10 OR p_radius_meters > 1000) THEN\n RAISE EXCEPTION 'agenda_turn_queue_configure: radius must be between 10 and 1000 meters' USING ERRCODE = '22023';\n END IF;\n\n INSERT INTO public.plg_agenda_turn_queues (tenant_id, unit_id) VALUES (v_tenant, p_unit_id)\n ON CONFLICT (tenant_id, unit_id, queue_key) DO NOTHING;\n PERFORM public.agenda_turn__put(p_unit_id, 'enabled', to_jsonb(p_enabled));\n PERFORM public.agenda_turn__put(p_unit_id, 'require_location', to_jsonb(p_geofence_required));\n PERFORM public.agenda_turn__put(p_unit_id, 'radius_m', to_jsonb(p_radius_meters));\n\n -- The return row carries the EFFECTIVE values, so the caller reads the same\n -- fields it always did.\n SELECT * INTO v_row FROM public.plg_agenda_turn_queues q\n WHERE q.tenant_id = v_tenant AND q.unit_id = p_unit_id AND q.queue_key = 'walk_in';\n v_cfg := public.agenda_turn__unit_cfg(v_tenant, p_unit_id);\n v_row.is_active := (v_cfg->>'enabled')::boolean;\n v_row.geofence_required := (v_cfg->>'requireLocation')::boolean;\n v_row.geofence_radius_meters := (v_cfg->>'radiusM')::integer;\n v_row.follows_appointments := (v_cfg->>'auto')::boolean;\n v_row.self_checkin := (v_cfg->>'selfCheckin')::boolean;\n RETURN v_row;\nEND $function$;\n\nCREATE OR REPLACE FUNCTION public.agenda_turn_queue_config(p_unit_id uuid)\nRETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\nBEGIN\n IF v_tenant IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;\n IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.read', p_unit_id) THEN\n RAISE EXCEPTION 'agenda.turn_queue.read is required' USING ERRCODE = '42501';\n END IF;\n RETURN jsonb_build_object(\n 'unit', public.agenda_turn__unit_cfg(v_tenant, p_unit_id),\n 'areas', coalesce((SELECT jsonb_agg(jsonb_build_object('key', d.key, 'name', d.name, 'color', d.color, 'keywords', to_jsonb(d.keywords)) ORDER BY d.sort_order, d.key)\n FROM public.agenda_turn__area_defs(v_tenant) d WHERE d.key <> 'geral'), '[]'::jsonb),\n 'professionals', coalesce((\n SELECT jsonb_agg(jsonb_build_object(\n 'professionalId', p.id, 'name', p.name,\n 'manualArea', pa.area_key,\n 'autoArea', public.agenda_turn__auto_area_of(v_tenant, p.id)\n ) ORDER BY p.name)\n FROM public.people p\n LEFT JOIN public.plg_agenda_turn_professional_areas pa ON pa.tenant_id = v_tenant AND pa.professional_id = p.id\n WHERE p.tenant_id = v_tenant AND p.is_active AND public.agenda_turn__in_unit(v_tenant, p_unit_id, p.id)), '[]'::jsonb),\n 'quickServiceIds', coalesce((SELECT jsonb_agg(q.product_id) FROM public.plg_agenda_turn_quick_services q WHERE q.tenant_id = v_tenant), '[]'::jsonb),\n 'services', coalesce((SELECT jsonb_agg(jsonb_build_object('id', s.id, 'name', s.name) ORDER BY s.name)\n FROM public.products s\n WHERE s.tenant_id = v_tenant AND s.kind = 'service' AND coalesce(s.is_active, true)), '[]'::jsonb)\n );\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.agenda_turn_queue_save_config(p_unit_id uuid, p_config jsonb)\nRETURNS jsonb LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_area jsonb;\n v_i int := 0;\nBEGIN\n IF v_tenant IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;\n IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.edit', p_unit_id) THEN\n RAISE EXCEPTION 'agenda.turn_queue.edit is required' USING ERRCODE = '42501';\n END IF;\n IF NOT EXISTS (SELECT 1 FROM app.units u WHERE u.id = p_unit_id AND u.tenant_id = v_tenant) THEN\n RAISE EXCEPTION 'unit does not belong to this tenant' USING ERRCODE = '42501';\n END IF;\n\n IF jsonb_typeof(p_config->'unit') = 'object' THEN\n PERFORM public.agenda_turn__put(p_unit_id, 'follows_appointments', p_config->'unit'->'auto');\n PERFORM public.agenda_turn__put(p_unit_id, 'self_checkin', p_config->'unit'->'selfCheckin');\n PERFORM public.agenda_turn__put(p_unit_id, 'require_location', p_config->'unit'->'requireLocation');\n END IF;\n\n -- The list sent replaces the network's list; a missing area is deactivated.\n IF jsonb_typeof(p_config->'areas') = 'array' THEN\n UPDATE public.plg_agenda_turn_areas SET is_active = false, updated_at = now()\n WHERE tenant_id = v_tenant AND key NOT IN (SELECT x->>'key' FROM jsonb_array_elements(p_config->'areas') x);\n FOR v_area IN SELECT * FROM jsonb_array_elements(p_config->'areas') LOOP\n v_i := v_i + 1;\n INSERT INTO public.plg_agenda_turn_areas (tenant_id, key, name, color, keywords, sort_order, is_active)\n VALUES (v_tenant, v_area->>'key', btrim(v_area->>'name'), coalesce(v_area->>'color', 'slate'),\n coalesce(ARRAY(SELECT btrim(k) FROM jsonb_array_elements_text(coalesce(v_area->'keywords', '[]'::jsonb)) k WHERE btrim(k) <> ''), ARRAY[]::text[]),\n v_i, true)\n ON CONFLICT (tenant_id, key) DO UPDATE SET\n name = excluded.name, color = excluded.color, keywords = excluded.keywords,\n sort_order = excluded.sort_order, is_active = true, updated_at = now();\n END LOOP;\n END IF;\n\n IF jsonb_typeof(p_config->'professionalAreas') = 'object' THEN\n DELETE FROM public.plg_agenda_turn_professional_areas pa\n WHERE pa.tenant_id = v_tenant\n AND pa.professional_id::text IN (SELECT jsonb_object_keys(p_config->'professionalAreas'));\n INSERT INTO public.plg_agenda_turn_professional_areas (tenant_id, professional_id, area_key)\n SELECT v_tenant, p.id, p_config->'professionalAreas'->>p.id::text\n FROM public.people p\n WHERE p.tenant_id = v_tenant\n AND p.id::text IN (SELECT jsonb_object_keys(p_config->'professionalAreas'))\n AND nullif(p_config->'professionalAreas'->>p.id::text, '') IS NOT NULL;\n END IF;\n\n IF jsonb_typeof(p_config->'quickServiceIds') = 'array' THEN\n DELETE FROM public.plg_agenda_turn_quick_services WHERE tenant_id = v_tenant;\n INSERT INTO public.plg_agenda_turn_quick_services (tenant_id, product_id)\n SELECT v_tenant, s.id FROM public.products s\n WHERE s.tenant_id = v_tenant\n AND s.id::text IN (SELECT x #>> '{}' FROM jsonb_array_elements(p_config->'quickServiceIds') x)\n ON CONFLICT DO NOTHING;\n END IF;\n\n RETURN jsonb_build_object('ok', true);\nEND $$;\n\n-- Fun\u00E7\u00F5es de 016/017/021 com a mesma l\u00F3gica, lendo a configura\u00E7\u00E3o pela cascata.\nCREATE OR REPLACE FUNCTION public.agenda_turn_queue_set_availability(\n p_available boolean,\n p_unit_id uuid,\n p_latitude numeric DEFAULT NULL,\n p_longitude numeric DEFAULT NULL,\n p_accuracy_meters numeric DEFAULT NULL,\n p_origin text DEFAULT 'app'\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_actor uuid := auth.uid();\n v_person uuid;\n v_queue public.plg_agenda_turn_queues%ROWTYPE;\n v_lat numeric;\n v_lng numeric;\n v_distance numeric;\n v_position bigint;\n v_entry uuid;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501';\n END IF;\n IF p_unit_id IS NULL THEN\n RAISE EXCEPTION 'unit_required' USING ERRCODE = '22023';\n END IF;\n IF p_origin <> 'app' THEN\n RAISE EXCEPTION 'self_availability_origin_must_be_app' USING ERRCODE = '22023';\n END IF;\n IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.edit', p_unit_id) THEN\n RAISE EXCEPTION 'agenda.turn_queue.edit is required' USING ERRCODE = '42501';\n END IF;\n\n v_person := app.person_for_login(v_tenant, v_actor);\n IF v_person IS NULL THEN\n RAISE EXCEPTION 'sem_ficha_de_profissional' USING ERRCODE = '42501';\n END IF;\n\n INSERT INTO public.plg_agenda_turn_queues (tenant_id, unit_id)\n VALUES (v_tenant, p_unit_id)\n ON CONFLICT (tenant_id, unit_id, queue_key) DO NOTHING;\n\n SELECT * INTO v_queue\n FROM public.plg_agenda_turn_queues q\n WHERE q.tenant_id = v_tenant AND q.unit_id = p_unit_id\n AND q.queue_key = 'walk_in' AND public.agenda_turn__on(q, 'enabled')\n FOR UPDATE;\n IF v_queue.id IS NULL THEN\n RAISE EXCEPTION 'turn_queue_disabled' USING ERRCODE = '42501';\n END IF;\n\n IF p_available AND public.agenda_turn__on(v_queue, 'require_location') THEN\n SELECT u.latitude, u.longitude INTO v_lat, v_lng\n FROM public.v_units u\n WHERE u.id = p_unit_id AND u.tenant_id = v_tenant;\n IF v_lat IS NULL OR v_lng IS NULL THEN\n RAISE EXCEPTION 'unit_has_no_coordinates' USING ERRCODE = '22023';\n END IF;\n IF p_latitude IS NULL OR p_longitude IS NULL THEN\n RAISE EXCEPTION 'location_required' USING ERRCODE = '22023';\n END IF;\n v_distance := 6371000 * acos(least(1, greatest(-1,\n cos(radians(v_lat::double precision)) * cos(radians(p_latitude::double precision))\n * cos(radians(p_longitude::double precision) - radians(v_lng::double precision))\n + sin(radians(v_lat::double precision)) * sin(radians(p_latitude::double precision))\n )));\n IF v_distance > public.agenda_turn__radius(v_queue) THEN\n RAISE EXCEPTION 'outside_geofence' USING ERRCODE = '42501';\n END IF;\n END IF;\n\n SELECT coalesce(max(e.position), 0) + 1 INTO v_position\n FROM public.plg_agenda_turn_entries e WHERE e.queue_id = v_queue.id;\n\n INSERT INTO public.plg_agenda_turn_entries\n (tenant_id, queue_id, unit_id, professional_id, status, position, available_at, state_changed_at, appointment_id, updated_at)\n VALUES\n (v_tenant, v_queue.id, p_unit_id, v_person,\n CASE WHEN p_available THEN 'available' ELSE 'unavailable' END,\n CASE WHEN p_available THEN v_position ELSE 0 END,\n CASE WHEN p_available THEN now() ELSE NULL END,\n now(), NULL, now())\n ON CONFLICT (queue_id, professional_id) DO UPDATE SET\n status = EXCLUDED.status,\n position = CASE WHEN p_available THEN v_position ELSE public.plg_agenda_turn_entries.position END,\n available_at = EXCLUDED.available_at,\n state_changed_at = now(),\n appointment_id = NULL,\n updated_at = now()\n RETURNING id INTO v_entry;\n\n INSERT INTO public.plg_agenda_turn_events\n (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id,\n inside_geofence, location_accuracy_meters, payload)\n VALUES\n (v_tenant, v_queue.id, p_unit_id, v_person,\n CASE WHEN p_available THEN 'available' ELSE 'left' END,\n 'app', v_actor, CASE WHEN p_available THEN true ELSE NULL END,\n CASE WHEN p_available THEN p_accuracy_meters ELSE NULL END,\n jsonb_build_object('entry_id', v_entry));\n\n RETURN jsonb_build_object('ok', true, 'status', CASE WHEN p_available THEN 'available' ELSE 'unavailable' END);\nEND $$;\n\n\nCREATE OR REPLACE FUNCTION public.agenda_turn_queue_snapshot(p_unit_id uuid)\nRETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' STABLE AS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_queue public.plg_agenda_turn_queues%ROWTYPE;\n v_person uuid;\n v_timezone text;\n v_day date;\n v_entries jsonb;\n v_events jsonb;\n v_professionals jsonb;\nBEGIN\n IF p_unit_id IS NULL THEN\n RAISE EXCEPTION 'unit_required' USING ERRCODE = '22023';\n END IF;\n IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.read', p_unit_id) THEN\n RAISE EXCEPTION 'agenda.turn_queue.read is required' USING ERRCODE = '42501';\n END IF;\n\n SELECT coalesce(t.settings->>'timezone', 'America/Sao_Paulo')\n INTO v_timezone\n FROM public.tenants t\n WHERE t.id = v_tenant;\n v_day := (now() AT TIME ZONE v_timezone)::date;\n v_person := app.person_for_login(v_tenant, auth.uid());\n\n SELECT * INTO v_queue\n FROM public.plg_agenda_turn_queues q\n WHERE q.tenant_id = v_tenant\n AND q.unit_id = p_unit_id\n AND q.queue_key = 'walk_in'\n AND public.agenda_turn__on(q, 'enabled');\n\n SELECT coalesce(jsonb_agg(jsonb_build_object(\n 'id', e.id, 'queue_id', e.queue_id, 'unit_id', e.unit_id,\n 'professional_id', e.professional_id, 'professional_name', p.name,\n 'professional_avatar_url', p.avatar_url, 'status', e.status, 'position', e.position,\n 'state_changed_at', e.state_changed_at, 'available_at', e.available_at,\n 'appointment_id', e.appointment_id\n ) ORDER BY CASE e.status WHEN 'available' THEN 0 WHEN 'serving' THEN 1 ELSE 2 END, e.position), '[]'::jsonb)\n INTO v_entries\n FROM public.plg_agenda_turn_entries e\n JOIN public.people p ON p.id = e.professional_id AND p.tenant_id = e.tenant_id\n WHERE e.queue_id = v_queue.id;\n\n SELECT coalesce(jsonb_agg(jsonb_build_object(\n 'id', ev.id, 'professional_id', ev.professional_id, 'professional_name', p.name,\n 'event_type', ev.event_type, 'origin', ev.origin, 'reason', ev.reason,\n 'occurred_at', ev.occurred_at\n ) ORDER BY ev.occurred_at DESC), '[]'::jsonb)\n INTO v_events\n FROM (\n SELECT * FROM public.plg_agenda_turn_events\n WHERE queue_id = v_queue.id\n AND (occurred_at AT TIME ZONE v_timezone)::date = v_day\n ORDER BY occurred_at DESC LIMIT 50\n ) ev\n JOIN public.people p ON p.id = ev.professional_id AND p.tenant_id = ev.tenant_id;\n\n WITH scheduled AS (\n SELECT DISTINCT s.assignee_id AS professional_id\n FROM public.schedules s\n WHERE s.tenant_id = v_tenant\n AND s.kind = 'working_hours'\n AND s.assignee_id IS NOT NULL\n AND coalesce(s.unit_id, s.location_id) = p_unit_id\n AND (s.valid_from IS NULL OR s.valid_from <= v_day)\n AND (s.valid_to IS NULL OR v_day <= s.valid_to)\n AND (\n (s.specific_date = v_day AND s.is_active)\n OR (\n s.specific_date IS NULL\n AND s.is_active\n AND s.day_of_week = extract(dow FROM v_day)::smallint\n AND public.agenda_week_parity_active(s.valid_from, v_day, s.week_interval)\n AND NOT EXISTS (\n SELECT 1 FROM public.schedules day_override\n WHERE day_override.tenant_id = s.tenant_id\n AND day_override.assignee_id = s.assignee_id\n AND coalesce(day_override.unit_id, day_override.location_id) = p_unit_id\n AND day_override.specific_date = v_day\n )\n )\n )\n ), booked AS (\n SELECT a.assignee_id AS professional_id,\n count(*)::integer AS appointment_count,\n min(a.starts_at) FILTER (WHERE a.starts_at >= now()) AS next_appointment_at\n FROM public.appointments a\n WHERE a.tenant_id = v_tenant\n AND a.assignee_id IS NOT NULL\n AND coalesce(a.unit_id, a.location_id) = p_unit_id\n AND (a.starts_at AT TIME ZONE v_timezone)::date = v_day\n AND coalesce(a.status, '') NOT IN ('cancelled', 'canceled')\n GROUP BY a.assignee_id\n ), candidates AS (\n SELECT professional_id FROM scheduled\n UNION\n SELECT professional_id FROM booked\n UNION\n SELECT e.professional_id FROM public.plg_agenda_turn_entries e WHERE e.queue_id = v_queue.id\n )\n SELECT coalesce(jsonb_agg(jsonb_build_object(\n 'professional_id', p.id,\n 'professional_name', p.name,\n 'professional_avatar_url', p.avatar_url,\n 'status', coalesce(e.status, 'unavailable'),\n 'position', coalesce(e.position, 0),\n 'last_checkin_at', last_checkin.occurred_at,\n 'is_scheduled', scheduled.professional_id IS NOT NULL,\n 'appointment_count', coalesce(booked.appointment_count, 0),\n 'next_appointment_at', booked.next_appointment_at\n ) ORDER BY\n CASE coalesce(e.status, 'unavailable') WHEN 'available' THEN 0 WHEN 'serving' THEN 1 ELSE 2 END,\n CASE WHEN booked.next_appointment_at IS NOT NULL THEN 0 ELSE 1 END,\n e.position NULLS LAST,\n p.name), '[]'::jsonb)\n INTO v_professionals\n FROM candidates c\n JOIN public.people p ON p.id = c.professional_id AND p.tenant_id = v_tenant AND p.is_active\n LEFT JOIN scheduled ON scheduled.professional_id = p.id\n LEFT JOIN booked ON booked.professional_id = p.id\n LEFT JOIN public.plg_agenda_turn_entries e ON e.queue_id = v_queue.id AND e.professional_id = p.id\n LEFT JOIN LATERAL (\n SELECT ev.occurred_at\n FROM public.plg_agenda_turn_events ev\n WHERE ev.queue_id = v_queue.id\n AND ev.professional_id = p.id\n AND ev.event_type IN ('available', 'override_available')\n AND (ev.occurred_at AT TIME ZONE v_timezone)::date = v_day\n ORDER BY ev.occurred_at DESC\n LIMIT 1\n ) last_checkin ON true;\n\n RETURN jsonb_build_object(\n 'queue_id', v_queue.id,\n 'unit_id', p_unit_id,\n 'queue_name', coalesce(v_queue.name, 'Pr\u00F3ximo da vez'),\n 'current_professional_id', v_person,\n 'date', v_day,\n 'professionals', v_professionals,\n 'entries', v_entries,\n 'events', v_events\n );\nEND $$;\n\n\nCREATE OR REPLACE FUNCTION public.agenda_turn_queue_override(\n p_professional_id uuid,\n p_unit_id uuid,\n p_available boolean,\n p_reason text\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_queue public.plg_agenda_turn_queues%ROWTYPE;\n v_position bigint;\nBEGIN\n IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.manage', p_unit_id) THEN\n RAISE EXCEPTION 'agenda.turn_queue.manage is required' USING ERRCODE = '42501';\n END IF;\n IF nullif(trim(p_reason), '') IS NULL THEN\n RAISE EXCEPTION 'override_reason_required' USING ERRCODE = '22023';\n END IF;\n SELECT * INTO v_queue FROM public.plg_agenda_turn_queues q\n WHERE q.tenant_id = v_tenant AND q.unit_id = p_unit_id AND q.queue_key = 'walk_in' AND public.agenda_turn__on(q, 'enabled')\n FOR UPDATE;\n IF v_queue.id IS NULL THEN RAISE EXCEPTION 'turn_queue_disabled' USING ERRCODE = '42501'; END IF;\n SELECT coalesce(max(position), 0) + 1 INTO v_position FROM public.plg_agenda_turn_entries WHERE queue_id = v_queue.id;\n INSERT INTO public.plg_agenda_turn_entries\n (tenant_id, queue_id, unit_id, professional_id, status, position, available_at, state_changed_at, updated_at)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id,\n CASE WHEN p_available THEN 'available' ELSE 'unavailable' END,\n CASE WHEN p_available THEN v_position ELSE 0 END,\n CASE WHEN p_available THEN now() ELSE NULL END, now(), now())\n ON CONFLICT (queue_id, professional_id) DO UPDATE SET\n status = EXCLUDED.status, position = CASE WHEN p_available THEN v_position ELSE public.plg_agenda_turn_entries.position END,\n available_at = EXCLUDED.available_at, state_changed_at = now(), appointment_id = NULL, updated_at = now();\n INSERT INTO public.plg_agenda_turn_events\n (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, reason)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id,\n CASE WHEN p_available THEN 'override_available' ELSE 'override_left' END,\n 'frontdesk', auth.uid(), trim(p_reason));\n RETURN jsonb_build_object('ok', true);\nEND $$;\n\n\nCREATE OR REPLACE FUNCTION public.agenda_turn_queue_on_appointment_status()\nRETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_unit uuid := coalesce(NEW.unit_id, NEW.location_id);\n v_queue public.plg_agenda_turn_queues;\n v_entry public.plg_agenda_turn_entries;\n v_tz text;\n v_pos bigint;\n v_quick boolean;\nBEGIN\n IF NEW.assignee_id IS NULL OR v_unit IS NULL OR NEW.status IS NOT DISTINCT FROM OLD.status THEN RETURN NEW; END IF;\n IF NEW.status <> 'in_progress' AND OLD.status IS DISTINCT FROM 'in_progress' THEN RETURN NEW; END IF;\n\n SELECT * INTO v_queue FROM public.plg_agenda_turn_queues q\n WHERE q.tenant_id = NEW.tenant_id AND q.unit_id = v_unit AND q.queue_key = 'walk_in'\n AND public.agenda_turn__on(q, 'enabled') AND public.agenda_turn__on(q, 'follows_appointments')\n FOR UPDATE;\n IF v_queue.id IS NULL THEN RETURN NEW; END IF;\n v_tz := public.agenda_turn__tz(NEW.tenant_id, v_unit);\n\n SELECT * INTO v_entry FROM public.plg_agenda_turn_entries e\n WHERE e.queue_id = v_queue.id AND e.professional_id = NEW.assignee_id AND e.status <> 'unavailable'\n AND (coalesce(e.joined_at, e.available_at, e.state_changed_at) AT TIME ZONE v_tz)::date = (now() AT TIME ZONE v_tz)::date;\n IF v_entry.id IS NULL THEN RETURN NEW; END IF;\n\n IF NEW.status = 'in_progress' THEN\n IF v_entry.status = 'serving' THEN RETURN NEW; END IF;\n SELECT bool_and(q.product_id IS NOT NULL) INTO v_quick\n FROM public.appointment_items ai\n LEFT JOIN public.plg_agenda_turn_quick_services q ON q.tenant_id = NEW.tenant_id AND q.product_id = ai.service_id\n WHERE ai.booking_id = NEW.id;\n IF coalesce(v_quick, false) THEN\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, appointment_id, area_key)\n VALUES (NEW.tenant_id, v_queue.id, v_unit, NEW.assignee_id, 'quick_service', 'automatic', NEW.id, v_entry.area_key);\n RETURN NEW;\n END IF;\n UPDATE public.plg_agenda_turn_entries SET status = 'serving', away_reason = NULL, appointment_id = NEW.id,\n previous_position = position, state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n PERFORM public.agenda_turn__renumber(v_queue.id, v_entry.area_key);\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, appointment_id, area_key)\n VALUES (NEW.tenant_id, v_queue.id, v_unit, NEW.assignee_id, 'started', 'automatic', NEW.id, v_entry.area_key);\n ELSIF v_entry.status = 'serving' AND v_entry.appointment_id = NEW.id THEN\n v_pos := public.agenda_turn__next_position(v_queue.id, v_entry.area_key);\n UPDATE public.plg_agenda_turn_entries SET status = 'available', appointment_id = NULL, position = v_pos,\n available_at = now(), state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, appointment_id, area_key, payload)\n VALUES (NEW.tenant_id, v_queue.id, v_unit, NEW.assignee_id, 'finished', 'automatic', NEW.id, v_entry.area_key,\n jsonb_build_object('status', NEW.status, 'to_position', v_pos));\n END IF;\n RETURN NEW;\nEXCEPTION WHEN others THEN\n RAISE WARNING 'agenda_turn_queue_on_appointment_status: %', SQLERRM;\n RETURN NEW;\nEND $$;\n\n\nCREATE OR REPLACE FUNCTION public.agenda_turn_queue_board(p_unit_id uuid)\nRETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_queue public.plg_agenda_turn_queues;\n v_tz text;\n v_day date;\n v_lat numeric;\n v_lng numeric;\n v_result jsonb;\nBEGIN\n IF v_tenant IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;\n IF p_unit_id IS NULL THEN RAISE EXCEPTION 'unit_required' USING ERRCODE = '22023'; END IF;\n IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.read', p_unit_id) THEN\n RAISE EXCEPTION 'agenda.turn_queue.read is required' USING ERRCODE = '42501';\n END IF;\n v_queue := public.agenda_turn__queue(v_tenant, p_unit_id);\n v_tz := public.agenda_turn__tz(v_tenant, p_unit_id);\n v_day := (now() AT TIME ZONE v_tz)::date;\n SELECT u.latitude, u.longitude INTO v_lat, v_lng FROM public.v_units u WHERE u.id = p_unit_id AND u.tenant_id = v_tenant;\n\n WITH ent AS (\n SELECT e.*, row_number() OVER (PARTITION BY e.area_key, (e.status IN ('available', 'away'))\n ORDER BY e.position, e.joined_at, e.id) AS rnk\n FROM public.plg_agenda_turn_entries e\n WHERE e.queue_id = v_queue.id AND e.status IN ('available', 'away', 'serving')\n AND (coalesce(e.joined_at, e.available_at, e.state_changed_at) AT TIME ZONE v_tz)::date = v_day\n ), appts AS (\n SELECT a.assignee_id AS professional_id, count(*)::int AS n,\n min(a.starts_at) FILTER (WHERE a.starts_at >= now() AND a.status NOT IN ('completed', 'cancelled', 'no_show')) AS proximo\n FROM public.appointments a\n WHERE a.tenant_id = v_tenant AND a.assignee_id IS NOT NULL\n AND coalesce(a.unit_id, a.location_id) = p_unit_id\n AND (a.starts_at AT TIME ZONE v_tz)::date = v_day\n AND a.status <> 'cancelled'\n GROUP BY 1\n ), roster AS (\n SELECT p.id, p.name, p.avatar_url, public.agenda_turn__area_of(v_tenant, p.id) AS area\n FROM public.people p\n WHERE p.tenant_id = v_tenant AND p.is_active\n AND (public.agenda_turn__in_unit(v_tenant, p_unit_id, p.id)\n OR EXISTS (SELECT 1 FROM appts t WHERE t.professional_id = p.id))\n ), served AS (\n SELECT ev.professional_id,\n count(*) FILTER (WHERE ev.event_type IN ('started', 'advanced_to_service'))::int AS atendimentos\n FROM public.plg_agenda_turn_events ev\n WHERE ev.queue_id = v_queue.id AND (ev.occurred_at AT TIME ZONE v_tz)::date = v_day\n GROUP BY 1\n )\n SELECT jsonb_build_object(\n 'unitId', p_unit_id,\n 'today', v_day,\n 'enabled', coalesce(public.agenda_turn__on(v_queue, 'enabled'), false),\n 'selfCheckin', coalesce(public.agenda_turn__on(v_queue, 'self_checkin'), true),\n 'auto', coalesce(public.agenda_turn__on(v_queue, 'follows_appointments'), true),\n 'requireLocation', coalesce(public.agenda_turn__on(v_queue, 'require_location'), false),\n 'locationConfigured', v_lat IS NOT NULL AND v_lng IS NOT NULL,\n 'areas', coalesce((SELECT jsonb_agg(jsonb_build_object('key', d.key, 'name', d.name, 'color', d.color) ORDER BY d.sort_order, d.key)\n FROM public.agenda_turn__area_defs(v_tenant) d), '[]'::jsonb),\n 'entries', coalesce((\n SELECT jsonb_agg(jsonb_build_object(\n 'professionalId', e.professional_id, 'name', p.name, 'avatarUrl', p.avatar_url,\n 'area', coalesce(e.area_key, 'geral'), 'status', e.status,\n 'rank', CASE WHEN e.status IN ('available', 'away') THEN e.rnk END,\n 'awayReason', e.away_reason, 'since', e.state_changed_at, 'joinedAt', e.joined_at,\n 'appointmentId', e.appointment_id,\n 'servedToday', coalesce(s.atendimentos, 0)\n ) ORDER BY e.area_key, (e.status = 'serving'), e.rnk)\n FROM ent e JOIN public.people p ON p.id = e.professional_id\n LEFT JOIN served s ON s.professional_id = e.professional_id), '[]'::jsonb),\n 'roster', coalesce((\n SELECT jsonb_agg(jsonb_build_object(\n 'professionalId', r.id, 'name', r.name, 'avatarUrl', r.avatar_url, 'area', r.area,\n 'appointmentsToday', coalesce(t.n, 0), 'nextAppointmentAt', t.proximo,\n 'servedToday', coalesce(s.atendimentos, 0)) ORDER BY r.name)\n FROM roster r\n LEFT JOIN appts t ON t.professional_id = r.id\n LEFT JOIN served s ON s.professional_id = r.id\n WHERE NOT EXISTS (SELECT 1 FROM ent e WHERE e.professional_id = r.id)), '[]'::jsonb),\n 'events', coalesce((\n SELECT jsonb_agg(x.item ORDER BY x.at DESC) FROM (\n SELECT jsonb_build_object('id', ev.id, 'professionalId', ev.professional_id, 'name', ep.name,\n 'type', ev.event_type, 'origin', ev.origin, 'area', ev.area_key,\n 'reason', coalesce(ev.reason, ev.payload->>'reason'), 'createdAt', ev.occurred_at) AS item,\n ev.occurred_at AS at\n FROM public.plg_agenda_turn_events ev\n JOIN public.people ep ON ep.id = ev.professional_id\n WHERE ev.queue_id = v_queue.id AND (ev.occurred_at AT TIME ZONE v_tz)::date = v_day\n ORDER BY ev.occurred_at DESC LIMIT 60) x), '[]'::jsonb)\n ) INTO v_result;\n RETURN v_result;\nEND $$;\n\n\nCREATE OR REPLACE FUNCTION public.agenda_turn_queue_act(\n p_unit_id uuid,\n p_professional_id uuid,\n p_action text,\n p_position integer DEFAULT NULL,\n p_reason text DEFAULT NULL\n) RETURNS jsonb LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_queue public.plg_agenda_turn_queues;\n v_entry public.plg_agenda_turn_entries;\n v_need text := CASE p_action WHEN 'join' THEN 'create' WHEN 'leave' THEN 'delete' ELSE 'edit' END;\n v_reason text := nullif(btrim(coalesce(p_reason, '')), '');\n v_area text;\n v_pos bigint;\n v_ids uuid[];\n v_users uuid[];\nBEGIN\n IF v_tenant IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;\n IF p_action NOT IN ('join', 'leave', 'away', 'back', 'move', 'start', 'finish', 'skip', 'call') THEN\n RAISE EXCEPTION 'turn_queue_invalid_action:%', p_action USING ERRCODE = '22023';\n END IF;\n IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.' || v_need, p_unit_id) THEN\n RAISE EXCEPTION 'agenda.turn_queue.% is required', v_need USING ERRCODE = '42501';\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.people p WHERE p.id = p_professional_id AND p.tenant_id = v_tenant AND p.is_active) THEN\n RAISE EXCEPTION 'turn_queue_invalid_professional' USING ERRCODE = '22023';\n END IF;\n\n SELECT * INTO v_queue FROM public.plg_agenda_turn_queues q\n WHERE q.tenant_id = v_tenant AND q.unit_id = p_unit_id AND q.queue_key = 'walk_in' FOR UPDATE;\n IF v_queue.id IS NULL OR NOT public.agenda_turn__on(v_queue, 'enabled') THEN\n RAISE EXCEPTION 'turn_queue_disabled' USING ERRCODE = '22023';\n END IF;\n PERFORM public.agenda_turn__purge_stale(v_queue.id, public.agenda_turn__tz(v_tenant, p_unit_id));\n\n SELECT * INTO v_entry FROM public.plg_agenda_turn_entries e\n WHERE e.queue_id = v_queue.id AND e.professional_id = p_professional_id;\n IF v_entry.id IS NOT NULL AND v_entry.status = 'unavailable' THEN v_entry := NULL; END IF;\n\n IF p_action = 'join' THEN\n IF v_entry.id IS NOT NULL THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;\n v_area := public.agenda_turn__area_of(v_tenant, p_professional_id);\n v_pos := public.agenda_turn__next_position(v_queue.id, v_area);\n INSERT INTO public.plg_agenda_turn_entries\n (tenant_id, queue_id, unit_id, professional_id, status, position, area_key, joined_at, available_at, state_changed_at, appointment_id, away_reason, updated_at)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'available', v_pos, v_area, now(), now(), now(), NULL, NULL, now())\n ON CONFLICT (queue_id, professional_id) DO UPDATE SET\n status = 'available', position = v_pos, area_key = v_area, joined_at = now(), available_at = now(),\n state_changed_at = now(), appointment_id = NULL, away_reason = NULL, updated_at = now();\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'joined', 'frontdesk', auth.uid(), v_area, jsonb_build_object('to_position', v_pos));\n RETURN jsonb_build_object('ok', true, 'changed', true, 'area', v_area, 'position', v_pos);\n END IF;\n\n IF p_action = 'call' THEN\n SELECT array_agg(DISTINCT m.user_id) INTO v_users FROM app.memberships m\n WHERE m.tenant_id = v_tenant AND m.person_id = p_professional_id AND m.active;\n IF v_users IS NOT NULL THEN\n INSERT INTO public.notifications (tenant_id, user_id, title, body, type, link, subject_type, subject_id)\n SELECT v_tenant, u, 'Cliente para voc\u00EA na recep\u00E7\u00E3o', coalesce(v_reason, 'Voc\u00EA \u00E9 o pr\u00F3ximo da vez.'),\n 'turn_queue_call', '#/agenda', 'person', p_professional_id\n FROM unnest(v_users) u;\n END IF;\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, reason, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'called', 'frontdesk', auth.uid(),\n coalesce(v_entry.area_key, public.agenda_turn__area_of(v_tenant, p_professional_id)), v_reason,\n jsonb_build_object('notified', v_users IS NOT NULL));\n RETURN jsonb_build_object('ok', true, 'changed', true, 'notified', v_users IS NOT NULL);\n END IF;\n\n IF v_entry.id IS NULL THEN RAISE EXCEPTION 'turn_queue_not_in_queue' USING ERRCODE = '22023'; END IF;\n\n IF p_action = 'leave' THEN\n UPDATE public.plg_agenda_turn_entries SET status = 'unavailable', away_reason = NULL, appointment_id = NULL,\n state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n PERFORM public.agenda_turn__renumber(v_queue.id, v_entry.area_key);\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'left', 'frontdesk', auth.uid(), v_entry.area_key, jsonb_build_object('from_position', v_entry.position));\n\n ELSIF p_action = 'away' THEN\n IF v_entry.status = 'serving' THEN RAISE EXCEPTION 'turn_queue_in_service' USING ERRCODE = '22023'; END IF;\n UPDATE public.plg_agenda_turn_entries SET status = 'away', away_reason = coalesce(v_reason, 'Ausente'),\n state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, reason, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'away', 'frontdesk', auth.uid(), v_entry.area_key, coalesce(v_reason, 'Ausente'),\n jsonb_build_object('from_position', v_entry.position));\n\n ELSIF p_action = 'back' THEN\n IF v_entry.status <> 'away' THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;\n UPDATE public.plg_agenda_turn_entries SET status = 'available', away_reason = NULL,\n state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'back', 'frontdesk', auth.uid(), v_entry.area_key, jsonb_build_object('to_position', v_entry.position));\n\n ELSIF p_action = 'move' THEN\n IF v_entry.status = 'serving' THEN RAISE EXCEPTION 'turn_queue_in_service' USING ERRCODE = '22023'; END IF;\n IF p_position IS NULL OR p_position < 1 THEN RAISE EXCEPTION 'turn_queue_invalid_position' USING ERRCODE = '22023'; END IF;\n SELECT array_agg(id ORDER BY position, joined_at, id) INTO v_ids FROM public.plg_agenda_turn_entries\n WHERE queue_id = v_queue.id AND area_key IS NOT DISTINCT FROM v_entry.area_key\n AND status IN ('available', 'away') AND id <> v_entry.id;\n v_ids := coalesce(v_ids, ARRAY[]::uuid[]);\n v_pos := least(p_position, coalesce(array_length(v_ids, 1), 0) + 1);\n v_ids := v_ids[1:v_pos - 1] || v_entry.id || v_ids[v_pos:];\n UPDATE public.plg_agenda_turn_entries e SET position = o.ord, updated_at = now()\n FROM unnest(v_ids) WITH ORDINALITY o(id, ord) WHERE e.id = o.id;\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'moved', 'frontdesk', auth.uid(), v_entry.area_key,\n jsonb_build_object('from_position', v_entry.position, 'to_position', v_pos));\n\n ELSIF p_action = 'start' THEN\n IF v_entry.status = 'serving' THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;\n UPDATE public.plg_agenda_turn_entries SET status = 'serving', away_reason = NULL, previous_position = position,\n state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n PERFORM public.agenda_turn__renumber(v_queue.id, v_entry.area_key);\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'started', 'frontdesk', auth.uid(), v_entry.area_key, jsonb_build_object('from_position', v_entry.position));\n\n ELSIF p_action = 'finish' THEN\n v_pos := public.agenda_turn__next_position(v_queue.id, v_entry.area_key);\n UPDATE public.plg_agenda_turn_entries SET status = 'available', away_reason = NULL, appointment_id = NULL,\n position = v_pos, available_at = now(), state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n PERFORM public.agenda_turn__renumber(v_queue.id, v_entry.area_key);\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'finished', 'frontdesk', auth.uid(), v_entry.area_key, jsonb_build_object('to_position', v_pos));\n\n ELSIF p_action = 'skip' THEN\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, reason, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'skipped', 'frontdesk', auth.uid(), v_entry.area_key, v_reason,\n jsonb_build_object('from_position', v_entry.position));\n END IF;\n\n RETURN jsonb_build_object('ok', true, 'changed', true);\nEND $$;\n\n\nCREATE OR REPLACE FUNCTION public.agenda_turn_queue_my_state(p_unit_id uuid)\nRETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_person uuid;\n v_queue public.plg_agenda_turn_queues;\n v_entry public.plg_agenda_turn_entries;\n v_tz text;\n v_rank bigint;\nBEGIN\n IF v_tenant IS NULL OR auth.uid() IS NULL OR p_unit_id IS NULL THEN RETURN NULL; END IF;\n SELECT m.person_id INTO v_person FROM app.memberships m\n WHERE m.tenant_id = v_tenant AND m.user_id = auth.uid() AND m.person_id IS NOT NULL LIMIT 1;\n IF v_person IS NULL OR NOT public.agenda_turn__in_unit(v_tenant, p_unit_id, v_person) THEN RETURN NULL; END IF;\n v_queue := public.agenda_turn__queue(v_tenant, p_unit_id);\n IF v_queue.id IS NULL OR NOT public.agenda_turn__on(v_queue, 'enabled') THEN RETURN NULL; END IF;\n v_tz := public.agenda_turn__tz(v_tenant, p_unit_id);\n\n SELECT * INTO v_entry FROM public.plg_agenda_turn_entries e\n WHERE e.queue_id = v_queue.id AND e.professional_id = v_person AND e.status <> 'unavailable'\n AND (coalesce(e.joined_at, e.available_at, e.state_changed_at) AT TIME ZONE v_tz)::date = (now() AT TIME ZONE v_tz)::date;\n IF v_entry.id IS NOT NULL AND v_entry.status IN ('available', 'away') THEN\n SELECT count(*) + 1 INTO v_rank FROM public.plg_agenda_turn_entries o\n WHERE o.queue_id = v_queue.id AND o.area_key IS NOT DISTINCT FROM v_entry.area_key\n AND o.status IN ('available', 'away') AND o.id <> v_entry.id\n AND (o.position, o.joined_at, o.id) < (v_entry.position, v_entry.joined_at, v_entry.id);\n END IF;\n RETURN jsonb_build_object(\n 'professionalId', v_person,\n 'selfCheckin', public.agenda_turn__on(v_queue, 'self_checkin'),\n 'requireLocation', public.agenda_turn__on(v_queue, 'require_location'),\n 'inQueue', v_entry.id IS NOT NULL,\n 'area', coalesce(v_entry.area_key, public.agenda_turn__area_of(v_tenant, v_person)),\n 'status', v_entry.status,\n 'awayReason', v_entry.away_reason,\n 'rank', v_rank);\nEND $$;\n\n\nCREATE OR REPLACE FUNCTION public.agenda_turn_queue_my_action(\n p_unit_id uuid,\n p_action text,\n p_latitude double precision DEFAULT NULL,\n p_longitude double precision DEFAULT NULL,\n p_accuracy_m double precision DEFAULT NULL,\n p_reason text DEFAULT NULL\n) RETURNS jsonb LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_person uuid;\n v_queue public.plg_agenda_turn_queues;\n v_entry public.plg_agenda_turn_entries;\n v_reason text := nullif(btrim(coalesce(p_reason, '')), '');\n v_lat numeric;\n v_lng numeric;\n v_distance double precision;\n v_area text;\n v_pos bigint;\nBEGIN\n IF v_tenant IS NULL OR auth.uid() IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;\n IF p_action NOT IN ('join', 'leave', 'away', 'back', 'finish') THEN\n RAISE EXCEPTION 'turn_queue_invalid_action:%', p_action USING ERRCODE = '22023';\n END IF;\n SELECT m.person_id INTO v_person FROM app.memberships m\n WHERE m.tenant_id = v_tenant AND m.user_id = auth.uid() AND m.person_id IS NOT NULL LIMIT 1;\n IF v_person IS NULL THEN RAISE EXCEPTION 'turn_queue_professional_not_linked' USING ERRCODE = '42501'; END IF;\n IF NOT public.agenda_turn__in_unit(v_tenant, p_unit_id, v_person) THEN\n RAISE EXCEPTION 'turn_queue_professional_outside_unit' USING ERRCODE = '42501';\n END IF;\n SELECT * INTO v_queue FROM public.plg_agenda_turn_queues q\n WHERE q.tenant_id = v_tenant AND q.unit_id = p_unit_id AND q.queue_key = 'walk_in' FOR UPDATE;\n IF v_queue.id IS NULL OR NOT public.agenda_turn__on(v_queue, 'enabled') THEN RAISE EXCEPTION 'turn_queue_disabled' USING ERRCODE = '22023'; END IF;\n IF NOT public.agenda_turn__on(v_queue, 'self_checkin') THEN RAISE EXCEPTION 'turn_queue_checkin_by_frontdesk' USING ERRCODE = '22023'; END IF;\n PERFORM public.agenda_turn__purge_stale(v_queue.id, public.agenda_turn__tz(v_tenant, p_unit_id));\n\n SELECT * INTO v_entry FROM public.plg_agenda_turn_entries e\n WHERE e.queue_id = v_queue.id AND e.professional_id = v_person AND e.status <> 'unavailable';\n\n IF p_action = 'join' THEN\n IF v_entry.id IS NOT NULL THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;\n IF public.agenda_turn__on(v_queue, 'require_location') THEN\n SELECT u.latitude, u.longitude INTO v_lat, v_lng FROM public.v_units u WHERE u.id = p_unit_id AND u.tenant_id = v_tenant;\n IF v_lat IS NULL OR v_lng IS NULL THEN RAISE EXCEPTION 'unit_has_no_coordinates' USING ERRCODE = '22023'; END IF;\n IF p_latitude IS NULL OR p_longitude IS NULL THEN RAISE EXCEPTION 'location_required' USING ERRCODE = '22023'; END IF;\n v_distance := 6371000 * acos(least(1, greatest(-1,\n cos(radians(v_lat::double precision)) * cos(radians(p_latitude)) * cos(radians(p_longitude) - radians(v_lng::double precision))\n + sin(radians(v_lat::double precision)) * sin(radians(p_latitude)))));\n IF v_distance > public.agenda_turn__radius(v_queue) + least(coalesce(p_accuracy_m, 0), 100) THEN\n RAISE EXCEPTION 'outside_geofence' USING ERRCODE = '42501';\n END IF;\n END IF;\n v_area := public.agenda_turn__area_of(v_tenant, v_person);\n v_pos := public.agenda_turn__next_position(v_queue.id, v_area);\n INSERT INTO public.plg_agenda_turn_entries\n (tenant_id, queue_id, unit_id, professional_id, status, position, area_key, joined_at, available_at, state_changed_at, updated_at)\n VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'available', v_pos, v_area, now(), now(), now(), now())\n ON CONFLICT (queue_id, professional_id) DO UPDATE SET\n status = 'available', position = v_pos, area_key = v_area, joined_at = now(), available_at = now(),\n state_changed_at = now(), appointment_id = NULL, away_reason = NULL, updated_at = now();\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key,\n inside_geofence, location_accuracy_meters, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'joined', 'app', auth.uid(), v_area,\n CASE WHEN public.agenda_turn__on(v_queue, 'require_location') THEN true END, p_accuracy_m, jsonb_build_object('to_position', v_pos));\n RETURN jsonb_build_object('ok', true, 'changed', true, 'area', v_area);\n END IF;\n\n IF v_entry.id IS NULL THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;\n\n IF p_action = 'leave' THEN\n UPDATE public.plg_agenda_turn_entries SET status = 'unavailable', away_reason = NULL, appointment_id = NULL,\n state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n PERFORM public.agenda_turn__renumber(v_queue.id, v_entry.area_key);\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key)\n VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'left', 'app', auth.uid(), v_entry.area_key);\n ELSIF p_action = 'away' THEN\n IF v_entry.status <> 'available' THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;\n UPDATE public.plg_agenda_turn_entries SET status = 'away', away_reason = coalesce(v_reason, 'Ausente'),\n state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, reason)\n VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'away', 'app', auth.uid(), v_entry.area_key, coalesce(v_reason, 'Ausente'));\n ELSIF p_action = 'back' THEN\n IF v_entry.status <> 'away' THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;\n UPDATE public.plg_agenda_turn_entries SET status = 'available', away_reason = NULL,\n state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key)\n VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'back', 'app', auth.uid(), v_entry.area_key);\n ELSIF p_action = 'finish' THEN\n IF v_entry.status <> 'serving' THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;\n v_pos := public.agenda_turn__next_position(v_queue.id, v_entry.area_key);\n UPDATE public.plg_agenda_turn_entries SET status = 'available', appointment_id = NULL, position = v_pos,\n available_at = now(), state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;\n INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)\n VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'finished', 'app', auth.uid(), v_entry.area_key, jsonb_build_object('to_position', v_pos));\n END IF;\n RETURN jsonb_build_object('ok', true, 'changed', true);\nEND $$;\n\n\nREVOKE ALL ON FUNCTION public.agenda_turn__on(public.plg_agenda_turn_queues, text) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.agenda_turn__radius(public.plg_agenda_turn_queues) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.agenda_turn__unit_cfg(uuid, uuid) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.agenda_turn__put(uuid, text, jsonb) FROM PUBLIC, anon, authenticated;\n";
22
24
  export declare const MIGRATIONS: Array<{
23
25
  id: string;
24
26
  sql: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,05vEAg2ClC,CAAA;AAED,eAAO,MAAM,uDAAuD,mvEAgDnE,CAAA;AAED,eAAO,MAAM,4CAA4C,w6EAqDyB,CAAA;AAElF,eAAO,MAAM,oDAAoD,61CAyBhE,CAAA;AAED,eAAO,MAAM,8CAA8C,w7EA4C1D,CAAA;AAED,eAAO,MAAM,iDAAiD,woFAiD7D,CAAA;AAED,eAAO,MAAM,oDAAoD,kxOAmHhE,CAAA;AAED,eAAO,MAAM,4CAA4C,mrZA2OxD,CAAA;AAED,eAAO,MAAM,sCAAsC,8tuBAoXlD,CAAA;AAED,eAAO,MAAM,4CAA4C,wkIA8DxD,CAAA;AAED,eAAO,MAAM,qCAAqC,q5pDAo1BjD,CAAA;AAED,eAAO,MAAM,yCAAyC,0rIA+ErD,CAAA;AAED,eAAO,MAAM,+CAA+C,+sIA0F3D,CAAA;AAED,eAAO,MAAM,kDAAkD,ogYAoK9D,CAAA;AAED,eAAO,MAAM,6CAA6C,w/GA0DzD,CAAA;AAED,eAAO,MAAM,wDAAwD,mtBAYpE,CAAA;AAED,eAAO,MAAM,6BAA6B,y4hBAmVzC,CAAA;AAED,eAAO,MAAM,qCAAqC,utMAwJjD,CAAA;AAED,eAAO,MAAM,yCAAyC,09fAoNrD,CAAA;AAED,eAAO,MAAM,0CAA0C,o4aAyOtD,CAAA;AAED,eAAO,MAAM,2DAA2D,s3FAwDvE,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAsBzD,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,05vEAg2ClC,CAAA;AAED,eAAO,MAAM,uDAAuD,mvEAgDnE,CAAA;AAED,eAAO,MAAM,4CAA4C,w6EAqDyB,CAAA;AAElF,eAAO,MAAM,oDAAoD,61CAyBhE,CAAA;AAED,eAAO,MAAM,8CAA8C,w7EA4C1D,CAAA;AAED,eAAO,MAAM,iDAAiD,woFAiD7D,CAAA;AAED,eAAO,MAAM,oDAAoD,kxOAmHhE,CAAA;AAED,eAAO,MAAM,4CAA4C,mrZA2OxD,CAAA;AAED,eAAO,MAAM,sCAAsC,8tuBAoXlD,CAAA;AAED,eAAO,MAAM,4CAA4C,wkIA8DxD,CAAA;AAED,eAAO,MAAM,qCAAqC,q5pDAo1BjD,CAAA;AAED,eAAO,MAAM,yCAAyC,0rIA+ErD,CAAA;AAED,eAAO,MAAM,+CAA+C,+sIA0F3D,CAAA;AAED,eAAO,MAAM,kDAAkD,ogYAoK9D,CAAA;AAED,eAAO,MAAM,6CAA6C,w/GA0DzD,CAAA;AAED,eAAO,MAAM,wDAAwD,mtBAYpE,CAAA;AAED,eAAO,MAAM,6BAA6B,y4hBAmVzC,CAAA;AAED,eAAO,MAAM,qCAAqC,utMAwJjD,CAAA;AAED,eAAO,MAAM,yCAAyC,09fAoNrD,CAAA;AAED,eAAO,MAAM,0CAA0C,o4aAyOtD,CAAA;AAED,eAAO,MAAM,2DAA2D,s3FAwDvE,CAAA;AAED,eAAO,MAAM,4BAA4B,i0jDA+yBxC,CAAA;AAED,eAAO,MAAM,uDAAuD,8wrDAk7BnE,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAwBzD,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"CalendarView.d.ts","sourceRoot":"","sources":["../../src/views/CalendarView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA6E,MAAM,OAAO,CAAA;AA4YjG,wBAAgB,YAAY,sBAq7D3B"}
1
+ {"version":3,"file":"CalendarView.d.ts","sourceRoot":"","sources":["../../src/views/CalendarView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA6E,MAAM,OAAO,CAAA;AA4YjG,wBAAgB,YAAY,sBAw7D3B"}
@@ -1,10 +1,8 @@
1
1
  import React from 'react';
2
2
  import type { TurnQueueDataProvider } from '../data/turn-queue-types';
3
- import type { AgendaDataProvider } from '../data/types';
4
- export declare function TurnQueuePage({ provider, agendaProvider, title, subtitle }: {
3
+ export declare function TurnQueuePage({ provider, title }: {
5
4
  provider: TurnQueueDataProvider;
6
- agendaProvider: Pick<AgendaDataProvider, 'getProfessionals'>;
7
5
  title: string;
8
- subtitle: string;
6
+ subtitle?: string;
9
7
  }): React.JSX.Element;
10
8
  //# sourceMappingURL=TurnQueuePage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TurnQueuePage.d.ts","sourceRoot":"","sources":["../../src/views/TurnQueuePage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAKzB,OAAO,KAAK,EAAE,qBAAqB,EAA4D,MAAM,0BAA0B,CAAA;AAC/H,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AA6GvD,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,cAAc,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE;IAC3E,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,cAAc,EAAE,IAAI,CAAC,kBAAkB,EAAE,kBAAkB,CAAC,CAAA;IAC5D,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;CACjB,qBA0JA"}
1
+ {"version":3,"file":"TurnQueuePage.d.ts","sourceRoot":"","sources":["../../src/views/TurnQueuePage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAazB,OAAO,KAAK,EACuC,qBAAqB,EACvE,MAAM,0BAA0B,CAAA;AAuVjC,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;IACjD,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,qBAmMA"}
@@ -1 +1 @@
1
- {"version":3,"file":"UnitTurnQueueWidget.d.ts","sourceRoot":"","sources":["../../src/views/UnitTurnQueueWidget.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAOzB;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAA;CAAE,qBA2H5H"}
1
+ {"version":3,"file":"UnitTurnQueueWidget.d.ts","sourceRoot":"","sources":["../../src/views/UnitTurnQueueWidget.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAOzB;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAA;CAAE,qBA4H5H"}
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import type { TurnQueueDataProvider } from '../../data/turn-queue-types';
3
+ export declare function TurnQueueConfigDialog({ provider, unitId, onClose, onSaved }: {
4
+ provider: TurnQueueDataProvider;
5
+ unitId: string;
6
+ onClose: () => void;
7
+ onSaved: () => void;
8
+ }): React.JSX.Element;
9
+ //# sourceMappingURL=TurnQueueConfigDialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TurnQueueConfigDialog.d.ts","sourceRoot":"","sources":["../../../src/views/turn-queue/TurnQueueConfigDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAOzB,OAAO,KAAK,EAA8B,qBAAqB,EAAE,MAAM,6BAA6B,CAAA;AA8CpG,wBAAgB,qBAAqB,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE;IAC5E,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,qBAqNA"}
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import type { TurnQueueDataProvider } from '../../data/turn-queue-types';
3
+ export declare function TurnQueueReportPage({ provider }: {
4
+ provider: TurnQueueDataProvider;
5
+ }): React.JSX.Element;
6
+ //# sourceMappingURL=TurnQueueReportPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TurnQueueReportPage.d.ts","sourceRoot":"","sources":["../../../src/views/turn-queue/TurnQueueReportPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAOzB,OAAO,KAAK,EAAE,qBAAqB,EAAiB,MAAM,6BAA6B,CAAA;AA2BvF,wBAAgB,mBAAmB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,qBAAqB,CAAA;CAAE,qBAmHpF"}
@@ -0,0 +1,29 @@
1
+ import type { TurnAreaColor, TurnBoardEvent } from '../../data/turn-queue-types';
2
+ export declare const AWAY_REASONS: readonly ["Banheiro", "Lanche", "Almoço", "Saiu da loja", "Serviço interno"];
3
+ export declare const AREA_COLORS: Array<{
4
+ value: TurnAreaColor;
5
+ label: string;
6
+ }>;
7
+ export declare const AREA_TONE: Record<TurnAreaColor, {
8
+ dot: string;
9
+ column: string;
10
+ stripe: string;
11
+ button: string;
12
+ text: string;
13
+ }>;
14
+ export declare function toneOf(color: string | undefined): {
15
+ dot: string;
16
+ column: string;
17
+ stripe: string;
18
+ button: string;
19
+ text: string;
20
+ };
21
+ export declare function firstName(name: string): string;
22
+ export declare function minutesSince(iso: string | null | undefined): number;
23
+ /** "agora" · "4 min" · "1h 05min" — the V1 wording without the "há". */
24
+ export declare function sinceLabel(iso: string | null | undefined): string;
25
+ export declare function clock(iso: string): string;
26
+ /** The trail line, word for word as V1 writes it ("foi pulado pela recepção — ausente (Almoço)"). */
27
+ export declare function eventPhrase(event: TurnBoardEvent): string;
28
+ export declare function turnErrorMessage(error: unknown): string;
29
+ //# sourceMappingURL=turn-queue-ui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turn-queue-ui.d.ts","sourceRoot":"","sources":["../../../src/views/turn-queue/turn-queue-ui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAEhF,eAAO,MAAM,YAAY,8EAA+E,CAAA;AAExG,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAStE,CAAA;AAGD,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,aAAa,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAS1H,CAAA;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS;SAXK,MAAM;YAAU,MAAM;YAAU,MAAM;YAAU,MAAM;UAAQ,MAAM;EAaxH;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAGnE;AAED,wEAAwE;AACxE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAKjE;AAED,wBAAgB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEzC;AAED,qGAAqG;AACrG,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAoCzD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAcvD"}
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "status": "beta",
5
5
  "dependencies": []
6
6
  },
7
- "version": "0.22.0",
7
+ "version": "0.23.2",
8
8
  "description": "Fayz SDK — plugin-agenda plugin",
9
9
  "type": "module",
10
10
  "sideEffects": [
@@ -42,11 +42,11 @@
42
42
  "@fullcalendar/resource-timegrid": "^6.1.0",
43
43
  "@fullcalendar/timegrid": "^6.1.20",
44
44
  "@tanstack/react-table": "^8.20.0",
45
- "@fayz-ai/admin": "^0.39.0",
46
- "@fayz-ai/core": "^0.39.0",
47
- "@fayz-ai/plugin-financial": "^0.21.1",
48
- "@fayz-ai/plugin-forms": "^0.14.0",
49
- "@fayz-ai/ui": "^0.39.0"
45
+ "@fayz-ai/admin": "^0.42.0",
46
+ "@fayz-ai/core": "^0.40.0",
47
+ "@fayz-ai/plugin-financial": "^0.22.1",
48
+ "@fayz-ai/plugin-forms": "^0.14.3",
49
+ "@fayz-ai/ui": "^0.42.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/react": "^18.3.0",