fix: 修复课程统计
This commit is contained in:
43
packages/app/preview/teaching-schedule.spec.mjs
Normal file
43
packages/app/preview/teaching-schedule.spec.mjs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { test } from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { readFile } from 'node:fs/promises'
|
||||||
|
import { createRequire } from 'node:module'
|
||||||
|
import { transform } from 'esbuild'
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url)
|
||||||
|
const source = await readFile(new URL('../src/utils/teaching-schedule.ts', import.meta.url), 'utf8')
|
||||||
|
const { code } = await transform(source, { loader: 'ts', format: 'cjs' })
|
||||||
|
const module = { exports: {} }
|
||||||
|
new Function('require', 'module', 'exports', code)(require, module, module.exports)
|
||||||
|
const { getTeachingScheduleLessons } = module.exports
|
||||||
|
|
||||||
|
function slot(id, statuses) {
|
||||||
|
return {
|
||||||
|
slotId: id, date: '2026-10-29', startTime: '18:00', endTime: '19:00', bookedCount: statuses.length, capacity: 5,
|
||||||
|
students: statuses.map((status, index) => ({ bookingId: `${id}-${index}`, userId: `u-${index}`, nickname: '测试', phone: null, status })),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('只计待确认、已确认、已完成;取消和未出席的独立时段不计课', () => {
|
||||||
|
const result = getTeachingScheduleLessons([
|
||||||
|
slot('pending', ['PENDING_CONFIRMATION']), slot('confirmed', ['CONFIRMED']), slot('completed', ['COMPLETED']),
|
||||||
|
slot('cancelled', ['CANCELLED']), slot('no-show', ['NO_SHOW']), slot('empty', []),
|
||||||
|
])
|
||||||
|
assert.deepEqual(result.map(item => item.slotId), ['pending', 'confirmed', 'completed'])
|
||||||
|
assert.equal(result.reduce((sum, item) => sum + item.students.length, 0), 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('混合状态团课仍计一节课,人数只计有效预约,且保留完成状态', () => {
|
||||||
|
const source = [slot('group', ['CANCELLED', 'CONFIRMED', 'COMPLETED', 'NO_SHOW'])]
|
||||||
|
const snapshot = structuredClone(source)
|
||||||
|
const result = getTeachingScheduleLessons(source)
|
||||||
|
assert.equal(result.length, 1)
|
||||||
|
assert.equal(result[0].students.length, 2)
|
||||||
|
assert.deepEqual(result[0].students.map(student => student.status), ['CONFIRMED', 'COMPLETED'])
|
||||||
|
assert.deepEqual(source, snapshot)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('全部取消或无预约时,课程数和人数均为零', () => {
|
||||||
|
assert.deepEqual(getTeachingScheduleLessons([slot('cancelled', ['CANCELLED', 'CANCELLED'])]), [])
|
||||||
|
assert.deepEqual(getTeachingScheduleLessons([]), [])
|
||||||
|
})
|
||||||
@@ -41,6 +41,7 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
|||||||
import { BookingStatus, type TeachingScheduleSlot } from '@mp-pilates/shared'
|
import { BookingStatus, type TeachingScheduleSlot } from '@mp-pilates/shared'
|
||||||
import { get } from '../utils/request'
|
import { get } from '../utils/request'
|
||||||
import { formatDate } from '../utils/format'
|
import { formatDate } from '../utils/format'
|
||||||
|
import { getTeachingScheduleLessons } from '../utils/teaching-schedule'
|
||||||
|
|
||||||
const props = defineProps<{ refreshKey: number }>()
|
const props = defineProps<{ refreshKey: number }>()
|
||||||
|
|
||||||
@@ -59,11 +60,10 @@ function toMinutes(time: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// One time slot is one lesson, so a small-group class is never double-counted.
|
// One time slot is one lesson, so a small-group class is never double-counted.
|
||||||
// Slots whose bookings were all cancelled are not lessons, and a lesson is
|
// Cancelled / no-show bookings are not lessons, and a lesson is
|
||||||
// done once at least one of its students has been verified (COMPLETED).
|
// done once at least one of its students has been verified (COMPLETED).
|
||||||
const stats = computed(() => {
|
const stats = computed(() => {
|
||||||
const lessons = slots.value
|
const lessons = getTeachingScheduleLessons(slots.value)
|
||||||
.filter(slot => slot.students.some(student => student.status !== BookingStatus.CANCELLED))
|
|
||||||
.map(slot => ({
|
.map(slot => ({
|
||||||
startTime: slot.startTime,
|
startTime: slot.startTime,
|
||||||
endTime: slot.endTime,
|
endTime: slot.endTime,
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ import { useUserStore } from '../../stores/user'
|
|||||||
import { formatDate, getWeekdayLabel, isToday } from '../../utils/format'
|
import { formatDate, getWeekdayLabel, isToday } from '../../utils/format'
|
||||||
import { getSystemLayout } from '../../utils/system'
|
import { getSystemLayout } from '../../utils/system'
|
||||||
import { getErrorMessage } from '../../utils/auth'
|
import { getErrorMessage } from '../../utils/auth'
|
||||||
|
import { getTeachingScheduleLessons } from '../../utils/teaching-schedule'
|
||||||
|
|
||||||
const bookingStore = useBookingStore()
|
const bookingStore = useBookingStore()
|
||||||
const { loggedIn, isAdmin } = storeToRefs(useUserStore())
|
const { loggedIn, isAdmin } = storeToRefs(useUserStore())
|
||||||
@@ -163,7 +164,7 @@ async function loadSchedule(date: string) {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await bookingStore.fetchTeachingSchedule(date)
|
const result = await bookingStore.fetchTeachingSchedule(date)
|
||||||
if (id === requestId) slots.value = [...result].sort((a, b) => a.startTime.localeCompare(b.startTime))
|
if (id === requestId) slots.value = getTeachingScheduleLessons(result).sort((a, b) => a.startTime.localeCompare(b.startTime))
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (id === requestId) error.value = getErrorMessage(err, '请检查网络后重试')
|
if (id === requestId) error.value = getErrorMessage(err, '请检查网络后重试')
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
14
packages/app/src/utils/teaching-schedule.ts
Normal file
14
packages/app/src/utils/teaching-schedule.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { BookingStatus, type TeachingScheduleSlot } from '@mp-pilates/shared'
|
||||||
|
|
||||||
|
const LESSON_STATUSES = new Set<BookingStatus>([
|
||||||
|
BookingStatus.PENDING_CONFIRMATION,
|
||||||
|
BookingStatus.CONFIRMED,
|
||||||
|
BookingStatus.COMPLETED,
|
||||||
|
])
|
||||||
|
|
||||||
|
/** 课表和今日课程采用同一口径;取消、未出席不计课,不修改原始预约记录。 */
|
||||||
|
export function getTeachingScheduleLessons(slots: readonly TeachingScheduleSlot[]): TeachingScheduleSlot[] {
|
||||||
|
return slots
|
||||||
|
.map(slot => ({ ...slot, students: slot.students.filter(student => LESSON_STATUSES.has(student.status)) }))
|
||||||
|
.filter(slot => slot.students.length > 0)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user