/** 预约状态:后端中文 ↔ 门店 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 || '' } }