first commit

This commit is contained in:
sajjadtalkhabi
2026-05-13 01:43:05 +03:30
commit d2a577f254
297 changed files with 36274 additions and 0 deletions
View File
@@ -0,0 +1,96 @@
export const adminAssignments = [
{
id: 301,
title: 'یادداشت‌برداری از جلسه اول',
description: 'خلاصه‌ای از مباحث جلسه اول را در دو صفحه بنویسید.',
courseTemplate: { id: 1, title: 'اصول اخلاق اسلامی' },
courseTemplateTitle: 'اصول اخلاق اسلامی',
session: { id: 101, title: 'مقدمه‌ای بر اخلاق اسلامی' },
sessionTitle: 'مقدمه‌ای بر اخلاق اسلامی',
startDate: '2025-09-26T00:00:00.000Z',
endDate: '2025-10-05T23:59:00.000Z',
durationDays: 9,
priority: 'mandatory',
submissionsCount: 2,
attachments: [],
createdAt: '2025-09-24T08:00:00.000Z',
},
{
id: 302,
title: 'تحلیل تفسیری سوره حمد',
description: 'تحلیل سه آیه از سوره حمد را ارسال نمایید.',
courseTemplate: { id: 2, title: 'مفاهیم قرآنی' },
courseTemplateTitle: 'مفاهیم قرآنی',
session: { id: 102, title: 'تفسیر سوره حمد' },
sessionTitle: 'تفسیر سوره حمد',
startDate: '2025-10-05T00:00:00.000Z',
endDate: '2025-10-20T23:59:00.000Z',
durationDays: 15,
priority: 'optional',
submissionsCount: 1,
attachments: [],
createdAt: '2025-10-02T08:00:00.000Z',
},
]
export const assignmentSubmissions = new Map([
[
301,
[
{
id: 1,
assignmentId: 301,
userId: 100,
user: {
id: 100,
firstName: 'فاطمه',
lastName: 'رضایی',
fullName: 'فاطمه رضایی',
avatarUrl: '',
address: { province: { name: 'تهران' }, city: { name: 'تهران' } },
profile: {
educationStatus: 'seminary_student',
seminaryLevel: 'level_3',
universityLevel: 'not_applicable',
},
},
termTitle: 'ترم پاییز ۱۴۰۴',
courseTemplateTitle: 'اصول اخلاق اسلامی',
sessionTitle: 'مقدمه‌ای بر اخلاق اسلامی',
userDescription: 'یادداشت‌های جلسه اول به همراه برداشت شخصی ضمیمه است.',
attachments: [{ id: 1, title: 'یادداشت-جلسه-اول.pdf', fileUrl: '#', type: 'document' }],
submittedAt: '2025-10-02T18:30:00.000Z',
status: 'pending',
score: null,
reviewerNote: '',
},
{
id: 2,
assignmentId: 301,
userId: 102,
user: {
id: 102,
firstName: 'مریم',
lastName: 'احمدی',
fullName: 'مریم احمدی',
avatarUrl: '',
address: { province: { name: 'اصفهان' }, city: { name: 'اصفهان' } },
profile: {
educationStatus: 'seminary_and_university_student',
seminaryLevel: 'level_2',
universityLevel: 'bachelor',
},
},
termTitle: 'ترم پاییز ۱۴۰۴',
courseTemplateTitle: 'اصول اخلاق اسلامی',
sessionTitle: 'مقدمه‌ای بر اخلاق اسلامی',
userDescription: 'خلاصه‌ای از جلسه به همراه پرسش پایان.',
attachments: [],
submittedAt: '2025-10-04T20:00:00.000Z',
status: 'approved',
score: 18,
reviewerNote: 'تحلیل خوبی ارائه شده است.',
},
],
],
])
@@ -0,0 +1,86 @@
export const adminConsultations = [
{
id: 387,
requestId: '387',
user: {
id: 201,
firstName: 'علیرضا',
lastName: 'محمدی',
fullName: 'علیرضا محمدی',
avatarUrl: '',
},
status: 'answered',
statusLabel: 'پاسخ داده شده',
createdAt: '2026-05-12T13:54:00.000Z',
faCreatedAt: '۱۴۰۵/۰۲/۲۲',
faCreatedTime: '۱۳:۵۴',
messages: [
{
id: 1,
sender: 'user',
text: 'سلام، در خصوص انتخاب رشته مشاوره می‌خواستم.',
time: '۱۳:۵۴',
},
{
id: 2,
sender: 'admin',
text: 'سلام و وقت بخیر، لطفا رشته فعلی و علاقه‌مندی‌های خود را بفرمایید.',
time: '۱۴:۰۲',
},
],
},
{
id: 388,
requestId: '388',
user: {
id: 202,
firstName: 'زهرا',
lastName: 'کریمی',
fullName: 'زهرا کریمی',
avatarUrl: '',
},
status: 'in_progress',
statusLabel: 'در حال گفتگو',
createdAt: '2026-05-11T09:30:00.000Z',
faCreatedAt: '۱۴۰۵/۰۲/۲۱',
faCreatedTime: '۰۹:۳۰',
messages: [
{
id: 1,
sender: 'user',
text: 'برای ادامه تحصیل در سطح ۳ راهنمایی نیاز دارم.',
time: '۰۹:۳۰',
},
],
},
{
id: 389,
requestId: '389',
user: {
id: 203,
firstName: 'محمد',
lastName: 'حسینی',
fullName: 'محمد حسینی',
avatarUrl: '',
},
status: 'closed',
statusLabel: 'بسته شده',
createdAt: '2026-05-09T16:10:00.000Z',
faCreatedAt: '۱۴۰۵/۰۲/۱۹',
faCreatedTime: '۱۶:۱۰',
messages: [
{
id: 1,
sender: 'user',
text: 'مشاوره دریافت شد، با تشکر.',
time: '۱۶:۱۰',
},
{
id: 2,
sender: 'admin',
text: 'موفق باشید.',
time: '۱۶:۱۲',
},
],
},
]
@@ -0,0 +1,89 @@
export const adminCourseTemplates = [
{
id: 1,
title: 'اصول اخلاق اسلامی',
description: 'دوره مقدماتی برای آشنایی با اخلاق اسلامی.',
image: 'https://picsum.photos/seed/course1/200/200',
defaultTeacher: { id: 5, firstName: 'علی', lastName: 'حسنی', fullName: 'علی حسنی' },
defaultCapacity: 30,
isActiveByDefault: true,
prerequisitesCount: 0,
prerequisites: [],
createdAt: '2025-08-10T08:00:00.000Z',
},
{
id: 2,
title: 'مفاهیم قرآنی',
description: 'بررسی مفاهیم قرآنی به همراه تفسیر مختصر.',
image: 'https://picsum.photos/seed/course2/200/200',
defaultTeacher: { id: 6, firstName: 'حسین', lastName: 'مرادی', fullName: 'حسین مرادی' },
defaultCapacity: 25,
isActiveByDefault: true,
prerequisitesCount: 1,
prerequisites: [{ courseId: 1, course: { id: 1, title: 'اصول اخلاق اسلامی' } }],
createdAt: '2025-09-12T08:00:00.000Z',
},
{
id: 3,
title: 'فقه عبادات',
description: 'مرور احکام عملی نماز و روزه.',
image: '',
defaultTeacher: { id: 7, firstName: 'مهدی', lastName: 'سهرابی', fullName: 'مهدی سهرابی' },
defaultCapacity: 20,
isActiveByDefault: false,
prerequisitesCount: 0,
prerequisites: [],
createdAt: '2026-01-05T08:00:00.000Z',
},
]
export const adminOfferedCourses = [
{
id: 11,
title: 'اصول اخلاق اسلامی - پاییز',
image: 'https://picsum.photos/seed/offered1/200/200',
template: { id: 1, title: 'اصول اخلاق اسلامی' },
templateId: 1,
term: { id: 1, title: 'ترم پاییز ۱۴۰۴' },
termId: 1,
teacher: { id: 5, firstName: 'علی', lastName: 'حسنی', fullName: 'علی حسنی' },
capacity: 30,
isActive: true,
prerequisitesCount: 0,
startDate: '2025-09-23T00:00:00.000Z',
endDate: '2025-11-20T00:00:00.000Z',
createdAt: '2025-09-01T08:00:00.000Z',
},
{
id: 12,
title: 'مفاهیم قرآنی - پاییز',
image: 'https://picsum.photos/seed/offered2/200/200',
template: { id: 2, title: 'مفاهیم قرآنی' },
templateId: 2,
term: { id: 1, title: 'ترم پاییز ۱۴۰۴' },
termId: 1,
teacher: { id: 6, firstName: 'حسین', lastName: 'مرادی', fullName: 'حسین مرادی' },
capacity: 25,
isActive: true,
prerequisitesCount: 1,
startDate: '2025-10-01T00:00:00.000Z',
endDate: '2025-12-15T00:00:00.000Z',
createdAt: '2025-09-10T08:00:00.000Z',
},
{
id: 13,
title: 'فقه عبادات - زمستان',
image: '',
template: { id: 3, title: 'فقه عبادات' },
templateId: 3,
term: { id: 2, title: 'ترم زمستان ۱۴۰۴' },
termId: 2,
teacher: { id: 7, firstName: 'مهدی', lastName: 'سهرابی', fullName: 'مهدی سهرابی' },
capacity: 20,
isActive: false,
prerequisitesCount: 0,
startDate: '2026-01-22T00:00:00.000Z',
endDate: '2026-03-15T00:00:00.000Z',
createdAt: '2025-12-10T08:00:00.000Z',
},
]
+123
View File
@@ -0,0 +1,123 @@
const sampleQuestions = (seed) => [
{
id: `${seed}-q1`,
title: 'کدام گزینه به مفهوم تقوا نزدیک‌تر است؟',
score: 5,
correctAnswerId: `${seed}-q1-a2`,
answers: [
{ id: `${seed}-q1-a1`, title: 'پرهیز از خطا و دوری از گناه' },
{ id: `${seed}-q1-a2`, title: 'خودنگه‌داری در محضر خداوند' },
{ id: `${seed}-q1-a3`, title: 'پرهیز از خوراکی‌های مضر' },
],
},
{
id: `${seed}-q2`,
title: 'منبع اصلی احکام شیعه چیست؟',
score: 5,
correctAnswerId: `${seed}-q2-a1`,
answers: [
{ id: `${seed}-q2-a1`, title: 'قرآن و سنت اهل بیت(ع)' },
{ id: `${seed}-q2-a2`, title: 'فقط قرآن کریم' },
{ id: `${seed}-q2-a3`, title: 'اجماع علما' },
],
},
]
export const adminExams = [
{
id: 201,
title: 'آزمون پایان فصل اول اخلاق',
description: 'آزمون چهار گزینه‌ای از مفاهیم درس‌های ۱ تا ۳.',
courseTemplate: { id: 1, title: 'اصول اخلاق اسلامی' },
courseTemplateTitle: 'اصول اخلاق اسلامی',
session: { id: 101, title: 'مقدمه‌ای بر اخلاق اسلامی' },
sessionTitle: 'مقدمه‌ای بر اخلاق اسلامی',
durationMinutes: 25,
passingScore: 12,
questionsCount: 2,
randomize: true,
endDate: '2026-05-15T00:00:00.000Z',
startDate: '2026-05-01T00:00:00.000Z',
createdAt: '2026-04-20T08:00:00.000Z',
questions: sampleQuestions(201),
usedInTerms: [{ termId: 1 }],
},
{
id: 202,
title: 'آزمون مفاهیم قرآنی - میان‌ترم',
description: 'آزمون میان‌ترم برای مرور آیات کلیدی.',
courseTemplate: { id: 2, title: 'مفاهیم قرآنی' },
courseTemplateTitle: 'مفاهیم قرآنی',
session: { id: 102, title: 'تفسیر سوره حمد' },
sessionTitle: 'تفسیر سوره حمد',
durationMinutes: 30,
passingScore: 14,
questionsCount: 2,
randomize: false,
endDate: '2026-06-10T00:00:00.000Z',
startDate: '2026-05-25T00:00:00.000Z',
createdAt: '2026-05-10T08:00:00.000Z',
questions: sampleQuestions(202),
usedInTerms: [{ termId: 1 }],
},
]
export const examParticipants = new Map([
[
201,
[
{
id: 1,
userId: 100,
firstName: 'فاطمه',
lastName: 'رضایی',
fullName: 'فاطمه رضایی',
avatarUrl: '',
address: { province: { name: 'تهران' }, city: { name: 'تهران' } },
location: 'تهران، تهران',
submittedAt: '2026-05-05T10:30:00.000Z',
attempts: [
{
id: 1,
title: 'تلاش اول',
courseTitle: 'اصول اخلاق اسلامی',
sessionTitle: 'مقدمه‌ای بر اخلاق اسلامی',
date: '2026-05-05T10:30:00.000Z',
score: 17,
scoreTone: 'good',
questions: sampleQuestions(201).map((q) => ({
...q,
userAnswerId: q.correctAnswerId,
})),
},
],
},
{
id: 2,
userId: 102,
firstName: 'مریم',
lastName: 'احمدی',
fullName: 'مریم احمدی',
avatarUrl: '',
address: { province: { name: 'اصفهان' }, city: { name: 'اصفهان' } },
location: 'اصفهان، اصفهان',
submittedAt: '2026-05-06T11:00:00.000Z',
attempts: [
{
id: 2,
title: 'تلاش اول',
courseTitle: 'اصول اخلاق اسلامی',
sessionTitle: 'مقدمه‌ای بر اخلاق اسلامی',
date: '2026-05-06T11:00:00.000Z',
score: 9,
scoreTone: 'bad',
questions: sampleQuestions(201).map((q, i) => ({
...q,
userAnswerId: i === 0 ? q.answers[0].id : q.correctAnswerId,
})),
},
],
},
],
],
])
@@ -0,0 +1,65 @@
const firstNames = ['سکینه', 'رقیه', 'فاطمه', 'مریم', 'لیلا', 'زهرا', 'فریده', 'نسرین']
const lastNames = ['طاهری', 'سلیمی', 'افشار', 'بختیاری', 'خراسانی', 'سهرابی', 'محمدی', 'بهرامی']
const pick = (arr, i) => arr[i % arr.length]
const makeStudent = (i) => {
const firstName = pick(firstNames, i)
const lastName = pick(lastNames, i + 2)
const status = i % 3 === 0 ? 'rejected' : 'pending'
const submittedAt = new Date(Date.now() - i * 86_400_000).toISOString()
return {
id: 500 + i,
firstName,
lastName,
fullName: `${firstName} ${lastName}`,
phoneNumber: `0935${String(2_000_000 + i).padStart(7, '0')}`,
nationalCode: String(2_000_000_000 + i * 11).padStart(10, '0'),
avatarUrl: '',
status,
submittedAt,
address: {
address: 'آدرس نمونه',
province: { id: 1, name: 'تهران' },
city: { id: 11, name: 'تهران' },
},
profile: {
birthDate: new Date(1992, i % 12, ((i * 5) % 27) + 1).toISOString(),
maritalStatus: i % 2 ? 'single' : 'married',
gender: 'female',
educationStatus: 'seminary_student',
seminaryLevel: 'level_3',
universityLevel: 'not_applicable',
universityName: 'حوزه علمیه قم',
fieldOfStudy: 'فقه و اصول',
workExperienceSummary: 'پنج سال تدریس در مدرسه دینی.',
activitySummary: 'برگزاری جلسات قرآن و حدیث به صورت هفتگی.',
propagationExperienceYears: 6,
propagationMethodDescription: 'محوریت بیان داستان‌های قرآنی و قرابت‌های اخلاقی.',
specializedTopics: 'تربیت دینی نوجوانان',
propagationPlatforms: [
{ platform: 'home_gathering_regular' },
{ platform: 'online', platformDetails: 'کانال اینستاگرام @example' },
],
responseToLowSatisfaction: 'change_the_method',
responseToSessionCancellation: 'teach_how_to_reduce_costs',
responseToAudienceConflict: 'divide_the_crowd',
responseToCompetingPropagator: 'strengthen_yourself',
responseToUnqualifiedAdvisors: 'تلاش بر شفاف‌سازی و معرفی مراجع تخصصی.',
relevantCertificates: 'سطح ۳ حوزوی، تدریس قرآن',
hijabApproach: 'گفتگوی همدلانه و آموزش‌محور.',
},
verification: {
status,
submittedAt,
rejectionReason: status === 'rejected' ? 'عدم تکمیل مدارک' : '',
adminNote: status === 'rejected' ? 'لطفا مدارک تحصیلی به‌روز ارسال شود.' : '',
verifiedBy: { firstName: 'سجاد', lastName: 'محمدی' },
media: [],
},
roles: ['student'],
createdAt: submittedAt,
}
}
export const pendingStudents = Array.from({ length: 14 }, (_, i) => makeStudent(i))
@@ -0,0 +1,107 @@
export const adminSessions = [
{
id: 101,
title: 'مقدمه‌ای بر اخلاق اسلامی',
description: 'جلسه نخست؛ تعاریف و چارچوب دوره.',
image: 'https://picsum.photos/seed/session1/200/200',
courseTemplate: { id: 1, title: 'اصول اخلاق اسلامی' },
sessionType: 'in_person',
sessionTypeFa: 'حضوری',
durationMinutes: 90,
order: 1,
sessionConfig: {
startTime: '2025-09-25T16:00:00.000Z',
location: 'سالن آمفی‌تئاتر ۲ - مدرسه قم',
},
materials: [],
usedInTerms: [{ termId: 1 }],
createdAt: '2025-09-10T08:00:00.000Z',
},
{
id: 102,
title: 'تفسیر سوره حمد',
description: 'تحلیل آیات سوره حمد.',
image: '',
courseTemplate: { id: 2, title: 'مفاهیم قرآنی' },
sessionType: 'online',
sessionTypeFa: 'آنلاین',
durationMinutes: 75,
order: 1,
sessionConfig: {
meetingLink: 'https://meet.example.com/quran-1',
platform: 'google_meet',
startTime: '2025-10-04T19:00:00.000Z',
},
materials: [],
usedInTerms: [{ termId: 1 }],
createdAt: '2025-09-25T08:00:00.000Z',
},
{
id: 103,
title: 'احکام نماز جماعت',
description: 'مرور احکام و شرایط نماز جماعت.',
image: '',
courseTemplate: { id: 3, title: 'فقه عبادات' },
sessionType: 'video',
sessionTypeFa: 'ویدئو',
durationMinutes: 50,
order: 2,
sessionConfig: {
minWatchedPercent: 80,
mustCompleteBeforeNext: true,
},
materials: [],
usedInTerms: [],
createdAt: '2026-01-12T08:00:00.000Z',
},
]
export const sessionAttendance = new Map([
[
101,
[
{
id: 1,
userId: 100,
sessionId: 101,
status: 'present',
user: {
id: 100,
firstName: 'فاطمه',
lastName: 'رضایی',
fullName: 'فاطمه رضایی',
avatarUrl: '',
address: { province: { name: 'تهران' }, city: { name: 'تهران' } },
},
},
{
id: 2,
userId: 102,
sessionId: 101,
status: 'absent',
user: {
id: 102,
firstName: 'مریم',
lastName: 'احمدی',
fullName: 'مریم احمدی',
avatarUrl: '',
address: { province: { name: 'اصفهان' }, city: { name: 'اصفهان' } },
},
},
{
id: 3,
userId: 103,
sessionId: 101,
status: 'late',
user: {
id: 103,
firstName: 'علی',
lastName: 'علوی',
fullName: 'علی علوی',
avatarUrl: '',
address: { province: { name: 'تهران' }, city: { name: 'تهران' } },
},
},
],
],
])
+57
View File
@@ -0,0 +1,57 @@
export const adminTerms = [
{
id: 1,
title: 'ترم پاییز ۱۴۰۴',
description: 'ترم پاییز با محوریت آموزش معارف اسلامی.',
image: 'https://picsum.photos/seed/term1/200/200',
startDate: '2025-09-23T00:00:00.000Z',
endDate: '2026-01-20T00:00:00.000Z',
isActive: true,
studentsCount: 18,
coursesCount: 4,
createdAt: '2025-08-01T08:00:00.000Z',
},
{
id: 2,
title: 'ترم زمستان ۱۴۰۴',
description: 'ترم زمستان با تمرکز بر فقه و اصول.',
image: 'https://picsum.photos/seed/term2/200/200',
startDate: '2026-01-21T00:00:00.000Z',
endDate: '2026-04-20T00:00:00.000Z',
isActive: true,
studentsCount: 12,
coursesCount: 3,
createdAt: '2025-11-10T08:00:00.000Z',
},
{
id: 3,
title: 'ترم بهار ۱۴۰۵',
description: 'ترم بهار ویژه دوره‌های تخصصی.',
image: '',
startDate: '2026-04-21T00:00:00.000Z',
endDate: '2026-08-21T00:00:00.000Z',
isActive: false,
studentsCount: 0,
coursesCount: 2,
createdAt: '2026-02-15T08:00:00.000Z',
},
]
export const termStudentLinks = new Map([
[
1,
[
{ userId: 100, isOnLeave: false },
{ userId: 101, isOnLeave: false },
{ userId: 102, isOnLeave: true },
{ userId: 103, isOnLeave: false },
],
],
[
2,
[
{ userId: 104, isOnLeave: false },
{ userId: 105, isOnLeave: false },
],
],
])
@@ -0,0 +1,92 @@
export const adminTickets = [
{
id: 401,
title: 'پیگیری وضعیت ثبت‌نام',
user: {
id: 100,
firstName: 'فاطمه',
lastName: 'رضایی',
fullName: 'فاطمه رضایی',
avatarUrl: '',
},
status: 'answered',
statusLabel: 'پاسخ داده شده',
createdAt: '2026-05-09T20:28:00.000Z',
faCreatedAt: '۱۴۰۵/۰۲/۱۹',
faCreatedTime: '۱۰:۳۰',
messages: [
{
id: 1,
sender: 'user',
text: 'سلام وقت بخیر، در فرایند ثبت‌نام به مشکل برخوردم.',
time: '۱۲:۳۵',
},
{
id: 2,
sender: 'admin',
text: 'سلام و عرض ادب. لطفا کد پیگیری ثبت‌نام را ارسال کنید.',
time: '۱۲:۴۰',
},
{
id: 3,
sender: 'user',
text: 'کد ثبت‌نام: ۱۲۳۴۵۶۷۸',
time: '۱۲:۴۲',
},
],
},
{
id: 402,
title: 'سوال درباره جلسه آموزشی',
user: {
id: 102,
firstName: 'مریم',
lastName: 'احمدی',
fullName: 'مریم احمدی',
avatarUrl: '',
},
status: 'pending',
statusLabel: 'در انتظار پاسخ',
createdAt: '2026-05-08T09:15:00.000Z',
faCreatedAt: '۱۴۰۵/۰۲/۱۸',
faCreatedTime: '۱۲:۴۵',
messages: [
{
id: 1,
sender: 'user',
text: 'سلام، درباره ساعت برگزاری جلسه چهارم سوال داشتم.',
time: '۱۲:۴۵',
},
],
},
{
id: 403,
title: 'درخواست بستن تیکت',
user: {
id: 103,
firstName: 'علی',
lastName: 'علوی',
fullName: 'علی علوی',
avatarUrl: '',
},
status: 'closed',
statusLabel: 'بسته شده',
createdAt: '2026-05-07T14:10:00.000Z',
faCreatedAt: '۱۴۰۵/۰۲/۱۷',
faCreatedTime: '۱۷:۴۰',
messages: [
{
id: 1,
sender: 'user',
text: 'مشکل برطرف شد. لطفا تیکت بسته شود.',
time: '۱۷:۴۰',
},
{
id: 2,
sender: 'admin',
text: 'با تشکر از شما. تیکت بسته شد.',
time: '۱۷:۴۲',
},
],
},
]
+96
View File
@@ -0,0 +1,96 @@
export const roles = [
{ id: 1, name: 'admin' },
{ id: 2, name: 'counselor' },
{ id: 3, name: 'missionary' },
{ id: 4, name: 'student' },
{ id: 5, name: 'teacher' },
]
const provinces = [
{ id: 1, name: 'تهران' },
{ id: 2, name: 'اصفهان' },
{ id: 3, name: 'فارس' },
{ id: 4, name: 'خراسان رضوی' },
]
const cities = {
1: [
{ id: 11, name: 'تهران' },
{ id: 12, name: 'شمیرانات' },
],
2: [{ id: 21, name: 'اصفهان' }],
3: [{ id: 31, name: 'شیراز' }],
4: [{ id: 41, name: 'مشهد' }],
}
const provinceById = (id) => provinces.find((p) => p.id === id) || provinces[0]
const cityForProvince = (id) => cities[id]?.[0] || cities[1][0]
const firstNames = [
'فاطمه',
'زهرا',
'مریم',
'علی',
'حسین',
'محمد',
'سمیه',
'نرگس',
'حسن',
'رضا',
'مهدی',
'الهام',
'پروانه',
'ساره',
'مهسا',
]
const lastNames = [
'رضایی',
'محمدی',
'حسینی',
'احمدی',
'جعفری',
'علوی',
'موسوی',
'صالحی',
'فاطمی',
'کریمی',
'تقوی',
'مرادی',
'هاشمی',
'بهرامی',
]
const pick = (arr, i) => arr[i % arr.length]
const makeUser = (i, overrides = {}) => {
const firstName = pick(firstNames, i)
const lastName = pick(lastNames, i + 3)
const province = provinceById((i % 4) + 1)
const city = cityForProvince(province.id)
const role = roles[(i % 4) + 1]?.name || 'student'
return {
id: 100 + i,
firstName,
lastName,
fullName: `${firstName} ${lastName}`,
phoneNumber: `0912${String(1_000_000 + i).padStart(7, '0')}`,
nationalCode: String(1_000_000_000 + i * 7).padStart(10, '0'),
avatarUrl: '',
status: i % 5 === 0 ? 'pending' : 'approved',
roles: [role],
roleId: roles.find((r) => r.name === role)?.id,
address: {
address: 'آدرس نمونه',
province,
city,
},
profile: {
bio: '',
birthDate: new Date(1990, i % 12, ((i * 3) % 27) + 1).toISOString(),
maritalStatus: i % 2 ? 'single' : 'married',
gender: i % 2 ? 'female' : 'male',
},
createdAt: new Date(Date.now() - i * 86_400_000).toISOString(),
...overrides,
}
}
export const adminUsers = Array.from({ length: 24 }, (_, i) => makeUser(i))
+47
View File
@@ -0,0 +1,47 @@
export const provinces = [
{ id: 1, name: 'تهران' },
{ id: 2, name: 'اصفهان' },
{ id: 3, name: 'فارس' },
{ id: 4, name: 'خراسان رضوی' },
{ id: 5, name: 'گیلان' },
{ id: 6, name: 'مازندران' },
{ id: 7, name: 'البرز' },
{ id: 8, name: 'یزد' },
]
export const cities = {
1: [
{ id: 11, name: 'تهران' },
{ id: 12, name: 'شمیرانات' },
{ id: 13, name: 'ری' },
],
2: [
{ id: 21, name: 'اصفهان' },
{ id: 22, name: 'کاشان' },
{ id: 23, name: 'نجف‌آباد' },
],
3: [
{ id: 31, name: 'شیراز' },
{ id: 32, name: 'مرودشت' },
],
4: [
{ id: 41, name: 'مشهد' },
{ id: 42, name: 'نیشابور' },
],
5: [
{ id: 51, name: 'رشت' },
{ id: 52, name: 'لاهیجان' },
],
6: [
{ id: 61, name: 'ساری' },
{ id: 62, name: 'بابل' },
],
7: [
{ id: 71, name: 'کرج' },
{ id: 72, name: 'نظرآباد' },
],
8: [
{ id: 81, name: 'یزد' },
{ id: 82, name: 'میبد' },
],
}
+28
View File
@@ -0,0 +1,28 @@
export const currentProfile = {
id: 1,
firstName: 'سجاد',
lastName: 'محمدی',
fullName: 'سجاد محمدی',
phoneNumber: '09123456789',
nationalCode: '0079827498',
avatarUrl: '',
status: 'approved',
roles: ['admin'],
address: {
address: 'خیابان آزادی، پلاک ۱۲',
province: { id: 1, name: 'تهران' },
city: { id: 11, name: 'تهران' },
},
profile: {
bio: 'مدیر سامانه و طلبه فعال در حوزه آموزش مجازی.',
birthDate: '1989-06-12T00:00:00.000Z',
maritalStatus: 'married',
gender: 'male',
educationStatus: 'graduate',
seminaryLevel: 'level_3',
universityLevel: 'master',
universityName: 'تهران',
fieldOfStudy: 'علوم قرآنی',
avatarId: null,
},
}
@@ -0,0 +1,72 @@
export const studentCourses = [
{
id: 11,
title: 'اصول اخلاق اسلامی - پاییز',
image: 'https://picsum.photos/seed/student1/200/200',
teacher: { firstName: 'علی', lastName: 'حسنی', fullName: 'علی حسنی' },
term: { id: 1, title: 'ترم پاییز ۱۴۰۴' },
startDate: '2025-09-23T00:00:00.000Z',
endDate: '2025-11-20T00:00:00.000Z',
status: 'watching',
averageScore: null,
sessions: [
{
id: 1001,
title: 'جلسه اول: مقدمه',
sessionTypeFa: 'حضوری',
durationMinutes: 90,
status: 'completed',
statusLabel: 'تکمیل شده',
},
{
id: 1002,
title: 'جلسه دوم: مبانی',
sessionTypeFa: 'حضوری',
durationMinutes: 90,
status: 'in_progress',
statusLabel: 'در حال بررسی',
},
{
id: 1003,
title: 'جلسه سوم: کاربردها',
sessionTypeFa: 'حضوری',
durationMinutes: 90,
status: 'locked',
statusLabel: 'قفل',
},
],
},
{
id: 12,
title: 'مفاهیم قرآنی - پاییز',
image: 'https://picsum.photos/seed/student2/200/200',
teacher: { firstName: 'حسین', lastName: 'مرادی', fullName: 'حسین مرادی' },
term: { id: 1, title: 'ترم پاییز ۱۴۰۴' },
startDate: '2025-10-01T00:00:00.000Z',
endDate: '2025-12-15T00:00:00.000Z',
status: 'waitForExam',
averageScore: 17.5,
sessions: [
{
id: 1004,
title: 'تفسیر سوره حمد',
sessionTypeFa: 'آنلاین',
durationMinutes: 75,
status: 'completed',
statusLabel: 'تکمیل شده',
},
],
},
{
id: 21,
title: 'مفاهیم قرآنی - بهار قبل',
image: '',
teacher: { firstName: 'حسین', lastName: 'مرادی', fullName: 'حسین مرادی' },
term: { id: 0, title: 'ترم بهار ۱۴۰۳' },
startDate: '2024-03-21T00:00:00.000Z',
endDate: '2024-06-21T00:00:00.000Z',
status: 'completed',
averageScore: 18.25,
sessions: [],
},
]
+71
View File
@@ -0,0 +1,71 @@
import { camelize } from '@/utils/convert-to-camel-snake'
import { findHandler } from '@/services/mock/registry'
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const stripUrl = (url) => (url ? url.split('?')[0] : '')
const safeParse = (value) => {
try {
return camelize(JSON.parse(value))
} catch {
return value
}
}
const mockAdapter = async (config) => {
const url = stripUrl(config.url || '')
const method = (config.method || 'get').toUpperCase()
const match = findHandler(method, url)
if (!match) {
const err = new Error(`No mock registered for ${method} ${url}`)
err.response = {
status: 404,
statusText: 'Not Mocked',
data: { message: err.message },
}
err.config = config
throw err
}
await sleep(150)
const query = config.params ? camelize(config.params) : {}
let data = config.data
if (data instanceof FormData) {
/* leave as-is */
} else if (typeof data === 'string') {
data = safeParse(data)
} else if (data) {
data = camelize(data)
}
const result = await match.entry.handler({
method,
url,
params: match.params,
query,
data,
headers: config.headers,
})
return {
data: result,
status: 200,
statusText: 'OK',
headers: {},
config,
}
}
export const isMocksEnabled = () => import.meta.env.VITE_USE_MOCKS === 'true'
import.meta.glob('@/services/mock/routes/*.js', { eager: true })
export const setupMocks = (instance) => {
if (!isMocksEnabled()) return
instance.defaults.adapter = mockAdapter
}
export { register } from '@/services/mock/registry'
+91
View File
@@ -0,0 +1,91 @@
export const ok = (data, meta) => ({ data, meta })
export const paginate = (items, query) => {
const page = Number(query.page || 1)
const perPage = Number(query.perPage || 10)
const total = items.length
const lastPage = Math.max(1, Math.ceil(total / perPage))
const start = (page - 1) * perPage
const sliced = items.slice(start, start + perPage)
return {
data: sliced,
meta: { page, perPage, total, lastPage, currentPage: page },
}
}
const stringIncludes = (haystack, needle) =>
String(haystack ?? '')
.toLowerCase()
.includes(String(needle ?? '').toLowerCase())
export const filterItems = (items, query, rules = {}) =>
items.filter((item) => {
for (const [key, rule] of Object.entries(rules)) {
const value = query[key]
if (value === '' || value == null) continue
if (typeof rule === 'function') {
if (!rule(item, value)) return false
} else
switch (rule) {
case 'eq': {
if (String(item[key]) !== String(value)) return false
break
}
case 'includes': {
if (!stringIncludes(item[key], value)) return false
break
}
case 'fromDate': {
if (!item[rule.field || 'createdAt']) continue
if (new Date(item[rule.field || 'createdAt']) < new Date(value)) return false
break
}
// No default
}
}
return true
})
export const filterDateRange = (items, query, field = 'createdAt') => {
if (!query.fromDate && !query.toDate) return items
return items.filter((item) => {
const v = item[field]
if (!v) return !query.fromDate && !query.toDate
const d = new Date(v).getTime()
if (query.fromDate && d < new Date(query.fromDate).getTime()) return false
if (query.toDate && d > new Date(query.toDate).getTime()) return false
return true
})
}
let nextId = 1000
export const makeId = () => ++nextId
export const findOrThrow = (list, id) => {
const found = list.find((item) => String(item.id) === String(id))
if (!found) {
const err = new Error('Not Found')
err.response = { status: 404, statusText: 'Not Found', data: { message: 'Not Found' } }
throw err
}
return found
}
export const removeById = (list, id) => {
const idx = list.findIndex((x) => String(x.id) === String(id))
if (idx !== -1) list.splice(idx, 1)
}
export const updateById = (list, id, patch) => {
const idx = list.findIndex((x) => String(x.id) === String(id))
if (idx === -1) return null
list[idx] = { ...list[idx], ...patch }
return list[idx]
}
export const isoDaysFromNow = (days) => new Date(Date.now() + days * 86_400_000).toISOString()
export const isoNow = () => new Date().toISOString()
+1
View File
@@ -0,0 +1 @@
export { register, setupMocks, isMocksEnabled } from '@/services/mock/handlers'
+27
View File
@@ -0,0 +1,27 @@
const handlers = []
export const register = (method, urlPattern, handler) => {
const paramNames = []
const regexSource = urlPattern.replace(/:([A-Za-z][\dA-Za-z]*)/g, (_, name) => {
paramNames.push(name)
return '([^/]+)'
})
const regex = new RegExp(`^${regexSource}$`)
handlers.push({ method: method.toUpperCase(), regex, paramNames, handler })
}
export const findHandler = (method, url) => {
const upper = method.toUpperCase()
for (const entry of handlers) {
if (entry.method !== upper) continue
const match = entry.regex.exec(url)
if (match) {
const params = {}
entry.paramNames.forEach((name, i) => {
params[name] = decodeURIComponent(match[i + 1])
})
return { entry, params }
}
}
return null
}
@@ -0,0 +1,107 @@
import { register } from '@/services/mock/registry'
import { endpoints } from '@/services/api/endpoints'
import { adminAssignments, assignmentSubmissions } from '@/services/mock/fixtures/admin-assignments'
import { adminCourseTemplates } from '@/services/mock/fixtures/admin-courses'
import { adminSessions } from '@/services/mock/fixtures/admin-sessions'
import {
filterDateRange,
filterItems,
findOrThrow,
isoNow,
makeId,
paginate,
removeById,
updateById,
} from '@/services/mock/helpers'
const computeDurationDays = (start, end) => {
if (!start || !end) return null
const diff = Math.ceil((new Date(end).getTime() - new Date(start).getTime()) / 86_400_000)
return diff >= 0 ? diff : null
}
register('GET', endpoints.getAssignmentsList, ({ query }) => {
let list = filterItems(adminAssignments, query, {
title: 'includes',
courseTemplateId: (item, v) => String(item.courseTemplate?.id) === String(v),
sessionId: (item, v) => String(item.session?.id) === String(v),
})
list = filterDateRange(list, query)
return paginate(list, query)
})
register('GET', endpoints.showAssignment, ({ params }) => ({
data: findOrThrow(adminAssignments, params.id),
}))
register('POST', endpoints.addNewAssignment, ({ data }) => {
const tpl = adminCourseTemplates.find((c) => c.id === Number(data.courseTemplateId))
const sess = adminSessions.find((s) => s.id === Number(data.sessionId))
const item = {
id: makeId(),
title: data.title || '',
description: data.description || '',
courseTemplate: tpl ? { id: tpl.id, title: tpl.title } : null,
courseTemplateTitle: tpl?.title || '',
session: sess ? { id: sess.id, title: sess.title } : null,
sessionTitle: sess?.title || '',
startDate: data.startDate || '',
endDate: data.endDate || '',
durationDays: computeDurationDays(data.startDate, data.endDate),
priority: data.priority || 'mandatory',
submissionsCount: 0,
attachments: data.attachments || [],
createdAt: isoNow(),
}
adminAssignments.unshift(item)
return { data: item }
})
register('PUT', endpoints.updateAssignment, ({ params, data }) => {
const tpl = adminCourseTemplates.find((c) => c.id === Number(data.courseTemplateId))
const sess = adminSessions.find((s) => s.id === Number(data.sessionId))
return {
data: updateById(adminAssignments, params.id, {
title: data.title,
description: data.description,
courseTemplate: tpl ? { id: tpl.id, title: tpl.title } : undefined,
courseTemplateTitle: tpl?.title,
session: sess ? { id: sess.id, title: sess.title } : undefined,
sessionTitle: sess?.title,
startDate: data.startDate,
endDate: data.endDate,
durationDays: computeDurationDays(data.startDate, data.endDate),
priority: data.priority,
attachments: data.attachments || [],
}),
}
})
register('DELETE', endpoints.deleteAssignment, ({ params }) => {
removeById(adminAssignments, params.id)
return { data: { message: 'حذف موفق' } }
})
register('GET', endpoints.getAssignmentSubmissions, ({ params, query }) => {
const list = assignmentSubmissions.get(Number(params.assignmentId)) || []
return paginate(list, query)
})
register('GET', endpoints.showAssignmentSubmission, ({ params }) => {
const list = assignmentSubmissions.get(Number(params.assignmentId)) || []
const found = list.find((s) => String(s.id) === String(params.submissionId))
return { data: found || null }
})
register('POST', endpoints.reviewAssignmentSubmission, ({ params, data }) => {
const list = assignmentSubmissions.get(Number(params.assignmentId)) || []
const sub = list.find((s) => String(s.id) === String(params.submissionId))
if (sub) {
Object.assign(sub, {
score: data.score ?? sub.score,
status: data.status ?? sub.status,
reviewerNote: data.reviewerNote ?? sub.reviewerNote,
})
}
return { data: sub || null }
})
@@ -0,0 +1,56 @@
import { register } from '@/services/mock/registry'
import { endpoints } from '@/services/api/endpoints'
import { adminConsultations } from '@/services/mock/fixtures/admin-consultations'
import {
filterDateRange,
filterItems,
findOrThrow,
isoNow,
makeId,
paginate,
updateById,
} from '@/services/mock/helpers'
const STATUS_LABELS = {
in_progress: 'در حال گفتگو',
answered: 'پاسخ داده شده',
closed: 'بسته شده',
}
register('GET', endpoints.getConsultationsList, ({ query }) => {
let list = filterItems(adminConsultations, query, {
status: 'eq',
userName: (item, v) =>
`${item.user?.firstName || ''} ${item.user?.lastName || ''}`
.toLowerCase()
.includes(String(v).toLowerCase()),
})
list = filterDateRange(list, query)
return paginate(list, query)
})
register('GET', endpoints.showConsultation, ({ params }) => ({
data: findOrThrow(adminConsultations, params.id),
}))
register('POST', endpoints.sendConsultationMessage, ({ params, data }) => {
const consultation = findOrThrow(adminConsultations, params.id)
const message = {
id: makeId(),
sender: 'admin',
text: data.text || '',
time: 'همین الان',
sentAt: isoNow(),
}
consultation.messages = [...(consultation.messages || []), message]
consultation.status = 'answered'
consultation.statusLabel = STATUS_LABELS.answered
return { data: consultation }
})
register('POST', endpoints.changeConsultationStatus, ({ params, data }) => ({
data: updateById(adminConsultations, params.id, {
status: data.status,
statusLabel: STATUS_LABELS[data.status] || data.status,
}),
}))
+166
View File
@@ -0,0 +1,166 @@
import { register } from '@/services/mock/registry'
import { endpoints } from '@/services/api/endpoints'
import { adminCourseTemplates, adminOfferedCourses } from '@/services/mock/fixtures/admin-courses'
import { adminTerms } from '@/services/mock/fixtures/admin-terms'
import { adminUsers } from '@/services/mock/fixtures/admin-users'
import {
filterDateRange,
filterItems,
findOrThrow,
isoNow,
makeId,
paginate,
removeById,
updateById,
} from '@/services/mock/helpers'
register('GET', endpoints.getCourseTemplatesList, ({ query }) => {
let list = filterItems(adminCourseTemplates, query, {
title: 'includes',
status: (item, v) => String(item.isActiveByDefault ? 1 : 0) === String(v),
})
list = filterDateRange(list, query)
return paginate(list, query)
})
register('GET', endpoints.showCourseTemplate, ({ params }) => ({
data: findOrThrow(adminCourseTemplates, params.id),
}))
register('POST', endpoints.addNewCourseTemplate, ({ data }) => {
const teacher = adminUsers.find((u) => u.id === Number(data.defaultTeacherId))
const item = {
id: makeId(),
title: data.title || '',
description: data.description || '',
image: data.imageId ? `https://picsum.photos/seed/course-${data.imageId}/200/200` : '',
defaultTeacher: teacher
? {
id: teacher.id,
firstName: teacher.firstName,
lastName: teacher.lastName,
fullName: `${teacher.firstName} ${teacher.lastName}`,
}
: null,
defaultCapacity: Number(data.defaultCapacity) || 0,
isActiveByDefault: !!data.isActiveByDefault,
prerequisitesCount: (data.prerequisites || []).length,
prerequisites: (data.prerequisites || []).map((id) => ({
courseId: id,
course: adminCourseTemplates.find((c) => c.id === Number(id)) || { id },
})),
createdAt: isoNow(),
}
adminCourseTemplates.unshift(item)
return { data: item }
})
register('PUT', endpoints.updateCourseTemplate, ({ params, data }) => {
const teacher = adminUsers.find((u) => u.id === Number(data.defaultTeacherId))
return {
data: updateById(adminCourseTemplates, params.id, {
title: data.title,
description: data.description,
image: data.imageId ? `https://picsum.photos/seed/course-${data.imageId}/200/200` : undefined,
defaultTeacher: teacher
? {
id: teacher.id,
firstName: teacher.firstName,
lastName: teacher.lastName,
fullName: `${teacher.firstName} ${teacher.lastName}`,
}
: undefined,
defaultCapacity: Number(data.defaultCapacity) || 0,
isActiveByDefault: !!data.isActiveByDefault,
prerequisites: (data.prerequisites || []).map((id) => ({
courseId: id,
course: adminCourseTemplates.find((c) => c.id === Number(id)) || { id },
})),
prerequisitesCount: (data.prerequisites || []).length,
}),
}
})
register('DELETE', endpoints.deleteCourseTemplate, ({ params }) => {
removeById(adminCourseTemplates, params.id)
return { data: { message: 'حذف موفق' } }
})
register('POST', endpoints.changeStatusCourseTemplate, ({ params, data }) => ({
data: updateById(adminCourseTemplates, params.id, {
isActiveByDefault: !!data.isActiveByDefault,
}),
}))
register('GET', endpoints.getCoursesList, ({ query }) => {
let list = filterItems(adminOfferedCourses, query, {
title: 'includes',
termId: 'eq',
status: (item, v) => String(item.isActive ? 1 : 0) === String(v),
})
list = filterDateRange(list, query, 'startDate')
return paginate(list, query)
})
register('GET', endpoints.showCourse, ({ params }) => ({
data: findOrThrow(adminOfferedCourses, params.id),
}))
register('POST', endpoints.addNewCourse, ({ data }) => {
const template = adminCourseTemplates.find((t) => t.id === Number(data.templateId))
const term = adminTerms.find((t) => t.id === Number(data.termId))
const teacher = adminUsers.find((u) => u.id === Number(data.teacherId))
const item = {
id: makeId(),
title: data.title || template?.title || '',
image: data.imageId ? `https://picsum.photos/seed/offered-${data.imageId}/200/200` : '',
template: template ? { id: template.id, title: template.title } : null,
templateId: template?.id,
term: term ? { id: term.id, title: term.title } : null,
termId: term?.id,
teacher: teacher
? {
id: teacher.id,
firstName: teacher.firstName,
lastName: teacher.lastName,
fullName: `${teacher.firstName} ${teacher.lastName}`,
}
: null,
capacity: Number(data.capacity) || 0,
isActive: !!data.isActive,
prerequisitesCount: 0,
startDate: term?.startDate || '',
endDate: term?.endDate || '',
createdAt: isoNow(),
}
adminOfferedCourses.unshift(item)
return { data: item }
})
register('PUT', endpoints.updateCourse, ({ params, data }) => {
const template = adminCourseTemplates.find((t) => t.id === Number(data.templateId))
const term = adminTerms.find((t) => t.id === Number(data.termId))
return {
data: updateById(adminOfferedCourses, params.id, {
title: data.title,
image: data.imageId
? `https://picsum.photos/seed/offered-${data.imageId}/200/200`
: undefined,
template: template ? { id: template.id, title: template.title } : undefined,
templateId: template?.id,
term: term ? { id: term.id, title: term.title } : undefined,
termId: term?.id,
capacity: Number(data.capacity) || 0,
isActive: !!data.isActive,
}),
}
})
register('DELETE', endpoints.deleteCourse, ({ params }) => {
removeById(adminOfferedCourses, params.id)
return { data: { message: 'حذف موفق' } }
})
register('POST', endpoints.changeStatusCourse, ({ params, data }) => ({
data: updateById(adminOfferedCourses, params.id, { isActive: !!data.isActive }),
}))
+94
View File
@@ -0,0 +1,94 @@
import { register } from '@/services/mock/registry'
import { endpoints } from '@/services/api/endpoints'
import { adminExams, examParticipants } from '@/services/mock/fixtures/admin-exams'
import { adminCourseTemplates } from '@/services/mock/fixtures/admin-courses'
import { adminSessions } from '@/services/mock/fixtures/admin-sessions'
import {
filterDateRange,
filterItems,
findOrThrow,
isoNow,
makeId,
paginate,
removeById,
updateById,
} from '@/services/mock/helpers'
register('GET', endpoints.getExamsList, ({ query }) => {
let list = filterItems(adminExams, query, {
title: 'includes',
courseTemplateId: (item, v) => String(item.courseTemplate?.id) === String(v),
})
list = filterDateRange(list, query)
return paginate(list, query)
})
register('GET', endpoints.showExam, ({ params }) => ({
data: findOrThrow(adminExams, params.id),
}))
register('POST', endpoints.addNewExam, ({ data }) => {
const session = adminSessions.find((s) => s.id === Number(data.sessionId))
const template = session?.courseTemplate
? adminCourseTemplates.find((c) => c.id === Number(session.courseTemplate.id))
: null
const item = {
id: makeId(),
title: data.title || '',
description: data.description || '',
session: session ? { id: session.id, title: session.title } : null,
sessionTitle: session?.title || '',
courseTemplate: template ? { id: template.id, title: template.title } : null,
courseTemplateTitle: template?.title || '',
durationMinutes: Number(data.durationMinutes) || 0,
passingScore: Number(data.passingScore) || 0,
questionsCount: data.questions?.length || 0,
randomize: !!data.randomize,
endDate: data.endDate || '',
startDate: isoNow(),
createdAt: isoNow(),
questions: data.questions || [],
usedInTerms: [],
}
adminExams.unshift(item)
return { data: item }
})
register('PUT', endpoints.updateExam, ({ params, data }) => {
const session = adminSessions.find((s) => s.id === Number(data.sessionId))
const template = session?.courseTemplate
? adminCourseTemplates.find((c) => c.id === Number(session.courseTemplate.id))
: null
return {
data: updateById(adminExams, params.id, {
title: data.title,
description: data.description,
session: session ? { id: session.id, title: session.title } : undefined,
sessionTitle: session?.title,
courseTemplate: template ? { id: template.id, title: template.title } : undefined,
courseTemplateTitle: template?.title,
durationMinutes: Number(data.durationMinutes) || 0,
passingScore: Number(data.passingScore) || 0,
questionsCount: data.questions?.length || 0,
randomize: !!data.randomize,
endDate: data.endDate,
questions: data.questions || [],
}),
}
})
register('DELETE', endpoints.deleteExam, ({ params }) => {
removeById(adminExams, params.id)
return { data: { message: 'حذف موفق' } }
})
register('GET', endpoints.getExamParticipants, ({ params, query }) => {
const list = examParticipants.get(Number(params.examId)) || []
return paginate(list, query)
})
register('GET', endpoints.showExamParticipant, ({ params }) => {
const list = examParticipants.get(Number(params.examId)) || []
const found = list.find((p) => String(p.id) === String(params.participantId))
return { data: found || null }
})
@@ -0,0 +1,49 @@
import { register } from '@/services/mock/registry'
import { endpoints } from '@/services/api/endpoints'
import { pendingStudents } from '@/services/mock/fixtures/admin-pending-students'
import {
filterDateRange,
filterItems,
findOrThrow,
paginate,
removeById,
updateById,
} from '@/services/mock/helpers'
register('GET', endpoints.getPendingStudents, ({ query }) => {
let list = filterItems(pendingStudents, query, {
name: (item, v) =>
`${item.firstName} ${item.lastName}`.toLowerCase().includes(String(v).toLowerCase()),
nationalCode: 'includes',
})
list = filterDateRange(list, query, 'submittedAt')
return paginate(list, query)
})
register('GET', endpoints.showPendingStudent, ({ params }) => ({
data: findOrThrow(pendingStudents, params.id),
}))
register('POST', endpoints.changeStatusPendingStudent, ({ params, data }) => {
const target = findOrThrow(pendingStudents, params.id)
const patch = {
status: data.status,
verification: {
...target.verification,
status: data.status,
rejectionReason: data.rejectionReason || '',
adminNote: data.adminNote || '',
},
}
updateById(pendingStudents, params.id, patch)
if (data.status === 'approved') {
removeById(pendingStudents, params.id)
}
return { data: { user: { ...target, ...patch } } }
})
register('GET', endpoints.downloadPdfPendingStudentInfo, ({ params }) => ({
data: {
downloadUrl: `https://example.com/mock-pdf-${params.id}.pdf`,
},
}))
@@ -0,0 +1,86 @@
import { register } from '@/services/mock/registry'
import { endpoints } from '@/services/api/endpoints'
import { adminSessions, sessionAttendance } from '@/services/mock/fixtures/admin-sessions'
import { adminCourseTemplates } from '@/services/mock/fixtures/admin-courses'
import { SESSION_TYPE } from '@/enums'
import {
filterDateRange,
filterItems,
findOrThrow,
isoNow,
makeId,
paginate,
removeById,
updateById,
} from '@/services/mock/helpers'
const enrich = (session) => ({
...session,
sessionTypeFa: SESSION_TYPE[session.sessionType] || session.sessionTypeFa || '',
})
register('GET', endpoints.getSessionsList, ({ query }) => {
let list = filterItems(adminSessions, query, {
title: 'includes',
courseTemplateId: (item, v) => String(item.courseTemplate?.id) === String(v),
sessionType: 'eq',
})
list = filterDateRange(list, query)
// eslint-disable-next-line unicorn/no-array-callback-reference
return paginate(list.map(enrich), query)
})
register('GET', endpoints.showSession, ({ params }) => ({
data: enrich(findOrThrow(adminSessions, params.id)),
}))
register('POST', endpoints.addNewSession, ({ data }) => {
const tpl = adminCourseTemplates.find((c) => c.id === Number(data.courseTemplateId))
const item = {
id: makeId(),
title: data.title || '',
description: data.description || '',
image: data.imageId ? `https://picsum.photos/seed/session-${data.imageId}/200/200` : '',
courseTemplate: tpl ? { id: tpl.id, title: tpl.title } : null,
sessionType: data.sessionType || 'in_person',
durationMinutes: Number(data.durationMinutes) || 0,
order: Number(data.order) || 1,
sessionConfig: data.sessionConfig || {},
materials: data.materials || [],
usedInTerms: [],
createdAt: isoNow(),
}
adminSessions.unshift(item)
return { data: enrich(item) }
})
register('PUT', endpoints.updateSession, ({ params, data }) => {
const tpl = adminCourseTemplates.find((c) => c.id === Number(data.courseTemplateId))
const updated = updateById(adminSessions, params.id, {
title: data.title,
description: data.description,
image: data.imageId ? `https://picsum.photos/seed/session-${data.imageId}/200/200` : undefined,
courseTemplate: tpl ? { id: tpl.id, title: tpl.title } : undefined,
sessionType: data.sessionType,
durationMinutes: Number(data.durationMinutes) || 0,
order: Number(data.order) || 1,
sessionConfig: data.sessionConfig ?? undefined,
materials: data.materials ?? undefined,
})
return { data: enrich(updated) }
})
register('DELETE', endpoints.deleteSession, ({ params }) => {
removeById(adminSessions, params.id)
return { data: { message: 'حذف موفق' } }
})
register('POST', endpoints.changeStatusSession, ({ params, data }) => {
const updated = updateById(adminSessions, params.id, { isActive: !!data.isActive })
return { data: enrich(updated) }
})
register('GET', endpoints.getSessionsAttendance, ({ params, query }) => {
const list = sessionAttendance.get(Number(params.sessionId)) || []
return paginate(list, query)
})
+153
View File
@@ -0,0 +1,153 @@
import { register } from '@/services/mock/registry'
import { endpoints } from '@/services/api/endpoints'
import { adminTerms, termStudentLinks } from '@/services/mock/fixtures/admin-terms'
import { adminUsers } from '@/services/mock/fixtures/admin-users'
import { adminOfferedCourses } from '@/services/mock/fixtures/admin-courses'
import {
filterDateRange,
filterItems,
findOrThrow,
isoNow,
makeId,
paginate,
removeById,
updateById,
} from '@/services/mock/helpers'
register('GET', endpoints.getTermsList, ({ query }) => {
let list = filterItems(adminTerms, query, {
title: 'includes',
status: (item, v) => String(item.isActive ? 1 : 0) === String(v),
})
list = filterDateRange(list, query, 'startDate')
return paginate(list, query)
})
register('GET', endpoints.showTerm, ({ params }) => ({
data: findOrThrow(adminTerms, params.id),
}))
register('POST', endpoints.addNewTerm, ({ data }) => {
const term = {
id: makeId(),
title: data.title || '',
description: data.description || '',
image: data.imageId ? `https://picsum.photos/seed/term-${data.imageId}/200/200` : '',
startDate: data.startDate || '',
endDate: data.endDate || '',
isActive: true,
studentsCount: 0,
coursesCount: 0,
createdAt: isoNow(),
}
adminTerms.unshift(term)
return { data: term }
})
register('PUT', endpoints.updateTerm, ({ params, data }) => ({
data: updateById(adminTerms, params.id, {
title: data.title,
description: data.description,
startDate: data.startDate,
endDate: data.endDate,
image: data.imageId ? `https://picsum.photos/seed/term-${data.imageId}/200/200` : undefined,
}),
}))
register('DELETE', endpoints.deleteTerm, ({ params }) => {
removeById(adminTerms, params.id)
return { data: { message: 'حذف موفق' } }
})
register('POST', endpoints.cloneTerm, ({ params }) => {
const src = findOrThrow(adminTerms, params.id)
const clone = { ...src, id: makeId(), title: `${src.title} (کپی)`, createdAt: isoNow() }
adminTerms.unshift(clone)
return { data: clone }
})
register('POST', endpoints.changeStatusTerm, ({ params, data }) => ({
data: updateById(adminTerms, params.id, { isActive: !!data.isActive }),
}))
const studentSnapshot = (link) => {
const user = adminUsers.find((u) => u.id === link.userId)
return user ? { ...user, isOnLeave: link.isOnLeave } : null
}
register('GET', endpoints.listUserTerm, ({ params, query }) => {
const links = termStudentLinks.get(Number(params.termId)) || []
const list = links.map((element) => studentSnapshot(element)).filter(Boolean)
return paginate(list, query)
})
register('POST', endpoints.addUserTerm, ({ params, data }) => {
const termId = Number(params.termId)
const links = termStudentLinks.get(termId) || []
const ids = Array.isArray(data.userIds) ? data.userIds : []
ids.forEach((userId) => {
if (!links.some((l) => l.userId === Number(userId))) {
links.push({ userId: Number(userId), isOnLeave: false })
}
})
termStudentLinks.set(termId, links)
const added = ids
.map((id) => adminUsers.find((u) => u.id === Number(id)))
.filter(Boolean)
.map((u) => ({ ...u, isOnLeave: false }))
const term = adminTerms.find((t) => t.id === termId)
if (term) term.studentsCount = links.length
return { data: added }
})
register('DELETE', endpoints.removeUserTerm, ({ params }) => {
const termId = Number(params.termId)
const links = termStudentLinks.get(termId) || []
const idx = links.findIndex((l) => String(l.userId) === String(params.userId))
if (idx !== -1) links.splice(idx, 1)
termStudentLinks.set(termId, links)
const term = adminTerms.find((t) => t.id === termId)
if (term) term.studentsCount = links.length
return { data: { message: 'حذف موفق' } }
})
register('POST', endpoints.changeLeaveStatus, ({ params, data }) => {
const termId = Number(params.termId)
const links = termStudentLinks.get(termId) || []
const link = links.find((l) => String(l.userId) === String(params.userId))
if (link) link.isOnLeave = !!data.setLeave
return { data: { isOnLeave: !!data.setLeave } }
})
register('GET', endpoints.listCourseTerm, ({ params, query }) => {
const termId = Number(params.termId)
const list = adminOfferedCourses.filter((c) => Number(c.termId) === termId)
return paginate(list, query)
})
register('POST', endpoints.addCourseTerm, ({ params, data }) => {
const termId = Number(params.termId)
const ids = Array.isArray(data.courseIds) ? data.courseIds : []
const linked = []
ids.forEach((id) => {
const course = adminOfferedCourses.find((c) => Number(c.id) === Number(id))
if (course) {
course.termId = termId
course.term = adminTerms.some((t) => t.id === termId)
? { id: termId, title: adminTerms.find((t) => t.id === termId).title }
: course.term
linked.push(course)
}
})
return { data: linked }
})
register('DELETE', endpoints.removeCourseTerm, ({ params }) => {
const courseId = Number(params.courseId)
const course = adminOfferedCourses.find((c) => c.id === courseId)
if (course) {
course.termId = null
course.term = null
}
return { data: { message: 'حذف موفق' } }
})
+55
View File
@@ -0,0 +1,55 @@
import { register } from '@/services/mock/registry'
import { endpoints } from '@/services/api/endpoints'
import { adminTickets } from '@/services/mock/fixtures/admin-tickets'
import {
filterDateRange,
filterItems,
findOrThrow,
isoNow,
makeId,
paginate,
updateById,
} from '@/services/mock/helpers'
register('GET', endpoints.getTicketsList, ({ query }) => {
let list = filterItems(adminTickets, query, {
status: 'eq',
userName: (item, v) =>
`${item.user?.firstName || ''} ${item.user?.lastName || ''}`
.toLowerCase()
.includes(String(v).toLowerCase()),
})
list = filterDateRange(list, query)
return paginate(list, query)
})
register('GET', endpoints.showTicket, ({ params }) => ({
data: findOrThrow(adminTickets, params.id),
}))
register('POST', endpoints.sendTicketMessage, ({ params, data }) => {
const ticket = findOrThrow(adminTickets, params.id)
const message = {
id: makeId(),
sender: 'admin',
text: data.text || '',
time: 'همین الان',
sentAt: isoNow(),
}
ticket.messages = [...(ticket.messages || []), message]
ticket.status = 'answered'
ticket.statusLabel = 'پاسخ داده شده'
return { data: ticket }
})
register('POST', endpoints.changeTicketStatus, ({ params, data }) => ({
data: updateById(adminTickets, params.id, {
status: data.status,
statusLabel:
data.status === 'closed'
? 'بسته شده'
: data.status === 'answered'
? 'پاسخ داده شده'
: 'در انتظار پاسخ',
}),
}))
+103
View File
@@ -0,0 +1,103 @@
import { register } from '@/services/mock/registry'
import { endpoints } from '@/services/api/endpoints'
import { adminUsers, roles } from '@/services/mock/fixtures/admin-users'
import {
filterDateRange,
filterItems,
findOrThrow,
isoNow,
makeId,
paginate,
removeById,
updateById,
} from '@/services/mock/helpers'
register('GET', endpoints.getApprovedUsers, ({ query }) => {
let list = filterItems(adminUsers, query, {
name: (item, v) =>
`${item.firstName} ${item.lastName}`.toLowerCase().includes(String(v).toLowerCase()),
nationalCode: 'includes',
phoneNumber: 'includes',
roleId: (item, v) => String(item.roleId) === String(v),
})
list = filterDateRange(list, query)
return paginate(list, query)
})
register('GET', endpoints.showUserDetails, ({ params }) => ({
data: findOrThrow(adminUsers, params.id),
}))
register('POST', endpoints.addNewUser, ({ data }) => {
const id = makeId()
const role = roles.find((r) => r.id === Number(data.roleId)) || null
const province = data.provinceId ? { id: data.provinceId, name: '' } : null
const city = data.cityId ? { id: data.cityId, name: '' } : null
const user = {
id,
firstName: data.firstName || '',
lastName: data.lastName || '',
fullName: `${data.firstName || ''} ${data.lastName || ''}`.trim(),
phoneNumber: data.phoneNumber || '',
nationalCode: data.nationalCode || '',
avatarUrl: '',
status: 'approved',
roles: role ? [role.name] : [],
roleId: role?.id,
address: { address: data.address || '', province, city },
profile: {
bio: data.bio || '',
birthDate: data.birthDate || '',
maritalStatus: data.maritalStatus || '',
gender: data.gender || '',
avatarId: data.avatarId || null,
},
createdAt: isoNow(),
}
adminUsers.unshift(user)
return { data: user }
})
register('PUT', endpoints.updateUser, ({ params, data }) => {
const role = roles.find((r) => r.id === Number(data.roleId))
const patch = {
firstName: data.firstName,
lastName: data.lastName,
fullName: `${data.firstName || ''} ${data.lastName || ''}`.trim(),
phoneNumber: data.phoneNumber,
nationalCode: data.nationalCode,
roleId: role?.id,
roles: role ? [role.name] : [],
address: {
address: data.address || '',
province: data.provinceId ? { id: data.provinceId, name: '' } : null,
city: data.cityId ? { id: data.cityId, name: '' } : null,
},
profile: {
bio: data.bio || '',
birthDate: data.birthDate || '',
maritalStatus: data.maritalStatus || '',
gender: data.gender || '',
avatarId: data.avatarId || null,
},
}
const updated = updateById(adminUsers, params.id, patch)
return { data: updated }
})
register('POST', endpoints.updateUserRole, ({ params, data }) => {
const role = roles.find((r) => r.id === Number(data.roleId))
const patch = role ? { roleId: role.id, roles: [role.name] } : {}
return { data: updateById(adminUsers, params.id, patch) }
})
register('POST', endpoints.changeUserStatus, ({ params, data }) => ({
data: updateById(adminUsers, params.id, { status: data.status }),
}))
register('DELETE', endpoints.deleteUser, ({ params }) => {
removeById(adminUsers, params.id)
return { data: { message: 'حذف موفق' } }
})
register('GET', endpoints.getRolesList, () => ({ data: roles }))
+89
View File
@@ -0,0 +1,89 @@
import { register } from '@/services/mock/registry'
import { endpoints } from '@/services/api/endpoints'
import { currentProfile } from '@/services/mock/fixtures/profile'
import { makeId } from '@/services/mock/helpers'
const fakeToken = 'mock-token-1234567890'
register('POST', endpoints.login, () => ({
data: { user: currentProfile, token: fakeToken },
}))
register('POST', endpoints.loginWithCode2Step, () => ({
data: { user: currentProfile, token: fakeToken },
}))
register('POST', endpoints.verifyCode, () => ({
data: { user: currentProfile, token: fakeToken },
}))
register('POST', endpoints.register, ({ data }) => ({
data: { user: { ...currentProfile, ...data }, token: fakeToken },
}))
register('POST', endpoints.resendVerificationCodeForRegister, () => ({
data: { message: 'کد جدید ارسال شد' },
}))
register('POST', endpoints.forgotPassword, () => ({
data: { message: 'کد بازیابی ارسال شد' },
}))
register('POST', endpoints.verifyForgotPasswordCode, () => ({
data: { token: fakeToken },
}))
register('POST', endpoints.resetPassword, () => ({
data: { message: 'رمز عبور با موفقیت تغییر کرد' },
}))
register('POST', endpoints.logout, () => ({ data: { message: 'خروج موفق' } }))
register('GET', endpoints.getProfile, () => ({ data: currentProfile }))
register('PUT', endpoints.updateProfile, ({ data }) => {
Object.assign(currentProfile, {
firstName: data.firstName ?? currentProfile.firstName,
lastName: data.lastName ?? currentProfile.lastName,
phoneNumber: data.phoneNumber ?? currentProfile.phoneNumber,
nationalCode: data.nationalCode ?? currentProfile.nationalCode,
})
if (data.provinceId) {
currentProfile.address = currentProfile.address || {}
currentProfile.address.province = { id: data.provinceId, name: '' }
}
if (data.cityId) {
currentProfile.address = currentProfile.address || {}
currentProfile.address.city = { id: data.cityId, name: '' }
}
if (data.address) {
currentProfile.address = { ...currentProfile.address, address: data.address }
}
currentProfile.profile = {
...currentProfile.profile,
bio: data.bio ?? currentProfile.profile?.bio,
birthDate: data.birthDate ?? currentProfile.profile?.birthDate,
maritalStatus: data.maritalStatus ?? currentProfile.profile?.maritalStatus,
gender: data.gender ?? currentProfile.profile?.gender,
}
return { data: currentProfile }
})
register('POST', endpoints.completeProfile, ({ data }) => ({
data: { user: { ...currentProfile, ...data } },
}))
register('GET', endpoints.getRegistrationQuestionVideo, () => ({
data: { url: '', id: 0 },
}))
register('POST', endpoints.uploadMediaTemporary, () => {
const id = makeId()
return {
data: {
id,
uploadId: id,
url: `https://picsum.photos/seed/${id}/240/240`,
},
}
})
+9
View File
@@ -0,0 +1,9 @@
import { register } from '@/services/mock/registry'
import { endpoints } from '@/services/api/endpoints'
import { cities, provinces } from '@/services/mock/fixtures/common'
register('GET', endpoints.provinceList, () => ({ data: provinces }))
register('GET', endpoints.citiesList, ({ params }) => ({
data: cities[params.provinceId] || [],
}))
@@ -0,0 +1,24 @@
import { register } from '@/services/mock/registry'
import { endpoints } from '@/services/api/endpoints'
import { studentCourses } from '@/services/mock/fixtures/student-courses'
import { findOrThrow, paginate } from '@/services/mock/helpers'
const filterByStatus = (status) => {
if (!status) return studentCourses
if (status === 'current') {
return studentCourses.filter((c) => ['watching', 'waitForExam'].includes(c.status))
}
if (status === 'ended') {
return studentCourses.filter((c) => ['completed', 'failed'].includes(c.status))
}
return studentCourses.filter((c) => c.status === status)
}
register('GET', endpoints.getStudentCourses, ({ query }) => {
const list = filterByStatus(query.status)
return paginate(list, query)
})
register('GET', endpoints.showStudentCourse, ({ params }) => ({
data: findOrThrow(studentCourses, params.id),
}))