init
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import React from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { toggleScheduleDrawer } from '../../../features/schedule/scheduleSlice';
|
||||
|
||||
type DayAllocationCellProps = {
|
||||
totalPerDayHours: number;
|
||||
loggedHours: number;
|
||||
workingHours: number;
|
||||
isWeekend: boolean;
|
||||
};
|
||||
|
||||
const DayAllocationCell = ({
|
||||
totalPerDayHours,
|
||||
loggedHours,
|
||||
workingHours,
|
||||
isWeekend,
|
||||
}: DayAllocationCellProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// If it's a weekend, override values and disable interaction
|
||||
const effectiveTotalPerDayHours = isWeekend ? 0 : totalPerDayHours;
|
||||
const effectiveLoggedHours = isWeekend ? 0 : loggedHours;
|
||||
const effectiveWorkingHours = isWeekend ? 1 : workingHours; // Avoid division by zero
|
||||
|
||||
const tooltipContent = isWeekend ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<span>Weekend</span>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<span>Total Allocation: {effectiveTotalPerDayHours + effectiveLoggedHours}h</span>
|
||||
<span>Time Logged: {effectiveLoggedHours}h</span>
|
||||
<span>Remaining Time: {effectiveTotalPerDayHours}h</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const gradientColor = isWeekend
|
||||
? 'rgba(200, 200, 200, 0.35)' // Inactive color for weekends
|
||||
: effectiveTotalPerDayHours <= 0
|
||||
? 'rgba(200, 200, 200, 0.35)'
|
||||
: effectiveTotalPerDayHours <= effectiveWorkingHours
|
||||
? 'rgba(6, 126, 252, 0.4)'
|
||||
: 'rgba(255, 0, 0, 0.4)';
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '10px 7px',
|
||||
height: '92px',
|
||||
flexDirection: 'column',
|
||||
pointerEvents: isWeekend ? 'none' : 'auto',
|
||||
}}
|
||||
>
|
||||
<Tooltip title={tooltipContent}>
|
||||
<div
|
||||
style={{
|
||||
width: '63px',
|
||||
background: `linear-gradient(to top, ${gradientColor} ${
|
||||
(effectiveTotalPerDayHours * 100) / effectiveWorkingHours
|
||||
}%, rgba(190, 190, 190, 0.25) ${
|
||||
(effectiveTotalPerDayHours * 100) / effectiveWorkingHours
|
||||
}%)`,
|
||||
justifyContent: effectiveLoggedHours > 0 ? 'flex-end' : 'center',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
borderRadius: '5px',
|
||||
flexDirection: 'column',
|
||||
cursor: isWeekend ? 'not-allowed' : 'pointer', // Change cursor for weekends
|
||||
}}
|
||||
onClick={!isWeekend ? () => dispatch(toggleScheduleDrawer()) : undefined}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: `${(effectiveTotalPerDayHours * 100) / effectiveWorkingHours}%`,
|
||||
}}
|
||||
>
|
||||
{effectiveTotalPerDayHours}h
|
||||
</span>
|
||||
{effectiveLoggedHours > 0 && (
|
||||
<span
|
||||
style={{
|
||||
height: `${(effectiveLoggedHours * 100) / effectiveWorkingHours}%`,
|
||||
backgroundColor: 'rgba(98, 210, 130, 1)',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderBottomLeftRadius: '5px',
|
||||
borderBottomRightRadius: '5px',
|
||||
}}
|
||||
>
|
||||
{effectiveLoggedHours}h
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(DayAllocationCell);
|
||||
@@ -0,0 +1,286 @@
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { fetchDateList, fetchTeamData } from '../../../features/schedule/scheduleSlice';
|
||||
import { themeWiseColor } from '../../../utils/themeWiseColor';
|
||||
import GranttMembersTable from './grantt-members-table';
|
||||
import { CELL_WIDTH } from '../../../shared/constants';
|
||||
import { Flex, Popover } from 'antd';
|
||||
import DayAllocationCell from './day-allocation-cell';
|
||||
import ProjectTimelineBar from './project-timeline-bar';
|
||||
import ProjectTimelineModal from '@/features/schedule/ProjectTimelineModal';
|
||||
|
||||
const GranttChart = React.forwardRef(({ type, date }: { type: string; date: Date }, ref) => {
|
||||
const [expandedProject, setExpandedProject] = useState<string | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string|undefined>(undefined);
|
||||
|
||||
const { teamData } = useAppSelector(state => state.scheduleReducer);
|
||||
const { dateList, loading, dayCount } = useAppSelector(state => state.scheduleReducer);
|
||||
|
||||
// get theme details from theme reducer
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const getAllData = async () => {
|
||||
await dispatch(fetchTeamData());
|
||||
await dispatch(fetchDateList({ date, type }));
|
||||
};
|
||||
|
||||
// useMemo(() => {
|
||||
// dispatch(fetchTeamData());
|
||||
// }, [date, type]);
|
||||
|
||||
useMemo(() => {
|
||||
getAllData();
|
||||
}, [date, type]);
|
||||
|
||||
// function to scroll the timeline header and body together
|
||||
|
||||
// refs
|
||||
const timelineScrollRef = useRef<HTMLDivElement>(null);
|
||||
const timelineHeaderScrollRef = useRef<HTMLDivElement>(null);
|
||||
const membersScrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Syncing scroll vertically between timeline and members
|
||||
const syncVerticalScroll = (source: 'timeline' | 'members') => {
|
||||
if (source === 'timeline') {
|
||||
if (membersScrollRef.current && timelineScrollRef.current) {
|
||||
membersScrollRef.current.scrollTop = timelineScrollRef.current.scrollTop;
|
||||
}
|
||||
} else {
|
||||
if (timelineScrollRef.current && membersScrollRef.current) {
|
||||
timelineScrollRef.current.scrollTop = membersScrollRef.current.scrollTop;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// syncing scroll horizontally between timeline and header
|
||||
const syncHorizontalScroll = (source: 'timeline' | 'header') => {
|
||||
if (source === 'timeline') {
|
||||
if (timelineHeaderScrollRef.current && timelineScrollRef.current) {
|
||||
timelineHeaderScrollRef.current.scrollLeft = timelineScrollRef.current.scrollLeft;
|
||||
}
|
||||
} else {
|
||||
if (timelineScrollRef.current && timelineHeaderScrollRef.current) {
|
||||
timelineScrollRef.current.scrollLeft = timelineHeaderScrollRef.current.scrollLeft;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const scrollToToday = () => {
|
||||
if (!timelineScrollRef.current || !dateList?.date_data) return;
|
||||
|
||||
// Find the index of the "Today" date
|
||||
let todayIndex = 0;
|
||||
dateList.date_data.some((date: any) => {
|
||||
const dayIndex = date.days.findIndex((day: any) => day.isToday);
|
||||
if (dayIndex !== -1) {
|
||||
todayIndex += dayIndex; // Add the index of today within the current month's days
|
||||
return true;
|
||||
}
|
||||
todayIndex += date.days.length; // Increment by the number of days in the current month
|
||||
return false;
|
||||
});
|
||||
|
||||
// Calculate the scroll position
|
||||
const scrollPosition = todayIndex * CELL_WIDTH;
|
||||
|
||||
// Scroll the timeline
|
||||
timelineScrollRef.current.scrollTo({
|
||||
left: scrollPosition,
|
||||
});
|
||||
};
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
scrollToToday,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '375px 1fr',
|
||||
overflow: 'hidden',
|
||||
height: 'calc(100vh - 206px)',
|
||||
border: themeMode === 'dark' ? '1px solid #303030' : '1px solid #e5e7eb',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: themeMode === 'dark' ? '#141414' : '',
|
||||
}}
|
||||
>
|
||||
{/* teams table */}
|
||||
<div
|
||||
style={{
|
||||
background: themeWiseColor('#fff', '#141414', themeMode),
|
||||
}}
|
||||
className={`after:content relative z-10 after:absolute after:-right-1 after:top-0 after:-z-10 after:h-full after:w-1.5 after:bg-transparent after:bg-gradient-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent`}
|
||||
>
|
||||
<GranttMembersTable
|
||||
members={teamData}
|
||||
expandedProject={expandedProject}
|
||||
setExpandedProject={setExpandedProject}
|
||||
membersScrollRef={membersScrollRef}
|
||||
syncVerticalScroll={syncVerticalScroll}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* timeline */}
|
||||
<div style={{ overflow: 'auto', position: 'relative' }}>
|
||||
<div
|
||||
ref={timelineHeaderScrollRef}
|
||||
style={{
|
||||
position: 'sticky',
|
||||
overflow: 'auto',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 100,
|
||||
backgroundColor: themeWiseColor('#fff', '#141414', themeMode),
|
||||
scrollbarWidth: 'none',
|
||||
borderBottom: themeMode === 'dark' ? '1px solid #303030' : '1px solid #e5e7eb',
|
||||
}}
|
||||
onScroll={() => syncHorizontalScroll('header')}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${dayCount}, ${CELL_WIDTH}px)`,
|
||||
}}
|
||||
>
|
||||
{dateList?.date_data?.map((date: any, index: number) =>
|
||||
date.days.map((day: any) => (
|
||||
<div
|
||||
key={index + day.day}
|
||||
style={{
|
||||
background: day.isWeekend
|
||||
? 'rgba(217, 217, 217, 0.4)'
|
||||
: day.isToday
|
||||
? '#69b6fb'
|
||||
: '',
|
||||
color: day.isToday ? '#fff' : '',
|
||||
padding: '8px 0',
|
||||
textAlign: 'center',
|
||||
height: 60,
|
||||
}}
|
||||
>
|
||||
<div>{day.name},</div>
|
||||
<div>
|
||||
{date?.month.substring(0, 4)} {day.day}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Flex
|
||||
vertical
|
||||
ref={timelineScrollRef}
|
||||
onScroll={() => {
|
||||
syncVerticalScroll('timeline');
|
||||
syncHorizontalScroll('timeline');
|
||||
}}
|
||||
style={{
|
||||
height: 'calc(100vh - 270px)',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{teamData.map((member: any) => (
|
||||
<div
|
||||
key={member.id}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${dayCount}, ${CELL_WIDTH}px)`,
|
||||
}}
|
||||
>
|
||||
{dateList?.date_data?.map((date: any) =>
|
||||
date.days.map((day: any) => (
|
||||
<div
|
||||
key={`${date.month}-${day.day}`}
|
||||
style={{
|
||||
background: day.isWeekend ? 'rgba(217, 217, 217, 0.4)' : '',
|
||||
color: day.isToday ? '#fff' : '',
|
||||
height: 90,
|
||||
}}
|
||||
>
|
||||
<DayAllocationCell
|
||||
workingHours={8}
|
||||
loggedHours={0}
|
||||
totalPerDayHours={0}
|
||||
isWeekend={day.isWeekend}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
{expandedProject === member.id && (
|
||||
<div>
|
||||
<Popover
|
||||
content={<ProjectTimelineModal memberId={member?.team_member_id} projectId={selectedProjectId} setIsModalOpen={setIsModalOpen} />}
|
||||
trigger={'click'}
|
||||
open={isModalOpen}
|
||||
></Popover>
|
||||
{member.projects.map((project: any) => (
|
||||
<div
|
||||
key={project.id}
|
||||
onClick={() => {
|
||||
if (!(project?.date_union?.start && project?.date_union?.end)) {
|
||||
setSelectedProjectId(project?.id);
|
||||
setIsModalOpen(true);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${dayCount}, ${CELL_WIDTH}px)`,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
align="center"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
zIndex: 50,
|
||||
height: 65,
|
||||
}}
|
||||
>
|
||||
{project?.date_union?.start && project?.date_union?.end && (
|
||||
<ProjectTimelineBar
|
||||
defaultData={project?.default_values}
|
||||
project={project}
|
||||
indicatorWidth={project?.indicator_width}
|
||||
indicatorOffset={project?.indicator_offset}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{dateList?.date_data?.map((date: any) =>
|
||||
date.days.map((day: any) => (
|
||||
<div
|
||||
key={`${date.month}-${day.day}`}
|
||||
style={{
|
||||
background: day.isWeekend ? 'rgba(217, 217, 217, 0.4)' : '',
|
||||
height: 65,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
className={`rounded-sm outline-1 hover:outline ${themeMode === 'dark' ? 'outline-white/10' : 'outline-black/10'}`}
|
||||
></div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default GranttChart;
|
||||
@@ -0,0 +1,147 @@
|
||||
import { Badge, Button, Flex, Tooltip } from 'antd';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import CustomAvatar from '../../CustomAvatar';
|
||||
import { fetchMemberProjects, toggleScheduleDrawer } from '../../../features/schedule/scheduleSlice';
|
||||
import { CaretDownOutlined, CaretRightFilled } from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
|
||||
type GranttChartMembersTabelProps = {
|
||||
members: any[];
|
||||
expandedProject: string | null;
|
||||
setExpandedProject: (id: string | null) => void;
|
||||
membersScrollRef: any;
|
||||
syncVerticalScroll: (source: 'timeline' | 'members') => void;
|
||||
};
|
||||
|
||||
const GranttMembersTable = React.memo(
|
||||
({
|
||||
members,
|
||||
expandedProject,
|
||||
setExpandedProject,
|
||||
membersScrollRef,
|
||||
syncVerticalScroll,
|
||||
}: GranttChartMembersTabelProps) => {
|
||||
// localization
|
||||
const { t } = useTranslation('schedule');
|
||||
|
||||
// get theme details
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleToggleDrawer = useCallback(() => {
|
||||
dispatch(toggleScheduleDrawer());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleToggleProject = useCallback(
|
||||
(id: string) => {
|
||||
if(expandedProject != id) {
|
||||
|
||||
dispatch(fetchMemberProjects({ id }));
|
||||
}
|
||||
setExpandedProject(expandedProject === id ? null : id);
|
||||
|
||||
},
|
||||
[expandedProject, setExpandedProject]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
vertical
|
||||
style={{
|
||||
width: 370,
|
||||
marginBlockStart: 60,
|
||||
borderTop: themeMode === 'dark' ? '1px solid #303030' : '1px solid #e5e7eb',
|
||||
}}
|
||||
>
|
||||
{/* right side of the table */}
|
||||
<div
|
||||
id="members-header"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
zIndex: 100,
|
||||
width: 370,
|
||||
height: '60px',
|
||||
backgroundColor: themeMode === 'dark' ? '#141414' : '#fff',
|
||||
}}
|
||||
></div>
|
||||
|
||||
<Flex
|
||||
vertical
|
||||
ref={membersScrollRef}
|
||||
onScroll={() => syncVerticalScroll('members')}
|
||||
style={{
|
||||
maxHeight: 'calc(100vh - 278px)',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{members.map(member => (
|
||||
<Flex vertical key={member.id}>
|
||||
<Flex
|
||||
gap={8}
|
||||
align="center"
|
||||
justify="space-between"
|
||||
style={{
|
||||
paddingInline: 12,
|
||||
height: 90,
|
||||
}}
|
||||
>
|
||||
<Flex gap={8} align="center">
|
||||
<CustomAvatar avatarName={member?.name} size={32} />
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
style={{ padding: 0 }}
|
||||
onClick={handleToggleDrawer}
|
||||
>
|
||||
{member.name}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Button size="small" type="text" onClick={() => handleToggleProject(member.id)}>
|
||||
{expandedProject === member.id ? <CaretDownOutlined /> : <CaretRightFilled />}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{expandedProject === member.id &&
|
||||
member.projects.map((project: any, index: any) => {
|
||||
return (
|
||||
<Flex
|
||||
gap={8}
|
||||
align="center"
|
||||
key={index}
|
||||
style={{
|
||||
paddingInline: 12,
|
||||
position: 'sticky',
|
||||
height: 65,
|
||||
}}
|
||||
>
|
||||
<Badge color="red" />
|
||||
<Tooltip
|
||||
title={
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<span>
|
||||
{t('startDate')}: {project?.date_union?.start}
|
||||
</span>
|
||||
<span>
|
||||
{t('endDate')}: {project?.date_union?.end}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{project.name}
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default GranttMembersTable;
|
||||
@@ -0,0 +1,166 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Flex, Popover, Typography } from 'antd';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { getWorking, toggleScheduleDrawer } from '../../../features/schedule/scheduleSlice';
|
||||
import ProjectTimelineModal from '../../../features/schedule/ProjectTimelineModal';
|
||||
import { Resizable } from 're-resizable';
|
||||
import { themeWiseColor } from '../../../utils/themeWiseColor';
|
||||
import { MoreOutlined } from '@ant-design/icons';
|
||||
import { CELL_WIDTH } from '../../../shared/constants';
|
||||
import { ScheduleData } from '@/types/schedule/schedule-v2.types';
|
||||
|
||||
type ProjectTimelineBarProps = {
|
||||
project: any;
|
||||
indicatorOffset: number;
|
||||
indicatorWidth: number;
|
||||
defaultData?: ScheduleData;
|
||||
};
|
||||
|
||||
const ProjectTimelineBar = ({
|
||||
project,
|
||||
indicatorOffset,
|
||||
indicatorWidth,
|
||||
defaultData,
|
||||
}: ProjectTimelineBarProps) => {
|
||||
const [width, setWidth] = useState(indicatorWidth);
|
||||
const [currentDuration, setCurrentDuration] = useState(indicatorWidth);
|
||||
const [totalHours, setTotalHours] = useState(project?.total_hours);
|
||||
const [leftOffset, setLeftOffset] = useState(indicatorOffset);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const { t } = useTranslation('schedule');
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleResize = (
|
||||
event: MouseEvent | TouchEvent,
|
||||
direction: string,
|
||||
ref: HTMLElement,
|
||||
delta: { width: number; height: number }
|
||||
) => {
|
||||
let newWidth = width;
|
||||
let newLeftOffset = leftOffset;
|
||||
|
||||
if (direction === 'right') {
|
||||
newWidth = Math.max(CELL_WIDTH, width + delta.width);
|
||||
if (newWidth <= CELL_WIDTH * 30) {
|
||||
setWidth(newWidth);
|
||||
const newDuration = Math.round(newWidth / CELL_WIDTH);
|
||||
setCurrentDuration(newDuration);
|
||||
setTotalHours(newDuration * project?.hours_per_day);
|
||||
}
|
||||
} else if (direction === 'left') {
|
||||
const deltaWidth = Math.min(leftOffset, delta.width);
|
||||
newLeftOffset = leftOffset - deltaWidth;
|
||||
newWidth = width + deltaWidth;
|
||||
|
||||
if (newLeftOffset >= 0 && newWidth >= CELL_WIDTH && newWidth <= CELL_WIDTH * 30) {
|
||||
setLeftOffset(newLeftOffset);
|
||||
setWidth(newWidth);
|
||||
const newDuration = Math.round(newWidth / CELL_WIDTH);
|
||||
setCurrentDuration(newDuration);
|
||||
setTotalHours(newDuration * project?.hours_per_day);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={<ProjectTimelineModal defaultData={defaultData} projectId={project?.id} setIsModalOpen={setIsModalOpen} />}
|
||||
trigger={'click'}
|
||||
open={isModalOpen}
|
||||
>
|
||||
<Resizable
|
||||
size={{ width, height: 56 }}
|
||||
onResizeStop={(e, direction, ref, delta) =>
|
||||
handleResize(e, direction as 'left' | 'right', ref, delta)
|
||||
}
|
||||
minWidth={CELL_WIDTH}
|
||||
maxWidth={CELL_WIDTH * 30}
|
||||
grid={[CELL_WIDTH, 1]}
|
||||
enable={{
|
||||
top: false,
|
||||
right: true,
|
||||
bottom: false,
|
||||
left: true,
|
||||
topRight: false,
|
||||
bottomRight: false,
|
||||
bottomLeft: false,
|
||||
topLeft: false,
|
||||
}}
|
||||
handleComponent={{
|
||||
right: <MoreOutlined style={{ fontSize: 24, color: 'white' }} />,
|
||||
left: <MoreOutlined style={{ fontSize: 24, color: 'white' }} />,
|
||||
}}
|
||||
handleClasses={{
|
||||
right:
|
||||
'hidden group-hover:flex -translate-x-[5px] bg-[#1890ff] px-1 justify-center rounded-tr rounded-br',
|
||||
left: 'hidden group-hover:flex translate-x-[5px] bg-[#1890ff] px-1 justify-center rounded-tl rounded-bl',
|
||||
}}
|
||||
className="group hover:shadow-md"
|
||||
style={{
|
||||
marginInlineStart: leftOffset,
|
||||
backgroundColor: themeWiseColor(
|
||||
'rgba(240, 248, 255, 1)',
|
||||
'rgba(0, 142, 204, 0.5)',
|
||||
themeMode
|
||||
),
|
||||
borderRadius: 6,
|
||||
border: `1px solid ${themeWiseColor(
|
||||
'rgba(149, 197, 248, 1)',
|
||||
'rgba(24, 144, 255, 1)',
|
||||
themeMode
|
||||
)}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
padding: '4px 10px',
|
||||
zIndex: 99,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
vertical
|
||||
align="center"
|
||||
justify="center"
|
||||
style={{ width: '100%' }}
|
||||
onClick={() => {setIsModalOpen(true);dispatch(getWorking());}}
|
||||
>
|
||||
<Typography.Text
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
ellipsis={{ expanded: false }}
|
||||
>
|
||||
{t('total')} {totalHours}h
|
||||
</Typography.Text>
|
||||
{currentDuration > 1 && (
|
||||
<Typography.Text style={{ fontSize: '10px' }} ellipsis={{ expanded: false }}>
|
||||
{t('perDay')} {project?.hours_per_day}h
|
||||
</Typography.Text>
|
||||
)}
|
||||
<Typography.Text
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
textDecoration: 'underline',
|
||||
width: 'fit-content',
|
||||
}}
|
||||
ellipsis={{ expanded: false }}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
dispatch(toggleScheduleDrawer());
|
||||
}}
|
||||
>
|
||||
20 {t('tasks')}
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
</Resizable>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ProjectTimelineBar);
|
||||
@@ -0,0 +1,72 @@
|
||||
import { TaskType } from '@/types/task.types';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import GroupByFilterDropdown from '@/components/project-task-filters/filter-dropdowns/group-by-filter-dropdown';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import StatusGroupTables from '@/pages/projects/project-view-1/taskList/statusTables/StatusGroupTables';
|
||||
import PriorityGroupTables from '@/pages/projects/projectView/taskList/groupTables/priorityTables/PriorityGroupTables';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||
|
||||
const WithStartAndEndDates = () => {
|
||||
const dataSource: ITaskListGroup[] = useAppSelector(state => state.taskReducer.taskGroups);
|
||||
const { t } = useTranslation('schedule');
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '5px',
|
||||
flexDirection: 'column',
|
||||
border: '1px solid rgba(0, 0, 0, 0.21)',
|
||||
padding: '20px',
|
||||
borderRadius: '15px',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '24px', fontWeight: 'bold', color: 'rgba(112, 113, 114, 1)' }}>
|
||||
2024-11-04 - 2024-12-24
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
gap: '200px',
|
||||
color: 'rgba(121, 119, 119, 1)',
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '50%' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<span>{t('allocatedTime')}</span>
|
||||
<span>8 {t('hours')}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<span>{t('totalLogged')}</span>
|
||||
<span>7 {t('hours')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: '50%' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<span>{t('loggedBillable')}</span>
|
||||
<span>5 {t('hours')}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<span>{t('loggedNonBillable')}</span>
|
||||
<span>2 {t('hours')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<GroupByFilterDropdown />
|
||||
</div>
|
||||
<div>
|
||||
{dataSource.map(group => (
|
||||
<StatusGroupTables key={group.id} group={group} />
|
||||
))}
|
||||
{/* <PriorityGroupTables datasource={dataSource} /> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WithStartAndEndDates;
|
||||
@@ -0,0 +1,108 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Flex, Typography } from 'antd';
|
||||
import { Member } from '../../../types/schedule/schedule.types';
|
||||
import DayAllocationCell from './day-allocation-cell';
|
||||
import { CELL_WIDTH } from '../../../shared/constants';
|
||||
|
||||
type DatesType = {
|
||||
date_data: {
|
||||
month: string;
|
||||
weeks: any[];
|
||||
days: {
|
||||
day: number;
|
||||
name: string;
|
||||
isWeekend: boolean;
|
||||
isToday: boolean;
|
||||
}[];
|
||||
}[];
|
||||
chart_start: Date | null;
|
||||
chart_end: Date | null;
|
||||
};
|
||||
|
||||
const Timeline = () => {
|
||||
const [dates, setDates] = useState<DatesType | null>(null);
|
||||
const [members, setMembers] = useState<Member[]>([]);
|
||||
|
||||
useMemo(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await fetch('/scheduler-data/TeamData.json');
|
||||
const data = await response.json();
|
||||
setMembers(data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
useMemo(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await fetch('/scheduler-data/scheduler-timeline-dates.json');
|
||||
const data = await response.json();
|
||||
setDates(data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const getDaysBetween = (start: Date | null, end: Date | null): number => {
|
||||
const validStart = start ? new Date(start) : new Date();
|
||||
const validEnd = end ? new Date(end) : new Date();
|
||||
|
||||
if (
|
||||
validStart instanceof Date &&
|
||||
!isNaN(validStart.getTime()) &&
|
||||
validEnd instanceof Date &&
|
||||
!isNaN(validEnd.getTime())
|
||||
) {
|
||||
const oneDay = 24 * 60 * 60 * 1000;
|
||||
return Math.round(Math.abs((validStart.getTime() - validEnd.getTime()) / oneDay));
|
||||
} else {
|
||||
console.error('Invalid date(s)');
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const numberOfDays =
|
||||
dates?.chart_start && dates?.chart_end ? getDaysBetween(dates.chart_start, dates.chart_end) : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${numberOfDays + 1}, ${CELL_WIDTH}px)`,
|
||||
}}
|
||||
>
|
||||
{dates?.date_data?.map((month, monthIndex) =>
|
||||
month.days.map((day, dayIndex) => (
|
||||
<div
|
||||
key={`${monthIndex}-${dayIndex}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: 60,
|
||||
background: day.isWeekend
|
||||
? 'rgba(217, 217, 217, 0.4)'
|
||||
: day.isToday
|
||||
? 'rgba(24, 144, 255, 1)'
|
||||
: '',
|
||||
}}
|
||||
>
|
||||
<Typography.Text>{day.name},</Typography.Text>
|
||||
<Typography.Text>
|
||||
{month.month.substring(0, 3)} {day.day}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Timeline;
|
||||
Reference in New Issue
Block a user