<template>
  <div class="schedule-overview-page" v-loading="loading">
    <lx-header icon="md-apps" :text="$route.meta.title" style="margin-bottom: 10px; border: 0; margin-top: 15px;">
      <div slot="content" class="header-actions">
        <el-date-picker
          v-model="currentYear"
          type="year"
          value-format="yyyy"
          format="yyyy"
          placeholder="选择年份"
          size="small"
          style="width: 120px;"
          @change="handleYearChange"
        />
        <el-button size="small" plain @click="openSystemCourseManager">计划体系管理</el-button>
        <el-button size="small" type="primary" plain @click="openScheduleManager">带班管理</el-button>
        <el-button size="small" plain @click="openLocationManager">新增地点</el-button>
        <el-button size="small" plain @click="openOwnerManager">新增负责人</el-button>
      </div>
    </lx-header>

    <summary-panel :summary-cards="summaryCards" />

    <monthly-heatmap
      :month-labels="monthLabels"
      :monthly-stats="monthlyStats"
      :heat-class="heatClass"
    />

    <member-overview :member-analysis="memberAnalysis" />

    <plan-matrix
      :year="currentYear"
      :month-labels="monthLabels"
      :plan-rows="planRows"
      @cell-click="handlePlanCellClick"
    />

    <system-course-manager-dialog
      :visible.sync="systemCourseDialogVisible"
      :tree-data="systemCourseTreeData"
      :selected-system="selectedSystem"
      :selected-tree-node="selectedTreeNode"
      :current-key="selectedTreeNodeKey"
      @tree-node-select="handleTreeNodeSelect"
      @open-system-form="openSystemForm"
      @open-course-form="handleOpenCourseFormFromDialog"
      @delete-system="handleDeleteSystem"
      @delete-course="handleDeleteCourse"
    />

    <schedule-manager-dialog
      :visible.sync="scheduleManagerDialogVisible"
      :rows="scheduleTableRows"
      :span-method="scheduleSpanMethod"
      @open-schedule-form="openScheduleForm"
      @delete-schedule="handleDeleteSchedule"
    />

    <system-form-dialog
      :visible.sync="systemFormVisible"
      :mode="systemFormMode"
      :form="systemForm"
      :rules="systemRules"
      @submit="submitSystemForm"
    />

    <course-form-dialog
      :visible.sync="courseFormVisible"
      :mode="courseFormMode"
      :form="courseForm"
      :rules="courseRules"
      :systems="systems"
      :course-types="courseTypes"
      @course-type-change="(val) => { courseForm.name = getCourseTypeNameById(val) || courseForm.name }"
      @submit="submitCourseForm"
    />

    <schedule-form-dialog
      :visible.sync="scheduleFormVisible"
      :mode="scheduleFormMode"
      :form="scheduleForm"
      :rules="scheduleRules"
      :systems="scheduleFormSystems"
      :course-options="scheduleFormCourseOptions"
      :month-options="monthOptions"
      :locations="locations"
      :owners="owners"
      @year-change="handleScheduleYearChange"
      @system-change="handleScheduleSystemChange"
      @open-location-manager="openLocationManager"
      @open-owner-manager="openOwnerManager"
      @cancel="handleScheduleDialogCancel"
      @delete="handleDeleteSchedule"
      @submit="submitScheduleForm"
    />

    <location-manager-dialog
      :visible.sync="locationManagerDialogVisible"
      :list="locations"
      @open="loadLocations"
      @add="handleAddLocation"
      @edit="handleEditLocation"
      @delete="handleDeleteLocation"
    />

    <owner-manager-dialog
      :visible.sync="ownerManagerDialogVisible"
      :list="owners"
      @open="loadOwners"
      @add="handleAddOwner"
      @edit="handleEditOwner"
      @delete="handleDeleteOwner"
    />
  </div>
</template>

<script>
import SummaryPanel from './components/SummaryPanel.vue'
import MonthlyHeatmap from './components/MonthlyHeatmap.vue'
import MemberOverview from './components/MemberOverview.vue'
import PlanMatrix from './components/PlanMatrix.vue'
import SystemCourseManagerDialog from './components/SystemCourseManagerDialog.vue'
import ScheduleManagerDialog from './components/ScheduleManagerDialog.vue'
import SystemFormDialog from './components/SystemFormDialog.vue'
import CourseFormDialog from './components/CourseFormDialog.vue'
import ScheduleFormDialog from './components/ScheduleFormDialog.vue'
import LocationManagerDialog from './components/LocationManagerDialog.vue'
import OwnerManagerDialog from './components/OwnerManagerDialog.vue'
import { index as courseTypeIndex } from '@/api/course/courseType.js'
import {
  destroyCourse,
  destroySchedule,
  destroySystem,
  getOverview,
  getLocationList,
  getOwnerList,
  saveCourse,
  saveLocation,
  saveOwner,
  destroyLocation,
  destroyOwner,
  getScheduleList,
  saveSchedule,
  saveSystem
} from '@/api/scheduleOverview/index'

const getCurrentYear = () => String(new Date().getFullYear())

const createSystemForm = () => ({
  id: '',
  year: getCurrentYear(),
  name: '',
  sort: 1
})

const createCourseForm = (systemId = '') => ({
  id: '',
  system_id: systemId || '',
  course_type_id: '',
  name: '',
  sort: 1
})

const createScheduleForm = () => ({
  id: '',
  year: getCurrentYear(),
  system_id: '',
  course_id: '',
  title: '',
  modules: []
})

