appointmentHelper.js 10.1 KB
/** 预约状态:后端中文 ↔ 门店 PC 日历/详情 UI */

export function pickRowField(row, ...keys) {
  if (!row) return undefined
  for (let i = 0; i < keys.length; i++) {
    const v = row[keys[i]]
    if (v !== undefined && v !== null && v !== '') return v
  }
  return undefined
}

export function parseApiDateTime(val) {
  if (val == null || val === '') return null
  if (val instanceof Date) return Number.isNaN(val.getTime()) ? null : val
  if (typeof val === 'number') {
    const d = new Date(val)
    return Number.isNaN(d.getTime()) ? null : d
  }
  const d = new Date(val)
  return Number.isNaN(d.getTime()) ? null : d
}

/** 后端 LqYyjl 列表 yysj 范围:毫秒时间戳,毫秒时间戳 */
export function buildYysjTimestampRange(startDate, endDate) {
  const s = parseApiDateTime(startDate)
  const e = parseApiDateTime(endDate)
  if (!s || !e) return ''
  return [s.getTime(), e.getTime()].join(',')
}

/** 解析 YYYY-MM-DD 为当日 0:00 ~ 23:59:59 */
export function parseDayRangeYmd(dateYmd) {
  if (!dateYmd) return null
  const parts = String(dateYmd).trim().split(/[-/]/)
  if (parts.length < 3) return null
  const y = parseInt(parts[0], 10)
  const m = parseInt(parts[1], 10)
  const d = parseInt(parts[2], 10)
  if (Number.isNaN(y) || Number.isNaN(m) || Number.isNaN(d)) return null
  return {
    dayStart: new Date(y, m - 1, d, 0, 0, 0, 0),
    dayEnd: new Date(y, m - 1, d, 23, 59, 59, 999)
  }
}

/** 将预约时段映射为 30 分钟格(0~47,共 24 小时) */
export function dateTimeToSlotRange(yysj, yyjs) {
  const start = parseApiDateTime(yysj)
  let end = parseApiDateTime(yyjs)
  if (!start) return null
  if (!end || end <= start) {
    end = new Date(start.getTime() + 30 * 60 * 1000)
  }
  const sameDay =
    start.getFullYear() === end.getFullYear() &&
    start.getMonth() === end.getMonth() &&
    start.getDate() === end.getDate()
  const startMin = start.getHours() * 60 + start.getMinutes()
  let endMin = sameDay ? end.getHours() * 60 + end.getMinutes() : 24 * 60
  let startSlot = Math.floor(startMin / 30)
  let endSlot = Math.ceil(endMin / 30)
  if (endSlot <= startSlot) endSlot = startSlot + 1
  if (startSlot < 0) startSlot = 0
  if (endSlot > 48) endSlot = 48
  return { startSlot, endSlot }
}

/** 按房间汇总当日占用格(供房态图) */
export function buildRoomBusyMap(bookings) {
  const busy = {}
  ;(bookings || []).forEach(row => {
    const status = (pickRowField(row, 'F_Status', 'f_Status') || '').trim()
    if (status === '已取消') return
    const roomId = String(pickRowField(row, 'roomId', 'RoomId', 'F_RoomId') || '')
    if (!roomId) return
    const yysj = pickRowField(row, 'yysj', 'Yysj')
    const yyjs = pickRowField(row, 'yyjs', 'Yyjs')
    const range = dateTimeToSlotRange(yysj, yyjs)
    if (!range) return
    const nm = pickRowField(row, 'gkxm', 'Gkxm') || '客户'
    const h0 = formatTimeHM(yysj)
    const h1 = formatTimeHM(yyjs || yysj)
    const label = !yyjs || h0 === h1 ? `${h0}${nm})` : `${h0}-${h1}${nm})`
    if (!busy[roomId]) busy[roomId] = []
    busy[roomId].push({ ...range, label })
  })
  return busy
}

