roles and team

main
alex 2024-09-08 11:46:43 +02:00
parent 73aad4727d
commit 61d01eedc7
13 changed files with 268 additions and 77 deletions

View File

@ -45,16 +45,16 @@ export default function App() {
}); });
if (response) { if (response) {
dispatch(setUserAuthenticated(true));
dispatch(setPrimaryColor(`#${response.Organization.PrimaryColor}`)); dispatch(setPrimaryColor(`#${response.Organization.PrimaryColor}`));
dispatch(setLogoUrl(response.Organization.LogoUrl)); dispatch(setLogoUrl(response.Organization.LogoUrl));
dispatch(setBannerUrl(response.Organization.BannerUrl)); dispatch(setBannerUrl(response.Organization.BannerUrl));
} dispatch(setUserAuthenticated(true));
} catch (error) {}
})();
webSocketService.connect(); webSocketService.connect();
webSocketService.setHandler(WebSocketMessageHandler, dispatch); webSocketService.setHandler(WebSocketMessageHandler, dispatch);
}
} catch (error) {}
})();
return () => { return () => {
webSocketService.disconnect(); webSocketService.disconnect();

View File

@ -214,6 +214,7 @@ export function SideMenuContent() {
<div> <div>
<Flex justify="center" style={{ paddingBottom: 24, width: "100%" }}> <Flex justify="center" style={{ paddingBottom: 24, width: "100%" }}>
{appLogoUrl !== null && (
<img <img
src={`${Constants.STATIC_CONTENT_ADDRESS}${ src={`${Constants.STATIC_CONTENT_ADDRESS}${
appLogoUrl || Constants.DEMO_LOGO_URL appLogoUrl || Constants.DEMO_LOGO_URL
@ -221,6 +222,7 @@ export function SideMenuContent() {
alt="logo" alt="logo"
style={{ height: 80 }} style={{ height: 80 }}
/> />
)}
</Flex> </Flex>
<Menu <Menu

View File

@ -6,8 +6,8 @@ export const appSlice = createSlice({
darkMode: false, darkMode: false,
userAuthenticated: null, userAuthenticated: null,
primaryColor: "#111", primaryColor: "#111",
logoUrl: "", logoUrl: null,
bannerUrl: "", bannerUrl: null,
}, },
reducers: { reducers: {
setDarkMode: (state, action) => { setDarkMode: (state, action) => {

View File

@ -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<OrganizationSettings, undefined>({ getOrganizationSettings: builder.query<OrganizationSettings, undefined>({
query: () => ({ query: () => ({
url: "organization/settings", url: "organization/settings",
@ -60,25 +75,17 @@ export const organizationApi = createApi({
method: "GET", method: "GET",
}), }),
}), }),
/* createRole: builder.mutation({
query: (name) => ({
url: "organization/roles",
method: "POST",
body: {
Name: name,
},
}),
}), */
}), }),
}); });
export const { export const {
useGetTeamQuery, useGetTeamQuery,
useCreateTeamMemberMutation, useCreateTeamMemberMutation,
useUpdateTeamMemberRoleMutation,
useDeleteTeamMemberMutation,
useGetOrganizationSettingsQuery, useGetOrganizationSettingsQuery,
useUpdateOrganizationSettingsMutation, useUpdateOrganizationSettingsMutation,
useIsSubdomainAvailableMutation, useIsSubdomainAvailableMutation,
useUpdateSubdomainMutation, useUpdateSubdomainMutation,
useGetRolesQuery, useGetRolesQuery,
// useCreateRoleMutation,
} = organizationApi; } = organizationApi;

View File

@ -6,8 +6,11 @@ import {
} from "core/reducers/appSlice"; } from "core/reducers/appSlice";
import { BrowserTabSession, Constants } from "core/utils/utils"; import { BrowserTabSession, Constants } from "core/utils/utils";
import { WebSocketReceivedMessagesCmds } from "core/utils/webSocket"; import { WebSocketReceivedMessagesCmds } from "core/utils/webSocket";
import { addTeamMember } from "features/Team/teamSlice"; import {
import { useDispatch } from "react-redux"; addTeamMember,
deleteTeamMember,
updateTeamMemberRole,
} from "features/Team/teamSlice";
interface WebSocketMessage { interface WebSocketMessage {
Cmd: number; Cmd: number;
@ -148,6 +151,12 @@ export function WebSocketMessageHandler(
case WebSocketReceivedMessagesCmds.TeamAddedMember: case WebSocketReceivedMessagesCmds.TeamAddedMember:
dispatch(addTeamMember(Body)); dispatch(addTeamMember(Body));
break; break;
case WebSocketReceivedMessagesCmds.TeamUpdatedMemberRole:
dispatch(updateTeamMemberRole(Body));
break;
case WebSocketReceivedMessagesCmds.TeamDeletedMember:
dispatch(deleteTeamMember(Body));
break;
default: default:
console.error("Unknown message type:", Cmd); console.error("Unknown message type:", Cmd);
} }

View File

@ -4,7 +4,7 @@ export interface TeamMember {
LastName: string; LastName: string;
Email: string; Email: string;
RoleId: string; RoleId: string;
Status: string; Online: boolean;
} }
export interface OrganizationSettings { export interface OrganizationSettings {

View File

@ -8,6 +8,8 @@ enum WebSocketReceivedMessagesCmds {
SettingsUpdatedBanner = 3, SettingsUpdatedBanner = 3,
SettingsUpdatedSubdomain = 4, SettingsUpdatedSubdomain = 4,
TeamAddedMember = 5, TeamAddedMember = 5,
TeamUpdatedMemberRole = 6,
TeamDeletedMember = 7,
} }
export { WebSocketSendMessagesCmds, WebSocketReceivedMessagesCmds }; export { WebSocketSendMessagesCmds, WebSocketReceivedMessagesCmds };

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from "react";
import { FloatButton } from 'antd'; import { FloatButton } from "antd";
import { CommentOutlined } from '@ant-design/icons'; import { CommentOutlined } from "@ant-design/icons";
import { DeepChat } from 'deep-chat-react'; import { DeepChat } from "deep-chat-react";
function AiChat() { function AiChat() {
const [visible, setVisible] = React.useState(false); const [visible, setVisible] = React.useState(false);
@ -9,14 +9,40 @@ function AiChat() {
return ( return (
<> <>
{visible ? ( {visible ? (
<div style={{ position: 'fixed', bottom: 100, right: 10, zIndex: 10000, maxWidth: '95vw', width: 500, height: 1000, maxHeight: 'calc(100vh - 165px)' }}> <div
style={{
position: "fixed",
bottom: 100,
right: 10,
zIndex: 10000,
maxWidth: "95vw",
width: 500,
height: 1000,
maxHeight: "calc(100vh - 165px)",
}}
>
<DeepChat <DeepChat
style={{ width: '100%', height: '100%', borderRadius: 10, boxShadow: '0 0 10px rgba(0,0,0,0.1)', bottom: 0, position: 'absolute' }} style={{
width: "100%",
height: "100%",
borderRadius: 10,
boxShadow: "0 0 10px rgba(0,0,0,0.1)",
bottom: 0,
position: "absolute",
}}
history={[ history={[
{ text: 'Show me a modern city', role: 'user' }, { text: "Show me a modern city", role: "user" },
{ files: [{ src: 'https://test.ex.umbach.dev/api/statico/809fe37e-8c41-4a44-98d1-d9247affd531/67c763b6-ea67-4b49-9621-2f78b85eb180.png', type: 'image' }], role: 'ai' }, {
{ text: 'Whats on your mind?', role: 'user' }, files: [
{ text: 'Peace and tranquility', role: 'ai' }, {
src: "https://test.ex.umbach.dev/api/statico/809fe37e-8c41-4a44-98d1-d9247affd531/67c763b6-ea67-4b49-9621-2f78b85eb180.png",
type: "image",
},
],
role: "ai",
},
{ text: "Whats on your mind?", role: "user" },
{ text: "Peace and tranquility", role: "ai" },
]} ]}
></DeepChat> ></DeepChat>
</div> </div>
@ -25,7 +51,7 @@ function AiChat() {
<FloatButton <FloatButton
icon={<CommentOutlined />} icon={<CommentOutlined />}
type="primary" type="primary"
onClick={() => console.log('onClick')} onClick={() => console.log("onClick")}
style={{ zIndex: 10000 }} style={{ zIndex: 10000 }}
onClickCapture={() => { onClickCapture={() => {
setVisible(!visible); setVisible(!visible);

View File

@ -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 HeaderBar from "../../core/components/Header";
import MyBanner from "../../shared/components/MyBanner"; import MyBanner from "../../shared/components/MyBanner";
import { MyContainer } from "../../shared/components/MyContainer"; import { MyContainer } from "../../shared/components/MyContainer";
@ -142,6 +142,30 @@ function RoleComponent({ role }: { role: Role }) {
]} ]}
/> />
), ),
extra: (
<>
{role.Users.length > 0 && (
<Avatar.Group
size="small"
max={{
count: 4,
}}
>
{role.Users.map((user, index) => (
<Tooltip
key={index}
title={`${user.FirstName} ${user.LastName}`}
placement="top"
>
<Avatar style={{ backgroundColor: "#f56a00" }}>
{user.FirstName[0]}
</Avatar>
</Tooltip>
))}
</Avatar.Group>
)}
</>
),
}, },
]} ]}
/> />

View File

@ -60,7 +60,7 @@ function GeneralCard({
isLoading, isLoading,
}: { data?: OrganizationSettings; isLoading?: boolean } = {}) { }: { data?: OrganizationSettings; isLoading?: boolean } = {}) {
const [form] = useForm<GeneralFieldType>(); const [form] = useForm<GeneralFieldType>();
const { success } = useMessage(); const { success, error: errorMessage } = useMessage();
const dispatch = useDispatch(); const dispatch = useDispatch();
const debounceRef = useRef<null | NodeJS.Timeout>(null); const debounceRef = useRef<null | NodeJS.Timeout>(null);
@ -86,6 +86,7 @@ function GeneralCard({
success("Settings updated successfully!"); success("Settings updated successfully!");
} catch (error) { } catch (error) {
console.error(error); console.error(error);
errorMessage("Failed to update settings!");
} }
}; };

View File

@ -2,26 +2,44 @@ import MyTable from "shared/components/MyTable";
import HeaderBar from "core/components/Header"; import HeaderBar from "core/components/Header";
import MyBanner from "shared/components/MyBanner"; import MyBanner from "shared/components/MyBanner";
import { MyContainer } from "shared/components/MyContainer"; 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 { UserAddOutlined } from "@ant-design/icons";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Constants } from "core/utils/utils"; 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 MyErrorResult from "shared/components/MyResult";
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { setTeamMembers, teamMembers } from "./teamSlice"; import { setTeamMembers, teamMembers } from "./teamSlice";
import { tmpRoleNames } from "features/Roles"; import { tmpRoleNames } from "features/Roles";
import {
addWebSocketReconnectListener,
removeWebSocketReconnectListener,
} from "core/services/websocketService";
import { useMessage } from "core/context/MessageContext";
const TeamList: React.FC = () => { const TeamList: React.FC = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { success, error: errorMessage } = useMessage();
const dataTeamMembers = useSelector(teamMembers); const dataTeamMembers = useSelector(teamMembers);
const { data, error, isLoading } = useGetTeamQuery(undefined, { const [selectedRoleId, setSelectedRoleId] = useState<string | undefined>();
const { data, error, isLoading, refetch } = useGetTeamQuery(undefined, {
refetchOnMountOrArgChange: true, refetchOnMountOrArgChange: true,
}); });
const [reqUpdateTeamMemberRole, { isLoading: loadingUpdateTeamMemberRole }] =
useUpdateTeamMemberRoleMutation();
const [reqDeleteTeamMember, { isLoading: loadingDeleteTeamMember }] =
useDeleteTeamMemberMutation();
const getTableContent = () => { const getTableContent = () => {
let items = [ let items = [
{ {
@ -53,6 +71,67 @@ const TeamList: React.FC = () => {
title: "Actions", title: "Actions",
dataIndex: "actions", dataIndex: "actions",
key: "actions", key: "actions",
render: (_: any, record: any) => (
<Space size="middle">
<Popconfirm
title="Change role to"
onConfirm={async () => {
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={
<Select
style={{ width: 150 }}
defaultValue={record.role}
value={selectedRoleId}
onChange={(value) => setSelectedRoleId(value)}
>
{Object.keys(tmpRoleNames).map((key) => (
<Select.Option key={key} value={key}>
{tmpRoleNames[key]}
</Select.Option>
))}
</Select>
}
>
<Button
type="link"
onClick={() => setSelectedRoleId(record.role)}
>
Change role
</Button>
</Popconfirm>
<Popconfirm
title="Confirm deletion of team member"
okButtonProps={{
loading: loadingDeleteTeamMember,
}}
onConfirm={async () => {
try {
await reqDeleteTeamMember(record.key).unwrap();
success("Team member deleted successfully");
} catch (error) {
console.error(error);
errorMessage("Error deleting team member");
}
}}
>
<Button type="link">Delete</Button>
</Popconfirm>
</Space>
),
}, },
]; ];
@ -71,10 +150,17 @@ const TeamList: React.FC = () => {
lastName: item.LastName, lastName: item.LastName,
email: item.Email, email: item.Email,
role: tmpRoleNames[item.RoleId], role: tmpRoleNames[item.RoleId],
status: item.Status, status: (
<Badge
status={item.Online ? "success" : "error"}
text={item.Online ? "Online" : "Offline"}
/>
),
}); });
}); });
items.sort((a, b) => a.role.localeCompare(b.role));
return items; return items;
}; };
@ -84,6 +170,12 @@ const TeamList: React.FC = () => {
dispatch(setTeamMembers(data)); dispatch(setTeamMembers(data));
}, [data]); }, [data]);
useEffect(() => {
addWebSocketReconnectListener(refetch);
return () => removeWebSocketReconnectListener(refetch);
}, []);
if (error) return <MyErrorResult />; if (error) return <MyErrorResult />;
return ( return (

View File

@ -18,12 +18,31 @@ export const teamSlice = createSlice({
setTeamMembers: (state, action) => { setTeamMembers: (state, action) => {
state.teamMembers = action.payload; 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: { selectors: {
teamMembers: (state) => state.teamMembers, teamMembers: (state) => state.teamMembers,
}, },
}); });
export const { addTeamMember, setTeamMembers } = teamSlice.actions; export const {
addTeamMember,
setTeamMembers,
updateTeamMemberRole,
deleteTeamMember,
} = teamSlice.actions;
export const { teamMembers } = teamSlice.selectors; export const { teamMembers } = teamSlice.selectors;

View File

@ -20,6 +20,7 @@ export default function MyBanner({
position: "relative", position: "relative",
}} }}
> >
{appBannerUrl !== null ? (
<img <img
src={`${Constants.STATIC_CONTENT_ADDRESS}${ src={`${Constants.STATIC_CONTENT_ADDRESS}${
appBannerUrl || Constants.DEMO_BANNER_URL appBannerUrl || Constants.DEMO_BANNER_URL
@ -32,6 +33,14 @@ export default function MyBanner({
userSelect: "none", userSelect: "none",
}} }}
/> />
) : (
<div
style={{
height: 228,
backgroundColor: "#000",
}}
/>
)}
<div className={styles.gradientContainer}></div> <div className={styles.gradientContainer}></div>