@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,135 @@
|
|
|
1
|
+
{
|
|
2
|
+
"entity": {
|
|
3
|
+
"name": "Product",
|
|
4
|
+
"plural": "Products",
|
|
5
|
+
"description": "Manage products and services"
|
|
6
|
+
},
|
|
7
|
+
"fields": {
|
|
8
|
+
"name": {
|
|
9
|
+
"label": "Product Name",
|
|
10
|
+
"description": "Product or service name",
|
|
11
|
+
"placeholder": "Enter product name..."
|
|
12
|
+
},
|
|
13
|
+
"sku": {
|
|
14
|
+
"label": "SKU",
|
|
15
|
+
"description": "Stock keeping unit identifier",
|
|
16
|
+
"placeholder": "Enter SKU..."
|
|
17
|
+
},
|
|
18
|
+
"description": {
|
|
19
|
+
"label": "Description",
|
|
20
|
+
"description": "Product description",
|
|
21
|
+
"placeholder": "Enter description..."
|
|
22
|
+
},
|
|
23
|
+
"category": {
|
|
24
|
+
"label": "Category",
|
|
25
|
+
"description": "Product category",
|
|
26
|
+
"placeholder": "Enter category..."
|
|
27
|
+
},
|
|
28
|
+
"type": {
|
|
29
|
+
"label": "Product Type",
|
|
30
|
+
"description": "Type of product or service",
|
|
31
|
+
"placeholder": "Select type..."
|
|
32
|
+
},
|
|
33
|
+
"status": {
|
|
34
|
+
"label": "Status",
|
|
35
|
+
"description": "Product status",
|
|
36
|
+
"placeholder": "Select status..."
|
|
37
|
+
},
|
|
38
|
+
"price": {
|
|
39
|
+
"label": "Unit Price",
|
|
40
|
+
"description": "Price per unit",
|
|
41
|
+
"placeholder": "0.00"
|
|
42
|
+
},
|
|
43
|
+
"cost": {
|
|
44
|
+
"label": "Unit Cost",
|
|
45
|
+
"description": "Cost per unit",
|
|
46
|
+
"placeholder": "0.00"
|
|
47
|
+
},
|
|
48
|
+
"currency": {
|
|
49
|
+
"label": "Currency",
|
|
50
|
+
"description": "Currency code",
|
|
51
|
+
"placeholder": "Select currency..."
|
|
52
|
+
},
|
|
53
|
+
"stockQuantity": {
|
|
54
|
+
"label": "Stock Quantity",
|
|
55
|
+
"description": "Available stock quantity",
|
|
56
|
+
"placeholder": "0"
|
|
57
|
+
},
|
|
58
|
+
"minStockLevel": {
|
|
59
|
+
"label": "Minimum Stock Level",
|
|
60
|
+
"description": "Minimum stock level threshold",
|
|
61
|
+
"placeholder": "0"
|
|
62
|
+
},
|
|
63
|
+
"features": {
|
|
64
|
+
"label": "Features",
|
|
65
|
+
"description": "Product features list"
|
|
66
|
+
},
|
|
67
|
+
"specifications": {
|
|
68
|
+
"label": "Specifications",
|
|
69
|
+
"description": "Technical specifications"
|
|
70
|
+
},
|
|
71
|
+
"images": {
|
|
72
|
+
"label": "Images",
|
|
73
|
+
"description": "Product images"
|
|
74
|
+
},
|
|
75
|
+
"isActive": {
|
|
76
|
+
"label": "Active",
|
|
77
|
+
"description": "Whether product is active"
|
|
78
|
+
},
|
|
79
|
+
"weight": {
|
|
80
|
+
"label": "Weight",
|
|
81
|
+
"description": "Product weight",
|
|
82
|
+
"placeholder": "0.00"
|
|
83
|
+
},
|
|
84
|
+
"dimensions": {
|
|
85
|
+
"label": "Dimensions",
|
|
86
|
+
"description": "Product dimensions",
|
|
87
|
+
"placeholder": "L x W x H"
|
|
88
|
+
},
|
|
89
|
+
"vendor": {
|
|
90
|
+
"label": "Vendor",
|
|
91
|
+
"description": "Product vendor or supplier",
|
|
92
|
+
"placeholder": "Enter vendor..."
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"options": {
|
|
96
|
+
"type": {
|
|
97
|
+
"physical": "Physical Product",
|
|
98
|
+
"digital": "Digital Product",
|
|
99
|
+
"service": "Service",
|
|
100
|
+
"subscription": "Subscription",
|
|
101
|
+
"bundle": "Product Bundle",
|
|
102
|
+
"variant": "Product Variant"
|
|
103
|
+
},
|
|
104
|
+
"status": {
|
|
105
|
+
"active": "Active",
|
|
106
|
+
"inactive": "Inactive",
|
|
107
|
+
"discontinued": "Discontinued",
|
|
108
|
+
"out_of_stock": "Out of Stock",
|
|
109
|
+
"coming_soon": "Coming Soon"
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"actions": {
|
|
113
|
+
"create": "Create Product",
|
|
114
|
+
"edit": "Edit Product",
|
|
115
|
+
"delete": "Delete Product",
|
|
116
|
+
"view": "View Product",
|
|
117
|
+
"list": "List Products",
|
|
118
|
+
"duplicate": "Duplicate Product",
|
|
119
|
+
"activate": "Activate Product",
|
|
120
|
+
"deactivate": "Deactivate Product",
|
|
121
|
+
"restock": "Restock Product"
|
|
122
|
+
},
|
|
123
|
+
"messages": {
|
|
124
|
+
"created": "Product created successfully",
|
|
125
|
+
"updated": "Product updated successfully",
|
|
126
|
+
"deleted": "Product deleted successfully",
|
|
127
|
+
"activated": "Product activated successfully",
|
|
128
|
+
"deactivated": "Product deactivated successfully",
|
|
129
|
+
"restocked": "Product restocked successfully",
|
|
130
|
+
"lowStock": "Product stock is running low",
|
|
131
|
+
"outOfStock": "Product is out of stock",
|
|
132
|
+
"notFound": "Product not found",
|
|
133
|
+
"error": "An error occurred while processing the product"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
{
|
|
2
|
+
"entity": {
|
|
3
|
+
"name": "Producto",
|
|
4
|
+
"plural": "Productos",
|
|
5
|
+
"description": "Gestiona productos y servicios"
|
|
6
|
+
},
|
|
7
|
+
"fields": {
|
|
8
|
+
"name": {
|
|
9
|
+
"label": "Nombre del Producto",
|
|
10
|
+
"description": "Nombre del producto o servicio",
|
|
11
|
+
"placeholder": "Ingrese nombre..."
|
|
12
|
+
},
|
|
13
|
+
"sku": {
|
|
14
|
+
"label": "SKU",
|
|
15
|
+
"description": "Código de producto",
|
|
16
|
+
"placeholder": "Ingrese SKU..."
|
|
17
|
+
},
|
|
18
|
+
"description": {
|
|
19
|
+
"label": "Descripción",
|
|
20
|
+
"description": "Descripción del producto",
|
|
21
|
+
"placeholder": "Ingrese descripción..."
|
|
22
|
+
},
|
|
23
|
+
"category": {
|
|
24
|
+
"label": "Categoría",
|
|
25
|
+
"description": "Categoría del producto",
|
|
26
|
+
"placeholder": "Ingrese categoría..."
|
|
27
|
+
},
|
|
28
|
+
"type": {
|
|
29
|
+
"label": "Tipo",
|
|
30
|
+
"description": "Tipo de producto o servicio",
|
|
31
|
+
"placeholder": "Seleccionar tipo..."
|
|
32
|
+
},
|
|
33
|
+
"status": {
|
|
34
|
+
"label": "Estado",
|
|
35
|
+
"description": "Estado del producto",
|
|
36
|
+
"placeholder": "Seleccionar estado..."
|
|
37
|
+
},
|
|
38
|
+
"price": {
|
|
39
|
+
"label": "Precio Unitario",
|
|
40
|
+
"description": "Precio por unidad",
|
|
41
|
+
"placeholder": "0.00"
|
|
42
|
+
},
|
|
43
|
+
"cost": {
|
|
44
|
+
"label": "Costo Unitario",
|
|
45
|
+
"description": "Costo por unidad",
|
|
46
|
+
"placeholder": "0.00"
|
|
47
|
+
},
|
|
48
|
+
"currency": {
|
|
49
|
+
"label": "Moneda",
|
|
50
|
+
"description": "Código de moneda",
|
|
51
|
+
"placeholder": "Seleccionar moneda..."
|
|
52
|
+
},
|
|
53
|
+
"stockQuantity": {
|
|
54
|
+
"label": "Cantidad en Stock",
|
|
55
|
+
"description": "Cantidad disponible",
|
|
56
|
+
"placeholder": "0"
|
|
57
|
+
},
|
|
58
|
+
"minStockLevel": {
|
|
59
|
+
"label": "Nivel Mínimo",
|
|
60
|
+
"description": "Nivel mínimo de stock",
|
|
61
|
+
"placeholder": "0"
|
|
62
|
+
},
|
|
63
|
+
"features": {
|
|
64
|
+
"label": "Características",
|
|
65
|
+
"description": "Lista de características"
|
|
66
|
+
},
|
|
67
|
+
"specifications": {
|
|
68
|
+
"label": "Especificaciones",
|
|
69
|
+
"description": "Especificaciones técnicas"
|
|
70
|
+
},
|
|
71
|
+
"images": {
|
|
72
|
+
"label": "Imágenes",
|
|
73
|
+
"description": "Imágenes del producto"
|
|
74
|
+
},
|
|
75
|
+
"isActive": {
|
|
76
|
+
"label": "Activo",
|
|
77
|
+
"description": "Si el producto está activo"
|
|
78
|
+
},
|
|
79
|
+
"weight": {
|
|
80
|
+
"label": "Peso",
|
|
81
|
+
"description": "Peso del producto",
|
|
82
|
+
"placeholder": "0.00"
|
|
83
|
+
},
|
|
84
|
+
"dimensions": {
|
|
85
|
+
"label": "Dimensiones",
|
|
86
|
+
"description": "Dimensiones del producto",
|
|
87
|
+
"placeholder": "L x A x P"
|
|
88
|
+
},
|
|
89
|
+
"vendor": {
|
|
90
|
+
"label": "Proveedor",
|
|
91
|
+
"description": "Proveedor del producto",
|
|
92
|
+
"placeholder": "Ingrese proveedor..."
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"options": {
|
|
96
|
+
"type": {
|
|
97
|
+
"physical": "Producto Físico",
|
|
98
|
+
"digital": "Producto Digital",
|
|
99
|
+
"service": "Servicio",
|
|
100
|
+
"subscription": "Suscripción",
|
|
101
|
+
"bundle": "Paquete",
|
|
102
|
+
"variant": "Variante"
|
|
103
|
+
},
|
|
104
|
+
"status": {
|
|
105
|
+
"active": "Activo",
|
|
106
|
+
"inactive": "Inactivo",
|
|
107
|
+
"discontinued": "Descontinuado",
|
|
108
|
+
"out_of_stock": "Sin Stock",
|
|
109
|
+
"coming_soon": "Próximamente"
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"actions": {
|
|
113
|
+
"create": "Crear Producto",
|
|
114
|
+
"edit": "Editar Producto",
|
|
115
|
+
"delete": "Eliminar Producto",
|
|
116
|
+
"view": "Ver Producto",
|
|
117
|
+
"list": "Listar Productos",
|
|
118
|
+
"duplicate": "Duplicar Producto",
|
|
119
|
+
"activate": "Activar Producto",
|
|
120
|
+
"deactivate": "Desactivar Producto",
|
|
121
|
+
"restock": "Reabastecer"
|
|
122
|
+
},
|
|
123
|
+
"messages": {
|
|
124
|
+
"created": "Producto creado exitosamente",
|
|
125
|
+
"updated": "Producto actualizado exitosamente",
|
|
126
|
+
"deleted": "Producto eliminado exitosamente",
|
|
127
|
+
"activated": "Producto activado exitosamente",
|
|
128
|
+
"deactivated": "Producto desactivado exitosamente",
|
|
129
|
+
"restocked": "Producto reabastecido exitosamente",
|
|
130
|
+
"lowStock": "Stock bajo del producto",
|
|
131
|
+
"outOfStock": "Producto sin stock",
|
|
132
|
+
"notFound": "Producto no encontrado",
|
|
133
|
+
"error": "Error al procesar el producto"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- Products Table Migration
|
|
3
|
+
-- CRM theme: Products and services catalog
|
|
4
|
+
-- Updated with team support and RLS
|
|
5
|
+
-- ============================================================================
|
|
6
|
+
|
|
7
|
+
-- ============================================
|
|
8
|
+
-- ENUM TYPES
|
|
9
|
+
-- ============================================
|
|
10
|
+
DO $$ BEGIN
|
|
11
|
+
CREATE TYPE product_type AS ENUM ('product', 'service', 'subscription');
|
|
12
|
+
EXCEPTION
|
|
13
|
+
WHEN duplicate_object THEN null;
|
|
14
|
+
END $$;
|
|
15
|
+
|
|
16
|
+
DO $$ BEGIN
|
|
17
|
+
CREATE TYPE product_unit AS ENUM ('piece', 'hour', 'day', 'month', 'year', 'user', 'license');
|
|
18
|
+
EXCEPTION
|
|
19
|
+
WHEN duplicate_object THEN null;
|
|
20
|
+
END $$;
|
|
21
|
+
|
|
22
|
+
-- ============================================
|
|
23
|
+
-- TABLE
|
|
24
|
+
-- ============================================
|
|
25
|
+
CREATE TABLE IF NOT EXISTS "products" (
|
|
26
|
+
"id" TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
27
|
+
|
|
28
|
+
-- Product info
|
|
29
|
+
"code" VARCHAR(50),
|
|
30
|
+
"name" VARCHAR(255) NOT NULL,
|
|
31
|
+
"category" VARCHAR(100),
|
|
32
|
+
"type" product_type DEFAULT 'product',
|
|
33
|
+
"description" TEXT,
|
|
34
|
+
|
|
35
|
+
-- Pricing
|
|
36
|
+
"price" DECIMAL(15,2) NOT NULL DEFAULT 0,
|
|
37
|
+
"cost" DECIMAL(15,2) DEFAULT 0,
|
|
38
|
+
"currency" VARCHAR(3) DEFAULT 'USD',
|
|
39
|
+
"unit" product_unit DEFAULT 'piece',
|
|
40
|
+
|
|
41
|
+
-- Status
|
|
42
|
+
"isActive" BOOLEAN DEFAULT true,
|
|
43
|
+
|
|
44
|
+
-- Media
|
|
45
|
+
"image" VARCHAR(500),
|
|
46
|
+
"brochureUrl" VARCHAR(500),
|
|
47
|
+
|
|
48
|
+
-- Sales config
|
|
49
|
+
"minimumQuantity" INTEGER DEFAULT 1,
|
|
50
|
+
"commissionRate" DECIMAL(5,2) DEFAULT 0,
|
|
51
|
+
|
|
52
|
+
-- Ownership
|
|
53
|
+
"userId" TEXT NOT NULL REFERENCES "users"("id") ON DELETE CASCADE,
|
|
54
|
+
"teamId" TEXT NOT NULL REFERENCES "teams"("id") ON DELETE CASCADE,
|
|
55
|
+
|
|
56
|
+
-- Timestamps
|
|
57
|
+
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
58
|
+
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
-- ============================================
|
|
62
|
+
-- INDEXES
|
|
63
|
+
-- ============================================
|
|
64
|
+
CREATE INDEX IF NOT EXISTS "products_teamId_idx" ON "products" ("teamId");
|
|
65
|
+
CREATE INDEX IF NOT EXISTS "products_userId_idx" ON "products" ("userId");
|
|
66
|
+
CREATE INDEX IF NOT EXISTS "products_code_idx" ON "products" ("code");
|
|
67
|
+
CREATE INDEX IF NOT EXISTS "products_name_idx" ON "products" ("name");
|
|
68
|
+
CREATE INDEX IF NOT EXISTS "products_category_idx" ON "products" ("category");
|
|
69
|
+
CREATE INDEX IF NOT EXISTS "products_type_idx" ON "products" ("type");
|
|
70
|
+
CREATE INDEX IF NOT EXISTS "products_isActive_idx" ON "products" ("isActive") WHERE "isActive" = true;
|
|
71
|
+
CREATE INDEX IF NOT EXISTS "products_price_idx" ON "products" ("price");
|
|
72
|
+
|
|
73
|
+
-- ============================================
|
|
74
|
+
-- RLS
|
|
75
|
+
-- ============================================
|
|
76
|
+
ALTER TABLE "products" ENABLE ROW LEVEL SECURITY;
|
|
77
|
+
|
|
78
|
+
DROP POLICY IF EXISTS "products_select_policy" ON "products";
|
|
79
|
+
DROP POLICY IF EXISTS "products_insert_policy" ON "products";
|
|
80
|
+
DROP POLICY IF EXISTS "products_update_policy" ON "products";
|
|
81
|
+
DROP POLICY IF EXISTS "products_delete_policy" ON "products";
|
|
82
|
+
|
|
83
|
+
CREATE POLICY "products_select_policy" ON "products"
|
|
84
|
+
FOR SELECT
|
|
85
|
+
USING ("teamId" = ANY(public.get_user_team_ids()) OR public.is_superadmin());
|
|
86
|
+
|
|
87
|
+
CREATE POLICY "products_insert_policy" ON "products"
|
|
88
|
+
FOR INSERT
|
|
89
|
+
WITH CHECK ("teamId" = ANY(public.get_user_team_ids()));
|
|
90
|
+
|
|
91
|
+
CREATE POLICY "products_update_policy" ON "products"
|
|
92
|
+
FOR UPDATE
|
|
93
|
+
USING ("teamId" = ANY(public.get_user_team_ids()) OR public.is_superadmin());
|
|
94
|
+
|
|
95
|
+
CREATE POLICY "products_delete_policy" ON "products"
|
|
96
|
+
FOR DELETE
|
|
97
|
+
USING ("teamId" = ANY(public.get_user_team_ids()) OR public.is_superadmin());
|
|
98
|
+
|
|
99
|
+
-- ============================================
|
|
100
|
+
-- TRIGGER updatedAt
|
|
101
|
+
-- ============================================
|
|
102
|
+
CREATE OR REPLACE FUNCTION update_products_updated_at()
|
|
103
|
+
RETURNS TRIGGER AS $$
|
|
104
|
+
BEGIN
|
|
105
|
+
NEW."updatedAt" = NOW();
|
|
106
|
+
RETURN NEW;
|
|
107
|
+
END;
|
|
108
|
+
$$ LANGUAGE plpgsql;
|
|
109
|
+
|
|
110
|
+
DROP TRIGGER IF EXISTS products_updated_at_trigger ON "products";
|
|
111
|
+
CREATE TRIGGER products_updated_at_trigger
|
|
112
|
+
BEFORE UPDATE ON "products"
|
|
113
|
+
FOR EACH ROW
|
|
114
|
+
EXECUTE FUNCTION update_products_updated_at();
|
|
115
|
+
|
|
116
|
+
COMMENT ON TABLE "products" IS 'Products and services catalog';
|
|
117
|
+
COMMENT ON COLUMN "products"."commissionRate" IS 'Sales commission percentage';
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
-- Migration: 002_products_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."products_metas" (
|
|
10
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
11
|
+
"entityId" TEXT NOT NULL REFERENCES public."products"(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 products_metas_unique_key UNIQUE ("entityId", "metaKey")
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
COMMENT ON TABLE public."products_metas" IS 'Contacts metadata table - stores additional key-value pairs for products';
|
|
23
|
+
COMMENT ON COLUMN public."products_metas"."entityId" IS 'Generic foreign key to parent product entity';
|
|
24
|
+
COMMENT ON COLUMN public."products_metas"."metaKey" IS 'Metadata key name';
|
|
25
|
+
COMMENT ON COLUMN public."products_metas"."metaValue" IS 'Metadata value as JSONB';
|
|
26
|
+
COMMENT ON COLUMN public."products_metas"."dataType" IS 'Type hint for the value: json, string, number, boolean';
|
|
27
|
+
COMMENT ON COLUMN public."products_metas"."isPublic" IS 'Whether this metadata is publicly readable';
|
|
28
|
+
COMMENT ON COLUMN public."products_metas"."isSearchable" IS 'Whether this metadata is searchable';
|
|
29
|
+
|
|
30
|
+
-- ============================================
|
|
31
|
+
-- TRIGGER updatedAt (uses Better Auth function)
|
|
32
|
+
-- ============================================
|
|
33
|
+
DROP TRIGGER IF EXISTS products_metas_set_updated_at ON public."products_metas";
|
|
34
|
+
CREATE TRIGGER products_metas_set_updated_at
|
|
35
|
+
BEFORE UPDATE ON public."products_metas"
|
|
36
|
+
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
|
37
|
+
|
|
38
|
+
-- ============================================
|
|
39
|
+
-- INDEXES
|
|
40
|
+
-- ============================================
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_products_metas_entity_id ON public."products_metas"("entityId");
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_products_metas_key ON public."products_metas"("metaKey");
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_products_metas_composite ON public."products_metas"("entityId", "metaKey", "isPublic");
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_products_metas_is_public ON public."products_metas"("isPublic") WHERE "isPublic" = true;
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_products_metas_is_searchable ON public."products_metas"("isSearchable") WHERE "isSearchable" = true;
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_products_metas_searchable_key ON public."products_metas"("metaKey") WHERE "isSearchable" = true;
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_products_metas_value_gin ON public."products_metas" USING GIN ("metaValue");
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_products_metas_value_ops ON public."products_metas" USING GIN ("metaValue" jsonb_path_ops);
|
|
49
|
+
|
|
50
|
+
-- ============================================
|
|
51
|
+
-- RLS
|
|
52
|
+
-- ============================================
|
|
53
|
+
ALTER TABLE public."products_metas" ENABLE ROW LEVEL SECURITY;
|
|
54
|
+
|
|
55
|
+
-- Cleanup existing policies
|
|
56
|
+
DROP POLICY IF EXISTS "Users can view product metas" ON public."products_metas";
|
|
57
|
+
DROP POLICY IF EXISTS "Users can create product metas" ON public."products_metas";
|
|
58
|
+
DROP POLICY IF EXISTS "Users can update product metas" ON public."products_metas";
|
|
59
|
+
DROP POLICY IF EXISTS "Users can delete product metas" ON public."products_metas";
|
|
60
|
+
|
|
61
|
+
-- ============================
|
|
62
|
+
-- AUTHENTICATED USER POLICIES
|
|
63
|
+
-- ============================
|
|
64
|
+
-- Inherit permissions from parent entity
|
|
65
|
+
CREATE POLICY "Users can view product metas"
|
|
66
|
+
ON public."products_metas"
|
|
67
|
+
FOR SELECT TO authenticated
|
|
68
|
+
USING (
|
|
69
|
+
EXISTS (
|
|
70
|
+
SELECT 1 FROM public."products" c
|
|
71
|
+
WHERE c.id = "entityId"
|
|
72
|
+
AND c."userId" = public.get_auth_user_id()
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
CREATE POLICY "Users can create product metas"
|
|
77
|
+
ON public."products_metas"
|
|
78
|
+
FOR INSERT TO authenticated
|
|
79
|
+
WITH CHECK (
|
|
80
|
+
EXISTS (
|
|
81
|
+
SELECT 1 FROM public."products" c
|
|
82
|
+
WHERE c.id = "entityId"
|
|
83
|
+
AND c."userId" = public.get_auth_user_id()
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
CREATE POLICY "Users can update product metas"
|
|
88
|
+
ON public."products_metas"
|
|
89
|
+
FOR UPDATE TO authenticated
|
|
90
|
+
USING (
|
|
91
|
+
EXISTS (
|
|
92
|
+
SELECT 1 FROM public."products" 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."products" c
|
|
100
|
+
WHERE c.id = "entityId"
|
|
101
|
+
AND c."userId" = public.get_auth_user_id()
|
|
102
|
+
)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
CREATE POLICY "Users can delete product metas"
|
|
106
|
+
ON public."products_metas"
|
|
107
|
+
FOR DELETE TO authenticated
|
|
108
|
+
USING (
|
|
109
|
+
EXISTS (
|
|
110
|
+
SELECT 1 FROM public."products" c
|
|
111
|
+
WHERE c.id = "entityId"
|
|
112
|
+
AND c."userId" = public.get_auth_user_id()
|
|
113
|
+
)
|
|
114
|
+
);
|