snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
|
||||
import type { Box } from '@rocket.chat/fuselage';
|
||||
import { useEndpoint, type TranslationKey } from '@rocket.chat/ui-contexts';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type * as chartjs from 'chart.js';
|
||||
import type { TFunction } from 'i18next';
|
||||
import type { ComponentPropsWithoutRef, MutableRefObject } from 'react';
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Chart from './Chart';
|
||||
import { useChartContext } from './useChartContext';
|
||||
import { useUpdateChartData } from './useUpdateChartData';
|
||||
import { omnichannelQueryKeys } from '../../../../lib/queryKeys';
|
||||
import { drawDoughnutChart } from '../../chartHandler';
|
||||
|
||||
const labels = ['Available', 'Away', 'Busy', 'Offline'];
|
||||
|
||||
const initialData = {
|
||||
available: 0,
|
||||
away: 0,
|
||||
busy: 0,
|
||||
offline: 0,
|
||||
};
|
||||
|
||||
const init = (canvas: HTMLCanvasElement, context: chartjs.Chart<'doughnut'> | undefined, t: TFunction) =>
|
||||
drawDoughnutChart(
|
||||
canvas,
|
||||
t('Agents'),
|
||||
context,
|
||||
labels.map((l) => t(l as TranslationKey)),
|
||||
Object.values(initialData),
|
||||
);
|
||||
|
||||
type AgentStatusChartsProps = {
|
||||
departmentId: ILivechatDepartment['_id'];
|
||||
} & ComponentPropsWithoutRef<typeof Box>;
|
||||
|
||||
const AgentStatusChart = ({ departmentId, ...props }: AgentStatusChartsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const canvas: MutableRefObject<HTMLCanvasElement | null> = useRef(null);
|
||||
|
||||
const getAgentStatus = useEndpoint('GET', '/v1/livechat/analytics/dashboards/charts/agents-status');
|
||||
const { isSuccess, data: { offline = 0, available = 0, away = 0, busy = 0 } = initialData } = useQuery({
|
||||
queryKey: omnichannelQueryKeys.analytics.agentsStatus(departmentId),
|
||||
queryFn: () => getAgentStatus({ departmentId }),
|
||||
gcTime: 0,
|
||||
});
|
||||
|
||||
const context = useChartContext({
|
||||
canvas,
|
||||
init,
|
||||
t,
|
||||
});
|
||||
|
||||
const updateChartData = useUpdateChartData({
|
||||
context,
|
||||
canvas,
|
||||
init,
|
||||
t,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateChartData(t('Offline'), [offline]);
|
||||
updateChartData(t('Available'), [available]);
|
||||
updateChartData(t('Away'), [away]);
|
||||
updateChartData(t('Busy'), [busy]);
|
||||
}, [context, available, away, busy, offline, isSuccess, t, updateChartData]);
|
||||
|
||||
return <Chart canvasRef={canvas} {...props} />;
|
||||
};
|
||||
|
||||
export default AgentStatusChart;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Box } from '@rocket.chat/fuselage';
|
||||
import type { MutableRefObject } from 'react';
|
||||
|
||||
export type ChartProps = { canvasRef: MutableRefObject<HTMLCanvasElement | null> };
|
||||
|
||||
const style = {
|
||||
minHeight: '250px',
|
||||
};
|
||||
const Chart = ({ canvasRef, ...props }: ChartProps) => (
|
||||
<Box padding='x20' height='x300' {...props}>
|
||||
<canvas ref={canvasRef} style={style}></canvas>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default Chart;
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
|
||||
import type { Box } from '@rocket.chat/fuselage';
|
||||
import { useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type * as chartjs from 'chart.js';
|
||||
import type { TFunction } from 'i18next';
|
||||
import type { ComponentPropsWithoutRef } from 'react';
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Chart from './Chart';
|
||||
import { getMomentChartLabelsAndData } from './getMomentChartLabelsAndData';
|
||||
import { getMomentCurrentLabel } from './getMomentCurrentLabel';
|
||||
import { useChartContext } from './useChartContext';
|
||||
import { useUpdateChartData } from './useUpdateChartData';
|
||||
import { secondsToHHMMSS } from '../../../../../lib/utils/secondsToHHMMSS';
|
||||
import { omnichannelQueryKeys } from '../../../../lib/queryKeys';
|
||||
import { drawLineChart } from '../../chartHandler';
|
||||
|
||||
const [labels, initialData] = getMomentChartLabelsAndData();
|
||||
const tooltipCallbacks = {
|
||||
callbacks: {
|
||||
title([ctx]: [chartjs.TooltipItem<'line'>]) {
|
||||
const { dataset } = ctx;
|
||||
return dataset.label;
|
||||
},
|
||||
label(ctx: chartjs.TooltipItem<'line'>) {
|
||||
const { dataset, dataIndex } = ctx;
|
||||
const item = dataset.data[dataIndex];
|
||||
return `${dataset.label}: ${secondsToHHMMSS(typeof item === 'number' ? item : 0)}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
const init = (canvas: HTMLCanvasElement, context: chartjs.Chart<'line'> | undefined, t: TFunction) =>
|
||||
drawLineChart(canvas, context, [t('Avg_chat_duration'), t('Longest_chat_duration')], labels, [initialData, initialData.slice()], {
|
||||
legends: true,
|
||||
anim: true,
|
||||
smallTicks: true,
|
||||
displayColors: false,
|
||||
tooltipCallbacks,
|
||||
});
|
||||
|
||||
export type ChatDurationChartProps = {
|
||||
departmentId: ILivechatDepartment['_id'];
|
||||
dateRange: { start: string; end: string };
|
||||
} & ComponentPropsWithoutRef<typeof Box>;
|
||||
|
||||
const ChatDurationChart = ({ departmentId, dateRange, ...props }: ChatDurationChartProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const canvas = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
const getTimings = useEndpoint('GET', '/v1/livechat/analytics/dashboards/charts/timings');
|
||||
const { isSuccess, data } = useQuery({
|
||||
queryKey: omnichannelQueryKeys.analytics.timings(departmentId, dateRange),
|
||||
queryFn: () => getTimings({ departmentId, ...dateRange }),
|
||||
gcTime: 0,
|
||||
});
|
||||
|
||||
const context = useChartContext({
|
||||
canvas,
|
||||
init,
|
||||
t,
|
||||
});
|
||||
|
||||
const updateChartData = useUpdateChartData({
|
||||
context,
|
||||
canvas,
|
||||
init,
|
||||
t,
|
||||
});
|
||||
|
||||
const {
|
||||
chatDuration: { avg, longest },
|
||||
} = data ?? {
|
||||
chatDuration: {
|
||||
avg: 0,
|
||||
longest: 0,
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
const label = getMomentCurrentLabel();
|
||||
updateChartData(label, [avg, longest]);
|
||||
}, [context, avg, longest, isSuccess, t, updateChartData]);
|
||||
|
||||
return <Chart canvasRef={canvas} {...props} />;
|
||||
};
|
||||
|
||||
export default ChatDurationChart;
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
|
||||
import type { Box } from '@rocket.chat/fuselage';
|
||||
import { useEndpoint, type TranslationKey } from '@rocket.chat/ui-contexts';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type * as chartjs from 'chart.js';
|
||||
import type { TFunction } from 'i18next';
|
||||
import type { ComponentPropsWithoutRef, MutableRefObject } from 'react';
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Chart from './Chart';
|
||||
import { useChartContext } from './useChartContext';
|
||||
import { useUpdateChartData } from './useUpdateChartData';
|
||||
import { omnichannelQueryKeys } from '../../../../lib/queryKeys';
|
||||
import { drawDoughnutChart } from '../../chartHandler';
|
||||
|
||||
const labels = ['Open', 'Queued', 'On_Hold_Chats', 'Closed'];
|
||||
|
||||
const initialData = {
|
||||
open: 0,
|
||||
queued: 0,
|
||||
onhold: 0,
|
||||
closed: 0,
|
||||
};
|
||||
|
||||
const init = (canvas: HTMLCanvasElement, context: chartjs.Chart<'doughnut'> | undefined, t: TFunction) =>
|
||||
drawDoughnutChart(
|
||||
canvas,
|
||||
t('Chats'),
|
||||
context,
|
||||
labels.map((l) => t(l as TranslationKey)),
|
||||
Object.values(initialData),
|
||||
);
|
||||
|
||||
export type ChatsChartProps = {
|
||||
departmentId: ILivechatDepartment['_id'];
|
||||
dateRange: { start: string; end: string };
|
||||
} & ComponentPropsWithoutRef<typeof Box>;
|
||||
|
||||
const ChatsChart = ({ departmentId, dateRange, ...props }: ChatsChartProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const canvas: MutableRefObject<HTMLCanvasElement | null> = useRef(null);
|
||||
|
||||
const getChats = useEndpoint('GET', '/v1/livechat/analytics/dashboards/charts/chats');
|
||||
const { isSuccess, data } = useQuery({
|
||||
queryKey: omnichannelQueryKeys.analytics.chats(departmentId, dateRange),
|
||||
queryFn: () => getChats({ departmentId, ...dateRange }),
|
||||
gcTime: 0,
|
||||
});
|
||||
|
||||
const context = useChartContext({
|
||||
canvas,
|
||||
init,
|
||||
t,
|
||||
});
|
||||
|
||||
const updateChartData = useUpdateChartData({
|
||||
context,
|
||||
canvas,
|
||||
init,
|
||||
t,
|
||||
});
|
||||
|
||||
const { open, queued, closed, onhold } = data ?? initialData;
|
||||
|
||||
useEffect(() => {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateChartData(t('Open'), [open]);
|
||||
updateChartData(t('Closed'), [closed]);
|
||||
updateChartData(t('On_Hold_Chats'), [onhold]);
|
||||
updateChartData(t('Queued'), [queued]);
|
||||
}, [context, closed, open, queued, onhold, isSuccess, t, updateChartData]);
|
||||
|
||||
return <Chart canvasRef={canvas} {...props} />;
|
||||
};
|
||||
|
||||
export default ChatsChart;
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
|
||||
import type { Box } from '@rocket.chat/fuselage';
|
||||
import { useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type * as chartjs from 'chart.js';
|
||||
import type { TFunction } from 'i18next';
|
||||
import type { ComponentPropsWithoutRef } from 'react';
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Chart from './Chart';
|
||||
import { useChartContext } from './useChartContext';
|
||||
import { useUpdateChartData } from './useUpdateChartData';
|
||||
import { omnichannelQueryKeys } from '../../../../lib/queryKeys';
|
||||
import { drawLineChart, resetChart } from '../../chartHandler';
|
||||
|
||||
const init = (canvas: HTMLCanvasElement, context: chartjs.Chart<'line'> | undefined, t: TFunction) =>
|
||||
drawLineChart(canvas, context, [t('Open'), t('Closed'), t('On_Hold_Chats')], [], [[], []], {
|
||||
legends: true,
|
||||
anim: true,
|
||||
smallTicks: true,
|
||||
});
|
||||
|
||||
export type ChatsPerAgentChartProps = {
|
||||
departmentId: ILivechatDepartment['_id'];
|
||||
dateRange: { start: string; end: string };
|
||||
} & ComponentPropsWithoutRef<typeof Box>;
|
||||
|
||||
const ChatsPerAgentChart = ({ departmentId, dateRange, ...props }: ChatsPerAgentChartProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const canvas = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
const getChatsPerAgent = useEndpoint('GET', '/v1/livechat/analytics/dashboards/charts/chats-per-agent');
|
||||
const { isSuccess, data } = useQuery({
|
||||
queryKey: omnichannelQueryKeys.analytics.chatsPerAgent(departmentId, dateRange),
|
||||
queryFn: () => getChatsPerAgent({ departmentId, ...dateRange }),
|
||||
select: ({ success: _, ...data }) => Object.entries(data),
|
||||
gcTime: 0,
|
||||
});
|
||||
|
||||
const context = useChartContext({
|
||||
canvas,
|
||||
init,
|
||||
t,
|
||||
});
|
||||
|
||||
const updateChartData = useUpdateChartData({
|
||||
context,
|
||||
canvas,
|
||||
init,
|
||||
t,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!context) return;
|
||||
|
||||
if (!isSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
resetChart(context);
|
||||
|
||||
data.forEach(([name, value]) => {
|
||||
updateChartData(name, [value.open, value.closed, value.onhold]);
|
||||
});
|
||||
}, [context, data, isSuccess, t, updateChartData]);
|
||||
|
||||
return <Chart canvasRef={canvas} {...props} />;
|
||||
};
|
||||
|
||||
export default ChatsPerAgentChart;
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
|
||||
import type { Box } from '@rocket.chat/fuselage';
|
||||
import { useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type * as chartjs from 'chart.js';
|
||||
import type { TFunction } from 'i18next';
|
||||
import type { ComponentPropsWithoutRef } from 'react';
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Chart from './Chart';
|
||||
import { useChartContext } from './useChartContext';
|
||||
import { useUpdateChartData } from './useUpdateChartData';
|
||||
import { omnichannelQueryKeys } from '../../../../lib/queryKeys';
|
||||
import { drawLineChart, resetChart } from '../../chartHandler';
|
||||
|
||||
const init = (canvas: HTMLCanvasElement, context: chartjs.Chart<'line'> | undefined, t: TFunction) =>
|
||||
drawLineChart(canvas, context, [t('Open'), t('Closed')], [], [[], []], {
|
||||
legends: true,
|
||||
anim: true,
|
||||
smallTicks: true,
|
||||
});
|
||||
|
||||
export type ChatsPerDepartmentChartProps = {
|
||||
departmentId: ILivechatDepartment['_id'];
|
||||
dateRange: { start: string; end: string };
|
||||
} & Omit<ComponentPropsWithoutRef<typeof Box>, 'data'>;
|
||||
|
||||
const ChatsPerDepartmentChart = ({ departmentId, dateRange, ...props }: ChatsPerDepartmentChartProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const canvas = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
const getChatsPerDepartment = useEndpoint('GET', '/v1/livechat/analytics/dashboards/charts/chats-per-department');
|
||||
const { isSuccess, data } = useQuery({
|
||||
queryKey: omnichannelQueryKeys.analytics.chatsPerDepartment(departmentId, dateRange),
|
||||
queryFn: () => getChatsPerDepartment({ departmentId, ...dateRange }),
|
||||
select: ({ success: _, ...data }) => Object.entries(data),
|
||||
gcTime: 0,
|
||||
});
|
||||
|
||||
const context = useChartContext({
|
||||
canvas,
|
||||
init,
|
||||
t,
|
||||
});
|
||||
|
||||
const updateChartData = useUpdateChartData({
|
||||
context,
|
||||
canvas,
|
||||
init,
|
||||
t,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
resetChart(context);
|
||||
|
||||
data.forEach(([name, value]) => {
|
||||
updateChartData(name, [value.open, value.closed]);
|
||||
});
|
||||
}, [context, data, isSuccess, t, updateChartData]);
|
||||
|
||||
return <Chart canvasRef={canvas} {...props} />;
|
||||
};
|
||||
|
||||
export default ChatsPerDepartmentChart;
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
|
||||
import type { Box } from '@rocket.chat/fuselage';
|
||||
import { useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type * as chartjs from 'chart.js';
|
||||
import type { TFunction } from 'i18next';
|
||||
import type { ComponentPropsWithoutRef } from 'react';
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Chart from './Chart';
|
||||
import { getMomentChartLabelsAndData } from './getMomentChartLabelsAndData';
|
||||
import { getMomentCurrentLabel } from './getMomentCurrentLabel';
|
||||
import { useChartContext } from './useChartContext';
|
||||
import { useUpdateChartData } from './useUpdateChartData';
|
||||
import { secondsToHHMMSS } from '../../../../../lib/utils/secondsToHHMMSS';
|
||||
import { omnichannelQueryKeys } from '../../../../lib/queryKeys';
|
||||
import { drawLineChart } from '../../chartHandler';
|
||||
|
||||
const [labels, initialData] = getMomentChartLabelsAndData();
|
||||
const tooltipCallbacks = {
|
||||
callbacks: {
|
||||
title([ctx]: [chartjs.TooltipItem<'line'>]) {
|
||||
const { dataset } = ctx;
|
||||
return dataset.label;
|
||||
},
|
||||
label(ctx: chartjs.TooltipItem<'line'>) {
|
||||
const { dataset, dataIndex } = ctx;
|
||||
const item = dataset.data[dataIndex];
|
||||
return `${dataset.label}: ${secondsToHHMMSS(typeof item === 'number' ? item : 0)}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
const init = (canvas: HTMLCanvasElement, context: chartjs.Chart<'line'> | undefined, t: TFunction) =>
|
||||
drawLineChart(
|
||||
canvas,
|
||||
context,
|
||||
[t('Avg_reaction_time'), t('Longest_reaction_time'), t('Avg_response_time'), t('Longest_response_time')],
|
||||
labels,
|
||||
[initialData, initialData.slice(), initialData.slice(), initialData.slice()],
|
||||
{ legends: true, anim: true, smallTicks: true, displayColors: false, tooltipCallbacks },
|
||||
);
|
||||
|
||||
export type ResponseTimesChartProps = {
|
||||
departmentId: ILivechatDepartment['_id'];
|
||||
dateRange: { start: string; end: string };
|
||||
} & Omit<ComponentPropsWithoutRef<typeof Box>, 'data'>;
|
||||
|
||||
const ResponseTimesChart = ({ departmentId, dateRange, ...props }: ResponseTimesChartProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const canvas = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
const getTimings = useEndpoint('GET', '/v1/livechat/analytics/dashboards/charts/timings');
|
||||
const {
|
||||
isSuccess,
|
||||
data: { reaction: { avg: reactionAvg, longest: reactionLongest }, response: { avg: responseAvg, longest: responseLongest } } = {
|
||||
reaction: {
|
||||
avg: 0,
|
||||
longest: 0,
|
||||
},
|
||||
response: {
|
||||
avg: 0,
|
||||
longest: 0,
|
||||
},
|
||||
},
|
||||
} = useQuery({
|
||||
queryKey: omnichannelQueryKeys.analytics.timings(departmentId, dateRange),
|
||||
queryFn: () => getTimings({ departmentId, ...dateRange }),
|
||||
gcTime: 0,
|
||||
});
|
||||
|
||||
const context = useChartContext({
|
||||
canvas,
|
||||
init,
|
||||
t,
|
||||
});
|
||||
|
||||
const updateChartData = useUpdateChartData({
|
||||
context,
|
||||
canvas,
|
||||
init,
|
||||
t,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
const label = getMomentCurrentLabel();
|
||||
updateChartData(label, [reactionAvg, reactionLongest, responseAvg, responseLongest]);
|
||||
}, [context, reactionAvg, reactionLongest, responseAvg, responseLongest, isSuccess, t, updateChartData]);
|
||||
|
||||
return <Chart canvasRef={canvas} {...props} />;
|
||||
};
|
||||
|
||||
export default ResponseTimesChart;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { getMomentChartLabelsAndData } from './getMomentChartLabelsAndData';
|
||||
|
||||
const expectedTimingLabels = [
|
||||
'12AM-1AM',
|
||||
'1AM-2AM',
|
||||
'2AM-3AM',
|
||||
'3AM-4AM',
|
||||
'4AM-5AM',
|
||||
'5AM-6AM',
|
||||
'6AM-7AM',
|
||||
'7AM-8AM',
|
||||
'8AM-9AM',
|
||||
'9AM-10AM',
|
||||
'10AM-11AM',
|
||||
'11AM-12PM',
|
||||
];
|
||||
|
||||
describe('getMomentChartLabelsAndData', () => {
|
||||
it('should create timing labels from midnight to noon', () => {
|
||||
const [timingLabels] = getMomentChartLabelsAndData(12 * 60 * 60 * 1000);
|
||||
expect(timingLabels).toStrictEqual(expectedTimingLabels);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { startOfDay, addHours, format } from 'date-fns';
|
||||
|
||||
export const getMomentChartLabelsAndData = (timestamp = Date.now()) => {
|
||||
const timingLabels: string[] = [];
|
||||
const initData: number[] = [];
|
||||
const now = new Date(timestamp);
|
||||
let m = startOfDay(timestamp);
|
||||
|
||||
while (m < now) {
|
||||
const n = addHours(m, 1);
|
||||
timingLabels.push(`${format(m, 'ha')}-${format(n, 'ha')}`);
|
||||
initData.push(0);
|
||||
m = n;
|
||||
}
|
||||
|
||||
return [timingLabels, initData] as const;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { getMomentCurrentLabel } from './getMomentCurrentLabel';
|
||||
|
||||
describe('getMomentCurrentLabel', () => {
|
||||
it('should create timing labels from midnight to noon', () => {
|
||||
const label = getMomentCurrentLabel(12 * 60 * 60 * 1000);
|
||||
expect(label).toStrictEqual('12PM-1PM');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { format, addHours } from 'date-fns';
|
||||
|
||||
export const getMomentCurrentLabel = (timestamp = Date.now()) => {
|
||||
const m = new Date(timestamp);
|
||||
const n = addHours(m, 1);
|
||||
|
||||
return `${format(m, 'ha')}-${format(n, 'ha')}`;
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { Chart, ChartType } from 'chart.js';
|
||||
import type { TFunction } from 'i18next';
|
||||
import type { MutableRefObject } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
type UseChartContextProps<TChart> = {
|
||||
canvas: MutableRefObject<HTMLCanvasElement | null>;
|
||||
init: (canvas: HTMLCanvasElement, context: TChart | undefined, t: TFunction) => Promise<TChart>;
|
||||
t: TFunction;
|
||||
};
|
||||
|
||||
export const useChartContext = <TChartType extends ChartType>({ canvas, init, t }: UseChartContextProps<Chart<TChartType>>) => {
|
||||
const [context, setContext] = useState<Chart<TChartType>>();
|
||||
|
||||
useEffect(() => {
|
||||
let chart: Chart<TChartType> | undefined;
|
||||
let unmounted = false;
|
||||
|
||||
const initializeChart = async () => {
|
||||
if (!canvas.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
chart = await init(canvas.current, undefined, t);
|
||||
|
||||
if (unmounted) {
|
||||
chart?.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
setContext(chart);
|
||||
};
|
||||
|
||||
void initializeChart();
|
||||
|
||||
return () => {
|
||||
unmounted = true;
|
||||
chart?.destroy();
|
||||
setContext(undefined);
|
||||
};
|
||||
}, [canvas, init, t]);
|
||||
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import type * as chartjs from 'chart.js';
|
||||
import type { TFunction } from 'i18next';
|
||||
import type { MutableRefObject } from 'react';
|
||||
|
||||
import { updateChart } from '../../chartHandler';
|
||||
|
||||
type UseUpdateChartDataOptions<TChart> = {
|
||||
context: TChart | undefined;
|
||||
canvas: MutableRefObject<HTMLCanvasElement | null>;
|
||||
init: (canvas: HTMLCanvasElement, context: TChart | undefined, t: TFunction) => Promise<TChart>;
|
||||
t: TFunction;
|
||||
};
|
||||
|
||||
export function useUpdateChartData<TChartType extends chartjs.ChartType>({
|
||||
canvas: canvasRef,
|
||||
context,
|
||||
init,
|
||||
t,
|
||||
}: UseUpdateChartDataOptions<chartjs.Chart<TChartType>>) {
|
||||
return useStableCallback(async (label: string, data: number[]) => {
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chartContext = context ?? (await init(canvas, undefined, t));
|
||||
|
||||
await updateChart(chartContext, label, data);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user