init
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
CreditCardOutlined,
|
||||
ProfileOutlined,
|
||||
TeamOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import React, { ReactNode } from 'react';
|
||||
import Overview from './overview/overview';
|
||||
import Users from './users/users';
|
||||
import Teams from './teams/teams';
|
||||
import Billing from './billing/billing';
|
||||
import Projects from './projects/projects';
|
||||
|
||||
// type of a menu item in admin center sidebar
|
||||
type AdminCenterMenuItems = {
|
||||
key: string;
|
||||
name: string;
|
||||
endpoint: string;
|
||||
icon: ReactNode;
|
||||
element: ReactNode;
|
||||
};
|
||||
// settings all element items use for sidebar and routes
|
||||
export const adminCenterItems: AdminCenterMenuItems[] = [
|
||||
{
|
||||
key: 'overview',
|
||||
name: 'overview',
|
||||
endpoint: 'overview',
|
||||
icon: React.createElement(AppstoreOutlined),
|
||||
element: React.createElement(Overview),
|
||||
},
|
||||
{
|
||||
key: 'users',
|
||||
name: 'users',
|
||||
endpoint: 'users',
|
||||
icon: React.createElement(UserOutlined),
|
||||
element: React.createElement(Users),
|
||||
},
|
||||
{
|
||||
key: 'teams',
|
||||
name: 'teams',
|
||||
endpoint: 'teams',
|
||||
icon: React.createElement(TeamOutlined),
|
||||
element: React.createElement(Teams),
|
||||
},
|
||||
{
|
||||
key: 'projects',
|
||||
name: 'projects',
|
||||
endpoint: 'projects',
|
||||
icon: React.createElement(ProfileOutlined),
|
||||
element: React.createElement(Projects),
|
||||
},
|
||||
{
|
||||
key: 'billing',
|
||||
name: 'billing',
|
||||
endpoint: 'billing',
|
||||
icon: React.createElement(CreditCardOutlined),
|
||||
element: React.createElement(Billing),
|
||||
},
|
||||
];
|
||||
32
worklenz-frontend/src/pages/admin-center/billing/billing.tsx
Normal file
32
worklenz-frontend/src/pages/admin-center/billing/billing.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { PageHeader } from '@ant-design/pro-components';
|
||||
import { Tabs, TabsProps } from 'antd';
|
||||
import React from 'react';
|
||||
import CurrentBill from '@/components/admin-center/billing/current-bill';
|
||||
import Configuration from '@/components/admin-center/configuration/configuration';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Billing: React.FC = () => {
|
||||
const { t } = useTranslation('admin-center/current-bill');;
|
||||
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: t('currentBill'),
|
||||
children: <CurrentBill />,
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: t('configuration'),
|
||||
children: <Configuration />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
<PageHeader title={<span>{t('title')}</span>} style={{ padding: '16px 0' }} />
|
||||
<Tabs defaultActiveKey="1" items={items} destroyInactiveTabPane />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Billing;
|
||||
@@ -0,0 +1,90 @@
|
||||
import { EditOutlined, MailOutlined, PhoneOutlined } from '@ant-design/icons';
|
||||
import { PageHeader } from '@ant-design/pro-components';
|
||||
import { Button, Card, Input, Space, Tooltip, Typography } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import OrganizationAdminsTable from '@/components/admin-center/overview/organization-admins-table/organization-admins-table';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { RootState } from '@/app/store';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import OrganizationName from '@/components/admin-center/overview/organization-name/organization-name';
|
||||
import OrganizationOwner from '@/components/admin-center/overview/organization-owner/organization-owner';
|
||||
import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service';
|
||||
import { IOrganization, IOrganizationAdmin } from '@/types/admin-center/admin-center.types';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import { tr } from 'date-fns/locale';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const Overview: React.FC = () => {
|
||||
const [organization, setOrganization] = useState<IOrganization | null>(null);
|
||||
const [organizationAdmins, setOrganizationAdmins] = useState<IOrganizationAdmin[] | null>(null);
|
||||
const [loadingAdmins, setLoadingAdmins] = useState(false);
|
||||
|
||||
const themeMode = useAppSelector((state: RootState) => state.themeReducer.mode);
|
||||
const { t } = useTranslation('admin-center/overview');
|
||||
|
||||
const getOrganizationDetails = async () => {
|
||||
try {
|
||||
const res = await adminCenterApiService.getOrganizationDetails();
|
||||
if (res.done) {
|
||||
setOrganization(res.body);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error getting organization details', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getOrganizationAdmins = async () => {
|
||||
setLoadingAdmins(true);
|
||||
try {
|
||||
const res = await adminCenterApiService.getOrganizationAdmins();
|
||||
if (res.done) {
|
||||
setOrganizationAdmins(res.body);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error getting organization admins', error);
|
||||
} finally {
|
||||
setLoadingAdmins(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getOrganizationDetails();
|
||||
getOrganizationAdmins();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
<PageHeader title={<span>{t('overview')}</span>} style={{ padding: '16px 0' }} />
|
||||
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={22}>
|
||||
<OrganizationName
|
||||
themeMode={themeMode}
|
||||
name={organization?.name || ''}
|
||||
t={t}
|
||||
refetch={getOrganizationDetails}
|
||||
/>
|
||||
|
||||
<OrganizationOwner
|
||||
themeMode={themeMode}
|
||||
organization={organization}
|
||||
t={t}
|
||||
refetch={getOrganizationDetails}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||
{t('admins')}
|
||||
</Typography.Title>
|
||||
<OrganizationAdminsTable
|
||||
organizationAdmins={organizationAdmins}
|
||||
loading={loadingAdmins}
|
||||
themeMode={themeMode}
|
||||
/>
|
||||
</Card>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Overview;
|
||||
@@ -0,0 +1,16 @@
|
||||
.project-table-row .row-buttons {
|
||||
visibility: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.project-table-row:hover .project-names,
|
||||
.project-table-row:hover .project-team,
|
||||
.project-table-row:hover .project-member-count,
|
||||
.project-table-row:hover .project-created-at {
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
.project-table-row:hover .row-buttons {
|
||||
visibility: visible;
|
||||
}
|
||||
227
worklenz-frontend/src/pages/admin-center/projects/projects.tsx
Normal file
227
worklenz-frontend/src/pages/admin-center/projects/projects.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RootState } from '@/app/store';
|
||||
import { IOrganizationProject } from '@/types/admin-center/admin-center.types';
|
||||
import { DEFAULT_PAGE_SIZE } from '@/shared/constants';
|
||||
import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service';
|
||||
import { formatDateTimeWithLocale } from '@/utils/format-date-time-with-locale';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import { deleteProject } from '@features/projects/projectsSlice';
|
||||
import './projects.css';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Flex,
|
||||
Input,
|
||||
Popconfirm,
|
||||
Table,
|
||||
TableProps,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { DeleteOutlined, SearchOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import { PageHeader } from '@ant-design/pro-components';
|
||||
import { projectsApiService } from '@/api/projects/projects.api.service';
|
||||
|
||||
const Projects: React.FC = () => {
|
||||
const themeMode = useAppSelector((state: RootState) => state.themeReducer.mode);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const isTablet = useMediaQuery({ query: '(min-width: 1000px)' });
|
||||
const [projects, setProjects] = useState<IOrganizationProject[]>([]);
|
||||
const [requestParams, setRequestParams] = useState({
|
||||
total: 0,
|
||||
index: 1,
|
||||
size: DEFAULT_PAGE_SIZE,
|
||||
field: 'name',
|
||||
order: 'desc',
|
||||
search: '',
|
||||
});
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { t } = useTranslation('admin-center/projects');
|
||||
|
||||
const fetchProjects = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await adminCenterApiService.getOrganizationProjects(requestParams);
|
||||
if (res.done) {
|
||||
setRequestParams(prev => ({ ...prev, total: res.body.total ?? 0 }));
|
||||
setProjects(res.body.data ?? []);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching teams', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteProject = async (id: string) => {
|
||||
if (!id) return;
|
||||
try {
|
||||
await projectsApiService.deleteProject(id);
|
||||
} catch (error) {
|
||||
logger.error('Error deleting project', error);
|
||||
} finally {
|
||||
fetchProjects();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchProjects();
|
||||
}, [
|
||||
requestParams.search,
|
||||
requestParams.index,
|
||||
requestParams.size,
|
||||
requestParams.field,
|
||||
requestParams.order,
|
||||
]);
|
||||
|
||||
const columns: TableProps['columns'] = [
|
||||
{
|
||||
title: 'Project name',
|
||||
key: 'projectName',
|
||||
render: (record: IOrganizationProject) => (
|
||||
<Typography.Text
|
||||
className="project-names"
|
||||
style={{ fontSize: `${isTablet ? '14px' : '10px'}` }}
|
||||
>
|
||||
{record.name}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Team',
|
||||
key: 'team',
|
||||
render: (record: IOrganizationProject) => (
|
||||
<Typography.Text
|
||||
className="project-team"
|
||||
style={{ fontSize: `${isTablet ? '14px' : '10px'}` }}
|
||||
>
|
||||
{record.team_name}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <span style={{ display: 'flex', justifyContent: 'center' }}>{t('membersCount')}</span>,
|
||||
key: 'membersCount',
|
||||
render: (record: IOrganizationProject) => (
|
||||
<Typography.Text
|
||||
className="project-member-count"
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
fontSize: `${isTablet ? '14px' : '10px'}`,
|
||||
}}
|
||||
>
|
||||
{record.member_count ?? 0}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <span style={{ display: 'flex', justifyContent: 'center' }}>Created at</span>,
|
||||
key: 'createdAt',
|
||||
render: (record: IOrganizationProject) => (
|
||||
<Typography.Text
|
||||
className="project-created-at"
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'right',
|
||||
fontSize: `${isTablet ? '14px' : '10px'}`,
|
||||
}}
|
||||
>
|
||||
{formatDateTimeWithLocale(record.created_at ?? '')}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'button',
|
||||
render: (record: IOrganizationProject) => (
|
||||
<div className="row-buttons">
|
||||
<Tooltip title={t('delete')}>
|
||||
<Popconfirm
|
||||
title={t('confirm')}
|
||||
description={t('deleteProject')}
|
||||
onConfirm={() => deleteProject(record.id ?? '')}
|
||||
>
|
||||
<Button size="small">
|
||||
<DeleteOutlined />
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
<PageHeader title={<span>Projects</span>} style={{ padding: '16px 0' }} />
|
||||
<PageHeader
|
||||
style={{
|
||||
paddingLeft: 0,
|
||||
paddingTop: 0,
|
||||
paddingRight: '24px',
|
||||
paddingBottom: '16px',
|
||||
}}
|
||||
subTitle={
|
||||
<span
|
||||
style={{
|
||||
color: `${themeMode === 'dark' ? '#ffffffd9' : '#000000d9'}`,
|
||||
fontWeight: 500,
|
||||
fontSize: '16px',
|
||||
}}
|
||||
>
|
||||
{projects.length} projects
|
||||
</span>
|
||||
}
|
||||
extra={
|
||||
<Flex gap={8} align="center">
|
||||
<Tooltip title={t('refreshProjects')}>
|
||||
<Button
|
||||
shape="circle"
|
||||
icon={<SyncOutlined spin={isLoading} />}
|
||||
onClick={() => fetchProjects()}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Input
|
||||
placeholder={t('searchPlaceholder')}
|
||||
suffix={<SearchOutlined />}
|
||||
type="text"
|
||||
value={requestParams.search}
|
||||
onChange={e => setRequestParams(prev => ({ ...prev, search: e.target.value }))}
|
||||
/>
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<Table<IOrganizationProject>
|
||||
rowClassName="project-table-row"
|
||||
className="project-table"
|
||||
columns={columns}
|
||||
dataSource={projects}
|
||||
rowKey={record => record.id ?? ''}
|
||||
loading={isLoading}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
defaultPageSize: 20,
|
||||
pageSizeOptions: ['5', '10', '15', '20', '50', '100'],
|
||||
size: 'small',
|
||||
total: requestParams.total,
|
||||
current: requestParams.index,
|
||||
pageSize: requestParams.size,
|
||||
onChange: (page, pageSize) =>
|
||||
setRequestParams(prev => ({ ...prev, index: page, size: pageSize })),
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Projects;
|
||||
@@ -0,0 +1,3 @@
|
||||
.admin-center-sidebar-button {
|
||||
border-radius: 0.5rem !important;
|
||||
}
|
||||
55
worklenz-frontend/src/pages/admin-center/sidebar/sidebar.tsx
Normal file
55
worklenz-frontend/src/pages/admin-center/sidebar/sidebar.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { RightOutlined } from '@ant-design/icons';
|
||||
import { ConfigProvider, Flex, Menu, MenuProps } from 'antd';
|
||||
import React from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { colors } from '../../../styles/colors';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { adminCenterItems } from '../admin-center-constants';
|
||||
import './sidebar.css';
|
||||
|
||||
const AdminCenterSidebar: React.FC = () => {
|
||||
const { t } = useTranslation('admin-center/sidebar');
|
||||
const location = useLocation();
|
||||
|
||||
type MenuItem = Required<MenuProps>['items'][number];
|
||||
const menuItems = adminCenterItems;
|
||||
|
||||
const items: MenuItem[] = [
|
||||
...menuItems.map(item => ({
|
||||
key: item.key,
|
||||
label: (
|
||||
<Flex gap={8} justify="space-between" className="admin-center-sidebar-button">
|
||||
<Flex gap={8}>
|
||||
{item.icon}
|
||||
<Link to={`/worklenz/admin-center/${item.endpoint}`}>{t(item.name)}</Link>
|
||||
</Flex>
|
||||
<RightOutlined style={{ fontSize: 12, fontWeight: 'bold' }} />
|
||||
</Flex>
|
||||
),
|
||||
})),
|
||||
];
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
components: {
|
||||
Menu: {
|
||||
itemHoverBg: colors.transparent,
|
||||
itemHoverColor: colors.skyBlue,
|
||||
borderRadius: 12,
|
||||
itemMarginBlock: 4,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
items={items}
|
||||
selectedKeys={[location.pathname.split('/worklenz/admin-center/')[1] || '']}
|
||||
mode="vertical"
|
||||
style={{ border: 'none', width: '100%' }}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminCenterSidebar;
|
||||
7
worklenz-frontend/src/pages/admin-center/teams/teams.css
Normal file
7
worklenz-frontend/src/pages/admin-center/teams/teams.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.team-table-row .row-buttons {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.team-table-row:hover .row-buttons {
|
||||
visibility: visible;
|
||||
}
|
||||
132
worklenz-frontend/src/pages/admin-center/teams/teams.tsx
Normal file
132
worklenz-frontend/src/pages/admin-center/teams/teams.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { SearchOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import { PageHeader } from '@ant-design/pro-components';
|
||||
import { Button, Flex, Input, Tooltip } from 'antd';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import {
|
||||
adminCenterApiService,
|
||||
IOrganizationTeamRequestParams,
|
||||
} from '@/api/admin-center/admin-center.api.service';
|
||||
|
||||
import { IOrganizationTeam } from '@/types/admin-center/admin-center.types';
|
||||
import './teams.css';
|
||||
import TeamsTable from '@/components/admin-center/teams/teams-table/teams-table';
|
||||
|
||||
import { DEFAULT_PAGE_SIZE } from '@/shared/constants';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import { RootState } from '@/app/store';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import AddTeamDrawer from '@/components/admin-center/teams/add-team-drawer/add-team-drawer';
|
||||
|
||||
export interface IRequestParams extends IOrganizationTeamRequestParams {
|
||||
total: number;
|
||||
}
|
||||
|
||||
const Teams: React.FC = () => {
|
||||
const themeMode = useAppSelector((state: RootState) => state.themeReducer.mode);
|
||||
const { t } = useTranslation('admin-center/teams');
|
||||
|
||||
const [showAddTeamDrawer, setShowAddTeamDrawer] = useState(false);
|
||||
|
||||
const [teams, setTeams] = useState<IOrganizationTeam[]>([]);
|
||||
const [currentTeam, setCurrentTeam] = useState<IOrganizationTeam | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [requestParams, setRequestParams] = useState<IRequestParams>({
|
||||
total: 0,
|
||||
index: 1,
|
||||
size: DEFAULT_PAGE_SIZE,
|
||||
field: 'name',
|
||||
order: 'desc',
|
||||
search: '',
|
||||
});
|
||||
|
||||
const fetchTeams = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await adminCenterApiService.getOrganizationTeams(requestParams);
|
||||
if (res.done) {
|
||||
setRequestParams(prev => ({ ...prev, total: res.body.total ?? 0 }));
|
||||
const mergedTeams = [...(res.body.data ?? [])];
|
||||
if (res.body.current_team_data) {
|
||||
mergedTeams.unshift(res.body.current_team_data);
|
||||
}
|
||||
setTeams(mergedTeams);
|
||||
setCurrentTeam(res.body.current_team_data ?? null);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching teams', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTeams();
|
||||
}, [requestParams.search]);
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
<PageHeader title={<span>{t('title')}</span>} style={{ padding: '16px 0' }} />
|
||||
<PageHeader
|
||||
style={{
|
||||
paddingLeft: 0,
|
||||
paddingTop: 0,
|
||||
paddingRight: '24px',
|
||||
paddingBottom: '16px',
|
||||
}}
|
||||
subTitle={
|
||||
<span
|
||||
style={{
|
||||
color: `${themeMode === 'dark' ? '#ffffffd9' : '#000000d9'}`,
|
||||
fontWeight: 500,
|
||||
fontSize: '16px',
|
||||
}}
|
||||
>
|
||||
{requestParams.total} {t('subtitle')}
|
||||
</span>
|
||||
}
|
||||
extra={
|
||||
<Flex gap={8} align="center">
|
||||
<Tooltip title={t('tooltip')}>
|
||||
<Button
|
||||
shape="circle"
|
||||
icon={<SyncOutlined spin={isLoading} />}
|
||||
onClick={() => fetchTeams()}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Input
|
||||
placeholder={t('placeholder')}
|
||||
suffix={<SearchOutlined />}
|
||||
type="text"
|
||||
value={requestParams.search ?? ''}
|
||||
onChange={e => setRequestParams(prev => ({ ...prev, search: e.target.value }))}
|
||||
/>
|
||||
<Button type="primary" onClick={() => setShowAddTeamDrawer(true)}>
|
||||
{t('addTeam')}
|
||||
</Button>
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
|
||||
<TeamsTable
|
||||
teams={teams}
|
||||
currentTeam={currentTeam}
|
||||
t={t}
|
||||
loading={isLoading}
|
||||
reloadTeams={fetchTeams}
|
||||
/>
|
||||
|
||||
<AddTeamDrawer
|
||||
isDrawerOpen={showAddTeamDrawer}
|
||||
onClose={() => setShowAddTeamDrawer(false)}
|
||||
reloadTeams={fetchTeams}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Teams;
|
||||
142
worklenz-frontend/src/pages/admin-center/users/users.tsx
Normal file
142
worklenz-frontend/src/pages/admin-center/users/users.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { SearchOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import { PageHeader } from '@ant-design/pro-components';
|
||||
import { Button, Card, Flex, Input, Table, TableProps, Tooltip, Typography } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { RootState } from '@/app/store';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service';
|
||||
import { IOrganizationUser } from '@/types/admin-center/admin-center.types';
|
||||
import { DEFAULT_PAGE_SIZE, PAGE_SIZE_OPTIONS } from '@/shared/constants';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import { formatDateTimeWithLocale } from '@/utils/format-date-time-with-locale';
|
||||
import SingleAvatar from '@/components/common/single-avatar/single-avatar';
|
||||
|
||||
const Users: React.FC = () => {
|
||||
const { t } = useTranslation('admin-center/users');
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [users, setUsers] = useState<IOrganizationUser[]>([]);
|
||||
const [requestParams, setRequestParams] = useState({
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
sort: 'name',
|
||||
order: 'desc',
|
||||
searchTerm: '',
|
||||
});
|
||||
|
||||
const themeMode = useAppSelector((state: RootState) => state.themeReducer.mode);
|
||||
|
||||
const fetchUsers = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await adminCenterApiService.getOrganizationUsers(requestParams);
|
||||
if (res.done) {
|
||||
setUsers(res.body.data ?? []);
|
||||
setRequestParams(prev => ({ ...prev, total: res.body.total ?? 0 }));
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching users', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const columns: TableProps<IOrganizationUser>['columns'] = [
|
||||
{
|
||||
title: t('user'),
|
||||
dataIndex: 'user',
|
||||
key: 'user',
|
||||
render: (_, record) => (
|
||||
<Flex gap={8} align="center">
|
||||
<SingleAvatar avatarUrl={record.avatar_url} name={record.name} />
|
||||
<Typography.Text>{record.name}</Typography.Text>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('email'),
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
render: text => (
|
||||
<span className="email-hover">
|
||||
<Typography.Text copyable={{ text }}>
|
||||
{text}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('lastActivity'),
|
||||
dataIndex: 'last_logged',
|
||||
key: 'last_logged',
|
||||
render: text => <span>{formatDateTimeWithLocale(text) || '-'}</span>,
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, [requestParams.searchTerm, requestParams.page, requestParams.pageSize]);
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
<PageHeader title={<span>{t('title')}</span>} style={{ padding: '16px 0' }} />
|
||||
<PageHeader
|
||||
style={{
|
||||
paddingLeft: 0,
|
||||
paddingTop: 0,
|
||||
paddingBottom: '16px',
|
||||
}}
|
||||
subTitle={
|
||||
<span
|
||||
style={{
|
||||
color: `${themeMode === 'dark' ? '#ffffffd9' : '#000000d9'}`,
|
||||
fontWeight: 500,
|
||||
fontSize: '16px',
|
||||
}}
|
||||
>
|
||||
{requestParams.total} {t('subTitle')}
|
||||
</span>
|
||||
}
|
||||
extra={
|
||||
<Flex gap={8} align="center">
|
||||
<Tooltip title={t('refresh')}>
|
||||
<Button
|
||||
shape="circle"
|
||||
icon={<SyncOutlined spin={isLoading} />}
|
||||
onClick={() => fetchUsers()}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Input
|
||||
placeholder={t('placeholder')}
|
||||
suffix={<SearchOutlined />}
|
||||
type="text"
|
||||
value={requestParams.searchTerm}
|
||||
onChange={e => setRequestParams(prev => ({ ...prev, searchTerm: e.target.value }))}
|
||||
/>
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
<Card>
|
||||
<Table
|
||||
rowClassName="users-table-row"
|
||||
size="small"
|
||||
columns={columns}
|
||||
dataSource={users}
|
||||
pagination={{
|
||||
defaultPageSize: DEFAULT_PAGE_SIZE,
|
||||
pageSizeOptions: PAGE_SIZE_OPTIONS,
|
||||
size: 'small',
|
||||
showSizeChanger: true,
|
||||
total: requestParams.total,
|
||||
onChange: (page, pageSize) => setRequestParams(prev => ({ ...prev, page, pageSize })),
|
||||
}}
|
||||
loading={isLoading}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Users;
|
||||
Reference in New Issue
Block a user