const TONE_CLASSES = ['tone-green', 'tone-blue', 'tone-purple', 'tone-sand', 'tone-cyan', 'tone-cream']
const HEAT_LEVELS = [
  { min: 0, class: 'heat-empty' },
  { min: 1, class: 'heat-level-1' },
  { min: 2, class: 'heat-level-2' },
  { min: 3, class: 'heat-level-3' },
  { min: 4, class: 'heat-level-4' }
]

export default {
  name: 'ScheduleOverview',
  components: {
    SummaryPanel,
    MonthlyHeatmap,
    MemberOverview,
    PlanMatrix,
    SystemCourseManagerDialog,
    ScheduleManagerDialog,
    SystemFormDialog,
    CourseFormDialog,
    ScheduleFormDialog,
    LocationManagerDialog,
    OwnerManagerDialog
  },
  data() {
    return {
      systems: [],
      courses: [],
      schedules: [],
      scheduleGroups: [],
      courseTypes: [],
      locations: [],
      owners: [],
      loading: false,
      monthLabels: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
      monthOptions: Array.from({ length: 12 }, (_, i) => ({ value: i + 1, label: `${i + 1}月` })),
      selectedSystemId: '',
      selectedTreeNodeKey: '',
      scheduleFormSystems: [],
      scheduleFormCourses: [],
      systemCourseDialogVisible: false,
      scheduleManagerDialogVisible: false,
      locationManagerDialogVisible: false,
      ownerManagerDialogVisible: false,
      systemFormVisible: false,
      courseFormVisible: false,
      scheduleFormVisible: false,
      systemFormMode: 'add',
      courseFormMode: 'add',
      scheduleFormMode: 'add',
      systemForm: createSystemForm(),
      courseForm: createCourseForm(),
      scheduleForm: createScheduleForm(),
      systemRules: {
        year: [{ required: true, message: '请选择年份', trigger: 'change' }],
        name: [{ required: true, message: '请输入计划体系名称', trigger: 'blur' }],
        sort: [{ required: true, message: '请输入排序', trigger: 'change' }]
      },
      courseRules: {
        system_id: [{ required: true, message: '请选择所属计划体系', trigger: 'change' }],
        course_type_id: [{ required: true, message: '请选择课程体系', trigger: 'change' }],
        sort: [{ required: true, message: '请输入排序', trigger: 'change' }]
      },
      scheduleRules: {
        year: [{ required: true, message: '请选择年份', trigger: 'change' }],
        system_id: [{ required: true, message: '请选择计划体系', trigger: 'change' }],
        modules: [{
          trigger: 'change',
          validator: (rule, value, callback) => {
            if (!Array.isArray(value) || !value.length) {
              callback(new Error('模块/期数至少需要一条数据'))
              return
            }
            callback()
          }
        }],
        title: [{ required: true, message: '请输入课程名称', trigger: 'blur' }],
        // modules 内部必填项由子组件在提交前做校验提示
      },
      currentYear: getCurrentYear()
    }
  },
  computed: {
    selectedSystem() {
      return this.systems.find((item) => String(item.id) === String(this.selectedSystemId)) || null
    },
    selectedTreeNode() {
      const key = String(this.selectedTreeNodeKey || '')
      if (!key) {
        return null
      }
      if (key.indexOf('system-') === 0) {
        const systemId = key.replace('system-', '')
        const system = this.systems.find((item) => String(item.id) === String(systemId))
        return system ? {
          treeKey: key,
          nodeType: 'system',
          name: system.name,
          year: system.year,
          sort: system.sort,
          raw: system
        } : null
      }
      if (key.indexOf('course-') === 0) {
        const courseId = key.replace('course-', '')
        const course = this.courses.find((item) => String(item.id) === String(courseId))
        const system = course ? this.systemMap[course.system_id] : null
        return course ? {
          treeKey: key,
          nodeType: 'course',
          name: course.name,
          year: system ? system.year : '-',
          sort: course.sort,
          systemName: system ? system.name : '-',
          raw: course
        } : null
      }
      return null
    },
    selectedSystemCourses() {
      return this.courses.filter((item) => String(item.system_id) === String(this.selectedSystemId))
    },
    systemCourseTreeData() {
      return this.systems.map((system) => ({
        treeKey: `system-${system.id}`,
        id: system.id,
        year: system.year,
        name: system.name,
        sort: system.sort,
        nodeType: 'system',
        raw: system,
        children: this.courses
          .filter((item) => String(item.system_id) === String(system.id))
          .sort((a, b) => Number(a.sort || 0) - Number(b.sort || 0))
          .map((course) => ({
            treeKey: `course-${course.id}`,
            id: course.id,
            name: course.name,
            sort: course.sort,
            nodeType: 'course',
            systemName: system.name,
            raw: course
          }))
      }))
    },
    scheduleFormCourseOptions() {
      return this.scheduleFormCourses.filter((item) => String(item.system_id) === String(this.scheduleForm.system_id))
    },
    summaryCards() {
      const totalCount = this.schedules.length
      const owners = [...new Set(this.schedules.map((item) => item.owner).filter(Boolean))]
      const activeMonths = [...new Set(this.schedules.map((item) => Number(item.month)).filter(Boolean))]
      const monthCounter = this.buildCounter(this.schedules, (item) => `${item.month}月`)
      const ownerCounter = this.buildCounter(this.schedules, (item) => item.owner || '未分配')
      const peakMonth = this.getTopCounterEntry(monthCounter)
      const peakOwner = this.getTopCounterEntry(ownerCounter)

      return [
        { label: '总开班次数(周期)', value: String(totalCount), unit: '次' },
        { label: '参与人员', value: String(owners.length), unit: '人' },
        { label: '活跃月度', value: String(activeMonths.length), unit: '个月' },
        { label: '峰值月度', value: peakMonth ? `${peakMonth.label}/${peakMonth.value}` : '-', unit: peakMonth ? '次' : '' },
        { label: '峰值人员', value: peakOwner ? `${peakOwner.label}/${peakOwner.value}` : '-', unit: peakOwner ? '次' : '' }
      ]
    },
    monthlyStats() {
      const ownerStatsMap = {}

      this.schedules.forEach((item) => {
        const name = item.owner || '未分配'
        if (!ownerStatsMap[name]) {
          ownerStatsMap[name] = {
            name,
            months: Array(12).fill(0),
            total: 0
          }
        }
        const monthIndex = Number(item.month) - 1
        if (monthIndex >= 0 && monthIndex < 12) {
          ownerStatsMap[name].months[monthIndex] += 1
          ownerStatsMap[name].total += 1
        }
      })

      return Object.values(ownerStatsMap).sort((a, b) => {
        if (b.total !== a.total) {
          return b.total - a.total
        }
        return a.name.localeCompare(b.name, 'zh-CN')
      })
    },
    memberAnalysis() {
      const systemMap = this.systemMap
      const statsMap = {}

      this.schedules.forEach((item) => {
        const name = item.owner || '未分配'
        if (!statsMap[name]) {
          statsMap[name] = {
            total: 0,
            monthCounter: {},
            locationCounter: {},
            systemCounter: {}
          }
        }

        statsMap[name].total += 1
        this.incrementCounter(statsMap[name].monthCounter, `${item.month}月`)
        this.incrementCounter(statsMap[name].locationCounter, item.location || '-')
        this.incrementCounter(statsMap[name].systemCounter, (systemMap[item.system_id] || {}).name || '-')
      })

      return Object.keys(statsMap).map((name) => ({
        name,
        total: `${statsMap[name].total}次`,
        month: this.getTopCounterEntry(statsMap[name].monthCounter)?.label || '-',
        location: this.getTopCounterEntry(statsMap[name].locationCounter)?.label || '-',
        tag: this.getTopCounterEntry(statsMap[name].systemCounter)?.label || '-'
      })).sort((a, b) => Number(b.total.replace('次', '')) - Number(a.total.replace('次', '')))
    },
    systemMap() {
      return this.systems.reduce((map, item) => {
        map[item.id] = item
        return map
      }, {})
    },
    courseMap() {
      return this.courses.reduce((map, item) => {
        map[item.id] = item
        return map
      }, {})
    },
    scheduleTableRows() {
      const groups = Array.isArray(this.scheduleGroups) ? this.scheduleGroups : []
      const rows = groups.reduce((list, group) => {
        const modules = Array.isArray(group.modules) ? group.modules : []
        if (!modules.length) {
          list.push({
            group_id: group.id,
            year: group.year,
            system_id: group.system_id,
            course_id: group.course_id,
            title: group.title,
            moduleName: '-',
            month: '',
            monthLabel: '-',
            location: '-',
            owner: '-',
            count_text: '',
            systemName: (this.systemMap[group.system_id] || {}).name || '-',
            courseName: (this.courseMap[group.course_id] || {}).name || '-'
          })
          return list
        }
        modules.forEach((m) => {
          list.push({
            id: m.id,
            group_id: group.id,
            year: group.year,
            system_id: group.system_id,
            course_id: group.course_id,
            title: group.title,
            moduleName: m.name || '-',
            month: m.month,
            monthLabel: m.month ? `${m.month}月` : '-',
            location: (m.location || {}).name || '-',
            owner: (m.owner || {}).name || '-',
            count_text: m.count_text || '',
            systemName: (this.systemMap[group.system_id] || {}).name || '-',
            courseName: (this.courseMap[group.course_id] || {}).name || '-'
          })
        })
        return list
      }, [])

      return rows.sort((a, b) => {
        const systemA = this.systemMap[a.system_id] || {}
        const systemB = this.systemMap[b.system_id] || {}
        const systemSortDiff = Number(systemA.sort || 0) - Number(systemB.sort || 0)
        if (systemSortDiff !== 0) {
          return systemSortDiff
        }
        const systemNameDiff = String(systemA.name || '').localeCompare(String(systemB.name || ''), 'zh-CN')
        if (systemNameDiff !== 0) {
          return systemNameDiff
        }
        const courseA = this.courseMap[a.course_id] || {}
        const courseB = this.courseMap[b.course_id] || {}
        const courseSortDiff = Number(courseA.sort || 0) - Number(courseB.sort || 0)
        if (courseSortDiff !== 0) {
          return courseSortDiff
        }
        const courseNameDiff = String(courseA.name || '').localeCompare(String(courseB.name || ''), 'zh-CN')
        if (courseNameDiff !== 0) {
          return courseNameDiff
        }
        // 先按课程名称（group_id）聚在一起，以便相同课程名称合并单元格；再按月份排序
        const groupDiff = String(a.group_id || '').localeCompare(String(b.group_id || ''))
        if (groupDiff !== 0) {
          return groupDiff
        }
        return Number(a.month || 0) - Number(b.month || 0)
      })
    },
    scheduleSpanMeta() {
      const rows = this.scheduleTableRows
      const systemMeta = rows.map(() => ({ rowspan: 1, colspan: 1 }))
      const courseMeta = rows.map(() => ({ rowspan: 1, colspan: 1 }))
      const titleMeta = rows.map(() => ({ rowspan: 1, colspan: 1 }))
      const opMeta = rows.map(() => ({ rowspan: 1, colspan: 1 }))

      // 计划体系列：按 system_id 合并连续行
      let i = 0
      while (i < rows.length) {
        const systemKey = String(rows[i].system_id)
        let j = i + 1
        while (j < rows.length && String(rows[j].system_id) === systemKey) {
          j += 1
        }
        const span = j - i
        systemMeta[i] = { rowspan: span, colspan: 1 }
        for (let k = i + 1; k < j; k += 1) {
          systemMeta[k] = { rowspan: 0, colspan: 0 }
        }
        i = j
      }

      // 课程体系列、课程名称列：在同一 system_id 分组内，按 course_id 合并；课程名称再按 group_id 合并
      i = 0
      while (i < rows.length) {
        const systemKey = String(rows[i].system_id)
        let j = i + 1
        while (j < rows.length && String(rows[j].system_id) === systemKey) {
          j += 1
        }

        let p = i
        while (p < j) {
          const courseKey = String(rows[p].course_id)
          let q = p + 1
          while (q < j && String(rows[q].course_id) === courseKey) {
            q += 1
          }
          const courseSpan = q - p
          courseMeta[p] = { rowspan: courseSpan, colspan: 1 }
          for (let k = p + 1; k < q; k += 1) {
            courseMeta[k] = { rowspan: 0, colspan: 0 }
          }

          // 课程名称列、操作列：在同一 course 分组内，按 group_id 合并连续行
          let u = p
          while (u < q) {
            const groupKey = String(rows[u].group_id || '')
            let v = u + 1
            while (v < q && String(rows[v].group_id || '') === groupKey) {
              v += 1
            }
            const span = v - u
            titleMeta[u] = { rowspan: span, colspan: 1 }
            opMeta[u] = { rowspan: span, colspan: 1 }
            for (let k = u + 1; k < v; k += 1) {
              titleMeta[k] = { rowspan: 0, colspan: 0 }
              opMeta[k] = { rowspan: 0, colspan: 0 }
            }
            u = v
          }

          p = q
        }

        i = j
      }

      return { systemMeta, courseMeta, titleMeta, opMeta }
    },
    planRows() {
      const rows = []
      const sortedSystems = [...this.systems].sort((a, b) => Number(a.sort || 0) - Number(b.sort || 0))
      const groups = Array.isArray(this.scheduleGroups) ? this.scheduleGroups : []

      sortedSystems.forEach((system, systemIndex) => {
        const systemGroups = groups
          .filter((g) => String(g.system_id) === String(system.id))
          .sort((a, b) => {
            const courseA = a.course || {}
            const courseB = b.course || {}
            const nameA = String(courseA.name || '')
            const nameB = String(courseB.name || '')

            // 先按是否有课程体系排序：有名称的排在前面，没有名称的排在后面
            const hasCourseA = nameA !== ''
            const hasCourseB = nameB !== ''
            if (hasCourseA !== hasCourseB) {
              return hasCourseA ? -1 : 1
            }

            const sortDiff = Number(courseA.sort || 0) - Number(courseB.sort || 0)
            if (sortDiff !== 0) return sortDiff

            const nameDiff = nameA.localeCompare(nameB, 'zh-CN')
            if (nameDiff !== 0) return nameDiff
            return String(a.title || '').localeCompare(String(b.title || ''), 'zh-CN')
          })

        const groupClass = TONE_CLASSES[systemIndex % TONE_CLASSES.length]

        // 计划体系下所有课程体系（来自 courses 表）都要显示
        const allCoursesUnderSystem = this.courses
          .filter((c) => String(c.system_id) === String(system.id))
          .sort((a, b) => {
            const sortDiff = Number(a.sort || 0) - Number(b.sort || 0)
            if (sortDiff !== 0) return sortDiff
            return String(a.name || '').localeCompare(String(b.name || ''), 'zh-CN')
          })

        // 有带班的课程体系：按 group 展开为多行（同月份多模块换行）
        systemGroups.forEach((group) => {
          const modules = Array.isArray(group.modules) ? group.modules : []
          const monthMap = this.monthLabels.reduce((map, monthLabel) => {
            const monthNumber = Number(monthLabel.replace('月', ''))
            map[monthLabel] = modules
              .filter((m) => Number(m.month) === monthNumber)
              .sort((a, b) => String(a.name || '').localeCompare(String(b.name || ''), 'zh-CN'))
              .map((m) => ({
                id: m.id,
                group_id: group.id,
                year: group.year,
                system_id: group.system_id,
                course_id: group.course_id,
                month: m.month,
                title: group.title || '',
                moduleName: m.name || '',
                owner: (m.owner || {}).name || '',
                location: (m.location || {}).name || '',
                count_text: m.count_text || '',
                ownerLocation: `${(m.owner || {}).name || '-'} / ${(m.location || {}).name || '-'}`,
                countText: m.count_text || ''
              }))
            return map
          }, {})

          const maxModules = Math.max(
            1,
            ...this.monthLabels.map((monthLabel) => (monthMap[monthLabel] || []).length || 0)
          )

          for (let idx = 0; idx < maxModules; idx += 1) {
            const plan = this.monthLabels.reduce((map, monthLabel) => {
              const list = monthMap[monthLabel] || []
              map[monthLabel] = list[idx] ? [list[idx]] : []
              return map
            }, {})

            rows.push({
              rowKey: `${system.id}-${group.course_id || 'none'}-${group.id}-${idx}`,
              systemId: system.id,
              group: system.name,
              groupClass,
              courseId: group.course_id || '',
              course: (group.course || {}).name || '-',
              plan
            })
          }
        })

        // 追加：尚未有带班的课程体系也各占一行（空数据）
        const appearedCourseIds = new Set(systemGroups.map((g) => String(g.course_id || '')).filter(Boolean))
        allCoursesUnderSystem.forEach((course) => {
          if (!appearedCourseIds.has(String(course.id))) {
            const emptyPlan = this.monthLabels.reduce((map, monthLabel) => {
              map[monthLabel] = []
              return map
            }, {})
            rows.push({
              rowKey: `${system.id}-${course.id}-empty`,
              systemId: system.id,
              group: system.name,
              groupClass,
              courseId: course.id,
              course: course.name || '-',
              plan: emptyPlan
            })
          }
        })

        // 若该计划体系下没有任何课程体系配置，至少显示一行空占位
        if (!allCoursesUnderSystem.length && !systemGroups.length) {
          const emptyPlan = this.monthLabels.reduce((map, monthLabel) => {
            map[monthLabel] = []
            return map
          }, {})
          rows.push({
            rowKey: `${system.id}-empty`,
            systemId: system.id,
            group: system.name,
            groupClass,
            courseId: '',
            course: '-',
            plan: emptyPlan
          })
        }
      })

      return rows
    }
  },
  created() {
    this.loadData()
    this.loadCourseTypes()
    this.loadLocations()
    this.loadOwners()
  },
  methods: {
    async loadLocations() {
      try {
        const res = await getLocationList()
        this.locations = Array.isArray(res) ? res : (res.data || res || [])
      } catch (e) {
        this.locations = []
      }
    },
    async loadOwners() {
      try {
        const res = await getOwnerList()
        this.owners = Array.isArray(res) ? res : (res.data || res || [])
      } catch (e) {
        this.owners = []
      }
    },
    async handleAddLocation(payload) {
      await saveLocation({ name: payload.name, status: payload.status, sort: payload.sort })
      await this.loadLocations()
      this.$message.success('地点新增成功')
    },
    async handleEditLocation(payload) {
      await saveLocation({ id: payload.id, name: payload.name, status: payload.status, sort: payload.sort })
      await this.loadLocations()
      this.$message.success('地点保存成功')
    },
    handleDeleteLocation(row) {
      this.$confirm(`确定删除地点“${row.name}”吗？`, '提示', { type: 'warning' })
        .then(async() => {
          await destroyLocation({ id: row.id })
          await this.loadLocations()
          this.$message.success('地点删除成功')
        })
        .catch(() => {})
    },
    async handleAddOwner(payload) {
      await saveOwner({ name: payload.name, status: payload.status, sort: payload.sort })
      await this.loadOwners()
      this.$message.success('负责人新增成功')
    },
    async handleEditOwner(payload) {
      await saveOwner({ id: payload.id, name: payload.name, status: payload.status, sort: payload.sort })
      await this.loadOwners()
      this.$message.success('负责人保存成功')
    },
    handleDeleteOwner(row) {
      this.$confirm(`确定删除负责人“${row.name}”吗？`, '提示', { type: 'warning' })
        .then(async() => {
          await destroyOwner({ id: row.id })
          await this.loadOwners()
          this.$message.success('负责人删除成功')
        })
        .catch(() => {})
    },
    openLocationManager() {
      this.locationManagerDialogVisible = true
    },
    openOwnerManager() {
      this.ownerManagerDialogVisible = true
    },
    handleOpenCourseFormFromDialog(payload = {}) {
      const mode = payload.mode || 'add'
      const row = payload.row || null
      const targetSystem = payload.targetSystem || null
      this.openCourseForm(mode, row, targetSystem)
    },
    scheduleSpanMethod({ rowIndex, columnIndex, column }) {
      const spanMeta = this.scheduleSpanMeta

      // 操作列：固定列时右侧表格中 columnIndex 为 0，需优先用 label 识别
      if (column && column.label === '操作') {
        const meta = spanMeta.opMeta[rowIndex]
        return meta ? { rowspan: meta.rowspan, colspan: meta.colspan } : { rowspan: 1, colspan: 1 }
      }

      // 第1列：计划体系（按 system_id 合并）
      if (columnIndex === 0) {
        const meta = spanMeta.systemMeta[rowIndex]
        return meta ? { rowspan: meta.rowspan, colspan: meta.colspan } : { rowspan: 1, colspan: 1 }
      }

      // 第2列：课程体系（同一计划体系内按 course_id 合并）
      if (columnIndex === 1) {
        const meta = spanMeta.courseMeta[rowIndex]
        return meta ? { rowspan: meta.rowspan, colspan: meta.colspan } : { rowspan: 1, colspan: 1 }
      }

      // 第3列：课程名称（同一课程体系内按 group_id 合并）
      if (columnIndex === 2) {
        const meta = spanMeta.titleMeta[rowIndex]
        return meta ? { rowspan: meta.rowspan, colspan: meta.colspan } : { rowspan: 1, colspan: 1 }
      }

      // 第9列：操作（主表体中的列索引）
      if (columnIndex === 8) {
        const meta = spanMeta.opMeta[rowIndex]
        return meta ? { rowspan: meta.rowspan, colspan: meta.colspan } : { rowspan: 1, colspan: 1 }
      }
      return { rowspan: 1, colspan: 1 }
    },
    async loadCourseTypes() {
      try {
        const res = await courseTypeIndex({
          page: 1,
          page_size: 9999,
          sort_name: 'sort',
          sort_type: 'ASC',
          filter: [{
            key: 'status',
            op: 'like',
            value: 1
          }]
        }, true)
        this.courseTypes = (res && res.data) ? res.data : []
      } catch (e) {
        this.courseTypes = []
      }
    },
    getCourseTypeNameById(id) {
      const hit = this.courseTypes.find((item) => String(item.id) === String(id))
      return hit ? hit.name : ''
    },
    async loadData() {
      this.loading = true
      try {
        const data = await getOverview({ year: this.currentYear })
        this.systems = data.systems || []
        this.courses = data.courses || []
        this.schedules = data.schedules || []
        // 带班管理：使用 group 列表用于编辑/回显 modules
        this.scheduleGroups = await getScheduleList({ year: this.currentYear }) || []

      if (!this.selectedSystemId || !this.systems.find((item) => String(item.id) === String(this.selectedSystemId))) {
        this.selectedSystemId = this.systems[0] ? this.systems[0].id : ''
      }
      this.syncSelectedTreeNode()
      } finally {
        this.loading = false
      }
    },
    async loadScheduleFormOptions(year) {
      const data = await getOverview({ year: year || this.currentYear })
      this.scheduleFormSystems = data.systems || []
      this.scheduleFormCourses = data.courses || []
    },
    buildCounter(list, getter) {
      return list.reduce((map, item) => {
        const label = getter(item)
        this.incrementCounter(map, label)
        return map
      }, {})
    },
    incrementCounter(counter, key) {
      const safeKey = key || '-'
      counter[safeKey] = (counter[safeKey] || 0) + 1
    },
    getTopCounterEntry(counter) {
      return Object.keys(counter || {}).map((label) => ({
        label,
        value: counter[label]
      })).sort((a, b) => {
        if (b.value !== a.value) {
          return b.value - a.value
        }
        return a.label.localeCompare(b.label, 'zh-CN')
      })[0]
    },
    heatClass(value) {
      const n = Number(value) || 0
      for (let i = HEAT_LEVELS.length - 1; i >= 0; i--) {
        if (n >= HEAT_LEVELS[i].min) return HEAT_LEVELS[i].class
      }
      return 'heat-empty'
    },
    openSystemCourseManager() {
      this.systemCourseDialogVisible = true
      if (!this.selectedSystemId && this.systems[0]) {
        this.selectedSystemId = this.systems[0].id
      }
      if (!this.selectedTreeNodeKey && this.selectedSystemId) {
        this.selectedTreeNodeKey = `system-${this.selectedSystemId}`
      }
    },
    openScheduleManager() {
      this.scheduleManagerDialogVisible = true
    },
    handleYearChange(value) {
      this.currentYear = value || getCurrentYear()
      this.loadData()
    },
    openSystemForm(mode, row) {
      this.systemFormMode = mode
      this.systemForm = row ? { id: row.id, year: row.year || this.currentYear, name: row.name, sort: row.sort } : { ...createSystemForm(), year: this.currentYear }
      this.systemFormVisible = true
    },
    submitSystemForm() {
      ;(async() => {
        await saveSystem(this.systemForm)
        await this.loadData()
        if (this.systemForm.id) {
          this.selectedTreeNodeKey = `system-${this.systemForm.id}`
        }
        if (!this.selectedSystemId && this.systems[0]) {
          this.selectedSystemId = this.systems[0].id
        }
        if (!this.systemForm.id && this.selectedSystemId) {
          this.selectedTreeNodeKey = `system-${this.selectedSystemId}`
        }
        this.systemFormVisible = false
        this.$message.success('计划体系保存成功')
      })()
    },
    handleDeleteSystem(row) {
      this.$confirm(`删除计划体系“${row.name}”后，其下课程体系和带班也会一并删除，是否继续？`, '提示', {
        type: 'warning'
      }).then(async() => {
        await destroySystem({ id: row.id })
        await this.loadData()
        this.selectedTreeNodeKey = this.selectedSystemId ? `system-${this.selectedSystemId}` : ''
        this.$message.success('计划体系删除成功')
      }).catch(() => {})
    },
    openCourseForm(mode, row, targetSystem) {
      this.courseFormMode = mode
      if (row) {
        this.courseForm = {
          id: row.id,
          system_id: row.system_id,
          course_type_id: row.course_type_id || (row.course_type ? row.course_type.id : ''),
          name: row.name,
          sort: row.sort
        }
      } else {
        this.courseForm = createCourseForm(targetSystem ? targetSystem.id : this.selectedSystemId)
      }
      this.courseFormVisible = true
    },
    submitCourseForm() {
      ;(async() => {
        const selectedName = this.getCourseTypeNameById(this.courseForm.course_type_id)
        if (selectedName) {
          this.courseForm.name = selectedName
        }
        await saveCourse(this.courseForm)
        await this.loadData()
        this.selectedSystemId = this.courseForm.system_id
        if (this.courseForm.id) {
          this.selectedTreeNodeKey = `course-${this.courseForm.id}`
        }
        this.courseFormVisible = false
        this.$message.success('课程体系保存成功')
      })()
    },
    handleDeleteCourse(row) {
      this.$confirm(`删除课程体系“${row.name}”后，其下带班也会一并删除，是否继续？`, '提示', {
        type: 'warning'
      }).then(async() => {
        await destroyCourse({ id: row.id })
        await this.loadData()
        this.selectedTreeNodeKey = this.selectedSystemId ? `system-${this.selectedSystemId}` : ''
        this.$message.success('课程体系删除成功')
      }).catch(() => {})
    },
    handleTreeNodeSelect(data) {
      this.selectedTreeNodeKey = data.treeKey
      if (data.nodeType === 'system') {
        this.selectedSystemId = data.id
      } else if (data.raw && data.raw.system_id) {
        this.selectedSystemId = data.raw.system_id
      }
    },
    syncSelectedTreeNode() {
      const exists = this.systemCourseTreeData.some((systemNode) => {
        if (systemNode.treeKey === this.selectedTreeNodeKey) {
          return true
        }
        return (systemNode.children || []).some((courseNode) => courseNode.treeKey === this.selectedTreeNodeKey)
      })

      if (!exists) {
        this.selectedTreeNodeKey = this.selectedSystemId ? `system-${this.selectedSystemId}` : (this.systemCourseTreeData[0] ? this.systemCourseTreeData[0].treeKey : '')
      }
    },
    async openScheduleForm(mode, row, preset = {}) {
      this.scheduleFormMode = mode
      if (row) {
        const groupId = row.group_id || row.id
        const group = (Array.isArray(this.scheduleGroups) ? this.scheduleGroups : []).find((g) => String(g.id) === String(groupId))
        const modules = group && Array.isArray(group.modules) ? group.modules : []
        this.scheduleForm = {
          id: group ? group.id : groupId,
          year: (group ? group.year : row.year) || getCurrentYear(),
          system_id: group ? group.system_id : row.system_id,
          course_id: group ? group.course_id : row.course_id,
          title: group ? group.title : row.title,
          modules: modules.map((m) => ({
            id: m.id,
            name: m.name || '',
            month: m.month,
            location_id: m.location_id || (m.location || {}).id || '',
            owner_id: m.owner_id || (m.owner || {}).id || '',
            count_text: m.count_text || ''
          }))
        }
        await this.loadScheduleFormOptions(this.scheduleForm.year)
      } else {
        const defaultYear = preset.year || this.currentYear
        await this.loadScheduleFormOptions(defaultYear)
        const defaultSystemId = preset.system_id || this.selectedSystemId || (this.scheduleFormSystems[0] ? this.scheduleFormSystems[0].id : '')
        const defaultCourse = this.scheduleFormCourses.find((item) => String(item.system_id) === String(defaultSystemId))
        this.scheduleForm = {
          ...createScheduleForm(),
          year: defaultYear,
          system_id: defaultSystemId,
          course_id: preset.course_id || (defaultCourse ? defaultCourse.id : '')
        }
        if (preset.month) {
          this.scheduleForm.modules = [{
            name: '',
            month: preset.month,
            location_id: '',
            owner_id: '',
            count_text: ''
          }]
        }
      }
      this.scheduleFormVisible = true
    },
    handlePlanCellClick(row, monthLabel, items) {
      if (items && items.length) {
        this.openScheduleForm('edit', items[0])
        return
      }
      const month = Number(String(monthLabel).replace('月', ''))
      this.openScheduleForm('add', null, {
        year: this.currentYear,
        system_id: row.systemId,
        course_id: row.courseId,
        month
      })
    },
    async handleScheduleYearChange(value) {
      const selectedYear = value || getCurrentYear()
      this.scheduleForm.year = selectedYear
      await this.loadScheduleFormOptions(selectedYear)
      const firstSystem = this.scheduleFormSystems[0]
      const firstCourse = this.scheduleFormCourses.find((item) => firstSystem && String(item.system_id) === String(firstSystem.id))
      this.scheduleForm.system_id = firstSystem ? firstSystem.id : ''
      this.scheduleForm.course_id = firstCourse ? firstCourse.id : ''
      this.scheduleForm.month = []
    },
    handleScheduleSystemChange(value) {
      const currentCourseExists = this.scheduleFormCourses.find((item) => String(item.id) === String(this.scheduleForm.course_id) && String(item.system_id) === String(value))
      if (!currentCourseExists) {
        const firstCourse = this.scheduleFormCourses.find((item) => String(item.system_id) === String(value))
        this.scheduleForm.course_id = firstCourse ? firstCourse.id : ''
      }
    },
    async refreshAfterScheduleAction(message) {
      this.scheduleFormVisible = false
      await this.loadData()
      if (message) {
        this.$message.success(message)
      }
    },
    handleScheduleDialogCancel() {
      this.scheduleFormVisible = false
      this.loadData()
    },
    submitScheduleForm() {
      ;(async() => {
        await saveSchedule(this.scheduleForm)
        await this.refreshAfterScheduleAction('带班保存成功')
      })()
    },
    handleDeleteSchedule(row) {
      const groupId = row.group_id || row.id
      this.$confirm(`确定删除带班“${row.title}”吗？`, '提示', {
        type: 'warning'
      }).then(async() => {
        await destroySchedule({ id: groupId })
        await this.refreshAfterScheduleAction('带班删除成功')
      }).catch(() => {})
    }
  }
}
</script>

