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