mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 05:18:11 +00:00
feat: add support to freight backoffice
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import type {
|
||||
SendSupportMessageDto,
|
||||
SupportConversationDto,
|
||||
SupportConversationListResult,
|
||||
SupportMessageDto,
|
||||
} from "@edr/types";
|
||||
|
||||
import { api } from "@/auth/http";
|
||||
|
||||
export interface ListConversationsParams {
|
||||
search?: string;
|
||||
unreadOnly?: boolean;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backoffice (agent) support-chat REST calls. The backoffice axios `api`
|
||||
* response interceptor already unwraps the `{ success, data }` envelope, so
|
||||
* `.data` here is the payload itself.
|
||||
*/
|
||||
export const supportApi = {
|
||||
listConversations: async (
|
||||
params: ListConversationsParams = {},
|
||||
): Promise<SupportConversationListResult> => {
|
||||
const { data } = await api.get<SupportConversationListResult>(
|
||||
"/support/agent/conversations",
|
||||
{ params },
|
||||
);
|
||||
return data;
|
||||
},
|
||||
listMessages: async (id: string): Promise<SupportMessageDto[]> => {
|
||||
const { data } = await api.get<SupportMessageDto[]>(
|
||||
`/support/agent/conversations/${id}/messages`,
|
||||
);
|
||||
return data;
|
||||
},
|
||||
sendMessage: async (
|
||||
id: string,
|
||||
body: SendSupportMessageDto,
|
||||
): Promise<SupportMessageDto> => {
|
||||
const { data } = await api.post<SupportMessageDto>(
|
||||
`/support/agent/conversations/${id}/messages`,
|
||||
body,
|
||||
);
|
||||
return data;
|
||||
},
|
||||
/** Open the thread with a company, or hand back the existing one. */
|
||||
startConversation: async (
|
||||
companyId: string,
|
||||
): Promise<SupportConversationDto> => {
|
||||
const { data } = await api.post<SupportConversationDto>(
|
||||
"/support/agent/conversations",
|
||||
{ companyId },
|
||||
);
|
||||
return data;
|
||||
},
|
||||
markRead: async (id: string): Promise<{ unreadCount: number }> => {
|
||||
const { data } = await api.post<{ unreadCount: number }>(
|
||||
`/support/agent/conversations/${id}/read`,
|
||||
);
|
||||
return data;
|
||||
},
|
||||
unreadCount: async (): Promise<number> => {
|
||||
const { data } = await api.get<{ unreadCount: number }>(
|
||||
"/support/agent/unread-count",
|
||||
);
|
||||
return data.unreadCount;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { supportApi, type ListConversationsParams } from "./supportApi";
|
||||
|
||||
export const SUPPORT_KEY = ["support"] as const;
|
||||
export const SUPPORT_CONVERSATIONS_KEY = ["support", "conversations"] as const;
|
||||
export const SUPPORT_UNREAD_KEY = ["support", "unread"] as const;
|
||||
export const supportMessagesKey = (id: string) =>
|
||||
["support", "messages", id] as const;
|
||||
|
||||
/** Shared inbox: every thread, filterable by unread + company-name search. */
|
||||
export function useConversations(params: ListConversationsParams = {}) {
|
||||
return useQuery({
|
||||
queryKey: [...SUPPORT_CONVERSATIONS_KEY, params],
|
||||
queryFn: () => supportApi.listConversations({ limit: 100, ...params }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useMessages(conversationId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: supportMessagesKey(conversationId ?? ""),
|
||||
queryFn: () => supportApi.listMessages(conversationId as string),
|
||||
enabled: !!conversationId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useSupportUnreadCount(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: SUPPORT_UNREAD_KEY,
|
||||
queryFn: () => supportApi.unreadCount(),
|
||||
enabled,
|
||||
refetchInterval: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useSendMessage(conversationId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: string) =>
|
||||
supportApi.sendMessage(conversationId, { body }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: supportMessagesKey(conversationId) });
|
||||
qc.invalidateQueries({ queryKey: SUPPORT_CONVERSATIONS_KEY });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start chatting with a company. Idempotent server-side, so picking a company
|
||||
* that already has a thread just selects it.
|
||||
*/
|
||||
export function useStartConversation() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (companyId: string) => supportApi.startConversation(companyId),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: SUPPORT_CONVERSATIONS_KEY });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useMarkConversationRead() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => supportApi.markRead(id),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: SUPPORT_CONVERSATIONS_KEY });
|
||||
qc.invalidateQueries({ queryKey: SUPPORT_UNREAD_KEY });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
SUPPORT_CHAT_WS_EVENTS,
|
||||
SUPPORT_CHAT_WS_NAMESPACE,
|
||||
type SupportConversationDto,
|
||||
type SupportMessageEvent,
|
||||
} from "@edr/types";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
import { AUTH_TOKEN_COOKIE, getCookie } from "@/auth/cookies";
|
||||
import { API_BASE_URL } from "@/constants/apiConfig";
|
||||
|
||||
import {
|
||||
SUPPORT_CONVERSATIONS_KEY,
|
||||
SUPPORT_UNREAD_KEY,
|
||||
supportMessagesKey,
|
||||
} from "./useSupport";
|
||||
|
||||
// The socket namespace lives at the server root, not under the `/api` REST
|
||||
// prefix — strip a trailing `/api` if the base URL carries one.
|
||||
const SOCKET_ORIGIN = String(API_BASE_URL ?? "").replace(/\/api\/?$/, "");
|
||||
|
||||
/**
|
||||
* Subscribes the signed-in agent to live support-chat pushes for the whole
|
||||
* shared inbox. Any new message or conversation change refreshes the affected
|
||||
* thread, the inbox list, and the unread badge; `onMessage` fires for toasts.
|
||||
*/
|
||||
export function useSupportSocket(
|
||||
enabled: boolean,
|
||||
onMessage?: (event: SupportMessageEvent) => void,
|
||||
) {
|
||||
const qc = useQueryClient();
|
||||
const onMessageRef = useRef(onMessage);
|
||||
onMessageRef.current = onMessage;
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
const token = getCookie(AUTH_TOKEN_COOKIE);
|
||||
if (!token) return;
|
||||
|
||||
const socket = io(`${SOCKET_ORIGIN}/${SUPPORT_CHAT_WS_NAMESPACE}`, {
|
||||
auth: { token },
|
||||
transports: ["websocket"],
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
socket.on(SUPPORT_CHAT_WS_EVENTS.MESSAGE_NEW, (event: SupportMessageEvent) => {
|
||||
qc.invalidateQueries({
|
||||
queryKey: supportMessagesKey(event.message.conversationId),
|
||||
});
|
||||
qc.invalidateQueries({ queryKey: SUPPORT_CONVERSATIONS_KEY });
|
||||
qc.invalidateQueries({ queryKey: SUPPORT_UNREAD_KEY });
|
||||
onMessageRef.current?.(event);
|
||||
});
|
||||
|
||||
socket.on(
|
||||
SUPPORT_CHAT_WS_EVENTS.CONVERSATION_UPDATED,
|
||||
(_conversation: SupportConversationDto) => {
|
||||
qc.invalidateQueries({ queryKey: SUPPORT_CONVERSATIONS_KEY });
|
||||
qc.invalidateQueries({ queryKey: SUPPORT_UNREAD_KEY });
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
socket.off();
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [enabled, qc]);
|
||||
}
|
||||
Reference in New Issue
Block a user