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:
Ryan Moon
2026-03-30 18:52:57 -05:00
parent 7680a73d88
commit 5ad27bc196
47 changed files with 6303 additions and 139 deletions

View 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;

View File

@@ -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
}
]
}

View File

@@ -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(),

View File

@@ -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) {

View File

@@ -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 (15)', sortOrder: 2 },
{ title: 'Relaxed wrists and forearms', sortOrder: 3 },
],
},
{
title: 'Reading Music',
sortOrder: 1,
items: [
{ title: 'Treble clef note names (C4G5)', sortOrder: 0 },
{ title: 'Bass clef note names (C3G4)', 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 (15)', 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 (C4G5)', status: 'mastered', sortOrder: 0 },
{ title: 'Bass clef note names (C3G4)', 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)