@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.
- package/CRM_PLAN.md +343 -0
- package/about.md +122 -0
- package/config/app.config.ts +185 -0
- package/config/billing.config.ts +187 -0
- package/config/dashboard.config.ts +372 -0
- package/config/dev.config.ts +55 -0
- package/config/features.config.ts +336 -0
- package/config/flows.config.ts +511 -0
- package/config/permissions.config.ts +297 -0
- package/config/theme.config.ts +111 -0
- package/entities/activities/activities.config.ts +61 -0
- package/entities/activities/activities.fields.ts +362 -0
- package/entities/activities/activities.service.ts +503 -0
- package/entities/activities/activities.types.ts +117 -0
- package/entities/activities/messages/en.json +123 -0
- package/entities/activities/messages/es.json +123 -0
- package/entities/activities/migrations/020_activities_table.sql +123 -0
- package/entities/activities/migrations/021_activities_metas.sql +114 -0
- package/entities/activities/migrations/022_activities_sample_data.sql +420 -0
- package/entities/campaigns/campaigns.config.ts +61 -0
- package/entities/campaigns/campaigns.fields.ts +413 -0
- package/entities/campaigns/campaigns.service.ts +426 -0
- package/entities/campaigns/campaigns.types.ts +124 -0
- package/entities/campaigns/messages/en.json +145 -0
- package/entities/campaigns/messages/es.json +145 -0
- package/entities/campaigns/migrations/001_campaigns_table.sql +127 -0
- package/entities/campaigns/migrations/002_campaigns_metas.sql +114 -0
- package/entities/campaigns/migrations/003_campaigns_sample_data.sql +364 -0
- package/entities/companies/companies.config.ts +61 -0
- package/entities/companies/companies.fields.ts +429 -0
- package/entities/companies/companies.service.ts +566 -0
- package/entities/companies/companies.types.ts +125 -0
- package/entities/companies/messages/en.json +146 -0
- package/entities/companies/messages/es.json +146 -0
- package/entities/companies/migrations/001_companies_table.sql +150 -0
- package/entities/companies/migrations/002_companies_metas.sql +114 -0
- package/entities/companies/migrations/003_companies_sample_data.sql +246 -0
- package/entities/contacts/contacts.config.ts +61 -0
- package/entities/contacts/contacts.fields.ts +359 -0
- package/entities/contacts/contacts.service.ts +509 -0
- package/entities/contacts/contacts.types.ts +108 -0
- package/entities/contacts/messages/en.json +117 -0
- package/entities/contacts/messages/es.json +117 -0
- package/entities/contacts/migrations/001_contacts_table.sql +134 -0
- package/entities/contacts/migrations/002_contacts_metas.sql +114 -0
- package/entities/contacts/migrations/003_contacts_sample_data.sql +421 -0
- package/entities/leads/leads.config.ts +61 -0
- package/entities/leads/leads.fields.ts +336 -0
- package/entities/leads/leads.service.ts +496 -0
- package/entities/leads/leads.types.ts +114 -0
- package/entities/leads/messages/en.json +132 -0
- package/entities/leads/messages/es.json +132 -0
- package/entities/leads/migrations/001_leads_table.sql +150 -0
- package/entities/leads/migrations/002_leads_metas.sql +120 -0
- package/entities/leads/migrations/003_leads_sample_data.sql +242 -0
- package/entities/notes/messages/en.json +114 -0
- package/entities/notes/messages/es.json +114 -0
- package/entities/notes/migrations/020_notes_table.sql +118 -0
- package/entities/notes/migrations/021_notes_metas.sql +114 -0
- package/entities/notes/migrations/022_notes_sample_data.sql +275 -0
- package/entities/notes/notes.config.ts +61 -0
- package/entities/notes/notes.fields.ts +283 -0
- package/entities/notes/notes.service.ts +320 -0
- package/entities/notes/notes.types.ts +102 -0
- package/entities/opportunities/messages/en.json +107 -0
- package/entities/opportunities/messages/es.json +107 -0
- package/entities/opportunities/migrations/010_opportunities_table.sql +145 -0
- package/entities/opportunities/migrations/011_opportunities_metas.sql +114 -0
- package/entities/opportunities/migrations/012_opportunities_sample_data.sql +438 -0
- package/entities/opportunities/opportunities.config.ts +61 -0
- package/entities/opportunities/opportunities.fields.ts +416 -0
- package/entities/opportunities/opportunities.service.ts +525 -0
- package/entities/opportunities/opportunities.types.ts +135 -0
- package/entities/pipelines/messages/en.json +115 -0
- package/entities/pipelines/messages/es.json +115 -0
- package/entities/pipelines/migrations/001_pipelines_table.sql +106 -0
- package/entities/pipelines/migrations/002_pipelines_metas.sql +114 -0
- package/entities/pipelines/migrations/003_pipelines_sample_data.sql +91 -0
- package/entities/pipelines/pipelines.config.ts +62 -0
- package/entities/pipelines/pipelines.fields.ts +193 -0
- package/entities/pipelines/pipelines.service.ts +383 -0
- package/entities/pipelines/pipelines.types.ts +78 -0
- package/entities/products/messages/en.json +135 -0
- package/entities/products/messages/es.json +135 -0
- package/entities/products/migrations/001_products_table.sql +117 -0
- package/entities/products/migrations/002_products_metas.sql +114 -0
- package/entities/products/migrations/003_products_sample_data.sql +247 -0
- package/entities/products/products.config.ts +62 -0
- package/entities/products/products.fields.ts +361 -0
- package/entities/products/products.service.ts +437 -0
- package/entities/products/products.types.ts +125 -0
- package/lib/crm-constants.ts +77 -0
- package/lib/crm-utils.ts +185 -0
- package/lib/selectors.ts +333 -0
- package/messages/en.json +131 -0
- package/messages/es.json +131 -0
- package/migrations/999_theme_sample_data.sql +473 -0
- package/package.json +18 -0
- package/pendings.md +205 -0
- package/permissions-matrix.md +216 -0
- package/styles/components.css +414 -0
- package/styles/crm-theme.css +358 -0
- package/styles/globals.css +576 -0
- package/styles/variables.css +111 -0
- package/templates/dashboard/(main)/activities/components/ActivityCard.tsx +169 -0
- package/templates/dashboard/(main)/activities/components/ActivityTimeline.tsx +165 -0
- package/templates/dashboard/(main)/activities/page.tsx +297 -0
- package/templates/dashboard/(main)/campaigns/page.tsx +373 -0
- package/templates/dashboard/(main)/companies/page.tsx +296 -0
- package/templates/dashboard/(main)/contacts/page.tsx +347 -0
- package/templates/dashboard/(main)/layout.tsx +98 -0
- package/templates/dashboard/(main)/leads/page.tsx +335 -0
- package/templates/dashboard/(main)/opportunities/[id]/edit/page.tsx +95 -0
- package/templates/dashboard/(main)/opportunities/create/page.tsx +94 -0
- package/templates/dashboard/(main)/opportunities/page.tsx +350 -0
- package/templates/dashboard/(main)/pipelines/[id]/edit/page.tsx +95 -0
- package/templates/dashboard/(main)/pipelines/[id]/page.tsx +143 -0
- package/templates/dashboard/(main)/pipelines/create/page.tsx +94 -0
- package/templates/dashboard/(main)/pipelines/page.tsx +234 -0
- package/templates/dashboard/(main)/products/[id]/edit/page.tsx +97 -0
- package/templates/dashboard/(main)/products/[id]/page.tsx +509 -0
- package/templates/dashboard/(main)/products/create/page.tsx +96 -0
- package/templates/dashboard/(main)/products/page.tsx +308 -0
- package/templates/shared/ActionButtons.tsx +41 -0
- package/templates/shared/CRMDashboard.tsx +519 -0
- package/templates/shared/CRMDataTable.tsx +441 -0
- package/templates/shared/CRMMetricCard.tsx +76 -0
- package/templates/shared/CRMMobileNav.tsx +172 -0
- package/templates/shared/CRMSidebar.tsx +346 -0
- package/templates/shared/CRMTopBar.tsx +265 -0
- package/templates/shared/DealCard.tsx +123 -0
- package/templates/shared/EntityCard.tsx +58 -0
- package/templates/shared/OpportunityForm.tsx +649 -0
- package/templates/shared/PipelineForm.tsx +367 -0
- package/templates/shared/PipelineKanban.tsx +194 -0
- package/templates/shared/QuickFilters.tsx +47 -0
- package/templates/shared/StageColumn.tsx +175 -0
- package/templates/shared/StageSelect.tsx +177 -0
- package/templates/shared/StagesRepeater.tsx +317 -0
- 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
|
+
);
|