From bcb9aabc35dfcdbe5f91977cab02595f040135bb Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 4 Nov 2025 11:05:39 +0700 Subject: [PATCH] improvement Quiz Interactive --- .../lms.uat-design-testing-20250127.md | 355 ++++++++++ src/app/exam-session/page.tsx | 619 +++++++++++++++--- src/app/exam-summary/page.tsx | 242 +++++++ src/app/payroll-demo/page.tsx | 42 +- src/components/admin/AdminTopBar.tsx | 10 +- .../admin/EnhancedQuestionBuilder.tsx | 25 - src/components/admin/QuizPreview.tsx | 41 -- .../quiz/InteractiveImageHotspot.tsx | 358 ++++++++++ .../quiz/InteractiveQuizRenderer.tsx | 103 ++- src/components/quiz/MediaGallery.tsx | 409 ++++++++++++ src/components/quiz/MiniSurveyReflection.tsx | 44 +- src/components/quiz/PuzzleQuestion.tsx | 522 ++++++++++++--- src/components/quiz/ScenarioIntroGuide.tsx | 75 +-- src/components/quiz/VideoScenario.tsx | 326 +++++++++ src/components/ui/ErrorBoundary.tsx | 101 +++ src/contexts/AuthContext.tsx | 37 +- src/contexts/ExamContext.tsx | 2 +- src/core/di/DIContainer.ts | 71 +- .../components/PayrollManagement.tsx | 6 +- .../components/RewardDashboard.tsx | 19 +- .../components/WalletBalance.tsx | 6 +- src/features/payroll-reward-system/index.ts | 33 +- .../services/PayrollCalculator.ts | 8 +- src/layouts/TopBar.tsx | 10 +- src/types/index.ts | 81 ++- 25 files changed, 3117 insertions(+), 428 deletions(-) create mode 100644 docs/qa/assessments/lms.uat-design-testing-20250127.md create mode 100644 src/app/exam-summary/page.tsx create mode 100644 src/components/quiz/InteractiveImageHotspot.tsx create mode 100644 src/components/quiz/MediaGallery.tsx create mode 100644 src/components/quiz/VideoScenario.tsx create mode 100644 src/components/ui/ErrorBoundary.tsx diff --git a/docs/qa/assessments/lms.uat-design-testing-20250127.md b/docs/qa/assessments/lms.uat-design-testing-20250127.md new file mode 100644 index 0000000..6a3c964 --- /dev/null +++ b/docs/qa/assessments/lms.uat-design-testing-20250127.md @@ -0,0 +1,355 @@ +# ๐Ÿงช Design Review: UAT Testing Strategy untuk LMS +**Epic**: LMS System +**Story**: UAT Design Testing +**Topic**: User Acceptance Testing +**Date**: 2025-01-27 +**Test Architect**: Quinn - BMad Method + +--- + +## ๐Ÿ“‹ **EXECUTIVE SUMMARY** + +Dokumen ini menyajikan design review khusus untuk User Acceptance Testing (UAT) pada sistem Learning Management System (LMS). Fokus utama adalah memastikan sistem siap untuk pengujian oleh end-user dengan skenario yang komprehensif dan realistis. + +--- + +## ๐ŸŽฏ **UAT SCOPE & OBJECTIVES** + +### **Primary Objectives:** +- โœ… Validasi user experience untuk pengguna Indonesia +- โœ… Verifikasi business requirements telah terpenuhi +- โœ… Konfirmasi sistem dapat digunakan dalam kondisi real-world +- โœ… Identifikasi gap antara ekspektasi user dan implementasi + +### **UAT Coverage Areas:** +1. **Authentication & Authorization** (Login/Register/Role Management) +2. **Course Management** (Browse, Enroll, Progress Tracking) +3. **Assessment System** (Quiz, Exam, Interactive Elements) +4. **Certificate Management** (Generation, Verification, Download) +5. **Dashboard & Analytics** (Student/Admin/Instructor Views) +6. **Payroll Reward System** (Learning Hours, Performance Bonus) +7. **AI Assistant** (Learning Support, Recommendations) +8. **Mobile Responsiveness** (Cross-device compatibility) + +--- + +## ๐Ÿ‘ฅ **USER PERSONAS FOR UAT** + +### **1. Peserta/Student (Primary User)** +- **Profile**: Karyawan perusahaan, usia 25-45 tahun +- **Tech Literacy**: Menengah (familiar dengan smartphone, basic computer) +- **Goals**: Menyelesaikan pelatihan, mendapat sertifikat, earning rewards +- **Pain Points**: Waktu terbatas, perlu interface yang intuitif + +### **2. Instruktur/Trainer** +- **Profile**: Professional trainer, usia 30-50 tahun +- **Tech Literacy**: Menengah-Tinggi +- **Goals**: Mengelola kursus, monitor progress siswa, evaluasi hasil +- **Pain Points**: Butuh tools yang efisien untuk manajemen konten + +### **3. Admin/HR** +- **Profile**: HR Manager atau Training Coordinator +- **Tech Literacy**: Menengah-Tinggi +- **Goals**: Oversight training program, generate reports, manage users +- **Pain Points**: Butuh dashboard yang comprehensive dan mudah dipahami + +### **4. Super Admin/System Administrator** +- **Profile**: IT Professional +- **Tech Literacy**: Tinggi +- **Goals**: System maintenance, user management, security oversight +- **Pain Points**: Butuh control panel yang powerful namun user-friendly + +--- + +## ๐Ÿงช **UAT TEST SCENARIOS** + +### **Scenario 1: Onboarding Journey (Critical Path)** +``` +User Story: "Sebagai karyawan baru, saya ingin dapat mendaftar dan mulai belajar dengan mudah" + +Test Steps: +1. Akses halaman registrasi +2. Isi form registrasi dengan data valid +3. Verifikasi email (jika ada) +4. Login pertama kali +5. Complete profile setup +6. Browse available courses +7. Enroll ke course pertama +8. Start learning module pertama + +Expected Results: +- Proses registrasi < 3 menit +- Interface dalam Bahasa Indonesia yang jelas +- Guidance yang membantu untuk first-time user +- Smooth transition antar step +``` + +### **Scenario 2: Learning Experience (Core Functionality)** +``` +User Story: "Sebagai peserta, saya ingin pengalaman belajar yang engaging dan mudah diikuti" + +Test Steps: +1. Login ke dashboard +2. Continue course yang sedang berjalan +3. Watch video pembelajaran +4. Complete interactive quiz +5. Submit assignment (jika ada) +6. Check progress tracking +7. Earn learning hours untuk payroll system +8. Receive notifications/feedback + +Expected Results: +- Video player berfungsi smooth di berbagai device +- Quiz interface intuitif dan responsive +- Progress tracking akurat dan real-time +- Payroll hours tercatat dengan benar +``` + +### **Scenario 3: Assessment & Certification (High Stakes)** +``` +User Story: "Sebagai peserta, saya ingin dapat mengikuti ujian dan mendapat sertifikat dengan confidence" + +Test Steps: +1. Access exam session +2. Review exam instructions +3. Complete exam dengan timer +4. Submit exam answers +5. Receive immediate feedback (jika applicable) +6. Check exam results +7. Download digital certificate +8. Verify certificate authenticity + +Expected Results: +- Exam interface stable dan tidak crash +- Timer berfungsi akurat +- Auto-save answers berfungsi +- Certificate generation berhasil +- QR code verification works +``` + +### **Scenario 4: Admin Management (Power User)** +``` +User Story: "Sebagai admin, saya ingin dapat mengelola sistem dengan efisien" + +Test Steps: +1. Login ke admin panel +2. Create new course +3. Upload learning materials +4. Set up quiz/exam +5. Manage user enrollments +6. Generate analytics reports +7. Configure payroll reward settings +8. Monitor system performance + +Expected Results: +- Admin interface responsive dan intuitive +- Bulk operations berfungsi dengan baik +- Reports generated accurately +- System performance metrics visible +``` + +### **Scenario 5: Mobile Experience (Cross-Platform)** +``` +User Story: "Sebagai mobile user, saya ingin dapat belajar dengan nyaman di smartphone" + +Test Steps: +1. Access LMS via mobile browser +2. Login dan navigate dashboard +3. Watch video di mobile +4. Complete quiz di mobile +5. Check progress dan notifications +6. Download certificate di mobile +7. Test offline capabilities (jika ada) + +Expected Results: +- Responsive design works seamlessly +- Touch interactions smooth +- Video playback optimized for mobile +- Text readable tanpa zoom +- Fast loading times +``` + +--- + +## ๐Ÿ” **UAT TEST DESIGN FRAMEWORK** + +### **Testing Approach:** +- **Exploratory Testing**: User bebas explore sistem secara natural +- **Scenario-Based Testing**: Guided scenarios berdasarkan real use cases +- **Usability Testing**: Focus pada ease of use dan user satisfaction +- **Acceptance Criteria Validation**: Verify business requirements terpenuhi + +### **Test Environment:** +- **Staging Environment**: Mirror production dengan test data +- **Multiple Devices**: Desktop, tablet, smartphone (Android/iOS) +- **Multiple Browsers**: Chrome, Firefox, Safari, Edge +- **Network Conditions**: Fast WiFi, slow 3G, intermittent connection + +### **Success Metrics:** +- **Task Completion Rate**: > 90% untuk critical paths +- **Time to Complete**: Sesuai dengan target yang ditetapkan +- **Error Rate**: < 5% untuk user-induced errors +- **User Satisfaction**: Rating > 4/5 pada post-test survey +- **Accessibility**: WCAG 2.1 AA compliance + +--- + +## ๐Ÿ“Š **UAT EXECUTION PLAN** + +### **Phase 1: Internal UAT (Week 1)** +- **Participants**: Internal team members (5-8 orang) +- **Focus**: Basic functionality dan critical bugs +- **Duration**: 3 hari +- **Deliverable**: Bug report dan initial feedback + +### **Phase 2: Stakeholder UAT (Week 2)** +- **Participants**: Key stakeholders dan power users (8-12 orang) +- **Focus**: Business requirements validation +- **Duration**: 5 hari +- **Deliverable**: Acceptance criteria validation report + +### **Phase 3: End-User UAT (Week 3)** +- **Participants**: Representative end users (15-20 orang) +- **Focus**: Real-world usage scenarios +- **Duration**: 1 minggu +- **Deliverable**: User experience report dan recommendations + +### **Phase 4: Performance UAT (Week 4)** +- **Participants**: Mixed user groups dengan concurrent access +- **Focus**: System performance under load +- **Duration**: 2 hari +- **Deliverable**: Performance validation report + +--- + +## ๐ŸŽฏ **UAT SUCCESS CRITERIA** + +### **Functional Criteria:** +- โœ… Semua critical user journeys dapat diselesaikan tanpa blocker +- โœ… Authentication dan authorization berfungsi sesuai role +- โœ… Course management dan learning experience smooth +- โœ… Assessment system reliable dan secure +- โœ… Certificate generation dan verification works +- โœ… Payroll reward system calculate accurately +- โœ… Admin functions accessible dan efficient + +### **Non-Functional Criteria:** +- โœ… Page load time < 3 detik untuk 95% requests +- โœ… System available 99.5% selama UAT period +- โœ… Mobile responsiveness works pada semua target devices +- โœ… Accessibility standards met (WCAG 2.1 AA) +- โœ… Security vulnerabilities addressed +- โœ… Data integrity maintained throughout testing + +### **User Experience Criteria:** +- โœ… Interface dalam Bahasa Indonesia yang natural +- โœ… Navigation intuitive untuk Indonesian users +- โœ… Error messages helpful dan actionable +- โœ… Feedback mechanisms responsive +- โœ… Help documentation accessible dan comprehensive + +--- + +## ๐Ÿšจ **RISK ASSESSMENT** + +### **High Risk Areas:** +1. **Exam System Stability**: Critical untuk certification process +2. **Mobile Performance**: Majority users akan akses via mobile +3. **Payroll Calculation**: Financial implications jika salah +4. **Certificate Verification**: Legal compliance requirements +5. **User Data Security**: Privacy dan GDPR compliance + +### **Mitigation Strategies:** +- **Comprehensive Test Data**: Cover edge cases dan boundary conditions +- **Rollback Plan**: Ready jika critical issues ditemukan +- **Performance Monitoring**: Real-time monitoring selama UAT +- **Security Review**: Penetration testing sebelum UAT +- **User Training**: Provide clear documentation dan support + +--- + +## ๐Ÿ“‹ **UAT DELIVERABLES** + +### **Test Documentation:** +- [ ] UAT Test Plan (This document) +- [ ] Test Scenarios dan Test Cases +- [ ] User Personas dan Journey Maps +- [ ] Test Data Requirements +- [ ] Environment Setup Guide + +### **Execution Artifacts:** +- [ ] Daily Test Execution Reports +- [ ] Bug Reports dengan severity classification +- [ ] User Feedback Compilation +- [ ] Performance Test Results +- [ ] Accessibility Audit Report + +### **Final Reports:** +- [ ] UAT Summary Report +- [ ] Business Requirements Traceability Matrix +- [ ] User Acceptance Sign-off Document +- [ ] Go-Live Readiness Assessment +- [ ] Post-UAT Recommendations + +--- + +## ๐ŸŽฏ **RECOMMENDATIONS** + +### **Pre-UAT Preparation:** +1. **Test Data Setup**: Prepare realistic test data yang represent production scenarios +2. **User Training**: Brief UAT participants tentang objectives dan expectations +3. **Environment Validation**: Ensure staging environment stable dan representative +4. **Communication Plan**: Clear channels untuk reporting issues dan feedback + +### **During UAT Execution:** +1. **Daily Standups**: Track progress dan address blockers immediately +2. **Real-time Monitoring**: Monitor system performance dan user behavior +3. **Feedback Collection**: Multiple channels untuk user input (forms, interviews, observations) +4. **Issue Triage**: Rapid classification dan resolution untuk critical issues + +### **Post-UAT Actions:** +1. **Lessons Learned**: Document insights untuk future UAT cycles +2. **User Training Materials**: Update based pada UAT feedback +3. **Performance Optimization**: Address performance issues identified +4. **Go-Live Planning**: Finalize deployment strategy based pada UAT results + +--- + +## ๐Ÿ **GATE ASSESSMENT** + +### **Current Status**: READY FOR UAT EXECUTION + +### **Gate Decision**: **PASS** โœ… + +**Justification:** +- โœ… Comprehensive UAT strategy telah didefinisikan +- โœ… Test scenarios cover semua critical user journeys +- โœ… Success criteria jelas dan measurable +- โœ… Risk mitigation strategies in place +- โœ… Execution plan realistic dan achievable +- โœ… Deliverables clearly defined + +### **Conditions for Success:** +1. Staging environment harus stable sebelum UAT dimulai +2. Test data harus representative dan comprehensive +3. UAT participants harus properly briefed +4. Issue resolution process harus efficient +5. Performance monitoring harus real-time + +### **Next Steps:** +1. Finalize UAT participant selection +2. Setup staging environment dengan production-like data +3. Conduct UAT kickoff meeting +4. Begin Phase 1: Internal UAT execution +5. Monitor progress dan adjust plan as needed + +--- + +**Document Prepared By**: Quinn - Test Architect, BMad Method +**Review Date**: 2025-01-27 +**Next Review**: Post-UAT Completion +**Approval Status**: Ready for Stakeholder Review + +--- + +*"Quality is not an act, it is a habit. UAT adalah kesempatan terakhir untuk memastikan sistem benar-benar siap melayani user dengan excellence."* - Quinn, BMad Method QA Team \ No newline at end of file diff --git a/src/app/exam-session/page.tsx b/src/app/exam-session/page.tsx index 12bc88d..9a9ea3f 100644 --- a/src/app/exam-session/page.tsx +++ b/src/app/exam-session/page.tsx @@ -6,7 +6,7 @@ import { cn } from '@/utils/cn'; import Link from 'next/link'; import { ExamProvider, useExam } from '@/contexts/ExamContext'; import { useExamPersistence } from '@/hooks/useExamPersistence'; -import { useSearchParams } from 'next/navigation'; +import { useSearchParams, useRouter } from 'next/navigation'; import { InteractiveQuestion, EnhancedExamData, Question, PuzzleData } from '@/types'; // Feature Flags @@ -23,6 +23,9 @@ import AnswerFeedback from '@/components/quiz/AnswerFeedback'; // Interactive Quiz Components import InteractiveQuizRenderer from '@/components/quiz/InteractiveQuizRenderer'; +import VideoScenario from '@/components/quiz/VideoScenario'; +import InteractiveImageHotspot from '@/components/quiz/InteractiveImageHotspot'; +import MediaGallery from '@/components/quiz/MediaGallery'; // Legacy Components (fallback) import QuestionNavigator from '@/components/exam/QuestionNavigator'; @@ -141,6 +144,235 @@ const sampleInteractiveExamData: EnhancedExamData = { } ] }, + // Video Scenario Question - NEW REALISTIC SCENARIO + { + id: 'video-scenario-1', + question: 'Analisis Situasi Dapur: Identifikasi Masalah Keamanan Pangan', + type: 'video_scenario' as const, + options: [], + correctAnswer: 0, + content: { + question: 'Tonton video situasi dapur berikut dan identifikasi masalah keamanan pangan yang terjadi', + instructions: 'Video akan berhenti pada titik-titik kritis. Jawab pertanyaan yang muncul untuk melanjutkan.', + media: { + type: 'video' as const, + src: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', + alt: 'Simulasi Dapur Komersial - Analisis Keamanan Pangan', + caption: 'Amati aktivitas dapur selama shift pagi dan identifikasi potensi masalah keamanan pangan' + } + }, + difficulty: 'medium' as const, + timeLimit: 600, + scenarios: [ + { + id: 'step1', + timestamp: 15, + title: 'Persiapan Bahan Makanan', + question: 'Berdasarkan simulasi video, apa masalah utama yang dapat terjadi dalam persiapan bahan makanan di dapur komersial?', + options: [ + { id: 'opt1', text: 'Daging mentah dan sayuran disiapkan di area yang sama tanpa pemisahan', consequence: 'Risiko kontaminasi silang sangat tinggi - dapat menyebabkan foodborne illness' }, + { id: 'opt2', text: 'Suhu ruangan tidak terkontrol dengan baik', consequence: 'Dapat mempercepat pertumbuhan bakteri patogen pada makanan' }, + { id: 'opt3', text: 'Peralatan masak tidak dibersihkan secara proper antara penggunaan', consequence: 'Menjadi sumber kontaminasi utama dan breeding ground bakteri' }, + { id: 'opt4', text: 'Semua masalah di atas dapat terjadi bersamaan', consequence: 'Analisis komprehensif - multiple hazards memerlukan sistem HACCP yang ketat' } + ], + correctAnswer: 3, + explanation: 'Dalam operasional dapur komersial, multiple food safety hazards sering terjadi bersamaan: kontaminasi silang, temperature abuse, dan poor sanitation. Sistem HACCP (Hazard Analysis Critical Control Points) diperlukan untuk mengidentifikasi dan mengontrol semua critical control points.', + pauseVideo: true + }, + { + id: 'step2', + timestamp: 45, + title: 'Kontrol Suhu dan Penyimpanan', + question: 'Dalam konteks keamanan pangan, apa yang harus diperhatikan dalam kontrol suhu?', + options: [ + { id: 'opt1', text: 'Danger Zone (4ยฐC - 60ยฐC) harus dihindari', consequence: 'Benar - bakteri berkembang pesat di suhu ini' }, + { id: 'opt2', text: 'Cold chain harus dijaga dari supplier hingga serving', consequence: 'Kritis untuk mencegah temperature abuse' }, + { id: 'opt3', text: 'Monitoring suhu harus dilakukan secara berkala', consequence: 'Dokumentasi suhu adalah requirement legal' }, + { id: 'opt4', text: 'Semua aspek kontrol suhu di atas penting', consequence: 'Temperature control adalah fundamental dalam food safety' } + ], + correctAnswer: 3, + explanation: 'Temperature control adalah salah satu pilar utama food safety. Danger zone, cold chain management, dan monitoring berkelanjutan harus diimplementasikan secara sistematis.', + pauseVideo: true + } + ], + interactiveElements: [ + { + type: 'hint', + content: 'Fokus pada prinsip-prinsip HACCP: identifikasi hazards, critical control points, monitoring procedures, dan corrective actions' + }, + { + type: 'learning_objective', + content: 'Setelah menyelesaikan scenario ini, Anda akan mampu mengidentifikasi critical control points dalam operasional dapur dan menerapkan prinsip food safety management' + } + ] + }, + // Image Hotspot Question - NEW REALISTIC SCENARIO + { + id: 'hotspot-scenario-1', + question: 'Identifikasi Area Bermasalah dalam Tata Letak Dapur', + type: 'image_hotspot' as const, + options: [], + correctAnswer: 0, + content: { + question: 'Klik pada area-area dalam gambar dapur yang menunjukkan pelanggaran keamanan pangan', + instructions: 'Temukan dan klik pada 5 area bermasalah dalam tata letak dapur. Setiap area yang benar akan memberikan poin.', + media: { + type: 'image' as const, + src: 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1000&h=600', + alt: 'Tata letak dapur dengan berbagai area bermasalah', + caption: 'Analisis Tata Letak Dapur MBG - Identifikasi masalah keamanan pangan' + } + }, + difficulty: 'hard' as const, + timeLimit: 300, + hotspots: [ + { + id: 'hotspot1', + x: 15, y: 25, width: 12, height: 15, + type: 'incorrect', + title: 'Area Penyimpanan Daging', + description: 'Daging mentah disimpan di rak atas', + feedback: 'SALAH! Daging mentah harus disimpan di rak paling bawah untuk mencegah tetesan ke makanan lain.', + points: 10, + isRequired: true + }, + { + id: 'hotspot2', + x: 45, y: 35, width: 15, height: 12, + type: 'incorrect', + title: 'Area Pencucian Sayuran', + description: 'Sayuran dicuci bersamaan dengan peralatan kotor', + feedback: 'SALAH! Sayuran harus dicuci di area terpisah dari peralatan kotor untuk mencegah kontaminasi silang.', + points: 10, + isRequired: true + }, + { + id: 'hotspot3', + x: 70, y: 20, width: 18, height: 10, + type: 'incorrect', + title: 'Area Penyimpanan Suhu Ruang', + description: 'Makanan mudah rusak disimpan pada suhu ruang', + feedback: 'SALAH! Makanan mudah rusak harus disimpan dalam refrigerator pada suhu 4ยฐC atau lebih rendah.', + points: 10, + isRequired: true + }, + { + id: 'hotspot4', + x: 25, y: 60, width: 20, height: 8, + type: 'incorrect', + title: 'Area Persiapan Makanan', + description: 'Talenan untuk daging dan sayuran tidak dipisah', + feedback: 'SALAH! Harus menggunakan talenan terpisah untuk daging mentah dan sayuran untuk mencegah kontaminasi silang.', + points: 10, + isRequired: true + }, + { + id: 'hotspot5', + x: 60, y: 70, width: 15, height: 12, + type: 'incorrect', + title: 'Area Pembuangan Sampah', + description: 'Tempat sampah terbuka dekat area persiapan makanan', + feedback: 'SALAH! Tempat sampah harus tertutup rapat dan dijauhkan dari area persiapan makanan.', + points: 10, + isRequired: true + }, + { + id: 'hotspot6', + x: 80, y: 45, width: 12, height: 18, + type: 'correct', + title: 'Area Hand Washing Station', + description: 'Stasiun cuci tangan yang tepat', + feedback: 'BENAR! Stasiun cuci tangan sudah ditempatkan dengan benar dan mudah diakses oleh staff.', + points: 15, + isRequired: false + } + ], + interactiveElements: [ + { + type: 'hint', + content: 'Fokus pada prinsip pemisahan: daging mentah, area pencucian, kontrol suhu, dan kebersihan. Cari area yang melanggar prinsip HACCP.' + } + ] + }, + // Media Gallery Question - NEW REALISTIC SCENARIO + { + id: 'media-gallery-1', + question: 'Pelajari Prosedur HACCP Melalui Media Pembelajaran', + type: 'media_gallery' as const, + options: [], + correctAnswer: 0, + content: { + question: 'Tinjau semua media pembelajaran tentang implementasi HACCP di Dapur MBG', + instructions: 'Pelajari setiap media untuk memahami prosedur HACCP secara komprehensif.', + description: 'Koleksi media pembelajaran untuk implementasi sistem HACCP' + }, + difficulty: 'medium' as const, + timeLimit: 480, + mediaItems: [ + { + id: 'video1', + type: 'video', + src: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4', + title: 'Pengenalan Sistem HACCP', + description: 'Video pengenalan konsep dasar HACCP dan implementasinya di industri makanan - 7 prinsip fundamental untuk keamanan pangan', + thumbnail: 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=400&h=300&fit=crop', + duration: 180 + }, + { + id: 'image1', + type: 'image', + src: 'https://images.unsplash.com/photo-1577308856961-8e4e0d5b63b8?w=800&h=600&fit=crop', + title: 'Diagram Alur HACCP', + description: 'Flowchart lengkap implementasi 7 prinsip HACCP: Analisis Bahaya, Identifikasi CCP, Penetapan Batas Kritis, Monitoring, Tindakan Koreksi, Verifikasi, dan Dokumentasi' + }, + { + id: 'video2', + type: 'video', + src: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4', + title: 'Critical Control Points (CCP)', + description: 'Panduan identifikasi dan monitoring Critical Control Points dalam operasional dapur komersial', + thumbnail: 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=400&h=300&fit=crop', + duration: 240 + }, + { + id: 'image2', + type: 'image', + src: 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=800&h=600&fit=crop', + title: 'Temperature Control Chart', + description: 'Grafik kontrol suhu untuk berbagai jenis makanan - Danger Zone, Safe Storage, dan Cooking Temperatures' + }, + { + id: 'document1', + type: 'document', + src: 'https://www.fda.gov/media/99016/download', + title: 'HACCP Implementation Guide', + description: 'Panduan resmi implementasi HACCP dari FDA - dokumen referensi lengkap untuk industri makanan', + fileType: 'PDF', + fileSize: '2.5 MB' + }, + { + id: 'image3', + type: 'image', + src: 'https://images.unsplash.com/photo-1583394838336-acd977736f90?w=800&h=600&fit=crop', + title: 'Food Safety Monitoring Log', + description: 'Contoh form monitoring harian untuk dokumentasi HACCP - temperature logs, cleaning schedules, dan corrective actions' + } + ], + interactiveElements: [ + { + type: 'hint', + content: 'Pelajari setiap media secara berurutan: Video โ†’ Diagram โ†’ Dokumentasi. Fokus pada 7 prinsip HACCP dan implementasi praktisnya' + }, + { + type: 'learning_objective', + content: 'Setelah mempelajari semua media, Anda akan memahami: (1) 7 Prinsip HACCP, (2) Identifikasi CCP, (3) Sistem Monitoring, (4) Dokumentasi yang diperlukan' + }, + { + type: 'assessment_criteria', + content: 'Pemahaman dinilai berdasarkan: kemampuan mengidentifikasi hazards, menetapkan CCP, merancang monitoring system, dan membuat dokumentasi HACCP' + } + ] + }, { id: '12', question: 'Identifikasi Suara Peralatan Dapur yang Bermasalah', @@ -278,7 +510,8 @@ const sampleInteractiveExamData: EnhancedExamData = { correctAnswer: 0, content: { question: 'Seret setiap istilah untuk mencocokkan dengan definisi yang benar', - instructions: 'Seret setiap istilah untuk mencocokkan dengan definisi yang benar.' + instructions: 'Seret setiap istilah untuk mencocokkan dengan definisi yang benar.', + puzzleType: 'matching' as const }, difficulty: 'medium' as const, puzzleData: { @@ -288,27 +521,33 @@ const sampleInteractiveExamData: EnhancedExamData = { { id: 'ccp', content: 'Titik Kendali Kritis (CCP)', - correctPosition: 0, + correctPosition: 'def1', isLocked: false }, { id: 'haccp', content: 'Analisis Bahaya dan Titik Kendali Kritis', - correctPosition: 1, + correctPosition: 'def2', isLocked: false }, { id: 'sanitasi', content: 'Proses pembersihan dan desinfeksi', - correctPosition: 2, + correctPosition: 'def3', isLocked: false }, { id: 'kontaminasi', content: 'Pencemaran makanan oleh zat berbahaya', - correctPosition: 3, + correctPosition: 'def4', isLocked: false } + ], + targetAreas: [ + { id: 'def1', label: 'Titik dalam proses produksi makanan di mana bahaya dapat dicegah, dieliminasi, atau dikurangi' }, + { id: 'def2', label: 'Sistem manajemen keamanan pangan yang mengidentifikasi, mengevaluasi, dan mengendalikan bahaya' }, + { id: 'def3', label: 'Tindakan untuk menghilangkan kotoran dan mikroorganisme dari permukaan' }, + { id: 'def4', label: 'Masuknya bahan kimia, fisik, atau biologis yang dapat membahayakan kesehatan' } ] } }, @@ -383,18 +622,51 @@ const sampleInteractiveExamData: EnhancedExamData = { correctAnswer: 0, content: { question: 'Susun langkah mencuci tangan dalam urutan yang benar', - instructions: 'Susun langkah mencuci tangan dalam urutan yang benar.' + instructions: 'Seret setiap langkah ke urutan yang benar (1-5).', + puzzleType: 'matching' as const }, difficulty: 'easy' as const, puzzleData: { - type: 'sequence' as const, + type: 'jigsaw' as const, instructions: 'Urutkan langkah-langkah mencuci tangan sesuai standar Dapur MBG.', - sequence: [ - 'Basahi tangan dengan air bersih', - 'Gunakan sabun dan gosok hingga berbusa', - 'Gosok selama minimal 20 detik', - 'Bilas hingga bersih', - 'Keringkan dengan handuk bersih' + pieces: [ + { + id: 'step1', + content: 'Basahi tangan dengan air bersih', + correctPosition: 'pos1', + isLocked: false + }, + { + id: 'step2', + content: 'Gunakan sabun dan gosok hingga berbusa', + correctPosition: 'pos2', + isLocked: false + }, + { + id: 'step3', + content: 'Gosok selama minimal 20 detik', + correctPosition: 'pos3', + isLocked: false + }, + { + id: 'step4', + content: 'Bilas hingga bersih', + correctPosition: 'pos4', + isLocked: false + }, + { + id: 'step5', + content: 'Keringkan dengan handuk bersih', + correctPosition: 'pos5', + isLocked: false + } + ], + targetAreas: [ + { id: 'pos1', label: 'Langkah 1' }, + { id: 'pos2', label: 'Langkah 2' }, + { id: 'pos3', label: 'Langkah 3' }, + { id: 'pos4', label: 'Langkah 4' }, + { id: 'pos5', label: 'Langkah 5' } ] } }, @@ -406,18 +678,51 @@ const sampleInteractiveExamData: EnhancedExamData = { correctAnswer: 0, content: { question: 'Urutkan tahapan persiapan makanan dari awal hingga penyajian', - instructions: 'Seret dan lepas untuk mengurutkan tahapan persiapan makanan.' + instructions: 'Seret setiap tahapan ke urutan yang benar (1-5).', + puzzleType: 'matching' as const }, difficulty: 'hard' as const, puzzleData: { - type: 'sequence' as const, + type: 'jigsaw' as const, instructions: 'Urutkan tahapan persiapan makanan bergizi gratis sesuai standar MBG.', - sequence: [ - 'Perencanaan menu bergizi seimbang', - 'Pemilihan dan pemeriksaan bahan baku', - 'Persiapan dan pengolahan makanan', - 'Kontrol kualitas dan suhu', - 'Penyajian dan distribusi' + pieces: [ + { + id: 'stage1', + content: 'Perencanaan menu bergizi seimbang', + correctPosition: 'order1', + isLocked: false + }, + { + id: 'stage2', + content: 'Pemilihan dan pemeriksaan bahan baku', + correctPosition: 'order2', + isLocked: false + }, + { + id: 'stage3', + content: 'Persiapan dan pengolahan makanan', + correctPosition: 'order3', + isLocked: false + }, + { + id: 'stage4', + content: 'Kontrol kualitas dan suhu', + correctPosition: 'order4', + isLocked: false + }, + { + id: 'stage5', + content: 'Penyajian dan distribusi', + correctPosition: 'order5', + isLocked: false + } + ], + targetAreas: [ + { id: 'order1', label: 'Tahap 1' }, + { id: 'order2', label: 'Tahap 2' }, + { id: 'order3', label: 'Tahap 3' }, + { id: 'order4', label: 'Tahap 4' }, + { id: 'order5', label: 'Tahap 5' } ] } }, @@ -659,33 +964,6 @@ const sampleInteractiveExamData: EnhancedExamData = { } ] }, - { - id: '11', - question: 'Puzzle Slider: Pengaturan Suhu Optimal', - type: 'puzzle' as const, - content: { - question: 'Atur suhu yang tepat untuk berbagai jenis penyimpanan makanan', - instructions: 'Geser slider untuk mengatur suhu optimal setiap area penyimpanan', - puzzleType: 'slider' as const - }, - difficulty: 'hard' as const, - timeLimit: 120, - puzzleData: { - type: 'jigsaw' as const, - sliders: [ - { id: 's1', label: 'Freezer', minValue: -25, maxValue: 0, correctValue: -18, unit: 'ยฐC' }, - { id: 's2', label: 'Kulkas Sayuran', minValue: 0, maxValue: 10, correctValue: 4, unit: 'ยฐC' }, - { id: 's3', label: 'Kulkas Daging', minValue: -5, maxValue: 5, correctValue: 2, unit: 'ยฐC' }, - { id: 's4', label: 'Display Makanan Panas', minValue: 50, maxValue: 80, correctValue: 65, unit: 'ยฐC' } - ] - }, - interactiveElements: [ - { - type: 'explanation', - content: 'Suhu optimal: Freezer -18ยฐC, Sayuran 4ยฐC, Daging 2ยฐC, Display Panas 65ยฐC' - } - ] - }, // New Quiz Type 1: Puzzle Gambar/Icon Match { id: '15', @@ -706,7 +984,7 @@ const sampleInteractiveExamData: EnhancedExamData = { leftItem: { id: 'img1', content: 'Termometer Digital', - image: '/images/thermometer-digital.png' + image: 'https://cdn-icons-png.flaticon.com/512/2913/2913465.png' }, rightItem: { id: 'area1', @@ -718,7 +996,7 @@ const sampleInteractiveExamData: EnhancedExamData = { leftItem: { id: 'img2', content: 'Cutting Board Berwarna', - image: '/images/cutting-board-colored.png' + image: 'https://cdn-icons-png.flaticon.com/512/2515/2515183.png' }, rightItem: { id: 'area2', @@ -730,7 +1008,7 @@ const sampleInteractiveExamData: EnhancedExamData = { leftItem: { id: 'img3', content: 'Timer Dapur', - image: '/images/kitchen-timer.png' + image: 'https://cdn-icons-png.flaticon.com/512/2921/2921222.png' }, rightItem: { id: 'area3', @@ -742,12 +1020,36 @@ const sampleInteractiveExamData: EnhancedExamData = { leftItem: { id: 'img4', content: 'Sarung Tangan Sekali Pakai', - image: '/images/disposable-gloves.png' + image: 'https://cdn-icons-png.flaticon.com/512/2913/2913423.png' }, rightItem: { id: 'area4', content: 'Melindungi dari kontaminasi' } + }, + { + id: 'pair5', + leftItem: { + id: 'img5', + content: 'Topi Chef', + image: 'https://cdn-icons-png.flaticon.com/512/2515/2515191.png' + }, + rightItem: { + id: 'area5', + content: 'Menjaga kebersihan rambut' + } + }, + { + id: 'pair6', + leftItem: { + id: 'img6', + content: 'Apron/Celemek', + image: 'https://cdn-icons-png.flaticon.com/512/2515/2515189.png' + }, + rightItem: { + id: 'area6', + content: 'Melindungi pakaian dari kontaminasi' + } } ], allowDragDrop: true, @@ -775,18 +1077,18 @@ const sampleInteractiveExamData: EnhancedExamData = { correctAnswer: 0, content: { question: 'Daging mentah boleh disimpan pada suhu 8ยฐC selama 2 hari', - instructions: 'Jawab dengan cepat! Anda memiliki waktu terbatas untuk menjawab.' + instructions: 'Jawab sesuai kecepatan Anda untuk testing.' }, difficulty: 'easy' as const, - timeLimit: 10, + timeLimit: 300, // 5 minutes - very long time limit reactionTimeConfig: { - timeLimit: 10, - showCountdown: true, - trackReactionTime: true, + timeLimit: 300000, // 5 minutes in milliseconds + showCountdown: false, // Hide countdown timer + trackReactionTime: true, // Still track reaction time for testing instantFeedback: true, - penaltyForWrongAnswer: 2, + penaltyForWrongAnswer: 0, // No penalty since this is for testing bonusForQuickAnswer: 5, - quickAnswerThreshold: 5 + quickAnswerThreshold: 3000 // 3 seconds threshold }, interactiveElements: [ { @@ -818,29 +1120,33 @@ const sampleInteractiveExamData: EnhancedExamData = { problemSolving: 60, communication: 75 }, - avatar: '/images/kitchen-manager-avatar.png' + avatar: 'https://cdn-icons-png.flaticon.com/512/3135/3135715.png' }, scenario: 'Anda adalah manajer dapur MBG yang menghadapi situasi krisis keracunan makanan. Setiap keputusan yang Anda buat akan mempengaruhi reputasi dapur dan keselamatan pelanggan.', + maxSteps: 3, decisions: [ { id: 'decision1', - step: 1, + step: 0, situation: 'Laporan keracunan makanan masuk. Apa langkah pertama Anda?', options: [ { id: 'opt1', text: 'Segera hentikan semua operasi dapur', - difficulty: 'medium' + difficulty: 'medium', + icon: '๐Ÿ›‘' }, { id: 'opt2', text: 'Investigasi sumber makanan yang dicurigai', - difficulty: 'easy' + difficulty: 'easy', + icon: '๐Ÿ”' }, { id: 'opt3', text: 'Hubungi manajemen dan otoritas kesehatan', - difficulty: 'medium' + difficulty: 'medium', + icon: '๐Ÿ“ž' } ], consequences: { @@ -875,9 +1181,125 @@ const sampleInteractiveExamData: EnhancedExamData = { nextStep: 'decision2c' } } + }, + { + id: 'decision2', + step: 1, + situation: 'Tim dapur mulai panik. Bagaimana Anda mengelola situasi ini?', + options: [ + { + id: 'opt1', + text: 'Berikan instruksi tegas dan jelas kepada semua staff', + difficulty: 'medium', + icon: '๐Ÿ‘จโ€๐Ÿ’ผ' + }, + { + id: 'opt2', + text: 'Tenangkan tim dan jelaskan situasi dengan empati', + difficulty: 'easy', + icon: '๐Ÿค' + }, + { + id: 'opt3', + text: 'Fokus pada dokumentasi dan bukti', + difficulty: 'hard', + icon: '๐Ÿ“‹' + } + ], + consequences: { + 'opt1': { + message: 'Tim mengikuti instruksi dengan baik', + statChanges: { + leadership: 10, + empathy: -5, + problemSolving: 5, + communication: 5 + }, + isEndingPath: false + }, + 'opt2': { + message: 'Tim merasa didukung dan bekerja lebih tenang', + statChanges: { + leadership: 5, + empathy: 15, + problemSolving: 0, + communication: 10 + }, + isEndingPath: false + }, + 'opt3': { + message: 'Dokumentasi lengkap membantu investigasi', + statChanges: { + leadership: 0, + empathy: -5, + problemSolving: 20, + communication: -5 + }, + isEndingPath: false + } + } + }, + { + id: 'decision3', + step: 2, + situation: 'Media mulai meliput kasus ini. Bagaimana respons Anda?', + options: [ + { + id: 'opt1', + text: 'Berikan pernyataan transparan kepada media', + difficulty: 'hard', + icon: '๐Ÿ“บ' + }, + { + id: 'opt2', + text: 'Hindari media dan fokus pada penyelesaian internal', + difficulty: 'medium', + icon: '๐Ÿšซ' + }, + { + id: 'opt3', + text: 'Koordinasi dengan tim PR perusahaan', + difficulty: 'easy', + icon: '๐Ÿ’ผ' + } + ], + consequences: { + 'opt1': { + message: 'Transparansi meningkatkan kepercayaan publik', + statChanges: { + leadership: 15, + empathy: 10, + problemSolving: 5, + communication: 20 + }, + isEndingPath: true, + endingType: 'success' + }, + 'opt2': { + message: 'Spekulasi media meningkat, reputasi terpengaruh', + statChanges: { + leadership: -10, + empathy: -5, + problemSolving: 10, + communication: -15 + }, + isEndingPath: true, + endingType: 'failure' + }, + 'opt3': { + message: 'Respons terkoordinasi dengan baik', + statChanges: { + leadership: 5, + empathy: 5, + problemSolving: 10, + communication: 15 + }, + isEndingPath: true, + endingType: 'neutral' + } + } } ], - maxSteps: 5, scoringSystem: { empathy: 10, leadership: 15, @@ -1041,14 +1463,21 @@ const sampleExamData = { }; function InteractiveQuizWrapper() { + const router = useRouter(); const { state, dispatch, setExamData, setAdminConfig, - setInteractiveState + setInteractiveState, + submitExam } = useExam(); + const handleSubmitExam = () => { + submitExam(); + router.push('/exam-summary'); + }; + useEffect(() => { // Set up interactive exam data setExamData(sampleInteractiveExamData); @@ -1108,13 +1537,33 @@ function InteractiveQuizWrapper() { > Previous - + {state.currentQuestion >= (state.examData.interactiveData?.questions.length || state.examData.questions.length) - 1 ? ( + + ) : ( + + )} @@ -1124,6 +1573,7 @@ function InteractiveQuizWrapper() { function ExamSessionContent() { const searchParams = useSearchParams(); + const router = useRouter(); const isInteractive = searchParams.get('type') === 'interactive'; // If this is an interactive quiz, render the InteractiveQuizWrapper @@ -1151,6 +1601,11 @@ function ExamSessionContent() { canSubmit, hasUnsavedChanges } = useExam(); + + const handleSubmitExam = () => { + submitExam(); + router.push('/exam-summary'); + }; const [showIntroModal, setShowIntroModal] = useState(true); const [showSettings, setShowSettings] = useState(false); @@ -1201,10 +1656,6 @@ function ExamSessionContent() { // Remove the invalid START_EXAM dispatch - not needed as the exam starts when intro modal closes }; - const handleSubmitExam = () => { - submitExam(); - }; - // Don't render main content if intro modal is showing and feature is enabled if (showIntroModal && isFeatureEnabled('quizIntroductionModal')) { return ( @@ -1465,10 +1916,10 @@ function ExamSessionContent() { {isLastQuestion ? ( + + + ); + } + + return ( +
+
+ {/* Header */} +
+
+ {examResult.score >= 80 ? ( + + ) : examResult.score >= 60 ? ( + + ) : ( + + )} +
+

