@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,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
|
+
);
|