Compare commits

..

No commits in common. "3500db643593158c7ed78d2baaa023808a55b460" and "d470b024b4033b30c402b10dd383a15c3bac1bf1" have entirely different histories.

49 changed files with 78 additions and 1765 deletions

View File

@ -90,17 +90,17 @@ created_at Audit timestamp
Relationships: Relationships:
One Course Category → Many Course Sub-categories One Course Category → Many Courses
Course Category Course Category
└── Course Sub-categories[] └── Courses[]
2. Course Sub-category 2. Course
Table: course_sub_categories Table: courses
Purpose: Purpose:
A grouping within a category (e.g., Speaking, Listening under Learning English). Represents a full course offering under a category.
Key Fields: Key Fields:
@ -114,23 +114,23 @@ Relationships:
Belongs to one Course Category Belongs to one Course Category
Has many Courses Has many Sub-courses
Course Category Course Category
└── Course Sub-category └── Course
└── Courses[] └── Sub-courses[]
3. Course 3. Sub-course
Table: courses Table: sub_courses
Purpose: Purpose:
A learning unit within a sub-category representing different skill levels A learning unit within a course representing different skill levels
(e.g., Beginner, Intermediate, Advanced). (e.g., Beginner, Intermediate, Advanced).
Key Fields: Key Fields:
sub_category_id FK → course_sub_categories.id course_id FK → courses.id
title, description title, description
@ -144,27 +144,27 @@ is_active
Relationships: Relationships:
Belongs to one Course Sub-category Belongs to one Course
Has many Course Videos Has many Sub-course Videos
Has many Practices Has many Practices
Course Sub-category Course
└── Course └── Sub-course
├── Course Videos[] ├── Sub-course Videos[]
└── Practices[] └── Practices[]
4. Course Video 4. Sub-course Video
Table: course_videos Table: sub_course_videos
Purpose: Purpose:
Video learning content attached to a course. Video learning content attached to a sub-course.
Key Fields: Key Fields:
course_id FK → courses.id sub_course_id FK → sub_courses.id
title, description title, description
@ -190,21 +190,21 @@ is_active
Relationships: Relationships:
Belongs to one Course Belongs to one Sub-course
Course Sub-course
└── Course Video └── Sub-course Video
5. Practice 5. Practice
Table: practices Table: practices
Purpose: Purpose:
Exercises or assessments that belong to a course. Exercises or assessments that belong to a sub-course.
Key Fields: Key Fields:
course_id FK → courses.id sub_course_id FK → sub_courses.id
title, description title, description
@ -216,11 +216,11 @@ is_active
Relationships: Relationships:
Belongs to one Course Belongs to one Sub-course
One Practice → Many Practice Questions One Practice → Many Practice Questions
Course Sub-course
└── Practice └── Practice
└── Practice Questions[] └── Practice Questions[]
@ -258,17 +258,17 @@ Practice
Complete Hierarchical Flow (Compact View) Complete Hierarchical Flow (Compact View)
Course Category Course Category
└── Course Sub-category └── Course
└── Course (with levels: BEGINNER, INTERMEDIATE, ADVANCED) └── Sub-course (with levels: BEGINNER, INTERMEDIATE, ADVANCED)
├── Course Video ├── Sub-course Video
└── Practice └── Practice
└── Practice Question └── Practice Question
Architectural Observations Architectural Observations
Simple three-level hierarchy: Category → Sub-category → Course Simple three-level hierarchy: Category → Course → Sub-course
Level is now a property of Course, not a separate entity Level is now a property of sub-course, not a separate entity
Cascade deletes ensure referential integrity Cascade deletes ensure referential integrity

View File

@ -1,316 +0,0 @@
-- ======================================================
-- Complete Course Management Seed Data
-- Covers: categories, courses, sub-courses, videos,
-- question sets, questions, options, prerequisites,
-- and user progress for admin panel integration
-- ======================================================
-- ======================================================
-- Course Categories (supplement existing 3 categories)
-- Existing: 1=Programming, 2=Data Science, 3=Web Development
-- ======================================================
INSERT INTO course_categories (id, name, is_active, created_at) VALUES
(4, 'Mobile Development', TRUE, CURRENT_TIMESTAMP),
(5, 'DevOps & Cloud', TRUE, CURRENT_TIMESTAMP),
(6, 'Cybersecurity', FALSE, CURRENT_TIMESTAMP)
ON CONFLICT (id) DO NOTHING;
-- ======================================================
-- Courses (supplement existing 7 courses)
-- Existing: 1-7 in categories 1-3
-- ======================================================
INSERT INTO courses (id, category_id, title, description, thumbnail, intro_video_url, is_active) VALUES
(8, 4, 'Flutter App Development', 'Build cross-platform mobile apps with Flutter and Dart', 'https://example.com/thumbnails/flutter.jpg', 'https://example.com/intro/flutter.mp4', TRUE),
(9, 4, 'React Native Essentials', 'Create native mobile apps using React Native', 'https://example.com/thumbnails/react-native.jpg', NULL, TRUE),
(10, 5, 'Docker & Kubernetes', 'Container orchestration and deployment strategies', 'https://example.com/thumbnails/docker-k8s.jpg', 'https://example.com/intro/docker.mp4', TRUE),
(11, 5, 'CI/CD Pipeline Mastery', 'Automate your build, test, and deployment workflows', 'https://example.com/thumbnails/cicd.jpg', NULL, FALSE),
(12, 6, 'Ethical Hacking Fundamentals', 'Learn penetration testing and security analysis', 'https://example.com/thumbnails/ethical-hacking.jpg', NULL, FALSE)
ON CONFLICT (id) DO NOTHING;
-- ======================================================
-- Sub-courses (supplement existing 17 sub-courses: IDs 1-17)
-- ======================================================
INSERT INTO sub_courses (id, course_id, title, description, thumbnail, display_order, level, is_active) VALUES
-- Flutter sub-courses (course 8) — IDs 18-21
(18, 8, 'Dart Language Basics', 'Learn Dart programming language fundamentals', NULL, 1, 'BEGINNER', TRUE),
(19, 8, 'Flutter UI Widgets', 'Build beautiful UIs with Flutter widgets', NULL, 2, 'BEGINNER', TRUE),
(20, 8, 'State Management', 'Manage app state with Provider and Riverpod', NULL, 3, 'INTERMEDIATE', TRUE),
(21, 8, 'Flutter Networking & APIs', 'HTTP requests, REST APIs, and data persistence', NULL, 4, 'ADVANCED', TRUE),
-- React Native sub-courses (course 9) — IDs 22-24
(22, 9, 'React Native Setup', 'Environment setup and first app', NULL, 1, 'BEGINNER', TRUE),
(23, 9, 'Navigation & Routing', 'React Navigation and screen management', NULL, 2, 'INTERMEDIATE', TRUE),
(24, 9, 'Native Modules', 'Bridge native code with React Native', NULL, 3, 'ADVANCED', TRUE),
-- Docker & Kubernetes sub-courses (course 10) — IDs 25-27
(25, 10, 'Docker Fundamentals', 'Containers, images, and Dockerfiles', NULL, 1, 'BEGINNER', TRUE),
(26, 10, 'Docker Compose', 'Multi-container applications', NULL, 2, 'INTERMEDIATE', TRUE),
(27, 10, 'Kubernetes Basics', 'Pods, services, and deployments', NULL, 3, 'ADVANCED', TRUE),
-- CI/CD sub-courses (course 11) — IDs 28-29
(28, 11, 'Git Workflows', 'Branching strategies and pull requests', NULL, 1, 'BEGINNER', TRUE),
(29, 11, 'GitHub Actions', 'Automate workflows with GitHub Actions', NULL, 2, 'INTERMEDIATE', TRUE),
-- Cybersecurity sub-courses (course 12) — IDs 30-31
(30, 12, 'Network Security Basics', 'Firewalls, VPNs, and network protocols', NULL, 1, 'BEGINNER', TRUE),
(31, 12, 'Penetration Testing', 'Tools and techniques for pen testing', NULL, 2, 'ADVANCED', TRUE)
ON CONFLICT (id) DO NOTHING;
-- ======================================================
-- Sub-course Videos (supplement existing 5 videos: IDs 1-5)
-- ======================================================
INSERT INTO sub_course_videos (
id, sub_course_id, title, description, video_url,
duration, resolution, visibility, display_order, status,
video_host_provider, vimeo_id, vimeo_embed_url, vimeo_status
) VALUES
-- Dart Language Basics videos (sub_course 18)
(6, 18, 'Introduction to Dart', 'Overview of Dart programming language', 'https://example.com/dart-intro.mp4', 720, '1080p', 'public', 1, 'PUBLISHED', 'DIRECT', NULL, NULL, NULL),
(7, 18, 'Variables and Data Types', 'Dart variables, constants, and types', 'https://example.com/dart-variables.mp4', 900, '1080p', 'public', 2, 'PUBLISHED', 'DIRECT', NULL, NULL, NULL),
(8, 18, 'Control Flow in Dart', 'If/else, loops, and switch statements', 'https://example.com/dart-control.mp4', 1100, '720p', 'public', 3, 'DRAFT', 'DIRECT', NULL, NULL, NULL),
-- Flutter UI Widgets videos (sub_course 19)
(9, 19, 'Widget Tree Basics', 'Understanding the Flutter widget tree', 'https://player.vimeo.com/video/100000001', 1500, '1080p', 'public', 1, 'PUBLISHED', 'VIMEO', '100000001', 'https://player.vimeo.com/video/100000001', 'available'),
(10, 19, 'Layout Widgets', 'Row, Column, Stack, and Container widgets', 'https://player.vimeo.com/video/100000002', 1800, '1080p', 'public', 2, 'PUBLISHED', 'VIMEO', '100000002', 'https://player.vimeo.com/video/100000002', 'available'),
(11, 19, 'Custom Widgets', 'Building reusable custom widgets', 'https://player.vimeo.com/video/100000003', 2100, '1080p', 'public', 3, 'DRAFT', 'VIMEO', '100000003', 'https://player.vimeo.com/video/100000003', 'transcoding'),
-- State Management videos (sub_course 20)
(12, 20, 'setState and Stateful Widgets', 'Managing local state in Flutter', 'https://example.com/flutter-setstate.mp4', 1200, '1080p', 'public', 1, 'PUBLISHED', 'DIRECT', NULL, NULL, NULL),
(13, 20, 'Provider Pattern', 'Global state management with Provider', 'https://example.com/flutter-provider.mp4', 1600, '1080p', 'public', 2, 'PUBLISHED', 'DIRECT', NULL, NULL, NULL),
-- Docker Fundamentals videos (sub_course 25)
(14, 25, 'What is Docker?', 'Introduction to containerization', 'https://example.com/docker-intro.mp4', 600, '1080p', 'public', 1, 'PUBLISHED', 'DIRECT', NULL, NULL, NULL),
(15, 25, 'Building Docker Images', 'Writing Dockerfiles and building images', 'https://example.com/docker-images.mp4', 1400, '1080p', 'public', 2, 'PUBLISHED', 'DIRECT', NULL, NULL, NULL),
-- Docker Compose videos (sub_course 26)
(16, 26, 'Docker Compose Basics', 'Defining multi-container applications', 'https://example.com/compose-basics.mp4', 1300, '1080p', 'public', 1, 'PUBLISHED', 'DIRECT', NULL, NULL, NULL),
-- React Native Setup videos (sub_course 22)
(17, 22, 'Setting Up React Native', 'Installing React Native CLI and Expo', 'https://example.com/rn-setup.mp4', 900, '1080p', 'public', 1, 'PUBLISHED', 'DIRECT', NULL, NULL, NULL),
(18, 22, 'Your First React Native App', 'Creating and running a basic app', 'https://example.com/rn-first-app.mp4', 1100, '1080p', 'public', 2, 'PUBLISHED', 'DIRECT', NULL, NULL, NULL)
ON CONFLICT (id) DO NOTHING;
-- ======================================================
-- Question Options for existing practice questions (17-20)
-- These were missing from the initial seed
-- ======================================================
INSERT INTO question_options (question_id, option_text, option_order, is_correct) VALUES
-- Q17: What is the correct way to print "Hello World" in Python?
(17, 'print("Hello World")', 1, TRUE),
(17, 'echo "Hello World"', 2, FALSE),
(17, 'console.log("Hello World")', 3, FALSE),
(17, 'System.out.println("Hello World")', 4, FALSE),
-- Q18: Which is a valid Python variable name?
(18, '2name', 1, FALSE),
(18, 'my_name', 2, TRUE),
(18, 'my-name', 3, FALSE),
(18, 'class', 4, FALSE),
-- Q19: How do you convert "123" to an integer?
(19, 'int("123")', 1, TRUE),
(19, 'integer("123")', 2, FALSE),
(19, 'str(123)', 3, FALSE),
(19, 'toInt("123")', 4, FALSE),
-- Q20: How many times does range(3) loop run?
(20, '2', 1, FALSE),
(20, '3', 2, TRUE),
(20, '4', 3, FALSE),
(20, '1', 4, FALSE);
-- ======================================================
-- Additional Practice Questions for new sub-courses
-- ======================================================
INSERT INTO questions (id, question_text, question_type, tips, status) VALUES
(21, 'What keyword is used to declare a variable in Dart?', 'MCQ', 'Dart uses var, final, or const', 'PUBLISHED'),
(22, 'Which widget is the root of every Flutter app?', 'MCQ', 'Think about the main() function', 'PUBLISHED'),
(23, 'What is a StatefulWidget?', 'MCQ', 'Consider mutable state', 'PUBLISHED'),
(24, 'What command creates a Docker container from an image?', 'MCQ', 'Think about docker run', 'PUBLISHED'),
(25, 'What file defines a Docker Compose application?', 'MCQ', 'It is a YAML file', 'PUBLISHED'),
(26, 'Which tool is used to create a new React Native project?', 'MCQ', 'Consider npx or expo', 'PUBLISHED')
ON CONFLICT (id) DO NOTHING;
INSERT INTO question_options (question_id, option_text, option_order, is_correct) VALUES
-- Q21: Dart variable declaration
(21, 'var', 1, TRUE),
(21, 'let', 2, FALSE),
(21, 'dim', 3, FALSE),
(21, 'define', 4, FALSE),
-- Q22: Root Flutter widget
(22, 'MaterialApp', 1, TRUE),
(22, 'Container', 2, FALSE),
(22, 'Scaffold', 3, FALSE),
(22, 'AppBar', 4, FALSE),
-- Q23: StatefulWidget
(23, 'A widget that can change its state during its lifetime', 1, TRUE),
(23, 'A widget that never changes', 2, FALSE),
(23, 'A widget for static content only', 3, FALSE),
(23, 'A widget that cannot have children', 4, FALSE),
-- Q24: Docker container creation
(24, 'docker run', 1, TRUE),
(24, 'docker create', 2, FALSE),
(24, 'docker start', 3, FALSE),
(24, 'docker build', 4, FALSE),
-- Q25: Docker Compose file
(25, 'docker-compose.yml', 1, TRUE),
(25, 'Dockerfile', 2, FALSE),
(25, 'docker.json', 3, FALSE),
(25, 'compose.xml', 4, FALSE),
-- Q26: React Native project creation
(26, 'npx react-native init', 1, TRUE),
(26, 'npm create react-native', 2, FALSE),
(26, 'react-native new', 3, FALSE),
(26, 'rn init', 4, FALSE);
-- ======================================================
-- Question Sets for new sub-courses
-- ======================================================
INSERT INTO question_sets (id, title, description, set_type, owner_type, owner_id, persona, status) VALUES
(5, 'Dart Basics Quiz', 'Test your Dart fundamentals', 'PRACTICE', 'SUB_COURSE', 18, 'beginner', 'PUBLISHED'),
(6, 'Flutter Widgets Assessment', 'Assess Flutter widget knowledge', 'PRACTICE', 'SUB_COURSE', 19, 'beginner', 'PUBLISHED'),
(7, 'State Management Quiz', 'Test state management concepts', 'PRACTICE', 'SUB_COURSE', 20, 'intermediate', 'DRAFT'),
(8, 'Docker Fundamentals Quiz', 'Test Docker basics', 'PRACTICE', 'SUB_COURSE', 25, 'beginner', 'PUBLISHED'),
(9, 'Docker Compose Assessment', 'Assess Docker Compose skills', 'PRACTICE', 'SUB_COURSE', 26, 'intermediate', 'PUBLISHED'),
(10, 'React Native Setup Quiz', 'Test React Native setup knowledge', 'PRACTICE', 'SUB_COURSE', 22, 'beginner', 'PUBLISHED')
ON CONFLICT (id) DO NOTHING;
-- Link questions to question sets
INSERT INTO question_set_items (set_id, question_id, display_order) VALUES
(5, 21, 1),
(6, 22, 1),
(7, 23, 1),
(8, 24, 1),
(9, 25, 1),
(10, 26, 1)
ON CONFLICT (set_id, question_id) DO NOTHING;
-- Link personas to question sets
INSERT INTO question_set_personas (question_set_id, user_id, display_order) VALUES
(5, 10, 1), (5, 11, 2),
(6, 10, 1), (6, 12, 2),
(8, 11, 1),
(10, 10, 1)
ON CONFLICT (question_set_id, user_id) DO NOTHING;
-- ======================================================
-- Sub-course Prerequisites
-- Defines the learning path / dependency graph
-- ======================================================
INSERT INTO sub_course_prerequisites (sub_course_id, prerequisite_sub_course_id) VALUES
-- Python course (IDs 1-5): linear progression
-- "Python Basics - Data Types" requires "Python Basics - Getting Started"
(2, 1),
-- "Python Intermediate - Functions" requires "Python Basics - Data Types"
(3, 2),
-- "Python Intermediate - Collections" requires "Python Intermediate - Functions"
(4, 3),
-- "Python Advanced - Best Practices" requires "Python Intermediate - Collections"
(5, 4),
-- JavaScript course (IDs 6-7): linear
-- "DOM Manipulation Basics" requires "JavaScript Fundamentals"
(7, 6),
-- Java course (IDs 8-9): linear
-- "Spring Framework Intro" requires "Java Core Concepts"
(9, 8),
-- Data Science course (IDs 10-11): linear
-- "Advanced Data Analysis" requires "Data Analysis Fundamentals"
(11, 10),
-- ML course (IDs 12-13): linear
-- "ML Algorithms" requires "ML Basics"
(13, 12),
-- Full Stack course (IDs 14-15): linear
-- "Backend Development" requires "Frontend Fundamentals"
(15, 14),
-- React course (IDs 16-17): linear
-- "React Advanced Patterns" requires "React Basics"
(17, 16),
-- Flutter course (IDs 18-21): structured path
-- "Flutter UI Widgets" requires "Dart Language Basics"
(19, 18),
-- "State Management" requires "Flutter UI Widgets"
(20, 19),
-- "Flutter Networking & APIs" requires "State Management"
(21, 20),
-- React Native course (IDs 22-24): linear
-- "Navigation & Routing" requires "React Native Setup"
(23, 22),
-- "Native Modules" requires "Navigation & Routing"
(24, 23),
-- Docker & Kubernetes course (IDs 25-27): structured
-- "Docker Compose" requires "Docker Fundamentals"
(26, 25),
-- "Kubernetes Basics" requires "Docker Compose"
(27, 26),
-- CI/CD course (IDs 28-29): linear
-- "GitHub Actions" requires "Git Workflows"
(29, 28),
-- Cybersecurity course (IDs 30-31): linear
-- "Penetration Testing" requires "Network Security Basics"
(31, 30)
ON CONFLICT (sub_course_id, prerequisite_sub_course_id) DO NOTHING;
-- ======================================================
-- User Sub-course Progress
-- Simulate realistic student progress for admin panel
-- ======================================================
INSERT INTO user_sub_course_progress (user_id, sub_course_id, status, progress_percentage, started_at, completed_at) VALUES
-- Student 10 (Demo Student): working through Python course
(10, 1, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '30 days', CURRENT_TIMESTAMP - INTERVAL '20 days'),
(10, 2, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '20 days', CURRENT_TIMESTAMP - INTERVAL '12 days'),
(10, 3, 'IN_PROGRESS', 65, CURRENT_TIMESTAMP - INTERVAL '12 days', NULL),
-- Student 10: started Flutter
(10, 18, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '15 days', CURRENT_TIMESTAMP - INTERVAL '8 days'),
(10, 19, 'IN_PROGRESS', 40, CURRENT_TIMESTAMP - INTERVAL '8 days', NULL),
-- Student 11 (Abebe): completed Python, started JavaScript
(11, 1, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '45 days', CURRENT_TIMESTAMP - INTERVAL '35 days'),
(11, 2, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '35 days', CURRENT_TIMESTAMP - INTERVAL '25 days'),
(11, 3, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '25 days', CURRENT_TIMESTAMP - INTERVAL '15 days'),
(11, 4, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '15 days', CURRENT_TIMESTAMP - INTERVAL '5 days'),
(11, 5, 'IN_PROGRESS', 30, CURRENT_TIMESTAMP - INTERVAL '5 days', NULL),
(11, 6, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '20 days', CURRENT_TIMESTAMP - INTERVAL '10 days'),
(11, 7, 'IN_PROGRESS', 50, CURRENT_TIMESTAMP - INTERVAL '10 days', NULL),
-- Student 11: Docker course
(11, 25, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '10 days', CURRENT_TIMESTAMP - INTERVAL '3 days'),
(11, 26, 'IN_PROGRESS', 20, CURRENT_TIMESTAMP - INTERVAL '3 days', NULL),
-- Student 12 (Sara): just started
(12, 1, 'IN_PROGRESS', 25, CURRENT_TIMESTAMP - INTERVAL '7 days', NULL),
(12, 18, 'IN_PROGRESS', 10, CURRENT_TIMESTAMP - INTERVAL '3 days', NULL),
(12, 22, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '14 days', CURRENT_TIMESTAMP - INTERVAL '7 days'),
(12, 23, 'IN_PROGRESS', 60, CURRENT_TIMESTAMP - INTERVAL '7 days', NULL)
ON CONFLICT (user_id, sub_course_id) DO NOTHING;
-- ======================================================
-- Reset sequences to avoid ID conflicts after seeding
-- ======================================================
SELECT setval(pg_get_serial_sequence('course_categories', 'id'), COALESCE((SELECT MAX(id) FROM course_categories), 1), true);
SELECT setval(pg_get_serial_sequence('courses', 'id'), COALESCE((SELECT MAX(id) FROM courses), 1), true);
SELECT setval(pg_get_serial_sequence('sub_courses', 'id'), COALESCE((SELECT MAX(id) FROM sub_courses), 1), true);
SELECT setval(pg_get_serial_sequence('sub_course_videos', 'id'), COALESCE((SELECT MAX(id) FROM sub_course_videos), 1), true);
SELECT setval(pg_get_serial_sequence('questions', 'id'), COALESCE((SELECT MAX(id) FROM questions), 1), true);
SELECT setval(pg_get_serial_sequence('question_options', 'id'), COALESCE((SELECT MAX(id) FROM question_options), 1), true);
SELECT setval(pg_get_serial_sequence('question_sets', 'id'), COALESCE((SELECT MAX(id) FROM question_sets), 1), true);
SELECT setval(pg_get_serial_sequence('question_set_items', 'id'), COALESCE((SELECT MAX(id) FROM question_set_items), 1), true);
SELECT setval(pg_get_serial_sequence('question_set_personas', 'id'), COALESCE((SELECT MAX(id) FROM question_set_personas), 1), true);
SELECT setval(pg_get_serial_sequence('sub_course_prerequisites', 'id'), COALESCE((SELECT MAX(id) FROM sub_course_prerequisites), 1), true);
SELECT setval(pg_get_serial_sequence('user_sub_course_progress', 'id'), COALESCE((SELECT MAX(id) FROM user_sub_course_progress), 1), true);

