Merge branch 'feature/CFE-1_implemention_of_history_in_call_widget' into 'develop'

CFE-1 complete programing part of call history of callwidget just need to work...

See merge request witel-front-end/crm-app!29
This commit is contained in:
AmirHossein Mahmoodi
2023-10-25 10:11:16 +00:00
16 changed files with 505 additions and 4 deletions

View File

@@ -250,5 +250,15 @@
"delete_tooltip": "حذف",
"update_tooltip": "ویرایش",
"update_expert": "ویرایش کارشناس"
},
"CallHistory": {
"category_name": "موضوع",
"subcategory_name": "زیر موضوع",
"operator": "اپراتور",
"date": "تاریخ",
"call_history_of": "تاریخچه تماس های",
"call_history_not_found": "تاریخچه ای برای تماس دریافتی یافت نشد",
"call_history_error_fetching": "خطا در دریافت تاریخچه تماس دریافتی",
"show_more": "نمایش موارد بیشتر"
}
}

View File

@@ -0,0 +1,19 @@
import {render, screen} from "@testing-library/react";
import ErrorOrEmpty from "../../ErrorOrEmpty";
import MockAppWithProviders from "../../../../../../../../../../../mocks/AppWithProvider";
describe("ErrorOrEmpty Component From call history", () => {
describe("Rendering", () => {
it("error fetching history of call text rendered", () => {
render(<MockAppWithProviders><ErrorOrEmpty isErrorOrEmpty={"error"}/></MockAppWithProviders>);
const errorText = screen.queryByText('خطا در دریافت تاریخچه تماس دریافتی');
expect(errorText).toBeInTheDocument();
});
it("empty fetching history of call text rendered", () => {
render(<MockAppWithProviders><ErrorOrEmpty isErrorOrEmpty={"empty"}/></MockAppWithProviders>);
const emptyText = screen.queryByText('تاریخچه ای برای تماس دریافتی یافت نشد');
expect(emptyText).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,33 @@
import {useTranslations} from "next-intl";
import {Typography} from "@mui/material";
const ErrorOrEmpty = ({isErrorOrEmpty}) => {
const t = useTranslations();
return (
<>
{isErrorOrEmpty === "error" ?
<Typography variant="subtitle1" sx={{
color: "#837e7e",
fontWeight: "500",
letterSpacing: 1,
textAlign: "center",
mt: "50%"
}}>
{t("CallHistory.call_history_error_fetching")}
</Typography>
: isErrorOrEmpty === "empty" ?
<Typography variant="subtitle1" sx={{
color: "#837e7e",
fontWeight: "500",
letterSpacing: 1,
textAlign: "center",
mt: "50%"
}}>
{t("CallHistory.call_history_not_found")}
</Typography> : <></>
}
</>
)
}
export default ErrorOrEmpty

View File

@@ -0,0 +1,21 @@
import {render, screen} from "@testing-library/react";
import HistoryHeader from "../../HistoryHeader";
import MockAppWithProviders from "../../../../../../../../../../../mocks/AppWithProvider";
describe("HistoryHeader Component From call history", () => {
describe("Rendering", () => {
const tab = {
phone_number: "09111111111"
}
it("text of header rendered", () => {
render(<MockAppWithProviders><HistoryHeader tab={tab}/></MockAppWithProviders>);
const textHeader = screen.queryByText(/تاریخچه تماس های/i);
expect(textHeader).toBeInTheDocument();
});
it("caller number in header rendered", () => {
render(<MockAppWithProviders><HistoryHeader tab={tab}/></MockAppWithProviders>);
const callerNumber = screen.queryByText(/09111111111/i);
expect(callerNumber).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,25 @@
import {useTranslations} from "next-intl";
import {Stack, Typography} from "@mui/material";
const HistoryHeader = ({tab}) => {
const t = useTranslations();
return (
<Stack sx={{
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
py: 2,
backgroundColor: "primary.main",
overflow: "hidden"
}}>
<Typography variant="subtitle1" sx={{marginRight: 1, color: "#fff"}}>
.... {t("CallHistory.call_history_of")}:
</Typography>
<Typography variant="subtitle1" sx={{color: "#fff"}}>
{tab.phone_number} ....
</Typography>
</Stack>
)
}
export default HistoryHeader

View File

@@ -0,0 +1,22 @@
import {render, screen} from "@testing-library/react";
import MockAppWithProviders from "../../../../../../../../../../../../mocks/AppWithProvider";
import PreviousOperatorData from "../../PreviousOperatorData";
describe("PreviousOperatorData Component From call history", () => {
describe("Rendering", () => {
const historyItem = {
operator_name: "test name",
answer_date: "test date"
}
it("operator name rendered", () => {
render(<MockAppWithProviders><PreviousOperatorData historyItem={historyItem}/></MockAppWithProviders>);
const operatorName = screen.queryByText("test name");
expect(operatorName).toBeInTheDocument();
});
it("operator answer date rendered", () => {
render(<MockAppWithProviders><PreviousOperatorData historyItem={historyItem}/></MockAppWithProviders>);
const operatorAnswerDate = screen.queryByText("test date");
expect(operatorAnswerDate).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,28 @@
import {useTranslations} from "next-intl";
import {Avatar, Box, ListItem, ListItemAvatar, ListItemText, Typography} from "@mui/material";
import HeadsetMicIcon from '@mui/icons-material/HeadsetMic';
const PreviousOperatorData = ({historyItem}) => {
const t = useTranslations();
return (
<ListItem>
<ListItemAvatar>
<Avatar sx={{backgroundColor: "primary.light"}}>
<HeadsetMicIcon/>
</Avatar>
</ListItemAvatar>
<ListItemText primary={
<Box sx={{display: "flex", alignItems: "end"}}>
<Typography sx={{marginRight: 1, color: "#545151"}}>
{historyItem.operator_name}
</Typography>
<Typography variant="caption" sx={{color: "secondary.main"}}>
({t("CallHistory.operator")})
</Typography>
</Box>
} secondary={historyItem.answer_date}/>
</ListItem>
)
}
export default PreviousOperatorData

View File

@@ -0,0 +1,22 @@
import {render, screen} from "@testing-library/react";
import MockAppWithProviders from "../../../../../../../../../../../../mocks/AppWithProvider";
import Topics from "../../Topics";
describe("Topics Component From call history", () => {
describe("Rendering", () => {
const historyItem = {
category_name: "test category",
subcategory_name: "test sub category"
}
it("operator name rendered", () => {
render(<MockAppWithProviders><Topics historyItem={historyItem}/></MockAppWithProviders>);
const operatorName = screen.queryByText("test category");
expect(operatorName).toBeInTheDocument();
});
it("operator answer date rendered", () => {
render(<MockAppWithProviders><Topics historyItem={historyItem}/></MockAppWithProviders>);
const operatorAnswerDate = screen.queryByText("test sub category");
expect(operatorAnswerDate).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,43 @@
import {useTranslations} from "next-intl";
import {Box, List, ListItem, Typography} from "@mui/material";
const Topics = ({historyItem}) => {
const t = useTranslations();
return (
<Box timeout="auto" unmountonexit="true">
<List component="div" disablePadding>
<ListItem sx={{px: 4}}>
<Box sx={{width: "100%", display: "flex", justifyContent: "space-between"}}>
<Typography variant="subtitle2" sx={{color: "#837e7e"}}>
{t("CallHistory.category_name")}:
</Typography>
<Typography variant="subtitle2" sx={{color: "#837e7e"}}>
{historyItem.category_name}
</Typography>
</Box>
</ListItem>
</List>
<List component="div" disablePadding>
<ListItem sx={{px: 4}}>
<Box sx={{width: "100%", display: "flex", justifyContent: "space-between"}}>
<Typography variant="subtitle2" sx={{color: "#837e7e"}}>
{t("CallHistory.subcategory_name")}:
</Typography>
<Typography variant="subtitle2" sx={{
width: "200px",
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: "nowrap",
textAlign: "end",
color: "#837e7e"
}}>
{historyItem.subcategory_name}
</Typography>
</Box>
</ListItem>
</List>
</Box>
)
}
export default Topics

View File

@@ -0,0 +1,21 @@
import {useTranslations} from "next-intl";
import {Grow, List} from "@mui/material";
import PreviousOperatorData from "./PreviousOperatorData";
import Topics from "./Topics";
const ListItemOfCalls = ({historyItem}) => {
const t = useTranslations();
// console.log(historyItem)
return (
<Grow in={true}>
<List sx={{width: '100%', bgcolor: 'rgba(222,234,215,0.11)'}}>
<PreviousOperatorData historyItem={historyItem}/>
<Topics historyItem={historyItem}/>
</List>
</Grow>
)
}
export default ListItemOfCalls

View File

@@ -0,0 +1,68 @@
import {render, screen} from "@testing-library/react";
import LoadingHistory from "../../LoadingHistory";
import MockAppWithProviders from "../../../../../../../../../../../mocks/AppWithProvider";
describe("LoadingHistory Component From call history", () => {
describe("Rendering", () => {
it("loading list rendered", () => {
render(<MockAppWithProviders><LoadingHistory/></MockAppWithProviders>);
const loadingList = screen.queryByTestId("loading-list");
expect(loadingList).toBeInTheDocument();
});
it("loading list item avatar rendered", () => {
render(<MockAppWithProviders><LoadingHistory/></MockAppWithProviders>);
const loadingListItemAvatar = screen.queryByTestId("loading-listItemAvatar");
expect(loadingListItemAvatar).toBeInTheDocument();
});
it("loading list item text rendered", () => {
render(<MockAppWithProviders><LoadingHistory/></MockAppWithProviders>);
const loadingListItemText = screen.queryByTestId("loading-listItemText");
expect(loadingListItemText).toBeInTheDocument();
});
it("loading list item category rendered", () => {
render(<MockAppWithProviders><LoadingHistory/></MockAppWithProviders>);
const loadingListItemCat = screen.queryByTestId("loading-listItem-cat");
expect(loadingListItemCat).toBeInTheDocument();
});
it("loading list item sub category rendered", () => {
render(<MockAppWithProviders><LoadingHistory/></MockAppWithProviders>);
const loadingListItemSubCat = screen.queryByTestId("loading-listItem-subCat");
expect(loadingListItemSubCat).toBeInTheDocument();
});
it("operator avatar skeleton rendered", () => {
render(<MockAppWithProviders><LoadingHistory/></MockAppWithProviders>);
const operatorAvatarSkeleton = screen.queryByTestId("operator-avatar-skeleton");
expect(operatorAvatarSkeleton).toBeInTheDocument();
});
it("operator name skeleton rendered", () => {
render(<MockAppWithProviders><LoadingHistory/></MockAppWithProviders>);
const operatorNameSkeleton = screen.queryByTestId("operator-name-skeleton");
expect(operatorNameSkeleton).toBeInTheDocument();
});
it("operator answer date skeleton rendered", () => {
render(<MockAppWithProviders><LoadingHistory/></MockAppWithProviders>);
const operatorAnswerDateSkeleton = screen.queryByTestId("operator-answer-date-skeleton");
expect(operatorAnswerDateSkeleton).toBeInTheDocument();
});
it("operator category header skeleton rendered", () => {
render(<MockAppWithProviders><LoadingHistory/></MockAppWithProviders>);
const operatorCategoryHeaderSkeleton = screen.queryByTestId("operator-category-header-skeleton");
expect(operatorCategoryHeaderSkeleton).toBeInTheDocument();
});
it("operator category value skeleton rendered", () => {
render(<MockAppWithProviders><LoadingHistory/></MockAppWithProviders>);
const operatorCategoryValueSkeleton = screen.queryByTestId("operator-category-value-skeleton");
expect(operatorCategoryValueSkeleton).toBeInTheDocument();
});
it("operator subCategory header skeleton rendered", () => {
render(<MockAppWithProviders><LoadingHistory/></MockAppWithProviders>);
const operatorSubCategoryHeaderSkeleton = screen.queryByTestId("operator-subCategory-header-skeleton");
expect(operatorSubCategoryHeaderSkeleton).toBeInTheDocument();
});
it("operator subCategory value skeleton rendered", () => {
render(<MockAppWithProviders><LoadingHistory/></MockAppWithProviders>);
const operatorSubCategoryValueSkeleton = screen.queryByTestId("operator-subCategory-value-skeleton");
expect(operatorSubCategoryValueSkeleton).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,86 @@
import {useTranslations} from "next-intl";
import {Box, List, ListItem, ListItemAvatar, ListItemText, Skeleton} from "@mui/material";
const LoadingHistory = () => {
const t = useTranslations();
return (
<>
<List data-testid="loading-list" sx={{
width: '100%',
bgcolor: 'rgba(222,234,215,0.11)',
}}>
<Box sx={{
display: "flex",
alignItems: "center",
px: 2
}}>
<ListItemAvatar data-testid="loading-listItemAvatar">
<Skeleton data-testid="operator-avatar-skeleton"
animation="wave" variant="circular"
width={60} height={60}/>
</ListItemAvatar>
<ListItemText data-testid="loading-listItemText" primary={
<Box sx={{display: "flex", alignItems: "end"}}>
<Skeleton
data-testid="operator-name-skeleton"
animation="wave"
height={15}
width="50%"
style={{marginRight: 10, marginBottom: 5}}
/>
</Box>
} secondary={
<Skeleton
data-testid="operator-answer-date-skeleton"
animation="wave"
height={10}
width="30%"
style={{marginRight: 10}}
/>
}/>
</Box>
<Box timeout="auto" sx={{py: 2}}>
<List component="div" disablePadding>
<ListItem data-testid="loading-listItem-cat" sx={{px: 4}}>
<Box sx={{width: "100%", display: "flex", justifyContent: "space-between"}}>
<Skeleton
data-testid="operator-category-header-skeleton"
animation="wave"
height={15}
width="15%"
/>
<Skeleton
data-testid="operator-category-value-skeleton"
animation="wave"
height={15}
width="30%"
/>
</Box>
</ListItem>
</List>
<List component="div" disablePadding>
<ListItem data-testid="loading-listItem-subCat" sx={{px: 4}}>
<Box sx={{width: "100%", display: "flex", justifyContent: "space-between"}}>
<Skeleton
data-testid="operator-subCategory-header-skeleton"
animation="wave"
height={15}
width="25%"
/>
<Skeleton
data-testid="operator-subCategory-value-skeleton"
animation="wave"
height={15}
width="50%"
/>
</Box>
</ListItem>
</List>
</Box>
</List>
</>
)
}
export default LoadingHistory

View File

@@ -1,6 +1,64 @@
import {useTranslations} from "next-intl";
import {Box, Chip, Divider, Stack} from "@mui/material";
import HeadphonesIcon from '@mui/icons-material/Headphones';
import LoadingHistory from "./LoadingHistory";
import HistoryHeader from "./HistoryHeader";
import ErrorOrEmpty from "./ErrorOrEmpty";
import useCallerHistory from "@/lib/callWidget/hooks/useCallerHistory";
import ListItemOfCalls from "./ListItemOfCalls";
import {useState} from "react";
const max_size = 10;
const CallHistory = ({tab}) => {
const t = useTranslations();
const {
firstItemOfHistory,
historyList,
isLoadingHistoryList,
errorHistoryList
} = useCallerHistory(tab.id, tab.phone_number, max_size);
const [showRestOfHistory, setShowRestOfHistory] = useState(false);
return (
<>CallHistory - {tab.id}</>
<Stack sx={{
height: "100%",
overflow: "hidden"
}}>
<HistoryHeader tab={tab}/>
<Stack sx={{overflow: "scroll"}}>
{errorHistoryList ? (
<ErrorOrEmpty isErrorOrEmpty="error"/>
) : isLoadingHistoryList ? (
<LoadingHistory/>
) : !firstItemOfHistory ? (
<ErrorOrEmpty isErrorOrEmpty="empty"/>
) : (
<Box>
<ListItemOfCalls historyItem={firstItemOfHistory}/>
{historyList && (
<Divider>
<Chip
icon={<HeadphonesIcon/>}
onClick={() => setShowRestOfHistory(true)}
label={t("show_more")}
disabled={showRestOfHistory}
/>
</Divider>
)}
{showRestOfHistory && (
<>
{historyList.map((historyItem, index) => (
<>
<ListItemOfCalls key={historyItem.id} historyItem={historyItem}/>
<Divider/>
</>
))}
</>)
}
</Box>
)}
</Stack>
</Stack>
)
}

View File

@@ -1,15 +1,19 @@
import {Grid} from "@mui/material";
import {Divider, Grid} from "@mui/material";
import CallActions from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions";
import CallHistory from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory";
const CallTabPanel = ({tab}) => {
return (
<Grid container columns={{xs: 3}} sx={{display: !tab.active && 'none'}} role="tabpanel"
<Grid container columns={{xs: 3}} sx={{display: !tab.active && 'none', height: '100%', overflow: "hidden"}}
role="tabpanel"
id={`answer-tabpanel-${tab.id}`} aria-labelledby={`answer-tab-${tab.id}`}>
<Grid item xs={2}>
<CallActions tab={tab}/>
</Grid>
<Grid item xs={1}>
<Grid item>
<Divider orientation="vertical"/>
</Grid>
<Grid item xs sx={{height: "100%", overflow: "scroll"}}>
<CallHistory tab={tab}/>
</Grid>
</Grid>

View File

@@ -16,6 +16,10 @@ export const GET_ROLE_LIST = BASE_URL + "/api/roles/list";
export const GET_PROVINCE_LIST = BASE_URL + "/api/provinces";
// province list
// province list
export const GET_CALLER_HISTORY = BASE_URL + "/api/calls/list";
// province list
//change password
export const SET_USER_PASSWORD = BASE_URL + "/api/profile/change_password";
//end change password

View File

@@ -0,0 +1,37 @@
import useRequest from "@/lib/app/hooks/useRequest";
import {useEffect, useState} from "react";
import useSWR from "swr";
import {GET_CALLER_HISTORY} from "@/core/data/apiRoutes";
const useCallerHistory = (call_id, phone_number, max_size) => {
const requestServer = useRequest({auth: true, notification: false});
const [validData, setValidData] = useState([]);
//swr config
const fetcher = (...args) => {
return requestServer(args, 'get').then(({data}) => {
return data.data;
}).catch(() => {
})
};
const {data, isLoading} = useSWR(`${GET_CALLER_HISTORY}?phone_number=${phone_number}&size=${max_size}`, fetcher, {
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
keepPreviousData: true
});
useEffect(() => {
data && setValidData(data.filter((item) => item.id != call_id));
}, [data, call_id]);
return {
firstItemOfHistory: validData.length === 0 ? false : validData[0],
historyList: validData.length < 2 ? false : validData.slice(1),
isLoadingHistoryList: true,
errorHistoryList: false
}
};
export default useCallerHistory;