diff --git a/drizzle/0000_icy_lizard.sql b/drizzle/0000_icy_lizard.sql index 74c1b39..d361d7f 100644 --- a/drizzle/0000_icy_lizard.sql +++ b/drizzle/0000_icy_lizard.sql @@ -3,7 +3,7 @@ CREATE TABLE "memberships" ( "user_id" text NOT NULL, "organization_id" text NOT NULL, "role" text DEFAULT 'user' NOT NULL, - "business_role" text DEFAULT 'viewer' NOT NULL, + "business_role" text DEFAULT 'sales_support' NOT NULL, "permissions" text[] DEFAULT '{}' NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL @@ -45,4 +45,4 @@ CREATE TABLE "users" ( ); --> statement-breakpoint CREATE UNIQUE INDEX "organizations_slug_idx" ON "organizations" USING btree ("slug");--> statement-breakpoint -CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email"); \ No newline at end of file +CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email"); diff --git a/drizzle/0008_clean_justin_hammer.sql b/drizzle/0008_clean_justin_hammer.sql new file mode 100644 index 0000000..b128240 --- /dev/null +++ b/drizzle/0008_clean_justin_hammer.sql @@ -0,0 +1,5 @@ +ALTER TABLE "crm_enquiries" ADD COLUMN "assigned_to_user_id" text;--> statement-breakpoint +ALTER TABLE "crm_enquiries" ADD COLUMN "assigned_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "crm_enquiries" ADD COLUMN "assigned_by" text;--> statement-breakpoint +ALTER TABLE "crm_enquiries" ADD COLUMN "assignment_remark" text;--> statement-breakpoint +ALTER TABLE "memberships" ALTER COLUMN "business_role" SET DEFAULT 'sales_support'; diff --git a/drizzle/meta/0008_snapshot.json b/drizzle/meta/0008_snapshot.json new file mode 100644 index 0000000..5ed3ebd --- /dev/null +++ b/drizzle/meta/0008_snapshot.json @@ -0,0 +1,3019 @@ +{ + "id": "0237eb23-9304-48dd-a770-8bf5013172da", + "prevId": "dae63ce5-8077-4920-abd6-a232df70e20a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.crm_approval_actions": { + "name": "crm_approval_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approval_request_id": { + "name": "approval_request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acted_by": { + "name": "acted_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acted_at": { + "name": "acted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_approval_requests": { + "name": "crm_approval_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_step": { + "name": "current_step", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_approval_steps": { + "name": "crm_approval_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role_code": { + "name": "role_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_name": { + "name": "role_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_required": { + "name": "is_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "crm_approval_steps_workflow_step_idx": { + "name": "crm_approval_steps_workflow_step_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_approval_workflows": { + "name": "crm_approval_workflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "crm_approval_workflows_org_code_idx": { + "name": "crm_approval_workflows_org_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_customer_contacts": { + "name": "crm_customer_contacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "department": { + "name": "department", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mobile": { + "name": "mobile", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_customers": { + "name": "crm_customers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_id": { + "name": "branch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "abbr": { + "name": "abbr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tax_id": { + "name": "tax_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_type": { + "name": "customer_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_status": { + "name": "customer_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "province": { + "name": "province", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "district": { + "name": "district", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub_district": { + "name": "sub_district", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postal_code": { + "name": "postal_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fax": { + "name": "fax", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lead_channel": { + "name": "lead_channel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_group": { + "name": "customer_group", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_customers_org_code_idx": { + "name": "crm_customers_org_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_document_template_mappings": { + "name": "crm_document_template_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_version_id": { + "name": "template_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "placeholder_key": { + "name": "placeholder_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data_type": { + "name": "data_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sheet_name": { + "name": "sheet_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_value": { + "name": "default_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format_mask": { + "name": "format_mask", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "crm_document_template_mappings_version_placeholder_idx": { + "name": "crm_document_template_mappings_version_placeholder_idx", + "columns": [ + { + "expression": "template_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "placeholder_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_document_template_table_columns": { + "name": "crm_document_template_table_columns", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mapping_id": { + "name": "mapping_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "column_name": { + "name": "column_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_field": { + "name": "source_field", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "column_letter": { + "name": "column_letter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "format_mask": { + "name": "format_mask", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "crm_document_template_table_columns_mapping_name_idx": { + "name": "crm_document_template_table_columns_mapping_name_idx", + "columns": [ + { + "expression": "mapping_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "column_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_document_template_versions": { + "name": "crm_document_template_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_json": { + "name": "schema_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "preview_image_url": { + "name": "preview_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_document_template_versions_template_version_idx": { + "name": "crm_document_template_versions_template_version_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_document_templates": { + "name": "crm_document_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_type": { + "name": "document_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_type": { + "name": "product_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_type": { + "name": "file_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_name": { + "name": "template_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_document_templates_org_doc_product_file_name_idx": { + "name": "crm_document_templates_org_doc_product_file_name_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "product_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_enquiries": { + "name": "crm_enquiries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_id": { + "name": "branch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requirement": { + "name": "requirement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_location": { + "name": "project_location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_type": { + "name": "product_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lead_channel": { + "name": "lead_channel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "estimated_value": { + "name": "estimated_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "chance_percent": { + "name": "chance_percent", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expected_close_date": { + "name": "expected_close_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "competitor": { + "name": "competitor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_hot_project": { + "name": "is_hot_project", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "assigned_to_user_id": { + "name": "assigned_to_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignment_remark": { + "name": "assignment_remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_enquiries_org_code_idx": { + "name": "crm_enquiries_org_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_enquiry_followups": { + "name": "crm_enquiry_followups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enquiry_id": { + "name": "enquiry_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "followup_date": { + "name": "followup_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "followup_type": { + "name": "followup_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_followup_date": { + "name": "next_followup_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_action": { + "name": "next_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotation_attachments": { + "name": "crm_quotation_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quotation_id": { + "name": "quotation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_file_name": { + "name": "original_file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_type": { + "name": "file_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotation_customers": { + "name": "crm_quotation_customers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quotation_id": { + "name": "quotation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotation_followups": { + "name": "crm_quotation_followups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quotation_id": { + "name": "quotation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "followup_date": { + "name": "followup_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "followup_type": { + "name": "followup_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_followup_date": { + "name": "next_followup_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_action": { + "name": "next_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotation_items": { + "name": "crm_quotation_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quotation_id": { + "name": "quotation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_number": { + "name": "item_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "product_type": { + "name": "product_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit_price": { + "name": "unit_price", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "discount": { + "name": "discount", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "discount_type": { + "name": "discount_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tax_rate": { + "name": "tax_rate", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_price": { + "name": "total_price", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotation_topic_items": { + "name": "crm_quotation_topic_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic_id": { + "name": "topic_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotation_topics": { + "name": "crm_quotation_topics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quotation_id": { + "name": "quotation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic_type": { + "name": "topic_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotations": { + "name": "crm_quotations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_id": { + "name": "branch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enquiry_id": { + "name": "enquiry_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quotation_date": { + "name": "quotation_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "valid_until": { + "name": "valid_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "quotation_type": { + "name": "quotation_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_location": { + "name": "project_location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attention": { + "name": "attention", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "parent_quotation_id": { + "name": "parent_quotation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision_remark": { + "name": "revision_remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "exchange_rate": { + "name": "exchange_rate", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "subtotal": { + "name": "subtotal", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "discount": { + "name": "discount", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "discount_type": { + "name": "discount_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tax_rate": { + "name": "tax_rate", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tax_amount": { + "name": "tax_amount", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_amount": { + "name": "total_amount", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "chance_percent": { + "name": "chance_percent", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_hot_project": { + "name": "is_hot_project", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "competitor": { + "name": "competitor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "salesman_id": { + "name": "salesman_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_sent": { + "name": "is_sent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_via": { + "name": "sent_via", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "approved_pdf_url": { + "name": "approved_pdf_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_snapshot": { + "name": "approved_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "approved_template_version_id": { + "name": "approved_template_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rejected_at": { + "name": "rejected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_quotations_org_code_idx": { + "name": "crm_quotations_org_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_sequences": { + "name": "document_sequences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_id": { + "name": "branch_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "document_type": { + "name": "document_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period": { + "name": "period", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_number": { + "name": "current_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "padding_length": { + "name": "padding_length", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_sequences_org_doc_period_branch_idx": { + "name": "document_sequences_org_doc_period_branch_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "branch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memberships": { + "name": "memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "business_role": { + "name": "business_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'viewer'" + }, + "permissions": { + "name": "permissions", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ms_options": { + "name": "ms_options", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ms_options_org_category_code_idx": { + "name": "ms_options_org_category_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organizations_slug_idx": { + "name": "organizations_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.products": { + "name": "products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "products_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "photo_url": { + "name": "photo_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tr_audit_logs": { + "name": "tr_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_id": { + "name": "branch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "before_data": { + "name": "before_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_data": { + "name": "after_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "system_role": { + "name": "system_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index a825cde..de1b017 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -57,6 +57,13 @@ "when": 1781589458455, "tag": "0007_luxuriant_malice", "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1781595962545, + "tag": "0008_clean_justin_hammer", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/api/crm/enquiries/[id]/_schemas.ts b/src/app/api/crm/enquiries/[id]/_schemas.ts new file mode 100644 index 0000000..d83c7fb --- /dev/null +++ b/src/app/api/crm/enquiries/[id]/_schemas.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; + +export const enquiryAssignmentRequestSchema = z.object({ + assignedToUserId: z.string().min(1, 'Sales user is required'), + remark: z.string().trim().max(1000).optional() +}); diff --git a/src/app/api/crm/enquiries/[id]/assign/route.ts b/src/app/api/crm/enquiries/[id]/assign/route.ts new file mode 100644 index 0000000..4b46925 --- /dev/null +++ b/src/app/api/crm/enquiries/[id]/assign/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { auditAction } from '@/features/foundation/audit-log/service'; +import { assignEnquiryToUser, getEnquiryDetail } from '@/features/crm/enquiries/server/service'; +import { PERMISSIONS } from '@/lib/auth/rbac'; +import { AuthError, requireOrganizationAccess } from '@/lib/auth/session'; +import { enquiryAssignmentRequestSchema } from '../_schemas'; + +type Params = { + params: Promise<{ id: string }>; +}; + +export async function POST(request: NextRequest, { params }: Params) { + try { + const { id } = await params; + const { organization, session } = await requireOrganizationAccess({ + permission: PERMISSIONS.crmEnquiryAssign + }); + const payload = enquiryAssignmentRequestSchema.parse(await request.json()); + const before = await getEnquiryDetail(id, organization.id); + const updated = await assignEnquiryToUser(id, organization.id, session.user.id, payload); + + await auditAction({ + organizationId: organization.id, + branchId: updated.branchId, + userId: session.user.id, + entityType: 'crm_enquiry', + entityId: id, + action: 'assign', + beforeData: { + oldAssignedToUserId: before.assignedToUserId + }, + afterData: { + newAssignedToUserId: updated.assignedToUserId, + remark: updated.assignmentRemark + } + }); + + return NextResponse.json({ + success: true, + message: 'Enquiry assigned successfully', + enquiry: updated + }); + } catch (error) { + if (error instanceof AuthError) { + return NextResponse.json({ message: error.message }, { status: error.status }); + } + + if (error instanceof z.ZodError) { + return NextResponse.json( + { message: error.issues[0]?.message ?? 'Invalid payload' }, + { status: 400 } + ); + } + + if (error instanceof Error) { + return NextResponse.json({ message: error.message }, { status: 400 }); + } + + return NextResponse.json({ message: 'Unable to assign enquiry' }, { status: 500 }); + } +} diff --git a/src/app/api/crm/enquiries/[id]/reassign/route.ts b/src/app/api/crm/enquiries/[id]/reassign/route.ts new file mode 100644 index 0000000..29997fd --- /dev/null +++ b/src/app/api/crm/enquiries/[id]/reassign/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { auditAction } from '@/features/foundation/audit-log/service'; +import { getEnquiryDetail, reassignEnquiryToUser } from '@/features/crm/enquiries/server/service'; +import { PERMISSIONS } from '@/lib/auth/rbac'; +import { AuthError, requireOrganizationAccess } from '@/lib/auth/session'; +import { enquiryAssignmentRequestSchema } from '../_schemas'; + +type Params = { + params: Promise<{ id: string }>; +}; + +export async function POST(request: NextRequest, { params }: Params) { + try { + const { id } = await params; + const { organization, session } = await requireOrganizationAccess({ + permission: PERMISSIONS.crmEnquiryReassign + }); + const payload = enquiryAssignmentRequestSchema.parse(await request.json()); + const before = await getEnquiryDetail(id, organization.id); + const updated = await reassignEnquiryToUser(id, organization.id, session.user.id, payload); + + await auditAction({ + organizationId: organization.id, + branchId: updated.branchId, + userId: session.user.id, + entityType: 'crm_enquiry', + entityId: id, + action: 'reassign', + beforeData: { + oldAssignedToUserId: before.assignedToUserId + }, + afterData: { + newAssignedToUserId: updated.assignedToUserId, + remark: updated.assignmentRemark + } + }); + + return NextResponse.json({ + success: true, + message: 'Enquiry reassigned successfully', + enquiry: updated + }); + } catch (error) { + if (error instanceof AuthError) { + return NextResponse.json({ message: error.message }, { status: error.status }); + } + + if (error instanceof z.ZodError) { + return NextResponse.json( + { message: error.issues[0]?.message ?? 'Invalid payload' }, + { status: 400 } + ); + } + + if (error instanceof Error) { + return NextResponse.json({ message: error.message }, { status: 400 }); + } + + return NextResponse.json({ message: 'Unable to reassign enquiry' }, { status: 500 }); + } +} diff --git a/src/app/api/organizations/route.ts b/src/app/api/organizations/route.ts index 6a8ad29..316261b 100644 --- a/src/app/api/organizations/route.ts +++ b/src/app/api/organizations/route.ts @@ -29,7 +29,7 @@ export async function GET(request: NextRequest) { plan: organization.plan, imageUrl: organization.imageUrl, role: 'admin', - businessRole: 'it_admin', + businessRole: 'sales_manager', canManageUsers: true })) }); @@ -51,7 +51,12 @@ export async function GET(request: NextRequest) { const organizationRows = await db .select() .from(organizations) - .where(inArray(organizations.id, filteredMemberships.map((membership) => membership.organizationId))); + .where( + inArray( + organizations.id, + filteredMemberships.map((membership) => membership.organizationId) + ) + ); const membershipMap = new Map( filteredMemberships.map((membership) => [membership.organizationId, membership]) @@ -68,7 +73,7 @@ export async function GET(request: NextRequest) { plan: organization.plan, imageUrl: organization.imageUrl, role: membership?.role ?? 'user', - businessRole: membership?.businessRole ?? 'viewer', + businessRole: membership?.businessRole ?? 'sales_support', canManageUsers: membership?.role === 'admin' }; }) @@ -109,8 +114,8 @@ export async function POST(request: NextRequest) { userId: session.user.id, organizationId, role: 'admin', - businessRole: 'it_admin', - permissions: getDefaultPermissions('admin', 'it_admin') + businessRole: 'sales_manager', + permissions: getDefaultPermissions('admin', 'sales_manager') }); await db diff --git a/src/app/api/users/_lib.ts b/src/app/api/users/_lib.ts index 3bb8506..5873949 100644 --- a/src/app/api/users/_lib.ts +++ b/src/app/api/users/_lib.ts @@ -1,6 +1,7 @@ import { eq, inArray } from 'drizzle-orm'; import { memberships, organizations, users } from '@/db/schema'; import { + type BusinessRole, getDefaultBusinessRole, getDefaultPermissions, isBusinessRole, @@ -13,7 +14,7 @@ import { db } from '@/lib/db'; export type UserMembershipInput = { organizationId: string; membershipRole: 'admin' | 'user'; - businessRole: 'it_admin' | 'helpdesk' | 'infrastructure' | 'application' | 'auditor' | 'viewer'; + businessRole: BusinessRole; }; export type UserMutationInput = { @@ -81,8 +82,7 @@ export function parseUserMutationPayload(body: unknown): UserMutationInput { const value = membership as Record; const organizationId = typeof value.organizationId === 'string' ? value.organizationId.trim() : ''; - const membershipRole = - typeof value.membershipRole === 'string' ? value.membershipRole : ''; + const membershipRole = typeof value.membershipRole === 'string' ? value.membershipRole : ''; const businessRole = typeof value.businessRole === 'string' ? value.businessRole @@ -102,7 +102,9 @@ export function parseUserMutationPayload(body: unknown): UserMutationInput { }) .filter((membership): membership is UserMembershipInput => membership !== null); - const uniqueMemberships = [...new Map(memberships.map((membership) => [membership.organizationId, membership])).values()]; + const uniqueMemberships = [ + ...new Map(memberships.map((membership) => [membership.organizationId, membership])).values() + ]; if (!uniqueMemberships.length) { throw new Error('Select at least one organization'); diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts index 232d286..c722330 100644 --- a/src/app/api/users/route.ts +++ b/src/app/api/users/route.ts @@ -3,6 +3,7 @@ import { asc, eq } from 'drizzle-orm'; import { NextRequest, NextResponse } from 'next/server'; import { memberships, organizations, users } from '@/db/schema'; import { db } from '@/lib/db'; +import { isBusinessRole } from '@/lib/auth/rbac'; import { AuthError } from '@/lib/auth/session'; import { buildMembershipValues, @@ -38,13 +39,7 @@ function formatUsers(rows: UserRow[]) { organizationId: string; organizationName: string; membershipRole: 'admin' | 'user'; - businessRole: - | 'it_admin' - | 'helpdesk' - | 'infrastructure' - | 'application' - | 'auditor' - | 'viewer'; + businessRole: string; }>; } >(); @@ -60,14 +55,7 @@ function formatUsers(rows: UserRow[]) { organizationId: row.organizationId, organizationName: row.organizationName, membershipRole: organization.role, - businessRole: - row.businessRole === 'it_admin' || - row.businessRole === 'helpdesk' || - row.businessRole === 'infrastructure' || - row.businessRole === 'application' || - row.businessRole === 'auditor' - ? row.businessRole - : 'viewer' + businessRole: isBusinessRole(row.businessRole) ? row.businessRole : 'sales_support' } as const; if (existing) { @@ -146,13 +134,13 @@ function sortUsers(usersList: ReturnType, sort?: string) { ? `${a.systemRole}:${a.activeMembershipRole ?? ''}` : primarySort.id === 'organizations' ? a.memberships.map((membership) => membership.organizationName).join(', ') - : a[primarySort.id as 'name' | 'email'] ?? a.name; + : (a[primarySort.id as 'name' | 'email'] ?? a.name); const right = primarySort.id === 'role' ? `${b.systemRole}:${b.activeMembershipRole ?? ''}` : primarySort.id === 'organizations' ? b.memberships.map((membership) => membership.organizationName).join(', ') - : b[primarySort.id as 'name' | 'email'] ?? b.name; + : (b[primarySort.id as 'name' | 'email'] ?? b.name); return String(left).localeCompare(String(right)); }); @@ -244,7 +232,11 @@ export async function POST(request: NextRequest) { ); } - await validateManagedOrganizations(payload.memberships, manageableOrganizationIds, isSuperAdmin); + await validateManagedOrganizations( + payload.memberships, + manageableOrganizationIds, + isSuperAdmin + ); const existingUser = await db.query.users.findFirst({ where: eq(users.email, payload.email) diff --git a/src/app/dashboard/crm/enquiries/[id]/page.tsx b/src/app/dashboard/crm/enquiries/[id]/page.tsx index 3f1ea7b..2c62b62 100644 --- a/src/app/dashboard/crm/enquiries/[id]/page.tsx +++ b/src/app/dashboard/crm/enquiries/[id]/page.tsx @@ -40,6 +40,16 @@ export default async function EnquiryDetailRoute({ params }: PageProps) { (!!session?.user?.activeOrganizationId && (session.user.activeMembershipRole === 'admin' || session.user.activePermissions.includes(PERMISSIONS.crmEnquiryFollowupDelete))); + const canAssign = + session?.user?.systemRole === 'super_admin' || + (!!session?.user?.activeOrganizationId && + (session.user.activeMembershipRole === 'admin' || + session.user.activePermissions.includes(PERMISSIONS.crmEnquiryAssign))); + const canReassign = + session?.user?.systemRole === 'super_admin' || + (!!session?.user?.activeOrganizationId && + (session.user.activeMembershipRole === 'admin' || + session.user.activePermissions.includes(PERMISSIONS.crmEnquiryReassign))); const queryClient = getQueryClient(); if (canRead) { @@ -74,6 +84,8 @@ export default async function EnquiryDetailRoute({ params }: PageProps) { referenceData={referenceData} relatedQuotations={relatedQuotations} canUpdate={canUpdate} + canAssign={canAssign} + canReassign={canReassign} canManageFollowups={{ create: canFollowupCreate, update: canFollowupUpdate, diff --git a/src/app/dashboard/crm/enquiries/page.tsx b/src/app/dashboard/crm/enquiries/page.tsx index 7dff8e4..d2a238c 100644 --- a/src/app/dashboard/crm/enquiries/page.tsx +++ b/src/app/dashboard/crm/enquiries/page.tsx @@ -38,6 +38,16 @@ export default async function EnquiriesRoute(props: PageProps) { (!!session?.user?.activeOrganizationId && (session.user.activeMembershipRole === 'admin' || session.user.activePermissions.includes(PERMISSIONS.crmEnquiryDelete))); + const canAssign = + session?.user?.systemRole === 'super_admin' || + (!!session?.user?.activeOrganizationId && + (session.user.activeMembershipRole === 'admin' || + session.user.activePermissions.includes(PERMISSIONS.crmEnquiryAssign))); + const canReassign = + session?.user?.systemRole === 'super_admin' || + (!!session?.user?.activeOrganizationId && + (session.user.activeMembershipRole === 'admin' || + session.user.activePermissions.includes(PERMISSIONS.crmEnquiryReassign))); searchParamsCache.parse(searchParams); @@ -63,7 +73,13 @@ export default async function EnquiriesRoute(props: PageProps) { } > {referenceData ? ( - + ) : null} ); diff --git a/src/db/schema.ts b/src/db/schema.ts index e489bb8..58d3548 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -49,7 +49,7 @@ export const memberships = pgTable('memberships', { userId: text('user_id').notNull(), organizationId: text('organization_id').notNull(), role: text('role').default('user').notNull(), - businessRole: text('business_role').default('viewer').notNull(), + businessRole: text('business_role').default('sales_support').notNull(), permissions: text('permissions').array().default([]).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() @@ -215,6 +215,10 @@ export const crmEnquiries = pgTable( notes: text('notes'), isHotProject: boolean('is_hot_project').default(false).notNull(), isActive: boolean('is_active').default(true).notNull(), + assignedToUserId: text('assigned_to_user_id'), + assignedAt: timestamp('assigned_at', { withTimezone: true }), + assignedBy: text('assigned_by'), + assignmentRemark: text('assignment_remark'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }), @@ -541,10 +545,9 @@ export const crmDocumentTemplateMappings = pgTable( deletedAt: timestamp('deleted_at', { withTimezone: true }) }, (table) => ({ - templatePlaceholderIdx: uniqueIndex('crm_document_template_mappings_version_placeholder_idx').on( - table.templateVersionId, - table.placeholderKey - ) + templatePlaceholderIdx: uniqueIndex( + 'crm_document_template_mappings_version_placeholder_idx' + ).on(table.templateVersionId, table.placeholderKey) }) ); diff --git a/src/features/crm/enquiries/api/mutations.ts b/src/features/crm/enquiries/api/mutations.ts index 578642b..a546d77 100644 --- a/src/features/crm/enquiries/api/mutations.ts +++ b/src/features/crm/enquiries/api/mutations.ts @@ -1,15 +1,21 @@ import { mutationOptions } from '@tanstack/react-query'; import { getQueryClient } from '@/lib/query-client'; import { + assignEnquiry, createEnquiry, createEnquiryFollowup, deleteEnquiry, deleteEnquiryFollowup, + reassignEnquiry, updateEnquiry, updateEnquiryFollowup } from './service'; import { enquiryKeys } from './queries'; -import type { EnquiryFollowupMutationPayload, EnquiryMutationPayload } from './types'; +import type { + EnquiryAssignmentMutationPayload, + EnquiryFollowupMutationPayload, + EnquiryMutationPayload +} from './types'; async function invalidateEnquiryLists() { await getQueryClient().invalidateQueries({ queryKey: enquiryKeys.lists() }); @@ -65,6 +71,26 @@ export const deleteEnquiryMutation = mutationOptions({ } }); +export const assignEnquiryMutation = mutationOptions({ + mutationFn: ({ id, values }: { id: string; values: EnquiryAssignmentMutationPayload }) => + assignEnquiry(id, values), + onSettled: async (_data, error, variables) => { + if (!error) { + await invalidateEnquiryMutationQueries(variables.id); + } + } +}); + +export const reassignEnquiryMutation = mutationOptions({ + mutationFn: ({ id, values }: { id: string; values: EnquiryAssignmentMutationPayload }) => + reassignEnquiry(id, values), + onSettled: async (_data, error, variables) => { + if (!error) { + await invalidateEnquiryMutationQueries(variables.id); + } + } +}); + export const createEnquiryFollowupMutation = mutationOptions({ mutationFn: ({ enquiryId, diff --git a/src/features/crm/enquiries/api/service.ts b/src/features/crm/enquiries/api/service.ts index 4e5c488..3041a21 100644 --- a/src/features/crm/enquiries/api/service.ts +++ b/src/features/crm/enquiries/api/service.ts @@ -1,5 +1,6 @@ import { apiClient } from '@/lib/api-client'; import type { + EnquiryAssignmentMutationPayload, EnquiryDetailResponse, EnquiryFilters, EnquiryFollowupMutationPayload, @@ -51,6 +52,20 @@ export async function deleteEnquiry(id: string) { }); } +export async function assignEnquiry(id: string, data: EnquiryAssignmentMutationPayload) { + return apiClient(`/crm/enquiries/${id}/assign`, { + method: 'POST', + body: JSON.stringify(data) + }); +} + +export async function reassignEnquiry(id: string, data: EnquiryAssignmentMutationPayload) { + return apiClient(`/crm/enquiries/${id}/reassign`, { + method: 'POST', + body: JSON.stringify(data) + }); +} + export async function getEnquiryFollowups(id: string): Promise { return apiClient(`/crm/enquiries/${id}/followups`); } diff --git a/src/features/crm/enquiries/api/types.ts b/src/features/crm/enquiries/api/types.ts index 2a54c84..479d77d 100644 --- a/src/features/crm/enquiries/api/types.ts +++ b/src/features/crm/enquiries/api/types.ts @@ -28,6 +28,13 @@ export interface EnquiryContactLookup { isPrimary: boolean; } +export interface EnquiryAssignableUserLookup { + id: string; + name: string; + membershipRole: 'admin' | 'user'; + businessRole: string; +} + export interface EnquiryRecord { id: string; organizationId: string; @@ -52,6 +59,12 @@ export interface EnquiryRecord { notes: string | null; isHotProject: boolean; isActive: boolean; + assignedToUserId: string | null; + assignedToName: string | null; + assignedAt: string | null; + assignedBy: string | null; + assignedByName: string | null; + assignmentRemark: string | null; createdAt: string; updatedAt: string; deletedAt: string | null; @@ -102,6 +115,7 @@ export interface EnquiryReferenceData { followupTypes: EnquiryOption[]; customers: EnquiryCustomerLookup[]; contacts: EnquiryContactLookup[]; + assignableUsers: EnquiryAssignableUserLookup[]; } export interface EnquiryFilters { @@ -174,6 +188,11 @@ export interface EnquiryFollowupMutationPayload { nextAction?: string; } +export interface EnquiryAssignmentMutationPayload { + assignedToUserId: string; + remark?: string; +} + export interface EnquiryCustomerRelationItem { id: string; code: string; diff --git a/src/features/crm/enquiries/components/enquiries-table.tsx b/src/features/crm/enquiries/components/enquiries-table.tsx index 4bf7a45..9f1bc2b 100644 --- a/src/features/crm/enquiries/components/enquiries-table.tsx +++ b/src/features/crm/enquiries/components/enquiries-table.tsx @@ -14,15 +14,19 @@ import { getEnquiryColumns } from './enquiry-columns'; export function EnquiriesTable({ referenceData, canUpdate, - canDelete + canDelete, + canAssign, + canReassign }: { referenceData: EnquiryReferenceData; canUpdate: boolean; canDelete: boolean; + canAssign: boolean; + canReassign: boolean; }) { const columns = useMemo( - () => getEnquiryColumns({ referenceData, canUpdate, canDelete }), - [referenceData, canUpdate, canDelete] + () => getEnquiryColumns({ referenceData, canUpdate, canDelete, canAssign, canReassign }), + [referenceData, canUpdate, canDelete, canAssign, canReassign] ); const columnIds = columns.map((column) => column.id).filter(Boolean) as string[]; const [params] = useQueryStates({ diff --git a/src/features/crm/enquiries/components/enquiry-assignment-dialog.tsx b/src/features/crm/enquiries/components/enquiry-assignment-dialog.tsx new file mode 100644 index 0000000..57de061 --- /dev/null +++ b/src/features/crm/enquiries/components/enquiry-assignment-dialog.tsx @@ -0,0 +1,165 @@ +'use client'; + +import { useEffect, useMemo } from 'react'; +import { z } from 'zod'; +import { useMutation } from '@tanstack/react-query'; +import { toast } from 'sonner'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { useAppForm, useFormFields } from '@/components/ui/tanstack-form'; +import { Icons } from '@/components/icons'; +import { assignEnquiryMutation, reassignEnquiryMutation } from '../api/mutations'; +import type { EnquiryRecord, EnquiryReferenceData } from '../api/types'; + +const enquiryAssignmentSchema = z.object({ + assignedToUserId: z.string().min(1, 'Sales user is required'), + remark: z.string().optional() +}); + +type EnquiryAssignmentFormValues = z.infer; + +function toDefaultValues( + enquiry: EnquiryRecord, + referenceData: EnquiryReferenceData +): EnquiryAssignmentFormValues { + return { + assignedToUserId: enquiry.assignedToUserId ?? referenceData.assignableUsers[0]?.id ?? '', + remark: enquiry.assignmentRemark ?? '' + }; +} + +export function EnquiryAssignmentDialog({ + enquiry, + referenceData, + mode, + open, + onOpenChange +}: { + enquiry: EnquiryRecord; + referenceData: EnquiryReferenceData; + mode: 'assign' | 'reassign'; + open: boolean; + onOpenChange: (open: boolean) => void; +}) { + const defaultValues = useMemo( + () => toDefaultValues(enquiry, referenceData), + [enquiry, referenceData] + ); + const { FormSelectField, FormTextareaField } = useFormFields(); + + const assignMutation = useMutation({ + ...assignEnquiryMutation, + onSuccess: () => { + toast.success('Enquiry assigned successfully'); + onOpenChange(false); + }, + onError: (error) => + toast.error(error instanceof Error ? error.message : 'Failed to assign enquiry') + }); + + const reassignMutation = useMutation({ + ...reassignEnquiryMutation, + onSuccess: () => { + toast.success('Enquiry reassigned successfully'); + onOpenChange(false); + }, + onError: (error) => + toast.error(error instanceof Error ? error.message : 'Failed to reassign enquiry') + }); + + const form = useAppForm({ + defaultValues, + validators: { + onSubmit: enquiryAssignmentSchema + }, + onSubmit: async ({ value }) => { + const values = { + assignedToUserId: value.assignedToUserId, + remark: value.remark?.trim() || undefined + }; + + if (mode === 'assign') { + await assignMutation.mutateAsync({ id: enquiry.id, values }); + return; + } + + await reassignMutation.mutateAsync({ id: enquiry.id, values }); + } + }); + + useEffect(() => { + if (!open) { + return; + } + + form.reset(defaultValues); + }, [defaultValues, form, open]); + + const isPending = assignMutation.isPending || reassignMutation.isPending; + const noUsersAvailable = referenceData.assignableUsers.length === 0; + + return ( + + + + {mode === 'assign' ? 'Assign Sales' : 'Reassign Sales'} + + {mode === 'assign' + ? 'Select the sales owner who should take this enquiry forward.' + : 'Move this enquiry to a different sales owner and keep the handoff note visible.'} + + + + + + {noUsersAvailable ? ( +
+ No assignable sales users are available in this workspace yet. +
+ ) : ( + <> + ({ + value: user.id, + label: `${user.name} (${user.businessRole.replaceAll('_', ' ')})` + }))} + placeholder='Select sales user' + /> + + + )} +
+
+ + + + + +
+
+ ); +} diff --git a/src/features/crm/enquiries/components/enquiry-cell-action.tsx b/src/features/crm/enquiries/components/enquiry-cell-action.tsx index 4c4c17a..666f6e3 100644 --- a/src/features/crm/enquiries/components/enquiry-cell-action.tsx +++ b/src/features/crm/enquiries/components/enquiry-cell-action.tsx @@ -22,12 +22,16 @@ export function EnquiryCellAction({ data, referenceData, canUpdate, - canDelete + canDelete, + canAssign: _canAssign, + canReassign: _canReassign }: { data: EnquiryListItem; referenceData: EnquiryReferenceData; canUpdate: boolean; canDelete: boolean; + canAssign: boolean; + canReassign: boolean; }) { const router = useRouter(); const [deleteOpen, setDeleteOpen] = useState(false); diff --git a/src/features/crm/enquiries/components/enquiry-columns.tsx b/src/features/crm/enquiries/components/enquiry-columns.tsx index 56caf02..1795300 100644 --- a/src/features/crm/enquiries/components/enquiry-columns.tsx +++ b/src/features/crm/enquiries/components/enquiry-columns.tsx @@ -12,11 +12,15 @@ import { EnquiryStatusBadge } from './enquiry-status-badge'; export function getEnquiryColumns({ referenceData, canUpdate, - canDelete + canDelete, + canAssign, + canReassign }: { referenceData: EnquiryReferenceData; canUpdate: boolean; canDelete: boolean; + canAssign: boolean; + canReassign: boolean; }): ColumnDef[] { const statusMap = new Map(referenceData.statuses.map((item) => [item.id, item])); const productTypeMap = new Map(referenceData.productTypes.map((item) => [item.id, item])); @@ -153,6 +157,20 @@ export function getEnquiryColumns({ }, enableColumnFilter: true }, + { + id: 'assignedSales', + header: 'Assigned Sales', + cell: ({ row }) => ( +
+ {row.original.assignedToName ?? 'Unassigned'} + {row.original.assignedAt ? ( + + {new Date(row.original.assignedAt).toLocaleDateString()} + + ) : null} +
+ ) + }, { id: 'followupCount', accessorKey: 'followupCount', @@ -169,6 +187,8 @@ export function getEnquiryColumns({ referenceData={referenceData} canUpdate={canUpdate} canDelete={canDelete} + canAssign={canAssign} + canReassign={canReassign} /> ) } diff --git a/src/features/crm/enquiries/components/enquiry-detail.tsx b/src/features/crm/enquiries/components/enquiry-detail.tsx index a3ac12d..eaa3670 100644 --- a/src/features/crm/enquiries/components/enquiry-detail.tsx +++ b/src/features/crm/enquiries/components/enquiry-detail.tsx @@ -12,6 +12,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { enquiryByIdOptions } from '../api/queries'; import type { EnquiryReferenceData } from '../api/types'; import type { QuotationRelationItem } from '@/features/crm/quotations/api/types'; +import { EnquiryAssignmentDialog } from './enquiry-assignment-dialog'; import { EnquiryFormSheet } from './enquiry-form-sheet'; import { EnquiryFollowupsTab } from './enquiry-followups-tab'; import { EnquiryStatusBadge } from './enquiry-status-badge'; @@ -30,12 +31,16 @@ export function EnquiryDetail({ referenceData, relatedQuotations, canUpdate, + canAssign, + canReassign, canManageFollowups }: { enquiryId: string; referenceData: EnquiryReferenceData; relatedQuotations: QuotationRelationItem[]; canUpdate: boolean; + canAssign: boolean; + canReassign: boolean; canManageFollowups: { create: boolean; update: boolean; @@ -44,6 +49,7 @@ export function EnquiryDetail({ }) { const { data } = useSuspenseQuery(enquiryByIdOptions(enquiryId)); const [editOpen, setEditOpen] = useState(false); + const [assignmentOpen, setAssignmentOpen] = useState(false); const enquiry = data.enquiry; const branchMap = useMemo( () => new Map(referenceData.branches.map((item) => [item.id, item.name])), @@ -76,6 +82,8 @@ export function EnquiryDetail({ const customer = customerMap.get(enquiry.customerId); const contact = enquiry.contactId ? contactMap.get(enquiry.contactId) : null; const status = statusMap.get(enquiry.status); + const canManageAssignment = enquiry.assignedToUserId ? canReassign : canAssign; + const assignmentMode = enquiry.assignedToUserId ? 'reassign' : 'assign'; return (
@@ -100,6 +108,12 @@ export function EnquiryDetail({
+ {canManageAssignment ? ( + + ) : null} {canUpdate ? (
+ - + ); } diff --git a/src/features/crm/enquiries/server/service.ts b/src/features/crm/enquiries/server/service.ts index 4adc61d..bcc9c75 100644 --- a/src/features/crm/enquiries/server/service.ts +++ b/src/features/crm/enquiries/server/service.ts @@ -4,6 +4,7 @@ import { crmCustomers, crmEnquiries, crmEnquiryFollowups, + memberships, users } from '@/db/schema'; import { db } from '@/lib/db'; @@ -14,7 +15,9 @@ import { generateNextDocumentCode } from '@/features/foundation/document-sequenc import { getActiveOptionsByCategory } from '@/features/foundation/master-options/service'; import type { EnquiryActivityRecord, + EnquiryAssignableUserLookup, EnquiryBranchOption, + EnquiryAssignmentMutationPayload, EnquiryCustomerRelationItem, EnquiryFilters, EnquiryFollowupMutationPayload, @@ -34,6 +37,8 @@ const ENQUIRY_OPTION_CATEGORIES = { followupType: 'crm_followup_type' } as const; +const ASSIGNABLE_BUSINESS_ROLES = new Set(['sales_manager', 'sales', 'sales_support']); + function mapOption( option: Awaited>[number] ): EnquiryOption { @@ -56,7 +61,10 @@ function mapBranch( }; } -function mapEnquiryRecord(row: typeof crmEnquiries.$inferSelect): EnquiryRecord { +function mapEnquiryRecord( + row: typeof crmEnquiries.$inferSelect, + options?: { assignedToName?: string | null; assignedByName?: string | null } +): EnquiryRecord { return { id: row.id, organizationId: row.organizationId, @@ -81,6 +89,12 @@ function mapEnquiryRecord(row: typeof crmEnquiries.$inferSelect): EnquiryRecord notes: row.notes, isHotProject: row.isHotProject, isActive: row.isActive, + assignedToUserId: row.assignedToUserId, + assignedToName: options?.assignedToName ?? null, + assignedAt: row.assignedAt?.toISOString() ?? null, + assignedBy: row.assignedBy, + assignedByName: options?.assignedByName ?? null, + assignmentRemark: row.assignmentRemark, createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString(), deletedAt: row.deletedAt?.toISOString() ?? null, @@ -220,6 +234,52 @@ export async function assertContactBelongsToOrganization( return contact; } +async function listAssignableUsers(organizationId: string): Promise { + const rows = await db + .select({ + userId: memberships.userId, + membershipRole: memberships.role, + businessRole: memberships.businessRole, + name: users.name + }) + .from(memberships) + .innerJoin(users, eq(memberships.userId, users.id)) + .where(eq(memberships.organizationId, organizationId)) + .orderBy(asc(users.name)); + + const uniqueRows = [...new Map(rows.map((row) => [row.userId, row])).values()]; + + return uniqueRows + .filter( + (row) => row.membershipRole === 'admin' || ASSIGNABLE_BUSINESS_ROLES.has(row.businessRole) + ) + .map((row) => ({ + id: row.userId, + name: row.name, + membershipRole: row.membershipRole === 'admin' ? 'admin' : 'user', + businessRole: row.businessRole + })); +} + +export async function assertAssignableUserBelongsToOrganization( + userId: string, + organizationId: string +) { + const [membership] = await db + .select({ + userId: memberships.userId + }) + .from(memberships) + .where(and(eq(memberships.organizationId, organizationId), eq(memberships.userId, userId))) + .limit(1); + + if (!membership) { + throw new AuthError('Assignee not found in organization', 404); + } + + return membership; +} + async function assertEnquiryBelongsToOrganization(id: string, organizationId: string) { const [enquiry] = await db .select() @@ -351,7 +411,8 @@ export async function getEnquiryReferenceData( leadChannels, followupTypes, customers, - contacts + contacts, + assignableUsers ] = await Promise.all([ getUserBranches(), getActiveOptionsByCategory(ENQUIRY_OPTION_CATEGORIES.status, { organizationId }), @@ -373,7 +434,8 @@ export async function getEnquiryReferenceData( isNull(crmCustomerContacts.deletedAt) ) ) - .orderBy(desc(crmCustomerContacts.isPrimary), asc(crmCustomerContacts.name)) + .orderBy(desc(crmCustomerContacts.isPrimary), asc(crmCustomerContacts.name)), + listAssignableUsers(organizationId) ]); return { @@ -396,7 +458,8 @@ export async function getEnquiryReferenceData( email: contact.email, mobile: contact.mobile, isPrimary: contact.isPrimary - })) + })), + assignableUsers }; } @@ -421,9 +484,12 @@ export async function listEnquiries( const customerIds = [...new Set(rows.map((row) => row.customerId))]; const contactIds = [...new Set(rows.map((row) => row.contactId).filter(Boolean) as string[])]; + const assignedUserIds = [ + ...new Set(rows.map((row) => row.assignedToUserId).filter(Boolean) as string[]) + ]; const enquiryIds = rows.map((row) => row.id); - const [customers, contacts, followupCounts] = await Promise.all([ + const [customers, contacts, assignedUsers, followupCounts] = await Promise.all([ customerIds.length ? db .select() @@ -446,6 +512,12 @@ export async function listEnquiries( ) ) : [], + assignedUserIds.length + ? db + .select({ id: users.id, name: users.name }) + .from(users) + .where(inArray(users.id, assignedUserIds)) + : [], enquiryIds.length ? db .select({ @@ -466,11 +538,16 @@ export async function listEnquiries( const customerMap = new Map(customers.map((customer) => [customer.id, customer.name])); const contactMap = new Map(contacts.map((contact) => [contact.id, contact.name])); + const assignedUserMap = new Map(assignedUsers.map((user) => [user.id, user.name])); const followupCountMap = new Map(followupCounts.map((item) => [item.enquiryId, item.value])); return { items: rows.map((row) => ({ - ...mapEnquiryRecord(row), + ...mapEnquiryRecord(row, { + assignedToName: row.assignedToUserId + ? (assignedUserMap.get(row.assignedToUserId) ?? null) + : null + }), customerName: customerMap.get(row.customerId) ?? 'Unknown customer', contactName: row.contactId ? (contactMap.get(row.contactId) ?? null) : null, followupCount: followupCountMap.get(row.id) ?? 0 @@ -481,7 +558,21 @@ export async function listEnquiries( export async function getEnquiryDetail(id: string, organizationId: string): Promise { const enquiry = await assertEnquiryBelongsToOrganization(id, organizationId); - return mapEnquiryRecord(enquiry); + const relatedUserIds = [enquiry.assignedToUserId, enquiry.assignedBy].filter(Boolean) as string[]; + const userRows = relatedUserIds.length + ? await db + .select({ id: users.id, name: users.name }) + .from(users) + .where(inArray(users.id, relatedUserIds)) + : []; + const userMap = new Map(userRows.map((row) => [row.id, row.name])); + + return mapEnquiryRecord(enquiry, { + assignedToName: enquiry.assignedToUserId + ? (userMap.get(enquiry.assignedToUserId) ?? null) + : null, + assignedByName: enquiry.assignedBy ? (userMap.get(enquiry.assignedBy) ?? null) : null + }); } export async function getEnquiryActivity( @@ -640,6 +731,59 @@ export async function softDeleteEnquiry(id: string, organizationId: string, user return updated; } +async function updateEnquiryAssignment( + id: string, + organizationId: string, + userId: string, + payload: EnquiryAssignmentMutationPayload, + mode: 'assign' | 'reassign' +) { + const enquiry = await assertEnquiryBelongsToOrganization(id, organizationId); + + if (mode === 'assign' && enquiry.assignedToUserId) { + throw new AuthError('Enquiry is already assigned. Use reassign instead.', 400); + } + + if (mode === 'reassign' && !enquiry.assignedToUserId) { + throw new AuthError('Enquiry is not assigned yet. Use assign instead.', 400); + } + + await assertAssignableUserBelongsToOrganization(payload.assignedToUserId, organizationId); + + const [updated] = await db + .update(crmEnquiries) + .set({ + assignedToUserId: payload.assignedToUserId, + assignedAt: new Date(), + assignedBy: userId, + assignmentRemark: payload.remark?.trim() || null, + updatedAt: new Date(), + updatedBy: userId + }) + .where(eq(crmEnquiries.id, id)) + .returning(); + + return updated; +} + +export async function assignEnquiryToUser( + id: string, + organizationId: string, + userId: string, + payload: EnquiryAssignmentMutationPayload +) { + return updateEnquiryAssignment(id, organizationId, userId, payload, 'assign'); +} + +export async function reassignEnquiryToUser( + id: string, + organizationId: string, + userId: string, + payload: EnquiryAssignmentMutationPayload +) { + return updateEnquiryAssignment(id, organizationId, userId, payload, 'reassign'); +} + export async function listEnquiryFollowups(id: string, organizationId: string) { await assertEnquiryBelongsToOrganization(id, organizationId); const rows = await db diff --git a/src/features/example-dashboard/data.ts b/src/features/example-dashboard/data.ts index c7ee8ed..ca61952 100644 --- a/src/features/example-dashboard/data.ts +++ b/src/features/example-dashboard/data.ts @@ -13,7 +13,8 @@ export const exampleProducts: Product[] = [ organization_id: 'example-org', photo_url: imageFor('Asset Portal', '1b4332'), name: 'Asset Portal', - description: 'Starter module showing a route-handler-backed listing with filters and pagination.', + description: + 'Starter module showing a route-handler-backed listing with filters and pagination.', created_at: now, price: 1290, category: 'Electronics', @@ -159,13 +160,13 @@ export const exampleUsers: User[] = [ organizationId: 'org-atlas', organizationName: 'Atlas Labs', membershipRole: 'admin', - businessRole: 'it_admin' + businessRole: 'sales_manager' }, { organizationId: 'org-nova', organizationName: 'Nova Retail', membershipRole: 'admin', - businessRole: 'auditor' + businessRole: 'department_manager' } ] }, @@ -182,7 +183,7 @@ export const exampleUsers: User[] = [ organizationId: 'org-atlas', organizationName: 'Atlas Labs', membershipRole: 'admin', - businessRole: 'helpdesk' + businessRole: 'sales_manager' } ] }, @@ -199,7 +200,7 @@ export const exampleUsers: User[] = [ organizationId: 'org-atlas', organizationName: 'Atlas Labs', membershipRole: 'user', - businessRole: 'application' + businessRole: 'sales' } ] }, @@ -216,7 +217,7 @@ export const exampleUsers: User[] = [ organizationId: 'org-nova', organizationName: 'Nova Retail', membershipRole: 'admin', - businessRole: 'infrastructure' + businessRole: 'department_manager' } ] }, @@ -233,7 +234,7 @@ export const exampleUsers: User[] = [ organizationId: 'org-nova', organizationName: 'Nova Retail', membershipRole: 'user', - businessRole: 'viewer' + businessRole: 'sales_support' } ] }, @@ -250,7 +251,7 @@ export const exampleUsers: User[] = [ organizationId: 'org-orbit', organizationName: 'Orbit Health', membershipRole: 'user', - businessRole: 'auditor' + businessRole: 'top_manager' } ] }, @@ -267,7 +268,7 @@ export const exampleUsers: User[] = [ organizationId: 'org-orbit', organizationName: 'Orbit Health', membershipRole: 'admin', - businessRole: 'it_admin' + businessRole: 'sales_manager' } ] }, @@ -287,13 +288,13 @@ export const exampleUsers: User[] = [ organizationId: 'org-atlas', organizationName: 'Atlas Labs', membershipRole: 'user', - businessRole: 'helpdesk' + businessRole: 'sales_support' }, { organizationId: 'org-orbit', organizationName: 'Orbit Health', membershipRole: 'user', - businessRole: 'viewer' + businessRole: 'sales' } ] } diff --git a/src/features/foundation/auth-context/service.ts b/src/features/foundation/auth-context/service.ts index 219ef4e..ab06123 100644 --- a/src/features/foundation/auth-context/service.ts +++ b/src/features/foundation/auth-context/service.ts @@ -49,7 +49,7 @@ export async function getCurrentUserContext(): Promise; }; diff --git a/src/features/users/components/user-form-sheet.tsx b/src/features/users/components/user-form-sheet.tsx index b3910b8..597133e 100644 --- a/src/features/users/components/user-form-sheet.tsx +++ b/src/features/users/components/user-form-sheet.tsx @@ -25,6 +25,7 @@ import { createUserMutation, updateUserMutation } from '../api/mutations'; import type { User } from '../api/types'; import { toast } from 'sonner'; import { type UserFormValues, userSchema } from '../schemas/user'; +import type { BusinessRole } from '@/lib/auth/rbac'; interface UserFormSheetProps { user?: User; @@ -46,12 +47,11 @@ const membershipRoleOptions = [ ] as const; const businessRoleOptions = [ - { value: 'it_admin', label: 'IT Admin' }, - { value: 'helpdesk', label: 'Helpdesk' }, - { value: 'infrastructure', label: 'Infrastructure' }, - { value: 'application', label: 'Application' }, - { value: 'auditor', label: 'Auditor' }, - { value: 'viewer', label: 'Viewer' } + { value: 'sales_manager', label: 'Sales Manager' }, + { value: 'sales', label: 'Sales' }, + { value: 'sales_support', label: 'Sales Support' }, + { value: 'department_manager', label: 'Department Manager' }, + { value: 'top_manager', label: 'Top Manager' } ] as const; export function UserFormSheet({ user, open, onOpenChange }: UserFormSheetProps) { @@ -194,7 +194,9 @@ export function UserFormSheet({ user, open, onOpenChange }: UserFormSheetProps) label={isEdit ? 'Password (optional)' : 'Password'} required={!isEdit} type='password' - placeholder={isEdit ? 'Leave blank to keep current password' : 'Minimum 8 characters'} + placeholder={ + isEdit ? 'Leave blank to keep current password' : 'Minimum 8 characters' + } /> {isEdit && user?.systemRole === 'super_admin' && ( @@ -254,7 +256,7 @@ export function UserFormSheet({ user, open, onOpenChange }: UserFormSheetProps) { organizationId: organization.id, membershipRole: 'user', - businessRole: 'viewer' + businessRole: 'sales_support' } ]); field.handleBlur(); @@ -298,7 +300,7 @@ export function UserFormSheet({ user, open, onOpenChange }: UserFormSheetProps)