调整目录
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,254 @@
|
||||
import { request } from "@/lib/api/client"
|
||||
|
||||
export type Paging = {
|
||||
page: number
|
||||
limit: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export type PageResult<T> = {
|
||||
results: T[]
|
||||
page: Paging
|
||||
}
|
||||
|
||||
export type CursorResult<T> = {
|
||||
results: T[]
|
||||
cursor: string
|
||||
hasMore: boolean
|
||||
}
|
||||
|
||||
export type AgentConversationTag = {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export type AgentConversationParticipant = {
|
||||
id: number
|
||||
participantType: string
|
||||
participantId: number
|
||||
externalParticipantId?: string
|
||||
joinedAt?: string
|
||||
leftAt?: string
|
||||
status: number
|
||||
}
|
||||
|
||||
export type AgentConversation = {
|
||||
id: number
|
||||
aiAgentId?: number
|
||||
customerId?: number
|
||||
externalSource: string
|
||||
externalId: string
|
||||
subject: string
|
||||
status: number
|
||||
serviceMode: number
|
||||
priority: number
|
||||
currentAssigneeId: number
|
||||
currentAssigneeName?: string
|
||||
lastMessageId: number
|
||||
lastMessageAt?: string
|
||||
lastActiveAt?: string
|
||||
lastMessageSummary?: string
|
||||
customerUnreadCount: number
|
||||
agentUnreadCount: number
|
||||
customerLastReadMessageId: number
|
||||
customerLastReadSeqNo: number
|
||||
customerLastReadAt?: string
|
||||
agentLastReadMessageId: number
|
||||
agentLastReadSeqNo: number
|
||||
agentLastReadAt?: string
|
||||
customerOnline: boolean
|
||||
closedAt?: string
|
||||
tags?: AgentConversationTag[]
|
||||
participants?: AgentConversationParticipant[]
|
||||
}
|
||||
|
||||
export type AgentConversationDetail = AgentConversation & {
|
||||
participants?: AgentConversationParticipant[]
|
||||
}
|
||||
|
||||
export type AgentMessage = {
|
||||
id: number
|
||||
conversationId: number
|
||||
clientMsgId?: string
|
||||
senderType: string
|
||||
senderId: number
|
||||
senderName?: string
|
||||
senderAvatar?: string
|
||||
messageType: string
|
||||
content: string
|
||||
payload?: string
|
||||
seqNo: number
|
||||
sendStatus: number
|
||||
sentAt?: string
|
||||
deliveredAt?: string
|
||||
readAt?: string
|
||||
customerRead: boolean
|
||||
customerReadAt?: string
|
||||
agentRead: boolean
|
||||
agentReadAt?: string
|
||||
recalledAt?: string
|
||||
quotedMessageId?: number
|
||||
}
|
||||
|
||||
export type AgentAsset = {
|
||||
id: number
|
||||
assetId: string
|
||||
provider: string
|
||||
storageKey: string
|
||||
filename: string
|
||||
fileSize: number
|
||||
mimeType: string
|
||||
status: number
|
||||
url: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
createUserId: number
|
||||
createUserName: string
|
||||
updateUserId: number
|
||||
updateUserName: string
|
||||
}
|
||||
|
||||
function toQueryString(query?: Record<string, string | number | undefined>) {
|
||||
if (!query) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const params = new URLSearchParams()
|
||||
Object.entries(query).forEach(([key, value]) => {
|
||||
if (value === undefined || value === "") {
|
||||
return
|
||||
}
|
||||
params.set(key, String(value))
|
||||
})
|
||||
const output = params.toString()
|
||||
return output ? `?${output}` : ""
|
||||
}
|
||||
|
||||
export function fetchAgentConversations(
|
||||
query?: Record<string, string | number | undefined>
|
||||
) {
|
||||
return request<PageResult<AgentConversation>>(
|
||||
`/api/dashboard/conversation/conversations${toQueryString(query)}`
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAgentConversationDetail(id: number) {
|
||||
return request<AgentConversationDetail>(`/api/dashboard/conversation/${id}`)
|
||||
}
|
||||
|
||||
export function fetchAgentMessages(
|
||||
query?: Record<string, string | number | undefined>
|
||||
) {
|
||||
return request<CursorResult<AgentMessage>>(
|
||||
`/api/dashboard/conversation/message_list${toQueryString(query)}`
|
||||
)
|
||||
}
|
||||
|
||||
export function sendAgentMessage(payload: {
|
||||
conversationId: number
|
||||
messageType: string
|
||||
content: string
|
||||
payload?: string
|
||||
clientMsgId?: string
|
||||
}) {
|
||||
return request<AgentMessage>("/api/dashboard/conversation/send_message", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function recallAgentMessage(messageId: number) {
|
||||
return request<AgentMessage>("/api/dashboard/conversation/recall_message", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ messageId }),
|
||||
})
|
||||
}
|
||||
|
||||
export function markAgentMessageRead(conversationId: number, messageId = 0) {
|
||||
return request<void>("/api/dashboard/conversation/read", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ conversationId, messageId }),
|
||||
})
|
||||
}
|
||||
|
||||
export function uploadAgentConversationImage(conversationId: number, file: File) {
|
||||
const formData = new FormData()
|
||||
formData.set("conversationId", String(conversationId))
|
||||
formData.set("file", file)
|
||||
return request<AgentAsset>("/api/dashboard/conversation/upload_image", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
}
|
||||
|
||||
export function uploadAgentConversationAttachment(conversationId: number, file: File) {
|
||||
const formData = new FormData()
|
||||
formData.set("conversationId", String(conversationId))
|
||||
formData.set("file", file)
|
||||
return request<AgentAsset>("/api/dashboard/conversation/upload_attachment", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
}
|
||||
|
||||
export function closeAgentConversation(
|
||||
conversationId: number,
|
||||
closeReason: string
|
||||
) {
|
||||
return request<void>("/api/dashboard/conversation/close", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ conversationId, closeReason }),
|
||||
})
|
||||
}
|
||||
|
||||
export function assignAgentConversation(
|
||||
conversationId: number,
|
||||
assigneeId: number,
|
||||
reason: string
|
||||
) {
|
||||
return request<void>("/api/dashboard/conversation/assign", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ conversationId, assigneeId, reason }),
|
||||
})
|
||||
}
|
||||
|
||||
export function transferAgentConversation(
|
||||
conversationId: number,
|
||||
toUserId: number,
|
||||
reason: string
|
||||
) {
|
||||
return request<void>("/api/dashboard/conversation/transfer", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ conversationId, toUserId, reason }),
|
||||
})
|
||||
}
|
||||
|
||||
export function linkConversationToCustomer(payload: {
|
||||
conversationId: number
|
||||
customerId: number
|
||||
}) {
|
||||
return request<void>("/api/dashboard/conversation/link_customer", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function addConversationTag(payload: {
|
||||
conversationId: number
|
||||
tagId: number
|
||||
}) {
|
||||
return request<void>("/api/dashboard/conversation/add_tag", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function removeConversationTag(payload: {
|
||||
conversationId: number
|
||||
tagId: number
|
||||
}) {
|
||||
return request<void>("/api/dashboard/conversation/remove_tag", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { clearSession, writeSession, type AuthSession } from "@/lib/auth"
|
||||
import { request } from "@/lib/api/client"
|
||||
|
||||
export type LoginRequest = {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export async function loginWithPassword(payload: LoginRequest) {
|
||||
const data = await request<AuthSession>("/api/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
skipAuth: true,
|
||||
})
|
||||
writeSession(data)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function exchangeWxWorkTicket(ticket: string) {
|
||||
const data = await request<AuthSession>("/api/auth/wxwork_exchange", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ ticket }),
|
||||
skipAuth: true,
|
||||
})
|
||||
writeSession(data)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function fetchProfile() {
|
||||
return request<AuthSession>("/api/auth/profile")
|
||||
}
|
||||
|
||||
export async function logout(refreshToken?: string) {
|
||||
try {
|
||||
await request("/api/auth/logout", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
refreshToken,
|
||||
}),
|
||||
})
|
||||
} finally {
|
||||
clearSession()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { clearSession, readSession, writeSession, type AuthSession } from "@/lib/auth"
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL?.trim() || ""
|
||||
|
||||
type JsonResult<T> = {
|
||||
errorCode: number
|
||||
message: string
|
||||
data: T
|
||||
success: boolean
|
||||
}
|
||||
|
||||
type RequestOptions = RequestInit & {
|
||||
skipAuth?: boolean
|
||||
retryOnAuthError?: boolean
|
||||
}
|
||||
|
||||
async function parseResult<T>(response: Response) {
|
||||
const payload = (await response.json()) as JsonResult<T>
|
||||
if (!response.ok || !payload.success) {
|
||||
const error = new Error(payload.message || "请求失败")
|
||||
;(error as Error & { errorCode?: number }).errorCode = payload.errorCode
|
||||
throw error
|
||||
}
|
||||
return payload.data
|
||||
}
|
||||
|
||||
async function refreshAccessToken() {
|
||||
const session = readSession()
|
||||
if (!session?.refreshToken) {
|
||||
clearSession()
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await request<AuthSession>(
|
||||
"/api/auth/refresh_token",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({ refreshToken: session.refreshToken }),
|
||||
skipAuth: true,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
false
|
||||
)
|
||||
const merged = {
|
||||
...data,
|
||||
refreshToken: data.refreshToken || session.refreshToken,
|
||||
}
|
||||
writeSession(merged)
|
||||
return merged
|
||||
}
|
||||
|
||||
export async function request<T>(
|
||||
path: string,
|
||||
options: RequestOptions = {},
|
||||
retryOnAuthError = true
|
||||
): Promise<T> {
|
||||
const { headers, skipAuth, ...rest } = options
|
||||
delete (rest as RequestOptions).retryOnAuthError
|
||||
const session = readSession()
|
||||
const authHeaders = new Headers(headers)
|
||||
|
||||
if (!skipAuth && session?.accessToken) {
|
||||
authHeaders.set("Authorization", `Bearer ${session.accessToken}`)
|
||||
}
|
||||
if (
|
||||
!authHeaders.has("Content-Type") &&
|
||||
rest.body &&
|
||||
!(typeof FormData !== "undefined" && rest.body instanceof FormData)
|
||||
) {
|
||||
authHeaders.set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
...rest,
|
||||
headers: authHeaders,
|
||||
cache: "no-store",
|
||||
})
|
||||
|
||||
try {
|
||||
return await parseResult<T>(response)
|
||||
} catch (error) {
|
||||
const errorCode = (error as Error & { errorCode?: number }).errorCode
|
||||
if (
|
||||
!skipAuth &&
|
||||
retryOnAuthError &&
|
||||
(errorCode === 3000 || errorCode === 3002) &&
|
||||
session?.refreshToken
|
||||
) {
|
||||
const refreshed = await refreshAccessToken()
|
||||
if (!refreshed) {
|
||||
throw error
|
||||
}
|
||||
return request<T>(path, options, false)
|
||||
}
|
||||
|
||||
if (errorCode === 3000 || errorCode === 3002) {
|
||||
clearSession()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { request } from "@/lib/api/client"
|
||||
import type { PageResult } from "@/lib/api/admin"
|
||||
|
||||
export type AdminCompany = {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
customerCount: number
|
||||
status: number
|
||||
remark: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type CreateAdminCompanyPayload = {
|
||||
name: string
|
||||
code: string
|
||||
remark: string
|
||||
}
|
||||
|
||||
export type UpdateAdminCompanyPayload = CreateAdminCompanyPayload & {
|
||||
id: number
|
||||
}
|
||||
|
||||
function toQueryString(query?: Record<string, string | number | undefined>) {
|
||||
if (!query) {
|
||||
return ""
|
||||
}
|
||||
const params = new URLSearchParams()
|
||||
Object.entries(query).forEach(([key, value]) => {
|
||||
if (value === undefined || value === "") {
|
||||
return
|
||||
}
|
||||
params.set(key, String(value))
|
||||
})
|
||||
const output = params.toString()
|
||||
return output ? `?${output}` : ""
|
||||
}
|
||||
|
||||
export function fetchCompanies(query?: Record<string, string | number | undefined>) {
|
||||
return request<PageResult<AdminCompany>>(
|
||||
`/api/dashboard/company/list${toQueryString(query)}`
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchCompany(id: number) {
|
||||
return request<AdminCompany>(`/api/dashboard/company/${id}`)
|
||||
}
|
||||
|
||||
export function createCompany(payload: CreateAdminCompanyPayload) {
|
||||
return request<AdminCompany>("/api/dashboard/company/create", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function updateCompany(payload: UpdateAdminCompanyPayload) {
|
||||
return request<void>("/api/dashboard/company/update", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function updateCompanyStatus(id: number, status: number) {
|
||||
return request<void>("/api/dashboard/company/update_status", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ id, status }),
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteCompany(id: number) {
|
||||
return request<void>("/api/dashboard/company/delete", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ id }),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { request } from "@/lib/api/client"
|
||||
import type { ContactType } from "@/lib/generated/enums"
|
||||
|
||||
export type AdminCustomerContact = {
|
||||
id: number
|
||||
customerId: number
|
||||
contactType: ContactType | string
|
||||
contactValue: string
|
||||
isPrimary: boolean
|
||||
isVerified: boolean
|
||||
verifiedAt?: string
|
||||
source: string
|
||||
status: number
|
||||
remark: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type CreateCustomerContactPayload = {
|
||||
customerId: number
|
||||
contactType: ContactType | string
|
||||
contactValue: string
|
||||
isPrimary: boolean
|
||||
isVerified: boolean
|
||||
source: string
|
||||
status: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
export type UpdateCustomerContactPayload = Omit<
|
||||
CreateCustomerContactPayload,
|
||||
"customerId"
|
||||
> & {
|
||||
id: number
|
||||
}
|
||||
|
||||
function toQueryString(query?: Record<string, string | number | undefined>) {
|
||||
if (!query) {
|
||||
return ""
|
||||
}
|
||||
const params = new URLSearchParams()
|
||||
Object.entries(query).forEach(([key, value]) => {
|
||||
if (value === undefined || value === "") {
|
||||
return
|
||||
}
|
||||
params.set(key, String(value))
|
||||
})
|
||||
const output = params.toString()
|
||||
return output ? `?${output}` : ""
|
||||
}
|
||||
|
||||
export function fetchCustomerContacts(customerId: number) {
|
||||
return request<AdminCustomerContact[]>(
|
||||
`/api/dashboard/customer-contact/list${toQueryString({ customerId })}`
|
||||
)
|
||||
}
|
||||
|
||||
export function createCustomerContact(payload: CreateCustomerContactPayload) {
|
||||
return request<AdminCustomerContact>("/api/dashboard/customer-contact/create", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function updateCustomerContact(payload: UpdateCustomerContactPayload) {
|
||||
return request<void>("/api/dashboard/customer-contact/update", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteCustomerContact(id: number) {
|
||||
return request<void>("/api/dashboard/customer-contact/delete", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ id }),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { request } from "@/lib/api/client"
|
||||
import type { PageResult } from "@/lib/api/admin"
|
||||
import type { ContactType } from "@/lib/generated/enums"
|
||||
import { AdminCompany } from "./company"
|
||||
|
||||
export type AdminCustomer = {
|
||||
id: number
|
||||
name: string
|
||||
gender: number
|
||||
companyId: number
|
||||
company?: AdminCompany
|
||||
lastActiveAt?: string
|
||||
primaryMobile: string
|
||||
primaryEmail: string
|
||||
status: number
|
||||
remark: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type CreateAdminCustomerPayload = {
|
||||
name: string
|
||||
gender: number
|
||||
companyId: number
|
||||
primaryMobile: string
|
||||
primaryEmail: string
|
||||
remark: string
|
||||
}
|
||||
|
||||
export type UpdateAdminCustomerPayload = CreateAdminCustomerPayload & {
|
||||
id: number
|
||||
}
|
||||
|
||||
/** 与 POST /customer/save/profile 请求体一致 */
|
||||
export type SaveCustomerProfileContactLine = {
|
||||
id?: number
|
||||
contactType: ContactType | string
|
||||
contactValue: string
|
||||
remark: string
|
||||
isPrimary: boolean
|
||||
}
|
||||
|
||||
export type SaveCustomerProfilePayload = {
|
||||
id?: number
|
||||
name: string
|
||||
gender: number
|
||||
companyId: number
|
||||
remark: string
|
||||
contacts: SaveCustomerProfileContactLine[]
|
||||
}
|
||||
|
||||
/** 与 POST /customer/list JSON Body 一致 */
|
||||
export type CustomerListRequest = {
|
||||
page: number
|
||||
limit: number
|
||||
status?: number
|
||||
gender?: number
|
||||
companyId?: number
|
||||
/** 模糊匹配:客户名、主手机、主邮箱、联系方式、公司名称 */
|
||||
keyword?: string
|
||||
}
|
||||
|
||||
export function fetchCustomers(body: CustomerListRequest) {
|
||||
return request<PageResult<AdminCustomer>>("/api/dashboard/customer/list", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchCustomer(id: number) {
|
||||
return request<AdminCustomer | null>(`/api/dashboard/customer/${id}`)
|
||||
}
|
||||
|
||||
export function createCustomer(payload: CreateAdminCustomerPayload) {
|
||||
return request<AdminCustomer>("/api/dashboard/customer/create", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
/** 单请求 + 单事务保存客户主信息与联系方式全量 */
|
||||
export function saveCustomerProfile(payload: SaveCustomerProfilePayload) {
|
||||
return request<AdminCustomer>("/api/dashboard/customer/save_profile", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function updateCustomer(payload: UpdateAdminCustomerPayload) {
|
||||
return request<void>("/api/dashboard/customer/update", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function updateCustomerStatus(id: number, status: number) {
|
||||
return request<void>("/api/dashboard/customer/update_status", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ id, status }),
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteCustomer(id: number) {
|
||||
return request<void>("/api/dashboard/customer/delete", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ id }),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { request } from "@/lib/api/client"
|
||||
|
||||
export type DashboardRange = "today" | "7d" | "30d"
|
||||
|
||||
export type DashboardStatusDistributionItem = {
|
||||
status: number
|
||||
label: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export type DashboardTrendItem = {
|
||||
date: string
|
||||
newCount: number
|
||||
closedCount: number
|
||||
}
|
||||
|
||||
export type DashboardTeamLoad = {
|
||||
teamId: number
|
||||
teamName: string
|
||||
totalAgents: number
|
||||
onlineAgents: number
|
||||
busyAgents: number
|
||||
offlineAgents: number
|
||||
waitingConversations: number
|
||||
processingConversations: number
|
||||
maxConcurrentCapacity: number
|
||||
loadRate: number
|
||||
hasScheduleNow: boolean
|
||||
}
|
||||
|
||||
export type DashboardAlert = {
|
||||
id: string
|
||||
level: "info" | "warning" | "error"
|
||||
title: string
|
||||
description: string
|
||||
count: number
|
||||
link: string
|
||||
}
|
||||
|
||||
export type DashboardQuickLink = {
|
||||
title: string
|
||||
description: string
|
||||
link: string
|
||||
}
|
||||
|
||||
export type DashboardOverview = {
|
||||
range: DashboardRange
|
||||
generatedAt: string
|
||||
summary: {
|
||||
todayNewConversations: number
|
||||
processingConversations: number
|
||||
pendingDispatchConversations: number
|
||||
onlineAgents: number
|
||||
aiServiceRate: number
|
||||
}
|
||||
conversationStats: {
|
||||
statusDistribution: DashboardStatusDistributionItem[]
|
||||
trend: DashboardTrendItem[]
|
||||
}
|
||||
agentStats: {
|
||||
onlineAgents: number
|
||||
busyAgents: number
|
||||
offlineAgents: number
|
||||
teamLoads: DashboardTeamLoad[]
|
||||
}
|
||||
aiStats: {
|
||||
enabledAiAgents: number
|
||||
enabledChannels: number
|
||||
todayKnowledgeRetrieves: number
|
||||
todayKnowledgeRetrieveFailCount: number
|
||||
todayKnowledgeRetrieveFailRate: number
|
||||
todaySkillRunFailCount: number
|
||||
todayAiHandoffCount: number
|
||||
}
|
||||
alerts: DashboardAlert[]
|
||||
quickLinks: DashboardQuickLink[]
|
||||
}
|
||||
|
||||
export function fetchDashboardOverview(range: DashboardRange) {
|
||||
return request<DashboardOverview>(`/api/dashboard/dashboard/overview?range=${range}`)
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
import { request } from "@/lib/api/client"
|
||||
import { generateUUID } from "@/lib/utils"
|
||||
|
||||
export type Paging = {
|
||||
page: number
|
||||
limit: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export type PageResult<T> = {
|
||||
results: T[]
|
||||
page: Paging
|
||||
}
|
||||
|
||||
export type ImConversationTag = {
|
||||
id: number
|
||||
name: string
|
||||
color: string
|
||||
}
|
||||
|
||||
export type ImConversationParticipant = {
|
||||
id: number
|
||||
participantType: string
|
||||
participantId: number
|
||||
externalParticipantId?: string
|
||||
joinedAt?: string
|
||||
leftAt?: string
|
||||
status: number
|
||||
}
|
||||
|
||||
export type ImConversation = {
|
||||
id: number
|
||||
externalSource: string
|
||||
externalId: string
|
||||
subject: string
|
||||
status: number
|
||||
serviceMode: number
|
||||
priority: number
|
||||
currentAssigneeId: number
|
||||
currentAssigneeName?: string
|
||||
lastMessageId: number
|
||||
lastMessageAt?: string
|
||||
lastActiveAt?: string
|
||||
lastMessageSummary?: string
|
||||
customerUnreadCount: number
|
||||
agentUnreadCount: number
|
||||
customerLastReadMessageId: number
|
||||
customerLastReadSeqNo: number
|
||||
customerLastReadAt?: string
|
||||
agentLastReadMessageId: number
|
||||
agentLastReadSeqNo: number
|
||||
agentLastReadAt?: string
|
||||
closedAt?: string
|
||||
tags?: ImConversationTag[]
|
||||
participants?: ImConversationParticipant[]
|
||||
}
|
||||
|
||||
export type ImConversationDetail = ImConversation
|
||||
|
||||
export type ImMessage = {
|
||||
id: number
|
||||
conversationId: number
|
||||
clientMsgId?: string
|
||||
senderType: string
|
||||
senderId: number
|
||||
senderName?: string
|
||||
senderAvatar?: string
|
||||
messageType: string
|
||||
content: string
|
||||
payload?: string
|
||||
seqNo: number
|
||||
sendStatus: number
|
||||
sentAt?: string
|
||||
deliveredAt?: string
|
||||
readAt?: string
|
||||
customerRead: boolean
|
||||
customerReadAt?: string
|
||||
agentRead: boolean
|
||||
agentReadAt?: string
|
||||
recalledAt?: string
|
||||
quotedMessageId?: number
|
||||
}
|
||||
|
||||
export type ImAsset = {
|
||||
id: number
|
||||
assetId: string
|
||||
provider: string
|
||||
storageKey: string
|
||||
filename: string
|
||||
fileSize: number
|
||||
mimeType: string
|
||||
status: number
|
||||
url: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
createUserId: number
|
||||
createUserName: string
|
||||
updateUserId: number
|
||||
updateUserName: string
|
||||
}
|
||||
|
||||
const VISITOR_STORAGE_KEY = "cs_agent_im_visitor_id"
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL?.trim() || ""
|
||||
const OPEN_IM_CHANNEL_ID =
|
||||
process.env.NEXT_PUBLIC_OPEN_IM_CHANNEL_ID?.trim() || ""
|
||||
const OPEN_IM_EXTERNAL_SOURCE =
|
||||
process.env.NEXT_PUBLIC_OPEN_IM_EXTERNAL_SOURCE?.trim() || "web_chat"
|
||||
|
||||
function buildVisitorId() {
|
||||
return `visitor_${generateUUID()}`
|
||||
}
|
||||
|
||||
export function getImVisitorId() {
|
||||
if (typeof window === "undefined") {
|
||||
return "visitor_ssr"
|
||||
}
|
||||
const existing = window.localStorage.getItem(VISITOR_STORAGE_KEY)?.trim()
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
const visitorId = buildVisitorId()
|
||||
window.localStorage.setItem(VISITOR_STORAGE_KEY, visitorId)
|
||||
return visitorId
|
||||
}
|
||||
|
||||
function createImHeaders() {
|
||||
return {
|
||||
"X-External-Source": OPEN_IM_EXTERNAL_SOURCE,
|
||||
"X-External-Id": getImVisitorId(),
|
||||
"X-Channel-Id": OPEN_IM_CHANNEL_ID,
|
||||
}
|
||||
}
|
||||
|
||||
function toQueryString(query?: Record<string, string | number | undefined>) {
|
||||
if (!query) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const params = new URLSearchParams()
|
||||
Object.entries(query).forEach(([key, value]) => {
|
||||
if (value === undefined || value === "") {
|
||||
return
|
||||
}
|
||||
params.set(key, String(value))
|
||||
})
|
||||
const output = params.toString()
|
||||
return output ? `?${output}` : ""
|
||||
}
|
||||
|
||||
export function fetchImConversationDetail(id: number) {
|
||||
return request<ImConversationDetail>(`/api/open/im/conversation/${id}`, {
|
||||
headers: createImHeaders(),
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchImMessages(
|
||||
query?: Record<string, string | number | undefined>
|
||||
) {
|
||||
return request<PageResult<ImMessage>>(
|
||||
`/api/open/im/message/list${toQueryString(query)}`,
|
||||
{ headers: createImHeaders() }
|
||||
)
|
||||
}
|
||||
|
||||
/** 外部身份仅通过 createImHeaders()(X-External-*)传递,无 JSON body */
|
||||
export function createOrMatchImConversation() {
|
||||
return request<ImConversation>("/api/open/im/conversation/create_or_match", {
|
||||
method: "POST",
|
||||
headers: createImHeaders(),
|
||||
})
|
||||
}
|
||||
|
||||
export function sendImMessage(payload: {
|
||||
conversationId: number
|
||||
messageType: string
|
||||
content: string
|
||||
payload?: string
|
||||
clientMsgId?: string
|
||||
}) {
|
||||
return request<ImMessage>("/api/open/im/message/send", {
|
||||
method: "POST",
|
||||
headers: createImHeaders(),
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function markImMessageRead(conversationId: number, messageId = 0) {
|
||||
return request<void>("/api/open/im/message/read", {
|
||||
method: "POST",
|
||||
headers: createImHeaders(),
|
||||
body: JSON.stringify({ conversationId, messageId }),
|
||||
})
|
||||
}
|
||||
|
||||
export function uploadImImage(conversationId: number, file: File) {
|
||||
const formData = new FormData()
|
||||
formData.set("conversationId", String(conversationId))
|
||||
formData.set("file", file)
|
||||
return request<ImAsset>("/api/open/im/message/upload_image", {
|
||||
method: "POST",
|
||||
headers: createImHeaders(),
|
||||
body: formData,
|
||||
})
|
||||
}
|
||||
|
||||
export function uploadImAttachment(conversationId: number, file: File) {
|
||||
const formData = new FormData()
|
||||
formData.set("conversationId", String(conversationId))
|
||||
formData.set("file", file)
|
||||
return request<ImAsset>("/api/open/im/message/upload_attachment", {
|
||||
method: "POST",
|
||||
headers: createImHeaders(),
|
||||
body: formData,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { request } from "@/lib/api/client"
|
||||
|
||||
export type Paging = {
|
||||
page: number
|
||||
limit: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export type PageResult<T> = {
|
||||
results: T[]
|
||||
page: Paging
|
||||
}
|
||||
|
||||
export type TicketResolutionCode = {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
sortNo: number
|
||||
status: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
export type TicketPriorityConfig = {
|
||||
id: number
|
||||
name: string
|
||||
sortNo: number
|
||||
firstResponseMinutes: number
|
||||
resolutionMinutes: number
|
||||
status: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
export type CreateTicketResolutionCodePayload = {
|
||||
name: string
|
||||
code: string
|
||||
sortNo: number
|
||||
status: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
export type UpdateTicketResolutionCodePayload = CreateTicketResolutionCodePayload & {
|
||||
id: number
|
||||
}
|
||||
|
||||
export type CreateTicketPriorityConfigPayload = {
|
||||
name: string
|
||||
firstResponseMinutes: number
|
||||
resolutionMinutes: number
|
||||
status: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
export type UpdateTicketPriorityConfigPayload = CreateTicketPriorityConfigPayload & {
|
||||
id: number
|
||||
}
|
||||
|
||||
function toQueryString(query?: Record<string, string | number | undefined>) {
|
||||
if (!query) {
|
||||
return ""
|
||||
}
|
||||
const params = new URLSearchParams()
|
||||
Object.entries(query).forEach(([key, value]) => {
|
||||
if (value === undefined || value === "") {
|
||||
return
|
||||
}
|
||||
params.set(key, String(value))
|
||||
})
|
||||
const output = params.toString()
|
||||
return output ? `?${output}` : ""
|
||||
}
|
||||
|
||||
export function fetchTicketResolutionCodes(query?: Record<string, string | number | undefined>) {
|
||||
return request<PageResult<TicketResolutionCode>>(
|
||||
`/api/dashboard/ticket-resolution-code/list${toQueryString(query)}`
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchTicketResolutionCodesAll() {
|
||||
return request<TicketResolutionCode[]>("/api/dashboard/ticket-resolution-code/list_all")
|
||||
}
|
||||
|
||||
export function createTicketResolutionCode(payload: CreateTicketResolutionCodePayload) {
|
||||
return request<TicketResolutionCode>("/api/dashboard/ticket-resolution-code/create", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function updateTicketResolutionCode(payload: UpdateTicketResolutionCodePayload) {
|
||||
return request<void>("/api/dashboard/ticket-resolution-code/update", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteTicketResolutionCode(id: number) {
|
||||
return request<void>("/api/dashboard/ticket-resolution-code/delete", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ id }),
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchTicketPriorityConfigs(query?: Record<string, string | number | undefined>) {
|
||||
return request<TicketPriorityConfig[]>(
|
||||
`/api/dashboard/ticket-priority-config/list${toQueryString(query)}`
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchTicketPriorityConfigsAll() {
|
||||
return request<TicketPriorityConfig[]>("/api/dashboard/ticket-priority-config/list_all")
|
||||
}
|
||||
|
||||
export function createTicketPriorityConfig(payload: CreateTicketPriorityConfigPayload) {
|
||||
return request<TicketPriorityConfig>("/api/dashboard/ticket-priority-config/create", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function updateTicketPriorityConfig(payload: UpdateTicketPriorityConfigPayload) {
|
||||
return request<void>("/api/dashboard/ticket-priority-config/update", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function updateTicketPriorityConfigSort(ids: number[]) {
|
||||
return request<void>("/api/dashboard/ticket-priority-config/update_sort", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(ids),
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteTicketPriorityConfig(id: number) {
|
||||
return request<void>("/api/dashboard/ticket-priority-config/delete", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ id }),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
import { request } from "@/lib/api/client"
|
||||
import type { Tag } from "@/lib/api/admin"
|
||||
|
||||
export type Paging = {
|
||||
page: number
|
||||
limit: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export type PageResult<T> = {
|
||||
results: T[]
|
||||
page: Paging
|
||||
}
|
||||
|
||||
export type TicketCustomer = {
|
||||
id: number
|
||||
name: string
|
||||
companyId?: number
|
||||
company?: {
|
||||
id: number
|
||||
name: string
|
||||
code?: string
|
||||
remark?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
primaryMobile?: string
|
||||
primaryEmail?: string
|
||||
}
|
||||
|
||||
export type TicketSLA = {
|
||||
slaType: string
|
||||
targetMinutes: number
|
||||
status: string
|
||||
startedAt?: string
|
||||
pausedAt?: string
|
||||
stoppedAt?: string
|
||||
breachedAt?: string
|
||||
elapsedMin: number
|
||||
}
|
||||
|
||||
export type TicketComment = {
|
||||
id: number
|
||||
ticketId: number
|
||||
commentType: string
|
||||
authorType: string
|
||||
authorId: number
|
||||
authorName?: string
|
||||
contentType: string
|
||||
content: string
|
||||
payload?: string
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export type TicketEvent = {
|
||||
id: number
|
||||
ticketId: number
|
||||
eventType: string
|
||||
operatorType: string
|
||||
operatorId: number
|
||||
operatorName?: string
|
||||
oldValue?: string
|
||||
newValue?: string
|
||||
content?: string
|
||||
payload?: string
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export type TicketItem = {
|
||||
id: number
|
||||
ticketNo: string
|
||||
title: string
|
||||
description: string
|
||||
source: string
|
||||
channel: string
|
||||
customerId: number
|
||||
conversationId: number
|
||||
tags?: Tag[]
|
||||
type: string
|
||||
priority: number
|
||||
priorityName?: string
|
||||
severity: number
|
||||
status: string
|
||||
currentTeamId: number
|
||||
currentTeamName?: string
|
||||
currentAssigneeId: number
|
||||
currentAssigneeName?: string
|
||||
watchedByMe: boolean
|
||||
pendingReason?: string
|
||||
closeReason?: string
|
||||
resolutionCode?: string
|
||||
resolutionCodeName?: string
|
||||
resolutionSummary?: string
|
||||
firstResponseAt?: string
|
||||
resolvedAt?: string
|
||||
closedAt?: string
|
||||
dueAt?: string
|
||||
nextReplyDeadlineAt?: string
|
||||
resolveDeadlineAt?: string
|
||||
reopenedCount: number
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
customer?: TicketCustomer
|
||||
sla?: TicketSLA[]
|
||||
}
|
||||
|
||||
export type TicketDetail = {
|
||||
ticket: TicketItem
|
||||
watchers?: Array<{
|
||||
id: number
|
||||
userId: number
|
||||
userName?: string
|
||||
}>
|
||||
collaborators?: TicketCollaborator[]
|
||||
comments?: TicketComment[]
|
||||
events?: TicketEvent[]
|
||||
relatedTickets?: TicketRelation[]
|
||||
}
|
||||
|
||||
export type TicketCollaborator = {
|
||||
id: number
|
||||
userId: number
|
||||
userName?: string
|
||||
teamName?: string
|
||||
}
|
||||
|
||||
export type TicketRelation = {
|
||||
id: number
|
||||
ticketId: number
|
||||
relatedTicketId: number
|
||||
relationType: string
|
||||
relatedTicketNo?: string
|
||||
relatedTicketTitle?: string
|
||||
relatedTicketStatus?: string
|
||||
currentTeamName?: string
|
||||
currentAssigneeName?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export type TicketSummary = {
|
||||
all: number
|
||||
mine: number
|
||||
watching: number
|
||||
collaboration: number
|
||||
participating: number
|
||||
mentioned: number
|
||||
unassigned: number
|
||||
pendingCustomer: number
|
||||
pendingInternal: number
|
||||
overdue: number
|
||||
}
|
||||
|
||||
export type TicketRiskReason = {
|
||||
code: string
|
||||
title: string
|
||||
description: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export type TicketRiskOverview = {
|
||||
overdue: number
|
||||
highRisk: number
|
||||
unassigned: number
|
||||
pendingInternal: number
|
||||
pendingCustomer: number
|
||||
riskWindowMins: number
|
||||
reasons?: TicketRiskReason[]
|
||||
}
|
||||
|
||||
export type TicketSavedView = {
|
||||
id: number
|
||||
name: string
|
||||
filters?: Record<string, unknown>
|
||||
sortNo: number
|
||||
}
|
||||
|
||||
export type TicketListQuery = {
|
||||
page?: number
|
||||
limit?: number
|
||||
keyword?: string
|
||||
status?: string
|
||||
priority?: number
|
||||
severity?: number
|
||||
tagId?: number
|
||||
currentTeamId?: number
|
||||
currentAssigneeId?: number
|
||||
customerId?: number
|
||||
conversationId?: number
|
||||
source?: string
|
||||
watching?: number
|
||||
collaboration?: number
|
||||
collaborating?: number
|
||||
mentioned?: number
|
||||
mine?: number
|
||||
unassigned?: number
|
||||
overdue?: number
|
||||
}
|
||||
|
||||
export type TicketRiskListQuery = {
|
||||
riskType: "overdue" | "high_risk" | "unassigned" | "pending_internal" | "pending_customer"
|
||||
currentTeamId?: number
|
||||
riskWindowMins?: number
|
||||
page?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export type CreateTicketPayload = {
|
||||
title: string
|
||||
description: string
|
||||
source?: string
|
||||
channel?: string
|
||||
customerId?: number
|
||||
conversationId?: number
|
||||
tagIds?: number[]
|
||||
type?: string
|
||||
priority: number
|
||||
severity: number
|
||||
currentTeamId?: number
|
||||
currentAssigneeId?: number
|
||||
dueAt?: string
|
||||
customFields?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type CreateTicketFromConversationPayload = {
|
||||
conversationId: number
|
||||
title: string
|
||||
description: string
|
||||
tagIds?: number[]
|
||||
priority: number
|
||||
severity: number
|
||||
currentTeamId?: number
|
||||
currentAssigneeId?: number
|
||||
syncToConversation: boolean
|
||||
customFields?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type UpdateTicketPayload = {
|
||||
ticketId: number
|
||||
title: string
|
||||
description: string
|
||||
tagIds?: number[]
|
||||
type?: string
|
||||
priority: number
|
||||
severity: number
|
||||
currentTeamId?: number
|
||||
currentAssigneeId?: number
|
||||
dueAt?: string
|
||||
customFields?: Record<string, unknown>
|
||||
}
|
||||
|
||||
function toQueryString(query?: Record<string, string | number | undefined>) {
|
||||
if (!query) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const params = new URLSearchParams()
|
||||
Object.entries(query).forEach(([key, value]) => {
|
||||
if (value === undefined || value === "") {
|
||||
return
|
||||
}
|
||||
params.set(key, String(value))
|
||||
})
|
||||
const output = params.toString()
|
||||
return output ? `?${output}` : ""
|
||||
}
|
||||
|
||||
export function fetchTickets(query?: TicketListQuery) {
|
||||
return request<PageResult<TicketItem>>(`/api/dashboard/ticket/list${toQueryString(query)}`)
|
||||
}
|
||||
|
||||
export function fetchTicketDetail(id: number) {
|
||||
return request<TicketDetail>(`/api/dashboard/ticket/${id}`)
|
||||
}
|
||||
|
||||
export function fetchTicketSummary() {
|
||||
return request<TicketSummary>("/api/dashboard/ticket/summary")
|
||||
}
|
||||
|
||||
export function fetchTicketViews() {
|
||||
return request<TicketSavedView[]>("/api/dashboard/ticket/view_list")
|
||||
}
|
||||
|
||||
export function saveTicketView(payload: {
|
||||
id?: number
|
||||
name: string
|
||||
filters?: Record<string, unknown>
|
||||
}) {
|
||||
return request<TicketSavedView>("/api/dashboard/ticket/save_view", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteTicketView(id: number) {
|
||||
return request<void>("/api/dashboard/ticket/delete_view", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ id }),
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchTicketRiskOverview(query?: {
|
||||
currentTeamId?: number
|
||||
riskWindowMins?: number
|
||||
}) {
|
||||
return request<TicketRiskOverview>(`/api/dashboard/ticket/risk_overview${toQueryString(query)}`)
|
||||
}
|
||||
|
||||
export function fetchTicketRiskList(query: TicketRiskListQuery) {
|
||||
return request<PageResult<TicketItem>>(`/api/dashboard/ticket/risk_list${toQueryString(query)}`)
|
||||
}
|
||||
|
||||
export function createTicket(payload: CreateTicketPayload) {
|
||||
return request<TicketItem>("/api/dashboard/ticket/create", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function createTicketFromConversation(payload: CreateTicketFromConversationPayload) {
|
||||
return request<TicketItem>("/api/dashboard/ticket/create_from_conversation", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function updateTicket(payload: UpdateTicketPayload) {
|
||||
return request<void>("/api/dashboard/ticket/update", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function linkTicketToCustomer(payload: {
|
||||
ticketId: number
|
||||
customerId: number
|
||||
}) {
|
||||
return request<void>("/api/dashboard/ticket/link_customer", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function assignTicket(payload: {
|
||||
ticketId: number
|
||||
toUserId: number
|
||||
toTeamId?: number
|
||||
reason?: string
|
||||
}) {
|
||||
return request<void>("/api/dashboard/ticket/assign", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function batchAssignTickets(payload: {
|
||||
ticketIds: number[]
|
||||
toUserId: number
|
||||
toTeamId?: number
|
||||
reason?: string
|
||||
}) {
|
||||
return request<void>("/api/dashboard/ticket/batch_assign", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function changeTicketStatus(payload: {
|
||||
ticketId: number
|
||||
status: string
|
||||
pendingReason?: string
|
||||
closeReason?: string
|
||||
resolutionCode?: string
|
||||
resolutionSummary?: string
|
||||
reason?: string
|
||||
}) {
|
||||
return request<void>("/api/dashboard/ticket/change_status", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function batchChangeTicketStatus(payload: {
|
||||
ticketIds: number[]
|
||||
status: string
|
||||
pendingReason?: string
|
||||
closeReason?: string
|
||||
resolutionCode?: string
|
||||
resolutionSummary?: string
|
||||
reason?: string
|
||||
}) {
|
||||
return request<void>("/api/dashboard/ticket/batch_change_status", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function replyTicket(payload: {
|
||||
ticketId: number
|
||||
contentType?: string
|
||||
content: string
|
||||
payload?: string
|
||||
}) {
|
||||
return request<TicketComment>("/api/dashboard/ticket/reply", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function addTicketInternalNote(payload: {
|
||||
ticketId: number
|
||||
contentType?: string
|
||||
content: string
|
||||
payload?: string
|
||||
}) {
|
||||
return request<TicketComment>("/api/dashboard/ticket/internal_note", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function closeTicket(payload: { ticketId: number; closeReason: string }) {
|
||||
return request<void>("/api/dashboard/ticket/close", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function reopenTicket(payload: { ticketId: number; reason: string }) {
|
||||
return request<void>("/api/dashboard/ticket/reopen", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function watchTicket(ticketId: number) {
|
||||
return request<void>("/api/dashboard/ticket/watch", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ ticketId }),
|
||||
})
|
||||
}
|
||||
|
||||
export function unwatchTicket(ticketId: number) {
|
||||
return request<void>("/api/dashboard/ticket/unwatch", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ ticketId }),
|
||||
})
|
||||
}
|
||||
|
||||
export function batchWatchTickets(payload: { ticketIds: number[]; watched: boolean }) {
|
||||
return request<void>("/api/dashboard/ticket/batch_watch", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function addTicketRelation(payload: {
|
||||
ticketId: number
|
||||
relatedTicketId?: number
|
||||
relatedTicketNo?: string
|
||||
relationType: string
|
||||
}) {
|
||||
return request<void>("/api/dashboard/ticket/add_relation", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteTicketRelation(payload: { ticketId: number; relationId: number }) {
|
||||
return request<void>("/api/dashboard/ticket/delete_relation", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function addTicketCollaborator(payload: { ticketId: number; userId: number }) {
|
||||
return request<void>("/api/dashboard/ticket/add_collaborator", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteTicketCollaborator(payload: { ticketId: number; collaboratorId: number }) {
|
||||
return request<void>("/api/dashboard/ticket/delete_collaborator", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL?.trim() ||
|
||||
(process.env.NODE_ENV === "development" ? "http://127.0.0.1:8083" : "")
|
||||
|
||||
export function createWebSocketBaseUrl() {
|
||||
if (API_BASE_URL) {
|
||||
return API_BASE_URL.replace(/^http/, "ws").replace(/\/$/, "")
|
||||
}
|
||||
|
||||
if (typeof window === "undefined") {
|
||||
return ""
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
|
||||
return `${protocol}//${window.location.host}`
|
||||
}
|
||||
Reference in New Issue
Block a user