diff --git a/docs/implementation/task-ep1.6-calendar-projection-workspace-big-calendar-foundation-2026-07-13.md b/docs/implementation/task-ep1.6-calendar-projection-workspace-big-calendar-foundation-2026-07-13.md new file mode 100644 index 0000000..f4f2a28 --- /dev/null +++ b/docs/implementation/task-ep1.6-calendar-projection-workspace-big-calendar-foundation-2026-07-13.md @@ -0,0 +1,153 @@ +# Task EP.1.6 Calendar Projection, Workspace & Big Calendar Foundation - 2026-07-13 + +## Scope Delivered + +- introduced `crm_calendar_projection` materialized read model schema and migration `drizzle/0006_massive_scalphunter.sql` +- added Calendar projection contracts under `src/features/crm/calendar/**` +- added Activity business-event mapping for scheduled Activity calendar rows +- added terminal Activity event handling that removes completed/cancelled/deleted Activity rows from Calendar projection +- added Calendar projection consumer, registration seam, rebuild plan contract, query service, API route, React Query service, and dashboard workspace route +- added `CalendarProjectionItem -> BigCalendarEvent` adapter so UI never consumes database rows directly +- added Calendar Workspace UI at `/dashboard/crm/calendar` with Agenda, Day, Week, Month tabs, summary cards, search, empty/loading states, and read-only/editable visual distinction +- added Calendar navigation item under CRM +- preserved Activity, Timeline, Dashboard, Reports, Notifications, Approval, Follow-up APIs, and source-domain ownership + +## Review Summary + +Reviewed before/during implementation: + +- `AGENTS.md` +- `plans/task-ep.1.6.md` +- `docs/standards/engineering-constitution.md` +- `docs/standards/project-foundations.md` +- `docs/standards/architecture-rules.md` +- `docs/standards/ui-ux-rules.md` +- `docs/standards/task-review-checklist.md` +- `docs/security/crm-authorization-boundaries.md` +- `docs/business/relationship-sales-workspace-blueprint-v1.md` +- `docs/implementation/task-ar.2-epic-technical-design-2026-07-07.md` +- `docs/implementation/task-ar.2-workspace-ui-ux-design-note-2026-07-07.md` +- `docs/implementation/task-bu-r.0.1-workspace-activity-business-blueprint-2026-07-07.md` +- `docs/implementation/task-ep1.4-projection-foundation-delivery-reliability-2026-07-13.md` +- `docs/implementation/task-ep1.4.1-transactional-outbox-activation-worker-runtime-2026-07-13.md` +- `docs/implementation/task-ep1.5-timeline-projection-foundation-2026-07-13.md` +- existing Projection Runtime, Activity Business Events, Timeline Projection, CRM security, and date formatting utilities + +## Big Calendar Source Audit + +Upstream: `https://github.com/lramos33/big-calendar` + +Findings: + +- Compatibility: upstream is a Next.js, TypeScript, Tailwind, shadcn-oriented calendar app, but targets Next.js 14 and Tailwind v3 while this repo is Next.js 16 and Tailwind v4. +- React/Next: source is useful as UI composition reference, but the upstream app shell, provider model, demo data, and direct event mutation flow are not compatible with ALLA OS projection governance. +- shadcn/Tailwind: visual approach is compatible, but code must be adapted to existing dashboard shell and token usage. +- Accessibility/dark mode/responsive: upstream explicitly supports responsive and dark-mode calendar behavior; ALLA OS implementation keeps shadcn primitives, semantic cards, badges, buttons, and deterministic date rendering. +- Timezone: upstream uses general date-fns/date string assumptions; ALLA OS calendar rendering uses `src/lib/date-format.ts` and stores projection timezone as `Asia/Bangkok`. +- Drag/drop: upstream supports direct drag/drop rescheduling. ALLA OS does not persist Calendar mutations directly; drag/resize must call Activity APIs, publish Business Events, and refresh the projection. +- License: upstream repository is MIT licensed. + +Adoption decision: + +- Adopted conceptually: Agenda/Day/Week/Month views, toolbar/search pattern, event rendering, editable vs read-only event behavior, user/lens filter seam, working-hours/timezone constraints. +- Rejected: upstream app shell, backend assumptions, mock event store, direct event CRUD, global Calendar context, uncontrolled user lists, direct drag/drop persistence. +- Modification strategy: implement native ALLA OS components under `src/features/crm/calendar/`, keep projection adapter as boundary, and wire future drag/resize only through Activity services. + +## Implementation Summary + +### Calendar Schema + +Added `crm_calendar_projection` with organization, branch, product, source refs, activity/customer/lead/opportunity/quotation/approval refs, owner/assignee, event provenance, title/summary/location, start/end/timezone, priority/category, editable/milestone/overdue/hot-project/all-day flags, visibility metadata, source event, projection version, timestamps. + +Indexes cover organization/date range, owner/assignee/date range, branch/date range, category/date range, customer/date range, activity/event lookup, and event/version idempotency. + +### Calendar Builder and Consumer + +Activity events supported: + +- `activity.created` +- `activity.assigned` +- `activity.reassigned` +- `activity.started` +- `activity.rescheduled` +- `activity.completed` +- `activity.cancelled` +- `activity.deleted` + +Non-terminal scheduled Activity events upsert one current Calendar row per Activity. Terminal events delete the Activity calendar row so Calendar remains a forward-looking operational workspace, not a history timeline. + +### Query and Security + +Calendar query service supports: + +- date range +- user/team user IDs +- branch/product +- customer/lead/opportunity/quotation +- activity type/category/priority +- overdue/milestone/hot-project flags +- search +- Personal/Team/Manager/Executive lens contract + +The service applies bounded SQL first, then server-side CRM scope checks through `CrmSecurityContext` and `canAccessScopedCrmRecord()`. Owner/assignee direct visibility is preserved. Internal-only rows remain restricted unless directly assigned or organization-wide scope applies. + +### Workspace + +Added `/dashboard/crm/calendar` using `PageContainer`, React Query prefetch/hydration, and existing shadcn UI primitives. + +Workspace includes: + +- summary cards: Due Today, Overdue, Meetings, Hot Projects +- Agenda default view +- Day/Week/Month tabs +- search +- empty/loading states +- event cards with read-only milestone vs editable activity distinction + +### Rebuild and Health + +Calendar rebuild contract added as a plan/result helper for hybrid rebuild: + +- sources: Activity, Opportunity, Quotation, Approval, Business Event replay +- supports organization/date range/dry-run/reset/restart contract + +Runtime checkpoint/retry/dead-letter behavior remains owned by Projection Runtime. Calendar consumer can be registered with runtime consumers without changing source-domain writes. + +## Delivery Matrix + +| Source | Business Event / Source Signal | Projection | Calendar Item | Workspace | +| --- | --- | --- | --- | --- | +| Activity | `activity.created`, assigned, reassigned, started, rescheduled | `crm_calendar_projection` | editable Activity event | Calendar Agenda/Day/Week/Month | +| Activity | `activity.completed`, cancelled, deleted | delete projection row | removed from forward schedule | Calendar refresh | +| Opportunity | Expected Award / Expected Delivery / Hot Project | rebuild contract seam | read-only milestone | future source adapter | +| Quotation | Valid Until / Delivery milestone | rebuild contract seam | read-only milestone | future source adapter | +| Approval | Due / Escalation | rebuild contract seam | read-only milestone | future source adapter | + +## UI/UX Review + +- Layout follows AR.2 Calendar Workspace note: PageContainer, header, compact filter/search, calendar body, summary cards. +- Agenda is default manager/mobile-friendly dense view. +- Day/Week/Month views are available as foundation tabs. +- State coverage includes loading skeleton and empty state. +- Status is not color-only: badges show overdue/hot-project/read-only/category text. +- Date/time rendering uses `src/lib/date-format.ts`. +- UI avoids importing upstream shell, backend, provider, or mock data. + +## Known Follow-ups + +- Opportunity, Quotation, and Approval milestone projection adapters are contract-ready but not populated until those domains publish durable milestone events or rebuild source adapters are expanded. +- Drag/drop and resize UI actions are represented as Activity-only affordances, but direct mutation wiring is intentionally deferred until Activity reschedule API integration is added to Calendar UI. +- User/team filter contract exists; UI currently exposes lens/search foundation only. Manager team hierarchy should be wired after team graph/scope UX is formalized. +- Working hours and conflict detection are schema/service-ready concepts but need fuller UI and source preference integration in a follow-up slice. +- `INITIAL_PROJECTION_CONSUMERS` remains skeleton-safe for foundation runtime tests; CRM runtime composition should opt into `CRM_CALENDAR_PROJECTION_CONSUMERS` or combined CRM registration when running production projection workers. + +## Verification + +- `npm run typecheck` +- `node --disable-warning=ExperimentalWarning --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types --test src/features/crm/calendar/server/builder.test.ts src/features/crm/timeline/server/builder.test.ts src/features/foundation/projections/runtime.test.ts` +- `npx oxlint src/features/crm/calendar src/app/api/crm/calendar src/app/dashboard/crm/calendar src/features/foundation/projections/consumers.ts src/features/foundation/projections/registry.ts src/features/crm/timeline/server/registration.ts src/db/schema.ts` +- `npm run db:generate` + +## Outcome + +EP.1.6 now has a governed Calendar Projection and first Calendar Workspace foundation. Calendar remains additive, rebuildable, and non-authoritative. Activity remains the only editable operational work source; Timeline remains unchanged as historical chronology. diff --git a/drizzle/0006_massive_scalphunter.sql b/drizzle/0006_massive_scalphunter.sql new file mode 100644 index 0000000..bae89f0 --- /dev/null +++ b/drizzle/0006_massive_scalphunter.sql @@ -0,0 +1,46 @@ +CREATE TABLE "crm_calendar_projection" ( + "id" text PRIMARY KEY NOT NULL, + "organization_id" text NOT NULL, + "branch_id" text, + "product_type" text, + "customer_id" text, + "lead_id" text, + "opportunity_id" text, + "quotation_id" text, + "approval_id" text, + "activity_id" text, + "owner_id" text, + "assignee_id" text, + "event_id" text NOT NULL, + "entity_type" text NOT NULL, + "entity_id" text NOT NULL, + "title" text NOT NULL, + "summary" text, + "location" text, + "start_at" timestamp with time zone NOT NULL, + "end_at" timestamp with time zone NOT NULL, + "timezone" text DEFAULT 'Asia/Bangkok' NOT NULL, + "priority" text DEFAULT 'normal' NOT NULL, + "category" text NOT NULL, + "activity_type" text, + "editable" boolean DEFAULT false NOT NULL, + "milestone" boolean DEFAULT false NOT NULL, + "overdue" boolean DEFAULT false NOT NULL, + "hot_project" boolean DEFAULT false NOT NULL, + "all_day" boolean DEFAULT false NOT NULL, + "visibility" jsonb NOT NULL, + "metadata" jsonb, + "source_event" jsonb NOT NULL, + "source_projection_version" integer DEFAULT 1 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_calendar_projection_event_version_idx" ON "crm_calendar_projection" USING btree ("event_id","source_projection_version");--> statement-breakpoint +CREATE INDEX "crm_calendar_projection_org_start_idx" ON "crm_calendar_projection" USING btree ("organization_id","start_at");--> statement-breakpoint +CREATE INDEX "crm_calendar_projection_org_assignee_start_idx" ON "crm_calendar_projection" USING btree ("organization_id","assignee_id","start_at");--> statement-breakpoint +CREATE INDEX "crm_calendar_projection_org_owner_start_idx" ON "crm_calendar_projection" USING btree ("organization_id","owner_id","start_at");--> statement-breakpoint +CREATE INDEX "crm_calendar_projection_org_branch_start_idx" ON "crm_calendar_projection" USING btree ("organization_id","branch_id","start_at");--> statement-breakpoint +CREATE INDEX "crm_calendar_projection_org_category_start_idx" ON "crm_calendar_projection" USING btree ("organization_id","category","start_at");--> statement-breakpoint +CREATE INDEX "crm_calendar_projection_customer_start_idx" ON "crm_calendar_projection" USING btree ("organization_id","customer_id","start_at");--> statement-breakpoint +CREATE INDEX "crm_calendar_projection_activity_event_idx" ON "crm_calendar_projection" USING btree ("organization_id","activity_id","event_id"); \ No newline at end of file diff --git a/drizzle/meta/0006_snapshot.json b/drizzle/meta/0006_snapshot.json new file mode 100644 index 0000000..cba2b45 --- /dev/null +++ b/drizzle/meta/0006_snapshot.json @@ -0,0 +1,8233 @@ +{ + "id": "38e3eea8-f9ab-41f3-8ecd-ab454237238d", + "prevId": "e00210f9-6176-4489-9289-f616755bacfa", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.app_notification_deliveries": { + "name": "app_notification_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_id": { + "name": "notification_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_address": { + "name": "recipient_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_notification_dispatches": { + "name": "app_notification_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_key": { + "name": "provider_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "recipient_to": { + "name": "recipient_to", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "recipient_cc": { + "name": "recipient_cc", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "recipient_bcc": { + "name": "recipient_bcc", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reply_to": { + "name": "reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attachments": { + "name": "attachments", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recipient_address": { + "name": "recipient_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_retry_count": { + "name": "max_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_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": { + "app_notification_dispatches_org_event_channel_recipient_idx": { + "name": "app_notification_dispatches_org_event_channel_recipient_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipient_address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_notification_events": { + "name": "app_notification_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "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 + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_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": { + "app_notification_events_org_dedupe_idx": { + "name": "app_notification_events_org_dedupe_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dedupe_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_notification_templates": { + "name": "app_notification_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_template": { + "name": "subject_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title_template": { + "name": "title_template", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_template": { + "name": "body_template", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_format": { + "name": "body_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'plain'" + }, + "link_template": { + "name": "link_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'th'" + }, + "variables": { + "name": "variables", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "app_notification_templates_org_event_channel_idx": { + "name": "app_notification_templates_org_event_channel_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_notifications": { + "name": "app_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "link_url": { + "name": "link_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unread'" + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "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": { + "app_notifications_recipient_event_idx": { + "name": "app_notifications_recipient_event_idx", + "columns": [ + { + "expression": "recipient_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.business_event_outbox": { + "name": "business_event_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "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 + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "causation_id": { + "name": "causation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_envelope": { + "name": "event_envelope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "delivery_status": { + "name": "delivery_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dispatched_at": { + "name": "dispatched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failed_at": { + "name": "failed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dead_lettered_at": { + "name": "dead_lettered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_message": { + "name": "last_error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "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": { + "business_event_outbox_status_available_idx": { + "name": "business_event_outbox_status_available_idx", + "columns": [ + { + "expression": "delivery_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "business_event_outbox_claim_lease_idx": { + "name": "business_event_outbox_claim_lease_idx", + "columns": [ + { + "expression": "delivery_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claim_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "business_event_outbox_org_event_idx": { + "name": "business_event_outbox_org_event_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_activities": { + "name": "crm_activities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary_entity_type": { + "name": "primary_entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary_entity_id": { + "name": "primary_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "primary_record_label": { + "name": "primary_record_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_records": { + "name": "related_records", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lead_id": { + "name": "lead_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "opportunity_id": { + "name": "opportunity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quotation_id": { + "name": "quotation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activity_type": { + "name": "activity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_to_id": { + "name": "assigned_to_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_start_at": { + "name": "scheduled_start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scheduled_end_at": { + "name": "scheduled_end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "skipped_at": { + "name": "skipped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome_summary": { + "name": "outcome_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cancellation_reason": { + "name": "cancellation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "skip_reason": { + "name": "skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_action": { + "name": "next_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch_id": { + "name": "branch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_type": { + "name": "product_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_internal_only": { + "name": "is_internal_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_pricing_sensitive": { + "name": "is_pricing_sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "parent_activity_id": { + "name": "parent_activity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_approval_actions": { + "name": "crm_approval_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approval_request_id": { + "name": "approval_request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acted_by": { + "name": "acted_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acted_at": { + "name": "acted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_approval_escalation_policies": { + "name": "crm_approval_escalation_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "trigger_after_hours": { + "name": "trigger_after_hours", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_value": { + "name": "target_value", + "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_approval_matrices": { + "name": "crm_approval_matrices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_id": { + "name": "branch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_type": { + "name": "product_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "min_amount": { + "name": "min_amount", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_amount": { + "name": "max_amount", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_approval_reminder_policies": { + "name": "crm_approval_reminder_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sla_hours": { + "name": "sla_hours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 24 + }, + "reminder_offsets_hours": { + "name": "reminder_offsets_hours", + "type": "integer[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "calendar_mode": { + "name": "calendar_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'calendar_days'" + }, + "timeout_action": { + "name": "timeout_action", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "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_approval_reminder_policies_workflow_step_idx": { + "name": "crm_approval_reminder_policies_workflow_step_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_approval_requests": { + "name": "crm_approval_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_step": { + "name": "current_step", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "current_step_started_at": { + "name": "current_step_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_approval_steps": { + "name": "crm_approval_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role_code": { + "name": "role_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_name": { + "name": "role_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approval_mode": { + "name": "approval_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'sequential'" + }, + "is_required": { + "name": "is_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sla_hours": { + "name": "sla_hours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 24 + }, + "calendar_mode": { + "name": "calendar_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'calendar_days'" + }, + "timeout_action": { + "name": "timeout_action", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "crm_approval_steps_workflow_step_idx": { + "name": "crm_approval_steps_workflow_step_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_approval_workflows": { + "name": "crm_approval_workflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "crm_approval_workflows_org_code_idx": { + "name": "crm_approval_workflows_org_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_calendar_projection": { + "name": "crm_calendar_projection", + "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 + }, + "product_type": { + "name": "product_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lead_id": { + "name": "lead_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "opportunity_id": { + "name": "opportunity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quotation_id": { + "name": "quotation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approval_id": { + "name": "approval_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_id": { + "name": "assignee_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_id": { + "name": "event_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 + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_at": { + "name": "start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_at": { + "name": "end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Asia/Bangkok'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "activity_type": { + "name": "activity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "editable": { + "name": "editable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "milestone": { + "name": "milestone", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "overdue": { + "name": "overdue", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hot_project": { + "name": "hot_project", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "all_day": { + "name": "all_day", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "visibility": { + "name": "visibility", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_event": { + "name": "source_event", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "source_projection_version": { + "name": "source_projection_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "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": { + "crm_calendar_projection_event_version_idx": { + "name": "crm_calendar_projection_event_version_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_projection_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "crm_calendar_projection_org_start_idx": { + "name": "crm_calendar_projection_org_start_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "crm_calendar_projection_org_assignee_start_idx": { + "name": "crm_calendar_projection_org_assignee_start_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "crm_calendar_projection_org_owner_start_idx": { + "name": "crm_calendar_projection_org_owner_start_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "crm_calendar_projection_org_branch_start_idx": { + "name": "crm_calendar_projection_org_branch_start_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "branch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "crm_calendar_projection_org_category_start_idx": { + "name": "crm_calendar_projection_org_category_start_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "crm_calendar_projection_customer_start_idx": { + "name": "crm_calendar_projection_customer_start_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "crm_calendar_projection_activity_event_idx": { + "name": "crm_calendar_projection_activity_event_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "activity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_contact_shares": { + "name": "crm_contact_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared_to_user_id": { + "name": "shared_to_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared_by_user_id": { + "name": "shared_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared_at": { + "name": "shared_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "crm_contact_shares_org_contact_shared_user_idx": { + "name": "crm_contact_shares_org_contact_shared_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "shared_to_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_customer_contacts": { + "name": "crm_customer_contacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "department": { + "name": "department", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mobile": { + "name": "mobile", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_customer_owner_history": { + "name": "crm_customer_owner_history", + "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 + }, + "old_owner_user_id": { + "name": "old_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "new_owner_user_id": { + "name": "new_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changed_by": { + "name": "changed_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "changed_at": { + "name": "changed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "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 + }, + "customer_sub_group": { + "name": "customer_sub_group", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_assigned_at": { + "name": "owner_assigned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "owner_assigned_by": { + "name": "owner_assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_customers_org_code_idx": { + "name": "crm_customers_org_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_document_artifacts": { + "name": "crm_document_artifacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_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 + }, + "document_type": { + "name": "document_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artifact_type": { + "name": "artifact_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_version_id": { + "name": "template_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_provider": { + "name": "storage_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "generated_by": { + "name": "generated_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generated_at": { + "name": "generated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "locked_by": { + "name": "locked_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "voided_at": { + "name": "voided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "voided_by": { + "name": "voided_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "void_reason": { + "name": "void_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_document_libraries": { + "name": "crm_document_libraries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_type": { + "name": "document_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_type": { + "name": "product_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_document_libraries_org_code_idx": { + "name": "crm_document_libraries_org_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_document_library_versions": { + "name": "crm_document_library_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "library_id": { + "name": "library_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_provider": { + "name": "storage_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "page_count": { + "name": "page_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "published_by": { + "name": "published_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_by": { + "name": "archived_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_document_library_versions_library_version_idx": { + "name": "crm_document_library_versions_library_version_idx", + "columns": [ + { + "expression": "library_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_document_template_mappings": { + "name": "crm_document_template_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_version_id": { + "name": "template_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "placeholder_key": { + "name": "placeholder_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data_type": { + "name": "data_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sheet_name": { + "name": "sheet_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_value": { + "name": "default_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format_mask": { + "name": "format_mask", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "crm_document_template_mappings_version_placeholder_idx": { + "name": "crm_document_template_mappings_version_placeholder_idx", + "columns": [ + { + "expression": "template_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "placeholder_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_document_template_table_columns": { + "name": "crm_document_template_table_columns", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mapping_id": { + "name": "mapping_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "column_name": { + "name": "column_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_field": { + "name": "source_field", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "column_letter": { + "name": "column_letter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "format_mask": { + "name": "format_mask", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "crm_document_template_table_columns_mapping_name_idx": { + "name": "crm_document_template_table_columns_mapping_name_idx", + "columns": [ + { + "expression": "mapping_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "column_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_document_template_versions": { + "name": "crm_document_template_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_json": { + "name": "schema_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata_json": { + "name": "metadata_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "preview_image_url": { + "name": "preview_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_document_template_versions_template_version_idx": { + "name": "crm_document_template_versions_template_version_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_document_templates": { + "name": "crm_document_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_type": { + "name": "document_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_type": { + "name": "product_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_type": { + "name": "file_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_name": { + "name": "template_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_document_templates_org_doc_product_file_name_idx": { + "name": "crm_document_templates_org_doc_product_file_name_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "product_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_leads": { + "name": "crm_leads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_id": { + "name": "branch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_location": { + "name": "project_location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lead_channel": { + "name": "lead_channel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_type": { + "name": "product_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "estimated_value": { + "name": "estimated_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "awareness_id": { + "name": "awareness_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "followup_status": { + "name": "followup_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lost_reason": { + "name": "lost_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "owner_marketing_user_id": { + "name": "owner_marketing_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_sales_owner_id": { + "name": "assigned_sales_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignment_remark": { + "name": "assignment_remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_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()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "crm_leads_org_code_idx": { + "name": "crm_leads_org_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_opportunities": { + "name": "crm_opportunities", + "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 + }, + "lead_id": { + "name": "lead_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requirement": { + "name": "requirement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_location": { + "name": "project_location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_type": { + "name": "product_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome_status": { + "name": "outcome_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lead_channel": { + "name": "lead_channel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "estimated_value": { + "name": "estimated_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "chance_percent": { + "name": "chance_percent", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expected_close_date": { + "name": "expected_close_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "project_close_date": { + "name": "project_close_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delivery_date": { + "name": "delivery_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "competitor": { + "name": "competitor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_hot_project": { + "name": "is_hot_project", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hot_project_auto_suggested": { + "name": "hot_project_auto_suggested", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hot_project_manually_overridden": { + "name": "hot_project_manually_overridden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "pipeline_stage": { + "name": "pipeline_stage", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'lead'" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closed_won_at": { + "name": "closed_won_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closed_lost_at": { + "name": "closed_lost_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closed_by_user_id": { + "name": "closed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "po_number": { + "name": "po_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "po_date": { + "name": "po_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "po_amount": { + "name": "po_amount", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "po_currency": { + "name": "po_currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lost_reason": { + "name": "lost_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lost_detail": { + "name": "lost_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lost_competitor": { + "name": "lost_competitor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lost_remark": { + "name": "lost_remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cancel_reason": { + "name": "cancel_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "no_quotation_reason": { + "name": "no_quotation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_to_user_id": { + "name": "assigned_to_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignment_remark": { + "name": "assignment_remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_opportunities_org_code_idx": { + "name": "crm_opportunities_org_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_opportunity_attachments": { + "name": "crm_opportunity_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opportunity_id": { + "name": "opportunity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_file_name": { + "name": "original_file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_provider": { + "name": "storage_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_type": { + "name": "file_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_opportunity_customers": { + "name": "crm_opportunity_customers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opportunity_id": { + "name": "opportunity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_opportunity_followups": { + "name": "crm_opportunity_followups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opportunity_id": { + "name": "opportunity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "followup_date": { + "name": "followup_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "followup_type": { + "name": "followup_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_followup_date": { + "name": "next_followup_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_action": { + "name": "next_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotation_attachments": { + "name": "crm_quotation_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quotation_id": { + "name": "quotation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_file_name": { + "name": "original_file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_type": { + "name": "file_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotation_customers": { + "name": "crm_quotation_customers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quotation_id": { + "name": "quotation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotation_followups": { + "name": "crm_quotation_followups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quotation_id": { + "name": "quotation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "followup_date": { + "name": "followup_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "followup_type": { + "name": "followup_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_followup_date": { + "name": "next_followup_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_action": { + "name": "next_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotation_items": { + "name": "crm_quotation_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quotation_id": { + "name": "quotation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_number": { + "name": "item_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "product_type": { + "name": "product_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit_price": { + "name": "unit_price", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "discount": { + "name": "discount", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "discount_type": { + "name": "discount_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tax_rate": { + "name": "tax_rate", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_price": { + "name": "total_price", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotation_topic_items": { + "name": "crm_quotation_topic_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic_id": { + "name": "topic_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotation_topics": { + "name": "crm_quotation_topics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quotation_id": { + "name": "quotation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic_type": { + "name": "topic_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotations": { + "name": "crm_quotations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_id": { + "name": "branch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opportunity_id": { + "name": "opportunity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quotation_date": { + "name": "quotation_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "valid_until": { + "name": "valid_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "quotation_type": { + "name": "quotation_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_location": { + "name": "project_location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attention": { + "name": "attention", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "parent_quotation_id": { + "name": "parent_quotation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision_remark": { + "name": "revision_remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "exchange_rate": { + "name": "exchange_rate", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "subtotal": { + "name": "subtotal", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "discount": { + "name": "discount", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "discount_type": { + "name": "discount_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tax_rate": { + "name": "tax_rate", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tax_amount": { + "name": "tax_amount", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_amount": { + "name": "total_amount", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "chance_percent": { + "name": "chance_percent", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "project_close_date": { + "name": "project_close_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delivery_date": { + "name": "delivery_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "is_hot_project": { + "name": "is_hot_project", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hot_project_auto_suggested": { + "name": "hot_project_auto_suggested", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hot_project_manually_overridden": { + "name": "hot_project_manually_overridden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "competitor": { + "name": "competitor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "salesman_id": { + "name": "salesman_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_sent": { + "name": "is_sent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_via": { + "name": "sent_via", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "approved_artifact_id": { + "name": "approved_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_pdf_url": { + "name": "approved_pdf_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_snapshot": { + "name": "approved_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "approved_template_version_id": { + "name": "approved_template_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rejected_at": { + "name": "rejected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_quotations_org_code_idx": { + "name": "crm_quotations_org_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_report_definitions": { + "name": "crm_report_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "crm_report_definitions_org_code_idx": { + "name": "crm_report_definitions_org_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_role_profiles": { + "name": "crm_role_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "branch_scope_mode": { + "name": "branch_scope_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'assigned'" + }, + "product_scope_mode": { + "name": "product_scope_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'assigned'" + }, + "ownership_scope": { + "name": "ownership_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'own'" + }, + "approval_authority": { + "name": "approval_authority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_role_profiles_org_code_idx": { + "name": "crm_role_profiles_org_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_timeline_projection": { + "name": "crm_timeline_projection", + "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 + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lead_id": { + "name": "lead_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "opportunity_id": { + "name": "opportunity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quotation_id": { + "name": "quotation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_id": { + "name": "event_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 + }, + "timeline_type": { + "name": "timeline_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timeline_category": { + "name": "timeline_category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_display": { + "name": "actor_display", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "visibility": { + "name": "visibility", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_event": { + "name": "source_event", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "source_projection_version": { + "name": "source_projection_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "crm_timeline_projection_event_version_idx": { + "name": "crm_timeline_projection_event_version_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_projection_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "crm_timeline_projection_org_occurred_idx": { + "name": "crm_timeline_projection_org_occurred_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "crm_timeline_projection_customer_occurred_idx": { + "name": "crm_timeline_projection_customer_occurred_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "crm_timeline_projection_activity_event_idx": { + "name": "crm_timeline_projection_activity_event_idx", + "columns": [ + { + "expression": "activity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_user_role_assignments": { + "name": "crm_user_role_assignments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_profile_id": { + "name": "role_profile_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_scope_mode": { + "name": "branch_scope_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'inherit'" + }, + "branch_scope_ids": { + "name": "branch_scope_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "product_type_scope_mode": { + "name": "product_type_scope_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'inherit'" + }, + "product_type_scope_ids": { + "name": "product_type_scope_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "crm_user_role_assignments_org_user_role_idx": { + "name": "crm_user_role_assignments_org_user_role_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_profile_id", + "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": "''" + }, + "product_type": { + "name": "product_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "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 + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{prefix}{period}-{running}'" + }, + "reset_policy": { + "name": "reset_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'period'" + }, + "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_branch_product_doc_period_idx": { + "name": "document_sequences_org_branch_product_doc_period_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "branch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "product_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_configurations": { + "name": "email_configurations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'smtp'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 587 + }, + "secure": { + "name": "secure", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_encrypted": { + "name": "password_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reply_to_email": { + "name": "reply_to_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_test_status": { + "name": "last_test_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not_tested'" + }, + "last_tested_at": { + "name": "last_tested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_test_error": { + "name": "last_test_error", + "type": "text", + "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 + }, + "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": { + "email_configurations_org_name_idx": { + "name": "email_configurations_org_name_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "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": "'sales_support'" + }, + "permissions": { + "name": "permissions", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "branch_scope_ids": { + "name": "branch_scope_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "product_type_scope_ids": { + "name": "product_type_scope_ids", + "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.projection_consumer_checkpoints": { + "name": "projection_consumer_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer_name": { + "name": "consumer_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projection_name": { + "name": "projection_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_attempted_at": { + "name": "first_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempted_at": { + "name": "last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_duration_ms": { + "name": "processing_duration_ms", + "type": "integer", + "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": { + "projection_checkpoints_consumer_event_idx": { + "name": "projection_checkpoints_consumer_event_idx", + "columns": [ + { + "expression": "consumer_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "projection_checkpoints_status_retry_idx": { + "name": "projection_checkpoints_status_retry_idx", + "columns": [ + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_retry_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projection_dead_letters": { + "name": "projection_dead_letters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer_name": { + "name": "consumer_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projection_name": { + "name": "projection_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "failure_metadata": { + "name": "failure_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_reason": { + "name": "resolution_reason", + "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": { + "projection_dead_letters_consumer_event_idx": { + "name": "projection_dead_letters_consumer_event_idx", + "columns": [ + { + "expression": "consumer_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "projection_dead_letters_org_status_idx": { + "name": "projection_dead_letters_org_status_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projection_health_snapshots": { + "name": "projection_health_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projection_name": { + "name": "projection_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer_name": { + "name": "consumer_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'healthy'" + }, + "last_processed_event_id": { + "name": "last_processed_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_successful_processing_at": { + "name": "last_successful_processing_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pending_count": { + "name": "pending_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "dead_letter_count": { + "name": "dead_letter_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "unsupported_version_count": { + "name": "unsupported_version_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_lag_ms": { + "name": "processing_lag_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "average_processing_duration_ms": { + "name": "average_processing_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_rebuild_at": { + "name": "last_rebuild_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rebuild_source": { + "name": "rebuild_source", + "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": { + "projection_health_org_consumer_idx": { + "name": "projection_health_org_consumer_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "consumer_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projection_rebuild_runs": { + "name": "projection_rebuild_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projection_name": { + "name": "projection_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "occurred_from": { + "name": "occurred_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "occurred_to": { + "name": "occurred_to", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "reset_existing": { + "name": "reset_existing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "source_strategy": { + "name": "source_strategy", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "projection_rebuild_runs_org_projection_idx": { + "name": "projection_rebuild_runs_org_projection_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "projection_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.seed_manifest_items": { + "name": "seed_manifest_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "seed_manifest_id": { + "name": "seed_manifest_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_key": { + "name": "item_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_file": { + "name": "source_file", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "imported_count": { + "name": "imported_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_count": { + "name": "updated_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_count": { + "name": "error_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "warning_count": { + "name": "warning_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "report_json": { + "name": "report_json", + "type": "jsonb", + "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": { + "seed_manifest_items_manifest_item_idx": { + "name": "seed_manifest_items_manifest_item_idx", + "columns": [ + { + "expression": "seed_manifest_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.seed_manifests": { + "name": "seed_manifests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "setup_run_id": { + "name": "setup_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_by": { + "name": "applied_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "report_json": { + "name": "report_json", + "type": "jsonb", + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.setup_run_steps": { + "name": "setup_run_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "setup_run_id": { + "name": "setup_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_key": { + "name": "step_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not_started'" + }, + "input_hash": { + "name": "input_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output_json": { + "name": "output_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "errors_json": { + "name": "errors_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "warnings_json": { + "name": "warnings_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "setup_run_steps_run_step_idx": { + "name": "setup_run_steps_run_step_idx", + "columns": [ + { + "expression": "setup_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.setup_runs": { + "name": "setup_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not_started'" + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'production'" + }, + "target_organization_id": { + "name": "target_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_by": { + "name": "started_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completed_by": { + "name": "completed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tr_audit_logs": { + "name": "tr_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_id": { + "name": "branch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "before_data": { + "name": "before_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_data": { + "name": "after_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "system_role": { + "name": "system_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index b4a1a2f..01ade9f 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1783925810194, "tag": "0005_freezing_wendigo", "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1783931878587, + "tag": "0006_massive_scalphunter", + "breakpoints": true } ] } \ No newline at end of file diff --git a/plans/task-ep.1.6.md b/plans/task-ep.1.6.md new file mode 100644 index 0000000..a444f44 --- /dev/null +++ b/plans/task-ep.1.6.md @@ -0,0 +1,844 @@ +# Task EP.1.6 – Calendar Projection, Workspace & Big Calendar Foundation + +**Status:** Foundation Implemented + +**Priority:** Critical + +**Type:** Projection Platform / Workspace / UI Foundation + +--- + +# Depends On + +* EP.1.1 Activity Domain Foundation +* EP.1.2 Activity Integration & Legacy Follow-up Adapter +* EP.1.3 Business Event Foundation +* EP.1.4 Projection Foundation & Delivery Reliability +* EP.1.4.1 Transactional Outbox Activation & Worker Runtime +* EP.1.5 Timeline Projection Foundation +* BU-R.0 Business Blueprint Freeze +* BU-R.0.1 Workspace & Activity Blueprint +* BU-R.1 Business Capability Audit +* AR.1 Architecture Transition Plan +* AR.2 Epic Technical Design +* AR.2 Workspace UI/UX Design Note +* ENG.0 Engineering Constitution + +--- + +# Objective + +Introduce the first production Calendar Workspace of ALLA OS by combining: + +* Calendar Projection +* Calendar Workspace +* Big Calendar UI Foundation +* Activity scheduling +* Business milestones +* Team visibility +* Calendar interaction + +Calendar becomes the operational scheduling workspace for CRM while preserving existing source-domain ownership. + +Calendar is **NOT**: + +* a source-of-truth +* another Activity module +* another Opportunity module +* another Quotation module + +Calendar is a governed projection and operational workspace. + +--- + +# Business Outcome + +After EP.1.6 users can answer: + +* What should I do today? +* Which customers should I visit? +* Which follow-ups are overdue? +* Which quotations are expiring? +* Which projects are Hot Projects? +* Which approvals are waiting? +* Which customer visits are scheduled? +* What is my team's workload today? +* What are tomorrow's priorities? + +Managers gain a Team Calendar without creating duplicate business data. + +--- + +# Calendar Business Principles + +## Calendar is Projection + +Calendar is generated from: + +* Activity +* Business Events +* Opportunity milestones +* Quotation milestones +* Approval milestones +* Legacy adapters during rebuild + +Calendar rows are rebuildable. + +--- + +## Activity Owns Operational Work + +Editable Calendar events always belong to Activity. + +Examples: + +* Follow-up +* Meeting +* Site Survey +* Visit +* Internal Task +* Phone Call +* Reminder + +Calendar actions must call Activity Services. + +Calendar never edits projection rows directly. + +--- + +## Milestones Remain Source-Owned + +Read-only Calendar milestones include: + +* Expected Award Date +* Expected Delivery Date +* Quotation Expiration +* Approval Due +* Approval Escalation +* PO Milestones + +Editing must always occur through the owning domain. + +--- + +## Calendar ≠ Timeline + +Timeline answers: + +> What happened? + +Calendar answers: + +> What must happen? + +Both consume Business Events but serve different business purposes. + +--- + +# UI Foundation Decision + +The Calendar UI must adopt: + +**lramos33/big-calendar** + +Repository: + +```text +https://github.com/lramos33/big-calendar +``` + +This repository becomes the Calendar UI foundation only. + +It does **NOT** become: + +* Calendar database +* Calendar backend +* Activity service +* Projection runtime +* CRM authorization +* Source-of-truth + +The governed architecture remains: + +```text +Activity / Opportunity / Quotation / Approval + ↓ + Business Events + ↓ + Projection Runtime + ↓ +crm_calendar_projection + ↓ +Calendar Adapter + ↓ +lramos33/big-calendar +``` + +--- + +# Review Required + +## Governance + +Review: + +* AGENTS.md +* Engineering Constitution +* Project Foundations +* Architecture Rules +* UI UX Rules +* Task Review Checklist +* CRM Authorization Boundaries +* LAYOUT.md + +## Business + +Review: + +* Relationship Sales Workspace Blueprint +* BU-R.1 + +## Architecture + +Review: + +* AR.1 +* AR.2 +* Workspace UI UX Design Note + +## Existing Runtime + +Review: + +* Projection Runtime +* Worker Runtime +* Timeline Projection +* Activity Service +* Business Event Registry +* Calendar-related Activity scheduling +* Date formatting utilities +* CRM security + +## UI Review + +Must use: + +* ui-ux-pro-max +* LAYOUT.md +* ui-ux-rules.md + +No code-first Calendar implementation is allowed. + +--- + +# Scope + +# Part 1 — Calendar Projection Schema + +Introduce: + +```text +crm_calendar_projection +``` + +Containing: + +* organization +* branch +* product +* activity +* customer +* lead +* opportunity +* quotation +* approval +* owner +* assignee +* title +* summary +* location +* start +* end +* timezone +* priority +* category +* editable +* milestone +* overdue +* hot project +* projection version +* source event +* metadata + +Projection rows remain rebuildable. + +--- + +# Part 2 — Calendar Projection Builder + +Implement + +CalendarProjectionBuilder + +Responsibilities + +* Business Event mapping +* milestone mapping +* display enrichment +* permission metadata +* icon registry +* urgency calculation +* projection persistence + +Builder never mutates source domains. + +--- + +# Part 3 — Activity Event Mapping + +Support + +* activity.created +* activity.assigned +* activity.reassigned +* activity.started +* activity.rescheduled +* activity.completed +* activity.cancelled +* activity.deleted + +Projection updates must be incremental through Business Events. + +--- + +# Part 4 — Milestone Projection + +Project: + +Opportunity + +* Expected Award Date +* Expected Delivery Date +* Hot Project review + +Quotation + +* Valid Until +* Delivery milestone + +Approval + +* Due +* Escalation + +Milestones are read-only. + +--- + +# Part 5 — Calendar Projection Consumer + +Activate Calendar consumer through Projection Runtime. + +Must support: + +* checkpoint +* retry +* dead letter +* rebuild +* idempotency + +--- + +# Part 6 — Calendar Query Service + +Provide secured query service. + +Filters: + +* date range +* user +* team +* branch +* product +* customer +* lead +* opportunity +* quotation +* activity type +* category +* priority +* overdue +* milestone +* Hot Project +* search + +Supported lenses: + +* Personal +* Team +* Manager +* Executive + +--- + +# Part 7 — Calendar Rebuild + +Hybrid rebuild. + +Initial build: + +* Activity +* Opportunity +* Quotation +* Approval + +Incremental: + +* Business Events + +Support: + +* organization +* customer +* opportunity +* quotation +* date range +* dry run +* reset existing +* restart + +--- + +# Part 8 — Calendar Workspace + +Route: + +```text +/dashboard/crm/calendar +``` + +Views: + +* Day +* Week +* Month +* Agenda + +Agenda is default for: + +* mobile +* manager +* high-density work + +--- + +# Part 9 — Big Calendar Source Audit + +Before implementation: + +Audit: + +* dependency compatibility +* React compatibility +* Next.js compatibility +* shadcn compatibility +* Tailwind compatibility +* accessibility +* dark mode +* timezone assumptions +* drag/drop libraries +* demo code +* mock data + +Document: + +* files adopted +* files rejected +* modification strategy + +--- + +# Part 10 — Big Calendar Adaptation + +Do **NOT** import the whole repository. + +Only adapt: + +* Calendar Views +* Toolbar +* Event Rendering +* Drag & Drop +* Resize +* Navigation +* User Filter +* Working Hours + +Move adapted source into: + +```text +src/features/crm/calendar/ +``` + +Maintain ALLA OS architecture. + +--- + +# Part 11 — Calendar Adapter Layer + +Introduce + +CalendarProjectionItem + +↓ + +BigCalendarEvent + +Implement + +```ts +mapCalendarProjectionToBigCalendarEvent() +``` + +UI must never consume database rows directly. + +--- + +# Part 12 — Calendar Rendering Strategy + +Differentiate: + +## Editable Activity + +Examples + +* Follow-up +* Meeting +* Visit +* Site Survey + +Supports + +* Complete +* Reschedule +* Cancel +* Open +* Reassign + +--- + +## Read-only Milestone + +Examples + +* Expected Award +* Expected Delivery +* Approval Due +* Quotation Expiration + +Supports + +* Open Source +* View Detail +* Create Related Activity + +No editing. + +--- + +# Part 13 — Drag and Drop + +For Activities + +Drag + +↓ + +Activity API + +↓ + +Business Event + +↓ + +Projection Update + +↓ + +Calendar Refresh + +No direct Calendar persistence. + +Milestones cannot be dragged. + +--- + +# Part 14 — User Filter + +Integrate upstream user filter with: + +* CRM permissions +* team hierarchy +* ownership +* manager scope + +Never load unauthorized users. + +--- + +# Part 15 — Working Hours + +Support + +* organization defaults +* user preferences +* Bangkok timezone +* all-day events +* current time +* business hours + +--- + +# Part 16 — Schedule Conflict Detection + +Detect + +* overlapping visits +* meetings +* site surveys +* tasks + +Read-only advisory. + +No blocking. + +--- + +# Part 17 — Calendar Summary + +Display + +* Due Today +* Overdue +* Meetings +* Visits +* Milestones +* Approval Due +* Hot Projects + +Derived from Calendar projection. + +--- + +# Part 18 — Calendar Performance + +Support + +* indexes +* bounded queries +* lazy loading +* virtual agenda +* cursor pagination +* projection batching + +No N+1. + +--- + +# Part 19 — Projection Health + +Track + +* lag +* retries +* dead letters +* row count +* stale rows +* rebuild status + +--- + +# Part 20 — Calendar Delivery Matrix + +Document + +Source + +↓ + +Business Event + +↓ + +Projection + +↓ + +Calendar Item + +↓ + +Workspace + +Include: + +* Activity +* Opportunity +* Quotation +* Approval + +--- + +# Part 21 — Calendar UI / UX Review + +Review with + +* ui-ux-pro-max +* LAYOUT.md +* ui-ux-rules.md + +Validate + +* hierarchy +* spacing +* typography +* icons +* filters +* density +* accessibility +* dark mode +* responsiveness +* keyboard +* loading +* empty state + +--- + +# Part 22 — Upstream License + +Retain + +MIT License + +Document + +* adopted files +* attribution +* modification history + +--- + +# Deliverables + +1. Calendar Projection Schema +2. Calendar Builder +3. Calendar Projection Consumer +4. Calendar Query Service +5. Calendar Rebuild +6. Calendar Workspace +7. Day / Week / Month / Agenda +8. Big Calendar Source Audit +9. Big Calendar Adaptation +10. Calendar Adapter Layer +11. Activity Calendar Actions +12. Milestone Projection +13. Drag & Drop Integration +14. User Filter Integration +15. Working Hours Support +16. Schedule Conflict Detection +17. Calendar Summary Panels +18. Projection Health +19. Calendar Delivery Matrix +20. UI/UX Review Report +21. Upstream License & Attribution Report +22. EP.1.7 Readiness Report + +--- + +# Constraints + +Must preserve: + +* Activity ownership +* Opportunity ownership +* Quotation ownership +* Approval ownership +* Timeline +* Dashboard +* Reports +* Notifications +* Audit Log +* CRM security +* Projection Runtime + +Do NOT implement: + +* Google Calendar Sync +* Outlook Sync +* recurring engine +* My Day +* Manager Workspace +* Executive Workspace +* Relationship Health +* Forecast +* automatic Activity generation +* legacy migration + +Calendar remains an additive projection. + +--- + +# Testing + +* Projection +* Builder +* Consumer +* Rebuild +* Activity Events +* Milestones +* Query +* Permissions +* Drag & Drop +* Resize +* Agenda +* Day +* Week +* Month +* Timezone +* Conflict Detection +* UI +* Accessibility +* Dark Mode +* Performance +* Compatibility + +--- + +# Acceptance Criteria + +* Calendar Projection is fully rebuildable. +* Calendar uses Business Events for incremental updates. +* Activity remains the only editable operational work source. +* Opportunity, Quotation, and Approval milestones are projected as read-only Calendar items. +* `lramos33/big-calendar` is successfully adopted as the UI foundation without importing its application shell or backend. +* A dedicated adapter maps `CalendarProjectionItem` to the Calendar UI event model. +* Drag-and-drop and resize update Activities only through Activity Services and Business Events. +* Read-only milestones cannot be dragged or resized. +* Personal, Team, Manager, and Executive lenses enforce existing CRM authorization. +* Day, Week, Month, and Agenda views are available, with Agenda optimized for managers and mobile. +* Working hours, timezone, and schedule conflict detection behave consistently. +* Calendar integrates with the Projection Runtime (checkpoint, retry, rebuild, health). +* UI conforms to `LAYOUT.md`, `ui-ux-rules.md`, and `ui-ux-pro-max`. +* Existing Activity, Timeline, Dashboard, Reports, Notifications, Approval flows, and Follow-up APIs remain unchanged. + +--- + +# Success Criteria + +ALLA OS gains a production-ready Calendar Workspace built on a governed Projection architecture. + +Users can plan and execute daily operational work from a single Calendar while preserving Activity, Opportunity, Quotation, and Approval as the authoritative business domains. + +The Calendar foundation is ready for: + +**EP.1.7 – My Day Workspace Foundation** diff --git a/src/app/api/crm/calendar/route.ts b/src/app/api/crm/calendar/route.ts new file mode 100644 index 0000000..40ce79d --- /dev/null +++ b/src/app/api/crm/calendar/route.ts @@ -0,0 +1,100 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { CRM_ACTIVITY_PERMISSIONS } from '@/features/crm/activity/types'; +import { buildCrmSecurityContext } from '@/features/crm/security/server/service'; +import { listCalendarItems } from '@/features/crm/calendar/server/service'; +import { AuthError, requireOrganizationAccess } from '@/lib/auth/session'; + +const calendarQuerySchema = z.object({ + startFrom: z.string().datetime(), + startTo: z.string().datetime(), + lens: z.enum(['personal', 'team', 'manager', 'executive']).optional(), + userId: z.string().optional(), + teamUserIds: z + .string() + .optional() + .transform((value) => value?.split(',').filter(Boolean)), + branchId: z.string().optional(), + productType: z.string().optional(), + customerId: z.string().optional(), + leadId: z.string().optional(), + opportunityId: z.string().optional(), + quotationId: z.string().optional(), + activityType: z.string().optional(), + category: z + .enum([ + 'activity', + 'meeting', + 'visit', + 'site_survey', + 'task', + 'reminder', + 'opportunity_milestone', + 'quotation_milestone', + 'approval_milestone' + ]) + .optional(), + priority: z.enum(['low', 'normal', 'high', 'critical']).optional(), + overdue: z.coerce.boolean().optional(), + milestone: z.coerce.boolean().optional(), + hotProject: z.coerce.boolean().optional(), + search: z.string().optional(), + limit: z.coerce.number().int().positive().max(500).optional(), + cursor: z.string().optional() +}); + +export async function GET(request: NextRequest) { + try { + const { organization, session, membership } = await requireOrganizationAccess({ + permission: CRM_ACTIVITY_PERMISSIONS.read + }); + const query = calendarQuerySchema.parse({ + startFrom: request.nextUrl.searchParams.get('startFrom') ?? undefined, + startTo: request.nextUrl.searchParams.get('startTo') ?? undefined, + lens: request.nextUrl.searchParams.get('lens') ?? undefined, + userId: request.nextUrl.searchParams.get('userId') ?? undefined, + teamUserIds: request.nextUrl.searchParams.get('teamUserIds') ?? undefined, + branchId: request.nextUrl.searchParams.get('branchId') ?? undefined, + productType: request.nextUrl.searchParams.get('productType') ?? undefined, + customerId: request.nextUrl.searchParams.get('customerId') ?? undefined, + leadId: request.nextUrl.searchParams.get('leadId') ?? undefined, + opportunityId: request.nextUrl.searchParams.get('opportunityId') ?? undefined, + quotationId: request.nextUrl.searchParams.get('quotationId') ?? undefined, + activityType: request.nextUrl.searchParams.get('activityType') ?? undefined, + category: request.nextUrl.searchParams.get('category') ?? undefined, + priority: request.nextUrl.searchParams.get('priority') ?? undefined, + overdue: request.nextUrl.searchParams.get('overdue') ?? undefined, + milestone: request.nextUrl.searchParams.get('milestone') ?? undefined, + hotProject: request.nextUrl.searchParams.get('hotProject') ?? undefined, + search: request.nextUrl.searchParams.get('search') ?? undefined, + limit: request.nextUrl.searchParams.get('limit') ?? undefined, + cursor: request.nextUrl.searchParams.get('cursor') ?? undefined + }); + const context = buildCrmSecurityContext({ + organizationId: organization.id, + userId: session.user.id, + membership + }); + + const response = await listCalendarItems( + { + ...query, + organizationId: organization.id + }, + context + ); + + return NextResponse.json(response); + } catch (error) { + if (error instanceof AuthError) { + return NextResponse.json({ message: error.message }, { status: error.status }); + } + + if (error instanceof z.ZodError) { + return NextResponse.json({ message: 'Invalid calendar filters' }, { status: 400 }); + } + + console.error('[crm/calendar] failed to list calendar projection', error); + return NextResponse.json({ message: 'Failed to load calendar' }, { status: 500 }); + } +} diff --git a/src/app/dashboard/crm/calendar/page.tsx b/src/app/dashboard/crm/calendar/page.tsx new file mode 100644 index 0000000..c5c62a0 --- /dev/null +++ b/src/app/dashboard/crm/calendar/page.tsx @@ -0,0 +1,40 @@ +import { HydrationBoundary, dehydrate } from '@tanstack/react-query'; +import { Suspense } from 'react'; +import PageContainer from '@/components/layout/page-container'; +import { + CalendarWorkspace, + CalendarWorkspaceSkeleton +} from '@/features/crm/calendar/components/calendar-workspace'; +import { calendarItemsQueryOptions } from '@/features/crm/calendar/api/queries'; +import { getQueryClient } from '@/lib/query-client'; + +function defaultCalendarRange() { + const now = new Date(); + const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1, 0, 0, 0)); + const end = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0, 23, 59, 59)); + return { + startFrom: start.toISOString(), + startTo: end.toISOString(), + lens: 'personal' as const, + limit: 300 + }; +} + +export default function CrmCalendarPage() { + const queryClient = getQueryClient(); + const filters = defaultCalendarRange(); + void queryClient.prefetchQuery(calendarItemsQueryOptions(filters)); + + return ( + + + }> + + + + + ); +} diff --git a/src/config/nav-config.ts b/src/config/nav-config.ts index f323827..1a66eab 100644 --- a/src/config/nav-config.ts +++ b/src/config/nav-config.ts @@ -12,6 +12,14 @@ import { NavGroup } from "@/types"; items: [], access: { requireOrg: true, permission: "crm.dashboard.read" }, }, + { + title: "Calendar", + url: "/dashboard/crm/calendar", + icon: "calendar", + isActive: false, + items: [], + access: { requireOrg: true, permission: "crm.activity.read" }, + }, { title: "ลูกค้า", url: "/dashboard/crm/customers", diff --git a/src/db/schema.ts b/src/db/schema.ts index dce9162..c637c66 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -676,6 +676,87 @@ export const crmTimelineProjection = pgTable( }) ); +export const crmCalendarProjection = pgTable( + 'crm_calendar_projection', + { + id: text('id').primaryKey(), + organizationId: text('organization_id').notNull(), + branchId: text('branch_id'), + productType: text('product_type'), + customerId: text('customer_id'), + leadId: text('lead_id'), + opportunityId: text('opportunity_id'), + quotationId: text('quotation_id'), + approvalId: text('approval_id'), + activityId: text('activity_id'), + ownerId: text('owner_id'), + assigneeId: text('assignee_id'), + eventId: text('event_id').notNull(), + entityType: text('entity_type').notNull(), + entityId: text('entity_id').notNull(), + title: text('title').notNull(), + summary: text('summary'), + location: text('location'), + startAt: timestamp('start_at', { withTimezone: true }).notNull(), + endAt: timestamp('end_at', { withTimezone: true }).notNull(), + timezone: text('timezone').default('Asia/Bangkok').notNull(), + priority: text('priority').default('normal').notNull(), + category: text('category').notNull(), + activityType: text('activity_type'), + editable: boolean('editable').default(false).notNull(), + milestone: boolean('milestone').default(false).notNull(), + overdue: boolean('overdue').default(false).notNull(), + hotProject: boolean('hot_project').default(false).notNull(), + allDay: boolean('all_day').default(false).notNull(), + visibility: jsonb('visibility').notNull(), + metadata: jsonb('metadata'), + sourceEvent: jsonb('source_event').notNull(), + sourceProjectionVersion: integer('source_projection_version').default(1).notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() + }, + (table) => ({ + eventVersionIdx: uniqueIndex('crm_calendar_projection_event_version_idx').on( + table.eventId, + table.sourceProjectionVersion + ), + organizationStartIdx: index('crm_calendar_projection_org_start_idx').on( + table.organizationId, + table.startAt + ), + organizationAssigneeStartIdx: index('crm_calendar_projection_org_assignee_start_idx').on( + table.organizationId, + table.assigneeId, + table.startAt + ), + organizationOwnerStartIdx: index('crm_calendar_projection_org_owner_start_idx').on( + table.organizationId, + table.ownerId, + table.startAt + ), + organizationBranchStartIdx: index('crm_calendar_projection_org_branch_start_idx').on( + table.organizationId, + table.branchId, + table.startAt + ), + organizationCategoryStartIdx: index('crm_calendar_projection_org_category_start_idx').on( + table.organizationId, + table.category, + table.startAt + ), + customerStartIdx: index('crm_calendar_projection_customer_start_idx').on( + table.organizationId, + table.customerId, + table.startAt + ), + activityEventIdx: index('crm_calendar_projection_activity_event_idx').on( + table.organizationId, + table.activityId, + table.eventId + ) + }) +); + export const crmCustomers = pgTable( 'crm_customers', { diff --git a/src/features/crm/calendar/api/queries.ts b/src/features/crm/calendar/api/queries.ts new file mode 100644 index 0000000..7643d85 --- /dev/null +++ b/src/features/crm/calendar/api/queries.ts @@ -0,0 +1,18 @@ +import { queryOptions } from '@tanstack/react-query'; +import { getCalendarItems } from './service'; +import type { CalendarListFilters } from './types'; + +export const calendarKeys = { + all: ['crm-calendar'] as const, + lists: () => [...calendarKeys.all, 'list'] as const, + list: (filters: Omit) => + [...calendarKeys.lists(), filters] as const +}; + +export const calendarItemsQueryOptions = ( + filters: Omit +) => + queryOptions({ + queryKey: calendarKeys.list(filters), + queryFn: () => getCalendarItems(filters) + }); diff --git a/src/features/crm/calendar/api/service.ts b/src/features/crm/calendar/api/service.ts new file mode 100644 index 0000000..da6078d --- /dev/null +++ b/src/features/crm/calendar/api/service.ts @@ -0,0 +1,34 @@ +import { apiClient } from '@/lib/api-client'; +import type { CalendarListFilters, CalendarListResponse } from './types'; + +function buildSearchParams(filters: Omit): string { + const params = new URLSearchParams(); + params.set('startFrom', filters.startFrom); + params.set('startTo', filters.startTo); + if (filters.lens) params.set('lens', filters.lens); + if (filters.userId) params.set('userId', filters.userId); + if (filters.teamUserIds?.length) params.set('teamUserIds', filters.teamUserIds.join(',')); + if (filters.branchId) params.set('branchId', filters.branchId); + if (filters.productType) params.set('productType', filters.productType); + if (filters.customerId) params.set('customerId', filters.customerId); + if (filters.leadId) params.set('leadId', filters.leadId); + if (filters.opportunityId) params.set('opportunityId', filters.opportunityId); + if (filters.quotationId) params.set('quotationId', filters.quotationId); + if (filters.activityType) params.set('activityType', filters.activityType); + if (filters.category) params.set('category', filters.category); + if (filters.priority) params.set('priority', filters.priority); + if (typeof filters.overdue === 'boolean') params.set('overdue', String(filters.overdue)); + if (typeof filters.milestone === 'boolean') params.set('milestone', String(filters.milestone)); + if (typeof filters.hotProject === 'boolean') params.set('hotProject', String(filters.hotProject)); + if (filters.search) params.set('search', filters.search); + if (filters.limit) params.set('limit', String(filters.limit)); + if (filters.cursor) params.set('cursor', filters.cursor); + return params.toString(); +} + +export async function getCalendarItems( + filters: Omit +): Promise { + const search = buildSearchParams(filters); + return apiClient(`/crm/calendar${search ? `?${search}` : ''}`); +} diff --git a/src/features/crm/calendar/api/types.ts b/src/features/crm/calendar/api/types.ts new file mode 100644 index 0000000..3f6fbf2 --- /dev/null +++ b/src/features/crm/calendar/api/types.ts @@ -0,0 +1,128 @@ +export type CalendarCategory = + | 'activity' + | 'meeting' + | 'visit' + | 'site_survey' + | 'task' + | 'reminder' + | 'opportunity_milestone' + | 'quotation_milestone' + | 'approval_milestone'; + +export type CalendarPriority = 'low' | 'normal' | 'high' | 'critical'; +export type CalendarLens = 'personal' | 'team' | 'manager' | 'executive'; +export type CalendarView = 'day' | 'week' | 'month' | 'agenda'; + +export interface CalendarVisibility { + productType?: string | null; + pricingSensitive: boolean; + internalOnly: boolean; +} + +export interface CalendarProjectionItem { + id: string; + organizationId: string; + branchId: string | null; + productType: string | null; + customerId: string | null; + leadId: string | null; + opportunityId: string | null; + quotationId: string | null; + approvalId: string | null; + activityId: string | null; + ownerId: string | null; + assigneeId: string | null; + eventId: string; + entityType: string; + entityId: string; + title: string; + summary: string | null; + location: string | null; + startAt: string; + endAt: string; + timezone: string; + priority: CalendarPriority; + category: CalendarCategory; + activityType: string | null; + editable: boolean; + milestone: boolean; + overdue: boolean; + hotProject: boolean; + allDay: boolean; + visibility: CalendarVisibility; + metadata: Record | null; + sourceProjectionVersion: number; + createdAt: string; + updatedAt: string; +} + +export interface CalendarListFilters { + organizationId: string; + startFrom: string; + startTo: string; + lens?: CalendarLens; + userId?: string; + teamUserIds?: string[]; + branchId?: string; + productType?: string; + customerId?: string; + leadId?: string; + opportunityId?: string; + quotationId?: string; + activityType?: string; + category?: CalendarCategory; + priority?: CalendarPriority; + overdue?: boolean; + milestone?: boolean; + hotProject?: boolean; + search?: string; + limit?: number; + cursor?: string; +} + +export interface CalendarSummary { + dueToday: number; + overdue: number; + meetings: number; + visits: number; + milestones: number; + approvalDue: number; + hotProjects: number; +} + +export interface CalendarListResponse { + items: CalendarProjectionItem[]; + summary: CalendarSummary; + nextCursor: string | null; + totalItems: number; +} + +export interface BigCalendarEvent { + id: string; + title: string; + start: string; + end: string; + allDay: boolean; + editable: boolean; + draggable: boolean; + resizable: boolean; + category: CalendarCategory; + priority: CalendarPriority; + source: { + entityType: string; + entityId: string; + activityId: string | null; + customerId: string | null; + leadId: string | null; + opportunityId: string | null; + quotationId: string | null; + approvalId: string | null; + }; + display: { + summary: string | null; + location: string | null; + overdue: boolean; + hotProject: boolean; + milestone: boolean; + }; +} diff --git a/src/features/crm/calendar/components/calendar-adapter.ts b/src/features/crm/calendar/components/calendar-adapter.ts new file mode 100644 index 0000000..07193e6 --- /dev/null +++ b/src/features/crm/calendar/components/calendar-adapter.ts @@ -0,0 +1,37 @@ +import type { BigCalendarEvent, CalendarProjectionItem } from '../api/types'; + +export function mapCalendarProjectionToBigCalendarEvent( + item: CalendarProjectionItem +): BigCalendarEvent { + const activityEditable = item.editable && !item.milestone && item.activityId !== null; + + return { + id: item.id, + title: item.title, + start: item.startAt, + end: item.endAt, + allDay: item.allDay, + editable: activityEditable, + draggable: activityEditable, + resizable: activityEditable, + category: item.category, + priority: item.priority, + source: { + entityType: item.entityType, + entityId: item.entityId, + activityId: item.activityId, + customerId: item.customerId, + leadId: item.leadId, + opportunityId: item.opportunityId, + quotationId: item.quotationId, + approvalId: item.approvalId + }, + display: { + summary: item.summary, + location: item.location, + overdue: item.overdue, + hotProject: item.hotProject, + milestone: item.milestone + } + }; +} diff --git a/src/features/crm/calendar/components/calendar-workspace.tsx b/src/features/crm/calendar/components/calendar-workspace.tsx new file mode 100644 index 0000000..14acc72 --- /dev/null +++ b/src/features/crm/calendar/components/calendar-workspace.tsx @@ -0,0 +1,295 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Icons } from '@/components/icons'; +import { cn } from '@/lib/utils'; +import { formatDate, formatTime } from '@/lib/date-format'; +import { calendarItemsQueryOptions } from '../api/queries'; +import type { BigCalendarEvent, CalendarListFilters, CalendarView } from '../api/types'; +import { mapCalendarProjectionToBigCalendarEvent } from './calendar-adapter'; + +interface CalendarWorkspaceProps { + filters: Omit; +} + +const VIEW_LABELS: Record = { + day: 'Day', + week: 'Week', + month: 'Month', + agenda: 'Agenda' +}; + +export function CalendarWorkspace({ filters }: CalendarWorkspaceProps) { + const [view, setView] = useState('agenda'); + const [search, setSearch] = useState(''); + const { data } = useSuspenseQuery(calendarItemsQueryOptions(filters)); + const events = useMemo( + () => + data.items + .map(mapCalendarProjectionToBigCalendarEvent) + .filter((event) => event.title.toLowerCase().includes(search.toLowerCase())), + [data.items, search] + ); + const groupedEvents = groupEventsByDate(events); + + return ( +
+ + + + +
+
+ + + Calendar Workspace + +

