Add lessons module, rate cycles, EC2 deploy scripts, and help content
- Lessons module: lesson types, instructors, schedule slots, enrollments, sessions (list + week grid view), lesson plans, grading scales, templates - Rate cycles: replace monthly_rate with billing_interval + billing_unit on enrollments; add weekly/monthly/quarterly rate presets to lesson types and schedule slots with auto-fill on enrollment form - Member detail page: tabbed layout for details, identity documents, enrollments - Sessions week view: custom 7-column grid replacing react-big-calendar - Music store seed: instructors, lesson types, slots, enrollments, sessions, grading scale, lesson plan template - Scrollbar styling: themed to match sidebar/app palette - deploy/: EC2 setup and redeploy scripts, nginx config, systemd service - Help: add Lessons category (overview, types, instructors, slots, enrollments, sessions, plans/grading); collapsible sidebar with independent scroll; remove POS/accounting references from docs
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { eq, and, ne, count, gte, lte, inArray, type Column, type SQL } from 'drizzle-orm'
|
||||
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
|
||||
import { instructors, lessonTypes, scheduleSlots, enrollments, lessonSessions, instructorBlockedDates, storeClosures, gradingScales, gradingScaleLevels, memberLessonPlans, lessonPlanSections, lessonPlanItems, lessonPlanTemplates, lessonPlanTemplateSections, lessonPlanTemplateItems, lessonPlanItemGradeHistory, lessonSessionPlanItems } from '../db/schema/lessons.js'
|
||||
import { members } from '../db/schema/accounts.js'
|
||||
import type {
|
||||
InstructorCreateInput,
|
||||
InstructorUpdateInput,
|
||||
@@ -108,7 +109,9 @@ export const LessonTypeService = {
|
||||
instrument: input.instrument,
|
||||
durationMinutes: input.durationMinutes,
|
||||
lessonFormat: input.lessonFormat,
|
||||
baseRateMonthly: input.baseRateMonthly?.toString(),
|
||||
rateWeekly: input.rateWeekly?.toString(),
|
||||
rateMonthly: input.rateMonthly?.toString(),
|
||||
rateQuarterly: input.rateQuarterly?.toString(),
|
||||
})
|
||||
.returning()
|
||||
return lessonType
|
||||
@@ -151,7 +154,9 @@ export const LessonTypeService = {
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, id: string, input: LessonTypeUpdateInput) {
|
||||
const values: Record<string, unknown> = { ...input, updatedAt: new Date() }
|
||||
if (input.baseRateMonthly !== undefined) values.baseRateMonthly = input.baseRateMonthly.toString()
|
||||
if (input.rateWeekly !== undefined) values.rateWeekly = input.rateWeekly?.toString() ?? null
|
||||
if (input.rateMonthly !== undefined) values.rateMonthly = input.rateMonthly?.toString() ?? null
|
||||
if (input.rateQuarterly !== undefined) values.rateQuarterly = input.rateQuarterly?.toString() ?? null
|
||||
|
||||
const [lessonType] = await db
|
||||
.update(lessonTypes)
|
||||
@@ -199,6 +204,9 @@ export const ScheduleSlotService = {
|
||||
startTime: input.startTime,
|
||||
room: input.room,
|
||||
maxStudents: input.maxStudents,
|
||||
rateWeekly: input.rateWeekly?.toString(),
|
||||
rateMonthly: input.rateMonthly?.toString(),
|
||||
rateQuarterly: input.rateQuarterly?.toString(),
|
||||
})
|
||||
.returning()
|
||||
return slot
|
||||
@@ -346,7 +354,9 @@ export const EnrollmentService = {
|
||||
status: 'active',
|
||||
startDate: input.startDate,
|
||||
endDate: input.endDate,
|
||||
monthlyRate: input.monthlyRate?.toString(),
|
||||
rate: input.rate?.toString(),
|
||||
billingInterval: input.billingInterval,
|
||||
billingUnit: input.billingUnit,
|
||||
notes: input.notes,
|
||||
})
|
||||
.returning()
|
||||
@@ -374,7 +384,6 @@ export const EnrollmentService = {
|
||||
if (filters?.accountId) conditions.push(eq(enrollments.accountId, filters.accountId))
|
||||
if (filters?.instructorId) conditions.push(eq(enrollments.instructorId, filters.instructorId))
|
||||
if (filters?.status?.length) {
|
||||
const { inArray } = await import('drizzle-orm')
|
||||
conditions.push(inArray(enrollments.status, filters.status as any))
|
||||
}
|
||||
|
||||
@@ -384,23 +393,65 @@ export const EnrollmentService = {
|
||||
start_date: enrollments.startDate,
|
||||
status: enrollments.status,
|
||||
created_at: enrollments.createdAt,
|
||||
member_name: members.firstName,
|
||||
}
|
||||
|
||||
let query = db.select().from(enrollments).where(where).$dynamic()
|
||||
let query = db
|
||||
.select({
|
||||
id: enrollments.id,
|
||||
memberId: enrollments.memberId,
|
||||
accountId: enrollments.accountId,
|
||||
scheduleSlotId: enrollments.scheduleSlotId,
|
||||
instructorId: enrollments.instructorId,
|
||||
status: enrollments.status,
|
||||
startDate: enrollments.startDate,
|
||||
endDate: enrollments.endDate,
|
||||
rate: enrollments.rate,
|
||||
billingInterval: enrollments.billingInterval,
|
||||
billingUnit: enrollments.billingUnit,
|
||||
makeupCredits: enrollments.makeupCredits,
|
||||
notes: enrollments.notes,
|
||||
createdAt: enrollments.createdAt,
|
||||
updatedAt: enrollments.updatedAt,
|
||||
memberName: members.firstName,
|
||||
memberLastName: members.lastName,
|
||||
instructorName: instructors.displayName,
|
||||
slotDayOfWeek: scheduleSlots.dayOfWeek,
|
||||
slotStartTime: scheduleSlots.startTime,
|
||||
lessonTypeName: lessonTypes.name,
|
||||
})
|
||||
.from(enrollments)
|
||||
.leftJoin(members, eq(enrollments.memberId, members.id))
|
||||
.leftJoin(instructors, eq(enrollments.instructorId, instructors.id))
|
||||
.leftJoin(scheduleSlots, eq(enrollments.scheduleSlotId, scheduleSlots.id))
|
||||
.leftJoin(lessonTypes, eq(scheduleSlots.lessonTypeId, lessonTypes.id))
|
||||
.where(where)
|
||||
.$dynamic()
|
||||
query = withSort(query, params.sort, params.order, sortableColumns, enrollments.createdAt)
|
||||
query = withPagination(query, params.page, params.limit)
|
||||
|
||||
const [data, [{ total }]] = await Promise.all([
|
||||
const [rows, [{ total }]] = await Promise.all([
|
||||
query,
|
||||
db.select({ total: count() }).from(enrollments).where(where),
|
||||
])
|
||||
|
||||
const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
||||
const data = rows.map((r) => ({
|
||||
...r,
|
||||
memberName: r.memberName && r.memberLastName ? `${r.memberName} ${r.memberLastName}` : undefined,
|
||||
slotInfo: r.slotDayOfWeek != null && r.slotStartTime
|
||||
? `${DAYS[r.slotDayOfWeek]} ${r.slotStartTime.slice(0, 5)}`
|
||||
: undefined,
|
||||
}))
|
||||
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, id: string, input: EnrollmentUpdateInput) {
|
||||
const values: Record<string, unknown> = { ...input, updatedAt: new Date() }
|
||||
if (input.monthlyRate !== undefined) values.monthlyRate = input.monthlyRate.toString()
|
||||
if (input.rate !== undefined) values.rate = input.rate?.toString() ?? null
|
||||
if (input.billingInterval !== undefined) values.billingInterval = input.billingInterval
|
||||
if (input.billingUnit !== undefined) values.billingUnit = input.billingUnit
|
||||
|
||||
const [enrollment] = await db
|
||||
.update(enrollments)
|
||||
@@ -542,29 +593,11 @@ export const LessonSessionService = {
|
||||
}) {
|
||||
const conditions: SQL[] = []
|
||||
|
||||
if (filters?.enrollmentId) {
|
||||
conditions.push(eq(lessonSessions.enrollmentId, filters.enrollmentId))
|
||||
}
|
||||
if (filters?.instructorId) {
|
||||
// Join through enrollment to filter by instructor
|
||||
const enrollmentIds = await db
|
||||
.select({ id: enrollments.id })
|
||||
.from(enrollments)
|
||||
.where(eq(enrollments.instructorId, filters.instructorId))
|
||||
if (enrollmentIds.length === 0) {
|
||||
return paginatedResponse([], 0, params.page, params.limit)
|
||||
}
|
||||
conditions.push(inArray(lessonSessions.enrollmentId, enrollmentIds.map((e) => e.id)))
|
||||
}
|
||||
if (filters?.status?.length) {
|
||||
conditions.push(inArray(lessonSessions.status, filters.status as any))
|
||||
}
|
||||
if (filters?.dateFrom) {
|
||||
conditions.push(gte(lessonSessions.scheduledDate, filters.dateFrom))
|
||||
}
|
||||
if (filters?.dateTo) {
|
||||
conditions.push(lte(lessonSessions.scheduledDate, filters.dateTo))
|
||||
}
|
||||
if (filters?.enrollmentId) conditions.push(eq(lessonSessions.enrollmentId, filters.enrollmentId))
|
||||
if (filters?.instructorId) conditions.push(eq(enrollments.instructorId, filters.instructorId))
|
||||
if (filters?.status?.length) conditions.push(inArray(lessonSessions.status, filters.status as any))
|
||||
if (filters?.dateFrom) conditions.push(gte(lessonSessions.scheduledDate, filters.dateFrom))
|
||||
if (filters?.dateTo) conditions.push(lte(lessonSessions.scheduledDate, filters.dateTo))
|
||||
|
||||
const where = conditions.length > 0 ? and(...conditions) : undefined
|
||||
|
||||
@@ -575,15 +608,54 @@ export const LessonSessionService = {
|
||||
created_at: lessonSessions.createdAt,
|
||||
}
|
||||
|
||||
let query = db.select().from(lessonSessions).where(where).$dynamic()
|
||||
let query = db
|
||||
.select({
|
||||
id: lessonSessions.id,
|
||||
enrollmentId: lessonSessions.enrollmentId,
|
||||
scheduledDate: lessonSessions.scheduledDate,
|
||||
scheduledTime: lessonSessions.scheduledTime,
|
||||
actualStartTime: lessonSessions.actualStartTime,
|
||||
actualEndTime: lessonSessions.actualEndTime,
|
||||
status: lessonSessions.status,
|
||||
instructorNotes: lessonSessions.instructorNotes,
|
||||
memberNotes: lessonSessions.memberNotes,
|
||||
homeworkAssigned: lessonSessions.homeworkAssigned,
|
||||
nextLessonGoals: lessonSessions.nextLessonGoals,
|
||||
topicsCovered: lessonSessions.topicsCovered,
|
||||
makeupForSessionId: lessonSessions.makeupForSessionId,
|
||||
substituteInstructorId: lessonSessions.substituteInstructorId,
|
||||
notesCompletedAt: lessonSessions.notesCompletedAt,
|
||||
createdAt: lessonSessions.createdAt,
|
||||
updatedAt: lessonSessions.updatedAt,
|
||||
memberName: members.firstName,
|
||||
memberLastName: members.lastName,
|
||||
instructorName: instructors.displayName,
|
||||
lessonTypeName: lessonTypes.name,
|
||||
})
|
||||
.from(lessonSessions)
|
||||
.leftJoin(enrollments, eq(lessonSessions.enrollmentId, enrollments.id))
|
||||
.leftJoin(members, eq(enrollments.memberId, members.id))
|
||||
.leftJoin(instructors, eq(enrollments.instructorId, instructors.id))
|
||||
.leftJoin(scheduleSlots, eq(enrollments.scheduleSlotId, scheduleSlots.id))
|
||||
.leftJoin(lessonTypes, eq(scheduleSlots.lessonTypeId, lessonTypes.id))
|
||||
.where(where)
|
||||
.$dynamic()
|
||||
query = withSort(query, params.sort, params.order, sortableColumns, lessonSessions.scheduledDate)
|
||||
query = withPagination(query, params.page, params.limit)
|
||||
|
||||
const [data, [{ total }]] = await Promise.all([
|
||||
const [rows, [{ total }]] = await Promise.all([
|
||||
query,
|
||||
db.select({ total: count() }).from(lessonSessions).where(where),
|
||||
db.select({ total: count() })
|
||||
.from(lessonSessions)
|
||||
.leftJoin(enrollments, eq(lessonSessions.enrollmentId, enrollments.id))
|
||||
.where(where),
|
||||
])
|
||||
|
||||
const data = rows.map((r) => ({
|
||||
...r,
|
||||
memberName: r.memberName && r.memberLastName ? `${r.memberName} ${r.memberLastName}` : undefined,
|
||||
}))
|
||||
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user