feat: add supervisor report modal

This commit is contained in:
SD 2024-11-28 18:27:16 +04:00
parent 08d12cd89e
commit 79a133c3ca
15 changed files with 377 additions and 21 deletions

View File

@ -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<Room>();
const [errorData, setErrorData] = useState<any>();
const [loading, setLoading] = useState<boolean>(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
};
};

View File

@ -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<Room[]> => 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<Report[]> => apiRequest({
url: `/home/getsessionsupervisorscores?sessionId=${id}`,
method: 'post',
locale,
token
});
export const saveReport = (locale: string, token: string, data: ReportData): Promise<any> => apiRequest({
url: '/home/setsessionsupervisorscores',
method: 'post',
data,
locale,
token
});

View File

@ -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<boolean>(false);
const [isOpenReport, setIsOpenReport] = useState<boolean>(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) =>
/>
</Loader>
</div>
{isSupervisor && room?.id && (
<SupervisorReportModal
open={isOpenReport}
handleCancel={() => setIsOpenReport(false)}
locale={locale}
refresh={fetchData}
roomId={room.id}
/>
)}
</div>
</>
);

View File

@ -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<boolean>(false);
const [report, setReport] = useState<Report[] | undefined>();
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 && (
<div className="card-detail__supervisor-comment">{room.supervisorComment}</div>
)}
{report && report.length > 0 && (
<div className="card-detail__report-list">
{report.map(({ key, score }) => (
<div key={key}>
<div>{i18nText(`room.rating_${key?.toLowerCase()}`, locale)}</div>
<div className="card-detail__report-list_divider" />
<div>{score || 0}</div>
</div>
))}
</div>
)}
</>
)}
{isTimeBeforeStart && room?.state === SessionState.COACH_APPROVED && !room?.supervisor && isCreator && activeType === RoomsType.UPCOMING && (

View File

@ -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<SupervisorReportModalProps> = ({
open,
handleCancel,
locale,
roomId,
refresh
}) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [loading, setLoading] = useState<boolean>(false);
const [report, setReport] = useState<Report[] | undefined>();
const reasonRef = useRef<HTMLTextAreaElement>(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 (
<Modal
className="b-modal"
open={open}
title={undefined}
onOk={undefined}
onCancel={handleCancel}
footer={false}
width={498}
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
>
<div className="b-modal__report__content">
<div className="b-rate-list">
{report && report.length > 0 && report.map(({ key, evaluationCriteriaId, score }) => (
<div key={evaluationCriteriaId} className="b-rate-list__item">
<div className="b-rate-list__item_title">{i18nText(`room.rating_${key?.toLowerCase()}`, locale)}</div>
<CustomRate
defaultValue={score || 0}
character={<StarFilled style={{ fontSize: 32 }} />}
onChange={(val: number) => onChangeRate(val, evaluationCriteriaId)}
/>
</div>
))}
</div>
<div>
<Input.TextArea
ref={reasonRef}
className="b-textarea"
rows={1}
placeholder={i18nText('room.tellAboutReason', locale)}
/>
</div>
<div>
<Button
className="btn-apply"
onClick={onSaveRate}
loading={loading}
>
{i18nText('room.rate', locale)}
</Button>
</div>
</div>
</Modal>
);
};

View File

@ -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',

View File

@ -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',

View File

@ -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',

View File

@ -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: '',

View File

@ -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: '',

View File

@ -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: 'Пользовательского соглашения',

View File

@ -26,7 +26,7 @@
}
}
&__comment__content {
&__comment__content, &__report__content {
display: flex;
flex-direction: column;
padding: 44px 40px;

View File

@ -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;

View File

@ -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;
}
}
}
}

View File

@ -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
};