openreplay/frontend/app/components/AssistStats/AssistStats.tsx
Andrey Babushkin fd5c0c9747
Add lokalisation (#3092)
* applied eslint

* add locales and lint the project

* removed error boundary

* updated locales

* fix min files

* fix locales
2025-03-06 17:43:15 +01:00

332 lines
10 KiB
TypeScript

import React from 'react';
import { Button, Typography, Tooltip } from 'antd';
import { Loader } from 'UI';
import {
generateListData,
defaultGraphs,
Graphs,
Member,
SessionsResponse,
PeriodKeys,
} from 'App/services/AssistStatsService';
import { FilePdfOutlined, ArrowUpOutlined } from '@ant-design/icons';
import Period, { LAST_24_HOURS } from 'Types/app/period';
import SelectDateRange from 'Shared/SelectDateRange/SelectDateRange';
import TeamMembers from 'Components/AssistStats/components/TeamMembers';
import { durationFromMsFormatted, formatTimeOrDate } from 'App/date';
import { exportCSVFile } from 'App/utils';
import { assistStatsService } from 'App/services';
import { getPdf2 } from 'Components/AssistStats/pdfGenerator';
import UserSearch from './components/UserSearch';
import Chart from './components/Charts';
import StatsTable from './components/Table';
import { useTranslation } from 'react-i18next';
import { TFunction } from 'i18next';
const chartNames = (t: TFunction) => ({
assistTotal: t('Total Live Duration'),
assistAvg: t('Avg Live Duration'),
callTotal: t('Total Call Duration'),
callAvg: t('Avg Call Duration'),
controlTotal: t('Total Remote Duration'),
controlAvg: t('Avg Remote Duration'),
});
function calculatePercentageDelta(currP: number, prevP: number) {
return ((currP - prevP) / prevP) * 100;
}
function AssistStats() {
const { t } = useTranslation();
const [selectedUser, setSelectedUser] = React.useState<any>(null);
const [period, setPeriod] = React.useState<any>(
Period({ rangeName: LAST_24_HOURS }),
);
const [membersSort, setMembersSort] = React.useState('sessionsAssisted');
const [tableSort, setTableSort] = React.useState('timestamp');
const [topMembers, setTopMembers] = React.useState<{
list: Member[];
total: number;
}>({
list: [],
total: 0,
});
const [graphs, setGraphs] = React.useState<Graphs>(defaultGraphs);
const [sessions, setSessions] = React.useState<SessionsResponse>({
list: [],
total: 0,
page: 1,
});
const [isLoading, setIsLoading] = React.useState(false);
const [page, setPage] = React.useState(1);
React.useEffect(() => {
void updateData();
}, []);
const onChangePeriod = async (period: any) => {
setPeriod(period);
void updateData(period);
};
const updateData = async (customPeriod?: any) => {
const usedP = customPeriod || period;
setIsLoading(true);
const topMembersPr = assistStatsService.getTopMembers({
startTimestamp: usedP.start,
endTimestamp: usedP.end,
userId: selectedUser || undefined,
sort: membersSort,
order: 'desc',
});
const graphsPr = assistStatsService.getGraphs(usedP);
const sessionsPr = assistStatsService.getSessions({
startTimestamp: usedP.start,
endTimestamp: usedP.end,
sort: tableSort,
order: 'desc',
userId: selectedUser || undefined,
page: 1,
limit: 10,
});
Promise.allSettled([topMembersPr, graphsPr, sessionsPr]).then(
([topMembers, graphs, sessions]) => {
topMembers.status === 'fulfilled' && setTopMembers(topMembers.value);
graphs.status === 'fulfilled' && setGraphs(graphs.value);
sessions.status === 'fulfilled' && setSessions(sessions.value);
},
);
setIsLoading(false);
};
const onPageChange = (page: number) => {
setPage(page);
assistStatsService
.getSessions({
startTimestamp: period.start,
endTimestamp: period.end,
sort: tableSort,
order: 'desc',
page,
limit: 10,
})
.then((sessions) => {
setSessions(sessions);
});
};
const onMembersSort = (sortBy: string) => {
setMembersSort(sortBy);
assistStatsService
.getTopMembers({
startTimestamp: period.start,
endTimestamp: period.end,
sort: sortBy,
order: 'desc',
})
.then((topMembers) => {
setTopMembers(topMembers);
});
};
const onTableSort = (sortBy: string) => {
setTableSort(sortBy);
assistStatsService
.getSessions({
startTimestamp: period.start,
endTimestamp: period.end,
sort: sortBy,
order: 'desc',
page: 1,
limit: 10,
})
.then((sessions) => {
setSessions(sessions);
});
};
const exportCSV = () => {
assistStatsService
.getSessions({
startTimestamp: period.start,
endTimestamp: period.end,
sort: tableSort,
order: 'desc',
page: 1,
limit: 10000,
})
.then((sessions) => {
const data = sessions.list.map((s) => ({
...s,
members: `"${s.teamMembers.map((m) => m.name).join(', ')}"`,
dateStr: `"${formatTimeOrDate(s.timestamp, undefined, true)}"`,
assistDuration: `"${durationFromMsFormatted(s.assistDuration)}"`,
callDuration: `"${durationFromMsFormatted(s.callDuration)}"`,
controlDuration: `"${durationFromMsFormatted(s.controlDuration)}"`,
}));
const headers = [
{ label: t('Date'), key: 'dateStr' },
{ label: t('Team Members'), key: 'members' },
{ label: t('Live Duration'), key: 'assistDuration' },
{ label: t('Call Duration'), key: 'callDuration' },
{ label: t('Remote Duration'), key: 'controlDuration' },
{ label: t('Session ID'), key: 'sessionId' },
];
exportCSVFile(
headers,
data,
`Assist_Stats_${new Date().toLocaleDateString()}`,
);
});
};
const onUserSelect = (id: any) => {
setSelectedUser(id);
setIsLoading(true);
const topMembersPr = assistStatsService.getTopMembers({
startTimestamp: period.start,
endTimestamp: period.end,
sort: membersSort,
userId: id,
order: 'desc',
});
const graphsPr = assistStatsService.getGraphs(period, id);
const sessionsPr = assistStatsService.getSessions({
startTimestamp: period.start,
endTimestamp: period.end,
sort: tableSort,
userId: id,
order: 'desc',
page: 1,
limit: 10,
});
Promise.allSettled([topMembersPr, graphsPr, sessionsPr]).then(
([topMembers, graphs, sessions]) => {
topMembers.status === 'fulfilled' && setTopMembers(topMembers.value);
graphs.status === 'fulfilled' && setGraphs(graphs.value);
sessions.status === 'fulfilled' && setSessions(sessions.value);
},
);
setIsLoading(false);
};
return (
<div className="w-full h-screen overflow-y-auto">
<div
className="mx-auto p-4 bg-white rounded border"
style={{ maxWidth: 1360 }}
id="pdf-anchor"
>
<div id="pdf-ignore" className="w-full flex items-center mb-2">
<Typography.Title style={{ marginBottom: 0 }} level={4}>
{t('Co-browsing Reports')}
</Typography.Title>
<div className="ml-auto flex items-center gap-2">
<UserSearch onUserSelect={onUserSelect} />
<SelectDateRange
period={period}
onChange={onChangePeriod}
right
isAnt
small
/>
<Tooltip
title={
!sessions || sessions.total === 0
? t('No data at the moment to export.')
: t('Export PDF')
}
>
<Button
onClick={getPdf2}
shape="default"
size="small"
disabled={!sessions || sessions.total === 0}
icon={<FilePdfOutlined rev={undefined} />}
/>
</Tooltip>
</div>
</div>
<div className="w-full grid grid-cols-3 gap-2 flex-2 col-span-2">
{Object.keys(graphs.currentPeriod).map((i: PeriodKeys) => (
<div className="bg-white rounded border">
<div className="pt-2 px-2">
<Typography.Text strong style={{ marginBottom: 0 }}>
{chartNames(t)[i]}
</Typography.Text>
<div className="flex gap-1 items-center">
<Typography.Title style={{ marginBottom: 0 }} level={5}>
{graphs.currentPeriod[i]
? durationFromMsFormatted(graphs.currentPeriod[i])
: null}
</Typography.Title>
{graphs.previousPeriod[i] ? (
<div
className={
graphs.currentPeriod[i] > graphs.previousPeriod[i]
? 'flex items-center gap-1 text-green'
: 'flex items-center gap-2 text-red'
}
>
<ArrowUpOutlined
rev={undefined}
rotate={
graphs.currentPeriod[i] > graphs.previousPeriod[i]
? 0
: 180
}
/>
{`${Math.round(
calculatePercentageDelta(
graphs.currentPeriod[i],
graphs.previousPeriod[i],
),
)}%`}
</div>
) : null}
</div>
</div>
<Loader
loading={isLoading}
style={{ minHeight: 90, height: 90 }}
size={36}
>
<Chart
data={generateListData(graphs.list, i)}
label={chartNames(t)[i]}
/>
</Loader>
</div>
))}
</div>
<div className="w-full mt-2">
<TeamMembers
isLoading={isLoading}
topMembers={topMembers}
onMembersSort={onMembersSort}
membersSort={membersSort}
/>
</div>
<div className="w-full mt-2">
<StatsTable
exportCSV={exportCSV}
sessions={sessions}
isLoading={isLoading}
onSort={onTableSort}
onPageChange={onPageChange}
page={page}
/>
</div>
</div>
<div id="stats-layer" />
</div>
);
}
export default AssistStats;