@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,132 @@
1
+ {
2
+ "entity": {
3
+ "name": "Lead",
4
+ "plural": "Leads",
5
+ "description": "Manage potential prospects and business opportunities"
6
+ },
7
+ "fields": {
8
+ "companyName": {
9
+ "label": "Company Name",
10
+ "description": "Name of the prospect company",
11
+ "placeholder": "Enter company name..."
12
+ },
13
+ "contactName": {
14
+ "label": "Contact Name",
15
+ "description": "Name of the contact person",
16
+ "placeholder": "Enter contact name..."
17
+ },
18
+ "email": {
19
+ "label": "Email",
20
+ "description": "Email address",
21
+ "placeholder": "Enter email address..."
22
+ },
23
+ "phone": {
24
+ "label": "Phone",
25
+ "description": "Phone number",
26
+ "placeholder": "Enter phone number..."
27
+ },
28
+ "website": {
29
+ "label": "Website",
30
+ "description": "Company website",
31
+ "placeholder": "https://example.com"
32
+ },
33
+ "source": {
34
+ "label": "Lead Source",
35
+ "description": "How this lead was acquired",
36
+ "placeholder": "Select source..."
37
+ },
38
+ "status": {
39
+ "label": "Status",
40
+ "description": "Current lead status",
41
+ "placeholder": "Select status..."
42
+ },
43
+ "score": {
44
+ "label": "Lead Score",
45
+ "description": "Lead score from 0 to 100",
46
+ "placeholder": "0"
47
+ },
48
+ "industry": {
49
+ "label": "Industry",
50
+ "description": "Industry sector",
51
+ "placeholder": "Enter industry..."
52
+ },
53
+ "companySize": {
54
+ "label": "Company Size",
55
+ "description": "Number of employees",
56
+ "placeholder": "Select company size..."
57
+ },
58
+ "budget": {
59
+ "label": "Estimated Budget",
60
+ "description": "Estimated budget amount",
61
+ "placeholder": "0.00"
62
+ },
63
+ "assignedTo": {
64
+ "label": "Assigned To",
65
+ "description": "Sales rep assigned to this lead",
66
+ "placeholder": "Select user..."
67
+ },
68
+ "notes": {
69
+ "label": "Notes",
70
+ "description": "Internal notes about the lead",
71
+ "placeholder": "Enter notes..."
72
+ },
73
+ "createdAt": {
74
+ "label": "Created At",
75
+ "description": "When the lead was created"
76
+ },
77
+ "updatedAt": {
78
+ "label": "Updated At",
79
+ "description": "When the lead was last updated"
80
+ }
81
+ },
82
+ "options": {
83
+ "source": {
84
+ "web": "Website",
85
+ "referral": "Referral",
86
+ "cold_call": "Cold Call",
87
+ "trade_show": "Trade Show",
88
+ "social_media": "Social Media",
89
+ "email": "Email",
90
+ "advertising": "Advertising",
91
+ "partner": "Partner",
92
+ "other": "Other"
93
+ },
94
+ "status": {
95
+ "new": "New",
96
+ "contacted": "Contacted",
97
+ "qualified": "Qualified",
98
+ "proposal": "Proposal",
99
+ "negotiation": "Negotiation",
100
+ "converted": "Converted",
101
+ "lost": "Lost"
102
+ },
103
+ "companySize": {
104
+ "1-10": "1-10 employees",
105
+ "11-50": "11-50 employees",
106
+ "51-200": "51-200 employees",
107
+ "201-500": "201-500 employees",
108
+ "500+": "500+ employees"
109
+ }
110
+ },
111
+ "actions": {
112
+ "create": "Create Lead",
113
+ "edit": "Edit Lead",
114
+ "delete": "Delete Lead",
115
+ "view": "View Lead",
116
+ "list": "List Leads",
117
+ "search": "Search Leads",
118
+ "export": "Export Leads",
119
+ "import": "Import Leads",
120
+ "convert": "Convert Lead",
121
+ "assign": "Assign Lead"
122
+ },
123
+ "messages": {
124
+ "created": "Lead created successfully",
125
+ "updated": "Lead updated successfully",
126
+ "deleted": "Lead deleted successfully",
127
+ "converted": "Lead converted successfully",
128
+ "assigned": "Lead assigned successfully",
129
+ "notFound": "Lead not found",
130
+ "error": "An error occurred while processing the lead"
131
+ }
132
+ }
@@ -0,0 +1,132 @@
1
+ {
2
+ "entity": {
3
+ "name": "Lead",
4
+ "plural": "Leads",
5
+ "description": "Gestiona prospectos potenciales y oportunidades de negocio"
6
+ },
7
+ "fields": {
8
+ "companyName": {
9
+ "label": "Nombre de Empresa",
10
+ "description": "Nombre de la empresa prospecto",
11
+ "placeholder": "Ingrese nombre de empresa..."
12
+ },
13
+ "contactName": {
14
+ "label": "Nombre de Contacto",
15
+ "description": "Nombre de la persona de contacto",
16
+ "placeholder": "Ingrese nombre de contacto..."
17
+ },
18
+ "email": {
19
+ "label": "Email",
20
+ "description": "Dirección de correo electrónico",
21
+ "placeholder": "Ingrese email..."
22
+ },
23
+ "phone": {
24
+ "label": "Teléfono",
25
+ "description": "Número de teléfono",
26
+ "placeholder": "Ingrese teléfono..."
27
+ },
28
+ "website": {
29
+ "label": "Sitio Web",
30
+ "description": "Sitio web de la empresa",
31
+ "placeholder": "https://ejemplo.com"
32
+ },
33
+ "source": {
34
+ "label": "Fuente del Lead",
35
+ "description": "Cómo se adquirió este lead",
36
+ "placeholder": "Seleccionar fuente..."
37
+ },
38
+ "status": {
39
+ "label": "Estado",
40
+ "description": "Estado actual del lead",
41
+ "placeholder": "Seleccionar estado..."
42
+ },
43
+ "score": {
44
+ "label": "Puntuación del Lead",
45
+ "description": "Puntuación del lead de 0 a 100",
46
+ "placeholder": "0"
47
+ },
48
+ "industry": {
49
+ "label": "Industria",
50
+ "description": "Sector industrial",
51
+ "placeholder": "Ingrese industria..."
52
+ },
53
+ "companySize": {
54
+ "label": "Tamaño de Empresa",
55
+ "description": "Número de empleados",
56
+ "placeholder": "Seleccionar tamaño..."
57
+ },
58
+ "budget": {
59
+ "label": "Presupuesto Estimado",
60
+ "description": "Monto de presupuesto estimado",
61
+ "placeholder": "0.00"
62
+ },
63
+ "assignedTo": {
64
+ "label": "Asignado a",
65
+ "description": "Representante de ventas asignado",
66
+ "placeholder": "Seleccionar usuario..."
67
+ },
68
+ "notes": {
69
+ "label": "Notas",
70
+ "description": "Notas internas sobre el lead",
71
+ "placeholder": "Ingrese notas..."
72
+ },
73
+ "createdAt": {
74
+ "label": "Creado el",
75
+ "description": "Cuándo se creó el lead"
76
+ },
77
+ "updatedAt": {
78
+ "label": "Actualizado el",
79
+ "description": "Cuándo se actualizó por última vez"
80
+ }
81
+ },
82
+ "options": {
83
+ "source": {
84
+ "web": "Sitio Web",
85
+ "referral": "Referido",
86
+ "cold_call": "Llamada en Frío",
87
+ "trade_show": "Feria Comercial",
88
+ "social_media": "Redes Sociales",
89
+ "email": "Email",
90
+ "advertising": "Publicidad",
91
+ "partner": "Socio",
92
+ "other": "Otro"
93
+ },
94
+ "status": {
95
+ "new": "Nuevo",
96
+ "contacted": "Contactado",
97
+ "qualified": "Calificado",
98
+ "proposal": "Propuesta",
99
+ "negotiation": "Negociación",
100
+ "converted": "Convertido",
101
+ "lost": "Perdido"
102
+ },
103
+ "companySize": {
104
+ "1-10": "1-10 empleados",
105
+ "11-50": "11-50 empleados",
106
+ "51-200": "51-200 empleados",
107
+ "201-500": "201-500 empleados",
108
+ "500+": "500+ empleados"
109
+ }
110
+ },
111
+ "actions": {
112
+ "create": "Crear Lead",
113
+ "edit": "Editar Lead",
114
+ "delete": "Eliminar Lead",
115
+ "view": "Ver Lead",
116
+ "list": "Listar Leads",
117
+ "search": "Buscar Leads",
118
+ "export": "Exportar Leads",
119
+ "import": "Importar Leads",
120
+ "convert": "Convertir Lead",
121
+ "assign": "Asignar Lead"
122
+ },
123
+ "messages": {
124
+ "created": "Lead creado exitosamente",
125
+ "updated": "Lead actualizado exitosamente",
126
+ "deleted": "Lead eliminado exitosamente",
127
+ "converted": "Lead convertido exitosamente",
128
+ "assigned": "Lead asignado exitosamente",
129
+ "notFound": "Lead no encontrado",
130
+ "error": "Ocurrió un error al procesar el lead"
131
+ }
132
+ }
@@ -0,0 +1,150 @@
1
+ -- ============================================================================
2
+ -- Leads Table Migration
3
+ -- CRM theme: Prospective customers before conversion
4
+ -- Updated with team support and RLS
5
+ -- ============================================================================
6
+
7
+ -- ============================================
8
+ -- ENUM TYPES
9
+ -- ============================================
10
+ DO $$ BEGIN
11
+ CREATE TYPE lead_source AS ENUM ('web', 'referral', 'cold_call', 'trade_show', 'social_media', 'email', 'advertising', 'partner', 'other');
12
+ EXCEPTION
13
+ WHEN duplicate_object THEN null;
14
+ END $$;
15
+
16
+ DO $$ BEGIN
17
+ CREATE TYPE lead_status AS ENUM ('new', 'contacted', 'qualified', 'proposal', 'negotiation', 'converted', 'lost');
18
+ EXCEPTION
19
+ WHEN duplicate_object THEN null;
20
+ END $$;
21
+
22
+ DO $$ BEGIN
23
+ CREATE TYPE company_size AS ENUM ('1-10', '11-50', '51-200', '201-500', '500+');
24
+ EXCEPTION
25
+ WHEN duplicate_object THEN null;
26
+ END $$;
27
+
28
+ -- ============================================
29
+ -- TABLE
30
+ -- ============================================
31
+ CREATE TABLE IF NOT EXISTS "leads" (
32
+ "id" TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
33
+
34
+ -- Contact information
35
+ "companyName" VARCHAR(255) NOT NULL,
36
+ "contactName" VARCHAR(255) NOT NULL,
37
+ "email" VARCHAR(255) NOT NULL,
38
+ "phone" VARCHAR(50),
39
+ "website" VARCHAR(500),
40
+
41
+ -- Lead qualification
42
+ "source" lead_source DEFAULT 'web',
43
+ "status" lead_status DEFAULT 'new',
44
+ "score" INTEGER DEFAULT 0 CHECK ("score" >= 0 AND "score" <= 100),
45
+ "industry" VARCHAR(100),
46
+ "companySize" company_size,
47
+ "budget" DECIMAL(15,2),
48
+
49
+ -- Assignment
50
+ "assignedTo" TEXT REFERENCES "users"("id") ON DELETE SET NULL,
51
+
52
+ -- Conversion tracking
53
+ "convertedDate" TIMESTAMPTZ,
54
+ "convertedToContactId" UUID,
55
+ "convertedToCompanyId" UUID,
56
+
57
+ -- Notes
58
+ "notes" TEXT,
59
+
60
+ -- Ownership
61
+ "userId" TEXT NOT NULL REFERENCES "users"("id") ON DELETE CASCADE,
62
+ "teamId" TEXT NOT NULL REFERENCES "teams"("id") ON DELETE CASCADE,
63
+
64
+ -- Timestamps
65
+ "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
66
+ "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
67
+
68
+ -- Constraints
69
+ CONSTRAINT leads_email_team_unique UNIQUE ("teamId", "email")
70
+ );
71
+
72
+ -- ============================================
73
+ -- INDEXES
74
+ -- ============================================
75
+ CREATE INDEX IF NOT EXISTS "leads_teamId_idx" ON "leads" ("teamId");
76
+ CREATE INDEX IF NOT EXISTS "leads_userId_idx" ON "leads" ("userId");
77
+ CREATE INDEX IF NOT EXISTS "leads_email_idx" ON "leads" ("email");
78
+ CREATE INDEX IF NOT EXISTS "leads_status_idx" ON "leads" ("status");
79
+ CREATE INDEX IF NOT EXISTS "leads_source_idx" ON "leads" ("source");
80
+ CREATE INDEX IF NOT EXISTS "leads_score_idx" ON "leads" ("score" DESC);
81
+ CREATE INDEX IF NOT EXISTS "leads_assignedTo_idx" ON "leads" ("assignedTo");
82
+ CREATE INDEX IF NOT EXISTS "leads_createdAt_idx" ON "leads" ("createdAt" DESC);
83
+ CREATE INDEX IF NOT EXISTS "leads_companyName_idx" ON "leads" ("companyName");
84
+
85
+ -- ============================================
86
+ -- RLS
87
+ -- ============================================
88
+ ALTER TABLE "leads" ENABLE ROW LEVEL SECURITY;
89
+
90
+ -- Drop existing policies
91
+ DROP POLICY IF EXISTS "leads_select_policy" ON "leads";
92
+ DROP POLICY IF EXISTS "leads_insert_policy" ON "leads";
93
+ DROP POLICY IF EXISTS "leads_update_policy" ON "leads";
94
+ DROP POLICY IF EXISTS "leads_delete_policy" ON "leads";
95
+
96
+ -- Policy: Team members can view leads
97
+ CREATE POLICY "leads_select_policy" ON "leads"
98
+ FOR SELECT
99
+ USING (
100
+ "teamId" = ANY(public.get_user_team_ids())
101
+ OR public.is_superadmin()
102
+ );
103
+
104
+ -- Policy: Team members can create leads
105
+ CREATE POLICY "leads_insert_policy" ON "leads"
106
+ FOR INSERT
107
+ WITH CHECK (
108
+ "teamId" = ANY(public.get_user_team_ids())
109
+ );
110
+
111
+ -- Policy: Team members can update leads
112
+ CREATE POLICY "leads_update_policy" ON "leads"
113
+ FOR UPDATE
114
+ USING (
115
+ "teamId" = ANY(public.get_user_team_ids())
116
+ OR public.is_superadmin()
117
+ );
118
+
119
+ -- Policy: Team members can delete leads (permission checked at app level)
120
+ CREATE POLICY "leads_delete_policy" ON "leads"
121
+ FOR DELETE
122
+ USING (
123
+ "teamId" = ANY(public.get_user_team_ids())
124
+ OR public.is_superadmin()
125
+ );
126
+
127
+ -- ============================================
128
+ -- TRIGGER updatedAt
129
+ -- ============================================
130
+ CREATE OR REPLACE FUNCTION update_leads_updated_at()
131
+ RETURNS TRIGGER AS $$
132
+ BEGIN
133
+ NEW."updatedAt" = NOW();
134
+ RETURN NEW;
135
+ END;
136
+ $$ LANGUAGE plpgsql;
137
+
138
+ DROP TRIGGER IF EXISTS leads_updated_at_trigger ON "leads";
139
+ CREATE TRIGGER leads_updated_at_trigger
140
+ BEFORE UPDATE ON "leads"
141
+ FOR EACH ROW
142
+ EXECUTE FUNCTION update_leads_updated_at();
143
+
144
+ -- ============================================
145
+ -- COMMENTS
146
+ -- ============================================
147
+ COMMENT ON TABLE "leads" IS 'Prospective customers before conversion to contacts/companies';
148
+ COMMENT ON COLUMN "leads"."score" IS 'Lead qualification score from 0 to 100';
149
+ COMMENT ON COLUMN "leads"."convertedToContactId" IS 'Reference to contact created from this lead';
150
+ COMMENT ON COLUMN "leads"."convertedToCompanyId" IS 'Reference to company created from this lead';
@@ -0,0 +1,120 @@
1
+ -- Migration: 002_leads_metas.sql
2
+ -- Description: Leads 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."leads_metas" (
10
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
11
+ "entityId" TEXT NOT NULL REFERENCES public."leads"(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 leads_metas_unique_key UNIQUE ("entityId", "metaKey")
20
+ );
21
+
22
+ COMMENT ON TABLE public."leads_metas" IS 'Leads metadata table - stores additional key-value pairs for leads';
23
+ COMMENT ON COLUMN public."leads_metas"."entityId" IS 'Generic foreign key to parent lead entity';
24
+ COMMENT ON COLUMN public."leads_metas"."metaKey" IS 'Metadata key name';
25
+ COMMENT ON COLUMN public."leads_metas"."metaValue" IS 'Metadata value as JSONB';
26
+ COMMENT ON COLUMN public."leads_metas"."dataType" IS 'Type hint for the value: json, string, number, boolean';
27
+ COMMENT ON COLUMN public."leads_metas"."isPublic" IS 'Whether this metadata is publicly readable';
28
+ COMMENT ON COLUMN public."leads_metas"."isSearchable" IS 'Whether this metadata is searchable';
29
+
30
+ -- ============================================
31
+ -- TRIGGER updatedAt (uses Better Auth function)
32
+ -- ============================================
33
+ DROP TRIGGER IF EXISTS leads_metas_set_updated_at ON public."leads_metas";
34
+ CREATE TRIGGER leads_metas_set_updated_at
35
+ BEFORE UPDATE ON public."leads_metas"
36
+ FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
37
+
38
+ -- ============================================
39
+ -- INDEXES
40
+ -- ============================================
41
+ CREATE INDEX IF NOT EXISTS idx_leads_metas_entity_id ON public."leads_metas"("entityId");
42
+ CREATE INDEX IF NOT EXISTS idx_leads_metas_key ON public."leads_metas"("metaKey");
43
+ CREATE INDEX IF NOT EXISTS idx_leads_metas_composite ON public."leads_metas"("entityId", "metaKey", "isPublic");
44
+ CREATE INDEX IF NOT EXISTS idx_leads_metas_is_public ON public."leads_metas"("isPublic") WHERE "isPublic" = true;
45
+ CREATE INDEX IF NOT EXISTS idx_leads_metas_is_searchable ON public."leads_metas"("isSearchable") WHERE "isSearchable" = true;
46
+ CREATE INDEX IF NOT EXISTS idx_leads_metas_searchable_key ON public."leads_metas"("metaKey") WHERE "isSearchable" = true;
47
+ CREATE INDEX IF NOT EXISTS idx_leads_metas_value_gin ON public."leads_metas" USING GIN ("metaValue");
48
+ CREATE INDEX IF NOT EXISTS idx_leads_metas_value_ops ON public."leads_metas" USING GIN ("metaValue" jsonb_path_ops);
49
+
50
+ -- ============================================
51
+ -- RLS
52
+ -- ============================================
53
+ ALTER TABLE public."leads_metas" ENABLE ROW LEVEL SECURITY;
54
+
55
+ -- Cleanup existing policies
56
+ DROP POLICY IF EXISTS "Users can view lead metas" ON public."leads_metas";
57
+ DROP POLICY IF EXISTS "Users can create lead metas" ON public."leads_metas";
58
+ DROP POLICY IF EXISTS "Users can update lead metas" ON public."leads_metas";
59
+ DROP POLICY IF EXISTS "Users can delete lead metas" ON public."leads_metas";
60
+
61
+ -- ============================
62
+ -- AUTHENTICATED USER POLICIES
63
+ -- ============================
64
+ -- Users can view metas for their own leads or assigned leads
65
+ CREATE POLICY "Users can view lead metas"
66
+ ON public."leads_metas"
67
+ FOR SELECT TO authenticated
68
+ USING (
69
+ EXISTS (
70
+ SELECT 1 FROM public."leads" l
71
+ WHERE l.id = "entityId"
72
+ AND (l."userId" = public.get_auth_user_id()
73
+ OR l."assignedTo" = public.get_auth_user_id())
74
+ )
75
+ );
76
+
77
+ -- Users can create metas for their own leads
78
+ CREATE POLICY "Users can create lead metas"
79
+ ON public."leads_metas"
80
+ FOR INSERT TO authenticated
81
+ WITH CHECK (
82
+ EXISTS (
83
+ SELECT 1 FROM public."leads" l
84
+ WHERE l.id = "entityId"
85
+ AND l."userId" = public.get_auth_user_id()
86
+ )
87
+ );
88
+
89
+ -- Users can update metas for their own leads or assigned leads
90
+ CREATE POLICY "Users can update lead metas"
91
+ ON public."leads_metas"
92
+ FOR UPDATE TO authenticated
93
+ USING (
94
+ EXISTS (
95
+ SELECT 1 FROM public."leads" l
96
+ WHERE l.id = "entityId"
97
+ AND (l."userId" = public.get_auth_user_id()
98
+ OR l."assignedTo" = public.get_auth_user_id())
99
+ )
100
+ )
101
+ WITH CHECK (
102
+ EXISTS (
103
+ SELECT 1 FROM public."leads" l
104
+ WHERE l.id = "entityId"
105
+ AND (l."userId" = public.get_auth_user_id()
106
+ OR l."assignedTo" = public.get_auth_user_id())
107
+ )
108
+ );
109
+
110
+ -- Users can delete metas for their own leads
111
+ CREATE POLICY "Users can delete lead metas"
112
+ ON public."leads_metas"
113
+ FOR DELETE TO authenticated
114
+ USING (
115
+ EXISTS (
116
+ SELECT 1 FROM public."leads" l
117
+ WHERE l.id = "entityId"
118
+ AND l."userId" = public.get_auth_user_id()
119
+ )
120
+ );