export function apiStatusToUi(fStatus) {
  const s = (fStatus || '').trim()
  if (s === '已预约' || s === '已确认') return 'booked'
  if (s === '服务中') return 'serving'
  if (s === '已转耗卡' || s === '已完成') return 'converted'
  if (s === '已取消') return 'cancelled'
  return 'booked'
}

export function uiStatusToApi(uiStatus) {
  if (uiStatus === 'booked') return '已预约'
  if (uiStatus === 'serving') return '服务中'
  if (uiStatus === 'converted') return '已完成'
  if (uiStatus === 'cancelled') return '已取消'
  return ''
}

export function statusText(uiOrApi) {
  const ui = apiStatusToUi(uiOrApi) === 'booked' && !['booked', 'serving', 'converted', 'cancelled'].includes(uiOrApi)
    ? uiOrApi
    : apiStatusToUi(uiOrApi)
  if (ui === 'booked' || uiOrApi === '已预约') return '已预约'
  if (ui === 'serving' || uiOrApi === '服务中') return '服务中'
  if (ui === 'converted' || uiOrApi === '已完成' || uiOrApi === '已转耗卡') return '已完成'
  if (ui === 'cancelled' || uiOrApi === '已取消') return '已取消'
  return uiOrApi || '无'
}

export function formatDateYMD(d) {
  const dt = d instanceof Date ? d : parseApiDateTime(d)
  if (!dt) return ''
  const y = dt.getFullYear()
  const m = String(dt.getMonth() + 1).padStart(2, '0')
  const day = String(dt.getDate()).padStart(2, '0')
  return `${y}-${m}-${day}`
}

/**
 * 待办截止文案:按本地自然日比较(避免「未满 24 小时却显示今天到期」)
 */
export function formatTodoDueLabel(dt) {
  const d = parseApiDateTime(dt)
  if (!d) return ''
  const now = new Date()
  const dayMs = 86400000
  const dueDay = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime()
  const days = Math.round((dueDay - today) / dayMs)
  const hm = formatTimeHM(d)
  if (days < 0) return `逾期${Math.abs(days)}天`
  if (days === 0) return hm ? `今天 ${hm} 到期` : '今天到期'
  if (days === 1) return hm ? `明天 ${hm} 到期` : '明天到期'
  const datePart = `${d.getMonth() + 1}/${d.getDate()}`
  return hm ? `${datePart} ${hm} 到期` : `${datePart} 到期`
}

export function formatTimeHM(dt) {
  if (!dt) return ''
  const d = dt instanceof Date ? dt : parseApiDateTime(dt)
  if (!d) {
    const str = String(dt)
    if (str.includes(' ')) {
      const part = str.split(' ')[1] || ''
      return part.substring(0, 5)
    }
    return str.length >= 5 ? str.substring(0, 5) : ''
  }
  return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
}

export function buildDateTimeHM(dateYmd, timeHm) {
  const tm = (timeHm || '09:00').substring(0, 5)
  return `${dateYmd} ${tm}`
}

export function mapYyjlListRow(row) {
  const yysj = pickRowField(row, 'yysj', 'Yysj')
  const yyjs = pickRowField(row, 'yyjs', 'Yyjs')
  const fStatus = pickRowField(row, 'F_Status', 'f_Status', 'fStatus')
  const date = formatDateYMD(yysj)
  const startTime = formatTimeHM(yysj)
  const endTime = formatTimeHM(yyjs)
  const status = apiStatusToUi(fStatus)
  return {
    id: pickRowField(row, 'id', 'Id', 'F_Id'),
    memberId: pickRowField(row, 'gk', 'Gk'),
    memberName: pickRowField(row, 'gkxm', 'Gkxm') || '无',
    date,
    startTime,
    endTime,
    timeRange: startTime && endTime ? `${startTime}-${endTime}` : '',
    roomId: pickRowField(row, 'roomId', 'RoomId', 'F_RoomId') || '',
    roomName: pickRowField(row, 'roomName', 'RoomName', 'F_RoomName') || '',
    therapistIds: pickRowField(row, 'yyjks', 'Yyjks') ? [pickRowField(row, 'yyjks', 'Yyjks')] : [],
    therapistNames: pickRowField(row, 'yyjksName', 'YyjksName') ? [pickRowField(row, 'yyjksName', 'YyjksName')] : [],
    staffName: pickRowField(row, 'yyjksName', 'YyjksName') || '无',
    project: pickRowField(row, 'yytyxm', 'Yytyxm') || pickRowField(row, 'gkxm', 'Gkxm') || '无',
    status,
    fStatus: fStatus,
    remark: pickRowField(row, 'remark', 'Remark', 'F_Remark') || '',
    mobile: '',
    items: [],
    itemLabels: row.yytyxm ? [row.yytyxm] : [],
    colorKey: 'blue'
  }
}