View File

@ -1,15 +0,0 @@
-- Revert AUDIO question type changes
-- 1. Drop question_audio_answers table
DROP TABLE IF EXISTS question_audio_answers CASCADE;
-- 2. Remove image_url column
ALTER TABLE questions DROP COLUMN IF EXISTS image_url;
-- 3. Revert question_type CHECK constraint
ALTER TABLE questions
DROP CONSTRAINT IF EXISTS questions_question_type_check;
ALTER TABLE questions
ADD CONSTRAINT questions_question_type_check
CHECK (question_type IN ('MCQ', 'TRUE_FALSE', 'SHORT_ANSWER'));

View File

@ -1,24 +0,0 @@
-- Add AUDIO question type and image_url support
-- 1. Extend question_type CHECK constraint to include AUDIO
ALTER TABLE questions
DROP CONSTRAINT IF EXISTS questions_question_type_check;
ALTER TABLE questions
ADD CONSTRAINT questions_question_type_check
CHECK (question_type IN ('MCQ', 'TRUE_FALSE', 'SHORT_ANSWER', 'AUDIO'));
-- 2. Add image_url column to questions
ALTER TABLE questions
ADD COLUMN IF NOT EXISTS image_url TEXT;
-- 3. Create question_audio_answers table for storing correct answer text
CREATE TABLE IF NOT EXISTS question_audio_answers (
id BIGSERIAL PRIMARY KEY,
question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE CASCADE,
correct_answer_text TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_question_audio_answers_question_id
ON question_audio_answers(question_id);

View File

@ -1,3 +0,0 @@
ALTER TABLE course_categories DROP COLUMN display_order;
ALTER TABLE courses DROP COLUMN display_order;
ALTER TABLE question_sets DROP COLUMN display_order;

View File

@ -1,3 +0,0 @@
ALTER TABLE course_categories ADD COLUMN display_order INT NOT NULL DEFAULT 0;
ALTER TABLE courses ADD COLUMN display_order INT NOT NULL DEFAULT 0;
ALTER TABLE question_sets ADD COLUMN display_order INT NOT NULL DEFAULT 0;

View File

@ -21,7 +21,7 @@ SELECT
is_active, is_active,
created_at created_at
FROM course_categories FROM course_categories
ORDER BY display_order ASC, created_at DESC ORDER BY created_at DESC
LIMIT sqlc.narg('limit')::INT LIMIT sqlc.narg('limit')::INT
OFFSET sqlc.narg('offset')::INT; OFFSET sqlc.narg('offset')::INT;
@ -37,11 +37,3 @@ WHERE id = $3;
-- name: DeleteCourseCategory :exec -- name: DeleteCourseCategory :exec
DELETE FROM course_categories DELETE FROM course_categories
WHERE id = $1; WHERE id = $1;
-- name: ReorderCourseCategories :exec
UPDATE course_categories
SET display_order = bulk.position
FROM (
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
) AS bulk
WHERE course_categories.id = bulk.id;

View File

@ -29,7 +29,7 @@ SELECT
is_active is_active
FROM courses FROM courses
WHERE category_id = $1 WHERE category_id = $1
ORDER BY display_order ASC, id ASC ORDER BY id DESC
LIMIT sqlc.narg('limit')::INT LIMIT sqlc.narg('limit')::INT
OFFSET sqlc.narg('offset')::INT; OFFSET sqlc.narg('offset')::INT;
@ -48,11 +48,3 @@ WHERE id = $6;
-- name: DeleteCourse :exec -- name: DeleteCourse :exec
DELETE FROM courses DELETE FROM courses
WHERE id = $1; WHERE id = $1;
-- name: ReorderCourses :exec
UPDATE courses
SET display_order = bulk.position
FROM (
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
) AS bulk
WHERE courses.id = bulk.id;

View File

@ -9,49 +9,3 @@ FROM courses c
LEFT JOIN sub_courses sc ON sc.course_id = c.id AND sc.is_active = true LEFT JOIN sub_courses sc ON sc.course_id = c.id AND sc.is_active = true
WHERE c.is_active = true WHERE c.is_active = true
ORDER BY c.id, sc.display_order, sc.id; ORDER BY c.id, sc.display_order, sc.id;
-- name: GetCourseLearningPath :many
SELECT
c.id AS course_id,
c.title AS course_title,
c.description AS course_description,
c.thumbnail AS course_thumbnail,
c.intro_video_url AS course_intro_video_url,
cc.id AS category_id,
cc.name AS category_name,
sc.id AS sub_course_id,
sc.title AS sub_course_title,
sc.description AS sub_course_description,
sc.thumbnail AS sub_course_thumbnail,
sc.display_order AS sub_course_display_order,
sc.level AS sub_course_level,
(SELECT COUNT(*) FROM sub_course_prerequisites WHERE sub_course_id = sc.id) AS prerequisite_count,
(SELECT COUNT(*) FROM sub_course_videos WHERE sub_course_id = sc.id AND status = 'PUBLISHED') AS video_count,
(SELECT COUNT(*) FROM question_sets WHERE set_type = 'PRACTICE' AND owner_type = 'SUB_COURSE' AND owner_id = sc.id AND status = 'PUBLISHED') AS practice_count
FROM courses c
JOIN course_categories cc ON cc.id = c.category_id
LEFT JOIN sub_courses sc ON sc.course_id = c.id AND sc.is_active = true
WHERE c.id = $1
ORDER BY sc.display_order, sc.id;
-- name: GetSubCourseVideosForLearningPath :many
SELECT id, title, description, video_url, duration, resolution, display_order,
vimeo_id, vimeo_embed_url, video_host_provider
FROM sub_course_videos
WHERE sub_course_id = $1 AND status = 'PUBLISHED'
ORDER BY display_order, id;
-- name: GetSubCoursePracticesForLearningPath :many
SELECT id, title, description, persona, status,
(SELECT COUNT(*) FROM question_set_items WHERE set_id = qs.id) AS question_count
FROM question_sets qs
WHERE qs.owner_type = 'SUB_COURSE' AND qs.owner_id = $1
AND qs.set_type = 'PRACTICE' AND qs.status = 'PUBLISHED'
ORDER BY qs.display_order ASC, qs.created_at;
-- name: GetSubCoursePrerequisitesForLearningPath :many
SELECT p.prerequisite_sub_course_id, sc.title, sc.level
FROM sub_course_prerequisites p
JOIN sub_courses sc ON sc.id = p.prerequisite_sub_course_id
WHERE p.sub_course_id = $1
ORDER BY sc.display_order;

View File

@ -1,18 +0,0 @@
-- name: CreateQuestionAudioAnswer :one
INSERT INTO question_audio_answers (question_id, correct_answer_text)
VALUES ($1, $2)
RETURNING *;
-- name: GetAudioAnswerByQuestionID :one
SELECT *
FROM question_audio_answers
WHERE question_id = $1;
-- name: GetAudioAnswersByQuestionIDs :many
SELECT *
FROM question_audio_answers
WHERE question_id = ANY($1::BIGINT[]);
-- name: DeleteAudioAnswerByQuestionID :exec
DELETE FROM question_audio_answers
WHERE question_id = $1;

View File

@ -21,7 +21,6 @@ SELECT
q.explanation, q.explanation,
q.tips, q.tips,
q.voice_prompt, q.voice_prompt,
q.image_url,
q.status as question_status q.status as question_status
FROM question_set_items qsi FROM question_set_items qsi
JOIN questions q ON q.id = qsi.question_id JOIN questions q ON q.id = qsi.question_id
@ -41,8 +40,7 @@ SELECT
q.points, q.points,
q.explanation, q.explanation,
q.tips, q.tips,
q.voice_prompt, q.voice_prompt
q.image_url
FROM question_set_items qsi FROM question_set_items qsi
JOIN questions q ON q.id = qsi.question_id JOIN questions q ON q.id = qsi.question_id
WHERE qsi.set_id = $1 WHERE qsi.set_id = $1

View File

@ -27,7 +27,7 @@ FROM question_sets
WHERE owner_type = $1 WHERE owner_type = $1
AND owner_id = $2 AND owner_id = $2
AND status != 'ARCHIVED' AND status != 'ARCHIVED'
ORDER BY display_order ASC, created_at DESC; ORDER BY created_at DESC;
-- name: GetQuestionSetsByType :many -- name: GetQuestionSetsByType :many
SELECT SELECT
@ -114,11 +114,3 @@ SET
sub_course_video_id = $1, sub_course_video_id = $1,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $2; WHERE id = $2;
-- name: ReorderQuestionSets :exec
UPDATE question_sets
SET display_order = bulk.position
FROM (
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
) AS bulk
WHERE question_sets.id = bulk.id;

View File

@ -8,10 +8,9 @@ INSERT INTO questions (
tips, tips,
voice_prompt, voice_prompt,
sample_answer_voice_prompt, sample_answer_voice_prompt,
image_url,
status status
) )
VALUES ($1, $2, $3, COALESCE($4, 1), $5, $6, $7, $8, $9, COALESCE($10, 'DRAFT')) VALUES ($1, $2, $3, COALESCE($4, 1), $5, $6, $7, $8, COALESCE($9, 'DRAFT'))
RETURNING *; RETURNING *;
-- name: GetQuestionByID :one -- name: GetQuestionByID :one
@ -60,10 +59,9 @@ SET
tips = COALESCE($6, tips), tips = COALESCE($6, tips),
voice_prompt = COALESCE($7, voice_prompt), voice_prompt = COALESCE($7, voice_prompt),
sample_answer_voice_prompt = COALESCE($8, sample_answer_voice_prompt), sample_answer_voice_prompt = COALESCE($8, sample_answer_voice_prompt),
image_url = COALESCE($9, image_url), status = COALESCE($9, status),
status = COALESCE($10, status),
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $11; WHERE id = $10;
-- name: ArchiveQuestion :exec -- name: ArchiveQuestion :exec
UPDATE questions UPDATE questions