<style lang="scss">
.schedule-overview-page {
  padding: 0 16px 24px;
  background: #eff2f9;
  min-height: calc(100vh - 84px);
}

.header-actions {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
}

.summary-panel,
.summary-panel {
  padding: 18px;
  margin-bottom: 14px;
  background: #fff;
  border: 1px solid #ebeef5;
  border-radius: 10px;
  box-shadow: 0 2px 8px rgba(31, 45, 61, 0.04);
}

.summary-grid {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 12px;
}

.summary-card {
  min-height: 72px;
  padding: 14px 16px;
  background: #fff;
  border: 1px solid #ebeef5;
  border-radius: 8px;
}

.summary-label {
  font-size: 12px;
  color: #96876d;
  margin-bottom: 8px;
  white-space: nowrap;
}

.summary-value {
  display: flex;
  align-items: baseline;
  flex-wrap: nowrap;
  color: #285b5c;
  font-weight: 700;
  white-space: nowrap;
}

.summary-number {
  font-size: 28px;
  line-height: 1;
}

.summary-unit {
  font-size: 14px;
  margin-left: 4px;
}

.panel {
  padding: 14px;
  margin-bottom: 14px;
  background: #fff;
  border: 1px solid #ebeef5;
  border-radius: 10px;
  box-shadow: 0 2px 8px rgba(31, 45, 61, 0.04);
}

