@fayz-ai/plugin-reports 0.16.29 → 0.16.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,15 +1,20 @@
1
- import { exportToExcel } from './chunk-VQJKOO2B.js';
2
- export { exportToExcel } from './chunk-VQJKOO2B.js';
3
1
  import { exportToPdf } from './chunk-BPTKNPXQ.js';
4
2
  export { exportToPdf } from './chunk-BPTKNPXQ.js';
5
- import { expandBagColumns, formatValuePlain, reportColumnsToTanstack } from './chunk-4QIHTUXD.js';
6
- import * as React10 from 'react';
7
- import React10__default, { useState, useEffect } from 'react';
8
- import { usePluginRuntime, getAnalyticsCards, useTranslation, exportCSV, registerTranslations, createSafeDataProvider, getSupabaseClientOptional, hashRouterAdapter } from '@fayz-ai/core';
9
- import { useOrganizationStore, askAI, useAgentSurface, QuickActionsButton, AssistantInvite, RightRailPage } from '@fayz-ai/admin';
10
- import { AnalyticsContextProvider, useModuleLayout, ModulePage, collectFilters, AnalyticsControls, AnalyticsGrid, Card, Input, FilterChip, DataTable, Button, SubpageHeader, useToolbarPanelClose, useAnalyticsCard, useAnalyticsCardRows, ToolbarAction, PageHeaderActions, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, useAnalyticsContext, useAnalyticsAllRows, StatementTable, formatBucket, AnalyticsCard, Dropdown, DropdownTrigger, DropdownContent, DropdownItem, Badge, getLucideIcon, Checkbox } from '@fayz-ai/ui';
11
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
12
- import { BarChart3, User, Search, ArrowLeft, Printer, RotateCcw, ChevronUp, ChevronDown, EyeOff, Eye, Trash2, Construction, Download, GripVertical, SlidersHorizontal, Radio, ChevronLeft, ChevronRight } from 'lucide-react';
3
+ import { exportToCsv } from './chunk-VGDOKFED.js';
4
+ export { AnalyticsReportViewer, exportToCsv } from './chunk-VGDOKFED.js';
5
+ export { AnalyticsModule } from './chunk-67JOMNSD.js';
6
+ export { AnalyticsOverview } from './chunk-S4MICM4C.js';
7
+ export { AnalyticsSettingsPage } from './chunk-DTUN4WLI.js';
8
+ export { ReportsListPage } from './chunk-DP7S37Y2.js';
9
+ import { ANALYTICS_BASE, REPORTS_BASE, SETTINGS_ROUTE, AnalyticsSurface } from './chunk-YVLQV4N7.js';
10
+ export { ANALYTICS_BASE, AnalyticsSurface, REPORTS_BASE, SETTINGS_ROUTE, cardColumns, clearCardPref, deleteCustomReport, fieldPalette, markViewed, resetAnalyticsPrefs, resolveCards, resolveCustomReport, saveCustomReport, setCardPref, setOverviewOrder, sourceKey, useAnalyticsCards, useAnalyticsCatalogue, useAnalyticsConfig, useAnalyticsPrefs, useLastViewed, useOverviewCards } from './chunk-YVLQV4N7.js';
11
+ import { exportToExcel } from './chunk-VQJKOO2B.js';
12
+ export { exportToExcel } from './chunk-VQJKOO2B.js';
13
+ import './chunk-4QIHTUXD.js';
14
+ import React from 'react';
15
+ import { registerTranslations, createSafeDataProvider, getSupabaseClientOptional } from '@fayz-ai/core';
16
+ import { useOrganizationStore } from '@fayz-ai/admin';
17
+ import { MIGRATIONS } from '#migrations/main';
13
18
 
14
19
  // src/data/aggregate.ts
