Appearance
Epic 28 — Reports & Help (Support Tickets)
Users can open a support/help request from anywhere in the platform — optionally linking it to a job, post, application, or user. Requests appear in the admin panel as support tickets. Admins reply inside the thread, change status, and can assign tickets to themselves. Users are notified on every admin reply. The ticket flows through New → Open → Waiting Response → Solved / Closed (Solved and Closed are terminal).
BE-SUPPORT-001 — SupportTicket + SupportTicketReply models
- [x] Done
Files:
- Edit:
prisma/schema.prisma— addSupportTicket,SupportTicketReplymodels +SupportTicketStatus,SupportTicketCategory,SupportLinkedEntityTypeenums - Run:
pnpm db:migrate
Schema:
prisma
enum SupportTicketStatus {
NEW
OPEN
WAITING_RESPONSE
SOLVED
CLOSED
}
enum SupportTicketCategory {
GENERAL
TECHNICAL
JOB
APPLICATION
PAYMENT
ACCOUNT
CONTENT
OTHER
}
enum SupportLinkedEntityType {
JOB
POST
APPLICATION
USER
PET_JOB
}
model SupportTicket {
id String @id @default(cuid())
userId String
user User @relation("SupportTicketUser", fields: [userId], references: [id], onDelete: Cascade)
subject String
body String
category SupportTicketCategory @default(GENERAL)
status SupportTicketStatus @default(NEW)
linkedEntityType SupportLinkedEntityType?
linkedEntityId String?
assignedAdminId String?
assignedAdmin User? @relation("SupportTicketAdmin", fields: [assignedAdminId], references: [id])
replies SupportTicketReply[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId, createdAt])
@@index([status, createdAt])
@@index([assignedAdminId])
@@map("support_tickets")
}
model SupportTicketReply {
id String @id @default(cuid())
ticketId String
ticket SupportTicket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
authorId String
author User @relation("SupportTicketReplyAuthor", fields: [authorId], references: [id])
body String
isAdminReply Boolean @default(false)
createdAt DateTime @default(now())
@@index([ticketId, createdAt])
@@map("support_ticket_replies")
}User model gets 3 new relations:
prisma
supportTickets SupportTicket[] @relation("SupportTicketUser")
assignedSupportTickets SupportTicket[] @relation("SupportTicketAdmin")
supportTicketReplies SupportTicketReply[] @relation("SupportTicketReplyAuthor")BE-SUPPORT-002 — Support ticket service + GraphQL
- [x] Done
Files:
- Create:
src/services/support/index.ts - Edit:
src/graphql/schema/index.ts— types + queries + mutations - Edit:
src/graphql/resolvers/admin.ts— admin mutations - Create:
src/graphql/resolvers/support.ts - Edit:
src/graphql/resolvers/index.ts - Create:
src/__tests__/services/support.test.ts - Create:
src/__tests__/resolvers/support.test.ts
GraphQL:
graphql
enum SupportTicketStatus { NEW OPEN WAITING_RESPONSE SOLVED CLOSED }
enum SupportTicketCategory { GENERAL TECHNICAL JOB APPLICATION PAYMENT ACCOUNT CONTENT OTHER }
enum SupportLinkedEntityType { JOB POST APPLICATION USER PET_JOB }
type SupportTicket {
id: ID!
userId: ID!
user: User!
subject: String!
body: String!
category: SupportTicketCategory!
status: SupportTicketStatus!
linkedEntityType: SupportLinkedEntityType
linkedEntityId: ID
assignedAdmin: User
replies: [SupportTicketReply!]!
replyCount: Int!
createdAt: DateTime!
updatedAt: DateTime!
}
type SupportTicketReply {
id: ID!
ticketId: ID!
authorId: ID!
author: User!
body: String!
isAdminReply: Boolean!
createdAt: DateTime!
}
type SupportTicketPage {
tickets: [SupportTicket!]!
page: Int!; pageSize: Int!; totalCount: Int!; totalPages: Int!
}
input CreateSupportTicketInput {
subject: String!
body: String!
category: SupportTicketCategory
linkedEntityType: SupportLinkedEntityType
linkedEntityId: ID
}
input SupportTicketFilter { status: SupportTicketStatus; category: SupportTicketCategory; assignedAdminId: ID; userId: ID }
# Queries
mySupportTickets(page: Int, pageSize: Int, status: SupportTicketStatus): SupportTicketPage!
supportTicket(id: ID!): SupportTicket
adminSupportTickets(filter: SupportTicketFilter, page: Int, pageSize: Int): SupportTicketPage! # ADMIN
# Mutations
createSupportTicket(input: CreateSupportTicketInput!): SupportTicket!
addSupportTicketReply(ticketId: ID!, body: String!): SupportTicketReply!
updateSupportTicketStatus(ticketId: ID!, status: SupportTicketStatus!): SupportTicket! # ADMIN
assignSupportTicket(ticketId: ID!, adminId: ID): SupportTicket! # ADMIN (adminId null = unassign)Business rules:
createSupportTicket: validates subject (1–200 chars), body (1–2000 chars). Auto-opens if user already has an open ticket in same category (no duplicate NEW tickets per category).addSupportTicketReply: both user (ticket owner) and admin can reply. When user replies → status auto-transitions NEW/SOLVED → OPEN; CLOSED tickets cannot receive replies. When admin replies → status auto-transitions NEW/OPEN → WAITING_RESPONSE; firesSUPPORT_REPLYnotification to ticket owner.updateSupportTicketStatus: admin only; SOLVED and CLOSED are terminal (cannot re-open). CLOSED = admin manually closes. SOLVED = issue resolved, waiting user confirmation.- Linked entity IDs are stored as plain strings (no FK constraint). Validation: if
linkedEntityTypeset,linkedEntityIdmust also be set.
Notifications: add SUPPORT_REPLY and SUPPORT_STATUS_CHANGED to the NotificationType enum.
FE-SUPPORT-001 — User-facing help/support pages
- [x] Done
Files:
- Create:
apps/app/src/pages/support/SupportPage.tsx— list my tickets + create button - Create:
apps/app/src/pages/support/SupportTicketDetailPage.tsx— thread view + reply - Create:
apps/app/src/hooks/useSupportTickets.ts - Create:
apps/app/src/lib/queries/support.ts - Edit:
apps/app/src/App.tsx— add routes/help,/help/:id - Edit:
apps/app/src/components/AppShell.tsx— add Help nav item (question mark icon)
Description:
/help— "My Support Tickets" list with status chips, empty state with "Open a ticket" CTA.- Create ticket form: subject, body (textarea), category select, optional entity link (type + ID fields).
/help/:id— conversation thread: original message + replies as a chat-style timeline. Admin replies visually distinct (CasTyou logo + "Support Team" name). Reply input at bottom (disabled if CLOSED). Status chip with timestamp.
FE-SUPPORT-002 — Admin support panel
- [x] Done
Files:
- Create:
apps/app/src/pages/admin/AdminSupportPage.tsx— paginated ticket list with filters - Create:
apps/app/src/pages/admin/AdminSupportTicketPage.tsx— ticket thread + admin controls - Create:
apps/app/src/hooks/useAdminSupport.ts - Edit:
apps/app/src/components/AdminShell.tsx— enable & wire "Support" nav item - Edit:
apps/app/src/App.tsx— add routes
Description:
- Admin ticket list: filter by status/category/assigned. Columns: status chip, subject, user, category, last activity, reply count, assigned admin.
- Ticket detail: same thread view as user side plus admin-only controls: status dropdown, assign-to-me button, linked entity section (shows a pill linking to the related entity). Reply input always shown for admins.