@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,146 @@
1
+ {
2
+ "entity": {
3
+ "name": "Company",
4
+ "plural": "Companies",
5
+ "description": "Manage client companies and prospects"
6
+ },
7
+ "fields": {
8
+ "name": {
9
+ "label": "Company Name",
10
+ "description": "Company display name",
11
+ "placeholder": "Enter company name..."
12
+ },
13
+ "legalName": {
14
+ "label": "Legal Name",
15
+ "description": "Legal registered company name",
16
+ "placeholder": "Enter legal name..."
17
+ },
18
+ "taxId": {
19
+ "label": "Tax ID",
20
+ "description": "Tax identification number",
21
+ "placeholder": "Enter tax ID..."
22
+ },
23
+ "website": {
24
+ "label": "Website",
25
+ "description": "Company website URL",
26
+ "placeholder": "https://example.com"
27
+ },
28
+ "email": {
29
+ "label": "Email",
30
+ "description": "General company email",
31
+ "placeholder": "info@company.com"
32
+ },
33
+ "phone": {
34
+ "label": "Phone",
35
+ "description": "Main company phone number",
36
+ "placeholder": "Enter phone number..."
37
+ },
38
+ "industry": {
39
+ "label": "Industry",
40
+ "description": "Industry sector",
41
+ "placeholder": "Enter industry..."
42
+ },
43
+ "type": {
44
+ "label": "Company Type",
45
+ "description": "Type of company relationship",
46
+ "placeholder": "Select type..."
47
+ },
48
+ "size": {
49
+ "label": "Company Size",
50
+ "description": "Company size by employees",
51
+ "placeholder": "Select size..."
52
+ },
53
+ "annualRevenue": {
54
+ "label": "Annual Revenue",
55
+ "description": "Annual revenue in currency",
56
+ "placeholder": "0.00"
57
+ },
58
+ "address": {
59
+ "label": "Address",
60
+ "description": "Street address",
61
+ "placeholder": "Enter address..."
62
+ },
63
+ "city": {
64
+ "label": "City",
65
+ "description": "City",
66
+ "placeholder": "Enter city..."
67
+ },
68
+ "state": {
69
+ "label": "State",
70
+ "description": "State or province",
71
+ "placeholder": "Enter state..."
72
+ },
73
+ "country": {
74
+ "label": "Country",
75
+ "description": "Country",
76
+ "placeholder": "US"
77
+ },
78
+ "postalCode": {
79
+ "label": "Postal Code",
80
+ "description": "Postal or ZIP code",
81
+ "placeholder": "Enter postal code..."
82
+ },
83
+ "logo": {
84
+ "label": "Logo",
85
+ "description": "Company logo URL",
86
+ "placeholder": "https://example.com/logo.png"
87
+ },
88
+ "rating": {
89
+ "label": "Rating",
90
+ "description": "Sales rating",
91
+ "placeholder": "Select rating..."
92
+ },
93
+ "assignedTo": {
94
+ "label": "Assigned To",
95
+ "description": "Account manager assigned",
96
+ "placeholder": "Select user..."
97
+ },
98
+ "createdAt": {
99
+ "label": "Created At",
100
+ "description": "When the company was created"
101
+ },
102
+ "updatedAt": {
103
+ "label": "Updated At",
104
+ "description": "When the company was last updated"
105
+ }
106
+ },
107
+ "options": {
108
+ "type": {
109
+ "prospect": "Prospect",
110
+ "customer": "Customer",
111
+ "partner": "Partner",
112
+ "competitor": "Competitor",
113
+ "vendor": "Vendor",
114
+ "other": "Other"
115
+ },
116
+ "size": {
117
+ "1-10": "1-10 employees",
118
+ "11-50": "11-50 employees",
119
+ "51-200": "51-200 employees",
120
+ "201-500": "201-500 employees",
121
+ "500+": "500+ employees"
122
+ },
123
+ "rating": {
124
+ "hot": "Hot",
125
+ "warm": "Warm",
126
+ "cold": "Cold"
127
+ }
128
+ },
129
+ "actions": {
130
+ "create": "Create Company",
131
+ "edit": "Edit Company",
132
+ "delete": "Delete Company",
133
+ "view": "View Company",
134
+ "list": "List Companies",
135
+ "search": "Search Companies",
136
+ "export": "Export Companies",
137
+ "import": "Import Companies"
138
+ },
139
+ "messages": {
140
+ "created": "Company created successfully",
141
+ "updated": "Company updated successfully",
142
+ "deleted": "Company deleted successfully",
143
+ "notFound": "Company not found",
144
+ "error": "An error occurred while processing the company"
145
+ }
146
+ }
@@ -0,0 +1,146 @@
1
+ {
2
+ "entity": {
3
+ "name": "Empresa",
4
+ "plural": "Empresas",
5
+ "description": "Gestiona empresas clientes y prospectos"
6
+ },
7
+ "fields": {
8
+ "name": {
9
+ "label": "Nombre de Empresa",
10
+ "description": "Nombre comercial de la empresa",
11
+ "placeholder": "Ingrese nombre de empresa..."
12
+ },
13
+ "legalName": {
14
+ "label": "Razón Social",
15
+ "description": "Nombre legal registrado",
16
+ "placeholder": "Ingrese razón social..."
17
+ },
18
+ "taxId": {
19
+ "label": "RFC/NIT",
20
+ "description": "Número de identificación fiscal",
21
+ "placeholder": "Ingrese RFC/NIT..."
22
+ },
23
+ "website": {
24
+ "label": "Sitio Web",
25
+ "description": "URL del sitio web",
26
+ "placeholder": "https://ejemplo.com"
27
+ },
28
+ "email": {
29
+ "label": "Email",
30
+ "description": "Email general de la empresa",
31
+ "placeholder": "info@empresa.com"
32
+ },
33
+ "phone": {
34
+ "label": "Teléfono",
35
+ "description": "Teléfono principal",
36
+ "placeholder": "Ingrese teléfono..."
37
+ },
38
+ "industry": {
39
+ "label": "Industria",
40
+ "description": "Sector industrial",
41
+ "placeholder": "Ingrese industria..."
42
+ },
43
+ "type": {
44
+ "label": "Tipo de Empresa",
45
+ "description": "Tipo de relación empresarial",
46
+ "placeholder": "Seleccionar tipo..."
47
+ },
48
+ "size": {
49
+ "label": "Tamaño",
50
+ "description": "Tamaño por empleados",
51
+ "placeholder": "Seleccionar tamaño..."
52
+ },
53
+ "annualRevenue": {
54
+ "label": "Ingresos Anuales",
55
+ "description": "Ingresos anuales en moneda",
56
+ "placeholder": "0.00"
57
+ },
58
+ "address": {
59
+ "label": "Dirección",
60
+ "description": "Dirección postal",
61
+ "placeholder": "Ingrese dirección..."
62
+ },
63
+ "city": {
64
+ "label": "Ciudad",
65
+ "description": "Ciudad",
66
+ "placeholder": "Ingrese ciudad..."
67
+ },
68
+ "state": {
69
+ "label": "Estado",
70
+ "description": "Estado o provincia",
71
+ "placeholder": "Ingrese estado..."
72
+ },
73
+ "country": {
74
+ "label": "País",
75
+ "description": "País",
76
+ "placeholder": "MX"
77
+ },
78
+ "postalCode": {
79
+ "label": "Código Postal",
80
+ "description": "Código postal",
81
+ "placeholder": "Ingrese código postal..."
82
+ },
83
+ "logo": {
84
+ "label": "Logo",
85
+ "description": "URL del logo",
86
+ "placeholder": "https://ejemplo.com/logo.png"
87
+ },
88
+ "rating": {
89
+ "label": "Calificación",
90
+ "description": "Calificación de ventas",
91
+ "placeholder": "Seleccionar calificación..."
92
+ },
93
+ "assignedTo": {
94
+ "label": "Asignado a",
95
+ "description": "Gerente de cuenta asignado",
96
+ "placeholder": "Seleccionar usuario..."
97
+ },
98
+ "createdAt": {
99
+ "label": "Creado el",
100
+ "description": "Cuándo se creó la empresa"
101
+ },
102
+ "updatedAt": {
103
+ "label": "Actualizado el",
104
+ "description": "Cuándo se actualizó por última vez"
105
+ }
106
+ },
107
+ "options": {
108
+ "type": {
109
+ "prospect": "Prospecto",
110
+ "customer": "Cliente",
111
+ "partner": "Socio",
112
+ "competitor": "Competidor",
113
+ "vendor": "Proveedor",
114
+ "other": "Otro"
115
+ },
116
+ "size": {
117
+ "1-10": "1-10 empleados",
118
+ "11-50": "11-50 empleados",
119
+ "51-200": "51-200 empleados",
120
+ "201-500": "201-500 empleados",
121
+ "500+": "500+ empleados"
122
+ },
123
+ "rating": {
124
+ "hot": "Caliente",
125
+ "warm": "Tibio",
126
+ "cold": "Frío"
127
+ }
128
+ },
129
+ "actions": {
130
+ "create": "Crear Empresa",
131
+ "edit": "Editar Empresa",
132
+ "delete": "Eliminar Empresa",
133
+ "view": "Ver Empresa",
134
+ "list": "Listar Empresas",
135
+ "search": "Buscar Empresas",
136
+ "export": "Exportar Empresas",
137
+ "import": "Importar Empresas"
138
+ },
139
+ "messages": {
140
+ "created": "Empresa creada exitosamente",
141
+ "updated": "Empresa actualizada exitosamente",
142
+ "deleted": "Empresa eliminada exitosamente",
143
+ "notFound": "Empresa no encontrada",
144
+ "error": "Ocurrió un error al procesar la empresa"
145
+ }
146
+ }
@@ -0,0 +1,150 @@
1
+ -- ============================================================================
2
+ -- Companies Table Migration
3
+ -- CRM theme: Customer and prospect companies
4
+ -- Updated with team support and RLS
5
+ -- ============================================================================
6
+
7
+ -- ============================================
8
+ -- ENUM TYPES
9
+ -- ============================================
10
+ DO $$ BEGIN
11
+ CREATE TYPE company_type AS ENUM ('prospect', 'customer', 'partner', 'competitor');
12
+ EXCEPTION
13
+ WHEN duplicate_object THEN null;
14
+ END $$;
15
+
16
+ DO $$ BEGIN
17
+ CREATE TYPE company_size AS ENUM ('1-10', '11-50', '51-200', '201-500', '500+');
18
+ EXCEPTION
19
+ WHEN duplicate_object THEN null;
20
+ END $$;
21
+
22
+ DO $$ BEGIN
23
+ CREATE TYPE company_rating AS ENUM ('hot', 'warm', 'cold');
24
+ EXCEPTION
25
+ WHEN duplicate_object THEN null;
26
+ END $$;
27
+
28
+ -- ============================================
29
+ -- TABLE
30
+ -- ============================================
31
+ CREATE TABLE IF NOT EXISTS "companies" (
32
+ "id" TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
33
+
34
+ -- Company information
35
+ "name" VARCHAR(255) NOT NULL,
36
+ "legalName" VARCHAR(255),
37
+ "taxId" VARCHAR(50),
38
+ "website" VARCHAR(500),
39
+ "email" VARCHAR(255),
40
+ "phone" VARCHAR(50),
41
+
42
+ -- Classification
43
+ "industry" VARCHAR(100),
44
+ "type" company_type DEFAULT 'prospect',
45
+ "size" company_size,
46
+ "annualRevenue" DECIMAL(15,2),
47
+ "rating" company_rating DEFAULT 'warm',
48
+
49
+ -- Address
50
+ "address" VARCHAR(500),
51
+ "city" VARCHAR(100),
52
+ "state" VARCHAR(100),
53
+ "country" VARCHAR(100),
54
+ "postalCode" VARCHAR(20),
55
+
56
+ -- Social
57
+ "logo" VARCHAR(500),
58
+ "linkedin" VARCHAR(500),
59
+ "facebook" VARCHAR(500),
60
+ "twitter" VARCHAR(100),
61
+
62
+ -- Assignment
63
+ "assignedTo" TEXT REFERENCES "users"("id") ON DELETE SET NULL,
64
+
65
+ -- Ownership
66
+ "userId" TEXT NOT NULL REFERENCES "users"("id") ON DELETE CASCADE,
67
+ "teamId" TEXT NOT NULL REFERENCES "teams"("id") ON DELETE CASCADE,
68
+
69
+ -- Timestamps
70
+ "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
71
+ "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
72
+ );
73
+
74
+ -- ============================================
75
+ -- INDEXES
76
+ -- ============================================
77
+ CREATE INDEX IF NOT EXISTS "companies_teamId_idx" ON "companies" ("teamId");
78
+ CREATE INDEX IF NOT EXISTS "companies_userId_idx" ON "companies" ("userId");
79
+ CREATE INDEX IF NOT EXISTS "companies_name_idx" ON "companies" ("name");
80
+ CREATE INDEX IF NOT EXISTS "companies_type_idx" ON "companies" ("type");
81
+ CREATE INDEX IF NOT EXISTS "companies_industry_idx" ON "companies" ("industry");
82
+ CREATE INDEX IF NOT EXISTS "companies_rating_idx" ON "companies" ("rating");
83
+ CREATE INDEX IF NOT EXISTS "companies_assignedTo_idx" ON "companies" ("assignedTo");
84
+ CREATE INDEX IF NOT EXISTS "companies_createdAt_idx" ON "companies" ("createdAt" DESC);
85
+
86
+ -- ============================================
87
+ -- RLS
88
+ -- ============================================
89
+ ALTER TABLE "companies" ENABLE ROW LEVEL SECURITY;
90
+
91
+ -- Drop existing policies
92
+ DROP POLICY IF EXISTS "companies_select_policy" ON "companies";
93
+ DROP POLICY IF EXISTS "companies_insert_policy" ON "companies";
94
+ DROP POLICY IF EXISTS "companies_update_policy" ON "companies";
95
+ DROP POLICY IF EXISTS "companies_delete_policy" ON "companies";
96
+
97
+ -- Policy: Team members can view companies
98
+ CREATE POLICY "companies_select_policy" ON "companies"
99
+ FOR SELECT
100
+ USING (
101
+ "teamId" = ANY(public.get_user_team_ids())
102
+ OR public.is_superadmin()
103
+ );
104
+
105
+ -- Policy: Team members can create companies
106
+ CREATE POLICY "companies_insert_policy" ON "companies"
107
+ FOR INSERT
108
+ WITH CHECK (
109
+ "teamId" = ANY(public.get_user_team_ids())
110
+ );
111
+
112
+ -- Policy: Team members can update companies
113
+ CREATE POLICY "companies_update_policy" ON "companies"
114
+ FOR UPDATE
115
+ USING (
116
+ "teamId" = ANY(public.get_user_team_ids())
117
+ OR public.is_superadmin()
118
+ );
119
+
120
+ -- Policy: Team members can delete companies
121
+ CREATE POLICY "companies_delete_policy" ON "companies"
122
+ FOR DELETE
123
+ USING (
124
+ "teamId" = ANY(public.get_user_team_ids())
125
+ OR public.is_superadmin()
126
+ );
127
+
128
+ -- ============================================
129
+ -- TRIGGER updatedAt
130
+ -- ============================================
131
+ CREATE OR REPLACE FUNCTION update_companies_updated_at()
132
+ RETURNS TRIGGER AS $$
133
+ BEGIN
134
+ NEW."updatedAt" = NOW();
135
+ RETURN NEW;
136
+ END;
137
+ $$ LANGUAGE plpgsql;
138
+
139
+ DROP TRIGGER IF EXISTS companies_updated_at_trigger ON "companies";
140
+ CREATE TRIGGER companies_updated_at_trigger
141
+ BEFORE UPDATE ON "companies"
142
+ FOR EACH ROW
143
+ EXECUTE FUNCTION update_companies_updated_at();
144
+
145
+ -- ============================================
146
+ -- COMMENTS
147
+ -- ============================================
148
+ COMMENT ON TABLE "companies" IS 'Customer and prospect companies';
149
+ COMMENT ON COLUMN "companies"."type" IS 'Company relationship type';
150
+ COMMENT ON COLUMN "companies"."rating" IS 'Company temperature rating';
@@ -0,0 +1,114 @@
1
+ -- Migration: 002_companies_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."companies_metas" (
10
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
11
+ "entityId" TEXT NOT NULL REFERENCES public."companies"(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 companies_metas_unique_key UNIQUE ("entityId", "metaKey")
20
+ );
21
+
22
+ COMMENT ON TABLE public."companies_metas" IS 'Contacts metadata table - stores additional key-value pairs for companies';
23
+ COMMENT ON COLUMN public."companies_metas"."entityId" IS 'Generic foreign key to parent companie entity';
24
+ COMMENT ON COLUMN public."companies_metas"."metaKey" IS 'Metadata key name';
25
+ COMMENT ON COLUMN public."companies_metas"."metaValue" IS 'Metadata value as JSONB';
26
+ COMMENT ON COLUMN public."companies_metas"."dataType" IS 'Type hint for the value: json, string, number, boolean';
27
+ COMMENT ON COLUMN public."companies_metas"."isPublic" IS 'Whether this metadata is publicly readable';
28
+ COMMENT ON COLUMN public."companies_metas"."isSearchable" IS 'Whether this metadata is searchable';
29
+
30
+ -- ============================================
31
+ -- TRIGGER updatedAt (uses Better Auth function)
32
+ -- ============================================
33
+ DROP TRIGGER IF EXISTS companies_metas_set_updated_at ON public."companies_metas";
34
+ CREATE TRIGGER companies_metas_set_updated_at
35
+ BEFORE UPDATE ON public."companies_metas"
36
+ FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
37
+
38
+ -- ============================================
39
+ -- INDEXES
40
+ -- ============================================
41
+ CREATE INDEX IF NOT EXISTS idx_companies_metas_entity_id ON public."companies_metas"("entityId");
42
+ CREATE INDEX IF NOT EXISTS idx_companies_metas_key ON public."companies_metas"("metaKey");
43
+ CREATE INDEX IF NOT EXISTS idx_companies_metas_composite ON public."companies_metas"("entityId", "metaKey", "isPublic");
44
+ CREATE INDEX IF NOT EXISTS idx_companies_metas_is_public ON public."companies_metas"("isPublic") WHERE "isPublic" = true;
45
+ CREATE INDEX IF NOT EXISTS idx_companies_metas_is_searchable ON public."companies_metas"("isSearchable") WHERE "isSearchable" = true;
46
+ CREATE INDEX IF NOT EXISTS idx_companies_metas_searchable_key ON public."companies_metas"("metaKey") WHERE "isSearchable" = true;
47
+ CREATE INDEX IF NOT EXISTS idx_companies_metas_value_gin ON public."companies_metas" USING GIN ("metaValue");
48
+ CREATE INDEX IF NOT EXISTS idx_companies_metas_value_ops ON public."companies_metas" USING GIN ("metaValue" jsonb_path_ops);
49
+
50
+ -- ============================================
51
+ -- RLS
52
+ -- ============================================
53
+ ALTER TABLE public."companies_metas" ENABLE ROW LEVEL SECURITY;
54
+
55
+ -- Cleanup existing policies
56
+ DROP POLICY IF EXISTS "Users can view companie metas" ON public."companies_metas";
57
+ DROP POLICY IF EXISTS "Users can create companie metas" ON public."companies_metas";
58
+ DROP POLICY IF EXISTS "Users can update companie metas" ON public."companies_metas";
59
+ DROP POLICY IF EXISTS "Users can delete companie metas" ON public."companies_metas";
60
+
61
+ -- ============================
62
+ -- AUTHENTICATED USER POLICIES
63
+ -- ============================
64
+ -- Inherit permissions from parent entity
65
+ CREATE POLICY "Users can view companie metas"
66
+ ON public."companies_metas"
67
+ FOR SELECT TO authenticated
68
+ USING (
69
+ EXISTS (
70
+ SELECT 1 FROM public."companies" c
71
+ WHERE c.id = "entityId"
72
+ AND c."userId" = public.get_auth_user_id()
73
+ )
74
+ );
75
+
76
+ CREATE POLICY "Users can create companie metas"
77
+ ON public."companies_metas"
78
+ FOR INSERT TO authenticated
79
+ WITH CHECK (
80
+ EXISTS (
81
+ SELECT 1 FROM public."companies" c
82
+ WHERE c.id = "entityId"
83
+ AND c."userId" = public.get_auth_user_id()
84
+ )
85
+ );
86
+
87
+ CREATE POLICY "Users can update companie metas"
88
+ ON public."companies_metas"
89
+ FOR UPDATE TO authenticated
90
+ USING (
91
+ EXISTS (
92
+ SELECT 1 FROM public."companies" 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."companies" c
100
+ WHERE c.id = "entityId"
101
+ AND c."userId" = public.get_auth_user_id()
102
+ )
103
+ );
104
+
105
+ CREATE POLICY "Users can delete companie metas"
106
+ ON public."companies_metas"
107
+ FOR DELETE TO authenticated
108
+ USING (
109
+ EXISTS (
110
+ SELECT 1 FROM public."companies" c
111
+ WHERE c.id = "entityId"
112
+ AND c."userId" = public.get_auth_user_id()
113
+ )
114
+ );