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:
28
packages/backend/src/db/migrations/0037_rate_cycles.sql
Normal file
28
packages/backend/src/db/migrations/0037_rate_cycles.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
-- Rate cycles: per-instructor rates on schedule_slot, generic billing terms on enrollment,
|
||||
-- replace base_rate_monthly on lesson_type with weekly/monthly/quarterly presets
|
||||
|
||||
CREATE TYPE billing_unit AS ENUM ('day', 'week', 'month', 'quarter', 'year');
|
||||
|
||||
-- lesson_type: replace base_rate_monthly varchar with three numeric rate columns
|
||||
ALTER TABLE lesson_type ADD COLUMN rate_weekly numeric(10,2);
|
||||
ALTER TABLE lesson_type ADD COLUMN rate_monthly numeric(10,2);
|
||||
ALTER TABLE lesson_type ADD COLUMN rate_quarterly numeric(10,2);
|
||||
UPDATE lesson_type
|
||||
SET rate_monthly = base_rate_monthly::numeric
|
||||
WHERE base_rate_monthly IS NOT NULL
|
||||
AND base_rate_monthly ~ '^\d+(\.\d+)?$';
|
||||
ALTER TABLE lesson_type DROP COLUMN base_rate_monthly;
|
||||
|
||||
-- schedule_slot: add per-instructor preset rates
|
||||
ALTER TABLE schedule_slot ADD COLUMN rate_weekly numeric(10,2);
|
||||
ALTER TABLE schedule_slot ADD COLUMN rate_monthly numeric(10,2);
|
||||
ALTER TABLE schedule_slot ADD COLUMN rate_quarterly numeric(10,2);
|
||||
|
||||
-- enrollment: replace monthly_rate with generic billing terms
|
||||
ALTER TABLE enrollment ADD COLUMN rate numeric(10,2);
|
||||
ALTER TABLE enrollment ADD COLUMN billing_interval integer;
|
||||
ALTER TABLE enrollment ADD COLUMN billing_unit billing_unit;
|
||||
UPDATE enrollment
|
||||
SET rate = monthly_rate, billing_interval = 1, billing_unit = 'month'
|
||||
WHERE monthly_rate IS NOT NULL;
|
||||
ALTER TABLE enrollment DROP COLUMN monthly_rate;
|
||||
@@ -260,6 +260,13 @@
|
||||
"when": 1774960000000,
|
||||
"tag": "0036_lesson_plan_templates",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 37,
|
||||
"version": "7",
|
||||
"when": 1774970000000,
|
||||
"tag": "0037_rate_cycles",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { accounts, members } from './accounts.js'
|
||||
// --- Enums ---
|
||||
|
||||
export const lessonFormatEnum = pgEnum('lesson_format', ['private', 'group'])
|
||||
export const billingUnitEnum = pgEnum('billing_unit', ['day', 'week', 'month', 'quarter', 'year'])
|
||||
|
||||
// --- Tables ---
|
||||
|
||||
@@ -37,7 +38,9 @@ export const lessonTypes = pgTable('lesson_type', {
|
||||
instrument: varchar('instrument', { length: 100 }),
|
||||
durationMinutes: integer('duration_minutes').notNull(),
|
||||
lessonFormat: lessonFormatEnum('lesson_format').notNull().default('private'),
|
||||
baseRateMonthly: varchar('base_rate_monthly', { length: 20 }),
|
||||
rateWeekly: numeric('rate_weekly', { precision: 10, scale: 2 }),
|
||||
rateMonthly: numeric('rate_monthly', { precision: 10, scale: 2 }),
|
||||
rateQuarterly: numeric('rate_quarterly', { precision: 10, scale: 2 }),
|
||||
isActive: boolean('is_active').notNull().default(true),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
@@ -55,6 +58,9 @@ export const scheduleSlots = pgTable('schedule_slot', {
|
||||
startTime: time('start_time').notNull(),
|
||||
room: varchar('room', { length: 100 }),
|
||||
maxStudents: integer('max_students').notNull().default(1),
|
||||
rateWeekly: numeric('rate_weekly', { precision: 10, scale: 2 }),
|
||||
rateMonthly: numeric('rate_monthly', { precision: 10, scale: 2 }),
|
||||
rateQuarterly: numeric('rate_quarterly', { precision: 10, scale: 2 }),
|
||||
isActive: boolean('is_active').notNull().default(true),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
@@ -84,7 +90,9 @@ export const enrollments = pgTable('enrollment', {
|
||||
status: enrollmentStatusEnum('status').notNull().default('active'),
|
||||
startDate: date('start_date').notNull(),
|
||||
endDate: date('end_date'),
|
||||
monthlyRate: numeric('monthly_rate', { precision: 10, scale: 2 }),
|
||||
rate: numeric('rate', { precision: 10, scale: 2 }),
|
||||
billingInterval: integer('billing_interval'),
|
||||
billingUnit: billingUnitEnum('billing_unit'),
|
||||
makeupCredits: integer('makeup_credits').notNull().default(0),
|
||||
notes: text('notes'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
|
||||
@@ -39,16 +39,17 @@ async function seed() {
|
||||
}
|
||||
|
||||
// --- Admin user (if not exists) ---
|
||||
const adminPassword = process.env.ADMIN_PASSWORD ?? 'admin1234'
|
||||
const [adminUser] = await sql`SELECT id FROM "user" WHERE email = 'admin@lunarfront.dev'`
|
||||
if (!adminUser) {
|
||||
const bcrypt = await import('bcrypt')
|
||||
const hashedPw = await (bcrypt.default || bcrypt).hash('admin1234', 10)
|
||||
const hashedPw = await (bcrypt.default || bcrypt).hash(adminPassword, 10)
|
||||
const [user] = await sql`INSERT INTO "user" (email, password_hash, first_name, last_name, role) VALUES ('admin@lunarfront.dev', ${hashedPw}, 'Admin', 'User', 'admin') RETURNING id`
|
||||
const [adminRole] = await sql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1`
|
||||
if (adminRole) {
|
||||
await sql`INSERT INTO user_role_assignment (user_id, role_id) VALUES (${user.id}, ${adminRole.id}) ON CONFLICT DO NOTHING`
|
||||
}
|
||||
console.log(' Created admin user: admin@lunarfront.dev / admin1234')
|
||||
console.log(` Created admin user: admin@lunarfront.dev / ${adminPassword}`)
|
||||
} else {
|
||||
const [adminRole] = await sql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1`
|
||||
if (adminRole) {
|
||||
|
||||
@@ -162,10 +162,478 @@ async function seed() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Enable lessons module ---
|
||||
await sql`UPDATE module_config SET enabled = true WHERE slug = 'lessons'`
|
||||
console.log(' Enabled lessons module')
|
||||
|
||||
// --- Lesson seed data ---
|
||||
await seedLessons(sql)
|
||||
|
||||
console.log('\nMusic store seed complete!')
|
||||
await sql.end()
|
||||
}
|
||||
|
||||
// Returns dates (YYYY-MM-DD) for every occurrence of `dayOfWeek` (0=Sun)
|
||||
// starting from `startDate`, for `count` weeks.
|
||||
function weeklyDates(startDate: string, dayOfWeek: number, count: number): string[] {
|
||||
const d = new Date(startDate + 'T12:00:00Z')
|
||||
while (d.getUTCDay() !== dayOfWeek) d.setUTCDate(d.getUTCDate() + 1)
|
||||
return Array.from({ length: count }, (_, i) => {
|
||||
const nd = new Date(d)
|
||||
nd.setUTCDate(nd.getUTCDate() + i * 7)
|
||||
return nd.toISOString().split('T')[0]
|
||||
})
|
||||
}
|
||||
|
||||
async function seedLessons(sql: any) {
|
||||
console.log('\nSeeding lessons data...')
|
||||
|
||||
// ── Grading scale ──────────────────────────────────────────────────────────
|
||||
const [existingScale] = await sql`SELECT id FROM grading_scale WHERE name = 'Standard Progress'`
|
||||
let scaleId: string
|
||||
if (existingScale) {
|
||||
scaleId = existingScale.id
|
||||
} else {
|
||||
const [scale] = await sql`
|
||||
INSERT INTO grading_scale (name, description, is_default, is_active)
|
||||
VALUES ('Standard Progress', 'Four-level scale used across all instruments', true, true)
|
||||
RETURNING id`
|
||||
scaleId = scale.id
|
||||
const levels = [
|
||||
{ value: '1', label: 'Introduced', numericValue: 1, colorHex: null, sortOrder: 1 },
|
||||
{ value: '2', label: 'Developing', numericValue: 2, colorHex: '#EAB308', sortOrder: 2 },
|
||||
{ value: '3', label: 'Proficient', numericValue: 3, colorHex: '#3B82F6', sortOrder: 3 },
|
||||
{ value: '4', label: 'Mastered', numericValue: 4, colorHex: '#22C55E', sortOrder: 4 },
|
||||
]
|
||||
for (const lv of levels) {
|
||||
await sql`INSERT INTO grading_scale_level (grading_scale_id, value, label, numeric_value, color_hex, sort_order)
|
||||
VALUES (${scaleId}, ${lv.value}, ${lv.label}, ${lv.numericValue}, ${lv.colorHex}, ${lv.sortOrder})`
|
||||
}
|
||||
console.log(' Grading scale: Standard Progress')
|
||||
}
|
||||
|
||||
// ── Instructors ────────────────────────────────────────────────────────────
|
||||
const instructorDefs = [
|
||||
{
|
||||
displayName: 'Sarah Mitchell',
|
||||
bio: 'Classical piano and violin instructor with 12 years of teaching experience. Suzuki-certified for violin.',
|
||||
instruments: ['Piano', 'Violin'],
|
||||
},
|
||||
{
|
||||
displayName: 'Marcus Webb',
|
||||
bio: 'Guitarist and bassist specializing in rock, blues, and jazz. 8 years of studio and teaching experience.',
|
||||
instruments: ['Guitar', 'Bass'],
|
||||
},
|
||||
{
|
||||
displayName: 'Diana Reyes',
|
||||
bio: 'Woodwind specialist with a music education degree. Teaches saxophone, clarinet, and flute at all levels.',
|
||||
instruments: ['Saxophone', 'Clarinet', 'Flute'],
|
||||
},
|
||||
]
|
||||
|
||||
const instrIds: Record<string, string> = {}
|
||||
for (const def of instructorDefs) {
|
||||
const [existing] = await sql`SELECT id FROM instructor WHERE display_name = ${def.displayName}`
|
||||
if (existing) { instrIds[def.displayName] = existing.id; continue }
|
||||
const [row] = await sql`
|
||||
INSERT INTO instructor (display_name, bio, instruments, is_active)
|
||||
VALUES (${def.displayName}, ${def.bio}, ${def.instruments}, true)
|
||||
RETURNING id`
|
||||
instrIds[def.displayName] = row.id
|
||||
console.log(` Instructor: ${def.displayName}`)
|
||||
}
|
||||
|
||||
// ── Lesson types ───────────────────────────────────────────────────────────
|
||||
const lessonTypeDefs = [
|
||||
{ name: '30-min Private Piano', instrument: 'Piano', durationMinutes: 30, lessonFormat: 'private', rateWeekly: '35.00', rateMonthly: '120.00', rateQuarterly: '330.00' },
|
||||
{ name: '45-min Private Piano', instrument: 'Piano', durationMinutes: 45, lessonFormat: 'private', rateWeekly: '50.00', rateMonthly: '175.00', rateQuarterly: '480.00' },
|
||||
{ name: '30-min Private Guitar', instrument: 'Guitar', durationMinutes: 30, lessonFormat: 'private', rateWeekly: '35.00', rateMonthly: '120.00', rateQuarterly: '330.00' },
|
||||
{ name: '30-min Private Violin', instrument: 'Violin', durationMinutes: 30, lessonFormat: 'private', rateWeekly: '35.00', rateMonthly: '120.00', rateQuarterly: '330.00' },
|
||||
{ name: '30-min Private Woodwind',instrument: null, durationMinutes: 30, lessonFormat: 'private', rateWeekly: '35.00', rateMonthly: '120.00', rateQuarterly: '330.00' },
|
||||
{ name: '45-min Group Guitar', instrument: 'Guitar', durationMinutes: 45, lessonFormat: 'group', rateWeekly: null, rateMonthly: '75.00', rateQuarterly: '200.00' },
|
||||
]
|
||||
|
||||
const ltIds: Record<string, string> = {}
|
||||
for (const lt of lessonTypeDefs) {
|
||||
const [existing] = await sql`SELECT id FROM lesson_type WHERE name = ${lt.name}`
|
||||
if (existing) { ltIds[lt.name] = existing.id; continue }
|
||||
const [row] = await sql`
|
||||
INSERT INTO lesson_type (name, instrument, duration_minutes, lesson_format, rate_weekly, rate_monthly, rate_quarterly, is_active)
|
||||
VALUES (${lt.name}, ${lt.instrument}, ${lt.durationMinutes}, ${lt.lessonFormat}, ${lt.rateWeekly}, ${lt.rateMonthly}, ${lt.rateQuarterly}, true)
|
||||
RETURNING id`
|
||||
ltIds[lt.name] = row.id
|
||||
console.log(` Lesson type: ${lt.name}`)
|
||||
}
|
||||
|
||||
// ── Schedule slots ─────────────────────────────────────────────────────────
|
||||
// dayOfWeek: 0=Sun, 1=Mon, 2=Tue, 3=Wed, 4=Thu, 5=Fri, 6=Sat
|
||||
const slotDefs = [
|
||||
// Sarah Mitchell — Piano & Violin
|
||||
{ key: 'sarah_mon_1530_piano30', instructor: 'Sarah Mitchell', lessonType: '30-min Private Piano', dayOfWeek: 1, startTime: '15:30', room: 'Studio 1', maxStudents: 1, rateMonthly: null },
|
||||
{ key: 'sarah_mon_1600_piano30', instructor: 'Sarah Mitchell', lessonType: '30-min Private Piano', dayOfWeek: 1, startTime: '16:00', room: 'Studio 1', maxStudents: 1, rateMonthly: null },
|
||||
{ key: 'sarah_mon_1700_piano45', instructor: 'Sarah Mitchell', lessonType: '45-min Private Piano', dayOfWeek: 1, startTime: '17:00', room: 'Studio 1', maxStudents: 1, rateMonthly: null },
|
||||
{ key: 'sarah_wed_1530_violin', instructor: 'Sarah Mitchell', lessonType: '30-min Private Violin', dayOfWeek: 3, startTime: '15:30', room: 'Studio 1', maxStudents: 1, rateMonthly: null },
|
||||
{ key: 'sarah_wed_1600_violin', instructor: 'Sarah Mitchell', lessonType: '30-min Private Violin', dayOfWeek: 3, startTime: '16:00', room: 'Studio 1', maxStudents: 1, rateMonthly: null },
|
||||
// Marcus Webb — Guitar (rate override: $115/mo, $400/qtr — slightly above default for experienced instructor)
|
||||
{ key: 'marcus_tue_1500_guitar', instructor: 'Marcus Webb', lessonType: '30-min Private Guitar', dayOfWeek: 2, startTime: '15:00', room: 'Studio 2', maxStudents: 1, rateMonthly: '115.00' },
|
||||
{ key: 'marcus_tue_1600_guitar', instructor: 'Marcus Webb', lessonType: '30-min Private Guitar', dayOfWeek: 2, startTime: '16:00', room: 'Studio 2', maxStudents: 1, rateMonthly: '115.00' },
|
||||
{ key: 'marcus_thu_1600_group', instructor: 'Marcus Webb', lessonType: '45-min Group Guitar', dayOfWeek: 4, startTime: '16:00', room: 'Studio 2', maxStudents: 5, rateMonthly: null },
|
||||
{ key: 'marcus_sat_1000_guitar', instructor: 'Marcus Webb', lessonType: '30-min Private Guitar', dayOfWeek: 6, startTime: '10:00', room: 'Studio 2', maxStudents: 1, rateMonthly: '115.00' },
|
||||
// Diana Reyes — Woodwind
|
||||
{ key: 'diana_mon_1630_woodwind', instructor: 'Diana Reyes', lessonType: '30-min Private Woodwind', dayOfWeek: 1, startTime: '16:30', room: 'Studio 3', maxStudents: 1, rateMonthly: null },
|
||||
{ key: 'diana_wed_1500_woodwind', instructor: 'Diana Reyes', lessonType: '30-min Private Woodwind', dayOfWeek: 3, startTime: '15:00', room: 'Studio 3', maxStudents: 1, rateMonthly: null },
|
||||
{ key: 'diana_fri_1600_woodwind', instructor: 'Diana Reyes', lessonType: '30-min Private Woodwind', dayOfWeek: 5, startTime: '16:00', room: 'Studio 3', maxStudents: 1, rateMonthly: null },
|
||||
]
|
||||
|
||||
const slotIds: Record<string, string> = {}
|
||||
for (const s of slotDefs) {
|
||||
const instrId = instrIds[s.instructor]
|
||||
const ltId = ltIds[s.lessonType]
|
||||
const [existing] = await sql`
|
||||
SELECT id FROM schedule_slot
|
||||
WHERE instructor_id = ${instrId} AND day_of_week = ${s.dayOfWeek} AND start_time = ${s.startTime + ':00'} AND is_active = true`
|
||||
if (existing) { slotIds[s.key] = existing.id; continue }
|
||||
const [row] = await sql`
|
||||
INSERT INTO schedule_slot (instructor_id, lesson_type_id, day_of_week, start_time, room, max_students, rate_monthly, is_active)
|
||||
VALUES (${instrId}, ${ltId}, ${s.dayOfWeek}, ${s.startTime}, ${s.room}, ${s.maxStudents}, ${s.rateMonthly}, true)
|
||||
RETURNING id`
|
||||
slotIds[s.key] = row.id
|
||||
console.log(` Slot: ${s.instructor} — ${s.lessonType} (${['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][s.dayOfWeek]} ${s.startTime})`)
|
||||
}
|
||||
|
||||
// ── Look up member IDs ─────────────────────────────────────────────────────
|
||||
const memberRows = await sql`SELECT m.id, m.first_name, m.last_name, m.account_id FROM member m`
|
||||
const memberMap: Record<string, { id: string; accountId: string }> = {}
|
||||
for (const m of memberRows) memberMap[`${m.first_name} ${m.last_name}`] = { id: m.id, accountId: m.account_id }
|
||||
|
||||
// ── Enrollments ────────────────────────────────────────────────────────────
|
||||
// Each entry: member, slot key, start date (must match slot's day of week), rate, billingInterval, billingUnit
|
||||
const enrollmentDefs = [
|
||||
{
|
||||
member: 'Tommy Smith', slotKey: 'sarah_mon_1530_piano30', startDate: '2026-01-05',
|
||||
rate: '120.00', billingInterval: 1, billingUnit: 'month',
|
||||
notes: 'Tommy is 8 years old, complete beginner. Parents prefer Monday after school.',
|
||||
},
|
||||
{
|
||||
member: 'Jake Johnson', slotKey: 'sarah_wed_1530_violin', startDate: '2026-01-07',
|
||||
rate: '120.00', billingInterval: 1, billingUnit: 'month',
|
||||
notes: 'Jake (age 10) started Suzuki Book 1 last year at his school program.',
|
||||
},
|
||||
{
|
||||
member: 'Emily Chen', slotKey: 'marcus_tue_1500_guitar', startDate: '2026-01-06',
|
||||
rate: '115.00', billingInterval: 1, billingUnit: 'month',
|
||||
notes: 'Emily plays some chords already — looking to improve technique and learn to read music.',
|
||||
},
|
||||
{
|
||||
member: 'Mike Thompson', slotKey: 'marcus_sat_1000_guitar', startDate: '2026-02-07',
|
||||
rate: '115.00', billingInterval: 1, billingUnit: 'month',
|
||||
notes: 'Mike is an adult beginner. Prefers Saturday mornings.',
|
||||
},
|
||||
{
|
||||
member: 'Carlos Garcia', slotKey: 'diana_fri_1600_woodwind', startDate: '2026-01-09',
|
||||
rate: '120.00', billingInterval: 1, billingUnit: 'month',
|
||||
notes: 'Carlos plays alto sax — returning student after a 2-year break.',
|
||||
},
|
||||
]
|
||||
|
||||
const enrollmentIds: Record<string, string> = {}
|
||||
for (const e of enrollmentDefs) {
|
||||
const m = memberMap[e.member]
|
||||
if (!m) { console.log(` ⚠ Member not found: ${e.member} — skipping enrollment`); continue }
|
||||
const slotId = slotIds[e.slotKey]
|
||||
if (!slotId) { console.log(` ⚠ Slot not found: ${e.slotKey} — skipping enrollment`); continue }
|
||||
const [slot] = await sql`SELECT instructor_id FROM schedule_slot WHERE id = ${slotId}`
|
||||
|
||||
const [existing] = await sql`
|
||||
SELECT id FROM enrollment WHERE member_id = ${m.id} AND schedule_slot_id = ${slotId}`
|
||||
if (existing) { enrollmentIds[e.member] = existing.id; continue }
|
||||
|
||||
const [row] = await sql`
|
||||
INSERT INTO enrollment (member_id, account_id, schedule_slot_id, instructor_id, status, start_date, rate, billing_interval, billing_unit, notes)
|
||||
VALUES (${m.id}, ${m.accountId}, ${slotId}, ${slot.instructor_id}, 'active', ${e.startDate}, ${e.rate}, ${e.billingInterval}, ${e.billingUnit}, ${e.notes})
|
||||
RETURNING id`
|
||||
enrollmentIds[e.member] = row.id
|
||||
console.log(` Enrollment: ${e.member}`)
|
||||
}
|
||||
|
||||
// ── Lesson sessions ────────────────────────────────────────────────────────
|
||||
// Generate past sessions (attended/missed) and a few upcoming (scheduled)
|
||||
// Format: [date, status, instructorNotes?]
|
||||
type SessionEntry = [string, string, string?]
|
||||
|
||||
interface SessionSet {
|
||||
enrollmentKey: string
|
||||
slotKey: string
|
||||
entries: SessionEntry[]
|
||||
}
|
||||
|
||||
const today = '2026-03-30'
|
||||
|
||||
const sessionSets: SessionSet[] = [
|
||||
{
|
||||
enrollmentKey: 'Tommy Smith',
|
||||
slotKey: 'sarah_mon_1530_piano30',
|
||||
entries: [
|
||||
// Past sessions — Monday 15:30
|
||||
['2026-01-05', 'attended'],
|
||||
['2026-01-12', 'attended'],
|
||||
['2026-01-19', 'attended', 'Great progress on Clair de Lune beginner arrangement. Hand position improving.'],
|
||||
['2026-01-26', 'missed'],
|
||||
['2026-02-02', 'attended'],
|
||||
['2026-02-09', 'attended', 'Worked on legato technique. Introduced bass clef reading.'],
|
||||
['2026-02-16', 'attended'],
|
||||
['2026-02-23', 'attended'],
|
||||
['2026-03-02', 'attended', 'Tommy played "Ode to Joy" hands together for the first time — great milestone!'],
|
||||
['2026-03-09', 'attended'],
|
||||
['2026-03-16', 'attended', 'Focusing on dynamics this month. Tommy responds well to storytelling analogies.'],
|
||||
['2026-03-23', 'attended'],
|
||||
// Upcoming
|
||||
['2026-03-30', 'scheduled'],
|
||||
['2026-04-06', 'scheduled'],
|
||||
['2026-04-13', 'scheduled'],
|
||||
],
|
||||
},
|
||||
{
|
||||
enrollmentKey: 'Jake Johnson',
|
||||
slotKey: 'sarah_wed_1530_violin',
|
||||
entries: [
|
||||
['2026-01-07', 'attended'],
|
||||
['2026-01-14', 'attended'],
|
||||
['2026-01-21', 'attended', 'Jake shifted to Suzuki Book 1 Track 4. Bow hold still needs work.'],
|
||||
['2026-01-28', 'attended'],
|
||||
['2026-02-04', 'missed'],
|
||||
['2026-02-11', 'attended', 'Good session — bow hold much improved. Started Perpetual Motion.'],
|
||||
['2026-02-18', 'attended'],
|
||||
['2026-02-25', 'attended'],
|
||||
['2026-03-04', 'attended', 'Perpetual Motion up to speed. Introduced Allegretto.'],
|
||||
['2026-03-11', 'attended'],
|
||||
['2026-03-18', 'attended', 'Jake performed Twinkle Variation A at slow tempo very cleanly.'],
|
||||
['2026-03-25', 'attended'],
|
||||
['2026-04-01', 'scheduled'],
|
||||
['2026-04-08', 'scheduled'],
|
||||
['2026-04-15', 'scheduled'],
|
||||
],
|
||||
},
|
||||
{
|
||||
enrollmentKey: 'Emily Chen',
|
||||
slotKey: 'marcus_tue_1500_guitar',
|
||||
entries: [
|
||||
['2026-01-06', 'attended'],
|
||||
['2026-01-13', 'attended', 'Emily knows open chords (G, C, D, Em, Am). Introduced barre chords.'],
|
||||
['2026-01-20', 'attended'],
|
||||
['2026-01-27', 'attended', 'F barre chord coming along. Introduced power chords and palm muting.'],
|
||||
['2026-02-03', 'attended'],
|
||||
['2026-02-10', 'attended', 'Working through major scale patterns. Emily picks things up fast.'],
|
||||
['2026-02-17', 'missed'],
|
||||
['2026-02-24', 'attended'],
|
||||
['2026-03-03', 'attended', 'Started pentatonic minor scale. Playing along to simple backing track.'],
|
||||
['2026-03-10', 'attended'],
|
||||
['2026-03-17', 'attended', 'Emily improvised a 4-bar phrase over a blues backing — sounded great!'],
|
||||
['2026-03-24', 'attended'],
|
||||
['2026-03-31', 'scheduled'],
|
||||
['2026-04-07', 'scheduled'],
|
||||
['2026-04-14', 'scheduled'],
|
||||
],
|
||||
},
|
||||
{
|
||||
enrollmentKey: 'Mike Thompson',
|
||||
slotKey: 'marcus_sat_1000_guitar',
|
||||
entries: [
|
||||
['2026-02-07', 'attended', 'First lesson — covered proper posture, right/left hand basics, and open E and A strings.'],
|
||||
['2026-02-14', 'attended'],
|
||||
['2026-02-21', 'attended', 'Introduced G, C, Em chords. Switching between G and Em getting smoother.'],
|
||||
['2026-02-28', 'attended'],
|
||||
['2026-03-07', 'attended', 'Added D chord. Working on "Knockin\' on Heaven\'s Door" intro pattern.'],
|
||||
['2026-03-14', 'attended'],
|
||||
['2026-03-21', 'attended', 'Mike played the intro riff cleanly at tempo. Really motivated student.'],
|
||||
['2026-03-28', 'attended'],
|
||||
['2026-04-04', 'scheduled'],
|
||||
['2026-04-11', 'scheduled'],
|
||||
['2026-04-18', 'scheduled'],
|
||||
],
|
||||
},
|
||||
{
|
||||
enrollmentKey: 'Carlos Garcia',
|
||||
slotKey: 'diana_fri_1600_woodwind',
|
||||
entries: [
|
||||
['2026-01-09', 'attended', 'Assessment session — Carlos retained most of his fundamentals. Some embouchure drift after the break.'],
|
||||
['2026-01-16', 'attended'],
|
||||
['2026-01-23', 'attended', 'Back up to speed on low register. Working on altissimo range.'],
|
||||
['2026-01-30', 'attended'],
|
||||
['2026-02-06', 'attended', 'Started working through Charlie Parker\'s "Billie\'s Bounce" head at half tempo.'],
|
||||
['2026-02-13', 'attended'],
|
||||
['2026-02-20', 'attended', 'Tone in the upper register sounding much stronger. Vibrato control improving.'],
|
||||
['2026-02-27', 'attended'],
|
||||
['2026-03-06', 'attended', '"Billie\'s Bounce" head now solid at tempo. Moving to comping rhythms.'],
|
||||
['2026-03-13', 'missed'],
|
||||
['2026-03-20', 'attended'],
|
||||
['2026-03-27', 'attended', 'Carlos brought in his own transcription of a Cannonball Adderley lick — great initiative.'],
|
||||
['2026-04-03', 'scheduled'],
|
||||
['2026-04-10', 'scheduled'],
|
||||
['2026-04-17', 'scheduled'],
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
for (const ss of sessionSets) {
|
||||
const enrollmentId = enrollmentIds[ss.enrollmentKey]
|
||||
if (!enrollmentId) continue
|
||||
const [slot] = await sql`SELECT start_time FROM schedule_slot WHERE id = ${slotIds[ss.slotKey]}`
|
||||
const scheduledTime = slot.start_time
|
||||
|
||||
for (const [date, status, instructorNotes] of ss.entries) {
|
||||
const [existing] = await sql`
|
||||
SELECT id FROM lesson_session WHERE enrollment_id = ${enrollmentId} AND scheduled_date = ${date}`
|
||||
if (existing) continue
|
||||
|
||||
await sql`
|
||||
INSERT INTO lesson_session (enrollment_id, scheduled_date, scheduled_time, status, instructor_notes, notes_completed_at)
|
||||
VALUES (
|
||||
${enrollmentId}, ${date}, ${scheduledTime}, ${status},
|
||||
${instructorNotes ?? null},
|
||||
${instructorNotes && status !== 'scheduled' ? new Date(date + 'T20:00:00Z').toISOString() : null}
|
||||
)`
|
||||
}
|
||||
const count = ss.entries.length
|
||||
console.log(` Sessions: ${ss.enrollmentKey} (${count})`)
|
||||
}
|
||||
|
||||
// ── Lesson plan template — Beginner Piano Fundamentals ─────────────────────
|
||||
const [existingTemplate] = await sql`SELECT id FROM lesson_plan_template WHERE name = 'Beginner Piano Fundamentals'`
|
||||
if (!existingTemplate) {
|
||||
const [tmpl] = await sql`
|
||||
INSERT INTO lesson_plan_template (name, description, instrument, skill_level, is_active)
|
||||
VALUES (
|
||||
'Beginner Piano Fundamentals',
|
||||
'Core skills for students in their first year of piano study — from posture and note reading through first hands-together pieces.',
|
||||
'Piano', 'beginner', true
|
||||
) RETURNING id`
|
||||
|
||||
const sections = [
|
||||
{
|
||||
title: 'Posture & Hand Position',
|
||||
sortOrder: 0,
|
||||
items: [
|
||||
{ title: 'Bench height and sitting posture', sortOrder: 0 },
|
||||
{ title: 'Hand arch and curved fingers', sortOrder: 1 },
|
||||
{ title: 'Finger numbering (1–5)', sortOrder: 2 },
|
||||
{ title: 'Relaxed wrists and forearms', sortOrder: 3 },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Reading Music',
|
||||
sortOrder: 1,
|
||||
items: [
|
||||
{ title: 'Treble clef note names (C4–G5)', sortOrder: 0 },
|
||||
{ title: 'Bass clef note names (C3–G4)', sortOrder: 1 },
|
||||
{ title: 'Note values: whole, half, quarter', sortOrder: 2 },
|
||||
{ title: 'Time signatures: 4/4 and 3/4', sortOrder: 3 },
|
||||
{ title: 'Bar lines and repeats', sortOrder: 4 },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Keyboard Geography',
|
||||
sortOrder: 2,
|
||||
items: [
|
||||
{ title: 'All white key names across full keyboard', sortOrder: 0 },
|
||||
{ title: 'Black key groups (2s and 3s)', sortOrder: 1 },
|
||||
{ title: 'Middle C and octave identification', sortOrder: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'First Pieces',
|
||||
sortOrder: 3,
|
||||
items: [
|
||||
{ title: 'Five-finger melody (right hand only)', sortOrder: 0 },
|
||||
{ title: 'Simple left hand accompaniment', sortOrder: 1 },
|
||||
{ title: 'Hands together at slow tempo', sortOrder: 2 },
|
||||
{ title: 'Mary Had a Little Lamb', sortOrder: 3 },
|
||||
{ title: 'Ode to Joy (Beethoven)', sortOrder: 4 },
|
||||
{ title: 'Merrily We Roll Along', sortOrder: 5 },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
for (const sec of sections) {
|
||||
const [s] = await sql`
|
||||
INSERT INTO lesson_plan_template_section (template_id, title, sort_order)
|
||||
VALUES (${tmpl.id}, ${sec.title}, ${sec.sortOrder})
|
||||
RETURNING id`
|
||||
for (const item of sec.items) {
|
||||
await sql`
|
||||
INSERT INTO lesson_plan_template_item (section_id, title, grading_scale_id, sort_order)
|
||||
VALUES (${s.id}, ${item.title}, ${scaleId}, ${item.sortOrder})`
|
||||
}
|
||||
}
|
||||
console.log(' Template: Beginner Piano Fundamentals')
|
||||
}
|
||||
|
||||
// ── Lesson plan for Tommy Smith ────────────────────────────────────────────
|
||||
const tommyEnrollmentId = enrollmentIds['Tommy Smith']
|
||||
const tommyMember = memberMap['Tommy Smith']
|
||||
if (tommyEnrollmentId && tommyMember) {
|
||||
const [existingPlan] = await sql`SELECT id FROM member_lesson_plan WHERE enrollment_id = ${tommyEnrollmentId} AND is_active = true`
|
||||
if (!existingPlan) {
|
||||
const [plan] = await sql`
|
||||
INSERT INTO member_lesson_plan (member_id, enrollment_id, title, description, is_active, started_date)
|
||||
VALUES (${tommyMember.id}, ${tommyEnrollmentId}, 'Piano Fundamentals — Tommy Smith', 'Working through first-year piano curriculum.', true, '2026-01-05')
|
||||
RETURNING id`
|
||||
|
||||
// Section 1 — Posture: all mastered
|
||||
const [sec1] = await sql`
|
||||
INSERT INTO lesson_plan_section (lesson_plan_id, title, sort_order)
|
||||
VALUES (${plan.id}, 'Posture & Hand Position', 0) RETURNING id`
|
||||
const posturItems = [
|
||||
{ title: 'Bench height and sitting posture', status: 'mastered', sortOrder: 0 },
|
||||
{ title: 'Hand arch and curved fingers', status: 'mastered', sortOrder: 1 },
|
||||
{ title: 'Finger numbering (1–5)', status: 'mastered', sortOrder: 2 },
|
||||
{ title: 'Relaxed wrists and forearms', status: 'in_progress', sortOrder: 3 },
|
||||
]
|
||||
for (const it of posturItems) {
|
||||
await sql`INSERT INTO lesson_plan_item (section_id, title, status, grading_scale_id, current_grade_value, sort_order)
|
||||
VALUES (${sec1.id}, ${it.title}, ${it.status}, ${scaleId}, ${it.status === 'mastered' ? '4' : it.status === 'in_progress' ? '2' : null}, ${it.sortOrder})`
|
||||
}
|
||||
|
||||
// Section 2 — Reading Music: in progress
|
||||
const [sec2] = await sql`
|
||||
INSERT INTO lesson_plan_section (lesson_plan_id, title, sort_order)
|
||||
VALUES (${plan.id}, 'Reading Music', 1) RETURNING id`
|
||||
const readingItems = [
|
||||
{ title: 'Treble clef note names (C4–G5)', status: 'mastered', sortOrder: 0 },
|
||||
{ title: 'Bass clef note names (C3–G4)', status: 'in_progress', sortOrder: 1 },
|
||||
{ title: 'Note values: whole, half, quarter', status: 'mastered', sortOrder: 2 },
|
||||
{ title: 'Time signatures: 4/4 and 3/4', status: 'in_progress', sortOrder: 3 },
|
||||
{ title: 'Bar lines and repeats', status: 'not_started', sortOrder: 4 },
|
||||
]
|
||||
for (const it of readingItems) {
|
||||
await sql`INSERT INTO lesson_plan_item (section_id, title, status, grading_scale_id, current_grade_value, sort_order)
|
||||
VALUES (${sec2.id}, ${it.title}, ${it.status}, ${scaleId}, ${it.status === 'mastered' ? '4' : it.status === 'in_progress' ? '2' : null}, ${it.sortOrder})`
|
||||
}
|
||||
|
||||
// Section 3 — First Pieces: mix of complete and in progress
|
||||
const [sec3] = await sql`
|
||||
INSERT INTO lesson_plan_section (lesson_plan_id, title, sort_order)
|
||||
VALUES (${plan.id}, 'First Pieces', 2) RETURNING id`
|
||||
const pieceItems = [
|
||||
{ title: 'Five-finger melody (right hand only)', status: 'mastered', sortOrder: 0 },
|
||||
{ title: 'Simple left hand accompaniment', status: 'mastered', sortOrder: 1 },
|
||||
{ title: 'Hands together at slow tempo', status: 'in_progress', sortOrder: 2 },
|
||||
{ title: 'Mary Had a Little Lamb', status: 'mastered', sortOrder: 3 },
|
||||
{ title: 'Ode to Joy (Beethoven)', status: 'in_progress', sortOrder: 4 },
|
||||
{ title: 'Merrily We Roll Along', status: 'not_started', sortOrder: 5 },
|
||||
]
|
||||
for (const it of pieceItems) {
|
||||
await sql`INSERT INTO lesson_plan_item (section_id, title, status, grading_scale_id, current_grade_value, sort_order)
|
||||
VALUES (${sec3.id}, ${it.title}, ${it.status}, ${scaleId}, ${it.status === 'mastered' ? '4' : it.status === 'in_progress' ? '2' : null}, ${it.sortOrder})`
|
||||
}
|
||||
|
||||
console.log(' Lesson plan: Tommy Smith — Piano Fundamentals')
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Lessons seed complete.')
|
||||
}
|
||||
|
||||
seed().catch((err) => {
|
||||
console.error('Seed failed:', err)
|
||||
process.exit(1)
|
||||
|
||||
@@ -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