@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,117 @@
|
|
|
1
|
+
{
|
|
2
|
+
"entity": {
|
|
3
|
+
"name": "Contact",
|
|
4
|
+
"plural": "Contacts",
|
|
5
|
+
"description": "Manage contact persons in companies"
|
|
6
|
+
},
|
|
7
|
+
"fields": {
|
|
8
|
+
"firstName": {
|
|
9
|
+
"label": "First Name",
|
|
10
|
+
"description": "Contact first name",
|
|
11
|
+
"placeholder": "Enter first name..."
|
|
12
|
+
},
|
|
13
|
+
"lastName": {
|
|
14
|
+
"label": "Last Name",
|
|
15
|
+
"description": "Contact last name",
|
|
16
|
+
"placeholder": "Enter last name..."
|
|
17
|
+
},
|
|
18
|
+
"email": {
|
|
19
|
+
"label": "Email",
|
|
20
|
+
"description": "Contact email address",
|
|
21
|
+
"placeholder": "Enter email address..."
|
|
22
|
+
},
|
|
23
|
+
"phone": {
|
|
24
|
+
"label": "Phone",
|
|
25
|
+
"description": "Office phone number",
|
|
26
|
+
"placeholder": "Enter phone number..."
|
|
27
|
+
},
|
|
28
|
+
"mobile": {
|
|
29
|
+
"label": "Mobile",
|
|
30
|
+
"description": "Mobile phone number",
|
|
31
|
+
"placeholder": "Enter mobile number..."
|
|
32
|
+
},
|
|
33
|
+
"companyId": {
|
|
34
|
+
"label": "Company",
|
|
35
|
+
"description": "Associated company",
|
|
36
|
+
"placeholder": "Select company..."
|
|
37
|
+
},
|
|
38
|
+
"position": {
|
|
39
|
+
"label": "Position",
|
|
40
|
+
"description": "Job position or title",
|
|
41
|
+
"placeholder": "Enter position..."
|
|
42
|
+
},
|
|
43
|
+
"department": {
|
|
44
|
+
"label": "Department",
|
|
45
|
+
"description": "Department in company",
|
|
46
|
+
"placeholder": "Enter department..."
|
|
47
|
+
},
|
|
48
|
+
"isPrimary": {
|
|
49
|
+
"label": "Primary Contact",
|
|
50
|
+
"description": "Is primary contact for company"
|
|
51
|
+
},
|
|
52
|
+
"birthDate": {
|
|
53
|
+
"label": "Birth Date",
|
|
54
|
+
"description": "Contact birth date",
|
|
55
|
+
"placeholder": "Select date..."
|
|
56
|
+
},
|
|
57
|
+
"linkedin": {
|
|
58
|
+
"label": "LinkedIn",
|
|
59
|
+
"description": "LinkedIn profile URL",
|
|
60
|
+
"placeholder": "https://linkedin.com/in/..."
|
|
61
|
+
},
|
|
62
|
+
"twitter": {
|
|
63
|
+
"label": "Twitter",
|
|
64
|
+
"description": "Twitter/X handle",
|
|
65
|
+
"placeholder": "@username"
|
|
66
|
+
},
|
|
67
|
+
"preferredChannel": {
|
|
68
|
+
"label": "Preferred Channel",
|
|
69
|
+
"description": "Preferred communication channel",
|
|
70
|
+
"placeholder": "Select channel..."
|
|
71
|
+
},
|
|
72
|
+
"timezone": {
|
|
73
|
+
"label": "Timezone",
|
|
74
|
+
"description": "Contact timezone",
|
|
75
|
+
"placeholder": "UTC"
|
|
76
|
+
},
|
|
77
|
+
"lastContactedAt": {
|
|
78
|
+
"label": "Last Contacted",
|
|
79
|
+
"description": "Last time this contact was reached"
|
|
80
|
+
},
|
|
81
|
+
"createdAt": {
|
|
82
|
+
"label": "Created At",
|
|
83
|
+
"description": "When the contact was created"
|
|
84
|
+
},
|
|
85
|
+
"updatedAt": {
|
|
86
|
+
"label": "Updated At",
|
|
87
|
+
"description": "When the contact was last updated"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"options": {
|
|
91
|
+
"preferredChannel": {
|
|
92
|
+
"email": "Email",
|
|
93
|
+
"phone": "Phone",
|
|
94
|
+
"whatsapp": "WhatsApp",
|
|
95
|
+
"linkedin": "LinkedIn",
|
|
96
|
+
"slack": "Slack",
|
|
97
|
+
"other": "Other"
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"actions": {
|
|
101
|
+
"create": "Create Contact",
|
|
102
|
+
"edit": "Edit Contact",
|
|
103
|
+
"delete": "Delete Contact",
|
|
104
|
+
"view": "View Contact",
|
|
105
|
+
"list": "List Contacts",
|
|
106
|
+
"search": "Search Contacts",
|
|
107
|
+
"export": "Export Contacts",
|
|
108
|
+
"import": "Import Contacts"
|
|
109
|
+
},
|
|
110
|
+
"messages": {
|
|
111
|
+
"created": "Contact created successfully",
|
|
112
|
+
"updated": "Contact updated successfully",
|
|
113
|
+
"deleted": "Contact deleted successfully",
|
|
114
|
+
"notFound": "Contact not found",
|
|
115
|
+
"error": "An error occurred while processing the contact"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{
|
|
2
|
+
"entity": {
|
|
3
|
+
"name": "Contacto",
|
|
4
|
+
"plural": "Contactos",
|
|
5
|
+
"description": "Gestiona personas de contacto en las empresas"
|
|
6
|
+
},
|
|
7
|
+
"fields": {
|
|
8
|
+
"firstName": {
|
|
9
|
+
"label": "Nombre",
|
|
10
|
+
"description": "Nombre del contacto",
|
|
11
|
+
"placeholder": "Ingrese nombre..."
|
|
12
|
+
},
|
|
13
|
+
"lastName": {
|
|
14
|
+
"label": "Apellido",
|
|
15
|
+
"description": "Apellido del contacto",
|
|
16
|
+
"placeholder": "Ingrese apellido..."
|
|
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 de oficina",
|
|
26
|
+
"placeholder": "Ingrese teléfono..."
|
|
27
|
+
},
|
|
28
|
+
"mobile": {
|
|
29
|
+
"label": "Móvil",
|
|
30
|
+
"description": "Número de teléfono móvil",
|
|
31
|
+
"placeholder": "Ingrese móvil..."
|
|
32
|
+
},
|
|
33
|
+
"companyId": {
|
|
34
|
+
"label": "Empresa",
|
|
35
|
+
"description": "Empresa asociada",
|
|
36
|
+
"placeholder": "Seleccionar empresa..."
|
|
37
|
+
},
|
|
38
|
+
"position": {
|
|
39
|
+
"label": "Cargo",
|
|
40
|
+
"description": "Posición o título de trabajo",
|
|
41
|
+
"placeholder": "Ingrese cargo..."
|
|
42
|
+
},
|
|
43
|
+
"department": {
|
|
44
|
+
"label": "Departamento",
|
|
45
|
+
"description": "Departamento en la empresa",
|
|
46
|
+
"placeholder": "Ingrese departamento..."
|
|
47
|
+
},
|
|
48
|
+
"isPrimary": {
|
|
49
|
+
"label": "Contacto Principal",
|
|
50
|
+
"description": "Es contacto principal de la empresa"
|
|
51
|
+
},
|
|
52
|
+
"birthDate": {
|
|
53
|
+
"label": "Fecha de Nacimiento",
|
|
54
|
+
"description": "Fecha de nacimiento del contacto",
|
|
55
|
+
"placeholder": "Seleccionar fecha..."
|
|
56
|
+
},
|
|
57
|
+
"linkedin": {
|
|
58
|
+
"label": "LinkedIn",
|
|
59
|
+
"description": "URL del perfil de LinkedIn",
|
|
60
|
+
"placeholder": "https://linkedin.com/in/..."
|
|
61
|
+
},
|
|
62
|
+
"twitter": {
|
|
63
|
+
"label": "Twitter",
|
|
64
|
+
"description": "Usuario de Twitter/X",
|
|
65
|
+
"placeholder": "@usuario"
|
|
66
|
+
},
|
|
67
|
+
"preferredChannel": {
|
|
68
|
+
"label": "Canal Preferido",
|
|
69
|
+
"description": "Canal de comunicación preferido",
|
|
70
|
+
"placeholder": "Seleccionar canal..."
|
|
71
|
+
},
|
|
72
|
+
"timezone": {
|
|
73
|
+
"label": "Zona Horaria",
|
|
74
|
+
"description": "Zona horaria del contacto",
|
|
75
|
+
"placeholder": "UTC"
|
|
76
|
+
},
|
|
77
|
+
"lastContactedAt": {
|
|
78
|
+
"label": "Último Contacto",
|
|
79
|
+
"description": "Última vez que se contactó"
|
|
80
|
+
},
|
|
81
|
+
"createdAt": {
|
|
82
|
+
"label": "Creado el",
|
|
83
|
+
"description": "Cuándo se creó el contacto"
|
|
84
|
+
},
|
|
85
|
+
"updatedAt": {
|
|
86
|
+
"label": "Actualizado el",
|
|
87
|
+
"description": "Cuándo se actualizó por última vez"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"options": {
|
|
91
|
+
"preferredChannel": {
|
|
92
|
+
"email": "Email",
|
|
93
|
+
"phone": "Teléfono",
|
|
94
|
+
"whatsapp": "WhatsApp",
|
|
95
|
+
"linkedin": "LinkedIn",
|
|
96
|
+
"slack": "Slack",
|
|
97
|
+
"other": "Otro"
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"actions": {
|
|
101
|
+
"create": "Crear Contacto",
|
|
102
|
+
"edit": "Editar Contacto",
|
|
103
|
+
"delete": "Eliminar Contacto",
|
|
104
|
+
"view": "Ver Contacto",
|
|
105
|
+
"list": "Listar Contactos",
|
|
106
|
+
"search": "Buscar Contactos",
|
|
107
|
+
"export": "Exportar Contactos",
|
|
108
|
+
"import": "Importar Contactos"
|
|
109
|
+
},
|
|
110
|
+
"messages": {
|
|
111
|
+
"created": "Contacto creado exitosamente",
|
|
112
|
+
"updated": "Contacto actualizado exitosamente",
|
|
113
|
+
"deleted": "Contacto eliminado exitosamente",
|
|
114
|
+
"notFound": "Contacto no encontrado",
|
|
115
|
+
"error": "Ocurrió un error al procesar el contacto"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- Contacts Table Migration
|
|
3
|
+
-- CRM theme: People contacts at companies
|
|
4
|
+
-- Updated with team support and RLS
|
|
5
|
+
-- ============================================================================
|
|
6
|
+
|
|
7
|
+
-- ============================================
|
|
8
|
+
-- ENUM TYPES
|
|
9
|
+
-- ============================================
|
|
10
|
+
DO $$ BEGIN
|
|
11
|
+
CREATE TYPE contact_channel AS ENUM ('email', 'phone', 'whatsapp', 'linkedin', 'slack', 'other');
|
|
12
|
+
EXCEPTION
|
|
13
|
+
WHEN duplicate_object THEN null;
|
|
14
|
+
END $$;
|
|
15
|
+
|
|
16
|
+
-- ============================================
|
|
17
|
+
-- TABLE
|
|
18
|
+
-- ============================================
|
|
19
|
+
CREATE TABLE IF NOT EXISTS "contacts" (
|
|
20
|
+
"id" TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
21
|
+
|
|
22
|
+
-- Contact information
|
|
23
|
+
"firstName" VARCHAR(100) NOT NULL,
|
|
24
|
+
"lastName" VARCHAR(100) NOT NULL,
|
|
25
|
+
"email" VARCHAR(255) NOT NULL,
|
|
26
|
+
"phone" VARCHAR(50),
|
|
27
|
+
"mobile" VARCHAR(50),
|
|
28
|
+
|
|
29
|
+
-- Professional information
|
|
30
|
+
"companyId" TEXT, -- Reference to companies table
|
|
31
|
+
"position" VARCHAR(100),
|
|
32
|
+
"department" VARCHAR(100),
|
|
33
|
+
"isPrimary" BOOLEAN DEFAULT false,
|
|
34
|
+
|
|
35
|
+
-- Personal information
|
|
36
|
+
"birthDate" DATE,
|
|
37
|
+
"linkedin" VARCHAR(500),
|
|
38
|
+
"twitter" VARCHAR(100),
|
|
39
|
+
|
|
40
|
+
-- Communication preferences
|
|
41
|
+
"preferredChannel" contact_channel DEFAULT 'email',
|
|
42
|
+
"timezone" VARCHAR(50) DEFAULT 'UTC',
|
|
43
|
+
"lastContactedAt" TIMESTAMPTZ,
|
|
44
|
+
|
|
45
|
+
-- Ownership
|
|
46
|
+
"userId" TEXT NOT NULL REFERENCES "users"("id") ON DELETE CASCADE,
|
|
47
|
+
"teamId" TEXT NOT NULL REFERENCES "teams"("id") ON DELETE CASCADE,
|
|
48
|
+
|
|
49
|
+
-- Timestamps
|
|
50
|
+
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
51
|
+
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
52
|
+
|
|
53
|
+
-- Constraints
|
|
54
|
+
CONSTRAINT contacts_email_team_unique UNIQUE ("teamId", "email")
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
-- ============================================
|
|
58
|
+
-- INDEXES
|
|
59
|
+
-- ============================================
|
|
60
|
+
CREATE INDEX IF NOT EXISTS "contacts_teamId_idx" ON "contacts" ("teamId");
|
|
61
|
+
CREATE INDEX IF NOT EXISTS "contacts_userId_idx" ON "contacts" ("userId");
|
|
62
|
+
CREATE INDEX IF NOT EXISTS "contacts_email_idx" ON "contacts" ("email");
|
|
63
|
+
CREATE INDEX IF NOT EXISTS "contacts_companyId_idx" ON "contacts" ("companyId");
|
|
64
|
+
CREATE INDEX IF NOT EXISTS "contacts_firstName_idx" ON "contacts" ("firstName");
|
|
65
|
+
CREATE INDEX IF NOT EXISTS "contacts_lastName_idx" ON "contacts" ("lastName");
|
|
66
|
+
CREATE INDEX IF NOT EXISTS "contacts_isPrimary_idx" ON "contacts" ("isPrimary") WHERE "isPrimary" = true;
|
|
67
|
+
CREATE INDEX IF NOT EXISTS "contacts_createdAt_idx" ON "contacts" ("createdAt" DESC);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS "contacts_lastContactedAt_idx" ON "contacts" ("lastContactedAt" DESC);
|
|
69
|
+
|
|
70
|
+
-- ============================================
|
|
71
|
+
-- RLS
|
|
72
|
+
-- ============================================
|
|
73
|
+
ALTER TABLE "contacts" ENABLE ROW LEVEL SECURITY;
|
|
74
|
+
|
|
75
|
+
-- Drop existing policies
|
|
76
|
+
DROP POLICY IF EXISTS "contacts_select_policy" ON "contacts";
|
|
77
|
+
DROP POLICY IF EXISTS "contacts_insert_policy" ON "contacts";
|
|
78
|
+
DROP POLICY IF EXISTS "contacts_update_policy" ON "contacts";
|
|
79
|
+
DROP POLICY IF EXISTS "contacts_delete_policy" ON "contacts";
|
|
80
|
+
|
|
81
|
+
-- Policy: Team members can view contacts
|
|
82
|
+
CREATE POLICY "contacts_select_policy" ON "contacts"
|
|
83
|
+
FOR SELECT
|
|
84
|
+
USING (
|
|
85
|
+
"teamId" = ANY(public.get_user_team_ids())
|
|
86
|
+
OR public.is_superadmin()
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
-- Policy: Team members can create contacts
|
|
90
|
+
CREATE POLICY "contacts_insert_policy" ON "contacts"
|
|
91
|
+
FOR INSERT
|
|
92
|
+
WITH CHECK (
|
|
93
|
+
"teamId" = ANY(public.get_user_team_ids())
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
-- Policy: Team members can update contacts
|
|
97
|
+
CREATE POLICY "contacts_update_policy" ON "contacts"
|
|
98
|
+
FOR UPDATE
|
|
99
|
+
USING (
|
|
100
|
+
"teamId" = ANY(public.get_user_team_ids())
|
|
101
|
+
OR public.is_superadmin()
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
-- Policy: Team members can delete contacts
|
|
105
|
+
CREATE POLICY "contacts_delete_policy" ON "contacts"
|
|
106
|
+
FOR DELETE
|
|
107
|
+
USING (
|
|
108
|
+
"teamId" = ANY(public.get_user_team_ids())
|
|
109
|
+
OR public.is_superadmin()
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
-- ============================================
|
|
113
|
+
-- TRIGGER updatedAt
|
|
114
|
+
-- ============================================
|
|
115
|
+
CREATE OR REPLACE FUNCTION update_contacts_updated_at()
|
|
116
|
+
RETURNS TRIGGER AS $$
|
|
117
|
+
BEGIN
|
|
118
|
+
NEW."updatedAt" = NOW();
|
|
119
|
+
RETURN NEW;
|
|
120
|
+
END;
|
|
121
|
+
$$ LANGUAGE plpgsql;
|
|
122
|
+
|
|
123
|
+
DROP TRIGGER IF EXISTS contacts_updated_at_trigger ON "contacts";
|
|
124
|
+
CREATE TRIGGER contacts_updated_at_trigger
|
|
125
|
+
BEFORE UPDATE ON "contacts"
|
|
126
|
+
FOR EACH ROW
|
|
127
|
+
EXECUTE FUNCTION update_contacts_updated_at();
|
|
128
|
+
|
|
129
|
+
-- ============================================
|
|
130
|
+
-- COMMENTS
|
|
131
|
+
-- ============================================
|
|
132
|
+
COMMENT ON TABLE "contacts" IS 'People contacts at companies';
|
|
133
|
+
COMMENT ON COLUMN "contacts"."isPrimary" IS 'Is this the primary contact for the company';
|
|
134
|
+
COMMENT ON COLUMN "contacts"."preferredChannel" IS 'Preferred communication channel';
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
-- Migration: 002_contacts_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."contacts_metas" (
|
|
10
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
11
|
+
"entityId" TEXT NOT NULL REFERENCES public."contacts"(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 contacts_metas_unique_key UNIQUE ("entityId", "metaKey")
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
COMMENT ON TABLE public."contacts_metas" IS 'Contacts metadata table - stores additional key-value pairs for contacts';
|
|
23
|
+
COMMENT ON COLUMN public."contacts_metas"."entityId" IS 'Generic foreign key to parent contact entity';
|
|
24
|
+
COMMENT ON COLUMN public."contacts_metas"."metaKey" IS 'Metadata key name';
|
|
25
|
+
COMMENT ON COLUMN public."contacts_metas"."metaValue" IS 'Metadata value as JSONB';
|
|
26
|
+
COMMENT ON COLUMN public."contacts_metas"."dataType" IS 'Type hint for the value: json, string, number, boolean';
|
|
27
|
+
COMMENT ON COLUMN public."contacts_metas"."isPublic" IS 'Whether this metadata is publicly readable';
|
|
28
|
+
COMMENT ON COLUMN public."contacts_metas"."isSearchable" IS 'Whether this metadata is searchable';
|
|
29
|
+
|
|
30
|
+
-- ============================================
|
|
31
|
+
-- TRIGGER updatedAt (uses Better Auth function)
|
|
32
|
+
-- ============================================
|
|
33
|
+
DROP TRIGGER IF EXISTS contacts_metas_set_updated_at ON public."contacts_metas";
|
|
34
|
+
CREATE TRIGGER contacts_metas_set_updated_at
|
|
35
|
+
BEFORE UPDATE ON public."contacts_metas"
|
|
36
|
+
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
|
37
|
+
|
|
38
|
+
-- ============================================
|
|
39
|
+
-- INDEXES
|
|
40
|
+
-- ============================================
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_contacts_metas_entity_id ON public."contacts_metas"("entityId");
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_contacts_metas_key ON public."contacts_metas"("metaKey");
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_contacts_metas_composite ON public."contacts_metas"("entityId", "metaKey", "isPublic");
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_contacts_metas_is_public ON public."contacts_metas"("isPublic") WHERE "isPublic" = true;
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_contacts_metas_is_searchable ON public."contacts_metas"("isSearchable") WHERE "isSearchable" = true;
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_contacts_metas_searchable_key ON public."contacts_metas"("metaKey") WHERE "isSearchable" = true;
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_contacts_metas_value_gin ON public."contacts_metas" USING GIN ("metaValue");
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_contacts_metas_value_ops ON public."contacts_metas" USING GIN ("metaValue" jsonb_path_ops);
|
|
49
|
+
|
|
50
|
+
-- ============================================
|
|
51
|
+
-- RLS
|
|
52
|
+
-- ============================================
|
|
53
|
+
ALTER TABLE public."contacts_metas" ENABLE ROW LEVEL SECURITY;
|
|
54
|
+
|
|
55
|
+
-- Cleanup existing policies
|
|
56
|
+
DROP POLICY IF EXISTS "Users can view contact metas" ON public."contacts_metas";
|
|
57
|
+
DROP POLICY IF EXISTS "Users can create contact metas" ON public."contacts_metas";
|
|
58
|
+
DROP POLICY IF EXISTS "Users can update contact metas" ON public."contacts_metas";
|
|
59
|
+
DROP POLICY IF EXISTS "Users can delete contact metas" ON public."contacts_metas";
|
|
60
|
+
|
|
61
|
+
-- ============================
|
|
62
|
+
-- AUTHENTICATED USER POLICIES
|
|
63
|
+
-- ============================
|
|
64
|
+
-- Inherit permissions from parent entity
|
|
65
|
+
CREATE POLICY "Users can view contact metas"
|
|
66
|
+
ON public."contacts_metas"
|
|
67
|
+
FOR SELECT TO authenticated
|
|
68
|
+
USING (
|
|
69
|
+
EXISTS (
|
|
70
|
+
SELECT 1 FROM public."contacts" c
|
|
71
|
+
WHERE c.id = "entityId"
|
|
72
|
+
AND c."userId" = public.get_auth_user_id()
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
CREATE POLICY "Users can create contact metas"
|
|
77
|
+
ON public."contacts_metas"
|
|
78
|
+
FOR INSERT TO authenticated
|
|
79
|
+
WITH CHECK (
|
|
80
|
+
EXISTS (
|
|
81
|
+
SELECT 1 FROM public."contacts" c
|
|
82
|
+
WHERE c.id = "entityId"
|
|
83
|
+
AND c."userId" = public.get_auth_user_id()
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
CREATE POLICY "Users can update contact metas"
|
|
88
|
+
ON public."contacts_metas"
|
|
89
|
+
FOR UPDATE TO authenticated
|
|
90
|
+
USING (
|
|
91
|
+
EXISTS (
|
|
92
|
+
SELECT 1 FROM public."contacts" 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."contacts" c
|
|
100
|
+
WHERE c.id = "entityId"
|
|
101
|
+
AND c."userId" = public.get_auth_user_id()
|
|
102
|
+
)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
CREATE POLICY "Users can delete contact metas"
|
|
106
|
+
ON public."contacts_metas"
|
|
107
|
+
FOR DELETE TO authenticated
|
|
108
|
+
USING (
|
|
109
|
+
EXISTS (
|
|
110
|
+
SELECT 1 FROM public."contacts" c
|
|
111
|
+
WHERE c.id = "entityId"
|
|
112
|
+
AND c."userId" = public.get_auth_user_id()
|
|
113
|
+
)
|
|
114
|
+
);
|