diff --git a/src/App.vue b/src/App.vue index 3ae8ca5..fba44b9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -15,6 +15,7 @@ import { useGetMeQuery } from '@/services/query/auth' import PublicLayout from '@/layouts/PublicLayout.vue' import StudentLayout from '@/layouts/StudentLayout.vue' import ConfirmModal from '@/components/ConfirmModal.vue' +import CounselorLayout from '@/layouts/CounselorLayout.vue' import { tokenService } from '@/services/api/token-service' import { VueQueryDevtools } from '@tanstack/vue-query-devtools' @@ -24,6 +25,7 @@ const layouts = { public: PublicLayout, admin: AdminLayout, student: StudentLayout, + counselor: CounselorLayout, } const currentLayout = computed(() => layouts[route.meta?.layout] || PublicLayout) diff --git a/src/enums/index.js b/src/enums/index.js index 75f4168..86722a7 100644 --- a/src/enums/index.js +++ b/src/enums/index.js @@ -165,6 +165,10 @@ export const ASSIGNMENT_PRIORITY = Object.freeze({ optional: 'اختیاری', }) +// Single ticket-lifecycle vocabulary. Used by every ticket-like resource — +// admin /messages, admin /services, admin /consultations, counselor, student +// /my-tickets, /user/services, /user/consultants. Backend exposes exactly +// these three values across all of them. export const TICKET_STATUS = Object.freeze({ open: 'باز', answered: 'پاسخ داده شده', @@ -182,22 +186,9 @@ export const PRIORITY_STATUS = Object.freeze({ low: 'پایین', }) -export const SERVICE_STATUS = Object.freeze({ - approved: 'تایید شده', - pending: 'در انتظار بررسی', - in_progress: 'در حال انجام', - completed: 'تکمیل شده', -}) - export const SERVICE_TYPE = Object.freeze({ loan: 'وام', consultation: 'مشاوره', educational: 'آموزشی', other: 'سایر', }) - -export const CONSULTATION_STATUS = Object.freeze({ - open: 'در حال گفتگو', - answered: 'پاسخ داده شده', - closed: 'بسته شده', -}) diff --git a/src/features/admin/consultations/components/ConsultationItem.vue b/src/features/admin/consultations/components/ConsultationItem.vue index 2747f65..01b2ec8 100644 --- a/src/features/admin/consultations/components/ConsultationItem.vue +++ b/src/features/admin/consultations/components/ConsultationItem.vue @@ -17,19 +17,15 @@
-
- تاریخ پیام : - {{ createdAt }} -
- + />
@@ -55,12 +51,19 @@ diff --git a/src/features/admin/services/pages/ServicesListPage.vue b/src/features/admin/services/pages/ServicesListPage.vue index e7ac00e..cc6aab2 100644 --- a/src/features/admin/services/pages/ServicesListPage.vue +++ b/src/features/admin/services/pages/ServicesListPage.vue @@ -18,7 +18,8 @@ -
+ +
+ +
@@ -37,56 +40,73 @@ diff --git a/src/features/counselor/components/modals/CounselorConsultationDetailsModal.vue b/src/features/counselor/components/modals/CounselorConsultationDetailsModal.vue new file mode 100644 index 0000000..b3cfcfc --- /dev/null +++ b/src/features/counselor/components/modals/CounselorConsultationDetailsModal.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/src/features/counselor/pages/CounselorConsultationsPage.vue b/src/features/counselor/pages/CounselorConsultationsPage.vue new file mode 100644 index 0000000..a202947 --- /dev/null +++ b/src/features/counselor/pages/CounselorConsultationsPage.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/src/features/counselor/router.js b/src/features/counselor/router.js new file mode 100644 index 0000000..38147b5 --- /dev/null +++ b/src/features/counselor/router.js @@ -0,0 +1,8 @@ +export default [ + { + path: '/counselor/consultations', + name: 'counselor-consultations', + component: () => import('@/features/counselor/pages/CounselorConsultationsPage.vue'), + meta: { layout: 'counselor', role: 'counselor', title: 'مشاوره' }, + }, +] diff --git a/src/features/student/consultants/components/ConsultantItem.vue b/src/features/student/consultants/components/ConsultantItem.vue index 03e38b6..502f5e6 100644 --- a/src/features/student/consultants/components/ConsultantItem.vue +++ b/src/features/student/consultants/components/ConsultantItem.vue @@ -22,10 +22,14 @@ import { computed } from 'vue' import Badge from '@/components/Badge.vue' +// Backend statuses are open/answered/closed. Legacy keys stay in place for any +// pre-migration cached data. const TONE_MAP = { approved: 'success', rejected: 'danger', pending: 'neutral', + open: 'neutral', + answered: 'success', closed: 'info', } diff --git a/src/features/student/consultants/components/RequestConsultantForm.vue b/src/features/student/consultants/components/RequestConsultantForm.vue index b0b7b11..20ff6a3 100644 --- a/src/features/student/consultants/components/RequestConsultantForm.vue +++ b/src/features/student/consultants/components/RequestConsultantForm.vue @@ -3,15 +3,12 @@
-
@@ -19,12 +16,12 @@
@@ -48,14 +45,13 @@ diff --git a/src/features/student/consultants/pages/StudentConsultantsPage.vue b/src/features/student/consultants/pages/StudentConsultantsPage.vue index cfd0463..66d8222 100644 --- a/src/features/student/consultants/pages/StudentConsultantsPage.vue +++ b/src/features/student/consultants/pages/StudentConsultantsPage.vue @@ -35,9 +35,11 @@ diff --git a/src/features/student/services/components/ServiceItem.vue b/src/features/student/services/components/ServiceItem.vue index 48b4630..bdd94bb 100644 --- a/src/features/student/services/components/ServiceItem.vue +++ b/src/features/student/services/components/ServiceItem.vue @@ -26,6 +26,9 @@ const TONE_MAP = { approved: 'success', rejected: 'danger', pending: 'neutral', + open: 'neutral', + answered: 'success', + closed: 'info', } const props = defineProps({ diff --git a/src/features/student/services/components/modals/ServiceDetailsModal.vue b/src/features/student/services/components/modals/ServiceDetailsModal.vue new file mode 100644 index 0000000..9626d2c --- /dev/null +++ b/src/features/student/services/components/modals/ServiceDetailsModal.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/src/features/student/services/pages/StudentServicesPage.vue b/src/features/student/services/pages/StudentServicesPage.vue index 18889ee..4f0da7b 100644 --- a/src/features/student/services/pages/StudentServicesPage.vue +++ b/src/features/student/services/pages/StudentServicesPage.vue @@ -33,12 +33,16 @@ + +
diff --git a/src/features/student/tickets/components/TicketItem.vue b/src/features/student/tickets/components/TicketItem.vue new file mode 100644 index 0000000..fd83ca3 --- /dev/null +++ b/src/features/student/tickets/components/TicketItem.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/src/features/student/tickets/components/modals/CreateTicketModal.vue b/src/features/student/tickets/components/modals/CreateTicketModal.vue new file mode 100644 index 0000000..d03a377 --- /dev/null +++ b/src/features/student/tickets/components/modals/CreateTicketModal.vue @@ -0,0 +1,249 @@ + + + + + diff --git a/src/features/student/tickets/components/modals/TicketDetailsModal.vue b/src/features/student/tickets/components/modals/TicketDetailsModal.vue new file mode 100644 index 0000000..ed79c1a --- /dev/null +++ b/src/features/student/tickets/components/modals/TicketDetailsModal.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/src/features/student/tickets/pages/StudentTicketsPage.vue b/src/features/student/tickets/pages/StudentTicketsPage.vue new file mode 100644 index 0000000..cf88bbb --- /dev/null +++ b/src/features/student/tickets/pages/StudentTicketsPage.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/src/layouts/CounselorLayout.vue b/src/layouts/CounselorLayout.vue new file mode 100644 index 0000000..c8d61e6 --- /dev/null +++ b/src/layouts/CounselorLayout.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/src/layouts/StudentLayout.vue b/src/layouts/StudentLayout.vue index e2a4591..5234c8c 100644 --- a/src/layouts/StudentLayout.vue +++ b/src/layouts/StudentLayout.vue @@ -95,6 +95,12 @@ const menuItems = computed(() => [ to: { name: 'student-consultants' }, active: route.name === 'student-consultants', }, + { + title: 'تیکت‌های من', + icon: 'chat-centered-dots', + to: { name: 'student-tickets' }, + active: route.name === 'student-tickets', + }, { title: 'گواهینامه ها', icon: 'scroll', diff --git a/src/layouts/sidebars/CounselorSidebar.vue b/src/layouts/sidebars/CounselorSidebar.vue new file mode 100644 index 0000000..74a8bc6 --- /dev/null +++ b/src/layouts/sidebars/CounselorSidebar.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/src/services/api/admin-services.js b/src/services/api/admin-services.js new file mode 100644 index 0000000..42deba2 --- /dev/null +++ b/src/services/api/admin-services.js @@ -0,0 +1,16 @@ +// Admin services UI hits the same /admin/tickets endpoints as the messages +// UI, scoped by ?type=service. Show/send/status reuse the admin-tickets +// helpers since the URL pattern is identical. +import { http } from '@/services/api/http' +import { buildUrl, endpoints } from '@/services/api/endpoints' + +export const apiGetAdminServices = (params) => + http.get(endpoints.getTicketsList, { params: { ...params, type: 'service' } }) + +export const apiShowAdminService = (id) => http.get(buildUrl(endpoints.showTicket, { id })) + +export const apiSendAdminServiceMessage = (id, payload) => + http.post(buildUrl(endpoints.sendTicketMessage, { id }), payload) + +export const apiChangeAdminServiceStatus = (id, payload) => + http.patch(buildUrl(endpoints.changeTicketStatus, { id }), payload) diff --git a/src/services/api/counselor-tickets.js b/src/services/api/counselor-tickets.js new file mode 100644 index 0000000..fa74713 --- /dev/null +++ b/src/services/api/counselor-tickets.js @@ -0,0 +1,14 @@ +import { http } from '@/services/api/http' +import { buildUrl, endpoints } from '@/services/api/endpoints' + +export const apiGetCounselorTickets = (params) => + http.get(endpoints.getCounselorTickets, { params }) + +export const apiShowCounselorTicket = (id) => + http.get(buildUrl(endpoints.showCounselorTicket, { id })) + +export const apiSendCounselorTicketMessage = (id, payload) => + http.post(buildUrl(endpoints.sendCounselorTicketMessage, { id }), payload) + +export const apiChangeCounselorTicketStatus = (id, payload) => + http.patch(buildUrl(endpoints.changeCounselorTicketStatus, { id }), payload) diff --git a/src/services/api/endpoints.js b/src/services/api/endpoints.js index 8e56477..3109267 100644 --- a/src/services/api/endpoints.js +++ b/src/services/api/endpoints.js @@ -35,14 +35,12 @@ export const endpoints = { getMissionaryDispatches: '/student/missionary/dispatches', getMissionaryNarratives: '/student/missionary/narratives', - getStudentServices: '/student/services', - createStudentService: '/student/services', - getStudentServiceTypes: '/student/service-types', - getStudentServiceTitles: '/student/service-titles', - - getStudentConsultants: '/student/consultants', - createStudentConsultant: '/student/consultants', - getStudentConsultantTitles: '/student/consultant-titles', + // Unified student tickets — handles services (type=service), consultants + // (type=advise) and inbox (type=ticket) via the same endpoint with a filter. + getStudentTickets: '/student/tickets', + createStudentTicket: '/student/tickets', + showStudentTicket: '/student/tickets/:id', + sendStudentTicketMessage: '/student/tickets/:id/messages', getStudentCertificates: '/student/certificates', downloadStudentCertificate: '/student/certificates/:id/download', @@ -140,18 +138,25 @@ export const endpoints = { showAssignmentSubmission: '/homework-submissions/:submissionId', // ─── 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). + // Admin sees every type via /admin/tickets, filtered by ?type=: + // - messages page → ?type=ticket + // - services page → ?type=service + // - consultations page → ?type=advise + // Counselor is restricted to /counselor/tickets (advise-only at the backend). getTicketsList: '/admin/tickets', showTicket: '/admin/tickets/:id', sendTicketMessage: '/admin/tickets/:id/messages', changeTicketStatus: '/admin/tickets/:id/status', - getConsultationsList: '/counselor/tickets', - showConsultation: '/counselor/tickets/:id', - sendConsultationMessage: '/counselor/tickets/:id/messages', - changeConsultationStatus: '/counselor/tickets/:id/status', + getConsultationsList: '/admin/tickets', + showConsultation: '/admin/tickets/:id', + sendConsultationMessage: '/admin/tickets/:id/messages', + changeConsultationStatus: '/admin/tickets/:id/status', + + getCounselorTickets: '/counselor/tickets', + showCounselorTicket: '/counselor/tickets/:id', + sendCounselorTicketMessage: '/counselor/tickets/:id/messages', + changeCounselorTicketStatus: '/counselor/tickets/:id/status', } export const buildUrl = (template, params = {}) => diff --git a/src/services/api/student-consultants.js b/src/services/api/student-consultants.js deleted file mode 100644 index 2fdedc8..0000000 --- a/src/services/api/student-consultants.js +++ /dev/null @@ -1,10 +0,0 @@ -import { http } from '@/services/api/http' -import { endpoints } from '@/services/api/endpoints' - -export const apiGetStudentConsultants = (params) => - http.get(endpoints.getStudentConsultants, { params }) - -export const apiCreateStudentConsultant = (payload) => - http.post(endpoints.createStudentConsultant, payload) - -export const apiGetStudentConsultantTitles = () => http.get(endpoints.getStudentConsultantTitles) diff --git a/src/services/api/student-services.js b/src/services/api/student-services.js deleted file mode 100644 index 128ef82..0000000 --- a/src/services/api/student-services.js +++ /dev/null @@ -1,12 +0,0 @@ -import { http } from '@/services/api/http' -import { endpoints } from '@/services/api/endpoints' - -export const apiGetStudentServices = (params) => http.get(endpoints.getStudentServices, { params }) - -export const apiCreateStudentService = (payload) => - http.post(endpoints.createStudentService, payload) - -export const apiGetStudentServiceTypes = () => http.get(endpoints.getStudentServiceTypes) - -export const apiGetStudentServiceTitles = (params) => - http.get(endpoints.getStudentServiceTitles, { params }) diff --git a/src/services/api/student-tickets.js b/src/services/api/student-tickets.js new file mode 100644 index 0000000..8b1e1cd --- /dev/null +++ b/src/services/api/student-tickets.js @@ -0,0 +1,11 @@ +import { http } from '@/services/api/http' +import { buildUrl, endpoints } from '@/services/api/endpoints' + +export const apiGetStudentTickets = (params) => http.get(endpoints.getStudentTickets, { params }) + +export const apiShowStudentTicket = (id) => http.get(buildUrl(endpoints.showStudentTicket, { id })) + +export const apiCreateStudentTicket = (payload) => http.post(endpoints.createStudentTicket, payload) + +export const apiSendStudentTicketMessage = (id, payload) => + http.post(buildUrl(endpoints.sendStudentTicketMessage, { id }), payload) diff --git a/src/services/mock/fixtures/admin-consultations.js b/src/services/mock/fixtures/admin-consultations.js deleted file mode 100644 index 24f54bf..0000000 --- a/src/services/mock/fixtures/admin-consultations.js +++ /dev/null @@ -1,86 +0,0 @@ -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: '۱۶:۱۲', - }, - ], - }, -] diff --git a/src/services/mock/fixtures/admin-tickets.js b/src/services/mock/fixtures/admin-tickets.js index 8bdb617..d20791a 100644 --- a/src/services/mock/fixtures/admin-tickets.js +++ b/src/services/mock/fixtures/admin-tickets.js @@ -1,9 +1,10 @@ // Shape mirrors backend Postman doc for /admin/tickets: -// ticket: { id, student_id, assigned_to_user_id, target_role, status, subject, -// created_at, student, assignee, messages: [{ id, ticket_id, -// sender_id, message, created_at, sender }] } -// Field names use camelCase here (snake_case bridge happens at the HTTP layer -// when we swap the mock for the real backend). +// ticket: { id, student_id, assigned_to_user_id, type, category, status, +// subject, created_at, media: [], +// student, assignee, +// messages: [{ id, ticket_id, sender_id, message, created_at, sender }] } +// Fixture uses camelCase — the mock adapter returns it as-is, mimicking what +// the FE sees post-camelize when wired to the real backend. const makeUser = (overrides) => ({ id: overrides.id, @@ -46,6 +47,26 @@ const studentAli = makeUser({ createdAt: '2026-02-10T10:00:00.000Z', }) +const studentReza = makeUser({ + id: 104, + name: 'رضا حسینی', + email: 'reza@example.com', + phone: '+989124444444', + roles: ['student'], + avatarUrl: '', + createdAt: '2026-02-12T10:00:00.000Z', +}) + +const studentZahra = makeUser({ + id: 105, + name: 'زهرا کریمی', + email: 'zahra@example.com', + phone: '+989125555555', + roles: ['student'], + avatarUrl: '', + createdAt: '2026-02-15T10:00:00.000Z', +}) + const adminUser = makeUser({ id: 1, name: 'مدیر سامانه', @@ -54,15 +75,25 @@ const adminUser = makeUser({ createdAt: '2026-01-01T00:00:00.000Z', }) +const counselorUser = makeUser({ + id: 7, + name: 'مشاور سامانه', + email: 'counselor@example.com', + roles: ['counselor'], + createdAt: '2026-01-15T00:00:00.000Z', +}) + export const adminTickets = [ { id: 401, studentId: studentJane.id, assignedToUserId: adminUser.id, - targetRole: 'admin', + type: 'ticket', + category: 'enrollment', status: 'answered', subject: 'پیگیری وضعیت ثبت‌نام', createdAt: '2026-05-09T10:30:00.000Z', + media: [], student: studentJane, assignee: adminUser, messages: [ @@ -96,10 +127,12 @@ export const adminTickets = [ id: 402, studentId: studentMaryam.id, assignedToUserId: null, - targetRole: 'admin', + type: 'ticket', + category: 'schedule', status: 'open', subject: 'سوال درباره جلسه آموزشی', createdAt: '2026-05-08T09:15:00.000Z', + media: [], student: studentMaryam, assignee: null, messages: [ @@ -117,10 +150,12 @@ export const adminTickets = [ id: 403, studentId: studentAli.id, assignedToUserId: adminUser.id, - targetRole: 'admin', + type: 'ticket', + category: 'billing', status: 'closed', subject: 'درخواست بستن تیکت', createdAt: '2026-05-07T14:10:00.000Z', + media: [], student: studentAli, assignee: adminUser, messages: [ @@ -142,7 +177,138 @@ export const adminTickets = [ }, ], }, + { + id: 501, + studentId: studentReza.id, + assignedToUserId: adminUser.id, + type: 'service', + category: 'loan', + status: 'open', + subject: 'درخواست وام تحصیلی', + createdAt: '2026-05-12T13:54:00.000Z', + media: [], + student: studentReza, + assignee: adminUser, + messages: [ + { + id: 1, + ticketId: 501, + senderId: studentReza.id, + message: 'برای ادامه تحصیل به وام نیاز دارم.', + createdAt: '2026-05-12T13:54:00.000Z', + sender: studentReza, + }, + ], + }, + { + id: 502, + studentId: studentJane.id, + assignedToUserId: null, + type: 'service', + category: 'insurance', + status: 'answered', + subject: 'درخواست بیمه دانشجویی', + createdAt: '2026-05-10T08:20:00.000Z', + media: [], + student: studentJane, + assignee: null, + messages: [ + { + id: 1, + ticketId: 502, + senderId: studentJane.id, + message: 'لطفا اطلاعات بیمه را ارسال کنید.', + createdAt: '2026-05-10T08:20:00.000Z', + sender: studentJane, + }, + ], + }, + { + id: 601, + studentId: studentZahra.id, + assignedToUserId: counselorUser.id, + type: 'advise', + category: 'education', + status: 'answered', + subject: 'مشاوره انتخاب رشته', + createdAt: '2026-05-12T13:54:00.000Z', + media: [], + student: studentZahra, + assignee: counselorUser, + messages: [ + { + id: 1, + ticketId: 601, + senderId: studentZahra.id, + message: 'سلام، در خصوص انتخاب رشته مشاوره می‌خواستم.', + createdAt: '2026-05-12T13:54:00.000Z', + sender: studentZahra, + }, + { + id: 2, + ticketId: 601, + senderId: counselorUser.id, + message: 'سلام و وقت بخیر، لطفا رشته فعلی و علاقه‌مندی‌های خود را بفرمایید.', + createdAt: '2026-05-12T14:02:00.000Z', + sender: counselorUser, + }, + ], + }, + { + id: 602, + studentId: studentMaryam.id, + assignedToUserId: null, + type: 'advise', + category: 'career', + status: 'open', + subject: 'مشاوره مسیر شغلی', + createdAt: '2026-05-11T09:30:00.000Z', + media: [], + student: studentMaryam, + assignee: null, + messages: [ + { + id: 1, + ticketId: 602, + senderId: studentMaryam.id, + message: 'برای ادامه تحصیل در سطح ۳ راهنمایی نیاز دارم.', + createdAt: '2026-05-11T09:30:00.000Z', + sender: studentMaryam, + }, + ], + }, + { + id: 603, + studentId: studentAli.id, + assignedToUserId: counselorUser.id, + type: 'advise', + category: 'personal', + status: 'closed', + subject: 'مشاوره فردی', + createdAt: '2026-05-09T16:10:00.000Z', + media: [], + student: studentAli, + assignee: counselorUser, + messages: [ + { + id: 1, + ticketId: 603, + senderId: studentAli.id, + message: 'مشاوره دریافت شد، با تشکر.', + createdAt: '2026-05-09T16:10:00.000Z', + sender: studentAli, + }, + { + id: 2, + ticketId: 603, + senderId: counselorUser.id, + message: 'موفق باشید.', + createdAt: '2026-05-09T16:12:00.000Z', + sender: counselorUser, + }, + ], + }, ] -// The signed-in admin used as `sender` when a message is posted from the FE. export const currentAdminUser = adminUser +export const currentCounselorUser = counselorUser diff --git a/src/services/mock/fixtures/student-consultants.js b/src/services/mock/fixtures/student-consultants.js deleted file mode 100644 index 56591dc..0000000 --- a/src/services/mock/fixtures/student-consultants.js +++ /dev/null @@ -1,50 +0,0 @@ -export const studentConsultantTitles = [ - { value: 'course-talk', label: 'گفتگو درباره دوره' }, - { value: 'academic', label: 'مشاوره تحصیلی' }, - { value: 'career', label: 'مشاوره شغلی' }, - { value: 'psychology', label: 'مشاوره روانشناسی' }, - { value: 'other', label: 'سایر' }, -] - -const STATUS_LABELS = { - pending: 'در حال بررسی', - approved: 'پاسخ داده شده', - rejected: 'رد شده', - closed: 'بسته شده', -} - -export const studentConsultants = [ - { - id: 'c-1', - requestNumber: 215, - title: 'درخواست مشاوره', - topicLabel: 'گفتگو درباره دوره', - status: 'pending', - statusLabel: STATUS_LABELS.pending, - description: 'سوال در مورد محتوای دوره', - timeLabel: '12:53', - dateLabel: '1404/12/21', - }, - { - id: 'c-2', - requestNumber: 198, - title: 'درخواست مشاوره', - topicLabel: 'مشاوره شغلی', - status: 'approved', - statusLabel: STATUS_LABELS.approved, - description: 'راهنمایی برای انتخاب مسیر شغلی', - timeLabel: '09:12', - dateLabel: '1404/11/05', - }, - { - id: 'c-3', - requestNumber: 174, - title: 'درخواست مشاوره', - topicLabel: 'مشاوره تحصیلی', - status: 'rejected', - statusLabel: STATUS_LABELS.rejected, - description: 'برنامه‌ریزی درسی', - timeLabel: '16:40', - dateLabel: '1404/10/02', - }, -] diff --git a/src/services/mock/fixtures/student-services.js b/src/services/mock/fixtures/student-services.js deleted file mode 100644 index 0dedc07..0000000 --- a/src/services/mock/fixtures/student-services.js +++ /dev/null @@ -1,61 +0,0 @@ -export const studentServiceTypes = [ - { value: 'loan', label: 'وام' }, - { value: 'consultation', label: 'مشاوره' }, - { value: 'insurance', label: 'بیمه' }, - { value: 'other', label: 'سایر' }, -] - -export const studentServiceTitlesByType = { - loan: [ - { value: 'tuition-loan', label: 'وام شهریه' }, - { value: 'living-loan', label: 'وام معیشت' }, - ], - consultation: [ - { value: 'course-talk', label: 'گفتگو درباره دوره' }, - { value: 'career-talk', label: 'مشاوره شغلی' }, - ], - insurance: [{ value: 'student-insurance', label: 'بیمه دانشجویی' }], - other: [{ value: 'other-issue', label: 'موضوع دیگر' }], -} - -const STATUS_LABELS = { - approved: 'تاییــد شـــــده', - pending: 'در انتظار بررسی', - rejected: 'رد شده', -} - -export const studentServices = [ - { - id: 's-1', - requestNumber: 387, - title: 'درخواست وام', - typeKey: 'loan', - typeLabel: 'وام', - status: 'approved', - statusLabel: STATUS_LABELS.approved, - description: 'لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ', - createdAt: '1404/05/12', - }, - { - id: 's-2', - requestNumber: 44, - title: 'درخواست وام', - typeKey: 'loan', - typeLabel: 'وام', - status: 'pending', - statusLabel: STATUS_LABELS.pending, - description: 'درخواست شهریه ترم جدید', - createdAt: '1404/06/02', - }, - { - id: 's-3', - requestNumber: 18, - title: 'گفتگو درباره دوره', - typeKey: 'consultation', - typeLabel: 'مشاوره', - status: 'rejected', - statusLabel: STATUS_LABELS.rejected, - description: 'سوال در مورد محتوای دوره', - createdAt: '1404/04/22', - }, -] diff --git a/src/services/mock/routes/admin-consultations.js b/src/services/mock/routes/admin-consultations.js deleted file mode 100644 index 99db986..0000000 --- a/src/services/mock/routes/admin-consultations.js +++ /dev/null @@ -1,56 +0,0 @@ -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, - }), -})) diff --git a/src/services/mock/routes/admin-tickets.js b/src/services/mock/routes/admin-tickets.js index b9f5b73..0069697 100644 --- a/src/services/mock/routes/admin-tickets.js +++ b/src/services/mock/routes/admin-tickets.js @@ -13,6 +13,7 @@ import { register('GET', endpoints.getTicketsList, ({ query }) => { let list = filterItems(adminTickets, query, { + type: 'eq', status: 'eq', subject: (item, v) => String(item.subject || '') @@ -51,7 +52,7 @@ register('POST', endpoints.sendTicketMessage, ({ params, data }) => { } ticket.messages = [...(ticket.messages || []), message] ticket.status = 'answered' - ticket.assigneeId = currentAdminUser.id + ticket.assignedToUserId = currentAdminUser.id ticket.assignee = currentAdminUser return { success: true, diff --git a/src/services/mock/routes/counselor-tickets.js b/src/services/mock/routes/counselor-tickets.js new file mode 100644 index 0000000..7d7762d --- /dev/null +++ b/src/services/mock/routes/counselor-tickets.js @@ -0,0 +1,58 @@ +import { register } from '@/services/mock/registry' +import { endpoints } from '@/services/api/endpoints' +import { adminTickets, currentCounselorUser } from '@/services/mock/fixtures/admin-tickets' +import { + filterDateRange, + filterItems, + findOrThrow, + isoNow, + makeId, + paginate, + updateById, +} from '@/services/mock/helpers' + +// Counselor scope: backend hides every ticket whose type !== 'advise'. +const counselorPool = () => adminTickets.filter((t) => t.type === 'advise') + +register('GET', endpoints.getCounselorTickets, ({ query }) => { + let list = filterItems(counselorPool(), query, { + type: 'eq', + status: 'eq', + userName: (item, v) => + String(item.student?.name || '') + .toLowerCase() + .includes(String(v).toLowerCase()), + }) + list = filterDateRange(list, query) + const { data: items, meta } = paginate(list, query) + return { success: true, message: 'OK', data: items, meta } +}) + +register('GET', endpoints.showCounselorTicket, ({ params }) => ({ + success: true, + message: 'OK', + data: findOrThrow(counselorPool(), params.id), +})) + +register('POST', endpoints.sendCounselorTicketMessage, ({ params, data }) => { + const ticket = findOrThrow(counselorPool(), params.id) + const message = { + id: makeId(), + ticketId: ticket.id, + senderId: currentCounselorUser.id, + message: data.message || '', + createdAt: isoNow(), + sender: currentCounselorUser, + } + ticket.messages = [...(ticket.messages || []), message] + ticket.status = 'answered' + ticket.assignedToUserId = currentCounselorUser.id + ticket.assignee = currentCounselorUser + return { success: true, message: 'Message posted.', data: message } +}) + +register('PATCH', endpoints.changeCounselorTicketStatus, ({ params, data }) => ({ + success: true, + message: 'Ticket status updated.', + data: updateById(adminTickets, params.id, { status: data.status }), +})) diff --git a/src/services/mock/routes/student-consultants.js b/src/services/mock/routes/student-consultants.js deleted file mode 100644 index f3f4739..0000000 --- a/src/services/mock/routes/student-consultants.js +++ /dev/null @@ -1,29 +0,0 @@ -import { paginate } from '@/services/mock/helpers' -import { register } from '@/services/mock/registry' -import { endpoints } from '@/services/api/endpoints' -import { - studentConsultantTitles, - studentConsultants, -} from '@/services/mock/fixtures/student-consultants' - -register('GET', endpoints.getStudentConsultants, ({ query }) => paginate(studentConsultants, query)) - -register('POST', endpoints.createStudentConsultant, ({ data }) => { - const id = `c-${studentConsultants.length + 1}` - const next = { - id, - requestNumber: 500 + studentConsultants.length + 1, - title: 'درخواست مشاوره', - topicLabel: - studentConsultantTitles.find((t) => t.value === data?.title)?.label || data?.title || '—', - status: 'pending', - statusLabel: 'در حال بررسی', - description: data?.description || '', - timeLabel: '12:00', - dateLabel: '1404/06/12', - } - studentConsultants.unshift(next) - return { data: next } -}) - -register('GET', endpoints.getStudentConsultantTitles, () => ({ data: studentConsultantTitles })) diff --git a/src/services/mock/routes/student-services.js b/src/services/mock/routes/student-services.js deleted file mode 100644 index f99b9bd..0000000 --- a/src/services/mock/routes/student-services.js +++ /dev/null @@ -1,33 +0,0 @@ -import { paginate } from '@/services/mock/helpers' -import { register } from '@/services/mock/registry' -import { endpoints } from '@/services/api/endpoints' -import { - studentServiceTitlesByType, - studentServiceTypes, - studentServices, -} from '@/services/mock/fixtures/student-services' - -register('GET', endpoints.getStudentServices, ({ query }) => paginate(studentServices, query)) - -register('POST', endpoints.createStudentService, ({ data }) => { - const id = `s-${studentServices.length + 1}` - const next = { - id, - requestNumber: 500 + studentServices.length + 1, - title: data?.title || '—', - typeKey: data?.typeKey || 'other', - typeLabel: studentServiceTypes.find((t) => t.value === data?.typeKey)?.label || 'سایر', - status: 'pending', - statusLabel: 'در انتظار بررسی', - description: data?.description || '', - createdAt: '1404/06/12', - } - studentServices.unshift(next) - return { data: next } -}) - -register('GET', endpoints.getStudentServiceTypes, () => ({ data: studentServiceTypes })) - -register('GET', endpoints.getStudentServiceTitles, ({ query }) => ({ - data: studentServiceTitlesByType[query.type] || [], -})) diff --git a/src/services/mock/routes/student-tickets.js b/src/services/mock/routes/student-tickets.js new file mode 100644 index 0000000..71168dc --- /dev/null +++ b/src/services/mock/routes/student-tickets.js @@ -0,0 +1,84 @@ +import { register } from '@/services/mock/registry' +import { endpoints } from '@/services/api/endpoints' +import { adminTickets } from '@/services/mock/fixtures/admin-tickets' +import { filterItems, findOrThrow, isoNow, makeId, paginate } from '@/services/mock/helpers' + +// Student scope: backend scopes results to the signed-in student. The mock +// pretends every ticket whose student is studentJane (id=100) belongs to "me". +const SELF_STUDENT_ID = 100 + +const myPool = () => adminTickets.filter((t) => t.studentId === SELF_STUDENT_ID) + +register('GET', endpoints.getStudentTickets, ({ query }) => { + const list = filterItems(myPool(), query, { + type: 'eq', + status: 'eq', + }) + const { data: items, meta } = paginate(list, query) + return { success: true, message: 'OK', data: items, meta } +}) + +register('GET', endpoints.showStudentTicket, ({ params }) => ({ + success: true, + message: 'OK', + data: findOrThrow(myPool(), params.id), +})) + +register('POST', endpoints.createStudentTicket, ({ data }) => { + const me = myPool()[0]?.student || adminTickets[0].student + const ticket = { + id: makeId(), + studentId: me.id, + assignedToUserId: null, + type: data.type || 'ticket', + category: data.category || null, + status: 'open', + subject: data.subject || '', + createdAt: isoNow(), + media: [], + student: me, + assignee: null, + messages: [ + { + id: makeId(), + ticketId: 0, + senderId: me.id, + message: data.message || '', + createdAt: isoNow(), + sender: me, + }, + ], + } + ticket.messages[0].ticketId = ticket.id + adminTickets.unshift(ticket) + return { + success: true, + message: 'Ticket created.', + data: { + id: ticket.id, + studentId: ticket.studentId, + assignedToUserId: ticket.assignedToUserId, + type: ticket.type, + category: ticket.category, + status: ticket.status, + subject: ticket.subject, + createdAt: ticket.createdAt, + media: ticket.media, + }, + } +}) + +register('POST', endpoints.sendStudentTicketMessage, ({ params, data }) => { + const ticket = findOrThrow(myPool(), params.id) + const me = ticket.student + const message = { + id: makeId(), + ticketId: ticket.id, + senderId: me.id, + message: data.message || '', + createdAt: isoNow(), + sender: me, + } + ticket.messages = [...(ticket.messages || []), message] + return { success: true, message: 'Message posted.', data: message } +}) diff --git a/src/services/query/admin-services.js b/src/services/query/admin-services.js new file mode 100644 index 0000000..652ae22 --- /dev/null +++ b/src/services/query/admin-services.js @@ -0,0 +1,44 @@ +import { cleanFilters } from '@/utils/clean-filters' +import { useMutation, useQuery } from '@tanstack/vue-query' +import { + apiChangeAdminServiceStatus, + apiGetAdminServices, + apiSendAdminServiceMessage, + apiShowAdminService, +} from '@/services/api/admin-services' + +export const adminServicesKeys = { + all: ['admin', 'services'], + list: (filters, pagination) => ['admin', 'services', 'list', filters, pagination], + detail: (id) => ['admin', 'services', 'detail', id], +} + +export const useAdminServicesListQuery = (filtersRef, paginationRef, options = {}) => + useQuery({ + queryKey: ['admin', 'services', 'list', filtersRef, paginationRef], + queryFn: () => + apiGetAdminServices({ ...cleanFilters(filtersRef.value), ...paginationRef.value }), + select: (response) => ({ + data: response?.data ?? [], + meta: response?.meta, + }), + ...options, + }) + +export const useAdminServiceQuery = (idRef, options = {}) => + useQuery({ + queryKey: ['admin', 'services', 'detail', idRef], + queryFn: () => apiShowAdminService(idRef.value), + select: (response) => response?.data ?? response, + ...options, + }) + +export const useSendAdminServiceMessageMutation = () => + useMutation({ + mutationFn: ({ id, payload }) => apiSendAdminServiceMessage(id, payload), + }) + +export const useChangeAdminServiceStatusMutation = () => + useMutation({ + mutationFn: ({ id, payload }) => apiChangeAdminServiceStatus(id, payload), + }) diff --git a/src/services/query/admin-tickets.js b/src/services/query/admin-tickets.js index 48693c3..601bdd4 100644 --- a/src/services/query/admin-tickets.js +++ b/src/services/query/admin-tickets.js @@ -13,15 +13,21 @@ export const adminTicketsKeys = { detail: (id) => ['admin', 'tickets', 'detail', id], } +// Backend may wrap the list as `{success, data: [...]}` (flat) or +// `{data: {data: [...], meta}}` (Laravel default). Normalize. +const selectList = (response) => { + const inner = response?.data + if (Array.isArray(inner)) return { data: inner, meta: response?.meta } + if (Array.isArray(inner?.data)) return { data: inner.data, meta: inner?.meta ?? response?.meta } + return { data: [], meta: response?.meta } +} + export const useAdminTicketsListQuery = (filtersRef, paginationRef, options = {}) => useQuery({ queryKey: ['admin', 'tickets', 'list', filtersRef, paginationRef], queryFn: () => apiGetAdminTickets({ ...cleanFilters(filtersRef.value), ...paginationRef.value }), - select: (response) => ({ - data: response?.data ?? [], - meta: response?.meta, - }), + select: selectList, ...options, }) diff --git a/src/services/query/counselor-tickets.js b/src/services/query/counselor-tickets.js new file mode 100644 index 0000000..825865a --- /dev/null +++ b/src/services/query/counselor-tickets.js @@ -0,0 +1,44 @@ +import { cleanFilters } from '@/utils/clean-filters' +import { useMutation, useQuery } from '@tanstack/vue-query' +import { + apiChangeCounselorTicketStatus, + apiGetCounselorTickets, + apiSendCounselorTicketMessage, + apiShowCounselorTicket, +} from '@/services/api/counselor-tickets' + +export const counselorTicketsKeys = { + all: ['counselor', 'tickets'], + list: (filters, pagination) => ['counselor', 'tickets', 'list', filters, pagination], + detail: (id) => ['counselor', 'tickets', 'detail', id], +} + +export const useCounselorTicketsListQuery = (filtersRef, paginationRef, options = {}) => + useQuery({ + queryKey: ['counselor', 'tickets', 'list', filtersRef, paginationRef], + queryFn: () => + apiGetCounselorTickets({ ...cleanFilters(filtersRef.value), ...paginationRef.value }), + select: (response) => ({ + data: response?.data ?? [], + meta: response?.meta, + }), + ...options, + }) + +export const useCounselorTicketQuery = (idRef, options = {}) => + useQuery({ + queryKey: ['counselor', 'tickets', 'detail', idRef], + queryFn: () => apiShowCounselorTicket(idRef.value), + select: (response) => response?.data ?? response, + ...options, + }) + +export const useSendCounselorTicketMessageMutation = () => + useMutation({ + mutationFn: ({ id, payload }) => apiSendCounselorTicketMessage(id, payload), + }) + +export const useChangeCounselorTicketStatusMutation = () => + useMutation({ + mutationFn: ({ id, payload }) => apiChangeCounselorTicketStatus(id, payload), + }) diff --git a/src/services/query/student-consultants.js b/src/services/query/student-consultants.js deleted file mode 100644 index 966a80f..0000000 --- a/src/services/query/student-consultants.js +++ /dev/null @@ -1,35 +0,0 @@ -import { cleanFilters } from '@/utils/clean-filters' -import { useMutation, useQuery } from '@tanstack/vue-query' -import { - apiCreateStudentConsultant, - apiGetStudentConsultantTitles, - apiGetStudentConsultants, -} from '@/services/api/student-consultants' - -export const studentConsultantsKeys = { - all: ['student', 'consultants'], - list: (filters, pagination) => ['student', 'consultants', 'list', filters, pagination], - titles: ['student', 'consultant-titles'], -} - -export const useStudentConsultantsQuery = (filtersRef, paginationRef, options = {}) => - useQuery({ - queryKey: ['student', 'consultants', 'list', filtersRef, paginationRef], - queryFn: () => - apiGetStudentConsultants({ - ...cleanFilters(filtersRef.value), - ...paginationRef.value, - }), - ...options, - }) - -export const useStudentConsultantTitlesQuery = (options = {}) => - useQuery({ - queryKey: studentConsultantsKeys.titles, - queryFn: () => apiGetStudentConsultantTitles(), - select: (response) => response?.data ?? [], - ...options, - }) - -export const useCreateStudentConsultantMutation = () => - useMutation({ mutationFn: (payload) => apiCreateStudentConsultant(payload) }) diff --git a/src/services/query/student-services.js b/src/services/query/student-services.js deleted file mode 100644 index d30c577..0000000 --- a/src/services/query/student-services.js +++ /dev/null @@ -1,45 +0,0 @@ -import { cleanFilters } from '@/utils/clean-filters' -import { useMutation, useQuery } from '@tanstack/vue-query' -import { - apiCreateStudentService, - apiGetStudentServiceTitles, - apiGetStudentServiceTypes, - apiGetStudentServices, -} from '@/services/api/student-services' - -export const studentServicesKeys = { - all: ['student', 'services'], - list: (filters, pagination) => ['student', 'services', 'list', filters, pagination], - types: ['student', 'service-types'], - titles: (typeKey) => ['student', 'service-titles', typeKey], -} - -export const useStudentServicesQuery = (filtersRef, paginationRef, options = {}) => - useQuery({ - queryKey: ['student', 'services', 'list', filtersRef, paginationRef], - queryFn: () => - apiGetStudentServices({ - ...cleanFilters(filtersRef.value), - ...paginationRef.value, - }), - ...options, - }) - -export const useStudentServiceTypesQuery = (options = {}) => - useQuery({ - queryKey: studentServicesKeys.types, - queryFn: () => apiGetStudentServiceTypes(), - select: (response) => response?.data ?? [], - ...options, - }) - -export const useStudentServiceTitlesQuery = (typeKeyRef, options = {}) => - useQuery({ - queryKey: ['student', 'service-titles', typeKeyRef], - queryFn: () => apiGetStudentServiceTitles({ type: typeKeyRef.value }), - select: (response) => response?.data ?? [], - ...options, - }) - -export const useCreateStudentServiceMutation = () => - useMutation({ mutationFn: (payload) => apiCreateStudentService(payload) }) diff --git a/src/services/query/student-tickets.js b/src/services/query/student-tickets.js new file mode 100644 index 0000000..a2a65ed --- /dev/null +++ b/src/services/query/student-tickets.js @@ -0,0 +1,43 @@ +import { cleanFilters } from '@/utils/clean-filters' +import { useMutation, useQuery } from '@tanstack/vue-query' +import { + apiCreateStudentTicket, + apiGetStudentTickets, + apiSendStudentTicketMessage, + apiShowStudentTicket, +} from '@/services/api/student-tickets' + +export const studentTicketsKeys = { + all: ['student', 'tickets'], + byType: (type) => ['student', 'tickets', 'type', type], + list: (filters, pagination) => ['student', 'tickets', 'list', filters, pagination], + detail: (id) => ['student', 'tickets', 'detail', id], +} + +export const useStudentTicketsQuery = (filtersRef, paginationRef, options = {}) => + useQuery({ + queryKey: ['student', 'tickets', 'list', filtersRef, paginationRef], + queryFn: () => + apiGetStudentTickets({ ...cleanFilters(filtersRef.value), ...paginationRef.value }), + select: (response) => ({ + data: response?.data ?? [], + meta: response?.meta, + }), + ...options, + }) + +export const useStudentTicketQuery = (idRef, options = {}) => + useQuery({ + queryKey: ['student', 'tickets', 'detail', idRef], + queryFn: () => apiShowStudentTicket(idRef.value), + select: (response) => response?.data ?? response, + ...options, + }) + +export const useCreateStudentTicketMutation = () => + useMutation({ mutationFn: (payload) => apiCreateStudentTicket(payload) }) + +export const useSendStudentTicketMessageMutation = () => + useMutation({ + mutationFn: ({ id, payload }) => apiSendStudentTicketMessage(id, payload), + })