View File

@ -112,11 +112,3 @@ WHERE id = $1;
-- name: DeleteSubCourseVideo :exec -- name: DeleteSubCourseVideo :exec
DELETE FROM sub_course_videos DELETE FROM sub_course_videos
WHERE id = $1; WHERE id = $1;
-- name: ReorderSubCourseVideos :exec
UPDATE sub_course_videos
SET display_order = bulk.position
FROM (
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
) AS bulk
WHERE sub_course_videos.id = bulk.id;

View File

@ -80,11 +80,3 @@ RETURNING *;
UPDATE sub_courses UPDATE sub_courses
SET is_active = FALSE SET is_active = FALSE
WHERE id = $1; WHERE id = $1;
-- name: ReorderSubCourses :exec
UPDATE sub_courses
SET display_order = bulk.position
FROM (
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
) AS bulk
WHERE sub_courses.id = bulk.id;

View File

@ -376,13 +376,6 @@ SET
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $2; WHERE id = $2;
-- name: GetUserSummary :one
SELECT
COUNT(*) AS total_users,
COUNT(*) FILTER (WHERE status = 'ACTIVE') AS active_users,
COUNT(*) FILTER (WHERE created_at >= date_trunc('month', CURRENT_DATE)) AS joined_this_month
FROM users;
-- name: UpdateUserKnowledgeLevel :exec -- name: UpdateUserKnowledgeLevel :exec
UPDATE users UPDATE users
SET SET

View File

@ -5,7 +5,7 @@ services:
container_name: yimaru-backend-postgres-1 container_name: yimaru-backend-postgres-1
image: postgres:16-alpine image: postgres:16-alpine
ports: ports:
- "5592:5422" - "5432:5422"
environment: environment:
- POSTGRES_PASSWORD=secret - POSTGRES_PASSWORD=secret
- POSTGRES_USER=root - POSTGRES_USER=root

View File

@ -17,7 +17,7 @@ INSERT INTO course_categories (
is_active is_active
) )
VALUES ($1, COALESCE($2, true)) VALUES ($1, COALESCE($2, true))
RETURNING id, name, is_active, created_at, display_order RETURNING id, name, is_active, created_at
` `
type CreateCourseCategoryParams struct { type CreateCourseCategoryParams struct {
@ -33,7 +33,6 @@ func (q *Queries) CreateCourseCategory(ctx context.Context, arg CreateCourseCate
&i.Name, &i.Name,
&i.IsActive, &i.IsActive,
&i.CreatedAt, &i.CreatedAt,
&i.DisplayOrder,
) )
return i, err return i, err
} }
@ -56,7 +55,7 @@ SELECT
is_active, is_active,
created_at created_at
FROM course_categories FROM course_categories
ORDER BY display_order ASC, created_at DESC ORDER BY created_at DESC
LIMIT $2::INT LIMIT $2::INT
OFFSET $1::INT OFFSET $1::INT
` `
@ -101,7 +100,7 @@ func (q *Queries) GetAllCourseCategories(ctx context.Context, arg GetAllCourseCa
} }
const GetCourseCategoryByID = `-- name: GetCourseCategoryByID :one const GetCourseCategoryByID = `-- name: GetCourseCategoryByID :one
SELECT id, name, is_active, created_at, display_order SELECT id, name, is_active, created_at
FROM course_categories FROM course_categories
WHERE id = $1 WHERE id = $1
` `
@ -114,30 +113,10 @@ func (q *Queries) GetCourseCategoryByID(ctx context.Context, id int64) (CourseCa
&i.Name, &i.Name,
&i.IsActive, &i.IsActive,
&i.CreatedAt, &i.CreatedAt,
&i.DisplayOrder,
) )
return i, err return i, err
} }
const ReorderCourseCategories = `-- name: ReorderCourseCategories :exec
UPDATE course_categories
SET display_order = bulk.position
FROM (
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
) AS bulk
WHERE course_categories.id = bulk.id
`
type ReorderCourseCategoriesParams struct {
Ids []int64 `json:"ids"`
Positions []int32 `json:"positions"`
}
func (q *Queries) ReorderCourseCategories(ctx context.Context, arg ReorderCourseCategoriesParams) error {
_, err := q.db.Exec(ctx, ReorderCourseCategories, arg.Ids, arg.Positions)
return err
}
const UpdateCourseCategory = `-- name: UpdateCourseCategory :exec const UpdateCourseCategory = `-- name: UpdateCourseCategory :exec
UPDATE course_categories UPDATE course_categories
SET SET

View File

@ -21,7 +21,7 @@ INSERT INTO courses (
is_active is_active
) )
VALUES ($1, $2, $3, $4, $5, COALESCE($6, true)) VALUES ($1, $2, $3, $4, $5, COALESCE($6, true))
RETURNING id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order RETURNING id, category_id, title, description, is_active, thumbnail, intro_video_url
` `
type CreateCourseParams struct { type CreateCourseParams struct {
@ -51,7 +51,6 @@ func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Cou
&i.IsActive, &i.IsActive,
&i.Thumbnail, &i.Thumbnail,
&i.IntroVideoUrl, &i.IntroVideoUrl,
&i.DisplayOrder,
) )
return i, err return i, err
} }
@ -67,7 +66,7 @@ func (q *Queries) DeleteCourse(ctx context.Context, id int64) error {
} }
const GetCourseByID = `-- name: GetCourseByID :one const GetCourseByID = `-- name: GetCourseByID :one
SELECT id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order SELECT id, category_id, title, description, is_active, thumbnail, intro_video_url
FROM courses FROM courses
WHERE id = $1 WHERE id = $1
` `
@ -83,7 +82,6 @@ func (q *Queries) GetCourseByID(ctx context.Context, id int64) (Course, error) {
&i.IsActive, &i.IsActive,
&i.Thumbnail, &i.Thumbnail,
&i.IntroVideoUrl, &i.IntroVideoUrl,
&i.DisplayOrder,
) )
return i, err return i, err
} }
@ -100,7 +98,7 @@ SELECT
is_active is_active
FROM courses FROM courses
WHERE category_id = $1 WHERE category_id = $1
ORDER BY display_order ASC, id ASC ORDER BY id DESC
LIMIT $3::INT LIMIT $3::INT
OFFSET $2::INT OFFSET $2::INT
` `
@ -151,25 +149,6 @@ func (q *Queries) GetCoursesByCategory(ctx context.Context, arg GetCoursesByCate
return items, nil return items, nil
} }
const ReorderCourses = `-- name: ReorderCourses :exec
UPDATE courses
SET display_order = bulk.position
FROM (
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
) AS bulk
WHERE courses.id = bulk.id
`
type ReorderCoursesParams struct {
Ids []int64 `json:"ids"`
Positions []int32 `json:"positions"`
}
func (q *Queries) ReorderCourses(ctx context.Context, arg ReorderCoursesParams) error {
_, err := q.db.Exec(ctx, ReorderCourses, arg.Ids, arg.Positions)
return err
}
const UpdateCourse = `-- name: UpdateCourse :exec const UpdateCourse = `-- name: UpdateCourse :exec
UPDATE courses UPDATE courses
SET SET

View File

