<template>
  <el-dialog
    :visible.sync="visibleProxy"
    :show-close="false"
    width="96%"
    :close-on-click-modal="false"
    custom-class="employee-schedule-dialog"
    append-to-body
  >
    <div class="dialog-inner">
      <div class="dialog-header">
        <div class="dialog-title">{{ selectedDay ? selectedDayLabel + ' · 排班详情' : '员工排班 · 预约一览' }}</div>
        <div class="header-actions">
          <span v-if="selectedDay" class="back-btn" @click="selectedDay = null"><i class="el-icon-arrow-left"></i> 返回周视图</span>
          <span class="dialog-close" @click="visibleProxy = false"><i class="el-icon-close"></i></span>
        </div>
      </div>

      <div class="schedule-toolbar">
        <div class="week-nav">
          <button class="nav-btn" @click="prevWeek"><i class="el-icon-arrow-left"></i></button>
          <span class="week-label">{{ weekLabel }}</span>
          <button class="nav-btn" @click="nextWeek"><i class="el-icon-arrow-right"></i></button>
        </div>
        <div class="day-pills" v-if="!selectedDay">
          <span
            v-for="d in weekDays"
            :key="d.date"
            :class="['day-pill', { 'day-pill--today': d.isToday }]"
            @click="selectedDay = d.date"
          >{{ d.weekday }} {{ d.dateStr }}</span>
        </div>
        <div class="schedule-legend">
          <span class="legend-group">
            <span class="legend-group__title">预约状态</span>
            <span class="legend-item"><i class="legend-dot legend-dot--booked"></i>已预约</span>
            <span class="legend-item"><i class="legend-dot legend-dot--serving"></i>服务中</span>
            <span class="legend-item"><i class="legend-dot legend-dot--completed"></i>已完成</span>
          </span>
          <span class="legend-divider"></span>
          <span class="legend-group">
            <span class="legend-group__title">人事状态</span>
            <span class="legend-item"><i class="legend-dot legend-dot--leave"></i>请假</span>
            <span class="legend-item"><i class="legend-dot legend-dot--vacation"></i>休假</span>
          </span>
          <span class="legend-divider"></span>
          <span class="legend-item"><i class="legend-dot legend-dot--slot"></i>30分钟/格</span>
        </div>
      </div>

      <div class="schedule-wrap" v-loading="loading">
        <div v-if="!loading && !employeeList.length" class="schedule-empty">当前门店暂无健康师，无法展示排班</div>
        <div v-else class="schedule-table" :class="{ 'schedule-table--day': selectedDay }">
          <div class="schedule-header">
            <div class="schedule-cell schedule-cell--employee">
              <span>员工</span>
              <span v-if="selectedDay" class="header-time-label">时间</span>
            </div>
            <div v-for="d in displayDays" :key="d.date" class="schedule-cell schedule-cell--day">
              <div class="day-name">{{ d.weekday }}</div>
              <div class="day-date">{{ d.dateStr }}</div>
              <!-- 日视图：时间刻度 -->
              <div v-if="selectedDay" class="day-slots day-slots--time">
                <div v-for="i in 48" :key="i" class="slot-time" :class="{ 'slot-time--show': (i - 1) % 2 === 0 }">
                  {{ (i - 1) % 2 === 0 ? formatSlotTime(i - 1) : '' }}
                </div>
              </div>
            </div>
          </div>
          <div v-for="emp in employeeList" :key="emp.id" class="schedule-row">
            <div
              class="schedule-cell schedule-cell--employee schedule-employee"
              :class="{ 'schedule-employee--clickable': selectedDay }"
            >
              <span
                class="employee-name"
                :class="{ 'employee-name--clickable': selectedDay }"
                :title="selectedDay ? '点击可代入健康师添加预约' : ''"
                @click="selectedDay && handleEmployeeClick(emp)"
              >{{ emp.name }}</span>
              <span class="employee-role">{{ emp.role }}</span>
            </div>
            <div v-for="d in displayDays" :key="d.date" class="schedule-cell schedule-cell--day">
              <div class="day-cell-inner">
                <div class="day-meta">
                  <span v-if="getClockIn(emp.id, d.date)" class="clock-in"><i class="el-icon-time"></i> {{ getClockIn(emp.id, d.date) }}</span>
                  <span v-else class="clock-in clock-in--none">未打卡</span>
                  <span :class="['status-tag', 'status-tag--' + (getEmployeeDayStatus(emp.id, d.date).type || 'normal')]">
                    {{ getEmployeeDayStatus(emp.id, d.date).text }}
                  </span>
                </div>
                <div
                  v-if="isFullDayStatus(emp.id, d.date)"
                  :class="['day-slots', 'day-slots--leave', getEmployeeDayStatus(emp.id, d.date).type === 'vacation' ? 'day-slots--vacation' : '']"
                >
                  <div class="leave-cover">{{ getEmployeeDayStatus(emp.id, d.date).text }}</div>
                </div>
                <div v-else class="day-slots">
                  <div
                    v-for="i in 48"
                    :key="i"
                    :class="['slot', slotClass(emp.id, d.date, i - 1), { 'slot--clickable': selectedDay }, { 'slot--in-drag': isSlotInDragRange(emp.id, d.date, i - 1) }]"
                    :title="getSlotTooltip(emp.id, d.date, i - 1) + (slotClass(emp.id, d.date, i - 1).includes('resize') ? ' · 拖动边缘可调整时长' : '')"
                    @mousedown="handleSlotMouseDown($event, emp, d.date, i - 1)"
                    @mouseenter="selectedDay && handleSlotMouseEnter(emp.id, d.date, i - 1)"
                  ></div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- 添加/修改预约：预约消耗单 -->
    <booking-consume-dialog
      :visible.sync="addBookingDialogVisible"
      :prefill="addBookingPrefill"
      @saved="handleBookingSaved"
    />

    <!-- 预约消耗单详情（新：支持取消/开始服务/转消耗） -->
    <booking-consume-detail-dialog
      :visible.sync="preConsumeDetailVisible"
      :booking="selectedPreConsumeBooking"
      @cancel="handlePreConsumeCancel"
      @edit="handlePreConsumeEdit"
      @start="handlePreConsumeStart"
      @convert="handlePreConsumeConvert"
    />

    <!-- 修改预约：复用预约消耗单弹窗，编辑同一条记录 -->
    <booking-consume-dialog
      :visible.sync="bookingConsumeDialogVisible"
      :prefill="bookingConsumePrefill"
      @saved="handlePreConsumeBookingUpdated"
    />
  </el-dialog>
