diff --git a/package-lock.json b/package-lock.json index 30f652c3..093bead3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "formik-mui": "^4.0.0-alpha.3", "git-url-parse": "^14.0.0", "history": "^5.3.0", + "jdenticon": "^3.3.0", "js-cookie": "^3.0.1", "katex": "^0.16.4", "lodash": "^4.17.21", @@ -43,6 +44,7 @@ "react-diff-viewer-continued": "^3.4.0", "react-dom": "^18.2.0", "react-helmet-async": "^2.0.4", + "react-jdenticon": "^1.4.0", "react-markdown": "^8.0.7", "react-markdown-editor-lite": "^1.3.4", "react-redux": "^8.0.5", @@ -7655,6 +7657,14 @@ } ] }, + "node_modules/canvas-renderer": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/canvas-renderer/-/canvas-renderer-2.2.1.tgz", + "integrity": "sha512-RrBgVL5qCEDIXpJ6NrzyRNoTnXxYarqm/cS/W6ERhUJts5UQtt/XPEosGN3rqUkZ4fjBArlnCbsISJ+KCFnIAg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/cbw-sdk": { "name": "@coinbase/wallet-sdk", "version": "3.9.3", @@ -10778,6 +10788,20 @@ "ws": "*" } }, + "node_modules/jdenticon": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/jdenticon/-/jdenticon-3.3.0.tgz", + "integrity": "sha512-DhuBRNRIybGPeAjMjdHbkIfiwZCCmf8ggu7C49jhp6aJ7DYsZfudnvnTY5/1vgUhrGA7JaDAx1WevnpjCPvaGg==", + "dependencies": { + "canvas-renderer": "~2.2.0" + }, + "bin": { + "jdenticon": "bin/jdenticon.js" + }, + "engines": { + "node": ">=6.4.0" + } + }, "node_modules/jest-environment-node": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", @@ -14670,6 +14694,18 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, + "node_modules/react-jdenticon": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/react-jdenticon/-/react-jdenticon-1.4.0.tgz", + "integrity": "sha512-yq9laq2ccH0MeSOZ7aTD21TNl0MsmADgDqVV5JIIMIaQOY7Ybpstbv/4T3ArSRatr+fuo2Xh2HUhwJG/UoELKQ==", + "dependencies": { + "jdenticon": "^3.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, "node_modules/react-markdown": { "version": "8.0.7", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.7.tgz", diff --git a/package.json b/package.json index 716eedb4..95a7f09d 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "formik-mui": "^4.0.0-alpha.3", "git-url-parse": "^14.0.0", "history": "^5.3.0", + "jdenticon": "^3.3.0", "js-cookie": "^3.0.1", "katex": "^0.16.4", "lodash": "^4.17.21", @@ -45,6 +46,7 @@ "react-diff-viewer-continued": "^3.4.0", "react-dom": "^18.2.0", "react-helmet-async": "^2.0.4", + "react-jdenticon": "^1.4.0", "react-markdown": "^8.0.7", "react-markdown-editor-lite": "^1.3.4", "react-redux": "^8.0.5", diff --git a/src/components/AddUserToOrganization/AddUserToOrganization.jsx b/src/components/AddUserToOrganization/AddUserToOrganization.jsx new file mode 100644 index 00000000..20fb2286 --- /dev/null +++ b/src/components/AddUserToOrganization/AddUserToOrganization.jsx @@ -0,0 +1,8 @@ +import React from 'react'; +import Box from '@mui/material/Box'; + +const AddUserToOrganization = () => { + return ; +}; +// +export default AddUserToOrganization; diff --git a/src/components/Audit-card.jsx b/src/components/Audit-card.jsx index 4b3b505e..1efca5d9 100644 --- a/src/components/Audit-card.jsx +++ b/src/components/Audit-card.jsx @@ -22,23 +22,58 @@ const AuditCard = ({ audit, request }) => { {audit.project_name} - - + + + {audit?.auditor_organization?.contacts?.email !== null + ? audit?.auditor_organization?.contacts?.email + : 'Hidden'} + + + + ) : ( + + + + {audit?.auditor_contacts.email !== null + ? audit?.auditor_contacts?.email + : 'Hidden'} + + + + )} + {audit?.auditor_organization?.id && ( + - - {audit?.auditor_contacts.email !== null - ? audit?.auditor_contacts?.email - : 'Hidden'} + Organization + + {audit?.auditor_organization?.name} - - + + )} el).join(', ') ?? ''} arrow diff --git a/src/components/Audit-request-card.jsx b/src/components/Audit-request-card.jsx index e8767033..f46203a8 100644 --- a/src/components/Audit-request-card.jsx +++ b/src/components/Audit-request-card.jsx @@ -48,6 +48,24 @@ const AuditRequestCard = ({ type, request, audit }) => { + {request.auditor_organization?.id && ( + + + Organization: + + + {request.auditor_organization.name} + + + )} {!budge && ( - + // + )} @@ -678,6 +689,7 @@ const dateWrapper = { }, }, }; + const dateStyle = { width: '150px', height: '40px', diff --git a/src/components/AuditorSearchModal.jsx b/src/components/AuditorSearchModal.jsx index 380727d7..aba4521e 100644 --- a/src/components/AuditorSearchModal.jsx +++ b/src/components/AuditorSearchModal.jsx @@ -8,7 +8,15 @@ import { Box } from '@mui/system'; import InputAdornment from '@mui/material/InputAdornment'; import SearchIcon from '@mui/icons-material/Search'; import { useEffect, useRef, useState, useCallback } from 'react'; -import { Paper, Typography } from '@mui/material'; +import Autocomplete from '@mui/material/Autocomplete'; +import { + Avatar, + Checkbox, + FormControlLabel, + Paper, + Slider, + Typography, +} from '@mui/material'; import AuditorSearchListBox from './custom/AuditorSearchListBox.jsx'; import IconButton from '@mui/material/IconButton'; import { ArrowBack } from '@mui/icons-material'; @@ -18,17 +26,25 @@ import dayjs from 'dayjs'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers'; -import { useNavigate } from 'react-router-dom/dist'; +import { useNavigate, useSearchParams } from 'react-router-dom/dist'; import { Field, Formik, Form } from 'formik'; import * as Yup from 'yup'; -import { useParams } from 'react-router-dom'; +import { useLocation, useParams } from 'react-router-dom'; import { addTestsLabel } from '../lib/helper.js'; +import CustomSnackbar from './custom/CustomSnackbar.jsx'; +import PriceCalculation from './PriceCalculation.jsx'; +import { ASSET_URL } from '../services/urls.js'; import TotalPrice from './forms/TotalPrice/TotalPrice.jsx'; -import { CLEAR_SEARCHED_AUDITOR } from '../redux/actions/types.js'; import Cookies from 'js-cookie'; import axios from 'axios'; import { API_URL } from '../services/urls.js'; import _ from 'lodash'; +import { CLEAR_SEARCHED_AUDITOR, CUSTOMER } from '../redux/actions/types.js'; +import { addUserInOrganization } from '../redux/actions/organizationAction.js'; +import { AUDITOR, CLEAR_SEARCH } from '../redux/actions/types.js'; +import { searchCustomers } from '../redux/actions/customerAction.js'; +import Radio from '@mui/material/Radio'; +import { getAuditors } from '../redux/actions/auditorAction.js'; export default function AuditorSearchModal({ open, @@ -37,16 +53,26 @@ export default function AuditorSearchModal({ setState, setError, projectInfo, + invite, + modeType, + customer, + type = 'auditor', }) { const navigate = useNavigate(); const dispatch = useDispatch(); const { id } = useParams(); - + const auditorReducer = useSelector(state => state.auditor.auditors); + const customersReducer = useSelector(state => state.customer.customers); const projectReducer = useSelector(state => state.project); const customerReducer = useSelector(state => state.customer); const [selectedAuditor, setSelectedAuditor] = useState({}); - const [mode, setMode] = useState('search'); + const organization = useSelector(s => s.organization.organization); + const [rulesOfMember, setRulesOfMember] = useState('Representative'); + const user = useSelector(s => s.user.user); + const [openDrop, setOpenDrop] = useState(false); + const [mode, setMode] = useState(modeType || 'search'); + const [inputValue, setInputValue] = useState(''); const [query, setQuery] = useState(''); const [page, setPage] = useState(1); const [auditors, setAuditors] = useState([]); @@ -56,10 +82,66 @@ export default function AuditorSearchModal({ const [searchValue, setSearchValue] = useState(''); const scrollTimeout = useRef(null); const listInnerRef = useRef(); + const [auditorPagination, setAuditorPagination] = useState({ + hasMore: true, + total: 0, + }); + const [organizationPagination, setOrganizationPagination] = useState({ + hasMore: true, + total: 0, + }); + + useEffect(() => { + dispatch(getAuditors(query, 15)); + }, [query]); + + useEffect(() => { + if (!modeType) { + if (organization.id) { + if ( + organization.organization_type.toLowerCase() === AUDITOR.toLowerCase() + ) { + dispatch(getAuditors(query, 15)); + } else { + dispatch(searchCustomers({ search: query, perPage: 15 })); + } + } else { + dispatch(getAuditors(query, 15)); + } + } + return () => { + if (!modeType) { + dispatch({ type: CLEAR_SEARCH }); + } + }; + }, [query, organization.id]); + + const handleInputChange = event => { + setQuery(event.target.value); + }; const handleOptionChange = option => { setSelectedAuditor(option); - setMode('offer'); + if (invite) { + setMode('invite'); + } else { + setMode('offer'); + } + }; + + const handleInviteUser = () => { + const data = [ + { + user_id: customer?.user_id ? customer.user_id : selectedAuditor.user_id, + access_level: rulesOfMember, + }, + ]; + dispatch( + addUserInOrganization(organization.link_id, data, organization.id), + ); + setMode(modeType || 'search'); + setQuery(''); + handleClose(); }; const handleSearch = async () => { @@ -69,10 +151,39 @@ export default function AuditorSearchModal({ await navigate( `/auditors?search=${query}&projectIdToInvite=${id || projectInfo.id}`, ); + // if (setState) { + // await setState(true); + // } + // if (handleSubmit) { + // handleSubmit(); + // } + // if (organization.id) { + // if ( + // organization.organization_type.toLowerCase() === AUDITOR.toLowerCase() + // ) { + // await navigate( + // `/auditors?search=${query}&organization=${organization.link_id}`, + // { + // state: { from: location.pathname }, + // }, + // ); + // } else { + // await navigate( + // `/customers?search=${query}&organization=${organization.link_id}`, + // { + // state: { from: location.pathname }, + // }, + // ); + // } + // } else { + // await navigate(`/auditors?search=${query}&projectIdToInvite=${id}`, { + // state: { from: location.pathname }, + // }); + // } }; useEffect(() => { - const fetchAuditors = async () => { + const fetchResults = async () => { try { setIsLoading(true); setPage(1); @@ -83,87 +194,184 @@ export default function AuditorSearchModal({ setScrollPosition(listInnerRef.current.scrollTop); } - const response = await axios.get( - `${API_URL}/search?query=${query}&sort_by=rating&tags=&sort_order=-1&page=1&per_page=15&kind=auditor badge`, - { headers: { Authorization: `Bearer ${token}` } }, + const requests = [ + axios.get( + `${API_URL}/search?query=${query}&sort_by=rating&tags=&sort_order=-1&page=1&per_page=15&kind=auditor badge`, + { headers: { Authorization: `Bearer ${token}` } }, + ), + axios.get( + `${API_URL}/search?query=${query}&sort_by=rating&tags=&sort_order=-1&page=1&per_page=15&kind=organization`, + { headers: { Authorization: `Bearer ${token}` } }, + ), + ]; + + const [auditorsResponse, organizationsResponse] = await Promise.all( + requests, ); - setAuditors(response.data.result); + // Update pagination info for both types + setAuditorPagination({ + hasMore: + auditorsResponse.data.result.length > 0 && + auditorsResponse.data.result.length < + auditorsResponse.data.totalDocuments, + total: auditorsResponse.data.totalDocuments, + }); + + setOrganizationPagination({ + hasMore: + organizationsResponse.data.result.length > 0 && + organizationsResponse.data.result.length < + organizationsResponse.data.totalDocuments, + total: organizationsResponse.data.totalDocuments, + }); + + const combinedResults = [ + ...auditorsResponse.data.result, + ...organizationsResponse.data.result, + ]; + + setAuditors(combinedResults); } catch (error) { - console.error('Error fetching auditors:', error); + console.error('Error fetching results:', error); } finally { setIsLoading(false); } }; if (query) { - fetchAuditors(); + fetchResults(); } }, [query]); useEffect(() => { - if (!isLoading && listInnerRef.current && scrollPosition > 0) { - requestAnimationFrame(() => { - listInnerRef.current.scrollTop = scrollPosition; - }); - } - }, [isLoading, auditors]); - - useEffect(() => { - const fetchMoreAuditors = async () => { - if (isLoading || lastList) return; + const fetchMoreResults = async () => { + if ( + isLoading || + (!auditorPagination.hasMore && !organizationPagination.hasMore) + ) + return; try { setIsLoading(true); const token = Cookies.get('token'); - const response = await axios.get( - `${API_URL}/search?query=${query}&sort_by=rating&tags=&sort_order=-1&page=${page}&per_page=15&kind=auditor badge`, - { headers: { Authorization: `Bearer ${token}` } }, - ); - if (response.data.result.length === 0) { + const requests = []; + + // Only fetch auditors if there are more to fetch + if (auditorPagination.hasMore) { + requests.push( + axios.get( + `${API_URL}/search?query=${query}&sort_by=rating&tags=&sort_order=-1&page=${page}&per_page=15&kind=auditor badge`, + { headers: { Authorization: `Bearer ${token}` } }, + ), + ); + } + + // Only fetch organizations if there are more to fetch + if (organizationPagination.hasMore) { + requests.push( + axios.get( + `${API_URL}/search?query=${query}&sort_by=rating&tags=&sort_order=-1&page=${page}&per_page=15&kind=organization`, + { headers: { Authorization: `Bearer ${token}` } }, + ), + ); + } + + if (requests.length === 0) { + setLastList(true); + return; + } + + const responses = await Promise.all(requests); + let newResults = []; + + responses.forEach((response, index) => { + const isAuditorResponse = auditorPagination.hasMore && index === 0; + const isOrgResponse = + organizationPagination.hasMore && + index === (auditorPagination.hasMore ? 1 : 0); + + if (isAuditorResponse) { + setAuditorPagination(prev => ({ + ...prev, + hasMore: + response.data.result.length > 0 && + page * 15 < response.data.totalDocuments, + })); + } + + if (isOrgResponse) { + setOrganizationPagination(prev => ({ + ...prev, + hasMore: + response.data.result.length > 0 && + page * 15 < response.data.totalDocuments, + })); + } + + newResults = [...newResults, ...response.data.result]; + }); + + if (newResults.length === 0) { setLastList(true); return; } setAuditors(prev => { - const newAuditors = response.data.result; - const uniqueAuditors = [...prev]; + const uniqueResults = [...prev]; - newAuditors.forEach(newAuditor => { + newResults.forEach(newItem => { if ( - !uniqueAuditors.some( - existing => existing.user_id === newAuditor.user_id, + !uniqueResults.some( + existing => + existing.user_id === newItem.user_id || + existing.id === newItem.id, ) ) { - uniqueAuditors.push(newAuditor); + uniqueResults.push(newItem); } }); - return uniqueAuditors; + return uniqueResults; }); } catch (error) { - console.error('Error fetching more auditors:', error); + console.error('Error fetching more results:', error); } finally { setIsLoading(false); } }; if (page > 1) { - fetchMoreAuditors(); + fetchMoreResults(); } - }, [page, query, lastList]); + }, [ + page, + query, + lastList, + auditorPagination.hasMore, + organizationPagination.hasMore, + ]); const handleScroll = useCallback( _.throttle(e => { - if (!isLoading && !lastList) { + if ( + !isLoading && + !lastList && + (auditorPagination.hasMore || organizationPagination.hasMore) + ) { const { scrollTop, scrollHeight, clientHeight } = e.target; if (scrollHeight - scrollTop <= clientHeight * 1.2) { setPage(prev => prev + 1); } } }, 300), - [isLoading, lastList, page], + [ + isLoading, + lastList, + auditorPagination.hasMore, + organizationPagination.hasMore, + ], ); const handleSearchInput = e => { @@ -181,6 +389,42 @@ export default function AuditorSearchModal({ }, 300); }; + // const renderSearchResult = item => { + // const isOrganization = type === 'organization'; + // return ( + // handleOptionChange(item)} + // > + // + // + // + // {isOrganization + // ? item.name + // : `${item.first_name} ${item.last_name}`} + // + // {item.description && ( + // + // {item.description} + // + // )} + // + // + // ); + // }; + return ( - - - - - ), - }} - /> - {searchValue && auditors.length > 0 && ( - - {auditors.map(option => ( - handleOptionChange(option)} - sx={{ - padding: '8px', - cursor: 'pointer', - '&:hover': { - backgroundColor: '#f5f5f5', - }, - }} - > - handleOptionChange(option)} - /> - - ))} - {isLoading && ( - - Loading... - - )} - - )} - + {auditorReducer && ( + + + + + ), + }} + /> + {searchValue && auditors.length > 0 && ( + + {(!!auditors.length ? auditors : customersReducer).map( + option => ( + handleOptionChange(option)} + sx={{ + padding: '8px', + cursor: 'pointer', + '&:hover': { + backgroundColor: '#f5f5f5', + }, + }} + > + + handleOptionChange(option) + } + /> + + ), + )} + {isLoading && ( + + Loading... + + )} + + )} + + )} + + + )} ); } @@ -396,6 +795,18 @@ const MakeOfferSchema = Yup.object().shape({ }), }); +const roleDescriptionTitle = theme => ({ + fontSize: '16px', + color: '#9f9f9f', + marginLeft: '42px', + [theme.breakpoints.down('md')]: { + fontSize: '12px', + }, + [theme.breakpoints.down('xs')]: { + fontSize: '10px', + }, +}); + const userListSx = theme => ({ position: 'fixed', top: 'auto', diff --git a/src/components/Chat/ChatList.jsx b/src/components/Chat/ChatList.jsx index 6c6e0f18..4b250f7b 100644 --- a/src/components/Chat/ChatList.jsx +++ b/src/components/Chat/ChatList.jsx @@ -14,11 +14,14 @@ import ChatListItem from './ChatListItem.jsx'; import { AUDITOR, CUSTOMER } from '../../redux/actions/types.js'; import { searchAuditor } from '../../redux/actions/auditorAction.js'; import { searchCustomers } from '../../redux/actions/customerAction.js'; +import { searchOrganization } from '../../redux/actions/organizationAction.js'; +import theme from '../../styles/themes.js'; -const ChatList = ({ chatList, chatListIsOpen, setChatListIsOpen }) => { +const ChatList = ({ chatList, chatListIsOpen, setChatListIsOpen, orgId }) => { const dispatch = useDispatch(); const { auditors } = useSelector(s => s.auditor); const { customers } = useSelector(s => s.customer); + const { searchOrganizations } = useSelector(s => s.organization); const { user } = useSelector(s => s.user); const [, startTransition] = useTransition(); @@ -33,87 +36,92 @@ const ChatList = ({ chatList, chatListIsOpen, setChatListIsOpen }) => { if (search.trim()) { dispatch(searchAuditor({ search, perPage: 20 }, false)); dispatch(searchCustomers({ search, perPage: 20 })); + dispatch(searchOrganization({ search, perPage: 20 })); } }); }, [search]); return ( - <> - - - - - - - ), - }} - /> - setChatListIsOpen(false)} - color="inherit" - sx={closeButtonSx} - > - - - - {/**/} - {/* Chats*/} - {/**/} + + + + + + + ), + }} + /> + setChatListIsOpen(false)} + color="inherit" + sx={closeButtonSx} + > + + + {/**/} + {/* Chats*/} + {/**/} + - - {chatList.length > 0 - ? chatList - ?.filter(chat => - chat.name - ?.toLowerCase() - .includes(search.toLowerCase().trim()), - ) - .reverse() - .map(chat => ( + + {chatList.length > 0 + ? chatList + ?.filter(chat => + chat.name?.toLowerCase().includes(search.toLowerCase().trim()), + ) + .reverse() + .map(chat => { + // const org = chat?.members.find(org => org.org_user_id); + // + return ( - )) - : !search && ( - You haven't written to anyone yet - )} + ); + }) + : !search && ( + You haven't written to anyone yet + )} - {search && - auditors - .filter( - auditor => - !chatList.some(chat => - chat.members.some( - member => - member.id === auditor.user_id && - member.role?.toLowerCase() === AUDITOR, - ), - ) && auditor.user_id !== user.id, - ) - .map(auditor => ( + {search && + auditors + .filter( + auditor => + !chatList.some(chat => + chat.members.some( + member => + member.id === auditor.user_id && + member.role?.toLowerCase() === AUDITOR, + ), + ) && auditor.user_id !== user.id, + ) + .map(auditor => { + return ( { members: [{ id: auditor.user_id }, { id: user.id }], }} /> - ))} + ); + })} - {search && - customers - .filter( - customer => - !chatList.some(chat => - chat.members.some( - member => - member.id === customer.user_id && - member.role?.toLowerCase() === CUSTOMER, - ), - ) && customer.user_id !== user.id, - ) - .map(customer => ( + {search && + customers + .filter( + customer => + !chatList.some(chat => + chat.members.some( + member => + member.id === customer.user_id && + member.role?.toLowerCase() === CUSTOMER, + ), + ) && customer.user_id !== user.id, + ) + .map(customer => ( + + ))} + + {search && + searchOrganizations + .filter( + organization => + !chatList.some(chat => + chat.members.some( + member => + member.id === organization.user_id && + member.role?.toLowerCase() === CUSTOMER, + ), + ) && organization.user_id !== user.id, + ) + .map(organization => { + return ( - ))} + ); + })} - {chatList.length > 0 && - !customers.length && - !auditors.length && - !chatList.find(chat => - chat.name?.toLowerCase().includes(search.toLowerCase().trim()), - ) && No search results} - + {chatList.length > 0 && + !customers.length && + !auditors.length && + !chatList.find(chat => + chat.name?.toLowerCase().includes(search.toLowerCase().trim()), + ) && No search results} - setChatListIsOpen(false)} - /> - + ); }; export default ChatList; -const wrapper = theme => ({ - width: '30%', +const wrapper = (theme, openOrgList) => ({ borderRight: '2px solid #e5e5e5', display: 'flex', flexDirection: 'column', - [theme.breakpoints.down('xs')]: { - display: 'none', - }, + width: '100%', + // width: `calc(100% - ${!openOrgList ? '70px' : '0px'} )`, + // width: `30%`, + // [theme.breakpoints.down('md')]: { + // width: `calc(100% - ${!openOrgList ? '73px' : '0px'})`, + // }, + // [theme.breakpoints.down('xs')]: { + // display: 'none', + // }, }); const mobileChatListOpen = theme => ({ diff --git a/src/components/Chat/ChatListItem.jsx b/src/components/Chat/ChatListItem.jsx index ab04d08c..2fea2d3c 100644 --- a/src/components/Chat/ChatListItem.jsx +++ b/src/components/Chat/ChatListItem.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Link as RouterLink } from 'react-router-dom'; +import { Link as RouterLink, useParams } from 'react-router-dom'; import { useDispatch } from 'react-redux'; import { Avatar, Box, Link, Tooltip } from '@mui/material'; import { ASSET_URL } from '../../services/urls.js'; @@ -16,14 +16,22 @@ const ChatListItem = ({ isNew = false, userDataId = false, role, + orgId, }) => { const dispatch = useDispatch(); + const { id } = useParams(); const getUnreadForUser = chat => chat.unread.find(unread => unread.id === user.id)?.unread || 0; - const getRole = () => - role || chat.members.find(member => member.id !== user.id)?.role; + const getRole = () => { + return ( + role || + chat.members.find( + member => (member?.org_user?.id ?? member.id) !== user.id, + )?.role + ); + }; const setChatHandle = () => { setListIsOpen(false); @@ -52,9 +60,9 @@ const ChatListItem = ({ return ( diff --git a/src/components/Chat/CurrentChat.jsx b/src/components/Chat/CurrentChat.jsx index 80a2f1be..57ddcb76 100644 --- a/src/components/Chat/CurrentChat.jsx +++ b/src/components/Chat/CurrentChat.jsx @@ -15,10 +15,12 @@ import { chatSendMessage, closeCurrentChat, getChatList, + getChatListByOrg, getChatMessages, } from '../../redux/actions/chatActions.js'; import AttachFileModal from './AttachFileModal.jsx'; import Headings from '../../router/Headings.jsx'; +import { useSearchParams } from 'react-router-dom/dist'; const CurrentChat = ({ chatMessages, @@ -30,7 +32,8 @@ const CurrentChat = ({ const navigate = useNavigate(); const { id } = useParams(); const { user } = useSelector(s => s.user); - + const [searchParams] = useSearchParams(); + const orgId = searchParams.get('org'); const [newMessage, setNewMessage] = useState(''); const [attachModalIsOpen, setAttachModalIsOpen] = useState(false); const [displayedMessages, setDisplayedMessages] = useState(20); @@ -43,6 +46,9 @@ const CurrentChat = ({ const messageBoxRef = useRef(); const newMessagesTextRef = useRef(); const userLinkDataRef = useRef({}); + const organization = useSelector(s => + s.organization.organizations.find(el => el.id === orgId), + ); useEffect(() => { if ( @@ -54,12 +60,17 @@ const CurrentChat = ({ setChatId(currentChat?.chatId); dispatch(getChatMessages(currentChat.chatId, user.id)); } - }, [currentChat, chatId]); + }, [currentChat, chatId, orgId]); useEffect(() => { if (currentChat?.chatId && id !== currentChat?.chatId) { - navigate(`/chat/${currentChat?.chatId}`); - dispatch(getChatList(user.current_role)); + if (!orgId) { + navigate(`/chat/${currentChat?.chatId}`); + dispatch(getChatList(user.current_role)); + } else { + navigate(`/chat/${currentChat?.chatId}?org=${orgId}`); + dispatch(getChatListByOrg('Organization', orgId)); + } } }, [currentChat?.chatId]); @@ -131,12 +142,22 @@ const CurrentChat = ({ const handleSend = () => { if (!newMessage.trim()) return; + // dispatch( + // chatSendMessage( + // newMessage.trim(), + // { id: currentChat?.chatId, role: currentChat?.role }, + // user.current_role, + // currentChat?.isNew, + // ), + // ); dispatch( chatSendMessage( newMessage.trim(), { id: currentChat?.chatId, role: currentChat?.role }, - user.current_role, + orgId ? 'Organization' : user.current_role, currentChat?.isNew, + 'Text', + orgId && orgId, ), ); setNewMessage(''); @@ -243,22 +264,28 @@ const CurrentChat = ({ )} {!currentChat?.isNew ? ( - chatMessages.slice(getDisplayedMessages()).map((msg, idx, ar) => { + chatMessages + .slice(getDisplayedMessages()) + .map((msg, idx, ar) => { const date = new Date(msg?.time / 1000).toDateString(); const prevMsgDate = new Date( ar[idx - 1]?.time / 1000, ).toDateString(); const unreadLabel = !!unread && ar.length - unread === idx; const isInterlocutorRead = idx < ar.length - interlocutorUnread; - return ( - + {date !== prevMsgDate && ( {date.replace(/[^ ]+/, '')} )} {unreadLabel && New messages:} ({ width: '70%', display: 'flex', flexDirection: 'column', - [theme.breakpoints.down('sm')]: { + [theme.breakpoints.down('xs')]: { width: '100%', }, }); diff --git a/src/components/Chat/Message.jsx b/src/components/Chat/Message.jsx index 017d07b0..6363d2ee 100644 --- a/src/components/Chat/Message.jsx +++ b/src/components/Chat/Message.jsx @@ -9,8 +9,19 @@ import { AUDITOR, CUSTOMER } from '../../redux/actions/types.js'; import ImageMessage from './ImageMessage.jsx'; import AuditMessage from './AuditMessage.jsx'; import theme from '../../styles/themes.js'; +import { Link, useLocation } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom/dist'; -const Message = ({ message, user, currentChat, isRead, previousMessage }) => { +const Message = ({ + message, + user, + currentChat, + isRead, + type, + orgId, + chatRole, + previousMessage +}) => { const { customer } = useSelector(state => state.customer); const { auditor } = useSelector(state => state.auditor); @@ -25,6 +36,8 @@ const Message = ({ message, user, currentChat, isRead, previousMessage }) => { fileMessage = {}; } + const location = useLocation(); + const navigate = useNavigate(); const userAvatar = useMemo(() => { if (user.current_role === AUDITOR && !!auditor?.avatar) { return auditor.avatar; @@ -38,12 +51,20 @@ const Message = ({ message, user, currentChat, isRead, previousMessage }) => { } }, [user.current_role, customer?.avatar, auditor?.avatar]); + const isOwn = () => { + return message.from.role.toLowerCase() === 'organization' + ? { isOwn: message.from?.org_user?.id === user.id } + : { isOwn: message.from?.id === user.id }; + }; + const getMessageAvatar = () => { + const avatar = isOwn() ? userAvatar : currentChat?.avatar; if (message?.from?.id === user?.id) { return userAvatar ? `${ASSET_URL}/id/${userAvatar}` : null; } return currentChat?.avatar ? `${ASSET_URL}/id/${currentChat.avatar}` : null; }; + // } const downloadFile = () => { const token = Cookies.get('token'); @@ -66,8 +87,17 @@ const Message = ({ message, user, currentChat, isRead, previousMessage }) => { const shouldShowAvatar = !previousMessage || previousMessage.from?.id !== message.from?.id; + const handleGoProfile = () => { + localStorage.setItem('prev', location.pathname); + navigate( + `/${message.from?.org_user?.role[0].toLowerCase()}/${ + message.from?.org_user?.id + }`, + ); + }; + return ( - + {shouldShowAvatar ? ( { + {message.from?.org_user?.id && ( + + {message.from?.org_user.name} + + )} {message.kind === 'Image' ? ( ) : message.kind === 'Audit' ? ( @@ -193,6 +230,16 @@ const messageSx = ({ isOwn }) => ({ '&:hover .avatar-plug': { opacity: 1, }, + '& a': { + textDecoration: 'unset', + }, +}); + +const orgNameSx = (theme, color) => ({ + padding: '5px!important', + paddingLeft: '18px!important', + color: `${color}!important`, + cursor: 'pointer', }); const avatarPlugSx = theme => ({ diff --git a/src/components/Chat/TypeChat.jsx b/src/components/Chat/TypeChat.jsx new file mode 100644 index 00000000..727601a9 --- /dev/null +++ b/src/components/Chat/TypeChat.jsx @@ -0,0 +1,228 @@ +import React, { useMemo, useState } from 'react'; +import { + Avatar, + Box, + Button, + Divider, + List, + ListItem, + ListItemAvatar, + ListItemText, + Popover, + Typography, +} from '@mui/material'; +import { addTestsLabel } from '../../lib/helper.js'; +import ChatIcon from '../icons/ChatIcon.jsx'; +import { AUDITOR, CUSTOMER } from '../../redux/actions/types.js'; +import { setCurrentChat } from '../../redux/actions/chatActions.js'; +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import { ASSET_URL } from '../../services/urls.js'; + +const TypeChat = ({ auditor, project }) => { + const [isOpen, setIsOpen] = useState(false); + const [anchorEl, setAnchorEl] = useState(null); + const { chatList } = useSelector(s => s.chat); + const { user } = useSelector(s => s.user); + const { organizations } = useSelector(s => s.organization); + const myAuditor = useSelector(s => s.auditor.auditor); + const myCustomer = useSelector(s => s.customer.customer); + const navigate = useNavigate(); + const dispatch = useDispatch(); + + const handleClose = () => { + setAnchorEl(null); + }; + + const open = Boolean(anchorEl); + const id = open ? 'simple-popover' : undefined; + + const startChat = () => { + window.scrollTo(0, 0); + + const existingChat = chatList.find(chat => + chat.members?.find( + member => + member.id === auditor?.user_id && + member.role?.toLowerCase() === AUDITOR, + ), + ); + const chatId = existingChat ? existingChat.id : auditor?.user_id; + const members = [auditor?.user_id, user.id]; + + dispatch( + setCurrentChat(chatId, { + name: auditor.first_name, + avatar: auditor.avatar, + role: AUDITOR, + isNew: !existingChat, + members, + }), + ); + localStorage.setItem('path', window.location.pathname); + navigate(`/chat/${existingChat ? existingChat.id : auditor?.user_id}`); + }; + + const myUser = useMemo(() => { + if (user?.current_role?.toLowerCase() === AUDITOR.toLowerCase()) { + return myAuditor; + } else { + return myCustomer; + } + }, [myAuditor, myCustomer]); + + const handleSendMessage = e => { + // TODO add check for pm or org chat + if (organizations.length) { + setAnchorEl(e.currentTarget); + } else { + startChat(); + } + }; + + const chatFromOrg = org => { + console.log(org); + window.scrollTo(0, 0); + + if (auditor) { + const existingChat = chatList.find(chat => + chat.members?.find( + member => + member.id === auditor?.user_id && + member.role?.toLowerCase() === AUDITOR, + ), + ); + + const chatId = existingChat ? existingChat.id : auditor?.user_id; + const members = [auditor?.user_id, user.id]; + + dispatch( + setCurrentChat(chatId, { + name: auditor.first_name, + avatar: auditor.avatar, + role: AUDITOR, + isNew: !existingChat, + members, + }), + ); + localStorage.setItem('path', window.location.pathname); + navigate( + `/chat/${existingChat ? existingChat.id : auditor?.user_id}?org=${ + org.id + }`, + ); + } else if (project) { + const existingChat = chatList.find(chat => + chat.members?.find( + member => + member.id === project?.customer_id && + member.role?.toLowerCase() === CUSTOMER, + ), + ); + const chatId = existingChat ? existingChat.id : project?.customer_id; + const members = [project?.customer_id, user.id]; + + dispatch( + setCurrentChat(chatId, { + role: CUSTOMER, + isNew: !existingChat, + userDataId: project?.customer_id, + members, + }), + ); + localStorage.setItem('path', window.location.pathname); + navigate(`/chat/${project?.customer_id}?org=${org.id}`); + } + }; + + return ( + <> + + + + + + + + + + + {organizations.map(org => { + return ( + + chatFromOrg(org)} + sx={{ + cursor: 'pointer', + alignItems: 'center', + '&:hover': { + backgroundColor: '#e8e8e8', + }, + }} + > + + + + + // {name} + // + // } + /> + + + + ); + })} + + + + ); +}; + +export default TypeChat; diff --git a/src/components/CreateProjectCard.jsx b/src/components/CreateProjectCard.jsx index 1b44c99c..6dd32e62 100644 --- a/src/components/CreateProjectCard.jsx +++ b/src/components/CreateProjectCard.jsx @@ -289,6 +289,33 @@ const CreateProjectCard = ({ projectInfo }) => { }; }, []); + useEffect(() => { + if (initialValues?.id && initialValues.scope.length) { + const getRepoUrl = initialValues.scope[0]; + function getShaFromGitHubUrl(url) { + const regex = /\/blob\/([0-9a-f]{40})\//; + const match = url.match(regex); + return match ? match[1] : null; + } + + function parseGitHubUrl(gitHubUrl) { + const urlParts = gitHubUrl.split('/'); + const owner = urlParts[3]; + const repo = urlParts[4]; + + return `${owner}/${repo}`; + } + + const githubRepo = parseGitHubUrl(getRepoUrl); + const sha = getShaFromGitHubUrl(getRepoUrl); + dispatch(getSha(sha)); + dispatch(getRepoOwner(githubRepo)); + } + return () => { + dispatch({ type: CLEAR_PROJECT }); + }; + }, []); + return ( { setState={setState} setError={setError} projectInfo={project} + type={'auditor'} /> { + const navigate = useNavigate(); + const user = useSelector(state => state.user.user); + const [openModal, setOpenModal] = useState(false); + const customerReducer = useSelector(state => state.customer.customer); + const [message, setMessage] = useState(''); + const [isForm, setIsForm] = useState(false); + const myProjects = useSelector(state => state.project.myProjects); + const dispatch = useDispatch(); + const userProjects = useSelector(s => s.project.myProjects); + const [errorMessage, setErrorMessage] = useState(null); + const [showAddUser, setShowAddUser] = useState(false); + + const handleView = () => { + setOpenModal(true); + }; + + const handleCloseModal = () => { + setOpenModal(false); + }; + + const handleError = () => { + setErrorMessage(null); + setMessage('Switched to customer role'); + const delayedFunc = setTimeout(() => { + if (userProjects.length) { + navigate(`/my-projects/${auditor.user_id}`); + } else { + setMessage(null); + setErrorMessage('No active projects'); + } + }, 1000); + return () => clearTimeout(delayedFunc); + }; + + const handleInvite = () => { + setShowAddUser(!showAddUser); + }; + + return ( + + { + setErrorMessage(null); + setMessage(null); + }} + severity={isForm || message ? 'success' : 'error'} + text={message || errorMessage} + /> + { + setShowAddUser(false); + }} + customer={customer} + modeType={'invite'} + setError={() => console.log('error')} + /> + + + + + + + + + + + {customer.first_name} {customer.last_name} + + + + + + + + + + + + + {/**/} + {/* View more*/} + {/**/} + + {budge && not registered} + + + ); +}; + +export default CustomerListCard; + +const budgeTitle = theme => ({ + color: '#B2B3B3', + fontSize: '12px!important', + marginTop: '-10px', + [theme.breakpoints.down('sm')]: { + fontSize: '10px!important', + marginTop: '-5px', + }, +}); + +const wrapper = theme => ({ + padding: '12px 20px 12px 45px', + display: 'flex', + gap: '10px', + height: '100%', + justifyContent: 'space-between', + [theme.breakpoints.down('lg')]: { + padding: '20px', + }, + [theme.breakpoints.down('xs')]: { + padding: '15px', + }, +}); + +const cardLeftSide = { + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + gap: '12px', + [theme.breakpoints.down('xs')]: { + gap: '15px', + }, +}; + +const cardRightSide = { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: '15px', + [theme.breakpoints.down('xs')]: { + gap: '12px', + }, +}; + +const avatarDescription = theme => ({ + display: 'flex', + flexDirection: 'row', + gap: '30px', + [theme.breakpoints.down('lg')]: { + gap: '20px', + }, + [theme.breakpoints.down('xs')]: { + gap: '10px', + }, +}); + +const descriptionStyle = theme => ({ + display: 'flex', + flexDirection: 'column', + gap: '15px', + [theme.breakpoints.down('xs')]: { + gap: '8px', + }, +}); + +const avatarStyle = theme => ({ + width: '65px', + height: '65px', + [theme.breakpoints.down('xs')]: { + width: '38px', + height: '38px', + }, +}); + +const nameStyle = { + fontWeight: '600', + fontSize: { + zero: '11px', + sm: '14px', + md: '16px', + lg: '18px', + }, + color: '#152BEA', +}; + +const inviteButtonStyle = theme => ({ + width: '130px', + textTransform: 'unset', + boxShadow: '0', + fontWeight: 600, + [theme.breakpoints.down('md')]: { + width: '130px', + }, + [theme.breakpoints.down('sm')]: { + width: '86px', + fontSize: '8px', + }, +}); + +const tagsWrapper = theme => ({ + [theme.breakpoints.down('xs')]: { + maxWidth: '130px', + }, +}); diff --git a/src/components/IdentitySetting/IdentitySetting.jsx b/src/components/IdentitySetting/IdentitySetting.jsx index b2ad2062..c8a43bb1 100644 --- a/src/components/IdentitySetting/IdentitySetting.jsx +++ b/src/components/IdentitySetting/IdentitySetting.jsx @@ -250,6 +250,9 @@ const buttonSx = theme => ({ fontSize: '18px', width: '214px', borderRadius: '10px', + [theme.breakpoints.down(850)]: { + width: '234px', + }, [theme.breakpoints.down('xs')]: { padding: '9px 10px', }, diff --git a/src/components/Organization.jsx b/src/components/Organization.jsx new file mode 100644 index 00000000..b098dd13 --- /dev/null +++ b/src/components/Organization.jsx @@ -0,0 +1,602 @@ +import React, { useMemo, useEffect, useState } from 'react'; +import { + Avatar, + Box, + Button, + Typography, + Link, + useMediaQuery, + Tooltip, + ListItemText, + List, + ListItem, + Checkbox, + ListItemAvatar, + Chip, + Modal, + FormControlLabel, +} from '@mui/material'; +import GitHubIcon from '@mui/icons-material/GitHub.js'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack.js'; +import theme from '../styles/themes.js'; +import { useNavigate, useParams } from 'react-router-dom/dist'; +import { useDispatch, useSelector } from 'react-redux'; +import Loader from './Loader.jsx'; +import { AUDITOR, CUSTOMER } from '../redux/actions/types.js'; +import TagsList from './tagsList'; +import { ASSET_URL } from '../services/urls.js'; +import MobileTagsList from './MobileTagsList/index.jsx'; +import { addTestsLabel, capitalize, isAuth } from '../lib/helper.js'; +import ShareProfileButton from './custom/ShareProfileButton.jsx'; +import IdentitySetting from './IdentitySetting/IdentitySetting.jsx'; +import LinkedinIcon from './icons/LinkedinIcon.jsx'; +import XTwitterLogo from './icons/XTwitter-logo.jsx'; +import { clearUserMessages } from '../redux/actions/userAction.js'; +import CustomSnackbar from './custom/CustomSnackbar.jsx'; +import Headings from '../router/Headings.jsx'; +import ListItemButton from '@mui/material/ListItemButton'; +import DeleteForeverRoundedIcon from '@mui/icons-material/DeleteForeverRounded'; +import Layout from '../styles/Layout.jsx'; +import { CustomCard } from './custom/Card.jsx'; +import { + acceptInvites, + clearOrganization, + deleteInvites, + deleteUserFromOrganization, + getOrganizationById, +} from '../redux/actions/organizationAction.js'; +import InfoCard from './custom/info-card.jsx'; +import AuditorSearchModal from './AuditorSearchModal.jsx'; +import ConfirmModal from './modal/ConfirmModal.jsx'; +import EditIcon from '@mui/icons-material/Edit'; +import UserLIstItem from './UserListItem/UserLIstItem.jsx'; +import { useLocation } from 'react-router-dom'; + +const Organization = ({ linkId }) => { + const role = useSelector(s => s.user.user.current_role); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const matchXs = useMediaQuery(theme.breakpoints.down('xs')); + const matchXxs = useMediaQuery(theme.breakpoints.down(590)); + const organization = useSelector(s => s.organization.organization); + const invites = useSelector(s => s.organization.invites); + const [showAddUser, setShowAddUser] = useState(false); + const [openConfirm, setIsOpenConfirm] = useState(false); + const location = useLocation(); + + const { + customer, + error: customerError, + successMessage: successMessage, + } = useSelector(s => s.organization); + const { + auditor, + error: auditorError, + success: auditorSuccess, + } = useSelector(s => s.auditor); + const { user, error } = useSelector(s => s.user); + + useEffect(() => { + dispatch(getOrganizationById(linkId)); + return () => { + dispatch(clearOrganization()); + }; + }, [linkId]); + + const handleEdit = () => { + navigate(`/edit-organization/${linkId}`); + }; + + const handleAddUser = () => { + setShowAddUser(!showAddUser); + }; + + const inviteMe = useMemo(() => { + return !!invites.find(el => el.id === organization.id); + }, [organization.id, invites]); + + const isMyOrg = useMemo(() => { + return !!( + isAuth() && + organization?.id && + organization?.owner.user_id === user.id + ); + }, [organization]); + if (!organization.id) { + return ( + + + + + + + ); + } else { + return ( + + + + + { + setShowAddUser(false); + }} + setError={() => console.log('error')} + type={ + organization?.organization_type?.toLowerCase() === + AUDITOR.toLowerCase() + ? 'auditor' + : 'organization' + } + /> + + {/**/} + + dispatch(clearUserMessages())} + /> + + + + + {organization.name} + + + + + + Name + {organization.name} + + + Telegram + + {organization.contacts?.telegram} + + + + E-mail + + {organization.contacts?.email} + + + + Type + + {organization.organization_type} + + + + + + + {organization.members.map(value => { + const labelId = `checkbox-list-secondary-label-${value}`; + return ( + + + + ); + })} + + + + + + + {/*{!matchXs && (*/} + {/* */} + {/*)}*/} + {/*{matchXs && }*/} + {/**/} + {/* {user.linked_accounts?.map(account => {*/} + {/* if (account.name.toLowerCase() === 'linkedin') {*/} + {/* return (*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* );*/} + {/* } else if (account.name.toLowerCase() === 'github') {*/} + {/* return (*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* );*/} + {/* } else {*/} + {/* return (*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* );*/} + {/* }*/} + {/* })}*/} + {/**/} + {isAuth() && isMyOrg && ( + + + + {/**/} + + )} + {inviteMe && !isMyOrg && ( + + + + + )} + { + dispatch(deleteInvites(organization.id, user.id)); + setIsOpenConfirm(false); + }} + handleDisagree={() => setIsOpenConfirm(false)} + isOpen={openConfirm} + /> + + + + + ); + } +}; + +export default Organization; + +const buttonsWrapper = theme => ({ + display: 'flex', + gap: '20px', + alignItems: 'center', + justifyContent: 'center', + '& button': { + margin: 'unset', + }, + [theme.breakpoints.down(630)]: { + flexDirection: 'column', + }, +}); + +const wrapper = theme => ({ + display: 'flex', + borderColor: 'grey', + flexDirection: 'column', + width: '100%', + '& ul': { + fontSize: '16px', + marginBottom: '28px', + '& li': { + marginLeft: '15px', + marginTop: '7px', + }, + }, + position: 'relative', + padding: '25px 30px 60px', + [theme.breakpoints.down('md')]: { + padding: '20px 24px 20px', + }, + [theme.breakpoints.down('sm')]: { + gap: '20px', + padding: '30px 10px 20px', + '& h3': { + fontSize: '20px', + }, + }, + [theme.breakpoints.down(780)]: { + borderRadius: '0!important', + }, +}); + +const aboutWrapper = theme => ({ + width: '100%', + '& span': { + marginRight: '50px', + maxWidth: '125px', + width: '100%', + }, + [theme.breakpoints.down('md')]: { + '& span': { + maxWidth: '90px', + marginRight: '20px', + }, + }, + [theme.breakpoints.down(450)]: { + '& span': { + maxWidth: '70px', + }, + }, +}); + +const innerWrapper = theme => ({}); + +const infoInnerStyle = theme => ({ + display: 'flex', + flexDirection: 'column', + gap: '16px', + paddingTop: '10px', +}); + +const userListSx = theme => ({ + width: '100%', + [theme.breakpoints.down('sm')]: { + '& li': { + marginLeft: '0px!important', + }, + }, +}); + +const infoStyle = theme => ({ + display: 'flex', + margin: '0 0 50px', + flexDirection: 'row', + // flexWrap: 'wrap', + width: '100%', + gap: '40px', + rowGap: '15px', + '& .tagsWrapper': { + width: '100%', + }, + [theme.breakpoints.down('md')]: { + gap: '10px', + }, + [theme.breakpoints.down('sm')]: { + flexDirection: 'column', + gap: '16px', + margin: 0, + '& .tagsWrapper': { + width: '520px', + }, + }, +}); + +const avatarStyle = theme => ({ + width: '205px', + height: '205px', + [theme.breakpoints.down('xs')]: { + width: '150px', + height: '150px', + }, +}); + +const contentWrapper = theme => ({ + display: 'flex', + gap: '70px', + // justifyContent: 'space-between', + [theme.breakpoints.down('md')]: { + gap: '50px', + }, + [theme.breakpoints.down('sm')]: { + flexDirection: 'column', + alignItems: 'center', + gap: '40px', + }, +}); + +const buttonSx = theme => ({ + margin: '0 auto', + display: 'block', + color: theme.palette.background.default, + textTransform: 'capitalize', + fontWeight: 600, + fontSize: '18px', + // padding: '9px 50px', + width: '234px', + borderRadius: '10px', + [theme.breakpoints.down('xs')]: { + padding: '9px 10px', + }, +}); + +const submitAuditor = theme => ({ + backgroundColor: theme.palette.secondary.main, + '&:hover': { + backgroundColor: '#450e5d', + }, +}); + +const infoWrapper = theme => ({ + display: 'flex', + alignItems: 'center', + fontWeight: 500, + color: '#434242', + '& p': { + fontSize: 'inherit', + maxWidth: '250px', + }, + '& span': { + width: '125px', + marginRight: '50px', + color: '#B2B3B3', + }, + fontSize: '15px', + [theme.breakpoints.down('md')]: { + '& span': { + width: '90px', + marginRight: '20px', + }, + '& p': { + maxWidth: '190px', + }, + }, + [theme.breakpoints.down('sm')]: { + '& p': { + maxWidth: '240px', + }, + }, + [theme.breakpoints.down('xs')]: { + fontSize: '12px', + }, + [theme.breakpoints.down(450)]: { + '& span': { + width: '70px', + marginRight: '20px', + }, + '& p': { + maxWidth: '180px', + }, + }, +}); + +const accountLink = { + height: '30px', + display: 'flex', + alignItems: 'center', + color: 'black', + textDecoration: 'none', +}; diff --git a/src/components/OrganizationCard.jsx b/src/components/OrganizationCard.jsx new file mode 100644 index 00000000..cdbcc196 --- /dev/null +++ b/src/components/OrganizationCard.jsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { CUSTOMER } from '../redux/actions/types.js'; +import theme from '../styles/themes.js'; +import { Avatar, Box, Button, Typography } from '@mui/material'; +import { ASSET_URL } from '../services/urls.js'; +import PeopleIcon from '@mui/icons-material/People.js'; +import { useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom/dist'; + +const OrganizationCard = ({ org }) => { + const role = useSelector(s => s.user.user.current_role); + const navigate = useNavigate(); + + return ( + + + + + {org.name} + + + + {org.members.length} + + {org.organization_type} + + + + + ); +}; + +export default OrganizationCard; + +const wrapper = (theme, role) => ({ + backgroundColor: '#fff', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: '20px', + gap: '15px', + borderRadius: '1.5rem', + border: `1px solid ${ + role === CUSTOMER + ? theme.palette.primary.main + : theme.palette.secondary.main + }!important`, + [theme.breakpoints.down('sm')]: { + padding: '15px 10px', + }, +}); + +const avatarSx = theme => ({ + height: '200px', + width: '200px', + [theme.breakpoints.down('md')]: { + width: '150px', + height: '150px', + }, + [theme.breakpoints.down('xs')]: { + width: '100px', + height: '100px', + }, +}); + +const infoWrapper = theme => ({ + mt: '10px', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + [theme.breakpoints.down('md')]: { + '& p': { + fontSize: '16px', + }, + }, + [theme.breakpoints.down('sm')]: { + '& p': { + fontSize: '14px', + }, + }, +}); + +const buttonSx = theme => ({ + width: '100%', + borderRadius: '8px', + textTransform: 'unset', + [theme.breakpoints.down('md')]: { + fontSize: '16px', + }, + [theme.breakpoints.down('sm')]: { + fontSize: '14px', + }, +}); + +const iconSx = theme => ({ + [theme.breakpoints.down('md')]: { + width: '18px', + height: '18px', + }, +}); diff --git a/src/components/OrganizationList/OrganizationList.jsx b/src/components/OrganizationList/OrganizationList.jsx new file mode 100644 index 00000000..8a34f828 --- /dev/null +++ b/src/components/OrganizationList/OrganizationList.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import { Avatar, Box, Tooltip } from '@mui/material'; +import { ASSET_URL } from '../../services/urls.js'; +import { CUSTOMER } from '../../redux/actions/types.js'; +import theme from '../../styles/themes.js'; +import { useSelector } from 'react-redux'; + +const OrganizationList = ({ organizations }) => { + const user = useSelector(s => s.user.user); + const location = useLocation(); + + return ( + + {organizations?.map(org => { + if (org.avatar) { + return ( + + + + + + ); + } else { + return ( + + + + {org.name} + + + + ); + } + })} + + ); +}; + +export default OrganizationList; diff --git a/src/components/Project-card.jsx b/src/components/Project-card.jsx index 3a446b3b..d94eaf17 100644 --- a/src/components/Project-card.jsx +++ b/src/components/Project-card.jsx @@ -232,6 +232,22 @@ const ProjectCard = ({ type, project, currentRole, isPublic }) => { label="Publish" /> )} + {!isPublic && project?.auditor_organization?.id && + project?.status.toLowerCase() !== RESOLVED.toLowerCase() && + + Organization + + {project?.auditor_organization?.name} + + + } {type !== AUDITOR && ( )} + {(!!organizations.length || !!invites.length) && ( + + {!!invites.length && ( + <> + Invites + + + )} + {!!organizations.length && ( + <> + + Organization + + + + )} + + )} {isDetailsOpen ? ( @@ -327,6 +360,13 @@ const UserInfo = ({ role, linkId }) => { Edit + @@ -448,7 +488,8 @@ const buttonSx = theme => ({ textTransform: 'capitalize', fontWeight: 600, fontSize: '18px', - width: '214px', + // padding: '9px 50px', + width: '234px', borderRadius: '10px', [theme.breakpoints.down('xs')]: { padding: '9px 10px', @@ -505,6 +546,14 @@ const infoWrapper = theme => ({ }, }); +const accountLink = { + height: '30px', + display: 'flex', + alignItems: 'center', + color: 'black', + textDecoration: 'none', +}; + const ratingButton = { color: 'black', mt: '20px', diff --git a/src/components/UserListItem/UserLIstItem.jsx b/src/components/UserListItem/UserLIstItem.jsx new file mode 100644 index 00000000..0379c162 --- /dev/null +++ b/src/components/UserListItem/UserLIstItem.jsx @@ -0,0 +1,303 @@ +import React, { useEffect, useState } from 'react'; +import { + Avatar, + Box, + Button, + Checkbox, + Chip, + FormControlLabel, + ListItem, + ListItemAvatar, + ListItemText, + Modal, + Typography, +} from '@mui/material'; +import EditIcon from '@mui/icons-material/Edit.js'; +import { + addUserInOrganization, + changeAccessLevel, + deleteUserFromOrganization, +} from '../../redux/actions/organizationAction.js'; +import DeleteForeverRoundedIcon from '@mui/icons-material/DeleteForeverRounded.js'; +import ListItemButton from '@mui/material/ListItemButton'; +import { ASSET_URL } from '../../services/urls.js'; +import { useDispatch, useSelector } from 'react-redux'; +import CloseOutlinedIcon from '@mui/icons-material/CloseOutlined'; +import IconButton from '@mui/material/IconButton'; +import { addTestsLabel } from '../../lib/helper.js'; +import { ArrowBack } from '@mui/icons-material'; +import Radio from '@mui/material/Radio'; +import { CUSTOMER } from '../../redux/actions/types.js'; +import theme from '../../styles/themes.js'; + +const UserLIstItem = ({ value, labelId, organization }) => { + const user = useSelector(s => s.user.user); + const [isOpen, setIsOpen] = useState(false); + const dispatch = useDispatch(); + const [rulesOfMember, setRulesOfMember] = useState('Representative'); + + const handleClose = () => { + setRulesOfMember(value.access_level); + setIsOpen(false); + }; + + const handleChangeAccess = () => { + const data = [ + { + user_id: value?.user_id, + access_level: rulesOfMember, + }, + ]; + dispatch( + changeAccessLevel( + organization.id, + value.user_id, + { access_level: rulesOfMember }, + organization.link_id, + ), + ); + handleClose(); + }; + + useEffect(() => { + setRulesOfMember(value.access_level || 'Representative'); + }, [value.access_level]); + + return ( + + + + + + + + + + + + + Current organization + + + + {organization.name} + + + {`Change the role of ${value.username}`} + + + + + setRulesOfMember('Owner')} + /> + } + sx={{ marginX: '0' }} + label="Owner" + labelPlacement="right" + /> + + Has full control over organization management. + + + + setRulesOfMember('Editor')} + /> + } + label="Editor" + sx={{ marginX: '0' }} + labelPlacement="right" + /> + + Can manage audits and communicate on behalf of the + organization. + + + + setRulesOfMember('Representative')} + /> + } + label="Representative" + sx={{ marginX: '0' }} + labelPlacement="right" + /> + + Can communicate on behalf of the organization but cannot + manage audits. + + + + + + + + + ) + } + disablePadding + > + + + + + + {value.access_level.includes('Owner') && ( + + )} + + + ); +}; + +export default UserLIstItem; + +const roleDescriptionTitle = theme => ({ + fontSize: '16px', + color: '#9f9f9f', + marginLeft: '42px', + [theme.breakpoints.down('md')]: { + fontSize: '12px', + }, + [theme.breakpoints.down('xs')]: { + fontSize: '10px', + }, +}); + +const modalSx = theme => ({ + position: 'absolute', + bgcolor: 'background.paper', + borderRadius: '8px', + boxShadow: 24, + p: 3, + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + backgroundColor: 'white', + padding: '10px', + width: '700px', + [theme.breakpoints.down('sm')]: { + paddingBottom: '30px', + height: '100%', + width: '100%', + }, + [theme.breakpoints.down(500)]: { + width: '310px', + p: 2, + }, +}); + +const userActionSx = theme => ({ + minWidth: 'unset', + padding: '5px', + mx: '5px', +}); diff --git a/src/components/audit-request-info.jsx b/src/components/audit-request-info.jsx index 66a471ff..69c6bcfd 100644 --- a/src/components/audit-request-info.jsx +++ b/src/components/audit-request-info.jsx @@ -13,6 +13,12 @@ import { Modal, Tooltip, Collapse, + Popover, + Avatar, + ListItemAvatar, + ListItem, + ListItemText, + List, } from '@mui/material'; import { CustomCard } from './custom/Card.jsx'; import theme from '../styles/themes.js'; @@ -39,6 +45,11 @@ import EditTags from './EditDescription/EditTags.jsx'; import EditPrice from './EditDescription/EditPrice.jsx'; import ExpandLessOutlinedIcon from '@mui/icons-material/ExpandLessOutlined.js'; import EditIcon from '@mui/icons-material/Edit.js'; +import Star from './icons/Star.jsx'; +import Currency from './icons/Currency.jsx'; +import { ASSET_URL } from '../services/urls.js'; +import ListItemButton from '@mui/material/ListItemButton'; +import TypeChat from './Chat/TypeChat.jsx'; const AuditRequestInfo = ({ project = null, @@ -55,20 +66,55 @@ const AuditRequestInfo = ({ const navigate = useNavigate(); const [open, setOpen] = useState(false); + const [anchorEl, setAnchorEl] = useState(null); const [confirmDeclineOpen, setConfirmDeclineOpen] = useState(false); const [showAcceptButton, setShowAcceptButton] = useState(true); const [showFullHeader, setShowFullHeader] = useState(false); + const [visible, setVisible] = useState(false); + const organizations = useSelector(state => state.organization.organizations); const { auditor } = useSelector(s => s.auditor); - const { auditRequest, auditRequests, successMessage } = useSelector( + const [auditorData, setAuditorData] = useState({}); + const { auditRequest, auditRequests, successMessage, organizationAuditRequests } = useSelector( s => s.audits, ); const { user } = useSelector(s => s.user); const { chatList } = useSelector(s => s.chat); const [showFull, setShowFull] = useState(false); - const handleOpen = () => { + const handleClick = event => { + setAnchorEl(event.currentTarget); + setVisible(true); + }; + + const handleCloseAnchor = () => { + setAnchorEl(null); + }; + + const anchorOrigin = { + vertical: visible ? 'bottom' : 'top', + horizontal: 'left', + }; + + const transformOrigin = { + vertical: visible ? 'top' : 'bottom', + horizontal: 'left', + }; + + const openAnchor = Boolean(anchorEl); + const id = openAnchor ? 'simple-popover' : undefined; + + const handleOpen = event => { if (user.current_role === AUDITOR && isAuth() && auditor?.first_name) { - setOpen(true); + if (!organizations.length) { + setAuditorData(auditor); + setOpen(true); + } else { + if (auditRequest?.auditor_organization) { + handleChose(auditRequest?.auditor_organization); + } else { + handleClick(event); + } + } } else if ( user.current_role !== AUDITOR && isAuth() && @@ -76,6 +122,7 @@ const AuditRequestInfo = ({ ) { dispatch(changeRolePublicAuditorNoRedirect(AUDITOR, user.id, auditor)); handleError(); + setAuditorData(auditor); setOpen(true); } else if ( !auditor?.first_name && @@ -90,12 +137,18 @@ const AuditRequestInfo = ({ ) { dispatch(changeRolePublicAuditor(AUDITOR, user.id, auditor)); handleError(); + setAuditorData(auditor); setOpen(true); } else { navigate('/sign-in'); } }; + const handleChose = auditor => { + setAuditorData(auditor); + setOpen(true); + }; + // const handleClose = () => { setOpen(false); }; @@ -140,11 +193,19 @@ const AuditRequestInfo = ({ }; const handleAccept = () => { + + const isRequestFound = auditRequests?.find( req => req.id === auditRequest.id, + ) || organizationAuditRequests?.find( + req => req.id === auditRequest.id, ); if (isRequestFound) { - dispatch(confirmAudit(auditRequest, true, `/audit/${auditRequest.id}`)); + if (isRequestFound?.auditor_organization) { + dispatch(confirmAudit({ ...isRequestFound, auditor_organization: isRequestFound?.auditor_organization.id }, true, `/audit/${isRequestFound.id}`)); + } else { + dispatch(confirmAudit(isRequestFound, true, `/audit/${isRequestFound.id}`)); + } } }; @@ -445,6 +506,66 @@ const AuditRequestInfo = ({ > Make offer + + + handleChose(auditor)}> + + + + + + + + {organizations.map(org => { + const member = org.members.find( + member => member.user_id === user.id, + ); + const hasEditorAccess = + member.access_level === 'Editor' || + member.access_level === 'Owner'; + + return ( + { + if (hasEditorAccess) { + handleChose(org); + } + }} + > + + + + + + + + ); + })} + + {showAcceptButton && auditRequest && !isModal && @@ -469,7 +590,7 @@ const AuditRequestInfo = ({ disableScrollLock > { + - {auditor.first_name} {auditor.last_name} - + {auditor.first_name || auditor.name} {auditor.last_name || ''} + + {auditor.owner && ( + + Organization + + )} + @@ -70,6 +77,13 @@ const nameStyle = theme => ({ }, }); +const organizationStyle = theme => ({ + fontWeight: '500', + color: '#434242', + fontSize: '12px!important', + marginTop: '2px', +}); + const statusStyle = theme => ({ fontWeight: '500', color: '#434242', diff --git a/src/components/custom/customMessage.jsx b/src/components/custom/customMessage.jsx index 34f2bfd3..9cbc582f 100644 --- a/src/components/custom/customMessage.jsx +++ b/src/components/custom/customMessage.jsx @@ -8,6 +8,7 @@ import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; import { readMessage } from '../../redux/actions/websocketAction.js'; import { useNavigate } from 'react-router-dom/dist'; import NotificationMessage from '../NotificationMessage.jsx'; +import { getOrganizationById } from '../../redux/actions/organizationAction.js'; const CustomMessage = ({ message }) => { const role = useSelector(s => s.user.user.current_role); @@ -116,6 +117,11 @@ const CustomMessage = ({ message }) => { + ); +}; + +const CreateEditOrganizationForm = ({ + role, + needLoad, + organization, + newLinkId, +}) => { + const matchSm = useMediaQuery(theme.breakpoints.down('sm')); + const matchXs = useMediaQuery(theme.breakpoints.down('xs')); + const dispatch = useDispatch(); + const { user } = useSelector(s => s.user); + const { customer } = useSelector(s => s.customer); + const { auditor } = useSelector(s => s.auditor); + const navigate = useNavigate(); + const [isDirty, setIsDirty] = useState(false); + const [deletedAvatar, setDeletedAvatar] = useState(null); + const formData = new FormData(); + + const sendAvatar = async (withSave = false) => { + if (formData.get('file')) { + + try { + + const { data } = await axios.post(ASSET_URL, formData, { + headers: { Authorization: 'Bearer ' + Cookies.get('token') }, + }); + + const avatar = data.id; + if (withSave) { + if (role === AUDITOR) { + dispatch(updateAuditor({ avatar }, false)); + } else { + dispatch(updateCustomer({ avatar }, false)); + } + } + return avatar; + } catch (err) { + setError('Error while uploading file'); + console.error(err); + console.log(err, 'err'); + } finally { + formData.delete('file'); + formData.delete('private'); + formData.delete('original_name'); + formData.delete('file_entity'); + formData.delete('parent_entity_id'); + formData.delete('parent_entity_source'); + } + } + return null; + }; + + if (!organization.id && needLoad) { + return ( + + + + ); + } else { + return ( + { + const avatar = await sendAvatar(); + + setSubmitting(true); + setIsDirty(false); + + if (deletedAvatar) { + try { + await axios.delete(`${ASSET_URL}/id/${deletedAvatar}`, { + headers: { Authorization: 'Bearer ' + Cookies.get('token') }, + }); + } catch (e) {} + } + + if (avatar) { + values.avatar = avatar; + } + + if (!values.id) { + if ( + values.organization_type?.toLowerCase() !== + user.current_role?.toLowerCase() + ) { + dispatch( + changeRoleCreateOrganization( + user.current_role?.toLowerCase() !== AUDITOR.toLowerCase() + ? AUDITOR + : CUSTOMER, + user.id, + values, + `/${user.current_role[0]}/${user.id}`, + ), + ); + } else { + dispatch( + createOrganization( + values, + `/${user.current_role[0]}/${user.id}`, + ), + ); + } + } else { + dispatch( + updateOrganization(values, `/${user.current_role[0]}/${user.id}`), + ); + } + resetForm(); + }} + > + {({ handleSubmit, values, setFieldValue, dirty }) => { + useEffect(() => { + setIsDirty(dirty); + }, [dirty]); + useEffect(() => { + const unblock = history.block(({ location }) => { + if (!isDirty) { + unblock(); + return navigate(location); + } + + const confirmed = window.confirm( + 'Do you want to save changes before leaving the page?', + ); + + if (confirmed) { + handleSubmit(values); + unblock(); + return navigate(location); + } else { + unblock(); + return navigate(location); + } + }); + + if (!isDirty) { + unblock(); + } + + return () => { + unblock(); + }; + }, [history, isDirty]); + + return ( +
+ + + + + + + {matchSm && ( + + + + )} + + + + {!matchSm && ( + <> + + {/**/} + + )} + + + + + { + setFieldValue( + 'contacts.public_contacts', + e.target.checked, + ); + }} + inputProps={{ + ...addTestsLabel('make-contacts-visible'), + }} + /> + + + + + + {/**/} + + setFieldValue('organization_type', e.target.value) + } + disabled={!!organization?.id} + > + + Organization type + + + + } + label="Customer" + /> + + } + label="Auditor" + /> + {user.current_role.slice(0, 1).toUpperCase() + + user.current_role.slice(1) !== + values.organization_type && ( + + The role you've assigned to the organization is + different from yours. As a result, your role will be + changed when the creation process is finished. + + )} + + + + + + +
+ ); + }} +
+ ); + } +}; + +export default CreateEditOrganizationForm; + +const EditOrganizationSchema = Yup.object().shape({ + name: Yup.string().required('required'), + // contacts: Yup.object().shape({ + // email: Yup.string().email('Invalid email').required('required'), + // telegram: Yup.string(), + // }), +}); + +const alertDescSx = theme => ({ + fontSize: '16px', + [theme.breakpoints.down('md')]: { + fontSize: '12px', + }, +}); + +const backBtnSx = theme => ({ + position: 'absolute', + left: '-70px', + top: '-30px', + [theme.breakpoints.down('sm')]: { + left: '-50px', + }, + [theme.breakpoints.down('xs')]: { + left: '-40px', + }, +}); + +const wrapper = theme => ({ + display: 'flex', + position: 'relative', + gap: '52px', + [theme.breakpoints.down('sm')]: { + flexDirection: 'column', + gap: '12px', + }, +}); + +const rateLabel = theme => ({ + fontSize: '15px', + color: '#B2B3B3', + fontWeight: 500, +}); + +const fieldsWrapper = theme => ({ + display: 'flex', + gap: '52px', + width: '100%', + '& p': { + color: '#B2B3B3', + }, + [theme.breakpoints.down('sm')]: { + flexDirection: 'column', + gap: '14px', + }, +}); + +const fieldWrapper = theme => ({ + width: '100%', + gap: '18px', + display: 'flex', + flexDirection: 'column', + '& .tags-array-wrapper': { + margin: 'auto 0 0 0', + }, + '& .password-wrapper': { + gap: '12px', + }, + '& .field-wrapper': { + gap: '12px', + }, + [theme.breakpoints.down('sm')]: { + gap: '14px', + '& .field-wrapper': { + gap: '8px', + }, + '& .password-wrapper': { + gap: '8px', + }, + }, + [theme.breakpoints.down('xs')]: { + '& p': { + fontSize: '12px', + }, + '& input': { + fontSize: '12px', + }, + }, +}); + +const buttonSx = { + display: 'block', + margin: '70px auto 0', + textTransform: 'unset', + padding: '13px 0', + fontWeight: 600, + fontSize: '14px', + lineHeight: 1.2, + width: '200px', + borderRadius: '10px', +}; + +const avatarWrapper = theme => ({ + '& button': { + textTransform: 'unset', + '& svg': { marginRight: '5px' }, + }, + '& .MuiAvatar-root': { + width: '205px', + height: '205px', + }, + [theme.breakpoints.down('md')]: { + '& .MuiAvatar-root': { + width: '158px', + height: '158px', + }, + '& button': { fontSize: '12px' }, + }, + [theme.breakpoints.down('sm')]: { + '& .MuiAvatar-root': { + width: '128px', + height: '128px', + }, + display: 'flex', + gap: '22px', + '& p': { color: '#B2B3B3' }, + }, + [theme.breakpoints.down('xs')]: { + '& .MuiAvatar-root': { + width: '80px', + height: '80px', + }, + '& button': { + paddingX: '4px', + fontSize: '10px', + '& svg': { marginRight: 0 }, + }, + }, +}); diff --git a/src/components/forms/edit-profile-form/edit-profile-form.jsx b/src/components/forms/edit-profile-form/edit-profile-form.jsx index fbe0b986..f17e716b 100644 --- a/src/components/forms/edit-profile-form/edit-profile-form.jsx +++ b/src/components/forms/edit-profile-form/edit-profile-form.jsx @@ -82,6 +82,7 @@ const EditProfileForm = ({ role, newLinkId }) => { }; const sendAvatar = async (withSave = false) => { + if (formData.get('file')) { try { const { data } = await axios.post(ASSET_URL, formData, { diff --git a/src/components/header/UserMenu.jsx b/src/components/header/UserMenu.jsx index 7a01e056..3613e398 100644 --- a/src/components/header/UserMenu.jsx +++ b/src/components/header/UserMenu.jsx @@ -26,6 +26,7 @@ export const UserMenu = ({ open, handleClose, anchor, userAvatar, pages }) => { const { customer } = useSelector(s => s.customer); const navigate = useNavigate(); const matchSm = useMediaQuery(theme.breakpoints.down(1080)); + const organizations = useSelector(s => s.organization.organizations); const currentRole = useSelector(s => s.user.user.current_role); const { differentRoleUnreadMessages } = useSelector(s => s.chat); @@ -49,6 +50,10 @@ export const UserMenu = ({ open, handleClose, anchor, userAvatar, pages }) => { navigate(`/${rolePrefix}/${link_id}`); }; + const handleMyOrganizationClick = () => { + navigate(`/my-organizations`); + }; + return ( { onClick={handleMyAccountClick} {...addTestsLabel('header_my-account')} > - My Account + My account + + + {/*{!!organizations.length && (*/} + + + {/*)}*/} {matchSm && pages?.map(el => el.menuOptions diff --git a/src/components/homepage/auditors-projects-section/PublicProjectCard.jsx b/src/components/homepage/auditors-projects-section/PublicProjectCard.jsx index 1ae06ab3..613ea40e 100644 --- a/src/components/homepage/auditors-projects-section/PublicProjectCard.jsx +++ b/src/components/homepage/auditors-projects-section/PublicProjectCard.jsx @@ -14,6 +14,8 @@ import React, { useState } from 'react'; import AuditRequestInfo from '../../audit-request-info.jsx'; import CustomSnackbar from '../../custom/CustomSnackbar.jsx'; import { addTestsLabel } from '../../../lib/helper.js'; +import { useDispatch, useSelector } from 'react-redux'; +import { clearMessage } from '../../../redux/actions/auditAction.js'; const PublicProjectCard = ({ project }) => { const navigate = useNavigate(); @@ -23,7 +25,10 @@ const PublicProjectCard = ({ project }) => { const handleView = e => { setOpenModal(true); }; - + const dispatch = useDispatch(); + const { auditRequest, auditRequests, successMessage } = useSelector( + s => s.audits, + ); const handleCloseModal = () => { setOpenModal(false); }; @@ -58,13 +63,14 @@ const PublicProjectCard = ({ project }) => { { + dispatch(clearMessage()); setMessage(null); setErrorMessage(null); }} severity={errorMessage ? 'error' : 'success'} - text={message || errorMessage} + text={message || errorMessage || successMessage} /> diff --git a/src/components/modal/OfferModal.jsx b/src/components/modal/OfferModal.jsx index ded816ff..6828e71b 100644 --- a/src/components/modal/OfferModal.jsx +++ b/src/components/modal/OfferModal.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { Field, Form, Formik } from 'formik'; import * as Yup from 'yup'; import dayjs from 'dayjs'; @@ -28,10 +28,12 @@ const OfferModal = ({ stayHere, }) => { const dispatch = useDispatch(); + const auditorReducer = useSelector(s => s.auditor.auditor); const initialValues = { - auditor_id: auditor?.user_id, - auditor_contacts: { ...auditor?.contacts }, + auditor_organization: auditor?.id, + auditor_id: auditorReducer?.user_id, + auditor_contacts: { ...auditorReducer?.contacts }, customer_contacts: { ...project?.creator_contacts }, customer_id: project?.customer_id, last_changer: user.current_role, @@ -50,7 +52,7 @@ const OfferModal = ({ scope: project?.scope || project?.project_scope || { type: SCOPE_LINKS, content: [] }, time_frame: '', - }; + } return ( @@ -68,7 +70,6 @@ const OfferModal = ({ onSubmit={values => { const newValue = { ...values, - auditor_id: auditor?.user_id, auditor_contacts: { ...auditor?.contacts }, price: parseInt(values.price), total_cost: parseInt(values.total_cost), diff --git a/src/pages/AuditorsPage.jsx b/src/pages/AuditorsPage.jsx index f7b68398..0aeabce6 100644 --- a/src/pages/AuditorsPage.jsx +++ b/src/pages/AuditorsPage.jsx @@ -75,6 +75,7 @@ const AuditorsPage = () => { return { ...data, page }; }); }; + const previousPath = location.state?.from || '/'; useEffect(() => { if (query) { diff --git a/src/pages/ChatPage.jsx b/src/pages/ChatPage.jsx index 1678fe83..3640ff3d 100644 --- a/src/pages/ChatPage.jsx +++ b/src/pages/ChatPage.jsx @@ -1,19 +1,34 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; -import { Box, Button, IconButton, useMediaQuery } from '@mui/material'; +import { + Avatar, + Box, + Button, + Collapse, + Divider, + IconButton, + Tooltip, + useMediaQuery, +} from '@mui/material'; import Layout from '../styles/Layout.jsx'; import { CustomCard } from '../components/custom/Card'; import ChatList from '../components/Chat/ChatList.jsx'; import CurrentChat from '../components/Chat/CurrentChat.jsx'; -import { chatSetError, setCurrentChat } from '../redux/actions/chatActions.js'; +import { + chatSetError, + getChatListByOrg, + setCurrentChat, +} from '../redux/actions/chatActions.js'; import theme from '../styles/themes.js'; import MenuIcon from '@mui/icons-material/Menu.js'; -import { useNavigate } from 'react-router-dom/dist'; +import { useNavigate, useSearchParams } from 'react-router-dom/dist'; import ArrowBackIcon from '@mui/icons-material/ArrowBack.js'; import { AUDITOR, CUSTOMER } from '../redux/actions/types.js'; import Headings from '../router/Headings.jsx'; import CustomSnackbar from '../components/custom/CustomSnackbar.jsx'; +import { ASSET_URL } from '../services/urls.js'; +import KeyboardDoubleArrowLeftIcon from '@mui/icons-material/KeyboardDoubleArrowLeft'; const ChatPage = () => { const dispatch = useDispatch(); @@ -22,12 +37,15 @@ const ChatPage = () => { const { id } = useParams(); const matchXs = useMediaQuery(theme.breakpoints.down('xs')); const [chatListIsOpen, setChatListIsOpen] = useState(matchXs && !id); - const { chatList, chatMessages, currentChat, error } = useSelector( - s => s.chat, - ); + const { chatList, chatMessages, currentChat, error, orgChatList } = + useSelector(s => s.chat); const { user } = useSelector(s => s.user); const { auditor } = useSelector(s => s.auditor); const { customer } = useSelector(s => s.customer); + const { organizations } = useSelector(s => s.organization); + const [searchParams, setSearchParams] = useSearchParams(); + const orgId = searchParams.get('org'); + const xsMatch = useMediaQuery(theme.breakpoints.down('xs')); useEffect(() => { if ( @@ -46,21 +64,44 @@ const ChatPage = () => { } }, [user, auditor, customer]); + const profile = useMemo(() => { + if (user.current_role.toLowerCase() === AUDITOR.toLowerCase()) { + return auditor; + } else { + return customer; + } + }, [user.current_role, auditor, customer]); + useEffect(() => { if (id && !currentChat?.isNew) { const chat = chatList.find(chat => chat.id === id); + const chatOrg = orgChatList?.find(chat => chat.id === id); const members = chat?.members.map(member => member.id); + const orgMembers = chatOrg?.members.map(member => member.id); const role = chat?.members.find(member => member.id !== user.id)?.role; - - if (chat) { - dispatch( - setCurrentChat(chat?.id, { - name: chat?.name, - avatar: chat?.avatar, - role, - members, - }), - ); + const orgRole = chatOrg?.members.find( + member => member.id !== user.id, + )?.role; + if (chat || chatOrg) { + if (orgId) { + dispatch( + setCurrentChat(chatOrg?.id, { + name: chatOrg?.name, + avatar: chatOrg?.avatar, + orgRole, + orgMembers, + }), + ); + } else { + dispatch( + setCurrentChat(chat?.id, { + name: chat?.name, + avatar: chat?.avatar, + role, + members, + }), + ); + } } } }, [id, chatList.length]); @@ -74,6 +115,31 @@ const ChatPage = () => { } }; + const handleChoose = org => { + const newSearchParams = new URLSearchParams(searchParams); + + if (org) { + newSearchParams.set('org', org.id); + navigate('/chat' + `?org=${org.id}`); + if (xsMatch) { + setChatListIsOpen(true); + } + } else { + newSearchParams.delete('org'); + } + + if (newSearchParams.get('org') === 'undefined') { + newSearchParams.delete('org'); + setSearchParams(newSearchParams); + } + }; + + useEffect(() => { + if (searchParams.get('org')) { + dispatch(getChatListByOrg('Organization', searchParams.get('org'))); + } + }, [searchParams.get('org')]); + return ( @@ -101,38 +167,97 @@ const ChatPage = () => { > - - - - {id ? ( - - ) : ( - - setChatListIsOpen(prev => !prev)} - color="inherit" - sx={menuButtonSx} + + + <> + - - - - - Please select a chat to start messaging... + + + + + {organizations.map(org => ( + handleChoose(org)} + > + + + + + ))} + + + + - )} + setChatListIsOpen(false)} + /> + {id ? ( + + ) : ( + + setChatListIsOpen(prev => !prev)} + color="inherit" + sx={menuButtonSx} + > + + + + + Please select a chat to start messaging... + + + )} + @@ -141,18 +266,109 @@ const ChatPage = () => { export default ChatPage; +const orgListItemSx = theme => ({ + padding: '5px', + borderRadius: '8px', + cursor: 'pointer', + width: '70px', + [theme.breakpoints.down('sm')]: { + width: '50px', + }, +}); + const layoutSx = theme => ({ paddingY: '10px !important', - [theme.breakpoints.down('xs')]: { + [theme.breakpoints.down('md')]: { paddingY: '10px !important', }, }); +const mobileChatListOpenBackground = theme => ({ + display: 'none', + [theme.breakpoints.down('xs')]: { + display: 'block', + width: '30%', + position: 'absolute', + top: 0, + right: 0, + bottom: '-1px', + zIndex: 20, + background: 'rgba(0, 0, 0, .1)', + }, + [theme.breakpoints.down(500)]: { + display: 'none', + }, +}); + +const orgListSx = theme => ({ + display: 'flex', + flexDirection: 'column', + minWidth: '72px', + border: '2px solid #e5e5e5', + borderRight: 'unset', + padding: '0 0 5px', + overflowY: 'auto', + overflowX: 'hidden', + height: '100%', + '::-webkit-scrollbar': { + width: '0px', + }, + [theme.breakpoints.down('sm')]: { + minWidth: '52px', + }, +}); + +const leftSideSx = theme => ({ + width: '30%', + display: 'flex', + justifyContent: 'end', + [theme.breakpoints.down('xs')]: { + display: 'none', + }, +}); + +const mobileChatListOpen = theme => ({ + overflowY: 'auto', + '::-webkit-scrollbar': { + width: '4px', + }, + [theme.breakpoints.down('xs')]: { + background: '#fcfaf6', + position: 'absolute', + top: 0, + left: 0, + bottom: 0, + zIndex: 20, + display: 'block', + width: '70%', + }, + [theme.breakpoints.down(500)]: { + width: '100%', + }, +}); + +const orgAvatarSx = theme => ({ + width: '60px', + height: '60px', + [theme.breakpoints.down('sm')]: { + width: '40px', + height: '40px', + }, + // backgroundColor: '#fff', +}); + +const selectedTab = (theme, primary) => ({ + backgroundColor: primary + ? theme.palette.secondary.main + : theme.palette.primary.main, +}); + const wrapper = theme => ({ minHeight: '300px', padding: '0px 20px 20px', position: 'relative', display: 'flex', + maxWidth: 'unset', flexDirection: 'column', alignItems: 'flex-start', [theme.breakpoints.down('sm')]: { @@ -161,11 +377,25 @@ const wrapper = theme => ({ [theme.breakpoints.down(780)]: { paddingX: '10px', borderRadius: '0', + // [theme.breakpoints.down('xs')]: { + // // padding: '20px 40px 50px', + // minHeight: '300px', + // gap: '8px', + // padding: '10px 10px 30px', + }, +}); + +const chatInnerWrapper = theme => ({ + display: 'flex', + flexDirection: 'row', + width: '100%', + height: 'calc(100vh - 160px)', + [theme.breakpoints.down('sm')]: { + height: 'calc(100vh - 130px)', }, }); const chatWrapper = { - height: 'calc(100vh - 126px)', width: '100%', display: 'flex', border: '2px solid #e5e5e5', @@ -175,6 +405,9 @@ const chatWrapper = { const selectLabelWrapper = { position: 'relative', width: '70%', + // [theme.breakpoints.down(1760)]: { + // width: '65%', + // }, [theme.breakpoints.down('sm')]: { width: '100%', }, diff --git a/src/pages/CreateEditOrganization.jsx b/src/pages/CreateEditOrganization.jsx new file mode 100644 index 00000000..f31e3dac --- /dev/null +++ b/src/pages/CreateEditOrganization.jsx @@ -0,0 +1,73 @@ +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Layout from '../styles/Layout.jsx'; +import { CustomCard } from '../components/custom/Card.jsx'; +import Headings from '../router/Headings.jsx'; +import CreateEditOrganizationForm from '../components/forms/create-edit-organization-form/CreateEditOrganizationForm.jsx'; +import { useParams } from 'react-router-dom/dist'; +import { + clearOrganization, + getOrganizationById, +} from '../redux/actions/organizationAction.js'; +import ChangeLinkId from '../components/forms/change-link-id/index.jsx'; +import NotFound from './Not-Found.jsx'; +import { Box } from '@mui/material'; +import Loader from '../components/Loader.jsx'; + +const CreateEditOrganization = () => { + const { id } = useParams(); + const dispatch = useDispatch(); + const role = useSelector(s => s.user.user.current_role); + const organization = useSelector(s => s.organization.organization); + const notFound = useSelector(s => s.organization.notFound); + const [newLinkId, setNewLinkId] = useState(null); + const [showChangeLinkId, setShowChangeLinkId] = useState(false); + + useEffect(() => { + if (id) { + dispatch(getOrganizationById(id)); + } + return () => { + // if (id) { + dispatch(clearOrganization()); + // } + }; + }, [id]); + + if (!notFound) { + return ( + + + + + + {organization.id && ( + + )} + + + ); + } else { + return ; + } +}; + +export default CreateEditOrganization; + +const editWrapper = theme => ({ + padding: '41px 68px 70px', + [theme.breakpoints.down('sm')]: { + padding: '41px 48px 50px', + }, + [theme.breakpoints.down('xs')]: { + padding: '31px 28px 40px', + }, +}); diff --git a/src/pages/CustomersPage.jsx b/src/pages/CustomersPage.jsx new file mode 100644 index 00000000..64503beb --- /dev/null +++ b/src/pages/CustomersPage.jsx @@ -0,0 +1,234 @@ +import React, { useEffect, useState } from 'react'; +import Layout from '../styles/Layout.jsx'; +import { Box, Button, useMediaQuery } from '@mui/material'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack.js'; +import Filter from '../components/forms/filter/index.jsx'; +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigate, useSearchParams } from 'react-router-dom/dist'; +import AuditorListCard from '../components/AuditorListCard.jsx'; +import theme from '../styles/themes.js'; +import CustomPagination from '../components/custom/CustomPagination.jsx'; +import { addTestsLabel } from '../lib/helper.js'; +import Headings from '../router/Headings.jsx'; +import { searchCustomer } from '../redux/actions/customerAction.js'; +import { useLocation } from 'react-router-dom'; +import CustomerListCard from '../components/CustomerListCard.jsx'; +import { getOrganizationById } from '../redux/actions/organizationAction.js'; +import { clearUserMessages } from '../redux/actions/userAction.js'; +import CustomSnackbar from '../components/custom/CustomSnackbar.jsx'; + +const CustomersPage = () => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const matchSm = useMediaQuery(theme.breakpoints.down('sm')); + const matchXs = useMediaQuery(theme.breakpoints.down('xs')); + const [searchParams, setSearchParams] = useSearchParams(); + const [query, setQuery] = useState(undefined); + const customers = useSelector(s => s.customer.customers); + const totalCustomers = useSelector(s => s.customer.searchTotalCustomers); + const [projectIdToInvite, setProjectIdToInvite] = useState(() => + searchParams.get('projectIdToInvite'), + ); + const successMessage = useSelector(s => s.customer.successMessage); + + const location = useLocation(); + const [currentPage, setCurrentPage] = useState( + +searchParams.get('page') || 1, + ); + + const applyFilter = filter => { + setQuery(query => { + const { ...data } = query || {}; + setCurrentPage(1); + return { + ...data, + page: 1, + sort: filter.sort || '', + sort_by: filter.sort_by || '', + search: filter.search || '', + tags: filter.tags || [], + dateFrom: filter.dateFrom || '', + dateTo: filter.dateTo || '', + from: filter.price.from || '', + to: filter.price.to || '', + readyToWait: filter.readyToWait || '', + }; + }); + dispatch(searchCustomer(filter)); + }; + + const initialFilter = { + page: searchParams.get('page') || 1, + search: searchParams.get('search') || '', + tags: searchParams.getAll('tags') || [], + dateFrom: searchParams.get('dateFrom') || new Date(), + dateTo: searchParams.get('dateTo') || new Date(), + }; + + const getNumberOfPages = () => { + return Math.ceil(totalCustomers / 10); + }; + + const handleChangePage = (e, page) => { + setCurrentPage(page); + setQuery(prev => { + const { ...data } = prev || initialFilter; + return { ...data, page }; + }); + }; + + useEffect(() => { + if (query) { + setSearchParams({ ...query }); + } + }, [query]); + + useEffect(() => { + dispatch(searchCustomer(initialFilter)); + }, [searchParams.toString()]); + + useEffect(() => { + setCurrentPage(+searchParams.get('page') || 1); + }, [searchParams.toString()]); + + useEffect(() => { + dispatch(getOrganizationById(searchParams.get('organization'))); + }, [searchParams.get('organization')]); + + const previousPath = location.state?.from || '/'; + + return ( + + + dispatch(clearUserMessages())} + /> + + + + + + + + 0} + count={getNumberOfPages()} + sx={{ mb: '20px' }} + page={currentPage} + onChange={handleChangePage} + showFirstLast={!matchXs} + size="small" + /> + {customers?.length > 0 && ( + + {customers?.map((customer, idx) => ( + + + + ))} + {!matchSm && customers?.length % 2 === 1 && ( + + )} + + )} + {customers?.length === 0 && No results} + 0} + count={getNumberOfPages()} + sx={{ display: 'flex', justifyContent: 'flex-end' }} + page={currentPage} + onChange={handleChangePage} + showFirstLast={!matchXs} + size="small" + /> + + + ); +}; + +export default CustomersPage; + +const wrapper = theme => ({ + width: '100%', + padding: '20px', + backgroundColor: '#FCFAF6', + border: '1.42857px solid #D9D9D9', + boxShadow: + '0px 71.4286px 57.1429px rgba(0, 0, 0, 0.07),' + + ' 0px 29.8412px 23.8729px rgba(0, 0, 0, 0.0503198), ' + + '0px 15.9545px 12.7636px rgba(0, 0, 0, 0.0417275), ' + + '0px 8.94397px 7.15517px rgba(0, 0, 0, 0.035), ' + + '0px 4.75007px 3.80006px rgba(0, 0, 0, 0.0282725), ' + + '0px 1.97661px 1.58129px rgba(0, 0, 0, 0.0196802)', + borderRadius: '10.7143px', + minHeight: '1000px', +}); + +const headWrapper = theme => ({ + display: 'flex', + justifyContent: 'space-between', + mb: '20px', + [theme.breakpoints.down('sm')]: { + mb: '10px', + }, +}); + +const contentWrapper = { + display: 'flex', + flexWrap: 'wrap', + mb: '20px', + borderLeft: '1px solid #B2B3B3', +}; + +const auditorContainerStyle = idx => ({ + maxHeight: '200px', + minHeight: '150px', + borderRight: '1px solid #B2B3B3', + borderBottom: '1px solid #B2B3B3', + borderTop: idx <= 1 ? '1px solid #B2B3B3' : 'none', + width: { + zero: '100%', + sm: '50%', + md: '50%', + lg: '50%', + }, + [theme.breakpoints.down('sm')]: { + borderTop: idx === 0 ? '1px solid #B2B3B3' : 'none', + }, + [theme.breakpoints.down('xs')]: { + height: '130px', + }, +}); + +const fakeContainerStyle = { + width: { + zero: '100%', + sm: '50%', + md: '50%', + lg: '50%', + }, + border: '0.5px solid #B2B3B3', +}; + +const noResults = { + paddingTop: '70px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', +}; diff --git a/src/pages/MyOrganizations.jsx b/src/pages/MyOrganizations.jsx new file mode 100644 index 00000000..3e94def8 --- /dev/null +++ b/src/pages/MyOrganizations.jsx @@ -0,0 +1,301 @@ +import React, { useMemo, useEffect } from 'react'; +import { + Avatar, + Box, + Button, + Typography, + useMediaQuery, + Tooltip, + Grid, + Card, + CardMedia, + CardContent, + CardActions, +} from '@mui/material'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack.js'; +import theme from '../styles/themes.js'; +import { useNavigate, useParams, Link } from 'react-router-dom/dist'; +import { useDispatch, useSelector } from 'react-redux'; +import { AUDITOR, CUSTOMER } from '../redux/actions/types.js'; +import Layout from '../styles/Layout.jsx'; +import Loader from '../components/Loader.jsx'; +import { CustomCard } from '../components/custom/Card.jsx'; +import OrganizationCard from '../components/OrganizationCard.jsx'; +import Badge from '@mui/material/Badge'; +import { CLEAR_NOT_FOUND_ERROR } from '../redux/actions/types.js'; + +const MyOrganization = () => { + const role = useSelector(s => s.user.user.current_role); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const matchXs = useMediaQuery(theme.breakpoints.down('xs')); + const matchXxs = useMediaQuery(theme.breakpoints.down(590)); + const own = useSelector(s => s.organization.own); + const organizations = useSelector(s => s.organization.includeMe); + const invites = useSelector(s => s.organization.invites); + const loading = useSelector(s => s.organization.loading); + const errorRequest = useSelector(s => s.organization.errorRequest); + const { + customer, + error: customerError, + success: customerSuccess, + } = useSelector(s => s.customer); + const { + auditor, + error: auditorError, + success: auditorSuccess, + } = useSelector(s => s.auditor); + const { user, error } = useSelector(s => s.user); + + useEffect(() => { + return () => { + if (errorRequest) { + dispatch({type: CLEAR_NOT_FOUND_ERROR}); + } + } + }, [errorRequest]); + + return ( + + + + + + {!organizations.length && !own.length && loading && !errorRequest ? ( + + + + ) : ( + <> + {!!invites.length && ( + + Invites + + {invites?.map(org => { + return ( + + + + + + ); + })} + + + )} + {!!own.length && ( + + My organizations + + {own?.map(org => { + return ( + + + + ); + })} + + + )} + {!!organizations.length && ( + + Organizations + + {organizations?.map(org => { + return ( + + + + ); + })} + + + )} + + )} + + + + + + ); + // } +}; + +export default MyOrganization; + +const buttonSx = theme => ({ + margin: '0 auto', + display: 'block', + color: theme.palette.background.default, + textTransform: 'capitalize', + fontWeight: 600, + fontSize: '18px', + // padding: '9px 50px', + width: '234px', + borderRadius: '10px', + [theme.breakpoints.down('xs')]: { + padding: '9px 10px', + }, +}); + +const gridSx = theme => ({ + mt: '5px!important', + marginLeft: '-16px', + [theme.breakpoints.down('xs')]: { + marginLeft: '-8px', + }, +}); + +const gridItemSx = theme => ({ + width: '20%', + '& .MuiBadge-root': { + width: '100%', + }, + '& .org-card': { + width: '100%', + }, + [theme.breakpoints.down('md')]: { + width: '25%', + }, + [theme.breakpoints.down('sm')]: { + width: '33.33%', + }, + [theme.breakpoints.down('xs')]: { + width: '50%', + paddingTop: '15px', + paddingLeft: '15px', + }, +}); + +const wrapper = theme => ({ + display: 'flex', + flexDirection: 'column', + width: '100%', + position: 'relative', + '& ul': { + fontSize: '16px', + marginBottom: '28px', + '& li': { + marginLeft: '15px', + marginTop: '7px', + }, + }, + padding: '25px 30px 60px', + [theme.breakpoints.down('md')]: { + padding: '20px 24px 20px', + }, + [theme.breakpoints.down('sm')]: { + gap: '20px', + padding: '30px 10px 20px', + '& h3': { + fontSize: '20px', + }, + }, + [theme.breakpoints.down(780)]: { + borderRadius: '0!important', + }, +}); + +const innerWrapper = theme => ({ + width: '100%', + minHeight: '520px', + display: 'flex', + flexDirection: 'column', + gap: '30px', + justifyContent: 'space-between', + '& h4': { + textAlign: 'center', + }, + [theme.breakpoints.down('sm')]: {}, + [theme.breakpoints.down('xs')]: { + width: '100%', + gap: '25px', + '& h4': { + fontSize: '20px', + }, + '& .mobile-tag-wrapper': { + maxWidth: '380px', + }, + }, +}); + +const avatarSx = theme => ({ + height: '100px', + width: '100px', + padding: '5px', + border: `1px solid ${theme.palette.primary.main}`, + [theme.breakpoints.down('sm')]: { + width: '80px', + height: '80px', + }, + [theme.breakpoints.down('xs')]: { + width: '50px', + height: '50px', + }, +}); + +const organizationSx = theme => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + [theme.breakpoints.down('sm')]: { + '& p': { + fontSize: '14px', + }, + }, +}); + +const contentWrapper = theme => ({ + display: 'flex', + gap: '30px', + flexDirection: 'column', + alignItems: 'center', + '& a': { + textDecoration: 'none', + color: 'black', + }, +}); diff --git a/src/pages/Public-profile.jsx b/src/pages/Public-profile.jsx index c72a23be..8b9fd10f 100644 --- a/src/pages/Public-profile.jsx +++ b/src/pages/Public-profile.jsx @@ -48,6 +48,7 @@ import UserFeedbacks from '../components/UserFeedbacks.jsx'; import WalletConnectIcon from '../components/icons/WalletConnectIcon.jsx'; import { getPublicAuditsAuditor } from '../redux/actions/auditAction.js'; import ProjectCardList from '../components/Project-card-list.jsx'; +import TypeChat from '../components/Chat/TypeChat.jsx'; const PublicProfile = ({ notFoundRedirect = true }) => { const navigate = useNavigate(); @@ -552,16 +553,17 @@ const PublicProfile = ({ notFoundRedirect = true }) => { {data.kind !== 'badge' && isAuth() && data?.user_id !== user?.id && ( - + // + )} {role === AUDITOR && ( diff --git a/src/pages/audit-info.jsx b/src/pages/audit-info.jsx index a3aa960a..b2aca5cf 100644 --- a/src/pages/audit-info.jsx +++ b/src/pages/audit-info.jsx @@ -64,6 +64,7 @@ const AuditInfo = ({ const navigate = useNavigate(); const dispatch = useDispatch(); const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); const { successMessage, error } = useSelector(s => s.audits); const { user } = useSelector(s => s.user); const { chatList } = useSelector(s => s.chat); @@ -73,7 +74,11 @@ const AuditInfo = ({ const [isFeedbackModalOpen, setIsFeedbackModalOpen] = useState(false); const handleConfirm = () => { - dispatch(confirmAudit(audit, true)); + if (audit?.auditor_organization) { + dispatch(confirmAudit({ ...audit, auditor_organization: audit?.auditor_organization.id }, true)); + } else { + dispatch(confirmAudit(audit, true)); + } }; const handleDecline = () => { @@ -262,6 +267,22 @@ const AuditInfo = ({ isPublic ? { alignItems: 'flex-start' } : {}, ]} > + { + audit?.auditor_organization ? + + : + } {!!audit?.time?.from && !isPublic && ( Time for project: diff --git a/src/pages/profile-page.jsx b/src/pages/profile-page.jsx index 8b40d764..eff0fcb9 100644 --- a/src/pages/profile-page.jsx +++ b/src/pages/profile-page.jsx @@ -15,6 +15,7 @@ import CustomSnackbar from '../components/custom/CustomSnackbar.jsx'; import { isAuth } from '../lib/helper.js'; import PublicProfile from './Public-profile.jsx'; import NotFound from './Not-Found.jsx'; +import Organization from '../components/Organization.jsx'; const ProfilePage = () => { const dispatch = useDispatch(); @@ -31,21 +32,25 @@ const ProfilePage = () => { }, [tab]); if (linkId && role) { - if (/^c|a$/i.test(role)) { - const userLinkId = - user?.current_role === AUDITOR ? auditor?.link_id : customer?.link_id; - if ( - !isAuth() || - (user?.id && - linkId.toLowerCase() !== userLinkId?.toLowerCase() && - linkId.toLowerCase() !== user?.id) - ) { - return ; - } else if ( - (user?.current_role === AUDITOR && !/^a$/i.test(role)) || - (user?.current_role === CUSTOMER && !/^c$/i.test(role)) - ) { - return ; + if (/^c|a|o$/i.test(role)) { + if (role !== 'o') { + const userLinkId = + user?.current_role === AUDITOR ? auditor?.link_id : customer?.link_id; + if ( + !isAuth() || + (user?.id && + linkId.toLowerCase() !== userLinkId?.toLowerCase() && + linkId.toLowerCase() !== user?.id) + ) { + return ; + } else if ( + (user?.current_role === AUDITOR && !/^a$/i.test(role)) || + (user?.current_role === CUSTOMER && !/^c$/i.test(role)) + ) { + return ; + } + } else { + return ; } } else { return ; diff --git a/src/redux/actions/auditAction.js b/src/redux/actions/auditAction.js index 6e9892ee..8021c83e 100644 --- a/src/redux/actions/auditAction.js +++ b/src/redux/actions/auditAction.js @@ -21,6 +21,7 @@ import { GET_AUDITS_OF_AUDITOR, GET_PUBLIC_AUDIT, GET_PUBLIC_REPORT, + GET_ORGANIZATION_AUDITS, GET_REQUEST, IN_PROGRESS, NOT_FOUND, @@ -33,6 +34,7 @@ import { SET_CURRENT_AUDIT_PARTNER, UPDATE_AUDIT, VERIFY_AUDIT_REPORT, + GET_ORG_AUDIT_REQUEST, } from './types.js'; import { history } from '../../services/history.js'; import { ASSET_URL } from '../../services/urls.js'; @@ -70,6 +72,23 @@ export const createRequest = (values, redirect, navigateTo, stay) => { }; }; +export const getOrganizationAuditRequests = (org_id) => { + return dispatch => { + const token = Cookies.get('token'); + axios.get(`${API_URL}/audit_request/organization/id/${org_id}`, { + headers: { Authorization: `Bearer ${token}` }, + }) + .then(({ data }) => { + dispatch({ type: GET_ORG_AUDIT_REQUEST, payload: data }); + }) + .catch(({ response }) => { + console.log(response, 'res'); + dispatch({ type: REQUEST_ERROR }); + }); + }; +}; + + export const createRequestModal = values => { return dispatch => { const token = Cookies.get('token'); @@ -147,6 +166,17 @@ export const getAudits = role => { }; }; +export const getOrganizationAudits = () => { + return dispatch => { + const token = Cookies.get('token'); + axios.get(`${API_URL}/audit/organization/all`, { + headers: { Authorization: `Bearer ${token}` }, + }) + .then(({ data }) => { + dispatch({ type: GET_ORGANIZATION_AUDITS, payload: data }); + }); + }; +}; export const getPublicAudit = (id, code) => { return dispatch => { const token = Cookies.get('token'); diff --git a/src/redux/actions/auditorAction.js b/src/redux/actions/auditorAction.js index 755386b6..b65dcbb6 100644 --- a/src/redux/actions/auditorAction.js +++ b/src/redux/actions/auditorAction.js @@ -177,9 +177,9 @@ export const getAuditorRating = (id, getDetails = false) => { axios.get(url).then(({ data }) => { dispatch({ type: GET_AUDITOR_RATING_DETAILS, payload: data }); - }); - }; -}; + }) + } +} export const searchAuditor = (values, badges = true) => { const kind = badges ? 'auditor badge' : 'auditor'; @@ -196,10 +196,10 @@ export const searchAuditor = (values, badges = true) => { dispatch({ type: GET_AUDITORS, payload: data }); }) .catch(({ response }) => { - console.error(response, 'res'); - }); - }; -}; + console.error(response, 'res') + }) + } +} export const deleteBadgeProfile = id => { return dispatch => { diff --git a/src/redux/actions/chatActions.js b/src/redux/actions/chatActions.js index d35baacd..8898ff75 100644 --- a/src/redux/actions/chatActions.js +++ b/src/redux/actions/chatActions.js @@ -16,6 +16,7 @@ import { CHAT_SET_ERROR, CHAT_DELETE_MESSAGE, RECEIVE_NEW_CHAT, + CHAT_GET_LIST_ORG, } from './types.js'; export const getChatList = role => { @@ -29,6 +30,19 @@ export const getChatList = role => { }; }; +export const getChatListByOrg = (role, id) => { + const token = Cookies.get('token'); + return dispatch => { + axios + .get(`${API_URL}/chat/preview/${role}?org_id=${id}`, { + headers: { Authorization: `Bearer ${token}` }, + }) + .then(({ data }) => { + dispatch({ type: CHAT_GET_LIST_ORG, payload: data }); + }); + }; +}; + export const getChatMessages = (chatId, userId) => { const token = Cookies.get('token'); return dispatch => { @@ -69,7 +83,6 @@ export const setCurrentChat = ( const previousChatId = chat?.currentChat?.chatId; if (previousChatId === chatId) return; - if (previousChatId) { const token = Cookies.get('token'); axios.patch( @@ -104,7 +117,14 @@ export const setCurrentChat = ( }; }; -export const chatSendMessage = (text, to, fromRole, isFirst, kind = 'Text') => { +export const chatSendMessage = ( + text, + to, + fromRole, + isFirst, + kind = 'Text', + from_org_id, +) => { const token = Cookies.get('token'); const user = JSON.parse(localStorage.getItem('user')); @@ -123,6 +143,7 @@ export const chatSendMessage = (text, to, fromRole, isFirst, kind = 'Text') => { role: fromRole, text, kind, + from_org_id: from_org_id, }; } else { values = { @@ -130,6 +151,7 @@ export const chatSendMessage = (text, to, fromRole, isFirst, kind = 'Text') => { role: fromRole, text, kind, + from_org_id: from_org_id, }; } diff --git a/src/redux/actions/customerAction.js b/src/redux/actions/customerAction.js index 1c1fc8d5..0f5ac3bc 100644 --- a/src/redux/actions/customerAction.js +++ b/src/redux/actions/customerAction.js @@ -104,6 +104,26 @@ export const createCustomer = values => { }; }; +export const searchCustomer = (values, badges = true) => { + const kind = 'customer'; + const queryString = createSearchValues(values, kind); + + return dispatch => { + const token = Cookies.get('token'); + axios + .get( + `${API_URL}/search?${queryString}`, + isAuth() ? { headers: { Authorization: `Bearer ${token}` } } : {}, + ) + .then(({ data }) => { + dispatch({ type: GET_CUSTOMERS, payload: data }); + }) + .catch(({ response }) => { + console.error(response, 'res'); + }); + }; +}; + export const updateCustomer = (values, redirect = true) => { const token = Cookies.get('token'); return dispatch => { diff --git a/src/redux/actions/organizationAction.js b/src/redux/actions/organizationAction.js new file mode 100644 index 00000000..136f14d5 --- /dev/null +++ b/src/redux/actions/organizationAction.js @@ -0,0 +1,220 @@ +import Cookies from 'js-cookie'; +import axios from 'axios'; +import { + ACCEPT_INVITE, + ADD_MEMBER_IN_ORGANIZATION, + CLEAR_ORGANIZATION, + CREATE_ORGANIZATION, + DELETE_INVITES, + GET_AUDITORS, + GET_MY_ORGANIZATION, + GET_ORGANIZATION_AUDIT_REQUESTS, + GET_ORGANIZATION_BY_ID, + GET_ORGANIZATIONS, + NOT_FOUND_ORGANIZATION, + UPDATE_ORGANIZATION, + ERROR_GET_MY_ORGANIZATIONS, +} from './types.js'; +import { history } from '../../services/history.js'; +import createSearchValues from '../../lib/createSearchValues.js'; +import { isAuth } from '../../lib/helper.js'; + +const API_URL = import.meta.env.VITE_API_BASE_URL; + +export const createOrganization = (value, navigateTo) => { + return dispatch => { + const token = Cookies.get('token'); + axios + .post(`${API_URL}/organization`, value, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + .then(({ data }) => { + dispatch({ type: CREATE_ORGANIZATION, payload: data }); + if (navigateTo) { + history.push({ pathname: navigateTo }, { some: true }); + } + }) + .catch(({ response }) => { + console.log(response, 'res'); + }); + }; +}; + +export const updateOrganization = (value, navigateTo) => { + return dispatch => { + const token = Cookies.get('token'); + axios + .patch(`${API_URL}/organization/${value.id}`, value, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + .then(({ data }) => { + dispatch({ type: UPDATE_ORGANIZATION, payload: data }); + if (navigateTo) { + history.push({ pathname: `/o/${data.link_id}` }, { some: true }); + } + }) + .catch(({ response }) => { + console.log(response, 'res'); + }); + }; +}; + +export const clearOrganization = () => { + return dispatch => { + dispatch({ type: CLEAR_ORGANIZATION }); + }; +}; + +export const getMyOrganizations = () => { + return dispatch => { + const token = Cookies.get('token'); + axios + .get(`${API_URL}/my_organizations`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + .then(({ data }) => { + dispatch({ type: GET_MY_ORGANIZATION, payload: data }); + }) + .catch(({ response }) => { + dispatch({ type: ERROR_GET_MY_ORGANIZATIONS }); + console.log(response, 'res'); + }); + }; +}; + +export const searchOrganization = (values, badges = true) => { + const queryString = createSearchValues(values, 'Organization'); + + return dispatch => { + const token = Cookies.get('token'); + axios + .get( + `${API_URL}/search?${queryString}`, + isAuth() ? { headers: { Authorization: `Bearer ${token}` } } : {}, + ) + .then(({ data }) => { + dispatch({ type: GET_ORGANIZATIONS, payload: data }); + }) + .catch(({ response }) => { + console.error(response, 'res'); + }); + }; +}; + +export const getOrganizationById = id => { + return dispatch => { + const token = Cookies.get('token'); + axios + .get(`${API_URL}/organization/link_id/${id}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + .then(({ data }) => { + dispatch({ type: GET_ORGANIZATION_BY_ID, payload: data }); + }) + .catch(e => dispatch({ type: NOT_FOUND_ORGANIZATION })); + }; +}; + +export const addUserInOrganization = (orgLinkId, data, id) => { + return dispatch => { + const token = Cookies.get('token'); + axios + .post(`${API_URL}/organization/${id}/members`, data, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + .then(({ data }) => { + dispatch({ type: ADD_MEMBER_IN_ORGANIZATION }); + dispatch(getOrganizationById(orgLinkId)); + }); + }; +}; + +export const changeAccessLevel = (org_id, user_id, data, orgLinkId) => { + const token = Cookies.get('token'); + return dispatch => { + axios + .patch(`${API_URL}/organization/${org_id}/members/${user_id}`, data, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + .then(({ data }) => { + dispatch(getOrganizationById(orgLinkId)); + }); + }; +}; + +export const deleteUserFromOrganization = (orgId, userId, linkId) => { + return dispatch => { + const token = Cookies.get('token'); + axios + .delete(`${API_URL}/organization/${orgId}/members/${userId}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + .then(({ data }) => { + dispatch(getOrganizationById(linkId)); + }); + }; +}; + +export const acceptInvites = org_id => { + return dispatch => { + const token = Cookies.get('token'); + axios + .post( + `${API_URL}/organization/${org_id}/invites/confirm`, + {}, + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + ) + .then(({ data }) => { + dispatch({ type: ACCEPT_INVITE, payload: data }); + }); + }; +}; + +export const getAuditRequests = org_id => { + return dispatch => { + const token = Cookies.get('token'); + axios + .get(`${API_URL}/audit_request/organization/all`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + .then(({ data }) => { + dispatch({ type: GET_ORGANIZATION_AUDIT_REQUESTS, payload: data }); + }); + }; +}; + +export const deleteInvites = (org_id, user_id) => { + return dispatch => { + const token = Cookies.get('token'); + axios + .delete(`${API_URL}/organization/${org_id}/invites/${user_id}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + .then(({ data }) => { + dispatch({ type: DELETE_INVITES, payload: { ...data, id: org_id } }); + }); + // dispatch({ type: 'CHECK_INVITES', payload: data }); + }; +}; diff --git a/src/redux/actions/types.js b/src/redux/actions/types.js index d521e7f2..f44e6206 100644 --- a/src/redux/actions/types.js +++ b/src/redux/actions/types.js @@ -15,6 +15,7 @@ export const AUDITOR_SET_ERROR = 'AUDITOR_SET_ERROR'; export const CUSTOMER_SET_ERROR = 'CUSTOMER_SET_ERROR'; export const GET_CUSTOMER = 'GET_CUSTOMER'; export const GET_CUSTOMERS = 'GET_CUSTOMERS'; +export const GET_ORGANIZATIONS = 'GET_ORGANIZATIONS'; export const GET_AUDITOR = 'GET_AUDITOR'; export const GET_AUDITORS = 'GET_AUDITORS'; export const AUDITOR = 'auditor'; @@ -70,8 +71,11 @@ export const VERIFY_AUDIT_REPORT = 'VERIFY_AUDIT_REPORT'; export const RESTORE_PASSWORD = 'RESTORE_PASSWORD'; export const SEND_EMAIL = 'SEND_EMAIL'; export const RECEIVE_NEW_CHAT = 'RECEIVE_NEW_CHAT'; +export const ORGANIZATION_INVITE = 'ORGANIZATION_INVITE'; export const WEBSOCKET_CONNECT = 'WEBSOCKET_CONNECT'; export const WEBSOCKET_DISCONNECT = 'WEBSOCKET_DISCONNECT'; +export const CLEAR_SEARCH = 'CLEAR_SEARCH'; +export const WEBSOCKET_SEND = 'WEBSOCKET_SEND'; export const CLEAR_PROJECT = 'CLEAR_PROJECT'; export const WEBSOCKET_CONNECTED = 'WEBSOCKET_CONNECTED'; export const RECEIVE_AUDITOR_MESSAGE = 'RECEIVE_AUDITOR_MESSAGE'; @@ -106,6 +110,7 @@ export const NEED_UPDATE = 'NEED_UPDATE'; export const MERGE_ACCOUNT = 'MERGE_ACCOUNT'; export const DELETE_BADGE = 'DELETE_BADGE'; export const CHAT_GET_LIST = 'CHAT_GET_LIST'; +export const CHAT_GET_LIST_ORG = 'CHAT_GET_LIST_ORG'; export const CHAT_GET_MESSAGES = 'CHAT_GET_MESSAGES'; export const CHAT_NEW_MESSAGE = 'CHAT_NEW_MESSAGE'; export const CHAT_DELETE_MESSAGE = 'CHAT_DELETE_MESSAGE'; @@ -154,11 +159,25 @@ export const EDIT_AUDIT_REQUEST_CUSTOMER = 'EDIT_AUDIT_REQUEST_CUSTOMER'; export const NEED_TO_AUTH_GITHUB = 'NEED_TO_AUTH_GITHUB'; export const SWITCH_REPO = 'SWITCH_REPO'; export const NOT_FOUND_REPOS = 'NOT_FOUND_REPOS'; +export const CREATE_ORGANIZATION = 'CREATE_ORGANIZATION'; +export const UPDATE_ORGANIZATION = 'UPDATE_ORGANIZATION'; +export const CLEAR_ORGANIZATION = 'CLEAR_ORGANIZATION'; +export const GET_ORGANIZATION_BY_ID = 'GET_ORGANIZATION_BY_ID'; +export const GET_MY_ORGANIZATION = 'GET_MY_ORGANIZATION'; +export const ERROR_GET_MY_ORGANIZATIONS = 'ERROR_GET_MY_ORGANIZATIONS'; export const CLEAR_NOT_FOUND_ERROR = 'CLEAR_NOT_FOUND_ERROR'; export const GET_AUDIT_HISTORY = 'GET_AUDIT_HISTORY'; +export const GET_ORG_AUDIT_REQUEST = 'GET_ORG_AUDIT_REQUEST'; export const READ_AUDIT_HISTORY = 'READ_AUDIT_HISTORY'; export const READ_AUDIT_REQUEST_HISTORY = 'READ_AUDIT_REQUEST_HISTORY'; +export const ADD_MEMBER_IN_ORGANIZATION = 'ADD_MEMBER_IN_ORGANIZATION'; export const EDIT_AUDIT_CUSTOMER = 'EDIT_AUDIT_CUSTOMER'; +export const GET_ORGANIZATION_AUDITS = 'GET_ORGANIZATION_AUDITS'; +export const DELETE_INVITES = 'DELETE_INVITES'; +export const ACCEPT_INVITE = 'ACCEPT_INVITE'; +export const NOT_FOUND_ORGANIZATION = 'NOT_FOUND_ORGANIZATION'; +export const GET_ORGANIZATION_AUDIT_REQUESTS = + 'GET_ORGANIZATION_AUDIT_REQUESTS'; export const GET_AUDIT_REQUEST_HISTORY = 'GET_AUDIT_REQUEST_HISTORY'; export const GET_AUDITS_OF_AUDITOR = 'GET_AUDITS_OF_AUDITOR'; export const GET_PUBLIC_AUDIT = 'GET_PUBLIC_AUDIT'; diff --git a/src/redux/actions/userAction.js b/src/redux/actions/userAction.js index 9d45b2e5..444a0bcc 100644 --- a/src/redux/actions/userAction.js +++ b/src/redux/actions/userAction.js @@ -31,6 +31,8 @@ import { CLEAR_MESSAGES, AUDITOR, CUSTOMER, + GET_AUDITS, + CREATE_ORGANIZATION, } from './types.js'; import { savePublicReport } from './auditAction.js'; @@ -568,6 +570,49 @@ export const changeRolePublicAuditorNoRedirect = (role, id) => { }; }; +export const changeRoleCreateOrganization = (role, id, value, navigateTo) => { + return dispatch => { + axios + .patch( + `${API_URL}/user/${id}`, + { current_role: role }, + { + headers: { + Authorization: 'Bearer ' + Cookies.get('token'), + 'Content-Type': 'application/json', + }, + }, + ) + .then(({ data }) => { + dispatch({ type: SELECT_ROLE, payload: data }); + localStorage.setItem('user', JSON.stringify(data)); + const token = Cookies.get('token'); + if (data?.name) { + axios + .post(`${API_URL}/organization`, value, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + .then(({ data: orgData }) => { + dispatch({ type: CREATE_ORGANIZATION, payload: orgData }); + if (navigateTo) { + history.push( + { pathname: `${data.current_role[0]}/${data.id}` }, + { some: true }, + ); + } + }) + .catch(({ response }) => { + console.log(response, 'res'); + }); + } else { + history.push({ pathname: '/edit-profile' }, { some: true }); + } + }); + }; +}; + export const changeRolePublicCustomerNoRedirect = (role, id) => { return dispatch => { axios diff --git a/src/redux/middleware/websocketMiddleware.js b/src/redux/middleware/websocketMiddleware.js index ded24ceb..b3727f08 100644 --- a/src/redux/middleware/websocketMiddleware.js +++ b/src/redux/middleware/websocketMiddleware.js @@ -7,6 +7,7 @@ import { GET_NEW_REQUEST, IN_PROGRESS, NEED_UPDATE, + ORGANIZATION_INVITE, REQUEST_DECLINE, UPDATE_AUDIT_ISSUE_WS, WEBSOCKET_CONNECT, @@ -92,7 +93,7 @@ const websocketMiddleware = () => { } else if (message.kind.toLowerCase() === 'chatmessage') { const sameRole = store.getState().user.user.current_role.toLowerCase() === - message.user_role.toLowerCase(); + message?.user_role?.toLowerCase(); store.dispatch( receiveNewChatMessage(message.payload.ChatMessage, sameRole), ); @@ -136,6 +137,11 @@ const websocketMiddleware = () => { payload: { issue: payload.issue, auditId: payload.audit }, }); } + } else if (message.kind.toLowerCase() === 'organizationinvite') { + store.dispatch({ + type: ORGANIZATION_INVITE, + payload: message.payload.OrganizationInvite, + }); } }; diff --git a/src/redux/reducers/auditReducer.js b/src/redux/reducers/auditReducer.js index 061e6d29..36218c0f 100644 --- a/src/redux/reducers/auditReducer.js +++ b/src/redux/reducers/auditReducer.js @@ -36,6 +36,9 @@ import { VERIFY_AUDIT_REPORT, UPDATE_AUDIT, CREATE_AUDIT_ISSUE, + GET_ORGANIZATION_AUDIT_REQUESTS, + GET_ORG_AUDIT_REQUEST, + GET_ORGANIZATION_AUDITS } from '../actions/types.js'; const initialState = { @@ -52,6 +55,8 @@ const initialState = { approvedHistory: null, unreadHistory: null, auditRequestHistory: [], + organizationAuditRequests: [], + organizationAudits: [], verifyAudit: null, }; @@ -64,6 +69,16 @@ export const auditReducer = (state = initialState, action) => { successMessage: 'Audit request created successfully', auditRequest: action.payload, }; + case GET_ORG_AUDIT_REQUEST: + return { + ...state, + organizationAuditRequests: [...state.organizationAuditRequests, ...action.payload], + }; + case GET_ORGANIZATION_AUDITS: + return { + ...state, + organizationAudits: [...state.organizationAudits, ...action.payload], + }; case GET_AUDIT_REQUEST: return { ...state, auditRequests: action.payload }; case DELETE_REQUEST: @@ -96,6 +111,14 @@ export const auditReducer = (state = initialState, action) => { request => request.id !== action.payload.id, ), }; + case GET_ORGANIZATION_AUDIT_REQUESTS: + return { + ...state, + organizationAuditRequests: [ + ...state.organizationAuditRequests, + ...action.payload, + ], + }; case EDIT_AUDIT: return { ...state, diff --git a/src/redux/reducers/auditorReducer.js b/src/redux/reducers/auditorReducer.js index 68de033b..76474e92 100644 --- a/src/redux/reducers/auditorReducer.js +++ b/src/redux/reducers/auditorReducer.js @@ -12,6 +12,7 @@ import { GET_AUDITOR_RATING_DETAILS, CLEAR_CURRENT_AUDITOR_CUSTOMER, CLEAR_SEARCHED_AUDITOR, + CLEAR_SEARCH, } from '../actions/types.js'; const initialState = { @@ -59,6 +60,12 @@ export const auditorReducer = (state = initialState, action) => { ...state, currentAuditor: action.payload, }; + case CLEAR_SEARCH: + return { + ...state, + searchAuditors: null, + searchTotalAuditors: 0, + }; case SELECT_ROLE: return { ...state, currentAuditor: null }; case LOG_OUT: diff --git a/src/redux/reducers/chatReducer.js b/src/redux/reducers/chatReducer.js index cfcdb0a4..b517cc9d 100644 --- a/src/redux/reducers/chatReducer.js +++ b/src/redux/reducers/chatReducer.js @@ -3,6 +3,7 @@ import { CHAT_CLOSE_CURRENT_CHAT, CHAT_DELETE_MESSAGE, CHAT_GET_LIST, + CHAT_GET_LIST_ORG, CHAT_GET_MESSAGES, CHAT_NEW_MESSAGE, CHAT_SEND_FIRST_MESSAGE, @@ -18,6 +19,7 @@ import { const initialState = { chatList: [], chatMessages: [], + orgChatList: [], currentChat: null, unreadMessages: 0, differentRoleUnreadMessages: 0, @@ -40,6 +42,12 @@ export const chatReducer = (state = initialState, action) => { ), }; } + case CHAT_GET_LIST_ORG: { + return { + ...state, + orgChatList: action.payload, + }; + } case RECEIVE_NEW_CHAT: return { ...state, diff --git a/src/redux/reducers/customerReducer.js b/src/redux/reducers/customerReducer.js index 6446a2b8..f4c15e36 100644 --- a/src/redux/reducers/customerReducer.js +++ b/src/redux/reducers/customerReducer.js @@ -1,6 +1,7 @@ import { CLEAR_CURRENT_AUDITOR_CUSTOMER, CLEAR_MESSAGES, + CLEAR_SEARCH, CUSTOMER_SET_ERROR, GET_CURRENT_CUSTOMER, GET_CUSTOMER, @@ -49,6 +50,12 @@ export const customerReducer = (state = initialState, action) => { ...state, error: action.payload, }; + case CLEAR_SEARCH: + return { + ...state, + searchCustomers: null, + searchTotalCustomers: 0, + }; case CLEAR_MESSAGES: return { ...state, diff --git a/src/redux/reducers/organizationReducer.js b/src/redux/reducers/organizationReducer.js new file mode 100644 index 00000000..32439141 --- /dev/null +++ b/src/redux/reducers/organizationReducer.js @@ -0,0 +1,127 @@ +import { + CREATE_ORGANIZATION, + GET_MY_ORGANIZATION, + GET_ORGANIZATION_BY_ID, + UPDATE_ORGANIZATION, + CLEAR_ORGANIZATION, + DELETE_INVITES, + ACCEPT_INVITE, + NOT_FOUND_ORGANIZATION, + ADD_MEMBER_IN_ORGANIZATION, + CLEAR_MESSAGES, + GET_ORGANIZATIONS, + ORGANIZATION_INVITE, + CLEAR_SEARCH, + ERROR_GET_MY_ORGANIZATIONS, + CLEAR_NOT_FOUND_ERROR +} from '../actions/types.js'; + +const initialState = { + organizations: [], + own: [], + searchOrganizations: [], + includeMe: [], + organization: {}, + invites: [], + notFound: false, + successMessage: '', + loading: true, + errorRequest: false, +}; + +export const organizationReducer = (state = initialState, action) => { + switch (action.type) { + case GET_MY_ORGANIZATION: + return { + ...state, + organizations: [...action.payload.owner, ...action.payload.member], + own: action.payload.owner, + includeMe: action.payload.member, + invites: action.payload.invites, + loading: false, + }; + case ERROR_GET_MY_ORGANIZATIONS: + return { + ...state, + errorRequest: true, + }; + case CLEAR_NOT_FOUND_ERROR: + return { + ...state, + errorRequest: false, + }; + case ADD_MEMBER_IN_ORGANIZATION: + return { + ...state, + successMessage: 'User successfully invited', + }; + case GET_ORGANIZATIONS: { + return { + ...state, + searchOrganizations: action.payload.result, + }; + } + case CLEAR_MESSAGES: + return { + ...state, + successMessage: '', + }; + case CREATE_ORGANIZATION: + return { + ...state, + organization: action.payload, + organizations: [...state.organizations, action.payload], + own: [...state.own, action.payload], + }; + case NOT_FOUND_ORGANIZATION: + return { + ...state, + notFound: true, + }; + case UPDATE_ORGANIZATION: + return { + ...state, + organization: action.payload, + own: state.own.map(el => + el.id === action.payload.id ? action.payload : el, + ), + organizations: state.organizations.map(el => + el.id === action.payload.id ? action.payload : el, + ), + }; + case GET_ORGANIZATION_BY_ID: + return { + ...state, + organization: action.payload, + }; + case ORGANIZATION_INVITE: + return { + ...state, + invites: [...state.invites, action.payload], + }; + case DELETE_INVITES: + return { + ...state, + invites: state.invites.filter(el => el.id !== action.payload.id), + }; + case CLEAR_SEARCH: + return { + ...state, + searchOrganizations: [], + }; + case ACCEPT_INVITE: + return { + ...state, + invites: state.invites.filter(el => el.id !== action.payload.id), + organizations: [...state.organizations, action.payload], + organization: action.payload, + }; + case CLEAR_ORGANIZATION: + return { + ...state, + organization: {}, + }; + default: + return state; + } +}; diff --git a/src/redux/store.js b/src/redux/store.js index 2e09ea08..9d74c127 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -14,6 +14,7 @@ import { notFoundReducer } from './reducers/notFoundReducer.js'; import { chatReducer } from './reducers/chatReducer.js'; import { githubReducer } from './reducers/githubReducer.js'; import { filterConfig } from './reducers/configReducer.js'; +import { organizationReducer } from './reducers/organizationReducer.js'; export const store = createStore( combineReducers({ @@ -29,6 +30,7 @@ export const store = createStore( chat: chatReducer, github: githubReducer, filter: filterConfig, + organization: organizationReducer, }), composeWithDevTools(applyMiddleware(thunk, websocketMiddleware())), ); diff --git a/src/routes/AppRoutes.jsx b/src/routes/AppRoutes.jsx index 4db3a8cb..d328a43b 100644 --- a/src/routes/AppRoutes.jsx +++ b/src/routes/AppRoutes.jsx @@ -14,7 +14,7 @@ import { getCustomer } from '../redux/actions/customerAction.js'; import ProjectPage from '../pages/Project-page.jsx'; import AuditRequestPage from '../pages/Audit-Request-Page.jsx'; import { getProjects } from '../redux/actions/projectAction.js'; -import { getAudits, getAuditsRequest } from '../redux/actions/auditAction.js'; +import { getAudits, getAuditsRequest, getOrganizationAudits } from '../redux/actions/auditAction.js'; import EditProject from '../pages/EditProject.jsx'; import ForCustomer from '../pages/For-customer.jsx'; import ForAuditor from '../pages/For-auditor.jsx'; @@ -53,6 +53,14 @@ import UserProjects from '../pages/UserProjects.jsx'; import PriceCalculationPage from '../pages/PriceCalculationPage.jsx'; import { refreshToken } from '../redux/actions/userAction.js'; import Audit from '../pages/Audit.jsx'; +import Organization from '../components/Organization.jsx'; +import CreateEditOrganization from '../pages/CreateEditOrganization.jsx'; +import { + getAuditRequests, + getMyOrganizations, +} from '../redux/actions/organizationAction.js'; +import MyOrganization from '../pages/MyOrganizations.jsx'; +import CustomersPage from '../pages/CustomersPage.jsx'; const AppRoutes = () => { const currentRole = useSelector(s => s.user.user.current_role); @@ -61,6 +69,7 @@ const AppRoutes = () => { const dispatch = useDispatch(); const { reconnect, connected, needUpdate } = useSelector(s => s.websocket); const [isOpen, setIsOpen] = React.useState(false); + const organizations = useSelector(s => s.organization.organizations); useEffect(() => { const refreshInterval = setInterval(() => { @@ -74,12 +83,25 @@ const AppRoutes = () => { return () => clearInterval(refreshInterval); }, [isAuth()]); + useEffect(() => { + if (isAuth()) { + dispatch(getMyOrganizations()); + } + }, [isAuth(), currentRole]); + + useEffect(() => { + if (organizations?.length) { + dispatch(getAuditRequests()); + } + }, [organizations]); + useEffect(() => { if (isAuth()) { dispatch(getProjects()); if (currentRole) { dispatch(getAuditsRequest(currentRole)); dispatch(getAudits(currentRole)); + dispatch(getOrganizationAudits()); } } }, [currentRole, isAuth()]); @@ -116,7 +138,7 @@ const AppRoutes = () => { dispatch(getChatList(currentRole)); dispatch(getUnreadForDifferentRole()); } - }, [currentRole]); + }, [currentRole, isAuth()]); useEffect(() => { return () => { @@ -158,6 +180,7 @@ const AppRoutes = () => { } /> } /> } /> + } /> } /> } /> } /> @@ -318,6 +341,33 @@ const AppRoutes = () => { } /> + + + + } + /> + + + + + } + /> + + + + + } + /> + {/*Add new routes here*/} } />