Skip to content

Commit 30b9d63

Browse files
authored
Feat/ticket-service (#57)
This pull request introduces a comprehensive "Ticket" system for handling user support requests (tickets), including database schema, backend API, and email notifications. The main changes add the Ticket data model, endpoints for creating and resolving tickets, and email templates/services for notifying users when tickets are created or solved. **Ticket System Implementation** * Added a new `Ticket` model to the database schema, including fields for user, staff, messages, solved status, and timestamps. Established relations to the `User` model for both students and staff. [[1]](diffhunk://#diff-5b443964f4f3a611682db8f7e02177b0a8c632b2039e2bd5e4dd7347815c565cR12-R29) [[2]](diffhunk://#diff-5b443964f4f3a611682db8f7e02177b0a8c632b2039e2bd5e4dd7347815c565cR308-R317) * Created and updated Prisma migrations to support the new `Ticket` table, its relations, and necessary schema changes. [[1]](diffhunk://#diff-c19c6c37a421c55a57c7f113332a28f14d8d6051ee92d02c03ae3b2614624d8aR1-R18) [[2]](diffhunk://#diff-2146e664709f6ef084dcd9b0259e3ec46733b69d4fd5e4aab6760dc6484567c0R1-R8) [[3]](diffhunk://#diff-39d77bf86ddf4845b19c682d91106b6d3f7703ec756ca75cddcf0635a595e843R1-R21) [[4]](diffhunk://#diff-d2454b553e8710eedd4f8cc2ccf5c0715466458cbc7e2a09458f44172ff829edR1-R2) **Backend API and Service** * Implemented the `TicketModule` with controller and service: - Endpoints for students to create tickets and for staff to view and resolve tickets. - Service methods for creating tickets, listing all tickets (for staff), and marking tickets as solved, including error handling and logging. [[1]](diffhunk://#diff-d8c2efd626b717858e33b91e89d064059ef830ff37be8dee052d2a745ff251d4R1-R11) [[2]](diffhunk://#diff-db2322f400d28ba6d10b5370284e1512e60e70e37c16f4d707234929510a58f5R1-R27) [[3]](diffhunk://#diff-c6a3c95d0a93bab25b14ecd16213fb98618c68e0c40c47f5e9b67c2e532ed120R1-R25) [[4]](diffhunk://#diff-889902f754836187f4f7a34a025383b5e8e8e8a6b80099f61e9179377bd1af44R1-R106) * Integrated the new module into the main application module. [[1]](diffhunk://#diff-089f4f2474b64391c42b6e66aed33977e132058d92108f0a63234a7862e1f8b8R41) [[2]](diffhunk://#diff-089f4f2474b64391c42b6e66aed33977e132058d92108f0a63234a7862e1f8b8R85) **Email Notification System** * Added email service methods and React email templates to notify users when tickets are created and when they are solved, including custom messages and ticket details. [[1]](diffhunk://#diff-f911d8c5543f958eb70c386615d7b21a2ee41444b87363d1ec166805cef6dbd5R8-R9) [[2]](diffhunk://#diff-f911d8c5543f958eb70c386615d7b21a2ee41444b87363d1ec166805cef6dbd5R64-R81) [[3]](diffhunk://#diff-b16fdf995522e27ffa1ca5230d935c1373a55283e1c40e4c6b0b7d1c06f70e63R1-R90) [[4]](diffhunk://#diff-99f222042331728798673ca39c0fc0927351631f2ab8b82d3194d5b12e74a4a8R1-R96) These changes together provide a full-featured support ticket workflow, allowing users to submit issues and receive updates, and enabling staff to manage and resolve tickets efficiently.
2 parents 8bd08b6 + 7054e31 commit 30b9d63

13 files changed

Lines changed: 447 additions & 0 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-- CreateTable
2+
CREATE TABLE "Ticket" (
3+
"std_application_id" TEXT NOT NULL,
4+
"ticket_id" TEXT NOT NULL,
5+
"ticket_message" TEXT,
6+
"ticket_solved" BOOLEAN NOT NULL DEFAULT false,
7+
"stf_user_id" TEXT NOT NULL,
8+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
9+
"updated_at" TIMESTAMP(3) NOT NULL,
10+
11+
CONSTRAINT "Ticket_pkey" PRIMARY KEY ("ticket_id")
12+
);
13+
14+
-- AddForeignKey
15+
ALTER TABLE "Ticket" ADD CONSTRAINT "Ticket_std_application_id_fkey" FOREIGN KEY ("std_application_id") REFERENCES "StudentApplication"("std_application_id") ON DELETE RESTRICT ON UPDATE CASCADE;
16+
17+
-- AddForeignKey
18+
ALTER TABLE "Ticket" ADD CONSTRAINT "Ticket_stf_user_id_fkey" FOREIGN KEY ("stf_user_id") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- DropForeignKey
2+
ALTER TABLE "Ticket" DROP CONSTRAINT "Ticket_stf_user_id_fkey";
3+
4+
-- AlterTable
5+
ALTER TABLE "Ticket" ALTER COLUMN "stf_user_id" DROP NOT NULL;
6+
7+
-- AddForeignKey
8+
ALTER TABLE "Ticket" ADD CONSTRAINT "Ticket_stf_user_id_fkey" FOREIGN KEY ("stf_user_id") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
Warnings:
3+
4+
- You are about to drop the column `std_application_id` on the `Ticket` table. All the data in the column will be lost.
5+
- You are about to drop the column `ticket_message` on the `Ticket` table. All the data in the column will be lost.
6+
- Added the required column `std_user_id` to the `Ticket` table without a default value. This is not possible if the table is not empty.
7+
- Added the required column `ticket_system_message` to the `Ticket` table without a default value. This is not possible if the table is not empty.
8+
9+
*/
10+
-- DropForeignKey
11+
ALTER TABLE "Ticket" DROP CONSTRAINT "Ticket_std_application_id_fkey";
12+
13+
-- AlterTable
14+
ALTER TABLE "Ticket" DROP COLUMN "std_application_id",
15+
DROP COLUMN "ticket_message",
16+
ADD COLUMN "std_user_id" TEXT NOT NULL,
17+
ADD COLUMN "ticket_system_message" TEXT NOT NULL,
18+
ADD COLUMN "ticket_user_message" TEXT;
19+
20+
-- AddForeignKey
21+
ALTER TABLE "Ticket" ADD CONSTRAINT "Ticket_std_user_id_fkey" FOREIGN KEY ("std_user_id") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "Ticket" ADD COLUMN "stf_solve_message" TEXT;

prisma/schema.prisma

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,24 @@ datasource db {
99
provider = "postgresql"
1010
}
1111

12+
model Ticket {
13+
14+
std_user_id String
15+
std_user User @relation("ticket_student", fields: [std_user_id], references: [id])
16+
17+
ticket_id String @id @default(uuid())
18+
ticket_system_message String @db.Text
19+
ticket_user_message String? @db.Text
20+
ticket_solved Boolean @default(false)
21+
22+
stf_user_id String?
23+
stf_user User? @relation("ticket_staff", fields: [stf_user_id], references: [id])
24+
stf_solve_message String? @db.Text
25+
26+
created_at DateTime @default(now())
27+
updated_at DateTime @updatedAt
28+
}
29+
1230
enum AppInfoGender {
1331
male
1432
female
@@ -287,13 +305,16 @@ model User {
287305
displayUsername String?
288306
289307
std_application StudentApplication[]
308+
std_ticket Ticket[] @relation("ticket_student")
290309
291310
stf_regis_question_score ApplicationRegisQuestionScore[]
292311
stf_academic_question_score ApplicationAcademicQuestionScore[]
293312
stf_academic_chaos_question_score ApplicationAcademicChaosQuestionScore[]
294313
295314
stf_info_check ApplicationInfoCheck[]
296315
316+
stf_ticket_solve Ticket[] @relation("ticket_staff")
317+
297318
@@unique([email])
298319
@@map("user")
299320
@@unique([username])

src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { StaffStatusModule } from "./modules/staff-status/staff-status.module";
3838
import { StaffStatusUpdaterModule } from "./modules/staff-status-updater/staff-status-updater.module";
3939
import { StatusUpdaterModule } from "./modules/status-updater/status-updater.module";
4040
import { StudentApplicationModule } from "./modules/student-application/student-application.module";
41+
import { TicketModule } from "./modules/ticket/ticket.module";
4142
import { UtilModule } from "./modules/util/util.module";
4243

4344
@Module({
@@ -81,6 +82,7 @@ import { UtilModule } from "./modules/util/util.module";
8182
StaffAcademicQuestionModule,
8283
StaffAcademicChaosQuestionModule,
8384
StaffAcademicChaosGradingModule,
85+
TicketModule,
8486
],
8587

8688
controllers: [AppController],

src/core/email/email.service.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { config } from "src/config/app.config";
55
import AnnouncementEmail from "./templates/AnnouncementEmail";
66
import ContentIssueEmail from "./templates/ContentIssueEmail";
77
import RegistrationConfirmEmail from "./templates/RegistrationConfirmEmail";
8+
import TicketCreatedEmail from "./templates/TicketCreatedEmail";
9+
import TicketSolvedEmail from "./templates/TicketSolvedEmail";
810
import TrackingEmail from "./templates/TrackingEmail";
911

1012
@Injectable()
@@ -59,6 +61,24 @@ export class EmailService {
5961
}
6062
}
6163

64+
async sendTicketCreated(email: string, name: string, ticketId: string, ticketSubject: string, ticketMessage?: string) {
65+
try {
66+
const html = await render(TicketCreatedEmail({ name, ticketId, ticketSubject, ticketMessage }));
67+
return await this.sendMail(email, `ได้รับ Ticket #${ticketId} ของคุณแล้ว`, html);
68+
} catch (error) {
69+
this.handleError("ticket created", email, error);
70+
}
71+
}
72+
73+
async sendTicketSolved(email: string, name: string, ticketId: string, ticketMessage: string, resolution?: string) {
74+
try {
75+
const html = await render(TicketSolvedEmail({ name, ticketId, ticketMessage, resolution }));
76+
return await this.sendMail(email, `Ticket #${ticketId} ได้รับการแก้ไขแล้ว`, html);
77+
} catch (error) {
78+
this.handleError("ticket solved", email, error);
79+
}
80+
}
81+
6282
private async sendMail(to: string, subject: string, html: string) {
6383
const info = await this.transporter.sendMail({
6484
from: config.email.nodemailer.from,
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { Body, Container, Font, Head, Heading, Html, Preview, Section, Tailwind, Text } from "@react-email/components";
2+
import * as React from "react";
3+
import { Footer } from "./layout/Footer";
4+
import { Header } from "./layout/Header";
5+
6+
interface TicketCreatedEmailProps {
7+
name: string;
8+
ticketId: string;
9+
ticketSubject: string;
10+
ticketMessage?: string;
11+
}
12+
13+
export default function TicketCreatedEmail({ name, ticketId, ticketSubject, ticketMessage }: TicketCreatedEmailProps) {
14+
return (
15+
<Html>
16+
<Tailwind
17+
config={{
18+
theme: {
19+
extend: {
20+
colors: {
21+
zootopia: {
22+
navy: "#1d3071",
23+
blue: "#004aad",
24+
orange: "#e98d55",
25+
yellow: "#f2d575",
26+
brown: "#504039",
27+
},
28+
},
29+
},
30+
},
31+
}}
32+
>
33+
<Head>
34+
<Font fontFamily="Helvetica" fallbackFontFamily="Arial" fontWeight={400} fontStyle="normal" />
35+
</Head>
36+
<Preview>ได้รับ Ticket ของคุณแล้ว! - #{ticketId}</Preview>
37+
38+
<Body className="bg-white font-sans">
39+
<Container className="mx-auto w-full max-w-[800px] my-10 bg-white rounded-2xl overflow-hidden shadow-lg">
40+
<Header />
41+
42+
<Section className="bg-white px-6 py-8 text-center">
43+
<Heading className="text-sm font-semibold text-[#1d3071] m-0 mb-2 tracking-widest uppercase" style={{ textShadow: "0 2px 8px rgba(0,0,0,0.1)" }}>
44+
Ticket
45+
</Heading>
46+
<Text className="text-2xl font-bold m-0 text-[#004aad]">ได้รับ Ticket ของน้องเรียบร้อยแล้ว</Text>
47+
</Section>
48+
49+
<Section className="px-6 pb-10 pt-4">
50+
<Section className="bg-[#004aad]/20 border-l-4 border-[#1d3071] rounded-lg px-6 py-5 mb-8">
51+
<Text className="text-base leading-relaxed text-[#004aad] m-0">
52+
สวัสดีครับน้อง <strong className="text-[#504039]">{name}</strong>
53+
</Text>
54+
</Section>
55+
56+
<Section className="bg-[#e3f2fd] border-2 border-[#2196f3] rounded-xl px-6 py-6 mb-8">
57+
<Text className="text-sm font-bold text-[#1976d2] m-0 mb-3">🎫 รายละเอียด Ticket:</Text>
58+
<Text className="text-sm leading-relaxed text-[#504039] m-0 mb-2">
59+
<strong>หมายเลข Ticket:</strong> #{ticketId}
60+
</Text>
61+
{ticketMessage && (
62+
<Text className="text-sm leading-relaxed text-[#504039] m-0">
63+
<strong>รายละเอียด:</strong> {ticketMessage}
64+
</Text>
65+
)}
66+
</Section>
67+
68+
<Text className="text-base leading-relaxed text-[#504039] mb-6">
69+
ทีมงาน <strong>ComCamp 37</strong> ได้รับ Ticket ของน้องเรียบร้อยแล้ว และจะดำเนินการตรวจสอบและตอกลับโดยเร็วที่สุด
70+
</Text>
71+
72+
<Section className="bg-[#f0f4ff] border-l-4 border-[#92a6d2] rounded-lg px-6 py-5 mb-8">
73+
<Text className="text-sm leading-relaxed text-[#504039] m-0 mb-2">- น้องจะได้รับอีเมลแจ้งเตือนเมื่อ Ticket ได้รับการแก้ไข</Text>
74+
<Text className="text-sm leading-relaxed text-[#504039] m-0">- สามารถติดต่อทีมงานผ่านช่องทางอื่นหากมีความเร่งด่วน</Text>
75+
</Section>
76+
77+
<Text className="text-base leading-relaxed text-[#504039] mb-6 text-center">ขอบคุณที่แจ้งปัญหามาให้ทีมงานทราบครับ 🙏</Text>
78+
79+
<Section className="text-center">
80+
<Text className="text-2xl m-0">🦊🐰🦁</Text>
81+
</Section>
82+
</Section>
83+
84+
<Footer />
85+
</Container>
86+
</Body>
87+
</Tailwind>
88+
</Html>
89+
);
90+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { Body, Container, Font, Head, Heading, Html, Preview, Section, Tailwind, Text } from "@react-email/components";
2+
import * as React from "react";
3+
import { Footer } from "./layout/Footer";
4+
import { Header } from "./layout/Header";
5+
6+
interface TicketSolvedEmailProps {
7+
name: string;
8+
ticketId: string;
9+
ticketMessage: string;
10+
resolution?: string;
11+
}
12+
13+
export default function TicketSolvedEmail({ name, ticketId, ticketMessage, resolution }: TicketSolvedEmailProps) {
14+
return (
15+
<Html>
16+
<Tailwind
17+
config={{
18+
theme: {
19+
extend: {
20+
colors: {
21+
zootopia: {
22+
navy: "#1d3071",
23+
blue: "#004aad",
24+
orange: "#e98d55",
25+
yellow: "#f2d575",
26+
brown: "#504039",
27+
},
28+
},
29+
},
30+
},
31+
}}
32+
>
33+
<Head>
34+
<Font fontFamily="Helvetica" fallbackFontFamily="Arial" fontWeight={400} fontStyle="normal" />
35+
</Head>
36+
<Preview>Ticket #{ticketId} ได้รับการแก้ไขแล้ว!</Preview>
37+
38+
<Body className="bg-white font-sans">
39+
<Container className="mx-auto w-full max-w-[800px] my-10 bg-white rounded-2xl overflow-hidden shadow-lg">
40+
<Header />
41+
42+
<Section className="bg-white px-6 py-8 text-center">
43+
<Heading className="text-sm font-semibold text-[#1d3071] m-0 mb-2 tracking-widest uppercase" style={{ textShadow: "0 2px 8px rgba(0,0,0,0.1)" }}>
44+
Ticket
45+
</Heading>
46+
<Text className="text-2xl font-bold m-0 text-[#4caf50]">ได้รับการแก้ไขแล้ว!</Text>
47+
</Section>
48+
49+
<Section className="px-6 pb-10 pt-4">
50+
<Section className="bg-[#004aad]/20 border-l-4 border-[#1d3071] rounded-lg px-6 py-5 mb-8">
51+
<Text className="text-base leading-relaxed text-[#004aad] m-0">
52+
สวัสดีครับน้อง <strong className="text-[#504039]">{name}</strong>
53+
</Text>
54+
</Section>
55+
56+
<Section className="bg-[#e8f5e9] border-2 border-[#4caf50] rounded-xl px-6 py-6 mb-8 text-center">
57+
<Text className="text-lg font-bold text-[#2e7d32] m-0">Ticket ของน้องได้รับการแก้ไขเรียบร้อยแล้ว!</Text>
58+
</Section>
59+
60+
<Section className="bg-[#f5f5f5] border-2 border-[#e0e0e0] rounded-xl px-6 py-6 mb-8">
61+
<Text className="text-sm font-bold text-[#616161] m-0 mb-3">🎫 รายละเอียด Ticket:</Text>
62+
<Text className="text-sm leading-relaxed text-[#504039] m-0 mb-2">
63+
<strong>หมายเลข Ticket:</strong> #{ticketId}
64+
</Text>
65+
{ticketMessage && (
66+
<Text className="text-sm leading-relaxed text-[#504039] m-0">
67+
<strong>รายละเอียด:</strong> {ticketMessage}
68+
</Text>
69+
)}
70+
</Section>
71+
72+
{resolution && (
73+
<Section className="bg-[#f5f5f5] border-2 border-[#e0e0e0] rounded-xl px-6 py-6 mb-8">
74+
<Text className="text-sm font-bold text-[#616161] m-0 mb-3">ข้อความจากทีมงาน:</Text>
75+
<Text className="text-sm leading-relaxed text-[#504039] m-0 mb-2">{resolution}</Text>
76+
</Section>
77+
)}
78+
79+
<Text className="text-base leading-relaxed text-[#504039] mb-6">
80+
หาก Ticket นี้ยังไม่ได้รับการแก้ไขตามที่คาดหวัง หรือมีคำถามเพิ่มเติม กรุณาติดต่อทีมงาน <strong>ComCamp 37</strong> ได้ทุกช่องทาง
81+
</Text>
82+
83+
<Text className="text-base leading-relaxed text-[#504039] mb-6 text-center">ขอบคุณที่แจ้งปัญหามาให้ทีมงานทราบครับ 🙏</Text>
84+
85+
<Section className="text-center">
86+
<Text className="text-2xl m-0">🦊🐰🦁</Text>
87+
</Section>
88+
</Section>
89+
90+
<Footer />
91+
</Container>
92+
</Body>
93+
</Tailwind>
94+
</Html>
95+
);
96+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { IsBoolean, IsNotEmpty, IsOptional, IsString, IsUUID } from "class-validator";
2+
3+
export class CreateTicketDto {
4+
@IsString()
5+
@IsNotEmpty()
6+
system_message: string;
7+
8+
@IsString()
9+
@IsOptional()
10+
user_message?: string;
11+
}
12+
13+
export class StaffSolveTicketDto {
14+
@IsUUID()
15+
@IsString()
16+
ticket_id: string;
17+
18+
@IsBoolean()
19+
@IsOptional()
20+
ticket_solved?: boolean;
21+
22+
@IsString()
23+
@IsOptional()
24+
solve_message?: string;
25+
}

0 commit comments

Comments
 (0)