Tickets with work in progress or ready for pickup now have realistic line items (labor, parts, flat rates, consumables). The ready ticket (David Smith — Violin) has billable items for POS checkout testing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1525 lines
97 KiB
TypeScript
1525 lines
97 KiB
TypeScript
/**
|
||
* Music store seed — populates the dev database with music-store-specific data.
|
||
* Run: bun run src/db/seeds/music-store-seed.ts
|
||
*
|
||
* Prerequisites: dev-seed must have been run first (company, accounts, RBAC already exist).
|
||
* This adds music-specific repair templates, tickets, and batch data.
|
||
*/
|
||
import postgres from 'postgres'
|
||
|
||
const DB_URL = process.env.DATABASE_URL ?? 'postgresql://lunarfront:lunarfront@localhost:5432/lunarfront'
|
||
const COMPANY_ID = 'a0000000-0000-4000-8000-000000000001'
|
||
|
||
const sql = postgres(DB_URL)
|
||
|
||
async function seed() {
|
||
console.log('Seeding music store data...')
|
||
|
||
// --- Bootstrap: company, location, RBAC, admin user (idempotent) ---
|
||
const [existingCompany] = await sql`SELECT id FROM company WHERE id = ${COMPANY_ID}`
|
||
if (!existingCompany) {
|
||
await sql`INSERT INTO company (id, name, timezone, phone, email, address) VALUES (${COMPANY_ID}, 'Harmony Music Shop', 'America/Chicago', '555-555-1234', 'info@harmonymusic.com', '{"street":"123 Main St","city":"San Antonio","state":"TX","zip":"78205"}'::jsonb)`
|
||
await sql`INSERT INTO location (id, name, tax_rate, service_tax_rate, phone, email, address) VALUES ('a0000000-0000-4000-8000-000000000002', 'Main Store', '0.0825', '0.0825', '555-555-1234', 'info@harmonymusic.com', '{"street":"123 Main St","city":"San Antonio","state":"TX","zip":"78205"}'::jsonb)`
|
||
console.log(' Created company and location')
|
||
} else {
|
||
await sql`UPDATE company SET name = 'Harmony Music Shop', phone = '555-555-1234', email = 'info@harmonymusic.com', address = '{"street":"123 Main St","city":"San Antonio","state":"TX","zip":"78205"}'::jsonb WHERE id = ${COMPANY_ID}`
|
||
console.log(' Updated company: Harmony Music Shop')
|
||
}
|
||
|
||
// RBAC
|
||
const { SYSTEM_PERMISSIONS, DEFAULT_ROLES } = await import('../seeds/rbac.js')
|
||
for (const p of SYSTEM_PERMISSIONS) {
|
||
await sql`INSERT INTO permission (slug, domain, action, description) VALUES (${p.slug}, ${p.domain}, ${p.action}, ${p.description}) ON CONFLICT (slug) DO NOTHING`
|
||
}
|
||
const permRows = await sql`SELECT id, slug FROM permission`
|
||
const permMap = new Map(permRows.map((r: any) => [r.slug, r.id]))
|
||
for (const roleDef of DEFAULT_ROLES) {
|
||
const [existingRole] = await sql`SELECT id FROM role WHERE slug = ${roleDef.slug}`
|
||
if (existingRole) continue
|
||
const [role] = await sql`INSERT INTO role (name, slug, description, is_system) VALUES (${roleDef.name}, ${roleDef.slug}, ${roleDef.description}, true) RETURNING id`
|
||
for (const permSlug of roleDef.permissions) {
|
||
const permId = permMap.get(permSlug)
|
||
if (permId) await sql`INSERT INTO role_permission (role_id, permission_id) VALUES (${role.id}, ${permId}) ON CONFLICT DO NOTHING`
|
||
}
|
||
}
|
||
console.log(' RBAC seeded')
|
||
|
||
// Admin user with POS PIN
|
||
const [existingAdmin] = await sql`SELECT id FROM "user" WHERE email = 'admin@harmonymusic.com'`
|
||
if (!existingAdmin) {
|
||
const bcrypt = await import('bcryptjs')
|
||
const hashedPw = await bcrypt.hash('admin1234', 10)
|
||
const pinHash = await bcrypt.hash('1234', 10)
|
||
const [user] = await sql`INSERT INTO "user" (email, password_hash, first_name, last_name, role, employee_number, pin_hash) VALUES ('admin@harmonymusic.com', ${hashedPw}, 'Admin', 'User', 'admin', '1001', ${pinHash}) 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: admin@harmonymusic.com / admin1234 (POS: 10011234)')
|
||
}
|
||
|
||
// Default modules
|
||
const modules = [
|
||
{ slug: 'inventory', name: 'Inventory', description: 'Product catalog and stock tracking', enabled: true },
|
||
{ slug: 'pos', name: 'Point of Sale', description: 'Sales, drawer, receipts', enabled: true },
|
||
{ slug: 'repairs', name: 'Repairs', description: 'Repair tickets and service', enabled: true },
|
||
{ slug: 'lessons', name: 'Lessons', description: 'Scheduling and instruction', enabled: true },
|
||
{ slug: 'files', name: 'Files', description: 'File storage', enabled: true },
|
||
{ slug: 'vault', name: 'Vault', description: 'Password manager', enabled: true },
|
||
{ slug: 'reports', name: 'Reports', description: 'Business reports', enabled: true },
|
||
{ slug: 'rentals', name: 'Rentals', description: 'Rental agreements', enabled: false },
|
||
{ slug: 'email', name: 'Email', description: 'Email campaigns', enabled: false },
|
||
]
|
||
for (const m of modules) {
|
||
await sql`INSERT INTO module_config (slug, name, description, licensed, enabled) VALUES (${m.slug}, ${m.name}, ${m.description}, true, ${m.enabled}) ON CONFLICT (slug) DO NOTHING`
|
||
}
|
||
console.log(' Modules configured')
|
||
|
||
// Default register
|
||
await sql`INSERT INTO register (location_id, name) SELECT 'a0000000-0000-4000-8000-000000000002', 'Register 1' WHERE NOT EXISTS (SELECT 1 FROM register WHERE name = 'Register 1' AND location_id = 'a0000000-0000-4000-8000-000000000002')`
|
||
console.log(' Default register created')
|
||
|
||
// --- Accounts & Members ---
|
||
const accounts = [
|
||
{ name: 'Smith Family', email: 'smith@example.com', phone: '555-0101', type: 'family' },
|
||
{ name: 'Johnson Family', email: 'johnson@example.com', phone: '555-0102', type: 'family' },
|
||
{ name: 'Garcia Workshop', email: 'carlos@studio.com', phone: '555-0103', type: 'business' },
|
||
{ name: 'Mike Thompson', email: 'mike.t@email.com', phone: '555-0104', type: 'individual' },
|
||
{ name: 'Emily Chen', email: 'emily.chen@email.com', phone: '555-0105', type: 'individual' },
|
||
{ name: 'Lincoln High School', email: 'band@lincoln.edu', phone: '555-0200', type: 'business' },
|
||
{ name: 'Westside Community Orchestra', email: 'info@westsideorch.org', phone: '555-0300', type: 'business' },
|
||
{ name: 'Rivera Family', email: 'rivera@email.com', phone: '555-0106', type: 'family' },
|
||
{ name: 'Patricia Williams', email: 'pwilliams@email.com', phone: '555-0107', type: 'individual' },
|
||
{ name: 'Oak Elementary PTA', email: 'pta@oakelementary.edu', phone: '555-0201', type: 'business' },
|
||
]
|
||
|
||
const acctIds: Record<string, string> = {}
|
||
for (const a of accounts) {
|
||
const [existing] = await sql`SELECT id FROM account WHERE name = ${a.name}`
|
||
if (existing) { acctIds[a.name] = existing.id; continue }
|
||
const num = String(Math.floor(100000 + Math.random() * 900000))
|
||
const [row] = await sql`INSERT INTO account (name, email, phone, account_number, billing_mode) VALUES (${a.name}, ${a.email}, ${a.phone}, ${num}, 'consolidated') RETURNING id`
|
||
acctIds[a.name] = row.id
|
||
}
|
||
console.log(` Created ${accounts.length} accounts`)
|
||
|
||
const members = [
|
||
// Smith Family — parent + 2 kids learning strings
|
||
{ account: 'Smith Family', firstName: 'David', lastName: 'Smith', email: 'david@example.com', phone: '555-0101' },
|
||
{ account: 'Smith Family', firstName: 'Sarah', lastName: 'Smith', email: 'sarah@example.com' },
|
||
{ account: 'Smith Family', firstName: 'Tommy', lastName: 'Smith', isMinor: true, dob: '2015-03-12' },
|
||
{ account: 'Smith Family', firstName: 'Lily', lastName: 'Smith', isMinor: true, dob: '2017-08-25' },
|
||
|
||
// Johnson Family — parent + kid learning viola
|
||
{ account: 'Johnson Family', firstName: 'Lisa', lastName: 'Johnson', email: 'lisa.j@example.com', phone: '555-0102' },
|
||
{ account: 'Johnson Family', firstName: 'Jake', lastName: 'Johnson', isMinor: true, dob: '2013-11-05' },
|
||
|
||
// Individual musicians
|
||
{ account: 'Mike Thompson', firstName: 'Mike', lastName: 'Thompson', email: 'mike.t@email.com', phone: '555-0104' },
|
||
{ account: 'Emily Chen', firstName: 'Emily', lastName: 'Chen', email: 'emily.chen@email.com', phone: '555-0105' },
|
||
{ account: 'Garcia Workshop', firstName: 'Carlos', lastName: 'Garcia', email: 'carlos@studio.com', phone: '555-0103' },
|
||
{ account: 'Patricia Williams', firstName: 'Patricia', lastName: 'Williams', email: 'pwilliams@email.com', phone: '555-0107' },
|
||
|
||
// Rivera Family — cellist family
|
||
{ account: 'Rivera Family', firstName: 'Maria', lastName: 'Rivera', email: 'rivera@email.com', phone: '555-0106' },
|
||
{ account: 'Rivera Family', firstName: 'Sofia', lastName: 'Rivera', isMinor: true, dob: '2014-06-18' },
|
||
{ account: 'Rivera Family', firstName: 'Diego', lastName: 'Rivera', isMinor: true, dob: '2016-01-30' },
|
||
|
||
// School contacts
|
||
{ account: 'Lincoln High School', firstName: 'Robert', lastName: 'Hayes', email: 'rhayes@lincoln.edu', phone: '555-0200' },
|
||
{ account: 'Oak Elementary PTA', firstName: 'Jennifer', lastName: 'Park', email: 'jpark@oakelementary.edu', phone: '555-0201' },
|
||
|
||
// Orchestra contact
|
||
{ account: 'Westside Community Orchestra', firstName: 'Margaret', lastName: 'Foster', email: 'mfoster@westsideorch.org', phone: '555-0300' },
|
||
]
|
||
|
||
const memberMap: Record<string, { id: string }> = {}
|
||
for (const m of members) {
|
||
const accountId = acctIds[m.account]
|
||
if (!accountId) continue
|
||
const [existing] = await sql`SELECT id FROM member WHERE first_name = ${m.firstName} AND last_name = ${m.lastName} AND account_id = ${accountId}`
|
||
if (existing) { memberMap[`${m.firstName} ${m.lastName}`] = existing; continue }
|
||
const num = String(Math.floor(100000 + Math.random() * 900000))
|
||
const [row] = await sql`
|
||
INSERT INTO member (account_id, first_name, last_name, email, phone, member_number, is_minor, date_of_birth)
|
||
VALUES (${accountId}, ${m.firstName}, ${m.lastName}, ${m.email ?? null}, ${m.phone ?? null}, ${num}, ${m.isMinor ?? false}, ${m.dob ?? null})
|
||
RETURNING id
|
||
`
|
||
memberMap[`${m.firstName} ${m.lastName}`] = row
|
||
}
|
||
console.log(` Created ${members.length} members`)
|
||
|
||
// --- Music Repair Service Templates ---
|
||
const templates = [
|
||
// Strings — Violin
|
||
{ name: 'Bow Rehair', itemCategory: 'Violin', size: '4/4', itemType: 'flat_rate', price: '65.00', cost: '15.00' },
|
||
{ name: 'Bow Rehair', itemCategory: 'Violin', size: '3/4', itemType: 'flat_rate', price: '55.00', cost: '12.00' },
|
||
{ name: 'Bow Rehair', itemCategory: 'Violin', size: '1/2', itemType: 'flat_rate', price: '50.00', cost: '10.00' },
|
||
{ name: 'Bridge Setup', itemCategory: 'Violin', size: '4/4', itemType: 'flat_rate', price: '40.00', cost: '10.00' },
|
||
{ name: 'Bridge Replacement', itemCategory: 'Violin', size: '4/4', itemType: 'flat_rate', price: '75.00', cost: '25.00' },
|
||
{ name: 'String Change', itemCategory: 'Violin', size: '4/4', itemType: 'flat_rate', price: '35.00', cost: '12.00' },
|
||
{ name: 'Peg Fitting', itemCategory: 'Violin', size: null, itemType: 'labor', price: '30.00', cost: null },
|
||
{ name: 'Soundpost Adjustment', itemCategory: 'Violin', size: null, itemType: 'labor', price: '25.00', cost: null },
|
||
{ name: 'Seam Repair', itemCategory: 'Violin', size: null, itemType: 'labor', price: '45.00', cost: null },
|
||
|
||
// Strings — Viola
|
||
{ name: 'Bow Rehair', itemCategory: 'Viola', size: null, itemType: 'flat_rate', price: '65.00', cost: '15.00' },
|
||
{ name: 'Bridge Setup', itemCategory: 'Viola', size: null, itemType: 'flat_rate', price: '45.00', cost: '12.00' },
|
||
{ name: 'String Change', itemCategory: 'Viola', size: null, itemType: 'flat_rate', price: '40.00', cost: '15.00' },
|
||
|
||
// Strings — Cello
|
||
{ name: 'Bow Rehair', itemCategory: 'Cello', size: null, itemType: 'flat_rate', price: '80.00', cost: '20.00' },
|
||
{ name: 'Bridge Setup', itemCategory: 'Cello', size: null, itemType: 'flat_rate', price: '55.00', cost: '15.00' },
|
||
{ name: 'String Change', itemCategory: 'Cello', size: null, itemType: 'flat_rate', price: '50.00', cost: '20.00' },
|
||
{ name: 'Endpin Repair', itemCategory: 'Cello', size: null, itemType: 'labor', price: '35.00', cost: null },
|
||
|
||
// Strings — Bass
|
||
{ name: 'Bow Rehair', itemCategory: 'Bass', size: null, itemType: 'flat_rate', price: '90.00', cost: '25.00' },
|
||
{ name: 'Bridge Setup', itemCategory: 'Bass', size: null, itemType: 'flat_rate', price: '65.00', cost: '20.00' },
|
||
{ name: 'String Change', itemCategory: 'Bass', size: null, itemType: 'flat_rate', price: '60.00', cost: '25.00' },
|
||
|
||
// Additional string services
|
||
{ name: 'Fingerboard Planing', itemCategory: 'Violin', size: null, itemType: 'labor', price: '85.00', cost: null },
|
||
{ name: 'Fingerboard Planing', itemCategory: 'Viola', size: null, itemType: 'labor', price: '95.00', cost: null },
|
||
{ name: 'Fingerboard Planing', itemCategory: 'Cello', size: null, itemType: 'labor', price: '120.00', cost: null },
|
||
{ name: 'Varnish Touch-Up', itemCategory: 'Violin', size: null, itemType: 'labor', price: '55.00', cost: null },
|
||
{ name: 'Varnish Touch-Up', itemCategory: 'Cello', size: null, itemType: 'labor', price: '75.00', cost: null },
|
||
{ name: 'Neck Reset', itemCategory: 'Violin', size: null, itemType: 'labor', price: '200.00', cost: null },
|
||
{ name: 'Neck Reset', itemCategory: 'Cello', size: null, itemType: 'labor', price: '350.00', cost: null },
|
||
{ name: 'Bass Bar Replacement', itemCategory: 'Violin', size: null, itemType: 'labor', price: '300.00', cost: null },
|
||
{ name: 'Tailgut Replacement', itemCategory: 'Violin', size: null, itemType: 'flat_rate', price: '20.00', cost: '5.00' },
|
||
{ name: 'Tailgut Replacement', itemCategory: 'Cello', size: null, itemType: 'flat_rate', price: '25.00', cost: '8.00' },
|
||
|
||
// General
|
||
{ name: 'General Cleaning', itemCategory: null, size: null, itemType: 'flat_rate', price: '30.00', cost: '5.00' },
|
||
{ name: 'Diagnostic Evaluation', itemCategory: null, size: null, itemType: 'flat_rate', price: '25.00', cost: null },
|
||
]
|
||
|
||
// Deactivate generic templates first
|
||
await sql`UPDATE repair_service_template SET is_active = false`
|
||
console.log(' Deactivated generic templates')
|
||
|
||
for (const t of templates) {
|
||
const existing = await sql`SELECT id FROM repair_service_template WHERE name = ${t.name} AND COALESCE(item_category, '') = ${t.itemCategory ?? ''} AND COALESCE(size, '') = ${t.size ?? ''}`
|
||
if (existing.length > 0) continue
|
||
await sql`INSERT INTO repair_service_template (name, item_category, size, item_type, default_price, default_cost, is_active) VALUES (${t.name}, ${t.itemCategory}, ${t.size}, ${t.itemType}, ${t.price}, ${t.cost}, true)`
|
||
console.log(` Template: ${t.name} — ${t.itemCategory ?? 'General'} ${t.size ?? ''}`)
|
||
}
|
||
|
||
// --- Music Repair Tickets ---
|
||
// First clear any generic tickets
|
||
await sql`DELETE FROM repair_ticket WHERE id NOT IN (SELECT DISTINCT repair_ticket_id FROM repair_note)`
|
||
|
||
const tickets: { customer: string; item: string; serial: string | null; problem: string; condition: string; status: string; estimate: string | null; lineItems: { type: string; desc: string; qty: number; price: number; cost?: number }[] }[] = [
|
||
{ customer: 'Mike Thompson', item: 'Jay Haide Cello 4/4', serial: 'JH-C44-1892', problem: 'Endpin mechanism worn, slips during playing. Seam opening near lower bout.', condition: 'fair', status: 'in_progress', estimate: '95.00', lineItems: [
|
||
{ type: 'labor', desc: 'Endpin repair — remove and refit mechanism', qty: 1, price: 45 },
|
||
{ type: 'labor', desc: 'Seam repair — lower bout', qty: 1, price: 45 },
|
||
{ type: 'consumable', desc: 'Hide glue', qty: 1, price: 5, cost: 5 },
|
||
]},
|
||
{ customer: 'Emily Chen', item: 'Scott Cao Viola 16"', serial: 'SC-VA16-0547', problem: 'Bridge warped, soundpost has shifted. Needs full setup.', condition: 'fair', status: 'pending_approval', estimate: '120.00', lineItems: [
|
||
{ type: 'flat_rate', desc: 'Bridge replacement — Viola', qty: 1, price: 75, cost: 18 },
|
||
{ type: 'labor', desc: 'Soundpost adjustment', qty: 1, price: 25 },
|
||
{ type: 'flat_rate', desc: 'String change — Viola', qty: 1, price: 35, cost: 32 },
|
||
]},
|
||
{ customer: 'David Smith', item: 'German Workshop Violin 4/4', serial: null, problem: 'Bow needs rehair, bridge slightly warped', condition: 'fair', status: 'ready', estimate: '105.00', lineItems: [
|
||
{ type: 'flat_rate', desc: 'Bow rehair — Violin 4/4', qty: 1, price: 65, cost: 15 },
|
||
{ type: 'flat_rate', desc: 'Bridge setup — Violin 4/4', qty: 1, price: 40, cost: 10 },
|
||
{ type: 'consumable', desc: 'Bow hair — Mongolian white', qty: 1, price: 18, cost: 18 },
|
||
]},
|
||
{ customer: 'Carlos Garcia', item: 'Eastman VL305 Violin 4/4', serial: 'EA-V305-X42', problem: 'Fingerboard wear near 3rd position, open seam on top plate', condition: 'good', status: 'new', estimate: null, lineItems: [] },
|
||
{ customer: 'Patricia Williams', item: 'Shen SB100 Bass 3/4', serial: 'SH-B34-0891', problem: 'Bridge feet not fitting soundboard, wolf tone on G string', condition: 'good', status: 'diagnosing', estimate: null, lineItems: [] },
|
||
{ customer: 'Walk-In Customer', item: 'Student Violin 3/4', serial: null, problem: 'Pegs slipping, E string buzzing against fingerboard', condition: 'fair', status: 'intake', estimate: null, lineItems: [] },
|
||
{ customer: 'Smith Family', item: 'Suzuki Student Violin 1/2', serial: null, problem: 'Pegs slipping, bridge leaning forward', condition: 'fair', status: 'new', estimate: null, lineItems: [] },
|
||
{ customer: 'Rivera Family', item: 'Eastman VC80 Cello 3/4', serial: 'EA-VC80-3Q-X01', problem: 'A string peg cracked, needs replacement. Bow rehair overdue.', condition: 'good', status: 'in_progress', estimate: '85.00', lineItems: [
|
||
{ type: 'part', desc: 'Pegs — Cello Boxwood Set (4)', qty: 1, price: 28, cost: 10 },
|
||
{ type: 'labor', desc: 'Peg fitting — Cello', qty: 1, price: 35 },
|
||
{ type: 'flat_rate', desc: 'Bow rehair — Cello', qty: 1, price: 75, cost: 18 },
|
||
{ type: 'consumable', desc: 'Peg compound', qty: 1, price: 6, cost: 6 },
|
||
]},
|
||
]
|
||
|
||
for (const t of tickets) {
|
||
const existing = await sql`SELECT id FROM repair_ticket WHERE customer_name = ${t.customer} AND problem_description = ${t.problem}`
|
||
if (existing.length > 0) continue
|
||
const num = String(Math.floor(100000 + Math.random() * 900000))
|
||
const acctId = acctIds[t.customer] ?? null
|
||
const [ticket] = await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, item_description, serial_number, problem_description, condition_in, status, estimated_cost) VALUES (${num}, ${t.customer}, ${acctId}, ${t.item}, ${t.serial}, ${t.problem}, ${t.condition}, ${t.status}, ${t.estimate}) RETURNING id`
|
||
for (const li of t.lineItems) {
|
||
const total = (li.qty * li.price).toFixed(2)
|
||
await sql`INSERT INTO repair_line_item (repair_ticket_id, item_type, description, qty, unit_price, total_price, cost) VALUES (${ticket.id}, ${li.type}, ${li.desc}, ${li.qty}, ${li.price.toFixed(2)}, ${total}, ${li.cost?.toFixed(2) ?? null})`
|
||
}
|
||
console.log(` Ticket: ${t.customer} — ${t.item} [${t.status}]${t.lineItems.length > 0 ? ` (${t.lineItems.length} items)` : ''}`)
|
||
}
|
||
|
||
// --- School Orchestra Batch ---
|
||
const batchExists = await sql`SELECT id FROM repair_batch WHERE contact_name = 'Ms. Park'`
|
||
if (batchExists.length === 0) {
|
||
const schoolId = acctIds['Oak Elementary PTA']
|
||
if (schoolId) {
|
||
const batchNum = String(Math.floor(100000 + Math.random() * 900000))
|
||
const [batch] = await sql`INSERT INTO repair_batch (batch_number, account_id, contact_name, contact_phone, contact_email, item_count, notes, status) VALUES (${batchNum}, ${schoolId}, 'Ms. Park', '555-0201', 'jpark@oakelementary.edu', 6, 'School orchestra summer maintenance — 6 string instruments before fall semester', 'intake') RETURNING id`
|
||
|
||
const batchTickets = [
|
||
{ item: 'Student Violin 4/4 — Eastman', problem: 'Bridge warped, pegs slipping, needs full setup', condition: 'fair' },
|
||
{ item: 'Student Violin 3/4 — Shen', problem: 'Open seam on lower bout, fingerboard wear', condition: 'fair' },
|
||
{ item: 'Student Violin 1/2 — Eastman', problem: 'Tailpiece gut broken, fine tuners corroded', condition: 'poor' },
|
||
{ item: 'Student Viola 15" — Eastman', problem: 'Chin rest loose, soundpost leaning', condition: 'good' },
|
||
{ item: 'Student Cello 3/4 — Eastman', problem: 'Endpin stuck, bridge feet not fitting', condition: 'fair' },
|
||
{ item: 'Student Cello 1/2 — Shen', problem: 'Bow rehair needed, strings old and fraying', condition: 'fair' },
|
||
]
|
||
|
||
for (const bt of batchTickets) {
|
||
const num = String(Math.floor(100000 + Math.random() * 900000))
|
||
await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, repair_batch_id, item_description, problem_description, condition_in, status) VALUES (${num}, 'Lincoln High School', ${schoolId}, ${batch.id}, ${bt.item}, ${bt.problem}, ${bt.condition}, 'new')`
|
||
console.log(` Batch ticket: ${bt.item}`)
|
||
}
|
||
console.log(' Batch: Oak Elementary — 6 string instruments')
|
||
}
|
||
}
|
||
|
||
// --- Enable inventory module ---
|
||
await sql`UPDATE module_config SET enabled = true WHERE slug = 'inventory'`
|
||
console.log(' Enabled inventory module')
|
||
|
||
// --- Inventory seed data ---
|
||
await seedInventory(sql)
|
||
|
||
// --- 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()
|
||
}
|
||
|
||
async function seedInventory(sql: any) {
|
||
console.log('\nSeeding inventory data...')
|
||
|
||
// ── Suppliers ─────────────────────────────────────────────────────────────
|
||
const supplierDefs = [
|
||
{ name: 'Shar Music', contactName: 'Sales Desk', email: 'orders@sharmusic.com', phone: '800-248-7427', website: 'sharmusic.com', paymentTerms: 'Net 30' },
|
||
{ name: 'Southwest Strings', contactName: 'Dealer Rep', email: 'dealer@swstrings.com', phone: '800-528-3430', website: 'swstrings.com', paymentTerms: 'Net 30' },
|
||
{ name: 'Connolly Music', contactName: 'Account Rep', email: 'wholesale@connollymusic.com', phone: '800-644-5268', website: 'connollymusic.com', paymentTerms: 'Net 60' },
|
||
{ name: 'D\'Addario', contactName: 'Dealer Services', email: 'dealer@daddario.com', phone: '800-323-2746', website: 'daddario.com', paymentTerms: 'Net 30' },
|
||
{ name: 'Pirastro', contactName: 'US Distributor', email: 'us@pirastro.com', phone: '800-223-2500', website: 'pirastro.com', paymentTerms: 'Net 30' },
|
||
]
|
||
|
||
const supplierIds: Record<string, string> = {}
|
||
for (const s of supplierDefs) {
|
||
const [existing] = await sql`SELECT id FROM supplier WHERE name = ${s.name}`
|
||
if (existing) { supplierIds[s.name] = existing.id; continue }
|
||
const [row] = await sql`INSERT INTO supplier (name, contact_name, email, phone, website, payment_terms) VALUES (${s.name}, ${s.contactName}, ${s.email}, ${s.phone}, ${s.website}, ${s.paymentTerms}) RETURNING id`
|
||
supplierIds[s.name] = row.id
|
||
console.log(` Supplier: ${s.name}`)
|
||
}
|
||
|
||
// ── Categories ─────────────────────────────────────────────────────────────
|
||
const categoryDefs = [
|
||
{ name: 'Violins', sortOrder: 1 },
|
||
{ name: 'Violas', sortOrder: 2 },
|
||
{ name: 'Cellos', sortOrder: 3 },
|
||
{ name: 'Basses', sortOrder: 4 },
|
||
{ name: 'Bows', sortOrder: 5 },
|
||
{ name: 'Strings & Accessories', sortOrder: 6 },
|
||
{ name: 'Cases & Bags', sortOrder: 7 },
|
||
{ name: 'Rosin', sortOrder: 8 },
|
||
{ name: 'Shoulder Rests', sortOrder: 9 },
|
||
{ name: 'Sheet Music & Books', sortOrder: 10 },
|
||
{ name: 'Maintenance & Care', sortOrder: 11 },
|
||
]
|
||
|
||
const catIds: Record<string, string> = {}
|
||
for (const c of categoryDefs) {
|
||
const [existing] = await sql`SELECT id FROM category WHERE name = ${c.name}`
|
||
if (existing) { catIds[c.name] = existing.id; continue }
|
||
const [row] = await sql`INSERT INTO category (name, sort_order) VALUES (${c.name}, ${c.sortOrder}) RETURNING id`
|
||
catIds[c.name] = row.id
|
||
console.log(` Category: ${c.name}`)
|
||
}
|
||
|
||
// ── Products ───────────────────────────────────────────────────────────────
|
||
// isSerialized=true → track individual units; isRental=true → available for rental
|
||
type ProductDef = {
|
||
name: string; sku: string; brand: string; model: string; category: string
|
||
price: string; minPrice?: string; rentalRateMonthly?: string
|
||
isSerialized: boolean; isRental: boolean; isDualUseRepair?: boolean
|
||
qtyOnHand?: number; qtyReorderPoint?: number; description?: string
|
||
}
|
||
|
||
const productDefs: ProductDef[] = [
|
||
// ── Violins ──
|
||
{
|
||
name: 'Violin 4/4 — Student Outfit', sku: 'VLN-44-STU', brand: 'Eastman', model: 'VL80',
|
||
category: 'Violins', price: '399.00', minPrice: '350.00', rentalRateMonthly: '25.00',
|
||
isSerialized: true, isRental: true, isDualUseRepair: false,
|
||
description: 'Full-size student violin outfit includes bow, rosin, and lightweight case.',
|
||
},
|
||
{
|
||
name: 'Violin 3/4 — Student Outfit', sku: 'VLN-34-STU', brand: 'Eastman', model: 'VL80',
|
||
category: 'Violins', price: '349.00', minPrice: '300.00', rentalRateMonthly: '22.00',
|
||
isSerialized: true, isRental: true, isDualUseRepair: false,
|
||
description: '3/4 size student violin outfit — ideal for players ages 7–10.',
|
||
},
|
||
{
|
||
name: 'Violin 1/2 — Student Outfit', sku: 'VLN-12-STU', brand: 'Eastman', model: 'VL80',
|
||
category: 'Violins', price: '299.00', minPrice: '260.00', rentalRateMonthly: '20.00',
|
||
isSerialized: true, isRental: true, isDualUseRepair: false,
|
||
description: '1/2 size student violin outfit — ideal for players ages 5–7.',
|
||
},
|
||
{
|
||
name: 'Violin 1/4 — Student Outfit', sku: 'VLN-14-STU', brand: 'Eastman', model: 'VL80',
|
||
category: 'Violins', price: '249.00', minPrice: '220.00', rentalRateMonthly: '18.00',
|
||
isSerialized: true, isRental: true, isDualUseRepair: false,
|
||
description: '1/4 size student violin outfit — ideal for players ages 4–6.',
|
||
},
|
||
{
|
||
name: 'Violin 4/4 — Intermediate', sku: 'VLN-44-INT', brand: 'Eastman', model: 'VL305',
|
||
category: 'Violins', price: '895.00', minPrice: '800.00', rentalRateMonthly: '45.00',
|
||
isSerialized: true, isRental: true, isDualUseRepair: false,
|
||
description: 'Carved spruce top and maple back/sides. Excellent step-up instrument.',
|
||
},
|
||
{
|
||
name: 'Violin 4/4 — Advanced', sku: 'VLN-44-ADV', brand: 'Eastman', model: 'VL605',
|
||
category: 'Violins', price: '2495.00', minPrice: '2200.00',
|
||
isSerialized: true, isRental: false, isDualUseRepair: false,
|
||
description: 'Hand-carved workshop violin with Dominant strings. Concert-ready.',
|
||
},
|
||
|
||
// ── Violas ──
|
||
{
|
||
name: 'Viola 15" — Student Outfit', sku: 'VLA-15-STU', brand: 'Eastman', model: 'VA80',
|
||
category: 'Violas', price: '449.00', minPrice: '400.00', rentalRateMonthly: '28.00',
|
||
isSerialized: true, isRental: true, isDualUseRepair: false,
|
||
description: '15-inch student viola outfit with bow and lightweight case.',
|
||
},
|
||
{
|
||
name: 'Viola 16" — Student Outfit', sku: 'VLA-16-STU', brand: 'Eastman', model: 'VA80',
|
||
category: 'Violas', price: '449.00', minPrice: '400.00', rentalRateMonthly: '28.00',
|
||
isSerialized: true, isRental: true, isDualUseRepair: false,
|
||
description: '16-inch student viola outfit with bow and lightweight case.',
|
||
},
|
||
{
|
||
name: 'Viola 16" — Intermediate', sku: 'VLA-16-INT', brand: 'Eastman', model: 'VA305',
|
||
category: 'Violas', price: '1095.00', minPrice: '950.00', rentalRateMonthly: '55.00',
|
||
isSerialized: true, isRental: true, isDualUseRepair: false,
|
||
description: 'Step-up viola with carved top, warm dark tone. Includes quality bow and case.',
|
||
},
|
||
|
||
// ── Cellos ──
|
||
{
|
||
name: 'Cello 4/4 — Student Outfit', sku: 'CLO-44-STU', brand: 'Eastman', model: 'VC80',
|
||
category: 'Cellos', price: '649.00', minPrice: '580.00', rentalRateMonthly: '40.00',
|
||
isSerialized: true, isRental: true, isDualUseRepair: false,
|
||
description: 'Full-size student cello outfit includes fiberglass bow and padded bag.',
|
||
},
|
||
{
|
||
name: 'Cello 3/4 — Student Outfit', sku: 'CLO-34-STU', brand: 'Eastman', model: 'VC80',
|
||
category: 'Cellos', price: '599.00', minPrice: '530.00', rentalRateMonthly: '36.00',
|
||
isSerialized: true, isRental: true, isDualUseRepair: false,
|
||
description: '3/4 student cello outfit — ideal for players ages 9–12.',
|
||
},
|
||
{
|
||
name: 'Cello 1/2 — Student Outfit', sku: 'CLO-12-STU', brand: 'Eastman', model: 'VC80',
|
||
category: 'Cellos', price: '549.00', minPrice: '490.00', rentalRateMonthly: '32.00',
|
||
isSerialized: true, isRental: true, isDualUseRepair: false,
|
||
description: '1/2 student cello outfit — ideal for players ages 7–10.',
|
||
},
|
||
{
|
||
name: 'Cello 4/4 — Intermediate', sku: 'CLO-44-INT', brand: 'Eastman', model: 'VC305',
|
||
category: 'Cellos', price: '1495.00', minPrice: '1300.00', rentalRateMonthly: '75.00',
|
||
isSerialized: true, isRental: true, isDualUseRepair: false,
|
||
description: 'Carved spruce top intermediate cello. Powerful projection, warm tone.',
|
||
},
|
||
|
||
// ── Basses ──
|
||
{
|
||
name: 'Bass 3/4 — Student Outfit', sku: 'BAS-34-STU', brand: 'Engelhardt', model: 'ES-1',
|
||
category: 'Basses', price: '1299.00', minPrice: '1150.00', rentalRateMonthly: '65.00',
|
||
isSerialized: true, isRental: true, isDualUseRepair: false,
|
||
description: '3/4 student laminate bass outfit. Standard school/orchestra size.',
|
||
},
|
||
{
|
||
name: 'Bass 1/2 — Student', sku: 'BAS-12-STU', brand: 'Eastman', model: 'VB80',
|
||
category: 'Basses', price: '1099.00', minPrice: '980.00', rentalRateMonthly: '55.00',
|
||
isSerialized: true, isRental: true, isDualUseRepair: false,
|
||
description: '1/2 size student double bass — ideal for younger players.',
|
||
},
|
||
|
||
// ── Bows ──
|
||
{
|
||
name: 'Violin Bow — Student Brazilwood', sku: 'BOW-VLN-STU', brand: 'CodaBow', model: 'Joule',
|
||
category: 'Bows', price: '89.00', minPrice: '75.00',
|
||
isSerialized: false, isRental: false,
|
||
qtyOnHand: 8, qtyReorderPoint: 3,
|
||
description: 'Brazilwood student violin bow with nickel-silver winding.',
|
||
},
|
||
{
|
||
name: 'Violin Bow — Carbon Fiber', sku: 'BOW-VLN-CF', brand: 'CodaBow', model: 'Diamond NX',
|
||
category: 'Bows', price: '295.00', minPrice: '265.00',
|
||
isSerialized: false, isRental: false,
|
||
qtyOnHand: 4, qtyReorderPoint: 2,
|
||
description: 'Lightweight carbon fiber violin bow. Excellent balance and response.',
|
||
},
|
||
{
|
||
name: 'Viola Bow — Student Brazilwood', sku: 'BOW-VLA-STU', brand: 'CodaBow', model: 'Joule',
|
||
category: 'Bows', price: '95.00', minPrice: '80.00',
|
||
isSerialized: false, isRental: false,
|
||
qtyOnHand: 5, qtyReorderPoint: 2,
|
||
},
|
||
{
|
||
name: 'Cello Bow — Student Brazilwood', sku: 'BOW-CLO-STU', brand: 'CodaBow', model: 'Joule',
|
||
category: 'Bows', price: '110.00', minPrice: '95.00',
|
||
isSerialized: false, isRental: false,
|
||
qtyOnHand: 5, qtyReorderPoint: 2,
|
||
},
|
||
{
|
||
name: 'Bass Bow — Student Brazilwood (German)', sku: 'BOW-BAS-STU-G', brand: 'Glasser', model: 'German',
|
||
category: 'Bows', price: '85.00', minPrice: '70.00',
|
||
isSerialized: false, isRental: false,
|
||
qtyOnHand: 4, qtyReorderPoint: 2,
|
||
},
|
||
|
||
// ── Strings (also used as repair line items) ──
|
||
{
|
||
name: 'Violin Strings — Dominant 4/4 Set', sku: 'STR-VLN-DOM-44', brand: 'Thomastik', model: 'Dominant',
|
||
category: 'Strings & Accessories', price: '38.00', minPrice: '32.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 20, qtyReorderPoint: 6,
|
||
description: 'Industry-standard synthetic core violin string set. Full size.',
|
||
},
|
||
{
|
||
name: 'Violin Strings — Dominant 3/4 Set', sku: 'STR-VLN-DOM-34', brand: 'Thomastik', model: 'Dominant',
|
||
category: 'Strings & Accessories', price: '38.00', minPrice: '32.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 12, qtyReorderPoint: 4,
|
||
},
|
||
{
|
||
name: 'Violin Strings — Prelude 4/4 Set', sku: 'STR-VLN-PRE-44', brand: "D'Addario", model: 'Prelude',
|
||
category: 'Strings & Accessories', price: '18.00', minPrice: '15.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 24, qtyReorderPoint: 8,
|
||
description: 'Budget-friendly solid steel core strings. Great for beginners.',
|
||
},
|
||
{
|
||
name: 'Viola Strings — Dominant Set', sku: 'STR-VLA-DOM', brand: 'Thomastik', model: 'Dominant',
|
||
category: 'Strings & Accessories', price: '52.00', minPrice: '45.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 10, qtyReorderPoint: 3,
|
||
},
|
||
{
|
||
name: 'Cello Strings — Prelude Set', sku: 'STR-CLO-PRE', brand: "D'Addario", model: 'Prelude',
|
||
category: 'Strings & Accessories', price: '38.00', minPrice: '32.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 12, qtyReorderPoint: 4,
|
||
},
|
||
{
|
||
name: 'Cello Strings — Larsen Set', sku: 'STR-CLO-LAR', brand: 'Larsen', model: 'Original',
|
||
category: 'Strings & Accessories', price: '95.00', minPrice: '85.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 6, qtyReorderPoint: 2,
|
||
},
|
||
{
|
||
name: 'Bass Strings — Helicore Orchestral 3/4 Set', sku: 'STR-BAS-HEL', brand: "D'Addario", model: 'Helicore Orchestral',
|
||
category: 'Strings & Accessories', price: '110.00', minPrice: '95.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 8, qtyReorderPoint: 2,
|
||
},
|
||
|
||
// ── Rosin ──
|
||
{
|
||
name: 'Rosin — Hill Light', sku: 'RSN-HILL-L', brand: 'Hill', model: 'Light',
|
||
category: 'Rosin', price: '8.00', minPrice: '6.50',
|
||
isSerialized: false, isRental: false,
|
||
qtyOnHand: 30, qtyReorderPoint: 10,
|
||
description: 'Light rosin for violin and viola. Low dust.',
|
||
},
|
||
{
|
||
name: 'Rosin — Hill Dark', sku: 'RSN-HILL-D', brand: 'Hill', model: 'Dark',
|
||
category: 'Rosin', price: '8.00', minPrice: '6.50',
|
||
isSerialized: false, isRental: false,
|
||
qtyOnHand: 20, qtyReorderPoint: 8,
|
||
description: 'Dark rosin for cello and bass.',
|
||
},
|
||
{
|
||
name: 'Rosin — Pirastro Goldflex', sku: 'RSN-PIR-GF', brand: 'Pirastro', model: 'Goldflex',
|
||
category: 'Rosin', price: '14.00', minPrice: '11.00',
|
||
isSerialized: false, isRental: false,
|
||
qtyOnHand: 15, qtyReorderPoint: 5,
|
||
},
|
||
|
||
// ── Shoulder Rests ──
|
||
{
|
||
name: 'Shoulder Rest — Kun Original 4/4', sku: 'SR-KUN-44', brand: 'Kun', model: 'Original',
|
||
category: 'Shoulder Rests', price: '28.00', minPrice: '23.00',
|
||
isSerialized: false, isRental: false,
|
||
qtyOnHand: 12, qtyReorderPoint: 4,
|
||
description: 'Collapsible violin shoulder rest with rubber feet.',
|
||
},
|
||
{
|
||
name: 'Shoulder Rest — Kun Collapsible 3/4–4/4', sku: 'SR-KUN-C', brand: 'Kun', model: 'Collapsible',
|
||
category: 'Shoulder Rests', price: '35.00', minPrice: '29.00',
|
||
isSerialized: false, isRental: false,
|
||
qtyOnHand: 8, qtyReorderPoint: 3,
|
||
},
|
||
{
|
||
name: 'Shoulder Rest — Bon Musica 4/4', sku: 'SR-BON-44', brand: 'Bon Musica', model: '4/4',
|
||
category: 'Shoulder Rests', price: '42.00', minPrice: '36.00',
|
||
isSerialized: false, isRental: false,
|
||
qtyOnHand: 6, qtyReorderPoint: 2,
|
||
description: 'Curved metal shoulder rest with contoured padding. Great for players with shoulder issues.',
|
||
},
|
||
|
||
// ── Maintenance ──
|
||
{
|
||
name: 'String Cleaner — Ultra X-Cleaner', sku: 'MNT-CLN-STR', brand: 'Ultra', model: 'X-Cleaner',
|
||
category: 'Maintenance & Care', price: '12.00', minPrice: '9.00',
|
||
isSerialized: false, isRental: false,
|
||
qtyOnHand: 18, qtyReorderPoint: 6,
|
||
},
|
||
{
|
||
name: 'Instrument Polish — Hill', sku: 'MNT-POL-HILL', brand: 'Hill', model: 'Polish',
|
||
category: 'Maintenance & Care', price: '10.00', minPrice: '8.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 15, qtyReorderPoint: 5,
|
||
},
|
||
// ── Repair Parts ──
|
||
{
|
||
name: 'Bridge — Violin 4/4 Blank', sku: 'RPR-BRG-VLN-44', brand: 'Aubert', model: 'Mirecourt',
|
||
category: 'Maintenance & Care', price: '18.00', minPrice: '14.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 10, qtyReorderPoint: 4,
|
||
description: 'Aubert Mirecourt violin bridge blank. Requires fitting.',
|
||
},
|
||
{
|
||
name: 'Bridge — Violin 3/4 Blank', sku: 'RPR-BRG-VLN-34', brand: 'Aubert', model: 'Mirecourt',
|
||
category: 'Maintenance & Care', price: '16.00', minPrice: '12.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 6, qtyReorderPoint: 2,
|
||
},
|
||
{
|
||
name: 'Bridge — Viola Blank', sku: 'RPR-BRG-VLA', brand: 'Aubert', model: 'Mirecourt',
|
||
category: 'Maintenance & Care', price: '22.00', minPrice: '17.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 6, qtyReorderPoint: 2,
|
||
},
|
||
{
|
||
name: 'Bridge — Cello Blank', sku: 'RPR-BRG-CLO', brand: 'Aubert', model: 'Mirecourt',
|
||
category: 'Maintenance & Care', price: '32.00', minPrice: '25.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 4, qtyReorderPoint: 2,
|
||
},
|
||
{
|
||
name: 'Bridge — Bass Blank', sku: 'RPR-BRG-BAS', brand: 'Aubert', model: 'Mirecourt',
|
||
category: 'Maintenance & Care', price: '48.00', minPrice: '38.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 3, qtyReorderPoint: 1,
|
||
},
|
||
{
|
||
name: 'Tailpiece — Violin 4/4 Composite', sku: 'RPR-TLP-VLN-44', brand: 'Wittner', model: 'Ultra',
|
||
category: 'Maintenance & Care', price: '14.00', minPrice: '11.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 8, qtyReorderPoint: 3,
|
||
description: 'Wittner Ultra composite tailpiece with integrated fine tuners.',
|
||
},
|
||
{
|
||
name: 'Tailpiece — Cello 4/4 Composite', sku: 'RPR-TLP-CLO-44', brand: 'Wittner', model: 'Ultra',
|
||
category: 'Maintenance & Care', price: '22.00', minPrice: '17.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 4, qtyReorderPoint: 2,
|
||
},
|
||
{
|
||
name: 'Pegs — Violin Boxwood Set (4)', sku: 'RPR-PEG-VLN-BX', brand: 'Generic', model: 'Boxwood',
|
||
category: 'Maintenance & Care', price: '12.00', minPrice: '9.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 12, qtyReorderPoint: 4,
|
||
description: 'Boxwood violin pegs, set of 4. Requires fitting and reaming.',
|
||
},
|
||
{
|
||
name: 'Pegs — Viola Boxwood Set (4)', sku: 'RPR-PEG-VLA-BX', brand: 'Generic', model: 'Boxwood',
|
||
category: 'Maintenance & Care', price: '14.00', minPrice: '11.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 8, qtyReorderPoint: 3,
|
||
},
|
||
{
|
||
name: 'Pegs — Cello Boxwood Set (4)', sku: 'RPR-PEG-CLO-BX', brand: 'Generic', model: 'Boxwood',
|
||
category: 'Maintenance & Care', price: '18.00', minPrice: '14.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 6, qtyReorderPoint: 2,
|
||
},
|
||
{
|
||
name: 'Fine Tuner — Single Violin', sku: 'RPR-FTN-VLN', brand: 'Wittner', model: 'Finetune',
|
||
category: 'Maintenance & Care', price: '4.00', minPrice: '3.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 20, qtyReorderPoint: 8,
|
||
},
|
||
{
|
||
name: 'Endpin — Cello Replacement', sku: 'RPR-EPN-CLO', brand: 'Wittner', model: 'Finetune',
|
||
category: 'Maintenance & Care', price: '24.00', minPrice: '18.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 4, qtyReorderPoint: 2,
|
||
description: 'Replacement cello endpin with aluminum sleeve.',
|
||
},
|
||
{
|
||
name: 'Endpin — Bass Replacement', sku: 'RPR-EPN-BAS', brand: 'Wittner', model: 'Finetune',
|
||
category: 'Maintenance & Care', price: '32.00', minPrice: '24.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 3, qtyReorderPoint: 1,
|
||
},
|
||
{
|
||
name: 'Nut — Violin Ebony Blank', sku: 'RPR-NUT-VLN', brand: 'Generic', model: 'Ebony',
|
||
category: 'Maintenance & Care', price: '6.00', minPrice: '4.50',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 10, qtyReorderPoint: 4,
|
||
},
|
||
{
|
||
name: 'Seam Glue — Hide Glue (4 oz)', sku: 'RPR-GLU-HIDE', brand: 'Franklin', model: 'Hide Glue',
|
||
category: 'Maintenance & Care', price: '8.00', minPrice: '6.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 6, qtyReorderPoint: 2,
|
||
description: 'Traditional hide glue for open seam and structural repairs.',
|
||
},
|
||
{
|
||
name: 'Sound Post — Violin', sku: 'RPR-SPS-VLN', brand: 'Generic', model: 'Spruce',
|
||
category: 'Maintenance & Care', price: '5.00', minPrice: '3.50',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 8, qtyReorderPoint: 3,
|
||
},
|
||
{
|
||
name: 'Sound Post — Cello', sku: 'RPR-SPS-CLO', brand: 'Generic', model: 'Spruce',
|
||
category: 'Maintenance & Care', price: '8.00', minPrice: '6.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 4, qtyReorderPoint: 2,
|
||
},
|
||
{
|
||
name: 'Bow Hair — Mongolian White (hank)', sku: 'RPR-HAIR-WH', brand: 'Generic', model: 'Mongolian White',
|
||
category: 'Maintenance & Care', price: '18.00', minPrice: '14.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 10, qtyReorderPoint: 3,
|
||
description: 'Mongolian white bow hair for rehairing violin, viola, cello, or bass bows.',
|
||
},
|
||
{
|
||
name: 'Peg Compound — Hill Peg Dope', sku: 'RPR-PEG-DOC', brand: 'Hill', model: 'Peg Compound',
|
||
category: 'Maintenance & Care', price: '6.00', minPrice: '4.50',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 12, qtyReorderPoint: 4,
|
||
description: 'Hill peg compound for slipping or sticking pegs.',
|
||
},
|
||
{
|
||
name: 'Fingerboard Oil — Lemon Oil (2 oz)', sku: 'RPR-OIL-LMN', brand: 'Music Nomad', model: 'Lemon Oil',
|
||
category: 'Maintenance & Care', price: '9.00', minPrice: '7.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 10, qtyReorderPoint: 4,
|
||
description: 'Conditions and cleans ebony and rosewood fingerboards.',
|
||
},
|
||
{
|
||
name: 'Chin Rest — Violin Guarneri Style', sku: 'RPR-CHN-VLN-GU', brand: 'Wittner', model: 'Guarneri',
|
||
category: 'Maintenance & Care', price: '16.00', minPrice: '12.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 8, qtyReorderPoint: 3,
|
||
description: 'Wittner composite Guarneri-style violin chin rest. Center mount.',
|
||
},
|
||
{
|
||
name: 'Chin Rest — Viola Side Mount', sku: 'RPR-CHN-VLA-SD', brand: 'Wittner', model: 'Augsburg',
|
||
category: 'Maintenance & Care', price: '18.00', minPrice: '14.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 6, qtyReorderPoint: 2,
|
||
},
|
||
{
|
||
name: 'Chin Rest Screws — Replacement Set', sku: 'RPR-CHN-SCR', brand: 'Generic', model: 'Brass',
|
||
category: 'Maintenance & Care', price: '3.00', minPrice: '2.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 20, qtyReorderPoint: 8,
|
||
description: 'Replacement brass chin rest barrel screws, set of 2.',
|
||
},
|
||
{
|
||
name: 'Bow Grip — Leather Violin/Viola', sku: 'RPR-GRP-VLN', brand: 'Generic', model: 'Leather',
|
||
category: 'Maintenance & Care', price: '5.00', minPrice: '3.50',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 15, qtyReorderPoint: 6,
|
||
description: 'Replacement leather bow grip for violin or viola bow.',
|
||
},
|
||
{
|
||
name: 'Bow Grip — Leather Cello/Bass', sku: 'RPR-GRP-CLO', brand: 'Generic', model: 'Leather',
|
||
category: 'Maintenance & Care', price: '6.00', minPrice: '4.50',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 10, qtyReorderPoint: 4,
|
||
},
|
||
{
|
||
name: 'Bow Winding — Silver Lapping', sku: 'RPR-WND-SLV', brand: 'Generic', model: 'Silver',
|
||
category: 'Maintenance & Care', price: '8.00', minPrice: '6.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 8, qtyReorderPoint: 3,
|
||
description: 'Silver lapping wire for bow winding replacement.',
|
||
},
|
||
{
|
||
name: 'Bow Tip Plate — Violin Ivory-Style', sku: 'RPR-TIP-VLN', brand: 'Generic', model: 'Mammoth Ivory',
|
||
category: 'Maintenance & Care', price: '7.00', minPrice: '5.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 10, qtyReorderPoint: 4,
|
||
description: 'Replacement bow tip plate (mammoth ivory substitute).',
|
||
},
|
||
{
|
||
name: 'Saddle — Violin Ebony', sku: 'RPR-SDL-VLN', brand: 'Generic', model: 'Ebony',
|
||
category: 'Maintenance & Care', price: '5.00', minPrice: '3.50',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 10, qtyReorderPoint: 4,
|
||
},
|
||
{
|
||
name: 'Saddle — Cello Ebony', sku: 'RPR-SDL-CLO', brand: 'Generic', model: 'Ebony',
|
||
category: 'Maintenance & Care', price: '8.00', minPrice: '6.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 6, qtyReorderPoint: 2,
|
||
},
|
||
{
|
||
name: 'Retouching Varnish — Violin/Viola', sku: 'RPR-VRN-RTH', brand: 'Hammerl', model: 'Retouching',
|
||
category: 'Maintenance & Care', price: '22.00', minPrice: '17.00',
|
||
isSerialized: false, isRental: false, isDualUseRepair: true,
|
||
qtyOnHand: 4, qtyReorderPoint: 2,
|
||
description: 'Spirit-based retouching varnish for cosmetic repairs and touch-ups.',
|
||
},
|
||
{
|
||
name: 'Humidifier — Dampit Violin', sku: 'MNT-HUM-VLN', brand: 'Dampit', model: 'Violin',
|
||
category: 'Maintenance & Care', price: '16.00', minPrice: '13.00',
|
||
isSerialized: false, isRental: false,
|
||
qtyOnHand: 12, qtyReorderPoint: 4,
|
||
},
|
||
{
|
||
name: 'Humidifier — Dampit Cello', sku: 'MNT-HUM-CLO', brand: 'Dampit', model: 'Cello',
|
||
category: 'Maintenance & Care', price: '18.00', minPrice: '15.00',
|
||
isSerialized: false, isRental: false,
|
||
qtyOnHand: 8, qtyReorderPoint: 3,
|
||
},
|
||
]
|
||
|
||
const productIds: Record<string, string> = {}
|
||
for (const p of productDefs) {
|
||
const [existing] = await sql`SELECT id FROM product WHERE sku = ${p.sku}`
|
||
if (existing) { productIds[p.sku] = existing.id; continue }
|
||
const catId = catIds[p.category] ?? null
|
||
const [row] = await sql`
|
||
INSERT INTO product (sku, name, brand, model, category_id, price, min_price, rental_rate_monthly,
|
||
is_serialized, is_rental, is_dual_use_repair, qty_on_hand, qty_reorder_point, description, is_active)
|
||
VALUES (${p.sku}, ${p.name}, ${p.brand}, ${p.model}, ${catId}, ${p.price}, ${p.minPrice ?? null},
|
||
${p.rentalRateMonthly ?? null}, ${p.isSerialized}, ${p.isRental}, ${p.isDualUseRepair ?? false},
|
||
${p.qtyOnHand ?? 0}, ${p.qtyReorderPoint ?? null}, ${p.description ?? null}, true)
|
||
RETURNING id`
|
||
productIds[p.sku] = row.id
|
||
console.log(` Product: ${p.name}`)
|
||
}
|
||
|
||
// ── Inventory Units (serialized instruments) ──────────────────────────────
|
||
type UnitDef = { sku: string; serial: string; condition: string; status: string; purchaseDate: string; cost: string; notes?: string }
|
||
|
||
const unitDefs: UnitDef[] = [
|
||
// Student violin 4/4 — rental fleet
|
||
{ sku: 'VLN-44-STU', serial: 'EA-V80-001', condition: 'good', status: 'available', purchaseDate: '2022-08-15', cost: '185.00' },
|
||
{ sku: 'VLN-44-STU', serial: 'EA-V80-002', condition: 'good', status: 'rented', purchaseDate: '2022-08-15', cost: '185.00' },
|
||
{ sku: 'VLN-44-STU', serial: 'EA-V80-003', condition: 'fair', status: 'rented', purchaseDate: '2022-08-15', cost: '185.00', notes: 'Minor scratches on back' },
|
||
{ sku: 'VLN-44-STU', serial: 'EA-V80-004', condition: 'good', status: 'rented', purchaseDate: '2022-08-15', cost: '185.00' },
|
||
{ sku: 'VLN-44-STU', serial: 'EA-V80-005', condition: 'excellent', status: 'available', purchaseDate: '2023-06-01', cost: '195.00' },
|
||
{ sku: 'VLN-44-STU', serial: 'EA-V80-006', condition: 'fair', status: 'in_repair', purchaseDate: '2021-08-01', cost: '175.00', notes: 'Pegs slipping, in shop' },
|
||
// Student violin 3/4
|
||
{ sku: 'VLN-34-STU', serial: 'EA-V80-3Q-001', condition: 'good', status: 'available', purchaseDate: '2022-09-01', cost: '160.00' },
|
||
{ sku: 'VLN-34-STU', serial: 'EA-V80-3Q-002', condition: 'good', status: 'rented', purchaseDate: '2022-09-01', cost: '160.00' },
|
||
{ sku: 'VLN-34-STU', serial: 'EA-V80-3Q-003', condition: 'fair', status: 'rented', purchaseDate: '2021-09-01', cost: '155.00', notes: 'Repaired open seam 2023' },
|
||
// Student violin 1/2
|
||
{ sku: 'VLN-12-STU', serial: 'EA-V80-HF-001', condition: 'good', status: 'rented', purchaseDate: '2022-09-01', cost: '140.00' },
|
||
{ sku: 'VLN-12-STU', serial: 'EA-V80-HF-002', condition: 'good', status: 'available', purchaseDate: '2023-07-01', cost: '145.00' },
|
||
// Student violin 1/4
|
||
{ sku: 'VLN-14-STU', serial: 'EA-V80-QT-001', condition: 'good', status: 'rented', purchaseDate: '2023-01-15', cost: '120.00' },
|
||
{ sku: 'VLN-14-STU', serial: 'EA-V80-QT-002', condition: 'fair', status: 'available', purchaseDate: '2022-01-15', cost: '115.00', notes: 'Replaced tailpiece 2023' },
|
||
// Intermediate violin
|
||
{ sku: 'VLN-44-INT', serial: 'EA-V305-001', condition: 'excellent', status: 'available', purchaseDate: '2023-08-01', cost: '420.00' },
|
||
{ sku: 'VLN-44-INT', serial: 'EA-V305-002', condition: 'good', status: 'rented', purchaseDate: '2023-08-01', cost: '420.00' },
|
||
// Advanced violin
|
||
{ sku: 'VLN-44-ADV', serial: 'EA-V605-001', condition: 'new', status: 'available', purchaseDate: '2024-02-01', cost: '1200.00' },
|
||
|
||
// Violas
|
||
{ sku: 'VLA-15-STU', serial: 'EA-VA80-15-001', condition: 'good', status: 'rented', purchaseDate: '2022-09-01', cost: '210.00' },
|
||
{ sku: 'VLA-15-STU', serial: 'EA-VA80-15-002', condition: 'excellent', status: 'available', purchaseDate: '2023-08-01', cost: '220.00' },
|
||
{ sku: 'VLA-16-STU', serial: 'EA-VA80-16-001', condition: 'good', status: 'rented', purchaseDate: '2022-09-01', cost: '210.00' },
|
||
{ sku: 'VLA-16-INT', serial: 'EA-VA305-001', condition: 'new', status: 'available', purchaseDate: '2024-01-15', cost: '520.00' },
|
||
|
||
// Cellos
|
||
{ sku: 'CLO-44-STU', serial: 'EA-VC80-001', condition: 'good', status: 'rented', purchaseDate: '2021-08-15', cost: '300.00' },
|
||
{ sku: 'CLO-44-STU', serial: 'EA-VC80-002', condition: 'good', status: 'rented', purchaseDate: '2021-08-15', cost: '300.00' },
|
||
{ sku: 'CLO-44-STU', serial: 'EA-VC80-003', condition: 'fair', status: 'in_repair', purchaseDate: '2020-08-15', cost: '285.00', notes: 'Endpin mechanism worn, awaiting part' },
|
||
{ sku: 'CLO-44-STU', serial: 'EA-VC80-004', condition: 'excellent', status: 'available', purchaseDate: '2023-09-01', cost: '315.00' },
|
||
{ sku: 'CLO-34-STU', serial: 'EA-VC80-3Q-001', condition: 'good', status: 'rented', purchaseDate: '2022-08-15', cost: '275.00' },
|
||
{ sku: 'CLO-12-STU', serial: 'EA-VC80-HF-001', condition: 'good', status: 'available', purchaseDate: '2023-08-01', cost: '255.00' },
|
||
{ sku: 'CLO-44-INT', serial: 'EA-VC305-001', condition: 'new', status: 'available', purchaseDate: '2024-03-01', cost: '720.00' },
|
||
|
||
// Basses
|
||
{ sku: 'BAS-34-STU', serial: 'ENG-ES1-001', condition: 'good', status: 'rented', purchaseDate: '2020-08-01', cost: '600.00', notes: 'New strings installed 2024' },
|
||
{ sku: 'BAS-34-STU', serial: 'ENG-ES1-002', condition: 'fair', status: 'available', purchaseDate: '2019-08-01', cost: '575.00', notes: 'Repaired bass bar 2022' },
|
||
{ sku: 'BAS-34-STU', serial: 'ENG-ES1-003', condition: 'excellent', status: 'available', purchaseDate: '2023-09-01', cost: '620.00' },
|
||
{ sku: 'BAS-12-STU', serial: 'EA-VB80-HF-001', condition: 'good', status: 'rented', purchaseDate: '2022-08-01', cost: '510.00' },
|
||
]
|
||
|
||
for (const u of unitDefs) {
|
||
const productId = productIds[u.sku]
|
||
if (!productId) continue
|
||
const [existing] = await sql`SELECT id FROM inventory_unit WHERE serial_number = ${u.serial}`
|
||
if (existing) continue
|
||
await sql`
|
||
INSERT INTO inventory_unit (product_id, serial_number, condition, status, purchase_date, purchase_cost, notes)
|
||
VALUES (${productId}, ${u.serial}, ${u.condition}, ${u.status}, ${u.purchaseDate}, ${u.cost}, ${u.notes ?? null})`
|
||
console.log(` Unit: ${u.serial} (${u.sku}) [${u.status}]`)
|
||
}
|
||
|
||
// ── Price History ──────────────────────────────────────────────────────────
|
||
// Simulate a couple of price changes per key product over the past year
|
||
type PriceHistoryDef = { sku: string; previousPrice: string; newPrice: string; previousMinPrice?: string; newMinPrice?: string; daysAgo: number }
|
||
|
||
const priceHistoryDefs: PriceHistoryDef[] = [
|
||
// Student violins — small price increases over two cycles
|
||
{ sku: 'VLN-44-STU', previousPrice: '379.00', newPrice: '389.00', previousMinPrice: '330.00', newMinPrice: '340.00', daysAgo: 365 },
|
||
{ sku: 'VLN-44-STU', previousPrice: '389.00', newPrice: '399.00', previousMinPrice: '340.00', newMinPrice: '350.00', daysAgo: 180 },
|
||
{ sku: 'VLN-34-STU', previousPrice: '329.00', newPrice: '349.00', previousMinPrice: '285.00', newMinPrice: '300.00', daysAgo: 180 },
|
||
{ sku: 'VLN-12-STU', previousPrice: '279.00', newPrice: '299.00', previousMinPrice: '245.00', newMinPrice: '260.00', daysAgo: 180 },
|
||
{ sku: 'VLN-14-STU', previousPrice: '229.00', newPrice: '249.00', previousMinPrice: '200.00', newMinPrice: '220.00', daysAgo: 180 },
|
||
// Intermediate violin — one increase
|
||
{ sku: 'VLN-44-INT', previousPrice: '849.00', newPrice: '895.00', previousMinPrice: '759.00', newMinPrice: '800.00', daysAgo: 270 },
|
||
// Advanced violin — introduced at lower price, increased
|
||
{ sku: 'VLN-44-ADV', previousPrice: '2295.00', newPrice: '2495.00', previousMinPrice: '2050.00', newMinPrice: '2200.00', daysAgo: 120 },
|
||
// Violas
|
||
{ sku: 'VLA-15-STU', previousPrice: '419.00', newPrice: '449.00', previousMinPrice: '370.00', newMinPrice: '400.00', daysAgo: 200 },
|
||
{ sku: 'VLA-16-STU', previousPrice: '419.00', newPrice: '449.00', previousMinPrice: '370.00', newMinPrice: '400.00', daysAgo: 200 },
|
||
{ sku: 'VLA-16-INT', previousPrice: '995.00', newPrice: '1095.00', previousMinPrice: '875.00', newMinPrice: '950.00', daysAgo: 150 },
|
||
// Cellos
|
||
{ sku: 'CLO-44-STU', previousPrice: '599.00', newPrice: '649.00', previousMinPrice: '535.00', newMinPrice: '580.00', daysAgo: 220 },
|
||
{ sku: 'CLO-34-STU', previousPrice: '549.00', newPrice: '599.00', previousMinPrice: '490.00', newMinPrice: '530.00', daysAgo: 220 },
|
||
{ sku: 'CLO-44-INT', previousPrice: '1349.00', newPrice: '1495.00', previousMinPrice: '1195.00', newMinPrice: '1300.00', daysAgo: 160 },
|
||
// Basses
|
||
{ sku: 'BAS-34-STU', previousPrice: '1199.00', newPrice: '1299.00', previousMinPrice: '1075.00', newMinPrice: '1150.00', daysAgo: 240 },
|
||
// Strings — supplier cost increases
|
||
{ sku: 'STR-VLN-DOM-44', previousPrice: '34.00', newPrice: '38.00', previousMinPrice: '28.00', newMinPrice: '32.00', daysAgo: 90 },
|
||
{ sku: 'STR-CLO-LAR', previousPrice: '88.00', newPrice: '95.00', previousMinPrice: '78.00', newMinPrice: '85.00', daysAgo: 90 },
|
||
{ sku: 'STR-BAS-HEL', previousPrice: '99.00', newPrice: '110.00', previousMinPrice: '87.00', newMinPrice: '95.00', daysAgo: 90 },
|
||
// Bridges — material cost increase
|
||
{ sku: 'RPR-BRG-VLN-44', previousPrice: '15.00', newPrice: '18.00', daysAgo: 120 },
|
||
{ sku: 'RPR-BRG-CLO', previousPrice: '28.00', newPrice: '32.00', daysAgo: 120 },
|
||
// Bow hair
|
||
{ sku: 'RPR-HAIR-WH', previousPrice: '15.00', newPrice: '18.00', daysAgo: 60 },
|
||
]
|
||
|
||
for (const h of priceHistoryDefs) {
|
||
const productId = productIds[h.sku]
|
||
if (!productId) continue
|
||
const changedAt = new Date(Date.now() - h.daysAgo * 24 * 60 * 60 * 1000).toISOString()
|
||
const [existing] = await sql`
|
||
SELECT id FROM price_history WHERE product_id = ${productId} AND new_price = ${h.newPrice} AND previous_price = ${h.previousPrice}`
|
||
if (existing) continue
|
||
await sql`
|
||
INSERT INTO price_history (product_id, previous_price, new_price, previous_min_price, new_min_price, created_at)
|
||
VALUES (${productId}, ${h.previousPrice}, ${h.newPrice}, ${h.previousMinPrice ?? null}, ${h.newMinPrice ?? null}, ${changedAt})`
|
||
console.log(` Price history: ${h.sku} $${h.previousPrice} → $${h.newPrice}`)
|
||
}
|
||
|
||
// ── Product-Supplier Links ─────────────────────────────────────────────────
|
||
type SupplierLink = { sku: string; supplier: string; supplierSku?: string; isPreferred: boolean }
|
||
|
||
const supplierLinks: SupplierLink[] = [
|
||
// Violins — Shar preferred, Southwest alternate
|
||
{ sku: 'VLN-44-STU', supplier: 'Shar Music', supplierSku: 'EA-VL80-44', isPreferred: true },
|
||
{ sku: 'VLN-44-STU', supplier: 'Southwest Strings', supplierSku: 'EAST-VL80-4', isPreferred: false },
|
||
{ sku: 'VLN-34-STU', supplier: 'Shar Music', supplierSku: 'EA-VL80-34', isPreferred: true },
|
||
{ sku: 'VLN-34-STU', supplier: 'Southwest Strings', supplierSku: 'EAST-VL80-3', isPreferred: false },
|
||
{ sku: 'VLN-12-STU', supplier: 'Shar Music', supplierSku: 'EA-VL80-12', isPreferred: true },
|
||
{ sku: 'VLN-14-STU', supplier: 'Shar Music', supplierSku: 'EA-VL80-14', isPreferred: true },
|
||
{ sku: 'VLN-44-INT', supplier: 'Shar Music', supplierSku: 'EA-VL305', isPreferred: true },
|
||
{ sku: 'VLN-44-INT', supplier: 'Southwest Strings', supplierSku: 'EAST-VL305', isPreferred: false },
|
||
{ sku: 'VLN-44-ADV', supplier: 'Shar Music', supplierSku: 'EA-VL605', isPreferred: true },
|
||
// Violas — Shar preferred, Connolly alternate
|
||
{ sku: 'VLA-15-STU', supplier: 'Shar Music', supplierSku: 'EA-VA80-15', isPreferred: true },
|
||
{ sku: 'VLA-15-STU', supplier: 'Connolly Music', supplierSku: 'EAST-VA80-15', isPreferred: false },
|
||
{ sku: 'VLA-16-STU', supplier: 'Shar Music', supplierSku: 'EA-VA80-16', isPreferred: true },
|
||
{ sku: 'VLA-16-INT', supplier: 'Shar Music', supplierSku: 'EA-VA305-16', isPreferred: true },
|
||
{ sku: 'VLA-16-INT', supplier: 'Connolly Music', supplierSku: 'EAST-VA305', isPreferred: false },
|
||
// Cellos — Connolly preferred, Shar alternate
|
||
{ sku: 'CLO-44-STU', supplier: 'Connolly Music', supplierSku: 'EAST-VC80-44', isPreferred: true },
|
||
{ sku: 'CLO-44-STU', supplier: 'Shar Music', supplierSku: 'EA-VC80-44', isPreferred: false },
|
||
{ sku: 'CLO-34-STU', supplier: 'Connolly Music', supplierSku: 'EAST-VC80-34', isPreferred: true },
|
||
{ sku: 'CLO-12-STU', supplier: 'Connolly Music', supplierSku: 'EAST-VC80-12', isPreferred: true },
|
||
{ sku: 'CLO-44-INT', supplier: 'Connolly Music', supplierSku: 'EAST-VC305', isPreferred: true },
|
||
{ sku: 'CLO-44-INT', supplier: 'Shar Music', supplierSku: 'EA-VC305', isPreferred: false },
|
||
// Basses — Connolly preferred
|
||
{ sku: 'BAS-34-STU', supplier: 'Connolly Music', supplierSku: 'ENG-ES1', isPreferred: true },
|
||
{ sku: 'BAS-34-STU', supplier: 'Southwest Strings', supplierSku: 'ENG-ES1-SW', isPreferred: false },
|
||
{ sku: 'BAS-12-STU', supplier: 'Connolly Music', supplierSku: 'EAST-VB80-12', isPreferred: true },
|
||
// Strings — D'Addario for Prelude/Helicore, Shar for Dominant, Pirastro for Larsen
|
||
{ sku: 'STR-VLN-PRE-44', supplier: "D'Addario", supplierSku: 'J810-4/4M', isPreferred: true },
|
||
{ sku: 'STR-VLN-DOM-44', supplier: 'Shar Music', supplierSku: 'TH-132B', isPreferred: true },
|
||
{ sku: 'STR-VLN-DOM-34', supplier: 'Shar Music', supplierSku: 'TH-132B-34', isPreferred: true },
|
||
{ sku: 'STR-VLA-DOM', supplier: 'Shar Music', supplierSku: 'TH-141B', isPreferred: true },
|
||
{ sku: 'STR-CLO-PRE', supplier: "D'Addario", supplierSku: 'J1010-4/4M', isPreferred: true },
|
||
{ sku: 'STR-CLO-LAR', supplier: 'Pirastro', supplierSku: 'LAR-SET-4', isPreferred: true },
|
||
{ sku: 'STR-BAS-HEL', supplier: "D'Addario", supplierSku: 'J6103/4M', isPreferred: true },
|
||
// Rosin
|
||
{ sku: 'RSN-HILL-L', supplier: 'Shar Music', supplierSku: 'HILL-RSN-L', isPreferred: true },
|
||
{ sku: 'RSN-HILL-D', supplier: 'Shar Music', supplierSku: 'HILL-RSN-D', isPreferred: true },
|
||
{ sku: 'RSN-PIR-GF', supplier: 'Pirastro', supplierSku: '900500', isPreferred: true },
|
||
// Bows
|
||
{ sku: 'BOW-VLN-STU', supplier: 'Shar Music', supplierSku: 'CB-JOULE-VLN', isPreferred: true },
|
||
{ sku: 'BOW-VLN-STU', supplier: 'Southwest Strings', supplierSku: 'CODA-JOULE-V', isPreferred: false },
|
||
{ sku: 'BOW-VLN-CF', supplier: 'Shar Music', supplierSku: 'CB-DNX-VLN', isPreferred: true },
|
||
{ sku: 'BOW-VLA-STU', supplier: 'Shar Music', supplierSku: 'CB-JOULE-VLA', isPreferred: true },
|
||
{ sku: 'BOW-CLO-STU', supplier: 'Shar Music', supplierSku: 'CB-JOULE-CLO', isPreferred: true },
|
||
{ sku: 'BOW-BAS-STU-G', supplier: 'Southwest Strings', supplierSku: 'GL-GER-BAS', isPreferred: true },
|
||
// Maintenance
|
||
{ sku: 'MNT-POL-HILL', supplier: 'Shar Music', supplierSku: 'HILL-POL', isPreferred: true },
|
||
{ sku: 'MNT-HUM-VLN', supplier: 'Shar Music', supplierSku: 'DAMP-VLN', isPreferred: true },
|
||
{ sku: 'MNT-HUM-CLO', supplier: 'Shar Music', supplierSku: 'DAMP-CLO', isPreferred: true },
|
||
]
|
||
|
||
for (const link of supplierLinks) {
|
||
const productId = productIds[link.sku]
|
||
const supplierId = supplierIds[link.supplier]
|
||
if (!productId || !supplierId) continue
|
||
const [existing] = await sql`SELECT id FROM product_supplier WHERE product_id = ${productId} AND supplier_id = ${supplierId}`
|
||
if (existing) continue
|
||
await sql`
|
||
INSERT INTO product_supplier (product_id, supplier_id, supplier_sku, is_preferred)
|
||
VALUES (${productId}, ${supplierId}, ${link.supplierSku ?? null}, ${link.isPreferred})`
|
||
console.log(` Linked ${link.sku} → ${link.supplier}${link.isPreferred ? ' (preferred)' : ''}`)
|
||
}
|
||
|
||
// ── Stock Receipts ─────────────────────────────────────────────────────────
|
||
type ReceiptDef = { sku: string; supplier?: string; qty: number; costPerUnit: string; receivedDate: string; invoiceNumber?: string; notes?: string }
|
||
|
||
const receiptDefs: ReceiptDef[] = [
|
||
// Initial instrument purchases
|
||
{ sku: 'VLN-44-STU', supplier: 'Shar Music', qty: 6, costPerUnit: '185.00', receivedDate: '2022-08-01', invoiceNumber: 'SHAR-2022-0801' },
|
||
{ sku: 'VLN-34-STU', supplier: 'Shar Music', qty: 3, costPerUnit: '160.00', receivedDate: '2022-09-01', invoiceNumber: 'SHAR-2022-0901' },
|
||
{ sku: 'VLN-12-STU', supplier: 'Shar Music', qty: 2, costPerUnit: '140.00', receivedDate: '2022-09-01', invoiceNumber: 'SHAR-2022-0901' },
|
||
{ sku: 'VLN-14-STU', supplier: 'Shar Music', qty: 2, costPerUnit: '115.00', receivedDate: '2022-09-01', invoiceNumber: 'SHAR-2022-0901' },
|
||
{ sku: 'VLN-44-INT', supplier: 'Shar Music', qty: 2, costPerUnit: '420.00', receivedDate: '2023-08-01', invoiceNumber: 'SHAR-2023-0801' },
|
||
{ sku: 'VLN-44-ADV', supplier: 'Shar Music', qty: 1, costPerUnit: '1200.00', receivedDate: '2024-02-01', invoiceNumber: 'SHAR-2024-0201' },
|
||
{ sku: 'VLA-15-STU', supplier: 'Shar Music', qty: 2, costPerUnit: '210.00', receivedDate: '2022-09-01', invoiceNumber: 'SHAR-2022-0901' },
|
||
{ sku: 'VLA-16-STU', supplier: 'Shar Music', qty: 1, costPerUnit: '210.00', receivedDate: '2022-09-01', invoiceNumber: 'SHAR-2022-0901' },
|
||
{ sku: 'VLA-16-INT', supplier: 'Shar Music', qty: 1, costPerUnit: '520.00', receivedDate: '2024-01-15', invoiceNumber: 'SHAR-2024-0115' },
|
||
{ sku: 'CLO-44-STU', supplier: 'Connolly Music', qty: 4, costPerUnit: '300.00', receivedDate: '2021-08-01', invoiceNumber: 'CON-2021-0801' },
|
||
{ sku: 'CLO-34-STU', supplier: 'Connolly Music', qty: 1, costPerUnit: '275.00', receivedDate: '2022-08-01', invoiceNumber: 'CON-2022-0801' },
|
||
{ sku: 'CLO-12-STU', supplier: 'Connolly Music', qty: 1, costPerUnit: '255.00', receivedDate: '2023-08-01', invoiceNumber: 'CON-2023-0801' },
|
||
{ sku: 'CLO-44-INT', supplier: 'Connolly Music', qty: 1, costPerUnit: '720.00', receivedDate: '2024-03-01', invoiceNumber: 'CON-2024-0301' },
|
||
{ sku: 'BAS-34-STU', supplier: 'Connolly Music', qty: 3, costPerUnit: '595.00', receivedDate: '2019-08-01', invoiceNumber: 'CON-2019-0801' },
|
||
{ sku: 'BAS-12-STU', supplier: 'Connolly Music', qty: 1, costPerUnit: '510.00', receivedDate: '2022-08-01', invoiceNumber: 'CON-2022-0802' },
|
||
// Bow stock
|
||
{ sku: 'BOW-VLN-STU', supplier: 'Shar Music', qty: 8, costPerUnit: '52.00', receivedDate: '2024-01-10', invoiceNumber: 'SHAR-2024-0110' },
|
||
{ sku: 'BOW-VLN-CF', supplier: 'Shar Music', qty: 4, costPerUnit: '175.00', receivedDate: '2024-01-10', invoiceNumber: 'SHAR-2024-0110' },
|
||
{ sku: 'BOW-VLA-STU', supplier: 'Shar Music', qty: 5, costPerUnit: '56.00', receivedDate: '2024-01-10', invoiceNumber: 'SHAR-2024-0110' },
|
||
{ sku: 'BOW-CLO-STU', supplier: 'Shar Music', qty: 5, costPerUnit: '65.00', receivedDate: '2024-01-10', invoiceNumber: 'SHAR-2024-0110' },
|
||
{ sku: 'BOW-BAS-STU-G', supplier: 'Southwest Strings', qty: 4, costPerUnit: '48.00', receivedDate: '2024-02-01', invoiceNumber: 'SW-2024-0201' },
|
||
// String restocks
|
||
{ sku: 'STR-VLN-DOM-44', supplier: 'Shar Music', qty: 24, costPerUnit: '22.00', receivedDate: '2025-09-01', invoiceNumber: 'SHAR-2025-0901' },
|
||
{ sku: 'STR-VLN-DOM-44', supplier: 'Shar Music', qty: 12, costPerUnit: '22.50', receivedDate: '2026-01-15', invoiceNumber: 'SHAR-2026-0115' },
|
||
{ sku: 'STR-VLN-DOM-34', supplier: 'Shar Music', qty: 12, costPerUnit: '22.00', receivedDate: '2025-09-01', invoiceNumber: 'SHAR-2025-0901' },
|
||
{ sku: 'STR-VLN-PRE-44', supplier: "D'Addario", qty: 36, costPerUnit: '10.50', receivedDate: '2025-09-01', invoiceNumber: 'DAD-2025-0901' },
|
||
{ sku: 'STR-VLN-PRE-44', supplier: "D'Addario", qty: 24, costPerUnit: '11.00', receivedDate: '2026-02-01', invoiceNumber: 'DAD-2026-0201' },
|
||
{ sku: 'STR-VLA-DOM', supplier: 'Shar Music', qty: 12, costPerUnit: '32.00', receivedDate: '2025-10-01', invoiceNumber: 'SHAR-2025-1001' },
|
||
{ sku: 'STR-CLO-PRE', supplier: "D'Addario", qty: 18, costPerUnit: '22.00', receivedDate: '2025-09-01', invoiceNumber: 'DAD-2025-0901' },
|
||
{ sku: 'STR-CLO-LAR', supplier: 'Pirastro', qty: 6, costPerUnit: '58.00', receivedDate: '2025-11-01', invoiceNumber: 'PIR-2025-1101' },
|
||
{ sku: 'STR-BAS-HEL', supplier: "D'Addario", qty: 10, costPerUnit: '65.00', receivedDate: '2025-09-01', invoiceNumber: 'DAD-2025-0901' },
|
||
// Rosin
|
||
{ sku: 'RSN-HILL-L', supplier: 'Shar Music', qty: 36, costPerUnit: '5.00', receivedDate: '2025-08-15', invoiceNumber: 'SHAR-2025-0815' },
|
||
{ sku: 'RSN-HILL-D', supplier: 'Shar Music', qty: 24, costPerUnit: '5.00', receivedDate: '2025-08-15', invoiceNumber: 'SHAR-2025-0815' },
|
||
{ sku: 'RSN-PIR-GF', supplier: 'Pirastro', qty: 18, costPerUnit: '8.50', receivedDate: '2025-08-15', invoiceNumber: 'PIR-2025-0815' },
|
||
// Repair parts
|
||
{ sku: 'RPR-BRG-VLN-44', supplier: 'Shar Music', qty: 12, costPerUnit: '10.00', receivedDate: '2025-07-01', invoiceNumber: 'SHAR-2025-0701' },
|
||
{ sku: 'RPR-BRG-VLN-34', supplier: 'Shar Music', qty: 8, costPerUnit: '9.00', receivedDate: '2025-07-01', invoiceNumber: 'SHAR-2025-0701' },
|
||
{ sku: 'RPR-BRG-VLA', supplier: 'Shar Music', qty: 8, costPerUnit: '13.00', receivedDate: '2025-07-01', invoiceNumber: 'SHAR-2025-0701' },
|
||
{ sku: 'RPR-BRG-CLO', supplier: 'Shar Music', qty: 6, costPerUnit: '19.00', receivedDate: '2025-07-01', invoiceNumber: 'SHAR-2025-0701' },
|
||
{ sku: 'RPR-BRG-BAS', supplier: 'Shar Music', qty: 4, costPerUnit: '29.00', receivedDate: '2025-07-01', invoiceNumber: 'SHAR-2025-0701' },
|
||
{ sku: 'RPR-PEG-VLN-BX', supplier: 'Shar Music', qty: 15, costPerUnit: '7.00', receivedDate: '2025-07-01', invoiceNumber: 'SHAR-2025-0701' },
|
||
{ sku: 'RPR-PEG-VLA-BX', supplier: 'Shar Music', qty: 10, costPerUnit: '8.00', receivedDate: '2025-07-01', invoiceNumber: 'SHAR-2025-0701' },
|
||
{ sku: 'RPR-PEG-CLO-BX', supplier: 'Shar Music', qty: 8, costPerUnit: '10.00', receivedDate: '2025-07-01', invoiceNumber: 'SHAR-2025-0701' },
|
||
{ sku: 'RPR-HAIR-WH', supplier: 'Shar Music', qty: 12, costPerUnit: '11.00', receivedDate: '2025-10-01', invoiceNumber: 'SHAR-2025-1001' },
|
||
{ sku: 'RPR-TLP-VLN-44', supplier: 'Shar Music', qty: 10, costPerUnit: '8.50', receivedDate: '2025-09-15', invoiceNumber: 'SHAR-2025-0915' },
|
||
{ sku: 'RPR-GLU-HIDE', supplier: 'Shar Music', qty: 8, costPerUnit: '4.50', receivedDate: '2025-09-15', invoiceNumber: 'SHAR-2025-0915' },
|
||
{ sku: 'RPR-POL-HILL', supplier: 'Shar Music', qty: 18, costPerUnit: '6.00', receivedDate: '2025-09-15', invoiceNumber: 'SHAR-2025-0915', notes: 'SKU changed from MNT-POL-HILL' },
|
||
{ sku: 'MNT-POL-HILL', supplier: 'Shar Music', qty: 18, costPerUnit: '6.00', receivedDate: '2025-09-15', invoiceNumber: 'SHAR-2025-0915' },
|
||
{ sku: 'RPR-PEG-DOC', supplier: 'Shar Music', qty: 15, costPerUnit: '3.50', receivedDate: '2025-09-15', invoiceNumber: 'SHAR-2025-0915' },
|
||
// Shoulder rests
|
||
{ sku: 'SR-KUN-44', supplier: 'Shar Music', qty: 12, costPerUnit: '17.00', receivedDate: '2025-08-01', invoiceNumber: 'SHAR-2025-0801' },
|
||
{ sku: 'SR-KUN-C', supplier: 'Shar Music', qty: 8, costPerUnit: '21.00', receivedDate: '2025-08-01', invoiceNumber: 'SHAR-2025-0801' },
|
||
{ sku: 'SR-BON-44', supplier: 'Shar Music', qty: 6, costPerUnit: '26.00', receivedDate: '2025-08-01', invoiceNumber: 'SHAR-2025-0801' },
|
||
]
|
||
|
||
for (const r of receiptDefs) {
|
||
const productId = productIds[r.sku]
|
||
const supplierId = r.supplier ? supplierIds[r.supplier] : null
|
||
if (!productId) continue
|
||
const [existing] = await sql`
|
||
SELECT id FROM stock_receipt WHERE product_id = ${productId} AND received_date = ${r.receivedDate} AND cost_per_unit = ${r.costPerUnit} AND qty = ${r.qty}`
|
||
if (existing) continue
|
||
const totalCost = (parseFloat(r.costPerUnit) * r.qty).toFixed(2)
|
||
await sql`
|
||
INSERT INTO stock_receipt (product_id, supplier_id, qty, cost_per_unit, total_cost, received_date, invoice_number, notes)
|
||
VALUES (${productId}, ${supplierId ?? null}, ${r.qty}, ${r.costPerUnit}, ${totalCost}, ${r.receivedDate}, ${r.invoiceNumber ?? null}, ${r.notes ?? null})`
|
||
console.log(` Receipt: ${r.sku} qty=${r.qty} @ $${r.costPerUnit}`)
|
||
}
|
||
|
||
console.log(' Inventory seed complete.')
|
||
}
|
||
|
||
|
||
async function seedLessons(sql: any) {
|
||
console.log('\nSeeding lessons data...')
|
||
|
||
// ── Clear old non-string lesson data ──────────────────────────────────────
|
||
// Delete in dependency order so FK constraints don't block
|
||
await sql`DELETE FROM lesson_plan_item`
|
||
await sql`DELETE FROM lesson_plan_section`
|
||
await sql`DELETE FROM member_lesson_plan`
|
||
await sql`DELETE FROM lesson_plan_template_item`
|
||
await sql`DELETE FROM lesson_plan_template_section`
|
||
await sql`DELETE FROM lesson_plan_template`
|
||
await sql`DELETE FROM lesson_session`
|
||
await sql`DELETE FROM enrollment`
|
||
await sql`DELETE FROM schedule_slot`
|
||
await sql`DELETE FROM lesson_type`
|
||
await sql`DELETE FROM instructor`
|
||
console.log(' Cleared old lesson 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: 'Violin and viola instructor with 12 years of teaching experience. Suzuki-certified through Book 6. Member of the City Symphony Orchestra.',
|
||
instruments: ['Violin', 'Viola'],
|
||
},
|
||
{
|
||
displayName: 'James Carter',
|
||
bio: 'Cellist and chamber musician with a masters in performance from Eastman School of Music. Teaches all levels from beginner through pre-college.',
|
||
instruments: ['Cello'],
|
||
},
|
||
{
|
||
displayName: 'Beth Romero',
|
||
bio: 'Double bassist with 10 years of orchestral and jazz experience. Former section principal. Warm, patient teaching style for all ages.',
|
||
instruments: ['Bass'],
|
||
},
|
||
]
|
||
|
||
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 Violin', instrument: 'Violin', durationMinutes: 30, lessonFormat: 'private', rateWeekly: '35.00', rateMonthly: '120.00', rateQuarterly: '330.00' },
|
||
{ name: '45-min Private Violin', instrument: 'Violin', durationMinutes: 45, lessonFormat: 'private', rateWeekly: '50.00', rateMonthly: '175.00', rateQuarterly: '480.00' },
|
||
{ name: '30-min Private Viola', instrument: 'Viola', durationMinutes: 30, lessonFormat: 'private', rateWeekly: '35.00', rateMonthly: '120.00', rateQuarterly: '330.00' },
|
||
{ name: '30-min Private Cello', instrument: 'Cello', durationMinutes: 30, lessonFormat: 'private', rateWeekly: '35.00', rateMonthly: '120.00', rateQuarterly: '330.00' },
|
||
{ name: '45-min Private Cello', instrument: 'Cello', durationMinutes: 45, lessonFormat: 'private', rateWeekly: '50.00', rateMonthly: '175.00', rateQuarterly: '480.00' },
|
||
{ name: '30-min Private Bass', instrument: 'Bass', durationMinutes: 30, lessonFormat: 'private', rateWeekly: '35.00', rateMonthly: '120.00', rateQuarterly: '330.00' },
|
||
{ name: '60-min String Ensemble', instrument: null, durationMinutes: 60, lessonFormat: 'group', rateWeekly: null, rateMonthly: '65.00', rateQuarterly: '180.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 — Violin & Viola
|
||
{ key: 'sarah_mon_1530_vln30', instructor: 'Sarah Mitchell', lessonType: '30-min Private Violin', dayOfWeek: 1, startTime: '15:30', room: 'Studio 1', maxStudents: 1, rateMonthly: null },
|
||
{ key: 'sarah_mon_1600_vln30', instructor: 'Sarah Mitchell', lessonType: '30-min Private Violin', dayOfWeek: 1, startTime: '16:00', room: 'Studio 1', maxStudents: 1, rateMonthly: null },
|
||
{ key: 'sarah_wed_1530_vln30', instructor: 'Sarah Mitchell', lessonType: '30-min Private Violin', dayOfWeek: 3, startTime: '15:30', room: 'Studio 1', maxStudents: 1, rateMonthly: null },
|
||
{ key: 'sarah_wed_1600_vln45', instructor: 'Sarah Mitchell', lessonType: '45-min Private Violin', dayOfWeek: 3, startTime: '16:00', room: 'Studio 1', maxStudents: 1, rateMonthly: '155.00' },
|
||
{ key: 'sarah_fri_1600_vla30', instructor: 'Sarah Mitchell', lessonType: '30-min Private Viola', dayOfWeek: 5, startTime: '16:00', room: 'Studio 1', maxStudents: 1, rateMonthly: null },
|
||
{ key: 'sarah_sat_1000_vln30', instructor: 'Sarah Mitchell', lessonType: '30-min Private Violin', dayOfWeek: 6, startTime: '10:00', room: 'Studio 1', maxStudents: 1, rateMonthly: null },
|
||
// James Carter — Cello
|
||
{ key: 'james_tue_1500_clo30', instructor: 'James Carter', lessonType: '30-min Private Cello', dayOfWeek: 2, startTime: '15:00', room: 'Studio 2', maxStudents: 1, rateMonthly: null },
|
||
{ key: 'james_tue_1600_clo30', instructor: 'James Carter', lessonType: '30-min Private Cello', dayOfWeek: 2, startTime: '16:00', room: 'Studio 2', maxStudents: 1, rateMonthly: null },
|
||
{ key: 'james_thu_1530_clo45', instructor: 'James Carter', lessonType: '45-min Private Cello', dayOfWeek: 4, startTime: '15:30', room: 'Studio 2', maxStudents: 1, rateMonthly: '155.00' },
|
||
{ key: 'james_sat_1000_clo30', instructor: 'James Carter', lessonType: '30-min Private Cello', dayOfWeek: 6, startTime: '10:00', room: 'Studio 2', maxStudents: 1, rateMonthly: null },
|
||
// Beth Romero — Bass
|
||
{ key: 'beth_wed_1700_bas30', instructor: 'Beth Romero', lessonType: '30-min Private Bass', dayOfWeek: 3, startTime: '17:00', room: 'Studio 3', maxStudents: 1, rateMonthly: null },
|
||
{ key: 'beth_fri_1500_bas30', instructor: 'Beth Romero', lessonType: '30-min Private Bass', dayOfWeek: 5, startTime: '15:00', room: 'Studio 3', maxStudents: 1, rateMonthly: null },
|
||
{ key: 'beth_sat_1100_bas30', instructor: 'Beth Romero', lessonType: '30-min Private Bass', dayOfWeek: 6, startTime: '11:00', room: 'Studio 3', maxStudents: 1, rateMonthly: null },
|
||
// String Ensemble — Saturdays (all three instructors rotate supervision; assign to Sarah)
|
||
{ key: 'ensemble_sat_1300', instructor: 'Sarah Mitchell', lessonType: '60-min String Ensemble', dayOfWeek: 6, startTime: '13:00', room: 'Main Hall', maxStudents: 12, 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 ────────────────────────────────────────────────────────────
|
||
const enrollmentDefs = [
|
||
{
|
||
member: 'Tommy Smith', slotKey: 'sarah_mon_1530_vln30', startDate: '2026-01-05',
|
||
rate: '120.00', billingInterval: 1, billingUnit: 'month',
|
||
notes: 'Tommy is 8 years old, complete beginner. Using a 1/2 size rental. Parents prefer Monday after school.',
|
||
},
|
||
{
|
||
member: 'Jake Johnson', slotKey: 'sarah_wed_1530_vln30', startDate: '2026-01-07',
|
||
rate: '120.00', billingInterval: 1, billingUnit: 'month',
|
||
notes: 'Jake (age 10) started Suzuki Book 1 at school. Moving to Book 2. Uses a 3/4 rental.',
|
||
},
|
||
{
|
||
member: 'Emily Chen', slotKey: 'sarah_fri_1600_vla30', startDate: '2026-01-09',
|
||
rate: '120.00', billingInterval: 1, billingUnit: 'month',
|
||
notes: 'Emily played violin through high school, now switching to viola. Strong reader, working on tone and shifting.',
|
||
},
|
||
{
|
||
member: 'Mike Thompson', slotKey: 'james_sat_1000_clo30', startDate: '2026-02-01',
|
||
rate: '120.00', billingInterval: 1, billingUnit: 'month',
|
||
notes: 'Mike is an adult beginner on cello. Very motivated. Renting a full-size cello.',
|
||
},
|
||
{
|
||
member: 'Carlos Garcia', slotKey: 'beth_fri_1500_bas30', startDate: '2026-01-09',
|
||
rate: '120.00', billingInterval: 1, billingUnit: 'month',
|
||
notes: 'Carlos played bass in high school orchestra. Returning student — solid fundamentals, working on thumb position.',
|
||
},
|
||
]
|
||
|
||
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 ────────────────────────────────────────────────────────
|
||
type SessionEntry = [string, string, string?]
|
||
interface SessionSet { enrollmentKey: string; slotKey: string; entries: SessionEntry[] }
|
||
|
||
const sessionSets: SessionSet[] = [
|
||
{
|
||
enrollmentKey: 'Tommy Smith',
|
||
slotKey: 'sarah_mon_1530_vln30',
|
||
entries: [
|
||
['2026-01-05', 'attended', 'First lesson. Introduced bow hold and open string bowing. Tommy is enthusiastic!'],
|
||
['2026-01-12', 'attended'],
|
||
['2026-01-19', 'attended', 'Bow arm improving. Introduced left hand finger placement — E and A strings.'],
|
||
['2026-01-26', 'missed'],
|
||
['2026-02-02', 'attended'],
|
||
['2026-02-09', 'attended', 'Playing Twinkle Variation A slowly with good intonation. Bow hold still needs reminders.'],
|
||
['2026-02-16', 'attended'],
|
||
['2026-02-23', 'attended'],
|
||
['2026-03-02', 'attended', 'Tommy played Twinkle Theme hands-down with confidence — great milestone!'],
|
||
['2026-03-09', 'attended'],
|
||
['2026-03-16', 'attended', 'Introduced Lightly Row. Working on smooth bow changes.'],
|
||
['2026-03-23', 'attended'],
|
||
['2026-03-30', 'scheduled'],
|
||
['2026-04-06', 'scheduled'],
|
||
['2026-04-13', 'scheduled'],
|
||
],
|
||
},
|
||
{
|
||
enrollmentKey: 'Jake Johnson',
|
||
slotKey: 'sarah_wed_1530_vln30',
|
||
entries: [
|
||
['2026-01-07', 'attended', 'Assessment: solid Book 1 foundation. Starting Book 2 — Chorus from Judas Maccabeus.'],
|
||
['2026-01-14', 'attended'],
|
||
['2026-01-21', 'attended', 'Shifting to 3rd position introduced. Intonation on D string needs focus.'],
|
||
['2026-01-28', 'attended'],
|
||
['2026-02-04', 'missed'],
|
||
['2026-02-11', 'attended', 'Third position becoming more reliable. Vibrato exercises started.'],
|
||
['2026-02-18', 'attended'],
|
||
['2026-02-25', 'attended'],
|
||
['2026-03-04', 'attended', 'Chorus from Judas Maccabeus up to performance tempo. Vibrato emerging nicely.'],
|
||
['2026-03-11', 'attended'],
|
||
['2026-03-18', 'attended', 'Jake performed the Chorus at slow recital tempo — very clean. Starting Hunters\' Chorus.'],
|
||
['2026-03-25', 'attended'],
|
||
['2026-04-01', 'scheduled'],
|
||
['2026-04-08', 'scheduled'],
|
||
['2026-04-15', 'scheduled'],
|
||
],
|
||
},
|
||
{
|
||
enrollmentKey: 'Emily Chen',
|
||
slotKey: 'sarah_fri_1600_vla30',
|
||
entries: [
|
||
['2026-01-09', 'attended', 'Assessment: violin background is strong. Adjusted chin rest and shoulder rest for viola. Alto clef introduced.'],
|
||
['2026-01-16', 'attended'],
|
||
['2026-01-23', 'attended', 'Alto clef reading improving quickly. Tone production on C string needs work — bowing angle adjustment.'],
|
||
['2026-01-30', 'attended'],
|
||
['2026-02-06', 'attended', 'Significantly richer tone on low strings. Started Telemann Viola Concerto in G major, 1st mvt.'],
|
||
['2026-02-13', 'attended'],
|
||
['2026-02-20', 'missed'],
|
||
['2026-02-27', 'attended'],
|
||
['2026-03-06', 'attended', 'Telemann 1st movement up to tempo. Emily\'s vibrato really coming through on viola — beautiful sound.'],
|
||
['2026-03-13', 'attended'],
|
||
['2026-03-20', 'attended', 'Starting 2nd movement. Discussing potential solo performance at spring recital.'],
|
||
['2026-03-27', 'attended'],
|
||
['2026-04-03', 'scheduled'],
|
||
['2026-04-10', 'scheduled'],
|
||
['2026-04-17', 'scheduled'],
|
||
],
|
||
},
|
||
{
|
||
enrollmentKey: 'Mike Thompson',
|
||
slotKey: 'james_sat_1000_clo30',
|
||
entries: [
|
||
['2026-02-01', 'attended', 'First lesson — proper sitting posture, bow hold, open string bowing. Mike is a quick learner.'],
|
||
['2026-02-08', 'attended'],
|
||
['2026-02-15', 'attended', 'Left hand frame introduced. Thumb position on neck. Playing first simple melodies.'],
|
||
['2026-02-22', 'attended'],
|
||
['2026-03-01', 'attended', 'Bass clef reading going well. Playing "Lightly Row" in D major. Intonation on A string improving.'],
|
||
['2026-03-08', 'attended'],
|
||
['2026-03-15', 'attended', 'Mike nailed the first cello piece from Mooney\'s "Position Pieces." Really motivated — practices daily.'],
|
||
['2026-03-22', 'attended'],
|
||
['2026-03-29', 'attended', 'Starting Suzuki Cello Book 1 formally. Tone on D and A strings sounding great.'],
|
||
['2026-04-05', 'scheduled'],
|
||
['2026-04-12', 'scheduled'],
|
||
['2026-04-19', 'scheduled'],
|
||
],
|
||
},
|
||
{
|
||
enrollmentKey: 'Carlos Garcia',
|
||
slotKey: 'beth_fri_1500_bas30',
|
||
entries: [
|
||
['2026-01-09', 'attended', 'Assessment — Carlos has solid orchestra foundation. Reviewing shifting, extensions, and bow distribution.'],
|
||
['2026-01-16', 'attended'],
|
||
['2026-01-23', 'attended', 'Thumb position work started. Carlos adapting well — good ear for intonation.'],
|
||
['2026-01-30', 'attended'],
|
||
['2026-02-06', 'attended', 'Working through Simandl Book 1 exercises in thumb position. Stamina building.'],
|
||
['2026-02-13', 'attended'],
|
||
['2026-02-20', 'attended', 'Thumb position solid up to 4th. Starting to work on Dragonetti Concerto opening.'],
|
||
['2026-02-27', 'attended'],
|
||
['2026-03-06', 'attended', 'Dragonetti opening phrase sounding musical. Bow arm and tone in upper positions much improved.'],
|
||
['2026-03-13', 'missed'],
|
||
['2026-03-20', 'attended'],
|
||
['2026-03-27', 'attended', 'Carlos brought in a jazz bassline he transcribed from Ray Brown — spent half the lesson on groove and feel. Excellent 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}
|
||
)`
|
||
}
|
||
console.log(` Sessions: ${ss.enrollmentKey} (${ss.entries.length})`)
|
||
}
|
||
|
||
// ── Lesson plan template — Beginner Violin (Suzuki Book 1) ─────────────────
|
||
const [existingTemplate] = await sql`SELECT id FROM lesson_plan_template WHERE name = 'Beginner Violin — Suzuki Book 1'`
|
||
if (!existingTemplate) {
|
||
const [tmpl] = await sql`
|
||
INSERT INTO lesson_plan_template (name, description, instrument, skill_level, is_active)
|
||
VALUES (
|
||
'Beginner Violin — Suzuki Book 1',
|
||
'Core skills for first-year violin students following the Suzuki method — from bow hold and posture through the full Book 1 repertoire.',
|
||
'Violin', 'beginner', true
|
||
) RETURNING id`
|
||
|
||
const sections = [
|
||
{
|
||
title: 'Posture & Setup',
|
||
sortOrder: 0,
|
||
items: [
|
||
{ title: 'Proper standing/sitting posture', sortOrder: 0 },
|
||
{ title: 'Violin hold and chin rest position', sortOrder: 1 },
|
||
{ title: 'Shoulder rest adjustment', sortOrder: 2 },
|
||
{ title: 'Bow hold (Suzuki style)', sortOrder: 3 },
|
||
{ title: 'Relaxed arm weight into string', sortOrder: 4 },
|
||
],
|
||
},
|
||
{
|
||
title: 'Bowing Technique',
|
||
sortOrder: 1,
|
||
items: [
|
||
{ title: 'Open string bowing — all four strings', sortOrder: 0 },
|
||
{ title: 'Straight bow path', sortOrder: 1 },
|
||
{ title: 'Smooth bow changes at frog and tip', sortOrder: 2 },
|
||
{ title: 'Detaché stroke', sortOrder: 3 },
|
||
{ title: 'Staccato introduction', sortOrder: 4 },
|
||
],
|
||
},
|
||
{
|
||
title: 'Left Hand & Intonation',
|
||
sortOrder: 2,
|
||
items: [
|
||
{ title: 'Finger placement — E and A strings', sortOrder: 0 },
|
||
{ title: 'Finger placement — D and G strings', sortOrder: 1 },
|
||
{ title: 'First finger frame', sortOrder: 2 },
|
||
{ title: 'Low 2nd finger (B♭, E♭)', sortOrder: 3 },
|
||
{ title: 'High 3rd finger', sortOrder: 4 },
|
||
],
|
||
},
|
||
{
|
||
title: 'Suzuki Book 1 Repertoire',
|
||
sortOrder: 3,
|
||
items: [
|
||
{ title: 'Twinkle Twinkle — Variation A', sortOrder: 0 },
|
||
{ title: 'Twinkle Twinkle — Theme', sortOrder: 1 },
|
||
{ title: 'Lightly Row', sortOrder: 2 },
|
||
{ title: 'Song of the Wind', sortOrder: 3 },
|
||
{ title: 'Go Tell Aunt Rhody', sortOrder: 4 },
|
||
{ title: 'O Come Little Children', sortOrder: 5 },
|
||
{ title: 'May Song', sortOrder: 6 },
|
||
{ title: 'Long, Long Ago', sortOrder: 7 },
|
||
{ title: 'Allegro (Suzuki)', sortOrder: 8 },
|
||
{ title: 'Perpetual Motion', sortOrder: 9 },
|
||
{ title: 'Allegretto (Suzuki)', sortOrder: 10 },
|
||
],
|
||
},
|
||
]
|
||
|
||
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 Violin — Suzuki Book 1')
|
||
}
|
||
|
||
// ── Lesson plan for Tommy Smith (violin beginner) ──────────────────────────
|
||
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}, 'Violin Suzuki Book 1 — Tommy Smith', 'Working through Suzuki Book 1.', true, '2026-01-05')
|
||
RETURNING id`
|
||
|
||
const [sec1] = await sql`INSERT INTO lesson_plan_section (lesson_plan_id, title, sort_order) VALUES (${plan.id}, 'Posture & Setup', 0) RETURNING id`
|
||
for (const it of [
|
||
{ title: 'Proper standing/sitting posture', status: 'mastered', sortOrder: 0 },
|
||
{ title: 'Violin hold and chin rest position', status: 'mastered', sortOrder: 1 },
|
||
{ title: 'Shoulder rest adjustment', status: 'mastered', sortOrder: 2 },
|
||
{ title: 'Bow hold (Suzuki style)', status: 'in_progress', sortOrder: 3 },
|
||
{ title: 'Relaxed arm weight into string', status: 'in_progress', sortOrder: 4 },
|
||
]) {
|
||
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})`
|
||
}
|
||
|
||
const [sec2] = await sql`INSERT INTO lesson_plan_section (lesson_plan_id, title, sort_order) VALUES (${plan.id}, 'Bowing Technique', 1) RETURNING id`
|
||
for (const it of [
|
||
{ title: 'Open string bowing — all four strings', status: 'mastered', sortOrder: 0 },
|
||
{ title: 'Straight bow path', status: 'in_progress', sortOrder: 1 },
|
||
{ title: 'Smooth bow changes at frog and tip', status: 'in_progress', sortOrder: 2 },
|
||
{ title: 'Detaché stroke', status: 'not_started', sortOrder: 3 },
|
||
{ title: 'Staccato introduction', status: 'not_started', sortOrder: 4 },
|
||
]) {
|
||
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})`
|
||
}
|
||
|
||
const [sec3] = await sql`INSERT INTO lesson_plan_section (lesson_plan_id, title, sort_order) VALUES (${plan.id}, 'Suzuki Book 1 Repertoire', 2) RETURNING id`
|
||
for (const it of [
|
||
{ title: 'Twinkle Twinkle — Variation A', status: 'mastered', sortOrder: 0 },
|
||
{ title: 'Twinkle Twinkle — Theme', status: 'in_progress', sortOrder: 1 },
|
||
{ title: 'Lightly Row', status: 'in_progress', sortOrder: 2 },
|
||
{ title: 'Song of the Wind', status: 'not_started', sortOrder: 3 },
|
||
{ title: 'Go Tell Aunt Rhody', status: 'not_started', sortOrder: 4 },
|
||
]) {
|
||
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 — Violin Suzuki Book 1')
|
||
}
|
||
}
|
||
|
||
console.log('Lessons seed complete.')
|
||
}
|
||
|
||
seed().catch((err) => {
|
||
console.error('Seed failed:', err)
|
||
process.exit(1)
|
||
})
|