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,222 @@
import { InputRef } from 'antd/es/input';
import Card from 'antd/es/card';
import Checkbox from 'antd/es/checkbox';
import Divider from 'antd/es/divider';
import Dropdown from 'antd/es/dropdown';
import Empty from 'antd/es/empty';
import Flex from 'antd/es/flex';
import Input from 'antd/es/input';
import List from 'antd/es/list';
import Typography from 'antd/es/typography';
import Button from 'antd/es/button';
import { useMemo, useRef, useState } from 'react';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { toggleProjectMemberDrawer } from '../../../features/projects/singleProject/members/projectMembersSlice';
import { colors } from '../../../styles/colors';
import { PlusOutlined, UsergroupAddOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import SingleAvatar from '@/components/common/single-avatar/single-avatar';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { ITeamMembersViewModel } from '@/types/teamMembers/teamMembersViewModel.types';
import { sortByBooleanField, sortBySelection, sortTeamMembers } from '@/utils/sort-team-members';
import { useAuthService } from '@/hooks/useAuth';
import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
import { getTeamMembers } from '@/features/team-members/team-members.slice';
interface BoardAssigneeSelectorProps {
task: IProjectTask;
groupId: string | null;
}
const BoardAssigneeSelector = ({ task, groupId = null }: BoardAssigneeSelectorProps) => {
const membersInputRef = useRef<InputRef>(null);
const [searchQuery, setSearchQuery] = useState<string>('');
const [teamMembers, setTeamMembers] = useState<ITeamMembersViewModel>({ data: [], total: 0 });
const { projectId } = useAppSelector(state => state.projectReducer);
const currentSession = useAuthService().getCurrentSession();
const { socket } = useSocket();
const themeMode = useAppSelector(state => state.themeReducer.mode);
const { t } = useTranslation('task-list-table');
const dispatch = useAppDispatch();
const members = useAppSelector(state => state.teamMembersReducer.teamMembers);
const filteredMembersData = useMemo(() => {
return teamMembers?.data?.filter(member =>
member.name?.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [teamMembers, searchQuery]);
const handleInviteProjectMemberDrawer = () => {
dispatch(toggleProjectMemberDrawer());
};
const handleMembersDropdownOpen = (open: boolean) => {
if (open) {
const assignees = task?.assignees?.map(assignee => assignee.team_member_id);
const membersData = (members?.data || []).map(member => ({
...member,
selected: assignees?.includes(member.id),
}));
let sortedMembers = sortTeamMembers(membersData);
setTeamMembers({ data: sortedMembers });
setTimeout(() => {
membersInputRef.current?.focus();
}, 0);
} else {
setTeamMembers(members || { data: [] });
}
};
const handleMemberChange = (e: CheckboxChangeEvent | null, memberId: string) => {
if (!memberId || !projectId || !task?.id || !currentSession?.id) return;
const checked =
e?.target.checked ||
!task?.assignees?.some(assignee => assignee.team_member_id === memberId) ||
false;
const body = {
team_member_id: memberId,
project_id: projectId,
task_id: task.id,
reporter_id: currentSession?.id,
mode: checked ? 0 : 1,
parent_task: task.parent_task_id,
};
socket?.emit(SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), JSON.stringify(body));
};
const checkMemberSelected = (memberId: string) => {
if (!memberId) return false;
const assignees = task?.assignees?.map(assignee => assignee.team_member_id);
return assignees?.includes(memberId);
};
const membersDropdownContent = (
<Card className="custom-card" styles={{ body: { padding: 8 } }}>
<Flex vertical>
<Input
ref={membersInputRef}
value={searchQuery}
onChange={e => setSearchQuery(e.currentTarget.value)}
placeholder={t('searchInputPlaceholder')}
/>
<List style={{ padding: 0, height: 250, overflow: 'auto' }}>
{filteredMembersData?.length ? (
filteredMembersData.map(member => (
<List.Item
className={`${themeMode === 'dark' ? 'custom-list-item dark' : 'custom-list-item'} ${member.pending_invitation ? 'disabled cursor-not-allowed' : ''}`}
key={member.id}
style={{
display: 'flex',
gap: 8,
justifyContent: 'flex-start',
padding: '4px 8px',
border: 'none',
cursor: 'pointer',
}}
onClick={e => handleMemberChange(null, member.id || '')}
>
<Checkbox
id={member.id}
checked={checkMemberSelected(member.id || '')}
onChange={e => handleMemberChange(e, member.id || '')}
disabled={member.pending_invitation}
/>
<div>
<SingleAvatar
avatarUrl={member.avatar_url}
name={member.name}
email={member.email}
/>
</div>
<Flex vertical>
<Typography.Text>{member.name}</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{member.email}&nbsp;
{member.pending_invitation && (
<Typography.Text type="danger" style={{ fontSize: 10 }}>
({t('pendingInvitation')})
</Typography.Text>
)}
</Typography.Text>
</Flex>
</List.Item>
))
) : (
<Empty />
)}
</List>
<Divider style={{ marginBlock: 0 }} />
<Button
icon={<UsergroupAddOutlined />}
type="text"
style={{
color: colors.skyBlue,
border: 'none',
backgroundColor: colors.transparent,
width: '100%',
}}
onClick={handleInviteProjectMemberDrawer}
>
{t('assigneeSelectorInviteButton')}
</Button>
{/* <Divider style={{ marginBlock: 8 }} /> */}
{/* <Button
type="primary"
style={{ alignSelf: 'flex-end' }}
size="small"
onClick={handleAssignMembers}
>
{t('okButton')}
</Button> */}
</Flex>
</Card>
);
return (
<Dropdown
overlayClassName="custom-dropdown"
trigger={['click']}
dropdownRender={() => membersDropdownContent}
onOpenChange={handleMembersDropdownOpen}
>
<Button
type="dashed"
shape="circle"
size="small"
onClick={(e) => e.stopPropagation()}
icon={
<PlusOutlined
style={{
fontSize: 12,
width: 22,
height: 22,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
/>
}
/>
</Dropdown>
);
};
export default BoardAssigneeSelector;

View File

@@ -0,0 +1,13 @@
.status-drawer-dropdown .ant-dropdown-menu {
padding: 0 !important;
margin-top: 8px !important;
overflow: hidden;
}
.status-drawer-dropdown .ant-dropdown-menu-item {
padding: 0 !important;
}
.status-drawer-dropdown .ant-card-body {
padding: 0 !important;
}

View File

@@ -0,0 +1,110 @@
import { Badge, Card, Dropdown, Flex, Menu, MenuProps } from 'antd';
import React from 'react';
import { TaskStatusType } from '../../../types/task.types';
import { colors } from '../../../styles/colors';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { RetweetOutlined, RightOutlined } from '@ant-design/icons';
import './ChangeCategoryDropdown.css';
import { updateStatusCategory } from '../../../features/projects/status/StatusSlice';
import { useTranslation } from 'react-i18next';
interface ChangeCategoryDropdownProps {
id: string;
}
const ChangeCategoryDropdown: React.FC<ChangeCategoryDropdownProps> = ({ id }) => {
const dispatch = useAppDispatch();
// const [currentStatus, setCurrentStatus] = useState(category);
const { t } = useTranslation('kanban-board');
const getStatuColor = (status: TaskStatusType) => {
if (status === 'todo') return colors.deepLightGray;
else if (status === 'doing') return colors.midBlue;
else return colors.lightGreen;
};
// menu type
type MenuItem = Required<MenuProps>['items'][number];
// status menu item
const statusMenuItems: MenuItem[] = [
{
key: 'todo',
label: (
<Flex gap={4}>
<Badge color={getStatuColor('todo')} /> Todo
</Flex>
),
},
{
key: 'doing',
label: (
<Flex gap={4}>
<Badge color={getStatuColor('doing')} /> Doing
</Flex>
),
},
{
key: 'done',
label: (
<Flex gap={4}>
<Badge color={getStatuColor('done')} /> Done
</Flex>
),
},
];
const onClick: MenuProps['onClick'] = e => {
if (e.key === 'todo') {
dispatch(updateStatusCategory({ id: id, category: 'todo' }));
} else if (e.key === 'doing') {
dispatch(updateStatusCategory({ id: id, category: 'doing' }));
} else if (e.key === 'done') {
dispatch(updateStatusCategory({ id: id, category: 'done' }));
}
};
const statusDropdownItems: MenuProps['items'] = [
{
key: '1',
label: (
<Card className="status-dropdown-card" bordered={false}>
<Menu
className="status-menu"
items={statusMenuItems}
defaultValue={'todo'}
onClick={onClick}
/>
</Card>
),
},
];
return (
<>
<Dropdown
menu={{ items: statusDropdownItems }}
overlayStyle={{
paddingLeft: '185px',
paddingBottom: '100px',
top: '350px',
}}
overlayClassName="status-drawer-dropdown"
>
<div
style={{
display: 'flex',
justifyContent: 'flex-start',
width: '100%',
padding: '5px 12px',
gap: '8px',
}}
>
<RetweetOutlined /> <span>{t('changeCategory')}</span>{' '}
<RightOutlined style={{ color: '#00000073', fontSize: '10px' }} />
</div>
</Dropdown>
</>
);
};
export default ChangeCategoryDropdown;

View File

@@ -0,0 +1,297 @@
import React, { useEffect, useRef, useState } from 'react';
import { Button, Dropdown, Input, InputRef, MenuProps, Typography } from 'antd';
import {
DeleteOutlined,
EditOutlined,
LoadingOutlined,
MoreOutlined,
PlusOutlined,
} from '@ant-design/icons';
import { setTaskCardDisabled, initializeStatus } from '../../../features/board/create-card.slice';
import { TaskType } from '../../../types/task.types';
import TaskCreateCard from '../taskCreateCard/TaskCreateCard';
import TaskCard from '../taskCard/TaskCard';
import { useAppSelector } from '@/hooks/useAppSelector';
import '../commonStatusSection/CommonStatusSection';
import { deleteStatus } from '../../../features/projects/status/StatusSlice';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import ChangeCategoryDropdown from '../changeCategoryDropdown/ChangeCategoryDropdown';
import { useTranslation } from 'react-i18next';
interface CommonMembersSectionProps {
status: string;
dataSource: TaskType[];
category: string;
id: string;
}
const CommonMembersSection: React.FC<CommonMembersSectionProps> = ({
status,
dataSource,
category,
id,
}) => {
const dispatch = useAppDispatch();
const createTaskInputRef = useRef<InputRef>(null);
const colorPalette = ['#d1d0d3', '#b9cef1', '#c2e4d0', '#f9e3b1', '#f6bfc0'];
const getRandomColorFromPalette = () =>
colorPalette[Math.floor(Math.random() * colorPalette.length)];
useEffect(() => {
dispatch(initializeStatus(status));
}, [dispatch, status]);
const isTopCardDisabled = useAppSelector(
state => state.createCardReducer.taskCardDisabledStatus[status]?.top
);
const isBottomCardDisabled = useAppSelector(
state => state.createCardReducer.taskCardDisabledStatus[status]?.bottom
);
const themeMode = useAppSelector(state => state.themeReducer.mode);
const [addTaskCount, setAddTaskCount] = useState(0);
const [name, setName] = useState(status);
const [isEditable, setIsEditable] = useState(false);
const inputRef = useRef<InputRef>(null);
const [isLoading, setIsLoading] = useState(false);
const taskCardRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation('kanban-board');
const handleAddTaskClick = () => {
dispatch(setTaskCardDisabled({ status, position: 'bottom', disabled: false }));
setAddTaskCount(prev => prev + 1);
};
const handleTopAddTaskClick = () => {
dispatch(setTaskCardDisabled({ status, position: 'top', disabled: false }));
setAddTaskCount(prev => prev + 1);
};
useEffect(() => {
if (isEditable && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [isEditable]);
useEffect(() => {
createTaskInputRef.current?.focus();
taskCardRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}, [dataSource, addTaskCount]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
};
const handleBlur = () => {
setIsEditable(false);
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
}, 3000);
};
const items: MenuProps['items'] = [
{
key: '1',
label: (
<div
style={{
display: 'flex',
justifyContent: 'flex-start',
width: '100%',
padding: '5px 12px',
gap: '8px',
}}
onClick={() => setIsEditable(true)}
>
<EditOutlined /> <span>{t('rename')}</span>
</div>
),
},
{
key: '2',
label: <ChangeCategoryDropdown id={id} />,
},
{
key: '3',
label: (
<div
style={{
display: 'flex',
justifyContent: 'flex-start',
width: '100%',
padding: '5px 12px',
gap: '8px',
}}
onClick={() => dispatch(deleteStatus(id))}
>
<DeleteOutlined /> <span>{t('delete')}</span>
</div>
),
},
];
return (
<div style={{ paddingTop: '6px' }}>
<div
className={`todo-wraper ${themeMode === 'dark' ? 'dark-mode' : ''}`}
style={{
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
flexBasis: 0,
maxWidth: '375px',
width: '375px',
marginRight: '8px',
padding: '8px',
borderRadius: '25px',
maxHeight: 'calc(100vh - 250px)',
backgroundColor: themeMode === 'dark' ? '#282828' : '#F8FAFC',
}}
>
<div
style={{
touchAction: 'none',
userSelect: 'none',
cursor: 'grab',
fontSize: '14px',
paddingTop: '0',
margin: '0.25rem',
}}
>
<div
style={{
fontWeight: 600,
marginBottom: '12px',
alignItems: 'center',
padding: '8px',
display: 'flex',
justifyContent: 'space-between',
backgroundColor: getRandomColorFromPalette(),
borderRadius: '10px',
}}
>
<div
style={{ display: 'flex', gap: '5px', alignItems: 'center' }}
onClick={() => setIsEditable(true)}
>
{isLoading ? (
<LoadingOutlined />
) : (
<Button
type="text"
size="small"
shape="circle"
style={{
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
}}
>
{dataSource.length}
</Button>
)}
{isEditable ? (
<Input
ref={inputRef}
value={name}
variant="borderless"
style={{
backgroundColor: themeMode === 'dark' ? 'black' : 'white',
}}
onChange={handleChange}
onBlur={handleBlur}
onPressEnter={handleBlur}
/>
) : (
<Typography.Text
style={{
textTransform: 'capitalize',
color: themeMode === 'dark' ? '#383838' : '',
}}
>
{name}
</Typography.Text>
)}
</div>
<div style={{ display: 'flex' }}>
<Button
type="text"
size="small"
shape="circle"
onClick={handleTopAddTaskClick}
style={{ color: themeMode === 'dark' ? '#383838' : '' }}
>
<PlusOutlined />
</Button>
<Dropdown
overlayClassName="todo-threedot-dropdown"
trigger={['click']}
menu={{ items }}
placement="bottomLeft"
>
<Button type="text" size="small" shape="circle">
<MoreOutlined
style={{
rotate: '90deg',
fontSize: '25px',
color: themeMode === 'dark' ? '#383838' : '',
}}
/>
</Button>
</Dropdown>
</div>
</div>
</div>
<div
style={{
overflowY: 'auto',
maxHeight: 'calc(100vh - 250px)',
padding: '2px 6px 2px 2px',
}}
>
{!isTopCardDisabled && (
<TaskCreateCard ref={createTaskInputRef} status={status} position={'top'} />
)}
{dataSource.map(task => (
<TaskCard key={task.taskId} task={task} />
))}
{!isBottomCardDisabled && (
<TaskCreateCard ref={createTaskInputRef} status={status} position={'bottom'} />
)}
</div>
<div
style={{
textAlign: 'center',
margin: '7px 8px 8px 8px',
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
padding: '0',
}}
>
<Button
type="text"
style={{
height: '38px',
width: '100%',
}}
icon={<PlusOutlined />}
onClick={handleAddTaskClick}
>
{t('addTask')}
</Button>
</div>
</div>
</div>
);
};
export default CommonMembersSection;

View File

@@ -0,0 +1,294 @@
import React, { useEffect, useRef, useState } from 'react';
import { Button, Dropdown, Input, InputRef, MenuProps, Typography } from 'antd';
import {
DeleteOutlined,
EditOutlined,
LoadingOutlined,
MoreOutlined,
PlusOutlined,
} from '@ant-design/icons';
import { setTaskCardDisabled, initializeStatus } from '../../../features/board/create-card.slice';
import { TaskType } from '../../../types/task.types';
import TaskCreateCard from '../taskCreateCard/TaskCreateCard';
import TaskCard from '../taskCard/TaskCard';
import { useAppSelector } from '@/hooks/useAppSelector';
import '../commonStatusSection/CommonStatusSection';
import { deleteStatus } from '../../../features/projects/status/StatusSlice';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import ChangeCategoryDropdown from '../changeCategoryDropdown/ChangeCategoryDropdown';
import { useTranslation } from 'react-i18next';
interface CommonPhaseSectionProps {
status: string;
dataSource: TaskType[];
category: string;
id: string;
}
const CommonPhaseSection: React.FC<CommonPhaseSectionProps> = ({
status,
dataSource,
category,
id,
}) => {
const dispatch = useAppDispatch();
const createTaskInputRef = useRef<InputRef>(null);
// Initialize status in the Redux store if not already set
useEffect(() => {
dispatch(initializeStatus(status));
}, [dispatch, status]);
// Get status-specific disable controls from Redux state
const isTopCardDisabled = useAppSelector(
state => state.createCardReducer.taskCardDisabledStatus[status]?.top
);
const isBottomCardDisabled = useAppSelector(
state => state.createCardReducer.taskCardDisabledStatus[status]?.bottom
);
const themeMode = useAppSelector(state => state.themeReducer.mode);
const [addTaskCount, setAddTaskCount] = useState(0);
const [name, setName] = useState(status);
const [isEditable, setIsEditable] = useState(false);
const inputRef = useRef<InputRef>(null);
const [isLoading, setIsLoading] = useState(false);
const taskCardRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation('kanban-board');
const handleAddTaskClick = () => {
dispatch(setTaskCardDisabled({ status, position: 'bottom', disabled: false }));
setAddTaskCount(prev => prev + 1);
};
const handleTopAddTaskClick = () => {
dispatch(setTaskCardDisabled({ status, position: 'top', disabled: false }));
setAddTaskCount(prev => prev + 1);
};
useEffect(() => {
if (isEditable && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [isEditable]);
useEffect(() => {
createTaskInputRef.current?.focus();
taskCardRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}, [dataSource, addTaskCount]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
};
const handleBlur = () => {
setIsEditable(false);
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
}, 3000);
};
const items: MenuProps['items'] = [
{
key: '1',
label: (
<div
style={{
display: 'flex',
justifyContent: 'flex-start',
width: '100%',
padding: '5px 12px',
gap: '8px',
}}
onClick={() => setIsEditable(true)}
>
<EditOutlined /> <span>{t('rename')}</span>
</div>
),
},
{
key: '2',
label: <ChangeCategoryDropdown id={id} />,
},
{
key: '3',
label: (
<div
style={{
display: 'flex',
justifyContent: 'flex-start',
width: '100%',
padding: '5px 12px',
gap: '8px',
}}
onClick={() => dispatch(deleteStatus(id))}
>
<DeleteOutlined /> <span>{t('delete')}</span>
</div>
),
},
];
return (
<div style={{ paddingTop: '6px' }}>
<div
className={`todo-wraper ${themeMode === 'dark' ? 'dark-mode' : ''}`}
style={{
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
flexBasis: 0,
maxWidth: '375px',
width: '375px',
marginRight: '8px',
padding: '8px',
borderRadius: '25px',
maxHeight: 'calc(100vh - 250px)',
backgroundColor: themeMode === 'dark' ? '#282828' : '#F8FAFC',
}}
>
<div
style={{
touchAction: 'none',
userSelect: 'none',
cursor: 'grab',
fontSize: '14px',
paddingTop: '0',
margin: '0.25rem',
}}
>
<div
style={{
fontWeight: 600,
marginBottom: '12px',
alignItems: 'center',
padding: '8px',
display: 'flex',
justifyContent: 'space-between',
backgroundColor: category === 'unmapped' ? 'rgba(251, 200, 76, 0.41)' : '#d1d0d3',
borderRadius: '10px',
}}
>
<div
style={{ display: 'flex', gap: '5px', alignItems: 'center' }}
onClick={() => setIsEditable(true)}
>
{isLoading ? (
<LoadingOutlined />
) : (
<Button
type="text"
size="small"
shape="circle"
style={{
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
}}
>
{dataSource.length}
</Button>
)}
{isEditable ? (
<Input
ref={inputRef}
value={name}
variant="borderless"
style={{
backgroundColor: themeMode === 'dark' ? 'black' : 'white',
}}
onChange={handleChange}
onBlur={handleBlur}
onPressEnter={handleBlur}
/>
) : (
<Typography.Text
style={{
textTransform: 'capitalize',
color: themeMode === 'dark' ? '#383838' : '',
}}
>
{name}
</Typography.Text>
)}
</div>
<div style={{ display: 'flex' }}>
<Button
type="text"
size="small"
shape="circle"
onClick={handleTopAddTaskClick}
style={{ color: themeMode === 'dark' ? '#383838' : '' }}
>
<PlusOutlined />
</Button>
<Dropdown
overlayClassName="todo-threedot-dropdown"
trigger={['click']}
menu={{ items }}
placement="bottomLeft"
>
<Button type="text" size="small" shape="circle">
<MoreOutlined
style={{
rotate: '90deg',
fontSize: '25px',
color: themeMode === 'dark' ? '#383838' : '',
}}
/>
</Button>
</Dropdown>
</div>
</div>
</div>
<div
style={{
overflowY: 'auto',
maxHeight: 'calc(100vh - 250px)',
padding: '2px 6px 2px 2px',
}}
>
{!isTopCardDisabled && (
<TaskCreateCard ref={createTaskInputRef} status={status} position={'top'} />
)}
{dataSource.map(task => (
<TaskCard key={task.taskId} task={task} />
))}
{!isBottomCardDisabled && (
<TaskCreateCard ref={createTaskInputRef} status={status} position={'bottom'} />
)}
</div>
<div
style={{
textAlign: 'center',
margin: '7px 8px 8px 8px',
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
padding: '0',
}}
>
<Button
type="text"
style={{
height: '38px',
width: '100%',
}}
icon={<PlusOutlined />}
onClick={handleAddTaskClick}
>
{t('addTask')}
</Button>
</div>
</div>
</div>
);
};
export default CommonPhaseSection;

View File

@@ -0,0 +1,295 @@
import React, { useEffect, useRef, useState } from 'react';
import { Button, Dropdown, Input, InputRef, MenuProps, Typography } from 'antd';
import {
DeleteOutlined,
EditOutlined,
LoadingOutlined,
MoreOutlined,
PlusOutlined,
} from '@ant-design/icons';
import { setTaskCardDisabled, initializeStatus } from '../../../features/board/create-card.slice';
import { TaskType } from '../../../types/task.types';
import TaskCreateCard from '../taskCreateCard/TaskCreateCard';
import TaskCard from '../taskCard/TaskCard';
import { useAppSelector } from '@/hooks/useAppSelector';
import '../commonStatusSection/CommonStatusSection';
import { deleteStatus } from '../../../features/projects/status/StatusSlice';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import ChangeCategoryDropdown from '../changeCategoryDropdown/ChangeCategoryDropdown';
import { useTranslation } from 'react-i18next';
interface CommonPrioritySectionProps {
status: string;
dataSource: TaskType[];
category: string;
id: string;
}
const CommonPrioritySection: React.FC<CommonPrioritySectionProps> = ({
status,
dataSource,
category,
id,
}) => {
const dispatch = useAppDispatch();
const createTaskInputRef = useRef<InputRef>(null);
// Initialize status in the Redux store if not already set
useEffect(() => {
dispatch(initializeStatus(status));
}, [dispatch, status]);
// Get status-specific disable controls from Redux state
const isTopCardDisabled = useAppSelector(
state => state.createCardReducer.taskCardDisabledStatus[status]?.top
);
const isBottomCardDisabled = useAppSelector(
state => state.createCardReducer.taskCardDisabledStatus[status]?.bottom
);
const themeMode = useAppSelector(state => state.themeReducer.mode);
const [addTaskCount, setAddTaskCount] = useState(0);
const [name, setName] = useState(status);
const [isEditable, setIsEditable] = useState(false);
const inputRef = useRef<InputRef>(null);
const [isLoading, setIsLoading] = useState(false);
const taskCardRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation('kanban-board');
const handleAddTaskClick = () => {
dispatch(setTaskCardDisabled({ status, position: 'bottom', disabled: false }));
setAddTaskCount(prev => prev + 1);
};
const handleTopAddTaskClick = () => {
dispatch(setTaskCardDisabled({ status, position: 'top', disabled: false }));
setAddTaskCount(prev => prev + 1);
};
useEffect(() => {
if (isEditable && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [isEditable]);
useEffect(() => {
createTaskInputRef.current?.focus();
taskCardRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}, [dataSource, addTaskCount]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
};
const handleBlur = () => {
setIsEditable(false);
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
}, 3000);
};
const items: MenuProps['items'] = [
{
key: '1',
label: (
<div
style={{
display: 'flex',
justifyContent: 'flex-start',
width: '100%',
padding: '5px 12px',
gap: '8px',
}}
onClick={() => setIsEditable(true)}
>
<EditOutlined /> <span>{t('rename')}</span>
</div>
),
},
{
key: '2',
label: <ChangeCategoryDropdown id={id} />,
},
{
key: '3',
label: (
<div
style={{
display: 'flex',
justifyContent: 'flex-start',
width: '100%',
padding: '5px 12px',
gap: '8px',
}}
onClick={() => dispatch(deleteStatus(id))}
>
<DeleteOutlined /> <span>{t('delete')}</span>
</div>
),
},
];
return (
<div style={{ paddingTop: '6px' }}>
<div
className={`todo-wraper ${themeMode === 'dark' ? 'dark-mode' : ''}`}
style={{
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
flexBasis: 0,
maxWidth: '375px',
width: '375px',
marginRight: '8px',
padding: '8px',
borderRadius: '25px',
maxHeight: 'calc(100vh - 250px)',
backgroundColor: themeMode === 'dark' ? '#282828' : '#F8FAFC',
}}
>
<div
style={{
touchAction: 'none',
userSelect: 'none',
cursor: 'grab',
fontSize: '14px',
paddingTop: '0',
margin: '0.25rem',
}}
>
<div
style={{
fontWeight: 600,
marginBottom: '12px',
alignItems: 'center',
padding: '8px',
display: 'flex',
justifyContent: 'space-between',
backgroundColor:
category === 'low' ? '#c2e4d0' : category === 'medium' ? '#f9e3b1' : '#f6bfc0',
borderRadius: '10px',
}}
>
<div
style={{ display: 'flex', gap: '5px', alignItems: 'center' }}
onClick={() => setIsEditable(true)}
>
{isLoading ? (
<LoadingOutlined />
) : (
<Button
type="text"
size="small"
shape="circle"
style={{
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
}}
>
{dataSource.length}
</Button>
)}
{isEditable ? (
<Input
ref={inputRef}
value={name}
variant="borderless"
style={{
backgroundColor: themeMode === 'dark' ? 'black' : 'white',
}}
onChange={handleChange}
onBlur={handleBlur}
onPressEnter={handleBlur}
/>
) : (
<Typography.Text
style={{
textTransform: 'capitalize',
color: themeMode === 'dark' ? '#383838' : '',
}}
>
{name}
</Typography.Text>
)}
</div>
<div style={{ display: 'flex' }}>
<Button
type="text"
size="small"
shape="circle"
onClick={handleTopAddTaskClick}
style={{ color: themeMode === 'dark' ? '#383838' : '' }}
>
<PlusOutlined />
</Button>
<Dropdown
overlayClassName="todo-threedot-dropdown"
trigger={['click']}
menu={{ items }}
placement="bottomLeft"
>
<Button type="text" size="small" shape="circle">
<MoreOutlined
style={{
rotate: '90deg',
fontSize: '25px',
color: themeMode === 'dark' ? '#383838' : '',
}}
/>
</Button>
</Dropdown>
</div>
</div>
</div>
<div
style={{
overflowY: 'auto',
maxHeight: 'calc(100vh - 250px)',
padding: '2px 6px 2px 2px',
}}
>
{!isTopCardDisabled && (
<TaskCreateCard ref={createTaskInputRef} status={status} position={'top'} />
)}
{dataSource.map(task => (
<TaskCard key={task.taskId} task={task} />
))}
{!isBottomCardDisabled && (
<TaskCreateCard ref={createTaskInputRef} status={status} position={'bottom'} />
)}
</div>
<div
style={{
textAlign: 'center',
margin: '7px 8px 8px 8px',
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
padding: '0',
}}
>
<Button
type="text"
style={{
height: '38px',
width: '100%',
}}
icon={<PlusOutlined />}
onClick={handleAddTaskClick}
>
{t('addTask')}
</Button>
</div>
</div>
</div>
);
};
export default CommonPrioritySection;

View File

@@ -0,0 +1,16 @@
.todo-wraper:hover {
border: 1px solid #f0f0f0;
}
.todo-wraper.dark-mode:hover {
border: 1px solid #3a3a3a;
}
.todo-threedot-dropdown .ant-dropdown-menu-item {
padding: 0 !important;
}
.todo-threedot-dropdown-button .ant-btn {
display: flex;
justify-content: left;
}

View File

@@ -0,0 +1,295 @@
import React, { useEffect, useRef, useState } from 'react';
import { Button, Dropdown, Input, InputRef, MenuProps, Typography } from 'antd';
import {
DeleteOutlined,
EditOutlined,
LoadingOutlined,
MoreOutlined,
PlusOutlined,
} from '@ant-design/icons';
import { setTaskCardDisabled, initializeStatus } from '../../../features/board/create-card.slice';
import { TaskType } from '../../../types/task.types';
import TaskCreateCard from '../taskCreateCard/TaskCreateCard';
import TaskCard from '../taskCard/TaskCard';
import { useAppSelector } from '@/hooks/useAppSelector';
import './CommonStatusSection.css';
import { deleteStatus } from '../../../features/projects/status/StatusSlice';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import ChangeCategoryDropdown from '../changeCategoryDropdown/ChangeCategoryDropdown';
import { useTranslation } from 'react-i18next';
interface CommonStatusSectionProps {
status: string;
dataSource: TaskType[];
category: string;
id: string;
}
const CommonStatusSection: React.FC<CommonStatusSectionProps> = ({
status,
dataSource,
category,
id,
}) => {
const dispatch = useAppDispatch();
const createTaskInputRef = useRef<InputRef>(null);
// Initialize status in the Redux store if not already set
useEffect(() => {
dispatch(initializeStatus(status));
}, [dispatch, status]);
// Get status-specific disable controls from Redux state
const isTopCardDisabled = useAppSelector(
state => state.createCardReducer.taskCardDisabledStatus[status]?.top
);
const isBottomCardDisabled = useAppSelector(
state => state.createCardReducer.taskCardDisabledStatus[status]?.bottom
);
const themeMode = useAppSelector(state => state.themeReducer.mode);
const [addTaskCount, setAddTaskCount] = useState(0);
const [name, setName] = useState(status);
const [isEditable, setIsEditable] = useState(false);
const inputRef = useRef<InputRef>(null);
const [isLoading, setIsLoading] = useState(false);
const taskCardRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation('kanban-board');
const handleAddTaskClick = () => {
dispatch(setTaskCardDisabled({ status, position: 'bottom', disabled: false }));
setAddTaskCount(prev => prev + 1);
};
const handleTopAddTaskClick = () => {
dispatch(setTaskCardDisabled({ status, position: 'top', disabled: false }));
setAddTaskCount(prev => prev + 1);
};
useEffect(() => {
if (isEditable && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [isEditable]);
useEffect(() => {
createTaskInputRef.current?.focus();
taskCardRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}, [dataSource, addTaskCount]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
};
const handleBlur = () => {
setIsEditable(false);
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
}, 3000);
};
const items: MenuProps['items'] = [
{
key: '1',
label: (
<div
style={{
display: 'flex',
justifyContent: 'flex-start',
width: '100%',
padding: '5px 12px',
gap: '8px',
}}
onClick={() => setIsEditable(true)}
>
<EditOutlined /> <span>{t('rename')}</span>
</div>
),
},
{
key: '2',
label: <ChangeCategoryDropdown id={id} />,
},
{
key: '3',
label: (
<div
style={{
display: 'flex',
justifyContent: 'flex-start',
width: '100%',
padding: '5px 12px',
gap: '8px',
}}
onClick={() => dispatch(deleteStatus(id))}
>
<DeleteOutlined /> <span>{t('delete')}</span>
</div>
),
},
];
return (
<div style={{ paddingTop: '6px' }}>
<div
className={`todo-wraper ${themeMode === 'dark' ? 'dark-mode' : ''}`}
style={{
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
flexBasis: 0,
maxWidth: '375px',
width: '375px',
marginRight: '8px',
padding: '8px',
borderRadius: '25px',
maxHeight: 'calc(100vh - 250px)',
backgroundColor: themeMode === 'dark' ? '#282828' : '#F8FAFC',
}}
>
<div
style={{
touchAction: 'none',
userSelect: 'none',
cursor: 'grab',
fontSize: '14px',
paddingTop: '0',
margin: '0.25rem',
}}
>
<div
style={{
fontWeight: 600,
marginBottom: '12px',
alignItems: 'center',
padding: '8px',
display: 'flex',
justifyContent: 'space-between',
backgroundColor:
category === 'todo' ? '#d1d0d3' : category === 'doing' ? '#b9cef1' : '#c2e4d0',
borderRadius: '10px',
}}
>
<div
style={{ display: 'flex', gap: '5px', alignItems: 'center' }}
onClick={() => setIsEditable(true)}
>
{isLoading ? (
<LoadingOutlined />
) : (
<Button
type="text"
size="small"
shape="circle"
style={{
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
}}
>
{dataSource.length}
</Button>
)}
{isEditable ? (
<Input
ref={inputRef}
value={name}
variant="borderless"
style={{
backgroundColor: themeMode === 'dark' ? 'black' : 'white',
}}
onChange={handleChange}
onBlur={handleBlur}
onPressEnter={handleBlur}
/>
) : (
<Typography.Text
style={{
textTransform: 'capitalize',
color: themeMode === 'dark' ? '#383838' : '',
}}
>
{name}
</Typography.Text>
)}
</div>
<div style={{ display: 'flex' }}>
<Button
type="text"
size="small"
shape="circle"
onClick={handleTopAddTaskClick}
style={{ color: themeMode === 'dark' ? '#383838' : '' }}
>
<PlusOutlined />
</Button>
<Dropdown
overlayClassName="todo-threedot-dropdown"
trigger={['click']}
menu={{ items }}
placement="bottomLeft"
>
<Button type="text" size="small" shape="circle">
<MoreOutlined
style={{
rotate: '90deg',
fontSize: '25px',
color: themeMode === 'dark' ? '#383838' : '',
}}
/>
</Button>
</Dropdown>
</div>
</div>
</div>
<div
style={{
overflowY: 'auto',
maxHeight: 'calc(100vh - 250px)',
padding: '2px 6px 2px 2px',
}}
>
{!isTopCardDisabled && (
<TaskCreateCard ref={createTaskInputRef} status={status} position={'top'} />
)}
{dataSource.map(task => (
<TaskCard key={task.taskId} task={task} />
))}
{!isBottomCardDisabled && (
<TaskCreateCard ref={createTaskInputRef} status={status} position={'bottom'} />
)}
</div>
<div
style={{
textAlign: 'center',
margin: '7px 8px 8px 8px',
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
padding: '0',
}}
>
<Button
type="text"
style={{
height: '38px',
width: '100%',
}}
icon={<PlusOutlined />}
onClick={handleAddTaskClick}
>
{t('addTask')}
</Button>
</div>
</div>
</div>
);
};
export default CommonStatusSection;

View File

@@ -0,0 +1,30 @@
import { Button, Flex } from 'antd';
import AddMembersDropdown from '@/components/add-members-dropdown/add-members-dropdown';
import Avatars from '../avatars/avatars';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import BoardAssigneeSelector from './board-assignee-selector/board-assignee-selector';
type CustomAvatarGroupProps = {
task: IProjectTask;
sectionId: string;
};
const CustomAvatarGroup = ({ task, sectionId }: CustomAvatarGroupProps) => {
return (
<Flex
gap={4}
align="center"
onClick={(e) => e.stopPropagation()}
style={{
borderRadius: 4,
cursor: 'pointer',
}}
>
<Avatars members={task?.names || []} />
<BoardAssigneeSelector task={task} groupId={sectionId} />
</Flex>
);
};
export default CustomAvatarGroup;

View File

@@ -0,0 +1,101 @@
import React, { useState, useRef } from 'react';
import { DatePicker, Button, Flex } from 'antd';
import { CalendarOutlined } from '@ant-design/icons';
import dayjs, { Dayjs } from 'dayjs';
import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import logger from '@/utils/errorLogger';
import { useAuthService } from '@/hooks/useAuth';
import { getUserSession } from '@/utils/session-helper';
const CustomDueDatePicker = ({
task,
onDateChange,
}: {
task: IProjectTask;
onDateChange: (date: Dayjs | null) => void;
}) => {
const { socket } = useSocket();
const [isDatePickerOpen, setIsDatePickerOpen] = useState<boolean>(false);
const containerRef = useRef<HTMLDivElement>(null); // Add ref to container
const dueDayjs = task?.end_date ? dayjs(task.end_date) : null;
const handleDateChange = (date: Dayjs | null) => {
onDateChange(date);
setIsDatePickerOpen(false);
try {
socket?.emit(
SocketEvents.TASK_END_DATE_CHANGE.toString(),
JSON.stringify({
task_id: task.id,
end_date: date?.format(),
parent_task: task.parent_task_id,
time_zone: getUserSession()?.timezone_name
? getUserSession()?.timezone_name
: Intl.DateTimeFormat().resolvedOptions().timeZone,
})
);
} catch (error) {
logger.error('Failed to update due date:', error);
}
};
// Stop propagation at the container level
const handleContainerClick = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
};
return (
<div ref={containerRef} onClick={handleContainerClick}>
{task && task.end_date ? (
<DatePicker
value={dueDayjs}
format={'MMM DD, YYYY'}
onChange={handleDateChange}
variant="borderless"
suffixIcon={null}
style={{ textAlign: 'right', padding: 0, maxWidth: 100 }}
// Remove individual onClick handler since container handles it
/>
) : (
<Flex gap={4} align="center" style={{ position: 'relative', width: 26, height: 26 }}>
<DatePicker
open={isDatePickerOpen}
value={dueDayjs}
format={'MMM DD, YYYY'}
onChange={handleDateChange}
style={{ opacity: 0, width: 0, height: 0, padding: 0 }}
popupStyle={{ paddingBlock: 12 }}
onBlur={() => setIsDatePickerOpen(false)}
onOpenChange={open => setIsDatePickerOpen(open)}
variant="borderless"
// Remove individual onClick handler
/>
<Button
shape="circle"
type="dashed"
size="small"
style={{
background: 'transparent',
boxShadow: 'none',
position: 'absolute',
top: 0,
left: 0,
width: 26,
height: 26,
}}
onClick={(e) => {
e.stopPropagation(); // Keep this as a backup
setIsDatePickerOpen(true);
}}
icon={<CalendarOutlined />}
/>
</Flex>
)}
</div>
);
};
export default CustomDueDatePicker;

View File

@@ -0,0 +1,16 @@
.todo-wraper:hover {
border: 1px solid #f0f0f0;
}
.todo-wraper.dark-mode:hover {
border: 1px solid #3a3a3a;
}
.todo-threedot-dropdown .ant-dropdown-menu-item {
padding: 0 !important;
}
.todo-threedot-dropdown-button .ant-btn {
display: flex;
justify-content: left;
}

View File

@@ -0,0 +1,330 @@
import React, { useEffect, useRef, useState } from 'react';
import { Button, Dropdown, Input, InputRef, MenuProps, Typography } from 'antd';
import {
DeleteOutlined,
EditOutlined,
LoadingOutlined,
MoreOutlined,
PlusOutlined,
} from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import { useDroppable } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { setTaskCardDisabled, initializeGroup } from '@/features/board/create-card.slice';
import TaskCreateCard from '../taskCreateCard/TaskCreateCard';
import TaskCard from '../taskCard/TaskCard';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { deleteStatus } from '@features/projects/status/StatusSlice';
import ChangeCategoryDropdown from '../changeCategoryDropdown/ChangeCategoryDropdown';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import './kanban-group.css';
interface KanbanGroupProps {
title: string;
tasks: IProjectTask[];
id: string;
color: string;
}
interface GroupState {
name: string;
isEditable: boolean;
isLoading: boolean;
addTaskCount: number;
}
const KanbanGroup: React.FC<KanbanGroupProps> = ({ title, tasks, id, color }) => {
// Refs
const inputRef = useRef<InputRef>(null);
const createTaskInputRef = useRef<InputRef>(null);
const taskCardRef = useRef<HTMLDivElement>(null);
// State
const [groupState, setGroupState] = useState<GroupState>({
name: title,
isEditable: false,
isLoading: false,
addTaskCount: 0,
});
// Hooks
const dispatch = useAppDispatch();
const { t } = useTranslation('kanban-board');
// Selectors
const themeMode = useAppSelector(state => state.themeReducer.mode);
const isTopCardDisabled = useAppSelector(
state => state.createCardReducer.taskCardDisabledStatus[id]?.top
);
const isBottomCardDisabled = useAppSelector(
state => state.createCardReducer.taskCardDisabledStatus[id]?.bottom
);
// Add droppable functionality
const { setNodeRef } = useDroppable({
id: id,
});
// Effects
useEffect(() => {
dispatch(initializeGroup(id));
}, [dispatch, id]);
useEffect(() => {
if (groupState.isEditable && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [groupState.isEditable]);
useEffect(() => {
createTaskInputRef.current?.focus();
taskCardRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}, [tasks, groupState.addTaskCount]);
// Handlers
const handleAddTaskClick = () => {
dispatch(setTaskCardDisabled({ group: id, position: 'bottom', disabled: false }));
setGroupState(prev => ({ ...prev, addTaskCount: prev.addTaskCount + 1 }));
};
const handleTopAddTaskClick = () => {
dispatch(setTaskCardDisabled({ group: id, position: 'top', disabled: false }));
setGroupState(prev => ({ ...prev, addTaskCount: prev.addTaskCount + 1 }));
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setGroupState(prev => ({ ...prev, name: e.target.value }));
};
const handleBlur = () => {
setGroupState(prev => ({ ...prev, isEditable: false, isLoading: true }));
setTimeout(() => {
setGroupState(prev => ({ ...prev, isLoading: false }));
}, 3000);
};
const handleEditClick = () => {
setGroupState(prev => ({ ...prev, isEditable: true }));
};
// Menu items
const items: MenuProps['items'] = [
{
key: '1',
label: (
<div
style={{
display: 'flex',
justifyContent: 'flex-start',
width: '100%',
padding: '5px 12px',
gap: '8px',
}}
onClick={handleEditClick}
>
<EditOutlined /> <span>{t('rename')}</span>
</div>
),
},
{
key: '2',
label: <ChangeCategoryDropdown id={id} />,
},
{
key: '3',
label: (
<div
style={{
display: 'flex',
justifyContent: 'flex-start',
width: '100%',
padding: '5px 12px',
gap: '8px',
}}
onClick={() => dispatch(deleteStatus(id))}
>
<DeleteOutlined /> <span>{t('delete')}</span>
</div>
),
},
];
// Styles
const containerStyle = {
paddingTop: '6px',
};
const wrapperStyle = {
display: 'flex',
flexDirection: 'column' as const,
flexGrow: 1,
flexBasis: 0,
maxWidth: '375px',
width: '375px',
marginRight: '8px',
padding: '8px',
borderRadius: '25px',
maxHeight: 'calc(100vh - 250px)',
backgroundColor: themeMode === 'dark' ? '#282828' : '#F8FAFC',
};
const headerStyle = {
touchAction: 'none' as const,
userSelect: 'none' as const,
cursor: 'grab',
fontSize: '14px',
paddingTop: '0',
margin: '0.25rem',
};
const titleBarStyle = {
fontWeight: 600,
marginBottom: '12px',
alignItems: 'center',
padding: '8px',
display: 'flex',
justifyContent: 'space-between',
backgroundColor: color,
borderRadius: '10px',
};
return (
<div style={containerStyle}>
<div
className={`todo-wraper ${themeMode === 'dark' ? 'dark-mode' : ''}`}
style={wrapperStyle}
>
<div style={headerStyle}>
<div style={titleBarStyle}>
<div
style={{ display: 'flex', gap: '5px', alignItems: 'center' }}
onClick={handleEditClick}
>
{groupState.isLoading ? (
<LoadingOutlined />
) : (
<Button
type="text"
size="small"
shape="circle"
style={{
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
}}
>
{tasks.length}
</Button>
)}
{groupState.isEditable ? (
<Input
ref={inputRef}
value={groupState.name}
variant="borderless"
style={{
backgroundColor: themeMode === 'dark' ? 'black' : 'white',
}}
onChange={handleChange}
onBlur={handleBlur}
onPressEnter={handleBlur}
/>
) : (
<Typography.Text
style={{
textTransform: 'capitalize',
color: themeMode === 'dark' ? '#383838' : '',
}}
>
{groupState.name}
</Typography.Text>
)}
</div>
<div style={{ display: 'flex' }}>
<Button
type="text"
size="small"
shape="circle"
onClick={handleTopAddTaskClick}
style={{ color: themeMode === 'dark' ? '#383838' : '' }}
>
<PlusOutlined />
</Button>
<Dropdown
overlayClassName="todo-threedot-dropdown"
trigger={['click']}
menu={{ items }}
placement="bottomLeft"
>
<Button type="text" size="small" shape="circle">
<MoreOutlined
style={{
rotate: '90deg',
fontSize: '25px',
color: themeMode === 'dark' ? '#383838' : '',
}}
/>
</Button>
</Dropdown>
</div>
</div>
</div>
<div
ref={setNodeRef}
style={{
overflowY: 'auto',
maxHeight: 'calc(100vh - 250px)',
padding: '2px 6px 2px 2px',
}}
>
{!isTopCardDisabled && (
<TaskCreateCard ref={createTaskInputRef} status={title} position={'top'} />
)}
<SortableContext
items={tasks.map(task => task.id)}
strategy={verticalListSortingStrategy}
>
<div className="App" style={{ display: 'flex', flexDirection: 'column' }}>
{tasks.map(task => (
<TaskCard key={task.id} task={task} />
))}
</div>
</SortableContext>
{!isBottomCardDisabled && (
<TaskCreateCard ref={createTaskInputRef} status={title} position={'bottom'} />
)}
</div>
<div
style={{
textAlign: 'center',
margin: '7px 8px 8px 8px',
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
padding: '0',
}}
>
<Button
type="text"
style={{
height: '38px',
width: '100%',
}}
icon={<PlusOutlined />}
onClick={handleAddTaskClick}
>
{t('addTask')}
</Button>
</div>
</div>
</div>
);
};
export default KanbanGroup;

View File

@@ -0,0 +1,158 @@
import React, { useEffect, useState } from 'react';
import { Avatar, Col, DatePicker, Divider, Flex, Row, Tooltip, Typography } from 'antd';
import StatusDropdown from '../../taskListCommon/statusDropdown/StatusDropdown';
import dayjs, { Dayjs } from 'dayjs';
import { useTranslation } from 'react-i18next';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import Avatars from '@/components/avatars/avatars';
interface SubTaskProps {
subtask: IProjectTask;
}
const SubTaskCard: React.FC<SubTaskProps> = ({ subtask }) => {
const [isSubToday, setIsSubToday] = useState(false);
const [isSubTomorrow, setIsSubTomorrow] = useState(false);
const [isItSubPrevDate, setIsItSubPrevDate] = useState(false);
const [subTaskDueDate, setSubTaskDueDate] = useState<Dayjs | null>(null);
const { t } = useTranslation('kanban-board');
const handleSubTaskDateChange = (date: Dayjs | null) => {
setSubTaskDueDate(date);
};
const formatDate = (date: Dayjs | null) => {
if (!date) return '';
const today = dayjs();
const tomorrow = today.add(1, 'day');
if (date.isSame(today, 'day')) {
return 'Today';
} else if (date.isSame(tomorrow, 'day')) {
return 'Tomorrow';
} else {
return date.isSame(today, 'year') ? date.format('MMM DD') : date.format('MMM DD, YYYY');
}
};
useEffect(() => {
if (subTaskDueDate) {
setIsSubToday(subTaskDueDate.isSame(dayjs(), 'day'));
setIsSubTomorrow(subTaskDueDate.isSame(dayjs().add(1, 'day'), 'day'));
setIsItSubPrevDate(subTaskDueDate.isBefore(dayjs()));
} else {
setIsSubToday(false);
setIsSubTomorrow(false);
setIsItSubPrevDate(false);
}
}, [subTaskDueDate]);
return (
<Row
key={subtask.id}
style={{
marginTop: '0.5rem',
width: '100%',
}}
>
<Col span={10}>
<Typography.Text
style={{ fontWeight: 500, fontSize: '12px' }}
delete={subtask.status === 'done'}
>
{subtask.name}
</Typography.Text>
</Col>
<Col span={4}>
<Avatar.Group
size="small"
max={{
count: 1,
style: {
color: '#f56a00',
backgroundColor: '#fde3cf',
},
}}
>
<Avatars members={subtask.names || []} />
</Avatar.Group>
</Col>
<Col span={10}>
<Flex>
<DatePicker
className={`custom-placeholder ${!subTaskDueDate ? 'empty-date' : isSubToday || isSubTomorrow ? 'selected-date' : isItSubPrevDate ? 'red-colored' : ''}`}
placeholder={t('dueDate')}
style={{
fontSize: '12px',
opacity: subTaskDueDate ? 1 : 0,
}}
onChange={handleSubTaskDateChange}
variant="borderless"
size="small"
suffixIcon={false}
format={value => formatDate(value)}
/>
<div>
<StatusDropdown currentStatus={subtask.status} />
</div>
</Flex>
</Col>
{/* <div
style={{
display: 'flex',
justifyContent: 'space-between',
}}
>
<Typography.Text
style={{ fontWeight: 500 }}
delete={subtask.status === 'done'}
>
{subtask.task}
</Typography.Text>
<StatusDropdown currentStatus={subtask.status} />
</div>
<div style={{ display: 'flex' }}>
<Avatar.Group
size="small"
max={{
count: 1,
style: {
color: '#f56a00',
backgroundColor: '#fde3cf',
},
}}
>
{subtask.members?.map((member) => (
<Avatar
style={{
backgroundColor: AvatarNamesMap[member.memberName.charAt(0)],
fontSize: '12px',
}}
size="small"
>
{member.memberName.charAt(0)}
</Avatar>
))}
</Avatar.Group>
<DatePicker
className={`custom-placeholder ${!subTaskDueDate ? 'empty-date' : isSubToday || isSubTomorrow ? 'selected-date' : isItSubPrevDate ? 'red-colored' : ''}`}
placeholder="Due date"
style={{
fontSize: '12px',
opacity: subTaskDueDate ? 1 : 0,
}}
onChange={handleSubTaskDateChange}
variant="borderless"
size="small"
suffixIcon={false}
format={(value) => formatDate(value)}
/>
</div> */}
<Divider style={{ margin: '5px' }} />
</Row>
);
};
export default SubTaskCard;

View File

@@ -0,0 +1,58 @@
.task-card {
transition: box-shadow 0.3s ease;
box-shadow:
#edeae9 0 0 0 1px,
#6d6e6f14 0 1px 4px;
}
.task-card:hover {
box-shadow:
#a8a8a8 0 0 0 1px,
#6d6e6f14 0 1px 4px;
}
.task-card.dark-mode {
box-shadow:
#2e2e2e 0 0 0 1px,
#0000001a 0 1px 4px;
}
.custom-placeholder .ant-picker-input input::placeholder {
font-size: 12px !important;
}
.task-card:hover .empty-date {
opacity: 1 !important;
}
.task-card:hover .hide-add-member-avatar {
opacity: 0.8;
}
.custom-placeholder .ant-picker-input input {
font-size: 12px !important;
}
.selected-date .ant-picker-input {
color: #87d068;
}
.red-colored .ant-picker-input {
color: red;
}
.sub-selected-date .ant-picker-input {
color: #87d068;
}
.sub-red-colored .ant-picker-input {
color: red;
}
.add-member-avatar {
opacity: 0.8;
}
.hide-add-member-avatar {
opacity: 0;
}

View File

@@ -0,0 +1,318 @@
import React, { useEffect, useState } from 'react';
import {
DatePicker,
Tooltip,
Tag,
Avatar,
Progress,
Typography,
Dropdown,
MenuProps,
Button,
} from 'antd';
import {
DoubleRightOutlined,
PauseOutlined,
UserAddOutlined,
InboxOutlined,
DeleteOutlined,
MinusOutlined,
ForkOutlined,
CaretRightFilled,
CaretDownFilled,
} from '@ant-design/icons';
import './TaskCard.css';
import dayjs, { Dayjs } from 'dayjs';
import AddMembersDropdown from '../../add-members-dropdown/add-members-dropdown';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { deleteTask } from '../../../features/tasks/tasks.slice';
import SubTaskCard from '../subTaskCard/SubTaskCard';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useTranslation } from 'react-i18next';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import Avatars from '@/components/avatars/avatars';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { UniqueIdentifier } from '@dnd-kit/core';
interface taskProps {
task: IProjectTask;
}
const TaskCard: React.FC<taskProps> = ({ task }) => {
const [isSubTaskShow, setIsSubTaskShow] = useState(false);
const [dueDate, setDueDate] = useState<Dayjs | null>(null);
const [isToday, setIsToday] = useState(false);
const [isTomorrow, setIsTomorrow] = useState(false);
const [isItPrevDate, setIsItPrevDate] = useState(false);
const themeMode = useAppSelector(state => state.themeReducer.mode);
const dispatch = useAppDispatch();
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: task.id as UniqueIdentifier,
data: {
type: 'task',
task,
},
});
const handleDateChange = (date: Dayjs | null) => {
setDueDate(date);
};
const { t } = useTranslation('kanban-board');
const formatDate = (date: Dayjs | null) => {
if (!date) return '';
const today = dayjs();
const tomorrow = today.add(1, 'day');
if (date.isSame(today, 'day')) {
return t('today');
} else if (date.isSame(tomorrow, 'day')) {
return t('tomorrow');
} else {
return date.isSame(today, 'year') ? date.format('MMM DD') : date.format('MMM DD, YYYY');
}
};
useEffect(() => {
if (dueDate) {
setIsToday(dueDate.isSame(dayjs(), 'day'));
setIsTomorrow(dueDate.isSame(dayjs().add(1, 'day'), 'day'));
setIsItPrevDate(dueDate.isBefore(dayjs()));
} else {
setIsToday(false);
setIsTomorrow(false);
setIsItPrevDate(false);
}
}, [dueDate]);
const handleDelete = () => {
if (!task.id) return;
dispatch(deleteTask(task.id)); // Call delete function with taskId
};
const items: MenuProps['items'] = [
{
label: (
<span>
<UserAddOutlined /> <Typography.Text>{t('assignToMe')}</Typography.Text>
</span>
),
key: '1',
},
{
label: (
<span>
<InboxOutlined /> <Typography.Text>{t('archive')}</Typography.Text>
</span>
),
key: '2',
},
{
label: (
<span onClick={handleDelete}>
<DeleteOutlined /> <Typography.Text>{t('delete')}</Typography.Text>
</span>
),
key: '3',
},
];
// const progress = (task.subTasks?.length || 0 + 1 )/ (task.subTasks?.length || 0 + 1)
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
position: 'relative',
touchAction: 'none',
};
return (
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
<Dropdown menu={{ items }} trigger={['contextMenu']}>
<div
className={`task-card ${themeMode === 'dark' ? 'dark-mode' : ''}`}
style={{
zIndex: 99,
padding: '12px',
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
borderRadius: '4px',
marginBottom: '12px',
cursor: 'pointer',
position: 'relative',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
gap: '10px',
}}
>
{/* Labels and Progress */}
<div style={{ display: 'flex' }}>
<div>
{task.labels?.length ? (
<>
{task.labels.slice(0, 2).map((label, index) => (
<Tag key={index} style={{ marginRight: '4px' }} color={label.color_code}>
<span style={{ color: themeMode === 'dark' ? '#383838' : '' }}>
{label.name}
</span>
</Tag>
))}
{task.labels?.length > 2 && <Tag>+ {task.labels.length - 2}</Tag>}
</>
) : (
''
)}
</div>
<div
style={{
maxWidth: '30px',
height: '30px',
marginLeft: 'auto',
}}
>
<Tooltip title="1/1">
<Progress type="circle" percent={task.progress} size={26} />
</Tooltip>
</div>
</div>
{/* Action Icons */}
<div style={{ display: 'flex' }}>
{task.priority === 'low' ? (
<MinusOutlined
style={{
color: '#52c41a',
marginRight: '0.25rem',
}}
/>
) : task.priority === 'medium' ? (
<PauseOutlined
style={{
color: '#faad14',
transform: 'rotate(90deg)',
marginRight: '0.25rem',
}}
/>
) : (
<DoubleRightOutlined
style={{
color: '#f5222d',
transform: 'rotate(-90deg)',
marginRight: '0.25rem',
}}
/>
)}
<Typography.Text style={{ fontWeight: 500 }}>{task.name}</Typography.Text>
</div>
{/* Subtask Section */}
<div>
<div
style={{
marginTop: '0.5rem',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<div
style={{
opacity: 1,
borderRadius: '4px',
cursor: 'pointer',
alignItems: 'center',
display: 'flex',
gap: '3px',
}}
>
<Avatars members={task.names || []} />
<Avatar
size="small"
className={
task.assignees?.length ? 'add-member-avatar' : 'hide-add-member-avatar'
}
style={{
backgroundColor: '#fff',
border: '1px dashed #c4c4c4',
color: '#000000d9',
fontSize: '12px',
}}
>
<AddMembersDropdown />
</Avatar>
</div>
<div
style={{
display: 'flex',
justifyContent: 'right',
alignItems: 'center',
}}
>
<div>
<DatePicker
className={`custom-placeholder ${
!dueDate
? 'empty-date'
: isToday
? 'selected-date'
: isTomorrow
? 'selected-date'
: isItPrevDate
? 'red-colored'
: ''
}`}
placeholder={t('dueDate')}
style={{
fontSize: '12px',
opacity: dueDate ? 1 : 0,
width: dueDate ? 'auto' : '100%',
maxWidth: '100px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
onChange={handleDateChange}
variant="borderless"
size="small"
suffixIcon={false}
format={value => formatDate(value)}
/>
</div>
{task.sub_tasks_count && task.sub_tasks_count > 0 && (
<Button
onClick={() => setIsSubTaskShow(!isSubTaskShow)}
size="small"
style={{ padding: 0 }}
type="text"
>
<Tag
bordered={false}
style={{ display: 'flex', alignItems: 'center', margin: 0 }}
>
<ForkOutlined rotate={90} />
<span>{task.sub_tasks_count}</span>
{isSubTaskShow ? <CaretDownFilled /> : <CaretRightFilled />}
</Tag>
</Button>
)}
</div>
</div>
{isSubTaskShow &&
task.sub_tasks_count &&
task.sub_tasks_count > 0 &&
task.sub_tasks?.map(subtask => <SubTaskCard subtask={subtask} />)}
</div>
</div>
</Dropdown>
</div>
);
};
export default TaskCard;

View File

@@ -0,0 +1,3 @@
.task-card .create-task-empty-date {
opacity: 1 !important;
}

View File

@@ -0,0 +1,255 @@
import { Avatar, Button, DatePicker, Input, InputRef } from 'antd';
import React, { forwardRef, useEffect, useRef, useState } from 'react';
import AddMembersDropdown from '../../add-members-dropdown/add-members-dropdown';
import dayjs, { Dayjs } from 'dayjs';
import './TaskCreateCard.css';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { addTask, addTaskToTop } from '../../../features/tasks/tasks.slice';
import { setTaskCardDisabled } from '../../../features/board/create-card.slice';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useTranslation } from 'react-i18next';
interface StatusProps {
status: string;
position: 'top' | 'bottom';
}
const TaskCreateCard = forwardRef<InputRef, StatusProps>(({ status, position }, ref) => {
const [characterLength, setCharacterLength] = useState<number>(0);
const [dueDate, setDueDate] = useState<Dayjs | null>(null);
const [isToday, setIsToday] = useState(false);
const [isTomorrow, setIsTomorrow] = useState(false);
const [isItPrevDate, setIsItPrevDate] = useState(false);
const [taskName, setTaskName] = useState('');
const dispatch = useAppDispatch();
const themeMode = useAppSelector(state => state.themeReducer.mode);
const { t } = useTranslation('kanban-board');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCharacterLength(e.target.value.length);
setTaskName(e.target.value);
};
const handleDateChange = (date: Dayjs | null) => {
setDueDate(date);
};
const cardRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (cardRef.current) {
cardRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
if (ref && typeof ref === 'object' && ref.current) {
ref.current.focus();
}
}, []);
const formatDate = (date: Dayjs | null) => {
if (!date) return '';
const today = dayjs();
const tomorrow = today.add(1, 'day');
if (date.isSame(today, 'day')) {
return 'Today';
} else if (date.isSame(tomorrow, 'day')) {
return 'Tomorrow';
} else {
return date.isSame(today, 'year') ? date.format('MMM DD') : date.format('MMM DD, YYYY');
}
};
useEffect(() => {
if (dueDate) {
setIsToday(dueDate.isSame(dayjs(), 'day'));
setIsTomorrow(dueDate.isSame(dayjs().add(1, 'day'), 'day'));
setIsItPrevDate(dueDate.isBefore(dayjs()));
} else {
setIsToday(false);
setIsTomorrow(false);
setIsItPrevDate(false);
}
}, [dueDate]);
const handleAddTask = () => {
if (taskName.trim()) {
if (position === 'bottom') {
dispatch(
addTask({
taskId: `SP-${Date.now()}`,
task: taskName,
description: '-',
progress: status === 'done' ? 100 : 0,
members: [],
labels: [],
status: status,
priority: 'medium',
timeTracking: 0,
estimation: '-',
startDate: new Date(),
dueDate: dueDate ? dueDate.toDate() : null,
completedDate: null,
createdDate: new Date(),
lastUpdated: new Date(),
reporter: '-',
phase: '',
subTasks: [],
})
);
} else if (position === 'top') {
dispatch(
addTaskToTop({
taskId: `SP-${Date.now()}`,
task: taskName,
description: '-',
progress: status === 'done' ? 100 : 0,
members: [],
labels: [],
status: status,
priority: 'medium',
timeTracking: 0,
estimation: '-',
startDate: new Date(),
dueDate: dueDate ? dueDate.toDate() : null,
completedDate: null,
createdDate: new Date(),
lastUpdated: new Date(),
reporter: '-',
phase: '-',
subTasks: [],
})
);
}
}
setTaskName('');
};
const handleClose = () => {
dispatch(setTaskCardDisabled({ status, position, disabled: true }));
};
return (
<div
ref={cardRef}
className={`task-card ${themeMode === 'dark' ? 'dark-mode' : ''}`}
style={{
zIndex: 99,
padding: '12px',
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
borderRadius: '4px',
marginBottom: '12px',
cursor: 'pointer',
position: 'relative',
overflow: 'hidden',
}}
>
{/* Input field */}
<div style={{ display: 'flex' }}>
<Input
ref={ref}
type="text"
maxLength={100}
onChange={handleChange}
value={taskName}
onPressEnter={handleAddTask}
placeholder="Enter task name"
/>
</div>
<div style={{ opacity: characterLength > 0 ? 1 : 0 }}>
{/* Character Length */}
<div
style={{
position: 'absolute',
zIndex: 1,
right: '15px',
top: '43px',
color: themeMode === 'dark' ? '#ffffffd9' : '#00000073',
fontSize: '10px',
}}
>
<span>{characterLength}/100</span>
</div>
{/* DatePicker and Avatars */}
<div
style={{
paddingTop: '0.25rem',
marginTop: '0.75rem',
display: 'flex',
marginBottom: '16px',
}}
>
<div style={{ height: '100%', width: '100%' }}>
<DatePicker
className={`custom-placeholder ${!dueDate ? 'create-task-empty-date' : isToday ? 'selected-date' : isTomorrow ? 'selected-date' : isItPrevDate ? 'red-colored' : ''}`}
placeholder={t('dueDate')}
style={{
fontSize: '12px',
opacity: dueDate ? 1 : 0,
}}
onChange={handleDateChange}
variant="borderless"
size="small"
suffixIcon={false}
format={value => formatDate(value)}
/>
</div>
<div style={{ marginLeft: 'auto' }}>
<div
style={{
opacity: 1,
borderRadius: '4px',
cursor: 'pointer',
alignItems: 'center',
height: '100%',
width: '100%',
display: 'flex',
gap: '3px',
}}
>
<Avatar.Group>
{/* <Avatar
style={{
backgroundColor:
AvatarNamesMap[
member?.charAt(0)
],
verticalAlign: 'middle',
fontSize: '12px',
}}
size="small"
>
{member.charAt(0)}
</Avatar> */}
</Avatar.Group>
<Avatar
size="small"
style={{
backgroundColor: '#fff',
border: '1px dashed #c4c4c4',
color: '#000000d9',
fontSize: '12px',
}}
>
<AddMembersDropdown />
</Avatar>
</div>
</div>
</div>
</div>
{/* Add Task Button and Cancel Button*/}
<div>
<Button size="small" style={{ marginRight: '8px', fontSize: '12px' }} onClick={handleClose}>
{t('cancel')}
</Button>
<Button size="small" type="primary" style={{ fontSize: '12px' }} onClick={handleAddTask}>
{t('addTask')}
</Button>
</div>
</div>
);
});
export default TaskCreateCard;

View File

@@ -0,0 +1,255 @@
import { Avatar, Button, DatePicker, Input, InputRef } from 'antd';
import React, { forwardRef, useEffect, useRef, useState } from 'react';
import AddMembersDropdown from '../../add-members-dropdown/add-members-dropdown';
import dayjs, { Dayjs } from 'dayjs';
import './TaskCreateCard.css';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { addTask, addTaskToTop } from '../../../features/tasks/tasks.slice';
import { setTaskCardDisabled } from '../../../features/board/create-card.slice';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useTranslation } from 'react-i18next';
interface PriorityProps {
status: string;
position: 'top' | 'bottom';
}
const PriorityTaskCreateCard = forwardRef<InputRef, PriorityProps>(({ status, position }, ref) => {
const [characterLength, setCharacterLength] = useState<number>(0);
const [dueDate, setDueDate] = useState<Dayjs | null>(null);
const [isToday, setIsToday] = useState(false);
const [isTomorrow, setIsTomorrow] = useState(false);
const [isItPrevDate, setIsItPrevDate] = useState(false);
const [taskName, setTaskName] = useState('');
const dispatch = useAppDispatch();
const themeMode = useAppSelector(state => state.themeReducer.mode);
const { t } = useTranslation('kanban-board');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCharacterLength(e.target.value.length);
setTaskName(e.target.value);
};
const handleDateChange = (date: Dayjs | null) => {
setDueDate(date);
};
const cardRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (cardRef.current) {
cardRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
if (ref && typeof ref === 'object' && ref.current) {
ref.current.focus();
}
}, []);
const formatDate = (date: Dayjs | null) => {
if (!date) return '';
const today = dayjs();
const tomorrow = today.add(1, 'day');
if (date.isSame(today, 'day')) {
return 'Today';
} else if (date.isSame(tomorrow, 'day')) {
return 'Tomorrow';
} else {
return date.isSame(today, 'year') ? date.format('MMM DD') : date.format('MMM DD, YYYY');
}
};
useEffect(() => {
if (dueDate) {
setIsToday(dueDate.isSame(dayjs(), 'day'));
setIsTomorrow(dueDate.isSame(dayjs().add(1, 'day'), 'day'));
setIsItPrevDate(dueDate.isBefore(dayjs()));
} else {
setIsToday(false);
setIsTomorrow(false);
setIsItPrevDate(false);
}
}, [dueDate]);
const handleAddTask = () => {
if (taskName.trim()) {
if (position === 'bottom') {
dispatch(
addTask({
taskId: `SP-${Date.now()}`,
task: taskName,
description: '-',
progress: status === 'done' ? 100 : 0,
members: [],
labels: [],
status: status,
priority: 'medium',
timeTracking: 0,
estimation: '-',
startDate: new Date(),
dueDate: dueDate ? dueDate.toDate() : null,
completedDate: null,
createdDate: new Date(),
lastUpdated: new Date(),
reporter: '-',
phase: '',
subTasks: [],
})
);
} else if (position === 'top') {
dispatch(
addTaskToTop({
taskId: `SP-${Date.now()}`,
task: taskName,
description: '-',
progress: status === 'done' ? 100 : 0,
members: [],
labels: [],
status: status,
priority: 'medium',
timeTracking: 0,
estimation: '-',
startDate: new Date(),
dueDate: dueDate ? dueDate.toDate() : null,
completedDate: null,
createdDate: new Date(),
lastUpdated: new Date(),
reporter: '-',
phase: '-',
subTasks: [],
})
);
}
}
setTaskName('');
};
const handleClose = () => {
dispatch(setTaskCardDisabled({ status, position, disabled: true }));
};
return (
<div
ref={cardRef}
className={`task-card ${themeMode === 'dark' ? 'dark-mode' : ''}`}
style={{
zIndex: 99,
padding: '12px',
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
borderRadius: '4px',
marginBottom: '12px',
cursor: 'pointer',
position: 'relative',
overflow: 'hidden',
}}
>
{/* Input field */}
<div style={{ display: 'flex' }}>
<Input
ref={ref}
type="text"
maxLength={100}
onChange={handleChange}
value={taskName}
onPressEnter={handleAddTask}
placeholder="Enter task name"
/>
</div>
<div style={{ opacity: characterLength > 0 ? 1 : 0 }}>
{/* Character Length */}
<div
style={{
position: 'absolute',
zIndex: 1,
right: '15px',
top: '43px',
color: themeMode === 'dark' ? '#ffffffd9' : '#00000073',
fontSize: '10px',
}}
>
<span>{characterLength}/100</span>
</div>
{/* DatePicker and Avatars */}
<div
style={{
paddingTop: '0.25rem',
marginTop: '0.75rem',
display: 'flex',
marginBottom: '16px',
}}
>
<div style={{ height: '100%', width: '100%' }}>
<DatePicker
className={`custom-placeholder ${!dueDate ? 'create-task-empty-date' : isToday ? 'selected-date' : isTomorrow ? 'selected-date' : isItPrevDate ? 'red-colored' : ''}`}
placeholder={t('dueDate')}
style={{
fontSize: '12px',
opacity: dueDate ? 1 : 0,
}}
onChange={handleDateChange}
variant="borderless"
size="small"
suffixIcon={false}
format={value => formatDate(value)}
/>
</div>
<div style={{ marginLeft: 'auto' }}>
<div
style={{
opacity: 1,
borderRadius: '4px',
cursor: 'pointer',
alignItems: 'center',
height: '100%',
width: '100%',
display: 'flex',
gap: '3px',
}}
>
<Avatar.Group>
{/* <Avatar
style={{
backgroundColor:
avatarNamesMap[
member?.charAt(0)
],
verticalAlign: 'middle',
fontSize: '12px',
}}
size="small"
>
{member.charAt(0)}
</Avatar> */}
</Avatar.Group>
<Avatar
size="small"
style={{
backgroundColor: '#fff',
border: '1px dashed #c4c4c4',
color: '#000000d9',
fontSize: '12px',
}}
>
<AddMembersDropdown />
</Avatar>
</div>
</div>
</div>
</div>
{/* Add Task Button and Cancel Button*/}
<div>
<Button size="small" style={{ marginRight: '8px', fontSize: '12px' }} onClick={handleClose}>
{t('cancel')}
</Button>
<Button size="small" type="primary" style={{ fontSize: '12px' }} onClick={handleAddTask}>
{t('addTask')}
</Button>
</div>
</div>
);
});
export default PriorityTaskCreateCard;