diff --git a/.gitignore b/.gitignore index 662e667..1e62a6d 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ cypress/screenshots .env* .env .idea -package-lock.json \ No newline at end of file +package-lock.json +ecosystem.config.js \ No newline at end of file diff --git a/ecosystem.config.js.example b/ecosystem.config.js.example new file mode 100644 index 0000000..6847796 --- /dev/null +++ b/ecosystem.config.js.example @@ -0,0 +1,18 @@ +module.exports = { + apps: [{ + name: "crm_app", + script: 'node_modules/next/dist/bin/next', // cluster mode run with node only, not npm + args: 'start -p 3003', + exec_mode: "cluster", // default fork + instances: "10", + kill_timeout: 4000, + wait_ready: true, + autorestart: true, + watch: false, + max_memory_restart: "1G", + log_date_format: "YYYY-MM-DD HH:mm Z", + env_prod: { + APP_ENV: 'prod' // APP_ENV=prod + } + }], +}; \ No newline at end of file diff --git a/example.env.local b/example.env.local index 922eec0..cc363e7 100644 --- a/example.env.local +++ b/example.env.local @@ -1,5 +1,5 @@ NEXT_PUBLIC_API_NAME = "CRM Dashboard" -NEXT_PUBLIC_API_VERSION = "0.0.1" +NEXT_PUBLIC_API_VERSION = "1.21.6" NEXT_PUBLIC_DEFAULT_LANGUAGE = "fa" NEXT_PUBLIC_DEFAULT_DIRECTION = "rtl" @@ -10,4 +10,8 @@ NEXT_PUBLIC_BASE_URL = "https://crm.witel.ir" NEXT_PUBLIC_SERVER_SOCKET_URL = "wss://crmws.witel.ir" NEXT_PUBLIC_POWERED_BY_URL = "https://witel.ir" -NODE_ENV = "development" \ No newline at end of file +#["NEXT_PUBLIC_HAS_WIDGET" , "NEXT_PUBLIC_HAS_NOTIFICATION"] +NEXT_PUBLIC_HAS_VALUE=["NEXT_PUBLIC_HAS_WIDGET"] + +NODE_ENV = "development" + diff --git a/mocks/handler.js b/mocks/handler.js index fbb803a..232c91e 100644 --- a/mocks/handler.js +++ b/mocks/handler.js @@ -1,13 +1,15 @@ -import {userHandler} from "./handlers/user"; -import {permissionsHandler} from "./handlers/permissions"; -import {rolesHandler} from "./handlers/roles"; -import {provincesHandler} from "./handlers/provinces"; -import {expertsHandler} from "./handlers/experts"; +import { userHandler } from "./handlers/user"; +import { permissionsHandler } from "./handlers/permissions"; +import { rolesHandler } from "./handlers/roles"; +import { provincesHandler } from "./handlers/provinces"; +import { expertsHandler } from "./handlers/experts"; +import { categoriesHandler } from "./handlers/categories"; export const handler = [ ...userHandler, ...permissionsHandler, ...rolesHandler, ...provincesHandler, - ...expertsHandler -] \ No newline at end of file + ...expertsHandler, + ...categoriesHandler, +]; diff --git a/mocks/handlers/categories.js b/mocks/handlers/categories.js new file mode 100644 index 0000000..6864331 --- /dev/null +++ b/mocks/handlers/categories.js @@ -0,0 +1,25 @@ +import { rest } from "msw"; +import { GET_CATEGORY } from "@/core/data/apiRoutes"; + +export const categoriesHandler = [ + rest.get(GET_CATEGORY, (req, res, ctx) => { + return res( + ctx.json({ + data: [ + { + category_id: 1, + category_name: "اطلاعات راه", + subcategory_id: 1, + subcategory_name: "اطلاعات باز و بسته", + }, + { + category_id: 3, + category_name: "سایر", + subcategory_id: 2, + subcategory_name: "مزاحم", + }, + ], + }) + ); + }), +]; diff --git a/mocks/handlers/user.js b/mocks/handlers/user.js index 3f1fbe3..7e84703 100644 --- a/mocks/handlers/user.js +++ b/mocks/handlers/user.js @@ -4,10 +4,14 @@ import {GET_SIDEBAR_NOTIFICATION, GET_USER, SET_USER_PASSWORD} from "@/core/data export const userHandler = [ rest.get(GET_USER, (req, res, ctx) => { return res(ctx.json({ - data: { + data:{ id: 10, - permissions: [ - "manage_users" + full_name: "Witel Company", + position: "Software Engineer", + permissions:[ + "manage_users", + "manage_roles", + "manage_boss" ], } })) diff --git a/public/locales/fa/app.json b/public/locales/fa/app.json index ae51967..7d89691 100644 --- a/public/locales/fa/app.json +++ b/public/locales/fa/app.json @@ -261,5 +261,13 @@ "call_history_not_found": "تاریخچه ای برای تماس دریافتی یافت نشد", "call_history_error_fetching": "خطا در دریافت تاریخچه تماس دریافتی", "show_more": "نمایش موارد بیشتر" + }, + "CallAction": { + "call_history_of": "عملیات های مربوط به تماس", + "categories" : "موضوع ها", + "subcategories" : "زیر موضوع ها", + "description" : "توضیحات تماس (اختیاری)", + "text_field_label" : "توضیحات", + "text_field_palacholder" : "لطفا توضیحات خود را وارد نمایید" } } diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/ActionHeader/__test__/index.test.js b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/ActionHeader/__test__/index.test.js new file mode 100644 index 0000000..69f9b52 --- /dev/null +++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/ActionHeader/__test__/index.test.js @@ -0,0 +1,43 @@ +import {render, screen, waitFor} from "@testing-library/react"; +import MockAppWithProviders from "../../../../../../../../../../../mocks/AppWithProvider"; +import {AnswersProvider} from "@/lib/callWidget/contexts/answers"; +import ActionHeader from ".."; + +const tab = { + active: true, + active_category_id: 1, + phone_number : "09134849737" +} + +describe('Action Header Button from Call Actions Categories', () => { + describe('Rendering', () => { + it('should see header text', async () => { + render( + + + + + + ) + const headingElement = screen.getByRole('heading', { + name: /\.\.\.\. عملیات های مربوط به تماس:/i + }) + await waitFor(() => { + expect(headingElement).toBeInTheDocument() + }) + }); + it('should see phone number', async () => { + render( + + + + + + ) + const phone_numberElement = screen.queryByText(/09134849737/i) + await waitFor(() => { + expect(phone_numberElement).toBeInTheDocument() + }) + }); + }); +}); \ No newline at end of file diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/ActionHeader/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/ActionHeader/index.jsx new file mode 100644 index 0000000..97b4297 --- /dev/null +++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/ActionHeader/index.jsx @@ -0,0 +1,25 @@ +import {useTranslations} from "next-intl"; +import {Stack, Typography} from "@mui/material"; + +const ActionHeader = ({tab}) => { + const t = useTranslations(); + return ( + + + .... {t("CallAction.call_history_of")}: + + + {tab.phone_number} .... + + + ) +} + +export default ActionHeader \ No newline at end of file diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/CallActionCategoriesButton.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/CallActionCategoriesButton.jsx new file mode 100644 index 0000000..40c2b18 --- /dev/null +++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/CallActionCategoriesButton.jsx @@ -0,0 +1,25 @@ +import { Button, Grid } from "@mui/material"; +import useCategories from "@/lib/callWidget/hooks/useCategories"; + +const CallActionCategoriesButton = ({ category, tab }) => { + const { setActiveCategoryID } = useCategories(); + + return ( + + + + + + ); +}; +export default CallActionCategoriesButton; diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/__test__/CallActionCategoriesButton.test.js b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/__test__/CallActionCategoriesButton.test.js new file mode 100644 index 0000000..195df02 --- /dev/null +++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/__test__/CallActionCategoriesButton.test.js @@ -0,0 +1,51 @@ +import {fireEvent, render, screen, waitFor} from "@testing-library/react"; +import MockAppWithProviders from "../../../../../../../../../../../mocks/AppWithProvider"; +import {AnswersProvider} from "@/lib/callWidget/contexts/answers"; +import CallActionCategoriesButton from "../CallActionCategoriesButton"; + +const category = { + category_id: 1, + category_name: "اطلاعات راه ها", +} +const tab = { + active: true, + active_category_id: 1, +} + +describe('Call Action Categories Button from Call Actions Categories', () => { + describe('Rendering', () => { + it('should categories names', async () => { + render( + + + + + + ) + const categoryElement = screen.queryByText("اطلاعات راه ها") + await waitFor(() => { + expect(categoryElement).toBeInTheDocument() + }) + }); + }); + describe("Behavior", ()=>{ + it("Should change the color when button clecked",async()=>{ + render( + + + + + + ) + const buttonElement = screen.getByRole('button', { + name: /اطلاعات راه ها/i + }) + + fireEvent.click(buttonElement); + + await waitFor(() => { + expect(buttonElement).toHaveStyle('background-color: rgb(12, 31, 23)'); + }) + }) + }) +}) \ No newline at end of file diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/__test__/index.test.js b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/__test__/index.test.js new file mode 100644 index 0000000..ee223ff --- /dev/null +++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/__test__/index.test.js @@ -0,0 +1,29 @@ +import { AnswersProvider } from "@/lib/callWidget/contexts/answers"; +import CallActionsCategories from ".."; +import MockAppWithProviders from "../../../../../../../../../../../mocks/AppWithProvider"; +import { render, screen, waitFor } from "@testing-library/react"; + +const tab = { + active: true, + active_category_id: 1, + id : 1, + phone_number : "09134849737" +} + +describe('Call Action Categories Button from Call Actions Categories', () => { + describe('Rendering', () => { + it('should see divider text', async () => { + render( + + + + + + ) + const divider_textElement = screen.queryByText("موضوع ها") + await waitFor(() => { + expect(divider_textElement).toBeInTheDocument() + }) + }); + }); +}) \ No newline at end of file diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/index.jsx new file mode 100644 index 0000000..dc814ec --- /dev/null +++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/index.jsx @@ -0,0 +1,22 @@ +import { Chip, Divider, Grid, Stack } from "@mui/material"; +import useCategories from "@/lib/callWidget/hooks/useCategories"; +import CallActionCategoriesButton from "./CallActionCategoriesButton"; +import { useTranslations } from "next-intl"; + +const CallActionsCategories = ({ tab }) => { + const { categoryLists } = useCategories(); + const t = useTranslations(); + return ( + <> + + + + {categoryLists.map((category) => { + return ; + })} + + + + ); +}; +export default CallActionsCategories; diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionDescription/__test__/index.test.js b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionDescription/__test__/index.test.js new file mode 100644 index 0000000..b7e0543 --- /dev/null +++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionDescription/__test__/index.test.js @@ -0,0 +1,54 @@ +import {fireEvent, render, screen, waitFor} from "@testing-library/react"; +import MockAppWithProviders from "../../../../../../../../../../../mocks/AppWithProvider"; +import {AnswersProvider} from "@/lib/callWidget/contexts/answers"; +import CallActionDescription from ".."; + +describe('Action Header Button from Call Actions Categories', () => { + describe('Rendering', () => { + it('should see placeholder text', async () => { + render( + + + + + + ) + const textElement = screen.getByPlaceholderText('لطفا توضیحات خود را وارد نمایید') + await waitFor(() => { + expect(textElement).toBeInTheDocument() + }) + }); + it('should see header text', async () => { + render( + + + + + + ) + const textElement = screen.getByText('توضیحات تماس (اختیاری)') + await waitFor(() => { + expect(textElement).toBeInTheDocument() + }) + }); + }); + describe("Behavior", ()=>{ + it("Should see what write in input", async()=>{ + const setDescription = jest.fn(); + render( + + + + + + ) + const textElement = screen.getByPlaceholderText('لطفا توضیحات خود را وارد نمایید') + + fireEvent.change(textElement, {target : {value : "امین"}}) + + await waitFor(() => { + expect(textElement).toHaveValue("امین") + }) + }) + }) +}); \ No newline at end of file diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionDescription/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionDescription/index.jsx new file mode 100644 index 0000000..6a29324 --- /dev/null +++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionDescription/index.jsx @@ -0,0 +1,27 @@ +import { Box, Chip, Divider, TextField } from "@mui/material" +import { useTranslations } from "next-intl"; + +const CallActionDescription = ({setDescription}) => { + const t = useTranslations(); + return( + <> + + + { + setDescription(e.target.value) + }} + /> + + + ) +} +export default CallActionDescription \ No newline at end of file diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/CallActionSubcategoriesButton.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/CallActionSubcategoriesButton.jsx new file mode 100644 index 0000000..2b4a479 --- /dev/null +++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/CallActionSubcategoriesButton.jsx @@ -0,0 +1,14 @@ +import { Button, Grid } from "@mui/material"; + +const CallActionSubcategoriesButton = ({ sub_category, tab, handleSubcategoryButton }) => { + + return sub_category.category_id === tab.active_category_id ? ( + + + + ) : null; +}; +export default CallActionSubcategoriesButton; diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/__test__/CallActionSubcategoriesButton.test.js b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/__test__/CallActionSubcategoriesButton.test.js new file mode 100644 index 0000000..718f7cd --- /dev/null +++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/__test__/CallActionSubcategoriesButton.test.js @@ -0,0 +1,37 @@ +import {render, screen, waitFor} from "@testing-library/react"; +import MockAppWithProviders from "../../../../../../../../../../../mocks/AppWithProvider"; +import {AnswersProvider} from "@/lib/callWidget/contexts/answers"; +import CallActionSubcategoriesButton from "../CallActionSubcategoriesButton"; + +const sub_category = { + category_id: 1, + category_name: "اطلاعات راه", + subcategory_id: 1, + subcategory_name: "اطلاعات باز و بسته" +}; + +const tab = { + active: true, + active_category_id: 1, + date: new Date(), + id: 1, + phone_number: "09134849737" +}; + +describe('Call Action Subcategories Button from Call Actions Categories', () => { + describe('Rendering', () => { + it('should render subcategory name', async () => { + render( + + + + + + ); + const subCategoryElement = screen.queryByText(/اطلاعات باز و بسته/i); + await waitFor(() => { + expect(subCategoryElement).toBeInTheDocument(); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/__test__/index.test.js b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/__test__/index.test.js new file mode 100644 index 0000000..2195bde --- /dev/null +++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/__test__/index.test.js @@ -0,0 +1,29 @@ +import { AnswersProvider } from "@/lib/callWidget/contexts/answers"; +import CallActionsSubcategories from ".."; +import MockAppWithProviders from "../../../../../../../../../../../mocks/AppWithProvider"; +import { render, screen, waitFor } from "@testing-library/react"; + +const tab = { + active: true, + active_category_id: 1, + id : 1, + phone_number : "09134849737" +} + +describe('Call Action Categories Button from Call Actions Categories', () => { + describe('Rendering', () => { + it('should see divider text', async () => { + render( + + + + + + ) + const divider_textElement = screen.queryByText("زیر موضوع ها") + await waitFor(() => { + expect(divider_textElement).toBeInTheDocument() + }) + }); + }); +}) \ No newline at end of file diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/index.jsx new file mode 100644 index 0000000..0b88cfa --- /dev/null +++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/index.jsx @@ -0,0 +1,22 @@ +import useCategories from "@/lib/callWidget/hooks/useCategories"; +import { Chip, Divider, Grid, Stack } from "@mui/material"; +import CallActionSubcategoriesButton from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/CallActionSubcategoriesButton"; +import { useTranslations } from "next-intl"; + +const CallActionsSubcategories = ({ tab, handleSubcategoryButton }) => { + const { subCategoryLists } = useCategories(); + const t = useTranslations(); + return ( + <> + + + + {subCategoryLists.map((sub_category, index) => { + return ; + })} + + + + ); +}; +export default CallActionsSubcategories; diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/index.jsx index d1a68f7..082d701 100644 --- a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/index.jsx +++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/index.jsx @@ -1,7 +1,50 @@ -const CallActions = ({tab}) => { +import CallActionsCategories from "./CallActionCategories"; +import CallActionsSubcategories from "./CallActionSubcategories"; +import ActionHeader from "./ActionHeader"; +import CallActionDescription from "./CallActionDescription"; +import {CALL_ACTION} from "@/core/data/apiRoutes"; +import useRequest from "@/lib/app/hooks/useRequest"; +import useAnswers from "@/lib/callWidget/hooks/useAnswers"; +import {useState} from "react"; +import {Stack} from "@mui/material"; + +function CallActions({tab}) { + const [description, setDescription] = useState('') + const requestServer = useRequest({auth: true}); + const {closeAnswerTab} = useAnswers(); + + const handleSubcategoryButton = (category_id, subcategory_id) => { + requestServer(`${CALL_ACTION}/${tab.id}`, "post", { + data: { + category_id, + subcategory_id, + description + }, + success: { + notification: {show: false}, + }, + }) + .then(() => { + closeAnswerTab(tab.id); + }) + .catch(() => { + }); + }; + + return ( - <>CallActions - {tab.id} - ) + + + + + + + + + ); } -export default CallActions +export default CallActions; diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/index.jsx index c83bc49..1e940cb 100644 --- a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/index.jsx +++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/index.jsx @@ -25,7 +25,7 @@ const CallHistory = ({tab}) => { overflow: "hidden" }}> - + {errorHistoryList ? ( ) : isLoadingHistoryList ? ( diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/index.jsx index 8fc1d87..be7e9d2 100644 --- a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/index.jsx +++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/index.jsx @@ -7,7 +7,7 @@ const CallTabPanel = ({tab}) => { - + diff --git a/src/components/layouts/Dashboard/Sidebar/SidebarDrawer.jsx b/src/components/layouts/Dashboard/Sidebar/SidebarDrawer.jsx index 33348a5..3f1d449 100644 --- a/src/components/layouts/Dashboard/Sidebar/SidebarDrawer.jsx +++ b/src/components/layouts/Dashboard/Sidebar/SidebarDrawer.jsx @@ -4,7 +4,7 @@ import SidebarList from "./SidebarList"; import useUser from "@/lib/app/hooks/useUser"; const SidebarDrawer = ({handleDrawerToggle}) => { - const {user} = useUser() + const {user} = useUser(); const t = useTranslations(); return ( <> diff --git a/src/components/layouts/Dashboard/Sidebar/SidebarList.jsx b/src/components/layouts/Dashboard/Sidebar/SidebarList.jsx index 040b455..1e570f5 100644 --- a/src/components/layouts/Dashboard/Sidebar/SidebarList.jsx +++ b/src/components/layouts/Dashboard/Sidebar/SidebarList.jsx @@ -10,29 +10,15 @@ function reducer(state, action) { case "COLLAPSE_MENU": return state.map((itemsArr) => itemsArr.map((item) => - action.key == item.key - ? {...item, showSubItem: !item.showSubItem} - : item + action.key === item.key ? { ...item, showSubItem: !item.showSubItem } : item ) ); case "SELECTED": return state.map((itemsArr) => - itemsArr.map((item) => { - if (item.type === "page") { - if (action.route === item.route) - return {...item, selected: true} - else - return {...item, selected: false} - } - return {...item} - // item.subItem.map((subitem) => { - // if (action.route === subitem.route) - // return {...item, showSubItem: true, subItem: {...subitem, selected: true}} - // else - // return {...item, showSubItem: false, subItem: {...subitem, selected: false}} - // }) - } - ) + itemsArr.map((item) => ({ + ...item, + selected: item.type === "page" && action.route === item.route, + })) ); default: throw new Error(); @@ -54,7 +40,7 @@ export default function SidebarList({handleDrawerToggle}) { {itemArr.map((item) => - {(user.permissions.includes(item.permission) || item.permission === "all") && + {(user?.permissions?.includes(item.permission) || item.permission === "all") && { const t = useTranslations(); - const {notification_count} = useNotification() + const { notification_count } = useNotification(); + + const hasSubItems = item.type === "menu" && item.subItem && item.subItem.length > 0; + + const renderBadge = () => ( + + + + ); return ( <> - - - - }> + { }, })} onClick={() => { - if (item.type == "menu") { - dispatch({type: "COLLAPSE_MENU", key: item.key}); + if (hasSubItems) { + dispatch({ type: "COLLAPSE_MENU", key: item.key }); } - handleDrawerToggle(); }} sx={{ minHeight: 48, @@ -66,9 +69,7 @@ const SidebarListItem = ({item, dispatch, handleDrawerToggle}) => { } /> - - {item.type == "menu" && - (item.showSubItem ? : )} + {hasSubItems && (item.showSubItem ? : )} {item.subItem && ( diff --git a/src/components/layouts/Dashboard/Sidebar/SidebarListSubItem.jsx b/src/components/layouts/Dashboard/Sidebar/SidebarListSubItem.jsx index c9c8a36..1803b96 100644 --- a/src/components/layouts/Dashboard/Sidebar/SidebarListSubItem.jsx +++ b/src/components/layouts/Dashboard/Sidebar/SidebarListSubItem.jsx @@ -15,25 +15,27 @@ import useNotification from "@/lib/app/hooks/useNotification"; const SidebarListSubItem = ({item, handleDrawerToggle}) => { const t = useTranslations(); - const {notification_count} = useNotification() + const {notification_count} = useNotification(); + + const renderBadge = (subitem) => ( + + + + ); return ( - {item.subItem.map((subitem, index) => ( - - - - }> + {item.subItem.map((subitem) => ( + { + describe('Rendering', () => { + it('renders system name in sidebar', async () => { + render(); + await waitFor(()=>{ + const systemName = screen.getByRole('heading', { level: 6 }); + expect(systemName).toHaveTextContent('سامانه CRM'); + }) + }); + it('renders user full name and position in sidebar', async () => { + localStorage.setItem("_token", 'token'); + render(); + await waitFor(()=>{ + const userName = screen.queryByText(/Witel Company | Software Engineer/i); + expect(userName).toBeInTheDocument(); + }) + }); + }) +}) diff --git a/src/components/layouts/Dashboard/Sidebar/__test__/SidebarList.test.js b/src/components/layouts/Dashboard/Sidebar/__test__/SidebarList.test.js new file mode 100644 index 0000000..1897c9b --- /dev/null +++ b/src/components/layouts/Dashboard/Sidebar/__test__/SidebarList.test.js @@ -0,0 +1,16 @@ +import {render, screen, waitFor} from "@testing-library/react"; +import MockAppWithProviders from "../../../../../../mocks/AppWithProvider"; +import SidebarList from "@/components/layouts/Dashboard/Sidebar/SidebarList"; + +describe('SidebarList', () => { + describe('Rendering', () => { + it('renders sidebar items based on user permissions', async () => { + localStorage.setItem("_token", 'token'); + render(); + await waitFor(()=>{ + const sidebarItem = screen.getByText('مدیریت نقش ها'); + expect(sidebarItem).toBeInTheDocument(); + }) + }); + }) +}) diff --git a/src/components/layouts/Dashboard/Sidebar/__test__/SidebarListItem.test.js b/src/components/layouts/Dashboard/Sidebar/__test__/SidebarListItem.test.js new file mode 100644 index 0000000..18313c5 --- /dev/null +++ b/src/components/layouts/Dashboard/Sidebar/__test__/SidebarListItem.test.js @@ -0,0 +1,125 @@ +import AccessibilityIcon from "@mui/icons-material/Accessibility"; +import {fireEvent, render, screen} from "@testing-library/react"; +import SidebarListItem from "@/components/layouts/Dashboard/Sidebar/SidebarListItem"; +import MockAppWithProviders from "../../../../../../mocks/AppWithProvider"; + +describe('SidebarListItemComponent', () => { + describe('Rendering', () => { + it('should show sidebarlistitem name in sidebar', () => { + const sampleItem={ + key: "sidebar.role-management", + name : "role_management", + type: "page", + route: "/dashboard/role-management", + icon: , + selected: false, + permission: "manage_roles", + } + render(); + expect(screen.getByText('مدیریت نقش ها')).toBeInTheDocument(); + }); + it('should show sidebarlistitem icon in sidebar', () => { + const sampleItem={ + key: "sidebar.role-management", + name : "role_management", + type: "page", + route: "/dashboard/role-management", + icon: , + selected: false, + permission: "manage_roles", + } + render(); + const iconElement = screen.getByLabelText("Sidebar Icon"); + expect(iconElement).toBeInTheDocument(); + }); + it('should have selected class for subitem', () => { + const sampleItem={ + key: "sidebar.role-management", + name : "role_management", + type: "page", + route: "/dashboard/role-management", + icon: , + selected: true, + permission: "manage_roles", + } + render(); + const element = screen.getByText('مدیریت نقش ها'); + expect(element.parentElement.parentElement).toHaveClass('Mui-selected'); + }); + + it('should not have selected class for subitem', () => { + const sampleItem={ + key: "sidebar.role-management", + name : "role_management", + type: "menu", + route: "/dashboard/role-management", + icon: , + selected: false, + permission: "manage_roles", + } + render(); + const element = screen.getByText('مدیریت نقش ها'); + expect(element.parentElement.parentElement).not.toHaveClass('Mui-selected'); + }); + + it('should show sidebarlistitem secondary in sidebar', () => { + const sampleItem={ + key: "sidebar.role-management", + name : "role_management", + secondary: "between", + type: "menu", + route: "/dashboard/role-management", + icon: , + selected: false, + permission: "manage_roles", + } + render(); + expect(screen.getByText('میان')).toBeInTheDocument(); + }); + + it('should not show sidebarlistitem secondary in sidebar', () => { + const sampleItem={ + key: "sidebar.role-management", + name : "role_management", + type: "menu", + route: "/dashboard/role-management", + icon: , + selected: false, + permission: "manage_roles", + } + render(); + expect(screen.queryByText('secondry')).not.toBeInTheDocument(); + }); + }) + describe('Behavior', () => { + it('should call dispatch when hasSubItems is true and item is clicked', () => { + const dispatchMock = jest.fn(); + const sampleItem={ + key: "sidebar.role-management", + name : "role_management", + type: "menu", + route: "/dashboard/role-management", + icon: , + selected: false, + permission: "manage_roles", + subItem :[ + { + key: "greaterThan", + route: "/dashboard/role-management", + secondary: "between", + name : "sidebar.admin", + selected: true, + icon: , + } + ] + } + render(); + const listItem = screen.getByText('مدیریت نقش ها'); + fireEvent.click(listItem); + expect(dispatchMock).toHaveBeenCalledWith({ + type: 'COLLAPSE_MENU', + key: "sidebar.role-management", + }); + }); + }) +}) \ No newline at end of file diff --git a/src/components/layouts/Dashboard/Sidebar/__test__/SidebarListSubitem.test.js b/src/components/layouts/Dashboard/Sidebar/__test__/SidebarListSubitem.test.js new file mode 100644 index 0000000..9551937 --- /dev/null +++ b/src/components/layouts/Dashboard/Sidebar/__test__/SidebarListSubitem.test.js @@ -0,0 +1,103 @@ +import {render, screen} from "@testing-library/react"; +import MockAppWithProviders from "../../../../../../mocks/AppWithProvider"; +import SidebarListSubItem from "@/components/layouts/Dashboard/Sidebar/SidebarListSubItem"; +import AccessibilityIcon from "@mui/icons-material/Accessibility"; + +describe('SidebarListSubItemComponent', () => { + describe('Rendering', () => { + const sampleItem={ + showSubItem:true, + key: "sidebar.role-management", + name : "role_management", + type: "menu", + route: "/dashboard/role-management", + icon: , + selected: false, + permission: "manage_roles", + subItem :[ + { + key: "greaterThan", + route: "/dashboard/role-management", + secondary: "between", + name : "sidebar.admin", + selected: true, + icon: , + } + ] + } + it('should show subitem name in sidebar', () => { + render(); + expect(screen.getByText('بزرگتر از')).toBeInTheDocument(); + + }); + it('should not show subitem name in sidebar', () => { + const sampleItem={ + showSubItem:false, + key: "sidebar.role-management", + name : "role_management", + type: "menu", + route: "/dashboard/role-management", + icon: , + selected: false, + permission: "manage_roles", + subItem :[ + { + key: "greaterThan", + route: "/dashboard/role-management", + secondary: "between", + name : "sidebar.admin", + selected: false, + icon: , + } + ] + } + render(); + expect(screen.queryByText('مدیریت نقش ها')).not.toBeInTheDocument(); + }); + it('should show subitem secondary in sidebar', () => { + render(); + expect(screen.getByText('میان')).toBeInTheDocument(); + }); + + it('should not show subitem secondary in sidebar', () => { + render(); + expect(screen.queryByText('secondry')).not.toBeInTheDocument(); + }); + it('should have selected class for subitem', () => { + render(); + const parentElement = screen.queryByText('بزرگتر از').parentElement.parentElement; + expect(parentElement).toHaveClass('Mui-selected'); + }); + + it('should not have selected class for subitem', () => { + const sampleItem={ + showSubItem:true, + key: "sidebar.role-management", + name : "role_management", + type: "menu", + route: "/dashboard/role-management", + icon: , + selected: false, + permission: "manage_roles", + subItem :[ + { + key: "greaterThan", + route: "/dashboard/role-management", + secondary: "between", + name : "sidebar.admin", + selected: false, + icon: , + } + ] + } + render(); + const parentElement = screen.queryByText('بزرگتر از').parentElement.parentElement; + expect(parentElement).not.toHaveClass('Mui-selected'); + }); + it('should show subitem icon', () => { + render(); + const iconElement = screen.getByLabelText("Sidebar Icon"); + expect(iconElement).toBeInTheDocument(); + }); + }) +}) \ No newline at end of file diff --git a/src/components/layouts/Dashboard/Sidebar/__test__/index.test.js b/src/components/layouts/Dashboard/Sidebar/__test__/index.test.js new file mode 100644 index 0000000..740c384 --- /dev/null +++ b/src/components/layouts/Dashboard/Sidebar/__test__/index.test.js @@ -0,0 +1,17 @@ +import {render, screen, act} from "@testing-library/react"; +import MockAppWithProviders from "../../../../../../mocks/AppWithProvider"; +import Sidebar from "@/components/layouts/Dashboard/Sidebar"; + + +describe('Sidebar', () => { + describe('Rendering', () => { + it('Mobile Drawer is displayed when mobileOpen is true', () => { + const props={ + mobileOpen: true, + }; + render(); + const mobileDrawer = screen.getByTestId('mobile-drawer'); + expect(mobileDrawer).toBeVisible(); + }); + }) +}) diff --git a/src/components/layouts/Dashboard/Sidebar/index.jsx b/src/components/layouts/Dashboard/Sidebar/index.jsx index 2498dfd..c0b6d6d 100644 --- a/src/components/layouts/Dashboard/Sidebar/index.jsx +++ b/src/components/layouts/Dashboard/Sidebar/index.jsx @@ -9,6 +9,7 @@ const Sidebar = (props) => { aria-label="mailbox folders" > { - setUpdateTime(moment().format("HH:mm | jYYYY/jM/jD")); + setUpdateTime(moment().format("HH:mm | jYYYY/jMM/jDD")); }, [isValidating, languageApp]); return ( diff --git a/src/core/data/apiRoutes.js b/src/core/data/apiRoutes.js index 127d1df..f949dac 100644 --- a/src/core/data/apiRoutes.js +++ b/src/core/data/apiRoutes.js @@ -29,21 +29,16 @@ export const GET_USER = BASE_URL + "/api/profile/info"; //user data //sidebar notification -export const GET_SIDEBAR_NOTIFICATION = BASE_URL + "/dashboard/notification" +export const GET_SIDEBAR_NOTIFICATION = BASE_URL + "/dashboard/notification"; //sidebar notification // role management -export const GET_ROLES = - BASE_URL + "/api/roles" -export const ADD_ROLE = - BASE_URL + "/api/roles" -export const UPDATE_ROLE = - BASE_URL + "/api/roles" -export const DELETE_ROLE = - BASE_URL + "/api/roles" +export const GET_ROLES = BASE_URL + "/api/roles"; +export const ADD_ROLE = BASE_URL + "/api/roles"; +export const UPDATE_ROLE = BASE_URL + "/api/roles"; +export const DELETE_ROLE = BASE_URL + "/api/roles"; -export const GET_PERMISSIONS_LIST = - BASE_URL + "/api/permissions/list" +export const GET_PERMISSIONS_LIST = BASE_URL + "/api/permissions/list"; //role management //expert management @@ -51,4 +46,9 @@ export const GET_EXPERTS = BASE_URL + "/api/users"; export const ADD_EXPERT = BASE_URL + "/api/users"; export const UPDATE_EXPERT = BASE_URL + "/api/users"; export const DELETE_EXPERT = BASE_URL + "/api/users"; -//expert management \ No newline at end of file +//expert management + +//call action history +export const GET_CATEGORY = BASE_URL + "/api/categories"; +export const CALL_ACTION = BASE_URL + "/api/calls"; +//call action history diff --git a/src/layouts/DashboardLayout.jsx b/src/layouts/DashboardLayout.jsx index 37554cd..72344d6 100644 --- a/src/layouts/DashboardLayout.jsx +++ b/src/layouts/DashboardLayout.jsx @@ -1,11 +1,15 @@ import Dashboard from "src/components/layouts/Dashboard"; +import AppPermission from "@/middlewares/AppPermission"; +import CallWidget from "@/components/layouts/Dashboard/CallWidget"; const DashboardLayout = (props) => { - + const permissions = ["NEXT_PUBLIC_HAS_WIDGET"]; return ( <> - {/**/} + + + ); }; diff --git a/src/lib/app/contexts/socket.jsx b/src/lib/app/contexts/socket.jsx index 68826e1..363faca 100644 --- a/src/lib/app/contexts/socket.jsx +++ b/src/lib/app/contexts/socket.jsx @@ -17,6 +17,7 @@ export const SocketProvider = ({children}) => { const {directionApp} = useDirection(); const socket = useMemo(() => io(process.env.NEXT_PUBLIC_SERVER_SOCKET_URL, { autoConnect: false, + transports: ['websocket'], auth: { token: "" } diff --git a/src/lib/app/hooks/useNotification.jsx b/src/lib/app/hooks/useNotification.jsx index e1cb932..6e80bca 100644 --- a/src/lib/app/hooks/useNotification.jsx +++ b/src/lib/app/hooks/useNotification.jsx @@ -1,8 +1,12 @@ import useSWR from 'swr' import {GET_SIDEBAR_NOTIFICATION} from "@/core/data/apiRoutes"; import useRequest from "@/lib/app/hooks/useRequest"; +const GLOBAL_HAS_VALUE = process.env.NEXT_PUBLIC_HAS_VALUE; +const hasPermissionsValue = GLOBAL_HAS_VALUE ? JSON.parse(GLOBAL_HAS_VALUE) : []; +const isNotificationEnabled = hasPermissionsValue.includes("NEXT_PUBLIC_HAS_NOTIFICATION"); const useNotification = () => { + const requestServer = useRequest({auth: true, notification: false}) //swr config @@ -13,7 +17,7 @@ const useNotification = () => { }) }; - const {data, mutate} = useSWR(GET_SIDEBAR_NOTIFICATION, fetcher) + const {data, mutate} = useSWR( isNotificationEnabled ? GET_SIDEBAR_NOTIFICATION : '', fetcher , {keepPreviousData:true}) //swr config // render data diff --git a/src/lib/callWidget/contexts/answers.jsx b/src/lib/callWidget/contexts/answers.jsx index b5c9c0b..96a150c 100644 --- a/src/lib/callWidget/contexts/answers.jsx +++ b/src/lib/callWidget/contexts/answers.jsx @@ -1,31 +1,86 @@ -import {createContext, useReducer, useState} from "react"; +import { createContext, useEffect, useReducer, useState } from "react"; +import { GET_CATEGORY } from "@/core/data/apiRoutes"; +import useRequest from "@/lib/app/hooks/useRequest"; const reducer = (state, action) => { switch (action.type) { case "CHANGE_ACTIVE_TAB": - return state.map((answer, index) => action.newValue === index ? {...answer, active: true} : { - ...answer, - active: false - }); + return state.map((answer, index) => + action.newValue === index + ? { ...answer, active: true } + : { + ...answer, + active: false, + } + ); case "CHANGE_LIST": - return [...action.newList] + return [...action.newList]; + case "CHANGE_ACTIVE_ID": + return state.map((answer, index) => + action.tab_id === answer.id + ? { + ...answer, + active_category_id: action.category_id, + } + : { ...answer } + ); default: return state; } -} -export const AnswersContext = createContext() -export const AnswersProvider = ({children}) => { - const [openCallDialog, setOpenCallDialog] = useState(false) - const [activeTab, setActiveTab] = useState(0) +}; + +const submission = (handleCategories, operation) => { + switch (operation.type) { + case "SET_CATEGORIES": + return { ...handleCategories, categoryLists: operation.categoriesByAPI }; + case "SET_SUBCATEGORIES": + return { ...handleCategories, subCategoryLists: operation.subCategoriesByAPI }; + default: + return handleCategories; + } +}; + +export const AnswersContext = createContext(); +export const AnswersProvider = ({ children }) => { + const requestServer = useRequest({ auth: true, notification: false }); + const [openCallDialog, setOpenCallDialog] = useState(false); + const [activeTab, setActiveTab] = useState(0); + const [handleCategories, operation] = useReducer(submission, { + categoryLists: [], + subCategoryLists: [], + }); const [state, dispatch] = useReducer(reducer, []); - return {children} -} + useEffect(() => { + requestServer(GET_CATEGORY, "get") + .then(({ data }) => { + const subCategoriesByAPI = data.data; + const categoriesByAPI = subCategoriesByAPI.reduce((accumulator, currentValue) => { + if (!accumulator.find((item) => item.category_id === currentValue.category_id)) { + accumulator.push(currentValue); + } + return accumulator; + }, []); + operation({ type: "SET_CATEGORIES", categoriesByAPI }); + operation({ type: "SET_SUBCATEGORIES", subCategoriesByAPI }); + }) + .catch(() => {}); + }, []); + + return ( + + {children} + + ); +}; diff --git a/src/lib/callWidget/contexts/categories.jsx b/src/lib/callWidget/contexts/categories.jsx new file mode 100644 index 0000000..533dfc3 --- /dev/null +++ b/src/lib/callWidget/contexts/categories.jsx @@ -0,0 +1,59 @@ +import { createContext, useCallback, useEffect, useReducer } from "react"; +import useRequest from "@/lib/app/hooks/useRequest"; +import { GET_CATEGORY } from "@/core/data/apiRoutes"; + +const reducer = (state, action) => { + switch (action.type) { + case "CHANGE_ACTIVE_ID": + return { ...state, active_id: action.id }; + case "SET_CATEGORIES": + return { ...state, categoryLists: action.cat }; + case "SET_SUBCATEGORIES": + return { ...state, subCategoryLists: action.subCat }; + default: + return state; + } +}; +export const CategoriesContext = createContext(); + +export const CategoriesProvider = ({ children }) => { + const requestServer = useRequest({ auth: true, notification: false }); + const [state, dispatch] = useReducer(reducer, { + categoryLists: [], + subCategoryLists: [], + active_id: null, + }); + + useEffect(() => { + requestServer(GET_CATEGORY, "get") + .then(({ data }) => { + const subCat = data.data; + const cat = subCat.reduce((accumulator, currentValue) => { + if (!accumulator.find((item) => item.category_id === currentValue.category_id)) { + accumulator.push(currentValue); + } + return accumulator; + }, []); + dispatch({ type: "SET_CATEGORIES", cat }); + dispatch({ type: "SET_SUBCATEGORIES", subCat }); + }) + .catch(() => {}); + }, []); + + const setCategoryID = useCallback((id) => { + dispatch({ type: "CHANGE_ACTIVE_ID", id }); + }); + + return ( + + {children} + + ); +}; diff --git a/src/lib/callWidget/hooks/useAnswers.jsx b/src/lib/callWidget/hooks/useAnswers.jsx index 68f8b46..149b1d3 100644 --- a/src/lib/callWidget/hooks/useAnswers.jsx +++ b/src/lib/callWidget/hooks/useAnswers.jsx @@ -1,38 +1,46 @@ -import {useContext} from "react"; -import {AnswersContext} from "@/lib/callWidget/contexts/answers"; +import { useContext } from "react"; +import { AnswersContext } from "@/lib/callWidget/contexts/answers"; const useAnswers = () => { - const {state, dispatch, activeTab, setActiveTab} = useContext(AnswersContext) + const { state, dispatch, activeTab, setActiveTab } = useContext(AnswersContext); const changeActiveTabAnswers = (newValue) => { - dispatch({type: 'CHANGE_ACTIVE_TAB', newValue}) - setActiveTab(newValue) - } + dispatch({ type: "CHANGE_ACTIVE_TAB", newValue }); + setActiveTab(newValue); + }; const newAnswerTab = (data) => { const newAnswer = { id: data.id, phone_number: data.phone_number, date: new Date(), - active: true - } - const newList = [...state, newAnswer] - dispatch({type: 'CHANGE_LIST', newList}) - changeActiveTabAnswers(newList.length - 1) - } + active: true, + active_category_id: null, + }; + const newList = [...state, newAnswer]; + dispatch({ type: "CHANGE_LIST", newList }); + changeActiveTabAnswers(newList.length - 1); + }; const closeAnswerTab = (id) => { - const index = state.findIndex(answer => answer.id === id) - const newIndex = index === 0 ? 0 : index - 1 - const newList = [...state] + const index = state.findIndex((answer) => answer.id === id); + const newIndex = index === 0 ? 0 : index - 1; + const newList = [...state]; newList.splice(index, 1); if (newList.length !== 0) { - newList[newIndex].active = true - setActiveTab(newIndex) + newList[newIndex].active = true; + setActiveTab(newIndex); } - dispatch({type: 'CHANGE_LIST', newList}) - } + dispatch({ type: "CHANGE_LIST", newList }); + }; - return {answersList: state, activeTab, setActiveTab, changeActiveTabAnswers, closeAnswerTab, newAnswerTab} -} + return { + answersList: state, + activeTab, + setActiveTab, + changeActiveTabAnswers, + closeAnswerTab, + newAnswerTab, + }; +}; -export default useAnswers \ No newline at end of file +export default useAnswers; diff --git a/src/lib/callWidget/hooks/useCallerHistory.jsx b/src/lib/callWidget/hooks/useCallerHistory.jsx index 68ee6d0..810209d 100644 --- a/src/lib/callWidget/hooks/useCallerHistory.jsx +++ b/src/lib/callWidget/hooks/useCallerHistory.jsx @@ -29,8 +29,8 @@ const useCallerHistory = (call_id, phone_number, max_size) => { return { firstItemOfHistory: validData.length === 0 ? false : validData[0], historyList: validData.length < 2 ? false : validData.slice(1), - isLoadingHistoryList: true, - errorHistoryList: false + isLoadingHistoryList: isLoading, + errorHistoryList: !data } }; diff --git a/src/lib/callWidget/hooks/useCategories.jsx b/src/lib/callWidget/hooks/useCategories.jsx new file mode 100644 index 0000000..423bd67 --- /dev/null +++ b/src/lib/callWidget/hooks/useCategories.jsx @@ -0,0 +1,17 @@ +import { useContext } from "react"; +import { AnswersContext } from "@/lib/callWidget/contexts/answers"; + +const useCategories = () => { + const { categoryLists, subCategoryLists, active_category_id, dispatch } = useContext(AnswersContext); + + const setActiveCategoryID = (tab_id, category_id) => { + dispatch({ type: "CHANGE_ACTIVE_ID", tab_id, category_id }); + }; + return { + categoryLists, + subCategoryLists, + active_category_id, + setActiveCategoryID, + }; +}; +export default useCategories; diff --git a/src/middlewares/AppPermission.jsx b/src/middlewares/AppPermission.jsx new file mode 100644 index 0000000..8f93bd3 --- /dev/null +++ b/src/middlewares/AppPermission.jsx @@ -0,0 +1,8 @@ +const GLOBAL_HAS_VALUE = process.env.NEXT_PUBLIC_HAS_VALUE; +const hasPermissionsValue = GLOBAL_HAS_VALUE ? JSON.parse(GLOBAL_HAS_VALUE) : []; + +const AppPermission = ({ children , permissions }) => { + return permissions.some(permission => hasPermissionsValue.includes(permission)) ? <>{children} : null; +}; + +export default AppPermission; diff --git a/src/middlewares/__test__/AppPermission.test.js b/src/middlewares/__test__/AppPermission.test.js new file mode 100644 index 0000000..82377ba --- /dev/null +++ b/src/middlewares/__test__/AppPermission.test.js @@ -0,0 +1,26 @@ +import {render, screen, waitFor} from '@testing-library/react'; +import MockAppWithProviders from "../../../mocks/AppWithProvider"; +import AppPermission from "@/middlewares/AppPermission"; + +describe('AppPermission From middlewares', () => { + describe('Behavior', () => { + it('show related child if permission exist', async () => { + + const requiredPermissions = ["NEXT_PUBLIC_HAS_WIDGET"]; + render({'children'}); + + await waitFor(()=>{ + expect(screen.queryByText('children')).toBeInTheDocument(); + }) + }); + it('do not show related child if permission does not exist', async () => { + + const requiredPermissions = ["MISSING_PERMISSION"]; + render(); + + await waitFor(() => { + expect(screen.queryByText('children')).not.toBeInTheDocument(); + }); + }); + }); +});