@ -11,87 +11,6 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
const GetCourseLearningPath = `-- name: GetCourseLearningPath :many
SELECT
c.id AS course_id,
c.title AS course_title,
c.description AS course_description,
c.thumbnail AS course_thumbnail,
c.intro_video_url AS course_intro_video_url,
cc.id AS category_id,
cc.name AS category_name,
sc.id AS sub_course_id,
sc.title AS sub_course_title,
sc.description AS sub_course_description,
sc.thumbnail AS sub_course_thumbnail,
sc.display_order AS sub_course_display_order,
sc.level AS sub_course_level,
(SELECT COUNT(*) FROM sub_course_prerequisites WHERE sub_course_id = sc.id) AS prerequisite_count,
(SELECT COUNT(*) FROM sub_course_videos WHERE sub_course_id = sc.id AND status = 'PUBLISHED') AS video_count,
(SELECT COUNT(*) FROM question_sets WHERE set_type = 'PRACTICE' AND owner_type = 'SUB_COURSE' AND owner_id = sc.id AND status = 'PUBLISHED') AS practice_count
FROM courses c
JOIN course_categories cc ON cc.id = c.category_id
LEFT JOIN sub_courses sc ON sc.course_id = c.id AND sc.is_active = true
WHERE c.id = $1
ORDER BY sc.display_order, sc.id
`
type GetCourseLearningPathRow struct {
CourseID int64 `json:"course_id"`
CourseTitle string `json:"course_title"`
CourseDescription pgtype.Text `json:"course_description"`
CourseThumbnail pgtype.Text `json:"course_thumbnail"`
CourseIntroVideoUrl pgtype.Text `json:"course_intro_video_url"`
CategoryID int64 `json:"category_id"`
CategoryName string `json:"category_name"`
SubCourseID pgtype.Int8 `json:"sub_course_id"`
SubCourseTitle pgtype.Text `json:"sub_course_title"`
SubCourseDescription pgtype.Text `json:"sub_course_description"`
SubCourseThumbnail pgtype.Text `json:"sub_course_thumbnail"`
SubCourseDisplayOrder pgtype.Int4 `json:"sub_course_display_order"`
SubCourseLevel pgtype.Text `json:"sub_course_level"`
PrerequisiteCount int64 `json:"prerequisite_count"`
VideoCount int64 `json:"video_count"`
PracticeCount int64 `json:"practice_count"`
}
func (q *Queries) GetCourseLearningPath(ctx context.Context, id int64) ([]GetCourseLearningPathRow, error) {
rows, err := q.db.Query(ctx, GetCourseLearningPath, id)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetCourseLearningPathRow
for rows.Next() {
var i GetCourseLearningPathRow
if err := rows.Scan(
&i.CourseID,
&i.CourseTitle,
&i.CourseDescription,
&i.CourseThumbnail,
&i.CourseIntroVideoUrl,
&i.CategoryID,
&i.CategoryName,
&i.SubCourseID,
&i.SubCourseTitle,
&i.SubCourseDescription,
&i.SubCourseThumbnail,
&i.SubCourseDisplayOrder,
&i.SubCourseLevel,
&i.PrerequisiteCount,
&i.VideoCount,
&i.PracticeCount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetFullLearningTree = `-- name: GetFullLearningTree :many const GetFullLearningTree = `-- name: GetFullLearningTree :many
SELECT SELECT
c.id AS course_id, c.id AS course_id,
@ -138,134 +57,3 @@ func (q *Queries) GetFullLearningTree(ctx context.Context) ([]GetFullLearningTre
} }
return items, nil return items, nil
} }
const GetSubCoursePracticesForLearningPath = `-- name: GetSubCoursePracticesForLearningPath :many
SELECT id, title, description, persona, status,
(SELECT COUNT(*) FROM question_set_items WHERE set_id = qs.id) AS question_count
FROM question_sets qs
WHERE qs.owner_type = 'SUB_COURSE' AND qs.owner_id = $1
AND qs.set_type = 'PRACTICE' AND qs.status = 'PUBLISHED'
ORDER BY qs.display_order ASC, qs.created_at
`
type GetSubCoursePracticesForLearningPathRow struct {
ID int64 `json:"id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
Persona pgtype.Text `json:"persona"`
Status string `json:"status"`
QuestionCount int64 `json:"question_count"`
}
func (q *Queries) GetSubCoursePracticesForLearningPath(ctx context.Context, ownerID pgtype.Int8) ([]GetSubCoursePracticesForLearningPathRow, error) {
rows, err := q.db.Query(ctx, GetSubCoursePracticesForLearningPath, ownerID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetSubCoursePracticesForLearningPathRow
for rows.Next() {
var i GetSubCoursePracticesForLearningPathRow
if err := rows.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.Persona,
&i.Status,
&i.QuestionCount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetSubCoursePrerequisitesForLearningPath = `-- name: GetSubCoursePrerequisitesForLearningPath :many
SELECT p.prerequisite_sub_course_id, sc.title, sc.level
FROM sub_course_prerequisites p
JOIN sub_courses sc ON sc.id = p.prerequisite_sub_course_id
WHERE p.sub_course_id = $1
ORDER BY sc.display_order
`
type GetSubCoursePrerequisitesForLearningPathRow struct {
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id"`
Title string `json:"title"`
Level string `json:"level"`
}
func (q *Queries) GetSubCoursePrerequisitesForLearningPath(ctx context.Context, subCourseID int64) ([]GetSubCoursePrerequisitesForLearningPathRow, error) {
rows, err := q.db.Query(ctx, GetSubCoursePrerequisitesForLearningPath, subCourseID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetSubCoursePrerequisitesForLearningPathRow
for rows.Next() {
var i GetSubCoursePrerequisitesForLearningPathRow
if err := rows.Scan(&i.PrerequisiteSubCourseID, &i.Title, &i.Level); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetSubCourseVideosForLearningPath = `-- name: GetSubCourseVideosForLearningPath :many
SELECT id, title, description, video_url, duration, resolution, display_order,
vimeo_id, vimeo_embed_url, video_host_provider
FROM sub_course_videos
WHERE sub_course_id = $1 AND status = 'PUBLISHED'
ORDER BY display_order, id
`
type GetSubCourseVideosForLearningPathRow struct {
ID int64 `json:"id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
VideoUrl string `json:"video_url"`
Duration int32 `json:"duration"`
Resolution pgtype.Text `json:"resolution"`
DisplayOrder int32 `json:"display_order"`
VimeoID pgtype.Text `json:"vimeo_id"`
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
VideoHostProvider pgtype.Text `json:"video_host_provider"`
}
func (q *Queries) GetSubCourseVideosForLearningPath(ctx context.Context, subCourseID int64) ([]GetSubCourseVideosForLearningPathRow, error) {
rows, err := q.db.Query(ctx, GetSubCourseVideosForLearningPath, subCourseID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetSubCourseVideosForLearningPathRow
for rows.Next() {
var i GetSubCourseVideosForLearningPathRow
if err := rows.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.VideoUrl,
&i.Duration,
&i.Resolution,
&i.DisplayOrder,
&i.VimeoID,
&i.VimeoEmbedUrl,
&i.VideoHostProvider,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -30,7 +30,6 @@ type Course struct {
IsActive bool `json:"is_active"` IsActive bool `json:"is_active"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
IntroVideoUrl pgtype.Text `json:"intro_video_url"` IntroVideoUrl pgtype.Text `json:"intro_video_url"`
DisplayOrder int32 `json:"display_order"`
} }
type CourseCategory struct { type CourseCategory struct {
@ -38,7 +37,6 @@ type CourseCategory struct {
Name string `json:"name"` Name string `json:"name"`
IsActive bool `json:"is_active"` IsActive bool `json:"is_active"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
DisplayOrder int32 `json:"display_order"`
} }
type Device struct { type Device struct {
@ -137,14 +135,6 @@ type Question struct {
Status string `json:"status"` Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ImageUrl pgtype.Text `json:"image_url"`
}
type QuestionAudioAnswer struct {
ID int64 `json:"id"`
QuestionID int64 `json:"question_id"`
CorrectAnswerText string `json:"correct_answer_text"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
} }
type QuestionOption struct { type QuestionOption struct {
@ -172,7 +162,6 @@ type QuestionSet struct {
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"` SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
DisplayOrder int32 `json:"display_order"`
} }
type QuestionSetItem struct { type QuestionSetItem struct {

View File

@ -1,92 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: question_audio_answers.sql
package dbgen
import (
"context"
)
const CreateQuestionAudioAnswer = `-- name: CreateQuestionAudioAnswer :one
INSERT INTO question_audio_answers (question_id, correct_answer_text)
VALUES ($1, $2)
RETURNING id, question_id, correct_answer_text, created_at
`
type CreateQuestionAudioAnswerParams struct {
QuestionID int64 `json:"question_id"`
CorrectAnswerText string `json:"correct_answer_text"`
}
func (q *Queries) CreateQuestionAudioAnswer(ctx context.Context, arg CreateQuestionAudioAnswerParams) (QuestionAudioAnswer, error) {
row := q.db.QueryRow(ctx, CreateQuestionAudioAnswer, arg.QuestionID, arg.CorrectAnswerText)
var i QuestionAudioAnswer
err := row.Scan(
&i.ID,
&i.QuestionID,
&i.CorrectAnswerText,
&i.CreatedAt,
)
return i, err
}
const DeleteAudioAnswerByQuestionID = `-- name: DeleteAudioAnswerByQuestionID :exec
DELETE FROM question_audio_answers
WHERE question_id = $1
`
func (q *Queries) DeleteAudioAnswerByQuestionID(ctx context.Context, questionID int64) error {
_, err := q.db.Exec(ctx, DeleteAudioAnswerByQuestionID, questionID)
return err
}
const GetAudioAnswerByQuestionID = `-- name: GetAudioAnswerByQuestionID :one
SELECT id, question_id, correct_answer_text, created_at
FROM question_audio_answers
WHERE question_id = $1
`
func (q *Queries) GetAudioAnswerByQuestionID(ctx context.Context, questionID int64) (QuestionAudioAnswer, error) {
row := q.db.QueryRow(ctx, GetAudioAnswerByQuestionID, questionID)
var i QuestionAudioAnswer
err := row.Scan(
&i.ID,
&i.QuestionID,
&i.CorrectAnswerText,
&i.CreatedAt,
)
return i, err
}
const GetAudioAnswersByQuestionIDs = `-- name: GetAudioAnswersByQuestionIDs :many
SELECT id, question_id, correct_answer_text, created_at
FROM question_audio_answers
WHERE question_id = ANY($1::BIGINT[])
`
func (q *Queries) GetAudioAnswersByQuestionIDs(ctx context.Context, dollar_1 []int64) ([]QuestionAudioAnswer, error) {
rows, err := q.db.Query(ctx, GetAudioAnswersByQuestionIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
var items []QuestionAudioAnswer
for rows.Next() {
var i QuestionAudioAnswer
if err := rows.Scan(
&i.ID,
&i.QuestionID,
&i.CorrectAnswerText,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -68,8 +68,7 @@ SELECT
q.points, q.points,
q.explanation, q.explanation,
q.tips, q.tips,
q.voice_prompt, q.voice_prompt
q.image_url
FROM question_set_items qsi FROM question_set_items qsi
JOIN questions q ON q.id = qsi.question_id JOIN questions q ON q.id = qsi.question_id
WHERE qsi.set_id = $1 WHERE qsi.set_id = $1
@ -89,7 +88,6 @@ type GetPublishedQuestionsInSetRow struct {
Explanation pgtype.Text `json:"explanation"` Explanation pgtype.Text `json:"explanation"`
Tips pgtype.Text `json:"tips"` Tips pgtype.Text `json:"tips"`
VoicePrompt pgtype.Text `json:"voice_prompt"` VoicePrompt pgtype.Text `json:"voice_prompt"`
ImageUrl pgtype.Text `json:"image_url"`
} }
func (q *Queries) GetPublishedQuestionsInSet(ctx context.Context, setID int64) ([]GetPublishedQuestionsInSetRow, error) { func (q *Queries) GetPublishedQuestionsInSet(ctx context.Context, setID int64) ([]GetPublishedQuestionsInSetRow, error) {
@ -113,7 +111,6 @@ func (q *Queries) GetPublishedQuestionsInSet(ctx context.Context, setID int64) (
&i.Explanation, &i.Explanation,
&i.Tips, &i.Tips,
&i.VoicePrompt, &i.VoicePrompt,
&i.ImageUrl,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -138,7 +135,6 @@ SELECT
q.explanation, q.explanation,
q.tips, q.tips,
q.voice_prompt, q.voice_prompt,
q.image_url,
q.status as question_status q.status as question_status
FROM question_set_items qsi FROM question_set_items qsi
JOIN questions q ON q.id = qsi.question_id JOIN questions q ON q.id = qsi.question_id
@ -159,7 +155,6 @@ type GetQuestionSetItemsRow struct {
Explanation pgtype.Text `json:"explanation"` Explanation pgtype.Text `json:"explanation"`
Tips pgtype.Text `json:"tips"` Tips pgtype.Text `json:"tips"`
VoicePrompt pgtype.Text `json:"voice_prompt"` VoicePrompt pgtype.Text `json:"voice_prompt"`
ImageUrl pgtype.Text `json:"image_url"`
QuestionStatus string `json:"question_status"` QuestionStatus string `json:"question_status"`
} }
@ -184,7 +179,6 @@ func (q *Queries) GetQuestionSetItems(ctx context.Context, setID int64) ([]GetQu
&i.Explanation, &i.Explanation,
&i.Tips, &i.Tips,
&i.VoicePrompt, &i.VoicePrompt,
&i.ImageUrl,
&i.QuestionStatus, &i.QuestionStatus,
); err != nil { ); err != nil {
return nil, err return nil, err
@ -198,7 +192,7 @@ func (q *Queries) GetQuestionSetItems(ctx context.Context, setID int64) ([]GetQu
} }
const GetQuestionSetsContainingQuestion = `-- name: GetQuestionSetsContainingQuestion :many const GetQuestionSetsContainingQuestion = `-- name: GetQuestionSetsContainingQuestion :many
SELECT qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id, qs.display_order SELECT qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id
FROM question_sets qs FROM question_sets qs
JOIN question_set_items qsi ON qsi.set_id = qs.id JOIN question_set_items qsi ON qsi.set_id = qs.id
WHERE qsi.question_id = $1 WHERE qsi.question_id = $1
@ -230,7 +224,6 @@ func (q *Queries) GetQuestionSetsContainingQuestion(ctx context.Context, questio
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.SubCourseVideoID, &i.SubCourseVideoID,
&i.DisplayOrder,
); err != nil { ); err != nil {
return nil, err return nil, err
} }

View File

@ -67,7 +67,7 @@ INSERT INTO question_sets (
sub_course_video_id sub_course_video_id
) )
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12)
RETURNING id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order RETURNING id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id
` `
type CreateQuestionSetParams struct { type CreateQuestionSetParams struct {
@ -117,7 +117,6 @@ func (q *Queries) CreateQuestionSet(ctx context.Context, arg CreateQuestionSetPa
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.SubCourseVideoID, &i.SubCourseVideoID,
&i.DisplayOrder,
) )
return i, err return i, err
} }
@ -133,7 +132,7 @@ func (q *Queries) DeleteQuestionSet(ctx context.Context, id int64) error {
} }
const GetInitialAssessmentSet = `-- name: GetInitialAssessmentSet :one const GetInitialAssessmentSet = `-- name: GetInitialAssessmentSet :one
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id
FROM question_sets FROM question_sets
WHERE set_type = 'INITIAL_ASSESSMENT' WHERE set_type = 'INITIAL_ASSESSMENT'
AND status = 'PUBLISHED' AND status = 'PUBLISHED'
@ -160,13 +159,12 @@ func (q *Queries) GetInitialAssessmentSet(ctx context.Context) (QuestionSet, err
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.SubCourseVideoID, &i.SubCourseVideoID,
&i.DisplayOrder,
) )
return i, err return i, err
} }
const GetPublishedQuestionSetsByOwner = `-- name: GetPublishedQuestionSetsByOwner :many const GetPublishedQuestionSetsByOwner = `-- name: GetPublishedQuestionSetsByOwner :many
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id
FROM question_sets FROM question_sets
WHERE owner_type = $1 WHERE owner_type = $1
AND owner_id = $2 AND owner_id = $2
@ -204,7 +202,6 @@ func (q *Queries) GetPublishedQuestionSetsByOwner(ctx context.Context, arg GetPu
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.SubCourseVideoID, &i.SubCourseVideoID,
&i.DisplayOrder,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -217,7 +214,7 @@ func (q *Queries) GetPublishedQuestionSetsByOwner(ctx context.Context, arg GetPu
} }
const GetQuestionSetByID = `-- name: GetQuestionSetByID :one const GetQuestionSetByID = `-- name: GetQuestionSetByID :one
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id
FROM question_sets FROM question_sets
WHERE id = $1 WHERE id = $1
` `
@ -241,18 +238,17 @@ func (q *Queries) GetQuestionSetByID(ctx context.Context, id int64) (QuestionSet
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.SubCourseVideoID, &i.SubCourseVideoID,
&i.DisplayOrder,
) )
return i, err return i, err
} }
const GetQuestionSetsByOwner = `-- name: GetQuestionSetsByOwner :many const GetQuestionSetsByOwner = `-- name: GetQuestionSetsByOwner :many
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id
FROM question_sets FROM question_sets
WHERE owner_type = $1 WHERE owner_type = $1
AND owner_id = $2 AND owner_id = $2
AND status != 'ARCHIVED' AND status != 'ARCHIVED'
ORDER BY display_order ASC, created_at DESC ORDER BY created_at DESC
` `
type GetQuestionSetsByOwnerParams struct { type GetQuestionSetsByOwnerParams struct {
@ -285,7 +281,6 @@ func (q *Queries) GetQuestionSetsByOwner(ctx context.Context, arg GetQuestionSet
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.SubCourseVideoID, &i.SubCourseVideoID,
&i.DisplayOrder,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -300,7 +295,7 @@ func (q *Queries) GetQuestionSetsByOwner(ctx context.Context, arg GetQuestionSet
const GetQuestionSetsByType = `-- name: GetQuestionSetsByType :many const GetQuestionSetsByType = `-- name: GetQuestionSetsByType :many
SELECT SELECT
COUNT(*) OVER () AS total_count, COUNT(*) OVER () AS total_count,
qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id, qs.display_order qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id
FROM question_sets qs FROM question_sets qs
WHERE set_type = $1 WHERE set_type = $1
AND status != 'ARCHIVED' AND status != 'ARCHIVED'
@ -332,7 +327,6 @@ type GetQuestionSetsByTypeRow struct {
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"` SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
DisplayOrder int32 `json:"display_order"`
} }
func (q *Queries) GetQuestionSetsByType(ctx context.Context, arg GetQuestionSetsByTypeParams) ([]GetQuestionSetsByTypeRow, error) { func (q *Queries) GetQuestionSetsByType(ctx context.Context, arg GetQuestionSetsByTypeParams) ([]GetQuestionSetsByTypeRow, error) {
@ -361,7 +355,6 @@ func (q *Queries) GetQuestionSetsByType(ctx context.Context, arg GetQuestionSets
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.SubCourseVideoID, &i.SubCourseVideoID,
&i.DisplayOrder,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -442,25 +435,6 @@ func (q *Queries) RemoveUserPersonaFromQuestionSet(ctx context.Context, arg Remo
return err return err
} }
const ReorderQuestionSets = `-- name: ReorderQuestionSets :exec
UPDATE question_sets
SET display_order = bulk.position
FROM (
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
) AS bulk
WHERE question_sets.id = bulk.id
`
type ReorderQuestionSetsParams struct {
Ids []int64 `json:"ids"`
Positions []int32 `json:"positions"`
}
func (q *Queries) ReorderQuestionSets(ctx context.Context, arg ReorderQuestionSetsParams) error {
_, err := q.db.Exec(ctx, ReorderQuestionSets, arg.Ids, arg.Positions)
return err
}
const UpdateQuestionSet = `-- name: UpdateQuestionSet :exec const UpdateQuestionSet = `-- name: UpdateQuestionSet :exec
UPDATE question_sets UPDATE question_sets
SET SET

View File

@ -32,11 +32,10 @@ INSERT INTO questions (
tips, tips,
voice_prompt, voice_prompt,
sample_answer_voice_prompt, sample_answer_voice_prompt,
image_url,
status status
) )
VALUES ($1, $2, $3, COALESCE($4, 1), $5, $6, $7, $8, $9, COALESCE($10, 'DRAFT')) VALUES ($1, $2, $3, COALESCE($4, 1), $5, $6, $7, $8, COALESCE($9, 'DRAFT'))
RETURNING id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at, image_url RETURNING id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at
` `
type CreateQuestionParams struct { type CreateQuestionParams struct {
@ -48,8 +47,7 @@ type CreateQuestionParams struct {
Tips pgtype.Text `json:"tips"` Tips pgtype.Text `json:"tips"`
VoicePrompt pgtype.Text `json:"voice_prompt"` VoicePrompt pgtype.Text `json:"voice_prompt"`
SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"` SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"`
ImageUrl pgtype.Text `json:"image_url"` Column9 interface{} `json:"column_9"`
Column10 interface{} `json:"column_10"`
} }
func (q *Queries) CreateQuestion(ctx context.Context, arg CreateQuestionParams) (Question, error) { func (q *Queries) CreateQuestion(ctx context.Context, arg CreateQuestionParams) (Question, error) {
@ -62,8 +60,7 @@ func (q *Queries) CreateQuestion(ctx context.Context, arg CreateQuestionParams)
arg.Tips, arg.Tips,
arg.VoicePrompt, arg.VoicePrompt,
arg.SampleAnswerVoicePrompt, arg.SampleAnswerVoicePrompt,
arg.ImageUrl, arg.Column9,
arg.Column10,
) )
var i Question var i Question
err := row.Scan( err := row.Scan(
@ -79,7 +76,6 @@ func (q *Queries) CreateQuestion(ctx context.Context, arg CreateQuestionParams)
&i.Status, &i.Status,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.ImageUrl,
) )
return i, err return i, err
} }
@ -95,7 +91,7 @@ func (q *Queries) DeleteQuestion(ctx context.Context, id int64) error {
} }
const GetQuestionByID = `-- name: GetQuestionByID :one const GetQuestionByID = `-- name: GetQuestionByID :one
SELECT id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at, image_url SELECT id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at
FROM questions FROM questions
WHERE id = $1 WHERE id = $1
` `
@ -116,7 +112,6 @@ func (q *Queries) GetQuestionByID(ctx context.Context, id int64) (Question, erro
&i.Status, &i.Status,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.ImageUrl,
) )
return i, err return i, err
} }
@ -193,7 +188,7 @@ func (q *Queries) GetQuestionWithOptions(ctx context.Context, id int64) ([]GetQu
} }
const GetQuestionsByIDs = `-- name: GetQuestionsByIDs :many const GetQuestionsByIDs = `-- name: GetQuestionsByIDs :many
SELECT id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at, image_url SELECT id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at
FROM questions FROM questions
WHERE id = ANY($1::BIGINT[]) WHERE id = ANY($1::BIGINT[])
ORDER BY id ORDER BY id
@ -221,7 +216,6 @@ func (q *Queries) GetQuestionsByIDs(ctx context.Context, dollar_1 []int64) ([]Qu
&i.Status, &i.Status,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.ImageUrl,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -236,7 +230,7 @@ func (q *Queries) GetQuestionsByIDs(ctx context.Context, dollar_1 []int64) ([]Qu
const ListQuestions = `-- name: ListQuestions :many const ListQuestions = `-- name: ListQuestions :many
SELECT SELECT
COUNT(*) OVER () AS total_count, COUNT(*) OVER () AS total_count,
q.id, q.question_text, q.question_type, q.difficulty_level, q.points, q.explanation, q.tips, q.voice_prompt, q.sample_answer_voice_prompt, q.status, q.created_at, q.updated_at, q.image_url q.id, q.question_text, q.question_type, q.difficulty_level, q.points, q.explanation, q.tips, q.voice_prompt, q.sample_answer_voice_prompt, q.status, q.created_at, q.updated_at
FROM questions q FROM questions q
WHERE status != 'ARCHIVED' WHERE status != 'ARCHIVED'
AND ($1::VARCHAR IS NULL OR $1 = '' OR question_type = $1) AND ($1::VARCHAR IS NULL OR $1 = '' OR question_type = $1)
@ -269,7 +263,6 @@ type ListQuestionsRow struct {
Status string `json:"status"` Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ImageUrl pgtype.Text `json:"image_url"`
} }
func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([]ListQuestionsRow, error) { func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([]ListQuestionsRow, error) {
@ -301,7 +294,6 @@ func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([
&i.Status, &i.Status,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.ImageUrl,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -316,7 +308,7 @@ func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([
const SearchQuestions = `-- name: SearchQuestions :many const SearchQuestions = `-- name: SearchQuestions :many
SELECT SELECT
COUNT(*) OVER () AS total_count, COUNT(*) OVER () AS total_count,
q.id, q.question_text, q.question_type, q.difficulty_level, q.points, q.explanation, q.tips, q.voice_prompt, q.sample_answer_voice_prompt, q.status, q.created_at, q.updated_at, q.image_url q.id, q.question_text, q.question_type, q.difficulty_level, q.points, q.explanation, q.tips, q.voice_prompt, q.sample_answer_voice_prompt, q.status, q.created_at, q.updated_at
FROM questions q FROM questions q
WHERE status != 'ARCHIVED' WHERE status != 'ARCHIVED'
AND question_text ILIKE '%' || $1 || '%' AND question_text ILIKE '%' || $1 || '%'
@ -345,7 +337,6 @@ type SearchQuestionsRow struct {
Status string `json:"status"` Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ImageUrl pgtype.Text `json:"image_url"`
} }
func (q *Queries) SearchQuestions(ctx context.Context, arg SearchQuestionsParams) ([]SearchQuestionsRow, error) { func (q *Queries) SearchQuestions(ctx context.Context, arg SearchQuestionsParams) ([]SearchQuestionsRow, error) {
@ -371,7 +362,6 @@ func (q *Queries) SearchQuestions(ctx context.Context, arg SearchQuestionsParams
&i.Status, &i.Status,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.ImageUrl,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -394,10 +384,9 @@ SET
tips = COALESCE($6, tips), tips = COALESCE($6, tips),
voice_prompt = COALESCE($7, voice_prompt), voice_prompt = COALESCE($7, voice_prompt),
sample_answer_voice_prompt = COALESCE($8, sample_answer_voice_prompt), sample_answer_voice_prompt = COALESCE($8, sample_answer_voice_prompt),
image_url = COALESCE($9, image_url), status = COALESCE($9, status),
status = COALESCE($10, status),
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $11 WHERE id = $10
` `
type UpdateQuestionParams struct { type UpdateQuestionParams struct {
@ -409,7 +398,6 @@ type UpdateQuestionParams struct {
Tips pgtype.Text `json:"tips"` Tips pgtype.Text `json:"tips"`
VoicePrompt pgtype.Text `json:"voice_prompt"` VoicePrompt pgtype.Text `json:"voice_prompt"`
SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"` SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"`
ImageUrl pgtype.Text `json:"image_url"`
Status string `json:"status"` Status string `json:"status"`
ID int64 `json:"id"` ID int64 `json:"id"`
} }
@ -424,7 +412,6 @@ func (q *Queries) UpdateQuestion(ctx context.Context, arg UpdateQuestionParams)
arg.Tips, arg.Tips,
arg.VoicePrompt, arg.VoicePrompt,
arg.SampleAnswerVoicePrompt, arg.SampleAnswerVoicePrompt,
arg.ImageUrl,
arg.Status, arg.Status,
arg.ID, arg.ID,
) )

View File

@ -345,25 +345,6 @@ func (q *Queries) PublishSubCourseVideo(ctx context.Context, id int64) error {
return err return err
} }
const ReorderSubCourseVideos = `-- name: ReorderSubCourseVideos :exec
UPDATE sub_course_videos
SET display_order = bulk.position
FROM (
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
) AS bulk
WHERE sub_course_videos.id = bulk.id
`
type ReorderSubCourseVideosParams struct {
Ids []int64 `json:"ids"`
Positions []int32 `json:"positions"`
}
func (q *Queries) ReorderSubCourseVideos(ctx context.Context, arg ReorderSubCourseVideosParams) error {
_, err := q.db.Exec(ctx, ReorderSubCourseVideos, arg.Ids, arg.Positions)
return err
}
const UpdateSubCourseVideo = `-- name: UpdateSubCourseVideo :exec const UpdateSubCourseVideo = `-- name: UpdateSubCourseVideo :exec
UPDATE sub_course_videos UPDATE sub_course_videos
SET SET

View File

@ -261,25 +261,6 @@ func (q *Queries) ListSubCoursesByCourse(ctx context.Context, courseID int64) ([
return items, nil return items, nil
} }
const ReorderSubCourses = `-- name: ReorderSubCourses :exec
UPDATE sub_courses
SET display_order = bulk.position
FROM (
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
) AS bulk
WHERE sub_courses.id = bulk.id
`
type ReorderSubCoursesParams struct {
Ids []int64 `json:"ids"`
Positions []int32 `json:"positions"`
}
func (q *Queries) ReorderSubCourses(ctx context.Context, arg ReorderSubCoursesParams) error {
_, err := q.db.Exec(ctx, ReorderSubCourses, arg.Ids, arg.Positions)
return err
}
const UpdateSubCourse = `-- name: UpdateSubCourse :exec const UpdateSubCourse = `-- name: UpdateSubCourse :exec
UPDATE sub_courses UPDATE sub_courses
SET SET

View File

@ -764,27 +764,6 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
return i, err return i, err
} }
const GetUserSummary = `-- name: GetUserSummary :one
SELECT
COUNT(*) AS total_users,
COUNT(*) FILTER (WHERE status = 'ACTIVE') AS active_users,
COUNT(*) FILTER (WHERE created_at >= date_trunc('month', CURRENT_DATE)) AS joined_this_month
FROM users
`
type GetUserSummaryRow struct {
TotalUsers int64 `json:"total_users"`
ActiveUsers int64 `json:"active_users"`
JoinedThisMonth int64 `json:"joined_this_month"`
}
func (q *Queries) GetUserSummary(ctx context.Context) (GetUserSummaryRow, error) {
row := q.db.QueryRow(ctx, GetUserSummary)
var i GetUserSummaryRow
err := row.Scan(&i.TotalUsers, &i.ActiveUsers, &i.JoinedThisMonth)
return i, err
}
const IsUserNameUnique = `-- name: IsUserNameUnique :one const IsUserNameUnique = `-- name: IsUserNameUnique :one
SELECT SELECT
CASE WHEN COUNT(*) = 0 THEN true ELSE false END AS is_unique CASE WHEN COUNT(*) = 0 THEN true ELSE false END AS is_unique