+ {examResult.score >= 80 ? 'Selamat!' : examResult.score >= 60 ? 'Cukup Baik!' : 'Perlu Perbaikan'} +

+

Anda telah menyelesaikan ujian

+
+ + {/* Score Summary */} +
+
+

{examResult.examTitle}

+
+
+
+ {examResult.score}% +
+

Skor Akhir

+
+
+
+ {examResult.correctAnswers}/{examResult.totalQuestions} +
+

Jawaban Benar

+
+
+
+ + {examResult.timeSpent} +
+

Waktu Pengerjaan

+
+
+
+ {examResult.completedAt} +
+

Selesai Pada

+
+
+
+
+ + {/* Detailed Results */} +
+

+ + Detail Jawaban +

+
+ {examResult.answers.map((answer, index) => ( +
+
+
+ {answer.isCorrect ? ( + + ) : ( + + )} +
+
+

+ Soal {index + 1}: {answer.question} +

+
+
+

Jawaban Anda:

+

+ {typeof answer.userAnswer === 'string' ? answer.userAnswer : 'Tidak dijawab'} +

+
+ {!answer.isCorrect && ( +
+

Jawaban Benar:

+

+ {typeof answer.correctAnswer === 'string' ? answer.correctAnswer : 'N/A'} +

+
+ )} +
+
+
+
+ ))} +
+
+ + {/* Action Buttons */} +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/payroll-demo/page.tsx b/src/app/payroll-demo/page.tsx index 36aa474..431fc6f 100644 --- a/src/app/payroll-demo/page.tsx +++ b/src/app/payroll-demo/page.tsx @@ -10,7 +10,7 @@ import { } from '../../features/payroll-reward-system/components'; import { PayrollRewardSystemModule } from '../../features/payroll-reward-system'; import { setupGlobalErrorHandling } from '../../core/errors'; -import { container } from '../../core/di/DIContainer'; +import { container as globalContainer } from '../../core/di/DIContainer'; export default function PayrollDemoPage() { const [moduleLoaded, setModuleLoaded] = useState(false); @@ -33,23 +33,43 @@ export default function PayrollDemoPage() { // Create module instance const moduleInstance = new PayrollRewardSystemModule(); - // Register services in DI container - const services = moduleInstance.getServices(); - - // Activate the module + // Activate the module first (this initializes the services) await moduleInstance.activate(); - // Register services with container + // Get services after activation + const services = moduleInstance.getServices(); + + // Register services with container using lazy initialization to avoid circular dependency for (const serviceConfig of services) { - const serviceInstance = serviceConfig.factory(); - container.registerInstance(serviceConfig.token, serviceInstance); + try { + console.log(`Registering service: ${serviceConfig.token}`); + + // Check if service is already registered to prevent duplicates + if (globalContainer.has(serviceConfig.token)) { + console.log(`Service ${serviceConfig.token} already registered, skipping`); + continue; + } + + // Register as singleton with lazy factory to avoid circular dependency + globalContainer.singleton(serviceConfig.token, () => { + const serviceInstance = serviceConfig.factory(); + console.log(`Created service instance: ${serviceConfig.token}`); + return serviceInstance; + }); + + console.log(`โœ… Registered service: ${serviceConfig.token}`); + } catch (serviceError) { + console.error(`โŒ Failed to register service ${serviceConfig.token}:`, serviceError); + throw new Error(`Failed to register ${serviceConfig.token}: ${serviceError instanceof Error ? serviceError.message : 'Unknown error'}`); + } } setModuleLoaded(true); - console.log('Payroll Reward System module loaded successfully'); + console.log('๐ŸŽ‰ Payroll Reward System module loaded successfully'); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to initialize module'); - console.error('Failed to initialize module:', err); + const errorMessage = err instanceof Error ? err.message : 'Failed to initialize module'; + setError(errorMessage); + console.error('โŒ Failed to initialize module:', err); } finally { setLoading(false); } diff --git a/src/components/admin/AdminTopBar.tsx b/src/components/admin/AdminTopBar.tsx index f69298b..796bad8 100644 --- a/src/components/admin/AdminTopBar.tsx +++ b/src/components/admin/AdminTopBar.tsx @@ -235,9 +235,13 @@ export default function AdminTopBar({ onMenuClick, sidebarCollapsed }: AdminTopB
- ))} -
- - )} )} diff --git a/src/components/admin/QuizPreview.tsx b/src/components/admin/QuizPreview.tsx index f4d95e8..098ce20 100644 --- a/src/components/admin/QuizPreview.tsx +++ b/src/components/admin/QuizPreview.tsx @@ -42,7 +42,6 @@ interface QuizPreviewProps { interface PreviewState { currentIndex: number; selectedAnswers: { [questionId: string]: any }; - confidenceScores: { [questionId: string]: number }; timeSpent: { [questionId: string]: number }; startTime: number; isPlaying: boolean; @@ -67,7 +66,6 @@ export const QuizPreview: React.FC = ({ const [previewState, setPreviewState] = useState({ currentIndex: currentQuestionIndex, selectedAnswers: {}, - confidenceScores: {}, timeSpent: {}, startTime: Date.now(), isPlaying: false, @@ -162,16 +160,6 @@ export const QuizPreview: React.FC = ({ })); }; - const handleConfidenceChange = (questionId: string, confidence: number) => { - setPreviewState(prev => ({ - ...prev, - confidenceScores: { - ...prev.confidenceScores, - [questionId]: confidence - } - })); - }; - const toggleHints = (questionId: string) => { setPreviewState(prev => ({ ...prev, @@ -204,7 +192,6 @@ export const QuizPreview: React.FC = ({ setPreviewState({ currentIndex: 0, selectedAnswers: {}, - confidenceScores: {}, timeSpent: {}, startTime: Date.now(), isPlaying: false, @@ -359,34 +346,6 @@ export const QuizPreview: React.FC = ({ ); })} - - {/* Confidence Scale */} - {currentQuestion.interactiveConfig?.showConfidenceScale && ( -
-

- Seberapa yakin Anda dengan jawaban ini? -

-
- {[1, 2, 3, 4, 5].map(level => ( - - ))} -
-
- Tidak yakin - Sangat yakin -
-
- )} )} diff --git a/src/components/quiz/InteractiveImageHotspot.tsx b/src/components/quiz/InteractiveImageHotspot.tsx new file mode 100644 index 0000000..d6afd4a --- /dev/null +++ b/src/components/quiz/InteractiveImageHotspot.tsx @@ -0,0 +1,358 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Eye, Target, CheckCircle, XCircle, AlertTriangle, Lightbulb, Zap } from 'lucide-react'; +import { cn } from '../../utils/cn'; + +interface InteractiveImageHotspotProps { + imageSrc: string; + title: string; + description: string; + hotspots: ImageHotspot[]; + onComplete: (results: HotspotResult[]) => void; + className?: string; + showHints?: boolean; + allowMultipleAttempts?: boolean; +} + +interface ImageHotspot { + id: string; + x: number; // percentage (0-100) + y: number; // percentage (0-100) + width: number; // percentage + height: number; // percentage + type: 'correct' | 'incorrect' | 'neutral' | 'warning'; + title: string; + description: string; + feedback: string; + points: number; + hint?: string; + consequence?: string; + isRequired?: boolean; +} + +interface HotspotResult { + hotspotId: string; + clicked: boolean; + timestamp: number; + attempts: number; + isCorrect: boolean; + points: number; +} + +const InteractiveImageHotspot: React.FC = ({ + imageSrc, + title, + description, + hotspots, + onComplete, + className, + showHints = true, + allowMultipleAttempts = true +}) => { + const imageRef = useRef(null); + const [imageLoaded, setImageLoaded] = useState(false); + const [clickedHotspots, setClickedHotspots] = useState>(new Set()); + const [hotspotResults, setHotspotResults] = useState>(new Map()); + const [showFeedback, setShowFeedback] = useState(null); + const [currentFeedback, setCurrentFeedback] = useState(null); + const [hintsVisible, setHintsVisible] = useState(false); + const [attempts, setAttempts] = useState>(new Map()); + + useEffect(() => { + // Check if all required hotspots are clicked + const requiredHotspots = hotspots.filter(h => h.isRequired); + const clickedRequired = requiredHotspots.filter(h => clickedHotspots.has(h.id)); + + if (requiredHotspots.length > 0 && clickedRequired.length === requiredHotspots.length) { + const results = Array.from(hotspotResults.values()); + onComplete(results); + } + }, [clickedHotspots, hotspots, hotspotResults, onComplete]); + + const handleHotspotClick = (hotspot: ImageHotspot, event: React.MouseEvent) => { + event.preventDefault(); + + const currentAttempts = attempts.get(hotspot.id) || 0; + const newAttempts = currentAttempts + 1; + + // Update attempts + setAttempts(prev => new Map(prev.set(hotspot.id, newAttempts))); + + // Check if already clicked and multiple attempts not allowed + if (clickedHotspots.has(hotspot.id) && !allowMultipleAttempts) { + return; + } + + // Add to clicked hotspots + setClickedHotspots(prev => new Set([...Array.from(prev), hotspot.id])); + + // Calculate points (reduce points for multiple attempts) + let points = hotspot.points; + if (newAttempts > 1) { + points = Math.max(0, hotspot.points - (newAttempts - 1) * 2); + } + + // Create result + const result: HotspotResult = { + hotspotId: hotspot.id, + clicked: true, + timestamp: Date.now(), + attempts: newAttempts, + isCorrect: hotspot.type === 'correct', + points: hotspot.type === 'correct' ? points : 0 + }; + + // Update results + setHotspotResults(prev => new Map(prev.set(hotspot.id, result))); + + // Show feedback + setCurrentFeedback(hotspot); + setShowFeedback(hotspot.id); + + // Auto-hide feedback after 3 seconds + setTimeout(() => { + setShowFeedback(null); + setCurrentFeedback(null); + }, 3000); + }; + + const getHotspotIcon = (type: string) => { + switch (type) { + case 'correct': + return ; + case 'incorrect': + return ; + case 'warning': + return ; + default: + return ; + } + }; + + const getHotspotColor = (hotspot: ImageHotspot) => { + const isClicked = clickedHotspots.has(hotspot.id); + + if (isClicked) { + switch (hotspot.type) { + case 'correct': + return 'border-green-500 bg-green-500/20'; + case 'incorrect': + return 'border-red-500 bg-red-500/20'; + case 'warning': + return 'border-yellow-500 bg-yellow-500/20'; + default: + return 'border-blue-500 bg-blue-500/20'; + } + } + + return showHints + ? 'border-blue-400 bg-blue-400/10 animate-pulse' + : 'border-transparent bg-transparent hover:border-blue-400 hover:bg-blue-400/10'; + }; + + const totalPoints = Array.from(hotspotResults.values()).reduce((sum, result) => sum + result.points, 0); + const maxPoints = hotspots.reduce((sum, hotspot) => sum + hotspot.points, 0); + const requiredHotspots = hotspots.filter(h => h.isRequired); + const clickedRequired = requiredHotspots.filter(h => clickedHotspots.has(h.id)); + const progress = requiredHotspots.length > 0 ? (clickedRequired.length / requiredHotspots.length) * 100 : 0; + + return ( +
+ {/* Header */} +
+

{title}

+

{description}

+ + {/* Instructions */} +
+

+ ๐Ÿ“ Instruksi: Klik pada area-area bermasalah dalam gambar. + Area yang wajib ditemukan: {requiredHotspots.length} +

+
+ +
+
+ + + {clickedRequired.length}/{requiredHotspots.length} Wajib + + + + {clickedHotspots.size}/{hotspots.length} Total + + + + {totalPoints}/{maxPoints} Poin + +
+ +
+
+ + {/* Progress Bar */} +
+
+ Progress Area Wajib + {Math.round(progress)}% +
+
+
+
+ {progress === 100 && ( +

+ โœ… Semua area wajib telah ditemukan! +

+ )} +
+ + {/* Interactive Image */} +
+ {title} setImageLoaded(true)} + /> + + {/* Hotspots Overlay */} + {imageLoaded && ( +
+ {hotspots.map((hotspot) => ( + + ))} +
+ )} + + {/* Feedback Overlay */} + {showFeedback && currentFeedback && ( +
+
+
+ {getHotspotIcon(currentFeedback.type)} +

+ {currentFeedback.title} +

+
+ +

{currentFeedback.feedback}

+ + {currentFeedback.consequence && ( +
+

+ Konsekuensi: {currentFeedback.consequence} +

+
+ )} + +
+ + {currentFeedback.type === 'correct' ? '+' : ''}{currentFeedback.points} poin + + + +
+
+
+ )} +
+ + {/* Summary */} +
+
+
+ Area Ditemukan: + + {clickedHotspots.size}/{hotspots.length} + +
+
+ Total Poin: + + {totalPoints}/{maxPoints} + +
+
+
+
+ ); +}; + +export default InteractiveImageHotspot; \ No newline at end of file diff --git a/src/components/quiz/InteractiveQuizRenderer.tsx b/src/components/quiz/InteractiveQuizRenderer.tsx index 257d8bd..b7c8718 100644 --- a/src/components/quiz/InteractiveQuizRenderer.tsx +++ b/src/components/quiz/InteractiveQuizRenderer.tsx @@ -26,6 +26,9 @@ import PuzzleGambarIconMatch from './PuzzleGambarIconMatch'; import TrueFalseCepatTepat from './TrueFalseCepatTepat'; import MiniSimulationRPG from './MiniSimulationRPG'; import MiniSurveyReflection from './MiniSurveyReflection'; +import VideoScenario from './VideoScenario'; +import InteractiveImageHotspot from './InteractiveImageHotspot'; +import MediaGallery from './MediaGallery'; interface InteractiveQuizRendererProps { question: InteractiveQuestion; @@ -56,8 +59,7 @@ const InteractiveQuizRenderer: React.FC = ({ const [startTime] = useState(Date.now()); const [lastInteraction, setLastInteraction] = useState(Date.now()); - const [confidenceLevel, setConfidenceLevel] = useState(3); - const [showConfidenceRating, setShowConfidenceRating] = useState(false); + // Guard clause untuk question yang undefined if (!question) { @@ -98,19 +100,9 @@ const InteractiveQuizRenderer: React.FC = ({ onAnswerChange?.(answer); }; - // Effect untuk menampilkan confidence rating - useEffect(() => { - if (question?.content?.instructions && state.answers[questionIndex]) { - setShowConfidenceRating(true); - } - }, [question?.content?.instructions, state.answers, questionIndex]); - // Handle confidence rating - const handleConfidenceRating = (rating: number) => { - setConfidenceLevel(rating); - setConfidenceRating(questionIndex, rating); - setShowConfidenceRating(false); - }; + + // Render ikon berdasarkan tipe pertanyaan const renderQuestionTypeIcon = () => { @@ -131,6 +123,12 @@ const InteractiveQuizRenderer: React.FC = ({ return ; case 'mini_survey_reflection': return ; + case 'video_scenario': + return ; + case 'image_hotspot': + return ; + case 'media_gallery': + return ; default: return ; } @@ -167,6 +165,41 @@ const InteractiveQuizRenderer: React.FC = ({ case 'mini_survey_reflection': return ; + case 'video_scenario': + return ( + { + if (onAnswerChange) { + onAnswerChange(results); + } + }} + /> + ); + + case 'image_hotspot': + return ( + { + if (onAnswerChange) { + onAnswerChange(results); + } + }} + showHints={true} + allowMultipleAttempts={true} + /> + ); + + case 'media_gallery': + return ; + default: return (
@@ -275,48 +308,6 @@ const InteractiveQuizRenderer: React.FC = ({ {/* Render komponen pertanyaan */} {renderQuestionComponent()}
- - {/* Confidence Rating Modal */} - {showConfidenceRating && ( -
-
-

- {adminLanguage === 'indonesia' - ? 'Seberapa yakin Anda dengan jawaban ini?' - : 'How confident are you with this answer?'} -

- -
- {[1, 2, 3, 4, 5].map((rating) => ( - - ))} -
- -
- {adminLanguage === 'indonesia' ? 'Tidak yakin' : 'Not sure'} - {adminLanguage === 'indonesia' ? 'Sangat yakin' : 'Very sure'} -
- - -
-
- )}
); }; diff --git a/src/components/quiz/MediaGallery.tsx b/src/components/quiz/MediaGallery.tsx new file mode 100644 index 0000000..679d6fa --- /dev/null +++ b/src/components/quiz/MediaGallery.tsx @@ -0,0 +1,409 @@ +'use client'; + +import React, { useState, useRef } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { InteractiveQuestion, MediaItem } from '@/types'; +import { + Play, + Pause, + Volume2, + VolumeX, + Download, + FileText, + Image as ImageIcon, + Video, + Music, + Maximize, + X, + ChevronLeft, + ChevronRight +} from 'lucide-react'; + +interface MediaGalleryProps { + question: InteractiveQuestion; + questionIndex: number; + onAnswerChange: (answer: any) => void; + adminLanguage: string; +} + +export default function MediaGallery({ + question, + questionIndex, + onAnswerChange, + adminLanguage +}: MediaGalleryProps) { + const [currentMediaIndex, setCurrentMediaIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [isMuted, setIsMuted] = useState(false); + const [isFullscreen, setIsFullscreen] = useState(false); + const [completedMedia, setCompletedMedia] = useState>(new Set()); + const [progress, setProgress] = useState<{ [key: string]: number }>({}); + + const videoRef = useRef(null); + const audioRef = useRef(null); + + const mediaItems = question.mediaItems || []; + const currentMedia = mediaItems[currentMediaIndex]; + + const handleMediaComplete = (mediaId: string) => { + setCompletedMedia(prev => new Set([...Array.from(prev), mediaId])); + + // Update progress + const newProgress = { ...progress }; + newProgress[mediaId] = 100; + setProgress(newProgress); + + // Report completion to parent + onAnswerChange({ + completedMedia: Array.from(new Set([...Array.from(completedMedia), mediaId])), + currentMediaIndex, + totalMedia: mediaItems.length, + progress: newProgress + }); + }; + + const handlePlayPause = () => { + if (currentMedia?.type === 'video' && videoRef.current) { + if (isPlaying) { + videoRef.current.pause(); + } else { + videoRef.current.play(); + } + setIsPlaying(!isPlaying); + } else if (currentMedia?.type === 'audio' && audioRef.current) { + if (isPlaying) { + audioRef.current.pause(); + } else { + audioRef.current.play(); + } + setIsPlaying(!isPlaying); + } + }; + + const handleMuteToggle = () => { + if (videoRef.current) { + videoRef.current.muted = !isMuted; + } + if (audioRef.current) { + audioRef.current.muted = !isMuted; + } + setIsMuted(!isMuted); + }; + + const handleDownload = (media: MediaItem) => { + // Create download link + const link = document.createElement('a'); + link.href = media.src; + link.download = media.title || 'media-file'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + const navigateMedia = (direction: 'prev' | 'next') => { + if (direction === 'prev' && currentMediaIndex > 0) { + setCurrentMediaIndex(currentMediaIndex - 1); + } else if (direction === 'next' && currentMediaIndex < mediaItems.length - 1) { + setCurrentMediaIndex(currentMediaIndex + 1); + } + setIsPlaying(false); + }; + + const getMediaIcon = (type: string) => { + switch (type) { + case 'video': return