.panel-title {
  font-size: 16px;
  font-weight: 600;
  color: #303133;
  margin-bottom: 10px;
  text-align: center;
}

.table-scroll {
  overflow-x: auto;
}

.table-scroll-y {
  overflow-y: auto;
}

.limit-rows-8 {
  max-height: 314px;
}

.data-table,
.plan-table {
  width: 100%;
  min-width: 920px;
  border-collapse: collapse;
  table-layout: fixed;
  background: #fff;
}

.data-table th,
.data-table td,
.plan-table th,
.plan-table td {
  border: 1px solid #ebeef5;
  padding: 8px 10px;
  font-size: 12px;
  color: #606266;
  text-align: center;
}

.data-table thead th,
.plan-table thead th {
  background: #f8f8f9;
  color: #606266;
  font-weight: 600;
}

.data-table tbody tr:hover td,
.plan-table tbody tr:hover td {
  background: #f5f7fa;
}

.name-cell,
.total-cell {
  font-weight: 600;
}

.empty-cell {
  color: #909399;
  background: #fff;
}

.heat-cell {
  transition: background-color 0.2s ease;
}

.heat-empty {
  background: #fff;
  color: #c0c4cc;
}

.heat-level-1 {
  background: #edf7f5;
}

.heat-level-2 {
  background: #d7eeea;
}