View File

@ -87,58 +87,3 @@ const (
VideoHostProviderDirect VideoHostProvider = "DIRECT" VideoHostProviderDirect VideoHostProvider = "DIRECT"
VideoHostProviderVimeo VideoHostProvider = "VIMEO" VideoHostProviderVimeo VideoHostProvider = "VIMEO"
) )
// Learning Path types — full nested structure for a course
type LearningPathVideo struct {
ID int64 `json:"id"`
Title string `json:"title"`
Description *string `json:"description,omitempty"`
VideoURL string `json:"video_url"`
Duration int32 `json:"duration"`
Resolution *string `json:"resolution,omitempty"`
DisplayOrder int32 `json:"display_order"`
VimeoID *string `json:"vimeo_id,omitempty"`
VimeoEmbedURL *string `json:"vimeo_embed_url,omitempty"`
VideoHostProvider *string `json:"video_host_provider,omitempty"`
}
type LearningPathPractice struct {
ID int64 `json:"id"`
Title string `json:"title"`
Description *string `json:"description,omitempty"`
Persona *string `json:"persona,omitempty"`
Status string `json:"status"`
QuestionCount int64 `json:"question_count"`
}
type LearningPathPrerequisite struct {
SubCourseID int64 `json:"sub_course_id"`
Title string `json:"title"`
Level string `json:"level"`
}
type LearningPathSubCourse struct {
ID int64 `json:"id"`
Title string `json:"title"`
Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
DisplayOrder int32 `json:"display_order"`
Level string `json:"level"`
PrerequisiteCount int64 `json:"prerequisite_count"`
VideoCount int64 `json:"video_count"`
PracticeCount int64 `json:"practice_count"`
Prerequisites []LearningPathPrerequisite `json:"prerequisites"`
Videos []LearningPathVideo `json:"videos"`
Practices []LearningPathPractice `json:"practices"`
}
type LearningPath struct {
CourseID int64 `json:"course_id"`
CourseTitle string `json:"course_title"`
Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
IntroVideoURL *string `json:"intro_video_url,omitempty"`
CategoryID int64 `json:"category_id"`
CategoryName string `json:"category_name"`
SubCourses []LearningPathSubCourse `json:"sub_courses"`
}

View File

