This commit is contained in:
chamikaJ
2025-04-17 18:28:54 +05:30
parent f583291d8a
commit 8825b0410a
2837 changed files with 241385 additions and 127578 deletions

View File

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

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

View File

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

View File

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

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

View File

@@ -0,0 +1,3 @@
.admin-center-sidebar-button {
border-radius: 0.5rem !important;
}

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

View File

@@ -0,0 +1,7 @@
.team-table-row .row-buttons {
visibility: hidden;
}
.team-table-row:hover .row-buttons {
visibility: visible;
}

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

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