From 79a133c3cafdf0bab06bc467349c89f9b012d94e Mon Sep 17 00:00:00 2001 From: SD Date: Thu, 28 Nov 2024 18:27:16 +0400 Subject: [PATCH] feat: add supervisor report modal --- src/actions/hooks/useRoomDetails.ts | 21 ++- src/actions/rooms.ts | 18 ++- src/components/Account/rooms/RoomDetails.tsx | 26 +++- .../Account/rooms/RoomDetailsContent.tsx | 32 ++++- .../Modals/SupervisorReportModal.tsx | 121 ++++++++++++++++++ src/i18nKeys/de.ts | 20 ++- src/i18nKeys/en.ts | 20 ++- src/i18nKeys/es.ts | 20 ++- src/i18nKeys/fr.ts | 20 ++- src/i18nKeys/it.ts | 20 ++- src/i18nKeys/ru.ts | 20 ++- src/styles/_modal.scss | 2 +- src/styles/sessions/_details.scss | 24 ++++ src/styles/view/_rate.scss | 21 +++ src/types/rooms.ts | 13 ++ 15 files changed, 377 insertions(+), 21 deletions(-) create mode 100644 src/components/Modals/SupervisorReportModal.tsx diff --git a/src/actions/hooks/useRoomDetails.ts b/src/actions/hooks/useRoomDetails.ts index 029337f..ff3163d 100644 --- a/src/actions/hooks/useRoomDetails.ts +++ b/src/actions/hooks/useRoomDetails.ts @@ -1,16 +1,18 @@ 'use client' -import { useCallback, useEffect, useState } from 'react'; -import { useLocalStorage } from '../../hooks/useLocalStorage'; -import { AUTH_TOKEN_KEY } from '../../constants/common'; -import { Room } from '../../types/rooms'; -import { getRoomDetails } from '../rooms'; +import {useCallback, useEffect, useState} from 'react'; +import {useLocalStorage} from '../../hooks/useLocalStorage'; +import {AUTH_TOKEN_KEY} from '../../constants/common'; +import {Room} from '../../types/rooms'; +import {getRoomDetails} from '../rooms'; +import {SessionState} from "../../types/sessions"; export const useRoomDetails = (locale: string, roomId: number) => { const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); const [room, setRoom] = useState(); const [errorData, setErrorData] = useState(); const [loading, setLoading] = useState(false); + const [isStarted, setIsStarted] = useState(false); const fetchData = useCallback(() => { setLoading(true); @@ -33,10 +35,17 @@ export const useRoomDetails = (locale: string, roomId: number) => { fetchData(); }, []); + useEffect(() => { + if (room?.state === SessionState.STARTED) { + setIsStarted(true); + } + }, [room?.state]) + return { fetchData, loading, room, - errorData + errorData, + isStarted }; }; diff --git a/src/actions/rooms.ts b/src/actions/rooms.ts index dd8f03d..c1a8b83 100644 --- a/src/actions/rooms.ts +++ b/src/actions/rooms.ts @@ -1,5 +1,5 @@ import { apiRequest } from './helpers'; -import {GetUsersForRooms, Room, RoomEdit, RoomEditDTO} from '../types/rooms'; +import {GetUsersForRooms, Report, ReportData, Room, RoomEdit, RoomEditDTO} from '../types/rooms'; export const getUpcomingRooms = (locale: string, token: string): Promise => apiRequest({ url: '/home/upcomingsessionsall', @@ -107,3 +107,19 @@ export const getRoomById = (locale: string, token: string, id: number): Promise< locale, token }); + +// report +export const getReport = (locale: string, token: string, id: number): Promise => apiRequest({ + url: `/home/getsessionsupervisorscores?sessionId=${id}`, + method: 'post', + locale, + token +}); + +export const saveReport = (locale: string, token: string, data: ReportData): Promise => apiRequest({ + url: '/home/setsessionsupervisorscores', + method: 'post', + data, + locale, + token +}); diff --git a/src/components/Account/rooms/RoomDetails.tsx b/src/components/Account/rooms/RoomDetails.tsx index d3d7a78..cf3c0d2 100644 --- a/src/components/Account/rooms/RoomDetails.tsx +++ b/src/components/Account/rooms/RoomDetails.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { RoomsType } from '../../../types/rooms'; import { useSessionTracking } from '../../../actions/hooks/useSessionTracking'; import { AccountMenu } from '../AccountMenu'; @@ -8,6 +8,9 @@ import { Loader } from '../../view/Loader'; import { RoomDetailsContent } from './RoomDetailsContent'; import { useRoomDetails } from '../../../actions/hooks/useRoomDetails'; import { AgoraClientGroup } from '../agora'; +import { SupervisorReportModal } from '../../Modals/SupervisorReportModal'; +import { useLocalStorage } from '../../../hooks/useLocalStorage'; +import { AUTH_USER } from '../../../constants/common'; type RoomDetailsProps = { locale: string; @@ -16,9 +19,13 @@ type RoomDetailsProps = { }; export const RoomDetails = ({ roomId, locale, activeType }: RoomDetailsProps) => { - const { room, errorData, loading, fetchData } = useRoomDetails(locale, roomId); + const { room, errorData, loading, fetchData, isStarted } = useRoomDetails(locale, roomId); const tracking = useSessionTracking(locale, roomId); const [isCalling, setIsCalling] = useState(false); + const [isOpenReport, setIsOpenReport] = useState(false); + const [userData] = useLocalStorage(AUTH_USER, ''); + const { id: userId = 0 } = userData ? JSON.parse(userData) : {}; + const isSupervisor = room?.supervisor && room.supervisor.id === +userId || false; useEffect(() => { if (isCalling) { @@ -28,6 +35,12 @@ export const RoomDetails = ({ roomId, locale, activeType }: RoomDetailsProps) => } }, [isCalling]); + useEffect(() => { + if (isSupervisor && isStarted) { + setIsOpenReport(true); + } + }, [isStarted]); + const stopCalling = () => { setIsCalling(false); fetchData(); @@ -60,6 +73,15 @@ export const RoomDetails = ({ roomId, locale, activeType }: RoomDetailsProps) => /> + {isSupervisor && room?.id && ( + setIsOpenReport(false)} + locale={locale} + refresh={fetchData} + roomId={room.id} + /> + )} ); diff --git a/src/components/Account/rooms/RoomDetailsContent.tsx b/src/components/Account/rooms/RoomDetailsContent.tsx index 00dcb82..ade0e89 100644 --- a/src/components/Account/rooms/RoomDetailsContent.tsx +++ b/src/components/Account/rooms/RoomDetailsContent.tsx @@ -1,22 +1,23 @@ 'use client' -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Button, notification, Tag } from 'antd'; import { DeleteOutlined, LeftOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import Image from 'next/image'; import { useRouter } from '../../../navigation'; -import { Room, RoomsType } from '../../../types/rooms'; +import { Report, Room, RoomsType } from '../../../types/rooms'; import { i18nText } from '../../../i18nKeys'; import { LinkButton } from '../../view/LinkButton'; -import { +import { addClient, addSupervisor, becomeRoomClient, becomeRoomSupervisor, deleteRoomClient, - deleteRoomSupervisor - } from '../../../actions/rooms'; + deleteRoomSupervisor, + getReport +} from '../../../actions/rooms'; import { AUTH_TOKEN_KEY, AUTH_USER } from '../../../constants/common'; import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { UserListModal } from '../../Modals/UsersListModal'; @@ -46,6 +47,16 @@ export const RoomDetailsContent = ({ room, startRoom, locale, activeType, refres const isClient = room?.clients && room.clients.length > 0 && room.clients.map(({ id }) => id).includes(+userId) || false; const isTimeBeforeStart = room?.scheduledStartAtUtc ? dayjs() < dayjs(room.scheduledStartAtUtc) : false; const [isEdit, setIsEdit] = useState(false); + const [report, setReport] = useState(); + + useEffect(() => { + if (room?.id && room?.supervisor && activeType === RoomsType.RECENT) { + getReport(locale, jwt, room.id) + .then((data) => { + setReport(data); + }) + } + }, [room]) const goBack = () => router.push(`/account/rooms/${activeType}`); @@ -248,6 +259,17 @@ export const RoomDetailsContent = ({ room, startRoom, locale, activeType, refres {room?.supervisorComment && (
{room.supervisorComment}
)} + {report && report.length > 0 && ( +
+ {report.map(({ key, score }) => ( +
+
{i18nText(`room.rating_${key?.toLowerCase()}`, locale)}
+
+
{score || 0}
+
+ ))} +
+ )} )} {isTimeBeforeStart && room?.state === SessionState.COACH_APPROVED && !room?.supervisor && isCreator && activeType === RoomsType.UPCOMING && ( diff --git a/src/components/Modals/SupervisorReportModal.tsx b/src/components/Modals/SupervisorReportModal.tsx new file mode 100644 index 0000000..f2d5223 --- /dev/null +++ b/src/components/Modals/SupervisorReportModal.tsx @@ -0,0 +1,121 @@ +'use client'; + +import React, { FC, useEffect, useRef, useState } from 'react'; +import { Modal, Button, message, Input } from 'antd'; +import { CloseOutlined, StarFilled } from '@ant-design/icons'; +import { i18nText } from '../../i18nKeys'; +import { useLocalStorage } from '../../hooks/useLocalStorage'; +import { AUTH_TOKEN_KEY } from '../../constants/common'; +import { getReport, saveReport } from '../../actions/rooms'; +import { Report, ReportData } from '../../types/rooms'; +import { CustomRate } from '../view/CustomRate'; + +type SupervisorReportModalProps = { + open: boolean; + handleCancel: () => void; + locale: string; + refresh: () => void; + roomId: number; +}; + +export const SupervisorReportModal: FC = ({ + open, + handleCancel, + locale, + roomId, + refresh +}) => { + const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); + const [loading, setLoading] = useState(false); + const [report, setReport] = useState(); + const reasonRef = useRef(null); + + useEffect(() => { + getReport(locale, jwt, roomId) + .then((data) => { + setReport(data); + }) + .catch(() => { + message.error('Не удалось получить отчет'); + }) + }, []); + + const onSaveRate = () => { + const result: ReportData = { + sessionId: roomId, + sessionSupervisorScores: report || [], + supervisorComment: reasonRef?.current?.resizableTextArea?.textArea?.value || '' + }; + + setLoading(true); + saveReport(locale, jwt, result) + .then(() => { + handleCancel(); + refresh(); + }) + .catch(() => { + message.error('Не удалось сохранить отчет'); + }) + .finally(() => { + setLoading(false); + }) + } + + const onChangeRate = (val: number, id: number) => { + setReport(report ? report.map((item) => { + if (item.evaluationCriteriaId === id) { + return { + ...item, + score: val + }; + } + + return item; + }) : undefined); + } + + return ( + } + > +
+
+ {report && report.length > 0 && report.map(({ key, evaluationCriteriaId, score }) => ( +
+
{i18nText(`room.rating_${key?.toLowerCase()}`, locale)}
+ } + onChange={(val: number) => onChangeRate(val, evaluationCriteriaId)} + /> +
+ ))} +
+
+ +
+
+ +
+
+
+ ); +}; diff --git a/src/i18nKeys/de.ts b/src/i18nKeys/de.ts index 53e2986..7134d77 100644 --- a/src/i18nKeys/de.ts +++ b/src/i18nKeys/de.ts @@ -65,7 +65,25 @@ export default { joinParticipant: 'Als Teilnehmer beitreten', rapport: 'Rapport', invite: 'Invite', - save: 'Raum speichern' + save: 'Raum speichern', + rate: 'Bewerten', + tellAboutReason: 'Sag uns, was passiert ist', + rating_raport: 'Rapport', + rating_position_and_presence: 'Position oder Präsenz eines Coaches', + rating_balance_and_frustration: 'Balance zwischen Unterstützung und Frustration', + rating_agreement: 'Erstellung einer Coaching-Vereinbarung (Sitzungsvertrag)', + rating_planning_and_goals: 'Planung und Zielsetzung', + rating_reality: 'Klärung der Realität', + rating_opportunities: 'Neue Möglichkeiten gefunden', + rating_action_plan: 'Es wurde ein Aktionsplan erstellt', + rating_motivation: 'Motivationsquellen gefunden', + rating_next_session_stretch: 'Es ist noch Zeit bis zur nächsten Sitzung', + rating_relationship: 'Aufbau einer vertrauensvollen Beziehung zum Klienten', + rating_listening: 'Tiefes, aktives Zuhören', + rating_questions: 'Verwendung „starker“ Fragen', + rating_communication: 'Direkte Kommunikation', + rating_awareness: 'Entwicklung und Stimulierung des Bewusstseins', + rating_progress: 'Fortschritts- und Verantwortungsmanagement' }, agreementText: 'Folgendes habe ich gelesen und erkläre mich damit einverstanden: Benutzervereinbarung,', userAgreement: 'Benutzervereinbarung', diff --git a/src/i18nKeys/en.ts b/src/i18nKeys/en.ts index bc6b080..fab0990 100644 --- a/src/i18nKeys/en.ts +++ b/src/i18nKeys/en.ts @@ -65,7 +65,25 @@ export default { joinParticipant: 'Join as a participant', rapport: 'Rapport', invite: 'Invite', - save: 'Save room' + save: 'Save room', + rate: 'Rate', + tellAboutReason: 'Tell us what happened', + rating_raport: 'Rapport', + rating_position_and_presence: 'Coaching position or coaching presence', + rating_balance_and_frustration: 'Balance of support and frustration', + rating_agreement: 'Creating a coaching agreement (session contract)', + rating_planning_and_goals: 'Planning and goal setting', + rating_reality: 'Clarifying reality', + rating_opportunities: 'New opportunities found', + rating_action_plan: 'An action plan has been drawn up', + rating_motivation: 'Sources of motivation found', + rating_next_session_stretch: 'There is a stretch for the next session', + rating_relationship: 'Establishing a trusting relationship with the client', + rating_listening: 'Deep, active listening', + rating_questions: 'Using "strong" questions', + rating_communication: 'Direct communication', + rating_awareness: 'Developing and stimulating awareness', + rating_progress: 'Progress and Responsibility Management' }, agreementText: 'I have read and agree with the terms of the User Agreement,', userAgreement: 'User Agreement', diff --git a/src/i18nKeys/es.ts b/src/i18nKeys/es.ts index 2922e94..a68ff8b 100644 --- a/src/i18nKeys/es.ts +++ b/src/i18nKeys/es.ts @@ -65,7 +65,25 @@ export default { joinParticipant: 'Unirse como participante', rapport: 'Buena relación', invite: 'Invitar', - save: 'Guardar sala' + save: 'Guardar sala', + rate: 'Valorar', + tellAboutReason: 'Cuéntanos qué ha pasado', + rating_raport: 'Buena relación', + rating_position_and_presence: 'Puesto de coach o presencia de coach', + rating_balance_and_frustration: 'Equilibrio entre apoyo y frustración', + rating_agreement: 'Crear un acuerdo de coaching (contrato de sesión)', + rating_planning_and_goals: 'Planear y establecer los objetivos', + rating_reality: 'Clarificar la realidad', + rating_opportunities: 'Nuevas oportunidades encontradas', + rating_action_plan: 'Se ha diseñado un plan de acción', + rating_motivation: 'Fuentes de motivación encontradas', + rating_next_session_stretch: 'Queda un poco para la siguiente sesión', + rating_relationship: 'Establecer una relación de confianza con el cliente', + rating_listening: 'Escucha activa y profunda', + rating_questions: 'Usar preguntas "contundentes"', + rating_communication: 'Comunicación directa', + rating_awareness: 'Desarrollar y estimular la conciencia', + rating_progress: 'Progreso y gestión de la responsabilidad' }, agreementText: 'He leído y acepto las condiciones del Acuerdo de usuario,', userAgreement: 'Acuerdo de usuario', diff --git a/src/i18nKeys/fr.ts b/src/i18nKeys/fr.ts index ab93f21..5d49044 100644 --- a/src/i18nKeys/fr.ts +++ b/src/i18nKeys/fr.ts @@ -65,7 +65,25 @@ export default { joinParticipant: 'Rejoindre en tant que participant', rapport: 'Rapport', invite: 'Inviter', - save: 'Sauvegarder la salle' + save: 'Sauvegarder la salle', + rate: 'Noter', + tellAboutReason: 'Dites-nous ce qui s\'est passé', + rating_raport: 'Rapport', + rating_position_and_presence: 'Poste de coach ou présence de coach', + rating_balance_and_frustration: 'Équilibre entre assistance et frustration', + rating_agreement: 'Création d\'un contrat de coaching (contrat de séance)', + rating_planning_and_goals: 'Planification et définition des objectifs', + rating_reality: 'Clarification de la réalité', + rating_opportunities: 'Nouvelles opportunités trouvées', + rating_action_plan: 'Un plan d\'action a été établi', + rating_motivation: 'Sources de motivation trouvées', + rating_next_session_stretch: 'Une période est présente pour la prochaine session', + rating_relationship: 'Établissement d\'une relation de confiance avec le client', + rating_listening: 'Écoute approfondie et active', + rating_questions: 'Utilisation de questions «fortes»', + rating_communication: 'Communication directe', + rating_awareness: 'Développement et stimulation de la prise de conscience', + rating_progress: 'Gestion de la progression et de la responsabilité' }, agreementText: 'J\'ai lu et j\'accepte les dispositions de l\'Accord Utilisateur et de la', userAgreement: '', diff --git a/src/i18nKeys/it.ts b/src/i18nKeys/it.ts index f2c17c6..a6b5db8 100644 --- a/src/i18nKeys/it.ts +++ b/src/i18nKeys/it.ts @@ -65,7 +65,25 @@ export default { joinParticipant: 'Partecipa come partecipante', rapport: 'Rapporto', invite: 'Invita', - save: 'Salva sala' + save: 'Salva sala', + rate: 'Valuta', + tellAboutReason: 'Descrivi cosa è successo', + rating_raport: 'Rapporto', + rating_position_and_presence: 'Posizione di coaching o presenza di coaching', + rating_balance_and_frustration: 'Equilibrio tra sostegno e frustrazione', + rating_agreement: 'Creazione di un accordo di coaching (contratto di sessione)', + rating_planning_and_goals: 'Pianificazione e definizione di obiettivi', + rating_reality: 'Chiarimento della realtà', + rating_opportunities: 'Nuove opportunità trovate', + rating_action_plan: 'È stato elaborato un piano d\'azione', + rating_motivation: 'Fonti di motivazione trovate', + rating_next_session_stretch: 'Esiste un\'estensione per la prossima sessione', + rating_relationship: 'Instaurazione di un rapporto di fiducia con il cliente', + rating_listening: 'Ascolto profondo e attivo', + rating_questions: 'Utilizzo di domande "forti"', + rating_communication: 'Comunicazione diretta', + rating_awareness: 'Sviluppo e stimolo della consapevolezza', + rating_progress: 'Gestione dei progressi e delle responsabilità' }, agreementText: 'Ho letto e accetto i termini dell\'Accordo con l\'utente,', userAgreement: '', diff --git a/src/i18nKeys/ru.ts b/src/i18nKeys/ru.ts index 7ef3681..cf1ed68 100644 --- a/src/i18nKeys/ru.ts +++ b/src/i18nKeys/ru.ts @@ -65,7 +65,25 @@ export default { joinParticipant: 'Присоединиться как участник', rapport: 'Раппорт', invite: 'Пригласить', - save: 'Сохранить комнату' + save: 'Сохранить комнату', + rate: 'Оценить', + tellAboutReason: 'Расскажите, что произошло', + rating_raport: 'Раппорт', + rating_position_and_presence: 'Коуч-позиция или коучинговое присутствие', + rating_balance_and_frustration: 'Баланс поддержки и фрустрации', + rating_agreement: 'Создание коучингового соглашения (контракт на сессию)', + rating_planning_and_goals: 'Планирование и постановка целей', + rating_reality: 'Прояснение реальности', + rating_opportunities: 'Найдены новые возможности', + rating_action_plan: 'Составлен план действий', + rating_motivation: 'Найдены источники мотивации', + rating_next_session_stretch: 'Есть "протяжка" на следующую сессию', + rating_relationship: 'Установление доверительных отношений с клиентом', + rating_listening: 'Глубокое активное слушание', + rating_questions: 'Использование сильных вопросов', + rating_communication: 'Прямая коммуникация', + rating_awareness: 'Развитие и стимулирование осознанности', + rating_progress: 'Управление прогрессом и ответственностью' }, agreementText: 'Я прочитал и согласен с условиями Пользовательского соглашения,', userAgreement: 'Пользовательского соглашения', diff --git a/src/styles/_modal.scss b/src/styles/_modal.scss index f72e0a1..0b9c91a 100644 --- a/src/styles/_modal.scss +++ b/src/styles/_modal.scss @@ -26,7 +26,7 @@ } } - &__comment__content { + &__comment__content, &__report__content { display: flex; flex-direction: column; padding: 44px 40px; diff --git a/src/styles/sessions/_details.scss b/src/styles/sessions/_details.scss index c421f92..dcbc525 100644 --- a/src/styles/sessions/_details.scss +++ b/src/styles/sessions/_details.scss @@ -57,6 +57,30 @@ line-height: 120%; } + &__report-list { + display: flex; + width: 100%; + flex-direction: column; + gap: 8px; + + & > div { + width: 100%; + color: #4E7C86; + @include rem(13); + font-weight: 500; + line-height: 120%; + display: flex; + gap: 8px; + justify-content: space-between; + align-items: flex-end; + } + + &_divider { + flex: 1; + border-bottom: 1px solid #E4F5FA; + } + } + &__comments { display: flex; flex-direction: column; diff --git a/src/styles/view/_rate.scss b/src/styles/view/_rate.scss index e194d35..f312b09 100644 --- a/src/styles/view/_rate.scss +++ b/src/styles/view/_rate.scss @@ -14,4 +14,25 @@ color: #c4dfe6 !important; } } + + &-list { + display: flex; + flex-direction: column; + gap: 24px; + + &__item { + display: flex; + flex-direction: column; + gap: 8px; + align-items: center; + + &_title { + color: #2C7873; + @include rem(13); + font-weight: 500; + line-height: 120%; + text-align: center; + } + } + } } diff --git a/src/types/rooms.ts b/src/types/rooms.ts index 95f4bf3..c191e3d 100644 --- a/src/types/rooms.ts +++ b/src/types/rooms.ts @@ -42,3 +42,16 @@ export type RoomEditDTO = { tags?: Tag[]; availableSlots: Slot[]; }; + +export type Report = { + evaluationCriteriaId: number, + evaluationCriteriaName?: string, + score?: number, + key?: string +}; + +export type ReportData = { + sessionId: number, + sessionSupervisorScores: Report[], + supervisorComment?: string +};