task-d.6.2
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generatedAt": "2026-07-01T02:01:56.976Z",
|
||||
"generatedAt": "2026-07-01T04:26:29.390Z",
|
||||
"overallStatus": "PASS",
|
||||
"quotationCode": "CRA2606-1107",
|
||||
"artifacts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# PDF Integrity Summary
|
||||
|
||||
- Generated At: 2026-07-01T02:01:56.976Z
|
||||
- Generated At: 2026-07-01T04:26:29.390Z
|
||||
- Overall Status: PASS
|
||||
- Quotation Code: CRA2606-1107
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generatedAt": "2026-07-01T02:01:55.746Z",
|
||||
"generatedAt": "2026-07-01T04:26:26.164Z",
|
||||
"overallStatus": "PASS",
|
||||
"quotationCode": "CRA2606-1107",
|
||||
"expectedFixtureCode": "QT-H5-AUDIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# PDF Audit Report
|
||||
|
||||
- Generated At: 2026-07-01T02:01:55.746Z
|
||||
- Generated At: 2026-07-01T04:26:26.164Z
|
||||
- Overall Status: PASS
|
||||
- Quotation Code: CRA2606-1107
|
||||
- Template: ALLA Demo Quotation Standard v1.0
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Task D.6.2 Implementation Report - 2026-07-01
|
||||
|
||||
## Scope
|
||||
- Prefill quotation creation from opportunity detail so users do not need to reselect the same opportunity.
|
||||
- Add `Project Close Date` and `Delivery Date` to both opportunity and quotation flows.
|
||||
- Enforce backend-supported hot-project auto suggestion with same-session manual override support.
|
||||
|
||||
## Review Summary
|
||||
Reviewed before implementation:
|
||||
- [AGENTS.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/AGENTS.md)
|
||||
- [plans/task-d.6.2.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/plans/task-d.6.2.md)
|
||||
- [docs/standards/project-foundations.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/standards/project-foundations.md)
|
||||
- [docs/standards/architecture-rules.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/standards/architecture-rules.md)
|
||||
- [docs/standards/ui-ux-rules.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/standards/ui-ux-rules.md)
|
||||
- [docs/standards/task-review-checklist.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/standards/task-review-checklist.md)
|
||||
- [docs/business/crm-terminology.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/business/crm-terminology.md)
|
||||
- [docs/adr/0018-lead-enquiry-domain-separation.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/adr/0018-lead-enquiry-domain-separation.md)
|
||||
- [docs/implementation/task-d.6-crm-activity-foundation-audit-log-separation-2026-07-01.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/implementation/task-d.6-crm-activity-foundation-audit-log-separation-2026-07-01.md)
|
||||
- [docs/implementation/task-d.6.1-lead-create-form-ux-default-rules-2026-07-01.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/implementation/task-d.6.1-lead-create-form-ux-default-rules-2026-07-01.md)
|
||||
|
||||
## Implementation Summary
|
||||
- Added shared hot-project decision logic in [hot-project.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/shared/hot-project.ts) so frontend and backend use the same `projectCloseDate <= current date + 3 months` rule.
|
||||
- Extended `crm_opportunities` and `crm_quotations` with `project_close_date`, `delivery_date`, `hot_project_auto_suggested`, and `hot_project_manually_overridden`, plus migration [0002_opportunity_quotation_timeline_hot_project.sql](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/drizzle/0002_opportunity_quotation_timeline_hot_project.sql).
|
||||
- Updated DTOs, Zod schemas, and server services so create/update flows accept the new date fields, validate `YYYY-MM-DD`, auto-suggest hot-project state, and preserve manual override flags.
|
||||
- Updated opportunity form to show calendar-based `Project Close Date` and `Delivery Date`, and to stop re-forcing the hot-project checkbox after the user manually changes it in the same session.
|
||||
- Rebuilt quotation form flow to support:
|
||||
- opening from opportunity detail with prefilled opportunity/customer/contact/project data
|
||||
- timeline field carry-forward (`projectCloseDate`, `deliveryDate`)
|
||||
- hot-project auto suggestion with manual override preservation
|
||||
- touched-field protection so post-open prefills do not overwrite user edits
|
||||
- Updated opportunity detail to pass quotation prefill context and display new timeline values.
|
||||
|
||||
## Files Changed
|
||||
- [src/db/schema.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/db/schema.ts)
|
||||
- [drizzle/0002_opportunity_quotation_timeline_hot_project.sql](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/drizzle/0002_opportunity_quotation_timeline_hot_project.sql)
|
||||
- [drizzle/meta/_journal.json](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/drizzle/meta/_journal.json)
|
||||
- [src/features/crm/shared/hot-project.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/shared/hot-project.ts)
|
||||
- [src/features/crm/opportunities/api/types.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/opportunities/api/types.ts)
|
||||
- [src/features/crm/opportunities/schemas/opportunity.schema.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/opportunities/schemas/opportunity.schema.ts)
|
||||
- [src/features/crm/opportunities/server/service.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/opportunities/server/service.ts)
|
||||
- [src/features/crm/opportunities/components/opportunity-form-sheet.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/opportunities/components/opportunity-form-sheet.tsx)
|
||||
- [src/features/crm/opportunities/components/opportunity-detail.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/opportunities/components/opportunity-detail.tsx)
|
||||
- [src/app/api/crm/opportunities/route.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/opportunities/route.ts)
|
||||
- [src/app/api/crm/opportunities/[id]/route.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/opportunities/[id]/route.ts)
|
||||
- [src/features/crm/quotations/api/types.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/quotations/api/types.ts)
|
||||
- [src/features/crm/quotations/schemas/quotation.schema.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/quotations/schemas/quotation.schema.ts)
|
||||
- [src/features/crm/quotations/server/service.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/quotations/server/service.ts)
|
||||
- [src/features/crm/quotations/components/quotation-form-sheet.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/quotations/components/quotation-form-sheet.tsx)
|
||||
|
||||
## Verification
|
||||
- `npm run typecheck` ✅
|
||||
- `npm run build` ✅
|
||||
- Existing warnings remained from Turbopack workspace-root inference and the broad local storage provider path pattern in `src/features/foundation/storage/server/local-provider.ts`.
|
||||
- `npm run audit:pdf` ✅
|
||||
- Regenerated audit artifacts and reports under `artifacts/pdf-audit/` and `docs/implementation/pdf-audit-report.*`.
|
||||
|
||||
## Outcome
|
||||
- Opportunity detail now launches quotation creation with selected opportunity context already attached.
|
||||
- Opportunity and quotation both persist project timeline dates.
|
||||
- Hot-project suggestion is deterministic, backend-backed, and no longer fights the user after manual override during the same edit session.
|
||||
11
drizzle/0002_opportunity_quotation_timeline_hot_project.sql
Normal file
11
drizzle/0002_opportunity_quotation_timeline_hot_project.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
ALTER TABLE "crm_opportunities"
|
||||
ADD COLUMN "project_close_date" timestamp with time zone,
|
||||
ADD COLUMN "delivery_date" timestamp with time zone,
|
||||
ADD COLUMN "hot_project_auto_suggested" boolean DEFAULT false NOT NULL,
|
||||
ADD COLUMN "hot_project_manually_overridden" boolean DEFAULT false NOT NULL;
|
||||
|
||||
ALTER TABLE "crm_quotations"
|
||||
ADD COLUMN "project_close_date" timestamp with time zone,
|
||||
ADD COLUMN "delivery_date" timestamp with time zone,
|
||||
ADD COLUMN "hot_project_auto_suggested" boolean DEFAULT false NOT NULL,
|
||||
ADD COLUMN "hot_project_manually_overridden" boolean DEFAULT false NOT NULL;
|
||||
10
drizzle/0003_curious_wild_child.sql
Normal file
10
drizzle/0003_curious_wild_child.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
ALTER TABLE "crm_leads" ALTER COLUMN "status" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "crm_leads" ADD COLUMN "lead_channel" text;--> statement-breakpoint
|
||||
ALTER TABLE "crm_opportunities" ADD COLUMN "project_close_date" timestamp with time zone;--> statement-breakpoint
|
||||
ALTER TABLE "crm_opportunities" ADD COLUMN "delivery_date" timestamp with time zone;--> statement-breakpoint
|
||||
ALTER TABLE "crm_opportunities" ADD COLUMN "hot_project_auto_suggested" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "crm_opportunities" ADD COLUMN "hot_project_manually_overridden" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "crm_quotations" ADD COLUMN "project_close_date" timestamp with time zone;--> statement-breakpoint
|
||||
ALTER TABLE "crm_quotations" ADD COLUMN "delivery_date" timestamp with time zone;--> statement-breakpoint
|
||||
ALTER TABLE "crm_quotations" ADD COLUMN "hot_project_auto_suggested" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "crm_quotations" ADD COLUMN "hot_project_manually_overridden" boolean DEFAULT false NOT NULL;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,20 @@
|
||||
"when": 1782896400000,
|
||||
"tag": "0001_lead_channel_and_status_default",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1782896500000,
|
||||
"tag": "0002_opportunity_quotation_timeline_hot_project",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1782881325779,
|
||||
"tag": "0003_curious_wild_child",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
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,
|
||||
"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 "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 "organizations_slug_idx" ON "organizations" USING btree ("slug");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email");
|
||||
@@ -1,46 +0,0 @@
|
||||
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 "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 "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 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");
|
||||
@@ -1,52 +0,0 @@
|
||||
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_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,
|
||||
"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 UNIQUE INDEX "crm_customers_org_code_idx" ON "crm_customers" USING btree ("organization_id","code");
|
||||
@@ -1,50 +0,0 @@
|
||||
CREATE TABLE "crm_enquiries" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"organization_id" text NOT NULL,
|
||||
"branch_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,
|
||||
"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_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 UNIQUE INDEX "crm_enquiries_org_code_idx" ON "crm_enquiries" USING btree ("organization_id","code");
|
||||
@@ -1,138 +0,0 @@
|
||||
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,
|
||||
"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,
|
||||
"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 UNIQUE INDEX "crm_quotations_org_code_idx" ON "crm_quotations" USING btree ("organization_id","code");
|
||||
@@ -1,57 +0,0 @@
|
||||
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 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");
|
||||
@@ -1,66 +0,0 @@
|
||||
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 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");
|
||||
@@ -1,4 +0,0 @@
|
||||
ALTER TABLE "crm_quotations" ADD COLUMN "approved_at" timestamp with time zone;--> statement-breakpoint
|
||||
ALTER TABLE "crm_quotations" ADD COLUMN "approved_pdf_url" text;--> statement-breakpoint
|
||||
ALTER TABLE "crm_quotations" ADD COLUMN "approved_snapshot" jsonb;--> statement-breakpoint
|
||||
ALTER TABLE "crm_quotations" ADD COLUMN "approved_template_version_id" text;
|
||||
@@ -1,5 +0,0 @@
|
||||
ALTER TABLE "crm_enquiries" ADD COLUMN "assigned_to_user_id" text;--> statement-breakpoint
|
||||
ALTER TABLE "crm_enquiries" ADD COLUMN "assigned_at" timestamp with time zone;--> statement-breakpoint
|
||||
ALTER TABLE "crm_enquiries" ADD COLUMN "assigned_by" text;--> statement-breakpoint
|
||||
ALTER TABLE "crm_enquiries" ADD COLUMN "assignment_remark" text;--> statement-breakpoint
|
||||
ALTER TABLE "memberships" ALTER COLUMN "business_role" SET DEFAULT 'sales_support';
|
||||
@@ -1,30 +0,0 @@
|
||||
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
|
||||
ALTER TABLE "memberships" ALTER COLUMN "business_role" SET DEFAULT 'sales_support';--> statement-breakpoint
|
||||
ALTER TABLE "crm_quotations" ADD COLUMN "approved_artifact_id" text;
|
||||
@@ -1,13 +0,0 @@
|
||||
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
|
||||
ALTER TABLE "crm_quotation_customers" ADD COLUMN "remark" text;
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE "crm_customers"
|
||||
ADD COLUMN "customer_sub_group" text;
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE "crm_customers" ADD COLUMN "customer_sub_group" text;
|
||||
@@ -1,31 +0,0 @@
|
||||
ALTER TABLE "crm_enquiries"
|
||||
ADD COLUMN "pipeline_stage" text;
|
||||
|
||||
UPDATE "crm_enquiries" AS enquiry
|
||||
SET "pipeline_stage" = CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM "ms_options" AS option
|
||||
WHERE option."id" = enquiry."status"
|
||||
AND option."category" = 'crm_enquiry_status'
|
||||
AND option."code" IN ('closed_lost', 'cancelled')
|
||||
AND option."deleted_at" IS NULL
|
||||
) THEN 'closed_lost'
|
||||
WHEN enquiry."assigned_to_user_id" IS NOT NULL THEN 'enquiry'
|
||||
ELSE 'lead'
|
||||
END
|
||||
WHERE enquiry."pipeline_stage" IS NULL;
|
||||
|
||||
UPDATE "crm_enquiries"
|
||||
SET "pipeline_stage" = 'lead'
|
||||
WHERE "pipeline_stage" IS NULL;
|
||||
|
||||
ALTER TABLE "crm_enquiries"
|
||||
ALTER COLUMN "pipeline_stage" SET DEFAULT 'lead';
|
||||
|
||||
ALTER TABLE "crm_enquiries"
|
||||
ALTER COLUMN "pipeline_stage" SET NOT NULL;
|
||||
|
||||
ALTER TABLE "crm_enquiries"
|
||||
ADD CONSTRAINT "crm_enquiries_pipeline_stage_check"
|
||||
CHECK ("pipeline_stage" IN ('lead', 'enquiry', 'closed_won', 'closed_lost'));
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE "crm_enquiries" ADD COLUMN "pipeline_stage" text DEFAULT 'lead' NOT NULL;
|
||||
@@ -1,23 +0,0 @@
|
||||
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
|
||||
ALTER TABLE "memberships" ADD COLUMN "branch_scope_ids" text[] DEFAULT '{}' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "memberships" ADD COLUMN "product_type_scope_ids" text[] DEFAULT '{}' NOT NULL;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "crm_role_profiles_org_code_idx" ON "crm_role_profiles" USING btree ("organization_id","code");
|
||||
@@ -1,19 +0,0 @@
|
||||
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 UNIQUE INDEX "crm_user_role_assignments_org_user_role_idx" ON "crm_user_role_assignments" USING btree ("organization_id","user_id","role_profile_id");
|
||||
@@ -1,20 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS "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
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "crm_user_role_assignments_org_user_role_idx"
|
||||
ON "crm_user_role_assignments" ("organization_id", "user_id", "role_profile_id");
|
||||
@@ -1,35 +0,0 @@
|
||||
ALTER TABLE "crm_customers"
|
||||
ADD COLUMN IF NOT EXISTS "owner_user_id" text,
|
||||
ADD COLUMN IF NOT EXISTS "owner_assigned_at" timestamp with time zone,
|
||||
ADD COLUMN IF NOT EXISTS "owner_assigned_by" text;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "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
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "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
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "crm_contact_shares_org_contact_shared_user_idx"
|
||||
ON "crm_contact_shares" ("organization_id", "contact_id", "shared_to_user_id");
|
||||
@@ -1,30 +0,0 @@
|
||||
ALTER TABLE "crm_enquiries"
|
||||
ADD COLUMN IF NOT EXISTS "closed_won_at" timestamp with time zone,
|
||||
ADD COLUMN IF NOT EXISTS "closed_lost_at" timestamp with time zone,
|
||||
ADD COLUMN IF NOT EXISTS "closed_by_user_id" text,
|
||||
ADD COLUMN IF NOT EXISTS "po_number" text,
|
||||
ADD COLUMN IF NOT EXISTS "po_date" timestamp with time zone,
|
||||
ADD COLUMN IF NOT EXISTS "po_amount" double precision,
|
||||
ADD COLUMN IF NOT EXISTS "po_currency" text,
|
||||
ADD COLUMN IF NOT EXISTS "lost_reason" text,
|
||||
ADD COLUMN IF NOT EXISTS "lost_competitor" text,
|
||||
ADD COLUMN IF NOT EXISTS "lost_remark" text;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "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
|
||||
);
|
||||
@@ -1,61 +0,0 @@
|
||||
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_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_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
|
||||
ALTER TABLE "crm_customers" ADD COLUMN "owner_user_id" text;--> statement-breakpoint
|
||||
ALTER TABLE "crm_customers" ADD COLUMN "owner_assigned_at" timestamp with time zone;--> statement-breakpoint
|
||||
ALTER TABLE "crm_customers" ADD COLUMN "owner_assigned_by" text;--> statement-breakpoint
|
||||
ALTER TABLE "crm_enquiries" ADD COLUMN "closed_won_at" timestamp with time zone;--> statement-breakpoint
|
||||
ALTER TABLE "crm_enquiries" ADD COLUMN "closed_lost_at" timestamp with time zone;--> statement-breakpoint
|
||||
ALTER TABLE "crm_enquiries" ADD COLUMN "closed_by_user_id" text;--> statement-breakpoint
|
||||
ALTER TABLE "crm_enquiries" ADD COLUMN "po_number" text;--> statement-breakpoint
|
||||
ALTER TABLE "crm_enquiries" ADD COLUMN "po_date" timestamp with time zone;--> statement-breakpoint
|
||||
ALTER TABLE "crm_enquiries" ADD COLUMN "po_amount" double precision;--> statement-breakpoint
|
||||
ALTER TABLE "crm_enquiries" ADD COLUMN "po_currency" text;--> statement-breakpoint
|
||||
ALTER TABLE "crm_enquiries" ADD COLUMN "lost_reason" text;--> statement-breakpoint
|
||||
ALTER TABLE "crm_enquiries" ADD COLUMN "lost_competitor" text;--> statement-breakpoint
|
||||
ALTER TABLE "crm_enquiries" ADD COLUMN "lost_remark" text;--> 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");
|
||||
@@ -1,15 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS "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
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "crm_report_definitions_org_code_idx"
|
||||
ON "crm_report_definitions" ("organization_id", "code");
|
||||
@@ -1,14 +0,0 @@
|
||||
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 UNIQUE INDEX "crm_report_definitions_org_code_idx" ON "crm_report_definitions" USING btree ("organization_id","code");
|
||||
@@ -1,24 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS "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,
|
||||
"project_name" text,
|
||||
"project_location" text,
|
||||
"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,
|
||||
"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 UNIQUE INDEX IF NOT EXISTS "crm_leads_org_code_idx" ON "crm_leads" USING btree ("organization_id","code");
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "crm_enquiries" ADD COLUMN IF NOT EXISTS "lead_id" text;
|
||||
@@ -1,23 +0,0 @@
|
||||
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,
|
||||
"project_name" text,
|
||||
"project_location" text,
|
||||
"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,
|
||||
"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
|
||||
ALTER TABLE "crm_enquiries" ADD COLUMN "lead_id" text;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "crm_leads_org_code_idx" ON "crm_leads" USING btree ("organization_id","code");
|
||||
@@ -1,9 +0,0 @@
|
||||
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;
|
||||
@@ -1,8 +0,0 @@
|
||||
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;
|
||||
@@ -1,331 +0,0 @@
|
||||
{
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
@@ -1,653 +0,0 @@
|
||||
{
|
||||
"id": "a21ab199-cb43-48b2-b810-7d4afb9baaef",
|
||||
"prevId": "1e236d6a-ca42-4830-b230-67a7c233d87b",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.document_sequences": {
|
||||
"name": "document_sequences",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"branch_id": {
|
||||
"name": "branch_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "''"
|
||||
},
|
||||
"document_type": {
|
||||
"name": "document_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"prefix": {
|
||||
"name": "prefix",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"period": {
|
||||
"name": "period",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"current_number": {
|
||||
"name": "current_number",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"padding_length": {
|
||||
"name": "padding_length",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 3
|
||||
},
|
||||
"is_active": {
|
||||
"name": "is_active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"document_sequences_org_doc_period_branch_idx": {
|
||||
"name": "document_sequences_org_doc_period_branch_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "organization_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "document_type",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "period",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "branch_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.memberships": {
|
||||
"name": "memberships",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'user'"
|
||||
},
|
||||
"business_role": {
|
||||
"name": "business_role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'viewer'"
|
||||
},
|
||||
"permissions": {
|
||||
"name": "permissions",
|
||||
"type": "text[]",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{}'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.ms_options": {
|
||||
"name": "ms_options",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"category": {
|
||||
"name": "category",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"code": {
|
||||
"name": "code",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"label": {
|
||||
"name": "label",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"parent_id": {
|
||||
"name": "parent_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"sort_order": {
|
||||
"name": "sort_order",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"is_active": {
|
||||
"name": "is_active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"ms_options_org_category_code_idx": {
|
||||
"name": "ms_options_org_category_code_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "organization_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "category",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "code",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.organizations": {
|
||||
"name": "organizations",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"image_url": {
|
||||
"name": "image_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"plan": {
|
||||
"name": "plan",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'free'"
|
||||
},
|
||||
"created_by": {
|
||||
"name": "created_by",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"organizations_slug_idx": {
|
||||
"name": "organizations_slug_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "slug",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.products": {
|
||||
"name": "products",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "always",
|
||||
"name": "products_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"category": {
|
||||
"name": "category",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"photo_url": {
|
||||
"name": "photo_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"price": {
|
||||
"name": "price",
|
||||
"type": "double precision",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.tr_audit_logs": {
|
||||
"name": "tr_audit_logs",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"branch_id": {
|
||||
"name": "branch_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"entity_type": {
|
||||
"name": "entity_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"entity_id": {
|
||||
"name": "entity_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"action": {
|
||||
"name": "action",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"before_data": {
|
||||
"name": "before_data",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"after_data": {
|
||||
"name": "after_data",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"request_id": {
|
||||
"name": "request_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"system_role": {
|
||||
"name": "system_role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'user'"
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"active_organization_id": {
|
||||
"name": "active_organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_email_idx": {
|
||||
"name": "users_email_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "email",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -1,978 +0,0 @@
|
||||
{
|
||||
"id": "9f3af9b8-d774-40bb-8b24-29b06332b453",
|
||||
"prevId": "a21ab199-cb43-48b2-b810-7d4afb9baaef",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.crm_customer_contacts": {
|
||||
"name": "crm_customer_contacts",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"position": {
|
||||
"name": "position",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"department": {
|
||||
"name": "department",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"phone": {
|
||||
"name": "phone",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"mobile": {
|
||||
"name": "mobile",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"is_primary": {
|
||||
"name": "is_primary",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"notes": {
|
||||
"name": "notes",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"is_active": {
|
||||
"name": "is_active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_by": {
|
||||
"name": "created_by",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_by": {
|
||||
"name": "updated_by",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.crm_customers": {
|
||||
"name": "crm_customers",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"branch_id": {
|
||||
"name": "branch_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"code": {
|
||||
"name": "code",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"abbr": {
|
||||
"name": "abbr",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"tax_id": {
|
||||
"name": "tax_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"customer_type": {
|
||||
"name": "customer_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"customer_status": {
|
||||
"name": "customer_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"address": {
|
||||
"name": "address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"province": {
|
||||
"name": "province",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"district": {
|
||||
"name": "district",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"sub_district": {
|
||||
"name": "sub_district",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"postal_code": {
|
||||
"name": "postal_code",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"country": {
|
||||
"name": "country",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"phone": {
|
||||
"name": "phone",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"fax": {
|
||||
"name": "fax",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"website": {
|
||||
"name": "website",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"lead_channel": {
|
||||
"name": "lead_channel",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"customer_group": {
|
||||
"name": "customer_group",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"notes": {
|
||||
"name": "notes",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"is_active": {
|
||||
"name": "is_active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_by": {
|
||||
"name": "created_by",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_by": {
|
||||
"name": "updated_by",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"crm_customers_org_code_idx": {
|
||||
"name": "crm_customers_org_code_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "organization_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "code",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.document_sequences": {
|
||||
"name": "document_sequences",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"branch_id": {
|
||||
"name": "branch_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "''"
|
||||
},
|
||||
"document_type": {
|
||||
"name": "document_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"prefix": {
|
||||
"name": "prefix",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"period": {
|
||||
"name": "period",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"current_number": {
|
||||
"name": "current_number",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"padding_length": {
|
||||
"name": "padding_length",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 3
|
||||
},
|
||||
"is_active": {
|
||||
"name": "is_active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"document_sequences_org_doc_period_branch_idx": {
|
||||
"name": "document_sequences_org_doc_period_branch_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "organization_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "document_type",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "period",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "branch_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.memberships": {
|
||||
"name": "memberships",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'user'"
|
||||
},
|
||||
"business_role": {
|
||||
"name": "business_role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'viewer'"
|
||||
},
|
||||
"permissions": {
|
||||
"name": "permissions",
|
||||
"type": "text[]",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{}'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.ms_options": {
|
||||
"name": "ms_options",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"category": {
|
||||
"name": "category",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"code": {
|
||||
"name": "code",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"label": {
|
||||
"name": "label",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"parent_id": {
|
||||
"name": "parent_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"sort_order": {
|
||||
"name": "sort_order",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"is_active": {
|
||||
"name": "is_active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"ms_options_org_category_code_idx": {
|
||||
"name": "ms_options_org_category_code_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "organization_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "category",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "code",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.organizations": {
|
||||
"name": "organizations",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"image_url": {
|
||||
"name": "image_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"plan": {
|
||||
"name": "plan",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'free'"
|
||||
},
|
||||
"created_by": {
|
||||
"name": "created_by",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"organizations_slug_idx": {
|
||||
"name": "organizations_slug_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "slug",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.products": {
|
||||
"name": "products",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "always",
|
||||
"name": "products_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"category": {
|
||||
"name": "category",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"photo_url": {
|
||||
"name": "photo_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"price": {
|
||||
"name": "price",
|
||||
"type": "double precision",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.tr_audit_logs": {
|
||||
"name": "tr_audit_logs",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"branch_id": {
|
||||
"name": "branch_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"entity_type": {
|
||||
"name": "entity_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"entity_id": {
|
||||
"name": "entity_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"action": {
|
||||
"name": "action",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"before_data": {
|
||||
"name": "before_data",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"after_data": {
|
||||
"name": "after_data",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"request_id": {
|
||||
"name": "request_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"system_role": {
|
||||
"name": "system_role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'user'"
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"active_organization_id": {
|
||||
"name": "active_organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_email_idx": {
|
||||
"name": "users_email_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "email",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,174 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
124
plans/task-d.6.2.md
Normal file
124
plans/task-d.6.2.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Task D.6.2 - Opportunity to Quotation Prefill & Project Timeline Fields
|
||||
|
||||
## Objective
|
||||
Improve the Opportunity -> Quotation workflow so users do not need to select the same Opportunity twice, while adding timeline fields and hot-project forecasting support.
|
||||
|
||||
## Requirements
|
||||
|
||||
### 1. Create Quotation from Opportunity Detail
|
||||
- Clicking `Create Quotation` from Opportunity detail must open the quotation form with the current Opportunity already selected.
|
||||
- Related customer/project/contact/product context should be prefilled where available.
|
||||
|
||||
### 2. Add Project Timeline Fields
|
||||
Add these fields to both Opportunity and Quotation:
|
||||
- `Project Close Date`
|
||||
- `Delivery Date`
|
||||
|
||||
Rules:
|
||||
- Use calendar date pickers.
|
||||
- Display format must be `YYYY-MM-DD`.
|
||||
|
||||
### 3. Auto Hot Project Rule
|
||||
- When `Project Close Date <= current date + 3 months`, the system should auto-suggest `hotProject = true`.
|
||||
- User may manually uncheck it.
|
||||
|
||||
### 4. Manual Override Rule
|
||||
Hot Project must support:
|
||||
- `auto suggested`
|
||||
- `manual override`
|
||||
|
||||
Recommended persisted fields:
|
||||
- `isHotProject`
|
||||
- `hotProjectAutoSuggested`
|
||||
- `hotProjectManuallyOverridden`
|
||||
|
||||
The UI must not keep forcing the checkbox back on during the same edit session after the user manually changes it.
|
||||
|
||||
### 5. Backend Enforcement
|
||||
Backend must:
|
||||
- accept `projectCloseDate`
|
||||
- accept `deliveryDate`
|
||||
- validate `YYYY-MM-DD`
|
||||
- calculate hot-project suggestion
|
||||
- preserve manual override
|
||||
- persist all values correctly
|
||||
|
||||
### 6. Quotation Prefill Rules
|
||||
When creating a quotation from an Opportunity, prefill where available:
|
||||
- `opportunityId`
|
||||
- customer / billing customer / project parties
|
||||
- project name
|
||||
- product type
|
||||
- branch
|
||||
- contact
|
||||
- `projectCloseDate`
|
||||
- `deliveryDate`
|
||||
- `hotProject`
|
||||
|
||||
Do not overwrite user edits after the form has already been opened.
|
||||
|
||||
### 7. UI Requirements
|
||||
Opportunity Detail:
|
||||
- `Create Quotation` must pass the current Opportunity context.
|
||||
- Quotation form shows selected Opportunity already filled.
|
||||
|
||||
Opportunity Form:
|
||||
- `Project Close Date`
|
||||
- `Delivery Date`
|
||||
- `Hot Project`
|
||||
|
||||
Quotation Form:
|
||||
- `Project Close Date`
|
||||
- `Delivery Date`
|
||||
- `Hot Project`
|
||||
|
||||
### 8. Testing
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm run build
|
||||
npm run audit:pdf
|
||||
```
|
||||
|
||||
Test cases:
|
||||
- Create quotation from Opportunity detail preselects Opportunity.
|
||||
- User does not need to reselect Opportunity.
|
||||
- Project Close Date within 3 months auto-checks Hot Project.
|
||||
- User can uncheck Hot Project.
|
||||
- Project Close Date beyond 3 months does not auto-check.
|
||||
- Delivery Date saves correctly.
|
||||
- Quotation receives timeline fields from Opportunity.
|
||||
- Existing quotation create flow still works without Opportunity.
|
||||
|
||||
## Acceptance Criteria
|
||||
- Create Quotation from Opportunity detail preselects current Opportunity.
|
||||
- Opportunity does not need to be selected twice.
|
||||
- Opportunity has Project Close Date and Delivery Date.
|
||||
- Quotation has Project Close Date and Delivery Date.
|
||||
- Date fields use calendar pickers.
|
||||
- Hot Project is auto-checked when Project Close Date is within 3 months.
|
||||
- User can manually uncheck Hot Project.
|
||||
- Manual override is preserved.
|
||||
- Existing create/edit flows remain compatible.
|
||||
- Typecheck, build, and PDF audit pass.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Result - 2026-07-01
|
||||
|
||||
Status: Completed
|
||||
|
||||
Summary:
|
||||
- Added `Project Close Date` and `Delivery Date` to opportunity and quotation persistence, DTOs, validation, and form UI.
|
||||
- Added shared hot-project auto-suggestion logic plus persisted `hotProjectAutoSuggested` and `hotProjectManuallyOverridden`.
|
||||
- Updated opportunity detail `Create Quotation` flow to pass current opportunity context into the quotation sheet, including project parties when already available.
|
||||
- Updated quotation prefill logic so automatic population no longer overwrites fields the user already edited in the same open session.
|
||||
|
||||
Verification:
|
||||
- `npm run typecheck` passed.
|
||||
- `npm run build` passed with existing Turbopack workspace-root and broad local storage path warnings.
|
||||
- `npm run audit:pdf` passed and refreshed PDF audit artifacts.
|
||||
|
||||
Implementation note:
|
||||
- [task-d.6.2-opportunity-quotation-prefill-project-timeline-2026-07-01.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/implementation/task-d.6.2-opportunity-quotation-prefill-project-timeline-2026-07-01.md)
|
||||
@@ -19,7 +19,9 @@ const opportunityRequestSchema = opportunitySchema.extend({
|
||||
contactId: z.string().nullable().optional(),
|
||||
branchId: z.string().nullable().optional(),
|
||||
leadChannel: z.string().nullable().optional(),
|
||||
expectedCloseDate: z.string().nullable().optional()
|
||||
expectedCloseDate: z.string().nullable().optional(),
|
||||
projectCloseDate: z.string().nullable().optional(),
|
||||
deliveryDate: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export async function GET(_request: NextRequest, { params }: Params) {
|
||||
@@ -169,4 +171,3 @@ export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: 'Unable to delete opportunity' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ const opportunityRequestSchema = opportunitySchema.extend({
|
||||
assignedToUserId: z.string().nullable().optional(),
|
||||
branchId: z.string().nullable().optional(),
|
||||
leadChannel: z.string().nullable().optional(),
|
||||
expectedCloseDate: z.string().nullable().optional()
|
||||
expectedCloseDate: z.string().nullable().optional(),
|
||||
projectCloseDate: z.string().nullable().optional(),
|
||||
deliveryDate: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
function buildAccessContext(
|
||||
|
||||
@@ -497,10 +497,16 @@ export const crmOpportunities = pgTable(
|
||||
estimatedValue: doublePrecision('estimated_value'),
|
||||
chancePercent: integer('chance_percent'),
|
||||
expectedCloseDate: timestamp('expected_close_date', { withTimezone: true }),
|
||||
projectCloseDate: timestamp('project_close_date', { withTimezone: true }),
|
||||
deliveryDate: timestamp('delivery_date', { withTimezone: true }),
|
||||
competitor: text('competitor'),
|
||||
source: text('source'),
|
||||
notes: text('notes'),
|
||||
isHotProject: boolean('is_hot_project').default(false).notNull(),
|
||||
hotProjectAutoSuggested: boolean('hot_project_auto_suggested').default(false).notNull(),
|
||||
hotProjectManuallyOverridden: boolean('hot_project_manually_overridden')
|
||||
.default(false)
|
||||
.notNull(),
|
||||
isActive: boolean('is_active').default(true).notNull(),
|
||||
pipelineStage: text('pipeline_stage').default('lead').notNull(),
|
||||
closedAt: timestamp('closed_at', { withTimezone: true }),
|
||||
@@ -637,7 +643,13 @@ export const crmQuotations = pgTable(
|
||||
taxAmount: doublePrecision('tax_amount').default(0).notNull(),
|
||||
totalAmount: doublePrecision('total_amount').default(0).notNull(),
|
||||
chancePercent: integer('chance_percent'),
|
||||
projectCloseDate: timestamp('project_close_date', { withTimezone: true }),
|
||||
deliveryDate: timestamp('delivery_date', { withTimezone: true }),
|
||||
isHotProject: boolean('is_hot_project').default(false).notNull(),
|
||||
hotProjectAutoSuggested: boolean('hot_project_auto_suggested').default(false).notNull(),
|
||||
hotProjectManuallyOverridden: boolean('hot_project_manually_overridden')
|
||||
.default(false)
|
||||
.notNull(),
|
||||
competitor: text('competitor'),
|
||||
salesmanId: text('salesman_id'),
|
||||
isSent: boolean('is_sent').default(false).notNull(),
|
||||
|
||||
@@ -40,6 +40,9 @@ export function LeadDetail({
|
||||
}) {
|
||||
const { data } = useSuspenseQuery(leadByIdOptions(leadId));
|
||||
const lead = data.lead;
|
||||
const priorityLabel =
|
||||
referenceData.priorities.find((priority) => priority.id === lead.priority)?.label ??
|
||||
lead.priority;
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [assignOpen, setAssignOpen] = useState(false);
|
||||
|
||||
@@ -96,7 +99,7 @@ export function LeadDetail({
|
||||
<FieldItem label='Project Location' value={lead.projectLocation} />
|
||||
<FieldItem label='Lead Source' value={lead.leadChannelLabel} />
|
||||
<FieldItem label='Awareness' value={lead.awarenessLabel} />
|
||||
<FieldItem label='Priority' value={lead.priority} />
|
||||
<FieldItem label='Priority' value={priorityLabel} />
|
||||
<FieldItem label='Marketing Owner' value={lead.ownerMarketingName} />
|
||||
<FieldItem label='Suggested Sales Owner' value={lead.suggestedSalesOwnerName} />
|
||||
<FieldItem label='Assigned Sales Owner' value={lead.assignedSalesOwnerName} />
|
||||
|
||||
@@ -100,10 +100,14 @@ export interface OpportunityRecord {
|
||||
estimatedValue: number | null;
|
||||
chancePercent: number | null;
|
||||
expectedCloseDate: string | null;
|
||||
projectCloseDate: string | null;
|
||||
deliveryDate: string | null;
|
||||
competitor: string | null;
|
||||
source: string | null;
|
||||
notes: string | null;
|
||||
isHotProject: boolean;
|
||||
hotProjectAutoSuggested: boolean;
|
||||
hotProjectManuallyOverridden: boolean;
|
||||
isActive: boolean;
|
||||
pipelineStage: OpportunityPipelineStage;
|
||||
closedAt: string | null;
|
||||
@@ -266,9 +270,13 @@ export interface OpportunityMutationPayload {
|
||||
estimatedValue?: number | null;
|
||||
chancePercent?: number | null;
|
||||
expectedCloseDate?: string | null;
|
||||
projectCloseDate?: string | null;
|
||||
deliveryDate?: string | null;
|
||||
competitor?: string;
|
||||
source?: string;
|
||||
isHotProject?: boolean;
|
||||
hotProjectAutoSuggested?: boolean;
|
||||
hotProjectManuallyOverridden?: boolean;
|
||||
notes?: string;
|
||||
isActive?: boolean;
|
||||
projectParties?: OpportunityProjectPartyMutationPayload[];
|
||||
|
||||
@@ -113,6 +113,29 @@ export function OpportunityDetail({
|
||||
const priorityMap = useMemo(
|
||||
() => new Map(referenceData.priorities.map((item) => [item.id, item.label])),
|
||||
[referenceData]
|
||||
);
|
||||
const quotationInitialPrefill = useMemo(
|
||||
() => ({
|
||||
opportunityId: opportunity.id,
|
||||
customerId: opportunity.customerId,
|
||||
contactId: opportunity.contactId,
|
||||
projectName: opportunity.projectName,
|
||||
projectLocation: opportunity.projectLocation,
|
||||
branchId: opportunity.branchId,
|
||||
quotationType: opportunity.productType,
|
||||
projectCloseDate: opportunity.projectCloseDate ? opportunity.projectCloseDate.slice(0, 10) : null,
|
||||
deliveryDate: opportunity.deliveryDate ? opportunity.deliveryDate.slice(0, 10) : null,
|
||||
isHotProject: opportunity.isHotProject,
|
||||
hotProjectAutoSuggested: opportunity.hotProjectAutoSuggested,
|
||||
hotProjectManuallyOverridden: opportunity.hotProjectManuallyOverridden,
|
||||
projectParties: projectPartiesData.items.map((item) => ({
|
||||
key: item.id,
|
||||
customerId: item.customerId,
|
||||
role: item.role,
|
||||
remark: item.remark ?? ''
|
||||
}))
|
||||
}),
|
||||
[opportunity, projectPartiesData.items]
|
||||
);
|
||||
|
||||
const customer = customerMap.get(opportunity.customerId);
|
||||
@@ -143,6 +166,7 @@ export function OpportunityDetail({
|
||||
open={quotationOpen}
|
||||
onOpenChange={setQuotationOpen}
|
||||
referenceData={quotationReferenceData}
|
||||
initialPrefill={quotationInitialPrefill}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -208,6 +232,18 @@ export function OpportunityDetail({
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<FieldItem
|
||||
label='Project Close Date'
|
||||
value={
|
||||
opportunity.projectCloseDate
|
||||
? formatDate(opportunity.projectCloseDate)
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<FieldItem
|
||||
label='Delivery Date'
|
||||
value={opportunity.deliveryDate ? formatDate(opportunity.deliveryDate) : null}
|
||||
/>
|
||||
<FieldItem
|
||||
label='Estimated Value'
|
||||
value={
|
||||
|
||||
@@ -25,11 +25,13 @@ import {
|
||||
SheetHeader,
|
||||
SheetTitle
|
||||
} from '@/components/ui/sheet';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { useAppForm, useFormFields } from '@/components/ui/tanstack-form';
|
||||
import { opportunityProjectPartiesOptions } from '../api/queries';
|
||||
import { createOpportunityMutation, updateOpportunityMutation } from '../api/mutations';
|
||||
import type { OpportunityMutationPayload, OpportunityRecord, OpportunityReferenceData } from '../api/types';
|
||||
import { opportunitySchema, type OpportunityFormValues } from '../schemas/opportunity.schema';
|
||||
import { isHotProjectSuggested } from '@/features/crm/shared/hot-project';
|
||||
|
||||
function toDefaultValues(
|
||||
opportunity: OpportunityRecord | undefined,
|
||||
@@ -53,9 +55,13 @@ function toDefaultValues(
|
||||
estimatedValue: opportunity?.estimatedValue ?? undefined,
|
||||
chancePercent: opportunity?.chancePercent ?? undefined,
|
||||
expectedCloseDate: opportunity?.expectedCloseDate ? opportunity.expectedCloseDate.slice(0, 10) : '',
|
||||
projectCloseDate: opportunity?.projectCloseDate ? opportunity.projectCloseDate.slice(0, 10) : '',
|
||||
deliveryDate: opportunity?.deliveryDate ? opportunity.deliveryDate.slice(0, 10) : '',
|
||||
competitor: opportunity?.competitor ?? '',
|
||||
source: opportunity?.source ?? '',
|
||||
isHotProject: opportunity?.isHotProject ?? false,
|
||||
hotProjectAutoSuggested: opportunity?.hotProjectAutoSuggested ?? false,
|
||||
hotProjectManuallyOverridden: opportunity?.hotProjectManuallyOverridden ?? false,
|
||||
notes: opportunity?.notes ?? '',
|
||||
isActive: opportunity?.isActive ?? true
|
||||
};
|
||||
@@ -145,9 +151,13 @@ export function OpportunityFormSheet({
|
||||
chancePercent:
|
||||
typeof value.chancePercent === 'number' ? value.chancePercent : null,
|
||||
expectedCloseDate: value.expectedCloseDate || null,
|
||||
projectCloseDate: value.projectCloseDate || null,
|
||||
deliveryDate: value.deliveryDate || null,
|
||||
competitor: value.competitor,
|
||||
source: value.source,
|
||||
isHotProject: value.isHotProject ?? false,
|
||||
hotProjectAutoSuggested: value.hotProjectAutoSuggested ?? false,
|
||||
hotProjectManuallyOverridden: value.hotProjectManuallyOverridden ?? false,
|
||||
notes: value.notes,
|
||||
isActive: value.isActive ?? true,
|
||||
projectParties: projectParties
|
||||
@@ -198,6 +208,24 @@ export function OpportunityFormSheet({
|
||||
form.setFieldValue('assignedToUserId', ownerUserId);
|
||||
}, [form, isEdit, open, referenceData.customers, selectedCustomerId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const projectCloseDate = form.getFieldValue('projectCloseDate');
|
||||
const manuallyOverridden = form.getFieldValue('hotProjectManuallyOverridden');
|
||||
|
||||
if (manuallyOverridden) {
|
||||
form.setFieldValue('hotProjectAutoSuggested', isHotProjectSuggested(projectCloseDate));
|
||||
return;
|
||||
}
|
||||
|
||||
const suggested = isHotProjectSuggested(projectCloseDate);
|
||||
form.setFieldValue('hotProjectAutoSuggested', suggested);
|
||||
form.setFieldValue('isHotProject', suggested);
|
||||
}, [form, open, form.state.values.projectCloseDate, form.state.values.hotProjectManuallyOverridden]);
|
||||
|
||||
const isPending = createMutation.isPending || updateMutation.isPending;
|
||||
|
||||
const recordLabel = workspace === 'lead' ? 'ลีด' : 'โอกาสขาย';
|
||||
@@ -376,6 +404,14 @@ export function OpportunityFormSheet({
|
||||
name='expectedCloseDate'
|
||||
label='วันที่คาดว่าจะปิดการขาย'
|
||||
/>
|
||||
<FormDatePickerField
|
||||
name='projectCloseDate'
|
||||
label='Project Close Date'
|
||||
/>
|
||||
<FormDatePickerField
|
||||
name='deliveryDate'
|
||||
label='Delivery Date'
|
||||
/>
|
||||
<FormTextField name='competitor' label='คู่แข่ง' placeholder='Other vendor name' />
|
||||
<div className='md:col-span-2'>
|
||||
<FormTextareaField
|
||||
@@ -402,10 +438,33 @@ export function OpportunityFormSheet({
|
||||
/>
|
||||
</div>
|
||||
<div className='md:col-span-2 grid gap-4 md:grid-cols-2'>
|
||||
<FormSwitchField
|
||||
<form.AppField
|
||||
name='isHotProject'
|
||||
label='โครงการด่วน'
|
||||
description='ใช้สำหรับงานเร่งด่วนหรือโครงการสำคัญเชิงกลยุทธ์'
|
||||
children={(field) => (
|
||||
<field.FieldSet>
|
||||
<field.Field orientation='horizontal'>
|
||||
<div className='flex flex-1 flex-col gap-1.5 leading-snug'>
|
||||
<field.FieldLabel className='text-base'>โครงการด่วน</field.FieldLabel>
|
||||
<field.FieldDescription>
|
||||
ใช้สำหรับงานเร่งด่วนหรือโครงการสำคัญเชิงกลยุทธ์
|
||||
</field.FieldDescription>
|
||||
</div>
|
||||
<Switch
|
||||
checked={Boolean(field.state.value)}
|
||||
onCheckedChange={(checked) => {
|
||||
field.handleChange(checked);
|
||||
field.handleBlur();
|
||||
form.setFieldValue('hotProjectManuallyOverridden', true);
|
||||
form.setFieldValue(
|
||||
'hotProjectAutoSuggested',
|
||||
isHotProjectSuggested(form.getFieldValue('projectCloseDate'))
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</field.Field>
|
||||
<field.FieldError />
|
||||
</field.FieldSet>
|
||||
)}
|
||||
/>
|
||||
<FormSwitchField
|
||||
name='isActive'
|
||||
@@ -462,4 +521,3 @@ export function OpportunityFormSheetTrigger({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as z from 'zod';
|
||||
import { isValidDateOnlyString } from '@/features/crm/shared/hot-project';
|
||||
|
||||
export {
|
||||
opportunityMarkCancelledSchema,
|
||||
@@ -36,6 +37,14 @@ function coerceOptionalNumber(message: string) {
|
||||
}, z.number().refine((value) => !Number.isNaN(value), message).optional());
|
||||
}
|
||||
|
||||
function optionalDateOnly(message: string) {
|
||||
return z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.refine((value) => !value || isValidDateOnlyString(value), message);
|
||||
}
|
||||
|
||||
export const opportunitySchema = z.object({
|
||||
customerId: z.string().min(1, 'Please select customer'),
|
||||
contactId: z.string().optional().nullable(),
|
||||
@@ -58,10 +67,14 @@ export const opportunitySchema = z.object({
|
||||
(value) => value === undefined || (value >= 0 && value <= 100),
|
||||
'Chance percent must be between 0 and 100'
|
||||
),
|
||||
expectedCloseDate: z.string().optional().nullable(),
|
||||
expectedCloseDate: optionalDateOnly('Expected close date must be YYYY-MM-DD'),
|
||||
projectCloseDate: optionalDateOnly('Project close date must be YYYY-MM-DD'),
|
||||
deliveryDate: optionalDateOnly('Delivery date must be YYYY-MM-DD'),
|
||||
competitor: z.string().optional(),
|
||||
source: z.string().optional(),
|
||||
isHotProject: z.boolean().default(false),
|
||||
hotProjectAutoSuggested: z.boolean().default(false),
|
||||
hotProjectManuallyOverridden: z.boolean().default(false),
|
||||
notes: z.string().optional(),
|
||||
isActive: z.boolean().default(true),
|
||||
projectParties: z
|
||||
|
||||
@@ -43,6 +43,7 @@ import type {
|
||||
OpportunityRecord,
|
||||
OpportunityReferenceData
|
||||
} from '../api/types';
|
||||
import { resolveHotProjectState } from '@/features/crm/shared/hot-project';
|
||||
import {
|
||||
buildProjectPartyKey,
|
||||
PROJECT_PARTY_OPTION_CATEGORY,
|
||||
@@ -148,10 +149,14 @@ function mapOpportunityRecord(
|
||||
estimatedValue: row.estimatedValue,
|
||||
chancePercent: row.chancePercent,
|
||||
expectedCloseDate: row.expectedCloseDate?.toISOString() ?? null,
|
||||
projectCloseDate: row.projectCloseDate?.toISOString() ?? null,
|
||||
deliveryDate: row.deliveryDate?.toISOString() ?? null,
|
||||
competitor: row.competitor,
|
||||
source: row.source,
|
||||
notes: row.notes,
|
||||
isHotProject: row.isHotProject,
|
||||
hotProjectAutoSuggested: row.hotProjectAutoSuggested,
|
||||
hotProjectManuallyOverridden: row.hotProjectManuallyOverridden,
|
||||
isActive: row.isActive,
|
||||
pipelineStage: row.pipelineStage as OpportunityPipelineStage,
|
||||
closedAt: row.closedAt?.toISOString() ?? null,
|
||||
@@ -1150,6 +1155,7 @@ export async function createOpportunity(
|
||||
payload: OpportunityMutationPayload
|
||||
) {
|
||||
await validateOpportunityPayload(organizationId, payload, accessContext);
|
||||
const hotProjectState = resolveHotProjectState(payload);
|
||||
const pipelineStage = await resolvePipelineStageForCreate(accessContext.businessRole);
|
||||
|
||||
const documentCode = await generateNextDocumentCode({
|
||||
@@ -1180,10 +1186,14 @@ export async function createOpportunity(
|
||||
estimatedValue: payload.estimatedValue ?? null,
|
||||
chancePercent: payload.chancePercent ?? null,
|
||||
expectedCloseDate: payload.expectedCloseDate ? new Date(payload.expectedCloseDate) : null,
|
||||
projectCloseDate: payload.projectCloseDate ? new Date(payload.projectCloseDate) : null,
|
||||
deliveryDate: payload.deliveryDate ? new Date(payload.deliveryDate) : null,
|
||||
competitor: payload.competitor?.trim() || null,
|
||||
source: payload.source?.trim() || null,
|
||||
notes: payload.notes?.trim() || null,
|
||||
isHotProject: payload.isHotProject ?? false,
|
||||
isHotProject: hotProjectState.isHotProject,
|
||||
hotProjectAutoSuggested: hotProjectState.hotProjectAutoSuggested,
|
||||
hotProjectManuallyOverridden: hotProjectState.hotProjectManuallyOverridden,
|
||||
isActive: payload.isActive ?? true,
|
||||
pipelineStage,
|
||||
assignedToUserId: payload.assignedToUserId ?? null,
|
||||
@@ -1215,6 +1225,7 @@ export async function updateOpportunity(
|
||||
accessContext?: OpportunityAccessContext
|
||||
) {
|
||||
await validateOpportunityPayload(organizationId, payload, accessContext);
|
||||
const hotProjectState = resolveHotProjectState(payload);
|
||||
const current = await assertOpportunityBelongsToOrganization(id, organizationId, accessContext);
|
||||
assertOpportunityOpenForEditing(current);
|
||||
const pipelineStage = await resolvePipelineStageForUpdate(current);
|
||||
@@ -1238,10 +1249,14 @@ export async function updateOpportunity(
|
||||
estimatedValue: payload.estimatedValue ?? null,
|
||||
chancePercent: payload.chancePercent ?? null,
|
||||
expectedCloseDate: payload.expectedCloseDate ? new Date(payload.expectedCloseDate) : null,
|
||||
projectCloseDate: payload.projectCloseDate ? new Date(payload.projectCloseDate) : null,
|
||||
deliveryDate: payload.deliveryDate ? new Date(payload.deliveryDate) : null,
|
||||
competitor: payload.competitor?.trim() || null,
|
||||
source: payload.source?.trim() || null,
|
||||
notes: payload.notes?.trim() || null,
|
||||
isHotProject: payload.isHotProject ?? false,
|
||||
isHotProject: hotProjectState.isHotProject,
|
||||
hotProjectAutoSuggested: hotProjectState.hotProjectAutoSuggested,
|
||||
hotProjectManuallyOverridden: hotProjectState.hotProjectManuallyOverridden,
|
||||
isActive: payload.isActive ?? true,
|
||||
pipelineStage,
|
||||
updatedBy: userId,
|
||||
|
||||
@@ -37,6 +37,12 @@ export interface QuotationOpportunityLookup {
|
||||
projectName: string | null;
|
||||
projectLocation: string | null;
|
||||
branchId: string | null;
|
||||
productType: string;
|
||||
projectCloseDate: string | null;
|
||||
deliveryDate: string | null;
|
||||
isHotProject: boolean;
|
||||
hotProjectAutoSuggested: boolean;
|
||||
hotProjectManuallyOverridden: boolean;
|
||||
}
|
||||
|
||||
export interface QuotationSalesmanLookup {
|
||||
@@ -121,7 +127,11 @@ export interface QuotationRecord {
|
||||
taxAmount: number;
|
||||
totalAmount: number;
|
||||
chancePercent: number | null;
|
||||
projectCloseDate: string | null;
|
||||
deliveryDate: string | null;
|
||||
isHotProject: boolean;
|
||||
hotProjectAutoSuggested: boolean;
|
||||
hotProjectManuallyOverridden: boolean;
|
||||
competitor: string | null;
|
||||
salesmanId: string | null;
|
||||
salesmanName: string | null;
|
||||
@@ -381,7 +391,11 @@ export interface QuotationMutationPayload {
|
||||
exchangeRate?: number | null;
|
||||
status: string;
|
||||
chancePercent?: number | null;
|
||||
projectCloseDate?: string | null;
|
||||
deliveryDate?: string | null;
|
||||
isHotProject?: boolean;
|
||||
hotProjectAutoSuggested?: boolean;
|
||||
hotProjectManuallyOverridden?: boolean;
|
||||
competitor?: string;
|
||||
reference?: string;
|
||||
notes?: string;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,10 +6,11 @@ import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { Icons } from '@/components/icons';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
ProjectPartiesEditor,
|
||||
type ProjectPartyEditorItem
|
||||
} from '@/features/crm/components/project-parties-editor';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { ProjectPartiesEditor, type ProjectPartyEditorItem } from '@/features/crm/components/project-parties-editor';
|
||||
import {
|
||||
CrmCurrencyInput,
|
||||
CrmDateInput,
|
||||
@@ -17,26 +18,16 @@ import {
|
||||
CrmPercentageInput,
|
||||
CrmTextarea
|
||||
} from '@/features/crm/components/crm-form-controls';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle
|
||||
} from '@/components/ui/sheet';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { quotationCustomersOptions } from '../api/queries';
|
||||
import { isHotProjectSuggested } from '@/features/crm/shared/hot-project';
|
||||
import { createQuotationMutation, updateQuotationMutation } from '../api/mutations';
|
||||
import type { QuotationMutationPayload, QuotationRecord, QuotationReferenceData } from '../api/types';
|
||||
import { quotationCustomersOptions } from '../api/queries';
|
||||
import type {
|
||||
QuotationMutationPayload,
|
||||
QuotationOpportunityLookup,
|
||||
QuotationRecord,
|
||||
QuotationReferenceData
|
||||
} from '../api/types';
|
||||
import { quotationSchema } from '../schemas/quotation.schema';
|
||||
|
||||
type FormState = {
|
||||
opportunityId: string;
|
||||
@@ -53,6 +44,8 @@ type FormState = {
|
||||
exchangeRate: string;
|
||||
status: string;
|
||||
chancePercent: string;
|
||||
projectCloseDate: string;
|
||||
deliveryDate: string;
|
||||
competitor: string;
|
||||
reference: string;
|
||||
notes: string;
|
||||
@@ -63,32 +56,105 @@ type FormState = {
|
||||
sentVia: string;
|
||||
revisionRemark: string;
|
||||
isHotProject: boolean;
|
||||
hotProjectAutoSuggested: boolean;
|
||||
hotProjectManuallyOverridden: boolean;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
export interface QuotationCreatePrefill {
|
||||
opportunityId?: string | null;
|
||||
customerId?: string | null;
|
||||
contactId?: string | null;
|
||||
projectName?: string | null;
|
||||
projectLocation?: string | null;
|
||||
branchId?: string | null;
|
||||
quotationType?: string | null;
|
||||
projectCloseDate?: string | null;
|
||||
deliveryDate?: string | null;
|
||||
isHotProject?: boolean | null;
|
||||
hotProjectAutoSuggested?: boolean | null;
|
||||
hotProjectManuallyOverridden?: boolean | null;
|
||||
projectParties?: ProjectPartyEditorItem[];
|
||||
}
|
||||
|
||||
type PrefillFieldKey =
|
||||
| 'customerId'
|
||||
| 'contactId'
|
||||
| 'projectName'
|
||||
| 'projectLocation'
|
||||
| 'branchId'
|
||||
| 'quotationType'
|
||||
| 'projectCloseDate'
|
||||
| 'deliveryDate'
|
||||
| 'isHotProject'
|
||||
| 'hotProjectAutoSuggested'
|
||||
| 'hotProjectManuallyOverridden';
|
||||
|
||||
const PREFILL_FIELD_KEYS: PrefillFieldKey[] = [
|
||||
'customerId',
|
||||
'contactId',
|
||||
'projectName',
|
||||
'projectLocation',
|
||||
'branchId',
|
||||
'quotationType',
|
||||
'projectCloseDate',
|
||||
'deliveryDate',
|
||||
'isHotProject',
|
||||
'hotProjectAutoSuggested',
|
||||
'hotProjectManuallyOverridden'
|
||||
];
|
||||
|
||||
function toOpportunityPrefill(
|
||||
opportunity: QuotationOpportunityLookup | undefined,
|
||||
referenceData: QuotationReferenceData
|
||||
): QuotationCreatePrefill | null {
|
||||
if (!opportunity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const quotationType = referenceData.quotationTypes.some((item) => item.id === opportunity.productType)
|
||||
? opportunity.productType
|
||||
: null;
|
||||
|
||||
return {
|
||||
opportunityId: opportunity.id,
|
||||
customerId: opportunity.customerId,
|
||||
contactId: opportunity.contactId,
|
||||
projectName: opportunity.projectName,
|
||||
projectLocation: opportunity.projectLocation,
|
||||
branchId: opportunity.branchId,
|
||||
quotationType,
|
||||
projectCloseDate: opportunity.projectCloseDate ? opportunity.projectCloseDate.slice(0, 10) : null,
|
||||
deliveryDate: opportunity.deliveryDate ? opportunity.deliveryDate.slice(0, 10) : null,
|
||||
isHotProject: opportunity.isHotProject,
|
||||
hotProjectAutoSuggested: opportunity.hotProjectAutoSuggested,
|
||||
hotProjectManuallyOverridden: opportunity.hotProjectManuallyOverridden
|
||||
};
|
||||
}
|
||||
|
||||
function toFormState(
|
||||
quotation: QuotationRecord | undefined,
|
||||
referenceData: QuotationReferenceData
|
||||
referenceData: QuotationReferenceData,
|
||||
initialPrefill?: QuotationCreatePrefill | null
|
||||
): FormState {
|
||||
return {
|
||||
opportunityId: quotation?.opportunityId ?? '',
|
||||
customerId: quotation?.customerId ?? referenceData.customers[0]?.id ?? '',
|
||||
contactId: quotation?.contactId ?? '',
|
||||
quotationDate:
|
||||
quotation?.quotationDate?.slice(0, 10) ?? new Date().toISOString().slice(0, 10),
|
||||
opportunityId: quotation?.opportunityId ?? initialPrefill?.opportunityId ?? '',
|
||||
customerId: quotation?.customerId ?? initialPrefill?.customerId ?? referenceData.customers[0]?.id ?? '',
|
||||
contactId: quotation?.contactId ?? initialPrefill?.contactId ?? '',
|
||||
quotationDate: quotation?.quotationDate?.slice(0, 10) ?? new Date().toISOString().slice(0, 10),
|
||||
validUntil: quotation?.validUntil?.slice(0, 10) ?? '',
|
||||
quotationType: quotation?.quotationType ?? referenceData.quotationTypes[0]?.id ?? '',
|
||||
projectName: quotation?.projectName ?? '',
|
||||
projectLocation: quotation?.projectLocation ?? '',
|
||||
quotationType:
|
||||
quotation?.quotationType ?? initialPrefill?.quotationType ?? referenceData.quotationTypes[0]?.id ?? '',
|
||||
projectName: quotation?.projectName ?? initialPrefill?.projectName ?? '',
|
||||
projectLocation: quotation?.projectLocation ?? initialPrefill?.projectLocation ?? '',
|
||||
attention: quotation?.attention ?? '',
|
||||
branchId: quotation?.branchId ?? '',
|
||||
branchId: quotation?.branchId ?? initialPrefill?.branchId ?? '',
|
||||
currency: quotation?.currency ?? referenceData.currencies[0]?.id ?? '',
|
||||
exchangeRate: String(quotation?.exchangeRate ?? 1),
|
||||
status: quotation?.status ?? referenceData.statuses[0]?.id ?? '',
|
||||
chancePercent:
|
||||
quotation?.chancePercent === null || quotation?.chancePercent === undefined
|
||||
? ''
|
||||
: String(quotation.chancePercent),
|
||||
chancePercent: quotation?.chancePercent !== null && quotation?.chancePercent !== undefined ? String(quotation.chancePercent) : '',
|
||||
projectCloseDate: quotation?.projectCloseDate?.slice(0, 10) ?? initialPrefill?.projectCloseDate ?? '',
|
||||
deliveryDate: quotation?.deliveryDate?.slice(0, 10) ?? initialPrefill?.deliveryDate ?? '',
|
||||
competitor: quotation?.competitor ?? '',
|
||||
reference: quotation?.reference ?? '',
|
||||
notes: quotation?.notes ?? '',
|
||||
@@ -98,25 +164,23 @@ function toFormState(
|
||||
taxRate: String(quotation?.taxRate ?? 0),
|
||||
sentVia: quotation?.sentVia ?? '',
|
||||
revisionRemark: quotation?.revisionRemark ?? '',
|
||||
isHotProject: quotation?.isHotProject ?? false,
|
||||
isHotProject: quotation?.isHotProject ?? initialPrefill?.isHotProject ?? false,
|
||||
hotProjectAutoSuggested:
|
||||
quotation?.hotProjectAutoSuggested ?? initialPrefill?.hotProjectAutoSuggested ?? false,
|
||||
hotProjectManuallyOverridden:
|
||||
quotation?.hotProjectManuallyOverridden ?? initialPrefill?.hotProjectManuallyOverridden ?? false,
|
||||
isActive: quotation?.isActive ?? true
|
||||
};
|
||||
}
|
||||
|
||||
function Field({
|
||||
label,
|
||||
required = false,
|
||||
children,
|
||||
className
|
||||
}: {
|
||||
label: string;
|
||||
required?: boolean;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
required,
|
||||
children
|
||||
}: React.PropsWithChildren<{ label: string; required?: boolean }>) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className='mb-2 text-sm font-medium'>
|
||||
<div className='space-y-2'>
|
||||
<div className='text-sm font-medium'>
|
||||
{label}
|
||||
{required ? ' *' : ''}
|
||||
</div>
|
||||
@@ -129,20 +193,28 @@ export function QuotationFormSheet({
|
||||
quotation,
|
||||
open,
|
||||
onOpenChange,
|
||||
referenceData
|
||||
referenceData,
|
||||
initialPrefill
|
||||
}: {
|
||||
quotation?: QuotationRecord;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
referenceData: QuotationReferenceData;
|
||||
initialPrefill?: QuotationCreatePrefill | null;
|
||||
}) {
|
||||
const isEdit = !!quotation;
|
||||
const defaultState = useMemo(() => toFormState(quotation, referenceData), [quotation, referenceData]);
|
||||
const isEdit = Boolean(quotation);
|
||||
const defaultState = useMemo(
|
||||
() => toFormState(quotation, referenceData, initialPrefill),
|
||||
[quotation, referenceData, initialPrefill]
|
||||
);
|
||||
const [state, setState] = useState<FormState>(defaultState);
|
||||
const [projectParties, setProjectParties] = useState<ProjectPartyEditorItem[]>([]);
|
||||
const [dirtyFields, setDirtyFields] = useState<Set<string>>(new Set());
|
||||
const [projectPartiesDirty, setProjectPartiesDirty] = useState(false);
|
||||
|
||||
const projectPartiesQuery = useQuery({
|
||||
...quotationCustomersOptions(quotation?.id ?? ''),
|
||||
enabled: open && !!quotation?.id
|
||||
enabled: open && Boolean(quotation?.id)
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
@@ -151,8 +223,9 @@ export function QuotationFormSheet({
|
||||
toast.success('Quotation created successfully');
|
||||
onOpenChange(false);
|
||||
},
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to create quotation')
|
||||
onError: (error) => {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to create quotation');
|
||||
}
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
@@ -161,46 +234,127 @@ export function QuotationFormSheet({
|
||||
toast.success('Quotation updated successfully');
|
||||
onOpenChange(false);
|
||||
},
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to update quotation')
|
||||
onError: (error) => {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to update quotation');
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(defaultState);
|
||||
setDirtyFields(new Set());
|
||||
setProjectPartiesDirty(false);
|
||||
|
||||
if (quotation) {
|
||||
setProjectParties(
|
||||
quotation
|
||||
? (projectPartiesQuery.data?.items ?? []).map((item) => ({
|
||||
(projectPartiesQuery.data?.items ?? []).map((item) => ({
|
||||
key: item.id,
|
||||
customerId: item.customerId,
|
||||
role: item.role,
|
||||
remark: item.remark ?? ''
|
||||
}))
|
||||
: []
|
||||
);
|
||||
return;
|
||||
}
|
||||
}, [defaultState, open, projectPartiesQuery.data?.items, quotation]);
|
||||
|
||||
setProjectParties(initialPrefill?.projectParties ?? []);
|
||||
}, [defaultState, initialPrefill, open, projectPartiesQuery.data?.items, quotation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const suggested = isHotProjectSuggested(state.projectCloseDate);
|
||||
|
||||
setState((current) => {
|
||||
if (current.hotProjectManuallyOverridden) {
|
||||
if (current.hotProjectAutoSuggested === suggested) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return { ...current, hotProjectAutoSuggested: suggested };
|
||||
}
|
||||
|
||||
if (current.hotProjectAutoSuggested === suggested && current.isHotProject === suggested) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return {
|
||||
...current,
|
||||
isHotProject: suggested,
|
||||
hotProjectAutoSuggested: suggested
|
||||
};
|
||||
});
|
||||
}, [open, state.projectCloseDate, state.hotProjectManuallyOverridden]);
|
||||
|
||||
const contacts = useMemo(
|
||||
() => referenceData.contacts.filter((item) => item.customerId === state.customerId),
|
||||
[referenceData.contacts, state.customerId]
|
||||
);
|
||||
|
||||
function setField<K extends keyof FormState>(key: K, value: FormState[K]) {
|
||||
function markDirty(key: string) {
|
||||
setDirtyFields((current) => {
|
||||
const next = new Set(current);
|
||||
next.add(key);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function setField<K extends keyof FormState>(key: K, value: FormState[K], dirty = true) {
|
||||
setState((current) => ({ ...current, [key]: value }));
|
||||
|
||||
if (dirty) {
|
||||
markDirty(key);
|
||||
}
|
||||
}
|
||||
|
||||
function applyPrefill(prefill: QuotationCreatePrefill | null) {
|
||||
if (!prefill) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState((current) => {
|
||||
const next = { ...current };
|
||||
|
||||
if (prefill.opportunityId !== undefined) {
|
||||
next.opportunityId = prefill.opportunityId ?? '';
|
||||
}
|
||||
|
||||
for (const key of PREFILL_FIELD_KEYS) {
|
||||
if (dirtyFields.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = prefill[key];
|
||||
|
||||
if (value === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
(next[key] as FormState[typeof key]) =
|
||||
typeof next[key] === 'boolean' ? Boolean(value) : ((value ?? '') as FormState[typeof key]);
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
|
||||
if (!projectPartiesDirty && prefill.projectParties) {
|
||||
setProjectParties(prefill.projectParties);
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpportunityChange(value: string) {
|
||||
const opportunity = referenceData.opportunities.find((item) => item.id === value);
|
||||
setState((current) => ({
|
||||
...current,
|
||||
opportunityId: value === '__none__' ? '' : value,
|
||||
customerId: opportunity?.customerId ?? current.customerId,
|
||||
contactId: opportunity?.contactId ?? '',
|
||||
projectName: opportunity?.projectName ?? current.projectName,
|
||||
projectLocation: opportunity?.projectLocation ?? current.projectLocation,
|
||||
branchId: opportunity?.branchId ?? current.branchId
|
||||
}));
|
||||
const nextOpportunityId = value === '__none__' ? '' : value;
|
||||
const opportunity = referenceData.opportunities.find((item) => item.id === nextOpportunityId);
|
||||
markDirty('opportunityId');
|
||||
applyPrefill({
|
||||
...toOpportunityPrefill(opportunity, referenceData),
|
||||
opportunityId: nextOpportunityId
|
||||
});
|
||||
}
|
||||
|
||||
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
@@ -221,7 +375,11 @@ export function QuotationFormSheet({
|
||||
exchangeRate: state.exchangeRate ? Number(state.exchangeRate) : 1,
|
||||
status: state.status,
|
||||
chancePercent: state.chancePercent ? Number(state.chancePercent) : null,
|
||||
projectCloseDate: state.projectCloseDate || null,
|
||||
deliveryDate: state.deliveryDate || null,
|
||||
isHotProject: state.isHotProject,
|
||||
hotProjectAutoSuggested: state.hotProjectAutoSuggested,
|
||||
hotProjectManuallyOverridden: state.hotProjectManuallyOverridden,
|
||||
competitor: state.competitor,
|
||||
reference: state.reference,
|
||||
notes: state.notes,
|
||||
@@ -237,10 +395,18 @@ export function QuotationFormSheet({
|
||||
.map((item) => ({
|
||||
customerId: item.customerId,
|
||||
role: item.role,
|
||||
isPrimary: false,
|
||||
remark: item.remark || null
|
||||
}))
|
||||
};
|
||||
|
||||
const parsed = quotationSchema.safeParse(payload);
|
||||
|
||||
if (!parsed.success) {
|
||||
toast.error(parsed.error.issues[0]?.message ?? 'Invalid quotation payload');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEdit && quotation) {
|
||||
await updateMutation.mutateAsync({ id: quotation.id, values: payload });
|
||||
return;
|
||||
@@ -261,8 +427,7 @@ export function QuotationFormSheet({
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<form id='quotation-form-sheet' onSubmit={onSubmit} className='flex-1 overflow-auto'>
|
||||
<div className='grid gap-4 py-4 md:grid-cols-2'>
|
||||
<form id='quotation-form-sheet' onSubmit={onSubmit} className='grid flex-1 gap-4 overflow-auto py-2 md:grid-cols-2'>
|
||||
<Field label='Opportunity'>
|
||||
<Select value={state.opportunityId || '__none__'} onValueChange={handleOpportunityChange}>
|
||||
<SelectTrigger>
|
||||
@@ -279,7 +444,7 @@ export function QuotationFormSheet({
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
<Field label='ลูกค้าผู้รับใบเสนอราคา'>
|
||||
<Field label='Customer' required>
|
||||
<Select
|
||||
value={state.customerId}
|
||||
onValueChange={(value) => {
|
||||
@@ -342,8 +507,19 @@ export function QuotationFormSheet({
|
||||
<CrmDateInput value={state.validUntil} onChange={(value) => setField('validUntil', value)} />
|
||||
</Field>
|
||||
|
||||
<Field label='Project Close Date'>
|
||||
<CrmDateInput value={state.projectCloseDate} onChange={(value) => setField('projectCloseDate', value)} />
|
||||
</Field>
|
||||
|
||||
<Field label='Delivery Date'>
|
||||
<CrmDateInput value={state.deliveryDate} onChange={(value) => setField('deliveryDate', value)} />
|
||||
</Field>
|
||||
|
||||
<Field label='Branch'>
|
||||
<Select value={state.branchId || '__none__'} onValueChange={(value) => setField('branchId', value === '__none__' ? '' : value)}>
|
||||
<Select
|
||||
value={state.branchId || '__none__'}
|
||||
onValueChange={(value) => setField('branchId', value === '__none__' ? '' : value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='Select branch' />
|
||||
</SelectTrigger>
|
||||
@@ -409,7 +585,10 @@ export function QuotationFormSheet({
|
||||
</Field>
|
||||
|
||||
<Field label='Salesman'>
|
||||
<Select value={state.salesmanId || '__none__'} onValueChange={(value) => setField('salesmanId', value === '__none__' ? '' : value)}>
|
||||
<Select
|
||||
value={state.salesmanId || '__none__'}
|
||||
onValueChange={(value) => setField('salesmanId', value === '__none__' ? '' : value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='Select salesman' />
|
||||
</SelectTrigger>
|
||||
@@ -433,7 +612,10 @@ export function QuotationFormSheet({
|
||||
</Field>
|
||||
|
||||
<Field label='Discount Type'>
|
||||
<Select value={state.discountType || '__none__'} onValueChange={(value) => setField('discountType', value === '__none__' ? '' : value)}>
|
||||
<Select
|
||||
value={state.discountType || '__none__'}
|
||||
onValueChange={(value) => setField('discountType', value === '__none__' ? '' : value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='Select discount type' />
|
||||
</SelectTrigger>
|
||||
@@ -453,7 +635,10 @@ export function QuotationFormSheet({
|
||||
</Field>
|
||||
|
||||
<Field label='Sent Via'>
|
||||
<Select value={state.sentVia || '__none__'} onValueChange={(value) => setField('sentVia', value === '__none__' ? '' : value)}>
|
||||
<Select
|
||||
value={state.sentVia || '__none__'}
|
||||
onValueChange={(value) => setField('sentVia', value === '__none__' ? '' : value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='Select sent via' />
|
||||
</SelectTrigger>
|
||||
@@ -477,7 +662,10 @@ export function QuotationFormSheet({
|
||||
customers={referenceData.customers}
|
||||
roles={referenceData.projectPartyRoles}
|
||||
items={projectParties}
|
||||
onChange={setProjectParties}
|
||||
onChange={(items) => {
|
||||
setProjectParties(items);
|
||||
setProjectPartiesDirty(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -489,16 +677,32 @@ export function QuotationFormSheet({
|
||||
|
||||
<div className='md:col-span-2'>
|
||||
<Field label='Revision Remark'>
|
||||
<CrmTextarea value={state.revisionRemark} onChange={(e) => setField('revisionRemark', e.target.value)} placeholder='Revision note or approval context' size='md' />
|
||||
<CrmTextarea
|
||||
value={state.revisionRemark}
|
||||
onChange={(e) => setField('revisionRemark', e.target.value)}
|
||||
placeholder='Revision note or approval context'
|
||||
size='md'
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between rounded-lg border px-4 py-3'>
|
||||
<div>
|
||||
<div className='font-medium'>Hot Project</div>
|
||||
<div className='text-muted-foreground text-sm'>Highlight urgent or strategic deals.</div>
|
||||
<div className='text-muted-foreground text-sm'>Auto-suggested when project close date is within 3 months.</div>
|
||||
</div>
|
||||
<Switch checked={state.isHotProject} onCheckedChange={(checked) => setField('isHotProject', checked)} />
|
||||
<Switch
|
||||
checked={state.isHotProject}
|
||||
onCheckedChange={(checked) => {
|
||||
setState((current) => ({
|
||||
...current,
|
||||
isHotProject: checked,
|
||||
hotProjectManuallyOverridden: true,
|
||||
hotProjectAutoSuggested: isHotProjectSuggested(current.projectCloseDate)
|
||||
}));
|
||||
markDirty('isHotProject');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between rounded-lg border px-4 py-3'>
|
||||
@@ -508,7 +712,6 @@ export function QuotationFormSheet({
|
||||
</div>
|
||||
<Switch checked={state.isActive} onCheckedChange={(checked) => setField('isActive', checked)} />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<SheetFooter>
|
||||
@@ -535,10 +738,10 @@ export function QuotationFormSheetTrigger({
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setOpen(true)}>
|
||||
<Icons.add className='mr-2 h-4 w-4' /> Add Quotation
|
||||
<Icons.add className='mr-2 h-4 w-4' />
|
||||
Add Quotation
|
||||
</Button>
|
||||
<QuotationFormSheet open={open} onOpenChange={setOpen} referenceData={referenceData} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as z from 'zod';
|
||||
import { isValidDateOnlyString } from '@/features/crm/shared/hot-project';
|
||||
|
||||
function coerceOptionalNumber(message: string) {
|
||||
return z.preprocess((value) => {
|
||||
@@ -38,41 +39,65 @@ function coerceRequiredNumber(message: string) {
|
||||
}, z.number().refine((value) => !Number.isNaN(value), message));
|
||||
}
|
||||
|
||||
function optionalDateOnly(message: string) {
|
||||
return z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.refine((value) => !value || isValidDateOnlyString(value), message);
|
||||
}
|
||||
|
||||
function requiredDateOnly(message: string) {
|
||||
return z
|
||||
.string()
|
||||
.min(1, message)
|
||||
.refine((value) => isValidDateOnlyString(value), message);
|
||||
}
|
||||
|
||||
export const quotationSchema = z.object({
|
||||
opportunityId: z.string().optional().nullable(),
|
||||
customerId: z.string().min(1, 'Please select a customer'),
|
||||
customerId: z.string().min(1, 'Please select customer'),
|
||||
contactId: z.string().optional().nullable(),
|
||||
quotationDate: z.string().min(1, 'Quotation date is required'),
|
||||
validUntil: z.string().optional().nullable(),
|
||||
quotationType: z.string().min(1, 'Please select a quotation type'),
|
||||
quotationDate: requiredDateOnly('Quotation date must be YYYY-MM-DD'),
|
||||
validUntil: optionalDateOnly('Valid until date must be YYYY-MM-DD'),
|
||||
quotationType: z.string().min(1, 'Please select quotation type'),
|
||||
projectName: z.string().optional(),
|
||||
projectLocation: z.string().optional(),
|
||||
attention: z.string().optional(),
|
||||
branchId: z.string().optional().nullable(),
|
||||
currency: z.string().min(1, 'Please select a currency'),
|
||||
exchangeRate: coerceOptionalNumber('Exchange rate must be a number'),
|
||||
status: z.string().min(1, 'Please select a status'),
|
||||
chancePercent: coerceOptionalNumber('Chance percent must be a number')
|
||||
.refine(
|
||||
currency: z.string().min(1, 'Please select currency'),
|
||||
exchangeRate: coerceOptionalNumber('Exchange rate must be number'),
|
||||
status: z.string().min(1, 'Please select status'),
|
||||
chancePercent: coerceOptionalNumber('Chance percent must be number').refine(
|
||||
(value) => value === undefined || (value >= 0 && value <= 100),
|
||||
'Chance percent must be between 0 and 100'
|
||||
),
|
||||
projectCloseDate: optionalDateOnly('Project close date must be YYYY-MM-DD'),
|
||||
deliveryDate: optionalDateOnly('Delivery date must be YYYY-MM-DD'),
|
||||
isHotProject: z.boolean().default(false),
|
||||
hotProjectAutoSuggested: z.boolean().default(false),
|
||||
hotProjectManuallyOverridden: z.boolean().default(false),
|
||||
competitor: z.string().optional(),
|
||||
reference: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
salesmanId: z.string().optional().nullable(),
|
||||
discount: coerceOptionalNumber('Discount must be a number'),
|
||||
discount: coerceOptionalNumber('Discount must be number').refine(
|
||||
(value) => value === undefined || value >= 0,
|
||||
'Discount must be 0 or greater'
|
||||
),
|
||||
discountType: z.string().optional().nullable(),
|
||||
taxRate: coerceOptionalNumber('Tax rate must be a number'),
|
||||
isActive: z.boolean().default(true),
|
||||
taxRate: coerceOptionalNumber('Tax rate must be number').refine(
|
||||
(value) => value === undefined || value >= 0,
|
||||
'Tax rate must be 0 or greater'
|
||||
),
|
||||
sentVia: z.string().optional().nullable(),
|
||||
revisionRemark: z.string().optional().nullable(),
|
||||
isActive: z.boolean().default(true),
|
||||
projectParties: z
|
||||
.array(
|
||||
z.object({
|
||||
customerId: z.string().min(1, 'Please select a customer'),
|
||||
role: z.string().min(1, 'Please select a role'),
|
||||
customerId: z.string().min(1, 'Please select customer'),
|
||||
role: z.string().min(1, 'Please select role'),
|
||||
isPrimary: z.boolean().default(false).optional(),
|
||||
remark: z.string().nullable().optional()
|
||||
})
|
||||
@@ -81,70 +106,70 @@ export const quotationSchema = z.object({
|
||||
});
|
||||
|
||||
export const quotationItemSchema = z.object({
|
||||
productType: z.string().min(1, 'Please select a product type'),
|
||||
productType: z.string().min(1, 'Please select product type'),
|
||||
description: z.string().min(2, 'Description must be at least 2 characters'),
|
||||
quantity: coerceRequiredNumber('Quantity must be a number').refine(
|
||||
quantity: coerceRequiredNumber('Quantity must be number').refine(
|
||||
(value) => value > 0,
|
||||
'Quantity must be greater than 0'
|
||||
),
|
||||
unit: z.string().optional(),
|
||||
unitPrice: coerceRequiredNumber('Unit price must be a number').refine(
|
||||
unitPrice: coerceRequiredNumber('Unit price must be number').refine(
|
||||
(value) => value >= 0,
|
||||
'Unit price must be 0 or greater'
|
||||
),
|
||||
discount: coerceOptionalNumber('Discount must be a number').refine(
|
||||
discount: coerceOptionalNumber('Discount must be number').refine(
|
||||
(value) => value === undefined || value >= 0,
|
||||
'Discount must be 0 or greater'
|
||||
),
|
||||
discountType: z.string().nullable().optional(),
|
||||
taxRate: coerceOptionalNumber('Tax rate must be a number').refine(
|
||||
taxRate: coerceOptionalNumber('Tax rate must be number').refine(
|
||||
(value) => value === undefined || value >= 0,
|
||||
'Tax rate must be 0 or greater'
|
||||
),
|
||||
notes: z.string().optional(),
|
||||
sortOrder: coerceOptionalNumber('Sort order must be a number')
|
||||
sortOrder: coerceOptionalNumber('Sort order must be number')
|
||||
});
|
||||
|
||||
export const quotationCustomerSchema = z.object({
|
||||
customerId: z.string().min(1, 'Please select a customer'),
|
||||
role: z.string().min(1, 'Please select a role'),
|
||||
customerId: z.string().min(1, 'Please select customer'),
|
||||
role: z.string().min(1, 'Please select role'),
|
||||
isPrimary: z.boolean().default(false),
|
||||
remark: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export const quotationTopicSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
topicType: z.string().min(1, 'Please select a topic type'),
|
||||
topicType: z.string().min(1, 'Please select topic type'),
|
||||
title: z.string().min(2, 'Title must be at least 2 characters'),
|
||||
sortOrder: coerceOptionalNumber('Sort order must be a number'),
|
||||
sortOrder: coerceOptionalNumber('Sort order must be number'),
|
||||
items: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
content: z.string().min(1, 'Topic item content is required'),
|
||||
sortOrder: coerceOptionalNumber('Sort order must be a number')
|
||||
content: z.string().min(1, 'Topic item content required'),
|
||||
sortOrder: coerceOptionalNumber('Sort order must be number')
|
||||
})
|
||||
)
|
||||
.min(1, 'At least one topic item is required')
|
||||
.min(1, 'At least one topic item required')
|
||||
});
|
||||
|
||||
export const quotationFollowupSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
followupDate: z.string().min(1, 'Follow-up date is required'),
|
||||
followupType: z.string().min(1, 'Please select a follow-up type'),
|
||||
followupDate: requiredDateOnly('Follow-up date must be YYYY-MM-DD'),
|
||||
followupType: z.string().min(1, 'Please select follow-up type'),
|
||||
contactId: z.string().optional().nullable(),
|
||||
outcome: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
nextFollowupDate: z.string().optional().nullable(),
|
||||
nextFollowupDate: optionalDateOnly('Next follow-up date must be YYYY-MM-DD'),
|
||||
nextAction: z.string().optional()
|
||||
});
|
||||
|
||||
export const quotationAttachmentSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
fileName: z.string().min(1, 'Stored file name is required'),
|
||||
originalFileName: z.string().min(1, 'Original file name is required'),
|
||||
filePath: z.string().min(1, 'File path is required'),
|
||||
fileSize: coerceOptionalNumber('File size must be a number'),
|
||||
fileName: z.string().min(1, 'Stored file name required'),
|
||||
originalFileName: z.string().min(1, 'Original file name required'),
|
||||
filePath: z.string().min(1, 'File path required'),
|
||||
fileSize: coerceOptionalNumber('File size must be number'),
|
||||
fileType: z.string().nullable().optional(),
|
||||
description: z.string().optional()
|
||||
});
|
||||
@@ -159,4 +184,3 @@ export type QuotationCustomerFormValues = z.input<typeof quotationCustomerSchema
|
||||
export type QuotationTopicFormValues = z.input<typeof quotationTopicSchema>;
|
||||
export type QuotationFollowupFormValues = z.input<typeof quotationFollowupSchema>;
|
||||
export type QuotationAttachmentFormValues = z.input<typeof quotationAttachmentSchema>;
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ import {
|
||||
PROJECT_PARTY_OPTION_CATEGORY,
|
||||
resolveProjectPartyRole
|
||||
} from '@/features/crm/shared/project-party';
|
||||
import { resolveHotProjectState } from '@/features/crm/shared/hot-project';
|
||||
|
||||
const QUOTATION_OPTION_CATEGORIES = {
|
||||
status: 'crm_quotation_status',
|
||||
@@ -271,7 +272,11 @@ function mapQuotationRecord(
|
||||
taxAmount: row.taxAmount,
|
||||
totalAmount: row.totalAmount,
|
||||
chancePercent: row.chancePercent,
|
||||
projectCloseDate: row.projectCloseDate?.toISOString() ?? null,
|
||||
deliveryDate: row.deliveryDate?.toISOString() ?? null,
|
||||
isHotProject: row.isHotProject,
|
||||
hotProjectAutoSuggested: row.hotProjectAutoSuggested,
|
||||
hotProjectManuallyOverridden: row.hotProjectManuallyOverridden,
|
||||
competitor: row.competitor,
|
||||
salesmanId: row.salesmanId,
|
||||
salesmanName: options?.salesmanName ?? null,
|
||||
@@ -1242,7 +1247,13 @@ export async function getQuotationReferenceData(
|
||||
title: opportunity.title,
|
||||
projectName: opportunity.projectName,
|
||||
projectLocation: opportunity.projectLocation,
|
||||
branchId: opportunity.branchId
|
||||
branchId: opportunity.branchId,
|
||||
productType: opportunity.productType,
|
||||
projectCloseDate: opportunity.projectCloseDate?.toISOString() ?? null,
|
||||
deliveryDate: opportunity.deliveryDate?.toISOString() ?? null,
|
||||
isHotProject: opportunity.isHotProject,
|
||||
hotProjectAutoSuggested: opportunity.hotProjectAutoSuggested,
|
||||
hotProjectManuallyOverridden: opportunity.hotProjectManuallyOverridden
|
||||
})),
|
||||
salesmen: salesmen.map<QuotationSalesmanLookup>((salesman) => ({
|
||||
id: salesman.id,
|
||||
@@ -1469,6 +1480,12 @@ export async function createQuotation(
|
||||
opportunityId: payload.opportunityId,
|
||||
customerId: payload.customerId
|
||||
});
|
||||
const hotProjectState = resolveHotProjectState({
|
||||
projectCloseDate: payload.projectCloseDate ?? opportunity?.projectCloseDate?.toISOString().slice(0, 10) ?? null,
|
||||
isHotProject: payload.isHotProject ?? opportunity?.isHotProject ?? false,
|
||||
hotProjectManuallyOverridden:
|
||||
payload.hotProjectManuallyOverridden ?? opportunity?.hotProjectManuallyOverridden ?? false
|
||||
});
|
||||
const documentCode = await generateNextDocumentCode({
|
||||
organizationId,
|
||||
documentType: 'quotation',
|
||||
@@ -1505,7 +1522,13 @@ const documentCode = await generateNextDocumentCode({
|
||||
discountType: payload.discountType ?? null,
|
||||
taxRate: payload.taxRate ?? 0,
|
||||
chancePercent: payload.chancePercent ?? opportunity?.chancePercent ?? null,
|
||||
isHotProject: payload.isHotProject ?? opportunity?.isHotProject ?? false,
|
||||
projectCloseDate: payload.projectCloseDate
|
||||
? new Date(payload.projectCloseDate)
|
||||
: opportunity?.projectCloseDate ?? null,
|
||||
deliveryDate: payload.deliveryDate ? new Date(payload.deliveryDate) : opportunity?.deliveryDate ?? null,
|
||||
isHotProject: hotProjectState.isHotProject,
|
||||
hotProjectAutoSuggested: hotProjectState.hotProjectAutoSuggested,
|
||||
hotProjectManuallyOverridden: hotProjectState.hotProjectManuallyOverridden,
|
||||
competitor: payload.competitor?.trim() || opportunity?.competitor || null,
|
||||
salesmanId: payload.salesmanId ?? null,
|
||||
isSent: false,
|
||||
@@ -1573,6 +1596,12 @@ export async function updateQuotation(
|
||||
opportunityId: payload.opportunityId,
|
||||
customerId: payload.customerId
|
||||
});
|
||||
const hotProjectState = resolveHotProjectState({
|
||||
projectCloseDate: payload.projectCloseDate ?? opportunity?.projectCloseDate?.toISOString().slice(0, 10) ?? null,
|
||||
isHotProject: payload.isHotProject ?? existing.isHotProject,
|
||||
hotProjectManuallyOverridden:
|
||||
payload.hotProjectManuallyOverridden ?? existing.hotProjectManuallyOverridden
|
||||
});
|
||||
|
||||
const updated = await db.transaction(async (tx) => {
|
||||
const [row] = await tx
|
||||
@@ -1598,7 +1627,13 @@ export async function updateQuotation(
|
||||
discountType: payload.discountType ?? null,
|
||||
taxRate: payload.taxRate ?? 0,
|
||||
chancePercent: payload.chancePercent ?? opportunity?.chancePercent ?? null,
|
||||
isHotProject: payload.isHotProject ?? false,
|
||||
projectCloseDate: payload.projectCloseDate
|
||||
? new Date(payload.projectCloseDate)
|
||||
: opportunity?.projectCloseDate ?? null,
|
||||
deliveryDate: payload.deliveryDate ? new Date(payload.deliveryDate) : opportunity?.deliveryDate ?? null,
|
||||
isHotProject: hotProjectState.isHotProject,
|
||||
hotProjectAutoSuggested: hotProjectState.hotProjectAutoSuggested,
|
||||
hotProjectManuallyOverridden: hotProjectState.hotProjectManuallyOverridden,
|
||||
competitor: payload.competitor?.trim() || opportunity?.competitor || null,
|
||||
salesmanId: payload.salesmanId ?? null,
|
||||
sentVia: payload.sentVia ?? null,
|
||||
|
||||
63
src/features/crm/shared/hot-project.ts
Normal file
63
src/features/crm/shared/hot-project.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { addMonths, endOfDay, isValid, parse } from 'date-fns';
|
||||
|
||||
const DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||
|
||||
export interface HotProjectResolutionInput {
|
||||
projectCloseDate?: string | null;
|
||||
isHotProject?: boolean | null;
|
||||
hotProjectManuallyOverridden?: boolean | null;
|
||||
}
|
||||
|
||||
export interface HotProjectResolution {
|
||||
isHotProject: boolean;
|
||||
hotProjectAutoSuggested: boolean;
|
||||
hotProjectManuallyOverridden: boolean;
|
||||
}
|
||||
|
||||
export function isValidDateOnlyString(value: string | null | undefined): value is string {
|
||||
if (!value || !DATE_ONLY_PATTERN.test(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parsed = parse(value, 'yyyy-MM-dd', new Date());
|
||||
return isValid(parsed);
|
||||
}
|
||||
|
||||
export function isHotProjectSuggested(projectCloseDate: string | null | undefined, now = new Date()) {
|
||||
if (!isValidDateOnlyString(projectCloseDate)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parsed = parse(projectCloseDate, 'yyyy-MM-dd', now);
|
||||
return parsed.getTime() <= endOfDay(addMonths(now, 3)).getTime();
|
||||
}
|
||||
|
||||
export function resolveHotProjectState(
|
||||
input: HotProjectResolutionInput,
|
||||
now = new Date()
|
||||
): HotProjectResolution {
|
||||
const hotProjectAutoSuggested = isHotProjectSuggested(input.projectCloseDate, now);
|
||||
const hotProjectManuallyOverridden = Boolean(input.hotProjectManuallyOverridden);
|
||||
|
||||
if (hotProjectManuallyOverridden) {
|
||||
return {
|
||||
isHotProject: Boolean(input.isHotProject),
|
||||
hotProjectAutoSuggested,
|
||||
hotProjectManuallyOverridden: true
|
||||
};
|
||||
}
|
||||
|
||||
if (hotProjectAutoSuggested) {
|
||||
return {
|
||||
isHotProject: true,
|
||||
hotProjectAutoSuggested: true,
|
||||
hotProjectManuallyOverridden: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isHotProject: Boolean(input.isHotProject),
|
||||
hotProjectAutoSuggested: false,
|
||||
hotProjectManuallyOverridden: false
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user