@ -8,7 +8,6 @@ const (
QuestionTypeMCQ QuestionType = "MCQ" QuestionTypeMCQ QuestionType = "MCQ"
QuestionTypeTrueFalse QuestionType = "TRUE_FALSE" QuestionTypeTrueFalse QuestionType = "TRUE_FALSE"
QuestionTypeShortAnswer QuestionType = "SHORT_ANSWER" QuestionTypeShortAnswer QuestionType = "SHORT_ANSWER"
QuestionTypeAudio QuestionType = "AUDIO"
) )
type DifficultyLevel string type DifficultyLevel string
@ -47,24 +46,15 @@ type Question struct {
Tips *string Tips *string
VoicePrompt *string VoicePrompt *string
SampleAnswerVoicePrompt *string SampleAnswerVoicePrompt *string
ImageURL *string
Status string Status string
CreatedAt time.Time CreatedAt time.Time
UpdatedAt *time.Time UpdatedAt *time.Time
} }
type QuestionAudioAnswer struct {
ID int64
QuestionID int64
CorrectAnswerText string
CreatedAt time.Time
}
type QuestionWithDetails struct { type QuestionWithDetails struct {
Question Question
Options []QuestionOption Options []QuestionOption
ShortAnswers []QuestionShortAnswer ShortAnswers []QuestionShortAnswer
AudioAnswer *QuestionAudioAnswer
} }
type QuestionOption struct { type QuestionOption struct {
@ -120,7 +110,6 @@ type QuestionSetItemWithQuestion struct {
Explanation *string Explanation *string
Tips *string Tips *string
VoicePrompt *string VoicePrompt *string
ImageURL *string
QuestionStatus string QuestionStatus string
} }
@ -133,11 +122,9 @@ type CreateQuestionInput struct {
Tips *string Tips *string
VoicePrompt *string VoicePrompt *string
SampleAnswerVoicePrompt *string SampleAnswerVoicePrompt *string
ImageURL *string
Status *string Status *string
Options []CreateQuestionOptionInput Options []CreateQuestionOptionInput
ShortAnswers []CreateShortAnswerInput ShortAnswers []CreateShortAnswerInput
AudioCorrectAnswerText *string
} }
type CreateQuestionOptionInput struct { type CreateQuestionOptionInput struct {

View File

@ -181,12 +181,6 @@ type UpdateUserStatusReq struct {
UserID int64 UserID int64
} }
type UserSummary struct {
TotalUsers int64 `json:"total_users"`
ActiveUsers int64 `json:"active_users"`
JoinedThisMonth int64 `json:"joined_this_month"`
}
type UpdateUserReq struct { type UpdateUserReq struct {
UserID int64 `json:"-"` UserID int64 `json:"-"`

View File

@ -172,16 +172,6 @@ type CourseStore interface {
// Learning Tree // Learning Tree
GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error) GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error)
// Learning Path (full nested structure for a course)
GetCourseLearningPath(ctx context.Context, courseID int64) (domain.LearningPath, error)
// Reorder (drag-and-drop support)
ReorderCourseCategories(ctx context.Context, ids []int64, positions []int32) error
ReorderCourses(ctx context.Context, ids []int64, positions []int32) error
ReorderSubCourses(ctx context.Context, ids []int64, positions []int32) error
ReorderSubCourseVideos(ctx context.Context, ids []int64, positions []int32) error
ReorderQuestionSets(ctx context.Context, ids []int64, positions []int32) error
} }
type ProgressionStore interface { type ProgressionStore interface {

View File

@ -58,7 +58,6 @@ type UserStore interface {
limit, offset int32, limit, offset int32,
) ([]domain.User, int64, error) ) ([]domain.User, int64, error)
GetTotalUsers(ctx context.Context, role *string) (int64, error) GetTotalUsers(ctx context.Context, role *string) (int64, error)
GetUserSummary(ctx context.Context) (domain.UserSummary, error)
SearchUserByNameOrPhone( SearchUserByNameOrPhone(
ctx context.Context, ctx context.Context,
search string, search string,

View File

@ -119,10 +119,3 @@ func (s *Store) DeleteCourseCategory(
return s.queries.DeleteCourseCategory(ctx, id) return s.queries.DeleteCourseCategory(ctx, id)
} }
func (s *Store) ReorderCourseCategories(ctx context.Context, ids []int64, positions []int32) error {
return s.queries.ReorderCourseCategories(ctx, dbgen.ReorderCourseCategoriesParams{
Ids: ids,
Positions: positions,
})
}

View File

@ -158,13 +158,6 @@ func mapCourse(row dbgen.Course) domain.Course {
} }
} }
func (s *Store) ReorderCourses(ctx context.Context, ids []int64, positions []int32) error {
return s.queries.ReorderCourses(ctx, dbgen.ReorderCoursesParams{
Ids: ids,
Positions: positions,
})
}
func ptrText(t pgtype.Text) *string { func ptrText(t pgtype.Text) *string {
if t.Valid { if t.Valid {
return &t.String return &t.String

View File

@ -3,9 +3,6 @@ package repository
import ( import (
"Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/domain"
"context" "context"
"fmt"
"github.com/jackc/pgx/v5/pgtype"
) )
func (s *Store) GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error) { func (s *Store) GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error) {
@ -44,115 +41,3 @@ func (s *Store) GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, e
return courses, nil return courses, nil
} }
func (s *Store) GetCourseLearningPath(ctx context.Context, courseID int64) (domain.LearningPath, error) {
rows, err := s.queries.GetCourseLearningPath(ctx, courseID)
if err != nil {
return domain.LearningPath{}, err
}
if len(rows) == 0 {
return domain.LearningPath{}, fmt.Errorf("course not found")
}
first := rows[0]
path := domain.LearningPath{
CourseID: first.CourseID,
CourseTitle: first.CourseTitle,
Description: ptrString(first.CourseDescription),
Thumbnail: ptrString(first.CourseThumbnail),
IntroVideoURL: ptrString(first.CourseIntroVideoUrl),
CategoryID: first.CategoryID,
CategoryName: first.CategoryName,
SubCourses: []domain.LearningPathSubCourse{},
}
for _, row := range rows {
if !row.SubCourseID.Valid {
continue
}
scID := row.SubCourseID.Int64
// Fetch prerequisites, videos, practices for this sub-course
prerequisites, _ := s.getSubCoursePrerequisitesForPath(ctx, scID)
videos, _ := s.getSubCourseVideosForPath(ctx, scID)
practices, _ := s.getSubCoursePracticesForPath(ctx, scID)
sc := domain.LearningPathSubCourse{
ID: scID,
Title: row.SubCourseTitle.String,
Description: ptrString(row.SubCourseDescription),
Thumbnail: ptrString(row.SubCourseThumbnail),
DisplayOrder: row.SubCourseDisplayOrder.Int32,
Level: row.SubCourseLevel.String,
PrerequisiteCount: row.PrerequisiteCount,
VideoCount: row.VideoCount,
PracticeCount: row.PracticeCount,
Prerequisites: prerequisites,
Videos: videos,
Practices: practices,
}
path.SubCourses = append(path.SubCourses, sc)
}
return path, nil
}
func (s *Store) getSubCoursePrerequisitesForPath(ctx context.Context, subCourseID int64) ([]domain.LearningPathPrerequisite, error) {
rows, err := s.queries.GetSubCoursePrerequisitesForLearningPath(ctx, subCourseID)
if err != nil {
return nil, err
}
result := make([]domain.LearningPathPrerequisite, len(rows))
for i, row := range rows {
result[i] = domain.LearningPathPrerequisite{
SubCourseID: row.PrerequisiteSubCourseID,
Title: row.Title,
Level: row.Level,
}
}
return result, nil
}
func (s *Store) getSubCourseVideosForPath(ctx context.Context, subCourseID int64) ([]domain.LearningPathVideo, error) {
rows, err := s.queries.GetSubCourseVideosForLearningPath(ctx, subCourseID)
if err != nil {
return nil, err
}
result := make([]domain.LearningPathVideo, len(rows))
for i, row := range rows {
result[i] = domain.LearningPathVideo{
ID: row.ID,
Title: row.Title,
Description: ptrString(row.Description),
VideoURL: row.VideoUrl,
Duration: row.Duration,
Resolution: ptrString(row.Resolution),
DisplayOrder: row.DisplayOrder,
VimeoID: ptrString(row.VimeoID),
VimeoEmbedURL: ptrString(row.VimeoEmbedUrl),
VideoHostProvider: ptrString(row.VideoHostProvider),
}
}
return result, nil
}
func (s *Store) getSubCoursePracticesForPath(ctx context.Context, subCourseID int64) ([]domain.LearningPathPractice, error) {
ownerID := pgtype.Int8{Int64: subCourseID, Valid: true}
rows, err := s.queries.GetSubCoursePracticesForLearningPath(ctx, ownerID)
if err != nil {
return nil, err
}
result := make([]domain.LearningPathPractice, len(rows))
for i, row := range rows {
result[i] = domain.LearningPathPractice{
ID: row.ID,
Title: row.Title,
Description: ptrString(row.Description),
Persona: ptrString(row.Persona),
Status: row.Status,
QuestionCount: row.QuestionCount,
}
}
return result, nil
}

View File

@ -70,7 +70,6 @@ func questionToDomain(q dbgen.Question) domain.Question {
Tips: fromPgText(q.Tips), Tips: fromPgText(q.Tips),
VoicePrompt: fromPgText(q.VoicePrompt), VoicePrompt: fromPgText(q.VoicePrompt),
SampleAnswerVoicePrompt: fromPgText(q.SampleAnswerVoicePrompt), SampleAnswerVoicePrompt: fromPgText(q.SampleAnswerVoicePrompt),
ImageURL: fromPgText(q.ImageUrl),
Status: q.Status, Status: q.Status,
CreatedAt: q.CreatedAt.Time, CreatedAt: q.CreatedAt.Time,
UpdatedAt: timePtr(q.UpdatedAt), UpdatedAt: timePtr(q.UpdatedAt),
@ -98,15 +97,6 @@ func questionShortAnswerToDomain(a dbgen.QuestionShortAnswer) domain.QuestionSho
} }
} }
func questionAudioAnswerToDomain(a dbgen.QuestionAudioAnswer) domain.QuestionAudioAnswer {
return domain.QuestionAudioAnswer{
ID: a.ID,
QuestionID: a.QuestionID,
CorrectAnswerText: a.CorrectAnswerText,
CreatedAt: a.CreatedAt.Time,
}
}
func questionSetToDomain(qs dbgen.QuestionSet) domain.QuestionSet { func questionSetToDomain(qs dbgen.QuestionSet) domain.QuestionSet {
return domain.QuestionSet{ return domain.QuestionSet{
ID: qs.ID, ID: qs.ID,
@ -162,8 +152,7 @@ func (s *Store) CreateQuestion(ctx context.Context, input domain.CreateQuestionI
Tips: toPgText(input.Tips), Tips: toPgText(input.Tips),
VoicePrompt: toPgText(input.VoicePrompt), VoicePrompt: toPgText(input.VoicePrompt),
SampleAnswerVoicePrompt: toPgText(input.SampleAnswerVoicePrompt), SampleAnswerVoicePrompt: toPgText(input.SampleAnswerVoicePrompt),
ImageUrl: toPgText(input.ImageURL), Column9: status,
Column10: status,
}) })
if err != nil { if err != nil {
return domain.Question{}, err return domain.Question{}, err
@ -200,16 +189,6 @@ func (s *Store) CreateQuestion(ctx context.Context, input domain.CreateQuestionI
} }
} }
if input.AudioCorrectAnswerText != nil && *input.AudioCorrectAnswerText != "" {
_, err = q.CreateQuestionAudioAnswer(ctx, dbgen.CreateQuestionAudioAnswerParams{
QuestionID: question.ID,
CorrectAnswerText: *input.AudioCorrectAnswerText,
})
if err != nil {
return domain.Question{}, err
}
}
if err = tx.Commit(ctx); err != nil { if err = tx.Commit(ctx); err != nil {
return domain.Question{}, err return domain.Question{}, err
} }
@ -251,18 +230,10 @@ func (s *Store) GetQuestionWithDetails(ctx context.Context, id int64) (domain.Qu
answers[i] = questionShortAnswerToDomain(a) answers[i] = questionShortAnswerToDomain(a)
} }
var audioAnswer *domain.QuestionAudioAnswer
aa, err := s.queries.GetAudioAnswerByQuestionID(ctx, id)
if err == nil {
mapped := questionAudioAnswerToDomain(aa)
audioAnswer = &mapped
}
return domain.QuestionWithDetails{ return domain.QuestionWithDetails{
Question: questionToDomain(q), Question: questionToDomain(q),
Options: options, Options: options,
ShortAnswers: answers, ShortAnswers: answers,
AudioAnswer: audioAnswer,
}, nil }, nil
} }
@ -305,7 +276,6 @@ func (s *Store) ListQuestions(ctx context.Context, questionType, difficulty, sta
Tips: fromPgText(r.Tips), Tips: fromPgText(r.Tips),
VoicePrompt: fromPgText(r.VoicePrompt), VoicePrompt: fromPgText(r.VoicePrompt),
SampleAnswerVoicePrompt: fromPgText(r.SampleAnswerVoicePrompt), SampleAnswerVoicePrompt: fromPgText(r.SampleAnswerVoicePrompt),
ImageURL: fromPgText(r.ImageUrl),
Status: r.Status, Status: r.Status,
CreatedAt: r.CreatedAt.Time, CreatedAt: r.CreatedAt.Time,
UpdatedAt: timePtr(r.UpdatedAt), UpdatedAt: timePtr(r.UpdatedAt),
@ -341,7 +311,6 @@ func (s *Store) SearchQuestions(ctx context.Context, query string, limit, offset
Tips: fromPgText(r.Tips), Tips: fromPgText(r.Tips),
VoicePrompt: fromPgText(r.VoicePrompt), VoicePrompt: fromPgText(r.VoicePrompt),
SampleAnswerVoicePrompt: fromPgText(r.SampleAnswerVoicePrompt), SampleAnswerVoicePrompt: fromPgText(r.SampleAnswerVoicePrompt),
ImageURL: fromPgText(r.ImageUrl),
Status: r.Status, Status: r.Status,
CreatedAt: r.CreatedAt.Time, CreatedAt: r.CreatedAt.Time,
UpdatedAt: timePtr(r.UpdatedAt), UpdatedAt: timePtr(r.UpdatedAt),
@ -361,7 +330,7 @@ func (s *Store) UpdateQuestion(ctx context.Context, id int64, input domain.Creat
status = *input.Status status = *input.Status
} }
err := s.queries.UpdateQuestion(ctx, dbgen.UpdateQuestionParams{ return s.queries.UpdateQuestion(ctx, dbgen.UpdateQuestionParams{
ID: id, ID: id,
QuestionText: input.QuestionText, QuestionText: input.QuestionText,
QuestionType: input.QuestionType, QuestionType: input.QuestionType,
@ -371,27 +340,8 @@ func (s *Store) UpdateQuestion(ctx context.Context, id int64, input domain.Creat
Tips: toPgText(input.Tips), Tips: toPgText(input.Tips),
VoicePrompt: toPgText(input.VoicePrompt), VoicePrompt: toPgText(input.VoicePrompt),
SampleAnswerVoicePrompt: toPgText(input.SampleAnswerVoicePrompt), SampleAnswerVoicePrompt: toPgText(input.SampleAnswerVoicePrompt),
ImageUrl: toPgText(input.ImageURL),
Status: status, Status: status,
}) })
if err != nil {
return err
}
if input.AudioCorrectAnswerText != nil {
_ = s.queries.DeleteAudioAnswerByQuestionID(ctx, id)
if *input.AudioCorrectAnswerText != "" {
_, err = s.queries.CreateQuestionAudioAnswer(ctx, dbgen.CreateQuestionAudioAnswerParams{
QuestionID: id,
CorrectAnswerText: *input.AudioCorrectAnswerText,
})
if err != nil {
return err
}
}
}
return nil
} }
func (s *Store) ArchiveQuestion(ctx context.Context, id int64) error { func (s *Store) ArchiveQuestion(ctx context.Context, id int64) error {
@ -703,7 +653,6 @@ func (s *Store) GetQuestionSetItems(ctx context.Context, setID int64) ([]domain.
Explanation: fromPgText(r.Explanation), Explanation: fromPgText(r.Explanation),
Tips: fromPgText(r.Tips), Tips: fromPgText(r.Tips),
VoicePrompt: fromPgText(r.VoicePrompt), VoicePrompt: fromPgText(r.VoicePrompt),
ImageURL: fromPgText(r.ImageUrl),
QuestionStatus: r.QuestionStatus, QuestionStatus: r.QuestionStatus,
} }
} }
@ -732,7 +681,6 @@ func (s *Store) GetPublishedQuestionsInSet(ctx context.Context, setID int64) ([]
Explanation: fromPgText(r.Explanation), Explanation: fromPgText(r.Explanation),
Tips: fromPgText(r.Tips), Tips: fromPgText(r.Tips),
VoicePrompt: fromPgText(r.VoicePrompt), VoicePrompt: fromPgText(r.VoicePrompt),
ImageURL: fromPgText(r.ImageUrl),
QuestionStatus: "PUBLISHED", QuestionStatus: "PUBLISHED",
} }
} }
@ -809,10 +757,3 @@ func (s *Store) GetUserPersonasByQuestionSetID(ctx context.Context, questionSetI
} }
return result, nil return result, nil
} }
func (s *Store) ReorderQuestionSets(ctx context.Context, ids []int64, positions []int32) error {
return s.queries.ReorderQuestionSets(ctx, dbgen.ReorderQuestionSetsParams{
Ids: ids,
Positions: positions,
})
}

View File

@ -283,10 +283,3 @@ func (s *Store) GetVideoByVimeoID(ctx context.Context, vimeoID string) (domain.S
} }
return mapSubCourseVideoRow(row), nil return mapSubCourseVideoRow(row), nil
} }
func (s *Store) ReorderSubCourseVideos(ctx context.Context, ids []int64, positions []int32) error {
return s.queries.ReorderSubCourseVideos(ctx, dbgen.ReorderSubCourseVideosParams{
Ids: ids,
Positions: positions,
})
}

View File

@ -233,11 +233,4 @@ func (s *Store) DeleteSubCourse(
}, nil }, nil
} }
func (s *Store) ReorderSubCourses(ctx context.Context, ids []int64, positions []int32) error {
return s.queries.ReorderSubCourses(ctx, dbgen.ReorderSubCoursesParams{
Ids: ids,
Positions: positions,
})
}

View File

