feat: add sessions actions

This commit is contained in:
SD 2024-05-28 01:44:50 +04:00
parent 8f00a5c41c
commit 0828e944b4
15 changed files with 531 additions and 98 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 106 KiB

View File

@ -1,7 +1,6 @@
import { AxiosResponse } from 'axios'; import { AxiosResponse } from 'axios';
import { apiClient } from '../lib/apiClient'; import { apiClient } from '../lib/apiClient';
import { Profile } from '../types/profile'; 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<AxiosResponse<{ userData: Profile }>> => ( export const setPersonData = (person: { login: string, password: string, role: string, languagesLinks: any[] }, locale: string, jwt: string): Promise<AxiosResponse<{ userData: Profile }>> => (
apiClient.post( apiClient.post(
@ -28,61 +27,3 @@ export const getPersonalData = (locale: string, jwt: string): Promise<AxiosRespo
} }
) )
); );
export const getUpcomingSessions = (locale: string, jwt: string, filter?: SessionsFilter): Promise<AxiosResponse<Session[]>> => (
apiClient.post(
'/home/upcomingsessionsall',
{
sessionType: 'session',
...(filter || {})
},
{
headers: {
'X-User-Language': locale,
Authorization: `Bearer ${jwt}`
}
}
)
);
export const getRequestedSessions = (locale: string, jwt: string): Promise<AxiosResponse<{ requestedSessions: Session[] }>> => (
apiClient.post(
'/home/coachhomedata',
{},
{
headers: {
'X-User-Language': locale,
Authorization: `Bearer ${jwt}`
}
}
)
);
export const getRecentSessions = (locale: string, jwt: string, filter?: SessionsFilter): Promise<AxiosResponse<Session[]>> => (
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<AxiosResponse<Session>> => (
apiClient.post(
'/home/session',
{ id },
{
headers: {
'X-User-Language': locale,
Authorization: `Bearer ${jwt}`
}
}
)
);

119
src/actions/sessions.ts Normal file
View File

@ -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<AxiosResponse<Session[]>> => (
apiClient.post(
'/home/upcomingsessionsall',
{
sessionType: 'session',
...(filter || {})
},
{
headers: {
'X-User-Language': locale,
Authorization: `Bearer ${jwt}`
}
}
)
);
export const getRequestedSessions = (locale: string, jwt: string): Promise<AxiosResponse<{ requestedSessions: Session[] }>> => (
apiClient.post(
'/home/coachhomedata',
{},
{
headers: {
'X-User-Language': locale,
Authorization: `Bearer ${jwt}`
}
}
)
);
export const getRecentSessions = (locale: string, jwt: string, filter?: SessionsFilter): Promise<AxiosResponse<Session[]>> => (
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<AxiosResponse<Session>> => (
apiClient.post(
'/home/session',
{ id },
{
headers: {
'X-User-Language': locale,
Authorization: `Bearer ${jwt}`
}
}
)
);
export const approveRequestedSession = (locale: string, jwt: string, sessionId: number): Promise<AxiosResponse> => (
apiClient.post(
'/home/approverequestedsession',
{ sessionId },
{
headers: {
'X-User-Language': locale,
Authorization: `Bearer ${jwt}`
}
}
)
);
export const declineRequestedSession = (locale: string, jwt: string, { sessionId, reason }: DeclineSessionData): Promise<AxiosResponse> => (
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<AxiosResponse> => (
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<AxiosResponse> => (
apiClient.post(
'/home/session_comment',
data,
{
headers: {
'X-User-Language': locale,
Authorization: `Bearer ${jwt}`
}
}
)
);

View File

@ -1,18 +1,20 @@
'use client' 'use client'
import React, { useCallback, useEffect, useState } from 'react'; 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 { RightOutlined, PlusOutlined, LeftOutlined } from '@ant-design/icons';
import Image from 'next/image'; import Image from 'next/image';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { Link } from '../../../navigation'; import { Link } from '../../../navigation';
import { i18nText } from '../../../i18nKeys'; import { i18nText } from '../../../i18nKeys';
import { getDuration, getPrice } from '../../../utils/expert'; 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 { 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 { useLocalStorage } from '../../../hooks/useLocalStorage';
import { Loader } from '../../view/Loader'; import { Loader } from '../../view/Loader';
import { DeclineSessionModal } from '../../Modals/DeclineSessionModal';
import { AddCommentModal } from '../../Modals/AddCommentModal';
type SessionDetailsProps = { type SessionDetailsProps = {
locale: string; locale: string;
@ -25,10 +27,11 @@ export const SessionDetails = ({ sessionId, locale, goBack, activeTab }: Session
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [userId] = useLocalStorage(AUTH_USER, ''); const [userId] = useLocalStorage(AUTH_USER, '');
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [approveLoading, setApproveLoading] = useState<boolean>(false);
const [errorData, setErrorData] = useState<any>(); const [errorData, setErrorData] = useState<any>();
const [session, setSession] = useState<Session>(); const [session, setSession] = useState<Session>();
const [openDeclineModal, setOpenDeclineModal] = useState<boolean>(false);
console.log('activeTab', activeTab); const [openAddCommentModal, setOpenAddCommentModal] = useState<boolean>(false);
const fetchData = useCallback(() => { const fetchData = useCallback(() => {
setLoading(true); setLoading(true);
@ -37,7 +40,6 @@ export const SessionDetails = ({ sessionId, locale, goBack, activeTab }: Session
getSessionDetails(locale, jwt, sessionId) getSessionDetails(locale, jwt, sessionId)
.then(({ data }) => { .then(({ data }) => {
console.log(data);
setSession(data); setSession(data);
}) })
.catch((err) => { .catch((err) => {
@ -52,6 +54,25 @@ export const SessionDetails = ({ sessionId, locale, goBack, activeTab }: Session
fetchData(); fetchData();
}, [sessionId]); }, [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 startDate = session?.scheduledStartAtUtc ? dayjs(session?.scheduledStartAtUtc).locale(locale) : null;
const endDate = session?.scheduledEndAtUtc ? dayjs(session?.scheduledEndAtUtc).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; 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; ) : null;
const client = session?.clients?.length ? session?.clients[0] : 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 ( return (
<Loader <Loader
@ -148,16 +170,29 @@ export const SessionDetails = ({ sessionId, locale, goBack, activeTab }: Session
{Current} {Current}
{(activeTab === 0 || activeTab === 1) && ( {(activeTab === 0 || activeTab === 1) && (
<div className="card-detail__actions"> <div className="card-detail__actions">
{activeTab === 0 ? ( <Button
<> className="card-detail__apply"
<Button className="card-detail__apply">Start Session</Button> onClick={() => onApproveSession(activeTab)}
<Button className="card-detail__decline">Decline Session</Button> loading={approveLoading}
</> >
) : ( {activeTab === 0 ? 'Start Session' : 'Confirm Session'}
<> </Button>
<Button className="card-detail__apply">Confirm Session</Button> <Button
<Button className="card-detail__decline">Decline Session</Button> className="card-detail__decline"
</> onClick={() => setOpenDeclineModal(true)}
disabled={approveLoading}
>
Decline Session
</Button>
{session?.id && (
<DeclineSessionModal
open={openDeclineModal}
handleCancel={() => setOpenDeclineModal(false)}
activeTab={activeTab}
locale={locale}
sessionId={session.id}
success={goBack}
/>
)} )}
</div> </div>
)} )}
@ -196,28 +231,52 @@ export const SessionDetails = ({ sessionId, locale, goBack, activeTab }: Session
<div className="card-detail__comments"> <div className="card-detail__comments">
<div className="card-detail__comments_header"> <div className="card-detail__comments_header">
<div className="card-detail__comments_title"> <div className="card-detail__comments_title">
My Comments {session?.clientComments?.length === 0 && session?.coachComments?.length === 0 ? 'Comments' : 'My Comments'}
</div> </div>
{activeTab === 0 && ( {activeTab === 0 && (
<>
<Button <Button
className="card-detail__comments_add" className="card-detail__comments_add"
type="link" type="link"
iconPosition="end" iconPosition="end"
icon={<PlusOutlined style={{ fontSize: 18 }} />} icon={<PlusOutlined style={{ fontSize: 18 }} />}
onClick={() => setOpenAddCommentModal(true)}
> >
Add new Add new
</Button> </Button>
<AddCommentModal
open={openAddCommentModal}
handleCancel={() => setOpenAddCommentModal(false)}
locale={locale}
sessionId={sessionId}
refresh={fetchData}
/>
</>
)} )}
</div> </div>
<div className="card-detail__comments_item"> {(session?.clientComments?.length > 0 || session?.coachComments?.length > 0) ? (
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. <>
{(isCoach ? session?.coachComments : session?.clientComments)?.map(({ id, comment }) => (
<div key={`my_${id}`} className="card-detail__comments_item">
{comment}
</div> </div>
))}
{(isCoach ? session?.clientComments : session?.coachComments)?.length > 0 && (
<div className="card-detail__comments_title"> <div className="card-detail__comments_title">
Coach Comments {isCoach ? 'Client Comments' : 'Coach Comments'}
</div> </div>
<div className="card-detail__comments_item"> )}
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. {(isCoach ? session?.clientComments : session?.coachComments)?.map(({ id , comment }) => (
<div key={`oth_${id}`} className="card-detail__comments_item">
{comment}
</div> </div>
))}
</>
) : (
<>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
</>
)}
</div> </div>
</> </>
)} )}

View File

@ -12,7 +12,7 @@ import 'dayjs/locale/es';
import { Loader } from '../../view/Loader'; import { Loader } from '../../view/Loader';
import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY, AUTH_USER } from '../../../constants/common'; 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 { Session, Sessions, SessionType } from '../../../types/sessions';
import { i18nText } from '../../../i18nKeys'; import { i18nText } from '../../../i18nKeys';

View File

@ -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<AddCommentModalProps> = ({
open,
handleCancel,
locale,
sessionId,
refresh
}) => {
const [form] = Form.useForm<{ comment: string }>();
const [loading, setLoading] = useState<boolean>(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 (
<Modal
className="b-modal"
open={open}
title={undefined}
onOk={undefined}
onCancel={handleCancel}
afterClose={onAfterClose}
footer={false}
width={498}
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
>
<div className="b-modal__comment__content">
<Form form={form} style={{ width: '100%' }}>
<Form.Item
name="comment"
noStyle
rules={[
{
required: true,
message: 'Please input your comment'
}
]}
>
<Input.TextArea
className="b-textarea"
rows={4}
maxLength={1000}
placeholder="Describe the reason for the rejection"
/>
</Form.Item>
</Form>
<div className="b-modal__decline__button">
<Button
className="card-detail__apply"
onClick={onAddComment}
loading={loading}
>
Send
</Button>
</div>
</div>
</Modal>
);
};

View File

@ -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<DeclineModalProps> = ({
open,
handleCancel,
activeTab,
locale,
sessionId,
success
}) => {
const [form] = Form.useForm<{ reason: string }>();
const [loading, setLoading] = useState<boolean>(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 (
<Modal
className="b-modal"
open={open}
title={undefined}
onOk={undefined}
onCancel={handleCancel}
afterClose={onAfterClose}
footer={false}
width={498}
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
>
<div className="b-modal__decline__content">
<div className="b-modal__decline__logo">
<img className="" src="/images/decline-sign.svg" alt=""/>
</div>
<div className="b-modal__decline__title">
Enter a reason for cancelling the session
</div>
<Form form={form} style={{ width: '100%' }}>
<Form.Item
name="reason"
noStyle
rules={[
{
required: true,
message: 'Please input the reason'
}
]}
>
<Input.TextArea
className="b-textarea"
rows={1}
placeholder="Describe the reason for the rejection"
/>
</Form.Item>
</Form>
<div className="b-modal__decline__button">
<FilledButton
type="primary"
danger
onClick={onDecline}
loading={loading}
>
Decline
</FilledButton>
</div>
</div>
</Modal>
);
};

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Button } from 'antd'; import { Button } from 'antd';
export const FilledButton = (props: any) => ( export const FilledButton = (props: any) => (
<Button className="b-button__filled" {...props}> <Button className={`b-button__filled${props?.danger ? ' danger': ''}`} {...props}>
{props.children} {props.children}
</Button> </Button>
); );

View File

@ -16,9 +16,24 @@
} }
} }
.ant-modal-footer {
margin: 0 !important;
}
&__content { &__content {
p { p {
margin: 16px 0; 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;
} }

View File

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

View File

@ -1 +1,2 @@
@import "_details.scss"; @import "_details.scss";
@import "_decline-modal.scss";

View File

@ -5,6 +5,11 @@
border-radius: 8px !important; border-radius: 8px !important;
height: 54px !important; height: 54px !important;
box-shadow: 0px 2px 4px 0px rgba(102, 165, 173, 0.32) !important; box-shadow: 0px 2px 4px 0px rgba(102, 165, 173, 0.32) !important;
&.danger {
background: #D93E5C !important;
box-shadow: none !important;
}
} }
&__link { &__link {

View File

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

View File

@ -108,7 +108,7 @@ export type SessionsFilter = {
export type SessionComment = { export type SessionComment = {
id: number; id: number;
createdAtUtc: string; createdAtUtc: string;
comment?: string; comment: string;
author?: PublicUser; author?: PublicUser;
authorId?: number; authorId?: number;
sessionId?: number; sessionId?: number;
@ -150,3 +150,14 @@ export type Sessions = {
[SessionType.REQUESTED]?: Session[]; [SessionType.REQUESTED]?: Session[];
[SessionType.RECENT]?: Session[]; [SessionType.RECENT]?: Session[];
}; };
export type DeclineSessionData = {
sessionId: number;
reason: string;
};
export type SessionCommentData = {
createdAtUtc: string;
comment: string;
sessionId: number;
}

View File

@ -1,4 +1,3 @@
import { SearchData } from '../types/tags';
import { AdditionalFilter, Filter, GeneralFilter } from '../types/experts'; import { AdditionalFilter, Filter, GeneralFilter } from '../types/experts';
import { DEFAULT_PAGE } from '../constants/common'; import { DEFAULT_PAGE } from '../constants/common';