@nextsparkjs/theme-crm 0.1.0-beta.1

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.
Files changed (140) hide show
  1. package/CRM_PLAN.md +343 -0
  2. package/about.md +122 -0
  3. package/config/app.config.ts +185 -0
  4. package/config/billing.config.ts +187 -0
  5. package/config/dashboard.config.ts +372 -0
  6. package/config/dev.config.ts +55 -0
  7. package/config/features.config.ts +336 -0
  8. package/config/flows.config.ts +511 -0
  9. package/config/permissions.config.ts +297 -0
  10. package/config/theme.config.ts +111 -0
  11. package/entities/activities/activities.config.ts +61 -0
  12. package/entities/activities/activities.fields.ts +362 -0
  13. package/entities/activities/activities.service.ts +503 -0
  14. package/entities/activities/activities.types.ts +117 -0
  15. package/entities/activities/messages/en.json +123 -0
  16. package/entities/activities/messages/es.json +123 -0
  17. package/entities/activities/migrations/020_activities_table.sql +123 -0
  18. package/entities/activities/migrations/021_activities_metas.sql +114 -0
  19. package/entities/activities/migrations/022_activities_sample_data.sql +420 -0
  20. package/entities/campaigns/campaigns.config.ts +61 -0
  21. package/entities/campaigns/campaigns.fields.ts +413 -0
  22. package/entities/campaigns/campaigns.service.ts +426 -0
  23. package/entities/campaigns/campaigns.types.ts +124 -0
  24. package/entities/campaigns/messages/en.json +145 -0
  25. package/entities/campaigns/messages/es.json +145 -0
  26. package/entities/campaigns/migrations/001_campaigns_table.sql +127 -0
  27. package/entities/campaigns/migrations/002_campaigns_metas.sql +114 -0
  28. package/entities/campaigns/migrations/003_campaigns_sample_data.sql +364 -0
  29. package/entities/companies/companies.config.ts +61 -0
  30. package/entities/companies/companies.fields.ts +429 -0
  31. package/entities/companies/companies.service.ts +566 -0
  32. package/entities/companies/companies.types.ts +125 -0
  33. package/entities/companies/messages/en.json +146 -0
  34. package/entities/companies/messages/es.json +146 -0
  35. package/entities/companies/migrations/001_companies_table.sql +150 -0
  36. package/entities/companies/migrations/002_companies_metas.sql +114 -0
  37. package/entities/companies/migrations/003_companies_sample_data.sql +246 -0
  38. package/entities/contacts/contacts.config.ts +61 -0
  39. package/entities/contacts/contacts.fields.ts +359 -0
  40. package/entities/contacts/contacts.service.ts +509 -0
  41. package/entities/contacts/contacts.types.ts +108 -0
  42. package/entities/contacts/messages/en.json +117 -0
  43. package/entities/contacts/messages/es.json +117 -0
  44. package/entities/contacts/migrations/001_contacts_table.sql +134 -0
  45. package/entities/contacts/migrations/002_contacts_metas.sql +114 -0
  46. package/entities/contacts/migrations/003_contacts_sample_data.sql +421 -0
  47. package/entities/leads/leads.config.ts +61 -0
  48. package/entities/leads/leads.fields.ts +336 -0
  49. package/entities/leads/leads.service.ts +496 -0
  50. package/entities/leads/leads.types.ts +114 -0
  51. package/entities/leads/messages/en.json +132 -0
  52. package/entities/leads/messages/es.json +132 -0
  53. package/entities/leads/migrations/001_leads_table.sql +150 -0
  54. package/entities/leads/migrations/002_leads_metas.sql +120 -0
  55. package/entities/leads/migrations/003_leads_sample_data.sql +242 -0
  56. package/entities/notes/messages/en.json +114 -0
  57. package/entities/notes/messages/es.json +114 -0
  58. package/entities/notes/migrations/020_notes_table.sql +118 -0
  59. package/entities/notes/migrations/021_notes_metas.sql +114 -0
  60. package/entities/notes/migrations/022_notes_sample_data.sql +275 -0
  61. package/entities/notes/notes.config.ts +61 -0
  62. package/entities/notes/notes.fields.ts +283 -0
  63. package/entities/notes/notes.service.ts +320 -0
  64. package/entities/notes/notes.types.ts +102 -0
  65. package/entities/opportunities/messages/en.json +107 -0
  66. package/entities/opportunities/messages/es.json +107 -0
  67. package/entities/opportunities/migrations/010_opportunities_table.sql +145 -0
  68. package/entities/opportunities/migrations/011_opportunities_metas.sql +114 -0
  69. package/entities/opportunities/migrations/012_opportunities_sample_data.sql +438 -0
  70. package/entities/opportunities/opportunities.config.ts +61 -0
  71. package/entities/opportunities/opportunities.fields.ts +416 -0
  72. package/entities/opportunities/opportunities.service.ts +525 -0
  73. package/entities/opportunities/opportunities.types.ts +135 -0
  74. package/entities/pipelines/messages/en.json +115 -0
  75. package/entities/pipelines/messages/es.json +115 -0
  76. package/entities/pipelines/migrations/001_pipelines_table.sql +106 -0
  77. package/entities/pipelines/migrations/002_pipelines_metas.sql +114 -0
  78. package/entities/pipelines/migrations/003_pipelines_sample_data.sql +91 -0
  79. package/entities/pipelines/pipelines.config.ts +62 -0
  80. package/entities/pipelines/pipelines.fields.ts +193 -0
  81. package/entities/pipelines/pipelines.service.ts +383 -0
  82. package/entities/pipelines/pipelines.types.ts +78 -0
  83. package/entities/products/messages/en.json +135 -0
  84. package/entities/products/messages/es.json +135 -0
  85. package/entities/products/migrations/001_products_table.sql +117 -0
  86. package/entities/products/migrations/002_products_metas.sql +114 -0
  87. package/entities/products/migrations/003_products_sample_data.sql +247 -0
  88. package/entities/products/products.config.ts +62 -0
  89. package/entities/products/products.fields.ts +361 -0
  90. package/entities/products/products.service.ts +437 -0
  91. package/entities/products/products.types.ts +125 -0
  92. package/lib/crm-constants.ts +77 -0
  93. package/lib/crm-utils.ts +185 -0
  94. package/lib/selectors.ts +333 -0
  95. package/messages/en.json +131 -0
  96. package/messages/es.json +131 -0
  97. package/migrations/999_theme_sample_data.sql +473 -0
  98. package/package.json +18 -0
  99. package/pendings.md +205 -0
  100. package/permissions-matrix.md +216 -0
  101. package/styles/components.css +414 -0
  102. package/styles/crm-theme.css +358 -0
  103. package/styles/globals.css +576 -0
  104. package/styles/variables.css +111 -0
  105. package/templates/dashboard/(main)/activities/components/ActivityCard.tsx +169 -0
  106. package/templates/dashboard/(main)/activities/components/ActivityTimeline.tsx +165 -0
  107. package/templates/dashboard/(main)/activities/page.tsx +297 -0
  108. package/templates/dashboard/(main)/campaigns/page.tsx +373 -0
  109. package/templates/dashboard/(main)/companies/page.tsx +296 -0
  110. package/templates/dashboard/(main)/contacts/page.tsx +347 -0
  111. package/templates/dashboard/(main)/layout.tsx +98 -0
  112. package/templates/dashboard/(main)/leads/page.tsx +335 -0
  113. package/templates/dashboard/(main)/opportunities/[id]/edit/page.tsx +95 -0
  114. package/templates/dashboard/(main)/opportunities/create/page.tsx +94 -0
  115. package/templates/dashboard/(main)/opportunities/page.tsx +350 -0
  116. package/templates/dashboard/(main)/pipelines/[id]/edit/page.tsx +95 -0
  117. package/templates/dashboard/(main)/pipelines/[id]/page.tsx +143 -0
  118. package/templates/dashboard/(main)/pipelines/create/page.tsx +94 -0
  119. package/templates/dashboard/(main)/pipelines/page.tsx +234 -0
  120. package/templates/dashboard/(main)/products/[id]/edit/page.tsx +97 -0
  121. package/templates/dashboard/(main)/products/[id]/page.tsx +509 -0
  122. package/templates/dashboard/(main)/products/create/page.tsx +96 -0
  123. package/templates/dashboard/(main)/products/page.tsx +308 -0
  124. package/templates/shared/ActionButtons.tsx +41 -0
  125. package/templates/shared/CRMDashboard.tsx +519 -0
  126. package/templates/shared/CRMDataTable.tsx +441 -0
  127. package/templates/shared/CRMMetricCard.tsx +76 -0
  128. package/templates/shared/CRMMobileNav.tsx +172 -0
  129. package/templates/shared/CRMSidebar.tsx +346 -0
  130. package/templates/shared/CRMTopBar.tsx +265 -0
  131. package/templates/shared/DealCard.tsx +123 -0
  132. package/templates/shared/EntityCard.tsx +58 -0
  133. package/templates/shared/OpportunityForm.tsx +649 -0
  134. package/templates/shared/PipelineForm.tsx +367 -0
  135. package/templates/shared/PipelineKanban.tsx +194 -0
  136. package/templates/shared/QuickFilters.tsx +47 -0
  137. package/templates/shared/StageColumn.tsx +175 -0
  138. package/templates/shared/StageSelect.tsx +177 -0
  139. package/templates/shared/StagesRepeater.tsx +317 -0
  140. package/templates/shared/index.ts +9 -0