@ -515,19 +515,6 @@ func (s *Store) GetAllUsers(
} }
// GetTotalUsers counts users with optional filters // GetTotalUsers counts users with optional filters
func (s *Store) GetUserSummary(ctx context.Context) (domain.UserSummary, error) {
res, err := s.queries.GetUserSummary(ctx)
if err != nil {
return domain.UserSummary{}, err
}
return domain.UserSummary{
TotalUsers: res.TotalUsers,
ActiveUsers: res.ActiveUsers,
JoinedThisMonth: res.JoinedThisMonth,
}, nil
}
func (s *Store) GetTotalUsers(ctx context.Context, role *string) (int64, error) { func (s *Store) GetTotalUsers(ctx context.Context, role *string) (int64, error) {
count, err := s.queries.GetTotalUsers(ctx, *role) count, err := s.queries.GetTotalUsers(ctx, *role)
if err != nil { if err != nil {

View File

@ -8,27 +8,3 @@ import (
func (s *Service) GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error) { func (s *Service) GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error) {
return s.courseStore.GetFullLearningTree(ctx) return s.courseStore.GetFullLearningTree(ctx)
} }
func (s *Service) GetCourseLearningPath(ctx context.Context, courseID int64) (domain.LearningPath, error) {
return s.courseStore.GetCourseLearningPath(ctx, courseID)
}
func (s *Service) ReorderCourseCategories(ctx context.Context, ids []int64, positions []int32) error {
return s.courseStore.ReorderCourseCategories(ctx, ids, positions)
}
func (s *Service) ReorderCourses(ctx context.Context, ids []int64, positions []int32) error {
return s.courseStore.ReorderCourses(ctx, ids, positions)
}
func (s *Service) ReorderSubCourses(ctx context.Context, ids []int64, positions []int32) error {
return s.courseStore.ReorderSubCourses(ctx, ids, positions)
}
func (s *Service) ReorderSubCourseVideos(ctx context.Context, ids []int64, positions []int32) error {
return s.courseStore.ReorderSubCourseVideos(ctx, ids, positions)
}
func (s *Service) ReorderQuestionSets(ctx context.Context, ids []int64, positions []int32) error {
return s.courseStore.ReorderQuestionSets(ctx, ids, positions)
}

View File

@ -9,7 +9,6 @@ var AllPermissions = []domain.PermissionSeed{
{Key: "course_categories.get", Name: "Get Course Category", Description: "Get a course category by ID", GroupName: "Course Categories"}, {Key: "course_categories.get", Name: "Get Course Category", Description: "Get a course category by ID", GroupName: "Course Categories"},
{Key: "course_categories.update", Name: "Update Course Category", Description: "Update a course category", GroupName: "Course Categories"}, {Key: "course_categories.update", Name: "Update Course Category", Description: "Update a course category", GroupName: "Course Categories"},
{Key: "course_categories.delete", Name: "Delete Course Category", Description: "Delete a course category", GroupName: "Course Categories"}, {Key: "course_categories.delete", Name: "Delete Course Category", Description: "Delete a course category", GroupName: "Course Categories"},
{Key: "course_categories.reorder", Name: "Reorder Course Categories", Description: "Reorder course categories", GroupName: "Course Categories"},
// Course Management - Courses // Course Management - Courses
{Key: "courses.create", Name: "Create Course", Description: "Create a new course", GroupName: "Courses"}, {Key: "courses.create", Name: "Create Course", Description: "Create a new course", GroupName: "Courses"},
@ -18,7 +17,6 @@ var AllPermissions = []domain.PermissionSeed{
{Key: "courses.update", Name: "Update Course", Description: "Update a course", GroupName: "Courses"}, {Key: "courses.update", Name: "Update Course", Description: "Update a course", GroupName: "Courses"},
{Key: "courses.upload_thumbnail", Name: "Upload Course Thumbnail", Description: "Upload course thumbnail image", GroupName: "Courses"}, {Key: "courses.upload_thumbnail", Name: "Upload Course Thumbnail", Description: "Upload course thumbnail image", GroupName: "Courses"},
{Key: "courses.delete", Name: "Delete Course", Description: "Delete a course", GroupName: "Courses"}, {Key: "courses.delete", Name: "Delete Course", Description: "Delete a course", GroupName: "Courses"},
{Key: "courses.reorder", Name: "Reorder Courses", Description: "Reorder courses", GroupName: "Courses"},
// Course Management - Sub-courses // Course Management - Sub-courses
{Key: "subcourses.create", Name: "Create Sub-course", Description: "Create a new sub-course", GroupName: "Sub-courses"}, {Key: "subcourses.create", Name: "Create Sub-course", Description: "Create a new sub-course", GroupName: "Sub-courses"},
@ -30,7 +28,6 @@ var AllPermissions = []domain.PermissionSeed{
{Key: "subcourses.upload_thumbnail", Name: "Upload Sub-course Thumbnail", Description: "Upload sub-course thumbnail", GroupName: "Sub-courses"}, {Key: "subcourses.upload_thumbnail", Name: "Upload Sub-course Thumbnail", Description: "Upload sub-course thumbnail", GroupName: "Sub-courses"},
{Key: "subcourses.deactivate", Name: "Deactivate Sub-course", Description: "Deactivate a sub-course", GroupName: "Sub-courses"}, {Key: "subcourses.deactivate", Name: "Deactivate Sub-course", Description: "Deactivate a sub-course", GroupName: "Sub-courses"},
{Key: "subcourses.delete", Name: "Delete Sub-course", Description: "Delete a sub-course", GroupName: "Sub-courses"}, {Key: "subcourses.delete", Name: "Delete Sub-course", Description: "Delete a sub-course", GroupName: "Sub-courses"},
{Key: "subcourses.reorder", Name: "Reorder Sub-courses", Description: "Reorder sub-courses", GroupName: "Sub-courses"},
// Course Management - Videos // Course Management - Videos
{Key: "videos.create", Name: "Create Video", Description: "Create a sub-course video", GroupName: "Videos"}, {Key: "videos.create", Name: "Create Video", Description: "Create a sub-course video", GroupName: "Videos"},
@ -43,11 +40,9 @@ var AllPermissions = []domain.PermissionSeed{
{Key: "videos.publish", Name: "Publish Video", Description: "Publish a video", GroupName: "Videos"}, {Key: "videos.publish", Name: "Publish Video", Description: "Publish a video", GroupName: "Videos"},
{Key: "videos.update", Name: "Update Video", Description: "Update a video", GroupName: "Videos"}, {Key: "videos.update", Name: "Update Video", Description: "Update a video", GroupName: "Videos"},
{Key: "videos.delete", Name: "Delete Video", Description: "Delete a video", GroupName: "Videos"}, {Key: "videos.delete", Name: "Delete Video", Description: "Delete a video", GroupName: "Videos"},
{Key: "videos.reorder", Name: "Reorder Videos", Description: "Reorder videos", GroupName: "Videos"},
// Learning Tree // Learning Tree
{Key: "learning_tree.get", Name: "Get Learning Tree", Description: "Get full learning tree", GroupName: "Learning Tree"}, {Key: "learning_tree.get", Name: "Get Learning Tree", Description: "Get full learning tree", GroupName: "Learning Tree"},
{Key: "practices.reorder", Name: "Reorder Practices", Description: "Reorder practices", GroupName: "Learning Tree"},
// Questions // Questions
{Key: "questions.create", Name: "Create Question", Description: "Create a new question", GroupName: "Questions"}, {Key: "questions.create", Name: "Create Question", Description: "Create a new question", GroupName: "Questions"},
@ -227,13 +222,13 @@ var AllPermissions = []domain.PermissionSeed{
var DefaultRolePermissions = map[string][]string{ var DefaultRolePermissions = map[string][]string{
"ADMIN": { "ADMIN": {
// Course Management (full access) // Course Management (full access)
"course_categories.create", "course_categories.list", "course_categories.get", "course_categories.update", "course_categories.delete", "course_categories.reorder", "course_categories.create", "course_categories.list", "course_categories.get", "course_categories.update", "course_categories.delete",
"courses.create", "courses.get", "courses.list_by_category", "courses.update", "courses.upload_thumbnail", "courses.delete", "courses.reorder", "courses.create", "courses.get", "courses.list_by_category", "courses.update", "courses.upload_thumbnail", "courses.delete",
"subcourses.create", "subcourses.get", "subcourses.list_by_course", "subcourses.list_by_course_list", "subcourses.list_active", "subcourses.create", "subcourses.get", "subcourses.list_by_course", "subcourses.list_by_course_list", "subcourses.list_active",
"subcourses.update", "subcourses.upload_thumbnail", "subcourses.deactivate", "subcourses.delete", "subcourses.reorder", "subcourses.update", "subcourses.upload_thumbnail", "subcourses.deactivate", "subcourses.delete",
"videos.create", "videos.create_vimeo", "videos.upload", "videos.import_vimeo", "videos.get", "videos.create", "videos.create_vimeo", "videos.upload", "videos.import_vimeo", "videos.get",
"videos.list_by_subcourse", "videos.list_published", "videos.publish", "videos.update", "videos.delete", "videos.reorder", "videos.list_by_subcourse", "videos.list_published", "videos.publish", "videos.update", "videos.delete",
"learning_tree.get", "practices.reorder", "learning_tree.get",
// Questions (full access) // Questions (full access)
"questions.create", "questions.list", "questions.search", "questions.get", "questions.update", "questions.delete", "questions.create", "questions.list", "questions.search", "questions.get", "questions.update", "questions.delete",

View File

@ -91,10 +91,6 @@ func (s *Service) GetAllUsers(
) )
} }
func (s *Service) GetUserSummary(ctx context.Context) (domain.UserSummary, error) {
return s.userStore.GetUserSummary(ctx)
}
func (s *Service) UpdateUserStatus(ctx context.Context, req domain.UpdateUserStatusReq) error { func (s *Service) UpdateUserStatus(ctx context.Context, req domain.UpdateUserStatusReq) error {
return s.userStore.UpdateUserStatus(ctx, req) return s.userStore.UpdateUserStatus(ctx, req)
} }

View File

@ -195,19 +195,3 @@ func (s *Service) GetOEmbed(ctx context.Context, vimeoURL string, width, height
func (s *Service) GeneratePlayerURL(videoID string, opts *vimeo.EmbedOptions) string { func (s *Service) GeneratePlayerURL(videoID string, opts *vimeo.EmbedOptions) string {
return vimeo.GenerateEmbedURL(videoID, opts) return vimeo.GenerateEmbedURL(videoID, opts)
} }
// GetSampleVideo fetches a public Vimeo video by ID and returns its info along with an embeddable iframe.
func (s *Service) GetSampleVideo(ctx context.Context, videoID string, width, height int) (*VideoInfo, string, error) {
info, err := s.GetVideoInfo(ctx, videoID)
if err != nil {
return nil, "", fmt.Errorf("failed to get sample video: %w", err)
}
iframe := vimeo.GenerateIframeEmbed(videoID, width, height, &vimeo.EmbedOptions{
Title: true,
Byline: true,
Portrait: true,
})
return info, iframe, nil
}

View File

@ -1420,292 +1420,6 @@ func (h *Handler) GetFullLearningTree(c *fiber.Ctx) error {
}) })
} }
// GetCourseLearningPath godoc
// @Summary Get course learning path
// @Description Returns the complete learning path for a course including sub-courses (by level),
// @Description video lessons, practices, prerequisites, and counts — structured for admin panel flexible path configuration
// @Tags learning-tree
// @Produce json
// @Param courseId path int true "Course ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/courses/{courseId}/learning-path [get]
func (h *Handler) GetCourseLearningPath(c *fiber.Ctx) error {
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid course ID",
Error: err.Error(),
})
}
path, err := h.courseMgmtSvc.GetCourseLearningPath(c.Context(), courseID)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Course not found or has no learning path",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Learning path retrieved successfully",
Data: path,
})
}
// Reorder Handlers — support drag-and-drop ordering from admin panel
type reorderItem struct {
ID int64 `json:"id" validate:"required"`
Position int32 `json:"position"`
}
type reorderReq struct {
Items []reorderItem `json:"items" validate:"required,min=1"`
}
func parseReorderItems(items []reorderItem) ([]int64, []int32) {
ids := make([]int64, len(items))
positions := make([]int32, len(items))
for i, item := range items {
ids[i] = item.ID
positions[i] = item.Position
}
return ids, positions
}
// ReorderCourseCategories godoc
// @Summary Reorder course categories
// @Description Updates the display_order of course categories for drag-and-drop sorting
// @Tags course-categories
// @Accept json
// @Produce json
// @Param body body reorderReq true "Reorder payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/categories/reorder [put]
func (h *Handler) ReorderCourseCategories(c *fiber.Ctx) error {
var req reorderReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
if len(req.Items) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Items array is required",
Error: "items must not be empty",
})
}
ids, positions := parseReorderItems(req.Items)
if err := h.courseMgmtSvc.ReorderCourseCategories(c.Context(), ids, positions); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to reorder course categories",
Error: err.Error(),
})
}
actorID := c.Locals("user_id").(int64)
actorRole := string(c.Locals("role").(domain.Role))
ip := c.IP()
ua := c.Get("User-Agent")
meta, _ := json.Marshal(map[string]interface{}{"ids": ids, "positions": positions})
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionCategoryUpdated, domain.ResourceCategory, nil, "Reordered course categories", meta, &ip, &ua)
return c.JSON(domain.Response{
Message: "Course categories reordered successfully",
})
}
// ReorderCourses godoc
// @Summary Reorder courses within a category
// @Description Updates the display_order of courses for drag-and-drop sorting
// @Tags courses
// @Accept json
// @Produce json
// @Param body body reorderReq true "Reorder payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/courses/reorder [put]
func (h *Handler) ReorderCourses(c *fiber.Ctx) error {
var req reorderReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
if len(req.Items) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Items array is required",
Error: "items must not be empty",
})
}
ids, positions := parseReorderItems(req.Items)
if err := h.courseMgmtSvc.ReorderCourses(c.Context(), ids, positions); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to reorder courses",
Error: err.Error(),
})
}
actorID := c.Locals("user_id").(int64)
actorRole := string(c.Locals("role").(domain.Role))
ip := c.IP()
ua := c.Get("User-Agent")
meta, _ := json.Marshal(map[string]interface{}{"ids": ids, "positions": positions})
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionCourseUpdated, domain.ResourceCourse, nil, "Reordered courses", meta, &ip, &ua)
return c.JSON(domain.Response{
Message: "Courses reordered successfully",
})
}
// ReorderSubCourses godoc
// @Summary Reorder sub-courses within a course
// @Description Updates the display_order of sub-courses for drag-and-drop sorting
// @Tags sub-courses
// @Accept json
// @Produce json
// @Param body body reorderReq true "Reorder payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/sub-courses/reorder [put]
func (h *Handler) ReorderSubCourses(c *fiber.Ctx) error {
var req reorderReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
if len(req.Items) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Items array is required",
Error: "items must not be empty",
})
}
ids, positions := parseReorderItems(req.Items)
if err := h.courseMgmtSvc.ReorderSubCourses(c.Context(), ids, positions); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to reorder sub-courses",
Error: err.Error(),
})
}
actorID := c.Locals("user_id").(int64)
actorRole := string(c.Locals("role").(domain.Role))
ip := c.IP()
ua := c.Get("User-Agent")
meta, _ := json.Marshal(map[string]interface{}{"ids": ids, "positions": positions})
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionSubCourseUpdated, domain.ResourceSubCourse, nil, "Reordered sub-courses", meta, &ip, &ua)
return c.JSON(domain.Response{
Message: "Sub-courses reordered successfully",
})
}
// ReorderSubCourseVideos godoc
// @Summary Reorder videos within a sub-course
// @Description Updates the display_order of videos for drag-and-drop sorting
// @Tags sub-course-videos
// @Accept json
// @Produce json
// @Param body body reorderReq true "Reorder payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/videos/reorder [put]
func (h *Handler) ReorderSubCourseVideos(c *fiber.Ctx) error {
var req reorderReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
if len(req.Items) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Items array is required",
Error: "items must not be empty",
})
}
ids, positions := parseReorderItems(req.Items)
if err := h.courseMgmtSvc.ReorderSubCourseVideos(c.Context(), ids, positions); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to reorder videos",
Error: err.Error(),
})
}
actorID := c.Locals("user_id").(int64)
actorRole := string(c.Locals("role").(domain.Role))
ip := c.IP()
ua := c.Get("User-Agent")
meta, _ := json.Marshal(map[string]interface{}{"ids": ids, "positions": positions})
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionVideoUpdated, domain.ResourceVideo, nil, "Reordered sub-course videos", meta, &ip, &ua)
return c.JSON(domain.Response{
Message: "Videos reordered successfully",
})
}
// ReorderPractices godoc
// @Summary Reorder practices (question sets) within a sub-course
// @Description Updates the display_order of practices for drag-and-drop sorting
// @Tags question-sets
// @Accept json
// @Produce json
// @Param body body reorderReq true "Reorder payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/practices/reorder [put]
func (h *Handler) ReorderPractices(c *fiber.Ctx) error {
var req reorderReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
if len(req.Items) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Items array is required",
Error: "items must not be empty",
})
}
ids, positions := parseReorderItems(req.Items)
if err := h.courseMgmtSvc.ReorderQuestionSets(c.Context(), ids, positions); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to reorder practices",
Error: err.Error(),
})
}
actorID := c.Locals("user_id").(int64)
actorRole := string(c.Locals("role").(domain.Role))
ip := c.IP()
ua := c.Get("User-Agent")
meta, _ := json.Marshal(map[string]interface{}{"ids": ids, "positions": positions})
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionSetUpdated, domain.ResourceQuestionSet, nil, "Reordered practices", meta, &ip, &ua)
return c.JSON(domain.Response{
Message: "Practices reordered successfully",
})
}
// UploadSubCourseVideo godoc // UploadSubCourseVideo godoc
// @Summary Upload a video file and create sub-course video // @Summary Upload a video file and create sub-course video
// @Description Accepts a video file upload, uploads it to Vimeo via TUS, and creates a sub-course video record // @Description Accepts a video file upload, uploads it to Vimeo via TUS, and creates a sub-course video record