.heat-level-3 {
  background: #b7ddd5;
}

.heat-level-4 {
  background: #8bc3b8;
  color: #fff;
}

.plan-table {
  min-width: 1600px;
}

.group-cell,
.course-cell {
  font-weight: 600;
}

.group-cell {
  width: 140px;
}

.course-cell {
  width: 140px;
}

.plan-month-cell {
  width: 140px;
  height: 74px;
  vertical-align: top;
  background: #fff;
  cursor: pointer;
  transition: background-color 0.2s ease;
}

.plan-table thead th {
  position: sticky;
  top: 0;
  z-index: 3;
  background: #f8f8f9;
}

.plan-table .sticky-col {
  position: sticky;
  left: 0;
  z-index: 4;
  background: #fff;
}

.plan-table .sticky-col-2 {
  left: 140px;
}

.plan-table thead .sticky-col-1,
.plan-table thead .sticky-col-2 {
  z-index: 5;
}

.plan-month-cell:hover {
  background: #f5f7fa;
}

.plan-chip-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.plan-chip {
  background: #ffffff;
  border: 1px solid #dcdfe6;
  border-radius: 6px;
  padding: 6px 4px;
  line-height: 1.45;
  box-shadow: 0 1px 2px rgba(31, 45, 61, 0.06);
}