+ Projection workspace for scheduled activities and read-only business milestones. +

+
+
+
+ + setSearch(event.target.value)} + placeholder='Search calendar' + className='w-full pl-8 sm:w-[240px]' + /> +
+ Lens: {filters.lens ?? 'personal'} +
+
+
+ + setView(value as CalendarView)}> + + {Object.entries(VIEW_LABELS).map(([value, label]) => ( + + {label} + + ))} + + + + + + + + + + + + + + + + +
+
+ ); +} + +export function CalendarWorkspaceSkeleton() { + return ( +
+
+ {Array.from({ length: 4 }).map((_, index) => ( + + ))} +
+ +
+ ); +} + +function CalendarSummaryCards({ summary }: { summary: CalendarWorkspaceSummary }) { + const cards = [ + { label: 'Due Today', value: summary.dueToday, icon: Icons.calendar }, + { label: 'Overdue', value: summary.overdue, icon: Icons.warning }, + { label: 'Meetings', value: summary.meetings, icon: Icons.clock }, + { label: 'Hot Projects', value: summary.hotProjects, icon: Icons.check } + ]; + + return ( +
+ {cards.map((card) => ( + + +
+ +
+
+

{card.value}

+

{card.label}

+
+
+
+ ))} +
+ ); +} + +function AgendaView({ groupedEvents }: { groupedEvents: Array<[string, BigCalendarEvent[]]> }) { + if (!groupedEvents.length) { + return ; + } + + return ( +
+ {groupedEvents.map(([date, events]) => ( +
+
+
+

+ {formatDate(date)} +

+
+
+
+ {events.map((event) => ( + + ))} +
+
+ ))} +
+ ); +} + +function CompactCalendarGrid({ + events, + columns, + month +}: { + events: BigCalendarEvent[]; + columns: number; + month?: boolean; +}) { + if (!events.length) { + return ; + } + + return ( +
+ {events.map((event) => ( +
+

{formatDate(event.start)}

+ +
+ ))} +
+ ); +} + +function CalendarEventCard({ event }: { event: BigCalendarEvent }) { + return ( +
+
+
+
+ + {event.category.replaceAll('_', ' ')} + + {event.display.overdue ? Overdue : null} + {event.display.hotProject ? Hot Project : null} + {!event.editable ? Read-only : null} +
+

{event.title}

+ {event.display.summary ? ( +

{event.display.summary}

+ ) : null} +

+ {formatTime(event.start)} - {formatTime(event.end)} + {event.display.location ? ` · ${event.display.location}` : ''} +

+
+
+ + {event.editable ? ( + <> + + + + ) : ( + + )} +
+
+
+ ); +} + +function CalendarEventPill({ event }: { event: BigCalendarEvent }) { + return ( +
+ {formatTime(event.start)} {event.title} +
+ ); +} + +function CalendarEmptyState() { + return ( +
+ +

No scheduled work in this period

+

+ Try changing filters or create an Activity from the owning CRM record. +

+
+ ); +} + +function groupEventsByDate(events: BigCalendarEvent[]): Array<[string, BigCalendarEvent[]]> { + const grouped = new Map(); + for (const event of events) { + const key = event.start.slice(0, 10); + grouped.set(key, [...(grouped.get(key) ?? []), event]); + } + return [...grouped.entries()]; +} + +type CalendarWorkspaceSummary = { + dueToday: number; + overdue: number; + meetings: number; + visits: number; + milestones: number; + approvalDue: number; + hotProjects: number; +}; diff --git a/src/features/crm/calendar/index.ts b/src/features/crm/calendar/index.ts new file mode 100644 index 0000000..3c2ac98 --- /dev/null +++ b/src/features/crm/calendar/index.ts @@ -0,0 +1,12 @@ +export type { + BigCalendarEvent, + CalendarCategory, + CalendarLens, + CalendarListFilters, + CalendarListResponse, + CalendarPriority, + CalendarProjectionItem, + CalendarSummary, + CalendarView +} from './api/types'; +export { mapCalendarProjectionToBigCalendarEvent } from './components/calendar-adapter'; diff --git a/src/features/crm/calendar/server/builder.test.ts b/src/features/crm/calendar/server/builder.test.ts new file mode 100644 index 0000000..1311791 --- /dev/null +++ b/src/features/crm/calendar/server/builder.test.ts @@ -0,0 +1,86 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createBusinessEvent } from '../../../foundation/business-events/publisher.ts'; +import { buildCalendarChangeFromEvent } from './builder.ts'; +import { createCalendarProjectionConsumer } from './consumer.ts'; + +function scheduledActivityEvent(eventType = 'activity.created') { + return createBusinessEvent({ + eventType, + organizationId: 'org-1', + branchId: 'branch-1', + entityType: 'activity', + entityId: 'activity-1', + primaryRecord: { type: 'opportunity', id: 'opp-1' }, + relatedRecords: [ + { type: 'customer', id: 'customer-1' }, + { type: 'quotation', id: 'quotation-1' } + ], + actorUserId: 'user-1', + occurredAt: '2026-07-13T00:00:00.000Z', + correlationId: 'corr-1', + visibility: { productType: 'product-a', pricingSensitive: true, internalOnly: false }, + payload: { + activityId: 'activity-1', + activityCode: 'ACT-1', + activityType: 'meeting', + subject: 'Customer planning meeting', + priority: 'high', + ownerId: 'owner-1', + assignedToId: 'assignee-1', + scheduledStartAt: '2026-07-14T03:00:00.000Z', + scheduledEndAt: '2026-07-14T04:00:00.000Z', + status: 'scheduled' + } + }); +} + +function unscheduledActivityEvent() { + return createBusinessEvent({ + eventType: 'activity.created', + organizationId: 'org-1', + branchId: 'branch-1', + entityType: 'activity', + entityId: 'activity-1', + primaryRecord: { type: 'activity', id: 'activity-1' }, + relatedRecords: [], + actorUserId: 'user-1', + occurredAt: '2026-07-13T00:00:00.000Z', + correlationId: 'corr-1', + visibility: { productType: null, pricingSensitive: false, internalOnly: false }, + payload: { + activityId: 'activity-1', + subject: 'No date' + } + }); +} + +test('calendar builder maps scheduled activity into editable calendar item', () => { + const change = buildCalendarChangeFromEvent(scheduledActivityEvent()); + assert.equal(change?.action, 'upsert'); + assert.equal(change?.item.category, 'meeting'); + assert.equal(change?.item.editable, true); + assert.equal(change?.item.milestone, false); + assert.equal(change?.item.customerId, 'customer-1'); + assert.equal(change?.item.quotationId, 'quotation-1'); + assert.equal(change?.item.ownerId, 'owner-1'); + assert.equal(change?.item.assigneeId, 'assignee-1'); + assert.equal(change?.item.visibility.pricingSensitive, true); +}); + +test('calendar builder deletes terminal activity from projection', () => { + const change = buildCalendarChangeFromEvent(scheduledActivityEvent('activity.cancelled')); + assert.equal(change?.action, 'delete'); + assert.equal(change?.activityId, 'activity-1'); +}); + +test('calendar consumer skips activity events without schedulable date', async () => { + const consumer = createCalendarProjectionConsumer({ + persist: async () => { + throw new Error('should not persist'); + } + }); + const result = await consumer.handle(unscheduledActivityEvent()); + assert.equal(result.status, 'skipped'); + assert.equal(result.sideEffectCount, 0); +}); diff --git a/src/features/crm/calendar/server/builder.ts b/src/features/crm/calendar/server/builder.ts new file mode 100644 index 0000000..46fd800 --- /dev/null +++ b/src/features/crm/calendar/server/builder.ts @@ -0,0 +1,184 @@ +import { BANGKOK_TIME_ZONE } from '../../../../lib/date-format.ts'; +import type { BusinessEvent } from '../../../foundation/business-events/types.ts'; +import type { CalendarCategory, CalendarPriority, CalendarProjectionItem } from '../api/types'; + +export const CALENDAR_PROJECTION_VERSION = 1; + +export type CalendarProjectionChange = + | { action: 'upsert'; item: CalendarProjectionItem } + | { action: 'delete'; organizationId: string; activityId: string; eventId: string }; + +const TERMINAL_ACTIVITY_EVENTS = new Set([ + 'activity.completed', + 'activity.cancelled', + 'activity.deleted' +]); + +const ACTIVITY_CATEGORY_BY_TYPE: Record = { + meeting: 'meeting', + visit: 'visit', + site_survey: 'site_survey', + internal_task: 'task', + reminder: 'reminder', + reminder_action: 'reminder', + follow_up: 'activity', + phone_call: 'activity', + email: 'activity', + email_follow_up: 'activity', + presentation: 'meeting', + delivery_coordination: 'activity', + approval_action: 'approval_milestone' +}; + +export function buildCalendarChangeFromEvent(event: BusinessEvent): CalendarProjectionChange | null { + if (!event.eventType.startsWith('activity.')) { + return null; + } + + const payload = event.payload as Record; + const activityId = text(payload.activityId) ?? (event.entityType === 'activity' ? event.entityId : null); + + if (!activityId) { + return null; + } + + if (TERMINAL_ACTIVITY_EVENTS.has(event.eventType)) { + return { + action: 'delete', + organizationId: event.organizationId, + activityId, + eventId: event.eventId + }; + } + + const startAt = dateText(payload.scheduledStartAt) ?? dateText(payload.dueAt); + if (!startAt) { + return null; + } + + const endAt = + dateText(payload.scheduledEndAt) ?? + addMinutesIso(startAt, defaultDurationMinutes(text(payload.activityType))); + const refs = resolveEntityRefs(event, payload); + const priority = priorityFromPayload(payload); + const category = categoryFromPayload(payload); + + return { + action: 'upsert', + item: { + id: `calendar:${event.organizationId}:${activityId}`, + organizationId: event.organizationId, + branchId: event.branchId ?? null, + productType: event.visibility.productType ?? null, + customerId: refs.customerId, + leadId: refs.leadId, + opportunityId: refs.opportunityId, + quotationId: refs.quotationId, + approvalId: refs.approvalId, + activityId, + ownerId: text(payload.ownerId), + assigneeId: text(payload.assignedToId), + eventId: event.eventId, + entityType: event.entityType, + entityId: event.entityId, + title: calendarTitle(payload, category), + summary: text(payload.description) ?? text(payload.outcomeSummary) ?? text(payload.nextAction), + location: text(payload.location) ?? text(payload.projectLocation), + startAt, + endAt, + timezone: BANGKOK_TIME_ZONE, + priority, + category, + activityType: text(payload.activityType), + editable: true, + milestone: false, + overdue: isOverdue(startAt, text(payload.status)), + hotProject: Boolean(payload.hotProject), + allDay: Boolean(payload.allDay), + visibility: { + productType: event.visibility.productType, + pricingSensitive: event.visibility.pricingSensitive ?? false, + internalOnly: event.visibility.internalOnly ?? false + }, + metadata: { + source: 'activity-business-event', + activityCode: text(payload.activityCode), + contentRedacted: Boolean(payload.contentRedacted), + primaryRecord: event.primaryRecord + }, + sourceProjectionVersion: CALENDAR_PROJECTION_VERSION, + createdAt: event.occurredAt, + updatedAt: event.occurredAt + } + }; +} + +function resolveEntityRefs(event: BusinessEvent, payload: Record) { + const refs = new Map(event.relatedRecords.map((record) => [record.type, record.id])); + return { + customerId: text(payload.customerId) ?? refs.get('customer') ?? null, + leadId: text(payload.leadId) ?? refs.get('lead') ?? null, + opportunityId: text(payload.opportunityId) ?? refs.get('opportunity') ?? null, + quotationId: text(payload.quotationId) ?? refs.get('quotation') ?? null, + approvalId: text(payload.approvalId) ?? refs.get('approval') ?? null + }; +} + +function calendarTitle(payload: Record, category: CalendarCategory): string { + const subject = text(payload.subject); + if (subject) return subject; + if (category === 'meeting') return 'Meeting'; + if (category === 'visit') return 'Customer visit'; + if (category === 'site_survey') return 'Site survey'; + if (category === 'reminder') return 'Reminder'; + return 'Activity'; +} + +function categoryFromPayload(payload: Record): CalendarCategory { + const activityType = text(payload.activityType); + if (activityType && ACTIVITY_CATEGORY_BY_TYPE[activityType]) { + return ACTIVITY_CATEGORY_BY_TYPE[activityType]; + } + return 'activity'; +} + +function priorityFromPayload(payload: Record): CalendarPriority { + const priority = text(payload.priority); + if (priority === 'critical' || priority === 'high' || priority === 'low') { + return priority; + } + return 'normal'; +} + +function defaultDurationMinutes(activityType: string | null): number { + if (activityType === 'visit' || activityType === 'site_survey' || activityType === 'presentation') { + return 120; + } + if (activityType === 'meeting') return 60; + return 30; +} + +function isOverdue(startAt: string, status: string | null): boolean { + if (status === 'completed' || status === 'cancelled' || status === 'skipped') { + return false; + } + return new Date(startAt).getTime() < Date.now(); +} + +function addMinutesIso(value: string, minutes: number): string { + const date = new Date(value); + date.setMinutes(date.getMinutes() + minutes); + return date.toISOString(); +} + +function dateText(value: unknown): string | null { + if (typeof value !== 'string' || value.trim().length === 0) { + return null; + } + const date = new Date(value); + return Number.isNaN(date.getTime()) ? null : date.toISOString(); +} + +function text(value: unknown): string | null { + return typeof value === 'string' && value.trim().length > 0 ? value : null; +} diff --git a/src/features/crm/calendar/server/consumer.ts b/src/features/crm/calendar/server/consumer.ts new file mode 100644 index 0000000..1c0187b --- /dev/null +++ b/src/features/crm/calendar/server/consumer.ts @@ -0,0 +1,69 @@ +import type { BusinessEvent } from '../../../foundation/business-events/types.ts'; +import type { ProjectionConsumer, ProjectionHandleResult } from '../../../foundation/projections/types.ts'; +import { getProjectionDefinition } from '../../../foundation/projections/registry.ts'; +import { buildCalendarChangeFromEvent } from './builder.ts'; + +const definition = getProjectionDefinition('Calendar'); + +interface CalendarProjectionConsumerOptions { + persist?: (change: ReturnType) => Promise; +} + +export function createCalendarProjectionConsumer( + options: CalendarProjectionConsumerOptions = {} +): ProjectionConsumer { + const persist = options.persist ?? persistCalendarChange; + + return { + consumerName: definition.consumerName, + projectionName: definition.projectionName, + supportedEventTypes: definition.consumedEventTypes, + supportedVersions: definition.supportedVersions, + async handle(event: BusinessEvent): Promise { + const change = buildCalendarChangeFromEvent(event); + if (!change) { + return { + status: 'skipped', + message: 'Calendar event has no schedulable date', + sideEffectCount: 0 + }; + } + + const sideEffectCount = await persist(change); + return { + status: 'processed', + message: + change.action === 'delete' + ? 'Calendar projection activity item removed' + : 'Calendar projection item persisted', + sideEffectCount + }; + } + }; +} + +export const calendarProjectionConsumer = createCalendarProjectionConsumer(); + +async function persistCalendarChange( + change: ReturnType +): Promise { + if (!change) return 0; + const { deleteCalendarProjectionForActivity, upsertCalendarProjectionItem } = await import( + './service.ts' + ); + + if (change.action === 'delete') { + await deleteCalendarProjectionForActivity({ + organizationId: change.organizationId, + activityId: change.activityId + }); + return 1; + } + + await deleteCalendarProjectionForActivity({ + organizationId: change.item.organizationId, + activityId: change.item.activityId ?? change.item.entityId + }); + await upsertCalendarProjectionItem(change.item); + return 1; +} diff --git a/src/features/crm/calendar/server/rebuild.ts b/src/features/crm/calendar/server/rebuild.ts new file mode 100644 index 0000000..0be32d3 --- /dev/null +++ b/src/features/crm/calendar/server/rebuild.ts @@ -0,0 +1,51 @@ +import type { + ProjectionRebuildRequest, + ProjectionRebuildResult +} from '../../../foundation/projections/types.ts'; + +export interface CalendarRebuildPlan { + projectionName: 'Calendar'; + organizationId: string; + sources: Array<'activity' | 'opportunity' | 'quotation' | 'approval' | 'business-event-replay'>; + resetExisting: boolean; + dryRun: boolean; + restart: boolean; + dateRange: { + from: string | null; + to: string | null; + }; +} + +export function createCalendarRebuildPlan( + request: ProjectionRebuildRequest & { restart?: boolean } +): CalendarRebuildPlan { + return { + projectionName: 'Calendar', + organizationId: request.organizationId, + sources: ['activity', 'opportunity', 'quotation', 'approval', 'business-event-replay'], + resetExisting: Boolean(request.resetExisting), + dryRun: Boolean(request.dryRun), + restart: Boolean(request.restart), + dateRange: { + from: request.occurredFrom ?? null, + to: request.occurredTo ?? null + } + }; +} + +export function createCalendarRebuildResult( + plan: CalendarRebuildPlan, + overrides: Partial = {} +): ProjectionRebuildResult { + return { + rebuildRunId: `calendar-rebuild:${plan.organizationId}:${Date.now()}`, + projectionName: plan.projectionName, + organizationId: plan.organizationId, + dryRun: plan.dryRun, + status: 'pending', + processedCount: 0, + skippedCount: 0, + failedCount: 0, + ...overrides + }; +} diff --git a/src/features/crm/calendar/server/registration.ts b/src/features/crm/calendar/server/registration.ts new file mode 100644 index 0000000..e3c1f4f --- /dev/null +++ b/src/features/crm/calendar/server/registration.ts @@ -0,0 +1,3 @@ +import { calendarProjectionConsumer } from './consumer.ts'; + +export const CRM_CALENDAR_PROJECTION_CONSUMERS = [calendarProjectionConsumer]; diff --git a/src/features/crm/calendar/server/service.ts b/src/features/crm/calendar/server/service.ts new file mode 100644 index 0000000..8f72607 --- /dev/null +++ b/src/features/crm/calendar/server/service.ts @@ -0,0 +1,260 @@ +import { and, asc, eq, gte, ilike, lte, or, type SQL } from 'drizzle-orm'; +import { crmCalendarProjection } from '@/db/schema'; +import { + buildCrmSecurityContext, + canAccessScopedCrmRecord, + type CrmSecurityContext +} from '@/features/crm/security/server/service'; +import { db } from '@/lib/db'; +import type { CalendarListFilters, CalendarListResponse, CalendarProjectionItem } from '../api/types'; + +export type CalendarProjectionRow = typeof crmCalendarProjection.$inferSelect; +export type CalendarDbClient = Pick; + +export async function upsertCalendarProjectionItem( + item: CalendarProjectionItem, + client: CalendarDbClient = db +): Promise { + await client + .insert(crmCalendarProjection) + .values(toInsertRow(item)) + .onConflictDoUpdate({ + target: crmCalendarProjection.id, + set: toInsertRow(item) + }); +} + +export async function deleteCalendarProjectionForActivity( + input: { organizationId: string; activityId: string }, + client: CalendarDbClient = db +): Promise { + await client + .delete(crmCalendarProjection) + .where( + and( + eq(crmCalendarProjection.organizationId, input.organizationId), + eq(crmCalendarProjection.activityId, input.activityId) + ) + ); +} + +export async function clearCalendarProjectionForOrganization( + organizationId: string, + client: CalendarDbClient = db +): Promise { + await client + .delete(crmCalendarProjection) + .where(eq(crmCalendarProjection.organizationId, organizationId)); +} + +export async function listCalendarItems( + filters: CalendarListFilters, + context: CrmSecurityContext, + client = db +): Promise { + const limit = Math.min(filters.limit ?? 200, 500); + const rows = await client + .select() + .from(crmCalendarProjection) + .where(and(...buildCalendarWhere(filters, context))) + .orderBy(asc(crmCalendarProjection.startAt)) + .limit(limit + 1); + + const visibleItems = rows.map(mapCalendarRow).filter((item) => canViewCalendarItem(context, item)); + const items = visibleItems.slice(0, limit); + + return { + items, + summary: summarizeCalendarItems(items), + nextCursor: visibleItems.length > limit ? items.at(-1)?.startAt ?? null : null, + totalItems: items.length + }; +} + +export function buildCalendarSecurityContext(input: Parameters[0]) { + return buildCrmSecurityContext(input); +} + +function buildCalendarWhere(filters: CalendarListFilters, context: CrmSecurityContext): SQL[] { + const where: SQL[] = [ + eq(crmCalendarProjection.organizationId, filters.organizationId), + gte(crmCalendarProjection.startAt, new Date(filters.startFrom)), + lte(crmCalendarProjection.startAt, new Date(filters.startTo)) + ]; + + if (filters.userId) { + where.push( + or( + eq(crmCalendarProjection.ownerId, filters.userId), + eq(crmCalendarProjection.assigneeId, filters.userId) + )! + ); + } else if (filters.lens === 'personal') { + where.push( + or( + eq(crmCalendarProjection.ownerId, context.userId), + eq(crmCalendarProjection.assigneeId, context.userId) + )! + ); + } + + if (filters.teamUserIds?.length) { + const teamPredicates = filters.teamUserIds.flatMap((userId) => [ + eq(crmCalendarProjection.ownerId, userId), + eq(crmCalendarProjection.assigneeId, userId) + ]); + where.push(or(...teamPredicates)!); + } + + if (filters.branchId) where.push(eq(crmCalendarProjection.branchId, filters.branchId)); + if (filters.productType) where.push(eq(crmCalendarProjection.productType, filters.productType)); + if (filters.customerId) where.push(eq(crmCalendarProjection.customerId, filters.customerId)); + if (filters.leadId) where.push(eq(crmCalendarProjection.leadId, filters.leadId)); + if (filters.opportunityId) where.push(eq(crmCalendarProjection.opportunityId, filters.opportunityId)); + if (filters.quotationId) where.push(eq(crmCalendarProjection.quotationId, filters.quotationId)); + if (filters.activityType) where.push(eq(crmCalendarProjection.activityType, filters.activityType)); + if (filters.category) where.push(eq(crmCalendarProjection.category, filters.category)); + if (filters.priority) where.push(eq(crmCalendarProjection.priority, filters.priority)); + if (typeof filters.overdue === 'boolean') where.push(eq(crmCalendarProjection.overdue, filters.overdue)); + if (typeof filters.milestone === 'boolean') where.push(eq(crmCalendarProjection.milestone, filters.milestone)); + if (typeof filters.hotProject === 'boolean') { + where.push(eq(crmCalendarProjection.hotProject, filters.hotProject)); + } + if (filters.search) { + where.push( + or( + ilike(crmCalendarProjection.title, `%${filters.search}%`), + ilike(crmCalendarProjection.summary, `%${filters.search}%`) + )! + ); + } + + return where; +} + +function canViewCalendarItem( + context: CrmSecurityContext, + item: CalendarProjectionItem +): boolean { + const directlyAssigned = item.ownerId === context.userId || item.assigneeId === context.userId; + if (item.visibility.internalOnly && !directlyAssigned && context.ownershipScope !== 'organization') { + return false; + } + + return ( + directlyAssigned || + canAccessScopedCrmRecord(context, { + branchId: item.branchId, + productType: item.productType, + createdBy: item.ownerId, + ownerUserId: item.assigneeId ?? item.ownerId + }) + ); +} + +function summarizeCalendarItems(items: CalendarProjectionItem[]) { + const today = new Date().toISOString().slice(0, 10); + return items.reduce( + (summary, item) => { + if (item.startAt.slice(0, 10) === today) summary.dueToday += 1; + if (item.overdue) summary.overdue += 1; + if (item.category === 'meeting') summary.meetings += 1; + if (item.category === 'visit') summary.visits += 1; + if (item.milestone) summary.milestones += 1; + if (item.category === 'approval_milestone') summary.approvalDue += 1; + if (item.hotProject) summary.hotProjects += 1; + return summary; + }, + { + dueToday: 0, + overdue: 0, + meetings: 0, + visits: 0, + milestones: 0, + approvalDue: 0, + hotProjects: 0 + } + ); +} + +function toInsertRow(item: CalendarProjectionItem): typeof crmCalendarProjection.$inferInsert { + return { + id: item.id, + organizationId: item.organizationId, + branchId: item.branchId, + productType: item.productType, + customerId: item.customerId, + leadId: item.leadId, + opportunityId: item.opportunityId, + quotationId: item.quotationId, + approvalId: item.approvalId, + activityId: item.activityId, + ownerId: item.ownerId, + assigneeId: item.assigneeId, + eventId: item.eventId, + entityType: item.entityType, + entityId: item.entityId, + title: item.title, + summary: item.summary, + location: item.location, + startAt: new Date(item.startAt), + endAt: new Date(item.endAt), + timezone: item.timezone, + priority: item.priority, + category: item.category, + activityType: item.activityType, + editable: item.editable, + milestone: item.milestone, + overdue: item.overdue, + hotProject: item.hotProject, + allDay: item.allDay, + visibility: item.visibility, + metadata: item.metadata, + sourceEvent: { + eventId: item.eventId, + entityType: item.entityType, + entityId: item.entityId + }, + sourceProjectionVersion: item.sourceProjectionVersion, + updatedAt: new Date(item.updatedAt) + }; +} + +function mapCalendarRow(row: CalendarProjectionRow): CalendarProjectionItem { + return { + id: row.id, + organizationId: row.organizationId, + branchId: row.branchId, + productType: row.productType, + customerId: row.customerId, + leadId: row.leadId, + opportunityId: row.opportunityId, + quotationId: row.quotationId, + approvalId: row.approvalId, + activityId: row.activityId, + ownerId: row.ownerId, + assigneeId: row.assigneeId, + eventId: row.eventId, + entityType: row.entityType, + entityId: row.entityId, + title: row.title, + summary: row.summary, + location: row.location, + startAt: row.startAt.toISOString(), + endAt: row.endAt.toISOString(), + timezone: row.timezone, + priority: row.priority as CalendarProjectionItem['priority'], + category: row.category as CalendarProjectionItem['category'], + activityType: row.activityType, + editable: row.editable, + milestone: row.milestone, + overdue: row.overdue, + hotProject: row.hotProject, + allDay: row.allDay, + visibility: row.visibility as CalendarProjectionItem['visibility'], + metadata: row.metadata as Record | null, + sourceProjectionVersion: row.sourceProjectionVersion, + createdAt: row.createdAt.toISOString(), + updatedAt: row.updatedAt.toISOString() + }; +} diff --git a/src/features/crm/timeline/server/registration.ts b/src/features/crm/timeline/server/registration.ts index 8ba480a..bc9cd8e 100644 --- a/src/features/crm/timeline/server/registration.ts +++ b/src/features/crm/timeline/server/registration.ts @@ -1,8 +1,8 @@ import { - calendarProjectionConsumer, dashboardProjectionConsumer, notificationProjectionConsumer } from '../../../foundation/projections/consumers.ts'; +import { calendarProjectionConsumer } from '../../calendar/server/consumer.ts'; import { timelineProjectionConsumer } from './consumer.ts'; export const CRM_TIMELINE_PROJECTION_CONSUMERS = [ diff --git a/src/features/foundation/projections/registry.ts b/src/features/foundation/projections/registry.ts index 83ad270..80e4947 100644 --- a/src/features/foundation/projections/registry.ts +++ b/src/features/foundation/projections/registry.ts @@ -33,16 +33,16 @@ export const PROJECTION_REGISTRY: ProjectionDefinition[] = [ { projectionName: 'Calendar', consumerName: 'calendar.projection.consumer', - owningFeature: 'foundation/projections', + owningFeature: 'crm/calendar', consumedEventTypes: ACTIVITY_EVENTS, supportedVersions: ACTIVITY_EVENT_VERSIONS, rebuildStrategy: 'hybrid', - persistence: 'cached-read-model', + persistence: 'materialized', securityClassification: 'organization-scoped; assignment and source-visibility-governed', idempotencyKey: 'consumerName + eventId', retryPolicy: 'projection-default-v1', retentionPolicy: 'calendar projection rows rebuildable from source activity and events', - implementationStatus: 'contract-only' + implementationStatus: 'active' }, { projectionName: 'Dashboard',