Skip to content

Commit 833a4fc

Browse files
committed
Show ticket title instead of id
1 parent c1781ab commit 833a4fc

File tree

5 files changed

+148
-151
lines changed

5 files changed

+148
-151
lines changed

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCaseDetails.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,11 @@ export function SupportCaseDetails({ ticket, team }: SupportCaseDetailsProps) {
101101
<div className="flex flex-col grow">
102102
<div className="border border-border bg-card p-6 rounded-lg">
103103
<div className="mb-6">
104-
<h2 className="text-2xl font-semibold text-foreground">
105-
Ticket #{ticket.id}
104+
<h2 className="text-2xl font-semibold text-foreground mb-1">
105+
{ticket.title}
106106
</h2>
107+
<p className="text-sm text-muted-foreground mb-2">#{ticket.id}</p>
108+
107109
<div className="flex items-center gap-4 mt-2">
108110
<Badge
109111
className={

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCasesClient.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export default function SupportCasesClient({
134134
<div className="flex-1 min-w-0">
135135
<div className="flex items-center gap-2">
136136
<span className="text-foreground font-medium break-all">
137-
Ticket #{ticket.id}
137+
{ticket.title}
138138
</span>
139139
</div>
140140
</div>
Lines changed: 115 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
2-
import type { SupportMessage, SupportTicket } from "../types/tickets";
2+
import type {
3+
SupportMessage,
4+
SupportTicket,
5+
SupportTicketListItem,
6+
} from "../types/tickets";
37

48
export async function getSupportTicketsByTeam(params: {
59
teamSlug: string;
610
authToken: string;
7-
}): Promise<SupportTicket[]> {
11+
}): Promise<SupportTicketListItem[]> {
812
const encodedTeamSlug = encodeURIComponent(params.teamSlug);
913
const apiUrl = `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations/list`;
1014

@@ -31,7 +35,7 @@ export async function getSupportTicketsByTeam(params: {
3135
throw new Error(`Failed to fetch support tickets: ${errorText}`);
3236
}
3337

34-
const data: { data?: SupportTicket[] } = await response.json();
38+
const data: { data?: SupportTicketListItem[] } = await response.json();
3539
const conversations = data.data || [];
3640
return conversations;
3741
}
@@ -50,138 +54,124 @@ type RawSupportMessage = {
5054
// Add any other fields you use from the API
5155
};
5256

53-
export async function getSupportTicket(
54-
ticketId: string,
55-
teamSlug: string,
56-
authToken: string,
57-
): Promise<{ data: SupportTicket } | { error: string } | null> {
58-
if (!ticketId || !teamSlug) {
59-
return { error: "Ticket ID and team slug are required" };
60-
}
61-
57+
export async function getSupportTicket(params: {
58+
ticketId: string;
59+
teamSlug: string;
60+
authToken: string;
61+
}): Promise<SupportTicket> {
6262
// URL encode the team slug to handle special characters like #
63-
const encodedTeamSlug = encodeURIComponent(teamSlug);
64-
const encodedTicketId = encodeURIComponent(ticketId);
63+
const encodedTeamSlug = encodeURIComponent(params.teamSlug);
64+
const encodedTicketId = encodeURIComponent(params.ticketId);
6565

6666
const messagesPayload = {
6767
limit: 100,
6868
descending: false,
6969
};
7070

71-
try {
72-
// Fetch conversation details and messages in parallel
73-
const [conversationResponse, messagesResponse] = await Promise.all([
74-
fetch(
75-
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations/${encodedTicketId}`,
76-
{
77-
cache: "no-store",
78-
headers: {
79-
Accept: "application/json",
80-
"Accept-Encoding": "identity",
81-
Authorization: `Bearer ${authToken}`,
82-
"Content-Type": "application/json",
83-
},
84-
method: "GET",
71+
// Fetch conversation details and messages in parallel
72+
const [conversationResponse, messagesResponse] = await Promise.all([
73+
fetch(
74+
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations/${encodedTicketId}`,
75+
{
76+
cache: "no-store",
77+
headers: {
78+
Accept: "application/json",
79+
"Accept-Encoding": "identity",
80+
Authorization: `Bearer ${params.authToken}`,
81+
"Content-Type": "application/json",
8582
},
86-
),
87-
fetch(
88-
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations/${encodedTicketId}/messages/list`,
89-
{
90-
body: JSON.stringify(messagesPayload),
91-
cache: "no-store",
92-
headers: {
93-
Accept: "application/json",
94-
"Accept-Encoding": "identity",
95-
Authorization: `Bearer ${authToken}`,
96-
"Content-Type": "application/json",
97-
},
98-
method: "POST",
83+
method: "GET",
84+
},
85+
),
86+
fetch(
87+
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations/${encodedTicketId}/messages/list`,
88+
{
89+
body: JSON.stringify(messagesPayload),
90+
cache: "no-store",
91+
headers: {
92+
Accept: "application/json",
93+
"Accept-Encoding": "identity",
94+
Authorization: `Bearer ${params.authToken}`,
95+
"Content-Type": "application/json",
9996
},
100-
),
101-
]);
102-
103-
if (!conversationResponse.ok) {
104-
if (conversationResponse.status === 404) {
105-
return null; // Ticket not found
106-
}
107-
const errorText = await conversationResponse.text();
108-
return {
109-
error: `API Server error: ${conversationResponse.status} - ${errorText}`,
110-
};
111-
}
112-
113-
const conversation: SupportTicket = await conversationResponse.json();
114-
115-
// Fetch and map messages if the messages request was successful
116-
if (messagesResponse.ok) {
117-
const messagesData: { data?: unknown[] } = await messagesResponse.json();
118-
const rawMessages = messagesData.data || [];
119-
// Transform the raw messages to match our interface
120-
const messages: SupportMessage[] = (rawMessages as RawSupportMessage[])
121-
.filter((msg) => {
122-
// Filter out messages without content - check both text and text fields
123-
const hasContent = msg.text && msg.text.length > 0;
124-
const hasText = msg.text && msg.text.trim().length > 0;
125-
// Filter out private notes - they should not be shown to customers
126-
const isNotPrivateNote = !msg.isPrivateNote;
127-
return (hasContent || hasText) && isNotPrivateNote;
128-
})
129-
.map((msg) => {
130-
// Use text if available and is a non-empty array, otherwise fall back to text
131-
let content = "";
132-
if (typeof msg.text === "string" && msg.text.trim().length > 0) {
133-
content = msg.text;
134-
}
135-
136-
// Clean up 'Email:' line to show only the plain email if it contains a mailto link
137-
if (content) {
138-
content = content
139-
.split("\n")
140-
.map((line) => {
141-
if (line.trim().toLowerCase().startsWith("email:")) {
142-
// Extract email from <mailto:...|...>
143-
const match = line.match(/<mailto:([^|>]+)\|[^>]+>/);
144-
if (match) {
145-
return `Email: ${match[1]}`;
146-
}
97+
method: "POST",
98+
},
99+
),
100+
]);
101+
102+
if (!conversationResponse.ok) {
103+
throw new Error(
104+
`Failed to fetch support ticket: $${await conversationResponse.text()}`,
105+
);
106+
}
107+
108+
const conversation: SupportTicket = await conversationResponse.json();
109+
110+
// Fetch and map messages if the messages request was successful
111+
if (messagesResponse.ok) {
112+
const messagesData: { data?: unknown[] } = await messagesResponse.json();
113+
const rawMessages = messagesData.data || [];
114+
// Transform the raw messages to match our interface
115+
const messages: SupportMessage[] = (rawMessages as RawSupportMessage[])
116+
.filter((msg) => {
117+
// Filter out messages without content - check both text and text fields
118+
const hasContent = msg.text && msg.text.length > 0;
119+
const hasText = msg.text && msg.text.trim().length > 0;
120+
// Filter out private notes - they should not be shown to customers
121+
const isNotPrivateNote = !msg.isPrivateNote;
122+
return (hasContent || hasText) && isNotPrivateNote;
123+
})
124+
.map((msg) => {
125+
// Use text if available and is a non-empty array, otherwise fall back to text
126+
let content = "";
127+
if (typeof msg.text === "string" && msg.text.trim().length > 0) {
128+
content = msg.text;
129+
}
130+
131+
// Clean up 'Email:' line to show only the plain email if it contains a mailto link
132+
if (content) {
133+
content = content
134+
.split("\n")
135+
.map((line) => {
136+
if (line.trim().toLowerCase().startsWith("email:")) {
137+
// Extract email from <mailto:...|...>
138+
const match = line.match(/<mailto:([^|>]+)\|[^>]+>/);
139+
if (match) {
140+
return `Email: ${match[1]}`;
147141
}
148-
return line;
149-
})
150-
.join("\n");
151-
}
152-
153-
// Map the author information from sentByUser if available
154-
const author = msg.sentByUser
155-
? {
156-
name: msg.sentByUser.name,
157-
email: msg.sentByUser.email,
158-
type: (msg.sentByUser.isExternal ? "customer" : "user") as
159-
| "user"
160-
| "customer",
161142
}
162-
: undefined;
163-
164-
return {
165-
id: msg.id,
166-
content: content,
167-
createdAt: msg.timestamp || msg.createdAt || "",
168-
timestamp: msg.timestamp || msg.createdAt || "",
169-
author: author,
170-
};
171-
});
172-
173-
conversation.messages = messages;
174-
} else {
175-
// Don't throw error, just leave messages empty
176-
const errorText = await messagesResponse.text();
177-
console.error("Failed to fetch messages:", errorText);
178-
conversation.messages = [];
179-
}
180-
181-
return { data: conversation };
182-
} catch (error) {
183-
return {
184-
error: `Failed to fetch support ticket: ${error instanceof Error ? error.message : "Unknown error"}`,
185-
};
143+
return line;
144+
})
145+
.join("\n");
146+
}
147+
148+
// Map the author information from sentByUser if available
149+
const author = msg.sentByUser
150+
? {
151+
name: msg.sentByUser.name,
152+
email: msg.sentByUser.email,
153+
type: (msg.sentByUser.isExternal ? "customer" : "user") as
154+
| "user"
155+
| "customer",
156+
}
157+
: undefined;
158+
159+
return {
160+
id: msg.id,
161+
content: content,
162+
createdAt: msg.timestamp || msg.createdAt || "",
163+
timestamp: msg.timestamp || msg.createdAt || "",
164+
author: author,
165+
};
166+
});
167+
168+
conversation.messages = messages;
169+
} else {
170+
// Don't throw error, just leave messages empty
171+
const errorText = await messagesResponse.text();
172+
console.error("Failed to fetch messages:", errorText);
173+
conversation.messages = [];
186174
}
175+
176+
return conversation;
187177
}
Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { notFound } from "next/navigation";
1+
import { notFound, redirect } from "next/navigation";
22
import { getAuthToken } from "@/api/auth-token";
33
import { getTeamBySlug } from "@/api/team";
4+
import { tryCatch } from "../../../../../../../../../@/utils/try-catch";
45
import { SupportCaseDetails } from "../../_components/SupportCaseDetails";
56
import { getSupportTicket } from "../../apis/tickets";
6-
import type { SupportTicket } from "../../types/tickets";
77

88
export default async function TicketPage(props: {
99
params: Promise<{
@@ -22,28 +22,17 @@ export default async function TicketPage(props: {
2222
notFound();
2323
}
2424

25-
// Fetch ticket details
26-
let ticket: SupportTicket | null = null;
25+
const ticketResult = await tryCatch(
26+
getSupportTicket({
27+
ticketId: params.id,
28+
teamSlug: params.team_slug,
29+
authToken: token,
30+
}),
31+
);
2732

28-
try {
29-
const result = await getSupportTicket(params.id, params.team_slug, token);
30-
if (result === null) {
31-
notFound();
32-
}
33-
if ("data" in result) {
34-
ticket = result.data;
35-
} else {
36-
console.error("Failed to load ticket:", result.error);
37-
notFound();
38-
}
39-
} catch (error) {
40-
console.error("Failed to load ticket:", error);
41-
notFound();
42-
}
43-
44-
if (!ticket) {
45-
notFound();
33+
if (ticketResult.error) {
34+
redirect(`/team/${params.team_slug}/~/support`);
4635
}
4736

48-
return <SupportCaseDetails team={team} ticket={ticket} />;
37+
return <SupportCaseDetails team={team} ticket={ticketResult.data} />;
4938
}

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/types/tickets.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,21 @@ export type SupportTicket = {
1515
status: "needs_response" | "in_progress" | "on_hold" | "closed" | "resolved";
1616
createdAt: string;
1717
updatedAt: string;
18+
closedAt: string | null;
19+
respondedAt: string | null;
1820
messages?: SupportMessage[];
21+
title: string;
22+
tags: Array<{ id: string; name: string }>;
23+
};
24+
25+
export type SupportTicketListItem = {
26+
id: string;
27+
status: "needs_response" | "in_progress" | "on_hold" | "closed" | "resolved";
28+
// timestamps
29+
createdAt: string;
30+
updatedAt: string;
31+
closedAt: string | null;
32+
respondedAt: string | null;
33+
title: string;
34+
tags: Array<{ id: string; name: string }>;
1935
};

0 commit comments

Comments
 (0)