</template>

<script>
import BookingConsumeDetailDialog from '@/components/booking-consume-detail-dialog.vue'
import BookingConsumeDialog from '@/components/booking-consume-dialog.vue'
import { getStoreSchedule, getYyjlInfo, updateYyjl, cancelYyjl, startYyjlService } from '@/api/lqYyjl'
import { getStoreUserList } from '@/api/storeUser'
import { getAttendanceMonthReport } from '@/api/lqAttendanceRecord'
import { buildYyjlSubmitBody, mapYyjlInfoToFormRecord } from '@/utils/appointmentHelper'
import {
  mapStoreScheduleItem,
  buildWeekRangeTimestamps,
  attachSlotRange,
  toSlotIndex,
  slotIndexToTime,
  mapAttendanceDayToScheduleStatus
} from '@/utils/scheduleHelper'

const CONSUME_PREFILL_ONCE_KEY = 'store_pc_consume_prefill_once'

export default {
  name: 'EmployeeScheduleDialog',
  components: { BookingConsumeDetailDialog, BookingConsumeDialog },
  props: {
    visible: { type: Boolean, default: false },
    openMode: { type: String, default: 'view' }
  },
  data() {
    return {
      weekStart: null,
      selectedDay: null,
      addBookingDialogVisible: false,
      addBookingPrefill: {},
      loading: false,
      dragState: null,
      preConsumeBookings: [],
      activePreConsumeId: '',
      preConsumeDetailVisible: false,
      selectedPreConsumeBooking: null,
      bookingConsumeDialogVisible: false,
      bookingConsumePrefill: {},
      employeeList: [],
      /** key = employeeId_yyyy-MM-dd */
      clockInData: {},
      employeeDayStatus: {},
      bookings: []
    }
  },
  computed: {
    visibleProxy: {
      get() { return this.visible },
      set(v) { this.$emit('update:visible', v) }
    },
    weekLabel() {
      if (!this.weekStart) return ''
      const start = this.weekStart
      const end = new Date(start)
      end.setDate(end.getDate() + 6)
      return `${start.getFullYear()}年${start.getMonth() + 1}月${start.getDate()}日 - ${end.getMonth() + 1}月${end.getDate()}日`
    },
    selectedDayLabel() {
      if (!this.selectedDay) return ''
      const d = new Date(this.selectedDay)
      const wd = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
      return `${this.selectedDay} ${wd[d.getDay()]}`
    },
    weekDays() {
      if (!this.weekStart) return []
      const today = new Date().toISOString().slice(0, 10)
      const days = []
      const wd = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
      for (let i = 0; i < 7; i++) {
        const d = new Date(this.weekStart)
        d.setDate(d.getDate() + i)
        const y = d.getFullYear()
        const m = String(d.getMonth() + 1).padStart(2, '0')
        const day = String(d.getDate()).padStart(2, '0')
        const dateStr = `${y}-${m}-${day}`
        days.push({
          date: dateStr,
          dateStr: `${m}/${day}`,
          weekday: wd[d.getDay()],
          dayOffset: i,
          isToday: dateStr === today
        })
      }
      return days
    },
    displayDays() {
      if (this.selectedDay) {
        const d = this.weekDays.find(x => x.date === this.selectedDay)
        return d ? [d] : []
      }
      return this.weekDays
    },
    healthWorkerOptionsFromEmployees() {
      return this.employeeList.map(e => ({ value: e.id, label: e.name }))
    },
    storeId() {
      return (this.$store.getters.storeInfo || {}).storeId || ''
    },
    userId() {
      const u = this.$store.getters.userInfo || {}
      return u.userId || u.id || ''
    },
    bookingsWithDate() {
      if (!this.weekStart) return []
      return this.bookings.map(b => {
        const d = new Date(this.weekStart)
        d.setDate(d.getDate() + b.dayOffset)
        const y = d.getFullYear()
        const m = String(d.getMonth() + 1).padStart(2, '0')
        const day = String(d.getDate()).padStart(2, '0')
        const timeStart = `${String(Math.floor(b.slotStart / 2)).padStart(2, '0')}:${(b.slotStart % 2) ? '30' : '00'}`
        const timeEnd = `${String(Math.floor(b.slotEnd / 2)).padStart(2, '0')}:${(b.slotEnd % 2) ? '30' : '00'}`
        return { ...b, date: `${y}-${m}-${day}`, timeRange: `${timeStart}-${timeEnd}` }
      })
    },
    preConsumeBookingsWithDate() {
      return (this.preConsumeBookings || []).map(b => attachSlotRange(b))
    }
  },
  watch: {
    visible(v) {
      if (v && !this.weekStart) this.initWeek()
      if (v) this.loadWeekData()
      if (v && this.openMode === 'set') {
        this.$nextTick(() => {
          this.gotoNextWeek()
          this.$store.commit('SET_SCHEDULE_DIALOG_MODE', 'view')
        })
      }
      if (!v) {
        this.selectedDay = null
        this.activePreConsumeId = ''
        this.preConsumeDetailVisible = false
      }
    },
    weekStart() {
      if (this.visibleProxy) this.loadWeekData()
    }
  },
  mounted() {
    this.initWeek()
    this._scheduleUpdatedHandler = () => {
      if (this.visibleProxy) this.loadWeekData()
    }
    try {
      window.addEventListener('store_pc_bookings_updated', this._scheduleUpdatedHandler)
    } catch (e) {}
  },
  beforeDestroy() {
    try {
      this._scheduleUpdatedHandler && window.removeEventListener('store_pc_bookings_updated', this._scheduleUpdatedHandler)
    } catch (e) {}
  },
  methods: {
    bookingStatusClass(status) {
      if (status === '服务中') return 'serving'
      if (status === '已完成') return 'completed'
      return 'booked'
    },
    async loadWeekData() {
      const storeId = this.storeId
      if (!storeId || !this.weekStart) {
        this.employeeList = []
        this.preConsumeBookings = []
        return
      }
      this.loading = true
      try {
        await this.loadEmployees(storeId)
        await Promise.all([
          this.loadScheduleBookings(storeId),
          this.loadAttendanceForWeek(storeId)
        ])
      } catch (e) {
        this.preConsumeBookings = []
      } finally {
        this.loading = false
      }
    },
    async loadEmployees(storeId) {
      try {
        const res = await getStoreUserList({
          mdid: storeId,
          gw: '健康师',
          currentPage: 1,
          pageSize: 500
        })
        const list = res.data?.list || []
        this.employeeList = list.map(u => ({
          id: u.id,
          name: u.realName || u.account || u.id,
          role: u.gw || '健康师'
        }))
      } catch (e) {
        this.employeeList = []
      }
    },
    async loadScheduleBookings(storeId) {
      const yysj = buildWeekRangeTimestamps(this.weekStart)
      if (!yysj) {
        this.preConsumeBookings = []
        return
      }
      const res = await getStoreSchedule({ djmd: storeId, yysj })
      const rows = Array.isArray(res.data) ? res.data : (res.data?.list || [])
      this.preConsumeBookings = rows.map((row, idx) => mapStoreScheduleItem(row, idx))
    },
    async loadAttendanceForWeek(storeId) {
      const clockIn = {}
      const dayStatus = {}
      const empIds = new Set(this.employeeList.map(e => e.id))
      const months = new Set()
      this.weekDays.forEach(d => {
        months.add(d.date.slice(0, 7))
      })
      for (const month of months) {
        try {
          const res = await getAttendanceMonthReport({
            Month: month,
            currentPage: 1,
            pageSize: 1000
          })
          const rows = res.data?.list || []
          rows.forEach(emp => {
            if (emp.storeId && emp.storeId !== storeId) return
            if (!empIds.has(emp.userId)) return
            ;(emp.dayRecords || []).forEach(day => {
              if (!day || !day.date) return
              const inWeek = this.weekDays.some(w => w.date === day.date)
              if (!inWeek) return
              const key = `${emp.userId}_${day.date}`
              if (day.punchInTime) clockIn[key] = day.punchInTime
              dayStatus[key] = mapAttendanceDayToScheduleStatus(day)
            })
          })
        } catch (e) {}
      }
      this.clockInData = clockIn
      this.employeeDayStatus = dayStatus
    },
    async patchBookingTime(id, patch) {
      const res = await getYyjlInfo(id)
      const info = res.data || {}
      const record = mapYyjlInfoToFormRecord(info, info.lqYyjlPxList || [])
      if (patch.startTime) record.startTime = patch.startTime
      if (patch.endTime) record.endTime = patch.endTime
      const body = buildYyjlSubmitBody(record, {
        storeId: this.storeId,
        userId: this.userId,
        memberName: record.memberName,
        healthWorkerOptions: this.healthWorkerOptionsFromEmployees
      })
      await updateYyjl(id, body)
    },
    handleBookingSaved() {
      this.addBookingDialogVisible = false
      this.loadWeekData()
      try {
        window.dispatchEvent(new Event('store_pc_bookings_updated'))
      } catch (e) {}
    },
    findPreConsumeForSlot(empId, date, slotIndex) {
      const list = this.preConsumeBookingsWithDate
      return list.find(x =>
        Array.isArray(x.therapistIds) &&
        x.therapistIds.includes(empId) &&
        x.date === date &&
        slotIndex >= x.slotStart &&
        slotIndex < x.slotEnd
      )
    },
    preConsumeStatusClass(status) {
      const s = status || 'booked'
      if (s === 'serving') return 'slot--preconsume-serving'
      if (s === 'converted') return 'slot--preconsume-converted' // 已完成（示例：转消耗后）
      if (s === 'cancelled') return 'slot--preconsume-cancelled'
      return 'slot--preconsume-booked'
    },
    initWeek() {
      const now = new Date()
      const dow = now.getDay()
      const start = new Date(now)
      start.setDate(now.getDate() - dow)
      start.setHours(0, 0, 0, 0)
      this.weekStart = start
    },
    prevWeek() {
      const d = new Date(this.weekStart)
      d.setDate(d.getDate() - 7)
      this.weekStart = d
      this.selectedDay = null
    },
    nextWeek() {
      const d = new Date(this.weekStart)
      d.setDate(d.getDate() + 7)
      this.weekStart = d
      this.selectedDay = null
    },
    gotoNextWeek() {
      const now = new Date()
      const dow = now.getDay()
      const daysUntilNextMonday = dow === 0 ? 1 : 8 - dow
      const nextMonday = new Date(now)
      nextMonday.setDate(now.getDate() + daysUntilNextMonday)
      nextMonday.setHours(0, 0, 0, 0)
      this.weekStart = nextMonday
      this.selectedDay = null
    },
    formatSlotTime(slotIndex) {
      const h = Math.floor(slotIndex / 2)
      const m = (slotIndex % 2) * 30
      return `${h}:${m === 0 ? '00' : '30'}`
    },
    handleEmployeeClick(emp) {
      if (!this.selectedDay) return
      this.addBookingPrefill = {
        date: this.selectedDay,
        items: [{ workers: [{ workerId: emp.id, workerName: emp.name }] }]
      }
      this.addBookingDialogVisible = true
    },
    getClockIn(empId, date) {
      return this.clockInData[`${empId}_${date}`] || null
    },
    getEmployeeDayStatus(empId, date) {
      return this.employeeDayStatus[`${empId}_${date}`] || { type: 'normal', text: '正常' }
    },
    isFullDayStatus(empId, date) {
      const s = this.getEmployeeDayStatus(empId, date)
      if (s.type !== 'leave' && s.type !== 'vacation') return false
      return s.slotStart == null && s.slotEnd == null
    },
    isSlotInLeaveRange(empId, date, slotIndex) {
      const s = this.getEmployeeDayStatus(empId, date)
      if (s.type !== 'leave' && s.type !== 'vacation') return false
      if (s.slotStart == null || s.slotEnd == null) return false
      return slotIndex >= s.slotStart && slotIndex < s.slotEnd
    },
    isSlotInDragRange(empId, date, slotIndex) {
      if (!this.dragState || this.dragState.employeeId !== empId) return false
      if (this.dragState.date && this.dragState.date !== date) return false
      const [lo, hi] = this.dragState.startSlot <= this.dragState.endSlot
        ? [this.dragState.startSlot, this.dragState.endSlot]
        : [this.dragState.endSlot, this.dragState.startSlot]
      return slotIndex >= lo && slotIndex <= hi
    },
    slotClass(empId, date, slotIndex) {
      const pcb = this.findPreConsumeForSlot(empId, date, slotIndex)
      if (pcb) {
        let c = `slot--preconsume ${this.preConsumeStatusClass(pcb.status)} slot--preconsume-color-${pcb.colorKey || 'blue'}`
        if (slotIndex === pcb.slotStart) c += ' slot--resize-start slot--preconsume-edge'
        if (slotIndex === pcb.slotEnd - 1) c += ' slot--resize-end slot--preconsume-edge'
        if (this.activePreConsumeId) {
          // 仅对“其他单子”做淡化；当前选中的同单保持正常显示
          if (pcb.id === this.activePreConsumeId) {
            // 仅在块两端加高亮描边，避免整块出现密集分割线
            if (slotIndex === pcb.slotStart || slotIndex === pcb.slotEnd - 1) c += ' slot--same-active'
          } else {
            c += ' slot--dimmed'
          }
        }
        return c
      }
      const b = this.bookingsWithDate.find(x =>
        x.employeeId === empId &&
        x.date === date &&
        slotIndex >= x.slotStart &&
        slotIndex < x.slotEnd
      )
      if (b) {
        let c = 'slot--booked'
        if (b.status === '服务中') c = 'slot--serving'
        else if (b.status === '已完成') c = 'slot--completed'
        if (slotIndex === b.slotStart) c += ' slot--resize-start'
        if (slotIndex === b.slotEnd - 1) c += ' slot--resize-end'
        return c
      }
      if (this.isSlotInLeaveRange(empId, date, slotIndex)) return 'slot--leave'
      return ''
    },
    getSlotTooltip(empId, date, slotIndex) {
      const pcb = this.findPreConsumeForSlot(empId, date, slotIndex)
      if (pcb) {
        const items = Array.isArray(pcb.itemLabels) && pcb.itemLabels.length ? pcb.itemLabels.join('、') : ''
        const statusMap = { booked: '已预约', serving: '服务中', converted: '已转消耗', cancelled: '已取消' }
        const st = statusMap[pcb.status] || '已预约'
        return `${pcb.startTime}-${pcb.endTime} ${pcb.memberName || '无'}（${st}）${items ? ' · ' + items : ''}`
      }
      const b = this.bookingsWithDate.find(x =>
        x.employeeId === empId &&
        x.date === date &&
        slotIndex >= x.slotStart &&
        slotIndex < x.slotEnd
      )
      if (b) {
        return `${b.timeRange} ${b.customerName}（${b.status}）${b.project ? ' · ' + b.project : ''}`
      }
      const h = Math.floor(slotIndex / 2)
      const m = (slotIndex % 2) * 30
      return `${h}:${m === 0 ? '00' : '30'} 空闲 · 点击可添加预约`
    },
    getDayTooltip(empId, date) {
      const list = this.bookingsWithDate.filter(x => x.employeeId === empId && x.date === date)
      if (list.length === 0) return '当日无预约 · 点击日期查看详情'
      return list.map(x => `${x.customerName} ${x.timeRange}`).join('\n') + '\n点击日期查看详情'
    },
    toSlotIndex(timeStr) {
      if (!timeStr) return null
      const [h, m] = timeStr.split(':').map(Number)
      if (Number.isNaN(h) || Number.isNaN(m)) return null
      return h * 2 + (m >= 30 ? 1 : 0)
    },
    handleSlotMouseDown(e, emp, date, slotIndex) {
      const pcb = this.findPreConsumeForSlot(emp.id, date, slotIndex)
      if (pcb) {
        // 仅允许从两端拖动调整时长；中间点击仅查看详情
        const cls = this.slotClass(emp.id, date, slotIndex)
        const isResizeStart = cls.includes('slot--resize-start')
        const isResizeEnd = cls.includes('slot--resize-end')
        if (this.selectedDay && (isResizeStart || isResizeEnd)) {
          const edge = isResizeStart ? 'start' : 'end'
          this.dragState = { type: 'preconsume_resize', preId: pcb.id, edge, employeeId: emp.id, date }
          const originalStart = pcb.startTime
          const originalEnd = pcb.endTime
          const onMove = (ev) => {
            const slotEl = document.elementFromPoint(ev.clientX, ev.clientY)?.closest('.slot')
            if (!slotEl) return
            const row = slotEl.closest('.schedule-row')
            const empName = row?.querySelector('.employee-name')?.textContent
            const empObj = this.employeeList.find(e => e.name === empName)
            if (!empObj || empObj.id !== this.dragState.employeeId) return
            const idx = Array.from(slotEl.parentElement?.children || []).indexOf(slotEl)
            if (idx < 0 || idx >= 48) return

            const current = (this.preConsumeBookingsWithDate || []).find(x => x.id === this.dragState.preId)
            if (!current) return
            const startSlot = this.toSlotIndex(current.startTime) ?? current.slotStart
            const endSlot = this.toSlotIndex(current.endTime) ?? current.slotEnd
            if (this.dragState.edge === 'start') {
              const newStart = Math.min(idx, (endSlot || 1) - 1)
              const startTime = this.formatSlotTime(Math.max(newStart, 0))
              this.updatePreConsumeRecord({ id: current.id, startTime })
            } else {
              const newEnd = Math.max(idx + 1, (startSlot || 0) + 1)
              const endTime = this.formatSlotTime(Math.min(newEnd, 48))
              this.updatePreConsumeRecord({ id: current.id, endTime })
            }
          }
          const onUp = () => {
            document.removeEventListener('mousemove', onMove)
            document.removeEventListener('mouseup', onUp)
            this.dragState = null
            // 若未发生变化，保持不弹窗
            const cur = (this.preConsumeBookings || []).find(x => x.id === pcb.id)
            if (!cur || (cur.startTime === originalStart && cur.endTime === originalEnd)) return
          }
          document.addEventListener('mousemove', onMove)
          document.addEventListener('mouseup', onUp)
          return
        }

        this.activePreConsumeId = pcb.id
        this.selectedPreConsumeBooking = pcb
        this.preConsumeDetailVisible = true
        return
      }
      const b = this.bookingsWithDate.find(x =>
        x.employeeId === emp.id &&
        x.date === date &&
        slotIndex >= x.slotStart &&
        slotIndex < x.slotEnd
      )
      // 周视图下允许点击块直接查看详情，不支持拖动修改/新建
      if (!this.selectedDay) {
        return
      }
      if (b) {
        const rect = e.currentTarget.getBoundingClientRect()
        const x = e.clientX - rect.left
        const w = rect.width
        if (w > 12 && x < w / 3) {
          const bk = this.bookings.find(x => x.id === b.id)
          const originalStart = bk ? bk.slotStart : b.slotStart
          const originalEnd = bk ? bk.slotEnd : b.slotEnd
          this.dragState = { type: 'resize', bookingId: b.id, edge: 'start', date, employeeId: emp.id }
          const onMove = (ev) => {
            const slotEl = document.elementFromPoint(ev.clientX, ev.clientY)?.closest('.slot')
            if (!slotEl) return
            const row = slotEl.closest('.schedule-row')
            const empName = row?.querySelector('.employee-name')?.textContent
            const empObj = this.employeeList.find(e => e.name === empName)
            if (!empObj || empObj.id !== this.dragState.employeeId) return
            const idx = Array.from(slotEl.parentElement?.children || []).indexOf(slotEl)
            if (idx >= 0 && idx < 48) {
              const bk2 = this.bookings.find(x => x.id === this.dragState.bookingId)
              if (bk2) {
                const newStart = Math.min(idx, bk2.slotEnd - 1)
                if (newStart !== bk2.slotStart) {
                  this.$set(bk2, 'slotStart', newStart)
                }
              }
            }
          }
          const onUp = () => {
            document.removeEventListener('mousemove', onMove)
            document.removeEventListener('mouseup', onUp)
            const bk3 = this.bookings.find(x => x.id === this.dragState.bookingId)
            this.dragState = null
            if (!bk3) return
            const newStart = bk3.slotStart
            if (newStart === originalStart) return
            const timeStart = `${String(Math.floor(newStart / 2)).padStart(2, '0')}:${(newStart % 2) ? '30' : '00'}`
            const timeEnd = `${String(Math.floor(bk3.slotEnd / 2)).padStart(2, '0')}:${(bk3.slotEnd % 2) ? '30' : '00'}`
            this.$confirm(`确定将预约时间调整为 ${timeStart} - ${timeEnd} 吗？`, '确认调整', {
              confirmButtonText: '确定',
              cancelButtonText: '取消',
              type: 'warning'
            }).then(() => {
              // 用户确认，保持当前已修改的值
            }).catch(() => {
              this.$set(bk3, 'slotStart', originalStart)
            })
          }
          document.addEventListener('mousemove', onMove)
          document.addEventListener('mouseup', onUp)
        } else if (w > 12 && x > (2 * w) / 3) {
          const bk = this.bookings.find(x => x.id === b.id)
          const originalStart = bk ? bk.slotStart : b.slotStart
          const originalEnd = bk ? bk.slotEnd : b.slotEnd
          this.dragState = { type: 'resize', bookingId: b.id, edge: 'end', date, employeeId: emp.id }
          const onMove = (ev) => {
            const slotEl = document.elementFromPoint(ev.clientX, ev.clientY)?.closest('.slot')
            if (!slotEl) return
            const row = slotEl.closest('.schedule-row')
            const empName = row?.querySelector('.employee-name')?.textContent
            const empObj = this.employeeList.find(e => e.name === empName)
            if (!empObj || empObj.id !== this.dragState.employeeId) return
            const idx = Array.from(slotEl.parentElement?.children || []).indexOf(slotEl)
            if (idx >= 0 && idx < 48) {
              const bk2 = this.bookings.find(x => x.id === this.dragState.bookingId)
              if (bk2) {
                const newEnd = Math.max(idx + 1, bk2.slotStart + 1)
                if (newEnd !== bk2.slotEnd) {
                  this.$set(bk2, 'slotEnd', newEnd)
                }
              }
            }
          }
          const onUp = () => {
            document.removeEventListener('mousemove', onMove)
            document.removeEventListener('mouseup', onUp)
            const bk3 = this.bookings.find(x => x.id === this.dragState.bookingId)
            this.dragState = null
            if (!bk3) return
            const newEnd = bk3.slotEnd
            if (newEnd === originalEnd) return
            const timeStart = `${String(Math.floor(bk3.slotStart / 2)).padStart(2, '0')}:${(bk3.slotStart % 2) ? '30' : '00'}`
            const timeEnd = `${String(Math.floor(newEnd / 2)).padStart(2, '0')}:${(newEnd % 2) ? '30' : '00'}`
            this.$confirm(`确定将预约时间调整为 ${timeStart} - ${timeEnd} 吗？`, '确认调整', {
              confirmButtonText: '确定',
              cancelButtonText: '取消',
              type: 'warning'
            }).then(() => {
              // 用户确认，保持当前已修改的值
            }).catch(() => {
              this.$set(bk3, 'slotEnd', originalEnd)
            })
          }
          document.addEventListener('mousemove', onMove)
          document.addEventListener('mouseup', onUp)
        } else {
          this.selectedBooking = b
          this.detailDialogVisible = true
        }
        return
      }
      const d = this.weekDays.find(x => x.date === date)
      if (!d) return
      if (this.isSlotInLeaveRange(emp.id, date, slotIndex)) return
      this.dragState = {
        type: 'select',
        employeeId: emp.id,
        employee: emp,
        date,
        startSlot: slotIndex,
        endSlot: slotIndex
      }
      const onUp = () => {
        document.removeEventListener('mouseup', onUp)
        if (!this.dragState) return
        const [s, e2] = this.dragState.startSlot <= this.dragState.endSlot
          ? [this.dragState.startSlot, this.dragState.endSlot]
          : [this.dragState.endSlot, this.dragState.startSlot]
        const timeStart = `${String(Math.floor(s / 2)).padStart(2, '0')}:${(s % 2) ? '30' : '00'}`
        const timeEnd = `${String(Math.floor((e2 + 1) / 2)).padStart(2, '0')}:${((e2 + 1) % 2) ? '30' : '00'}`
        this.addBookingPrefill = {
          date: this.dragState.date,
          startTime: timeStart,
          endTime: timeEnd,
          items: [{
            workers: [{
              workerId: this.dragState.employeeId,
              workerName: (this.dragState.employee && this.dragState.employee.name) || ''
            }]
          }]
        }
        this.dragState = null
        this.$nextTick(() => {
          this.addBookingDialogVisible = true
        })
      }
      document.addEventListener('mouseup', onUp)
    },
    handleSlotMouseEnter(empId, date, slotIndex) {
      if (!this.dragState || this.dragState.type !== 'select' || this.dragState.employeeId !== empId) return
      if (this.dragState.date && this.dragState.date !== date) return
      this.dragState.endSlot = slotIndex
    },
    async updatePreConsumeRecord(record) {
      if (!record || !record.id) return null
      try {
        await this.patchBookingTime(record.id, {
          startTime: record.startTime,
          endTime: record.endTime
        })
        await this.loadWeekData()
        const updated = (this.preConsumeBookings || []).find(x => x.id === record.id)
        return updated || null
      } catch (e) {
        return null
      }
    },
    handlePreConsumeCancel(b) {
      if (!b) return
      this.$confirm(`确定取消「${b.memberName || '该会员'}」在 ${b.date} ${b.startTime}-${b.endTime} 的预约吗？`, '取消预约确认', {
        confirmButtonText: '确定',
        cancelButtonText: '再想想',
        type: 'warning'
      }).then(async () => {
        await cancelYyjl(b.id, '')
        this.preConsumeDetailVisible = false
        this.selectedPreConsumeBooking = null
        await this.loadWeekData()
        try {
          window.dispatchEvent(new Event('store_pc_bookings_updated'))
        } catch (e) {}
        this.$message.success('已取消预约')
      }).catch(() => {})
    },
    async handlePreConsumeStart(b) {
      if (!b) return
      try {
        await startYyjlService(b.id)
        await this.loadWeekData()
        const updated = (this.preConsumeBookings || []).find(x => x.id === b.id)
        if (updated) this.selectedPreConsumeBooking = updated
        this.$message.success('已开始服务')
      } catch (e) {}
    },
    handlePreConsumeEdit(b) {
      if (!b) return
      this.bookingConsumePrefill = { ...b }
      this.preConsumeDetailVisible = false
      this.$nextTick(() => {
        this.bookingConsumeDialogVisible = true
      })
    },
    async handlePreConsumeBookingUpdated() {
      this.bookingConsumeDialogVisible = false
      this.preConsumeDetailVisible = false
      await this.loadWeekData()
      try {
        window.dispatchEvent(new Event('store_pc_bookings_updated'))
      } catch (e) {}
      this.$message.success('预约已更新')
    },
    handlePreConsumeConvert(b) {
      if (!b) return
      this.preConsumeDetailVisible = false
      const prefillOnce = {
        appointmentId: b.id,
        memberId: b.memberId,
        name: b.memberName,
        consumeDate: b.date,
        remark: `转自预约：${b.id}${b.remark ? ' · ' + b.remark : ''}`,
        therapistIds: Array.from(new Set([].concat(b.therapistIds || []))).filter(Boolean),
        items: (b.itemLabels || []).map(label => ({ label, count: 1 }))
      }
      try {
        localStorage.setItem(CONSUME_PREFILL_ONCE_KEY, JSON.stringify(prefillOnce))
      } catch (e) {}
      this.$router.push({ path: '/dashboard', query: { openConsume: '1' } })
    }
  }
}
</script>

