Files
worklenz/worklenz-frontend/src/pages/projects/projectView/insights/project-view-insights.tsx
2025-07-03 01:31:05 +05:30

180 lines
5.8 KiB
TypeScript

import { DownloadOutlined } from '@ant-design/icons';
import { Badge, Button, Checkbox, Flex, Segmented } from 'antd';
import { useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { colors } from '@/styles/colors';
import InsightsMembers from './insights-members/insights-members';
import InsightsOverview from './insights-overview/insights-overview';
import InsightsTasks from './insights-tasks/insights-tasks';
import {
setActiveSegment,
setIncludeArchivedTasks,
setProjectId,
} from '@/features/projects/insights/project-insights.slice';
import { format } from 'date-fns';
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
import logo from '@/assets/images/worklenz-light-mode.png';
import {
evt_project_insights_members_visit,
evt_project_insights_overview_visit,
evt_project_insights_tasks_visit,
} from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
type SegmentType = 'Overview' | 'Members' | 'Tasks';
const ProjectViewInsights = () => {
const { projectId } = useParams();
const { t } = useTranslation('project-view-insights');
const { trackMixpanelEvent } = useMixpanelTracking();
const exportRef = useRef<HTMLDivElement>(null);
const { refreshTimestamp } = useAppSelector(state => state.projectReducer);
const themeMode = useAppSelector(state => state.themeReducer.mode);
const dispatch = useAppDispatch();
const [exportLoading, setExportLoading] = useState(false);
const { activeSegment, includeArchivedTasks } = useAppSelector(
state => state.projectInsightsReducer
);
const { project: selectedProject } = useAppSelector(state => state.projectReducer);
const handleSegmentChange = (value: SegmentType) => {
dispatch(setActiveSegment(value));
};
const toggleArchivedTasks = () => {
dispatch(setIncludeArchivedTasks(!includeArchivedTasks));
};
useEffect(() => {
if (projectId) {
dispatch(setProjectId(projectId));
}
}, [projectId]);
const renderSegmentContent = () => {
if (!projectId) return null;
switch (activeSegment) {
case 'Overview':
trackMixpanelEvent(evt_project_insights_overview_visit);
return <InsightsOverview t={t} />;
case 'Members':
trackMixpanelEvent(evt_project_insights_members_visit);
return <InsightsMembers t={t} />;
case 'Tasks':
trackMixpanelEvent(evt_project_insights_tasks_visit);
return <InsightsTasks t={t} />;
}
};
const handleExport = async () => {
if (!projectId) return;
try {
setExportLoading(true);
await dispatch(setActiveSegment(activeSegment));
await exportPdf(selectedProject?.name || '', activeSegment);
} catch (error) {
console.error(error);
} finally {
setExportLoading(false);
}
};
const exportPdf = async (projectName: string | null, activeSegment: string | '') => {
if (!exportRef.current) return;
const element = exportRef.current;
const canvas = await html2canvas(element);
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF('p', 'mm', 'a4');
const bufferX = 5;
const bufferY = 28;
const imgProps = pdf.getImageProperties(imgData);
const pdfWidth = pdf.internal.pageSize.getWidth() - 2 * bufferX;
const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width;
const logoImg = new Image();
logoImg.src = logo;
logoImg.onload = () => {
pdf.addImage(logoImg, 'PNG', pdf.internal.pageSize.getWidth() / 2 - 12, 5, 30, 6.5);
pdf.setFontSize(14);
pdf.setTextColor(0, 0, 0, 0.85);
pdf.text(
[`Insights - ${projectName} - ${activeSegment}`, format(new Date(), 'yyyy-MM-dd')],
pdf.internal.pageSize.getWidth() / 2,
17,
{ align: 'center' }
);
pdf.addImage(imgData, 'PNG', bufferX, bufferY, pdfWidth, pdfHeight);
pdf.save(`${activeSegment} ${format(new Date(), 'yyyy-MM-dd')}.pdf`);
};
logoImg.onerror = error => {
pdf.setFontSize(14);
pdf.setTextColor(0, 0, 0, 0.85);
pdf.text(
[`Insights - ${projectName} - ${activeSegment}`, format(new Date(), 'yyyy-MM-dd')],
pdf.internal.pageSize.getWidth() / 2,
17,
{ align: 'center' }
);
pdf.addImage(imgData, 'PNG', bufferX, bufferY, pdfWidth, pdfHeight);
pdf.save(`${activeSegment} ${format(new Date(), 'yyyy-MM-dd')}.pdf`);
};
};
useEffect(() => {
if (projectId) {
dispatch(setActiveSegment('Overview'));
}
}, [refreshTimestamp]);
return (
<Flex vertical gap={24}>
<Flex align="center" justify="space-between">
<Segmented
options={['Overview', 'Members', 'Tasks']}
defaultValue={activeSegment}
value={activeSegment}
onChange={handleSegmentChange}
/>
<Flex gap={8}>
<Flex
gap={8}
align="center"
style={{
backgroundColor: themeMode === 'dark' ? '#141414' : '#f5f5f5',
padding: '6px 15px',
borderRadius: 4,
}}
>
<Checkbox checked={includeArchivedTasks} onClick={toggleArchivedTasks} />
<Badge color={includeArchivedTasks ? colors.limeGreen : colors.vibrantOrange} dot>
{t('common.includeArchivedTasks')}
</Badge>
</Flex>
<Button
type="primary"
icon={<DownloadOutlined />}
onClick={handleExport}
loading={exportLoading}
>
{t('common.export')}
</Button>
</Flex>
</Flex>
<div ref={exportRef}>{renderSegmentContent()}</div>
</Flex>
);
};
export default ProjectViewInsights;