init
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { Flex, Skeleton } from 'antd';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import CustomSearchbar from '../../../../CustomSearchbar';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import OverviewReportsMembersTable from './reporting-overview-members-table';
|
||||
import { IRPTMember } from '@/types/reporting/reporting.types';
|
||||
import { reportingApiService } from '@/api/reporting/reporting.api.service';
|
||||
|
||||
type OverviewReportsMembersTabProps = { teamsId?: string | null };
|
||||
|
||||
const OverviewReportsMembersTab = ({ teamsId = null }: OverviewReportsMembersTabProps) => {
|
||||
const { t } = useTranslation('reporting-overview-drawer');
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
|
||||
return (
|
||||
<Flex vertical gap={24}>
|
||||
<CustomSearchbar
|
||||
placeholderText={t('searchByNameInputPlaceholder')}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
/>
|
||||
|
||||
{<OverviewReportsMembersTable teamsId={teamsId} searchQuery={searchQuery} />}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default OverviewReportsMembersTab;
|
||||
@@ -0,0 +1,127 @@
|
||||
import React, { memo, useEffect, useMemo, useState } from 'react';
|
||||
import { ConfigProvider, Table, TableColumnsType } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import CustomTableTitle from '../../../../CustomTableTitle';
|
||||
import { reportingApiService } from '@/api/reporting/reporting.api.service';
|
||||
import { IRPTMember } from '@/types/reporting/reporting.types';
|
||||
|
||||
type OverviewReportsMembersReportsTableProps = {
|
||||
teamsId: string | null;
|
||||
searchQuery: string;
|
||||
};
|
||||
|
||||
const OverviewReportsMembersReportsTable = ({
|
||||
teamsId,
|
||||
searchQuery,
|
||||
}: OverviewReportsMembersReportsTableProps) => {
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [membersList, setMembersList] = useState<IRPTMember[]>([]);
|
||||
// localization
|
||||
const { t } = useTranslation('reporting-overview-drawer');
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// function to handle drawer toggle
|
||||
const handleDrawerOpen = (id: string) => {
|
||||
setSelectedId(id);
|
||||
// dispatch(toggleMembersReportsDrawer());
|
||||
};
|
||||
|
||||
const getMembersList = async () => {
|
||||
if (!teamsId) return;
|
||||
|
||||
const res = await reportingApiService.getOverviewMembersByTeam(teamsId, false);
|
||||
if (res.done) {
|
||||
setMembersList(res.body);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredMembersList = useMemo(() => {
|
||||
return membersList?.filter(item => item.name.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||
}, [searchQuery, membersList]);
|
||||
|
||||
useEffect(() => {
|
||||
getMembersList();
|
||||
}, []);
|
||||
|
||||
const columns: TableColumnsType = [
|
||||
{
|
||||
key: 'name',
|
||||
title: <CustomTableTitle title={t('nameColumn')} />,
|
||||
className: 'group-hover:text-[#1890ff]',
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
title: <CustomTableTitle title={t('emailColumn')} />,
|
||||
className: 'group-hover:text-[#1890ff]',
|
||||
dataIndex: 'email',
|
||||
},
|
||||
{
|
||||
key: 'projects',
|
||||
title: <CustomTableTitle title={t('projectsColumn')} />,
|
||||
className: 'text-center group-hover:text-[#1890ff]',
|
||||
dataIndex: 'projects',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
key: 'tasks',
|
||||
title: <CustomTableTitle title={t('tasksColumn')} />,
|
||||
className: 'text-center group-hover:text-[#1890ff]',
|
||||
dataIndex: 'tasks',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
key: 'overdueTasks',
|
||||
title: <CustomTableTitle title={t('overdueTasksColumn')} />,
|
||||
className: 'text-center group-hover:text-[#1890ff]',
|
||||
dataIndex: 'overdue',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
key: 'completedTasks',
|
||||
title: <CustomTableTitle title={t('completedTasksColumn')} />,
|
||||
className: 'text-center group-hover:text-[#1890ff]',
|
||||
dataIndex: 'completed',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
key: 'ongoingTasks',
|
||||
title: <CustomTableTitle title={t('ongoingTasksColumn')} />,
|
||||
className: 'text-center group-hover:text-[#1890ff]',
|
||||
dataIndex: 'ongoing',
|
||||
width: 120,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
components: {
|
||||
Table: {
|
||||
cellPaddingBlock: 8,
|
||||
cellPaddingInline: 10,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={filteredMembersList}
|
||||
scroll={{ x: 'max-content' }}
|
||||
rowKey={record => record.id}
|
||||
onRow={record => {
|
||||
return {
|
||||
style: { height: 38, cursor: 'pointer' },
|
||||
className: 'group even:bg-[#4e4e4e10]',
|
||||
};
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* <MembersReportsDrawer memberId={selectedId} /> */}
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(OverviewReportsMembersReportsTable);
|
||||
@@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import { Doughnut } from 'react-chartjs-2';
|
||||
import { Chart, ArcElement, Tooltip, ChartOptions } from 'chart.js';
|
||||
import { Badge, Card, Flex, Typography } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IRPTOverviewTeamChartData } from '@/types/reporting/reporting.types';
|
||||
|
||||
Chart.register(ArcElement, Tooltip);
|
||||
|
||||
const OverviewReportsProjectCategoryGraph = ({
|
||||
data,
|
||||
}: {
|
||||
data: IRPTOverviewTeamChartData | undefined;
|
||||
}) => {
|
||||
// localization
|
||||
const { t } = useTranslation('reporting-overview-drawer');
|
||||
|
||||
type CategoryGraphItemType = {
|
||||
name: string;
|
||||
color: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
// mock data
|
||||
const categoryGraphItems: CategoryGraphItemType[] =
|
||||
data?.data.map(category => ({
|
||||
name: category.label,
|
||||
color: category.color,
|
||||
count: category.count,
|
||||
})) ?? [];
|
||||
|
||||
// chart data
|
||||
const chartData = {
|
||||
labels: categoryGraphItems.map(item => item.name),
|
||||
datasets: [
|
||||
{
|
||||
label: t('projectsText'),
|
||||
data: categoryGraphItems.map(item => item.count),
|
||||
backgroundColor: categoryGraphItems.map(item => item.color),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const totalTasks = categoryGraphItems.reduce((sum, item) => sum + item.count, 0);
|
||||
|
||||
const options: ChartOptions<'doughnut'> = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
datalabels: {
|
||||
display: false,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: context => {
|
||||
const value = context.raw as number;
|
||||
return `${context.label}: ${value} task${value !== 1 ? 's' : ''}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<Typography.Text style={{ fontSize: 16, fontWeight: 500 }}>
|
||||
{t('projectsByCategoryText')}
|
||||
</Typography.Text>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-center gap-6 xl:flex-nowrap">
|
||||
<Doughnut
|
||||
data={chartData}
|
||||
options={options}
|
||||
className="max-h-[200px] w-full max-w-[200px]"
|
||||
/>
|
||||
|
||||
<div className="flex flex-row flex-wrap gap-3 xl:flex-col">
|
||||
{/* total tasks */}
|
||||
<Flex gap={4} align="center">
|
||||
<Badge color="#a9a9a9" />
|
||||
<Typography.Text ellipsis>
|
||||
{t('allText')} ({totalTasks})
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
|
||||
{/* category-specific tasks */}
|
||||
{categoryGraphItems.map(item => (
|
||||
<Flex key={item.name} gap={4} align="center">
|
||||
<Badge color={item.color} />
|
||||
<Typography.Text ellipsis>
|
||||
{item.name} ({item.count})
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default OverviewReportsProjectCategoryGraph;
|
||||
@@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import { Doughnut } from 'react-chartjs-2';
|
||||
import { Chart, ArcElement, Tooltip, ChartOptions } from 'chart.js';
|
||||
import { Badge, Card, Flex, Typography } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
IRPTOverviewTeamByHealth,
|
||||
IRPTOverviewTeamChartData,
|
||||
} from '@/types/reporting/reporting.types';
|
||||
import { ALPHA_CHANNEL } from '@/shared/constants';
|
||||
|
||||
Chart.register(ArcElement, Tooltip);
|
||||
|
||||
const OverviewReportsProjectHealthGraph = ({
|
||||
data,
|
||||
}: {
|
||||
data: IRPTOverviewTeamByHealth | undefined;
|
||||
}) => {
|
||||
const { t } = useTranslation('reporting-overview-drawer');
|
||||
|
||||
type HealthGraphItemType = {
|
||||
name: string;
|
||||
color: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
const options: ChartOptions<'doughnut'> = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
datalabels: {
|
||||
display: false,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: context => {
|
||||
const value = context.raw as number;
|
||||
return `${context.label}: ${value} task${value !== 1 ? 's' : ''}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// mock data
|
||||
const healthGraphItems: HealthGraphItemType[] = [
|
||||
{ name: 'notSet', color: '#a9a9a9', count: data?.not_set ?? 0 },
|
||||
{ name: 'needsAttention', color: '#f37070', count: data?.needs_attention ?? 0 },
|
||||
{ name: 'atRisk', color: '#fbc84c', count: data?.at_risk ?? 0 },
|
||||
{ name: 'good', color: '#75c997', count: data?.good ?? 0 },
|
||||
];
|
||||
|
||||
const chartData = {
|
||||
labels: healthGraphItems.map(item => item.name),
|
||||
datasets: [
|
||||
{
|
||||
data: healthGraphItems.map(item => item.count),
|
||||
backgroundColor: healthGraphItems.map(item => item.color + ALPHA_CHANNEL),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<Typography.Text style={{ fontSize: 16, fontWeight: 500 }}>
|
||||
{t('projectsByHealthText')}
|
||||
</Typography.Text>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-center gap-6 xl:flex-nowrap">
|
||||
<Doughnut
|
||||
data={chartData}
|
||||
options={options}
|
||||
className="max-h-[200px] w-full max-w-[200px]"
|
||||
/>
|
||||
|
||||
<div className="flex flex-row flex-wrap gap-3 xl:flex-col">
|
||||
{/* total tasks */}
|
||||
<Flex gap={4} align="center">
|
||||
<Badge color="#a9a9a9" />
|
||||
<Typography.Text ellipsis>
|
||||
{t('allText')} ({data?.all})
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
|
||||
{/* health-specific tasks */}
|
||||
{healthGraphItems.map(item => (
|
||||
<Flex key={item.name} gap={4} align="center">
|
||||
<Badge color={item.color} />
|
||||
<Typography.Text ellipsis>
|
||||
{t(`${item.name}Text`)} ({item.count})
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default OverviewReportsProjectHealthGraph;
|
||||
@@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import { Doughnut } from 'react-chartjs-2';
|
||||
import { Chart, ArcElement, Tooltip, ChartOptions } from 'chart.js';
|
||||
import { Badge, Card, Flex, Typography } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IRPTOverviewTeamByStatus, IRPTOverviewTeamInfo } from '@/types/reporting/reporting.types';
|
||||
import { ALPHA_CHANNEL } from '@/shared/constants';
|
||||
|
||||
Chart.register(ArcElement, Tooltip);
|
||||
|
||||
const OverviewReportsProjectStatusGraph = ({
|
||||
data,
|
||||
}: {
|
||||
data: IRPTOverviewTeamByStatus | undefined;
|
||||
}) => {
|
||||
const { t } = useTranslation('reporting-overview-drawer');
|
||||
|
||||
type StatusGraphItemType = {
|
||||
name: string;
|
||||
color: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
const statusGraphItems: StatusGraphItemType[] = [
|
||||
{ name: 'inProgress', color: '#80ca79', count: data?.in_progress ?? 0 },
|
||||
{ name: 'inPlanning', color: '#cbc8a1', count: data?.in_planning ?? 0 },
|
||||
{ name: 'completed', color: '#80ca79', count: data?.completed ?? 0 },
|
||||
{ name: 'proposed', color: '#cbc8a1', count: data?.proposed ?? 0 },
|
||||
{ name: 'onHold', color: '#cbc8a1', count: data?.on_hold ?? 0 },
|
||||
{ name: 'blocked', color: '#cbc8a1', count: data?.blocked ?? 0 },
|
||||
{ name: 'cancelled', color: '#f37070', count: data?.cancelled ?? 0 },
|
||||
];
|
||||
|
||||
const chartData = {
|
||||
labels: statusGraphItems.map(item => item.name),
|
||||
datasets: [
|
||||
{
|
||||
data: statusGraphItems.map(item => item.count),
|
||||
backgroundColor: statusGraphItems.map(item => item.color + ALPHA_CHANNEL),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options: ChartOptions<'doughnut'> = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
datalabels: {
|
||||
display: false,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: context => {
|
||||
const value = context.raw as number;
|
||||
return `${context.label}: ${value} task${value !== 1 ? 's' : ''}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const totalTasks = statusGraphItems.reduce((sum, item) => sum + item.count, 0);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<Typography.Text style={{ fontSize: 16, fontWeight: 500 }}>
|
||||
{t('projectsByStatusText')}
|
||||
</Typography.Text>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-center gap-6 xl:flex-nowrap">
|
||||
<Doughnut
|
||||
data={chartData}
|
||||
options={options}
|
||||
className="max-h-[200px] w-full max-w-[200px]"
|
||||
/>
|
||||
|
||||
<div className="flex flex-row flex-wrap gap-3 xl:flex-col">
|
||||
{/* total tasks */}
|
||||
<Flex gap={4} align="center">
|
||||
<Badge color="#a9a9a9" />
|
||||
<Typography.Text ellipsis>
|
||||
{t('allText')} ({data?.all})
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
|
||||
{/* status-specific tasks */}
|
||||
{statusGraphItems.map(item => (
|
||||
<Flex key={item.name} gap={4} align="center">
|
||||
<Badge color={item.color} />
|
||||
<Typography.Text ellipsis>
|
||||
{t(`${item.name}Text`)} ({item.count})
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default OverviewReportsProjectStatusGraph;
|
||||
@@ -0,0 +1,46 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ReportsOverviewStatusGraph from './reports-overview-status-graph';
|
||||
import OverviewReportsProjectCategoryGraph from './reports-overview-category-graph';
|
||||
import OverviewReportsProjectHealthGraph from './reports-overview-project-health-graph';
|
||||
import { IRPTOverviewTeamInfo } from '@/types/reporting/reporting.types';
|
||||
import { reportingApiService } from '@/api/reporting/reporting.api.service';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
|
||||
type OverviewReportsOverviewTabProps = {
|
||||
teamId?: string | null;
|
||||
};
|
||||
|
||||
const OverviewReportsOverviewTab = ({ teamId = null }: OverviewReportsOverviewTabProps) => {
|
||||
const [model, setModel] = useState<IRPTOverviewTeamInfo | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { includeArchivedProjects } = useAppSelector(state => state.reportingReducer);
|
||||
|
||||
const getModelData = async () => {
|
||||
if (!teamId) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
const { done, body } = await reportingApiService.getTeamInfo(teamId, includeArchivedProjects);
|
||||
if (done) {
|
||||
setModel(body);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getModelData();
|
||||
}, [includeArchivedProjects]);
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<ReportsOverviewStatusGraph data={model?.by_status} />
|
||||
<OverviewReportsProjectCategoryGraph data={model?.by_category} />
|
||||
<OverviewReportsProjectHealthGraph data={model?.by_health} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OverviewReportsOverviewTab;
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Tabs } from 'antd';
|
||||
import { TabsProps } from 'antd/lib';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import OverviewReportsOverviewTab from './overview-tab/reports-overview-tab';
|
||||
import OverviewReportsProjectsTab from './projects-tab/reporting-overview-projects-tab';
|
||||
import OverviewReportsMembersTab from './members-tab/reporting-overview-members-tab';
|
||||
|
||||
type OverviewTeamInfoDrawerProps = {
|
||||
teamsId?: string | null;
|
||||
};
|
||||
|
||||
const OverviewTeamInfoDrawerTabs = ({ teamsId = null }: OverviewTeamInfoDrawerProps) => {
|
||||
const { t } = useTranslation('reporting-overview-drawer');
|
||||
|
||||
const tabItems: TabsProps['items'] = [
|
||||
{
|
||||
key: 'overview',
|
||||
label: t('overviewTab'),
|
||||
children: <OverviewReportsOverviewTab teamId={teamsId} />,
|
||||
},
|
||||
{
|
||||
key: 'projects',
|
||||
label: t('projectsTab'),
|
||||
children: <OverviewReportsProjectsTab teamsId={teamsId} />,
|
||||
},
|
||||
{
|
||||
key: 'members',
|
||||
label: t('membersTab'),
|
||||
children: <OverviewReportsMembersTab teamsId={teamsId} />,
|
||||
},
|
||||
];
|
||||
|
||||
return <Tabs type="card" items={tabItems} destroyInactiveTabPane defaultActiveKey="overview" />;
|
||||
};
|
||||
|
||||
export default OverviewTeamInfoDrawerTabs;
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Drawer, Typography, Flex, Button, Dropdown } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { useAppSelector } from '../../../../hooks/useAppSelector';
|
||||
import { useAppDispatch } from '../../../../hooks/useAppDispatch';
|
||||
import { BankOutlined, DownOutlined } from '@ant-design/icons';
|
||||
import { colors } from '../../../../styles/colors';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import OverviewTeamInfoDrawerTabs from './overview-team-info-drawer-tabs';
|
||||
import { toggleOverViewTeamDrawer } from '@/features/reporting/reporting.slice';
|
||||
import { IRPTTeam } from '@/types/reporting/reporting.types';
|
||||
|
||||
type OverviewTeamInfoDrawerProps = {
|
||||
team: IRPTTeam | null;
|
||||
};
|
||||
|
||||
const OverviewTeamInfoDrawer = ({ team }: OverviewTeamInfoDrawerProps) => {
|
||||
const { t } = useTranslation('reporting-overview-drawer');
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const isDrawerOpen = useAppSelector(state => state.reportingReducer.showOverViewTeamDrawer);
|
||||
|
||||
const handleClose = () => {
|
||||
dispatch(toggleOverViewTeamDrawer());
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open={isDrawerOpen}
|
||||
destroyOnClose
|
||||
onClose={handleClose}
|
||||
width={900}
|
||||
title={
|
||||
team && (
|
||||
<Flex align="center" justify="space-between">
|
||||
<Flex gap={4} align="center" style={{ fontWeight: 500 }}>
|
||||
<BankOutlined style={{ color: colors.lightGray }} />
|
||||
<Typography.Text style={{ fontSize: 16 }}>{team.name}</Typography.Text>
|
||||
</Flex>
|
||||
|
||||
{/* <Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{ key: '1', label: t('projectsButton') },
|
||||
{ key: '2', label: t('membersButton') },
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Button type="primary" icon={<DownOutlined />} iconPosition="end">
|
||||
{t('exportButton')}
|
||||
</Button>
|
||||
</Dropdown> */}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
>
|
||||
<OverviewTeamInfoDrawerTabs teamsId={team?.id} />
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default OverviewTeamInfoDrawer;
|
||||
@@ -0,0 +1,57 @@
|
||||
[data-theme="dark"] {
|
||||
--table-cell-bg: #181818;
|
||||
--hover-bg: #000;
|
||||
--base-bg: #181818;
|
||||
}
|
||||
|
||||
[data-theme="default"] {
|
||||
--table-cell-bg: #4e4e4e10;
|
||||
--hover-bg: #edebf0;
|
||||
--base-bg: #ffffff;
|
||||
}
|
||||
|
||||
/* change the scroll bar in the table */
|
||||
:where(.css-dev-only-do-not-override-ifo476).ant-table-wrapper .ant-table {
|
||||
scrollbar-color: unset;
|
||||
}
|
||||
|
||||
/* Fix sticky column background */
|
||||
.ant-table-wrapper .ant-table-cell-fix-left {
|
||||
background-color: var(--base-bg) !important;
|
||||
z-index: 2 !important;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table-row:nth-child(even) .ant-table-cell-fix-left {
|
||||
background-color: var(--table-cell-bg) !important;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table-row:hover .ant-table-cell-fix-left {
|
||||
background-color: var(--hover-bg) !important;
|
||||
}
|
||||
|
||||
/* Shadow for sticky columns */
|
||||
:where(.css-dev-only-do-not-override-aw0rds).ant-table-wrapper
|
||||
.ant-table-ping-left
|
||||
.ant-table-cell-fix-left-first::after {
|
||||
box-shadow: inset 10px 0 8px -8px rgba(0, 0, 0, 0.15);
|
||||
right: -1px;
|
||||
}
|
||||
|
||||
:where(.css-dev-only-do-not-override-aw0rds).ant-table-wrapper
|
||||
.ant-table-ping-right:not(.ant-table-has-fix-right)
|
||||
.ant-table-container::after {
|
||||
box-shadow: inset -10px 0 8px -8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* remove select border */
|
||||
:where(.css-dev-only-do-not-override-tmno9t).ant-select-outlined:not(.ant-select-customize-input)
|
||||
.ant-select-selector {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ant-select-focused:where(.css-dev-only-do-not-override-tmno9t).ant-select-outlined:not(
|
||||
.ant-select-disabled
|
||||
):not(.ant-select-customize-input):not(.ant-pagination-size-changer)
|
||||
.ant-select-selector {
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Flex } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import CustomSearchbar from '@components/CustomSearchbar';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ReportingOverviewProjectsTable from './reporting-overview-projects-table';
|
||||
|
||||
interface OverviewReportsProjectsTabProps {
|
||||
teamsId?: string | null;
|
||||
}
|
||||
|
||||
const OverviewReportsProjectsTab = ({ teamsId = null }: OverviewReportsProjectsTabProps) => {
|
||||
const { t } = useTranslation('reporting-projects-drawer');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
|
||||
return (
|
||||
<Flex vertical gap={24}>
|
||||
<CustomSearchbar
|
||||
placeholderText={t('searchByNameInputPlaceholder')}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
/>
|
||||
|
||||
<ReportingOverviewProjectsTable searchQuery={searchQuery} teamsId={teamsId} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default OverviewReportsProjectsTab;
|
||||
@@ -0,0 +1,331 @@
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import { Button, ConfigProvider, Flex, PaginationProps, Table, TableColumnsType } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { ExpandAltOutlined } from '@ant-design/icons';
|
||||
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import ProjectCell from '@/pages/reporting/projects-reports/projects-reports-table/table-cells/project-cell/project-cell';
|
||||
import EstimatedVsActualCell from '@/pages/reporting/projects-reports/projects-reports-table/table-cells/estimated-vs-actual-cell/estimated-vs-actual-cell';
|
||||
import TasksProgressCell from '@/pages/reporting/projects-reports/projects-reports-table/table-cells/tasks-progress-cell/tasks-progress-cell';
|
||||
import LastActivityCell from '@/pages/reporting/projects-reports/projects-reports-table/table-cells/last-activity-cell/last-activity-cell';
|
||||
import ProjectStatusCell from '@/pages/reporting/projects-reports/projects-reports-table/table-cells/project-status-cell/project-status-cell';
|
||||
import ProjectClientCell from '@/pages/reporting/projects-reports/projects-reports-table/table-cells/project-client-cell/project-client-cell';
|
||||
import ProjectTeamCell from '@/pages/reporting/projects-reports/projects-reports-table/table-cells/project-team-cell/project-team-cell';
|
||||
import ProjectManagerCell from '@/pages/reporting/projects-reports/projects-reports-table/table-cells/project-manager-cell/project-manager-cell';
|
||||
import ProjectDatesCell from '@/pages/reporting/projects-reports/projects-reports-table/table-cells/project-dates-cell/project-dates-cell';
|
||||
import ProjectHealthCell from '@/pages/reporting/projects-reports/projects-reports-table/table-cells/project-health-cell/project-health-cell';
|
||||
import ProjectCategoryCell from '@/pages/reporting/projects-reports/projects-reports-table/table-cells/project-category-cell/project-category-cell';
|
||||
import ProjectDaysLeftAndOverdueCell from '@/pages/reporting/projects-reports/projects-reports-table/table-cells/project-days-left-and-overdue-cell/project-days-left-and-overdue-cell';
|
||||
import ProjectUpdateCell from '@/pages/reporting/projects-reports/projects-reports-table/table-cells/project-update-cell/project-update-cell';
|
||||
import {
|
||||
resetProjectReports,
|
||||
setField,
|
||||
setIndex,
|
||||
setOrder,
|
||||
setPageSize,
|
||||
toggleProjectReportsDrawer,
|
||||
} from '@/features/reporting/projectReports/project-reports-slice';
|
||||
import { colors } from '@/styles/colors';
|
||||
import CustomTableTitle from '@/components/CustomTableTitle';
|
||||
import { IRPTProject } from '@/types/reporting/reporting.types';
|
||||
import ProjectReportsDrawer from '@/features/reporting/projectReports/projectReportsDrawer/ProjectReportsDrawer';
|
||||
import { DEFAULT_PAGE_SIZE, PAGE_SIZE_OPTIONS } from '@/shared/constants';
|
||||
import './projects-reports-table.css';
|
||||
import { fetchProjectStatuses } from '@/features/projects/lookups/projectStatuses/projectStatusesSlice';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import { reportingApiService } from '@/api/reporting/reporting.api.service';
|
||||
|
||||
interface ReportingOverviewProjectsTableProps {
|
||||
searchQuery: string;
|
||||
teamsId: string | null;
|
||||
}
|
||||
|
||||
const ReportingOverviewProjectsTable = ({
|
||||
searchQuery,
|
||||
teamsId,
|
||||
}: ReportingOverviewProjectsTableProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation('reporting-projects');
|
||||
|
||||
const { includeArchivedProjects } = useAppSelector(state => state.reportingReducer);
|
||||
const [projectList, setProjectList] = useState<IRPTProject[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [pagination, setPagination] = useState<PaginationProps>({
|
||||
current: 1,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const [order, setOrder] = useState<'asc' | 'desc'>('asc');
|
||||
const [field, setField] = useState<string>('name');
|
||||
|
||||
const [selectedProject, setSelectedProject] = useState<IRPTProject | null>(null);
|
||||
const { projectStatuses, loading: projectStatusesLoading } = useAppSelector(
|
||||
state => state.projectStatusesReducer
|
||||
);
|
||||
|
||||
const handleDrawerOpen = (record: IRPTProject) => {
|
||||
setSelectedProject(record);
|
||||
dispatch(toggleProjectReportsDrawer());
|
||||
};
|
||||
|
||||
const columns: TableColumnsType<IRPTProject> = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'name',
|
||||
dataIndex: 'name',
|
||||
title: <CustomTableTitle title={t('projectColumn')} />,
|
||||
width: 300,
|
||||
sorter: true,
|
||||
defaultSortOrder: order === 'asc' ? 'ascend' : 'descend',
|
||||
fixed: 'left' as const,
|
||||
onCell: record => ({
|
||||
onClick: () => handleDrawerOpen(record as IRPTProject),
|
||||
}),
|
||||
render: (_, record: { id: string; name: string; color_code: string }) => (
|
||||
<Flex gap={16} align="center" justify="space-between">
|
||||
<ProjectCell
|
||||
projectId={record.id}
|
||||
project={record.name}
|
||||
projectColor={record.color_code}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="hidden group-hover:flex"
|
||||
type="text"
|
||||
style={{
|
||||
backgroundColor: colors.transparent,
|
||||
padding: 0,
|
||||
height: 22,
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
{t('openButton')} <ExpandAltOutlined />
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'estimatedVsActual',
|
||||
title: <CustomTableTitle title={t('estimatedVsActualColumn')} />,
|
||||
render: record => (
|
||||
<EstimatedVsActualCell
|
||||
actualTime={record.actual_time || 0}
|
||||
actualTimeString={record.actual_time_string}
|
||||
estimatedTime={record.estimated_time * 60 || 0}
|
||||
estimatedTimeString={record.estimated_time_string}
|
||||
/>
|
||||
),
|
||||
width: 230,
|
||||
},
|
||||
{
|
||||
key: 'tasksProgress',
|
||||
title: <CustomTableTitle title={t('tasksProgressColumn')} />,
|
||||
render: record => <TasksProgressCell tasksStat={record.tasks_stat} />,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
key: 'lastActivity',
|
||||
title: <CustomTableTitle title={t('lastActivityColumn')} />,
|
||||
render: record => (
|
||||
<LastActivityCell activity={record.last_activity?.last_activity_string} />
|
||||
),
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
dataIndex: 'status_id',
|
||||
defaultSortOrder: order === 'asc' ? 'ascend' : 'descend',
|
||||
title: <CustomTableTitle title={t('statusColumn')} />,
|
||||
render: (_, record: IRPTProject) => (
|
||||
<ProjectStatusCell currentStatus={record.status_id} projectId={record.id} />
|
||||
),
|
||||
width: 200,
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
key: 'dates',
|
||||
title: <CustomTableTitle title={t('datesColumn')} />,
|
||||
render: record => (
|
||||
<ProjectDatesCell
|
||||
projectId={record.id}
|
||||
startDate={record.start_date}
|
||||
endDate={record.end_date}
|
||||
/>
|
||||
),
|
||||
width: 275,
|
||||
},
|
||||
{
|
||||
key: 'daysLeft',
|
||||
title: <CustomTableTitle title={t('daysLeftColumn')} />,
|
||||
render: record => (
|
||||
<ProjectDaysLeftAndOverdueCell
|
||||
daysLeft={record.days_left}
|
||||
isOverdue={record.is_overdue}
|
||||
isToday={record.is_today}
|
||||
/>
|
||||
),
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
key: 'projectHealth',
|
||||
dataIndex: 'project_health',
|
||||
defaultSortOrder: order === 'asc' ? 'ascend' : 'descend',
|
||||
title: <CustomTableTitle title={t('projectHealthColumn')} />,
|
||||
sorter: true,
|
||||
render: (_, record: IRPTProject) => (
|
||||
<ProjectHealthCell
|
||||
value={record.project_health}
|
||||
label={record.health_name}
|
||||
color={record.health_color}
|
||||
projectId={record.id}
|
||||
/>
|
||||
),
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
key: 'category',
|
||||
title: <CustomTableTitle title={t('categoryColumn')} />,
|
||||
render: (_, record: IRPTProject) => (
|
||||
<ProjectCategoryCell
|
||||
projectId={record.id}
|
||||
id={record.category_id || ''}
|
||||
name={record.category_name || ''}
|
||||
color_code={record.category_color || ''}
|
||||
/>
|
||||
),
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
key: 'projectUpdate',
|
||||
title: <CustomTableTitle title={t('projectUpdateColumn')} />,
|
||||
render: (_, record: IRPTProject) =>
|
||||
record.comment ? <ProjectUpdateCell updates={record.comment} /> : '-',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
key: 'client',
|
||||
dataIndex: 'client',
|
||||
defaultSortOrder: order === 'asc' ? 'ascend' : 'descend',
|
||||
title: <CustomTableTitle title={t('clientColumn')} />,
|
||||
render: (_, record: IRPTProject) =>
|
||||
record?.client ? <ProjectClientCell client={record.client} /> : '-',
|
||||
sorter: true,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
key: 'team',
|
||||
dataIndex: 'team_name',
|
||||
defaultSortOrder: order === 'asc' ? 'ascend' : 'descend',
|
||||
title: <CustomTableTitle title={t('teamColumn')} />,
|
||||
render: (_, record: IRPTProject) =>
|
||||
record.team_name ? <ProjectTeamCell team={record.team_name} /> : '-',
|
||||
sorter: true,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
key: 'projectManager',
|
||||
title: <CustomTableTitle title={t('projectManagerColumn')} />,
|
||||
render: (_, record: IRPTProject) =>
|
||||
record.project_manager ? <ProjectManagerCell manager={record.project_manager} /> : '-',
|
||||
width: 200,
|
||||
},
|
||||
],
|
||||
[t, order]
|
||||
);
|
||||
|
||||
const handleTableChange = (pagination: PaginationProps, filters: any, sorter: any) => {
|
||||
if (sorter.order) setOrder(sorter.order);
|
||||
if (sorter.field) setField(sorter.field);
|
||||
setPagination({ ...pagination, current: pagination.current });
|
||||
setPagination({ ...pagination, pageSize: pagination.pageSize });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (projectStatuses.length === 0 && !projectStatusesLoading) dispatch(fetchProjectStatuses());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dispatch(resetProjectReports());
|
||||
};
|
||||
}, []);
|
||||
|
||||
const tableRowProps = useMemo(
|
||||
() => ({
|
||||
style: { height: 56, cursor: 'pointer' },
|
||||
className: 'group even:bg-[#4e4e4e10]',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const tableConfig = useMemo(
|
||||
() => ({
|
||||
theme: {
|
||||
components: {
|
||||
Table: {
|
||||
cellPaddingBlock: 12,
|
||||
cellPaddingInline: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const fetchOverviewProjects = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const params = {
|
||||
team: teamsId,
|
||||
index: pagination.current,
|
||||
size: pagination.pageSize,
|
||||
search: searchQuery,
|
||||
filter: 0,
|
||||
order: order,
|
||||
field: field,
|
||||
archived: includeArchivedProjects,
|
||||
};
|
||||
const response = await reportingApiService.getOverviewProjects(params);
|
||||
if (response.done) {
|
||||
setProjectList(response.body.projects || []);
|
||||
setPagination({ ...pagination, total: response.body.total });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('fetchOverviewProjects', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchOverviewProjects();
|
||||
}, [searchQuery, order, field]);
|
||||
|
||||
return (
|
||||
<ConfigProvider {...tableConfig}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={projectList}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
defaultPageSize: 10,
|
||||
total: pagination.total,
|
||||
current: pagination.current,
|
||||
pageSizeOptions: PAGE_SIZE_OPTIONS,
|
||||
}}
|
||||
scroll={{ x: 'max-content' }}
|
||||
loading={isLoading}
|
||||
onChange={handleTableChange}
|
||||
rowKey={record => record.id}
|
||||
onRow={() => tableRowProps}
|
||||
/>
|
||||
{createPortal(<ProjectReportsDrawer selectedProject={selectedProject} />, document.body)}
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportingOverviewProjectsTable;
|
||||
188
worklenz-frontend/src/components/reporting/time-wise-filter.tsx
Normal file
188
worklenz-frontend/src/components/reporting/time-wise-filter.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import { CaretDownFilled, DownOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, DatePicker, Divider, Dropdown, Flex, List, Typography } from 'antd';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { colors } from '@/styles/colors';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { durations } from '@/shared/constants';
|
||||
import { setDateRange, setDuration } from '@/features/reporting/reporting.slice';
|
||||
|
||||
const TimeWiseFilter = () => {
|
||||
const { t } = useTranslation('reporting-members');
|
||||
const { mode: themeMode } = useAppSelector(state => state.themeReducer);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// Get values from Redux store
|
||||
const { duration, dateRange } = useAppSelector(state => state.reportingReducer);
|
||||
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [selectedTimeFrame, setSelectedTimeFrame] = useState<string>(
|
||||
durations.find(item => item.key === duration)?.label || 'lastSevenDaysText'
|
||||
);
|
||||
// const [customRange, setCustomRange] = useState<[string, string] | null>(
|
||||
// dateRange.length === 2 ? [dateRange[0], dateRange[1]] : null
|
||||
// );
|
||||
const [customRange, setCustomRange] = useState<[string, string] | null>(null);
|
||||
|
||||
// Format customRange for display
|
||||
const getDisplayLabel = () => {
|
||||
const f = 'YY-MM-DD';
|
||||
if (customRange && customRange.length === 2) {
|
||||
return `${dayjs(customRange[0]).format(f)} - ${dayjs(customRange[1]).format(f)}`;
|
||||
}
|
||||
return t(selectedTimeFrame);
|
||||
};
|
||||
|
||||
// Apply changes when date range is selected
|
||||
const handleDateRangeChange = (dates: any, dateStrings: [string, string]) => {
|
||||
if (dates) {
|
||||
setSelectedTimeFrame('');
|
||||
setCustomRange([dates[0].$d.toString(), dates[1].$d.toString()]);
|
||||
} else {
|
||||
setCustomRange(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Apply custom date filter
|
||||
const applyCustomDateFilter = () => {
|
||||
if (customRange) {
|
||||
setSelectedTimeFrame('customRange');
|
||||
setIsDropdownOpen(false);
|
||||
dispatch(setDateRange([customRange[0], customRange[1]]));
|
||||
}
|
||||
};
|
||||
|
||||
// Handle duration item selection
|
||||
const handleDurationSelect = (item: any) => {
|
||||
setSelectedTimeFrame(item.label);
|
||||
setCustomRange(null);
|
||||
dispatch(setDuration(item.key));
|
||||
if (item.key === 'YESTERDAY') {
|
||||
const yesterday = dayjs().subtract(1, 'day').format('YYYY-MM-DD');
|
||||
dispatch(setDateRange([yesterday, yesterday]));
|
||||
} else if (item.dates) {
|
||||
const [startDate, endDate] = item.dates.split(' - ');
|
||||
dispatch(setDateRange([startDate, endDate]));
|
||||
} else {
|
||||
// For ALL_TIME or any other case without specific dates, use a default range
|
||||
const defaultStartDate = dayjs().subtract(1, 'year').format('YYYY-MM-DD');
|
||||
const defaultEndDate = dayjs().format('YYYY-MM-DD');
|
||||
dispatch(setDateRange([defaultStartDate, defaultEndDate]));
|
||||
}
|
||||
setIsDropdownOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const selectedDuration = durations.find(item => item.key === duration);
|
||||
if (selectedDuration?.dates) {
|
||||
if (duration === 'YESTERDAY') {
|
||||
const yesterday = dayjs().subtract(1, 'day').format('YYYY-MM-DD');
|
||||
dispatch(setDateRange([yesterday, yesterday]));
|
||||
} else {
|
||||
const [startDate, endDate] = selectedDuration.dates.split(' - ');
|
||||
dispatch(setDateRange([startDate, endDate]));
|
||||
}
|
||||
} else {
|
||||
dispatch(setDateRange([]));
|
||||
}
|
||||
}, [duration]);
|
||||
|
||||
// custom dropdown content
|
||||
const timeWiseDropdownContent = (
|
||||
<Card
|
||||
className="custom-card"
|
||||
styles={{
|
||||
body: {
|
||||
padding: 0,
|
||||
minWidth: 320,
|
||||
maxHeight: 330,
|
||||
overflowY: 'auto',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<List style={{ padding: 0 }}>
|
||||
{durations.map(item => (
|
||||
<List.Item
|
||||
className={`custom-list-item ${themeMode === 'dark' ? 'dark' : ''}`}
|
||||
key={item.key}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: 24,
|
||||
padding: '4px 8px',
|
||||
backgroundColor:
|
||||
selectedTimeFrame === item.label && themeMode === 'dark'
|
||||
? '#424242'
|
||||
: selectedTimeFrame === item.label && themeMode === 'light'
|
||||
? colors.paleBlue
|
||||
: colors.transparent,
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => handleDurationSelect(item)}
|
||||
>
|
||||
<Typography.Text
|
||||
style={{
|
||||
color: selectedTimeFrame === item.label ? colors.skyBlue : 'inherit',
|
||||
}}
|
||||
>
|
||||
{t(item.label)}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{item.dates ? dayjs(item.dates.split(' - ')[0]).format('MMM DD, YYYY') + ' - ' + dayjs(item.dates.split(' - ')[1]).format('MMM DD, YYYY') : ''}
|
||||
</Typography.Text>
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<Divider style={{ marginBlock: 12 }} />
|
||||
|
||||
<Flex vertical gap={8} style={{ padding: 8 }}>
|
||||
<Typography.Text>{t('customRangeText')}</Typography.Text>
|
||||
|
||||
<DatePicker.RangePicker
|
||||
format={'MMM DD, YYYY'}
|
||||
onChange={handleDateRangeChange}
|
||||
value={customRange ? [dayjs(customRange[0]), dayjs(customRange[1])] : null}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
style={{ width: 'fit-content', alignSelf: 'flex-end' }}
|
||||
onClick={applyCustomDateFilter}
|
||||
disabled={!customRange}
|
||||
>
|
||||
{t('filterButton')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
overlayClassName="custom-dropdown"
|
||||
trigger={['click']}
|
||||
dropdownRender={() => timeWiseDropdownContent}
|
||||
onOpenChange={open => setIsDropdownOpen(open)}
|
||||
open={isDropdownOpen}
|
||||
>
|
||||
<Button
|
||||
icon={<DownOutlined />}
|
||||
iconPosition="end"
|
||||
className={`transition-colors duration-300 ${
|
||||
isDropdownOpen
|
||||
? 'border-[#1890ff] text-[#1890ff]'
|
||||
: 'hover:text-[#1890ff hover:border-[#1890ff]'
|
||||
}`}
|
||||
>
|
||||
{getDisplayLabel()}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimeWiseFilter;
|
||||
Reference in New Issue
Block a user