diff --git a/public/images/decline-sign.svg b/public/images/decline-sign.svg new file mode 100644 index 0000000..4be8a74 --- /dev/null +++ b/public/images/decline-sign.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/actions/profile.ts b/src/actions/profile.ts index a3a98de..8e78fca 100644 --- a/src/actions/profile.ts +++ b/src/actions/profile.ts @@ -1,7 +1,6 @@ import { AxiosResponse } from 'axios'; import { apiClient } from '../lib/apiClient'; import { Profile } from '../types/profile'; -import { Session, SessionsFilter } from '../types/sessions'; export const setPersonData = (person: { login: string, password: string, role: string, languagesLinks: any[] }, locale: string, jwt: string): Promise> => ( apiClient.post( @@ -28,61 +27,3 @@ export const getPersonalData = (locale: string, jwt: string): Promise> => ( - apiClient.post( - '/home/upcomingsessionsall', - { - sessionType: 'session', - ...(filter || {}) - }, - { - headers: { - 'X-User-Language': locale, - Authorization: `Bearer ${jwt}` - } - } - ) -); - -export const getRequestedSessions = (locale: string, jwt: string): Promise> => ( - apiClient.post( - '/home/coachhomedata', - {}, - { - headers: { - 'X-User-Language': locale, - Authorization: `Bearer ${jwt}` - } - } - ) -); - -export const getRecentSessions = (locale: string, jwt: string, filter?: SessionsFilter): Promise> => ( - apiClient.post( - '/home/historicalmeetings', - { - sessionType: 'session', - ...(filter || {}) - }, - { - headers: { - 'X-User-Language': locale, - Authorization: `Bearer ${jwt}` - } - } - ) -); - -export const getSessionDetails = (locale: string, jwt: string, id: number): Promise> => ( - apiClient.post( - '/home/session', - { id }, - { - headers: { - 'X-User-Language': locale, - Authorization: `Bearer ${jwt}` - } - } - ) -); diff --git a/src/actions/sessions.ts b/src/actions/sessions.ts new file mode 100644 index 0000000..cee7b15 --- /dev/null +++ b/src/actions/sessions.ts @@ -0,0 +1,119 @@ +import { AxiosResponse } from 'axios'; +import { apiClient } from '../lib/apiClient'; +import { DeclineSessionData, Session, SessionsFilter, SessionCommentData } from '../types/sessions'; + +export const getUpcomingSessions = (locale: string, jwt: string, filter?: SessionsFilter): Promise> => ( + apiClient.post( + '/home/upcomingsessionsall', + { + sessionType: 'session', + ...(filter || {}) + }, + { + headers: { + 'X-User-Language': locale, + Authorization: `Bearer ${jwt}` + } + } + ) +); + +export const getRequestedSessions = (locale: string, jwt: string): Promise> => ( + apiClient.post( + '/home/coachhomedata', + {}, + { + headers: { + 'X-User-Language': locale, + Authorization: `Bearer ${jwt}` + } + } + ) +); + +export const getRecentSessions = (locale: string, jwt: string, filter?: SessionsFilter): Promise> => ( + apiClient.post( + '/home/historicalmeetings', + { + sessionType: 'session', + ...(filter || {}) + }, + { + headers: { + 'X-User-Language': locale, + Authorization: `Bearer ${jwt}` + } + } + ) +); + +export const getSessionDetails = (locale: string, jwt: string, id: number): Promise> => ( + apiClient.post( + '/home/session', + { id }, + { + headers: { + 'X-User-Language': locale, + Authorization: `Bearer ${jwt}` + } + } + ) +); + +export const approveRequestedSession = (locale: string, jwt: string, sessionId: number): Promise => ( + apiClient.post( + '/home/approverequestedsession', + { sessionId }, + { + headers: { + 'X-User-Language': locale, + Authorization: `Bearer ${jwt}` + } + } + ) +); + +export const declineRequestedSession = (locale: string, jwt: string, { sessionId, reason }: DeclineSessionData): Promise => ( + apiClient.post( + '/home/declinerequestedsession', + { + sessionId, + coachDeclineReason: reason + }, + { + headers: { + 'X-User-Language': locale, + Authorization: `Bearer ${jwt}` + } + } + ) +); + +export const cancelUpcomingSession = (locale: string, jwt: string, { sessionId, reason }: DeclineSessionData): Promise => ( + apiClient.post( + '/home/cancelupcomingsession', + { + sessionId, + coachCancelReason: reason + }, + { + headers: { + 'X-User-Language': locale, + Authorization: `Bearer ${jwt}` + } + } + ) +); + +export const addSessionComment = (locale: string, jwt: string, data: SessionCommentData): Promise => ( + apiClient.post( + '/home/session_comment', + data, + { + headers: { + 'X-User-Language': locale, + Authorization: `Bearer ${jwt}` + } + } + ) +); diff --git a/src/components/Account/sessions/SessionDetails.tsx b/src/components/Account/sessions/SessionDetails.tsx index 762f995..b7dfd91 100644 --- a/src/components/Account/sessions/SessionDetails.tsx +++ b/src/components/Account/sessions/SessionDetails.tsx @@ -1,18 +1,20 @@ 'use client' import React, { useCallback, useEffect, useState } from 'react'; -import { Tag, Button } from 'antd'; +import {Tag, Button, notification, Empty} from 'antd'; import { RightOutlined, PlusOutlined, LeftOutlined } from '@ant-design/icons'; import Image from 'next/image'; import dayjs from 'dayjs'; import { Link } from '../../../navigation'; import { i18nText } from '../../../i18nKeys'; import { getDuration, getPrice } from '../../../utils/expert'; -import {PublicUser, Session} from '../../../types/sessions'; +import { PublicUser, Session } from '../../../types/sessions'; import { AUTH_TOKEN_KEY, AUTH_USER } from '../../../constants/common'; -import { getSessionDetails } from '../../../actions/profile'; +import { approveRequestedSession, getSessionDetails } from '../../../actions/sessions'; import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { Loader } from '../../view/Loader'; +import { DeclineSessionModal } from '../../Modals/DeclineSessionModal'; +import { AddCommentModal } from '../../Modals/AddCommentModal'; type SessionDetailsProps = { locale: string; @@ -25,10 +27,11 @@ export const SessionDetails = ({ sessionId, locale, goBack, activeTab }: Session const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); const [userId] = useLocalStorage(AUTH_USER, ''); const [loading, setLoading] = useState(false); + const [approveLoading, setApproveLoading] = useState(false); const [errorData, setErrorData] = useState(); const [session, setSession] = useState(); - - console.log('activeTab', activeTab); + const [openDeclineModal, setOpenDeclineModal] = useState(false); + const [openAddCommentModal, setOpenAddCommentModal] = useState(false); const fetchData = useCallback(() => { setLoading(true); @@ -37,7 +40,6 @@ export const SessionDetails = ({ sessionId, locale, goBack, activeTab }: Session getSessionDetails(locale, jwt, sessionId) .then(({ data }) => { - console.log(data); setSession(data); }) .catch((err) => { @@ -52,6 +54,25 @@ export const SessionDetails = ({ sessionId, locale, goBack, activeTab }: Session fetchData(); }, [sessionId]); + const onApproveSession = (tab: typeof activeTab) => { + if (tab === 1) { + setApproveLoading(true); + approveRequestedSession(locale, jwt, sessionId) + .then(() => { + goBack(); + }) + .catch((err) => { + notification.error({ + message: 'Error', + description: err?.response?.data?.errMessage + }); + }) + .finally(() => { + setLoading(false); + }); + } + }; + const startDate = session?.scheduledStartAtUtc ? dayjs(session?.scheduledStartAtUtc).locale(locale) : null; const endDate = session?.scheduledEndAtUtc ? dayjs(session?.scheduledEndAtUtc).locale(locale) : null; const today = startDate ? dayjs().format('YYYY-MM-DD') === startDate.format('YYYY-MM-DD') : false; @@ -126,7 +147,8 @@ export const SessionDetails = ({ sessionId, locale, goBack, activeTab }: Session ) : null; const client = session?.clients?.length ? session?.clients[0] : null; - const Current = +userId !== client?.id ? StudentCard(client) : CoachCard(session?.coach); + const isCoach = +userId !== client?.id; + const Current = isCoach ? StudentCard(client) : CoachCard(session?.coach); return ( - {activeTab === 0 ? ( - <> - - - - ) : ( - <> - - - + + + {session?.id && ( + setOpenDeclineModal(false)} + activeTab={activeTab} + locale={locale} + sessionId={session.id} + success={goBack} + /> )} )} @@ -196,28 +231,52 @@ export const SessionDetails = ({ sessionId, locale, goBack, activeTab }: Session
- My Comments + {session?.clientComments?.length === 0 && session?.coachComments?.length === 0 ? 'Comments' : 'My Comments'}
{activeTab === 0 && ( - + <> + + setOpenAddCommentModal(false)} + locale={locale} + sessionId={sessionId} + refresh={fetchData} + /> + )}
-
- Sed tincidunt finibus eros nec feugiat. Nulla facilisi. Nunc maximus magna et egestas tincidunt. Integer lobortis laoreet neque at sodales. Aenean eget risus pharetra, efficitur dolor ut, commodo lacus. Sed vitae nunc odio. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis et velit et dolor rutrum euismod a pretium est. -
-
- Coach Comments -
-
- Sed tincidunt finibus eros nec feugiat. Nulla facilisi. Nunc maximus magna et egestas tincidunt. Integer lobortis laoreet neque at sodales. Aenean eget risus pharetra, efficitur dolor ut, commodo lacus. Sed vitae nunc odio. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis et velit et dolor rutrum euismod a pretium est. -
+ {(session?.clientComments?.length > 0 || session?.coachComments?.length > 0) ? ( + <> + {(isCoach ? session?.coachComments : session?.clientComments)?.map(({ id, comment }) => ( +
+ {comment} +
+ ))} + {(isCoach ? session?.clientComments : session?.coachComments)?.length > 0 && ( +
+ {isCoach ? 'Client Comments' : 'Coach Comments'} +
+ )} + {(isCoach ? session?.clientComments : session?.coachComments)?.map(({ id , comment }) => ( +
+ {comment} +
+ ))} + + ) : ( + <> + + + )}
)} diff --git a/src/components/Account/sessions/SessionsTabs.tsx b/src/components/Account/sessions/SessionsTabs.tsx index 2a998a8..6c80bc6 100644 --- a/src/components/Account/sessions/SessionsTabs.tsx +++ b/src/components/Account/sessions/SessionsTabs.tsx @@ -12,7 +12,7 @@ import 'dayjs/locale/es'; import { Loader } from '../../view/Loader'; import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { AUTH_TOKEN_KEY, AUTH_USER } from '../../../constants/common'; -import { getRecentSessions, getRequestedSessions, getUpcomingSessions } from '../../../actions/profile'; +import { getRecentSessions, getRequestedSessions, getUpcomingSessions } from '../../../actions/sessions'; import { Session, Sessions, SessionType } from '../../../types/sessions'; import { i18nText } from '../../../i18nKeys'; diff --git a/src/components/Modals/AddCommentModal.tsx b/src/components/Modals/AddCommentModal.tsx new file mode 100644 index 0000000..6041871 --- /dev/null +++ b/src/components/Modals/AddCommentModal.tsx @@ -0,0 +1,110 @@ +'use client'; + +import React, { FC, useEffect, useState } from 'react'; +import { Modal, Form, Input, notification, Button } from 'antd'; +import { CloseOutlined } from '@ant-design/icons'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import { i18nText } from '../../i18nKeys'; +import { addSessionComment } from '../../actions/sessions'; +import { useLocalStorage } from '../../hooks/useLocalStorage'; +import { AUTH_TOKEN_KEY } from '../../constants/common'; + +dayjs.extend(utc); + +type AddCommentModalProps = { + open: boolean; + handleCancel: () => void; + locale: string; + sessionId: number; + refresh: () => void; +}; + +export const AddCommentModal: FC = ({ + open, + handleCancel, + locale, + sessionId, + refresh +}) => { + const [form] = Form.useForm<{ comment: string }>(); + const [loading, setLoading] = useState(false); + const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); + + const onAfterClose = () => { + form.resetFields(); + }; + + useEffect(() => { + if (form) { + form.resetFields(); + } + }, []); + + const onAddComment = () => { + form.validateFields().then(({ comment }) => { + const createdAtUtc = dayjs().utc().format(); + + setLoading(true); + addSessionComment(locale, jwt, { sessionId, comment, createdAtUtc }) + .then(() => { + handleCancel(); + refresh(); + }) + .catch((err) => { + notification.error({ + message: 'Error', + description: err?.response?.data?.errMessage + }); + }) + .finally(() => { + setLoading(false); + }); + }) + }; + + return ( + } + > +
+
+ + + +
+
+ +
+
+
+ ); +}; diff --git a/src/components/Modals/DeclineSessionModal.tsx b/src/components/Modals/DeclineSessionModal.tsx new file mode 100644 index 0000000..cc25525 --- /dev/null +++ b/src/components/Modals/DeclineSessionModal.tsx @@ -0,0 +1,114 @@ +'use client'; + +import React, { FC, useEffect, useState } from 'react'; +import { Modal, Form, Input, notification } from 'antd'; +import { CloseOutlined } from '@ant-design/icons'; +import { FilledButton } from '../view/FilledButton'; +import { i18nText } from '../../i18nKeys'; +import { cancelUpcomingSession, declineRequestedSession } from '../../actions/sessions'; +import { useLocalStorage } from '../../hooks/useLocalStorage'; +import { AUTH_TOKEN_KEY } from '../../constants/common'; + +type DeclineModalProps = { + open: boolean; + handleCancel: () => void; + activeTab: 0 | 1; + locale: string; + sessionId: number; + success: () => void; +}; + +export const DeclineSessionModal: FC = ({ + open, + handleCancel, + activeTab, + locale, + sessionId, + success +}) => { + const [form] = Form.useForm<{ reason: string }>(); + const [loading, setLoading] = useState(false); + const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); + + const onAfterClose = () => { + form.resetFields(); + }; + + useEffect(() => { + if (form) { + form.resetFields(); + } + }, [activeTab]); + + const onDecline = () => { + form.validateFields().then(({ reason }) => { + const fetchFunc = activeTab === 0 ? cancelUpcomingSession : declineRequestedSession; + + setLoading(true); + fetchFunc(locale, jwt, { sessionId, reason }) + .then(() => { + success(); + }) + .catch((err) => { + notification.error({ + message: 'Error', + description: err?.response?.data?.errMessage + }); + }) + .finally(() => { + setLoading(false); + }); + }) + }; + + return ( + } + > +
+
+ +
+
+ Enter a reason for cancelling the session +
+
+ + + +
+
+ + Decline + +
+
+
+ ); +}; diff --git a/src/components/view/FilledButton.tsx b/src/components/view/FilledButton.tsx index f289d20..4f42a78 100644 --- a/src/components/view/FilledButton.tsx +++ b/src/components/view/FilledButton.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Button } from 'antd'; export const FilledButton = (props: any) => ( - ); diff --git a/src/styles/_modal.scss b/src/styles/_modal.scss index 7e551ed..c04824a 100644 --- a/src/styles/_modal.scss +++ b/src/styles/_modal.scss @@ -16,9 +16,24 @@ } } + .ant-modal-footer { + margin: 0 !important; + } + &__content { p { margin: 16px 0; } } + + &__comment__content { + display: flex; + flex-direction: column; + padding: 44px 40px; + gap: 24px; + } +} + +.ant-modal-mask { + background-color: rgba(0, 59, 70, 0.4) !important; } diff --git a/src/styles/sessions/_decline-modal.scss b/src/styles/sessions/_decline-modal.scss new file mode 100644 index 0000000..41fde1e --- /dev/null +++ b/src/styles/sessions/_decline-modal.scss @@ -0,0 +1,26 @@ +.b-modal__decline { + &__content { + display: flex; + flex-direction: column; + gap: 24px; + align-items: center; + justify-content: stretch; + margin: 44px 0; + } + + &__title { + color: #2C7873; + @include rem(18); + font-style: normal; + font-weight: 600; + line-height: 150%; + } + + &__button { + width: 100%; + + button { + width: 100%; + } + } +} diff --git a/src/styles/sessions/style.scss b/src/styles/sessions/style.scss index 34589d5..f5c1c1f 100644 --- a/src/styles/sessions/style.scss +++ b/src/styles/sessions/style.scss @@ -1 +1,2 @@ @import "_details.scss"; +@import "_decline-modal.scss"; diff --git a/src/styles/view/_buttons.scss b/src/styles/view/_buttons.scss index 2ece829..4af8ab7 100644 --- a/src/styles/view/_buttons.scss +++ b/src/styles/view/_buttons.scss @@ -5,6 +5,11 @@ border-radius: 8px !important; height: 54px !important; box-shadow: 0px 2px 4px 0px rgba(102, 165, 173, 0.32) !important; + + &.danger { + background: #D93E5C !important; + box-shadow: none !important; + } } &__link { diff --git a/src/styles/view/_input.scss b/src/styles/view/_input.scss index 9e25aee..3f6149d 100644 --- a/src/styles/view/_input.scss +++ b/src/styles/view/_input.scss @@ -51,3 +51,27 @@ } } } + +.b-textarea { + padding: 15px 16px !important; + background: #F8F8F7 !important; + border: 1px solid #F8F8F7 !important; + border-radius: 8px !important; + color: #000 !important; + align-items: center; + resize: none !important; + + &:focus, &:hover, &:focus-within { + border-color: #66A5AD !important; + box-shadow: none !important; + } + + &::placeholder { + color: #000 !important; + opacity: .4 !important; + } + + &.ant-input-status-error:not(.ant-input-disabled):not(.ant-input-borderless) { + border-color: #ff4d4f !important; + } +} diff --git a/src/types/sessions.ts b/src/types/sessions.ts index 12545b5..7b1c84a 100644 --- a/src/types/sessions.ts +++ b/src/types/sessions.ts @@ -108,7 +108,7 @@ export type SessionsFilter = { export type SessionComment = { id: number; createdAtUtc: string; - comment?: string; + comment: string; author?: PublicUser; authorId?: number; sessionId?: number; @@ -150,3 +150,14 @@ export type Sessions = { [SessionType.REQUESTED]?: Session[]; [SessionType.RECENT]?: Session[]; }; + +export type DeclineSessionData = { + sessionId: number; + reason: string; +}; + +export type SessionCommentData = { + createdAtUtc: string; + comment: string; + sessionId: number; +} diff --git a/src/utils/filter.ts b/src/utils/filter.ts index 7cee4ce..9a7a4bd 100644 --- a/src/utils/filter.ts +++ b/src/utils/filter.ts @@ -1,4 +1,3 @@ -import { SearchData } from '../types/tags'; import { AdditionalFilter, Filter, GeneralFilter } from '../types/experts'; import { DEFAULT_PAGE } from '../constants/common';