diff --git a/src/App.tsx b/src/App.tsx index 9aa063c..9f089d0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -45,17 +45,17 @@ export default function App() { }); if (response) { - dispatch(setUserAuthenticated(true)); dispatch(setPrimaryColor(`#${response.Organization.PrimaryColor}`)); dispatch(setLogoUrl(response.Organization.LogoUrl)); dispatch(setBannerUrl(response.Organization.BannerUrl)); + dispatch(setUserAuthenticated(true)); + + webSocketService.connect(); + webSocketService.setHandler(WebSocketMessageHandler, dispatch); } } catch (error) {} })(); - webSocketService.connect(); - webSocketService.setHandler(WebSocketMessageHandler, dispatch); - return () => { webSocketService.disconnect(); }; diff --git a/src/core/components/SideMenu/index.tsx b/src/core/components/SideMenu/index.tsx index e58b46c..03a24f7 100644 --- a/src/core/components/SideMenu/index.tsx +++ b/src/core/components/SideMenu/index.tsx @@ -214,13 +214,15 @@ export function SideMenuContent() {
- logo + {appLogoUrl !== null && ( + logo + )} { diff --git a/src/core/services/organization.ts b/src/core/services/organization.ts index f8edc4b..fadae9f 100644 --- a/src/core/services/organization.ts +++ b/src/core/services/organization.ts @@ -29,6 +29,21 @@ export const organizationApi = createApi({ }, }), }), + updateTeamMemberRole: builder.mutation({ + query: ({ memberId, roleId }) => ({ + url: `organization/team/members/${memberId}/role`, + method: "PATCH", + body: { + RoleId: roleId, + }, + }), + }), + deleteTeamMember: builder.mutation({ + query: (memberId) => ({ + url: `organization/team/members/${memberId}`, + method: "DELETE", + }), + }), getOrganizationSettings: builder.query({ query: () => ({ url: "organization/settings", @@ -60,25 +75,17 @@ export const organizationApi = createApi({ method: "GET", }), }), - /* createRole: builder.mutation({ - query: (name) => ({ - url: "organization/roles", - method: "POST", - body: { - Name: name, - }, - }), - }), */ }), }); export const { useGetTeamQuery, useCreateTeamMemberMutation, + useUpdateTeamMemberRoleMutation, + useDeleteTeamMemberMutation, useGetOrganizationSettingsQuery, useUpdateOrganizationSettingsMutation, useIsSubdomainAvailableMutation, useUpdateSubdomainMutation, useGetRolesQuery, - // useCreateRoleMutation, } = organizationApi; diff --git a/src/core/services/websocketService.ts b/src/core/services/websocketService.ts index 963f6a8..ffb1f2e 100644 --- a/src/core/services/websocketService.ts +++ b/src/core/services/websocketService.ts @@ -6,8 +6,11 @@ import { } from "core/reducers/appSlice"; import { BrowserTabSession, Constants } from "core/utils/utils"; import { WebSocketReceivedMessagesCmds } from "core/utils/webSocket"; -import { addTeamMember } from "features/Team/teamSlice"; -import { useDispatch } from "react-redux"; +import { + addTeamMember, + deleteTeamMember, + updateTeamMemberRole, +} from "features/Team/teamSlice"; interface WebSocketMessage { Cmd: number; @@ -148,6 +151,12 @@ export function WebSocketMessageHandler( case WebSocketReceivedMessagesCmds.TeamAddedMember: dispatch(addTeamMember(Body)); break; + case WebSocketReceivedMessagesCmds.TeamUpdatedMemberRole: + dispatch(updateTeamMemberRole(Body)); + break; + case WebSocketReceivedMessagesCmds.TeamDeletedMember: + dispatch(deleteTeamMember(Body)); + break; default: console.error("Unknown message type:", Cmd); } diff --git a/src/core/types/organization.ts b/src/core/types/organization.ts index 2358477..2e3f4dd 100644 --- a/src/core/types/organization.ts +++ b/src/core/types/organization.ts @@ -4,7 +4,7 @@ export interface TeamMember { LastName: string; Email: string; RoleId: string; - Status: string; + Online: boolean; } export interface OrganizationSettings { diff --git a/src/core/utils/webSocket.ts b/src/core/utils/webSocket.ts index 90a4ef7..6ef2078 100644 --- a/src/core/utils/webSocket.ts +++ b/src/core/utils/webSocket.ts @@ -8,6 +8,8 @@ enum WebSocketReceivedMessagesCmds { SettingsUpdatedBanner = 3, SettingsUpdatedSubdomain = 4, TeamAddedMember = 5, + TeamUpdatedMemberRole = 6, + TeamDeletedMember = 7, } export { WebSocketSendMessagesCmds, WebSocketReceivedMessagesCmds }; diff --git a/src/features/AiChat/index.tsx b/src/features/AiChat/index.tsx index bf03bba..d97de09 100644 --- a/src/features/AiChat/index.tsx +++ b/src/features/AiChat/index.tsx @@ -1,38 +1,64 @@ -import React from 'react'; -import { FloatButton } from 'antd'; -import { CommentOutlined } from '@ant-design/icons'; -import { DeepChat } from 'deep-chat-react'; +import React from "react"; +import { FloatButton } from "antd"; +import { CommentOutlined } from "@ant-design/icons"; +import { DeepChat } from "deep-chat-react"; function AiChat() { - const [visible, setVisible] = React.useState(false); + const [visible, setVisible] = React.useState(false); - return ( - <> - {visible ? ( -
- -
- ) : null} + return ( + <> + {visible ? ( +
+ +
+ ) : null} - } - type="primary" - onClick={() => console.log('onClick')} - style={{ zIndex: 10000 }} - onClickCapture={() => { - setVisible(!visible); - }} - /> - - ); + } + type="primary" + onClick={() => console.log("onClick")} + style={{ zIndex: 10000 }} + onClickCapture={() => { + setVisible(!visible); + }} + /> + + ); } export default AiChat; diff --git a/src/features/Roles/index.tsx b/src/features/Roles/index.tsx index 48893b8..5eb9780 100644 --- a/src/features/Roles/index.tsx +++ b/src/features/Roles/index.tsx @@ -1,4 +1,4 @@ -import { Checkbox, Collapse, Form } from "antd"; +import { Avatar, Checkbox, Collapse, Form, Tooltip } from "antd"; import HeaderBar from "../../core/components/Header"; import MyBanner from "../../shared/components/MyBanner"; import { MyContainer } from "../../shared/components/MyContainer"; @@ -142,6 +142,30 @@ function RoleComponent({ role }: { role: Role }) { ]} /> ), + extra: ( + <> + {role.Users.length > 0 && ( + + {role.Users.map((user, index) => ( + + + {user.FirstName[0]} + + + ))} + + )} + + ), }, ]} /> diff --git a/src/features/Settings/index.tsx b/src/features/Settings/index.tsx index fefc9ef..9ea8de7 100644 --- a/src/features/Settings/index.tsx +++ b/src/features/Settings/index.tsx @@ -60,7 +60,7 @@ function GeneralCard({ isLoading, }: { data?: OrganizationSettings; isLoading?: boolean } = {}) { const [form] = useForm(); - const { success } = useMessage(); + const { success, error: errorMessage } = useMessage(); const dispatch = useDispatch(); const debounceRef = useRef(null); @@ -86,6 +86,7 @@ function GeneralCard({ success("Settings updated successfully!"); } catch (error) { console.error(error); + errorMessage("Failed to update settings!"); } }; diff --git a/src/features/Team/index.tsx b/src/features/Team/index.tsx index f6f2f77..effca58 100644 --- a/src/features/Team/index.tsx +++ b/src/features/Team/index.tsx @@ -2,26 +2,44 @@ import MyTable from "shared/components/MyTable"; import HeaderBar from "core/components/Header"; import MyBanner from "shared/components/MyBanner"; import { MyContainer } from "shared/components/MyContainer"; -import { Button, Flex } from "antd"; +import { Badge, Button, Flex, Popconfirm, Select, Space } from "antd"; import { UserAddOutlined } from "@ant-design/icons"; import { Link } from "react-router-dom"; import { Constants } from "core/utils/utils"; -import { useGetTeamQuery } from "core/services/organization"; +import { + useDeleteTeamMemberMutation, + useGetTeamQuery, + useUpdateTeamMemberRoleMutation, +} from "core/services/organization"; import MyErrorResult from "shared/components/MyResult"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { setTeamMembers, teamMembers } from "./teamSlice"; import { tmpRoleNames } from "features/Roles"; +import { + addWebSocketReconnectListener, + removeWebSocketReconnectListener, +} from "core/services/websocketService"; +import { useMessage } from "core/context/MessageContext"; const TeamList: React.FC = () => { const dispatch = useDispatch(); + const { success, error: errorMessage } = useMessage(); const dataTeamMembers = useSelector(teamMembers); - const { data, error, isLoading } = useGetTeamQuery(undefined, { + const [selectedRoleId, setSelectedRoleId] = useState(); + + const { data, error, isLoading, refetch } = useGetTeamQuery(undefined, { refetchOnMountOrArgChange: true, }); + const [reqUpdateTeamMemberRole, { isLoading: loadingUpdateTeamMemberRole }] = + useUpdateTeamMemberRoleMutation(); + + const [reqDeleteTeamMember, { isLoading: loadingDeleteTeamMember }] = + useDeleteTeamMemberMutation(); + const getTableContent = () => { let items = [ { @@ -53,6 +71,67 @@ const TeamList: React.FC = () => { title: "Actions", dataIndex: "actions", key: "actions", + render: (_: any, record: any) => ( + + { + try { + await reqUpdateTeamMemberRole({ + memberId: record.key, + roleId: selectedRoleId, + }).unwrap(); + + success("Role updated successfully"); + } catch (error) { + console.error(error); + errorMessage("Error updating role"); + } + }} + okButtonProps={{ loading: loadingUpdateTeamMemberRole }} + description={ + + } + > + + + + { + try { + await reqDeleteTeamMember(record.key).unwrap(); + + success("Team member deleted successfully"); + } catch (error) { + console.error(error); + errorMessage("Error deleting team member"); + } + }} + > + + + + ), }, ]; @@ -71,10 +150,17 @@ const TeamList: React.FC = () => { lastName: item.LastName, email: item.Email, role: tmpRoleNames[item.RoleId], - status: item.Status, + status: ( + + ), }); }); + items.sort((a, b) => a.role.localeCompare(b.role)); + return items; }; @@ -84,6 +170,12 @@ const TeamList: React.FC = () => { dispatch(setTeamMembers(data)); }, [data]); + useEffect(() => { + addWebSocketReconnectListener(refetch); + + return () => removeWebSocketReconnectListener(refetch); + }, []); + if (error) return ; return ( diff --git a/src/features/Team/teamSlice.ts b/src/features/Team/teamSlice.ts index ac2213f..b588071 100644 --- a/src/features/Team/teamSlice.ts +++ b/src/features/Team/teamSlice.ts @@ -18,12 +18,31 @@ export const teamSlice = createSlice({ setTeamMembers: (state, action) => { state.teamMembers = action.payload; }, + updateTeamMemberRole: (state, action) => { + const member = state.teamMembers.find( + (member) => member.Id === action.payload.MemberId + ); + + if (member) { + member.RoleId = action.payload.RoleId; + } + }, + deleteTeamMember: (state, action) => { + state.teamMembers = state.teamMembers.filter( + (member) => member.Id !== action.payload + ); + }, }, selectors: { teamMembers: (state) => state.teamMembers, }, }); -export const { addTeamMember, setTeamMembers } = teamSlice.actions; +export const { + addTeamMember, + setTeamMembers, + updateTeamMemberRole, + deleteTeamMember, +} = teamSlice.actions; export const { teamMembers } = teamSlice.selectors; diff --git a/src/shared/components/MyBanner/index.tsx b/src/shared/components/MyBanner/index.tsx index cf9c3d3..8943630 100644 --- a/src/shared/components/MyBanner/index.tsx +++ b/src/shared/components/MyBanner/index.tsx @@ -20,18 +20,27 @@ export default function MyBanner({ position: "relative", }} > - banner + {appBannerUrl !== null ? ( + banner + ) : ( +
+ )}