roles and team
parent
73aad4727d
commit
61d01eedc7
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -214,13 +214,15 @@ export function SideMenuContent() {
|
|||
|
||||
<div>
|
||||
<Flex justify="center" style={{ paddingBottom: 24, width: "100%" }}>
|
||||
<img
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}${
|
||||
appLogoUrl || Constants.DEMO_LOGO_URL
|
||||
}`}
|
||||
alt="logo"
|
||||
style={{ height: 80 }}
|
||||
/>
|
||||
{appLogoUrl !== null && (
|
||||
<img
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}${
|
||||
appLogoUrl || Constants.DEMO_LOGO_URL
|
||||
}`}
|
||||
alt="logo"
|
||||
style={{ height: 80 }}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Menu
|
||||
|
|
|
@ -6,8 +6,8 @@ export const appSlice = createSlice({
|
|||
darkMode: false,
|
||||
userAuthenticated: null,
|
||||
primaryColor: "#111",
|
||||
logoUrl: "",
|
||||
bannerUrl: "",
|
||||
logoUrl: null,
|
||||
bannerUrl: null,
|
||||
},
|
||||
reducers: {
|
||||
setDarkMode: (state, action) => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ export interface TeamMember {
|
|||
LastName: string;
|
||||
Email: string;
|
||||
RoleId: string;
|
||||
Status: string;
|
||||
Online: boolean;
|
||||
}
|
||||
|
||||
export interface OrganizationSettings {
|
||||
|
|
|
@ -8,6 +8,8 @@ enum WebSocketReceivedMessagesCmds {
|
|||
SettingsUpdatedBanner = 3,
|
||||
SettingsUpdatedSubdomain = 4,
|
||||
TeamAddedMember = 5,
|
||||
TeamUpdatedMemberRole = 6,
|
||||
TeamDeletedMember = 7,
|
||||
}
|
||||
|
||||
export { WebSocketSendMessagesCmds, WebSocketReceivedMessagesCmds };
|
||||
|
|
|
@ -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 ? (
|
||||
<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' }}
|
||||
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' },
|
||||
]}
|
||||
></DeepChat>
|
||||
</div>
|
||||
) : null}
|
||||
return (
|
||||
<>
|
||||
{visible ? (
|
||||
<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",
|
||||
}}
|
||||
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" },
|
||||
]}
|
||||
></DeepChat>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<FloatButton
|
||||
icon={<CommentOutlined />}
|
||||
type="primary"
|
||||
onClick={() => console.log('onClick')}
|
||||
style={{ zIndex: 10000 }}
|
||||
onClickCapture={() => {
|
||||
setVisible(!visible);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
<FloatButton
|
||||
icon={<CommentOutlined />}
|
||||
type="primary"
|
||||
onClick={() => console.log("onClick")}
|
||||
style={{ zIndex: 10000 }}
|
||||
onClickCapture={() => {
|
||||
setVisible(!visible);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AiChat;
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -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!");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -20,18 +20,27 @@ export default function MyBanner({
|
|||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}${
|
||||
appBannerUrl || Constants.DEMO_BANNER_URL
|
||||
}`}
|
||||
alt="banner"
|
||||
style={{
|
||||
height: 228,
|
||||
width: "100%",
|
||||
objectFit: "cover",
|
||||
userSelect: "none",
|
||||
}}
|
||||
/>
|
||||
{appBannerUrl !== null ? (
|
||||
<img
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}${
|
||||
appBannerUrl || Constants.DEMO_BANNER_URL
|
||||
}`}
|
||||
alt="banner"
|
||||
style={{
|
||||
height: 228,
|
||||
width: "100%",
|
||||
objectFit: "cover",
|
||||
userSelect: "none",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
height: 228,
|
||||
backgroundColor: "#000",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={styles.gradientContainer}></div>
|
||||
|
||||
|
|
Loading…
Reference in New Issue