.tone-green {
  background: #dfead6;
}

.tone-blue {
  background: #dbe6f7;
}

.tone-purple {
  background: #eadff0;
}

.tone-sand {
  background: #efe4d2;
}

.tone-cyan {
  background: #d8edf0;
}

.tone-cream {
  background: #f0e5d7;
}

.manager-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 12px;
}

.second-toolbar {
  margin-top: 18px;
}

.manager-title {
  font-size: 14px;
  font-weight: 600;
  color: #303133;
}

.manager-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}

.selected-tip {
  color: #909399;
  font-size: 12px;
}

.danger-text {
  color: #d94b4b;
}

.tree-manager {
  display: grid;
  grid-template-columns: minmax(360px, 1fr) minmax(280px, 360px);
  gap: 16px;
}

.tree-panel,
.tree-detail-panel {
  border: 1px solid #ebeef5;
  border-radius: 10px;
  background: #fff;
  padding: 14px;
}

.tree-wrap {
  max-height: 520px;
  overflow: auto;
  padding-right: 4px;
}

.tree-node {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  padding: 4px 0;
}

.tree-node-main {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.tree-node-tag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 34px;
  height: 20px;
  padding: 0 6px;
  border-radius: 10px;
  font-size: 11px;
  line-height: 20px;
}