export function mapYyjlInfoToFormRecord(info, pxList = []) {
  const date = formatDateYMD(info.yysj)
  const items = (pxList || []).map(px => ({
    projectId: px.billingItemId || px.px,
    itemId: px.px,
    billingItemId: px.billingItemId,
    label: px.pxmc,
    count: Number(px.projectNumber) || 1,
    workers: (px.jksList || []).map(j => ({ workerId: j.jks, workerName: j.jksxm }))
  }))
  const itemLabels = items.map(x => `${x.label}×${x.count}`)
  const therapistIds = []
  const therapistNames = []
  items.forEach(it => {
    ;(it.workers || []).forEach(w => {
      if (w.workerId && !therapistIds.includes(w.workerId)) {
        therapistIds.push(w.workerId)
        therapistNames.push(w.workerName || w.workerId)
      }
    })
  })
  return {
    id: info.id,
    memberId: info.gk,
    memberName: info.gkxm,
    date,
    startTime: formatTimeHM(info.yysj),
    endTime: formatTimeHM(info.yyjs),
    roomId: info.roomId,
    roomName: info.roomName,
    remark: info.remark,
    status: apiStatusToUi(info.F_Status),
    fStatus: info.F_Status,
    items,
    itemLabels,
    therapistIds,
    therapistNames
  }
}

export function buildYyjlSubmitBody(form, { storeId, userId, memberName, healthWorkerOptions }) {
  const dateStr = formatDateYMD(form.date)
  const hwMap = new Map((healthWorkerOptions || []).map(x => [x.value, x.label]))
  const memberLabel = memberName || ''
  const lqYyjlPxList = (form.items || [])
    .filter(x => x && x.projectId)
    .map((row, sortNo) => {
      const pn = parseInt(row.count, 10)
      const projectNumber = Number.isNaN(pn) || pn < 1 ? 1 : pn
      const jksArr = Array.isArray(row.workers) ? row.workers : []
      return {
        px: String(row.itemId || row.px || row.projectId),
        pxmc: row.label || '',
        pxjg: Number(row.price) || 0,
        projectNumber,
        sourceType: row.sourceType || '',
        billingItemId: row.billingItemId || row.projectId || '',
        sortNo,
        jksList: jksArr
          .filter(w => w && w.workerId)
          .map((w, ji) => ({
            jks: w.workerId,
            jksxm: hwMap.get(w.workerId) || w.workerName || '',
            sortNo: ji
          }))
      }
    })

  return {
    djmd: storeId,
    yyr: userId,
    gk: form.memberId,
    gkxm: memberLabel,
    yysj: buildDateTimeHM(dateStr, form.startTime),
    yyjs: buildDateTimeHM(dateStr, form.endTime),
    roomId: form.roomId || undefined,
    roomName: form.roomName || undefined,
    remark: form.remark || '',
    lqYyjlPxList
  }
}

export function mapRemainingItemToOption(item) {
  const remaining = Number(item.RemainingCount) || 0
  return {
    value: item.BillingItemId,
    label: `${item.ItemName}(剩余${remaining}次)`,
    itemId: item.ItemId,
    billingItemId: item.BillingItemId,
    price: Number(item.ItemPrice) || 0,
    remaining,
    totalPurchased: Number(item.TotalPurchased) || 0,
    consumed: Number(item.ConsumedCount) || 0,
    sourceType: item.SourceType || '',
    qt2: item.qt2 || ''
  }
}