task-d.5.3
This commit is contained in:
@@ -0,0 +1,108 @@
|
|||||||
|
# Task D.5.3: Lead Assignment -> Create Enquiry Foundation
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Establish the first production lead-assignment flow that preserves `crm_leads` as the
|
||||||
|
marketing-owned source record while creating or reusing a linked `crm_enquiries` record for the
|
||||||
|
sales execution workspace.
|
||||||
|
|
||||||
|
## Review Completed
|
||||||
|
|
||||||
|
- `AGENTS.md`
|
||||||
|
- `docs/standards/project-foundations.md`
|
||||||
|
- `docs/standards/task-catalog.md`
|
||||||
|
- `docs/adr/0018-lead-enquiry-domain-separation.md`
|
||||||
|
- `docs/implementation/task-d5-lead-enquiry-domain-separation.md`
|
||||||
|
- `docs/implementation/task-d52-lead-domain-service-api-foundation.md`
|
||||||
|
- `docs/security/crm-authorization-boundaries.md`
|
||||||
|
- `src/features/crm/leads/server/service.ts`
|
||||||
|
- `src/features/crm/enquiries/server/service.ts`
|
||||||
|
- `src/features/crm/customers/server/service.ts`
|
||||||
|
- `src/features/foundation/audit-log/service.ts`
|
||||||
|
- `src/features/foundation/document-sequence/service.ts`
|
||||||
|
|
||||||
|
## What Changed
|
||||||
|
|
||||||
|
- Added lead bootstrap and assignment fields to `crm_leads`:
|
||||||
|
- `description`
|
||||||
|
- `product_type`
|
||||||
|
- `priority`
|
||||||
|
- `estimated_value`
|
||||||
|
- `assigned_sales_owner_id`
|
||||||
|
- `assigned_at`
|
||||||
|
- `assigned_by`
|
||||||
|
- `assignment_remark`
|
||||||
|
- Added [assignment.service.ts](C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/leads/server/assignment.service.ts)
|
||||||
|
with:
|
||||||
|
- `assignLead()`
|
||||||
|
- internal `createEnquiryFromLead()`
|
||||||
|
- Added [route.ts](C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/leads/[id]/assign/route.ts)
|
||||||
|
for `POST /api/crm/leads/[id]/assign`
|
||||||
|
- Extended lead DTOs and schemas so new lead records can carry the data needed to bootstrap an
|
||||||
|
enquiry
|
||||||
|
- Added `assigned` to seeded `crm_lead_status`
|
||||||
|
- Tightened enquiry-owner validation so assignees must be sales-assignable members, not merely any
|
||||||
|
organization member
|
||||||
|
|
||||||
|
## Assignment Behavior
|
||||||
|
|
||||||
|
- route permission is `crm.lead.assign`
|
||||||
|
- service validates:
|
||||||
|
- lead exists and is active
|
||||||
|
- actor can access the lead through resolved CRM scope
|
||||||
|
- assignee belongs to the same organization and is a sales-assignable member
|
||||||
|
- if an active enquiry already exists for the lead:
|
||||||
|
- no second enquiry is created
|
||||||
|
- the existing enquiry is returned
|
||||||
|
- lead assignment metadata is synchronized
|
||||||
|
- if no active enquiry exists:
|
||||||
|
- enquiry code generation reuses `generateNextDocumentCode()` with `documentType = crm_enquiry`
|
||||||
|
- enquiry is created with `lead_id = crm_leads.id`
|
||||||
|
- enquiry owner compatibility fields use the existing enquiry model:
|
||||||
|
- `assigned_to_user_id`
|
||||||
|
- `assigned_at`
|
||||||
|
- `assigned_by`
|
||||||
|
- `assignment_remark`
|
||||||
|
|
||||||
|
## Lead -> Enquiry Bootstrap Mapping
|
||||||
|
|
||||||
|
When creating the enquiry from a lead, the flow copies:
|
||||||
|
|
||||||
|
- `customerId`
|
||||||
|
- `contactId`
|
||||||
|
- `description`
|
||||||
|
- `projectName`
|
||||||
|
- `projectLocation`
|
||||||
|
- `productType`
|
||||||
|
- `estimatedValue`
|
||||||
|
- `priority`
|
||||||
|
|
||||||
|
The enquiry title is derived from `lead.projectName` and falls back to `Lead <code>` when the
|
||||||
|
project name is empty.
|
||||||
|
|
||||||
|
## Audit Actions
|
||||||
|
|
||||||
|
This task records:
|
||||||
|
|
||||||
|
- `assign_lead` on `crm_lead`
|
||||||
|
- `create_enquiry_from_lead` on `crm_enquiry`
|
||||||
|
|
||||||
|
Captured linkage includes:
|
||||||
|
|
||||||
|
- `leadId`
|
||||||
|
- `enquiryId`
|
||||||
|
- `salesOwnerId`
|
||||||
|
|
||||||
|
## Compatibility Notes
|
||||||
|
|
||||||
|
This task intentionally does not change:
|
||||||
|
|
||||||
|
- quotation flows
|
||||||
|
- approval flows
|
||||||
|
- dashboard datasets
|
||||||
|
- reporting datasets
|
||||||
|
- won/lost lifecycle flows
|
||||||
|
- legacy enquiry list/detail APIs outside the stricter assignee validation
|
||||||
|
|
||||||
|
`crm_enquiries.pipeline_stage` remains in place for compatibility, but lead assignment now creates a
|
||||||
|
dedicated enquiry record instead of mutating a lead record in place.
|
||||||
657
drizzle/0000_swift_prism.sql
Normal file
657
drizzle/0000_swift_prism.sql
Normal file
@@ -0,0 +1,657 @@
|
|||||||
|
CREATE TABLE "crm_approval_actions" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"approval_request_id" text NOT NULL,
|
||||||
|
"step_number" integer NOT NULL,
|
||||||
|
"action" text NOT NULL,
|
||||||
|
"remark" text,
|
||||||
|
"acted_by" text NOT NULL,
|
||||||
|
"acted_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_approval_requests" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"workflow_id" text NOT NULL,
|
||||||
|
"entity_type" text NOT NULL,
|
||||||
|
"entity_id" text NOT NULL,
|
||||||
|
"current_step" integer DEFAULT 1 NOT NULL,
|
||||||
|
"status" text NOT NULL,
|
||||||
|
"requested_by" text NOT NULL,
|
||||||
|
"requested_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"completed_at" timestamp with time zone,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_approval_steps" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"workflow_id" text NOT NULL,
|
||||||
|
"step_number" integer NOT NULL,
|
||||||
|
"role_code" text NOT NULL,
|
||||||
|
"role_name" text NOT NULL,
|
||||||
|
"is_required" boolean DEFAULT true NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_approval_workflows" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"code" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"entity_type" text NOT NULL,
|
||||||
|
"is_active" boolean DEFAULT true NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_contact_shares" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"contact_id" text NOT NULL,
|
||||||
|
"shared_to_user_id" text NOT NULL,
|
||||||
|
"shared_by_user_id" text NOT NULL,
|
||||||
|
"shared_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"is_active" boolean DEFAULT true NOT NULL,
|
||||||
|
"remark" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_customer_contacts" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"customer_id" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"position" text,
|
||||||
|
"department" text,
|
||||||
|
"phone" text,
|
||||||
|
"mobile" text,
|
||||||
|
"email" text,
|
||||||
|
"is_primary" boolean DEFAULT false NOT NULL,
|
||||||
|
"notes" text,
|
||||||
|
"is_active" boolean DEFAULT true NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone,
|
||||||
|
"created_by" text NOT NULL,
|
||||||
|
"updated_by" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_customer_owner_history" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"customer_id" text NOT NULL,
|
||||||
|
"old_owner_user_id" text,
|
||||||
|
"new_owner_user_id" text,
|
||||||
|
"changed_by" text NOT NULL,
|
||||||
|
"changed_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"remark" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_customers" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"branch_id" text,
|
||||||
|
"code" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"abbr" text,
|
||||||
|
"tax_id" text,
|
||||||
|
"customer_type" text NOT NULL,
|
||||||
|
"customer_status" text NOT NULL,
|
||||||
|
"address" text,
|
||||||
|
"province" text,
|
||||||
|
"district" text,
|
||||||
|
"sub_district" text,
|
||||||
|
"postal_code" text,
|
||||||
|
"country" text,
|
||||||
|
"phone" text,
|
||||||
|
"fax" text,
|
||||||
|
"email" text,
|
||||||
|
"website" text,
|
||||||
|
"lead_channel" text,
|
||||||
|
"customer_group" text,
|
||||||
|
"customer_sub_group" text,
|
||||||
|
"owner_user_id" text,
|
||||||
|
"owner_assigned_at" timestamp with time zone,
|
||||||
|
"owner_assigned_by" text,
|
||||||
|
"notes" text,
|
||||||
|
"is_active" boolean DEFAULT true NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone,
|
||||||
|
"created_by" text NOT NULL,
|
||||||
|
"updated_by" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_document_artifacts" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"entity_type" text NOT NULL,
|
||||||
|
"entity_id" text NOT NULL,
|
||||||
|
"document_type" text NOT NULL,
|
||||||
|
"artifact_type" text NOT NULL,
|
||||||
|
"template_version_id" text,
|
||||||
|
"storage_provider" text NOT NULL,
|
||||||
|
"storage_key" text NOT NULL,
|
||||||
|
"file_name" text NOT NULL,
|
||||||
|
"content_type" text NOT NULL,
|
||||||
|
"file_size" integer,
|
||||||
|
"checksum" text,
|
||||||
|
"status" text DEFAULT 'active' NOT NULL,
|
||||||
|
"generated_by" text NOT NULL,
|
||||||
|
"generated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"locked_at" timestamp with time zone,
|
||||||
|
"locked_by" text,
|
||||||
|
"voided_at" timestamp with time zone,
|
||||||
|
"voided_by" text,
|
||||||
|
"void_reason" text,
|
||||||
|
"metadata" jsonb,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_document_template_mappings" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"template_version_id" text NOT NULL,
|
||||||
|
"placeholder_key" text NOT NULL,
|
||||||
|
"source_path" text NOT NULL,
|
||||||
|
"data_type" text NOT NULL,
|
||||||
|
"sheet_name" text,
|
||||||
|
"default_value" text,
|
||||||
|
"format_mask" text,
|
||||||
|
"sort_order" integer DEFAULT 0 NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_document_template_table_columns" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"mapping_id" text NOT NULL,
|
||||||
|
"column_name" text NOT NULL,
|
||||||
|
"source_field" text NOT NULL,
|
||||||
|
"column_letter" text,
|
||||||
|
"sort_order" integer DEFAULT 0 NOT NULL,
|
||||||
|
"format_mask" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_document_template_versions" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"template_id" text NOT NULL,
|
||||||
|
"version" text NOT NULL,
|
||||||
|
"file_path" text,
|
||||||
|
"schema_json" jsonb NOT NULL,
|
||||||
|
"preview_image_url" text,
|
||||||
|
"is_active" boolean DEFAULT true NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone,
|
||||||
|
"created_by" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_document_templates" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"document_type" text NOT NULL,
|
||||||
|
"product_type" text NOT NULL,
|
||||||
|
"file_type" text NOT NULL,
|
||||||
|
"template_name" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"is_default" boolean DEFAULT false NOT NULL,
|
||||||
|
"is_active" boolean DEFAULT true NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone,
|
||||||
|
"created_by" text NOT NULL,
|
||||||
|
"updated_by" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_enquiries" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"branch_id" text,
|
||||||
|
"lead_id" text,
|
||||||
|
"code" text NOT NULL,
|
||||||
|
"customer_id" text NOT NULL,
|
||||||
|
"contact_id" text,
|
||||||
|
"title" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"requirement" text,
|
||||||
|
"project_name" text,
|
||||||
|
"project_location" text,
|
||||||
|
"product_type" text NOT NULL,
|
||||||
|
"status" text NOT NULL,
|
||||||
|
"priority" text NOT NULL,
|
||||||
|
"lead_channel" text,
|
||||||
|
"estimated_value" double precision,
|
||||||
|
"chance_percent" integer,
|
||||||
|
"expected_close_date" timestamp with time zone,
|
||||||
|
"competitor" text,
|
||||||
|
"source" text,
|
||||||
|
"notes" text,
|
||||||
|
"is_hot_project" boolean DEFAULT false NOT NULL,
|
||||||
|
"is_active" boolean DEFAULT true NOT NULL,
|
||||||
|
"pipeline_stage" text DEFAULT 'lead' NOT NULL,
|
||||||
|
"closed_won_at" timestamp with time zone,
|
||||||
|
"closed_lost_at" timestamp with time zone,
|
||||||
|
"closed_by_user_id" text,
|
||||||
|
"po_number" text,
|
||||||
|
"po_date" timestamp with time zone,
|
||||||
|
"po_amount" double precision,
|
||||||
|
"po_currency" text,
|
||||||
|
"lost_reason" text,
|
||||||
|
"lost_competitor" text,
|
||||||
|
"lost_remark" text,
|
||||||
|
"assigned_to_user_id" text,
|
||||||
|
"assigned_at" timestamp with time zone,
|
||||||
|
"assigned_by" text,
|
||||||
|
"assignment_remark" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone,
|
||||||
|
"created_by" text NOT NULL,
|
||||||
|
"updated_by" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_enquiry_attachments" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"enquiry_id" text NOT NULL,
|
||||||
|
"category" text NOT NULL,
|
||||||
|
"file_name" text NOT NULL,
|
||||||
|
"original_file_name" text NOT NULL,
|
||||||
|
"storage_provider" text NOT NULL,
|
||||||
|
"storage_key" text NOT NULL,
|
||||||
|
"file_size" integer,
|
||||||
|
"file_type" text,
|
||||||
|
"description" text,
|
||||||
|
"uploaded_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"uploaded_by" text NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_enquiry_customers" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"enquiry_id" text NOT NULL,
|
||||||
|
"customer_id" text NOT NULL,
|
||||||
|
"role" text NOT NULL,
|
||||||
|
"remark" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_enquiry_followups" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"enquiry_id" text NOT NULL,
|
||||||
|
"followup_date" timestamp with time zone NOT NULL,
|
||||||
|
"followup_type" text NOT NULL,
|
||||||
|
"contact_id" text,
|
||||||
|
"outcome" text,
|
||||||
|
"notes" text,
|
||||||
|
"next_followup_date" timestamp with time zone,
|
||||||
|
"next_action" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone,
|
||||||
|
"created_by" text NOT NULL,
|
||||||
|
"updated_by" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_leads" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"branch_id" text,
|
||||||
|
"code" text NOT NULL,
|
||||||
|
"customer_id" text,
|
||||||
|
"contact_id" text,
|
||||||
|
"description" text,
|
||||||
|
"project_name" text,
|
||||||
|
"project_location" text,
|
||||||
|
"product_type" text,
|
||||||
|
"priority" text,
|
||||||
|
"estimated_value" double precision,
|
||||||
|
"awareness_id" text,
|
||||||
|
"status" text DEFAULT 'new_job' NOT NULL,
|
||||||
|
"followup_status" text,
|
||||||
|
"lost_reason" text,
|
||||||
|
"outcome" text DEFAULT 'open' NOT NULL,
|
||||||
|
"owner_marketing_user_id" text,
|
||||||
|
"assigned_sales_owner_id" text,
|
||||||
|
"assigned_at" timestamp with time zone,
|
||||||
|
"assigned_by" text,
|
||||||
|
"assignment_remark" text,
|
||||||
|
"created_by" text NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_quotation_attachments" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"quotation_id" text NOT NULL,
|
||||||
|
"file_name" text NOT NULL,
|
||||||
|
"original_file_name" text NOT NULL,
|
||||||
|
"file_path" text NOT NULL,
|
||||||
|
"file_size" integer,
|
||||||
|
"file_type" text,
|
||||||
|
"description" text,
|
||||||
|
"uploaded_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"uploaded_by" text NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_quotation_customers" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"quotation_id" text NOT NULL,
|
||||||
|
"customer_id" text NOT NULL,
|
||||||
|
"role" text NOT NULL,
|
||||||
|
"is_primary" boolean DEFAULT false NOT NULL,
|
||||||
|
"remark" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_quotation_followups" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"quotation_id" text NOT NULL,
|
||||||
|
"followup_date" timestamp with time zone NOT NULL,
|
||||||
|
"followup_type" text NOT NULL,
|
||||||
|
"contact_id" text,
|
||||||
|
"outcome" text,
|
||||||
|
"notes" text,
|
||||||
|
"next_followup_date" timestamp with time zone,
|
||||||
|
"next_action" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone,
|
||||||
|
"created_by" text NOT NULL,
|
||||||
|
"updated_by" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_quotation_items" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"quotation_id" text NOT NULL,
|
||||||
|
"item_number" integer NOT NULL,
|
||||||
|
"product_type" text NOT NULL,
|
||||||
|
"description" text NOT NULL,
|
||||||
|
"quantity" double precision DEFAULT 0 NOT NULL,
|
||||||
|
"unit" text,
|
||||||
|
"unit_price" double precision DEFAULT 0 NOT NULL,
|
||||||
|
"discount" double precision DEFAULT 0 NOT NULL,
|
||||||
|
"discount_type" text,
|
||||||
|
"tax_rate" double precision DEFAULT 0 NOT NULL,
|
||||||
|
"total_price" double precision DEFAULT 0 NOT NULL,
|
||||||
|
"notes" text,
|
||||||
|
"sort_order" integer DEFAULT 0 NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_quotation_topic_items" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"topic_id" text NOT NULL,
|
||||||
|
"content" text NOT NULL,
|
||||||
|
"sort_order" integer DEFAULT 0 NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_quotation_topics" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"quotation_id" text NOT NULL,
|
||||||
|
"topic_type" text NOT NULL,
|
||||||
|
"title" text NOT NULL,
|
||||||
|
"sort_order" integer DEFAULT 0 NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_quotations" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"branch_id" text,
|
||||||
|
"code" text NOT NULL,
|
||||||
|
"enquiry_id" text,
|
||||||
|
"customer_id" text NOT NULL,
|
||||||
|
"contact_id" text,
|
||||||
|
"quotation_date" timestamp with time zone NOT NULL,
|
||||||
|
"valid_until" timestamp with time zone,
|
||||||
|
"quotation_type" text NOT NULL,
|
||||||
|
"project_name" text,
|
||||||
|
"project_location" text,
|
||||||
|
"attention" text,
|
||||||
|
"reference" text,
|
||||||
|
"notes" text,
|
||||||
|
"status" text NOT NULL,
|
||||||
|
"revision" integer DEFAULT 0 NOT NULL,
|
||||||
|
"parent_quotation_id" text,
|
||||||
|
"revision_remark" text,
|
||||||
|
"currency" text NOT NULL,
|
||||||
|
"exchange_rate" double precision DEFAULT 1 NOT NULL,
|
||||||
|
"subtotal" double precision DEFAULT 0 NOT NULL,
|
||||||
|
"discount" double precision DEFAULT 0 NOT NULL,
|
||||||
|
"discount_type" text,
|
||||||
|
"tax_rate" double precision DEFAULT 0 NOT NULL,
|
||||||
|
"tax_amount" double precision DEFAULT 0 NOT NULL,
|
||||||
|
"total_amount" double precision DEFAULT 0 NOT NULL,
|
||||||
|
"chance_percent" integer,
|
||||||
|
"is_hot_project" boolean DEFAULT false NOT NULL,
|
||||||
|
"competitor" text,
|
||||||
|
"salesman_id" text,
|
||||||
|
"is_sent" boolean DEFAULT false NOT NULL,
|
||||||
|
"sent_at" timestamp with time zone,
|
||||||
|
"sent_via" text,
|
||||||
|
"approved_at" timestamp with time zone,
|
||||||
|
"approved_artifact_id" text,
|
||||||
|
"approved_pdf_url" text,
|
||||||
|
"approved_snapshot" jsonb,
|
||||||
|
"approved_template_version_id" text,
|
||||||
|
"accepted_at" timestamp with time zone,
|
||||||
|
"rejected_at" timestamp with time zone,
|
||||||
|
"rejection_reason" text,
|
||||||
|
"is_active" boolean DEFAULT true NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone,
|
||||||
|
"created_by" text NOT NULL,
|
||||||
|
"updated_by" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_report_definitions" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"code" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"category" text NOT NULL,
|
||||||
|
"is_system" boolean DEFAULT true NOT NULL,
|
||||||
|
"is_active" boolean DEFAULT true NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_role_profiles" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"code" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"permissions" text[] DEFAULT '{}' NOT NULL,
|
||||||
|
"branch_scope_mode" text DEFAULT 'assigned' NOT NULL,
|
||||||
|
"product_scope_mode" text DEFAULT 'assigned' NOT NULL,
|
||||||
|
"ownership_scope" text DEFAULT 'own' NOT NULL,
|
||||||
|
"approval_authority" text DEFAULT 'none' NOT NULL,
|
||||||
|
"is_system" boolean DEFAULT false NOT NULL,
|
||||||
|
"is_active" boolean DEFAULT true NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone,
|
||||||
|
"created_by" text NOT NULL,
|
||||||
|
"updated_by" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "crm_user_role_assignments" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"role_profile_id" text NOT NULL,
|
||||||
|
"branch_scope_mode" text DEFAULT 'inherit' NOT NULL,
|
||||||
|
"branch_scope_ids" text[] DEFAULT '{}' NOT NULL,
|
||||||
|
"product_type_scope_mode" text DEFAULT 'inherit' NOT NULL,
|
||||||
|
"product_type_scope_ids" text[] DEFAULT '{}' NOT NULL,
|
||||||
|
"is_primary" boolean DEFAULT false NOT NULL,
|
||||||
|
"is_active" boolean DEFAULT true NOT NULL,
|
||||||
|
"assigned_by" text NOT NULL,
|
||||||
|
"assigned_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "document_sequences" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"branch_id" text DEFAULT '' NOT NULL,
|
||||||
|
"document_type" text NOT NULL,
|
||||||
|
"prefix" text NOT NULL,
|
||||||
|
"period" text NOT NULL,
|
||||||
|
"current_number" integer DEFAULT 0 NOT NULL,
|
||||||
|
"padding_length" integer DEFAULT 3 NOT NULL,
|
||||||
|
"is_active" boolean DEFAULT true NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "memberships" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"role" text DEFAULT 'user' NOT NULL,
|
||||||
|
"business_role" text DEFAULT 'sales_support' NOT NULL,
|
||||||
|
"permissions" text[] DEFAULT '{}' NOT NULL,
|
||||||
|
"branch_scope_ids" text[] DEFAULT '{}' NOT NULL,
|
||||||
|
"product_type_scope_ids" text[] DEFAULT '{}' NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "ms_options" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"category" text NOT NULL,
|
||||||
|
"code" text NOT NULL,
|
||||||
|
"label" text NOT NULL,
|
||||||
|
"value" text,
|
||||||
|
"parent_id" text,
|
||||||
|
"sort_order" integer DEFAULT 0 NOT NULL,
|
||||||
|
"is_active" boolean DEFAULT true NOT NULL,
|
||||||
|
"metadata" jsonb,
|
||||||
|
"deleted_at" timestamp with time zone,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "organizations" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"slug" text NOT NULL,
|
||||||
|
"image_url" text,
|
||||||
|
"plan" text DEFAULT 'free' NOT NULL,
|
||||||
|
"created_by" text NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "products" (
|
||||||
|
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "products_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"category" text NOT NULL,
|
||||||
|
"description" text NOT NULL,
|
||||||
|
"photo_url" text NOT NULL,
|
||||||
|
"price" double precision NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "tr_audit_logs" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"branch_id" text,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"entity_type" text NOT NULL,
|
||||||
|
"entity_id" text NOT NULL,
|
||||||
|
"action" text NOT NULL,
|
||||||
|
"before_data" jsonb,
|
||||||
|
"after_data" jsonb,
|
||||||
|
"request_id" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "users" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"email" text NOT NULL,
|
||||||
|
"password_hash" text NOT NULL,
|
||||||
|
"system_role" text DEFAULT 'user' NOT NULL,
|
||||||
|
"image" text,
|
||||||
|
"active_organization_id" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "crm_approval_steps_workflow_step_idx" ON "crm_approval_steps" USING btree ("workflow_id","step_number");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "crm_approval_workflows_org_code_idx" ON "crm_approval_workflows" USING btree ("organization_id","code");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "crm_contact_shares_org_contact_shared_user_idx" ON "crm_contact_shares" USING btree ("organization_id","contact_id","shared_to_user_id");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "crm_customers_org_code_idx" ON "crm_customers" USING btree ("organization_id","code");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "crm_document_template_mappings_version_placeholder_idx" ON "crm_document_template_mappings" USING btree ("template_version_id","placeholder_key");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "crm_document_template_table_columns_mapping_name_idx" ON "crm_document_template_table_columns" USING btree ("mapping_id","column_name");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "crm_document_template_versions_template_version_idx" ON "crm_document_template_versions" USING btree ("template_id","version");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "crm_document_templates_org_doc_product_file_name_idx" ON "crm_document_templates" USING btree ("organization_id","document_type","product_type","file_type","template_name");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "crm_enquiries_org_code_idx" ON "crm_enquiries" USING btree ("organization_id","code");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "crm_leads_org_code_idx" ON "crm_leads" USING btree ("organization_id","code");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "crm_quotations_org_code_idx" ON "crm_quotations" USING btree ("organization_id","code");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "crm_report_definitions_org_code_idx" ON "crm_report_definitions" USING btree ("organization_id","code");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "crm_role_profiles_org_code_idx" ON "crm_role_profiles" USING btree ("organization_id","code");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "crm_user_role_assignments_org_user_role_idx" ON "crm_user_role_assignments" USING btree ("organization_id","user_id","role_profile_id");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "document_sequences_org_doc_period_branch_idx" ON "document_sequences" USING btree ("organization_id","document_type","period","branch_id");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "ms_options_org_category_code_idx" ON "ms_options" USING btree ("organization_id","category","code");--> 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");
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -5,155 +5,8 @@
|
|||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1781148450992,
|
"when": 1782285058938,
|
||||||
"tag": "0000_icy_lizard",
|
"tag": "0000_swift_prism",
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 1,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1781171200406,
|
|
||||||
"tag": "0001_orange_mandarin",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 2,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1781498575433,
|
|
||||||
"tag": "0002_plain_anthem",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 3,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1781505431077,
|
|
||||||
"tag": "0003_blushing_bruce_banner",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 4,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1781511534267,
|
|
||||||
"tag": "0004_worthless_ender_wiggin",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 5,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1781575976324,
|
|
||||||
"tag": "0005_numerous_blob",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 6,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1781579746622,
|
|
||||||
"tag": "0006_worried_mordo",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 7,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1781589458455,
|
|
||||||
"tag": "0007_luxuriant_malice",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 8,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1781595962545,
|
|
||||||
"tag": "0008_clean_justin_hammer",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 9,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1781603421415,
|
|
||||||
"tag": "0009_lazy_shard",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 10,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1781675409847,
|
|
||||||
"tag": "0010_calm_wendell_rand",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 11,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1781747812297,
|
|
||||||
"tag": "0011_goofy_the_hood",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 12,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1781836261952,
|
|
||||||
"tag": "0012_simple_bucky",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 13,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1782097963376,
|
|
||||||
"tag": "0013_great_nicolaos",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 14,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1782099743528,
|
|
||||||
"tag": "0014_bored_valkyrie",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 15,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1782549600000,
|
|
||||||
"tag": "0015_customer_ownership_contact_shares",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 16,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1782795600000,
|
|
||||||
"tag": "0016_won_lost_lifecycle_governance",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 17,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1782121929212,
|
|
||||||
"tag": "0017_short_swordsman",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 18,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1782799200000,
|
|
||||||
"tag": "0018_report_foundation",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 19,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1782137095907,
|
|
||||||
"tag": "0019_sleepy_ultron",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 20,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1782262800000,
|
|
||||||
"tag": "0020_lead_foundation_schema",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 21,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1782279100730,
|
|
||||||
"tag": "0021_worried_nebula",
|
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
9
drizzle_bk/0022_lead_assignment_foundation.sql
Normal file
9
drizzle_bk/0022_lead_assignment_foundation.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
ALTER TABLE "crm_leads"
|
||||||
|
ADD COLUMN IF NOT EXISTS "description" text,
|
||||||
|
ADD COLUMN IF NOT EXISTS "product_type" text,
|
||||||
|
ADD COLUMN IF NOT EXISTS "priority" text,
|
||||||
|
ADD COLUMN IF NOT EXISTS "estimated_value" double precision,
|
||||||
|
ADD COLUMN IF NOT EXISTS "assigned_sales_owner_id" text,
|
||||||
|
ADD COLUMN IF NOT EXISTS "assigned_at" timestamp with time zone,
|
||||||
|
ADD COLUMN IF NOT EXISTS "assigned_by" text,
|
||||||
|
ADD COLUMN IF NOT EXISTS "assignment_remark" text;
|
||||||
8
drizzle_bk/0023_yellow_carlie_cooper.sql
Normal file
8
drizzle_bk/0023_yellow_carlie_cooper.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
ALTER TABLE "crm_leads" ADD COLUMN "description" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "crm_leads" ADD COLUMN "product_type" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "crm_leads" ADD COLUMN "priority" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "crm_leads" ADD COLUMN "estimated_value" double precision;--> statement-breakpoint
|
||||||
|
ALTER TABLE "crm_leads" ADD COLUMN "assigned_sales_owner_id" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "crm_leads" ADD COLUMN "assigned_at" timestamp with time zone;--> statement-breakpoint
|
||||||
|
ALTER TABLE "crm_leads" ADD COLUMN "assigned_by" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "crm_leads" ADD COLUMN "assignment_remark" text;
|
||||||
331
drizzle_bk/meta/0000_snapshot.json
Normal file
331
drizzle_bk/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
{
|
||||||
|
"id": "1e236d6a-ca42-4830-b230-67a7c233d87b",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"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.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.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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
4263
drizzle_bk/meta/0023_snapshot.json
Normal file
4263
drizzle_bk/meta/0023_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
174
drizzle_bk/meta/_journal.json
Normal file
174
drizzle_bk/meta/_journal.json
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1781148450992,
|
||||||
|
"tag": "0000_icy_lizard",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1781171200406,
|
||||||
|
"tag": "0001_orange_mandarin",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1781498575433,
|
||||||
|
"tag": "0002_plain_anthem",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1781505431077,
|
||||||
|
"tag": "0003_blushing_bruce_banner",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1781511534267,
|
||||||
|
"tag": "0004_worthless_ender_wiggin",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 5,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1781575976324,
|
||||||
|
"tag": "0005_numerous_blob",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 6,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1781579746622,
|
||||||
|
"tag": "0006_worried_mordo",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 7,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1781589458455,
|
||||||
|
"tag": "0007_luxuriant_malice",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1781595962545,
|
||||||
|
"tag": "0008_clean_justin_hammer",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 9,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1781603421415,
|
||||||
|
"tag": "0009_lazy_shard",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 10,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1781675409847,
|
||||||
|
"tag": "0010_calm_wendell_rand",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 11,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1781747812297,
|
||||||
|
"tag": "0011_goofy_the_hood",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 12,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1781836261952,
|
||||||
|
"tag": "0012_simple_bucky",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 13,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1782097963376,
|
||||||
|
"tag": "0013_great_nicolaos",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 14,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1782099743528,
|
||||||
|
"tag": "0014_bored_valkyrie",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 15,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1782549600000,
|
||||||
|
"tag": "0015_customer_ownership_contact_shares",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 16,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1782795600000,
|
||||||
|
"tag": "0016_won_lost_lifecycle_governance",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 17,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1782121929212,
|
||||||
|
"tag": "0017_short_swordsman",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 18,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1782799200000,
|
||||||
|
"tag": "0018_report_foundation",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 19,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1782137095907,
|
||||||
|
"tag": "0019_sleepy_ultron",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 20,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1782262800000,
|
||||||
|
"tag": "0020_lead_foundation_schema",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 21,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1782279100730,
|
||||||
|
"tag": "0021_worried_nebula",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 22,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1782286200000,
|
||||||
|
"tag": "0022_lead_assignment_foundation",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 23,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1782284237696,
|
||||||
|
"tag": "0023_yellow_carlie_cooper",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
537
plans/task-d.5.3.md
Normal file
537
plans/task-d.5.3.md
Normal file
@@ -0,0 +1,537 @@
|
|||||||
|
# Task D.5.3: Lead Assignment → Create Enquiry Foundation
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Planned
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Implement the first production transition from Lead into Enquiry.
|
||||||
|
|
||||||
|
This task establishes the business boundary between:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Marketing Domain
|
||||||
|
↓
|
||||||
|
Lead
|
||||||
|
|
||||||
|
Sales Domain
|
||||||
|
↓
|
||||||
|
Enquiry
|
||||||
|
```
|
||||||
|
|
||||||
|
A Lead remains the marketing-owned source record.
|
||||||
|
|
||||||
|
An Enquiry becomes the sales execution workspace.
|
||||||
|
|
||||||
|
This task introduces:
|
||||||
|
|
||||||
|
* Lead assignment workflow
|
||||||
|
* Enquiry creation from Lead
|
||||||
|
* Lead ↔ Enquiry relationship
|
||||||
|
* Initial ownership transfer
|
||||||
|
* Enquiry bootstrap generation
|
||||||
|
|
||||||
|
without changing quotation, dashboard, reporting, or approval behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Mandatory Review
|
||||||
|
|
||||||
|
Review:
|
||||||
|
|
||||||
|
* AGENTS.md
|
||||||
|
* Project Foundations Registry
|
||||||
|
* Task Catalog
|
||||||
|
* ADR-0018 Lead / Enquiry Domain Separation
|
||||||
|
* Task D.5
|
||||||
|
* Task D.5.1
|
||||||
|
* Task D.5.2
|
||||||
|
* Customer Ownership Foundation
|
||||||
|
* CRM Security Foundation
|
||||||
|
* Audit Foundation
|
||||||
|
* Document Sequence Foundation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Existing Foundation
|
||||||
|
|
||||||
|
Verified available:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
crm_leads
|
||||||
|
crm_enquiries
|
||||||
|
crm_enquiries.lead_id
|
||||||
|
|
||||||
|
crm_lead sequence
|
||||||
|
crm_enquiry sequence
|
||||||
|
|
||||||
|
Lead Service
|
||||||
|
Lead API
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not recreate:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
tables
|
||||||
|
migrations
|
||||||
|
seed data
|
||||||
|
master-option categories
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Business Model
|
||||||
|
|
||||||
|
## Before Assignment
|
||||||
|
|
||||||
|
Owner:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Marketing
|
||||||
|
```
|
||||||
|
|
||||||
|
Workspace:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Lead
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## After Assignment
|
||||||
|
|
||||||
|
Owner:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Sales
|
||||||
|
```
|
||||||
|
|
||||||
|
Workspace:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Enquiry
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Relationship
|
||||||
|
|
||||||
|
```txt
|
||||||
|
1 Lead
|
||||||
|
↓
|
||||||
|
0..N Enquiries
|
||||||
|
```
|
||||||
|
|
||||||
|
Lead remains immutable historical source.
|
||||||
|
|
||||||
|
Enquiries become sales execution records.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope D5.3.1 Lead Assignment Service
|
||||||
|
|
||||||
|
Create:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
src/features/crm/leads/server/assignment.service.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Required operation:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
assignLead()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Input:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
AssignLeadInput
|
||||||
|
```
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
leadId
|
||||||
|
salesOwnerId
|
||||||
|
assignmentRemark
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
lead exists
|
||||||
|
lead active
|
||||||
|
salesOwnerId valid
|
||||||
|
same organization
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope D5.3.2 Create Enquiry From Lead
|
||||||
|
|
||||||
|
Create:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
createEnquiryFromLead()
|
||||||
|
```
|
||||||
|
|
||||||
|
inside assignment service.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Rules
|
||||||
|
|
||||||
|
Assignment automatically creates:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
crm_enquiry
|
||||||
|
```
|
||||||
|
|
||||||
|
linked to:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
crm_leads.id
|
||||||
|
```
|
||||||
|
|
||||||
|
through:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
crm_enquiries.lead_id
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Reuse:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
generateNextDocumentCode()
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
crm_enquiry
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
EN2606-001
|
||||||
|
```
|
||||||
|
|
||||||
|
or existing enquiry format.
|
||||||
|
|
||||||
|
No custom numbering logic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope D5.3.3 Enquiry Bootstrap Mapping
|
||||||
|
|
||||||
|
When enquiry is created:
|
||||||
|
|
||||||
|
Copy:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
customerId
|
||||||
|
contactId
|
||||||
|
projectName
|
||||||
|
projectLocation
|
||||||
|
productType
|
||||||
|
description
|
||||||
|
estimatedValue
|
||||||
|
priority
|
||||||
|
```
|
||||||
|
|
||||||
|
from Lead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Do NOT copy:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
marketing-only notes
|
||||||
|
internal marketing activity history
|
||||||
|
audit events
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Lead remains unchanged.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope D5.3.4 Lead Assignment API
|
||||||
|
|
||||||
|
Create:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
POST /api/crm/leads/[id]/assign
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Handler must call:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Lead Assignment Service
|
||||||
|
```
|
||||||
|
|
||||||
|
No direct DB access.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
leadId: string;
|
||||||
|
enquiryId: string;
|
||||||
|
enquiryCode: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope D5.3.5 Ownership Transfer
|
||||||
|
|
||||||
|
After assignment:
|
||||||
|
|
||||||
|
Lead:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
status = assigned
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Lead:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
assigned_sales_owner_id
|
||||||
|
assigned_at
|
||||||
|
assigned_by
|
||||||
|
```
|
||||||
|
|
||||||
|
updated.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Enquiry:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
owner_user_id
|
||||||
|
```
|
||||||
|
|
||||||
|
set to assigned salesperson.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Marketing remains able to:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
view lead
|
||||||
|
view assignment result
|
||||||
|
```
|
||||||
|
|
||||||
|
but not manage enquiry.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope D5.3.6 CRM Security
|
||||||
|
|
||||||
|
Reuse:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
resolveCrmMembershipAccess()
|
||||||
|
```
|
||||||
|
|
||||||
|
Assignment permission:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
lead.assign
|
||||||
|
```
|
||||||
|
|
||||||
|
Only:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Marketing Manager
|
||||||
|
CRM Manager
|
||||||
|
Admin
|
||||||
|
```
|
||||||
|
|
||||||
|
or equivalent resolved permission.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Do not check role strings directly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope D5.3.7 Duplicate Protection
|
||||||
|
|
||||||
|
Prevent:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
multiple enquiry creation
|
||||||
|
```
|
||||||
|
|
||||||
|
for the same assignment action.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
If assignment already produced an active enquiry:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
return existing enquiry
|
||||||
|
```
|
||||||
|
|
||||||
|
instead of creating a second one.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope D5.3.8 Audit Logging
|
||||||
|
|
||||||
|
Reuse audit foundation.
|
||||||
|
|
||||||
|
Required actions:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
assign_lead
|
||||||
|
create_enquiry_from_lead
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Audit must capture:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
leadId
|
||||||
|
enquiryId
|
||||||
|
salesOwnerId
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope D5.3.9 Legacy Compatibility
|
||||||
|
|
||||||
|
Do not modify:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
quotation flow
|
||||||
|
approval flow
|
||||||
|
dashboard
|
||||||
|
reports
|
||||||
|
won/lost process
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Current enquiry APIs must continue functioning.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Deliverables
|
||||||
|
|
||||||
|
New:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
src/features/crm/leads/server/assignment.service.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
New API:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
POST /api/crm/leads/[id]/assign
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Documentation:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
docs/implementation/task-d53-lead-assignment-create-enquiry-foundation.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Verification
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
npm exec tsc --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Assign Lead
|
||||||
|
Create Enquiry
|
||||||
|
Reuse Existing Enquiry
|
||||||
|
Lead Status Update
|
||||||
|
Audit Logging
|
||||||
|
Security Enforcement
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Quotation APIs still work
|
||||||
|
Dashboard still works
|
||||||
|
Reports still work
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Explicit Non-Scope
|
||||||
|
|
||||||
|
Do not:
|
||||||
|
|
||||||
|
* create Lead UI
|
||||||
|
* create Opportunity UI
|
||||||
|
* create quotation conversion
|
||||||
|
* change approval workflow
|
||||||
|
* change reports
|
||||||
|
* change dashboard
|
||||||
|
* migrate historical data
|
||||||
|
* implement won/lost synchronization
|
||||||
|
|
||||||
|
These belong to later D.5 phases.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Definition of Done
|
||||||
|
|
||||||
|
Task is complete when:
|
||||||
|
|
||||||
|
* Lead assignment API exists
|
||||||
|
* Assignment creates or reuses enquiry
|
||||||
|
* crm_enquiries.lead_id is populated
|
||||||
|
* Enquiry owner is assigned salesperson
|
||||||
|
* Lead assignment is audited
|
||||||
|
* Existing CRM functionality remains unchanged
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Lead Assignment = Established
|
||||||
|
|
||||||
|
Lead → Enquiry Transition = Established
|
||||||
|
|
||||||
|
Ready for Opportunity Workspace Separation
|
||||||
|
```
|
||||||
506
plans/task-d.5.4.md
Normal file
506
plans/task-d.5.4.md
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
# Task D.5.4: Lead / Enquiry Workspace Separation UI Foundation
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Planned
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Separate the user-facing CRM workspace between Lead and Enquiry.
|
||||||
|
|
||||||
|
This task makes Lead and Enquiry visible as different working areas while preserving existing enquiry, quotation, dashboard, and reporting behavior.
|
||||||
|
|
||||||
|
Business boundary:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Lead = Marketing-owned source record
|
||||||
|
Enquiry = Sales execution workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
This task focuses on UI routing, navigation, data-source separation, and safe compatibility wiring.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mandatory Review
|
||||||
|
|
||||||
|
Review before implementation:
|
||||||
|
|
||||||
|
* `AGENTS.md`
|
||||||
|
* `docs/standards/project-foundations.md`
|
||||||
|
* `docs/standards/task-catalog.md`
|
||||||
|
* `docs/adr/0018-lead-enquiry-domain-separation.md`
|
||||||
|
* `docs/implementation/task-d5-lead-enquiry-domain-separation.md`
|
||||||
|
* `docs/implementation/task-d51-lead-foundation-stabilization.md`
|
||||||
|
* `docs/implementation/task-d52-lead-domain-service-api-foundation.md`
|
||||||
|
* `docs/implementation/task-d53-lead-assignment-create-enquiry-foundation.md`
|
||||||
|
* `docs/security/crm-authorization-boundaries.md`
|
||||||
|
* Existing CRM navigation/sidebar configuration
|
||||||
|
* Existing enquiry list/detail/form UI
|
||||||
|
* Existing lead API routes
|
||||||
|
* Existing enquiry API routes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Existing Foundation
|
||||||
|
|
||||||
|
Already available:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
crm_leads
|
||||||
|
crm_enquiries
|
||||||
|
crm_enquiries.lead_id
|
||||||
|
|
||||||
|
/api/crm/leads
|
||||||
|
/api/crm/leads/[id]
|
||||||
|
/api/crm/leads/[id]/followups
|
||||||
|
/api/crm/leads/[id]/assign
|
||||||
|
|
||||||
|
existing enquiry APIs
|
||||||
|
existing quotation APIs
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not recreate:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
tables
|
||||||
|
migrations
|
||||||
|
seed data
|
||||||
|
service foundation
|
||||||
|
assignment service
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workspace Rules
|
||||||
|
|
||||||
|
### Lead Workspace
|
||||||
|
|
||||||
|
Audience:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Marketing
|
||||||
|
Marketing Manager
|
||||||
|
CRM Manager
|
||||||
|
Admin
|
||||||
|
```
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
capture lead
|
||||||
|
qualify lead
|
||||||
|
record awareness/status/lost reason
|
||||||
|
assign lead to sales
|
||||||
|
view linked enquiry result
|
||||||
|
```
|
||||||
|
|
||||||
|
Source:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
crm_leads
|
||||||
|
/api/crm/leads
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Enquiry Workspace
|
||||||
|
|
||||||
|
Audience:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Sales
|
||||||
|
Sales Manager
|
||||||
|
CRM Manager
|
||||||
|
Admin
|
||||||
|
```
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
work assigned opportunity
|
||||||
|
prepare for quotation
|
||||||
|
track sales execution
|
||||||
|
continue existing enquiry lifecycle
|
||||||
|
```
|
||||||
|
|
||||||
|
Source:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
crm_enquiries
|
||||||
|
existing /api/crm/enquiries
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope D5.4.1 Navigation Separation
|
||||||
|
|
||||||
|
Update CRM navigation so Lead and Enquiry are separate menu items.
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
CRM > Leads
|
||||||
|
CRM > Enquiries
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
* Lead menu uses lead permission/scope
|
||||||
|
* Enquiry menu keeps existing permission/scope
|
||||||
|
* Do not rename Enquiry menu globally if it breaks existing routes
|
||||||
|
* Do not remove old enquiry route yet
|
||||||
|
|
||||||
|
Suggested routes:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
/dashboard/crm/leads
|
||||||
|
/dashboard/crm/leads/[id]
|
||||||
|
|
||||||
|
/dashboard/crm/enquiries
|
||||||
|
/dashboard/crm/enquiries/[id]
|
||||||
|
```
|
||||||
|
|
||||||
|
If project uses another route prefix, follow existing app route convention.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope D5.4.2 Lead List Page
|
||||||
|
|
||||||
|
Create Lead list page using `/api/crm/leads`.
|
||||||
|
|
||||||
|
Required columns:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Lead Code
|
||||||
|
Customer
|
||||||
|
Project Name
|
||||||
|
Status
|
||||||
|
Awareness
|
||||||
|
Estimated Value
|
||||||
|
Suggested Sales Owner
|
||||||
|
Assigned Sales Owner
|
||||||
|
Created Date
|
||||||
|
```
|
||||||
|
|
||||||
|
Required actions:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
View
|
||||||
|
Edit
|
||||||
|
Assign
|
||||||
|
Delete
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
* Assign action appears only when user has assignment permission
|
||||||
|
* Delete action appears only when user has delete permission
|
||||||
|
* No direct enquiry mutation from Lead list
|
||||||
|
* Use existing shadcn/table/list conventions
|
||||||
|
* Use design tokens only
|
||||||
|
* Follow existing CRM page layout pattern
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope D5.4.3 Lead Detail Page
|
||||||
|
|
||||||
|
Create Lead detail page using `/api/crm/leads/[id]`.
|
||||||
|
|
||||||
|
Sections:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Lead Summary
|
||||||
|
Customer Information
|
||||||
|
Qualification Information
|
||||||
|
Follow-up History
|
||||||
|
Assignment Information
|
||||||
|
Linked Enquiries
|
||||||
|
Audit-safe Activity View
|
||||||
|
```
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Edit Lead
|
||||||
|
Assign Lead
|
||||||
|
Create Follow-up
|
||||||
|
Delete Lead
|
||||||
|
Open Linked Enquiry
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
* Assignment creates or reuses enquiry through `/api/crm/leads/[id]/assign`
|
||||||
|
* After assignment, show linked enquiry
|
||||||
|
* Marketing can see assignment result but must not edit enquiry unless permitted
|
||||||
|
* Follow-up history uses D.5.2 audit-backed implementation until future ADR decides final persistence
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope D5.4.4 Lead Create / Edit Form
|
||||||
|
|
||||||
|
Create or adapt Lead form components.
|
||||||
|
|
||||||
|
Required fields:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Customer
|
||||||
|
Contact
|
||||||
|
Project Name
|
||||||
|
Project Location
|
||||||
|
Description
|
||||||
|
Product Type
|
||||||
|
Priority
|
||||||
|
Estimated Value
|
||||||
|
Awareness
|
||||||
|
Status
|
||||||
|
Lost Reason
|
||||||
|
Notes
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
* Use master options for awareness, status, lost reason
|
||||||
|
* No hardcoded labels
|
||||||
|
* Product type must use existing option/source convention
|
||||||
|
* Validate with D.5.2 lead schemas
|
||||||
|
* On create, call `POST /api/crm/leads`
|
||||||
|
* On edit, call `PATCH /api/crm/leads/[id]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope D5.4.5 Lead Assignment UI
|
||||||
|
|
||||||
|
Create assignment dialog or sheet.
|
||||||
|
|
||||||
|
Input:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Sales Owner
|
||||||
|
Assignment Remark
|
||||||
|
```
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
POST /api/crm/leads/[id]/assign
|
||||||
|
```
|
||||||
|
|
||||||
|
After success:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
show enquiry code
|
||||||
|
show Open Enquiry action
|
||||||
|
refresh lead detail
|
||||||
|
refresh lead list
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
* Sales owner list must use assignable sales members only
|
||||||
|
* If API returns existing enquiry, show reuse message instead of duplicate-created message
|
||||||
|
* Do not create enquiry client-side
|
||||||
|
* Do not mutate `crm_enquiries` directly from UI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope D5.4.6 Enquiry UI Compatibility
|
||||||
|
|
||||||
|
Keep current enquiry UI operational.
|
||||||
|
|
||||||
|
Add optional linked-lead display if `lead_id` exists:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Source Lead
|
||||||
|
Lead Code
|
||||||
|
Open Lead
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
* Do not rewrite enquiry workflow
|
||||||
|
* Do not change quotation conversion behavior
|
||||||
|
* Do not change dashboard/report queries
|
||||||
|
* Do not remove `pipeline_stage`
|
||||||
|
* Existing enquiry list/detail must still work
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope D5.4.7 Permission / Visibility Handling
|
||||||
|
|
||||||
|
UI must respect backend permissions but not replace backend enforcement.
|
||||||
|
|
||||||
|
Lead UI:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
crm.lead.read
|
||||||
|
crm.lead.create
|
||||||
|
crm.lead.update
|
||||||
|
crm.lead.delete
|
||||||
|
crm.lead.assign
|
||||||
|
```
|
||||||
|
|
||||||
|
Enquiry UI:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
existing enquiry permissions
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
* Hide unauthorized actions
|
||||||
|
* Handle 403 gracefully
|
||||||
|
* Do not check role strings directly if access helpers exist
|
||||||
|
* Follow existing CRM access resolver/client conventions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope D5.4.8 Empty / Loading / Error States
|
||||||
|
|
||||||
|
Implement consistent states:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
loading
|
||||||
|
empty
|
||||||
|
not authorized
|
||||||
|
not found
|
||||||
|
server error
|
||||||
|
validation error
|
||||||
|
```
|
||||||
|
|
||||||
|
Messaging:
|
||||||
|
|
||||||
|
* Lead empty state should guide Marketing to create first Lead
|
||||||
|
* Enquiry empty state should remain sales-oriented
|
||||||
|
* Assignment success should clearly identify created/reused enquiry
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope D5.4.9 Documentation
|
||||||
|
|
||||||
|
Create:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
docs/implementation/task-d54-lead-enquiry-workspace-separation-ui-foundation.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Document:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
routes added
|
||||||
|
components added
|
||||||
|
APIs consumed
|
||||||
|
permission mapping
|
||||||
|
compatibility notes
|
||||||
|
known limitations
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deliverables
|
||||||
|
|
||||||
|
New or updated pages:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
src/app/**/crm/leads/page.tsx
|
||||||
|
src/app/**/crm/leads/[id]/page.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
New or updated components:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
src/features/crm/leads/components/lead-list.tsx
|
||||||
|
src/features/crm/leads/components/lead-form.tsx
|
||||||
|
src/features/crm/leads/components/lead-detail.tsx
|
||||||
|
src/features/crm/leads/components/lead-assignment-dialog.tsx
|
||||||
|
src/features/crm/leads/components/lead-followup-panel.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
Updated:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
CRM navigation/sidebar
|
||||||
|
Enquiry detail optional Source Lead display
|
||||||
|
```
|
||||||
|
|
||||||
|
Documentation:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
docs/implementation/task-d54-lead-enquiry-workspace-separation-ui-foundation.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Explicit Non-Scope
|
||||||
|
|
||||||
|
Do not:
|
||||||
|
|
||||||
|
* create new tables
|
||||||
|
* create migrations
|
||||||
|
* change lead assignment service behavior
|
||||||
|
* change enquiry service behavior beyond read-only linked lead display
|
||||||
|
* change quotation flow
|
||||||
|
* change approval flow
|
||||||
|
* change dashboard datasets
|
||||||
|
* change reports
|
||||||
|
* implement won/lost synchronization
|
||||||
|
* replace audit-backed lead follow-up persistence
|
||||||
|
* migrate historical enquiry records into leads
|
||||||
|
|
||||||
|
These belong to later D.5 phases.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
npm exec tsc --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional if project supports:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
npm run lint
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Manual verification:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Lead menu appears for permitted users
|
||||||
|
Lead menu hidden or blocked for unauthorized users
|
||||||
|
Lead list loads from /api/crm/leads
|
||||||
|
Lead create works
|
||||||
|
Lead edit works
|
||||||
|
Lead detail works
|
||||||
|
Lead assign creates or reuses enquiry
|
||||||
|
Linked enquiry opens from lead detail
|
||||||
|
Existing enquiry page still works
|
||||||
|
Existing quotation flow still works
|
||||||
|
Dashboard still works
|
||||||
|
Reports still work
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
|
||||||
|
Task is complete when:
|
||||||
|
|
||||||
|
* Lead and Enquiry appear as separate workspaces
|
||||||
|
* Lead UI uses Lead APIs
|
||||||
|
* Enquiry UI remains compatible
|
||||||
|
* Lead assignment UI creates or reuses enquiry through API
|
||||||
|
* Linked enquiry is visible from Lead detail
|
||||||
|
* Existing enquiry, quotation, dashboard, and reporting flows remain unchanged
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Lead Workspace = Established
|
||||||
|
Enquiry Workspace = Preserved
|
||||||
|
Lead / Enquiry UI Boundary = Established
|
||||||
|
Ready for D.5.5 Opportunity / Sales Workspace Refinement
|
||||||
|
```
|
||||||
62
src/app/api/crm/leads/[id]/assign/route.ts
Normal file
62
src/app/api/crm/leads/[id]/assign/route.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { assignLeadSchema } from '@/features/crm/leads/schemas/lead.schema';
|
||||||
|
import { assignLead } from '@/features/crm/leads/server/assignment.service';
|
||||||
|
import { buildLeadAccessContext } from '@/features/crm/leads/server/service';
|
||||||
|
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||||
|
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest, { params }: Params) {
|
||||||
|
try {
|
||||||
|
const { id } = await params;
|
||||||
|
const { organization, session, membership } = await requireOrganizationAccess({
|
||||||
|
permission: PERMISSIONS.crmLeadAssign
|
||||||
|
});
|
||||||
|
const payload = assignLeadSchema.parse(await request.json());
|
||||||
|
const accessContext = buildLeadAccessContext({
|
||||||
|
organizationId: organization.id,
|
||||||
|
userId: session.user.id,
|
||||||
|
membership
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await assignLead(
|
||||||
|
organization.id,
|
||||||
|
session.user.id,
|
||||||
|
{
|
||||||
|
leadId: id,
|
||||||
|
salesOwnerId: payload.salesOwnerId,
|
||||||
|
assignmentRemark: payload.assignmentRemark
|
||||||
|
},
|
||||||
|
accessContext
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Lead assigned successfully',
|
||||||
|
leadId: result.leadId,
|
||||||
|
enquiryId: result.enquiryId,
|
||||||
|
enquiryCode: result.enquiryCode
|
||||||
|
});
|
||||||
|
} 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 lead' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -275,14 +275,22 @@ export const crmLeads = pgTable(
|
|||||||
code: text('code').notNull(),
|
code: text('code').notNull(),
|
||||||
customerId: text('customer_id'),
|
customerId: text('customer_id'),
|
||||||
contactId: text('contact_id'),
|
contactId: text('contact_id'),
|
||||||
|
description: text('description'),
|
||||||
projectName: text('project_name'),
|
projectName: text('project_name'),
|
||||||
projectLocation: text('project_location'),
|
projectLocation: text('project_location'),
|
||||||
|
productType: text('product_type'),
|
||||||
|
priority: text('priority'),
|
||||||
|
estimatedValue: doublePrecision('estimated_value'),
|
||||||
awarenessId: text('awareness_id'),
|
awarenessId: text('awareness_id'),
|
||||||
status: text('status').default('new_job').notNull(),
|
status: text('status').default('new_job').notNull(),
|
||||||
followupStatus: text('followup_status'),
|
followupStatus: text('followup_status'),
|
||||||
lostReason: text('lost_reason'),
|
lostReason: text('lost_reason'),
|
||||||
outcome: text('outcome').default('open').notNull(),
|
outcome: text('outcome').default('open').notNull(),
|
||||||
ownerMarketingUserId: text('owner_marketing_user_id'),
|
ownerMarketingUserId: text('owner_marketing_user_id'),
|
||||||
|
assignedSalesOwnerId: text('assigned_sales_owner_id'),
|
||||||
|
assignedAt: timestamp('assigned_at', { withTimezone: true }),
|
||||||
|
assignedBy: text('assigned_by'),
|
||||||
|
assignmentRemark: text('assignment_remark'),
|
||||||
createdBy: text('created_by').notNull(),
|
createdBy: text('created_by').notNull(),
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
|||||||
@@ -384,8 +384,9 @@ const FOUNDATION_OPTIONS = {
|
|||||||
crm_lead_status: [
|
crm_lead_status: [
|
||||||
{ code: 'new_job', label: 'New Job', value: 'new_job', sortOrder: 1 },
|
{ code: 'new_job', label: 'New Job', value: 'new_job', sortOrder: 1 },
|
||||||
{ code: 'follow_up', label: 'Follow Up', value: 'follow_up', sortOrder: 2 },
|
{ code: 'follow_up', label: 'Follow Up', value: 'follow_up', sortOrder: 2 },
|
||||||
{ code: 'closed_lost', label: 'Closed Lost', value: 'closed_lost', sortOrder: 3 },
|
{ code: 'assigned', label: 'Assigned', value: 'assigned', sortOrder: 3 },
|
||||||
{ code: 'cancel', label: 'Cancel', value: 'cancel', sortOrder: 4 }
|
{ code: 'closed_lost', label: 'Closed Lost', value: 'closed_lost', sortOrder: 4 },
|
||||||
|
{ code: 'cancel', label: 'Cancel', value: 'cancel', sortOrder: 5 }
|
||||||
],
|
],
|
||||||
crm_lead_followup_status: [
|
crm_lead_followup_status: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -448,7 +448,9 @@ export async function assertAssignableUserBelongsToOrganization(
|
|||||||
) {
|
) {
|
||||||
const [membership] = await db
|
const [membership] = await db
|
||||||
.select({
|
.select({
|
||||||
userId: memberships.userId
|
userId: memberships.userId,
|
||||||
|
role: memberships.role,
|
||||||
|
businessRole: memberships.businessRole
|
||||||
})
|
})
|
||||||
.from(memberships)
|
.from(memberships)
|
||||||
.where(and(eq(memberships.organizationId, organizationId), eq(memberships.userId, userId)))
|
.where(and(eq(memberships.organizationId, organizationId), eq(memberships.userId, userId)))
|
||||||
@@ -458,6 +460,10 @@ export async function assertAssignableUserBelongsToOrganization(
|
|||||||
throw new AuthError('Assignee not found in organization', 404);
|
throw new AuthError('Assignee not found in organization', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (membership.role !== 'admin' && !ASSIGNABLE_BUSINESS_ROLES.has(membership.businessRole)) {
|
||||||
|
throw new AuthError('Assignee must be a sales owner', 400);
|
||||||
|
}
|
||||||
|
|
||||||
return membership;
|
return membership;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ export const leadSchema = z.object({
|
|||||||
branchId: z.string().nullable().optional(),
|
branchId: z.string().nullable().optional(),
|
||||||
customerId: z.string().nullable().optional(),
|
customerId: z.string().nullable().optional(),
|
||||||
contactId: z.string().nullable().optional(),
|
contactId: z.string().nullable().optional(),
|
||||||
|
description: z.string().nullable().optional(),
|
||||||
projectName: z.string().nullable().optional(),
|
projectName: z.string().nullable().optional(),
|
||||||
projectLocation: z.string().nullable().optional(),
|
projectLocation: z.string().nullable().optional(),
|
||||||
|
productType: z.string().nullable().optional(),
|
||||||
|
priority: z.string().nullable().optional(),
|
||||||
|
estimatedValue: z.number().nullable().optional(),
|
||||||
awarenessId: z.string().nullable().optional(),
|
awarenessId: z.string().nullable().optional(),
|
||||||
status: z.string().min(1, 'Please select a lead status'),
|
status: z.string().min(1, 'Please select lead status'),
|
||||||
followupStatus: z.string().nullable().optional(),
|
followupStatus: z.string().nullable().optional(),
|
||||||
lostReason: z.string().nullable().optional(),
|
lostReason: z.string().nullable().optional(),
|
||||||
outcome: z.enum(['open', 'won', 'lost']).default('open'),
|
outcome: z.enum(['open', 'won', 'lost']).default('open'),
|
||||||
@@ -16,9 +20,14 @@ export const leadSchema = z.object({
|
|||||||
|
|
||||||
export const updateLeadSchema = leadSchema.partial();
|
export const updateLeadSchema = leadSchema.partial();
|
||||||
|
|
||||||
|
export const assignLeadSchema = z.object({
|
||||||
|
salesOwnerId: z.string().min(1, 'Sales owner is required'),
|
||||||
|
assignmentRemark: z.string().nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
export const leadFollowupSchema = z.object({
|
export const leadFollowupSchema = z.object({
|
||||||
followupDate: z.string().min(1, 'Follow-up date is required'),
|
followupDate: z.string().min(1, 'Follow-up date required'),
|
||||||
followupStatus: z.string().min(1, 'Please select a follow-up status'),
|
followupStatus: z.string().min(1, 'Please select follow-up status'),
|
||||||
note: z.string().nullable().optional(),
|
note: z.string().nullable().optional(),
|
||||||
nextFollowupDate: z.string().nullable().optional()
|
nextFollowupDate: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|||||||
300
src/features/crm/leads/server/assignment.service.ts
Normal file
300
src/features/crm/leads/server/assignment.service.ts
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
import { and, asc, eq, inArray, isNull } from 'drizzle-orm';
|
||||||
|
import { crmEnquiries, crmLeads } from '@/db/schema';
|
||||||
|
import { db } from '@/lib/db';
|
||||||
|
import { AuthError } from '@/lib/auth/session';
|
||||||
|
import { generateNextDocumentCode } from '@/features/foundation/document-sequence/service';
|
||||||
|
import { auditAction } from '@/features/foundation/audit-log/service';
|
||||||
|
import { getActiveOptionsByCategory } from '@/features/foundation/master-options/service';
|
||||||
|
import {
|
||||||
|
canAccessScopedCrmRecord,
|
||||||
|
type CrmSecurityContext
|
||||||
|
} from '@/features/crm/security/server/service';
|
||||||
|
import {
|
||||||
|
assertAssignableUserBelongsToOrganization,
|
||||||
|
assertContactBelongsToOrganization,
|
||||||
|
assertCustomerBelongsToOrganization
|
||||||
|
} from '@/features/crm/enquiries/server/service';
|
||||||
|
import type { AssignLeadInput, AssignLeadResult } from '../types';
|
||||||
|
|
||||||
|
export type LeadAssignmentAccessContext = CrmSecurityContext;
|
||||||
|
|
||||||
|
type LeadRow = typeof crmLeads.$inferSelect;
|
||||||
|
type EnquiryRow = typeof crmEnquiries.$inferSelect;
|
||||||
|
|
||||||
|
function canAccessLeadRow(row: LeadRow, accessContext: LeadAssignmentAccessContext) {
|
||||||
|
return canAccessScopedCrmRecord(accessContext, {
|
||||||
|
branchId: row.branchId,
|
||||||
|
createdBy: row.createdBy,
|
||||||
|
ownerUserId: row.ownerMarketingUserId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertLeadBelongsToOrganization(
|
||||||
|
leadId: string,
|
||||||
|
organizationId: string,
|
||||||
|
accessContext?: LeadAssignmentAccessContext
|
||||||
|
) {
|
||||||
|
const [lead] = await db
|
||||||
|
.select()
|
||||||
|
.from(crmLeads)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(crmLeads.id, leadId),
|
||||||
|
eq(crmLeads.organizationId, organizationId),
|
||||||
|
isNull(crmLeads.deletedAt)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!lead) {
|
||||||
|
throw new AuthError('Lead not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessContext && !canAccessLeadRow(lead, accessContext)) {
|
||||||
|
throw new AuthError('Forbidden', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lead;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveDefaultEnquiryStatusId(organizationId: string) {
|
||||||
|
const options = await getActiveOptionsByCategory('crm_enquiry_status', { organizationId });
|
||||||
|
const preferred = options.find((option) => option.code === 'new');
|
||||||
|
const resolved = preferred ?? options[0];
|
||||||
|
|
||||||
|
if (!resolved) {
|
||||||
|
throw new AuthError('Enquiry status options are not configured', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findActiveEnquiryByLead(leadId: string, organizationId: string) {
|
||||||
|
const [enquiry] = await db
|
||||||
|
.select()
|
||||||
|
.from(crmEnquiries)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(crmEnquiries.leadId, leadId),
|
||||||
|
eq(crmEnquiries.organizationId, organizationId),
|
||||||
|
inArray(crmEnquiries.pipelineStage, ['lead', 'enquiry']),
|
||||||
|
isNull(crmEnquiries.deletedAt)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(asc(crmEnquiries.createdAt))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return enquiry ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncLeadAssignment(input: {
|
||||||
|
leadId: string;
|
||||||
|
status: string;
|
||||||
|
salesOwnerId: string;
|
||||||
|
assignedBy: string;
|
||||||
|
assignmentRemark?: string | null;
|
||||||
|
}) {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(crmLeads)
|
||||||
|
.set({
|
||||||
|
status: input.status,
|
||||||
|
assignedSalesOwnerId: input.salesOwnerId,
|
||||||
|
assignedAt: now,
|
||||||
|
assignedBy: input.assignedBy,
|
||||||
|
assignmentRemark: input.assignmentRemark?.trim() || null,
|
||||||
|
updatedAt: now
|
||||||
|
})
|
||||||
|
.where(eq(crmLeads.id, input.leadId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createEnquiryFromLead(input: {
|
||||||
|
lead: LeadRow;
|
||||||
|
organizationId: string;
|
||||||
|
actorUserId: string;
|
||||||
|
salesOwnerId: string;
|
||||||
|
assignmentRemark?: string | null;
|
||||||
|
}) {
|
||||||
|
if (!input.lead.customerId) {
|
||||||
|
throw new AuthError('Lead requires a customer before assignment', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!input.lead.productType) {
|
||||||
|
throw new AuthError('Lead requires a product type before assignment', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!input.lead.priority) {
|
||||||
|
throw new AuthError('Lead requires a priority before assignment', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
await assertCustomerBelongsToOrganization(input.lead.customerId, input.organizationId);
|
||||||
|
|
||||||
|
if (input.lead.contactId) {
|
||||||
|
await assertContactBelongsToOrganization(
|
||||||
|
input.lead.contactId,
|
||||||
|
input.organizationId,
|
||||||
|
input.lead.customerId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [statusId, documentCode] = await Promise.all([
|
||||||
|
resolveDefaultEnquiryStatusId(input.organizationId),
|
||||||
|
generateNextDocumentCode({
|
||||||
|
organizationId: input.organizationId,
|
||||||
|
documentType: 'crm_enquiry',
|
||||||
|
branchId: input.lead.branchId ?? null
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const [created] = await db
|
||||||
|
.insert(crmEnquiries)
|
||||||
|
.values({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
organizationId: input.organizationId,
|
||||||
|
branchId: input.lead.branchId ?? null,
|
||||||
|
leadId: input.lead.id,
|
||||||
|
code: documentCode.code,
|
||||||
|
customerId: input.lead.customerId,
|
||||||
|
contactId: input.lead.contactId ?? null,
|
||||||
|
title: input.lead.projectName?.trim() || `Lead ${input.lead.code}`,
|
||||||
|
description: input.lead.description?.trim() || null,
|
||||||
|
requirement: null,
|
||||||
|
projectName: input.lead.projectName?.trim() || null,
|
||||||
|
projectLocation: input.lead.projectLocation?.trim() || null,
|
||||||
|
productType: input.lead.productType,
|
||||||
|
status: statusId,
|
||||||
|
priority: input.lead.priority,
|
||||||
|
leadChannel: null,
|
||||||
|
estimatedValue: input.lead.estimatedValue ?? null,
|
||||||
|
chancePercent: null,
|
||||||
|
expectedCloseDate: null,
|
||||||
|
competitor: null,
|
||||||
|
source: `lead:${input.lead.code}`,
|
||||||
|
notes: null,
|
||||||
|
isHotProject: false,
|
||||||
|
isActive: true,
|
||||||
|
pipelineStage: 'enquiry',
|
||||||
|
assignedToUserId: input.salesOwnerId,
|
||||||
|
assignedAt: now,
|
||||||
|
assignedBy: input.actorUserId,
|
||||||
|
assignmentRemark: input.assignmentRemark?.trim() || null,
|
||||||
|
createdBy: input.actorUserId,
|
||||||
|
updatedBy: input.actorUserId
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assignLead(
|
||||||
|
organizationId: string,
|
||||||
|
actorUserId: string,
|
||||||
|
input: AssignLeadInput,
|
||||||
|
accessContext?: LeadAssignmentAccessContext
|
||||||
|
): Promise<AssignLeadResult> {
|
||||||
|
const lead = await assertLeadBelongsToOrganization(input.leadId, organizationId, accessContext);
|
||||||
|
await assertAssignableUserBelongsToOrganization(input.salesOwnerId, organizationId);
|
||||||
|
|
||||||
|
const existingEnquiry = await findActiveEnquiryByLead(input.leadId, organizationId);
|
||||||
|
|
||||||
|
if (existingEnquiry) {
|
||||||
|
const resolvedSalesOwnerId = existingEnquiry.assignedToUserId ?? input.salesOwnerId;
|
||||||
|
|
||||||
|
if (!existingEnquiry.assignedToUserId) {
|
||||||
|
await db
|
||||||
|
.update(crmEnquiries)
|
||||||
|
.set({
|
||||||
|
pipelineStage: 'enquiry',
|
||||||
|
assignedToUserId: input.salesOwnerId,
|
||||||
|
assignedAt: new Date(),
|
||||||
|
assignedBy: actorUserId,
|
||||||
|
assignmentRemark: input.assignmentRemark?.trim() || null,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
updatedBy: actorUserId
|
||||||
|
})
|
||||||
|
.where(eq(crmEnquiries.id, existingEnquiry.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
await syncLeadAssignment({
|
||||||
|
leadId: input.leadId,
|
||||||
|
status: 'assigned',
|
||||||
|
salesOwnerId: resolvedSalesOwnerId,
|
||||||
|
assignedBy: actorUserId,
|
||||||
|
assignmentRemark: input.assignmentRemark
|
||||||
|
});
|
||||||
|
|
||||||
|
await auditAction({
|
||||||
|
organizationId,
|
||||||
|
branchId: lead.branchId,
|
||||||
|
userId: actorUserId,
|
||||||
|
entityType: 'crm_lead',
|
||||||
|
entityId: input.leadId,
|
||||||
|
action: 'assign_lead',
|
||||||
|
afterData: {
|
||||||
|
leadId: input.leadId,
|
||||||
|
enquiryId: existingEnquiry.id,
|
||||||
|
salesOwnerId: resolvedSalesOwnerId,
|
||||||
|
reusedExistingEnquiry: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
leadId: input.leadId,
|
||||||
|
enquiryId: existingEnquiry.id,
|
||||||
|
enquiryCode: existingEnquiry.code
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdEnquiry = await createEnquiryFromLead({
|
||||||
|
lead,
|
||||||
|
organizationId,
|
||||||
|
actorUserId,
|
||||||
|
salesOwnerId: input.salesOwnerId,
|
||||||
|
assignmentRemark: input.assignmentRemark
|
||||||
|
});
|
||||||
|
|
||||||
|
await syncLeadAssignment({
|
||||||
|
leadId: input.leadId,
|
||||||
|
status: 'assigned',
|
||||||
|
salesOwnerId: input.salesOwnerId,
|
||||||
|
assignedBy: actorUserId,
|
||||||
|
assignmentRemark: input.assignmentRemark
|
||||||
|
});
|
||||||
|
|
||||||
|
await auditAction({
|
||||||
|
organizationId,
|
||||||
|
branchId: lead.branchId,
|
||||||
|
userId: actorUserId,
|
||||||
|
entityType: 'crm_enquiry',
|
||||||
|
entityId: createdEnquiry.id,
|
||||||
|
action: 'create_enquiry_from_lead',
|
||||||
|
afterData: {
|
||||||
|
leadId: input.leadId,
|
||||||
|
enquiryId: createdEnquiry.id,
|
||||||
|
salesOwnerId: input.salesOwnerId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await auditAction({
|
||||||
|
organizationId,
|
||||||
|
branchId: lead.branchId,
|
||||||
|
userId: actorUserId,
|
||||||
|
entityType: 'crm_lead',
|
||||||
|
entityId: input.leadId,
|
||||||
|
action: 'assign_lead',
|
||||||
|
afterData: {
|
||||||
|
leadId: input.leadId,
|
||||||
|
enquiryId: createdEnquiry.id,
|
||||||
|
salesOwnerId: input.salesOwnerId,
|
||||||
|
reusedExistingEnquiry: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
leadId: input.leadId,
|
||||||
|
enquiryId: createdEnquiry.id,
|
||||||
|
enquiryCode: createdEnquiry.code
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -49,7 +49,9 @@ const LEAD_OPTION_CATEGORIES = {
|
|||||||
awareness: 'crm_lead_awareness',
|
awareness: 'crm_lead_awareness',
|
||||||
status: 'crm_lead_status',
|
status: 'crm_lead_status',
|
||||||
followupStatus: 'crm_lead_followup_status',
|
followupStatus: 'crm_lead_followup_status',
|
||||||
lostReason: 'crm_lead_lost_reason'
|
lostReason: 'crm_lead_lost_reason',
|
||||||
|
productType: 'crm_product_type',
|
||||||
|
priority: 'crm_priority'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type LeadAccessContext = CrmSecurityContext;
|
export type LeadAccessContext = CrmSecurityContext;
|
||||||
@@ -61,6 +63,8 @@ type LeadLookupMaps = {
|
|||||||
statusLabels: Map<string, string>;
|
statusLabels: Map<string, string>;
|
||||||
followupStatusLabels: Map<string, string>;
|
followupStatusLabels: Map<string, string>;
|
||||||
lostReasonLabels: Map<string, string>;
|
lostReasonLabels: Map<string, string>;
|
||||||
|
productTypeLabels: Map<string, string>;
|
||||||
|
priorityLabels: Map<string, string>;
|
||||||
customerNames: Map<string, string>;
|
customerNames: Map<string, string>;
|
||||||
contactNames: Map<string, string>;
|
contactNames: Map<string, string>;
|
||||||
userNames: Map<string, string>;
|
userNames: Map<string, string>;
|
||||||
@@ -121,8 +125,12 @@ function mapLeadSummary(row: LeadRow, lookups: LeadLookupMaps): LeadSummary {
|
|||||||
customerName: row.customerId ? (lookups.customerNames.get(row.customerId) ?? null) : null,
|
customerName: row.customerId ? (lookups.customerNames.get(row.customerId) ?? null) : null,
|
||||||
contactId: row.contactId ?? null,
|
contactId: row.contactId ?? null,
|
||||||
contactName: row.contactId ? (lookups.contactNames.get(row.contactId) ?? null) : null,
|
contactName: row.contactId ? (lookups.contactNames.get(row.contactId) ?? null) : null,
|
||||||
|
description: row.description ?? null,
|
||||||
projectName: row.projectName ?? null,
|
projectName: row.projectName ?? null,
|
||||||
projectLocation: row.projectLocation ?? null,
|
projectLocation: row.projectLocation ?? null,
|
||||||
|
productType: row.productType ?? null,
|
||||||
|
priority: row.priority ?? null,
|
||||||
|
estimatedValue: row.estimatedValue ?? null,
|
||||||
awarenessId: row.awarenessId ?? null,
|
awarenessId: row.awarenessId ?? null,
|
||||||
awarenessLabel: row.awarenessId ? (lookups.awarenessLabels.get(row.awarenessId) ?? null) : null,
|
awarenessLabel: row.awarenessId ? (lookups.awarenessLabels.get(row.awarenessId) ?? null) : null,
|
||||||
status: row.status,
|
status: row.status,
|
||||||
@@ -138,6 +146,14 @@ function mapLeadSummary(row: LeadRow, lookups: LeadLookupMaps): LeadSummary {
|
|||||||
ownerMarketingName: row.ownerMarketingUserId
|
ownerMarketingName: row.ownerMarketingUserId
|
||||||
? (lookups.userNames.get(row.ownerMarketingUserId) ?? null)
|
? (lookups.userNames.get(row.ownerMarketingUserId) ?? null)
|
||||||
: null,
|
: null,
|
||||||
|
assignedSalesOwnerId: row.assignedSalesOwnerId ?? null,
|
||||||
|
assignedSalesOwnerName: row.assignedSalesOwnerId
|
||||||
|
? (lookups.userNames.get(row.assignedSalesOwnerId) ?? null)
|
||||||
|
: null,
|
||||||
|
assignedAt: toIsoDateTime(row.assignedAt),
|
||||||
|
assignedBy: row.assignedBy ?? null,
|
||||||
|
assignedByName: row.assignedBy ? (lookups.userNames.get(row.assignedBy) ?? null) : null,
|
||||||
|
assignmentRemark: row.assignmentRemark ?? null,
|
||||||
createdBy: row.createdBy,
|
createdBy: row.createdBy,
|
||||||
createdAt: row.createdAt.toISOString(),
|
createdAt: row.createdAt.toISOString(),
|
||||||
updatedAt: row.updatedAt.toISOString(),
|
updatedAt: row.updatedAt.toISOString(),
|
||||||
@@ -339,11 +355,14 @@ async function assertLeadBelongsToOrganization(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function buildLookupMaps(organizationId: string, rows: LeadRow[]): Promise<LeadLookupMaps> {
|
async function buildLookupMaps(organizationId: string, rows: LeadRow[]): Promise<LeadLookupMaps> {
|
||||||
const [awarenesses, statuses, followupStatuses, lostReasons] = await Promise.all([
|
const [awarenesses, statuses, followupStatuses, lostReasons, productTypes, priorities] =
|
||||||
|
await Promise.all([
|
||||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.awareness, { organizationId }),
|
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.awareness, { organizationId }),
|
||||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.status, { organizationId }),
|
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.status, { organizationId }),
|
||||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.followupStatus, { organizationId }),
|
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.followupStatus, { organizationId }),
|
||||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.lostReason, { organizationId })
|
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.lostReason, { organizationId }),
|
||||||
|
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.productType, { organizationId }),
|
||||||
|
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.priority, { organizationId })
|
||||||
]);
|
]);
|
||||||
const customerIds = [
|
const customerIds = [
|
||||||
...new Set(rows.map((row) => row.customerId).filter((value): value is string => Boolean(value)))
|
...new Set(rows.map((row) => row.customerId).filter((value): value is string => Boolean(value)))
|
||||||
@@ -354,7 +373,12 @@ async function buildLookupMaps(organizationId: string, rows: LeadRow[]): Promise
|
|||||||
const userIds = [
|
const userIds = [
|
||||||
...new Set(
|
...new Set(
|
||||||
rows
|
rows
|
||||||
.flatMap((row) => [row.ownerMarketingUserId, row.createdBy])
|
.flatMap((row) => [
|
||||||
|
row.ownerMarketingUserId,
|
||||||
|
row.assignedSalesOwnerId,
|
||||||
|
row.assignedBy,
|
||||||
|
row.createdBy
|
||||||
|
])
|
||||||
.filter((value): value is string => Boolean(value))
|
.filter((value): value is string => Boolean(value))
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
@@ -385,6 +409,8 @@ async function buildLookupMaps(organizationId: string, rows: LeadRow[]): Promise
|
|||||||
statusLabels: new Map(statuses.map((item) => [item.id, item.label])),
|
statusLabels: new Map(statuses.map((item) => [item.id, item.label])),
|
||||||
followupStatusLabels: new Map(followupStatuses.map((item) => [item.id, item.label])),
|
followupStatusLabels: new Map(followupStatuses.map((item) => [item.id, item.label])),
|
||||||
lostReasonLabels: new Map(lostReasons.map((item) => [item.id, item.label])),
|
lostReasonLabels: new Map(lostReasons.map((item) => [item.id, item.label])),
|
||||||
|
productTypeLabels: new Map(productTypes.map((item) => [item.id, item.label])),
|
||||||
|
priorityLabels: new Map(priorities.map((item) => [item.id, item.label])),
|
||||||
customerNames: new Map(customerRows.map((item) => [item.id, item.name])),
|
customerNames: new Map(customerRows.map((item) => [item.id, item.name])),
|
||||||
contactNames: new Map(contactRows.map((item) => [item.id, item.name])),
|
contactNames: new Map(contactRows.map((item) => [item.id, item.name])),
|
||||||
userNames: new Map(userRows.map((item) => [item.id, item.name]))
|
userNames: new Map(userRows.map((item) => [item.id, item.name]))
|
||||||
@@ -399,6 +425,12 @@ async function validateLeadPayload(
|
|||||||
) {
|
) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
assertMasterOptionValue(organizationId, LEAD_OPTION_CATEGORIES.awareness, payload.awarenessId),
|
assertMasterOptionValue(organizationId, LEAD_OPTION_CATEGORIES.awareness, payload.awarenessId),
|
||||||
|
assertMasterOptionValue(
|
||||||
|
organizationId,
|
||||||
|
LEAD_OPTION_CATEGORIES.productType,
|
||||||
|
payload.productType
|
||||||
|
),
|
||||||
|
assertMasterOptionValue(organizationId, LEAD_OPTION_CATEGORIES.priority, payload.priority),
|
||||||
payload.status
|
payload.status
|
||||||
? assertMasterOptionValue(organizationId, LEAD_OPTION_CATEGORIES.status, payload.status)
|
? assertMasterOptionValue(organizationId, LEAD_OPTION_CATEGORIES.status, payload.status)
|
||||||
: Promise.resolve(),
|
: Promise.resolve(),
|
||||||
@@ -454,11 +486,14 @@ async function validateLeadPayload(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getLeadReferenceData(organizationId: string): Promise<LeadReferenceData> {
|
export async function getLeadReferenceData(organizationId: string): Promise<LeadReferenceData> {
|
||||||
const [awarenesses, statuses, followupStatuses, lostReasons, branches] = await Promise.all([
|
const [awarenesses, statuses, followupStatuses, lostReasons, productTypes, priorities, branches] =
|
||||||
|
await Promise.all([
|
||||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.awareness, { organizationId }),
|
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.awareness, { organizationId }),
|
||||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.status, { organizationId }),
|
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.status, { organizationId }),
|
||||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.followupStatus, { organizationId }),
|
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.followupStatus, { organizationId }),
|
||||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.lostReason, { organizationId }),
|
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.lostReason, { organizationId }),
|
||||||
|
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.productType, { organizationId }),
|
||||||
|
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.priority, { organizationId }),
|
||||||
getUserBranches()
|
getUserBranches()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -467,6 +502,8 @@ export async function getLeadReferenceData(organizationId: string): Promise<Lead
|
|||||||
statuses: statuses.map(mapOption),
|
statuses: statuses.map(mapOption),
|
||||||
followupStatuses: followupStatuses.map(mapOption),
|
followupStatuses: followupStatuses.map(mapOption),
|
||||||
lostReasons: lostReasons.map(mapOption),
|
lostReasons: lostReasons.map(mapOption),
|
||||||
|
productTypes: productTypes.map(mapOption),
|
||||||
|
priorities: priorities.map(mapOption),
|
||||||
branches: branches.map(mapBranch)
|
branches: branches.map(mapBranch)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -570,8 +607,12 @@ export async function createLead(
|
|||||||
code: codeResult.code,
|
code: codeResult.code,
|
||||||
customerId: payload.customerId ?? null,
|
customerId: payload.customerId ?? null,
|
||||||
contactId: payload.contactId ?? null,
|
contactId: payload.contactId ?? null,
|
||||||
|
description: payload.description?.trim() || null,
|
||||||
projectName: payload.projectName?.trim() || null,
|
projectName: payload.projectName?.trim() || null,
|
||||||
projectLocation: payload.projectLocation?.trim() || null,
|
projectLocation: payload.projectLocation?.trim() || null,
|
||||||
|
productType: payload.productType ?? null,
|
||||||
|
priority: payload.priority ?? null,
|
||||||
|
estimatedValue: payload.estimatedValue ?? null,
|
||||||
awarenessId: payload.awarenessId ?? null,
|
awarenessId: payload.awarenessId ?? null,
|
||||||
status: payload.status,
|
status: payload.status,
|
||||||
followupStatus: payload.followupStatus ?? null,
|
followupStatus: payload.followupStatus ?? null,
|
||||||
@@ -600,9 +641,14 @@ export async function updateLead(
|
|||||||
payload.contactId === undefined
|
payload.contactId === undefined
|
||||||
? current.contactId
|
? current.contactId
|
||||||
: payload.contactId,
|
: payload.contactId,
|
||||||
|
description: payload.description === undefined ? current.description : payload.description,
|
||||||
projectName: payload.projectName === undefined ? current.projectName : payload.projectName,
|
projectName: payload.projectName === undefined ? current.projectName : payload.projectName,
|
||||||
projectLocation:
|
projectLocation:
|
||||||
payload.projectLocation === undefined ? current.projectLocation : payload.projectLocation,
|
payload.projectLocation === undefined ? current.projectLocation : payload.projectLocation,
|
||||||
|
productType: payload.productType === undefined ? current.productType : payload.productType,
|
||||||
|
priority: payload.priority === undefined ? current.priority : payload.priority,
|
||||||
|
estimatedValue:
|
||||||
|
payload.estimatedValue === undefined ? current.estimatedValue : payload.estimatedValue,
|
||||||
awarenessId: payload.awarenessId === undefined ? current.awarenessId : payload.awarenessId,
|
awarenessId: payload.awarenessId === undefined ? current.awarenessId : payload.awarenessId,
|
||||||
status: payload.status ?? current.status,
|
status: payload.status ?? current.status,
|
||||||
followupStatus:
|
followupStatus:
|
||||||
@@ -623,8 +669,12 @@ export async function updateLead(
|
|||||||
branchId: nextPayload.branchId ?? null,
|
branchId: nextPayload.branchId ?? null,
|
||||||
customerId: nextPayload.customerId ?? null,
|
customerId: nextPayload.customerId ?? null,
|
||||||
contactId: nextPayload.contactId ?? null,
|
contactId: nextPayload.contactId ?? null,
|
||||||
|
description: nextPayload.description?.trim() || null,
|
||||||
projectName: nextPayload.projectName?.trim() || null,
|
projectName: nextPayload.projectName?.trim() || null,
|
||||||
projectLocation: nextPayload.projectLocation?.trim() || null,
|
projectLocation: nextPayload.projectLocation?.trim() || null,
|
||||||
|
productType: nextPayload.productType ?? null,
|
||||||
|
priority: nextPayload.priority ?? null,
|
||||||
|
estimatedValue: nextPayload.estimatedValue ?? null,
|
||||||
awarenessId: nextPayload.awarenessId ?? null,
|
awarenessId: nextPayload.awarenessId ?? null,
|
||||||
status: nextPayload.status,
|
status: nextPayload.status,
|
||||||
followupStatus: nextPayload.followupStatus ?? null,
|
followupStatus: nextPayload.followupStatus ?? null,
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export interface LeadReferenceData {
|
|||||||
statuses: LeadOption[];
|
statuses: LeadOption[];
|
||||||
followupStatuses: LeadOption[];
|
followupStatuses: LeadOption[];
|
||||||
lostReasons: LeadOption[];
|
lostReasons: LeadOption[];
|
||||||
|
productTypes: LeadOption[];
|
||||||
|
priorities: LeadOption[];
|
||||||
branches: LeadBranchOption[];
|
branches: LeadBranchOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,8 +26,12 @@ export interface CreateLeadInput {
|
|||||||
branchId?: string | null;
|
branchId?: string | null;
|
||||||
customerId?: string | null;
|
customerId?: string | null;
|
||||||
contactId?: string | null;
|
contactId?: string | null;
|
||||||
|
description?: string | null;
|
||||||
projectName?: string | null;
|
projectName?: string | null;
|
||||||
projectLocation?: string | null;
|
projectLocation?: string | null;
|
||||||
|
productType?: string | null;
|
||||||
|
priority?: string | null;
|
||||||
|
estimatedValue?: number | null;
|
||||||
awarenessId?: string | null;
|
awarenessId?: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
followupStatus?: string | null;
|
followupStatus?: string | null;
|
||||||
@@ -38,8 +44,12 @@ export interface UpdateLeadInput {
|
|||||||
branchId?: string | null;
|
branchId?: string | null;
|
||||||
customerId?: string | null;
|
customerId?: string | null;
|
||||||
contactId?: string | null;
|
contactId?: string | null;
|
||||||
|
description?: string | null;
|
||||||
projectName?: string | null;
|
projectName?: string | null;
|
||||||
projectLocation?: string | null;
|
projectLocation?: string | null;
|
||||||
|
productType?: string | null;
|
||||||
|
priority?: string | null;
|
||||||
|
estimatedValue?: number | null;
|
||||||
awarenessId?: string | null;
|
awarenessId?: string | null;
|
||||||
status?: string;
|
status?: string;
|
||||||
followupStatus?: string | null;
|
followupStatus?: string | null;
|
||||||
@@ -69,6 +79,12 @@ export interface CreateLeadFollowupInput {
|
|||||||
nextFollowupDate?: string | null;
|
nextFollowupDate?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AssignLeadInput {
|
||||||
|
leadId: string;
|
||||||
|
salesOwnerId: string;
|
||||||
|
assignmentRemark?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LeadSummary {
|
export interface LeadSummary {
|
||||||
id: string;
|
id: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
@@ -78,8 +94,12 @@ export interface LeadSummary {
|
|||||||
customerName: string | null;
|
customerName: string | null;
|
||||||
contactId: string | null;
|
contactId: string | null;
|
||||||
contactName: string | null;
|
contactName: string | null;
|
||||||
|
description: string | null;
|
||||||
projectName: string | null;
|
projectName: string | null;
|
||||||
projectLocation: string | null;
|
projectLocation: string | null;
|
||||||
|
productType: string | null;
|
||||||
|
priority: string | null;
|
||||||
|
estimatedValue: number | null;
|
||||||
awarenessId: string | null;
|
awarenessId: string | null;
|
||||||
awarenessLabel: string | null;
|
awarenessLabel: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
@@ -91,6 +111,12 @@ export interface LeadSummary {
|
|||||||
outcome: string;
|
outcome: string;
|
||||||
ownerMarketingUserId: string | null;
|
ownerMarketingUserId: string | null;
|
||||||
ownerMarketingName: string | null;
|
ownerMarketingName: string | null;
|
||||||
|
assignedSalesOwnerId: string | null;
|
||||||
|
assignedSalesOwnerName: string | null;
|
||||||
|
assignedAt: string | null;
|
||||||
|
assignedBy: string | null;
|
||||||
|
assignedByName: string | null;
|
||||||
|
assignmentRemark: string | null;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -119,3 +145,9 @@ export interface LeadFollowupSummary {
|
|||||||
createdBy: string;
|
createdBy: string;
|
||||||
createdByName: string | null;
|
createdByName: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AssignLeadResult {
|
||||||
|
leadId: string;
|
||||||
|
enquiryId: string;
|
||||||
|
enquiryCode: string;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user