/** * seed.example.ts — Contoh data Kodeva (Sistem Manajemen Kursus Coding) * --------------------------------------------------------------------- * Cara pakai: * 1) Salin ke `prisma/seed.ts` * 2) Tambahkan ke package.json → "prisma": { "seed": "tsx prisma/seed.ts" } * 3) Jalankan: pnpm db:push && pnpm db:seed * * Login contoh: * admin@kodeva.id / password123 (ADMIN) * budi@kodeva.id / password123 (TEACHER) * rina@kodeva.id / password123 (TEACHER) * * Catatan: cocok dengan skema di AGENTS.md (PostgreSQL). */ import { PrismaClient, AttendanceStatus, PaymentStatus, PlacementLevel } from "@prisma/client"; import bcrypt from "bcryptjs"; const prisma = new PrismaClient(); const BASE = new Date("2026-09-18T00:00:00.000Z"); const addDays = (n: number, from: Date = BASE) => new Date(from.getTime() + n * 86_400_000); const addWeeks = (n: number, from: Date = BASE) => addDays(n * 7, from); const PASS = bcrypt.hashSync("password123", 10); const avatar = (seed: string) => `https://i.pravatar.cc/240?u=${encodeURIComponent(seed)}`; const photo = (seed: string) => `https://picsum.photos/seed/${encodeURIComponent(seed)}/900/600`; async function main() { console.log("🌱 Seed Kodeva dimulai..."); // Bersihkan (urutan penting karena relasi) await prisma.followUpActivity.deleteMany(); await prisma.followUp.deleteMany(); await prisma.placementScore.deleteMany(); await prisma.placementTest.deleteMany(); await prisma.reportScore.deleteMany(); await prisma.reportCard.deleteMany(); await prisma.journalStudent.deleteMany(); await prisma.journalPhoto.deleteMany(); await prisma.journal.deleteMany(); await prisma.payment.deleteMany(); await prisma.attendance.deleteMany(); await prisma.session.deleteMany(); await prisma.enrollment.deleteMany(); await prisma.student.deleteMany(); await prisma.class.deleteMany(); await prisma.user.deleteMany(); await prisma.setting.deleteMany(); // ───────────────────────────────────────────────────────────── // 1. USERS (Admin + Teacher) // ───────────────────────────────────────────────────────────── const sari = await prisma.user.create({ data: { name: "Sari Wulandari", email: "admin@kodeva.id", passwordHash: PASS, role: "ADMIN", phone: "0811-2000-100", avatarUrl: avatar("sari"), }, }); const budi = await prisma.user.create({ data: { name: "Budi Santoso", email: "budi@kodeva.id", passwordHash: PASS, role: "TEACHER", phone: "0812-3000-200", avatarUrl: avatar("budi"), }, }); const rina = await prisma.user.create({ data: { name: "Rina Kartika", email: "rina@kodeva.id", passwordHash: PASS, role: "TEACHER", phone: "0813-4000-300", avatarUrl: avatar("rina"), }, }); const ahmad = await prisma.user.create({ data: { name: "Ahmad Fauzi", email: "ahmad@kodeva.id", passwordHash: PASS, role: "TEACHER", phone: "0814-5000-400", avatarUrl: avatar("ahmad"), }, }); // ───────────────────────────────────────────────────────────── // 2. KELAS (konfigurasi kelas) // pricePerMeeting = packagePrice / totalMeetings // ───────────────────────────────────────────────────────────── const classesData = [ { code: "KOD-SCR-01", name: "Scratch Junior", level: "BEGINNER" as PlacementLevel, teacherId: rina.id, totalMeetings: 8, packagePrice: 1_200_000, pricePerMeeting: 150_000, capacity: 10, schedule: "Senin & Rabu 16:00-17:30", color: "#2AA9FF", description: "Belajar logika pemrograman lewat game visual Scratch." }, { code: "KOD-PYT-01", name: "Python Dasar", level: "BEGINNER" as PlacementLevel, teacherId: budi.id, totalMeetings: 10, packagePrice: 2_000_000, pricePerMeeting: 200_000, capacity: 12, schedule: "Selasa & Kamis 17:00-18:30", color: "#7C5CFC", description: "Fondasi Python: variabel, logika, loop, dan fungsi." }, { code: "KOD-WEB-01", name: "Web Dev: HTML & CSS", level: "BEGINNER" as PlacementLevel, teacherId: ahmad.id, totalMeetings: 12, packagePrice: 2_400_000, pricePerMeeting: 200_000, capacity: 14, schedule: "Sabtu 09:00-12:00", color: "#12B886", description: "Membangun website pertamamu dari nol." }, { code: "KOD-JS-01", name: "JavaScript Intermediate", level: "INTERMEDIATE" as PlacementLevel, teacherId: budi.id, totalMeetings: 10, packagePrice: 2_500_000, pricePerMeeting: 250_000, capacity: 12, schedule: "Selasa & Kamis 18:45-20:15", color: "#FFB020", description: "DOM, event, async, dan mini project interaktif." }, { code: "KOD-ARD-01", name: "Arduino & Robotics", level: "INTERMEDIATE" as PlacementLevel, teacherId: rina.id, totalMeetings: 8, packagePrice: 1_800_000, pricePerMeeting: 225_000, capacity: 8, schedule: "Jumat 15:30-17:30", color: "#F65FA7", description: "Rakit dan program robot pertamamu dengan Arduino." }, ]; const klass = Object.fromEntries( await Promise.all( classesData.map(async (c) => [c.code, await prisma.class.create({ data: { ...c, startDate: addWeeks(-10), status: "ACTIVE" } })]), ), ) as Record>>; // ───────────────────────────────────────────────────────────── // 3. SISWA (10 orang: 4 calon, 4 student, 2 ex student) // Field: nama, telepon, email, foto, orang tua, sumber lead, status // ───────────────────────────────────────────────────────────── const studentsSeed = [ { key: "andi", fullName: "Andi Pratama", phone: "0812-1111-0001", email: "andi@example.com", parentName: "Budi Pratama", parentPhone: "0812-1111-9001", leadSource: "REFERRAL" as const, status: "STUDENT" as const, joinedAt: addWeeks(-10) }, { key: "citra", fullName: "Citra Lestari", phone: "0812-1111-0002", email: "citra@example.com", parentName: "Sari Lestari", parentPhone: "0812-1111-9002", leadSource: "WHATSAPP" as const, status: "CALON_STUDENT" as const, joinedAt: null }, { key: "dimas", fullName: "Dimas Anggara", phone: "0812-1111-0003", email: "dimas@example.com", parentName: "Hendra Anggara", parentPhone: "0812-1111-9003", leadSource: "BANNER" as const, status: "STUDENT" as const, joinedAt: addWeeks(-9) }, { key: "elsa", fullName: "Elsa Maharani", phone: "0812-1111-0004", email: "elsa@example.com", parentName: "Rina Maharani", parentPhone: "0812-1111-9004", leadSource: "SOCIAL_MEDIA" as const, status: "CALON_STUDENT" as const, joinedAt: null }, { key: "fajar", fullName: "Fajar Nugroho", phone: "0812-1111-0005", email: "fajar@example.com", parentName: "Agus Nugroho", parentPhone: "0812-1111-9005", leadSource: "WHATSAPP" as const, status: "STUDENT" as const, joinedAt: addWeeks(-8) }, { key: "gita", fullName: "Gita Permata", phone: "0812-1111-0006", email: "gita@example.com", parentName: "Dwi Permata", parentPhone: "0812-1111-9006", leadSource: "REFERRAL" as const, status: "EX_STUDENT" as const, joinedAt: addWeeks(-40) }, { key: "hadi", fullName: "Hadi Susanto", phone: "0812-1111-0007", email: "hadi@example.com", parentName: "Tono Susanto", parentPhone: "0812-1111-9007", leadSource: "SOCIAL_MEDIA" as const, status: "STUDENT" as const, joinedAt: addWeeks(-6) }, { key: "intan", fullName: "Intan Puspita", phone: "0812-1111-0008", email: "intan@example.com", parentName: "Maya Puspita", parentPhone: "0812-1111-9008", leadSource: "BANNER" as const, status: "CALON_STUDENT" as const, joinedAt: null }, { key: "joko", fullName: "Joko Wijaya", phone: "0812-1111-0009", email: "joko@example.com", parentName: "Slamet Wijaya", parentPhone: "0812-1111-9009", leadSource: "WHATSAPP" as const, status: "STUDENT" as const, joinedAt: addWeeks(-4) }, { key: "kirana", fullName: "Kirana Dewi", phone: "0812-1111-0010", email: "kirana@example.com", parentName: "Ratna Dewi", parentPhone: "0812-1111-9010", leadSource: "REFERRAL" as const, status: "EX_STUDENT" as const, joinedAt: addWeeks(-36) }, ]; const student = {} as Record>>; for (const s of studentsSeed) { student[s.key] = await prisma.student.create({ data: { fullName: s.fullName, phone: s.phone, email: s.email, photoUrl: avatar(s.key), parentName: s.parentName, parentPhone: s.parentPhone, leadSource: s.leadSource, status: s.status, joinedAt: s.joinedAt, birthDate: new Date("2012-05-10T00:00:00.000Z"), gender: "MALE", address: "Jl. Merdeka No. 10, Bandung", }, }); } // ───────────────────────────────────────────────────────────── // 4. ENROLLMENT ( siswa ↔ kelas ) // ───────────────────────────────────────────────────────────── const enrollments = [ { key: "andi", code: "KOD-PYT-01", status: "ACTIVE" as const }, { key: "dimas", code: "KOD-SCR-01", status: "ACTIVE" as const }, { key: "fajar", code: "KOD-WEB-01", status: "ACTIVE" as const }, { key: "gita", code: "KOD-JS-01", status: "COMPLETED" as const }, { key: "hadi", code: "KOD-ARD-01", status: "ACTIVE" as const }, { key: "joko", code: "KOD-PYT-01", status: "ACTIVE" as const }, { key: "kirana", code: "KOD-SCR-01", status: "COMPLETED" as const }, ]; for (const e of enrollments) { await prisma.enrollment.create({ data: { studentId: student[e.key].id, classId: klass[e.code].id, status: e.status, joinedAt: student[e.key].joinedAt ?? BASE, leftAt: e.status === "COMPLETED" ? addWeeks(-2) : null }, }); } // ───────────────────────────────────────────────────────────── // 5. SESSION (pertemuan per kelas) — dasar prorata // ───────────────────────────────────────────────────────────── const session: Record = {}; for (const c of Object.values(klass)) { session[c.code] = []; for (let i = 1; i <= c.totalMeetings; i++) { const s = await prisma.session.create({ data: { classId: c.id, meetingNumber: i, date: addWeeks(-10 + i - 1), topic: `Pertemuan ${i}: ${c.name}`, teacherId: c.teacherId }, }); session[c.code].push({ id: s.id, meetingNumber: i }); } } // ───────────────────────────────────────────────────────────── // 6. KEHADIRAN (PRESENT/LATE/EXCUSED/SICK/ABSENT) // Terpakai = jumlah PRESENT + LATE → dasar hitung sisa kuota // ───────────────────────────────────────────────────────────── const plan: Record = { andi: { code: "KOD-PYT-01", statuses: ["PRESENT", "PRESENT", "LATE", "PRESENT", "PRESENT", "PRESENT"] }, // 6 terpakai dimas: { code: "KOD-SCR-01", statuses: ["PRESENT", "PRESENT", "ABSENT", "PRESENT"] }, // 3 hadir, 1 alpa fajar: { code: "KOD-WEB-01", statuses: ["PRESENT", "LATE", "PRESENT", "PRESENT", "EXCUSED", "PRESENT"] }, // 5 hadir gita: { code: "KOD-JS-01", statuses: ["PRESENT", "PRESENT", "PRESENT", "PRESENT", "PRESENT", "PRESENT", "PRESENT", "PRESENT", "PRESENT", "PRESENT"] }, hadi: { code: "KOD-ARD-01", statuses: ["PRESENT", "PRESENT", "PRESENT", "PRESENT"] }, joko: { code: "KOD-PYT-01", statuses: ["PRESENT", "SICK", "PRESENT", "PRESENT", "PRESENT"] }, kirana: { code: "KOD-SCR-01", statuses: ["PRESENT", "PRESENT", "LATE", "PRESENT", "PRESENT", "PRESENT"] }, }; let attendanceCount = 0; for (const [key, p] of Object.entries(plan)) { const sessions = session[p.code]; for (let i = 0; i < p.statuses.length; i++) { await prisma.attendance.create({ data: { sessionId: sessions[i].id, studentId: student[key].id, status: p.statuses[i], recordedById: klass[p.code].teacherId, note: p.statuses[i] === "SICK" ? "Demam" : undefined }, }); attendanceCount++; } } // ───────────────────────────────────────────────────────────── // 7. PEMBAYARAN PRORATA // amount = meetingsPaid × pricePerMeeting // Contoh: Andi 10 × 200.000 = 2.000.000 // Hadir 6 → terpakai 1.200.000 → sisa 4 pertemuan = 800.000 // ───────────────────────────────────────────────────────────── const paymentsSeed = [ { key: "andi", code: "KOD-PYT-01", meetingsPaid: 10, method: "TRANSFER" as const, status: "PAID" as const, paidAt: addWeeks(-10) }, { key: "dimas", code: "KOD-SCR-01", meetingsPaid: 8, method: "CASH" as const, status: "PAID" as const, paidAt: addWeeks(-9) }, { key: "fajar", code: "KOD-WEB-01", meetingsPaid: 6, method: "QRIS" as const, status: "PAID" as const, paidAt: addWeeks(-8), note: "Bayar 6 pertemuan dulu" }, { key: "gita", code: "KOD-JS-01", meetingsPaid: 10, method: "TRANSFER" as const, status: "PAID" as const, paidAt: addWeeks(-40) }, { key: "hadi", code: "KOD-ARD-01", meetingsPaid: 8, method: "TRANSFER" as const, status: "PAID" as const, paidAt: addWeeks(-6) }, { key: "joko", code: "KOD-PYT-01", meetingsPaid: 5, method: "QRIS" as const, status: "PARTIAL" as const, paidAt: addWeeks(-4), note: "Cicilan 1 dari 2" }, { key: "kirana", code: "KOD-SCR-01", meetingsPaid: 8, method: "CASH" as const, status: "PAID" as const, paidAt: addWeeks(-36) }, ]; let paySeq = 1; for (const p of paymentsSeed) { const c = klass[p.code]; const ppm = c.pricePerMeeting ?? Math.round(c.packagePrice / c.totalMeetings); await prisma.payment.create({ data: { invoiceNumber: `INV-2026-${String(paySeq++).padStart(4, "0")}`, studentId: student[p.key].id, classId: c.id, meetingsPaid: p.meetingsPaid, pricePerMeeting: ppm, amount: p.meetingsPaid * ppm, method: p.method, status: p.status as PaymentStatus, paidAt: p.paidAt, note: p.note, createdById: sari.id, }, }); } // ───────────────────────────────────────────────────────────── // 8. JURNAL AKTIVITAS + FOTO // ───────────────────────────────────────────────────────────── const journalsSeed = [ { code: "KOD-SCR-01", title: "Membuat Game Ular dengan Scratch", description: "Anak-anak belajar event, loop, dan variabel skor sambil membuat game ular versi mereka sendiri.", tags: ["game", "scratch", "logika"], date: addWeeks(-2), students: ["dimas", "kirana"], photos: ["ular-1", "ular-2", "ular-3"] }, { code: "KOD-PYT-01", title: "Dasar Variabel & Tipe Data Python", description: "Sesi seru bereksperimen dengan string, integer, dan input dari pengguna.", tags: ["python", "variable", "basic"], date: addWeeks(-3), students: ["andi", "joko"], photos: ["python-1", "python-2"] }, { code: "KOD-WEB-01", title: "Landing Page Pertama", description: "Setiap siswa mempublikasikan halaman profil pribadinya menggunakan HTML & CSS.", tags: ["html", "css", "project"], date: addWeeks(-1), students: ["fajar"], photos: ["web-1", "web-2", "web-3", "web-4"] }, { code: "KOD-ARD-01", title: "Sensor Cahaya dengan Arduino", description: "Merakit lampu otomatis yang menyala saat ruangan gelap.", tags: ["arduino", "sensor", "robotik"], date: addDays(-5), students: ["hadi"], photos: ["arduino-1", "arduino-2"] }, ]; for (const j of journalsSeed) { const c = klass[j.code]; await prisma.journal.create({ data: { classId: c.id, sessionId: session[j.code][0].id, title: j.title, description: j.description, activityDate: j.date, tags: j.tags, authorId: c.teacherId!, photos: { create: j.photos.map((s, i) => ({ url: photo(s), caption: `Dokumentasi ${i + 1}`, sortOrder: i })) }, students: { create: j.students.map((k) => ({ studentId: student[k].id })) }, }, }); } // ───────────────────────────────────────────────────────────── // 9. RAPORT // ───────────────────────────────────────────────────────────── const reportsSeed = [ { key: "andi", code: "KOD-PYT-01", period: "2026-Term1", scores: [["Logika", 88], ["Computational Thinking", 85], ["Problem Solving", 82], ["Kreativitas", 90], ["Kolaborasi", 86]], notes: "Andi sangat aktif bertanya dan cepat memahami konsep loop." }, { key: "dimas", code: "KOD-SCR-01", period: "2026-Term1", scores: [["Logika", 80], ["Computational Thinking", 78], ["Problem Solving", 75], ["Kreativitas", 92], ["Kolaborasi", 88]], notes: "Kreativitas Dimas luar biasa, perlu latihan ketelitian." }, { key: "fajar", code: "KOD-WEB-01", period: "2026-Term1", scores: [["Logika", 84], ["Computational Thinking", 82], ["Problem Solving", 86], ["Kreativitas", 85], ["Kolaborasi", 80]], notes: "Progres pesat di CSS layout." }, ] as const; for (const r of reportsSeed) { const avg = Math.round(r.scores.reduce((a, [, v]) => a + v, 0) / r.scores.length); const grade = avg >= 90 ? "A" : avg >= 80 ? "B" : avg >= 70 ? "C" : "D"; await prisma.reportCard.create({ data: { studentId: student[r.key].id, classId: klass[r.code].id, period: r.period, teacherId: klass[r.code].teacherId!, finalScore: avg, grade, teacherNotes: r.notes, attendanceSummary: "Hadir 6 dari 6 pertemuan", publishedAt: addDays(-3), scores: { create: r.scores.map(([aspect, score]) => ({ aspect, score })) }, }, }); } // ───────────────────────────────────────────────────────────── // 10. PLACEMENT TEST (hasil tes penempatan → tampil di detail siswa) // ───────────────────────────────────────────────────────────── const placementsSeed = [ { key: "elsa", level: "INTERMEDIATE" as PlacementLevel, rec: "KOD-JS-01", scores: [["Logika", 82], ["Dasar Coding", 76], ["Problem Solving", 80], ["Kreativitas", 74]] }, { key: "intan", level: "BEGINNER" as PlacementLevel, rec: "KOD-SCR-01", scores: [["Logika", 62], ["Dasar Coding", 48], ["Problem Solving", 55], ["Kreativitas", 70]] }, { key: "citra", level: "BEGINNER" as PlacementLevel, rec: "KOD-PYT-01", scores: [["Logika", 50], ["Dasar Coding", 40], ["Problem Solving", 45], ["Kreativitas", 60]] }, { key: "andi", level: "INTERMEDIATE" as PlacementLevel, rec: "KOD-JS-01", scores: [["Logika", 80], ["Dasar Coding", 78], ["Problem Solving", 72], ["Kreativitas", 68]] }, ]; for (const p of placementsSeed) { const total = Math.round(p.scores.reduce((a, [, v]) => a + v, 0) / p.scores.length); await prisma.placementTest.create({ data: { studentId: student[p.key].id, testDate: addDays(-12), levelResult: p.level, recommendedClassId: klass[p.rec].id, totalScore: total, examinerId: rina.id, notes: `Direkomendasikan masuk kelas ${klass[p.rec].name}.`, scores: { create: p.scores.map(([category, score]) => ({ category, score })) }, }, }); } // ───────────────────────────────────────────────────────────── // 11. FOLLOW UP (berapa yang harus di-follow up) // due = nextFollowUpAt <= hari ini && status ∉ (ENROLLED, LOST) // ───────────────────────────────────────────────────────────── const followUpsSeed = [ { key: "citra", status: "CONTACTED" as const, picId: sari.id, next: addDays(-1), last: addDays(-3), channel: "WHATSAPP" as const, activities: [["wa", "Kirim brosur Python Dasar, ortu minta dihubungi lagi."]] }, { key: "elsa", status: "NEW" as const, picId: budi.id, next: addDays(0), last: null, channel: "SOCIAL_MEDIA" as const, activities: [["wa", "Lead dari Instagram, tanya jadwal kelas."]] }, { key: "intan", status: "TRIAL" as const, picId: rina.id, next: addDays(2), last: addDays(-1), channel: "BANNER" as const, activities: [["call", "Sudah trial Scratch, tinggal konfirmasi pembayaran."]] }, ]; for (const f of followUpsSeed) { await prisma.followUp.create({ data: { studentId: student[f.key].id, status: f.status, picId: f.picId, nextFollowUpAt: f.next, lastContactedAt: f.last, channel: f.channel, activities: { create: f.activities.map(([type, note]) => ({ type, note, authorId: f.picId })) }, }, }); } // ───────────────────────────────────────────────────────────── // 12. SETTINGS // ───────────────────────────────────────────────────────────── await prisma.setting.createMany({ data: [ { key: "academy", value: { name: "Kodeva Academy", address: "Jl. Coding Raya No. 1, Bandung", phone: "0821-1000-2000", email: "halo@kodeva.id" } }, { key: "leadSources", value: [ { value: "WHATSAPP", label: "WhatsApp", color: "#22C55E", active: true }, { value: "REFERRAL", label: "Referral", color: "#7C5CFC", active: true }, { value: "BANNER", label: "Banner", color: "#FFB020", active: true }, { value: "SOCIAL_MEDIA", label: "Social Media", color: "#2AA9FF", active: true }, ] }, { key: "currency", value: { code: "IDR", locale: "id-ID", timezone: "Asia/Jakarta" } }, ], }); // ───────────────────────────────────────────────────────────── // Ringkasan (untuk verifikasi dashboard) // ───────────────────────────────────────────────────────────── const [activeStudents, exStudents, followUpDue] = await Promise.all([ prisma.student.count({ where: { status: "STUDENT" } }), prisma.student.count({ where: { status: "EX_STUDENT" } }), prisma.followUp.count({ where: { status: { notIn: ["ENROLLED", "LOST"] }, nextFollowUpAt: { lte: addDays(1) } } }), ]); console.log("✅ Seed selesai."); console.log(` 👥 Siswa aktif : ${activeStudents}`); console.log(` 🎓 Ex student : ${exStudents}`); console.log(` 🔔 Perlu follow up: ${followUpDue}`); console.log(` 📚 Kelas : ${Object.keys(klass).length}`); console.log(` 🗓️ Kehadiran : ${attendanceCount} record`); console.log("\n Login: admin@kodeva.id / password123"); } main() .catch((e) => { console.error("❌ Seed gagal:", e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });