-- PostgreSQL 17 full initialization script for public schema
-- Usage:
--   psql -d your_database -f sql/init_full_pg17.sql
--
-- Structure:
--   1) base schema objects
--   2) indexes / triggers / comments
--   3) compatibility absorption for old environments
--
-- Notes:
--   - This is the current canonical initialization entry.
--   - Historical incremental scripts are archived under sql/archive/.

begin;

set search_path to public;

-- =====================================================================
-- Section 1. Utilities
-- =====================================================================

-- Unified trigger function for updated_at maintenance.
create or replace function set_updated_at()
returns trigger
language plpgsql
as $$
begin
  new.updated_at = now();
  return new;
end;
$$;

create or replace function create_trigger_if_not_exists(trigger_name text, table_name text)
returns void
language plpgsql
as $$
begin
  if not exists (
    select 1
    from pg_trigger t
    join pg_class c on c.oid = t.tgrelid
    join pg_namespace n on n.oid = c.relnamespace
    where t.tgname = trigger_name
      and c.relname = table_name
      and n.nspname = current_schema()
  ) then
    execute format(
      'create trigger %I before update on %I for each row execute function set_updated_at()',
      trigger_name,
      table_name
    );
  end if;
end;
$$;

create or replace function comment_on_column_if_exists(p_table_name text, p_column_name text, p_comment_text text)
returns void
language plpgsql
as $$
begin
  if exists (
    select 1
    from information_schema.columns c
    where c.table_schema = current_schema()
      and c.table_name = p_table_name
      and c.column_name = p_column_name
  ) then
    execute format(
      'comment on column %I.%I is %L',
      p_table_name,
      p_column_name,
      p_comment_text
    );
  end if;
end;
$$;

-- =====================================================================
-- Section 2. Base tables
-- =====================================================================

create table if not exists sys_user (
  id bigint generated by default as identity primary key,
  user_code varchar(50),
  username varchar(50) not null,
  real_name varchar(50) not null,
  mobile varchar(20),
  email varchar(100),
  org_id bigint,
  job_title varchar(100),
  status smallint not null default 1,
  hire_date date,
  avatar_url varchar(255),
  password_hash varchar(255),
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  constraint uk_sys_user_username unique (username),
  constraint uk_sys_user_mobile unique (mobile)
);

create table if not exists crm_customer (
  id bigint generated by default as identity primary key,
  customer_code varchar(50),
  customer_name varchar(200) not null,
  customer_type varchar(50),
  industry varchar(50),
  province varchar(50),
  city varchar(50),
  address varchar(255),
  owner_user_id bigint,
  source varchar(50),
  status varchar(30) not null default 'potential'
    check (status in ('potential', 'following', 'won', 'lost')),
  remark text,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  constraint uk_crm_customer_code unique (customer_code)
);

create table if not exists crm_opportunity (
  id bigint generated by default as identity primary key,
  opportunity_code varchar(50) not null,
  opportunity_name varchar(200) not null,
  customer_id bigint not null,
  owner_user_id bigint not null,
  sales_expansion_id bigint,
  channel_expansion_id bigint,
  pre_sales_id bigint,
  pre_sales_name varchar(100),
  project_location varchar(100),
  project_ownership_location varchar(100),
  operator_name varchar(100),
  amount numeric(18, 2) not null default 0,
  actual_signed_amount numeric(18, 2),
  is_poc boolean not null default false,
  expected_close_date date,
  confidence_pct varchar(1) not null default 'C' check (confidence_pct in ('A', 'B', 'C')),
  stage varchar(50) not null default 'initial_contact',
  opportunity_type varchar(50),
  product_type varchar(100),
  source varchar(50),
  competitor_name varchar(200),
  latest_progress text,
  next_plan text,
  updated_by bigint,
  archived boolean not null default false,
  archived_at timestamptz,
  pushed_to_oms boolean not null default false,
  oms_push_time timestamptz,
  description text,
  status varchar(30) not null default 'active'
    check (status in ('active', 'won', 'lost', 'closed')),
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  constraint uk_crm_opportunity_code unique (opportunity_code),
  constraint fk_crm_opportunity_customer foreign key (customer_id) references crm_customer(id)
);

create table if not exists crm_opportunity_followup (
  id bigint generated by default as identity primary key,
  opportunity_id bigint not null,
  followup_time timestamptz not null,
  followup_type varchar(50) not null,
  content text not null,
  next_action varchar(255),
  followup_user_id bigint not null,
  source_type varchar(30),
  source_id bigint,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  constraint fk_crm_opportunity_followup_opportunity
    foreign key (opportunity_id) references crm_opportunity(id) on delete cascade
);