.system-tag {
  background: #ecf5ff;
  color: #409eff;
}

.course-tag {
  background: #f0f9eb;
  color: #67c23a;
}

.tree-node-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: #606266;
}

.tree-node-actions {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

.node-detail-card {
  min-height: 220px;
  padding: 16px;
  border-radius: 8px;
  background: #fff;
  border: 1px solid #ebeef5;
}

.node-detail-row {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px dashed #ebeef5;
}

.node-detail-row:last-of-type {
  border-bottom: 0;
}

.node-detail-label {
  width: 72px;
  color: #909399;
}

.node-detail-value {
  flex: 1;
  color: #606266;
  word-break: break-all;
}

.node-detail-actions {
  display: flex;
  gap: 10px;
  margin-top: 16px;
}

.tree-empty-state {
  min-height: 220px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #909399;
  background: #fff;
  border: 1px dashed #dcdfe6;
  border-radius: 8px;
}

::v-deep .el-tree {
  background: transparent;
}

::v-deep .el-tree-node__content {
  height: auto;
  min-height: 36px;
  border-radius: 8px;
  margin-bottom: 4px;
  padding-right: 8px;
}

::v-deep .el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
  background: #f5f7fa;
}

::v-deep .overview-dialog .el-dialog {
  background: #eff2f9;
  border-radius: 12px;
  margin: 10vh auto 0;
}

::v-deep .overview-dialog .el-dialog__header {
  background: #eff2f9;
  border-bottom: 1px solid #e7dccb;
  border-radius: 12px 12px 0 0;
}

::v-deep .overview-dialog .el-dialog__body {
  background: #eff2f9;
}

@media (max-width: 1200px) {
  .summary-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .tree-manager {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 768px) {
  .schedule-overview-page {
    padding: 0 10px 18px;
  }

  .header-actions {
    justify-content: flex-start;
    flex-wrap: wrap;
  }

  .summary-grid {
    grid-template-columns: 1fr;
  }

  .manager-toolbar,
  .manager-actions {
    flex-direction: column;
    align-items: flex-start;
  }
}
</style>
