From 9d22ef95315e1fe7ce7fe6216b3a494855962890 Mon Sep 17 00:00:00 2001 From: "mateusz.bieniek" Date: Thu, 16 Apr 2026 08:57:40 +0200 Subject: [PATCH 1/9] Add enable ai analysis switch in consultation and webinar forms --- src/locales/pl-PL.ts | 2 ++ src/pages/Consultations/form.tsx | 10 +++++++++- src/pages/Webinars/form.tsx | 12 +++++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/locales/pl-PL.ts b/src/locales/pl-PL.ts index 29c74acd..a3e450b2 100644 --- a/src/locales/pl-PL.ts +++ b/src/locales/pl-PL.ts @@ -379,6 +379,8 @@ export default { target_group: 'Grupa docelowa', author_tutor: 'Autor / Nauczyciel', short_description: 'Krótki opis', + ai_analysis_enable_label: 'Analiza nagrania AI', + ai_analysis_enable_tooltip: 'Zaznacz tę opcję aby włączyć analizę badania atencji oraz satystakcji użytkowników na spotkaniu', summary: 'Podsumowanie', summary_tooltip: 'Dany edytor WYSIWYG zawiera narzędzia do formatowania, zachowując jednocześnie możliwość pisania Markdown z klawiatury oraz wyświetlania zwykłego Markdown.', diff --git a/src/pages/Consultations/form.tsx b/src/pages/Consultations/form.tsx index a993cdac..2d6ecc90 100644 --- a/src/pages/Consultations/form.tsx +++ b/src/pages/Consultations/form.tsx @@ -2,7 +2,7 @@ import ProCard from '@ant-design/pro-card'; import ProForm, { ProFormDatePicker, ProFormDigit, - ProFormSelect, + ProFormSelect, ProFormSwitch, ProFormText, } from '@ant-design/pro-form'; import { Alert, Button, Col, Row, Spin } from 'antd'; @@ -357,6 +357,14 @@ const ConsultationForm = () => { fieldProps={{ step: 1 }} /> + + } + tooltip={} + width="lg" + /> + { + + } + tooltip={} + width="lg" + /> + Date: Thu, 16 Apr 2026 09:53:20 +0200 Subject: [PATCH 2/9] Add translations, change rating color display method --- src/locales/en-US.ts | 2 ++ src/locales/fr-FR.ts | 2 ++ .../components/EffectivenessAnalysis.tsx | 28 +++++++++++-------- .../EffectivenessAnalysisDetails.tsx | 11 ++++++-- src/utils/utils.ts | 6 ++++ 5 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index e35f7dbd..1afafe7c 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -398,6 +398,8 @@ export default { short_description: 'Short description', description_tooltip: 'The editor is WYSIWYG and includes formatting tools whilst retaining the ability to write markdown shortcuts inline and output plain Markdown.', + ai_analysis_enable_label: 'AI Recording Analysis', + ai_analysis_enable_tooltip: 'Check this option to enable AI-powered analysis of user attention and satisfaction during the meeting', attributes: 'Attributes', new_course: 'New course', new_questionnaire: 'New Questionnaire', diff --git a/src/locales/fr-FR.ts b/src/locales/fr-FR.ts index 21d38f5c..ba1430b7 100644 --- a/src/locales/fr-FR.ts +++ b/src/locales/fr-FR.ts @@ -256,6 +256,8 @@ export default { summary_tooltip: 'The editor is WYSIWYG and includes formatting tools whilst retaining the ability to write markdown shortcuts inline and output plain Markdown.', short_description: 'Short description', + ai_analysis_enable_label: 'Analyse de l’enregistrement par IA', + ai_analysis_enable_tooltip: 'Cochez cette option pour activer l’analyse de l’attention et de la satisfaction des utilisateurs pendant la réunion', description: 'Description', description_tooltip: 'The editor is WYSIWYG and includes formatting tools whilst retaining the ability to write markdown shortcuts inline and output plain Markdown.', diff --git a/src/pages/Consultations/components/EffectivenessAnalysis.tsx b/src/pages/Consultations/components/EffectivenessAnalysis.tsx index 4a3c6d12..15e5b907 100644 --- a/src/pages/Consultations/components/EffectivenessAnalysis.tsx +++ b/src/pages/Consultations/components/EffectivenessAnalysis.tsx @@ -6,7 +6,7 @@ import { createTableOrderObject, EMOTION_POOL, formatPercent, - getLabelColorByValue, + getLabelColorByValue, getRatingLabelColorByValue, } from '@/utils/utils'; import { Link } from '@@/exports'; import { @@ -79,10 +79,12 @@ const ValueTag = React.memo( value, suffix = '', isRaw = false, + rating, }: { value: string | number; suffix?: string; isRaw?: boolean; + rating?: boolean; }) => { const displayValue = useMemo(() => { if (isRaw) return formatPercent(value); @@ -90,7 +92,7 @@ const ValueTag = React.memo( return isNaN(num) ? '0.00' : num.toFixed(2); }, [value, isRaw]); - const color = useMemo(() => getLabelColorByValue(parseFloat(displayValue)), [displayValue]); + const color = useMemo(() => rating ? getRatingLabelColorByValue(parseFloat(displayValue)) : getLabelColorByValue(parseFloat(displayValue)), [displayValue]); return ( @@ -101,6 +103,15 @@ const ValueTag = React.memo( }, ); +const createEmotionColumn = (emoji: string, dataKey: string): ProColumns => ({ + title: {emoji}, + dataIndex: `avg_emotions_${dataKey}` as keyof RecommenderTerm, + hideInSearch: true, + align: 'center', + width: 45, + render: (val) => `${formatPercent(val as string)}%`, +}); + export const EffectivenessAnalysis = ({ modelType = 'consultation', }: EffectivenessAnalysisProps) => { @@ -117,14 +128,7 @@ export const EffectivenessAnalysis = ({ [modelType], ); - const createEmotionColumn = (emoji: string, dataKey: string): ProColumns => ({ - title: {emoji}, - dataIndex: `avg_emotions_${dataKey}` as keyof RecommenderTerm, - hideInSearch: true, - align: 'center', - width: 45, - render: (val) => `${formatPercent(val as string)}%`, - }); + const columns: ProColumns[] = useMemo( () => [ @@ -156,7 +160,7 @@ export const EffectivenessAnalysis = ({ title: , dataIndex: 'category_id', hideInTable: true, - renderFormItem: ({ type, ...rest }) => , + renderFormItem: ({ ...rest }) => , }, { title: , @@ -182,7 +186,7 @@ export const EffectivenessAnalysis = ({ dataIndex: 'rating', hideInSearch: true, width: 60, - render: (_, record) => , + render: (_, record) => , }, { title: , diff --git a/src/pages/Consultations/components/EffectivenessAnalysisDetails.tsx b/src/pages/Consultations/components/EffectivenessAnalysisDetails.tsx index de92ab48..7555effc 100644 --- a/src/pages/Consultations/components/EffectivenessAnalysisDetails.tsx +++ b/src/pages/Consultations/components/EffectivenessAnalysisDetails.tsx @@ -6,7 +6,10 @@ import type { ChartPoint, } from '@/pages/Consultations/components/types'; import { getAnalyticsChartFrames, getModelAnalytics } from '@/services/escola-lms/consultations'; -import {ANALYSIS_COLORS, EmotionKey, formatExpirationTime, formatRating, getLabelColorByValue} from '@/utils/utils'; +import { + ANALYSIS_COLORS, EmotionKey, formatExpirationTime, formatRating, getLabelColorByValue, + getRatingLabelColorByValue +} from '@/utils/utils'; import { PageContainer } from '@ant-design/pro-components'; import { Card, Col, Select, Space, Typography, message } from 'antd'; import React, { useEffect, useMemo, useState } from 'react'; @@ -80,6 +83,10 @@ const EffectivenessAnalysisDetails = () => { () => getLabelColorByValue(analysisMeta?.rating ? analysisMeta.rating : 0), [analysisMeta?.rating], ); + const ratingColor = useMemo( + () => getRatingLabelColorByValue(analysisMeta?.rating ? analysisMeta.rating : 0), + [analysisMeta?.rating], + ); console.log(color, 'color'); @@ -189,7 +196,7 @@ const EffectivenessAnalysisDetails = () => { - {formatRating(analysisMeta?.rating || 0)} + {formatRating(analysisMeta?.rating || 0)} { return ANALYSIS_COLORS.green; }; +export const getRatingLabelColorByValue = (val: number) => { + if (val < 3.5) return ANALYSIS_COLORS.red; + if (val < 7.0) return ANALYSIS_COLORS.orange; + return ANALYSIS_COLORS.green; +}; + export enum EmotionKey { SURPRISED = 'surprised', DISGUSTED = 'disgusted', From 62719a521810089670cb91e52ae02773cae8dd97 Mon Sep 17 00:00:00 2001 From: "mateusz.bieniek" Date: Thu, 16 Apr 2026 09:56:55 +0200 Subject: [PATCH 3/9] Lint update --- .../Consultations/components/EffectivenessAnalysisDetails.tsx | 2 +- src/pages/Webinars/form.tsx | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/Consultations/components/EffectivenessAnalysisDetails.tsx b/src/pages/Consultations/components/EffectivenessAnalysisDetails.tsx index 7555effc..b1817f65 100644 --- a/src/pages/Consultations/components/EffectivenessAnalysisDetails.tsx +++ b/src/pages/Consultations/components/EffectivenessAnalysisDetails.tsx @@ -7,7 +7,7 @@ import type { } from '@/pages/Consultations/components/types'; import { getAnalyticsChartFrames, getModelAnalytics } from '@/services/escola-lms/consultations'; import { - ANALYSIS_COLORS, EmotionKey, formatExpirationTime, formatRating, getLabelColorByValue, + ANALYSIS_COLORS, EmotionKey, formatRating, getLabelColorByValue, getRatingLabelColorByValue } from '@/utils/utils'; import { PageContainer } from '@ant-design/pro-components'; diff --git a/src/pages/Webinars/form.tsx b/src/pages/Webinars/form.tsx index 84066575..2d6501f2 100644 --- a/src/pages/Webinars/form.tsx +++ b/src/pages/Webinars/form.tsx @@ -11,7 +11,6 @@ import { createWebinar, getWebinar, updateWebinar } from '@/services/escola-lms/ import { splitImagePath, tagsArrToIds } from '@/utils/utils'; import ProCard from '@ant-design/pro-card'; import ProForm, { - ProFormCheckbox, ProFormDateTimePicker, ProFormSelect, ProFormSwitch, ProFormText, @@ -21,7 +20,6 @@ import { PageContainer } from '@ant-design/pro-layout'; import { Alert, Button, Col, Row, Spin, message } from 'antd'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { FormattedMessage, history, useIntl, useParams } from 'umi'; -import CustomCheckbox from "@/pages/Roles/components/CustomCheckbox"; enum TabNames { ATTRIBUTES = 'attributes', From d371755b54db4fd4716f926fa498365526d1127f Mon Sep 17 00:00:00 2001 From: "mateusz.bieniek" Date: Thu, 16 Apr 2026 10:24:09 +0200 Subject: [PATCH 4/9] Remove add webinar/consultation from analysis table --- src/pages/Consultations/components/EffectivenessAnalysis.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pages/Consultations/components/EffectivenessAnalysis.tsx b/src/pages/Consultations/components/EffectivenessAnalysis.tsx index 15e5b907..d17b4ff6 100644 --- a/src/pages/Consultations/components/EffectivenessAnalysis.tsx +++ b/src/pages/Consultations/components/EffectivenessAnalysis.tsx @@ -235,9 +235,6 @@ export const EffectivenessAnalysis = ({ }} columnEmptyText="0%" toolBarRender={() => [ - , Date: Thu, 16 Apr 2026 10:29:17 +0200 Subject: [PATCH 5/9] Lint update --- src/pages/Consultations/components/EffectivenessAnalysis.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/Consultations/components/EffectivenessAnalysis.tsx b/src/pages/Consultations/components/EffectivenessAnalysis.tsx index d17b4ff6..c2c80dab 100644 --- a/src/pages/Consultations/components/EffectivenessAnalysis.tsx +++ b/src/pages/Consultations/components/EffectivenessAnalysis.tsx @@ -11,7 +11,6 @@ import { import { Link } from '@@/exports'; import { DownloadOutlined, - PlusOutlined, ReloadOutlined, SettingOutlined, VerticalAlignMiddleOutlined, From 2065ba252387fec7a26e05c2b336cdd0c5d547ca Mon Sep 17 00:00:00 2001 From: "mateusz.bieniek" Date: Thu, 30 Apr 2026 11:42:38 +0200 Subject: [PATCH 6/9] Add processing_video warning in ai analysis details --- src/locales/pl-PL.ts | 1 + .../EffectivenessAnalysisDetails.tsx | 34 ++++++++++++++----- src/pages/Consultations/components/types.ts | 1 + 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/locales/pl-PL.ts b/src/locales/pl-PL.ts index a3e450b2..7052c2b9 100644 --- a/src/locales/pl-PL.ts +++ b/src/locales/pl-PL.ts @@ -235,6 +235,7 @@ export default { recording_will_be_deleted: 'Po tym czasie zostanie ono skasowane', engagement_rating: 'Ocena zaangażowania', ai_analysis_average: 'Średni wynik analizy AI dla całego nagrania {modelType}.', + ai_warn_video_buffer: "Video nagrania jest obecnie przetwarzane, zdjęcia podglądu nagrania na wykresie w poszczególnych przedziałach czasowych mogą być obecnie niedostępne. Spróbuj ponowanie za kilka minut.", consultationFragment: "konsultacji", webinarFragment: "webinaru", resolution: 'Rozdzielczość danych', diff --git a/src/pages/Consultations/components/EffectivenessAnalysisDetails.tsx b/src/pages/Consultations/components/EffectivenessAnalysisDetails.tsx index b1817f65..4345e7cc 100644 --- a/src/pages/Consultations/components/EffectivenessAnalysisDetails.tsx +++ b/src/pages/Consultations/components/EffectivenessAnalysisDetails.tsx @@ -7,7 +7,7 @@ import type { } from '@/pages/Consultations/components/types'; import { getAnalyticsChartFrames, getModelAnalytics } from '@/services/escola-lms/consultations'; import { - ANALYSIS_COLORS, EmotionKey, formatRating, getLabelColorByValue, + ANALYSIS_COLORS, EmotionKey, formatRating, getRatingLabelColorByValue } from '@/utils/utils'; import { PageContainer } from '@ant-design/pro-components'; @@ -15,12 +15,15 @@ import { Card, Col, Select, Space, Typography, message } from 'antd'; import React, { useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { FormattedMessage, Link, useParams, useSelectedRoutes } from 'umi'; +import {WarningOutlined} from "@ant-design/icons"; const { Text } = Typography; const PageWrapper = styled.div` padding: 0; min-height: 100vh; + background: ${ANALYSIS_COLORS.bgLight}; + border-radius: 10px; `; const StyledCard = styled(Card)` @@ -65,6 +68,20 @@ const SectionTitle = styled(Text)` font-size: 16px; `; +const VideoScreenWarning = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; + color: ${ANALYSIS_COLORS.orange}; +`; + +const VideoScreenWarningText = styled(Text)` + margin: 0; + color: ${ANALYSIS_COLORS.orange}; +`; + + const TIME_OPTIONS = [ { value: 15, label: }, { value: 30, label: }, @@ -79,17 +96,11 @@ const EffectivenessAnalysisDetails = () => { const [analysisMeta, setAnalysisMeta] = useState(null); const [chartData, setChartData] = useState([]); const routes = useSelectedRoutes(); - const color = useMemo( - () => getLabelColorByValue(analysisMeta?.rating ? analysisMeta.rating : 0), - [analysisMeta?.rating], - ); const ratingColor = useMemo( () => getRatingLabelColorByValue(analysisMeta?.rating ? analysisMeta.rating : 0), [analysisMeta?.rating], ); - console.log(color, 'color'); - const modelType = useMemo(() => { const currentRoute = routes[routes.length - 1]?.route as any; return currentRoute?.modelType; @@ -220,7 +231,14 @@ const EffectivenessAnalysisDetails = () => { - + {analysisMeta?.processing_video && ( + + + + + + + )} {chartData && ( Date: Thu, 30 Apr 2026 12:12:29 +0200 Subject: [PATCH 7/9] Webinar sort fix --- .../Consultations/components/EffectivenessAnalysis.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/pages/Consultations/components/EffectivenessAnalysis.tsx b/src/pages/Consultations/components/EffectivenessAnalysis.tsx index c2c80dab..e7692465 100644 --- a/src/pages/Consultations/components/EffectivenessAnalysis.tsx +++ b/src/pages/Consultations/components/EffectivenessAnalysis.tsx @@ -262,7 +262,7 @@ export const EffectivenessAnalysis = ({ page: current, date_from, date_to, - ...createTableOrderObject(sort, 'term'), + ...createTableOrderObject(sort, 'id'), }); setLoading(false); @@ -282,10 +282,6 @@ export const EffectivenessAnalysis = ({ } }} columns={columns} - pagination={{ - pageSize: 10, - showSizeChanger: true, - }} /> ); }; From 72f2d79e008ecbd7c5914cd0bc94c5093b231784 Mon Sep 17 00:00:00 2001 From: "mateusz.bieniek" Date: Tue, 5 May 2026 10:36:11 +0200 Subject: [PATCH 8/9] REK-170 Add webinars screen saves tab --- src/locales/en-US.ts | 2 + src/locales/pl-PL.ts | 5 + .../Consultations/components/ScreenSaves.tsx | 299 ++++++++++++------ src/pages/Webinars/form.tsx | 19 +- src/services/escola-lms/webinars.ts | 14 + 5 files changed, 237 insertions(+), 102 deletions(-) diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index 1afafe7c..2512de6b 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -160,6 +160,8 @@ export default { access: 'Access', answers: 'Answers', answer: 'Answer', + webinars: "Webinars", + 'webinars.screenSaves': 'Screen saves', 'menu.Webinars': 'Webinars', 'menu.NewWebinar': 'New webinar', 'menu.Courses': 'Courses', diff --git a/src/locales/pl-PL.ts b/src/locales/pl-PL.ts index 7052c2b9..190d2a8d 100644 --- a/src/locales/pl-PL.ts +++ b/src/locales/pl-PL.ts @@ -167,7 +167,12 @@ export default { 'menu.Other activities.StationaryEvents': 'Wydarzenia stacjonarne', 'menu.Other activities.Questionnaire Form': 'Ankiety', 'menu.Other activities.Dictionary': 'Słownik', + 'menu.Other activities.Word': 'Słowo', 'stationary_event.edit': 'Formularz Wydarzenia stacjonarne', + 'menu.My Profile': 'Mój profil', + 'menu.My Profile.My Profile': 'Mój profil', + Webinars: "Webinary", + 'webinars.screenSaves': 'Zapis Ekranu', stationary_event: 'Wydarzenie stacjonarne', 'menu.reset': 'reset', finished_at: 'Data zakończenia', diff --git a/src/pages/Consultations/components/ScreenSaves.tsx b/src/pages/Consultations/components/ScreenSaves.tsx index 76257d5b..4d27ba2f 100644 --- a/src/pages/Consultations/components/ScreenSaves.tsx +++ b/src/pages/Consultations/components/ScreenSaves.tsx @@ -1,132 +1,229 @@ -import { getSchedule } from '@/services/escola-lms/consultations'; +import { getSchedule as getConsultationSchedule } from '@/services/escola-lms/consultations'; +import { getWebinarUsers } from '@/services/escola-lms/webinars'; import type { ProColumns } from '@ant-design/pro-table'; +import ProTable from '@ant-design/pro-table'; import { Badge, Spin, message } from 'antd'; import moment from 'moment'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { FormattedMessage, useIntl } from 'umi'; import FilesBrowser from '@/components/FilesBrowser'; import { sortArrayByKey } from '@/utils/utils'; -import ProTable from '@ant-design/pro-table'; -const consultationStatus = { +const statusMap = { reported: 'warning', reject: 'error', approved: 'success', }; -export const TableColumns: ProColumns[] = [ - { - title: , - dataIndex: 'consultation_term_id', - hideInSearch: true, - sorter: true, - width: '80px', - }, - { - title: , - dataIndex: 'user', - render: (_, item) => `${item.user?.first_name} ${item.user?.last_name} ${item.user?.email}`, - sorter: true, - }, - { - title: , - dataIndex: 'date', - sorter: true, - render: (_, item) => moment(item.date).format('YYYY-MM-DD HH:mm'), - }, - { - title: , - dataIndex: 'status', - sorter: true, - render: (_, item) => ( - } - /> - ), - }, -]; +interface Props { + consultation?: number; + webinar?: number; + webinarTimestamp?: number; +} -const ScreenSaves: React.FC<{ consultation: number }> = ({ consultation }) => { +const ScreenSaves: React.FC = ({ consultation, webinar, webinarTimestamp }) => { const [loading, setLoading] = useState(false); const [appointments, setAppointments] = useState([]); + const [webinarUsers, setWebinarUsers] = useState([]); const intl = useIntl(); + const resourceId = consultation || webinar; + + const consultationColumns = useMemo( + (): ProColumns[] => [ + { + title: , + dataIndex: 'consultation_term_id', + hideInSearch: true, + sorter: true, + width: 80, + }, + { + title: , + dataIndex: 'user', + sorter: true, + render: (_, item) => + `${item.user?.first_name ?? ''} ${item.user?.last_name ?? ''} ${item.user?.email ?? ''}`, + }, + { + title: , + dataIndex: 'date', + sorter: true, + render: (_, item) => moment(item.date).format('YYYY-MM-DD HH:mm'), + }, + { + title: , + dataIndex: 'status', + sorter: true, + render: (_, item) => ( + } + /> + ), + }, + { + title: 'Analiza obrazu', + dataIndex: 'files', + render: (_, item) => ( + + ), + }, + ], + [resourceId], + ); + + const webinarColumns = useMemo( + (): ProColumns[] => [ + { + title: , + dataIndex: 'id', + hideInSearch: true, + sorter: true, + width: 80, + }, + { + title: , + dataIndex: 'user', + sorter: true, + render: (_, item) => + `${item.first_name ?? ''} ${item.last_name ?? ''} ${item.email ?? ''}`, + }, + { + title: , + dataIndex: 'active_to', + render: () => + webinarTimestamp + ? moment.unix(webinarTimestamp).format('YYYY-MM-DD HH:mm') + : '-', + }, + { + title: 'Analiza obrazu', + dataIndex: 'files', + render: (_, item) => ( + + ), + }, + ], + [resourceId], + ); + + const fetchData = useCallback(() => { + if (!resourceId) return; - const fetchAppointments = useCallback(() => { setLoading(true); - getSchedule(consultation) + + const promise = consultation + ? getConsultationSchedule(consultation) + : getWebinarUsers(webinar as number); + + promise .then((response) => { - if (response.success) { - setAppointments(response.data); + if (!response?.success) return; + if (consultation) { + setAppointments(response.data as API.ConsultationAppointment[]); + } else { + setWebinarUsers(response.data as API.UserItem[]); } }) .catch(() => { message.error(); }) .finally(() => setLoading(false)); - }, [consultation]); + }, [consultation, webinar, resourceId]); useEffect(() => { - fetchAppointments(); - }, [consultation]); - - return ( - - {loading ? ( - - ) : ( - - headerTitle={intl.formatMessage({ - id: 'Consultations', - defaultMessage: 'Consultations', - })} - loading={loading} - rowKey="consultation_term_id" - search={false} - request={async ({}, sort) => { - const sortArr = sort && Object.entries(sort)[0]; - let newArray = appointments.filter((item) => item.status === 'approved'); - - if (sortArr) { - newArray = sortArrayByKey( - newArray, - sortArr[0], - sortArr[1] === 'ascend' ? false : true, - ); - } - return { - data: newArray, - total: newArray.length, - success: true, - }; - }} - columns={[ - ...TableColumns, - - { - title: 'Analiza obrazu', - dataIndex: 'status', - sorter: true, - render: (_, item) => ( -
- -
- ), - }, - ]} - /> - )} -
- ); + fetchData(); + }, [fetchData]); + + if (loading) { + return ; + } + + if (consultation) { + return ( + + headerTitle={intl.formatMessage({ + id: 'Consultations', + defaultMessage: 'Consultations', + })} + loading={loading} + rowKey="consultation_term_id" + search={false} + options={{ + reload: false, + }} + dataSource={appointments} + request={async (_params, sort) => { + const sortArr = sort && Object.entries(sort)[0]; + let filteredData = appointments.filter((item) => item.status === 'approved'); + + if (sortArr) { + filteredData = sortArrayByKey( + filteredData, + sortArr[0], + sortArr[1] !== 'ascend', + ); + } + return { + data: filteredData, + total: filteredData.length, + success: true, + }; + }} + columns={consultationColumns} + /> + ); + } + + if (webinar) { + return ( + + headerTitle={intl.formatMessage({ + id: 'Webinars', + defaultMessage: 'Webinars', + })} + loading={loading} + rowKey="id" + search={false} + options={{ + reload: false, + }} + dataSource={webinarUsers} + request={async (_params, sort) => { + const sortArr = sort && Object.entries(sort)[0]; + let filteredData = [...webinarUsers]; + + if (sortArr) { + filteredData = sortArrayByKey( + filteredData, + sortArr[0], + sortArr[1] !== 'ascend', + ); + } + return { + data: filteredData, + total: filteredData.length, + success: true, + }; + }} + columns={webinarColumns} + /> + ); + } + + return null; }; export default ScreenSaves; diff --git a/src/pages/Webinars/form.tsx b/src/pages/Webinars/form.tsx index 2d6501f2..b44601bb 100644 --- a/src/pages/Webinars/form.tsx +++ b/src/pages/Webinars/form.tsx @@ -20,6 +20,9 @@ import { PageContainer } from '@ant-design/pro-layout'; import { Alert, Button, Col, Row, Spin, message } from 'antd'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { FormattedMessage, history, useIntl, useParams } from 'umi'; +import ScreenSaves from "@/pages/Consultations/components/ScreenSaves"; +import {settings} from "@/services/escola-lms/settings"; +import moment from "moment"; enum TabNames { ATTRIBUTES = 'attributes', @@ -28,6 +31,7 @@ enum TabNames { TAGS = 'tags', BRANDING = 'branding', USER_SUBMISSION = 'user_submission', + SCREENSAVES = 'screensaves', } const WebinarForm = () => { @@ -37,10 +41,15 @@ const WebinarForm = () => { const isNew = webinar === 'new'; const [data, setData] = useState>(); const { manageCourseEdit, setManageCourseEdit, validateCourseEdit } = useValidateFormEdit(); - + const [showScreenSaves, setShowScreenSaves] = useState(false); const [form] = ProForm.useForm(); const fetchData = useCallback(async () => { + const config = await settings({ per_page: -1 }); + + if ('data' in config) { + setShowScreenSaves(config.data.find((c) => c.key === 'show_screen_saves')?.value === '1'); + } const response = await getWebinar(Number(webinar)); if (response.success) { if (tab === TabNames.ATTRIBUTES) { @@ -445,6 +454,14 @@ const WebinarForm = () => { {webinar && } )} + {!isNew && showScreenSaves && ( + } + > + + + )} {/* CONFIRM MODAL COMPONENT */} >( + `/api/admin/webinars/${webinarId}/users`, + { + method: 'GET', + ...(options || {}), + }, + ); +} From d734b15615b664918ca7ded4904e4bb1aa5e9da5 Mon Sep 17 00:00:00 2001 From: "mateusz.bieniek" Date: Tue, 5 May 2026 11:38:15 +0200 Subject: [PATCH 9/9] REK-170 Fix sorting in screen saves tab, lint update --- src/locales/en-US.ts | 6 +- src/locales/fr-FR.ts | 3 +- src/locales/pl-PL.ts | 13 ++-- .../components/EffectivenessAnalysis.tsx | 13 ++-- .../EffectivenessAnalysisDetails.tsx | 39 ++++++----- .../Consultations/components/ScreenSaves.tsx | 65 ++++++++----------- src/pages/Consultations/form.tsx | 3 +- src/pages/Webinars/form.tsx | 18 ++--- src/services/escola-lms/webinars.ts | 16 ++--- src/utils/utils.ts | 26 ++++++++ 10 files changed, 116 insertions(+), 86 deletions(-) diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index 2512de6b..41a05e5e 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -160,7 +160,8 @@ export default { access: 'Access', answers: 'Answers', answer: 'Answer', - webinars: "Webinars", + webinars: 'Webinars', + screensaves: 'Screen saves', 'webinars.screenSaves': 'Screen saves', 'menu.Webinars': 'Webinars', 'menu.NewWebinar': 'New webinar', @@ -401,7 +402,8 @@ export default { description_tooltip: 'The editor is WYSIWYG and includes formatting tools whilst retaining the ability to write markdown shortcuts inline and output plain Markdown.', ai_analysis_enable_label: 'AI Recording Analysis', - ai_analysis_enable_tooltip: 'Check this option to enable AI-powered analysis of user attention and satisfaction during the meeting', + ai_analysis_enable_tooltip: + 'Check this option to enable AI-powered analysis of user attention and satisfaction during the meeting', attributes: 'Attributes', new_course: 'New course', new_questionnaire: 'New Questionnaire', diff --git a/src/locales/fr-FR.ts b/src/locales/fr-FR.ts index ba1430b7..98ed27ed 100644 --- a/src/locales/fr-FR.ts +++ b/src/locales/fr-FR.ts @@ -257,7 +257,8 @@ export default { 'The editor is WYSIWYG and includes formatting tools whilst retaining the ability to write markdown shortcuts inline and output plain Markdown.', short_description: 'Short description', ai_analysis_enable_label: 'Analyse de l’enregistrement par IA', - ai_analysis_enable_tooltip: 'Cochez cette option pour activer l’analyse de l’attention et de la satisfaction des utilisateurs pendant la réunion', + ai_analysis_enable_tooltip: + 'Cochez cette option pour activer l’analyse de l’attention et de la satisfaction des utilisateurs pendant la réunion', description: 'Description', description_tooltip: 'The editor is WYSIWYG and includes formatting tools whilst retaining the ability to write markdown shortcuts inline and output plain Markdown.', diff --git a/src/locales/pl-PL.ts b/src/locales/pl-PL.ts index 190d2a8d..bc12dc14 100644 --- a/src/locales/pl-PL.ts +++ b/src/locales/pl-PL.ts @@ -171,8 +171,9 @@ export default { 'stationary_event.edit': 'Formularz Wydarzenia stacjonarne', 'menu.My Profile': 'Mój profil', 'menu.My Profile.My Profile': 'Mój profil', - Webinars: "Webinary", + Webinars: 'Webinary', 'webinars.screenSaves': 'Zapis Ekranu', + screensaves: 'Zapis Ekranu', stationary_event: 'Wydarzenie stacjonarne', 'menu.reset': 'reset', finished_at: 'Data zakończenia', @@ -240,9 +241,10 @@ export default { recording_will_be_deleted: 'Po tym czasie zostanie ono skasowane', engagement_rating: 'Ocena zaangażowania', ai_analysis_average: 'Średni wynik analizy AI dla całego nagrania {modelType}.', - ai_warn_video_buffer: "Video nagrania jest obecnie przetwarzane, zdjęcia podglądu nagrania na wykresie w poszczególnych przedziałach czasowych mogą być obecnie niedostępne. Spróbuj ponowanie za kilka minut.", - consultationFragment: "konsultacji", - webinarFragment: "webinaru", + ai_warn_video_buffer: + 'Video nagrania jest obecnie przetwarzane, zdjęcia podglądu nagrania na wykresie w poszczególnych przedziałach czasowych mogą być obecnie niedostępne. Spróbuj ponowanie za kilka minut.', + consultationFragment: 'konsultacji', + webinarFragment: 'webinaru', resolution: 'Rozdzielczość danych', listeners_engagement: 'Zaangażowanie słuchaczy (%)', detected_emotions: 'Wykryte emocje', @@ -386,7 +388,8 @@ export default { author_tutor: 'Autor / Nauczyciel', short_description: 'Krótki opis', ai_analysis_enable_label: 'Analiza nagrania AI', - ai_analysis_enable_tooltip: 'Zaznacz tę opcję aby włączyć analizę badania atencji oraz satystakcji użytkowników na spotkaniu', + ai_analysis_enable_tooltip: + 'Zaznacz tę opcję aby włączyć analizę badania atencji oraz satystakcji użytkowników na spotkaniu', summary: 'Podsumowanie', summary_tooltip: 'Dany edytor WYSIWYG zawiera narzędzia do formatowania, zachowując jednocześnie możliwość pisania Markdown z klawiatury oraz wyświetlania zwykłego Markdown.', diff --git a/src/pages/Consultations/components/EffectivenessAnalysis.tsx b/src/pages/Consultations/components/EffectivenessAnalysis.tsx index e7692465..c5320f02 100644 --- a/src/pages/Consultations/components/EffectivenessAnalysis.tsx +++ b/src/pages/Consultations/components/EffectivenessAnalysis.tsx @@ -6,7 +6,8 @@ import { createTableOrderObject, EMOTION_POOL, formatPercent, - getLabelColorByValue, getRatingLabelColorByValue, + getLabelColorByValue, + getRatingLabelColorByValue, } from '@/utils/utils'; import { Link } from '@@/exports'; import { @@ -91,7 +92,13 @@ const ValueTag = React.memo( return isNaN(num) ? '0.00' : num.toFixed(2); }, [value, isRaw]); - const color = useMemo(() => rating ? getRatingLabelColorByValue(parseFloat(displayValue)) : getLabelColorByValue(parseFloat(displayValue)), [displayValue]); + const color = useMemo( + () => + rating + ? getRatingLabelColorByValue(parseFloat(displayValue)) + : getLabelColorByValue(parseFloat(displayValue)), + [displayValue], + ); return ( @@ -127,8 +134,6 @@ export const EffectivenessAnalysis = ({ [modelType], ); - - const columns: ProColumns[] = useMemo( () => [ { diff --git a/src/pages/Consultations/components/EffectivenessAnalysisDetails.tsx b/src/pages/Consultations/components/EffectivenessAnalysisDetails.tsx index 4345e7cc..b55886e1 100644 --- a/src/pages/Consultations/components/EffectivenessAnalysisDetails.tsx +++ b/src/pages/Consultations/components/EffectivenessAnalysisDetails.tsx @@ -7,15 +7,17 @@ import type { } from '@/pages/Consultations/components/types'; import { getAnalyticsChartFrames, getModelAnalytics } from '@/services/escola-lms/consultations'; import { - ANALYSIS_COLORS, EmotionKey, formatRating, - getRatingLabelColorByValue + ANALYSIS_COLORS, + EmotionKey, + formatRating, + getRatingLabelColorByValue, } from '@/utils/utils'; +import { WarningOutlined } from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-components'; import { Card, Col, Select, Space, Typography, message } from 'antd'; -import React, { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { FormattedMessage, Link, useParams, useSelectedRoutes } from 'umi'; -import {WarningOutlined} from "@ant-design/icons"; const { Text } = Typography; @@ -69,19 +71,18 @@ const SectionTitle = styled(Text)` `; const VideoScreenWarning = styled.div` - display: flex; - flex-direction: row; - align-items: center; - gap: 10px; + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; color: ${ANALYSIS_COLORS.orange}; `; const VideoScreenWarningText = styled(Text)` - margin: 0; - color: ${ANALYSIS_COLORS.orange}; + margin: 0; + color: ${ANALYSIS_COLORS.orange}; `; - const TIME_OPTIONS = [ { value: 15, label: }, { value: 30, label: }, @@ -207,12 +208,18 @@ const EffectivenessAnalysisDetails = () => { - {formatRating(analysisMeta?.rating || 0)} + + {formatRating(analysisMeta?.rating || 0)} + , + modelType: ( + + ), }} /> @@ -233,10 +240,10 @@ const EffectivenessAnalysisDetails = () => { {analysisMeta?.processing_video && ( - + - - + + )} {chartData && ( diff --git a/src/pages/Consultations/components/ScreenSaves.tsx b/src/pages/Consultations/components/ScreenSaves.tsx index 4d27ba2f..cde0d016 100644 --- a/src/pages/Consultations/components/ScreenSaves.tsx +++ b/src/pages/Consultations/components/ScreenSaves.tsx @@ -8,7 +8,8 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { FormattedMessage, useIntl } from 'umi'; import FilesBrowser from '@/components/FilesBrowser'; -import { sortArrayByKey } from '@/utils/utils'; +import type { SortAccessors } from '@/utils/utils'; +import { applySort } from '@/utils/utils'; const statusMap = { reported: 'warning', @@ -29,6 +30,24 @@ const ScreenSaves: React.FC = ({ consultation, webinar, webinarTimestamp const intl = useIntl(); const resourceId = consultation || webinar; + const consultationSorters: SortAccessors = useMemo( + () => ({ + consultation_term_id: (i) => i.consultation_term_id ?? 0, + user: (i) => `${i.user?.first_name ?? ''} ${i.user?.last_name ?? ''}`.toLowerCase(), + date: (i) => new Date(i.date).getTime(), + status: (i) => i.status ?? '', + }), + [], + ); + + const webinarSorters: SortAccessors = useMemo( + () => ({ + id: (i) => i.id ?? 0, + user: (i) => `${i.first_name ?? ''} ${i.last_name ?? ''}`.toLowerCase(), + }), + [], + ); + const consultationColumns = useMemo( (): ProColumns[] => [ { @@ -93,16 +112,13 @@ const ScreenSaves: React.FC = ({ consultation, webinar, webinarTimestamp title: , dataIndex: 'user', sorter: true, - render: (_, item) => - `${item.first_name ?? ''} ${item.last_name ?? ''} ${item.email ?? ''}`, + render: (_, item) => `${item.first_name ?? ''} ${item.last_name ?? ''} ${item.email ?? ''}`, }, { title: , dataIndex: 'active_to', render: () => - webinarTimestamp - ? moment.unix(webinarTimestamp).format('YYYY-MM-DD HH:mm') - : '-', + webinarTimestamp ? moment.unix(webinarTimestamp).format('YYYY-MM-DD HH:mm') : '-', }, { title: 'Analiza obrazu', @@ -164,23 +180,10 @@ const ScreenSaves: React.FC = ({ consultation, webinar, webinarTimestamp options={{ reload: false, }} - dataSource={appointments} request={async (_params, sort) => { - const sortArr = sort && Object.entries(sort)[0]; - let filteredData = appointments.filter((item) => item.status === 'approved'); - - if (sortArr) { - filteredData = sortArrayByKey( - filteredData, - sortArr[0], - sortArr[1] !== 'ascend', - ); - } - return { - data: filteredData, - total: filteredData.length, - success: true, - }; + const filtered = appointments.filter((item) => item.status === 'approved'); + const data = applySort(filtered, sort as any, consultationSorters); + return { data, total: data.length, success: true }; }} columns={consultationColumns} /> @@ -200,23 +203,9 @@ const ScreenSaves: React.FC = ({ consultation, webinar, webinarTimestamp options={{ reload: false, }} - dataSource={webinarUsers} request={async (_params, sort) => { - const sortArr = sort && Object.entries(sort)[0]; - let filteredData = [...webinarUsers]; - - if (sortArr) { - filteredData = sortArrayByKey( - filteredData, - sortArr[0], - sortArr[1] !== 'ascend', - ); - } - return { - data: filteredData, - total: filteredData.length, - success: true, - }; + const data = applySort(webinarUsers, sort as any, webinarSorters); + return { data, total: data.length, success: true }; }} columns={webinarColumns} /> diff --git a/src/pages/Consultations/form.tsx b/src/pages/Consultations/form.tsx index 2d6ecc90..57a75460 100644 --- a/src/pages/Consultations/form.tsx +++ b/src/pages/Consultations/form.tsx @@ -2,7 +2,8 @@ import ProCard from '@ant-design/pro-card'; import ProForm, { ProFormDatePicker, ProFormDigit, - ProFormSelect, ProFormSwitch, + ProFormSelect, + ProFormSwitch, ProFormText, } from '@ant-design/pro-form'; import { Alert, Button, Col, Row, Spin } from 'antd'; diff --git a/src/pages/Webinars/form.tsx b/src/pages/Webinars/form.tsx index b44601bb..77f22549 100644 --- a/src/pages/Webinars/form.tsx +++ b/src/pages/Webinars/form.tsx @@ -7,22 +7,23 @@ import UserSelect from '@/components/UserSelect'; import UserSubmissions from '@/components/UsersSubmissions'; import WysiwygMarkdown from '@/components/WysiwygMarkdown'; import useValidateFormEdit from '@/hooks/useValidateFormEdit'; +import ScreenSaves from '@/pages/Consultations/components/ScreenSaves'; +import { settings } from '@/services/escola-lms/settings'; import { createWebinar, getWebinar, updateWebinar } from '@/services/escola-lms/webinars'; import { splitImagePath, tagsArrToIds } from '@/utils/utils'; import ProCard from '@ant-design/pro-card'; import ProForm, { ProFormDateTimePicker, - ProFormSelect, ProFormSwitch, + ProFormSelect, + ProFormSwitch, ProFormText, ProFormTextArea, } from '@ant-design/pro-form'; import { PageContainer } from '@ant-design/pro-layout'; import { Alert, Button, Col, Row, Spin, message } from 'antd'; +import moment from 'moment'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { FormattedMessage, history, useIntl, useParams } from 'umi'; -import ScreenSaves from "@/pages/Consultations/components/ScreenSaves"; -import {settings} from "@/services/escola-lms/settings"; -import moment from "moment"; enum TabNames { ATTRIBUTES = 'attributes', @@ -159,9 +160,7 @@ const WebinarForm = () => { }, { path: '/', - breadcrumbName: intl.formatMessage({ - id: String(data.name), - }), + breadcrumbName: String(data.name), }, { path: String(tab), @@ -459,7 +458,10 @@ const WebinarForm = () => { key={TabNames.SCREENSAVES} tab={} > - + )} diff --git a/src/services/escola-lms/webinars.ts b/src/services/escola-lms/webinars.ts index 2a5c2c67..d52ed1bb 100644 --- a/src/services/escola-lms/webinars.ts +++ b/src/services/escola-lms/webinars.ts @@ -70,15 +70,9 @@ export async function generateYoutubeToken(body?: { email: string }, options?: A } /** GET /api/admin/webinars/:webinarId/users */ -export async function getWebinarUsers( - webinarId: number, - options?: AxiosRequestConfig, -) { - return request>( - `/api/admin/webinars/${webinarId}/users`, - { - method: 'GET', - ...(options || {}), - }, - ); +export async function getWebinarUsers(webinarId: number, options?: AxiosRequestConfig) { + return request>(`/api/admin/webinars/${webinarId}/users`, { + method: 'GET', + ...(options || {}), + }); } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 77cef396..ab4e80c1 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -649,3 +649,29 @@ export const formatExpirationTime = (ms: number | null) => { const pad = (num: number) => num.toString().padStart(2, '0'); return hours > 0 ? `${hours}h ${pad(minutes)}m ${pad(seconds)}s` : `${minutes}m ${pad(seconds)}s`; }; + +export type SortableValue = string | number; +export type SortAccessors = Record SortableValue>; + +export function applySort( + data: T[], + sort: Record | undefined, + accessors: SortAccessors, +): T[] { + const sortArr = sort && Object.entries(sort)[0]; + if (!sortArr) return data; + + const [key, order] = sortArr; + const accessor = accessors[key]; + if (!accessor) return data; + + const asc = order === 'ascend'; + + return [...data].sort((a, b) => { + const valA = accessor(a); + const valB = accessor(b); + if (valA < valB) return asc ? -1 : 1; + if (valA > valB) return asc ? 1 : -1; + return 0; + }); +}