fix: admin courses

This commit is contained in:
sajjadtalkhabi
2026-05-27 14:47:19 +03:30
parent 7eb2984f5e
commit cb86920015
50 changed files with 787 additions and 649 deletions
+2 -1
View File
@@ -10,5 +10,6 @@ export const apiShowAdminConsultation = (id) =>
export const apiSendAdminConsultationMessage = (id, payload) =>
http.post(buildUrl(endpoints.sendConsultationMessage, { id }), payload)
// Backend uses PATCH (not POST).
export const apiChangeAdminConsultationStatus = (id, payload) =>
http.post(buildUrl(endpoints.changeConsultationStatus, { id }), payload)
http.patch(buildUrl(endpoints.changeConsultationStatus, { id }), payload)
+3
View File
@@ -15,6 +15,9 @@ export const apiDeleteAdminCourse = (id) => http.delete(buildUrl(endpoints.delet
export const apiGetAdminCourseStudents = (courseId, params) =>
http.get(buildUrl(endpoints.listCourseStudents, { courseId }), { params })
export const apiGetCourseAttendees = (courseId, params) =>
http.get(buildUrl(endpoints.getCourseAttendees, { courseId }), { params })
export const apiAddAdminCourseStudent = (courseId, payload) =>
http.post(buildUrl(endpoints.addCourseStudent, { courseId }), payload)
+3
View File
@@ -17,6 +17,9 @@ export const apiCloneAdminTerm = (id) => http.post(buildUrl(endpoints.cloneTerm,
export const apiGetAdminTermStudents = (termId, params) =>
http.get(buildUrl(endpoints.listUserTerm, { termId }), { params })
export const apiGetTermAttendees = (termId, params) =>
http.get(buildUrl(endpoints.getTermAttendees, { termId }), { params })
export const apiAddAdminTermStudents = (termId, payload) =>
http.post(buildUrl(endpoints.addUserTerm, { termId }), payload)
+12 -1
View File
@@ -1,6 +1,12 @@
import { http } from '@/services/api/http'
import { endpoints } from '@/services/api/endpoints'
// Backend (Postman: Auth) is the source of truth. Payloads use the backend's
// field names — `name`, `phone`, `password`, `password_confirmation`,
// `device_name`, `avatar_media_id` — and responses are passed through unchanged.
// Callers (forms / store) are responsible for translating UI-local field names
// (firstName/lastName/phoneNumber) into this shape.
export const apiLogin = (payload) => http.post(endpoints.login, payload)
export const apiLoginWithCode = (payload) => http.post(endpoints.loginWithCode2Step, payload)
@@ -25,7 +31,12 @@ export const apiGetMe = () => http.get(endpoints.me)
export const apiCompleteProfile = (payload) => http.post(endpoints.completeProfile, payload)
export const apiUpdateProfile = (payload) => http.put(endpoints.updateProfile, payload)
export const apiUpdateProfile = (payload) => http.patch(endpoints.updateProfile, payload)
// `data` is an array of `{ key, value }` entries. The backend echoes whatever
// we POST back from the GET, so this is effectively a free-form key/value bag.
export const apiGetRegisterData = () => http.get(endpoints.registerData)
export const apiSaveRegisterData = (data) => http.post(endpoints.registerData, { data })
export const apiGetRegistrationQuestionVideo = () =>
http.get(endpoints.getRegistrationQuestionVideo)
+19 -10
View File
@@ -1,18 +1,22 @@
export const endpoints = {
// ─── Auth & profile ────────────────────────────────────────────────────────
register: '/register',
// Backend-aligned (Postman: Auth collection)
register: '/auth/register',
login: '/auth/login',
logout: '/auth/logout',
me: '/auth/me',
updateProfile: '/me',
registerData: '/me/register-data',
// FE-only — NOT in backend Postman doc; kept so existing UI/mock layer works.
resendVerificationCodeForRegister: '/resend-verification-code',
verifyCode: '/verify-code',
completeProfile: '/complete-profile',
getRegistrationQuestionVideo: '/verification-registration-media',
login: '/login',
loginWithCode2Step: '/login-with-code',
forgotPassword: '/forgot-password',
verifyForgotPasswordCode: '/verify-forgot-password-code',
resetPassword: '/reset-password',
logout: '/logout',
me: '/auth/me',
updateProfile: '/profile',
// ─── Geo ───────────────────────────────────────────────────────────────────
provinceList: '/provinces',
@@ -67,6 +71,7 @@ export const endpoints = {
showTerm: '/terms/:id',
updateTerm: '/terms/:id',
deleteTerm: '/terms/:id',
getTermAttendees: '/terms/:termId/attendees',
// ─── Backend-aligned: Courses ──────────────────────────────────────────────
getCoursesList: '/courses',
@@ -74,6 +79,7 @@ export const endpoints = {
showCourse: '/courses/:id',
updateCourse: '/courses/:id',
deleteCourse: '/courses/:id',
getCourseAttendees: '/courses/:courseId/attendees',
// ─── Backend-aligned: Sessions ─────────────────────────────────────────────
getSessionsList: '/sessions',
@@ -146,16 +152,19 @@ export const endpoints = {
getAssignmentSubmissions: '/homeworks/:homeworkId/submissions',
showAssignmentSubmission: '/homework-submissions/:submissionId',
// ─── Tickets / Consultations (out of scope of Terms/Courses backend doc) ───
// ─── Backend-aligned: Tickets ─────────────────────────────────────────────
// Admin "messages" UI talks to /admin/tickets.
// Admin "consultations" UI talks to /counselor/tickets (same schema; the
// counselor scope is the consultation pool).
getTicketsList: '/admin/tickets',
showTicket: '/admin/tickets/:id',
sendTicketMessage: '/admin/tickets/:id/messages',
changeTicketStatus: '/admin/tickets/:id/status',
getConsultationsList: '/admin/consultations',
showConsultation: '/admin/consultations/:id',
sendConsultationMessage: '/admin/consultations/:id/messages',
changeConsultationStatus: '/admin/consultations/:id/status',
getConsultationsList: '/counselor/tickets',
showConsultation: '/counselor/tickets/:id',
sendConsultationMessage: '/counselor/tickets/:id/messages',
changeConsultationStatus: '/counselor/tickets/:id/status',
}
export const buildUrl = (template, params = {}) =>
+1 -1
View File
@@ -89,7 +89,7 @@ const createInstance = (baseUrl, { headers = {}, ...configs } = {}) => {
addResponseUnwrapInterceptor(instance)
addCamelizeInterceptor(instance)
addSnakizeInterceptor(instance)
addUnauthorizeInterceptor(instance)
// addUnauthorizeInterceptor(instance)
return instance
}
+1 -1
View File
@@ -45,7 +45,7 @@ register('GET', endpoints.me, () => ({
data: currentMe,
}))
register('PUT', endpoints.updateProfile, ({ data }) => {
register('PATCH', endpoints.updateProfile, ({ data }) => {
Object.assign(currentMe, {
name: data.name ?? currentMe.name,
email: data.email ?? currentMe.email,
@@ -18,6 +18,10 @@ export const useAdminConsultationsListQuery = (filtersRef, paginationRef, option
queryKey: ['admin', 'consultations', 'list', filtersRef, paginationRef],
queryFn: () =>
apiGetAdminConsultations({ ...cleanFilters(filtersRef.value), ...paginationRef.value }),
select: (response) => ({
data: response?.data ?? [],
meta: response?.meta,
}),
...options,
})
+16
View File
@@ -9,6 +9,7 @@ import {
apiGetAdminCourseSessions,
apiGetAdminCourseStudents,
apiGetAdminCourses,
apiGetCourseAttendees,
apiRemoveAdminCourseStudent,
apiShowAdminCourse,
apiUpdateAdminCourse,
@@ -78,6 +79,21 @@ export const useAdminCourseStudentsQuery = (courseIdRef, filtersRef, paginationR
...options,
})
export const useCourseAttendeesQuery = (courseIdRef, filtersRef, paginationRef, options = {}) =>
useQuery({
queryKey: ['courses', 'attendees', courseIdRef, filtersRef, paginationRef],
queryFn: () =>
apiGetCourseAttendees(courseIdRef.value, {
...cleanFilters(filtersRef?.value || {}),
...paginationRef?.value,
}),
select: (response) => ({
data: response?.data?.items ?? response?.data ?? [],
meta: response?.data?.meta ?? response?.meta,
}),
...options,
})
export const useAddAdminCourseStudentMutation = () =>
useMutation({
mutationFn: ({ courseId, payload }) => apiAddAdminCourseStudent(courseId, payload),
+23
View File
@@ -9,6 +9,7 @@ import {
apiGetAdminTerms,
apiGetAdminTermCourses,
apiGetAdminTermStudents,
apiGetTermAttendees,
apiRemoveAdminTermCourse,
apiRemoveAdminTermStudent,
apiShowAdminTerm,
@@ -28,6 +29,13 @@ export const adminTermsKeys = {
filters,
pagination,
],
attendees: (termId, filters, pagination) => [
'terms',
'attendees',
termId,
filters,
pagination,
],
courses: (termId, filters, pagination) => [
'admin',
'terms',
@@ -80,6 +88,21 @@ export const useAdminTermStudentsQuery = (termIdRef, filtersRef, paginationRef,
...options,
})
export const useTermAttendeesQuery = (termIdRef, filtersRef, paginationRef, options = {}) =>
useQuery({
queryKey: ['terms', 'attendees', termIdRef, filtersRef, paginationRef],
queryFn: () =>
apiGetTermAttendees(termIdRef.value, {
...cleanFilters(filtersRef?.value || {}),
...paginationRef?.value,
}),
select: (response) => ({
data: response?.data?.items ?? response?.data ?? [],
meta: response?.data?.meta ?? response?.meta,
}),
...options,
})
export const useAddAdminTermStudentsMutation = () =>
useMutation({
mutationFn: ({ termId, payload }) => apiAddAdminTermStudents(termId, payload),
+19
View File
@@ -4,6 +4,7 @@ import {
apiCompleteProfile,
apiForgotPassword,
apiGetMe,
apiGetRegisterData,
apiGetRegistrationQuestionVideo,
apiLogin,
apiLoginWithCode,
@@ -11,6 +12,7 @@ import {
apiRegister,
apiResendVerificationCode,
apiResetPassword,
apiSaveRegisterData,
apiUpdateProfile,
apiUploadMedia,
apiVerifyCode,
@@ -20,6 +22,7 @@ import {
export const authKeys = {
me: () => ['auth', 'me'],
registrationVideo: () => ['auth', 'registration-video'],
registerData: () => ['auth', 'register-data'],
}
export const useLoginMutation = () => useMutation({ mutationFn: apiLogin })
@@ -63,3 +66,19 @@ export const useGetRegistrationVideoQuery = (options = {}) =>
})
export const useUploadMediaMutation = () => useMutation({ mutationFn: apiUploadMedia })
export const useGetRegisterDataQuery = (options = {}) =>
useQuery({
queryKey: authKeys.registerData(),
queryFn: () => apiGetRegisterData(),
select: (response) => {
const data = response?.data ?? response
if (Array.isArray(data)) return data
if (Array.isArray(data?.data)) return data.data
return []
},
...options,
})
export const useSaveRegisterDataMutation = () =>
useMutation({ mutationFn: apiSaveRegisterData })
+38
View File
@@ -7,11 +7,48 @@ export const commonKeys = {
cities: (provinceId) => ['common', 'cities', provinceId],
}
// Sample data so the register form is usable while the backend geo endpoints
// are still being built. Once /provinces and /provinces/:id/cities are live,
// the real response overwrites these and the keys stay the same.
const SAMPLE_PROVINCES = [
{ id: 1, name: 'تهران' },
{ id: 2, name: 'اصفهان' },
{ id: 3, name: 'فارس' },
{ id: 4, name: 'خراسان رضوی' },
{ id: 5, name: 'قم' },
]
const SAMPLE_CITIES_BY_PROVINCE = {
1: [
{ id: 101, name: 'تهران' },
{ id: 102, name: 'ری' },
{ id: 103, name: 'شمیرانات' },
{ id: 104, name: 'اسلامشهر' },
],
2: [
{ id: 201, name: 'اصفهان' },
{ id: 202, name: 'کاشان' },
{ id: 203, name: 'نجف‌آباد' },
],
3: [
{ id: 301, name: 'شیراز' },
{ id: 302, name: 'مرودشت' },
{ id: 303, name: 'کازرون' },
],
4: [
{ id: 401, name: 'مشهد' },
{ id: 402, name: 'نیشابور' },
{ id: 403, name: 'سبزوار' },
],
5: [{ id: 501, name: 'قم' }],
}
export const useGetProvincesQuery = (options = {}) =>
useQuery({
queryKey: commonKeys.provinces(),
queryFn: () => apiGetProvinces(),
select: (response) => response?.data ?? response,
initialData: SAMPLE_PROVINCES,
...options,
})
@@ -20,5 +57,6 @@ export const useGetCitiesOfProvinceQuery = (provinceIdRef, options = {}) =>
queryKey: ['common', 'cities', provinceIdRef],
queryFn: () => apiGetCitiesOfProvince(provinceIdRef.value),
select: (response) => response?.data ?? response,
initialData: () => SAMPLE_CITIES_BY_PROVINCE[provinceIdRef.value] || [],
...options,
})