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) {
dispatch(setUserAuthenticated(true));
dispatch(setPrimaryColor(`#${response.Organization.PrimaryColor}`));
dispatch(setLogoUrl(response.Organization.LogoUrl));
dispatch(setBannerUrl(response.Organization.BannerUrl));
}
} catch (error) {}
})();
dispatch(setUserAuthenticated(true));
webSocketService.connect();
webSocketService.setHandler(WebSocketMessageHandler, dispatch);
}
} catch (error) {}
})();
return () => {
webSocketService.disconnect();

View File

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

View File

@ -6,8 +6,8 @@ export const appSlice = createSlice({
darkMode: false,
userAuthenticated: null,
primaryColor: "#111",
logoUrl: "",
bannerUrl: "",
logoUrl: null,
bannerUrl: null,
},
reducers: {
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>({
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;

View File

@ -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);
}

View File

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

View File

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

View File

@ -1,7 +1,7 @@
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);
@ -9,14 +9,40 @@ function AiChat() {
return (
<>
{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
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={[
{ 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' },
{ text: 'Peace and tranquility', role: 'ai' },
{ 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" },
{ text: "Peace and tranquility", role: "ai" },
]}
></DeepChat>
</div>
@ -25,7 +51,7 @@ function AiChat() {
<FloatButton
icon={<CommentOutlined />}
type="primary"
onClick={() => console.log('onClick')}
onClick={() => console.log("onClick")}
style={{ zIndex: 10000 }}
onClickCapture={() => {
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 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 && (
<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,
}: { data?: OrganizationSettings; isLoading?: boolean } = {}) {
const [form] = useForm<GeneralFieldType>();
const { success } = useMessage();
const { success, error: errorMessage } = useMessage();
const dispatch = useDispatch();
const debounceRef = useRef<null | NodeJS.Timeout>(null);
@ -86,6 +86,7 @@ function GeneralCard({
success("Settings updated successfully!");
} catch (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 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<string | undefined>();
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) => (
<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,
email: item.Email,
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;
};
@ -84,6 +170,12 @@ const TeamList: React.FC = () => {
dispatch(setTeamMembers(data));
}, [data]);
useEffect(() => {
addWebSocketReconnectListener(refetch);
return () => removeWebSocketReconnectListener(refetch);
}, []);
if (error) return <MyErrorResult />;
return (

View File

@ -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;

View File

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