create table if not exists crm_sales_expansion (
  id bigint generated by default as identity primary key,
  employee_no varchar(50) not null,
  candidate_name varchar(50) not null,
  office_name varchar(100),
  mobile varchar(20),
  email varchar(100),
  target_dept varchar(100),
  industry varchar(50),
  title varchar(100),
  intent_level varchar(20) not null default 'medium'
    check (intent_level in ('high', 'medium', 'low')),
  stage varchar(50) not null default 'initial_contact',
  has_desktop_exp boolean not null default false,
  in_progress boolean not null default true,
  employment_status varchar(20) not null default 'active'
    check (employment_status in ('active', 'left', 'joined', 'abandoned')),
  expected_join_date date,
  owner_user_id bigint not null,
  remark text,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table if not exists crm_channel_expansion (
  id bigint generated by default as identity primary key,
  channel_code varchar(50),
  province varchar(50),
  city varchar(50),
  channel_name varchar(200) not null,
  office_address varchar(255),
  channel_industry varchar(100),
  certification_level varchar(100),
  annual_revenue numeric(18, 2),
  staff_size integer check (staff_size is null or staff_size >= 0),
  contact_established_date date,
  intent_level varchar(20) not null default 'medium'
    check (intent_level in ('high', 'medium', 'low')),
  has_desktop_exp boolean not null default false,
  contact_name varchar(50),
  contact_title varchar(100),
  contact_mobile varchar(20),
  channel_attribute varchar(100),
  internal_attribute varchar(100),
  stage varchar(50) not null default 'initial_contact',
  landed_flag boolean not null default false,
  expected_sign_date date,
  owner_user_id bigint not null,
  remark text,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table if not exists crm_channel_expansion_contact (
  id bigint generated by default as identity primary key,
  channel_expansion_id bigint not null,
  contact_name varchar(50),
  contact_mobile varchar(20),
  contact_title varchar(100),
  sort_order integer not null default 1,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  constraint fk_crm_channel_expansion_contact_channel
    foreign key (channel_expansion_id) references crm_channel_expansion(id) on delete cascade
);

create table if not exists crm_expansion_followup (
  id bigint generated by default as identity primary key,
  biz_type varchar(20) not null check (biz_type in ('sales', 'channel')),
  biz_id bigint not null,
  followup_time timestamptz not null,
  followup_type varchar(50) not null,
  content text not null,
  next_action varchar(255),
  followup_user_id bigint not null,
  visit_start_time timestamptz,
  evaluation_content text,
  next_plan text,
  source_type varchar(30),
  source_id bigint,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table if not exists work_checkin (
  id bigint generated by default as identity primary key,
  user_id bigint not null,
  checkin_date date not null,
  checkin_time timestamptz not null,
  biz_type varchar(20) check (biz_type is null or biz_type in ('sales', 'channel', 'opportunity')),
  biz_id bigint,
  biz_name varchar(200),
  longitude numeric(10, 6),
  latitude numeric(10, 6),
  location_text varchar(255) not null,
  remark varchar(500),
  user_name varchar(100),
  dept_name varchar(200),
  status varchar(30) not null default 'normal'
    check (status in ('normal', 'abnormal', 'reissue')),
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table if not exists work_daily_report (
  id bigint generated by default as identity primary key,
  user_id bigint not null,
  report_date date not null,
  work_content text,
  tomorrow_plan text,
  source_type varchar(30) not null default 'manual'
    check (source_type in ('manual', 'voice')),
  submit_time timestamptz,
  status varchar(30) not null default 'draft'
    check (status in ('draft', 'submitted', 'read', 'reviewed')),
  score integer check (score is null or score between 0 and 100),
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  constraint uk_work_daily_report_user_date unique (user_id, report_date)
);

create table if not exists work_daily_report_comment (
  id bigint generated by default as identity primary key,
  report_id bigint not null,
  reviewer_user_id bigint not null,
  score integer check (score is null or score between 0 and 100),
  comment_content text,
  reviewed_at timestamptz not null default now(),
  created_at timestamptz not null default now(),
  constraint fk_work_daily_report_comment_report
    foreign key (report_id) references work_daily_report(id) on delete cascade
);

create table if not exists work_report_message (
  id bigint generated by default as identity primary key,
  report_id bigint not null,
  sender_user_id bigint not null,
  receiver_user_id bigint not null,
  report_date date not null,
  line_index integer not null default 0,
  biz_type varchar(20) check (biz_type is null or biz_type in ('sales', 'channel', 'opportunity')),
  biz_id bigint,
  biz_name varchar(200),
  content text not null,
  read_at timestamptz,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  constraint fk_work_report_message_report
    foreign key (report_id) references work_daily_report(id) on delete cascade,
  constraint uk_work_report_message_report_line_receiver
    unique (report_id, line_index, receiver_user_id)
);

create index if not exists idx_work_report_message_receiver_time
  on work_report_message (receiver_user_id, created_at desc);

create index if not exists idx_work_report_message_receiver_read_time
  on work_report_message (receiver_user_id, read_at, created_at desc);

create index if not exists idx_work_report_message_report
  on work_report_message (report_id);

create table if not exists work_todo (
  id bigint generated by default as identity primary key,
  user_id bigint not null,
  title varchar(200) not null,
  biz_type varchar(30) not null default 'other'
    check (biz_type in ('opportunity', 'expansion', 'report', 'other')),
  biz_id bigint,
  due_date timestamptz,
  status varchar(20) not null default 'todo'
    check (status in ('todo', 'done', 'canceled')),
  priority varchar(20) not null default 'medium'
    check (priority in ('high', 'medium', 'low')),
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table if not exists sys_activity_log (
  id bigint generated by default as identity primary key,
  biz_type varchar(30) not null,
  biz_id bigint,
  action_type varchar(50) not null,
  title varchar(200) not null,
  content varchar(500),
  operator_user_id bigint,
  created_at timestamptz not null default now()
);

-- =====================================================================
-- Section 3. Indexes / triggers / comments
-- =====================================================================

do $$
begin
  if not exists (
    select 1
    from pg_constraint
    where conname = 'fk_crm_opportunity_channel_expansion'
  ) then
    alter table crm_opportunity
      add constraint fk_crm_opportunity_channel_expansion
      foreign key (channel_expansion_id) references crm_channel_expansion(id);
  end if;
end $$;

create index if not exists idx_crm_customer_owner on crm_customer(owner_user_id);
create index if not exists idx_crm_customer_name on crm_customer(customer_name);
do $$
begin
  if exists (
    select 1
    from information_schema.columns
    where table_schema = current_schema()
      and table_name = 'sys_user'
      and column_name = 'org_id'
  ) then
    execute 'create index if not exists idx_sys_user_org_id on sys_user(org_id)';
  end if;
end;
$$;
create unique index if not exists uk_crm_sales_expansion_owner_employee_no
  on crm_sales_expansion(owner_user_id, employee_no);
create sequence if not exists crm_channel_expansion_code_seq start with 1 increment by 1 minvalue 1;
create index if not exists idx_crm_opportunity_customer on crm_opportunity(customer_id);
create index if not exists idx_crm_opportunity_owner on crm_opportunity(owner_user_id);
create index if not exists idx_crm_opportunity_sales_expansion on crm_opportunity(sales_expansion_id);
create index if not exists idx_crm_opportunity_stage on crm_opportunity(stage);
create index if not exists idx_crm_opportunity_expected_close on crm_opportunity(expected_close_date);
create index if not exists idx_crm_opportunity_archived on crm_opportunity(archived);
create index if not exists idx_crm_opportunity_archived_at on crm_opportunity(archived_at);
create index if not exists idx_crm_opportunity_followup_opportunity_time
  on crm_opportunity_followup(opportunity_id, followup_time desc);
create index if not exists idx_crm_opportunity_followup_user on crm_opportunity_followup(followup_user_id);
create index if not exists idx_crm_sales_expansion_owner on crm_sales_expansion(owner_user_id);
create index if not exists idx_crm_sales_expansion_stage on crm_sales_expansion(stage);
create index if not exists idx_crm_sales_expansion_mobile on crm_sales_expansion(mobile);
create index if not exists idx_crm_channel_expansion_owner on crm_channel_expansion(owner_user_id);
create index if not exists idx_crm_channel_expansion_stage on crm_channel_expansion(stage);
create index if not exists idx_crm_channel_expansion_name on crm_channel_expansion(channel_name);
create unique index if not exists uk_crm_channel_expansion_code
  on crm_channel_expansion(channel_code)
  where channel_code is not null;
create index if not exists idx_crm_channel_expansion_contact_channel on crm_channel_expansion_contact(channel_expansion_id);
create index if not exists idx_crm_opportunity_channel_expansion on crm_opportunity(channel_expansion_id);
create index if not exists idx_crm_expansion_followup_biz_time
  on crm_expansion_followup(biz_type, biz_id, followup_time desc);
create index if not exists idx_crm_expansion_followup_user on crm_expansion_followup(followup_user_id);
create index if not exists idx_work_checkin_user_date on work_checkin(user_id, checkin_date desc);
create index if not exists idx_work_daily_report_user_date on work_daily_report(user_id, report_date desc);
create index if not exists idx_work_daily_report_status on work_daily_report(status);
create index if not exists idx_work_daily_report_comment_report on work_daily_report_comment(report_id);
create index if not exists idx_sys_activity_log_created on sys_activity_log(created_at desc);
create index if not exists idx_sys_activity_log_biz on sys_activity_log(biz_type, biz_id);

select create_trigger_if_not_exists('trg_sys_user_updated_at', 'sys_user');
select create_trigger_if_not_exists('trg_crm_customer_updated_at', 'crm_customer');
select create_trigger_if_not_exists('trg_crm_opportunity_updated_at', 'crm_opportunity');
select create_trigger_if_not_exists('trg_crm_opportunity_followup_updated_at', 'crm_opportunity_followup');
select create_trigger_if_not_exists('trg_crm_sales_expansion_updated_at', 'crm_sales_expansion');
select create_trigger_if_not_exists('trg_crm_channel_expansion_updated_at', 'crm_channel_expansion');
select create_trigger_if_not_exists('trg_crm_channel_expansion_contact_updated_at', 'crm_channel_expansion_contact');
select create_trigger_if_not_exists('trg_crm_expansion_followup_updated_at', 'crm_expansion_followup');
select create_trigger_if_not_exists('trg_work_checkin_updated_at', 'work_checkin');
select create_trigger_if_not_exists('trg_work_daily_report_updated_at', 'work_daily_report');
select create_trigger_if_not_exists('trg_work_todo_updated_at', 'work_todo');

comment on table sys_user is '系统用户';
comment on table crm_customer is '客户主表';
comment on table crm_opportunity is '商机主表';
comment on table crm_opportunity_followup is '商机跟进记录';
comment on table crm_sales_expansion is '销售人员拓展';
comment on table crm_channel_expansion is '渠道拓展';
comment on table crm_channel_expansion_contact is '渠道拓展联系人';
comment on table crm_expansion_followup is '拓展跟进记录';
comment on table work_checkin is '外勤打卡';
comment on table work_daily_report is '日报';
comment on table work_daily_report_comment is '日报点评';
comment on table work_todo is '待办事项';
comment on table sys_activity_log is '首页动态日志';

commit;

-- =====================================================================
-- Compatibility DDL section
-- Purpose:
-- 1) make this script usable for both fresh installs and old-environment upgrades
-- 2) absorb historical DDL migration scripts into a single deployment entry
-- 3) keep base-framework tables managed by unisbase itself
--
-- Base framework dependencies not created here:
--   sys_org, sys_dict_item, sys_tenant_user, sys_role, sys_user_role ...
-- =====================================================================

begin;

set search_path to public;

-- sys_user compatibility: dept_id -> org_id
DO $$
BEGIN
  IF to_regclass('public.sys_user') IS NOT NULL THEN
    IF NOT EXISTS (
      SELECT 1
      FROM information_schema.columns
      WHERE table_schema = 'public'
        AND table_name = 'sys_user'
        AND column_name = 'org_id'
    ) THEN
      ALTER TABLE public.sys_user ADD COLUMN org_id bigint;
    END IF;

    IF EXISTS (
      SELECT 1
      FROM information_schema.columns
      WHERE table_schema = 'public'
        AND table_name = 'sys_user'
        AND column_name = 'dept_id'
    ) THEN
      EXECUTE 'update public.sys_user set org_id = coalesce(org_id, dept_id) where dept_id is not null';
      ALTER TABLE public.sys_user DROP COLUMN dept_id;
    END IF;
  END IF;
END $$;

DROP INDEX IF EXISTS public.idx_sys_user_dept_id;
CREATE INDEX IF NOT EXISTS idx_sys_user_org_id ON public.sys_user(org_id);

-- crm_sales_expansion compatibility: ensure employee_no / office_name / target_dept text
ALTER TABLE IF EXISTS crm_sales_expansion
  ADD COLUMN IF NOT EXISTS employee_no varchar(50),
  ADD COLUMN IF NOT EXISTS office_name varchar(100),
  ADD COLUMN IF NOT EXISTS target_dept varchar(100);

DO $$
BEGIN
  IF to_regclass('public.crm_sales_expansion') IS NOT NULL THEN
    IF EXISTS (
      SELECT 1
      FROM information_schema.columns
      WHERE table_schema = 'public'
        AND table_name = 'crm_sales_expansion'
        AND column_name = 'target_dept_id'
    ) THEN
      UPDATE crm_sales_expansion s
      SET target_dept = COALESCE(
        NULLIF(s.target_dept, ''),
        (
          SELECT d.item_label
          FROM sys_dict_item d
          WHERE d.type_code = 'tz_ssbm'
            AND d.item_value = s.target_dept_id::varchar
            AND d.status = 1
            AND COALESCE(d.is_deleted, 0) = 0
          ORDER BY d.sort_order ASC NULLS LAST, d.dict_item_id ASC
          LIMIT 1
        ),
        (
          SELECT o.org_name
          FROM sys_org o
          WHERE o.id = s.target_dept_id
          LIMIT 1
        ),
        s.target_dept_id::varchar
      )
      WHERE s.target_dept_id IS NOT NULL;

      ALTER TABLE crm_sales_expansion DROP COLUMN target_dept_id;
    END IF;

    UPDATE crm_sales_expansion
    SET employee_no = concat('EMP', lpad(id::text, 6, '0'))
    WHERE employee_no IS NULL OR btrim(employee_no) = '';

    WITH duplicated AS (
      SELECT
        id,
        row_number() over (partition by owner_user_id, employee_no order by id asc) AS rn
      FROM crm_sales_expansion
      WHERE employee_no IS NOT NULL
        AND btrim(employee_no) <> ''
    )
    UPDATE crm_sales_expansion s
    SET employee_no = concat(s.employee_no, '-', s.id)
    FROM duplicated d
    WHERE s.id = d.id
      AND d.rn > 1;

    IF NOT EXISTS (
      SELECT 1 FROM crm_sales_expansion WHERE employee_no IS NULL OR btrim(employee_no) = ''
    ) THEN
      ALTER TABLE crm_sales_expansion
        ALTER COLUMN employee_no SET NOT NULL;
    END IF;
  END IF;
END $$;

DO $$
BEGIN
  IF to_regclass('public.crm_sales_expansion') IS NOT NULL THEN
    IF NOT EXISTS (
      SELECT 1
      FROM pg_indexes
      WHERE schemaname = 'public'
        AND indexname = 'uk_crm_sales_expansion_owner_employee_no'
    ) THEN
      CREATE UNIQUE INDEX uk_crm_sales_expansion_owner_employee_no
        ON crm_sales_expansion(owner_user_id, employee_no);
    END IF;
  END IF;
END $$;

-- crm_opportunity compatibility: absorb old extension fields and relationships
ALTER TABLE IF EXISTS crm_opportunity
  ADD COLUMN IF NOT EXISTS sales_expansion_id bigint,
  ADD COLUMN IF NOT EXISTS channel_expansion_id bigint,
  ADD COLUMN IF NOT EXISTS pre_sales_id bigint,
  ADD COLUMN IF NOT EXISTS pre_sales_name varchar(100),
  ADD COLUMN IF NOT EXISTS project_location varchar(100),
  ADD COLUMN IF NOT EXISTS project_ownership_location varchar(100),
  ADD COLUMN IF NOT EXISTS operator_name varchar(100),
  ADD COLUMN IF NOT EXISTS competitor_name varchar(200),
  ADD COLUMN IF NOT EXISTS actual_signed_amount numeric(18, 2),
  ADD COLUMN IF NOT EXISTS is_poc boolean not null default false;

DO $$
BEGIN
  IF to_regclass('public.crm_opportunity') IS NOT NULL THEN
    ALTER TABLE public.crm_opportunity
      DROP CONSTRAINT IF EXISTS crm_opportunity_stage_check;

    IF NOT EXISTS (
      SELECT 1 FROM pg_constraint WHERE conname = 'fk_crm_opportunity_sales_expansion'
    ) THEN
      ALTER TABLE public.crm_opportunity
        ADD CONSTRAINT fk_crm_opportunity_sales_expansion
        FOREIGN KEY (sales_expansion_id) REFERENCES public.crm_sales_expansion(id);
    END IF;

    IF NOT EXISTS (
      SELECT 1 FROM pg_constraint WHERE conname = 'fk_crm_opportunity_channel_expansion'
    ) THEN
      ALTER TABLE public.crm_opportunity
        ADD CONSTRAINT fk_crm_opportunity_channel_expansion
        FOREIGN KEY (channel_expansion_id) REFERENCES public.crm_channel_expansion(id);
    END IF;
  END IF;
END $$;

-- crm_channel_expansion compatibility: absorb detail columns and contact sub-table
ALTER TABLE IF EXISTS crm_channel_expansion
  ADD COLUMN IF NOT EXISTS channel_code varchar(50),
  ADD COLUMN IF NOT EXISTS office_address varchar(255),
  ADD COLUMN IF NOT EXISTS channel_industry varchar(100),
  ADD COLUMN IF NOT EXISTS city varchar(50),
  ADD COLUMN IF NOT EXISTS certification_level varchar(100),
  ADD COLUMN IF NOT EXISTS contact_established_date date,
  ADD COLUMN IF NOT EXISTS intent_level varchar(20),
  ADD COLUMN IF NOT EXISTS has_desktop_exp boolean,
  ADD COLUMN IF NOT EXISTS channel_attribute varchar(100),
  ADD COLUMN IF NOT EXISTS internal_attribute varchar(100);

DO $$
BEGIN
  IF to_regclass('public.crm_channel_expansion') IS NOT NULL THEN
    IF EXISTS (
      SELECT 1
      FROM information_schema.columns
      WHERE table_schema = 'public'
        AND table_name = 'crm_channel_expansion'
        AND column_name = 'industry'
    ) THEN
      EXECUTE 'update public.crm_channel_expansion set channel_industry = coalesce(channel_industry, industry) where channel_industry is null and industry is not null';
    END IF;

    UPDATE crm_channel_expansion
    SET intent_level = COALESCE(intent_level, 'medium'),
        has_desktop_exp = COALESCE(has_desktop_exp, false);

    ALTER TABLE crm_channel_expansion
      ALTER COLUMN intent_level SET DEFAULT 'medium',
      ALTER COLUMN has_desktop_exp SET DEFAULT false;

    IF EXISTS (
      SELECT 1 FROM crm_channel_expansion WHERE intent_level IS NULL
    ) THEN
      RAISE NOTICE 'crm_channel_expansion.intent_level still has null values before not-null enforcement';
    ELSE
      ALTER TABLE crm_channel_expansion
        ALTER COLUMN intent_level SET NOT NULL;
    END IF;

    IF EXISTS (
      SELECT 1 FROM crm_channel_expansion WHERE has_desktop_exp IS NULL
    ) THEN
      RAISE NOTICE 'crm_channel_expansion.has_desktop_exp still has null values before not-null enforcement';
    ELSE
      ALTER TABLE crm_channel_expansion
        ALTER COLUMN has_desktop_exp SET NOT NULL;
    END IF;

    IF NOT EXISTS (
      SELECT 1 FROM pg_constraint WHERE conname = 'crm_channel_expansion_intent_level_check'
    ) THEN
      ALTER TABLE crm_channel_expansion
        ADD CONSTRAINT crm_channel_expansion_intent_level_check
        CHECK (intent_level IN ('high', 'medium', 'low'));
    END IF;
  END IF;
END $$;

CREATE TABLE IF NOT EXISTS crm_channel_expansion_contact (
  id bigint generated by default as identity primary key,
  channel_expansion_id bigint not null,
  contact_name varchar(50),
  contact_mobile varchar(20),
  contact_title varchar(100),
  sort_order integer not null default 1,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  constraint fk_crm_channel_expansion_contact_channel
    foreign key (channel_expansion_id) references crm_channel_expansion(id) on delete cascade
);

DO $$
BEGIN
  IF to_regclass('public.crm_channel_expansion') IS NOT NULL
     AND to_regclass('public.crm_channel_expansion_contact') IS NOT NULL THEN
    INSERT INTO crm_channel_expansion_contact (
      channel_expansion_id,
      contact_name,
      contact_mobile,
      contact_title,
      sort_order,
      created_at,
      updated_at
    )
    SELECT c.id, c.contact_name, c.contact_mobile, c.contact_title, 1, now(), now()
    FROM crm_channel_expansion c
    WHERE (COALESCE(btrim(c.contact_name), '') <> ''
        OR COALESCE(btrim(c.contact_mobile), '') <> ''
        OR COALESCE(btrim(c.contact_title), '') <> '')
      AND NOT EXISTS (
        SELECT 1
        FROM crm_channel_expansion_contact cc
        WHERE cc.channel_expansion_id = c.id
      );
  END IF;
END $$;

ALTER TABLE IF EXISTS crm_opportunity
  ADD COLUMN IF NOT EXISTS latest_progress text,
  ADD COLUMN IF NOT EXISTS next_plan text;

-- follow-up compatibility: source fields and structured work-report fields
ALTER TABLE IF EXISTS crm_expansion_followup
  ADD COLUMN IF NOT EXISTS visit_start_time timestamptz,
  ADD COLUMN IF NOT EXISTS evaluation_content text,
  ADD COLUMN IF NOT EXISTS next_plan text,
  ADD COLUMN IF NOT EXISTS source_type varchar(30),
  ADD COLUMN IF NOT EXISTS source_id bigint;

ALTER TABLE IF EXISTS crm_opportunity_followup
  ADD COLUMN IF NOT EXISTS source_type varchar(30),
  ADD COLUMN IF NOT EXISTS source_id bigint;

-- work_checkin compatibility: relation fields
ALTER TABLE IF EXISTS work_checkin
  ADD COLUMN IF NOT EXISTS biz_type varchar(20),
  ADD COLUMN IF NOT EXISTS biz_id bigint,
  ADD COLUMN IF NOT EXISTS biz_name varchar(200),
  ADD COLUMN IF NOT EXISTS user_name varchar(100),
  ADD COLUMN IF NOT EXISTS dept_name varchar(200);

DO $$
BEGIN
  IF to_regclass('public.work_checkin') IS NOT NULL
     AND NOT EXISTS (
       SELECT 1
       FROM pg_constraint
       WHERE conrelid = 'public.work_checkin'::regclass
         AND conname = 'work_checkin_biz_type_check'
     ) THEN
    ALTER TABLE public.work_checkin
      ADD CONSTRAINT work_checkin_biz_type_check
      CHECK (biz_type IS NULL OR biz_type IN ('sales', 'channel', 'opportunity'));
  END IF;
END $$;

-- additional indexes absorbed from historical DDLs
CREATE INDEX IF NOT EXISTS idx_crm_opportunity_channel_expansion
  ON crm_opportunity(channel_expansion_id);
CREATE INDEX IF NOT EXISTS idx_crm_expansion_followup_source
  ON crm_expansion_followup(source_type, source_id);
CREATE INDEX IF NOT EXISTS idx_crm_opportunity_followup_source
  ON crm_opportunity_followup(source_type, source_id);
CREATE INDEX IF NOT EXISTS idx_crm_channel_expansion_contact_channel
  ON crm_channel_expansion_contact(channel_expansion_id);

CREATE TABLE IF NOT EXISTS business_calendar_day (
  id bigint generated by default as identity primary key,
  calendar_date date not null,
  calendar_year integer not null,
  day_of_week integer not null,
  day_type_code integer not null default 0,
  day_type varchar(32) not null default 'WORKDAY',
  day_name varchar(64) not null,
  holiday_name varchar(100),
  holiday_target varchar(100),
  holiday_wage integer,
  is_weekend boolean not null default false,
  is_rest_day boolean not null default false,
  is_workday boolean not null default true,
  is_holiday boolean not null default false,
  is_legal_holiday boolean not null default false,
  is_makeup_workday boolean not null default false,
  source varchar(50) not null default 'timor',
  synced_by_user_id bigint,
  synced_at timestamptz not null default now(),
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  constraint uk_business_calendar_day_date unique (calendar_date)
);

CREATE INDEX IF NOT EXISTS idx_business_calendar_day_year_date
  ON business_calendar_day(calendar_year, calendar_date);

CREATE INDEX IF NOT EXISTS idx_business_calendar_day_type
  ON business_calendar_day(day_type, calendar_date);

-- Column comments
WITH column_comments(table_name, column_name, comment_text) AS (
  VALUES
    ('sys_user', 'id', '用户主键'),
    ('sys_user', 'user_id', '用户ID'),
    ('sys_user', 'user_code', '工号/员工编号'),
    ('sys_user', 'username', '登录账号'),
    ('sys_user', 'real_name', '姓名'),
    ('sys_user', 'display_name', '显示名称'),
    ('sys_user', 'mobile', '手机号'),
    ('sys_user', 'phone', '手机号'),
    ('sys_user', 'email', '邮箱'),
    ('sys_user', 'org_id', '所属组织ID'),
    ('sys_user', 'job_title', '职位'),
    ('sys_user', 'status', '用户状态'),
    ('sys_user', 'hire_date', '入职日期'),
    ('sys_user', 'avatar_url', '头像地址'),
    ('sys_user', 'password_hash', '密码哈希'),
    ('sys_user', 'created_at', '创建时间'),
    ('sys_user', 'updated_at', '更新时间'),
    ('sys_user', 'is_deleted', '逻辑删除标记'),
    ('sys_user', 'pwd_reset_required', '首次登录是否需要重置密码'),
    ('sys_user', 'is_platform_admin', '是否平台管理员'),

    ('crm_customer', 'id', '客户主键'),
    ('crm_customer', 'customer_code', '客户编码'),
    ('crm_customer', 'customer_name', '客户名称'),
    ('crm_customer', 'customer_type', '客户类型'),
    ('crm_customer', 'industry', '行业'),
    ('crm_customer', 'province', '省份'),
    ('crm_customer', 'city', '城市'),
    ('crm_customer', 'address', '详细地址'),
    ('crm_customer', 'owner_user_id', '当前负责人ID'),
    ('crm_customer', 'source', '客户来源'),
    ('crm_customer', 'status', '客户状态'),
    ('crm_customer', 'remark', '备注说明'),
    ('crm_customer', 'created_at', '创建时间'),
    ('crm_customer', 'updated_at', '更新时间'),

    ('crm_opportunity', 'id', '商机主键'),
    ('crm_opportunity', 'opportunity_code', '商机编号'),
    ('crm_opportunity', 'opportunity_name', '商机名称'),
    ('crm_opportunity', 'customer_id', '客户ID'),
    ('crm_opportunity', 'owner_user_id', '商机负责人ID'),
    ('crm_opportunity', 'sales_expansion_id', '关联销售拓展ID'),
    ('crm_opportunity', 'channel_expansion_id', '关联渠道拓展ID'),
    ('crm_opportunity', 'pre_sales_id', '售前ID'),
    ('crm_opportunity', 'pre_sales_name', '售前姓名'),
    ('crm_opportunity', 'project_location', '项目所在地'),
    ('crm_opportunity', 'project_ownership_location', '业绩归属地编码，对应 cnarea.area_code'),
    ('crm_opportunity', 'operator_name', '运作方'),
    ('crm_opportunity', 'amount', '商机金额'),
    ('crm_opportunity', 'actual_signed_amount', '实际签约金额'),
    ('crm_opportunity', 'is_poc', '是否POC测试项目'),
    ('crm_opportunity', 'expected_close_date', '预计结单日期'),
    ('crm_opportunity', 'confidence_pct', '把握度等级(A/B/C)'),
    ('crm_opportunity', 'stage', '商机阶段'),
    ('crm_opportunity', 'opportunity_type', '商机类型'),
    ('crm_opportunity', 'product_type', '产品类型'),
    ('crm_opportunity', 'source', '商机来源'),
    ('crm_opportunity', 'competitor_name', '竞品名称'),
    ('crm_opportunity', 'latest_progress', '项目最新进展'),
    ('crm_opportunity', 'next_plan', '下一步销售计划'),
    ('crm_opportunity', 'updated_by', '更新人ID'),
    ('crm_opportunity', 'archived', '是否归档'),
    ('crm_opportunity', 'archived_at', '归档时间'),
    ('crm_opportunity', 'pushed_to_oms', '是否已推送OMS'),
    ('crm_opportunity', 'oms_push_time', '推送OMS时间'),
    ('crm_opportunity', 'description', '商机说明/备注'),
    ('crm_opportunity', 'status', '商机状态'),
    ('crm_opportunity', 'created_at', '创建时间'),
    ('crm_opportunity', 'updated_at', '更新时间'),

    ('crm_opportunity_followup', 'id', '跟进记录主键'),
    ('crm_opportunity_followup', 'opportunity_id', '商机ID'),
    ('crm_opportunity_followup', 'followup_time', '跟进时间'),
    ('crm_opportunity_followup', 'followup_type', '跟进方式'),
    ('crm_opportunity_followup', 'content', '跟进内容'),
    ('crm_opportunity_followup', 'next_action', '下一步动作'),
    ('crm_opportunity_followup', 'followup_user_id', '跟进人ID'),
    ('crm_opportunity_followup', 'source_type', '来源类型'),
    ('crm_opportunity_followup', 'source_id', '来源记录ID'),
    ('crm_opportunity_followup', 'created_at', '创建时间'),
    ('crm_opportunity_followup', 'updated_at', '更新时间'),

    ('crm_sales_expansion', 'id', '销售拓展主键'),
    ('crm_sales_expansion', 'employee_no', '工号/员工编号'),
    ('crm_sales_expansion', 'candidate_name', '候选人姓名'),
    ('crm_sales_expansion', 'office_name', '办事处/代表处'),
    ('crm_sales_expansion', 'mobile', '手机号'),
    ('crm_sales_expansion', 'email', '邮箱'),
    ('crm_sales_expansion', 'target_dept', '所属部门'),
    ('crm_sales_expansion', 'industry', '所属行业'),
    ('crm_sales_expansion', 'title', '职务'),
    ('crm_sales_expansion', 'intent_level', '合作意向'),
    ('crm_sales_expansion', 'stage', '跟进阶段'),
    ('crm_sales_expansion', 'has_desktop_exp', '是否有云桌面经验'),
    ('crm_sales_expansion', 'in_progress', '是否持续跟进中'),
    ('crm_sales_expansion', 'employment_status', '候选人状态'),
    ('crm_sales_expansion', 'expected_join_date', '预计入职日期'),
    ('crm_sales_expansion', 'owner_user_id', '负责人ID'),
    ('crm_sales_expansion', 'remark', '备注说明'),
    ('crm_sales_expansion', 'created_at', '创建时间'),
    ('crm_sales_expansion', 'updated_at', '更新时间'),

    ('crm_channel_expansion', 'id', '渠道拓展主键'),
    ('crm_channel_expansion', 'channel_code', '渠道编码'),
    ('crm_channel_expansion', 'province', '省份'),
    ('crm_channel_expansion', 'city', '市'),
    ('crm_channel_expansion', 'channel_name', '渠道名称'),
    ('crm_channel_expansion', 'office_address', '办公地址'),
    ('crm_channel_expansion', 'channel_industry', '聚焦行业'),
    ('crm_channel_expansion', 'certification_level', '汇智内部认证级别'),
    ('crm_channel_expansion', 'industry', '行业(兼容旧字段)'),
    ('crm_channel_expansion', 'annual_revenue', '年度营业额(万元)'),
    ('crm_channel_expansion', 'staff_size', '人员规模'),
    ('crm_channel_expansion', 'contact_established_date', '建立联系日期'),
    ('crm_channel_expansion', 'intent_level', '合作意向'),
    ('crm_channel_expansion', 'has_desktop_exp', '是否有云桌面经验'),
    ('crm_channel_expansion', 'contact_name', '主联系人姓名(兼容旧结构)'),
    ('crm_channel_expansion', 'contact_title', '主联系人职务(兼容旧结构)'),
    ('crm_channel_expansion', 'contact_mobile', '主联系人电话(兼容旧结构)'),
    ('crm_channel_expansion', 'channel_attribute', '渠道属性编码，多个值逗号分隔'),
    ('crm_channel_expansion', 'internal_attribute', '新华三内部属性编码，多个值逗号分隔'),
    ('crm_channel_expansion', 'stage', '渠道合作阶段'),
    ('crm_channel_expansion', 'landed_flag', '是否已落地'),
    ('crm_channel_expansion', 'expected_sign_date', '预计签约日期'),
    ('crm_channel_expansion', 'owner_user_id', '负责人ID'),
    ('crm_channel_expansion', 'remark', '备注说明'),
    ('crm_channel_expansion', 'created_at', '创建时间'),
    ('crm_channel_expansion', 'updated_at', '更新时间'),

    ('crm_channel_expansion_contact', 'id', '联系人主键'),
    ('crm_channel_expansion_contact', 'channel_expansion_id', '渠道拓展ID'),
    ('crm_channel_expansion_contact', 'contact_name', '联系人姓名'),
    ('crm_channel_expansion_contact', 'contact_mobile', '联系人电话'),
    ('crm_channel_expansion_contact', 'contact_title', '联系人职务'),
    ('crm_channel_expansion_contact', 'sort_order', '排序号'),
    ('crm_channel_expansion_contact', 'created_at', '创建时间'),
    ('crm_channel_expansion_contact', 'updated_at', '更新时间'),

    ('crm_expansion_followup', 'id', '跟进记录主键'),
    ('crm_expansion_followup', 'biz_type', '业务类型'),
    ('crm_expansion_followup', 'biz_id', '业务对象ID'),
    ('crm_expansion_followup', 'followup_time', '跟进时间'),
    ('crm_expansion_followup', 'followup_type', '跟进方式'),
    ('crm_expansion_followup', 'content', '跟进内容'),
    ('crm_expansion_followup', 'next_action', '下一步动作'),
    ('crm_expansion_followup', 'followup_user_id', '跟进人ID'),
    ('crm_expansion_followup', 'visit_start_time', '拜访开始时间'),
    ('crm_expansion_followup', 'evaluation_content', '评估内容'),
    ('crm_expansion_followup', 'next_plan', '后续规划'),
    ('crm_expansion_followup', 'source_type', '来源类型'),
    ('crm_expansion_followup', 'source_id', '来源记录ID'),
    ('crm_expansion_followup', 'created_at', '创建时间'),
    ('crm_expansion_followup', 'updated_at', '更新时间'),

    ('work_checkin', 'id', '打卡记录主键'),
    ('work_checkin', 'user_id', '打卡人ID'),
    ('work_checkin', 'checkin_date', '打卡日期'),
    ('work_checkin', 'checkin_time', '打卡时间'),
    ('work_checkin', 'biz_type', '关联对象类型'),
    ('work_checkin', 'biz_id', '关联对象ID'),
    ('work_checkin', 'biz_name', '关联对象名称'),
    ('work_checkin', 'longitude', '经度'),
    ('work_checkin', 'latitude', '纬度'),
    ('work_checkin', 'location_text', '打卡地点'),
    ('work_checkin', 'remark', '备注说明(含现场照片元数据)'),
    ('work_checkin', 'user_name', '打卡人姓名快照'),
    ('work_checkin', 'dept_name', '所属部门快照'),
    ('work_checkin', 'status', '打卡状态'),
    ('work_checkin', 'created_at', '创建时间'),
    ('work_checkin', 'updated_at', '更新时间'),

    ('work_daily_report', 'id', '日报主键'),
    ('work_daily_report', 'user_id', '提交人ID'),
    ('work_daily_report', 'report_date', '日报日期'),
    ('work_daily_report', 'work_content', '今日工作内容(含结构化明细元数据)'),
    ('work_daily_report', 'tomorrow_plan', '明日工作计划(含结构化计划项元数据)'),
    ('work_daily_report', 'source_type', '提交来源'),
    ('work_daily_report', 'submit_time', '提交时间'),
    ('work_daily_report', 'status', '日报状态'),
    ('work_daily_report', 'score', '日报评分'),
    ('work_daily_report', 'created_at', '创建时间'),
    ('work_daily_report', 'updated_at', '更新时间'),

    ('work_daily_report_comment', 'id', '点评记录主键'),
    ('work_daily_report_comment', 'report_id', '日报ID'),
    ('work_daily_report_comment', 'reviewer_user_id', '点评人ID'),
    ('work_daily_report_comment', 'score', '点评评分'),
    ('work_daily_report_comment', 'comment_content', '点评内容'),
    ('work_daily_report_comment', 'reviewed_at', '点评时间'),
    ('work_daily_report_comment', 'created_at', '创建时间'),

    ('work_todo', 'id', '待办主键'),
    ('work_todo', 'user_id', '所属用户ID'),
    ('work_todo', 'title', '待办标题'),
    ('work_todo', 'biz_type', '业务类型'),
    ('work_todo', 'biz_id', '业务对象ID'),
    ('work_todo', 'due_date', '截止时间'),
    ('work_todo', 'status', '待办状态'),
    ('work_todo', 'priority', '优先级'),
    ('work_todo', 'created_at', '创建时间'),
    ('work_todo', 'updated_at', '更新时间'),

    ('business_calendar_day', 'id', '业务日历主键'),
    ('business_calendar_day', 'calendar_date', '日历日期'),
    ('business_calendar_day', 'calendar_year', '年份'),
    ('business_calendar_day', 'day_of_week', '星期，1-7 对应周一到周日'),
    ('business_calendar_day', 'day_type_code', '日历类型编码：0工作日、1周末休息、2节假日、3调休补班'),
    ('business_calendar_day', 'day_type', '日历类型'),
    ('business_calendar_day', 'day_name', '日历展示名称'),
    ('business_calendar_day', 'holiday_name', '节假日名称'),
    ('business_calendar_day', 'holiday_target', '调休关联节日'),
    ('business_calendar_day', 'holiday_wage', '节假日工资倍数'),
    ('business_calendar_day', 'is_weekend', '是否周末'),
    ('business_calendar_day', 'is_rest_day', '是否休息日'),
    ('business_calendar_day', 'is_workday', '是否工作日'),
    ('business_calendar_day', 'is_holiday', '是否节假日'),
    ('business_calendar_day', 'is_legal_holiday', '是否法定节假日'),
    ('business_calendar_day', 'is_makeup_workday', '是否法定调休补班'),
    ('business_calendar_day', 'source', '日历来源'),
    ('business_calendar_day', 'synced_by_user_id', '最近同步人ID'),
    ('business_calendar_day', 'synced_at', '最近同步时间'),
    ('business_calendar_day', 'created_at', '创建时间'),
    ('business_calendar_day', 'updated_at', '更新时间'),

    ('sys_activity_log', 'id', '动态主键'),
    ('sys_activity_log', 'biz_type', '业务类型'),
    ('sys_activity_log', 'biz_id', '业务对象ID'),
    ('sys_activity_log', 'action_type', '动作类型'),
    ('sys_activity_log', 'title', '动态标题'),
    ('sys_activity_log', 'content', '动态内容'),
    ('sys_activity_log', 'operator_user_id', '操作人ID'),
    ('sys_activity_log', 'created_at', '创建时间')
)
SELECT comment_on_column_if_exists(table_name, column_name, comment_text)
FROM column_comments;

INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order, status, is_deleted, remark)
SELECT v.type_code, v.item_label, v.item_value, v.sort_order, 1, 0, '商机建设类型'
FROM (
  VALUES
    ('sj_jslx', '新建', '新建', 1),
    ('sj_jslx', '扩容', '扩容', 2),
    ('sj_jslx', '替换', '替换', 3),
    ('sj_xmbwd', 'A', 'A', 1),
    ('sj_xmbwd', 'B', 'B', 2),
    ('sj_xmbwd', 'C', 'C', 3)
) AS v(type_code, item_label, item_value, sort_order)
WHERE NOT EXISTS (
  SELECT 1
  FROM sys_dict_item s
  WHERE s.type_code = v.type_code
    AND s.item_value = v.item_value
    AND COALESCE(s.is_deleted, 0) = 0
);

commit;