View File

@ -25,18 +25,16 @@ type shortAnswerInput struct {
type createQuestionReq struct { type createQuestionReq struct {
QuestionText string `json:"question_text" validate:"required"` QuestionText string `json:"question_text" validate:"required"`
QuestionType string `json:"question_type" validate:"required,oneof=MCQ TRUE_FALSE SHORT_ANSWER AUDIO"` QuestionType string `json:"question_type" validate:"required,oneof=MCQ TRUE_FALSE SHORT_ANSWER"`
DifficultyLevel *string `json:"difficulty_level"` DifficultyLevel *string `json:"difficulty_level"`
Points *int32 `json:"points"` Points *int32 `json:"points"`
Explanation *string `json:"explanation"` Explanation *string `json:"explanation"`
Tips *string `json:"tips"` Tips *string `json:"tips"`
VoicePrompt *string `json:"voice_prompt"` VoicePrompt *string `json:"voice_prompt"`
SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt"` SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt"`
ImageURL *string `json:"image_url"`
Status *string `json:"status"` Status *string `json:"status"`
Options []optionInput `json:"options"` Options []optionInput `json:"options"`
ShortAnswers []shortAnswerInput `json:"short_answers"` ShortAnswers []shortAnswerInput `json:"short_answers"`
AudioCorrectAnswerText *string `json:"audio_correct_answer_text"`
} }
type optionRes struct { type optionRes struct {
@ -62,12 +60,10 @@ type questionRes struct {
Tips *string `json:"tips,omitempty"` Tips *string `json:"tips,omitempty"`
VoicePrompt *string `json:"voice_prompt,omitempty"` VoicePrompt *string `json:"voice_prompt,omitempty"`
SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt,omitempty"` SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt,omitempty"`
ImageURL *string `json:"image_url,omitempty"`
Status string `json:"status"` Status string `json:"status"`
CreatedAt string `json:"created_at"` CreatedAt string `json:"created_at"`
Options []optionRes `json:"options,omitempty"` Options []optionRes `json:"options,omitempty"`
ShortAnswers []shortAnswerRes `json:"short_answers,omitempty"` ShortAnswers []shortAnswerRes `json:"short_answers,omitempty"`
AudioCorrectAnswerText *string `json:"audio_correct_answer_text,omitempty"`
} }
type listQuestionsRes struct { type listQuestionsRes struct {
@ -123,11 +119,9 @@ func (h *Handler) CreateQuestion(c *fiber.Ctx) error {
Tips: req.Tips, Tips: req.Tips,
VoicePrompt: req.VoicePrompt, VoicePrompt: req.VoicePrompt,
SampleAnswerVoicePrompt: req.SampleAnswerVoicePrompt, SampleAnswerVoicePrompt: req.SampleAnswerVoicePrompt,
ImageURL: req.ImageURL,
Status: req.Status, Status: req.Status,
Options: options, Options: options,
ShortAnswers: shortAnswers, ShortAnswers: shortAnswers,
AudioCorrectAnswerText: req.AudioCorrectAnswerText,
} }
question, err := h.questionsSvc.CreateQuestion(c.Context(), input) question, err := h.questionsSvc.CreateQuestion(c.Context(), input)
@ -157,7 +151,6 @@ func (h *Handler) CreateQuestion(c *fiber.Ctx) error {
Tips: question.Tips, Tips: question.Tips,
VoicePrompt: question.VoicePrompt, VoicePrompt: question.VoicePrompt,
SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt, SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt,
ImageURL: question.ImageURL,
Status: question.Status, Status: question.Status,
CreatedAt: question.CreatedAt.String(), CreatedAt: question.CreatedAt.String(),
}, },
@ -211,11 +204,6 @@ func (h *Handler) GetQuestionByID(c *fiber.Ctx) error {
}) })
} }
var audioCorrectAnswerText *string
if question.AudioAnswer != nil {
audioCorrectAnswerText = &question.AudioAnswer.CorrectAnswerText
}
return c.JSON(domain.Response{ return c.JSON(domain.Response{
Message: "Question retrieved successfully", Message: "Question retrieved successfully",
Data: questionRes{ Data: questionRes{
@ -228,12 +216,10 @@ func (h *Handler) GetQuestionByID(c *fiber.Ctx) error {
Tips: question.Tips, Tips: question.Tips,
VoicePrompt: question.VoicePrompt, VoicePrompt: question.VoicePrompt,
SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt, SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt,
ImageURL: question.ImageURL,
Status: question.Status, Status: question.Status,
CreatedAt: question.CreatedAt.String(), CreatedAt: question.CreatedAt.String(),
Options: options, Options: options,
ShortAnswers: shortAnswers, ShortAnswers: shortAnswers,
AudioCorrectAnswerText: audioCorrectAnswerText,
}, },
}) })
} }
@ -370,11 +356,9 @@ type updateQuestionReq struct {
Tips *string `json:"tips"` Tips *string `json:"tips"`
VoicePrompt *string `json:"voice_prompt"` VoicePrompt *string `json:"voice_prompt"`
SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt"` SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt"`
ImageURL *string `json:"image_url"`
Status *string `json:"status"` Status *string `json:"status"`
Options []optionInput `json:"options"` Options []optionInput `json:"options"`
ShortAnswers []shortAnswerInput `json:"short_answers"` ShortAnswers []shortAnswerInput `json:"short_answers"`
AudioCorrectAnswerText *string `json:"audio_correct_answer_text"`
} }
// UpdateQuestion godoc // UpdateQuestion godoc
@ -442,11 +426,9 @@ func (h *Handler) UpdateQuestion(c *fiber.Ctx) error {
Tips: req.Tips, Tips: req.Tips,
VoicePrompt: req.VoicePrompt, VoicePrompt: req.VoicePrompt,
SampleAnswerVoicePrompt: req.SampleAnswerVoicePrompt, SampleAnswerVoicePrompt: req.SampleAnswerVoicePrompt,
ImageURL: req.ImageURL,
Status: req.Status, Status: req.Status,
Options: options, Options: options,
ShortAnswers: shortAnswers, ShortAnswers: shortAnswers,
AudioCorrectAnswerText: req.AudioCorrectAnswerText,
} }
err = h.questionsSvc.UpdateQuestion(c.Context(), id, input) err = h.questionsSvc.UpdateQuestion(c.Context(), id, input)

View File

@ -22,33 +22,6 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
// GetUserSummary godoc
// @Summary Get user summary statistics
// @Description Returns total users, active users, and users who joined this month
// @Tags user
// @Accept json
// @Produce json
// @Security Bearer
// @Success 200 {object} domain.Response{data=domain.UserSummary}
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/users/summary [get]
func (h *Handler) GetUserSummary(c *fiber.Ctx) error {
summary, err := h.userSvc.GetUserSummary(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to get user summary",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "User summary retrieved successfully",
Data: summary,
Success: true,
StatusCode: fiber.StatusOK,
})
}
// CheckProfileCompleted godoc // CheckProfileCompleted godoc
// @Summary Check if user profile is completed // @Summary Check if user profile is completed
// @Description Returns the profile completion status and percentage for the specified user // @Description Returns the profile completion status and percentage for the specified user

View File

@ -378,63 +378,6 @@ func (h *Handler) GetTranscodeStatus(c *fiber.Ctx) error {
}) })
} }
// GetSampleVideo godoc
// @Summary Get a sample Vimeo video with iframe embed
// @Description Fetches a sample video from Vimeo and returns video details along with an embeddable iframe for client-side integration
// @Tags Vimeo
// @Accept json
// @Produce json
// @Param video_id query string false "Vimeo Video ID to use as sample" default(76979871)
// @Param width query int false "Player width" default(640)
// @Param height query int false "Player height" default(360)
// @Success 200 {object} domain.Response
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/vimeo/sample [get]
func (h *Handler) GetSampleVideo(c *fiber.Ctx) error {
if h.vimeoSvc == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(domain.ErrorResponse{
Message: "Vimeo service is not configured",
Error: "Vimeo service is not enabled or missing access token",
})
}
videoID := c.Query("video_id", "76979871")
width, _ := strconv.Atoi(c.Query("width", "640"))
height, _ := strconv.Atoi(c.Query("height", "360"))
info, iframe, err := h.vimeoSvc.GetSampleVideo(c.Context(), videoID, width, height)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to get sample video",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Sample video retrieved successfully",
Data: fiber.Map{
"video": VimeoVideoResponse{
VimeoID: info.VimeoID,
URI: info.URI,
Name: info.Name,
Description: info.Description,
Duration: info.Duration,
Width: info.Width,
Height: info.Height,
Link: info.Link,
EmbedURL: info.EmbedURL,
EmbedHTML: info.EmbedHTML,
ThumbnailURL: info.ThumbnailURL,
Status: info.Status,
TranscodeStatus: info.TranscodeStatus,
},
"iframe": iframe,
},
Success: true,
StatusCode: fiber.StatusOK,
})
}
// GetOEmbed godoc // GetOEmbed godoc
// @Summary Get oEmbed data for a Vimeo URL // @Summary Get oEmbed data for a Vimeo URL
// @Description Fetches oEmbed metadata for a Vimeo video URL // @Description Fetches oEmbed metadata for a Vimeo video URL

View File

@ -71,15 +71,6 @@ func (a *App) initAppRoutes() {
groupV1.Get("/assessment/questions", h.ListAssessmentQuestions) groupV1.Get("/assessment/questions", h.ListAssessmentQuestions)
groupV1.Get("/assessment/questions/:id", h.GetAssessmentQuestionByID) groupV1.Get("/assessment/questions/:id", h.GetAssessmentQuestionByID)
// Reorder (drag-and-drop support)
// Keep static reorder routes before dynamic `/:id` routes to avoid route collisions
// (e.g., `/courses/reorder` being parsed as `/courses/:id`).
groupV1.Put("/course-management/categories/reorder", a.authMiddleware, a.RequirePermission("course_categories.reorder"), h.ReorderCourseCategories)
groupV1.Put("/course-management/courses/reorder", a.authMiddleware, a.RequirePermission("courses.reorder"), h.ReorderCourses)
groupV1.Put("/course-management/sub-courses/reorder", a.authMiddleware, a.RequirePermission("subcourses.reorder"), h.ReorderSubCourses)
groupV1.Put("/course-management/videos/reorder", a.authMiddleware, a.RequirePermission("videos.reorder"), h.ReorderSubCourseVideos)
groupV1.Put("/course-management/practices/reorder", a.authMiddleware, a.RequirePermission("practices.reorder"), h.ReorderPractices)
// Course Categories // Course Categories
groupV1.Post("/course-management/categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseCategory) groupV1.Post("/course-management/categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseCategory)
groupV1.Get("/course-management/categories", a.authMiddleware, a.RequirePermission("course_categories.list"), h.GetAllCourseCategories) groupV1.Get("/course-management/categories", a.authMiddleware, a.RequirePermission("course_categories.list"), h.GetAllCourseCategories)
@ -120,7 +111,6 @@ func (a *App) initAppRoutes() {
// Learning Tree // Learning Tree
groupV1.Get("/course-management/learning-tree", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetFullLearningTree) groupV1.Get("/course-management/learning-tree", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetFullLearningTree)
groupV1.Get("/course-management/courses/:courseId/learning-path", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetCourseLearningPath)
// Questions // Questions
groupV1.Post("/questions", a.authMiddleware, a.RequirePermission("questions.create"), h.CreateQuestion) groupV1.Post("/questions", a.authMiddleware, a.RequirePermission("questions.create"), h.CreateQuestion)
@ -218,7 +208,6 @@ func (a *App) initAppRoutes() {
// User Routes // User Routes
groupV1.Get("/user/:user_id/is-profile-completed", a.authMiddleware, a.RequirePermission("users.profile_completed"), h.CheckProfileCompleted) groupV1.Get("/user/:user_id/is-profile-completed", a.authMiddleware, a.RequirePermission("users.profile_completed"), h.CheckProfileCompleted)
groupV1.Get("/users", a.authMiddleware, a.RequirePermission("users.list"), h.GetAllUsers) groupV1.Get("/users", a.authMiddleware, a.RequirePermission("users.list"), h.GetAllUsers)
groupV1.Get("/users/summary", a.authMiddleware, a.RequirePermission("users.summary"), h.GetUserSummary)
groupV1.Put("/user", a.authMiddleware, a.RequirePermission("users.update_self"), h.UpdateUser) groupV1.Put("/user", a.authMiddleware, a.RequirePermission("users.update_self"), h.UpdateUser)
groupV1.Patch("/user/status", a.authMiddleware, a.RequirePermission("users.update_status"), h.UpdateUserStatus) groupV1.Patch("/user/status", a.authMiddleware, a.RequirePermission("users.update_status"), h.UpdateUserStatus)
groupV1.Put("/user/knowledge-level", h.UpdateUserKnowledgeLevel) groupV1.Put("/user/knowledge-level", h.UpdateUserKnowledgeLevel)
@ -302,7 +291,6 @@ func (a *App) initAppRoutes() {
vimeoGroup.Post("/uploads/pull", a.authMiddleware, a.RequirePermission("vimeo.uploads.pull"), h.CreatePullUpload) vimeoGroup.Post("/uploads/pull", a.authMiddleware, a.RequirePermission("vimeo.uploads.pull"), h.CreatePullUpload)
vimeoGroup.Post("/uploads/tus", a.authMiddleware, a.RequirePermission("vimeo.uploads.tus"), h.CreateTusUpload) vimeoGroup.Post("/uploads/tus", a.authMiddleware, a.RequirePermission("vimeo.uploads.tus"), h.CreateTusUpload)
vimeoGroup.Get("/oembed", h.GetOEmbed) vimeoGroup.Get("/oembed", h.GetOEmbed)
vimeoGroup.Get("/sample", h.GetSampleVideo)
// Team Management // Team Management
teamGroup := groupV1.Group("/team") teamGroup := groupV1.Group("/team")