15
20
  function num(value) {
@@ -673,695 +678,6 @@ var reportsLocales = {
673
678
  "pt-BR": ptBR
674
679
  };
675
680
 
676
- // src/migrations/index.ts
677
- var MIGRATION_000_BASELINE = `-- ============================================================================
678
- -- plugins/plugin-reports/src/migrations/000_baseline.sql \u2014 what installing this provisions, as one file.
679
- --
680
- -- GENERATED by scripts/emit-unit-baselines.mjs (#338). Do not edit by hand:
681
- -- change the schema, regenerate, and let scripts/check-chain-equivalence.mjs
682
- -- prove the result still matches.
683
- --
684
- -- This replaced 3 migration files. They are kept, unapplied, in
685
- -- migrations.archive/ \u2014 a year of decisions is worth reading even when it is no
686
- -- longer worth replaying.
687
- --
688
- -- Object order is fixed rather than dependency-sorted: schemas, types,
689
- -- sequences, tables, defaults, functions, constraints, views, indexes, foreign
690
- -- keys, triggers, row security, policies, grants. Dependencies BETWEEN units
691
- -- are carried by the order packages/db/chain.json declares.
692
- -- ============================================================================
693
-
694
- -- A LANGUAGE sql function binds its body at CREATE time, and 570 functions
695
- -- sorted by name are not sorted by who calls whom. This is what lets them load
696
- -- in any order; every body is still compiled on first call, so a genuinely
697
- -- broken reference surfaces then rather than never. Session-scoped.
698
- SET check_function_bodies = false;
699
-
700
-
701
-
702
- -- \u2500\u2500 default privileges: reset \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
703
- -- Cleared BEFORE anything is created, so every object below gets the ACL the
704
- -- dump describes rather than the image's defaults on top of it. Restored at
705
- -- the end of this file.
706
-
707
- DO $dp$ BEGIN
708
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
709
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
710
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
711
- END $dp$;
712
-
713
- DO $dp$ BEGIN
714
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
715
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
716
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
717
- END $dp$;
718
-
719
- DO $dp$ BEGIN
720
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
721
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
722
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
723
- END $dp$;
724
-
725
- DO $dp$ BEGIN
726
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
727
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
728
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
729
- END $dp$;
730
-
731
- DO $dp$ BEGIN
732
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
733
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
734
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
735
- END $dp$;
736
-
737
- DO $dp$ BEGIN
738
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
739
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
740
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
741
- END $dp$;
742
-
743
-
744
- -- \u2500\u2500 table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
745
-
746
- CREATE TABLE public.plg_reports_definitions (
747
- id uuid DEFAULT gen_random_uuid() NOT NULL,
748
- tenant_id uuid,
749
- unit_id uuid,
750
- key text NOT NULL,
751
- version integer DEFAULT 1 NOT NULL,
752
- title text NOT NULL,
753
- description text,
754
- module text NOT NULL,
755
- source_view text NOT NULL,
756
- params jsonb DEFAULT '[]'::jsonb NOT NULL,
757
- columns jsonb DEFAULT '[]'::jsonb NOT NULL,
758
- default_order_by text,
759
- required_permission text NOT NULL,
760
- is_active boolean DEFAULT true NOT NULL,
761
- metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
762
- created_by uuid,
763
- owner_id uuid,
764
- created_at timestamp with time zone DEFAULT now() NOT NULL,
765
- updated_at timestamp with time zone DEFAULT now() NOT NULL,
766
- CONSTRAINT plg_reports_definitions_required_permission_check CHECK ((btrim(required_permission) <> ''::text)),
767
- CONSTRAINT plg_reports_definitions_version_check CHECK ((version >= 1))
768
- );
769
-
770
- ALTER TABLE ONLY public.plg_reports_definitions FORCE ROW LEVEL SECURITY;
771
-
772
- CREATE TABLE public.plg_reports_runs (
773
- id uuid DEFAULT gen_random_uuid() NOT NULL,
774
- tenant_id uuid NOT NULL,
775
- unit_id uuid,
776
- definition_key text NOT NULL,
777
- definition_version integer DEFAULT 1 NOT NULL,
778
- params jsonb DEFAULT '{}'::jsonb NOT NULL,
779
- requested_by uuid,
780
- row_count bigint,
781
- duration_ms integer,
782
- status text DEFAULT 'ok'::text NOT NULL,
783
- error text,
784
- format text,
785
- owner_id uuid,
786
- created_at timestamp with time zone DEFAULT now() NOT NULL,
787
- updated_at timestamp with time zone DEFAULT now() NOT NULL,
788
- CONSTRAINT plg_reports_runs_status_check CHECK ((status = ANY (ARRAY['ok'::text, 'refused'::text, 'error'::text])))
789
- );
790
-
791
- ALTER TABLE ONLY public.plg_reports_runs FORCE ROW LEVEL SECURITY;
792
-
793
- CREATE TABLE public.plg_reports_schedules (
794
- id uuid DEFAULT gen_random_uuid() NOT NULL,
795
- tenant_id uuid NOT NULL,
796
- unit_id uuid,
797
- definition_key text NOT NULL,
798
- cron text NOT NULL,
799
- params jsonb DEFAULT '{}'::jsonb NOT NULL,
800
- recipients jsonb DEFAULT '[]'::jsonb NOT NULL,
801
- format text DEFAULT 'csv'::text NOT NULL,
802
- template_key text DEFAULT 'report.delivered'::text NOT NULL,
803
- is_active boolean DEFAULT true NOT NULL,
804
- last_run_at timestamp with time zone,
805
- next_run_at timestamp with time zone,
806
- owner_id uuid,
807
- created_at timestamp with time zone DEFAULT now() NOT NULL,
808
- updated_at timestamp with time zone DEFAULT now() NOT NULL,
809
- CONSTRAINT plg_reports_schedules_format_check CHECK ((format = ANY (ARRAY['csv'::text, 'xlsx'::text, 'pdf'::text])))
810
- );
811
-
812
- ALTER TABLE ONLY public.plg_reports_schedules FORCE ROW LEVEL SECURITY;
813
-
814
-
815
- -- \u2500\u2500 function \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
816
-
817
- CREATE FUNCTION public.reports_definition(p_key text, p_tenant uuid DEFAULT NULL::uuid) RETURNS public.plg_reports_definitions
818
- LANGUAGE sql STABLE SECURITY DEFINER
819
- SET search_path TO ''
820
- AS $$
821
- SELECT d.* FROM public.plg_reports_definitions d
822
- WHERE d.key = p_key AND d.is_active
823
- AND (d.tenant_id = coalesce(p_tenant, app.current_tenant_id()) OR d.tenant_id IS NULL)
824
- -- A tenant's own definition beats the platform one, and the newest version
825
- -- beats an older one.
826
- ORDER BY (d.tenant_id IS NOT NULL) DESC, d.version DESC
827
- LIMIT 1
828
- $$;
829
-
830
- CREATE FUNCTION public.reports_deliver_due(p_limit integer DEFAULT 50) RETURNS jsonb
831
- LANGUAGE plpgsql SECURITY DEFINER
832
- SET search_path TO ''
833
- AS $$
834
- DECLARE
835
- s public.plg_reports_schedules%ROWTYPE;
836
- v_recipient jsonb;
837
- v_res jsonb;
838
- v_sent integer := 0;
839
- v_runs integer := 0;
840
- BEGIN
841
- IF to_regprocedure('public.notifications_enqueue(uuid, text, uuid, jsonb, text, timestamptz, text, text, uuid, uuid)') IS NULL THEN
842
- RETURN jsonb_build_object('skipped', 'plugin-notifications is not installed');
843
- END IF;
844
-
845
- FOR s IN
846
- SELECT * FROM public.plg_reports_schedules
847
- WHERE is_active AND (next_run_at IS NULL OR next_run_at <= now())
848
- ORDER BY next_run_at NULLS FIRST
849
- LIMIT greatest(p_limit, 0)
850
- FOR UPDATE SKIP LOCKED
851
- LOOP
852
- v_runs := v_runs + 1;
853
- FOR v_recipient IN SELECT * FROM jsonb_array_elements(coalesce(s.recipients, '[]'::jsonb)) LOOP
854
- -- The campaign rule applies to a report too: this ENQUEUES, it does not
855
- -- send. One engine, one queue, one place a delivery can be retried.
856
- v_res := to_jsonb(public.notifications_enqueue(
857
- p_tenant_id => s.tenant_id,
858
- p_template_key => s.template_key,
859
- p_person_id => nullif(v_recipient ->> 'person_id', '')::uuid,
860
- p_payload => jsonb_build_object('report_key', s.definition_key, 'format', s.format),
861
- p_dedupe_key => 'report:' || s.id::text || ':' || to_char(now(), 'YYYY-MM-DD') || ':' || coalesce(v_recipient ->> 'person_id', ''),
862
- p_unit_id => s.unit_id));
863
- v_sent := v_sent + 1;
864
- END LOOP;
865
-
866
- UPDATE public.plg_reports_schedules
867
- SET last_run_at = now(),
868
- next_run_at = now() + interval '1 day'
869
- WHERE id = s.id;
870
- END LOOP;
871
-
872
- RETURN jsonb_build_object('schedules', v_runs, 'queued', v_sent);
873
- END $$;
874
-
875
- CREATE FUNCTION public.reports_ensure_delivery_template(p_tenant uuid, p_channel text DEFAULT 'email'::text) RETURNS uuid
876
- LANGUAGE plpgsql SECURITY DEFINER
877
- SET search_path TO ''
878
- AS $$
879
- DECLARE v_id uuid;
880
- BEGIN
881
- IF to_regclass('public.plg_notifications_templates') IS NULL THEN
882
- RAISE EXCEPTION 'reports_ensure_delivery_template: plugin-notifications is not installed' USING ERRCODE = '0A000';
883
- END IF;
884
- INSERT INTO public.plg_notifications_templates (tenant_id, key, channel, locale, subject, body, is_default, purpose)
885
- VALUES (p_tenant, 'report.delivered', p_channel, 'pt-BR',
886
- 'Seu relat\xF3rio est\xE1 pronto',
887
- 'O relat\xF3rio {{report_key}} foi gerado em {{format}} e est\xE1 dispon\xEDvel.', true, 'transactional')
888
- ON CONFLICT (tenant_id, key, channel, locale) DO UPDATE SET is_active = true
889
- RETURNING id INTO v_id;
890
- RETURN v_id;
891
- END $$;
892
-
893
- CREATE FUNCTION public.reports_run(p_key text, p_params jsonb DEFAULT '{}'::jsonb, p_limit integer DEFAULT 1000, p_offset integer DEFAULT 0, p_format text DEFAULT NULL::text) RETURNS jsonb
894
- LANGUAGE plpgsql SECURITY DEFINER
895
- SET search_path TO ''
896
- AS $$
897
- DECLARE
898
- v_tenant uuid := app.current_tenant_id();
899
- v_def public.plg_reports_definitions%ROWTYPE;
900
- v_sql text;
901
- v_where text := '';
902
- v_param jsonb;
903
- v_name text;
904
- v_value text;
905
- v_rows jsonb;
906
- v_count bigint := 0;
907
- v_start timestamptz := clock_timestamp();
908
- v_has_unit boolean;
909
- BEGIN
910
- IF v_tenant IS NULL THEN
911
- RAISE EXCEPTION 'reports_run: no active tenant' USING ERRCODE = '42501';
912
- END IF;
913
-
914
- v_def := public.reports_definition(p_key, v_tenant);
915
- IF v_def.id IS NULL THEN
916
- RAISE EXCEPTION 'reports_run: no active report called %', p_key USING ERRCODE = 'P0002';
917
- END IF;
918
-
919
- -- \u2500\u2500 refuse BEFORE the query, not after the rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
920
- --
921
- -- The refusal is RETURNED rather than raised, and the reason is mechanical:
922
- -- raising rolls the statement back, taking the run record with it, and a
923
- -- refusal nobody can see afterwards is the thing this history exists for.
924
- -- No rows are read either way \u2014 which is what "refuse before the query" means
925
- -- \u2014 and a caller that ignores \`status\` still gets nothing.
926
- IF NOT (select app.has_permission(v_def.required_permission, NULL)) THEN
927
- INSERT INTO public.plg_reports_runs (tenant_id, definition_key, definition_version, params, requested_by, status, error)
928
- VALUES (v_tenant, p_key, v_def.version, coalesce(p_params, '{}'::jsonb), auth.uid(), 'refused',
929
- format('%s is required', v_def.required_permission));
930
- RETURN jsonb_build_object(
931
- 'key', p_key, 'version', v_def.version, 'title', v_def.title,
932
- 'status', 'refused', 'required_permission', v_def.required_permission,
933
- 'columns', '[]'::jsonb, 'rows', '[]'::jsonb, 'row_count', 0);
934
- END IF;
935
-
936
- -- \u2500\u2500 scope the engine injects, and no definition can switch off \u2500\u2500\u2500\u2500\u2500\u2500\u2500
937
- v_where := format('%I = %L', 'tenant_id', v_tenant);
938
-
939
- SELECT EXISTS (
940
- SELECT 1 FROM information_schema.columns
941
- WHERE table_schema = 'public' AND table_name = v_def.source_view AND column_name = 'unit_id')
942
- INTO v_has_unit;
943
- IF v_has_unit THEN
944
- v_where := v_where || ' AND (unit_id IS NULL OR (select app.has_unit(unit_id)))';
945
- END IF;
946
-
947
- -- \u2500\u2500 the declared parameters, and only those \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
948
- FOR v_param IN SELECT * FROM jsonb_array_elements(coalesce(v_def.params, '[]'::jsonb)) LOOP
949
- v_name := v_param ->> 'name';
950
- v_value := coalesce(p_params ->> v_name, v_param ->> 'default');
951
- IF v_value IS NULL THEN
952
- IF coalesce((v_param ->> 'required')::boolean, false) THEN
953
- RAISE EXCEPTION 'reports_run: % requires the parameter %', p_key, v_name USING ERRCODE = '22023';
954
- END IF;
955
- CONTINUE;
956
- END IF;
957
- IF NOT EXISTS (SELECT 1 FROM information_schema.columns
958
- WHERE table_schema = 'public' AND table_name = v_def.source_view
959
- AND column_name = coalesce(v_param ->> 'column', v_name)) THEN
960
- RAISE EXCEPTION 'reports_run: parameter % has no column on %', v_name, v_def.source_view USING ERRCODE = '42703';
961
- END IF;
962
- v_where := v_where || format(' AND %I %s %L',
963
- coalesce(v_param ->> 'column', v_name),
964
- CASE v_param ->> 'operator' WHEN '>=' THEN '>=' WHEN '<=' THEN '<=' WHEN '>' THEN '>' WHEN '<' THEN '<' WHEN 'ilike' THEN 'ILIKE' ELSE '=' END,
965
- v_value);
966
- END LOOP;
967
-
968
- -- A parameter the caller sent that the definition never declared is refused
969
- -- rather than ignored: silently dropping a filter shows the wrong number.
970
- IF p_params IS NOT NULL AND jsonb_typeof(p_params) = 'object' THEN
971
- FOR v_name IN SELECT key FROM jsonb_each_text(p_params) LOOP
972
- IF NOT EXISTS (SELECT 1 FROM jsonb_array_elements(coalesce(v_def.params, '[]'::jsonb)) d WHERE d ->> 'name' = v_name) THEN
973
- RAISE EXCEPTION 'reports_run: % does not declare a parameter called %', p_key, v_name USING ERRCODE = '22023';
974
- END IF;
975
- END LOOP;
976
- END IF;
977
-
978
- v_sql := format('SELECT coalesce(jsonb_agg(t), ''[]''::jsonb), count(*) FROM (SELECT * FROM public.%I WHERE %s %s LIMIT %s OFFSET %s) t',
979
- v_def.source_view, v_where,
980
- CASE WHEN v_def.default_order_by IS NULL THEN '' ELSE 'ORDER BY ' || quote_ident(v_def.default_order_by) END,
981
- greatest(coalesce(p_limit, 1000), 0), greatest(coalesce(p_offset, 0), 0));
982
-
983
- EXECUTE v_sql INTO v_rows, v_count;
984
-
985
- INSERT INTO public.plg_reports_runs (tenant_id, definition_key, definition_version, params, requested_by,
986
- row_count, duration_ms, status, format)
987
- VALUES (v_tenant, p_key, v_def.version, coalesce(p_params, '{}'::jsonb), auth.uid(),
988
- v_count, (extract(epoch FROM clock_timestamp() - v_start) * 1000)::integer, 'ok', p_format);
989
-
990
- RETURN jsonb_build_object(
991
- 'key', p_key, 'version', v_def.version, 'title', v_def.title,
992
- 'status', 'ok', 'columns', v_def.columns, 'rows', v_rows, 'row_count', v_count);
993
- END $$;
994
-
995
- CREATE FUNCTION public.trg_plg_reports_definitions_source() RETURNS trigger
996
- LANGUAGE plpgsql SECURITY DEFINER
997
- SET search_path TO ''
998
- AS $_$
999
- DECLARE v_kind char;
1000
- BEGIN
1001
- IF NEW.source_view !~ '^[a-z_][a-z0-9_]*$' THEN
1002
- RAISE EXCEPTION 'plg_reports_definitions: source_view % is not a bare relation name in public', NEW.source_view
1003
- USING ERRCODE = '22023';
1004
- END IF;
1005
-
1006
- SELECT c.relkind INTO v_kind
1007
- FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
1008
- WHERE n.nspname = 'public' AND c.relname = NEW.source_view;
1009
-
1010
- IF v_kind IS NULL THEN
1011
- RAISE EXCEPTION 'plg_reports_definitions: public.% does not exist', NEW.source_view USING ERRCODE = '42P01';
1012
- END IF;
1013
-
1014
- -- A view or a materialized view is a read model by construction. A TABLE is
1015
- -- only allowed when its name says so \u2014 that is the \`plg_<x>_rep_*\` convention
1016
- -- Phase 1 established, and it is checked rather than trusted.
1017
- IF v_kind NOT IN ('v', 'm') AND NEW.source_view !~ '^plg_[a-z0-9_]+_rep_' THEN
1018
- RAISE EXCEPTION 'plg_reports_definitions: public.% is a business table, not a read model', NEW.source_view
1019
- USING ERRCODE = '23514',
1020
- HINT = 'Point the definition at a view or at a plg_<plugin>_rep_* read model. Reporting off a business table is how a report starts holding a schema still.';
1021
- END IF;
1022
- RETURN NEW;
1023
- END $_$;
1024
-
1025
-
1026
- -- \u2500\u2500 constraint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1027
-
1028
- ALTER TABLE ONLY public.plg_reports_definitions
1029
- ADD CONSTRAINT plg_reports_definitions_pkey PRIMARY KEY (id);
1030
-
1031
- ALTER TABLE ONLY public.plg_reports_definitions
1032
- ADD CONSTRAINT plg_reports_definitions_tenant_id_id_key UNIQUE (tenant_id, id);
1033
-
1034
- ALTER TABLE ONLY public.plg_reports_runs
1035
- ADD CONSTRAINT plg_reports_runs_pkey PRIMARY KEY (id);
1036
-
1037
- ALTER TABLE ONLY public.plg_reports_runs
1038
- ADD CONSTRAINT plg_reports_runs_tenant_id_id_key UNIQUE (tenant_id, id);
1039
-
1040
- ALTER TABLE ONLY public.plg_reports_schedules
1041
- ADD CONSTRAINT plg_reports_schedules_pkey PRIMARY KEY (id);
1042
-
1043
- ALTER TABLE ONLY public.plg_reports_schedules
1044
- ADD CONSTRAINT plg_reports_schedules_tenant_id_id_key UNIQUE (tenant_id, id);
1045
-
1046
-
1047
- -- \u2500\u2500 index \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1048
-
1049
- CREATE UNIQUE INDEX plg_reports_definitions_key_uq ON public.plg_reports_definitions USING btree (COALESCE(tenant_id, '00000000-0000-0000-0000-000000000000'::uuid), key, version);
1050
-
1051
- CREATE INDEX plg_reports_definitions_module_idx ON public.plg_reports_definitions USING btree (module) WHERE is_active;
1052
-
1053
- CREATE INDEX plg_reports_definitions_tenant_owner_idx ON public.plg_reports_definitions USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);
1054
-
1055
- CREATE INDEX plg_reports_definitions_tenant_unit_idx ON public.plg_reports_definitions USING btree (tenant_id, unit_id);
1056
-
1057
- CREATE INDEX plg_reports_runs_key_idx ON public.plg_reports_runs USING btree (tenant_id, definition_key, created_at DESC);
1058
-
1059
- CREATE INDEX plg_reports_runs_tenant_owner_idx ON public.plg_reports_runs USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);
1060
-
1061
- CREATE INDEX plg_reports_runs_tenant_unit_idx ON public.plg_reports_runs USING btree (tenant_id, unit_id);
1062
-
1063
- CREATE INDEX plg_reports_schedules_due_idx ON public.plg_reports_schedules USING btree (next_run_at) WHERE is_active;
1064
-
1065
- CREATE INDEX plg_reports_schedules_tenant_owner_idx ON public.plg_reports_schedules USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);
1066
-
1067
- CREATE INDEX plg_reports_schedules_tenant_unit_idx ON public.plg_reports_schedules USING btree (tenant_id, unit_id);
1068
-
1069
-
1070
- -- \u2500\u2500 fk constraint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1071
-
1072
- ALTER TABLE ONLY public.plg_reports_definitions
1073
- ADD CONSTRAINT plg_reports_definitions_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;
1074
-
1075
- ALTER TABLE ONLY public.plg_reports_definitions
1076
- ADD CONSTRAINT plg_reports_definitions_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
1077
-
1078
- ALTER TABLE ONLY public.plg_reports_definitions
1079
- ADD CONSTRAINT plg_reports_definitions_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
1080
-
1081
- ALTER TABLE ONLY public.plg_reports_runs
1082
- ADD CONSTRAINT plg_reports_runs_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;
1083
-
1084
- ALTER TABLE ONLY public.plg_reports_runs
1085
- ADD CONSTRAINT plg_reports_runs_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
1086
-
1087
- ALTER TABLE ONLY public.plg_reports_runs
1088
- ADD CONSTRAINT plg_reports_runs_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
1089
-
1090
- ALTER TABLE ONLY public.plg_reports_schedules
1091
- ADD CONSTRAINT plg_reports_schedules_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;
1092
-
1093
- ALTER TABLE ONLY public.plg_reports_schedules
1094
- ADD CONSTRAINT plg_reports_schedules_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
1095
-
1096
- ALTER TABLE ONLY public.plg_reports_schedules
1097
- ADD CONSTRAINT plg_reports_schedules_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
1098
-
1099
-
1100
- -- \u2500\u2500 table data \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1101
-
1102
- COPY public.plg_reports_definitions (id, tenant_id, unit_id, key, version, title, description, module, source_view, params, columns, default_order_by, required_permission, is_active, metadata, created_by, owner_id, created_at, updated_at) FROM stdin;
1103
- b9bdb96e-2219-4bea-a83d-be54e28c4bda \\N \\N orders.sales 1 Vendas Every sale, whichever door it came through \u2014 the query the order vocabulary made possible. orders v_sales [{"name": "channel", "column": "channel"}, {"name": "status", "column": "status"}, {"name": "from", "column": "created_at", "operator": ">="}, {"name": "to", "column": "created_at", "operator": "<="}] [] created_at orders.read t {} \\N \\N 2026-09-04 15:03:27.267053+00 2026-09-04 15:03:27.267053+00
1104
- 706d0d78-193c-4abe-8828-5e00d719d155 \\N \\N orders.fulfillment 1 Entregas pendentes Ordered against delivered, per line. A positive pending is what a boolean could never describe. orders v_order_fulfillment [{"name": "state", "column": "state"}] [] \\N orders.read t {} \\N \\N 2026-09-04 15:03:27.267053+00 2026-09-04 15:03:27.267053+00
1105
- 6e27d897-55d8-4f39-b963-b5cb3121069d \\N \\N financial.receivables 1 Contas a receber Instalments and what is still owed on them, straight from the allocations. financial v_financial_installment_balances [{"name": "status", "column": "status"}, {"name": "due_from", "column": "due_date", "operator": ">="}, {"name": "due_to", "column": "due_date", "operator": "<="}] [] due_date financial.read t {} \\N \\N 2026-09-04 15:03:27.267053+00 2026-09-04 15:03:27.267053+00
1106
- 66d7f336-fff1-40ea-b754-6b158f1b0dea \\N \\N financial.uninvoiced 1 Vendas sem t\xEDtulo Sales that produced no invoice. Empty is the bar this phase closes on. financial v_orders_sem_invoice [{"name": "kind", "column": "kind"}] [] created_at financial.read t {} \\N \\N 2026-09-04 15:03:27.267053+00 2026-09-04 15:03:27.267053+00
1107
- a5a3ffab-e6e1-46ad-942f-f36582b4e126 \\N \\N inventory.balances 1 Saldo de estoque The balance, from the one ledger that owns it. inventory v_inventory_balances [] [] \\N inventory.read t {} \\N \\N 2026-09-04 15:03:27.267053+00 2026-09-04 15:03:27.267053+00
1108
- a2d3191e-5a8d-4e03-892f-0c9573675ea5 \\N \\N notifications.queue 1 Fila de notifica\xE7\xF5es What is waiting, what is overdue, what was abandoned and what was refused. notifications v_notification_queue [{"name": "status", "column": "status"}, {"name": "channel", "column": "channel"}] [] \\N notifications.read t {} \\N \\N 2026-09-04 15:03:27.267053+00 2026-09-04 15:03:27.267053+00
1109
- c616ae83-b861-41fb-959a-bbb85431ed77 \\N \\N agenda.appointments 1 Agendamentos The calendar as a table, with the Venda and the payment state each booking carries. agenda v_appointments [{"name": "status", "column": "status"}, {"name": "from", "column": "starts_at", "operator": ">="}, {"name": "to", "column": "starts_at", "operator": "<="}] [] starts_at agenda.read t {} \\N \\N 2026-09-04 15:03:27.267053+00 2026-09-04 15:03:27.267053+00
1110
- 5af4a8c8-903b-4ea6-8829-98877b017883 \\N \\N crm.funnel 1 Funil The funnel over plg_crm_deals, with won_at and lost_at \u2014 which is what makes a cycle length measurable. crm v_deals [{"name": "status", "column": "status"}, {"name": "stage", "column": "stage_name"}] [] created_at crm.pipeline.read t {} \\N \\N 2026-09-04 15:03:27.267053+00 2026-09-04 15:03:27.267053+00
1111
- 9143ce67-ecaa-46a3-8028-7c354c34f301 \\N \\N marketing.campaigns 1 Campanhas Reach, delivery and conversion per campaign \u2014 the metric comes from here, never from a counter on the campaign. marketing v_marketing_campaign_metrics [{"name": "status", "column": "status"}] [] \\N marketing.read t {} \\N \\N 2026-09-04 15:03:27.267053+00 2026-09-04 15:03:27.267053+00
1112
- \\.
1113
-
1114
-
1115
- -- \u2500\u2500 trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1116
-
1117
- CREATE TRIGGER plg_reports_definitions_source BEFORE INSERT OR UPDATE OF source_view ON public.plg_reports_definitions FOR EACH ROW EXECUTE FUNCTION public.trg_plg_reports_definitions_source();
1118
-
1119
- CREATE TRIGGER plg_reports_definitions_updated_at BEFORE UPDATE ON public.plg_reports_definitions FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
1120
-
1121
- CREATE TRIGGER plg_reports_runs_updated_at BEFORE UPDATE ON public.plg_reports_runs FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
1122
-
1123
- CREATE TRIGGER plg_reports_schedules_updated_at BEFORE UPDATE ON public.plg_reports_schedules FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
1124
-
1125
-
1126
- -- \u2500\u2500 row security \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1127
-
1128
- ALTER TABLE public.plg_reports_definitions ENABLE ROW LEVEL SECURITY;
1129
-
1130
- ALTER TABLE public.plg_reports_runs ENABLE ROW LEVEL SECURITY;
1131
-
1132
- ALTER TABLE public.plg_reports_schedules ENABLE ROW LEVEL SECURITY;
1133
-
1134
-
1135
- -- \u2500\u2500 policy \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1136
-
1137
- CREATE POLICY plg_reports_definitions_authz_delete ON public.plg_reports_definitions FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_definitions.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('reports.definition'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('reports.definition'::text, plg_reports_definitions.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('reports.read_all'::text, plg_reports_definitions.unit_id) AS has_permission)) AND ( SELECT app.has_permission('reports.delete'::text, plg_reports_definitions.unit_id) AS has_permission)));
1138
-
1139
- CREATE POLICY plg_reports_definitions_authz_insert ON public.plg_reports_definitions FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_definitions.unit_id) AS has_unit)) AND ( SELECT app.has_permission('reports.create'::text, plg_reports_definitions.unit_id) AS has_permission)));
1140
-
1141
- CREATE POLICY plg_reports_definitions_authz_select ON public.plg_reports_definitions FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_definitions.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('reports.definition'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('reports.definition'::text, plg_reports_definitions.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('reports.read_all'::text, plg_reports_definitions.unit_id) AS has_permission)) AND ( SELECT app.has_permission('reports.read'::text, plg_reports_definitions.unit_id) AS has_permission)));
1142
-
1143
- CREATE POLICY plg_reports_definitions_authz_update ON public.plg_reports_definitions FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_definitions.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('reports.definition'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('reports.definition'::text, plg_reports_definitions.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('reports.read_all'::text, plg_reports_definitions.unit_id) AS has_permission)) AND ( SELECT app.has_permission('reports.edit'::text, plg_reports_definitions.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_definitions.unit_id) AS has_unit)) AND ( SELECT app.has_permission('reports.edit'::text, plg_reports_definitions.unit_id) AS has_permission)));
1144
-
1145
- CREATE POLICY plg_reports_definitions_platform_read ON public.plg_reports_definitions FOR SELECT TO authenticated USING (((tenant_id IS NULL) AND is_active));
1146
-
1147
- CREATE POLICY plg_reports_runs_authz_delete ON public.plg_reports_runs FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_runs.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('reports.run'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('reports.run'::text, plg_reports_runs.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('reports.read_all'::text, plg_reports_runs.unit_id) AS has_permission)) AND ( SELECT app.has_permission('reports.delete'::text, plg_reports_runs.unit_id) AS has_permission)));
1148
-
1149
- CREATE POLICY plg_reports_runs_authz_insert ON public.plg_reports_runs FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_runs.unit_id) AS has_unit)) AND ( SELECT app.has_permission('reports.create'::text, plg_reports_runs.unit_id) AS has_permission)));
1150
-
1151
- CREATE POLICY plg_reports_runs_authz_select ON public.plg_reports_runs FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_runs.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('reports.run'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('reports.run'::text, plg_reports_runs.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('reports.read_all'::text, plg_reports_runs.unit_id) AS has_permission)) AND ( SELECT app.has_permission('reports.read'::text, plg_reports_runs.unit_id) AS has_permission)));
1152
-
1153
- CREATE POLICY plg_reports_runs_authz_update ON public.plg_reports_runs FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_runs.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('reports.run'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('reports.run'::text, plg_reports_runs.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('reports.read_all'::text, plg_reports_runs.unit_id) AS has_permission)) AND ( SELECT app.has_permission('reports.edit'::text, plg_reports_runs.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_runs.unit_id) AS has_unit)) AND ( SELECT app.has_permission('reports.edit'::text, plg_reports_runs.unit_id) AS has_permission)));
1154
-
1155
- CREATE POLICY plg_reports_schedules_authz_delete ON public.plg_reports_schedules FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_schedules.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('reports.schedule'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('reports.schedule'::text, plg_reports_schedules.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('reports.read_all'::text, plg_reports_schedules.unit_id) AS has_permission)) AND ( SELECT app.has_permission('reports.delete'::text, plg_reports_schedules.unit_id) AS has_permission)));
1156
-
1157
- CREATE POLICY plg_reports_schedules_authz_insert ON public.plg_reports_schedules FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_schedules.unit_id) AS has_unit)) AND ( SELECT app.has_permission('reports.create'::text, plg_reports_schedules.unit_id) AS has_permission)));
1158
-
1159
- CREATE POLICY plg_reports_schedules_authz_select ON public.plg_reports_schedules FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_schedules.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('reports.schedule'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('reports.schedule'::text, plg_reports_schedules.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('reports.read_all'::text, plg_reports_schedules.unit_id) AS has_permission)) AND ( SELECT app.has_permission('reports.read'::text, plg_reports_schedules.unit_id) AS has_permission)));
1160
-
1161
- CREATE POLICY plg_reports_schedules_authz_update ON public.plg_reports_schedules FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_schedules.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('reports.schedule'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('reports.schedule'::text, plg_reports_schedules.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('reports.read_all'::text, plg_reports_schedules.unit_id) AS has_permission)) AND ( SELECT app.has_permission('reports.edit'::text, plg_reports_schedules.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_schedules.unit_id) AS has_unit)) AND ( SELECT app.has_permission('reports.edit'::text, plg_reports_schedules.unit_id) AS has_permission)));
1162
-
1163
-
1164
- -- \u2500\u2500 acl \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1165
-
1166
- GRANT MAINTAIN ON TABLE public.plg_reports_definitions TO anon;
1167
- GRANT ALL ON TABLE public.plg_reports_definitions TO authenticated;
1168
- GRANT ALL ON TABLE public.plg_reports_definitions TO service_role;
1169
-
1170
- REVOKE ALL ON FUNCTION public.reports_definition(p_key text, p_tenant uuid) FROM PUBLIC;
1171
- GRANT ALL ON FUNCTION public.reports_definition(p_key text, p_tenant uuid) TO authenticated;
1172
- GRANT ALL ON FUNCTION public.reports_definition(p_key text, p_tenant uuid) TO service_role;
1173
-
1174
- REVOKE ALL ON FUNCTION public.reports_deliver_due(p_limit integer) FROM PUBLIC;
1175
- GRANT ALL ON FUNCTION public.reports_deliver_due(p_limit integer) TO service_role;
1176
-
1177
- REVOKE ALL ON FUNCTION public.reports_ensure_delivery_template(p_tenant uuid, p_channel text) FROM PUBLIC;
1178
- GRANT ALL ON FUNCTION public.reports_ensure_delivery_template(p_tenant uuid, p_channel text) TO authenticated;
1179
- GRANT ALL ON FUNCTION public.reports_ensure_delivery_template(p_tenant uuid, p_channel text) TO service_role;
1180
-
1181
- REVOKE ALL ON FUNCTION public.reports_run(p_key text, p_params jsonb, p_limit integer, p_offset integer, p_format text) FROM PUBLIC;
1182
- GRANT ALL ON FUNCTION public.reports_run(p_key text, p_params jsonb, p_limit integer, p_offset integer, p_format text) TO authenticated;
1183
- GRANT ALL ON FUNCTION public.reports_run(p_key text, p_params jsonb, p_limit integer, p_offset integer, p_format text) TO service_role;
1184
-
1185
- REVOKE ALL ON FUNCTION public.trg_plg_reports_definitions_source() FROM PUBLIC;
1186
- GRANT ALL ON FUNCTION public.trg_plg_reports_definitions_source() TO service_role;
1187
-
1188
- GRANT MAINTAIN ON TABLE public.plg_reports_runs TO anon;
1189
- GRANT ALL ON TABLE public.plg_reports_runs TO authenticated;
1190
- GRANT ALL ON TABLE public.plg_reports_runs TO service_role;
1191
-
1192
- GRANT MAINTAIN ON TABLE public.plg_reports_schedules TO anon;
1193
- GRANT ALL ON TABLE public.plg_reports_schedules TO authenticated;
1194
- GRANT ALL ON TABLE public.plg_reports_schedules TO service_role;
1195
-
1196
-
1197
- -- \u2500\u2500 comment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1198
-
1199
- COMMENT ON TABLE public.plg_reports_definitions IS 'The report catalogue (PRD 08). Versioned, so changing a report does not rewrite the history of everyone who ran the previous one; and \`required_permission\` is declared HERE rather than on a screen, so the engine can refuse before it queries.';
1200
-
1201
- COMMENT ON FUNCTION public.reports_deliver_due(p_limit integer) IS 'Queue the scheduled reports that are due (PRD 08). It enqueues through the notification engine rather than sending \u2014 the same mechanism as dunning and as a campaign, which is what made building that engine first worth doing.';
1202
-
1203
- COMMENT ON FUNCTION public.reports_run(p_key text, p_params jsonb, p_limit integer, p_offset integer, p_format text) IS 'Run a report from the catalogue (PRD 08). The permission is checked BEFORE the query is built \u2014 a refusal reads no rows and comes back as status=refused, recorded in plg_reports_runs \u2014 the tenant and unit predicates are injected by the engine and cannot be switched off by a definition, and an undeclared parameter is refused rather than ignored.';
1204
-
1205
- COMMENT ON TABLE public.plg_reports_runs IS 'Who ran what, with which parameters, against which VERSION of the definition (PRD 08). A refusal is recorded too \u2014 "the report is empty" and "you were not allowed to see it" are different answers and the history has to keep them apart.';
1206
-
1207
- COMMENT ON TABLE public.plg_reports_schedules IS 'A report on a schedule (PRD 08). Delivery is the notification engine''s job (PRD 03) \u2014 the same queue, retry and opt-out that carry a receipt carry a report, which is the whole reason that engine was built first.';
1208
-
1209
-
1210
- -- \u2500\u2500 default privileges: restore \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1211
- -- Put back, so the NEXT thing installed inherits what the chain had.
1212
-
1213
- DO $dp$ BEGIN
1214
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
1215
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1216
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
1217
- END $dp$;
1218
-
1219
- DO $dp$ BEGIN
1220
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
1221
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1222
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
1223
- END $dp$;
1224
-
1225
- DO $dp$ BEGIN
1226
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
1227
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1228
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
1229
- END $dp$;
1230
-
1231
- DO $dp$ BEGIN
1232
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
1233
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1234
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
1235
- END $dp$;
1236
-
1237
- DO $dp$ BEGIN
1238
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
1239
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1240
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
1241
- END $dp$;
1242
-
1243
- DO $dp$ BEGIN
1244
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
1245
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1246
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
1247
- END $dp$;
1248
-
1249
- DO $dp$ BEGIN
1250
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
1251
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1252
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
1253
- END $dp$;
1254
-
1255
- DO $dp$ BEGIN
1256
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
1257
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1258
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
1259
- END $dp$;
1260
-
1261
- DO $dp$ BEGIN
1262
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
1263
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1264
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
1265
- END $dp$;
1266
-
1267
- DO $dp$ BEGIN
1268
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
1269
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1270
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
1271
- END $dp$;
1272
-
1273
- DO $dp$ BEGIN
1274
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
1275
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1276
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
1277
- END $dp$;
1278
-
1279
- DO $dp$ BEGIN
1280
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
1281
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1282
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
1283
- END $dp$;
1284
-
1285
- DO $dp$ BEGIN
1286
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;
1287
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1288
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;
1289
- END $dp$;
1290
-
1291
- DO $dp$ BEGIN
1292
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
1293
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1294
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
1295
- END $dp$;
1296
-
1297
- DO $dp$ BEGIN
1298
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
1299
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1300
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
1301
- END $dp$;
1302
-
1303
- DO $dp$ BEGIN
1304
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
1305
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1306
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
1307
- END $dp$;
1308
-
1309
- DO $dp$ BEGIN
1310
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;
1311
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1312
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;
1313
- END $dp$;
1314
-
1315
- DO $dp$ BEGIN
1316
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
1317
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1318
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
1319
- END $dp$;
1320
-
1321
- DO $dp$ BEGIN
1322
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
1323
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1324
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
1325
- END $dp$;
1326
-
1327
- DO $dp$ BEGIN
1328
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
1329
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1330
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
1331
- END $dp$;
1332
-
1333
- DO $dp$ BEGIN
1334
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;
1335
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1336
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;
1337
- END $dp$;
1338
-
1339
- DO $dp$ BEGIN
1340
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
1341
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1342
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
1343
- END $dp$;
1344
-
1345
- DO $dp$ BEGIN
1346
- EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
1347
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1348
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
1349
- END $dp$;
1350
-
1351
- DO $dp$ BEGIN
1352
- EXECUTE $stmt$--
1353
- -- PostgreSQL database dump complete
1354
- --$stmt$;
1355
- EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1356
- RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$--
1357
- -- PostgreSQL database dump complete
1358
- --$stmt$;
1359
- END $dp$;
1360
- `;
1361
- var MIGRATIONS = [
1362
- { id: "000_baseline", sql: MIGRATION_000_BASELINE }
1363
- ];
1364
-
1365
681
  // src/lib/legacy.ts
1366
682
  function reportDefToCard(report, createdBy) {
1367
683
  return {
@@ -1418,1389 +734,6 @@ function reportDefToCard(report, createdBy) {
1418
734
  function reportDefsToCards(reports, createdBy) {
1419
735
  return reports.map((r) => reportDefToCard(r, createdBy));
1420
736
  }
1421
- var KEY = "fayz.analytics.lastViewed";
1422
- function read() {
1423
- if (typeof window === "undefined") return {};
1424
- try {
1425
- const raw = window.localStorage.getItem(KEY);
1426
- return raw ? JSON.parse(raw) : {};
1427
- } catch {
1428
- return {};
1429
- }
1430
- }
1431
- function write(map) {
1432
- if (typeof window === "undefined") return;
1433
- try {
1434
- window.localStorage.setItem(KEY, JSON.stringify(map));
1435
- } catch {
1436
- }
1437
- }
1438
- var listeners = /* @__PURE__ */ new Set();
1439
- function markViewed(cardId) {
1440
- const next = { ...read(), [cardId]: (/* @__PURE__ */ new Date()).toISOString() };
1441
- write(next);
1442
- for (const listener of listeners) listener(next);
1443
- }
1444
- function useLastViewed() {
1445
- const [map, setMap] = React10.useState(() => read());
1446
- React10.useEffect(() => {
1447
- listeners.add(setMap);
1448
- return () => {
1449
- listeners.delete(setMap);
1450
- };
1451
- }, []);
1452
- return map;
1453
- }
1454
- var KEY2 = "fayz.analytics.prefs";
1455
- var EMPTY = { version: 1, cards: {}, custom: [] };
1456
- function read2() {
1457
- if (typeof window === "undefined") return EMPTY;
1458
- try {
1459
- const raw = window.localStorage.getItem(KEY2);
1460
- if (!raw) return EMPTY;
1461
- const parsed = JSON.parse(raw);
1462
- return {
1463
- version: 1,
1464
- cards: parsed.cards && typeof parsed.cards === "object" ? parsed.cards : {},
1465
- custom: Array.isArray(parsed.custom) ? parsed.custom : []
1466
- };
1467
- } catch {
1468
- return EMPTY;
1469
- }
1470
- }
1471
- function write2(prefs) {
1472
- if (typeof window === "undefined") return;
1473
- try {
1474
- const empty = Object.keys(prefs.cards).length === 0 && prefs.custom.length === 0;
1475
- if (empty) window.localStorage.removeItem(KEY2);
1476
- else window.localStorage.setItem(KEY2, JSON.stringify(prefs));
1477
- } catch {
1478
- }
1479
- }
1480
- var listeners2 = /* @__PURE__ */ new Set();
1481
- function publish(next) {
1482
- write2(next);
1483
- for (const listener of listeners2) listener(next);
1484
- }
1485
- function useAnalyticsPrefs() {
1486
- const [prefs, setPrefs] = React10.useState(() => read2());
1487
- React10.useEffect(() => {
1488
- listeners2.add(setPrefs);
1489
- return () => {
1490
- listeners2.delete(setPrefs);
1491
- };
1492
- }, []);
1493
- return prefs;
1494
- }
1495
- function setCardPref(cardId, patch) {
1496
- const current = read2();
1497
- const merged = { ...current.cards[cardId], ...patch };
1498
- const meaningful = Object.entries(merged).filter(([, v]) => v !== void 0 && v !== null);
1499
- const cards = { ...current.cards };
1500
- if (meaningful.length === 0) delete cards[cardId];
1501
- else cards[cardId] = Object.fromEntries(meaningful);
1502
- publish({ ...current, cards });
1503
- }
1504
- function clearCardPref(cardId) {
1505
- const current = read2();
1506
- const cards = { ...current.cards };
1507
- delete cards[cardId];
1508
- publish({ ...current, cards });
1509
- }
1510
- function setOverviewOrder(ids) {
1511
- const current = read2();
1512
- const cards = { ...current.cards };
1513
- ids.forEach((id, index) => {
1514
- cards[id] = { ...cards[id], order: index, pinned: cards[id]?.pinned ?? true };
1515
- });
1516
- publish({ ...current, cards });
1517
- }
1518
- function saveCustomReport(report) {
1519
- const current = read2();
1520
- const custom = current.custom.some((r) => r.id === report.id) ? current.custom.map((r) => r.id === report.id ? report : r) : [...current.custom, report];
1521
- publish({ ...current, custom });
1522
- }
1523
- function deleteCustomReport(id) {
1524
- const current = read2();
1525
- const cards = { ...current.cards };
1526
- delete cards[id];
1527
- publish({ ...current, cards, custom: current.custom.filter((r) => r.id !== id) });
1528
- }
1529
- function resetAnalyticsPrefs() {
1530
- publish({ ...EMPTY });
1531
- }
1532
- function sourceKey(card) {
1533
- const source = card.query.source;
1534
- if (source.kind === "fn") return card.grain ? `fn:${card.grain}` : `fn:${card.id}`;
1535
- return `${source.kind}:${source.name ?? card.id}`;
1536
- }
1537
- function analyticsSources(pool) {
1538
- const groups = /* @__PURE__ */ new Map();
1539
- for (const card of pool) {
1540
- const key = sourceKey(card);
1541
- const list = groups.get(key);
1542
- if (list) list.push(card);
1543
- else groups.set(key, [card]);
1544
- }
1545
- const out = [];
1546
- for (const [key, cards] of groups) {
1547
- const base = cards.find((c) => (c.query.measures?.length ?? 0) > 0);
1548
- if (!base) continue;
1549
- const palette = fieldPalette(pool, base);
1550
- if (palette.measures.length === 0) continue;
1551
- out.push({ key, base, label: base.name, ...palette });
1552
- }
1553
- return out;
1554
- }
1555
- function fieldPalette(pool, base) {
1556
- const key = sourceKey(base);
1557
- const dimensions = /* @__PURE__ */ new Map();
1558
- const measures = /* @__PURE__ */ new Map();
1559
- for (const card of pool) {
1560
- if (sourceKey(card) !== key) continue;
1561
- for (const d of card.query.dimensions ?? []) {
1562
- if (!d.hidden && !dimensions.has(d.key)) dimensions.set(d.key, d);
1563
- }
1564
- for (const m of card.query.measures ?? []) {
1565
- if (!measures.has(m.key)) measures.set(m.key, m);
1566
- }
1567
- }
1568
- return { dimensions: [...dimensions.values()], measures: [...measures.values()] };
1569
- }
1570
- function withRatioOperands(kept, all) {
1571
- const set = new Set(kept);
1572
- for (const key of kept) {
1573
- const measure = all.find((m) => m.key === key);
1574
- for (const operand of measure?.ratioOf ?? []) set.add(operand);
1575
- }
1576
- return set;
1577
- }
1578
- function applyPref(card, pref) {
1579
- if (!pref) return card;
1580
- const next = { ...card, query: { ...card.query }, viz: { ...card.viz } };
1581
- if (pref.name) next.name = pref.name;
1582
- if (pref.viz) next.viz.type = pref.viz;
1583
- if (pref.measures?.length) {
1584
- const all = card.query.measures ?? [];
1585
- const keep = withRatioOperands(pref.measures, all);
1586
- next.query.measures = all.filter((m) => keep.has(m.key));
1587
- if (card.viz.measures?.length) {
1588
- next.viz.measures = card.viz.measures.filter((k) => keep.has(k));
1589
- }
1590
- }
1591
- if (pref.dimensions?.length) {
1592
- const keep = new Set(pref.dimensions);
1593
- next.query.dimensions = (card.query.dimensions ?? []).filter((d) => d.hidden || keep.has(d.key));
1594
- }
1595
- const placed = pref.pinned ?? Boolean(card.dashboard && card.dashboard.defaultVisible !== false);
1596
- if (!placed) {
1597
- next.dashboard = void 0;
1598
- } else {
1599
- next.dashboard = {
1600
- ...card.dashboard,
1601
- defaultVisible: true,
1602
- order: pref.order ?? card.dashboard?.order ?? 999,
1603
- span: pref.span ?? card.dashboard?.span
1604
- };
1605
- }
1606
- return next;
1607
- }
1608
- function resolveCustomReport(report, pool) {
1609
- const base = pool.find((c) => c.id === report.baseId);
1610
- if (!base) return null;
1611
- const palette = fieldPalette(pool, base);
1612
- const dimensions = report.dimensions.map((key) => palette.dimensions.find((d) => d.key === key)).filter((d) => Boolean(d));
1613
- const keep = withRatioOperands(report.measures, palette.measures);
1614
- const measures = palette.measures.filter((m) => keep.has(m.key));
1615
- return {
1616
- id: report.id,
1617
- name: report.name,
1618
- description: report.description,
1619
- icon: report.icon ?? "FileText",
1620
- category: report.category,
1621
- createdBy: report.createdBy,
1622
- grain: base.grain,
1623
- available: base.available,
1624
- compare: base.compare,
1625
- permission: base.permission,
1626
- query: {
1627
- source: base.query.source,
1628
- dateColumn: base.query.dateColumn,
1629
- dimensions,
1630
- measures,
1631
- filters: base.query.filters,
1632
- defaultSort: report.measures[0] ?? dimensions[0]?.key,
1633
- defaultSortDir: "desc"
1634
- },
1635
- viz: { type: report.viz }
1636
- };
1637
- }
1638
- function resolveCards(catalogue, prefs) {
1639
- const custom = prefs.custom.map((report) => resolveCustomReport(report, catalogue)).filter((c) => Boolean(c));
1640
- return [...catalogue, ...custom].filter((card) => !prefs.cards[card.id]?.hidden).map((card) => applyPref(card, prefs.cards[card.id]));
1641
- }
1642
- var ANALYTICS_BASE = "/analytics";
1643
- var REPORTS_BASE = `${ANALYTICS_BASE}/reports`;
1644
- var SETTINGS_ROUTE = `${ANALYTICS_BASE}/settings`;
1645
- var ConfigContext = React10__default.createContext(null);
1646
- function useAnalyticsConfig() {
1647
- const ctx = React10__default.useContext(ConfigContext);
1648
- if (!ctx) throw new Error("useAnalyticsConfig must be used inside <AnalyticsSurface>");
1649
- return ctx;
1650
- }
1651
- function useAnalyticsCatalogue() {
1652
- const runtime = usePluginRuntime();
1653
- const { cards, createdBy } = useAnalyticsConfig();
1654
- return React10__default.useMemo(() => {
1655
- const byId = /* @__PURE__ */ new Map();
1656
- for (const card of getAnalyticsCards(runtime)) byId.set(card.id, card);
1657
- for (const card of cards) byId.set(card.id, card);
1658
- return [...byId.values()].map((c) => c.createdBy ? c : { ...c, createdBy });
1659
- }, [runtime, cards, createdBy]);
1660
- }
1661
- function useAnalyticsCards() {
1662
- const catalogue = useAnalyticsCatalogue();
1663
- const prefs = useAnalyticsPrefs();
1664
- return React10__default.useMemo(() => resolveCards(catalogue, prefs), [catalogue, prefs]);
1665
- }
1666
- function useOverviewCards() {
1667
- const all = useAnalyticsCards();
1668
- return React10__default.useMemo(
1669
- () => all.filter((c) => c.dashboard && c.dashboard.defaultVisible !== false).sort((a, b) => (a.dashboard?.order ?? 0) - (b.dashboard?.order ?? 0)),
1670
- [all]
1671
- );
1672
- }
1673
- function cardColumns(card) {
1674
- if (card.columns?.length) return card.columns;
1675
- return [
1676
- // A hidden dimension is group-by only — the id a person column reads from,
1677
- // never a column of UUIDs next to the name it belongs to.
1678
- ...(card.query.dimensions ?? []).filter((d) => !d.hidden).map((d) => ({
1679
- key: d.key,
1680
- label: d.label,
1681
- type: d.person ? "person" : d.format ?? (d.grain ? "date" : "text"),
1682
- idKey: d.person?.idKey,
1683
- personKind: d.person?.kind,
1684
- align: "left"
1685
- })),
1686
- ...(card.query.measures ?? []).map((m) => ({
1687
- key: m.key,
1688
- label: m.label,
1689
- type: m.format ?? "number",
1690
- align: "right",
1691
- sortable: true
1692
- }))
1693
- ];
1694
- }
1695
- function navigateAnalytics(path) {
1696
- hashRouterAdapter().navigate(path);
1697
- }
1698
- function AnalyticsSurface({ config, provider, children }) {
1699
- const openCard = React10__default.useCallback((card) => {
1700
- markViewed(card.id);
1701
- navigateAnalytics(`${REPORTS_BASE}/${encodeURIComponent(card.id)}`);
1702
- }, []);
1703
- return /* @__PURE__ */ jsx(ConfigContext.Provider, { value: config, children: /* @__PURE__ */ jsx(
1704
- AnalyticsContextProvider,
1705
- {
1706
- provider,
1707
- currency: config.currency,
1708
- defaultPreset: config.defaultRange,
1709
- defaultCompare: config.defaultCompare,
1710
- defaultPageSize: config.defaultPageSize,
1711
- onOpenCard: openCard,
1712
- createdBy: config.createdBy,
1713
- children
1714
- }
1715
- ) });
1716
- }
1717
- function AnalyticsAssistant() {
1718
- const { assistant } = useAnalyticsConfig();
1719
- const t = useTranslation();
1720
- const suggestions = React10__default.useMemo(
1721
- () => assistant?.suggestions ?? [0, 1, 2].map((i) => ({ label: t(`reports.assistant.suggestion.${i}`) })),
1722
- [assistant?.suggestions, t]
1723
- );
1724
- if (!assistant) return null;
1725
- return /* @__PURE__ */ jsx(
1726
- AssistantInvite,
1727
- {
1728
- id: "analytics",
1729
- auto: assistant.auto ?? false,
1730
- role: assistant.role ?? "growth",
1731
- persona: assistant.persona ?? t("reports.assistant.persona"),
1732
- headline: assistant.headline ?? t("reports.assistant.headline"),
1733
- pitch: assistant.pitch ?? t("reports.assistant.pitch"),
1734
- description: assistant.description ?? t("reports.assistant.description"),
1735
- placeholder: assistant.placeholder ?? t("reports.assistant.placeholder"),
1736
- suggestions
1737
- }
1738
- );
1739
- }
1740
- var reportRoute = (cardId) => `${REPORTS_BASE}/${encodeURIComponent(cardId)}`;
1741
- function groupCards(cards) {
1742
- const groups = /* @__PURE__ */ new Map();
1743
- for (const card of cards) {
1744
- const list = groups.get(card.category);
1745
- if (list) list.push(card);
1746
- else groups.set(card.category, [card]);
1747
- }
1748
- return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([category, list]) => [category, [...list].sort((a, b) => a.name.localeCompare(b.name))]);
1749
- }
1750
- function useAnalyticsTree() {
1751
- const t = useTranslation();
1752
- const { labels, showLiveView } = useAnalyticsConfig();
1753
- const cards = useAnalyticsCards();
1754
- return React10__default.useMemo(() => ({
1755
- title: labels.analytics,
1756
- // Only the overview. The searchable catalogue (/analytics/reports) is still
1757
- // routed — bookmarks and the legacy /reports link resolve — but it is not
1758
- // listed: the tree below already IS the catalogue, and a link named "all
1759
- // reports" sitting above the reports reads as a different, fuller list.
1760
- fixed: [
1761
- { id: "overview", label: labels.overview ?? t("reports.overview"), icon: "BarChart3", route: ANALYTICS_BASE, view: "overview" }
1762
- ],
1763
- groups: groupCards(cards),
1764
- live: showLiveView ? { id: "live", label: labels.liveView, icon: "Radio", route: `${ANALYTICS_BASE}/live`, view: "live" } : null,
1765
- // Depois dos relatórios, não antes: é a tela que fala SOBRE a lista, e uma
1766
- // engrenagem no topo empurra para baixo o que a pessoa veio ver.
1767
- settings: {
1768
- id: "settings",
1769
- label: labels.settings ?? t("reports.settings.nav"),
1770
- icon: "SlidersHorizontal",
1771
- route: SETTINGS_ROUTE,
1772
- view: "settings"
1773
- }
1774
- }), [labels, showLiveView, cards, t]);
1775
- }
1776
- function AnalyticsModule({ view, cardId, children }) {
1777
- const t = useTranslation();
1778
- const { labels } = useAnalyticsConfig();
1779
- useModuleLayout();
1780
- const tree = useAnalyticsTree();
1781
- const activeId = cardId ? decodeURIComponent(cardId) : void 0;
1782
- const nav = React10__default.useMemo(() => {
1783
- return [
1784
- ...tree.fixed.map((f) => ({
1785
- id: f.id,
1786
- label: f.label,
1787
- icon: f.icon,
1788
- active: view === f.view,
1789
- onClick: () => navigateAnalytics(f.route)
1790
- })),
1791
- ...tree.groups.map(([category, list]) => ({
1792
- id: `group:${category}`,
1793
- label: category,
1794
- children: list.map((card) => ({
1795
- id: card.id,
1796
- label: card.name,
1797
- active: view === "report" && card.id === activeId,
1798
- onClick: () => navigateAnalytics(reportRoute(card.id))
1799
- }))
1800
- })),
1801
- ...tree.live ? [{
1802
- id: tree.live.id,
1803
- label: tree.live.label,
1804
- icon: tree.live.icon,
1805
- active: view === "live",
1806
- onClick: () => navigateAnalytics(tree.live.route)
1807
- }] : [],
1808
- {
1809
- id: tree.settings.id,
1810
- label: tree.settings.label,
1811
- icon: tree.settings.icon,
1812
- active: view === "settings",
1813
- onClick: () => navigateAnalytics(tree.settings.route)
1814
- }
1815
- ];
1816
- }, [tree, view, activeId]);
1817
- return /* @__PURE__ */ jsxs(
1818
- ModulePage,
1819
- {
1820
- title: tree.title,
1821
- nav,
1822
- navVariant: "rail",
1823
- navFilterPlaceholder: labels.searchReports ?? t("reports.search"),
1824
- showHeader: false,
1825
- viewKey: view === "report" ? `report:${activeId}` : view,
1826
- children: [
1827
- /* @__PURE__ */ jsx(AnalyticsAssistant, {}),
1828
- /* @__PURE__ */ jsx("div", { className: "md:pb-16", children })
1829
- ]
1830
- }
1831
- );
1832
- }
1833
- function AnalyticsOverview() {
1834
- const { labels } = useAnalyticsConfig();
1835
- const cards = useOverviewCards();
1836
- const filters = React10__default.useMemo(() => collectFilters(cards), [cards]);
1837
- return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1838
- /* @__PURE__ */ jsx(AnalyticsControls, { filters }),
1839
- cards.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center rounded-xl border border-dashed py-16 text-center", children: [
1840
- /* @__PURE__ */ jsx("div", { className: "mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: /* @__PURE__ */ jsx(BarChart3, { className: "h-5 w-5 text-muted-foreground" }) }),
1841
- /* @__PURE__ */ jsx("p", { className: "font-medium", children: labels.emptyOverviewTitle }),
1842
- /* @__PURE__ */ jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: labels.emptyOverviewDescription })
1843
- ] }) : /* @__PURE__ */ jsx(AnalyticsGrid, { cards })
1844
- ] }) });
1845
- }
1846
- function formatViewed(iso, locale, never) {
1847
- if (!iso) return never;
1848
- const d = new Date(iso);
1849
- if (Number.isNaN(d.getTime())) return never;
1850
- return d.toLocaleDateString(locale, { day: "numeric", month: "short", year: "numeric" });
1851
- }
1852
- function ReportsListPage() {
1853
- const { labels, currency, showHeader } = useAnalyticsConfig();
1854
- const cards = useAnalyticsCards();
1855
- const lastViewed = useLastViewed();
1856
- const [search, setSearch] = React10__default.useState("");
1857
- const [category, setCategory] = React10__default.useState();
1858
- const [author, setAuthor] = React10__default.useState();
1859
- const categoryOptions = React10__default.useMemo(
1860
- () => [...new Set(cards.map((c) => c.category))].sort((a, b) => a.localeCompare(b)).map((c) => ({ value: c, label: c })),
1861
- [cards]
1862
- );
1863
- const authorOptions = React10__default.useMemo(
1864
- () => [...new Set(cards.map((c) => c.createdBy ?? "\u2014"))].sort((a, b) => a.localeCompare(b)).map((a) => ({ value: a, label: a })),
1865
- [cards]
1866
- );
1867
- const rows = React10__default.useMemo(() => {
1868
- const q = search.trim().toLowerCase();
1869
- return cards.filter((card) => {
1870
- if (category && card.category !== category) return false;
1871
- if (author && (card.createdBy ?? "\u2014") !== author) return false;
1872
- if (!q) return true;
1873
- return card.name.toLowerCase().includes(q) || card.description?.toLowerCase().includes(q) || card.category.toLowerCase().includes(q);
1874
- });
1875
- }, [cards, search, category, author]);
1876
- const columns = React10__default.useMemo(() => [
1877
- {
1878
- id: "name",
1879
- accessorFn: (card) => card.name,
1880
- header: labels.name,
1881
- cell: ({ row }) => /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
1882
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: row.original.name }),
1883
- row.original.available === false ? /* @__PURE__ */ jsx("span", { className: "ml-2 rounded-full border px-1.5 py-0.5 text-[10px] text-muted-foreground", children: labels.unavailableTitle }) : null,
1884
- row.original.description ? /* @__PURE__ */ jsx("p", { className: "mt-0.5 line-clamp-1 text-muted-foreground sm:hidden", children: row.original.description }) : null
1885
- ] })
1886
- },
1887
- {
1888
- id: "category",
1889
- accessorFn: (card) => card.category,
1890
- header: labels.category,
1891
- cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "rounded-md bg-muted px-2 py-0.5", children: String(getValue() ?? "") })
1892
- },
1893
- {
1894
- id: "lastViewed",
1895
- // Sorts on the timestamp, renders the date. Never-opened reports carry
1896
- // undefined so they sink to the bottom in BOTH directions rather than
1897
- // topping "most recent" as an epoch-zero date.
1898
- accessorFn: (card) => {
1899
- const iso = lastViewed[card.id];
1900
- return iso ? new Date(iso).getTime() : void 0;
1901
- },
1902
- sortUndefined: 1,
1903
- header: labels.lastViewed,
1904
- cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatViewed(lastViewed[row.original.id], currency.locale, labels.never) })
1905
- },
1906
- {
1907
- id: "createdBy",
1908
- accessorFn: (card) => card.createdBy ?? "\u2014",
1909
- header: labels.createdBy,
1910
- cell: ({ getValue }) => /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5 text-muted-foreground", children: [
1911
- /* @__PURE__ */ jsx(User, { className: "h-3 w-3" }),
1912
- String(getValue() ?? "\u2014")
1913
- ] })
1914
- }
1915
- ], [labels, currency.locale, lastViewed]);
1916
- const open = (card) => {
1917
- markViewed(card.id);
1918
- navigateAnalytics(`${REPORTS_BASE}/${encodeURIComponent(card.id)}`);
1919
- };
1920
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1921
- showHeader ? /* @__PURE__ */ jsxs("div", { children: [
1922
- /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold tracking-tight", children: labels.reports }),
1923
- /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: labels.reportsSubtitle })
1924
- ] }) : null,
1925
- /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
1926
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2 border-b border-border px-3 py-2", children: [
1927
- /* @__PURE__ */ jsx("div", { className: "min-w-[10rem] flex-1", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
1928
- /* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
1929
- /* @__PURE__ */ jsx(
1930
- Input,
1931
- {
1932
- value: search,
1933
- onChange: (e) => setSearch(e.target.value),
1934
- placeholder: labels.searchReports,
1935
- className: "h-auto rounded-none border-0 bg-transparent py-1.5 pl-8 pr-3 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
1936
- }
1937
- )
1938
- ] }) }),
1939
- /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-wrap items-center gap-1.5", children: [
1940
- /* @__PURE__ */ jsx(
1941
- FilterChip,
1942
- {
1943
- label: labels.category,
1944
- allLabel: labels.allCategories,
1945
- options: categoryOptions,
1946
- value: category,
1947
- onChange: setCategory
1948
- }
1949
- ),
1950
- /* @__PURE__ */ jsx(
1951
- FilterChip,
1952
- {
1953
- label: labels.createdBy,
1954
- allLabel: labels.allCreatedBy,
1955
- options: authorOptions,
1956
- value: author,
1957
- onChange: setAuthor
1958
- }
1959
- )
1960
- ] })
1961
- ] }),
1962
- /* @__PURE__ */ jsx(
1963
- DataTable,
1964
- {
1965
- columns,
1966
- data: rows,
1967
- variant: "bare",
1968
- compact: true,
1969
- onRowClick: open,
1970
- emptyMessage: labels.noResults
1971
- }
1972
- ),
1973
- /* @__PURE__ */ jsxs("div", { className: "border-t border-border px-3 py-2 text-xs text-muted-foreground", children: [
1974
- rows.length,
1975
- " ",
1976
- labels.results
1977
- ] })
1978
- ] })
1979
- ] });
1980
- }
1981
- function exportToCsv(columns, data, filename, currency) {
1982
- const { columns: cols, rows } = expandBagColumns(columns, data);
1983
- const visibleCols = cols.filter((c) => c.visible !== false);
1984
- const csvColumns = visibleCols.map((col) => ({
1985
- key: col.key,
1986
- label: col.label,
1987
- format: (value) => formatValuePlain(value, col, currency)
1988
- }));
1989
- exportCSV(csvColumns, rows, filename);
1990
- }
1991
- function ExportDropdown({ columns, data, filename, currency, title, fetchAll }) {
1992
- const t = useTranslation();
1993
- const [excelAvailable, setExcelAvailable] = useState(false);
1994
- const [pdfAvailable, setPdfAvailable] = useState(false);
1995
- const [busy, setBusy] = useState(false);
1996
- useEffect(() => {
1997
- import('./excel-NKWJ2AMC.js').then((m) => m.isExcelAvailable()).then(setExcelAvailable).catch(() => {
1998
- });
1999
- import('./pdf-H73MJGB6.js').then((m) => m.isPdfAvailable()).then(setPdfAvailable).catch(() => {
2000
- });
2001
- }, []);
2002
- const withRows = async (write3) => {
2003
- if (busy) return;
2004
- setBusy(true);
2005
- try {
2006
- const rows = fetchAll ? await fetchAll().catch(() => data) : data;
2007
- await write3(rows.length ? rows : data);
2008
- } finally {
2009
- setBusy(false);
2010
- }
2011
- };
2012
- const handleCsv = () => withRows((rows) => exportToCsv(columns, rows, filename, currency));
2013
- const handleExcel = () => withRows(async (rows) => {
2014
- const { exportToExcel: exportToExcel2 } = await import('./excel-NKWJ2AMC.js');
2015
- await exportToExcel2(columns, rows, filename, currency);
2016
- });
2017
- const handlePdf = () => withRows(async (rows) => {
2018
- const { exportToPdf: exportToPdf2 } = await import('./pdf-H73MJGB6.js');
2019
- await exportToPdf2(columns, rows, filename, { title, currency });
2020
- });
2021
- return /* @__PURE__ */ jsxs(Dropdown, { children: [
2022
- /* @__PURE__ */ jsx(DropdownTrigger, { asChild: true, children: /* @__PURE__ */ jsx(ToolbarAction, { icon: Download, caret: true, disabled: busy, children: t("reports.export") }) }),
2023
- /* @__PURE__ */ jsxs(DropdownContent, { align: "end", children: [
2024
- /* @__PURE__ */ jsx(DropdownItem, { onClick: handleCsv, children: t("reports.exportCsv") }),
2025
- excelAvailable && /* @__PURE__ */ jsx(DropdownItem, { onClick: handleExcel, children: t("reports.exportExcel") }),
2026
- pdfAvailable && /* @__PURE__ */ jsx(DropdownItem, { onClick: handlePdf, children: t("reports.exportPdf") })
2027
- ] })
2028
- ] });
2029
- }
2030
- function ReportPagination({ page: page2, pageSize, total, onPageChange, onPageSizeChange }) {
2031
- const t = useTranslation();
2032
- const totalPages = Math.max(1, Math.ceil(total / pageSize));
2033
- const from = (page2 - 1) * pageSize + 1;
2034
- const to = Math.min(page2 * pageSize, total);
2035
- return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-t px-4 py-3", children: [
2036
- /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: total > 0 ? t("reports.pagination.range", { from, to, total }) : t("reports.pagination.empty") }),
2037
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
2038
- /* @__PURE__ */ jsxs(Select, { value: String(pageSize), onValueChange: (v) => onPageSizeChange(Number(v)), children: [
2039
- /* @__PURE__ */ jsx(SelectTrigger, { className: "h-8 w-auto gap-2 bg-card px-2 shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)]", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
2040
- /* @__PURE__ */ jsx(SelectContent, { children: [25, 50, 100].map((size) => /* @__PURE__ */ jsx(SelectItem, { value: String(size), children: t("reports.pagination.perPage", { size }) }, size)) })
2041
- ] }),
2042
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
2043
- /* @__PURE__ */ jsx(
2044
- Button,
2045
- {
2046
- variant: "outline",
2047
- size: "sm",
2048
- disabled: page2 <= 1,
2049
- onClick: () => onPageChange(page2 - 1),
2050
- children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" })
2051
- }
2052
- ),
2053
- /* @__PURE__ */ jsxs("span", { className: "text-sm px-2", children: [
2054
- page2,
2055
- " / ",
2056
- totalPages
2057
- ] }),
2058
- /* @__PURE__ */ jsx(
2059
- Button,
2060
- {
2061
- variant: "outline",
2062
- size: "sm",
2063
- disabled: page2 >= totalPages,
2064
- onClick: () => onPageChange(page2 + 1),
2065
- children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" })
2066
- }
2067
- )
2068
- ] })
2069
- ] })
2070
- ] });
2071
- }
2072
- function ViewerBody({ card }) {
2073
- const { labels, currency, defaultRange } = useAnalyticsConfig();
2074
- const { state, setPage, setPageSize, setPreset } = useAnalyticsContext();
2075
- const cardDefaultRange = card.defaultRange;
2076
- React10__default.useEffect(() => {
2077
- if (!cardDefaultRange) return;
2078
- setPreset(cardDefaultRange);
2079
- return () => setPreset(defaultRange);
2080
- }, [card.id, cardDefaultRange, defaultRange, setPreset]);
2081
- const isStatement = card.viz.type === "statement" && !!card.viz.statement;
2082
- const { result, loading, error, engineMissing } = useAnalyticsCard(card, { enabled: !isStatement });
2083
- const all = useAnalyticsAllRows(card, isStatement && card.available !== false);
2084
- const columns = React10__default.useMemo(() => cardColumns(card), [card]);
2085
- const expanded = React10__default.useMemo(
2086
- () => expandBagColumns(columns, result?.rows ?? []),
2087
- [columns, result?.rows]
2088
- );
2089
- const tanstackColumns = React10__default.useMemo(
2090
- () => reportColumnsToTanstack(expanded.columns, currency.code),
2091
- [expanded.columns, currency.code]
2092
- );
2093
- const rows = expanded.rows;
2094
- if (card.available === false) {
2095
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center rounded-xl border border-dashed py-16 text-center", children: [
2096
- /* @__PURE__ */ jsx("div", { className: "mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: /* @__PURE__ */ jsx(Construction, { className: "h-5 w-5 text-muted-foreground" }) }),
2097
- /* @__PURE__ */ jsx("p", { className: "font-medium", children: labels.unavailableTitle }),
2098
- /* @__PURE__ */ jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: labels.unavailableDescription })
2099
- ] });
2100
- }
2101
- const hasViz = card.viz.type !== "table" || (card.query.measures?.length ?? 0) > 0;
2102
- if (isStatement && card.viz.statement) {
2103
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2104
- engineMissing ? /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-muted/40 p-3 text-sm", children: [
2105
- /* @__PURE__ */ jsx("p", { className: "font-medium", children: "Analytics engine not installed" }),
2106
- /* @__PURE__ */ jsxs("p", { className: "mt-0.5 text-muted-foreground", children: [
2107
- "This report aggregates server-side and needs migration 0039 (",
2108
- /* @__PURE__ */ jsx("code", { className: "text-xs", children: "analytics_run" }),
2109
- " + the read models) applied to this project."
2110
- ] })
2111
- ] }) : error ?? all.error ? /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive", children: error ?? all.error }) : null,
2112
- /* @__PURE__ */ jsx(
2113
- StatementTable,
2114
- {
2115
- rows: all.rows,
2116
- def: card.viz.statement,
2117
- currency,
2118
- formatPeriod: (p) => formatBucket(p, "month"),
2119
- emptyMessage: labels.noResults,
2120
- loading: all.loading
2121
- }
2122
- )
2123
- ] });
2124
- }
2125
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2126
- hasViz ? /* @__PURE__ */ jsx(
2127
- AnalyticsCard,
2128
- {
2129
- card: { ...card, viz: { ...card.viz, height: card.viz.height ?? 280, limit: void 0 } },
2130
- hideOpen: true
2131
- }
2132
- ) : null,
2133
- engineMissing ? /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-muted/40 p-3 text-sm", children: [
2134
- /* @__PURE__ */ jsx("p", { className: "font-medium", children: "Analytics engine not installed" }),
2135
- /* @__PURE__ */ jsxs("p", { className: "mt-0.5 text-muted-foreground", children: [
2136
- "This report aggregates server-side and needs migration 0039 (",
2137
- /* @__PURE__ */ jsx("code", { className: "text-xs", children: "analytics_run" }),
2138
- " + the read models) applied to this project."
2139
- ] })
2140
- ] }) : error ? /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive", children: error }) : null,
2141
- /* @__PURE__ */ jsx("div", { className: "print-flow", children: /* @__PURE__ */ jsx(
2142
- DataTable,
2143
- {
2144
- columns: tanstackColumns,
2145
- data: rows,
2146
- loading,
2147
- skeletonRows: 5,
2148
- emptyMessage: labels.noResults,
2149
- variant: "card",
2150
- compact: true
2151
- }
2152
- ) }),
2153
- !loading ? /* @__PURE__ */ jsx("div", { className: "no-print", children: /* @__PURE__ */ jsx(
2154
- ReportPagination,
2155
- {
2156
- page: state.page,
2157
- pageSize: state.pageSize,
2158
- total: result?.total ?? 0,
2159
- onPageChange: setPage,
2160
- onPageSizeChange: setPageSize
2161
- }
2162
- ) }) : null
2163
- ] });
2164
- }
2165
- function AnalyticsReportViewer({ cardId }) {
2166
- const { labels } = useAnalyticsConfig();
2167
- const cards = useAnalyticsCards();
2168
- const card = cards.find((c) => c.id === decodeURIComponent(cardId ?? ""));
2169
- React10__default.useEffect(() => {
2170
- if (card) markViewed(card.id);
2171
- }, [card?.id]);
2172
- if (!card) {
2173
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2174
- /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigateAnalytics(REPORTS_BASE), children: [
2175
- /* @__PURE__ */ jsx(ArrowLeft, { className: "mr-1.5 h-3.5 w-3.5" }),
2176
- labels.reports
2177
- ] }),
2178
- /* @__PURE__ */ jsx("p", { className: "py-12 text-center text-sm text-muted-foreground", children: labels.noResults })
2179
- ] });
2180
- }
2181
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2182
- /* @__PURE__ */ jsx(
2183
- SubpageHeader,
2184
- {
2185
- title: card.name,
2186
- description: card.description,
2187
- parentLabel: labels.analytics,
2188
- onBack: () => navigateAnalytics(ANALYTICS_BASE)
2189
- }
2190
- ),
2191
- /* @__PURE__ */ jsx(
2192
- AnalyticsControls,
2193
- {
2194
- filters: card.query.filters ?? [],
2195
- showCompare: card.compare !== false,
2196
- className: "no-print",
2197
- actions: /* @__PURE__ */ jsx(ViewerActions, { card })
2198
- }
2199
- ),
2200
- /* @__PURE__ */ jsx(ViewerBody, { card })
2201
- ] });
2202
- }
2203
- function ViewerActions({ card }) {
2204
- const { currency, labels } = useAnalyticsConfig();
2205
- const closePanel = useToolbarPanelClose();
2206
- const print = React10__default.useCallback(() => {
2207
- closePanel();
2208
- requestAnimationFrame(() => {
2209
- requestAnimationFrame(() => window.setTimeout(() => window.print(), 120));
2210
- });
2211
- }, [closePanel]);
2212
- const { result } = useAnalyticsCard(card);
2213
- const columns = React10__default.useMemo(() => cardColumns(card), [card]);
2214
- const fetchAll = useAnalyticsCardRows(card);
2215
- if (!result?.rows?.length) return null;
2216
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2217
- /* @__PURE__ */ jsx(
2218
- ExportDropdown,
2219
- {
2220
- columns,
2221
- data: result.rows,
2222
- fetchAll,
2223
- filename: card.id,
2224
- currency: currency.code,
2225
- title: card.name
2226
- }
2227
- ),
2228
- /* @__PURE__ */ jsx(ToolbarAction, { icon: Printer, onClick: print, children: labels.print })
2229
- ] });
2230
- }
2231
- function LiveViewPage() {
2232
- const { labels } = useAnalyticsConfig();
2233
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center rounded-xl border border-dashed py-20 text-center", children: [
2234
- /* @__PURE__ */ jsx("div", { className: "mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: /* @__PURE__ */ jsx(Radio, { className: "h-5 w-5 text-muted-foreground" }) }),
2235
- /* @__PURE__ */ jsx("p", { className: "font-medium", children: labels.liveView }),
2236
- /* @__PURE__ */ jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: labels.liveViewSoon })
2237
- ] });
2238
- }
2239
- var VIZ_TYPES = ["kpi", "line", "bar", "area", "donut", "breakdown", "table"];
2240
- function vizOptionsFor(card) {
2241
- const hasDimension = (card.query.dimensions ?? []).some((d) => !d.hidden);
2242
- return hasDimension ? VIZ_TYPES : ["kpi", "table"];
2243
- }
2244
- function CardIcon({ name, className }) {
2245
- const Icon = getLucideIcon(name ?? "FileText") ?? getLucideIcon("FileText");
2246
- return Icon ? /* @__PURE__ */ jsx(Icon, { className }) : null;
2247
- }
2248
- function ReportRow({ card, custom, onEdit, children, drag }) {
2249
- const t = useTranslation();
2250
- return /* @__PURE__ */ jsxs(
2251
- "li",
2252
- {
2253
- draggable: Boolean(drag),
2254
- onDragStart: drag?.onDragStart,
2255
- onDragOver: drag?.onDragOver,
2256
- onDragEnd: drag?.onDragEnd,
2257
- className: [
2258
- "flex items-center gap-2 border-b border-border/60 px-3 py-2 last:border-0",
2259
- drag ? "cursor-grab active:cursor-grabbing" : "",
2260
- drag?.dragging ? "opacity-40" : ""
2261
- ].join(" "),
2262
- children: [
2263
- drag ? /* @__PURE__ */ jsx(GripVertical, { className: "h-4 w-4 shrink-0 text-muted-foreground/50", "aria-hidden": true }) : /* @__PURE__ */ jsx("span", { className: "w-4 shrink-0", "aria-hidden": true }),
2264
- /* @__PURE__ */ jsx(CardIcon, { name: card.icon, className: "h-4 w-4 shrink-0 text-muted-foreground" }),
2265
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
2266
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2267
- /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-medium", children: card.name }),
2268
- custom ? /* @__PURE__ */ jsx(Badge, { variant: "secondary", children: t("reports.settings.custom") }) : null
2269
- ] }),
2270
- /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: card.category })
2271
- ] }),
2272
- /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [
2273
- children,
2274
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: onEdit, "aria-label": t("reports.settings.edit"), title: t("reports.settings.edit"), children: /* @__PURE__ */ jsx(SlidersHorizontal, { className: "h-3.5 w-3.5" }) })
2275
- ] })
2276
- ]
2277
- }
2278
- );
2279
- }
2280
- function CardEditor({ card, onClose }) {
2281
- const t = useTranslation();
2282
- const prefs = useAnalyticsPrefs();
2283
- const pref = prefs.cards[card.id] ?? {};
2284
- const isCustom = prefs.custom.some((r) => r.id === card.id);
2285
- const [name, setName] = React10__default.useState(card.name);
2286
- const measures = card.query.measures ?? [];
2287
- const dimensions = (card.query.dimensions ?? []).filter((d) => !d.hidden);
2288
- const keptMeasures = new Set(pref.measures ?? measures.map((m) => m.key));
2289
- const keptDimensions = new Set(pref.dimensions ?? dimensions.map((d) => d.key));
2290
- const toggle = (set, key, field, all) => {
2291
- const next = new Set(set);
2292
- if (next.has(key)) next.delete(key);
2293
- else next.add(key);
2294
- setCardPref(card.id, { [field]: next.size === all.length ? void 0 : [...next] });
2295
- };
2296
- return (
2297
- // A coluna da direita, não uma gaveta só desta tela: ajustar um relatório é
2298
- // olhar a lista enquanto se mexe nele, e uma gaveta cobre exatamente a
2299
- // lista. É a mesma coluna do assistente — RightRailPage cai numa Sheet
2300
- // sozinha quando a shell não tem coluna nenhuma.
2301
- /* @__PURE__ */ jsxs(RightRailPage, { title: card.name, open: true, onClose, width: "max-w-md", children: [
2302
- /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-4", children: /* @__PURE__ */ jsxs("div", { className: "space-y-5", children: [
2303
- card.description ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: card.description }) : null,
2304
- /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
2305
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("reports.settings.name") }),
2306
- /* @__PURE__ */ jsx(
2307
- Input,
2308
- {
2309
- value: name,
2310
- onChange: (e) => setName(e.target.value),
2311
- onBlur: () => setCardPref(card.id, { name: name.trim() && name !== card.name ? name.trim() : void 0 })
2312
- }
2313
- )
2314
- ] }),
2315
- /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
2316
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("reports.settings.viz") }),
2317
- /* @__PURE__ */ jsxs(
2318
- Select,
2319
- {
2320
- value: card.viz.type,
2321
- onValueChange: (value) => setCardPref(card.id, { viz: value }),
2322
- children: [
2323
- /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
2324
- /* @__PURE__ */ jsx(SelectContent, { children: vizOptionsFor(card).map((viz) => /* @__PURE__ */ jsx(SelectItem, { value: viz, children: t(`reports.viz.${viz}`) }, viz)) })
2325
- ]
2326
- }
2327
- )
2328
- ] }),
2329
- dimensions.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2330
- /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground", children: t("reports.settings.dimensions") }),
2331
- dimensions.map((d) => /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-2 text-sm", children: [
2332
- /* @__PURE__ */ jsx(
2333
- Checkbox,
2334
- {
2335
- checked: keptDimensions.has(d.key),
2336
- onChange: () => toggle(keptDimensions, d.key, "dimensions", dimensions.map((x) => x.key))
2337
- }
2338
- ),
2339
- d.label
2340
- ] }, d.key))
2341
- ] }) : null,
2342
- measures.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2343
- /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground", children: t("reports.settings.measures") }),
2344
- measures.map((m) => /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-2 text-sm", children: [
2345
- /* @__PURE__ */ jsx(
2346
- Checkbox,
2347
- {
2348
- checked: keptMeasures.has(m.key),
2349
- onChange: () => toggle(keptMeasures, m.key, "measures", measures.map((x) => x.key))
2350
- }
2351
- ),
2352
- m.label
2353
- ] }, m.key))
2354
- ] }) : null
2355
- ] }) }),
2356
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-t border-border p-3", children: [
2357
- isCustom ? /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => {
2358
- deleteCustomReport(card.id);
2359
- onClose();
2360
- }, children: [
2361
- /* @__PURE__ */ jsx(Trash2, { className: "mr-1.5 h-3.5 w-3.5" }),
2362
- t("reports.settings.delete")
2363
- ] }) : /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => {
2364
- clearCardPref(card.id);
2365
- onClose();
2366
- }, children: [
2367
- /* @__PURE__ */ jsx(RotateCcw, { className: "mr-1.5 h-3.5 w-3.5" }),
2368
- t("reports.settings.resetCard")
2369
- ] }),
2370
- /* @__PURE__ */ jsx(Button, { size: "sm", onClick: onClose, children: t("reports.settings.done") })
2371
- ] })
2372
- ] })
2373
- );
2374
- }
2375
- function slugify(value) {
2376
- return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "report";
2377
- }
2378
- function buildCustomReport(input) {
2379
- const dimensionKeys = new Set(input.source.dimensions.map((d) => d.key));
2380
- const measureKeys = new Set(input.source.measures.map((m) => m.key));
2381
- return {
2382
- id: `custom.${slugify(input.name)}-${Date.now().toString(36)}`,
2383
- name: input.name.trim(),
2384
- category: input.category.trim() || "Geral",
2385
- icon: "FileText",
2386
- createdBy: input.createdBy,
2387
- baseId: input.source.base.id,
2388
- // Filtered, not trusted: a field the source does not have would resolve to
2389
- // nothing and the report would open empty with no explanation.
2390
- dimensions: input.dimensions.filter((k) => dimensionKeys.has(k)),
2391
- measures: input.measures.filter((k) => measureKeys.has(k)),
2392
- viz: input.viz,
2393
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
2394
- };
2395
- }
2396
- function NewReportPanel({
2397
- sources,
2398
- categories,
2399
- onClose
2400
- }) {
2401
- const t = useTranslation();
2402
- const { createdBy } = useAnalyticsConfig();
2403
- const [name, setName] = React10__default.useState("");
2404
- const [category, setCategory] = React10__default.useState(categories[0] ?? "Geral");
2405
- const [sourceKeyState, setSourceKey] = React10__default.useState(sources[0]?.key ?? "");
2406
- const [dimensions, setDimensions] = React10__default.useState([]);
2407
- const [measures, setMeasures] = React10__default.useState([]);
2408
- const [viz, setViz] = React10__default.useState("table");
2409
- const source = sources.find((s) => s.key === sourceKeyState);
2410
- React10__default.useEffect(() => {
2411
- setDimensions([]);
2412
- setMeasures([]);
2413
- }, [sourceKeyState]);
2414
- const toggle = (list, set, key) => {
2415
- set(list.includes(key) ? list.filter((k) => k !== key) : [...list, key]);
2416
- };
2417
- const error = !name.trim() ? t("reports.settings.needName") : measures.length === 0 ? t("reports.settings.needMeasure") : null;
2418
- const submit = () => {
2419
- if (error || !source) return;
2420
- saveCustomReport(buildCustomReport({
2421
- name,
2422
- category,
2423
- createdBy,
2424
- source,
2425
- dimensions,
2426
- measures,
2427
- viz
2428
- }));
2429
- onClose();
2430
- };
2431
- return /* @__PURE__ */ jsxs(RightRailPage, { title: t("reports.settings.new"), open: true, onClose, width: "max-w-md", children: [
2432
- /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-4", children: /* @__PURE__ */ jsxs("div", { className: "space-y-5", children: [
2433
- /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
2434
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("reports.settings.name") }),
2435
- /* @__PURE__ */ jsx(Input, { value: name, onChange: (e) => setName(e.target.value) })
2436
- ] }),
2437
- /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
2438
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("reports.settings.category") }),
2439
- /* @__PURE__ */ jsx(Input, { value: category, onChange: (e) => setCategory(e.target.value), list: "fayz-analytics-categories" }),
2440
- /* @__PURE__ */ jsx("datalist", { id: "fayz-analytics-categories", children: categories.map((c) => /* @__PURE__ */ jsx("option", { value: c }, c)) })
2441
- ] }),
2442
- /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
2443
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("reports.settings.source") }),
2444
- /* @__PURE__ */ jsxs(Select, { value: sourceKeyState, onValueChange: setSourceKey, children: [
2445
- /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
2446
- /* @__PURE__ */ jsx(SelectContent, { children: sources.map((s) => /* @__PURE__ */ jsx(SelectItem, { value: s.key, children: s.label }, s.key)) })
2447
- ] }),
2448
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("reports.settings.sourceHint") })
2449
- ] }),
2450
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2451
- /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground", children: t("reports.settings.dimensions") }),
2452
- (source?.dimensions ?? []).length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "\u2014" }) : source.dimensions.map((d) => /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-2 text-sm", children: [
2453
- /* @__PURE__ */ jsx(Checkbox, { checked: dimensions.includes(d.key), onChange: () => toggle(dimensions, setDimensions, d.key) }),
2454
- d.label
2455
- ] }, d.key))
2456
- ] }),
2457
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2458
- /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground", children: t("reports.settings.measures") }),
2459
- (source?.measures ?? []).map((m) => /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-2 text-sm", children: [
2460
- /* @__PURE__ */ jsx(Checkbox, { checked: measures.includes(m.key), onChange: () => toggle(measures, setMeasures, m.key) }),
2461
- m.label
2462
- ] }, m.key))
2463
- ] }),
2464
- /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
2465
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("reports.settings.viz") }),
2466
- /* @__PURE__ */ jsxs(Select, { value: viz, onValueChange: (value) => setViz(value), children: [
2467
- /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
2468
- /* @__PURE__ */ jsx(SelectContent, { children: (dimensions.length > 0 ? VIZ_TYPES : ["kpi", "table"]).map((v) => /* @__PURE__ */ jsx(SelectItem, { value: v, children: t(`reports.viz.${v}`) }, v)) })
2469
- ] })
2470
- ] })
2471
- ] }) }),
2472
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 border-t border-border p-3", children: [
2473
- /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-destructive", children: error }),
2474
- /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
2475
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: onClose, children: t("reports.settings.cancel") }),
2476
- /* @__PURE__ */ jsx(Button, { size: "sm", onClick: submit, disabled: Boolean(error), children: t("reports.settings.save") })
2477
- ] })
2478
- ] })
2479
- ] });
2480
- }
2481
- var CREATE_REPORT_TOOL = {
2482
- id: "reports.createAnalyticsReport",
2483
- name: "createAnalyticsReport",
2484
- description: "Creates a custom analytics report for this user. Pick ONE source from the `sources` list in the surface state, then group_by and measures USING ONLY that source's field keys \u2014 a key from another source resolves to nothing and the report opens empty. Understand the intent and propose the whole report in one call instead of asking field by field. Rules of thumb: one time dimension + one measure reads best as a line; a category dimension with one measure as bar or breakdown; three or more measures as a table; no dimension at all as a kpi.",
2485
- icon: "FileText",
2486
- mode: "persist",
2487
- execution: { plane: "client" },
2488
- category: "Analytics",
2489
- suggestions: [
2490
- { label: "Um relat\xF3rio de receita por mesa" },
2491
- { label: "Quanto cada pra\xE7a produz por turno" }
2492
- ],
2493
- parameters: {
2494
- type: "object",
2495
- properties: {
2496
- name: { type: "string", description: "Report name, in the user's language." },
2497
- source: { type: "string", description: "A `key` from the surface state's `sources`." },
2498
- group_by: {
2499
- type: "array",
2500
- description: "Dimension keys from that source. Empty means a single headline number.",
2501
- items: { type: "string" }
2502
- },
2503
- measures: {
2504
- type: "array",
2505
- description: "Measure keys from that source. At least one.",
2506
- items: { type: "string" }
2507
- },
2508
- viz: {
2509
- type: "string",
2510
- enum: ["kpi", "line", "bar", "area", "donut", "breakdown", "table"],
2511
- description: "How to draw it. Defaults to table."
2512
- },
2513
- category: { type: "string", description: "Group it sits under in the rail. Reuse an existing one when it fits." },
2514
- pin: { type: "boolean", description: "Also place it on the overview grid." }
2515
- },
2516
- required: ["name", "source", "measures"]
2517
- }
2518
- };
2519
- var AGENT_INSTRUCTIONS = "The user is on the report configuration screen. `sources` in the surface state lists every data source with the exact dimension and measure keys askable of it \u2014 use those keys verbatim. When they describe a report, call createAnalyticsReport once with the full shape; do not interview them field by field. Name the report the way they said it. After creating, say in one line what it groups by and what it measures.";
2520
- function AnalyticsSettingsPage() {
2521
- const t = useTranslation();
2522
- const { labels } = useAnalyticsConfig();
2523
- const catalogue = useAnalyticsCatalogue();
2524
- const cards = useAnalyticsCards();
2525
- const prefs = useAnalyticsPrefs();
2526
- const { createdBy } = useAnalyticsConfig();
2527
- const [editing, setEditing] = React10__default.useState(null);
2528
- const [creating, setCreating] = React10__default.useState(false);
2529
- const [dragId, setDragId] = React10__default.useState(null);
2530
- const askIntro = React10__default.useMemo(() => ({
2531
- role: "setup",
2532
- persona: t("reports.settings.askPersona"),
2533
- pitch: t("reports.settings.askPitch"),
2534
- description: t("reports.settings.askDescription"),
2535
- suggestions: [0, 1, 2].map((i) => ({ label: t(`reports.settings.askSuggestion.${i}`) }))
2536
- }), [t]);
2537
- const createActions = React10__default.useMemo(() => [
2538
- {
2539
- id: "with-ai",
2540
- label: t("assistant.askAI"),
2541
- description: t("reports.settings.newWithAIHint"),
2542
- icon: "WandSparkles",
2543
- highlight: true,
2544
- action: () => askAI({ hint: t("reports.settings.askPlaceholder"), intro: askIntro })
2545
- },
2546
- {
2547
- id: "by-hand",
2548
- label: t("reports.settings.newByHand"),
2549
- description: t("reports.settings.newByHandHint"),
2550
- icon: "SlidersHorizontal",
2551
- action: () => setCreating(true)
2552
- }
2553
- ], [t, askIntro]);
2554
- const sources = React10__default.useMemo(() => analyticsSources(cards), [cards]);
2555
- const categories = React10__default.useMemo(
2556
- () => [...new Set(cards.map((c) => c.category))].sort((a, b) => a.localeCompare(b)),
2557
- [cards]
2558
- );
2559
- const pinned = React10__default.useMemo(
2560
- () => cards.filter((c) => c.dashboard).sort((a, b) => (a.dashboard?.order ?? 0) - (b.dashboard?.order ?? 0)),
2561
- [cards]
2562
- );
2563
- const unpinned = React10__default.useMemo(
2564
- () => cards.filter((c) => !c.dashboard).sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name)),
2565
- [cards]
2566
- );
2567
- const removed = React10__default.useMemo(
2568
- () => catalogue.filter((c) => prefs.cards[c.id]?.hidden),
2569
- [catalogue, prefs]
2570
- );
2571
- const [order, setOrder] = React10__default.useState(null);
2572
- const pinnedIds = order ?? pinned.map((c) => c.id);
2573
- const pinnedInOrder = pinnedIds.map((id) => pinned.find((c) => c.id === id)).filter((c) => Boolean(c));
2574
- const move = (id, delta) => {
2575
- const ids = pinned.map((c) => c.id);
2576
- const from = ids.indexOf(id);
2577
- const to = from + delta;
2578
- if (from < 0 || to < 0 || to >= ids.length) return;
2579
- ids.splice(to, 0, ids.splice(from, 1)[0]);
2580
- setOverviewOrder(ids);
2581
- };
2582
- const onDragOver = (targetId) => (event) => {
2583
- event.preventDefault();
2584
- if (!dragId || dragId === targetId) return;
2585
- const ids = [...pinnedIds];
2586
- const from = ids.indexOf(dragId);
2587
- const to = ids.indexOf(targetId);
2588
- if (from < 0 || to < 0) return;
2589
- ids.splice(to, 0, ids.splice(from, 1)[0]);
2590
- setOrder(ids);
2591
- };
2592
- const commitDrag = () => {
2593
- if (order) setOverviewOrder(order);
2594
- setOrder(null);
2595
- setDragId(null);
2596
- };
2597
- const editingCard = cards.find((c) => c.id === editing);
2598
- const isCustom = (id) => prefs.custom.some((r) => r.id === id);
2599
- const surfaceRef = React10__default.useRef({ sources, categories, cards, prefs, createdBy });
2600
- surfaceRef.current = { sources, categories, cards, prefs, createdBy };
2601
- const agentSurface = React10__default.useMemo(() => ({
2602
- id: "analytics.reports",
2603
- pluginId: "reports",
2604
- instructions: AGENT_INSTRUCTIONS,
2605
- describe: () => {
2606
- const o = surfaceRef.current;
2607
- return {
2608
- title: "Configura\xE7\xE3o de relat\xF3rios",
2609
- state: {
2610
- sources: o.sources.map((source) => ({
2611
- key: source.key,
2612
- label: source.label,
2613
- grain: source.base.grain ?? null,
2614
- group_by: source.dimensions.map((d) => ({ key: d.key, label: d.label })),
2615
- measures: source.measures.map((m) => ({ key: m.key, label: m.label, format: m.format ?? "number" }))
2616
- })),
2617
- categories: o.categories,
2618
- onOverview: o.cards.filter((c) => c.dashboard).map((c) => c.name),
2619
- yourReports: o.prefs.custom.map((r) => r.name)
2620
- }
2621
- };
2622
- },
2623
- tools: [{
2624
- tool: CREATE_REPORT_TOOL,
2625
- execute: (args) => {
2626
- const o = surfaceRef.current;
2627
- const source = o.sources.find((s) => s.key === args.source) ?? o.sources.find((s) => s.label === args.source);
2628
- if (!source) {
2629
- return { ok: false, error: "unknown_source", sources: o.sources.map((s) => s.key) };
2630
- }
2631
- const report = buildCustomReport({
2632
- name: String(args.name ?? "").trim() || "Relat\xF3rio",
2633
- category: String(args.category ?? o.categories[0] ?? "Geral"),
2634
- createdBy: o.createdBy,
2635
- source,
2636
- dimensions: Array.isArray(args.group_by) ? args.group_by : [],
2637
- measures: Array.isArray(args.measures) ? args.measures : [],
2638
- viz: args.viz ?? "table"
2639
- });
2640
- if (report.measures.length === 0) {
2641
- return {
2642
- ok: false,
2643
- error: "no_valid_measure",
2644
- available: source.measures.map((m) => m.key)
2645
- };
2646
- }
2647
- saveCustomReport(report);
2648
- if (args.pin) setCardPref(report.id, { pinned: true, order: 999 });
2649
- navigateAnalytics(`${REPORTS_BASE}/${encodeURIComponent(report.id)}`);
2650
- return {
2651
- ok: true,
2652
- id: report.id,
2653
- name: report.name,
2654
- group_by: report.dimensions,
2655
- measures: report.measures,
2656
- viz: report.viz
2657
- };
2658
- }
2659
- }]
2660
- }), []);
2661
- useAgentSurface(agentSurface);
2662
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2663
- /* @__PURE__ */ jsx(
2664
- SubpageHeader,
2665
- {
2666
- title: t("reports.settings.title"),
2667
- parentLabel: labels.analytics,
2668
- onBack: () => navigateAnalytics(ANALYTICS_BASE)
2669
- }
2670
- ),
2671
- /* @__PURE__ */ jsxs(PageHeaderActions, { children: [
2672
- /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => resetAnalyticsPrefs(), children: [
2673
- /* @__PURE__ */ jsx(RotateCcw, { className: "mr-1.5 h-3.5 w-3.5" }),
2674
- t("reports.settings.reset")
2675
- ] }),
2676
- /* @__PURE__ */ jsx(QuickActionsButton, { label: t("reports.settings.new"), variant: "default", actions: createActions })
2677
- ] }),
2678
- /* @__PURE__ */ jsx("p", { className: "max-w-2xl text-sm text-muted-foreground", children: t("reports.settings.subtitle") }),
2679
- /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
2680
- /* @__PURE__ */ jsxs("div", { className: "border-b border-border px-3 py-2", children: [
2681
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t("reports.settings.onOverview") }),
2682
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("reports.settings.onOverviewHint") })
2683
- ] }),
2684
- pinnedInOrder.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-3 py-6 text-center text-sm text-muted-foreground", children: t("reports.settings.empty") }) : /* @__PURE__ */ jsx("ul", { children: pinnedInOrder.map((card, index) => /* @__PURE__ */ jsxs(
2685
- ReportRow,
2686
- {
2687
- card,
2688
- custom: isCustom(card.id),
2689
- onEdit: () => setEditing(card.id),
2690
- drag: {
2691
- dragging: dragId === card.id,
2692
- onDragStart: () => setDragId(card.id),
2693
- onDragOver: onDragOver(card.id),
2694
- onDragEnd: commitDrag
2695
- },
2696
- children: [
2697
- /* @__PURE__ */ jsxs(
2698
- Select,
2699
- {
2700
- value: String(card.dashboard?.span ?? ""),
2701
- onValueChange: (value) => setCardPref(card.id, { span: Number(value) }),
2702
- children: [
2703
- /* @__PURE__ */ jsx(SelectTrigger, { className: "h-8 w-[150px]", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("reports.settings.width") }) }),
2704
- /* @__PURE__ */ jsx(SelectContent, { children: [1, 2, 3, 4].map((span) => /* @__PURE__ */ jsx(SelectItem, { value: String(span), children: t(`reports.settings.width.${span}`) }, span)) })
2705
- ]
2706
- }
2707
- ),
2708
- /* @__PURE__ */ jsx(
2709
- Button,
2710
- {
2711
- variant: "ghost",
2712
- size: "sm",
2713
- disabled: index === 0,
2714
- onClick: () => move(card.id, -1),
2715
- "aria-label": t("reports.settings.up"),
2716
- title: t("reports.settings.up"),
2717
- children: /* @__PURE__ */ jsx(ChevronUp, { className: "h-3.5 w-3.5" })
2718
- }
2719
- ),
2720
- /* @__PURE__ */ jsx(
2721
- Button,
2722
- {
2723
- variant: "ghost",
2724
- size: "sm",
2725
- disabled: index === pinnedInOrder.length - 1,
2726
- onClick: () => move(card.id, 1),
2727
- "aria-label": t("reports.settings.down"),
2728
- title: t("reports.settings.down"),
2729
- children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-3.5 w-3.5" })
2730
- }
2731
- ),
2732
- /* @__PURE__ */ jsx(
2733
- Button,
2734
- {
2735
- variant: "ghost",
2736
- size: "sm",
2737
- onClick: () => setCardPref(card.id, { pinned: false }),
2738
- "aria-label": t("reports.settings.unpin"),
2739
- title: t("reports.settings.unpin"),
2740
- children: /* @__PURE__ */ jsx(EyeOff, { className: "h-3.5 w-3.5" })
2741
- }
2742
- )
2743
- ]
2744
- },
2745
- card.id
2746
- )) })
2747
- ] }),
2748
- /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
2749
- /* @__PURE__ */ jsxs("div", { className: "border-b border-border px-3 py-2", children: [
2750
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t("reports.settings.offOverview") }),
2751
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("reports.settings.offOverviewHint") })
2752
- ] }),
2753
- /* @__PURE__ */ jsx("ul", { children: unpinned.map((card) => /* @__PURE__ */ jsxs(ReportRow, { card, custom: isCustom(card.id), onEdit: () => setEditing(card.id), children: [
2754
- /* @__PURE__ */ jsx(
2755
- Button,
2756
- {
2757
- variant: "ghost",
2758
- size: "sm",
2759
- onClick: () => setCardPref(card.id, { pinned: true, order: pinned.length }),
2760
- "aria-label": t("reports.settings.pin"),
2761
- title: t("reports.settings.pin"),
2762
- children: /* @__PURE__ */ jsx(Eye, { className: "h-3.5 w-3.5" })
2763
- }
2764
- ),
2765
- isCustom(card.id) ? /* @__PURE__ */ jsx(
2766
- Button,
2767
- {
2768
- variant: "ghost",
2769
- size: "sm",
2770
- onClick: () => deleteCustomReport(card.id),
2771
- "aria-label": t("reports.settings.delete"),
2772
- title: t("reports.settings.delete"),
2773
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
2774
- }
2775
- ) : /* @__PURE__ */ jsx(
2776
- Button,
2777
- {
2778
- variant: "ghost",
2779
- size: "sm",
2780
- onClick: () => setCardPref(card.id, { hidden: true }),
2781
- "aria-label": t("reports.settings.remove"),
2782
- title: t("reports.settings.remove"),
2783
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
2784
- }
2785
- )
2786
- ] }, card.id)) })
2787
- ] }),
2788
- removed.length > 0 ? /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
2789
- /* @__PURE__ */ jsx("div", { className: "border-b border-border px-3 py-2", children: /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t("reports.settings.hidden") }) }),
2790
- /* @__PURE__ */ jsx("ul", { children: removed.map((card) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-2 border-b border-border/60 px-3 py-2 last:border-0", children: [
2791
- /* @__PURE__ */ jsx("span", { className: "w-4 shrink-0", "aria-hidden": true }),
2792
- /* @__PURE__ */ jsx(CardIcon, { name: card.icon, className: "h-4 w-4 shrink-0 text-muted-foreground" }),
2793
- /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx("span", { className: "truncate text-sm text-muted-foreground", children: card.name }) }),
2794
- /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setCardPref(card.id, { hidden: void 0 }), children: [
2795
- /* @__PURE__ */ jsx(RotateCcw, { className: "mr-1.5 h-3.5 w-3.5" }),
2796
- t("reports.settings.restore")
2797
- ] })
2798
- ] }, card.id)) })
2799
- ] }) : null,
2800
- editingCard ? /* @__PURE__ */ jsx(CardEditor, { card: editingCard, onClose: () => setEditing(null) }) : null,
2801
- creating ? /* @__PURE__ */ jsx(NewReportPanel, { sources, categories, onClose: () => setCreating(false) }) : null
2802
- ] });
2803
- }
2804
737
  function db() {
2805
738
  const client = getSupabaseClientOptional();
2806
739
  if (!client) throw new Error("Supabase client is not configured");
@@ -2961,6 +894,12 @@ function createMockReportProvider() {
2961
894
  }
2962
895
 
2963
896
  // src/index.ts
897
+ var AnalyticsModule2 = React.lazy(() => import('./AnalyticsModule-YIH6MDCC.js').then((m) => ({ default: m.AnalyticsModule })));
898
+ var AnalyticsOverview2 = React.lazy(() => import('./AnalyticsOverview-R3JEMUQL.js').then((m) => ({ default: m.AnalyticsOverview })));
899
+ var ReportsListPage2 = React.lazy(() => import('./ReportsListPage-7FAKQZ4T.js').then((m) => ({ default: m.ReportsListPage })));
900
+ var AnalyticsReportViewer2 = React.lazy(() => import('./AnalyticsReportViewer-ORI3XI5D.js').then((m) => ({ default: m.AnalyticsReportViewer })));
901
+ var LiveViewPage = React.lazy(() => import('./LiveViewPage-XMVZHJ3U.js').then((m) => ({ default: m.LiveViewPage })));
902
+ var AnalyticsSettingsPage2 = React.lazy(() => import('./AnalyticsSettingsPage-XWPM3VSA.js').then((m) => ({ default: m.AnalyticsSettingsPage })));
2964
903
  var DEFAULT_LABELS = {
2965
904
  analytics: "Analytics",
2966
905
  reports: "Reports",
@@ -3013,13 +952,13 @@ function createAnalyticsPlugin(options = {}) {
3013
952
  () => createSupabaseAnalyticsProvider({ tenantId: () => getReportsTenantId() }),
3014
953
  () => createMockAnalyticsProvider()
3015
954
  );
3016
- const page2 = (Body, view) => (props) => React10__default.createElement(
955
+ const page2 = (Body, view) => (props) => React.createElement(
3017
956
  AnalyticsSurface,
3018
957
  { config, provider },
3019
- React10__default.createElement(
3020
- AnalyticsModule,
958
+ React.createElement(
959
+ AnalyticsModule2,
3021
960
  { view, cardId: props?.cardId },
3022
- React10__default.createElement(Body, props)
961
+ React.createElement(Body, props)
3023
962
  )
3024
963
  );
3025
964
  const READ = { feature: "reports", action: "read" };
@@ -3050,8 +989,8 @@ function createAnalyticsPlugin(options = {}) {
3050
989
  ],
3051
990
  navigation: nav,
3052
991
  routes: [
3053
- { path: ANALYTICS_BASE, component: page2(AnalyticsOverview, "overview"), permission: READ },
3054
- { path: REPORTS_BASE, component: page2(ReportsListPage, "reports"), permission: READ },
992
+ { path: ANALYTICS_BASE, component: page2(AnalyticsOverview2, "overview"), permission: READ },
993
+ { path: REPORTS_BASE, component: page2(ReportsListPage2, "reports"), permission: READ },
3055
994
  // Registered before the list would ever shadow it: matchRoute scores
3056
995
  // static segments above params, so /analytics/reports still wins its own
3057
996
  // exact path and only /analytics/reports/<id> lands here.
@@ -3059,15 +998,15 @@ function createAnalyticsPlugin(options = {}) {
3059
998
  // (the card's name), so it claims the shell bar itself via PageHeader.
3060
999
  // Declaring both gave the page two stacked back-crumbs saying the same
3061
1000
  // thing, one of them generic.
3062
- { path: `${REPORTS_BASE}/:cardId`, component: page2(AnalyticsReportViewer, "report"), permission: READ },
1001
+ { path: `${REPORTS_BASE}/:cardId`, component: page2(AnalyticsReportViewer2, "report"), permission: READ },
3063
1002
  { path: `${ANALYTICS_BASE}/live`, component: page2(LiveViewPage, "live"), permission: READ },
3064
1003
  // Read permission on purpose: the screen writes a per-user preference,
3065
1004
  // not tenant data — gating it behind a write scope would mean a viewer
3066
1005
  // cannot choose which of the reports they can see are on their overview.
3067
- { path: SETTINGS_ROUTE, component: page2(AnalyticsSettingsPage, "settings"), permission: READ },
1006
+ { path: SETTINGS_ROUTE, component: page2(AnalyticsSettingsPage2, "settings"), permission: READ },
3068
1007
  // The old top-level route kept as a redirect target: bookmarks, saved
3069
1008
  // links and the five apps still pointing at /reports must not 404.
3070
- { path: "/reports", component: page2(ReportsListPage, "reports"), permission: READ }
1009
+ { path: "/reports", component: page2(ReportsListPage2, "reports"), permission: READ }
3071
1010
  ],
3072
1011
  widgets: [],
3073
1012
  registries: [],
@@ -3106,6 +1045,6 @@ function createReportsPlugin(options) {
3106
1045
  });
3107
1046
  }
3108
1047
 
3109
- export { ANALYTICS_BASE, AnalyticsModule, AnalyticsOverview, AnalyticsReportViewer, AnalyticsSettingsPage, AnalyticsSurface, REPORTS_BASE, ReportsListPage, SETTINGS_ROUTE, aggregateRows, cardColumns, clearCardPref, createAnalyticsPlugin, createMockAnalyticsProvider, createMockReportProvider, createReportsPlugin, createSupabaseAnalyticsProvider, createSupabaseReportProvider, deleteCustomReport, exportToCsv, fieldPalette, getReportsTenantId, markViewed, reportDefToCard, reportDefsToCards, resetAnalyticsPrefs, resolveCards, resolveCustomReport, runAndExportReport, runReport, saveCustomReport, setCardPref, setOverviewOrder, setReportsTenantId, sourceKey, useAnalyticsCards, useAnalyticsCatalogue, useAnalyticsConfig, useAnalyticsPrefs, useLastViewed, useOverviewCards };
1048
+ export { aggregateRows, createAnalyticsPlugin, createMockAnalyticsProvider, createMockReportProvider, createReportsPlugin, createSupabaseAnalyticsProvider, createSupabaseReportProvider, getReportsTenantId, reportDefToCard, reportDefsToCards, runAndExportReport, runReport, setReportsTenantId };
3110
1049
  //# sourceMappingURL=index.js.map
3111
1050
  //# sourceMappingURL=index.js.map