<style lang="scss" scoped>
::v-deep .employee-schedule-dialog {
  max-width: 1400px;
  margin-top: 2vh !important;
  border-radius: 20px;
  padding: 0;
  background: radial-gradient(circle at 0 0, rgba(255,255,255,0.96) 0, rgba(248,250,252,0.98) 40%, rgba(241,245,249,0.98) 100%);
  box-shadow: 0 24px 48px rgba(15,23,42,0.18), 0 0 0 1px rgba(255,255,255,0.9);
  backdrop-filter: blur(22px);
  -webkit-backdrop-filter: blur(22px);
}

::v-deep .el-dialog__header { display: none; }
::v-deep .el-dialog__body { padding: 0; }

.dialog-inner {
  display: flex;
  flex-direction: column;
  max-height: 90vh;
}

.dialog-header {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 18px 22px 0;
  padding: 10px 14px;
  border-radius: 14px;
  background: rgba(219,234,254,0.96);
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 12px;
}

.back-btn {
  font-size: 13px;
  color: #6366f1;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 4px;
  &:hover { text-decoration: underline; }
}

.dialog-title {
  font-size: 17px;
  font-weight: 600;
  color: #0f172a;
}

.dialog-close {
  cursor: pointer;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  color: #64748b;
  transition: all 0.15s;
  &:hover { background: rgba(0,0,0,0.06); color: #0f172a; }
}

.schedule-toolbar {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
  padding: 14px 22px;
}

.week-nav {
  display: flex;
  align-items: center;
  gap: 12px;
}

.nav-btn {
  width: 32px;
  height: 32px;
  border-radius: 8px;
  border: 1px solid #e2e8f0;
  background: #fff;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #64748b;
  transition: all 0.2s;
  &:hover {
    background: rgba(99, 102, 241, 0.08);
    border-color: rgba(99, 102, 241, 0.25);
    color: #4f46e5;
  }
}

.week-label {
  font-size: 14px;
  font-weight: 600;
  color: #1e293b;
  min-width: 260px;
  text-align: center;
}

.day-pills {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.day-pill {
  padding: 6px 12px;
  border-radius: 8px;
  font-size: 12px;
  color: #64748b;
  background: #f1f5f9;
  cursor: pointer;
  transition: all 0.2s;
  &:hover {
    background: rgba(99, 102, 241, 0.1);
    color: #4f46e5;
  }
  &--today {
    background: rgba(99, 102, 241, 0.15);
    color: #4f46e5;
    font-weight: 600;
  }
}

.schedule-legend {
  display: flex;
  align-items: center;
  gap: 14px;
  font-size: 12px;
  color: #64748b;
  flex-wrap: wrap;
}

.legend-group {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  padding: 6px 10px;
  border-radius: 10px;
  background: rgba(241, 245, 249, 0.75);
}

.legend-group__title {
  font-size: 12px;
  font-weight: 600;
  color: #475569;
  margin-right: 2px;
  white-space: nowrap;
}

.legend-divider {
  width: 1px;
  height: 18px;
  background: rgba(226, 232, 240, 0.9);
}

.legend-item {
  display: flex;
  align-items: center;
  gap: 6px;
}

.legend-dot {
  width: 10px;
  height: 10px;
  border-radius: 2px;
  display: inline-block;
}

.legend-dot--booked { background: #3b82f6; }
.legend-dot--serving { background: #f97316; }
.legend-dot--completed { background: #22c55e; }
.legend-dot--leave { background: #94a3b8; }
.legend-dot--vacation { background: #8b5cf6; }
.legend-dot--slot { background: #e2e8f0; }

.schedule-wrap {
  flex: 1;
  min-height: 0;
  overflow: auto;
  padding: 0 22px 22px;
}

.schedule-empty {
  padding: 48px 20px;
  text-align: center;
  color: #94a3b8;
  font-size: 14px;
}

.schedule-table {
  display: grid;
  grid-template-columns: 120px repeat(7, 1fr);
  min-width: 900px;
  border: 1px solid #e2e8f0;
  border-radius: 12px;
  overflow: hidden;
  background: #fff;

  &--day {
    grid-template-columns: 180px 1fr;
  }
}

.schedule-header {
  display: contents;
}

.schedule-row {
  display: contents;
}

.schedule-cell {
  padding: 10px 8px;
  border-bottom: 1px solid #f1f5f9;
  border-right: 1px solid #f1f5f9;
}

.schedule-cell--employee {
  position: sticky;
  left: 0;
  z-index: 2;
  background: #f8fafc;
  font-weight: 500;
}

.schedule-cell--day {
  min-width: 0;
}

.day-cell-inner {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.day-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 11px;
}

.schedule-header .schedule-cell--employee {
  background: #f1f5f9;
  font-size: 13px;
  font-weight: 600;
  color: #64748b;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.header-time-label {
  font-size: 11px;
  font-weight: 500;
  color: #94a3b8;
}

.schedule-header .schedule-cell--day {
  text-align: center;
  padding: 8px;
}

.day-name { font-size: 12px; color: #94a3b8; }
.day-date { font-size: 13px; font-weight: 600; color: #374151; margin-top: 2px; }

/* 日视图时间刻度 */
.day-slots--time {
  min-height: 18px;
  margin-top: 6px;
}

.slot-time {
  font-size: 9px;
  color: #94a3b8;
  display: flex;
  align-items: center;
  justify-content: center;
  white-space: nowrap;
  overflow: hidden;
  min-width: 0;
  &.slot-time--show {
    color: #64748b;
    font-weight: 500;
  }
}

.employee-name--clickable {
  cursor: pointer;
  padding: 2px 4px;
  margin: -2px -4px;
  border-radius: 4px;
  transition: background 0.15s;
  &:hover {
    background: rgba(99, 102, 241, 0.12);
    color: #4f46e5;
  }
}

.schedule-employee {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.employee-name { font-size: 14px; color: #111827; }
.employee-role { font-size: 11px; color: #94a3b8; }

.clock-in {
  color: #22c55e;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  &--none { color: #94a3b8; }
}

.status-tag {
  font-size: 10px;
  padding: 2px 6px;
  border-radius: 4px;
  display: inline-block;
  &--normal { background: #f1f5f9; color: #64748b; }
  &--leave { background: rgba(148, 163, 184, 0.2); color: #64748b; }
  &--vacation { background: rgba(139, 92, 246, 0.15); color: #7c3aed; }
}

.day-slots {
  display: grid;
  grid-template-columns: repeat(48, 1fr);
  gap: 0;
  min-height: 24px;

  &--leave {
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(148, 163, 184, 0.16);
  }
  &--vacation {
    background: rgba(139, 92, 246, 0.08);
    .leave-cover { color: #7c3aed; }
  }
}

.leave-cover {
  font-size: 12px;
  color: #475569;
  font-weight: 500;
}

.slot {
  min-height: 20px;
  background: #f8fafc;
  /* 默认格子边线（预占用块会覆盖为透明以减少分割线） */
  border: 1px solid rgba(241, 245, 249, 0.95);

  &--clickable {
    cursor: pointer;
    &:hover { background: #e2e8f0; }
  }
}

.slot--booked {
  background: rgba(59, 130, 246, 0.5);
  &:hover { background: rgba(59, 130, 246, 0.7); }
  &.slot--resize-start.slot--resize-end { border-radius: 2px; }
  &.slot--resize-start:not(.slot--resize-end) { border-radius: 2px 0 0 2px; }
  &.slot--resize-end:not(.slot--resize-start) { border-radius: 0 2px 2px 0; }
  &:not(.slot--resize-start):not(.slot--resize-end) { border-radius: 0; }
}

.slot--serving {
  background: rgba(249, 115, 22, 0.5);
  &:hover { background: rgba(249, 115, 22, 0.7); }
  &.slot--resize-start.slot--resize-end { border-radius: 2px; }
  &.slot--resize-start:not(.slot--resize-end) { border-radius: 2px 0 0 2px; }
  &.slot--resize-end:not(.slot--resize-start) { border-radius: 0 2px 2px 0; }
  &:not(.slot--resize-start):not(.slot--resize-end) { border-radius: 0; }
}

.slot--completed {
  background: rgba(34, 197, 94, 0.5);
  &:hover { background: rgba(34, 197, 94, 0.7); }
  &.slot--resize-start.slot--resize-end { border-radius: 2px; }
  &.slot--resize-start:not(.slot--resize-end) { border-radius: 2px 0 0 2px; }
  &.slot--resize-end:not(.slot--resize-start) { border-radius: 0 2px 2px 0; }
  &:not(.slot--resize-start):not(.slot--resize-end) { border-radius: 0; }
}

.slot--leave {
  background: rgba(148, 163, 184, 0.35);
}

.slot--in-drag {
  background: rgba(99, 102, 241, 0.35);
}

.slot--resize-start { cursor: w-resize; }
.slot--resize-end { cursor: e-resize; }

/* 预约消耗单块：不同状态 + 同单高亮 */
.slot--preconsume {
  position: relative;
  background: rgba(37, 99, 235, 0.42);
  &:hover { background: rgba(37, 99, 235, 0.6); }
  /* 让块内部看起来是“整块”，压掉密集分割线 */
  border-color: transparent !important;
  border-width: 0 !important;
  box-shadow: none;
}
.slot--preconsume-booked { }
.slot--preconsume-serving {
  background: rgba(249, 115, 22, 0.55);
  &:hover { background: rgba(249, 115, 22, 0.7); }
}
.slot--preconsume-converted {
  background: rgba(34, 197, 94, 0.5);
  &:hover { background: rgba(34, 197, 94, 0.7); }
}
.slot--preconsume-cancelled {
  background: rgba(239, 68, 68, 0.45);
  &:hover { background: rgba(239, 68, 68, 0.6); }
}

/* 颜色识别不要贯穿整段底边，避免中间出现“下边框” */
.slot--preconsume-color-blue { box-shadow: none; }
.slot--preconsume-color-green { box-shadow: none; }
.slot--preconsume-color-orange { box-shadow: none; }
.slot--preconsume-color-purple { box-shadow: none; }
.slot--preconsume-color-gray { box-shadow: none; }

/* 仅在块两端做轻微色条提示 */
.slot--preconsume-edge.slot--preconsume-color-blue { box-shadow: inset 0 -2px 0 rgba(37, 99, 235, 0.65); }
.slot--preconsume-edge.slot--preconsume-color-green { box-shadow: inset 0 -2px 0 rgba(34, 197, 94, 0.7); }
.slot--preconsume-edge.slot--preconsume-color-orange { box-shadow: inset 0 -2px 0 rgba(249, 115, 22, 0.75); }
.slot--preconsume-edge.slot--preconsume-color-purple { box-shadow: inset 0 -2px 0 rgba(139, 92, 246, 0.75); }
.slot--preconsume-edge.slot--preconsume-color-gray { box-shadow: inset 0 -2px 0 rgba(148, 163, 184, 0.8); }

.slot--same-active {
  /* 用内描边提示边缘，不改变格子视觉尺寸 */
  box-shadow: inset 0 0 0 2px rgba(17, 24, 39, 0.22);
  z-index: 1;
}
.slot--dimmed {
  opacity: 0.35;
}

.slot--preconsume.slot--resize-start {
  /* 用内阴影提示左边缘，避免“变大” */
  border-left: 0 !important;
  box-shadow: inset 2px 0 0 rgba(15, 23, 42, 0.14);
}
.slot--preconsume.slot--resize-end {
  border-right: 0 !important;
  box-shadow: inset -2px 0 0 rgba(15, 23, 42, 0.14);
}

/* 预约详情弹窗 - 与主弹窗风格统一 */
::v-deep .schedule-detail-dialog {
  border-radius: 20px;
  padding: 0;
  background: radial-gradient(circle at 0 0, rgba(255,255,255,0.96) 0, rgba(248,250,252,0.98) 40%, rgba(241,245,249,0.98) 100%);
  box-shadow: 0 24px 48px rgba(15,23,42,0.18), 0 0 0 1px rgba(255,255,255,0.9);
  backdrop-filter: blur(22px);
  -webkit-backdrop-filter: blur(22px);
}

::v-deep .schedule-detail-dialog .el-dialog__header { display: none; }
::v-deep .schedule-detail-dialog .el-dialog__body {
  padding: 22px;
}

.booking-detail-inner {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.booking-detail-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 0 0 16px;
  padding: 12px 16px;
  border-radius: 14px;
  background: rgba(219,234,254,0.96);
}

.booking-detail-title { font-size: 17px; font-weight: 600; color: #0f172a; }

.booking-detail-close {
  cursor: pointer;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  color: #64748b;
  transition: all 0.15s;
  &:hover { background: rgba(0,0,0,0.06); color: #0f172a; }
}

.booking-detail-body {
  padding: 4px 0 16px;
}

.detail-row {
  display: flex;
  padding: 10px 0;
  border-bottom: 1px solid #f1f5f9;
  font-size: 14px;
  .label { width: 90px; color: #64748b; flex-shrink: 0; }
  .status-booked { color: #3b82f6; font-weight: 600; }
  .status-serving { color: #f97316; font-weight: 600; }
  .status-completed { color: #22c55e; font-weight: 600; }
}

.booking-detail-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  padding: 16px 0 0;
  border-top: 1px solid #f1f5f9;
}

::v-deep .schedule-detail-dialog .el-button--primary { border-radius: 999px; }
::v-deep .schedule-detail-dialog .el-button--default { border-radius: 999px; }
</style>
