fix: ticket

This commit is contained in:
sajjadtalkhabi
2026-06-05 18:43:02 +03:30
parent 1c665830b0
commit ea562aeefa
53 changed files with 2475 additions and 859 deletions
+16
View File
@@ -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)
+14
View File
@@ -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)
+20 -15
View File
@@ -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 = {}) =>
-10
View File
@@ -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)
-12
View File
@@ -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 })
+11
View File
@@ -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)
@@ -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: '۱۶:۱۲',
},
],
},
]
+175 -9
View File
@@ -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
@@ -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',
},
]
@@ -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',
},
]
@@ -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,
}),
}))
+2 -1
View File
@@ -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,
@@ -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 }),
}))
@@ -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 }))
@@ -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] || [],
}))
@@ -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 }
})
+44
View File
@@ -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),
})
+10 -4
View File
@@ -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,
})
+44
View File
@@ -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),
})
-35
View File
@@ -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) })
-45
View File
@@ -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) })
+43
View File
@@ -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),
})