@@ -0,0 +1,145 @@
1
+ {
2
+ "entity": {
3
+ "name": "Campaña",
4
+ "plural": "Campañas",
5
+ "description": "Gestiona campañas de marketing y promoción"
6
+ },
7
+ "fields": {
8
+ "name": {
9
+ "label": "Nombre de Campaña",
10
+ "description": "Nombre o título de la campaña",
11
+ "placeholder": "Ingrese nombre..."
12
+ },
13
+ "type": {
14
+ "label": "Tipo",
15
+ "description": "Tipo de campaña de marketing",
16
+ "placeholder": "Seleccionar tipo..."
17
+ },
18
+ "status": {
19
+ "label": "Estado",
20
+ "description": "Estado actual de la campaña",
21
+ "placeholder": "Seleccionar estado..."
22
+ },
23
+ "channel": {
24
+ "label": "Canal",
25
+ "description": "Canal de marketing utilizado",
26
+ "placeholder": "Seleccionar canal..."
27
+ },
28
+ "budget": {
29
+ "label": "Presupuesto",
30
+ "description": "Presupuesto de la campaña",
31
+ "placeholder": "0.00"
32
+ },
33
+ "spend": {
34
+ "label": "Gastado",
35
+ "description": "Cantidad ya gastada",
36
+ "placeholder": "0.00"
37
+ },
38
+ "targetAudience": {
39
+ "label": "Audiencia Objetivo",
40
+ "description": "Descripción de la audiencia",
41
+ "placeholder": "Ingrese audiencia objetivo..."
42
+ },
43
+ "startDate": {
44
+ "label": "Fecha de Inicio",
45
+ "description": "Fecha de inicio",
46
+ "placeholder": "Seleccionar fecha..."
47
+ },
48
+ "endDate": {
49
+ "label": "Fecha de Fin",
50
+ "description": "Fecha de finalización",
51
+ "placeholder": "Seleccionar fecha..."
52
+ },
53
+ "impressions": {
54
+ "label": "Impresiones",
55
+ "description": "Número de impresiones",
56
+ "placeholder": "0"
57
+ },
58
+ "clicks": {
59
+ "label": "Clics",
60
+ "description": "Número de clics",
61
+ "placeholder": "0"
62
+ },
63
+ "conversions": {
64
+ "label": "Conversiones",
65
+ "description": "Número de conversiones",
66
+ "placeholder": "0"
67
+ },
68
+ "ctr": {
69
+ "label": "CTR (%)",
70
+ "description": "Porcentaje de clics",
71
+ "placeholder": "0.00"
72
+ },
73
+ "conversionRate": {
74
+ "label": "Tasa de Conversión (%)",
75
+ "description": "Porcentaje de conversiones",
76
+ "placeholder": "0.00"
77
+ },
78
+ "roi": {
79
+ "label": "ROI (%)",
80
+ "description": "Retorno de inversión",
81
+ "placeholder": "0.00"
82
+ },
83
+ "assignedTo": {
84
+ "label": "Responsable",
85
+ "description": "Usuario responsable",
86
+ "placeholder": "Seleccionar usuario..."
87
+ }
88
+ },
89
+ "options": {
90
+ "type": {
91
+ "email": "Email Marketing",
92
+ "social": "Redes Sociales",
93
+ "ppc": "Pago por Clic",
94
+ "content": "Marketing de Contenido",
95
+ "webinar": "Webinar",
96
+ "event": "Evento",
97
+ "referral": "Referidos",
98
+ "affiliate": "Afiliados",
99
+ "retargeting": "Retargeting"
100
+ },
101
+ "status": {
102
+ "draft": "Borrador",
103
+ "scheduled": "Programada",
104
+ "active": "Activa",
105
+ "paused": "Pausada",
106
+ "completed": "Completada",
107
+ "cancelled": "Cancelada"
108
+ },
109
+ "channel": {
110
+ "email": "Email",
111
+ "facebook": "Facebook",
112
+ "instagram": "Instagram",
113
+ "linkedin": "LinkedIn",
114
+ "twitter": "Twitter",
115
+ "google_ads": "Google Ads",
116
+ "youtube": "YouTube",
117
+ "tiktok": "TikTok",
118
+ "website": "Sitio Web",
119
+ "blog": "Blog",
120
+ "seo": "SEO",
121
+ "other": "Otro"
122
+ }
123
+ },
124
+ "actions": {
125
+ "create": "Crear Campaña",
126
+ "edit": "Editar Campaña",
127
+ "delete": "Eliminar Campaña",
128
+ "view": "Ver Campaña",
129
+ "list": "Listar Campañas",
130
+ "launch": "Lanzar Campaña",
131
+ "pause": "Pausar Campaña",
132
+ "resume": "Reanudar Campaña",
133
+ "duplicate": "Duplicar Campaña"
134
+ },
135
+ "messages": {
136
+ "created": "Campaña creada exitosamente",
137
+ "updated": "Campaña actualizada exitosamente",
138
+ "deleted": "Campaña eliminada exitosamente",
139
+ "launched": "Campaña lanzada exitosamente",
140
+ "paused": "Campaña pausada exitosamente",
141
+ "resumed": "Campaña reanudada exitosamente",
142
+ "notFound": "Campaña no encontrada",
143
+ "error": "Error al procesar la campaña"
144
+ }
145
+ }
@@ -0,0 +1,127 @@
1
+ -- ============================================================================
2
+ -- Campaigns Table Migration
3
+ -- CRM theme: Marketing campaigns
4
+ -- Updated with team support and RLS
5
+ -- ============================================================================
6
+
7
+ -- ============================================
8
+ -- ENUM TYPES
9
+ -- ============================================
10
+ DO $$ BEGIN
11
+ CREATE TYPE campaign_type AS ENUM ('email', 'social', 'event', 'webinar', 'advertising', 'content', 'other');
12
+ EXCEPTION
13
+ WHEN duplicate_object THEN null;
14
+ END $$;
15
+
16
+ DO $$ BEGIN
17
+ CREATE TYPE campaign_status AS ENUM ('planned', 'active', 'paused', 'completed', 'cancelled');
18
+ EXCEPTION
19
+ WHEN duplicate_object THEN null;
20
+ END $$;
21
+
22
+ DO $$ BEGIN
23
+ CREATE TYPE campaign_channel AS ENUM ('email', 'social_media', 'web', 'print', 'tv', 'radio', 'other');
24
+ EXCEPTION
25
+ WHEN duplicate_object THEN null;
26
+ END $$;
27
+
28
+ -- ============================================
29
+ -- TABLE
30
+ -- ============================================
31
+ CREATE TABLE IF NOT EXISTS "campaigns" (
32
+ "id" TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
33
+
34
+ -- Campaign info
35
+ "name" VARCHAR(255) NOT NULL,
36
+ "type" campaign_type DEFAULT 'email',
37
+ "status" campaign_status DEFAULT 'planned',
38
+ "objective" TEXT,
39
+ "description" TEXT,
40
+
41
+ -- Dates
42
+ "startDate" DATE,
43
+ "endDate" DATE,
44
+
45
+ -- Budget
46
+ "budget" DECIMAL(15,2) DEFAULT 0,
47
+ "actualCost" DECIMAL(15,2) DEFAULT 0,
48
+
49
+ -- Targets and results
50
+ "targetAudience" TEXT,
51
+ "targetLeads" INTEGER DEFAULT 0,
52
+ "actualLeads" INTEGER DEFAULT 0,
53
+ "targetRevenue" DECIMAL(15,2) DEFAULT 0,
54
+ "actualRevenue" DECIMAL(15,2) DEFAULT 0,
55
+ "roi" DECIMAL(10,2),
56
+
57
+ -- Channel
58
+ "channel" campaign_channel DEFAULT 'email',
59
+
60
+ -- Assignment
61
+ "assignedTo" TEXT REFERENCES "users"("id") ON DELETE SET NULL,
62
+
63
+ -- Ownership
64
+ "userId" TEXT NOT NULL REFERENCES "users"("id") ON DELETE CASCADE,
65
+ "teamId" TEXT NOT NULL REFERENCES "teams"("id") ON DELETE CASCADE,
66
+
67
+ -- Timestamps
68
+ "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
69
+ "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
70
+ );
71
+
72
+ -- ============================================
73
+ -- INDEXES
74
+ -- ============================================
75
+ CREATE INDEX IF NOT EXISTS "campaigns_teamId_idx" ON "campaigns" ("teamId");
76
+ CREATE INDEX IF NOT EXISTS "campaigns_userId_idx" ON "campaigns" ("userId");
77
+ CREATE INDEX IF NOT EXISTS "campaigns_type_idx" ON "campaigns" ("type");
78
+ CREATE INDEX IF NOT EXISTS "campaigns_status_idx" ON "campaigns" ("status");
79
+ CREATE INDEX IF NOT EXISTS "campaigns_startDate_idx" ON "campaigns" ("startDate");
80
+ CREATE INDEX IF NOT EXISTS "campaigns_endDate_idx" ON "campaigns" ("endDate");
81
+ CREATE INDEX IF NOT EXISTS "campaigns_assignedTo_idx" ON "campaigns" ("assignedTo");
82
+
83
+ -- ============================================
84
+ -- RLS
85
+ -- ============================================
86
+ ALTER TABLE "campaigns" ENABLE ROW LEVEL SECURITY;
87
+
88
+ DROP POLICY IF EXISTS "campaigns_select_policy" ON "campaigns";
89
+ DROP POLICY IF EXISTS "campaigns_insert_policy" ON "campaigns";
90
+ DROP POLICY IF EXISTS "campaigns_update_policy" ON "campaigns";
91
+ DROP POLICY IF EXISTS "campaigns_delete_policy" ON "campaigns";
92
+
93
+ CREATE POLICY "campaigns_select_policy" ON "campaigns"
94
+ FOR SELECT
95
+ USING ("teamId" = ANY(public.get_user_team_ids()) OR public.is_superadmin());
96
+
97
+ CREATE POLICY "campaigns_insert_policy" ON "campaigns"
98
+ FOR INSERT
99
+ WITH CHECK ("teamId" = ANY(public.get_user_team_ids()));
100
+
101
+ CREATE POLICY "campaigns_update_policy" ON "campaigns"
102
+ FOR UPDATE
103
+ USING ("teamId" = ANY(public.get_user_team_ids()) OR public.is_superadmin());
104
+
105
+ CREATE POLICY "campaigns_delete_policy" ON "campaigns"
106
+ FOR DELETE
107
+ USING ("teamId" = ANY(public.get_user_team_ids()) OR public.is_superadmin());
108
+
109
+ -- ============================================
110
+ -- TRIGGER updatedAt
111
+ -- ============================================
112
+ CREATE OR REPLACE FUNCTION update_campaigns_updated_at()
113
+ RETURNS TRIGGER AS $$
114
+ BEGIN
115
+ NEW."updatedAt" = NOW();
116
+ RETURN NEW;
117
+ END;
118
+ $$ LANGUAGE plpgsql;
119
+
120
+ DROP TRIGGER IF EXISTS campaigns_updated_at_trigger ON "campaigns";
121
+ CREATE TRIGGER campaigns_updated_at_trigger
122
+ BEFORE UPDATE ON "campaigns"
123
+ FOR EACH ROW
124
+ EXECUTE FUNCTION update_campaigns_updated_at();
125
+
126
+ COMMENT ON TABLE "campaigns" IS 'Marketing campaigns';
127
+ COMMENT ON COLUMN "campaigns"."roi" IS 'Return on investment percentage';
@@ -0,0 +1,114 @@
1
+ -- Migration: 002_campaigns_metas.sql
2
+ -- Description: Contacts metas (table, indexes, RLS)
3
+ -- Date: 2025-09-27
4
+
5
+ -- ============================================
6
+ -- TABLE
7
+ -- ============================================
8
+ -- No DROP needed - removed automatically by parent table CASCADE
9
+ CREATE TABLE IF NOT EXISTS public."campaigns_metas" (
10
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
11
+ "entityId" TEXT NOT NULL REFERENCES public."campaigns"(id) ON DELETE CASCADE,
12
+ "metaKey" TEXT NOT NULL,
13
+ "metaValue" JSONB NOT NULL DEFAULT '{}'::jsonb,
14
+ "dataType" TEXT DEFAULT 'json',
15
+ "isPublic" BOOLEAN NOT NULL DEFAULT false,
16
+ "isSearchable" BOOLEAN NOT NULL DEFAULT false,
17
+ "createdAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
18
+ "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
19
+ CONSTRAINT campaigns_metas_unique_key UNIQUE ("entityId", "metaKey")
20
+ );
21
+
22
+ COMMENT ON TABLE public."campaigns_metas" IS 'Contacts metadata table - stores additional key-value pairs for campaigns';
23
+ COMMENT ON COLUMN public."campaigns_metas"."entityId" IS 'Generic foreign key to parent campaign entity';
24
+ COMMENT ON COLUMN public."campaigns_metas"."metaKey" IS 'Metadata key name';
25
+ COMMENT ON COLUMN public."campaigns_metas"."metaValue" IS 'Metadata value as JSONB';
26
+ COMMENT ON COLUMN public."campaigns_metas"."dataType" IS 'Type hint for the value: json, string, number, boolean';
27
+ COMMENT ON COLUMN public."campaigns_metas"."isPublic" IS 'Whether this metadata is publicly readable';
28
+ COMMENT ON COLUMN public."campaigns_metas"."isSearchable" IS 'Whether this metadata is searchable';
29
+
30
+ -- ============================================
31
+ -- TRIGGER updatedAt (uses Better Auth function)
32
+ -- ============================================
33
+ DROP TRIGGER IF EXISTS campaigns_metas_set_updated_at ON public."campaigns_metas";
34
+ CREATE TRIGGER campaigns_metas_set_updated_at
35
+ BEFORE UPDATE ON public."campaigns_metas"
36
+ FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
37
+
38
+ -- ============================================
39
+ -- INDEXES
40
+ -- ============================================
41
+ CREATE INDEX IF NOT EXISTS idx_campaigns_metas_entity_id ON public."campaigns_metas"("entityId");
42
+ CREATE INDEX IF NOT EXISTS idx_campaigns_metas_key ON public."campaigns_metas"("metaKey");
43
+ CREATE INDEX IF NOT EXISTS idx_campaigns_metas_composite ON public."campaigns_metas"("entityId", "metaKey", "isPublic");
44
+ CREATE INDEX IF NOT EXISTS idx_campaigns_metas_is_public ON public."campaigns_metas"("isPublic") WHERE "isPublic" = true;
45
+ CREATE INDEX IF NOT EXISTS idx_campaigns_metas_is_searchable ON public."campaigns_metas"("isSearchable") WHERE "isSearchable" = true;
46
+ CREATE INDEX IF NOT EXISTS idx_campaigns_metas_searchable_key ON public."campaigns_metas"("metaKey") WHERE "isSearchable" = true;
47
+ CREATE INDEX IF NOT EXISTS idx_campaigns_metas_value_gin ON public."campaigns_metas" USING GIN ("metaValue");
48
+ CREATE INDEX IF NOT EXISTS idx_campaigns_metas_value_ops ON public."campaigns_metas" USING GIN ("metaValue" jsonb_path_ops);
49
+
50
+ -- ============================================
51
+ -- RLS
52
+ -- ============================================
53
+ ALTER TABLE public."campaigns_metas" ENABLE ROW LEVEL SECURITY;
54
+
55
+ -- Cleanup existing policies
56
+ DROP POLICY IF EXISTS "Users can view campaign metas" ON public."campaigns_metas";
57
+ DROP POLICY IF EXISTS "Users can create campaign metas" ON public."campaigns_metas";
58
+ DROP POLICY IF EXISTS "Users can update campaign metas" ON public."campaigns_metas";
59
+ DROP POLICY IF EXISTS "Users can delete campaign metas" ON public."campaigns_metas";
60
+
61
+ -- ============================
62
+ -- AUTHENTICATED USER POLICIES
63
+ -- ============================
64
+ -- Inherit permissions from parent entity
65
+ CREATE POLICY "Users can view campaign metas"
66
+ ON public."campaigns_metas"
67
+ FOR SELECT TO authenticated
68
+ USING (
69
+ EXISTS (
70
+ SELECT 1 FROM public."campaigns" c
71
+ WHERE c.id = "entityId"
72
+ AND c."userId" = public.get_auth_user_id()
73
+ )
74
+ );
75
+
76
+ CREATE POLICY "Users can create campaign metas"
77
+ ON public."campaigns_metas"
78
+ FOR INSERT TO authenticated
79
+ WITH CHECK (
80
+ EXISTS (
81
+ SELECT 1 FROM public."campaigns" c
82
+ WHERE c.id = "entityId"
83
+ AND c."userId" = public.get_auth_user_id()
84
+ )
85
+ );
86
+
87
+ CREATE POLICY "Users can update campaign metas"
88
+ ON public."campaigns_metas"
89
+ FOR UPDATE TO authenticated
90
+ USING (
91
+ EXISTS (
92
+ SELECT 1 FROM public."campaigns" c
93
+ WHERE c.id = "entityId"
94
+ AND c."userId" = public.get_auth_user_id()
95
+ )
96
+ )
97
+ WITH CHECK (
98
+ EXISTS (
99
+ SELECT 1 FROM public."campaigns" c
100
+ WHERE c.id = "entityId"
101
+ AND c."userId" = public.get_auth_user_id()
102
+ )
103
+ );
104
+
105
+ CREATE POLICY "Users can delete campaign metas"
106
+ ON public."campaigns_metas"
107
+ FOR DELETE TO authenticated
108
+ USING (
109
+ EXISTS (
110
+ SELECT 1 FROM public."campaigns" c
111
+ WHERE c.id = "entityId"
112
+ AND c."userId" = public.get_auth_user_id()
113
+ )
114
+ );