expand sub tasks

This commit is contained in:
chamiakJ
2025-07-03 01:31:05 +05:30
parent 3bef18901a
commit ecd4d29a38
435 changed files with 13150 additions and 11087 deletions

View File

@@ -1,5 +1,9 @@
import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service';
import { IBillingAccountInfo, IBillingAccountStorage, IFreePlanSettings } from '@/types/admin-center/admin-center.types';
import {
IBillingAccountInfo,
IBillingAccountStorage,
IFreePlanSettings,
} from '@/types/admin-center/admin-center.types';
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
interface adminCenterState {
@@ -27,10 +31,13 @@ export const fetchBillingInfo = createAsyncThunk('adminCenter/fetchBillingInfo',
return res.body;
});
export const fetchFreePlanSettings = createAsyncThunk('adminCenter/fetchFreePlanSettings', async () => {
const res = await adminCenterApiService.getFreePlanSettings();
return res.body;
});
export const fetchFreePlanSettings = createAsyncThunk(
'adminCenter/fetchFreePlanSettings',
async () => {
const res = await adminCenterApiService.getFreePlanSettings();
return res.body;
}
);
export const fetchStorageInfo = createAsyncThunk('adminCenter/fetchStorageInfo', async () => {
const res = await adminCenterApiService.getAccountStorage();
@@ -42,10 +49,14 @@ const adminCenterSlice = createSlice({
initialState,
reducers: {
toggleRedeemCodeDrawer: state => {
state.isRedeemCodeDrawerOpen ? (state.isRedeemCodeDrawerOpen = false) : (state.isRedeemCodeDrawerOpen = true);
state.isRedeemCodeDrawerOpen
? (state.isRedeemCodeDrawerOpen = false)
: (state.isRedeemCodeDrawerOpen = true);
},
toggleUpgradeModal: state => {
state.isUpgradeModalOpen ? (state.isUpgradeModalOpen = false) : (state.isUpgradeModalOpen = true);
state.isUpgradeModalOpen
? (state.isUpgradeModalOpen = false)
: (state.isUpgradeModalOpen = true);
},
},
extraReducers: builder => {
@@ -78,6 +89,5 @@ const adminCenterSlice = createSlice({
},
});
export const { toggleRedeemCodeDrawer, toggleUpgradeModal } = adminCenterSlice.actions;
export default adminCenterSlice.reducer;

View File

@@ -407,11 +407,11 @@ const boardSlice = createSlice({
section.tasks.splice(taskIndex, 1);
return;
}
// Check if task is in subtasks
for (const parentTask of section.tasks) {
if (!parentTask.sub_tasks) continue;
const subtaskIndex = parentTask.sub_tasks.findIndex(st => st.id === taskId);
if (subtaskIndex !== -1) {
parentTask.sub_tasks.splice(subtaskIndex, 1);
@@ -430,11 +430,11 @@ const boardSlice = createSlice({
group.tasks.splice(taskIndex, 1);
return;
}
// Check subtasks
for (const parentTask of group.tasks) {
if (!parentTask.sub_tasks) continue;
const subtaskIndex = parentTask.sub_tasks.findIndex(st => st.id === taskId);
if (subtaskIndex !== -1) {
parentTask.sub_tasks.splice(subtaskIndex, 1);
@@ -477,7 +477,7 @@ const boardSlice = createSlice({
// If not found in main tasks, look in subtasks
for (const parentTask of section.tasks) {
if (!parentTask.sub_tasks) continue;
const subtask = parentTask.sub_tasks.find(st => st.id === taskId);
if (subtask) {
subtask.assignees = body.assignees;
@@ -813,7 +813,8 @@ const boardSlice = createSlice({
state.taskGroups = action.payload && action.payload.groups ? action.payload.groups : [];
state.allTasks = action.payload && action.payload.allTasks ? action.payload.allTasks : [];
state.grouping = action.payload && action.payload.grouping ? action.payload.grouping : '';
state.totalTasks = action.payload && action.payload.totalTasks ? action.payload.totalTasks : 0;
state.totalTasks =
action.payload && action.payload.totalTasks ? action.payload.totalTasks : 0;
})
.addCase(fetchBoardTaskGroups.rejected, (state, action) => {
state.loadingGroups = false;

View File

@@ -181,7 +181,9 @@ export const fetchEnhancedKanbanGroups = createAsyncThunk(
id: projectId,
archived: enhancedKanbanReducer.archived,
group: enhancedKanbanReducer.groupBy,
field: enhancedKanbanReducer.fields.map(field => `${field.key} ${field.sort_order}`).join(','),
field: enhancedKanbanReducer.fields
.map(field => `${field.key} ${field.sort_order}`)
.join(','),
order: '',
search: enhancedKanbanReducer.search || '',
statuses: '',
@@ -373,8 +375,6 @@ const deleteTaskFromGroup = (
}
};
const enhancedKanbanSlice = createSlice({
name: 'enhancedKanbanReducer',
initialState,
@@ -415,7 +415,7 @@ const enhancedKanbanSlice = createSlice({
state.selectedTaskIds = state.selectedTaskIds.filter(id => id !== action.payload);
},
clearSelection: (state) => {
clearSelection: state => {
state.selectedTaskIds = [];
},
@@ -443,7 +443,7 @@ const enhancedKanbanSlice = createSlice({
state.groupCache[action.payload.id] = action.payload.group;
},
clearCaches: (state) => {
clearCaches: state => {
state.taskCache = {};
state.groupCache = {};
},
@@ -514,8 +514,18 @@ const enhancedKanbanSlice = createSlice({
},
// Enhanced Kanban external status update (for use in task drawer dropdown)
updateEnhancedKanbanTaskStatus: (state, action: PayloadAction<ITaskListStatusChangeResponse>) => {
const { id: task_id, status_id, color_code, color_code_dark, complete_ratio, statusCategory } = action.payload;
updateEnhancedKanbanTaskStatus: (
state,
action: PayloadAction<ITaskListStatusChangeResponse>
) => {
const {
id: task_id,
status_id,
color_code,
color_code_dark,
complete_ratio,
statusCategory,
} = action.payload;
let oldGroupId: string | null = null;
let foundTask: IProjectTask | null = null;
// Find the task and its group
@@ -528,14 +538,14 @@ const enhancedKanbanSlice = createSlice({
}
}
if (!foundTask) return;
// Update the task properties
foundTask.status_color = color_code;
foundTask.status_color_dark = color_code_dark;
foundTask.complete_ratio = +complete_ratio;
foundTask.status = status_id;
foundTask.status_category = statusCategory;
// If grouped by status and the group changes, move the task
if (state.groupBy === IGroupBy.STATUS && oldGroupId && oldGroupId !== status_id) {
// Remove from old group
@@ -558,7 +568,10 @@ const enhancedKanbanSlice = createSlice({
},
// Enhanced Kanban priority update (for use in task drawer dropdown)
updateEnhancedKanbanTaskPriority: (state, action: PayloadAction<ITaskListPriorityChangeResponse>) => {
updateEnhancedKanbanTaskPriority: (
state,
action: PayloadAction<ITaskListPriorityChangeResponse>
) => {
const { id, priority_id, color_code, color_code_dark } = action.payload;
// Find the task in any group
@@ -573,11 +586,7 @@ const enhancedKanbanSlice = createSlice({
task.priority_color_dark = color_code_dark;
// If grouped by priority and not a subtask, move the task to the new priority group
if (
state.groupBy === IGroupBy.PRIORITY &&
!task.is_sub_task &&
groupId !== priority_id
) {
if (state.groupBy === IGroupBy.PRIORITY && !task.is_sub_task && groupId !== priority_id) {
// Remove from current group
deleteTaskFromGroup(state.taskGroups, task, groupId);
@@ -588,15 +597,18 @@ const enhancedKanbanSlice = createSlice({
state.groupCache[priority_id] = newGroup;
}
}
// Update cache
state.taskCache[id] = task;
},
// Enhanced Kanban assignee update (for use in task drawer dropdown)
updateEnhancedKanbanTaskAssignees: (state, action: PayloadAction<ITaskAssigneesUpdateResponse>) => {
updateEnhancedKanbanTaskAssignees: (
state,
action: PayloadAction<ITaskAssigneesUpdateResponse>
) => {
const { id, assignees, names } = action.payload;
// Find the task in any group
const taskInfo = findTaskInAllGroups(state.taskGroups, id);
if (!taskInfo) return;
@@ -606,7 +618,7 @@ const enhancedKanbanSlice = createSlice({
// Update the task properties
task.assignees = assignees as ITaskAssignee[];
task.names = names as InlineMember[];
// Update cache
state.taskCache[id] = task;
},
@@ -642,7 +654,8 @@ const enhancedKanbanSlice = createSlice({
parent_task: string;
}>
) => {
const { id, complete_ratio, completed_count, total_tasks_count, parent_task } = action.payload;
const { id, complete_ratio, completed_count, total_tasks_count, parent_task } =
action.payload;
// Find the task in any group
const taskInfo = findTaskInAllGroups(state.taskGroups, parent_task || id);
@@ -656,7 +669,7 @@ const enhancedKanbanSlice = createSlice({
task.complete_ratio = +complete_ratio;
task.completed_count = completed_count;
task.total_tasks_count = total_tasks_count;
// Update cache
state.taskCache[parent_task || id] = task;
},
@@ -709,20 +722,23 @@ const enhancedKanbanSlice = createSlice({
},
// Reset state
resetState: (state) => {
resetState: state => {
return { ...initialState, groupBy: state.groupBy };
},
// Synchronous reorder for tasks
reorderTasks: (state, action: PayloadAction<{
activeGroupId: string;
overGroupId: string;
fromIndex: number;
toIndex: number;
task: IProjectTask;
updatedSourceTasks: IProjectTask[];
updatedTargetTasks: IProjectTask[];
}>) => {
reorderTasks: (
state,
action: PayloadAction<{
activeGroupId: string;
overGroupId: string;
fromIndex: number;
toIndex: number;
task: IProjectTask;
updatedSourceTasks: IProjectTask[];
updatedTargetTasks: IProjectTask[];
}>
) => {
const { activeGroupId, overGroupId, updatedSourceTasks, updatedTargetTasks } = action.payload;
const sourceGroupIndex = state.taskGroups.findIndex(group => group.id === activeGroupId);
const targetGroupIndex = state.taskGroups.findIndex(group => group.id === overGroupId);
@@ -737,17 +753,23 @@ const enhancedKanbanSlice = createSlice({
},
// Synchronous reorder for groups
reorderGroups: (state, action: PayloadAction<{
fromIndex: number;
toIndex: number;
reorderedGroups: ITaskListGroup[];
}>) => {
reorderGroups: (
state,
action: PayloadAction<{
fromIndex: number;
toIndex: number;
reorderedGroups: ITaskListGroup[];
}>
) => {
const { reorderedGroups } = action.payload;
state.taskGroups = reorderedGroups;
state.groupCache = reorderedGroups.reduce((cache, group) => {
cache[group.id] = group;
return cache;
}, {} as Record<string, ITaskListGroup>);
state.groupCache = reorderedGroups.reduce(
(cache, group) => {
cache[group.id] = group;
return cache;
},
{} as Record<string, ITaskListGroup>
);
state.columnOrder = reorderedGroups.map(group => group.id);
},
@@ -844,7 +866,7 @@ const enhancedKanbanSlice = createSlice({
task.sub_tasks = task.sub_tasks.filter(t => t.id !== subtask.id);
task.sub_tasks_count = Math.max(0, (task.sub_tasks_count || 1) - 1);
}
// Update cache
state.taskCache[task.id!] = task;
return true;
@@ -872,9 +894,9 @@ const enhancedKanbanSlice = createSlice({
}
},
},
extraReducers: (builder) => {
extraReducers: builder => {
builder
.addCase(fetchEnhancedKanbanGroups.pending, (state) => {
.addCase(fetchEnhancedKanbanGroups.pending, state => {
state.loadingGroups = true;
state.error = null;
})
@@ -902,7 +924,7 @@ const enhancedKanbanSlice = createSlice({
if (task.sub_tasks_count === undefined) {
task.sub_tasks_count = 0;
}
state.taskCache[task.id!] = task;
});
});
@@ -917,7 +939,8 @@ const enhancedKanbanSlice = createSlice({
state.error = action.payload as string;
})
.addCase(reorderEnhancedKanbanTasks.fulfilled, (state, action) => {
const { activeGroupId, overGroupId, updatedSourceTasks, updatedTargetTasks } = action.payload;
const { activeGroupId, overGroupId, updatedSourceTasks, updatedTargetTasks } =
action.payload;
// Update groups
const sourceGroupIndex = state.taskGroups.findIndex(group => group.id === activeGroupId);
@@ -938,16 +961,19 @@ const enhancedKanbanSlice = createSlice({
// Update groups
state.taskGroups = reorderedGroups;
state.groupCache = reorderedGroups.reduce((cache, group) => {
cache[group.id] = group;
return cache;
}, {} as Record<string, ITaskListGroup>);
state.groupCache = reorderedGroups.reduce(
(cache, group) => {
cache[group.id] = group;
return cache;
},
{} as Record<string, ITaskListGroup>
);
// Update column order
state.columnOrder = reorderedGroups.map(group => group.id);
})
// Fetch Task Assignees
.addCase(fetchEnhancedKanbanTaskAssignees.pending, (state) => {
.addCase(fetchEnhancedKanbanTaskAssignees.pending, state => {
state.loadingAssignees = true;
state.error = null;
})
@@ -962,7 +988,7 @@ const enhancedKanbanSlice = createSlice({
state.error = action.payload as string;
})
// Fetch Labels
.addCase(fetchEnhancedKanbanLabels.pending, (state) => {
.addCase(fetchEnhancedKanbanLabels.pending, state => {
state.loadingLabels = true;
state.error = null;
})
@@ -991,18 +1017,18 @@ const enhancedKanbanSlice = createSlice({
.addCase(fetchBoardSubTasks.fulfilled, (state, action: PayloadAction<IProjectTask[]>) => {
const taskId = (action as any).meta?.arg?.taskId;
const result = findTaskInAllGroups(state.taskGroups, taskId);
if (result) {
result.task.sub_tasks_loading = false;
result.task.sub_tasks = action.payload;
result.task.show_sub_tasks = true;
// Only update the count if we don't have a count yet or if the API returned a different count
// This preserves the original count from the initial data load
if (!result.task.sub_tasks_count || result.task.sub_tasks_count === 0) {
result.task.sub_tasks_count = action.payload.length;
}
// Update cache
state.taskCache[taskId] = result.task;
}
@@ -1063,4 +1089,4 @@ export const {
toggleTaskExpansion,
} = enhancedKanbanSlice.actions;
export default enhancedKanbanSlice.reducer;
export default enhancedKanbanSlice.reducer;

View File

@@ -35,15 +35,18 @@ const Navbar = () => {
const authService = useAuthService();
const [navRoutesList, setNavRoutesList] = useState<NavRoutesType[]>(navRoutes);
const [isOwnerOrAdmin, setIsOwnerOrAdmin] = useState<boolean>(authService.isOwnerOrAdmin());
const showUpgradeTypes = [ISUBSCRIPTION_TYPE.TRIAL]
const showUpgradeTypes = [ISUBSCRIPTION_TYPE.TRIAL];
useEffect(() => {
authApiService.verify().then(authorizeResponse => {
authApiService
.verify()
.then(authorizeResponse => {
if (authorizeResponse.authenticated) {
authService.setCurrentSession(authorizeResponse.user);
setIsOwnerOrAdmin(!!(authorizeResponse.user.is_admin || authorizeResponse.user.owner));
}
}).catch(error => {
})
.catch(error => {
logger.error('Error during authorization', error);
});
}, []);
@@ -67,9 +70,13 @@ const Navbar = () => {
() =>
navRoutesList
.filter(route => {
if (!route.freePlanFeature && currentSession?.subscription_type === ISUBSCRIPTION_TYPE.FREE) return false;
if (route.adminOnly && !isOwnerOrAdmin) return false;
if (
!route.freePlanFeature &&
currentSession?.subscription_type === ISUBSCRIPTION_TYPE.FREE
)
return false;
if (route.adminOnly && !isOwnerOrAdmin) return false;
return true;
})
.map((route, index) => ({
@@ -91,7 +98,6 @@ const Navbar = () => {
}, [location]);
return (
<Col
style={{
width: '100%',
@@ -139,9 +145,10 @@ const Navbar = () => {
<ConfigProvider wave={{ disabled: true }}>
{isDesktop && (
<Flex gap={20} align="center">
{isOwnerOrAdmin && showUpgradeTypes.includes(currentSession?.subscription_type as ISUBSCRIPTION_TYPE) && (
<UpgradePlanButton />
)}
{isOwnerOrAdmin &&
showUpgradeTypes.includes(
currentSession?.subscription_type as ISUBSCRIPTION_TYPE
) && <UpgradePlanButton />}
{isOwnerOrAdmin && <InviteButton />}
<Flex align="center">
<SwitchTeamButton />

View File

@@ -33,9 +33,9 @@ const TimerButton = () => {
try {
setLoading(true);
setError(null);
const response = await taskTimeLogsApiService.getRunningTimers();
if (response && response.done) {
const timers = Array.isArray(response.body) ? response.body : [];
setRunningTimers(timers);
@@ -54,24 +54,25 @@ const TimerButton = () => {
const updateCurrentTimes = useCallback(() => {
try {
if (!Array.isArray(runningTimers) || runningTimers.length === 0) return;
const newTimes: Record<string, string> = {};
runningTimers.forEach(timer => {
try {
if (!timer || !timer.task_id || !timer.start_time) return;
const startTime = moment(timer.start_time);
if (!startTime.isValid()) {
logError(`Invalid start time for timer ${timer.task_id}: ${timer.start_time}`);
return;
}
const now = moment();
const duration = moment.duration(now.diff(startTime));
const hours = Math.floor(duration.asHours());
const minutes = duration.minutes();
const seconds = duration.seconds();
newTimes[timer.task_id] = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
newTimes[timer.task_id] =
`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
} catch (error) {
logError(`Error updating time for timer ${timer?.task_id}`, error);
}
@@ -84,12 +85,12 @@ const TimerButton = () => {
useEffect(() => {
fetchRunningTimers();
// Set up polling to refresh timers every 30 seconds
const pollInterval = setInterval(() => {
fetchRunningTimers();
}, 30000);
return () => clearInterval(pollInterval);
}, [fetchRunningTimers]);
@@ -175,12 +176,12 @@ const TimerButton = () => {
logError('Socket not available for stopping timer');
return;
}
if (!taskId) {
logError('Invalid task ID for stopping timer');
return;
}
try {
socket.emit(SocketEvents.TASK_TIMER_STOP.toString(), JSON.stringify({ task_id: taskId }));
dispatch(updateTaskTimeTracking({ taskId, timeTracking: null }));
@@ -200,15 +201,15 @@ const TimerButton = () => {
}
return (
<div
style={{
width: 350,
maxHeight: 400,
<div
style={{
width: 350,
maxHeight: 400,
overflow: 'auto',
backgroundColor: token.colorBgElevated,
borderRadius: token.borderRadius,
boxShadow: token.boxShadowSecondary,
border: `1px solid ${token.colorBorderSecondary}`
border: `1px solid ${token.colorBorderSecondary}`,
}}
>
{!Array.isArray(runningTimers) || runningTimers.length === 0 ? (
@@ -218,15 +219,15 @@ const TimerButton = () => {
) : (
<List
dataSource={runningTimers}
renderItem={(timer) => {
renderItem={timer => {
if (!timer || !timer.task_id) return null;
return (
<List.Item
style={{
<List.Item
style={{
padding: '12px 16px',
borderBottom: `1px solid ${token.colorBorderSecondary}`,
backgroundColor: 'transparent'
backgroundColor: 'transparent',
}}
>
<div style={{ width: '100%' }}>
@@ -234,16 +235,18 @@ const TimerButton = () => {
<Text strong style={{ fontSize: 14, color: token.colorText }}>
{timer.task_name || 'Unnamed Task'}
</Text>
<div style={{
display: 'inline-block',
backgroundColor: token.colorPrimaryBg,
color: token.colorPrimary,
padding: '2px 8px',
borderRadius: token.borderRadiusSM,
fontSize: 11,
fontWeight: 500,
marginTop: 2
}}>
<div
style={{
display: 'inline-block',
backgroundColor: token.colorPrimaryBg,
color: token.colorPrimary,
padding: '2px 8px',
borderRadius: token.borderRadiusSM,
fontSize: 11,
fontWeight: 500,
marginTop: 2,
}}
>
{timer.project_name || 'Unnamed Project'}
</div>
{timer.parent_task_name && (
@@ -251,18 +254,34 @@ const TimerButton = () => {
Parent: {timer.parent_task_name}
</Text>
)}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 4,
}}
>
<Text type="secondary" style={{ fontSize: 11 }}>
Started: {timer.start_time ? moment(timer.start_time).format('HH:mm') : '--:--'}
Started:{' '}
{timer.start_time
? moment(timer.start_time).format('HH:mm')
: '--:--'}
</Text>
<Text
strong
style={{
fontSize: 14,
<Text
strong
style={{
fontSize: 14,
color: token.colorPrimary,
fontFamily: 'monospace'
fontFamily: 'monospace',
}}
>
{currentTimes[timer.task_id] || '00:00:00'}
@@ -272,7 +291,7 @@ const TimerButton = () => {
<Button
size="small"
icon={<StopOutlined />}
onClick={(e) => {
onClick={e => {
e.stopPropagation();
handleStopTimer(timer.task_id);
}}
@@ -280,7 +299,7 @@ const TimerButton = () => {
backgroundColor: token.colorErrorBg,
borderColor: token.colorError,
color: token.colorError,
fontWeight: 500
fontWeight: 500,
}}
>
Stop
@@ -296,13 +315,13 @@ const TimerButton = () => {
{hasRunningTimers() && (
<>
<Divider style={{ margin: 0, borderColor: token.colorBorderSecondary }} />
<div
style={{
padding: '8px 16px',
textAlign: 'center',
<div
style={{
padding: '8px 16px',
textAlign: 'center',
backgroundColor: token.colorFillQuaternary,
borderBottomLeftRadius: token.borderRadius,
borderBottomRightRadius: token.borderRadius
borderBottomRightRadius: token.borderRadius,
}}
>
<Text type="secondary" style={{ fontSize: 11 }}>
@@ -376,4 +395,4 @@ const TimerButton = () => {
}
};
export default TimerButton;
export default TimerButton;

View File

@@ -23,7 +23,7 @@ const ProfileButton = ({ isOwnerOrAdmin }: ProfileButtonProps) => {
const { t } = useTranslation('navbar');
const authService = useAuthService();
const currentSession = useAppSelector((state: RootState) => state.userReducer);
const role = getRole();
const themeMode = useAppSelector((state: RootState) => state.themeReducer.mode);
@@ -49,13 +49,24 @@ const ProfileButton = ({ isOwnerOrAdmin }: ProfileButtonProps) => {
<Flex vertical style={{ flex: 1, minWidth: 0 }}>
<Typography.Text
ellipsis={{ tooltip: currentSession?.name }} // Show tooltip on hover
style={{ width: '100%', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
style={{
width: '100%',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{currentSession?.name}
</Typography.Text>
<Typography.Text
ellipsis={{ tooltip: currentSession?.email }} // Show tooltip on hover
style={{ fontSize: 12, width: '100%', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
style={{
fontSize: 12,
width: '100%',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{currentSession?.email}
</Typography.Text>

View File

@@ -16,7 +16,6 @@ const initialState: IProjectDrawerState = {
project: null,
};
export const fetchProjectData = createAsyncThunk(
'project/fetchProjectData',
async (projectId: string, { rejectWithValue, dispatch }) => {

View File

@@ -11,12 +11,12 @@ const LOCAL_STORAGE_KEY = 'project_view_preferences';
const loadInitialState = (): ProjectViewState => {
const saved = localStorage.getItem(LOCAL_STORAGE_KEY);
return saved
return saved
? JSON.parse(saved)
: {
mode: ProjectViewType.LIST,
groupBy: ProjectGroupBy.CATEGORY,
lastUpdated: new Date().toISOString()
lastUpdated: new Date().toISOString(),
};
};
@@ -39,9 +39,9 @@ export const projectViewSlice = createSlice({
resetViewState: () => {
localStorage.removeItem(LOCAL_STORAGE_KEY);
return loadInitialState();
}
}
},
},
});
export const { setViewMode, setGroupBy, resetViewState } = projectViewSlice.actions;
export default projectViewSlice.reducer;
export default projectViewSlice.reducer;

View File

@@ -170,7 +170,7 @@ const projectSlice = createSlice({
}
},
reset: () => initialState,
setRefreshTimestamp: (state) => {
setRefreshTimestamp: state => {
state.refreshTimestamp = new Date().getTime().toString();
},
setProjectView: (state, action: PayloadAction<'list' | 'kanban'>) => {
@@ -214,7 +214,7 @@ export const {
setCreateTaskTemplateDrawerOpen,
setProjectView,
updatePhaseLabel,
setRefreshTimestamp
setRefreshTimestamp,
} = projectSlice.actions;
export default projectSlice.reducer;

View File

@@ -39,4 +39,4 @@ export const updateProject = (payload: UpdateProjectPayload) => async (dispatch:
}
};
export default projectsSlice.reducer;
export default projectsSlice.reducer;

View File

@@ -267,7 +267,10 @@ const projectSlice = createSlice({
...action.payload,
};
},
setGroupedRequestParams: (state, action: PayloadAction<Partial<ProjectState['groupedRequestParams']>>) => {
setGroupedRequestParams: (
state,
action: PayloadAction<Partial<ProjectState['groupedRequestParams']>>
) => {
state.groupedRequestParams = {
...state.groupedRequestParams,
...action.payload,

View File

@@ -75,7 +75,7 @@ const PhaseDrawer = () => {
const handleAddOptions = async () => {
if (!projectId) return;
await dispatch(addPhaseOption({ projectId: projectId }));
await dispatch(fetchPhasesByProjectId(projectId));
await refreshTasks();

View File

@@ -2,7 +2,12 @@ import { Button, ColorPicker, ConfigProvider, Flex, Input } from 'antd';
import { CloseCircleOutlined, HolderOutlined } from '@ant-design/icons';
import { nanoid } from '@reduxjs/toolkit';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { deletePhaseOption, fetchPhasesByProjectId, updatePhaseColor, updatePhaseName } from './phases.slice';
import {
deletePhaseOption,
fetchPhasesByProjectId,
updatePhaseColor,
updatePhaseName,
} from './phases.slice';
import { PhaseColorCodes } from '@/shared/constants';
import { ITaskPhase } from '@/types/tasks/taskPhase.types';
import { TFunction } from 'i18next';
@@ -27,7 +32,7 @@ const PhaseOptionItem = ({ option, projectId, t }: PhaseOptionItemProps) => {
const { projectView } = useTabSearchParam();
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
id: option?.id || 'temp-id'
id: option?.id || 'temp-id',
});
const style = {
@@ -50,14 +55,16 @@ const PhaseOptionItem = ({ option, projectId, t }: PhaseOptionItemProps) => {
const handlePhaseNameChange = async (e: React.FocusEvent<HTMLInputElement>) => {
if (!projectId || !option || phaseName.trim() === option.name.trim()) return;
try {
const updatedPhase = { ...option, name: phaseName.trim() };
const response = await dispatch(updatePhaseName({
phaseId: option.id,
phase: updatedPhase,
projectId
})).unwrap();
const response = await dispatch(
updatePhaseName({
phaseId: option.id,
phase: updatedPhase,
projectId,
})
).unwrap();
if (response.done) {
dispatch(fetchPhasesByProjectId(projectId));
@@ -71,7 +78,7 @@ const PhaseOptionItem = ({ option, projectId, t }: PhaseOptionItemProps) => {
const handleDeletePhaseOption = async () => {
if (!option?.id || !projectId) return;
try {
const response = await dispatch(
deletePhaseOption({ phaseOptionId: option.id, projectId })
@@ -88,7 +95,7 @@ const PhaseOptionItem = ({ option, projectId, t }: PhaseOptionItemProps) => {
const handleColorChange = async () => {
if (!projectId || !option) return;
try {
const updatedPhase = { ...option, color_code: color };
const response = await dispatch(updatePhaseColor({ projectId, body: updatedPhase })).unwrap();
@@ -112,13 +119,13 @@ const PhaseOptionItem = ({ option, projectId, t }: PhaseOptionItemProps) => {
<Input
type="text"
value={phaseName}
onChange={(e) => setPhaseName(e.target.value)}
onChange={e => setPhaseName(e.target.value)}
onBlur={handlePhaseNameChange}
onPressEnter={(e) => e.currentTarget.blur()}
onPressEnter={e => e.currentTarget.blur()}
placeholder={t('enterPhaseName')}
/>
<ColorPicker
onChange={(value) => setColor(value.toHexString())}
onChange={value => setColor(value.toHexString())}
onChangeComplete={handleColorChange}
value={color}
/>

View File

@@ -40,7 +40,10 @@ export const fetchPhasesByProjectId = createAsyncThunk(
export const deletePhaseOption = createAsyncThunk(
'phase/deletePhaseOption',
async ({ phaseOptionId, projectId }: { phaseOptionId: string; projectId: string }, { rejectWithValue }) => {
async (
{ phaseOptionId, projectId }: { phaseOptionId: string; projectId: string },
{ rejectWithValue }
) => {
try {
const response = await phasesApiService.deletePhaseOption(phaseOptionId, projectId);
return response;
@@ -64,14 +67,17 @@ export const updatePhaseColor = createAsyncThunk(
export const updatePhaseOrder = createAsyncThunk(
'phases/updatePhaseOrder',
async ({ projectId, body }: {
projectId: string,
body: {
async ({
projectId,
body,
}: {
projectId: string;
body: {
from_index: number;
to_index: number;
phases: ITaskPhase[];
project_id: string;
}
};
}) => {
try {
const response = await phasesApiService.updatePhaseOrder(projectId, body);
@@ -84,7 +90,10 @@ export const updatePhaseOrder = createAsyncThunk(
export const updateProjectPhaseLabel = createAsyncThunk(
'phase/updateProjectPhaseLabel',
async ({ projectId, phaseLabel }: { projectId: string; phaseLabel: string }, { rejectWithValue }) => {
async (
{ projectId, phaseLabel }: { projectId: string; phaseLabel: string },
{ rejectWithValue }
) => {
try {
const response = await phasesApiService.updateProjectPhaseLabel(projectId, phaseLabel);
return response;
@@ -96,7 +105,10 @@ export const updateProjectPhaseLabel = createAsyncThunk(
export const updatePhaseName = createAsyncThunk(
'phase/updatePhaseName',
async ({ phaseId, phase, projectId }: { phaseId: string; phase: ITaskPhase; projectId: string }, { rejectWithValue }) => {
async (
{ phaseId, phase, projectId }: { phaseId: string; phase: ITaskPhase; projectId: string },
{ rejectWithValue }
) => {
try {
const response = await phasesApiService.updateNameOfPhase(phaseId, phase, projectId);
return response;
@@ -127,13 +139,13 @@ const phaseSlice = createSlice({
builder.addCase(fetchPhasesByProjectId.rejected, state => {
state.loadingPhases = false;
});
builder.addCase(updatePhaseOrder.pending, (state) => {
builder.addCase(updatePhaseOrder.pending, state => {
state.loadingPhases = true;
});
builder.addCase(updatePhaseOrder.fulfilled, (state, action) => {
state.loadingPhases = false;
});
builder.addCase(updatePhaseOrder.rejected, (state) => {
builder.addCase(updatePhaseOrder.rejected, state => {
state.loadingPhases = false;
});
},

View File

@@ -21,7 +21,7 @@ type TaskListCustomColumnsState = {
isCustomColumnModalOpen: boolean;
customColumnModalType: 'create' | 'edit';
customColumnId: string | null;
customFieldType: CustomFieldsTypes;
customFieldNumberType: CustomFieldNumberTypes;
decimals: number;
@@ -60,7 +60,10 @@ const taskListCustomColumnsSlice = createSlice({
toggleCustomColumnModalOpen: (state, action: PayloadAction<boolean>) => {
state.isCustomColumnModalOpen = action.payload;
},
setCustomColumnModalAttributes: (state, action: PayloadAction<{modalType: 'create' | 'edit', columnId: string | null}>) => {
setCustomColumnModalAttributes: (
state,
action: PayloadAction<{ modalType: 'create' | 'edit'; columnId: string | null }>
) => {
state.customColumnModalType = action.payload.modalType;
state.customColumnId = action.payload.columnId;
},

View File

@@ -24,7 +24,10 @@ const deleteStatusSlice = createSlice({
deleteStatusToggleDrawer: state => {
state.isDeleteStatusDrawerOpen = !state.isDeleteStatusDrawerOpen;
},
seletedStatusCategory: (state, action: PayloadAction<{ id: string; name: string; category_id: string; message: string}>) => {
seletedStatusCategory: (
state,
action: PayloadAction<{ id: string; name: string; category_id: string; message: string }>
) => {
state.status = action.payload;
},
// deleteStatus: (state, action: PayloadAction<string>) => {
@@ -33,9 +36,9 @@ const deleteStatusSlice = createSlice({
},
});
export const {
deleteStatusToggleDrawer,
seletedStatusCategory,
// deleteStatus
export const {
deleteStatusToggleDrawer,
seletedStatusCategory,
// deleteStatus
} = deleteStatusSlice.actions;
export default deleteStatusSlice.reducer;

View File

@@ -13,7 +13,11 @@ import {
Typography,
} from 'antd';
import React, { useRef, useState } from 'react';
import { healthStatusData, projectColors, statusData } from '../../../lib/project/project-constants';
import {
healthStatusData,
projectColors,
statusData,
} from '../../../lib/project/project-constants';
import { PlusCircleOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { colors } from '../../../styles/colors';
import { useAppSelector } from '@/hooks/useAppSelector';

View File

@@ -3,8 +3,15 @@ import { simpleDateFormat } from '@/utils/simpleDateFormat';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { colors } from '../../../../../styles/colors';
import { useTranslation } from 'react-i18next';
import { fetchTask, setSelectedTaskId, setShowTaskDrawer } from '@/features/task-drawer/task-drawer.slice';
import { ISingleMemberActivityLog, ISingleMemberActivityLogs } from '@/types/reporting/reporting.types';
import {
fetchTask,
setSelectedTaskId,
setShowTaskDrawer,
} from '@/features/task-drawer/task-drawer.slice';
import {
ISingleMemberActivityLog,
ISingleMemberActivityLogs,
} from '@/types/reporting/reporting.types';
import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/phases.slice';
type TaskStatus = {

View File

@@ -28,7 +28,7 @@ const MembersReportsOverviewTab = ({ memberId }: MembersReportsOverviewTabProps)
teamMemberId: memberId,
duration: duration,
date_range: dateRange,
archived
archived,
};
const response = await reportingApiService.getMemberInfo(body);
if (response.done) {

View File

@@ -44,7 +44,7 @@ const MembersOverviewProjectsStatsDrawer = ({
archived: false,
};
const response = await reportingApiService.getSingleMemberProjects(body);
if (response.done){
if (response.done) {
setProjectsData(response.body.projects || []);
} else {
setProjectsData([]);
@@ -74,12 +74,9 @@ const MembersOverviewProjectsStatsDrawer = ({
)
}
>
<MembersOverviewProjectsStatsTable
projectList={projectsData}
loading={loading}
/>
<MembersOverviewProjectsStatsTable projectList={projectsData} loading={loading} />
</Drawer>
);
};
export default MembersOverviewProjectsStatsDrawer;
export default MembersOverviewProjectsStatsDrawer;

View File

@@ -78,19 +78,19 @@ const MembersOverviewProjectsStatsTable = ({ projectList, loading }: ProjectRepo
key: 'status',
title: <CustomTableTitle title={t('statusColumn')} />,
// render: record => {
// const statusItem = statusData.find(item => item.label === record.status_name);
// const statusItem = statusData.find(item => item.label === record.status_name);
// return statusItem ? (
// <Typography.Text
// style={{ display: 'flex', alignItems: 'center', gap: 4 }}
// className="group-hover:text-[#1890ff]"
// >
// {statusItem.icon}
// {t(`${statusItem.value}Text`)}
// </Typography.Text>
// ) : (
// <Typography.Text>-</Typography.Text>
// );
// return statusItem ? (
// <Typography.Text
// style={{ display: 'flex', alignItems: 'center', gap: 4 }}
// className="group-hover:text-[#1890ff]"
// >
// {statusItem.icon}
// {t(`${statusItem.value}Text`)}
// </Typography.Text>
// ) : (
// <Typography.Text>-</Typography.Text>
// );
// },
width: 120,
},
@@ -156,29 +156,29 @@ const MembersOverviewProjectsStatsTable = ({ projectList, loading }: ProjectRepo
return (
<ConfigProvider
theme={{
components: {
Table: {
cellPaddingBlock: 8,
cellPaddingInline: 8,
components: {
Table: {
cellPaddingBlock: 8,
cellPaddingInline: 8,
},
},
},
}}
>
{loading ? (
<Skeleton style={{ paddingTop: 16 }} />
<Skeleton style={{ paddingTop: 16 }} />
) : (
<Table
columns={columns}
dataSource={projectList}
pagination={{ showSizeChanger: true, defaultPageSize: 10 }}
scroll={{ x: 'max-content' }}
onRow={record => {
return {
style: { height: 38, cursor: 'pointer' },
className: 'group even:bg-[#4e4e4e10]',
};
}}
/>
<Table
columns={columns}
dataSource={projectList}
pagination={{ showSizeChanger: true, defaultPageSize: 10 }}
scroll={{ x: 'max-content' }}
onRow={record => {
return {
style: { height: 38, cursor: 'pointer' },
className: 'group even:bg-[#4e4e4e10]',
};
}}
/>
)}
</ConfigProvider>
);

View File

@@ -13,10 +13,7 @@ type MembersReportsTasksTableProps = {
loading: boolean;
};
const MembersReportsTasksTable = ({
tasksData,
loading,
}: MembersReportsTasksTableProps) => {
const MembersReportsTasksTable = ({ tasksData, loading }: MembersReportsTasksTableProps) => {
// localization
const { t } = useTranslation('reporting-members-drawer');

View File

@@ -42,13 +42,16 @@ const BillableFilter = ({ billable, onBillableChange }: BillableFilterProps) =>
}}
>
<Space>
<Checkbox
id={item.key}
checked={billable[item.key as keyof typeof billable]}
onChange={() => onBillableChange({
...billable,
[item.key as keyof typeof billable]: !billable[item.key as keyof typeof billable]
})}
<Checkbox
id={item.key}
checked={billable[item.key as keyof typeof billable]}
onChange={() =>
onBillableChange({
...billable,
[item.key as keyof typeof billable]:
!billable[item.key as keyof typeof billable],
})
}
/>
{t(`${item.key}Text`)}
</Space>

View File

@@ -2,7 +2,11 @@ import { Card, ConfigProvider, Tag, Timeline, Typography } from 'antd';
import { simpleDateFormat } from '@/utils/simpleDateFormat';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useTranslation } from 'react-i18next';
import { fetchTask, setSelectedTaskId, setShowTaskDrawer } from '@/features/task-drawer/task-drawer.slice';
import {
fetchTask,
setSelectedTaskId,
setShowTaskDrawer,
} from '@/features/task-drawer/task-drawer.slice';
import { ISingleMemberLogs } from '@/types/reporting/reporting.types';
import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/phases.slice';

View File

@@ -4,7 +4,12 @@ import { IProjectCategory } from '@/types/project/projectCategory.types';
import { IProjectHealth } from '@/types/project/projectHealth.types';
import { IProjectManager } from '@/types/project/projectManager.types';
import { IProjectStatus } from '@/types/project/projectStatus.types';
import { IGetProjectsRequestBody, IRPTOverviewProject, IRPTOverviewProjectMember, IRPTProject } from '@/types/reporting/reporting.types';
import {
IGetProjectsRequestBody,
IRPTOverviewProject,
IRPTOverviewProjectMember,
IRPTProject,
} from '@/types/reporting/reporting.types';
import { getFromLocalStorage } from '@/utils/localStorageFunctions';
import { createAsyncThunk, createSlice, createAction } from '@reduxjs/toolkit';
@@ -75,7 +80,7 @@ const initialState: ProjectReportsState = {
isProjectReportsMembersTaskDrawerOpen: false,
selectedMember: null,
selectedProject: null,
selectedProject: null,
projectList: [],
total: 0,
@@ -226,7 +231,7 @@ const projectReportsSlice = createSlice({
.addCase(updateProjectCategory, (state, action) => {
const { projectId, category } = action.payload;
const projectIndex = state.projectList.findIndex(project => project.id === projectId);
if (projectIndex !== -1) {
state.projectList[projectIndex].category_id = category.id || null;
state.projectList[projectIndex].category_name = category.name ?? '';
@@ -236,7 +241,7 @@ const projectReportsSlice = createSlice({
.addCase(updateProjectStatus, (state, action) => {
const { projectId, status } = action.payload;
const projectIndex = state.projectList.findIndex(project => project.id === projectId);
if (projectIndex !== -1) {
state.projectList[projectIndex].status_id = status.id || '';
state.projectList[projectIndex].status_name = status.name ?? '';

View File

@@ -1,5 +1,9 @@
import { PROJECT_LIST_COLUMNS } from '@/shared/constants';
import { getJSONFromLocalStorage, saveJSONToLocalStorage, saveToLocalStorage } from '@/utils/localStorageFunctions';
import {
getJSONFromLocalStorage,
saveJSONToLocalStorage,
saveToLocalStorage,
} from '@/utils/localStorageFunctions';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
type ColumnsVisibilityState = {
@@ -8,21 +12,23 @@ type ColumnsVisibilityState = {
const getInitialState = () => {
const savedState = getJSONFromLocalStorage(PROJECT_LIST_COLUMNS);
return savedState || {
name: true,
projectHealth: true,
category: true,
projectUpdate: true,
client: true,
team: true,
projectManager: true,
estimatedVsActual: true,
tasksProgress: true,
lastActivity: true,
status: true,
dates: true,
daysLeft: true,
};
return (
savedState || {
name: true,
projectHealth: true,
category: true,
projectUpdate: true,
client: true,
team: true,
projectManager: true,
estimatedVsActual: true,
tasksProgress: true,
lastActivity: true,
status: true,
dates: true,
daysLeft: true,
}
);
};
const initialState: ColumnsVisibilityState = getInitialState();

View File

@@ -23,7 +23,7 @@ const ProjectReportsMembersTab = ({ projectId = null }: ProjectReportsMembersTab
const fetchMembersData = async () => {
if (!projectId || loading) return;
try {
setLoading(true);
const res = await reportingProjectsApiService.getProjectMembers(projectId);

View File

@@ -2,7 +2,10 @@ import { Progress, Table, TableColumnsType } from 'antd';
import React from 'react';
import CustomTableTitle from '../../../../../components/CustomTableTitle';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { setSelectedMember, toggleProjectReportsMembersTaskDrawer } from '../../project-reports-slice';
import {
setSelectedMember,
toggleProjectReportsMembersTaskDrawer,
} from '../../project-reports-slice';
import { useTranslation } from 'react-i18next';
import ProjectReportsMembersTaskDrawer from './projectReportsMembersTaskDrawer/ProjectReportsMembersTaskDrawer';
import { createPortal } from 'react-dom';
@@ -107,7 +110,11 @@ const ProjectReportsMembersTable = ({ membersData, loading }: ProjectReportsMemb
};
}}
/>
{createPortal(<ProjectReportsMembersTaskDrawer />, document.body, 'project-reports-members-task-drawer')}
{createPortal(
<ProjectReportsMembersTaskDrawer />,
document.body,
'project-reports-members-task-drawer'
)}
</>
);
};

View File

@@ -27,7 +27,6 @@ const ProjectReportsMembersTaskDrawer = () => {
const handleAfterOpenChange = (open: boolean) => {
if (open) {
}
};
@@ -61,9 +60,7 @@ const ProjectReportsMembersTaskDrawer = () => {
setSearchQuery={setSearchQuery}
/>
<ProjectReportsMembersTasksTable
tasksData={filteredTaskData}
/>
<ProjectReportsMembersTasksTable tasksData={filteredTaskData} />
</Flex>
</Drawer>
);

View File

@@ -4,7 +4,11 @@ import CustomTableTitle from '@/components/CustomTableTitle';
import { colors } from '@/styles/colors';
import dayjs from 'dayjs';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { setShowTaskDrawer, fetchTask, setSelectedTaskId } from '@/features/task-drawer/task-drawer.slice';
import {
setShowTaskDrawer,
fetchTask,
setSelectedTaskId,
} from '@/features/task-drawer/task-drawer.slice';
import { DoubleRightOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import { fetchPriorities } from '@/features/taskAttributes/taskPrioritySlice';
@@ -33,11 +37,13 @@ const ProjectReportsTasksTable = ({
const dispatch = useAppDispatch();
useEffect(()=>{
useEffect(() => {
dispatch(fetchPriorities());
dispatch(fetchLabels());
dispatch(getTeamMembers({ index: 0, size: 100, field: null, order: null, search: null, all: true }));
},[dispatch])
dispatch(
getTeamMembers({ index: 0, size: 100, field: null, order: null, search: null, all: true })
);
}, [dispatch]);
// function to handle task drawer open
const handleUpdateTaskDrawer = (id: string) => {

View File

@@ -31,9 +31,9 @@ const ProjectReportsTasksTab = ({ projectId = null }: ProjectReportsTasksTabProp
.filter(item => item.tasks.length > 0)
.map(item => ({
...item,
tasks: item.tasks.filter(task =>
tasks: item.tasks.filter(task =>
task.name?.toLowerCase().includes(searchQuery.toLowerCase())
)
),
}))
.filter(item => item.tasks.length > 0);
}, [groups, searchQuery]);

View File

@@ -197,11 +197,18 @@ const roadmapSlice = createSlice({
state.tasksList = updateTask(state.tasksList);
},
updateTaskProgress: (state, action: PayloadAction<{ taskId: string; progress: number, totalTasksCount: number, completedCount: number }>) => {
updateTaskProgress: (
state,
action: PayloadAction<{
taskId: string;
progress: number;
totalTasksCount: number;
completedCount: number;
}>
) => {
const { taskId, progress, totalTasksCount, completedCount } = action.payload;
const updateTask = (tasks: NewTaskType[]) => {
tasks.forEach(task => {
if (task.id === taskId) {
task.progress = progress;
} else if (task.subTasks) {

View File

@@ -8,14 +8,26 @@ import { useAppSelector } from '@/hooks/useAppSelector';
import { getDayName } from '@/utils/schedule';
import dayjs from 'dayjs';
const ProjectTimelineModal = ({ setIsModalOpen, defaultData, projectId, memberId }: { setIsModalOpen: (x: boolean) => void, defaultData?: ScheduleData, projectId?:string, memberId?:string }) => {
const ProjectTimelineModal = ({
setIsModalOpen,
defaultData,
projectId,
memberId,
}: {
setIsModalOpen: (x: boolean) => void;
defaultData?: ScheduleData;
projectId?: string;
memberId?: string;
}) => {
const [form] = Form.useForm();
const { t } = useTranslation('schedule');
const { workingDays } = useAppSelector(state => state.scheduleReducer);
const dispatch = useAppDispatch();
const handleFormSubmit = async (values: any) => {
dispatch(createSchedule({schedule:{ ...values, project_id:projectId, team_member_id:memberId }}));
dispatch(
createSchedule({ schedule: { ...values, project_id: projectId, team_member_id: memberId } })
);
form.resetFields();
setIsModalOpen(false);
dispatch(fetchTeamData());
@@ -25,13 +37,13 @@ const ProjectTimelineModal = ({ setIsModalOpen, defaultData, projectId, memberId
const startDate = form.getFieldValue('allocated_from'); // Start date
const endDate = form.getFieldValue('allocated_to'); // End date
const secondsPerDay = form.getFieldValue('seconds_per_day'); // Seconds per day
if (startDate && endDate && secondsPerDay && !isNaN(Number(secondsPerDay))) {
const start: any = new Date(startDate);
const end: any = new Date(endDate);
if (start > end) {
console.error("Start date cannot be after end date");
console.error('Start date cannot be after end date');
return;
}
@@ -41,11 +53,11 @@ const ProjectTimelineModal = ({ setIsModalOpen, defaultData, projectId, memberId
totalWorkingDays++;
}
}
const hoursPerDay = secondsPerDay;
const totalHours = totalWorkingDays * hoursPerDay;
form.setFieldsValue({ total_seconds: totalHours.toFixed(2) });
} else {
form.setFieldsValue({ total_seconds: 0 });
@@ -65,7 +77,6 @@ const ProjectTimelineModal = ({ setIsModalOpen, defaultData, projectId, memberId
useEffect(() => {
form.setFieldsValue({ allocated_from: dayjs(defaultData?.allocated_from) });
form.setFieldsValue({ allocated_to: dayjs(defaultData?.allocated_to) });
}, [defaultData]);
return (
@@ -95,7 +106,7 @@ const ProjectTimelineModal = ({ setIsModalOpen, defaultData, projectId, memberId
>
<span>{t('endDate')}</span>
<Form.Item name="allocated_to">
<DatePicker disabledDate={disabledEndDate} onChange={e => calTotalHours()}/>
<DatePicker disabledDate={disabledEndDate} onChange={e => calTotalHours()} />
</Form.Item>
</Col>
</Row>
@@ -103,14 +114,26 @@ const ProjectTimelineModal = ({ setIsModalOpen, defaultData, projectId, memberId
<Col span={12} style={{ paddingRight: '20px' }}>
<span>{t('hoursPerDay')}</span>
<Form.Item name="seconds_per_day">
<Input max={24} onChange={e => calTotalHours()} defaultValue={defaultData?.seconds_per_day} type="number" suffix="hours" />
<Input
max={24}
onChange={e => calTotalHours()}
defaultValue={defaultData?.seconds_per_day}
type="number"
suffix="hours"
/>
</Form.Item>
</Col>
<Col span={12} style={{ paddingLeft: '20px' }}>
<span>{t('totalHours')}</span>
<Form.Item name="total_seconds">
<Input readOnly max={24} defaultValue={defaultData?.total_seconds} type="number" suffix="hours" />
<Input
readOnly
max={24}
defaultValue={defaultData?.total_seconds}
type="number"
suffix="hours"
/>
</Form.Item>
</Col>
</Row>
@@ -124,7 +147,7 @@ const ProjectTimelineModal = ({ setIsModalOpen, defaultData, projectId, memberId
<Button type="link">{t('deleteButton')}</Button>
<div style={{ display: 'flex', gap: '5px' }}>
<Button onClick={() => setIsModalOpen(false)}>{t('cancelButton')}</Button>
<Button htmlType='submit' type="primary">
<Button htmlType="submit" type="primary">
{t('saveButton')}
</Button>
</div>

View File

@@ -1,7 +1,14 @@
import { Button, Checkbox, Col, Drawer, Form, Input, Row } from 'antd';
import React, { ReactHTMLElement, useEffect, useState } from 'react';
import { useAppSelector } from '@/hooks/useAppSelector';
import { fetchDateList, fetchTeamData, getWorking, toggleSettingsDrawer, updateSettings, updateWorking } from './scheduleSlice';
import {
fetchDateList,
fetchTeamData,
getWorking,
toggleSettingsDrawer,
updateSettings,
updateWorking,
} from './scheduleSlice';
import { useDispatch } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { scheduleAPIService } from '@/api/schedule/schedule.api.service';
@@ -17,7 +24,6 @@ const ScheduleSettingsDrawer: React.FC = () => {
const { workingDays, workingHours, loading } = useAppSelector(state => state.scheduleReducer);
const { date, type } = useAppSelector(state => state.scheduleReducer);
const handleFormSubmit = async (values: any) => {
await dispatch(updateWorking(values));
dispatch(toggleSettingsDrawer());

View File

@@ -43,7 +43,10 @@ export const fetchTeamData = createAsyncThunk('schedule/fetchTeamData', async ()
export const fetchDateList = createAsyncThunk(
'schedule/fetchDateList',
async ({ date, type }: { date: Date; type: string }) => {
const response = await scheduleAPIService.fetchScheduleDates({ date: date.toISOString(), type });
const response = await scheduleAPIService.fetchScheduleDates({
date: date.toISOString(),
type,
});
if (!response.done) {
throw new Error('Failed to fetch date list');
}
@@ -97,7 +100,7 @@ export const fetchMemberProjects = createAsyncThunk(
export const createSchedule = createAsyncThunk(
'schedule/createSchedule',
async ({ schedule }: { schedule: ScheduleData}) => {
async ({ schedule }: { schedule: ScheduleData }) => {
const response = await scheduleAPIService.submitScheduleData({ schedule });
if (!response.done) {
throw new Error('Failed to fetch date list');
@@ -187,7 +190,8 @@ const scheduleSlice = createSlice({
.addCase(getWorking.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || 'Failed to fetch list';
}).addCase(fetchMemberProjects.pending, state => {
})
.addCase(fetchMemberProjects.pending, state => {
state.loading = true;
state.error = null;
})
@@ -196,15 +200,16 @@ const scheduleSlice = createSlice({
state.teamData.find((team: any) => {
if (team.id === data.id) {
team.projects = data.projects||[];
team.projects = data.projects || [];
}
})
});
state.loading = false;
})
.addCase(fetchMemberProjects.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || 'Failed to fetch date list';
}).addCase(createSchedule.pending, state => {
})
.addCase(createSchedule.pending, state => {
state.loading = true;
state.error = null;
})
@@ -218,6 +223,13 @@ const scheduleSlice = createSlice({
},
});
export const { toggleSettingsDrawer, updateSettings, toggleScheduleDrawer, getWorkingSettings, setDate, setType, setDayCount } =
scheduleSlice.actions;
export const {
toggleSettingsDrawer,
updateSettings,
toggleScheduleDrawer,
getWorkingSettings,
setDate,
setType,
setDayCount,
} = scheduleSlice.actions;
export default scheduleSlice.reducer;

View File

@@ -45,9 +45,6 @@ const memberSlice = createSlice({
},
});
export const {
toggleInviteMemberDrawer,
toggleUpdateMemberDrawer,
triggerTeamMembersRefresh,
} = memberSlice.actions;
export const { toggleInviteMemberDrawer, toggleUpdateMemberDrawer, triggerTeamMembersRefresh } =
memberSlice.actions;
export default memberSlice.reducer;

View File

@@ -62,7 +62,7 @@ const taskDrawerSlice = createSlice({
if (state.taskFormViewModel?.task && state.taskFormViewModel.task.id === taskId) {
state.taskFormViewModel.task.status_id = status_id;
state.taskFormViewModel.task.status_color = color_code;
state.taskFormViewModel.task.status_color_dark = color_code_dark
state.taskFormViewModel.task.status_color_dark = color_code_dark;
}
},
setStartDate: (state, action: PayloadAction<IProjectTask>) => {
@@ -99,22 +99,28 @@ const taskDrawerSlice = createSlice({
setTaskSubscribers: (state, action: PayloadAction<InlineMember[]>) => {
state.subscribers = action.payload;
},
setTimeLogEditing: (state, action: PayloadAction<{
isEditing: boolean;
logBeingEdited: ITaskLogViewModel | null;
}>) => {
setTimeLogEditing: (
state,
action: PayloadAction<{
isEditing: boolean;
logBeingEdited: ITaskLogViewModel | null;
}>
) => {
state.timeLogEditing = action.payload;
},
setTaskRecurringSchedule: (state, action: PayloadAction<{
schedule_id: string;
task_id: string;
}>) => {
setTaskRecurringSchedule: (
state,
action: PayloadAction<{
schedule_id: string;
task_id: string;
}>
) => {
const { schedule_id, task_id } = action.payload;
if (state.taskFormViewModel?.task && state.taskFormViewModel.task.id === task_id) {
state.taskFormViewModel.task.schedule_id = schedule_id;
}
},
resetTaskDrawer: (state) => {
resetTaskDrawer: state => {
return initialState;
},
},
@@ -146,6 +152,6 @@ export const {
setTaskSubscribers,
setTimeLogEditing,
setTaskRecurringSchedule,
resetTaskDrawer
resetTaskDrawer,
} = taskDrawerSlice.actions;
export default taskDrawerSlice.reducer;

View File

@@ -21,7 +21,7 @@ const groupingSlice = createSlice({
setCurrentGrouping: (state, action: PayloadAction<'status' | 'priority' | 'phase'>) => {
state.currentGrouping = action.payload;
},
addCustomPhase: (state, action: PayloadAction<string>) => {
const phase = action.payload.trim();
if (phase && !state.customPhases.includes(phase)) {
@@ -29,23 +29,23 @@ const groupingSlice = createSlice({
state.groupOrder.phase.push(phase);
}
},
removeCustomPhase: (state, action: PayloadAction<string>) => {
const phase = action.payload;
state.customPhases = state.customPhases.filter(p => p !== phase);
state.groupOrder.phase = state.groupOrder.phase.filter(p => p !== phase);
},
updateCustomPhases: (state, action: PayloadAction<string[]>) => {
state.customPhases = action.payload;
state.groupOrder.phase = action.payload;
},
updateGroupOrder: (state, action: PayloadAction<{ groupType: string; order: string[] }>) => {
const { groupType, order } = action.payload;
state.groupOrder[groupType] = order;
},
toggleGroupCollapsed: (state, action: PayloadAction<string>) => {
const groupId = action.payload;
if (!state.groupStates[groupId]) {
@@ -53,7 +53,7 @@ const groupingSlice = createSlice({
}
state.groupStates[groupId].collapsed = !state.groupStates[groupId].collapsed;
},
setGroupCollapsed: (state, action: PayloadAction<{ groupId: string; collapsed: boolean }>) => {
const { groupId, collapsed } = action.payload;
if (!state.groupStates[groupId]) {
@@ -61,14 +61,14 @@ const groupingSlice = createSlice({
}
state.groupStates[groupId].collapsed = collapsed;
},
collapseAllGroups: (state) => {
collapseAllGroups: state => {
Object.keys(state.groupStates).forEach(groupId => {
state.groupStates[groupId].collapsed = true;
});
},
expandAllGroups: (state) => {
expandAllGroups: state => {
Object.keys(state.groupStates).forEach(groupId => {
state.groupStates[groupId].collapsed = false;
});
@@ -104,27 +104,40 @@ export const selectCurrentGroupOrder = createSelector(
);
export const selectTaskGroups = createSelector(
[taskManagementSelectors.selectAll, selectCurrentGrouping, selectCurrentGroupOrder, selectGroupStates],
[
taskManagementSelectors.selectAll,
selectCurrentGrouping,
selectCurrentGroupOrder,
selectGroupStates,
],
(tasks, currentGrouping, groupOrder, groupStates) => {
const groups: TaskGroup[] = [];
// Get unique values for the current grouping
const groupValues = groupOrder.length > 0 ? groupOrder :
[...new Set(tasks.map(task => {
if (currentGrouping === 'status') return task.status;
if (currentGrouping === 'priority') return task.priority;
return task.phase;
}))];
const groupValues =
groupOrder.length > 0
? groupOrder
: [
...new Set(
tasks.map(task => {
if (currentGrouping === 'status') return task.status;
if (currentGrouping === 'priority') return task.priority;
return task.phase;
})
),
];
groupValues.forEach(value => {
const tasksInGroup = tasks.filter(task => {
if (currentGrouping === 'status') return task.status === value;
if (currentGrouping === 'priority') return task.priority === value;
return task.phase === value;
}).sort((a, b) => a.order - b.order);
const tasksInGroup = tasks
.filter(task => {
if (currentGrouping === 'status') return task.status === value;
if (currentGrouping === 'priority') return task.priority === value;
return task.phase === value;
})
.sort((a, b) => a.order - b.order);
const groupId = `${currentGrouping}-${value}`;
groups.push({
id: groupId,
title: value.charAt(0).toUpperCase() + value.slice(1),
@@ -135,7 +148,7 @@ export const selectTaskGroups = createSelector(
color: getGroupColor(currentGrouping, value),
});
});
return groups;
}
);
@@ -144,22 +157,22 @@ export const selectTasksByCurrentGrouping = createSelector(
[taskManagementSelectors.selectAll, selectCurrentGrouping],
(tasks, currentGrouping) => {
const grouped: Record<string, typeof tasks> = {};
tasks.forEach(task => {
let key: string;
if (currentGrouping === 'status') key = task.status;
else if (currentGrouping === 'priority') key = task.priority;
else key = task.phase;
if (!grouped[key]) grouped[key] = [];
grouped[key].push(task);
});
// Sort tasks within each group by order
Object.keys(grouped).forEach(key => {
grouped[key].sort((a, b) => a.order - b.order);
});
return grouped;
}
);
@@ -185,8 +198,8 @@ const getGroupColor = (groupType: string, value: string): string => {
Deployment: '#52c41a',
},
};
return colorMaps[groupType as keyof typeof colorMaps]?.[value as keyof any] || '#d9d9d9';
};
export default groupingSlice.reducer;
export default groupingSlice.reducer;

View File

@@ -14,16 +14,16 @@ const selectionSlice = createSlice({
toggleTaskSelection: (state, action: PayloadAction<string>) => {
const taskId = action.payload;
const index = state.selectedTaskIds.indexOf(taskId);
if (index === -1) {
state.selectedTaskIds.push(taskId);
} else {
state.selectedTaskIds.splice(index, 1);
}
state.lastSelectedId = taskId;
},
selectTask: (state, action: PayloadAction<string>) => {
const taskId = action.payload;
if (!state.selectedTaskIds.includes(taskId)) {
@@ -31,7 +31,7 @@ const selectionSlice = createSlice({
}
state.lastSelectedId = taskId;
},
deselectTask: (state, action: PayloadAction<string>) => {
const taskId = action.payload;
state.selectedTaskIds = state.selectedTaskIds.filter(id => id !== taskId);
@@ -39,7 +39,7 @@ const selectionSlice = createSlice({
state.lastSelectedId = state.selectedTaskIds[state.selectedTaskIds.length - 1] || null;
}
},
selectMultipleTasks: (state, action: PayloadAction<string[]>) => {
const taskIds = action.payload;
// Add new task IDs that aren't already selected
@@ -50,37 +50,41 @@ const selectionSlice = createSlice({
});
state.lastSelectedId = taskIds[taskIds.length - 1] || state.lastSelectedId;
},
selectRangeTasks: (state, action: PayloadAction<{ startId: string; endId: string; allTaskIds: string[] }>) => {
selectRangeTasks: (
state,
action: PayloadAction<{ startId: string; endId: string; allTaskIds: string[] }>
) => {
const { startId, endId, allTaskIds } = action.payload;
const startIndex = allTaskIds.indexOf(startId);
const endIndex = allTaskIds.indexOf(endId);
if (startIndex !== -1 && endIndex !== -1) {
const [start, end] = startIndex <= endIndex ? [startIndex, endIndex] : [endIndex, startIndex];
const [start, end] =
startIndex <= endIndex ? [startIndex, endIndex] : [endIndex, startIndex];
const rangeIds = allTaskIds.slice(start, end + 1);
// Add range IDs that aren't already selected
rangeIds.forEach(id => {
if (!state.selectedTaskIds.includes(id)) {
state.selectedTaskIds.push(id);
}
});
state.lastSelectedId = endId;
}
},
selectAllTasks: (state, action: PayloadAction<string[]>) => {
state.selectedTaskIds = action.payload;
state.lastSelectedId = action.payload[action.payload.length - 1] || null;
},
clearSelection: (state) => {
clearSelection: state => {
state.selectedTaskIds = [];
state.lastSelectedId = null;
},
setSelection: (state, action: PayloadAction<string[]>) => {
state.selectedTaskIds = action.payload;
state.lastSelectedId = action.payload[action.payload.length - 1] || null;
@@ -103,11 +107,15 @@ export const {
} = selectionSlice.actions;
// Selectors
export const selectSelectedTaskIds = (state: RootState) => state.taskManagementSelection.selectedTaskIds;
export const selectLastSelectedId = (state: RootState) => state.taskManagementSelection.lastSelectedId;
export const selectHasSelection = (state: RootState) => state.taskManagementSelection.selectedTaskIds.length > 0;
export const selectSelectionCount = (state: RootState) => state.taskManagementSelection.selectedTaskIds.length;
export const selectIsTaskSelected = (taskId: string) => (state: RootState) =>
export const selectSelectedTaskIds = (state: RootState) =>
state.taskManagementSelection.selectedTaskIds;
export const selectLastSelectedId = (state: RootState) =>
state.taskManagementSelection.lastSelectedId;
export const selectHasSelection = (state: RootState) =>
state.taskManagementSelection.selectedTaskIds.length > 0;
export const selectSelectionCount = (state: RootState) =>
state.taskManagementSelection.selectedTaskIds.length;
export const selectIsTaskSelected = (taskId: string) => (state: RootState) =>
state.taskManagementSelection.selectedTaskIds.includes(taskId);
export default selectionSlice.reducer;
export default selectionSlice.reducer;

View File

@@ -1,7 +1,16 @@
import { createSlice, createEntityAdapter, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
import {
createSlice,
createEntityAdapter,
PayloadAction,
createAsyncThunk,
} from '@reduxjs/toolkit';
import { Task, TaskManagementState } from '@/types/task-management.types';
import { RootState } from '@/app/store';
import { tasksApiService, ITaskListConfigV2, ITaskListV3Response } from '@/api/tasks/tasks.api.service';
import {
tasksApiService,
ITaskListConfigV2,
ITaskListV3Response,
} from '@/api/tasks/tasks.api.service';
import logger from '@/utils/errorLogger';
// Entity adapter for normalized state
@@ -27,7 +36,7 @@ export const fetchTasks = createAsyncThunk(
try {
const state = getState() as RootState;
const currentGrouping = state.grouping.currentGrouping;
const config: ITaskListConfigV2 = {
id: projectId,
archived: false,
@@ -44,7 +53,7 @@ export const fetchTasks = createAsyncThunk(
};
const response = await tasksApiService.getTaskList(config);
// Helper function to safely convert time values
const convertTimeValue = (value: any): number => {
if (typeof value === 'number') return value;
@@ -57,7 +66,7 @@ export const fetchTasks = createAsyncThunk(
if ('hours' in value || 'minutes' in value) {
const hours = Number(value.hours || 0);
const minutes = Number(value.minutes || 0);
return hours + (minutes / 60);
return hours + minutes / 60;
}
}
return 0;
@@ -66,7 +75,7 @@ export const fetchTasks = createAsyncThunk(
// Create a mapping from status IDs to group names
const statusIdToNameMap: Record<string, string> = {};
const priorityIdToNameMap: Record<string, string> = {};
response.body.forEach((group: any) => {
statusIdToNameMap[group.id] = group.name.toLowerCase();
});
@@ -78,18 +87,27 @@ export const fetchTasks = createAsyncThunk(
// Map priority value to name (this is an assumption based on common patterns)
if (task.priority_value !== undefined) {
switch (task.priority_value) {
case 0: priorityIdToNameMap[task.priority] = 'low'; break;
case 1: priorityIdToNameMap[task.priority] = 'medium'; break;
case 2: priorityIdToNameMap[task.priority] = 'high'; break;
case 3: priorityIdToNameMap[task.priority] = 'critical'; break;
default: priorityIdToNameMap[task.priority] = 'medium';
case 0:
priorityIdToNameMap[task.priority] = 'low';
break;
case 1:
priorityIdToNameMap[task.priority] = 'medium';
break;
case 2:
priorityIdToNameMap[task.priority] = 'high';
break;
case 3:
priorityIdToNameMap[task.priority] = 'critical';
break;
default:
priorityIdToNameMap[task.priority] = 'medium';
}
}
});
});
// Transform the API response to our Task type
const tasks: Task[] = response.body.flatMap((group: any) =>
const tasks: Task[] = response.body.flatMap((group: any) =>
group.tasks.map((task: any) => ({
id: task.id,
task_key: task.task_key || '',
@@ -101,13 +119,14 @@ export const fetchTasks = createAsyncThunk(
progress: typeof task.complete_ratio === 'number' ? task.complete_ratio : 0,
assignees: task.assignees?.map((a: any) => a.team_member_id) || [],
assignee_names: task.assignee_names || task.names || [],
labels: task.labels?.map((l: any) => ({
id: l.id || l.label_id,
name: l.name,
color: l.color_code || '#1890ff',
end: l.end,
names: l.names
})) || [],
labels:
task.labels?.map((l: any) => ({
id: l.id || l.label_id,
name: l.name,
color: l.color_code || '#1890ff',
end: l.end,
names: l.names,
})) || [],
dueDate: task.end_date,
timeTracking: {
estimated: convertTimeValue(task.total_time),
@@ -138,25 +157,31 @@ export const fetchTasksV3 = createAsyncThunk(
try {
const state = getState() as RootState;
const currentGrouping = state.grouping.currentGrouping;
// Get selected labels from taskReducer
const selectedLabels = state.taskReducer.labels
? state.taskReducer.labels.filter(l => l.selected).map(l => l.id).join(' ')
? state.taskReducer.labels
.filter(l => l.selected)
.map(l => l.id)
.join(' ')
: '';
// Get selected assignees from taskReducer
const selectedAssignees = state.taskReducer.taskAssignees
? state.taskReducer.taskAssignees.filter(m => m.selected).map(m => m.id).join(' ')
? state.taskReducer.taskAssignees
.filter(m => m.selected)
.map(m => m.id)
.join(' ')
: '';
// Get selected priorities from taskReducer (consistent with other slices)
const selectedPriorities = state.taskReducer.priorities
? state.taskReducer.priorities.join(' ')
: '';
// Get search value from taskReducer
const searchValue = state.taskReducer.search || '';
const config: ITaskListConfigV2 = {
id: projectId,
archived: false,
@@ -173,13 +198,13 @@ export const fetchTasksV3 = createAsyncThunk(
};
const response = await tasksApiService.getTaskListV3(config);
// Minimal processing - tasks are already processed by backend
return {
tasks: response.body.allTasks,
groups: response.body.groups,
grouping: response.body.grouping,
totalTasks: response.body.totalTasks
totalTasks: response.body.totalTasks,
};
} catch (error) {
logger.error('Fetch Tasks V3', error);
@@ -192,6 +217,44 @@ export const fetchTasksV3 = createAsyncThunk(
);
// Refresh task progress separately to avoid slowing down initial load
export const fetchSubTasks = createAsyncThunk(
'taskManagement/fetchSubTasks',
async (
{ taskId, projectId }: { taskId: string; projectId: string },
{ rejectWithValue, getState }
) => {
try {
const state = getState() as RootState;
const currentGrouping = state.grouping.currentGrouping;
const config: ITaskListConfigV2 = {
id: projectId,
archived: false,
group: currentGrouping,
field: '',
order: '',
search: '',
statuses: '',
members: '',
projects: '',
isSubtasksInclude: false,
labels: '',
priorities: '',
parent_task: taskId,
};
const response = await tasksApiService.getTaskListV3(config);
return { parentTaskId: taskId, subtasks: response.body.allTasks };
} catch (error) {
logger.error('Fetch Sub Tasks', error);
if (error instanceof Error) {
return rejectWithValue(error.message);
}
return rejectWithValue('Failed to fetch sub tasks');
}
}
);
export const refreshTaskProgress = createAsyncThunk(
'taskManagement/refreshTaskProgress',
async (projectId: string, { rejectWithValue }) => {
@@ -211,7 +274,10 @@ export const refreshTaskProgress = createAsyncThunk(
// Async thunk to reorder tasks with API call
export const reorderTasksWithAPI = createAsyncThunk(
'taskManagement/reorderTasksWithAPI',
async ({ taskIds, newOrder, projectId }: { taskIds: string[]; newOrder: number[]; projectId: string }, { rejectWithValue }) => {
async (
{ taskIds, newOrder, projectId }: { taskIds: string[]; newOrder: number[]; projectId: string },
{ rejectWithValue }
) => {
try {
// Make API call to update task order
const response = await tasksApiService.reorderTasks({
@@ -219,7 +285,7 @@ export const reorderTasksWithAPI = createAsyncThunk(
newOrder,
projectId,
});
if (response.done) {
return { taskIds, newOrder };
} else {
@@ -235,12 +301,20 @@ export const reorderTasksWithAPI = createAsyncThunk(
// Async thunk to move task between groups with API call
export const moveTaskToGroupWithAPI = createAsyncThunk(
'taskManagement/moveTaskToGroupWithAPI',
async ({ taskId, groupType, groupValue, projectId }: {
taskId: string;
groupType: 'status' | 'priority' | 'phase';
groupValue: string;
projectId: string;
}, { rejectWithValue }) => {
async (
{
taskId,
groupType,
groupValue,
projectId,
}: {
taskId: string;
groupType: 'status' | 'priority' | 'phase';
groupValue: string;
projectId: string;
},
{ rejectWithValue }
) => {
try {
// Make API call to update task group
const response = await tasksApiService.updateTaskGroup({
@@ -249,7 +323,7 @@ export const moveTaskToGroupWithAPI = createAsyncThunk(
groupValue,
projectId,
});
if (response.done) {
return { taskId, groupType, groupValue };
} else {
@@ -272,18 +346,17 @@ const taskManagementSlice = createSlice({
state.loading = false;
state.error = null;
},
addTask: (state, action: PayloadAction<Task>) => {
tasksAdapter.addOne(state, action.payload);
},
addTaskToGroup: (state, action: PayloadAction<{ task: Task; groupId?: string }>) => {
const { task, groupId } = action.payload;
// Add to entity adapter
tasksAdapter.addOne(state, task);
// Add to groups array for V3 API compatibility
if (state.groups && state.groups.length > 0) {
// Find the target group using the provided UUID
@@ -292,14 +365,14 @@ const taskManagementSlice = createSlice({
if (groupId && group.id === groupId) {
return true;
}
return false;
});
if (targetGroup) {
// Add task ID to the end of the group's taskIds array (newest last)
targetGroup.taskIds.push(task.id);
// Also add to the tasks array if it exists (for backward compatibility)
if ((targetGroup as any).tasks) {
(targetGroup as any).tasks.push(task);
@@ -307,7 +380,7 @@ const taskManagementSlice = createSlice({
}
}
},
updateTask: (state, action: PayloadAction<{ id: string; changes: Partial<Task> }>) => {
tasksAdapter.updateOne(state, {
id: action.payload.id,
@@ -317,11 +390,11 @@ const taskManagementSlice = createSlice({
},
});
},
deleteTask: (state, action: PayloadAction<string>) => {
tasksAdapter.removeOne(state, action.payload);
},
// Bulk operations
bulkUpdateTasks: (state, action: PayloadAction<{ ids: string[]; changes: Partial<Task> }>) => {
const { ids, changes } = action.payload;
@@ -334,33 +407,40 @@ const taskManagementSlice = createSlice({
}));
tasksAdapter.updateMany(state, updates);
},
bulkDeleteTasks: (state, action: PayloadAction<string[]>) => {
tasksAdapter.removeMany(state, action.payload);
},
// Optimized drag and drop operations
reorderTasks: (state, action: PayloadAction<{ taskIds: string[]; newOrder: number[] }>) => {
const { taskIds, newOrder } = action.payload;
// Batch update for better performance
const updates = taskIds.map((id, index) => ({
id,
changes: {
changes: {
order: newOrder[index],
updatedAt: new Date().toISOString(),
},
}));
tasksAdapter.updateMany(state, updates);
},
moveTaskToGroup: (state, action: PayloadAction<{ taskId: string; groupType: 'status' | 'priority' | 'phase'; groupValue: string }>) => {
moveTaskToGroup: (
state,
action: PayloadAction<{
taskId: string;
groupType: 'status' | 'priority' | 'phase';
groupValue: string;
}>
) => {
const { taskId, groupType, groupValue } = action.payload;
const changes: Partial<Task> = {
updatedAt: new Date().toISOString(),
};
// Update the appropriate field based on group type
if (groupType === 'status') {
changes.status = groupValue as Task['status'];
@@ -369,19 +449,22 @@ const taskManagementSlice = createSlice({
} else if (groupType === 'phase') {
changes.phase = groupValue;
}
tasksAdapter.updateOne(state, { id: taskId, changes });
},
// New action to move task between groups with proper group management
moveTaskBetweenGroups: (state, action: PayloadAction<{
taskId: string;
fromGroupId: string;
toGroupId: string;
taskUpdate: Partial<Task>;
}>) => {
moveTaskBetweenGroups: (
state,
action: PayloadAction<{
taskId: string;
fromGroupId: string;
toGroupId: string;
taskUpdate: Partial<Task>;
}>
) => {
const { taskId, fromGroupId, toGroupId, taskUpdate } = action.payload;
// Update the task entity with new values
tasksAdapter.updateOne(state, {
id: taskId,
@@ -390,7 +473,7 @@ const taskManagementSlice = createSlice({
updatedAt: new Date().toISOString(),
},
});
// Update groups if they exist
if (state.groups && state.groups.length > 0) {
// Remove task from old group
@@ -398,7 +481,7 @@ const taskManagementSlice = createSlice({
if (fromGroup) {
fromGroup.taskIds = fromGroup.taskIds.filter(id => id !== taskId);
}
// Add task to new group
const toGroup = state.groups.find(group => group.id === toGroupId);
if (toGroup) {
@@ -407,22 +490,25 @@ const taskManagementSlice = createSlice({
}
}
},
// Optimistic update for drag operations - reduces perceived lag
optimisticTaskMove: (state, action: PayloadAction<{ taskId: string; newGroupId: string; newIndex: number }>) => {
optimisticTaskMove: (
state,
action: PayloadAction<{ taskId: string; newGroupId: string; newIndex: number }>
) => {
const { taskId, newGroupId, newIndex } = action.payload;
const task = state.entities[taskId];
if (task) {
// Parse group ID to determine new values
const [groupType, ...groupValueParts] = newGroupId.split('-');
const groupValue = groupValueParts.join('-');
const changes: Partial<Task> = {
order: newIndex,
updatedAt: new Date().toISOString(),
};
// Update group-specific field
if (groupType === 'status') {
changes.status = groupValue as Task['status'];
@@ -431,10 +517,10 @@ const taskManagementSlice = createSlice({
} else if (groupType === 'phase') {
changes.phase = groupValue;
}
// Update the task entity
tasksAdapter.updateOne(state, { id: taskId, changes });
// Update groups if they exist
if (state.groups && state.groups.length > 0) {
// Find the target group
@@ -444,7 +530,7 @@ const taskManagementSlice = createSlice({
state.groups.forEach(group => {
group.taskIds = group.taskIds.filter(id => id !== taskId);
});
// Add task to target group at the specified index
if (newIndex >= targetGroup.taskIds.length) {
targetGroup.taskIds.push(taskId);
@@ -455,25 +541,29 @@ const taskManagementSlice = createSlice({
}
}
},
// Proper reorder action that handles both task entities and group arrays
reorderTasksInGroup: (state, action: PayloadAction<{
taskId: string;
fromGroupId: string;
toGroupId: string;
fromIndex: number;
toIndex: number;
groupType: 'status' | 'priority' | 'phase';
groupValue: string;
}>) => {
const { taskId, fromGroupId, toGroupId, fromIndex, toIndex, groupType, groupValue } = action.payload;
reorderTasksInGroup: (
state,
action: PayloadAction<{
taskId: string;
fromGroupId: string;
toGroupId: string;
fromIndex: number;
toIndex: number;
groupType: 'status' | 'priority' | 'phase';
groupValue: string;
}>
) => {
const { taskId, fromGroupId, toGroupId, fromIndex, toIndex, groupType, groupValue } =
action.payload;
// Update the task entity
const changes: Partial<Task> = {
order: toIndex,
updatedAt: new Date().toISOString(),
};
// Update group-specific field
if (groupType === 'status') {
changes.status = groupValue as Task['status'];
@@ -482,9 +572,9 @@ const taskManagementSlice = createSlice({
} else if (groupType === 'phase') {
changes.phase = groupValue;
}
tasksAdapter.updateOne(state, { id: taskId, changes });
// Update groups if they exist
if (state.groups && state.groups.length > 0) {
// Remove task from source group
@@ -492,7 +582,7 @@ const taskManagementSlice = createSlice({
if (fromGroup) {
fromGroup.taskIds = fromGroup.taskIds.filter(id => id !== taskId);
}
// Add task to target group
const toGroup = state.groups.find(group => group.id === toGroupId);
if (toGroup) {
@@ -504,12 +594,12 @@ const taskManagementSlice = createSlice({
}
}
},
// Loading states
setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload;
},
setError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload;
state.loading = false;
@@ -529,10 +619,32 @@ const taskManagementSlice = createSlice({
resetTaskManagement: (state) => {
return tasksAdapter.getInitialState(initialState);
},
toggleTaskExpansion: (state, action: PayloadAction<string>) => {
const taskId = action.payload;
const task = state.entities[taskId];
if (task) {
task.show_sub_tasks = !task.show_sub_tasks;
}
},
addSubtaskToParent: (state, action: PayloadAction<{ subtask: Task; parentTaskId: string }>) => {
const { subtask, parentTaskId } = action.payload;
const parentTask = state.entities[parentTaskId];
if (parentTask) {
if (!parentTask.sub_tasks) {
parentTask.sub_tasks = [];
}
parentTask.sub_tasks.push(subtask);
parentTask.sub_tasks_count = (parentTask.sub_tasks_count || 0) + 1;
// Ensure the parent task is expanded to show the new subtask
parentTask.show_sub_tasks = true;
// Add the subtask to the main entities as well
tasksAdapter.addOne(state, subtask);
}
},
},
extraReducers: (builder) => {
extraReducers: builder => {
builder
.addCase(fetchTasks.pending, (state) => {
.addCase(fetchTasks.pending, state => {
state.loading = true;
state.error = null;
})
@@ -543,9 +655,9 @@ const taskManagementSlice = createSlice({
})
.addCase(fetchTasks.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string || 'Failed to fetch tasks';
state.error = (action.payload as string) || 'Failed to fetch tasks';
})
.addCase(fetchTasksV3.pending, (state) => {
.addCase(fetchTasksV3.pending, state => {
state.loading = true;
state.error = null;
})
@@ -559,18 +671,28 @@ const taskManagementSlice = createSlice({
})
.addCase(fetchTasksV3.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string || 'Failed to fetch tasks';
state.error = (action.payload as string) || 'Failed to fetch tasks';
})
.addCase(refreshTaskProgress.pending, (state) => {
.addCase(fetchSubTasks.fulfilled, (state, action) => {
const { parentTaskId, subtasks } = action.payload;
const parentTask = state.entities[parentTaskId];
if (parentTask) {
parentTask.sub_tasks = subtasks;
parentTask.show_sub_tasks = true;
// Add subtasks to the main entities as well
tasksAdapter.addMany(state, subtasks);
}
})
.addCase(refreshTaskProgress.pending, state => {
// Don't set loading to true for refresh to avoid UI blocking
state.error = null;
})
.addCase(refreshTaskProgress.fulfilled, (state) => {
.addCase(refreshTaskProgress.fulfilled, state => {
state.error = null;
// Progress refresh completed successfully
})
.addCase(refreshTaskProgress.rejected, (state, action) => {
state.error = action.payload as string || 'Failed to refresh task progress';
state.error = (action.payload as string) || 'Failed to refresh task progress';
});
},
});
@@ -593,13 +715,15 @@ export const {
setSelectedPriorities,
setSearch,
resetTaskManagement,
toggleTaskExpansion,
addSubtaskToParent,
} = taskManagementSlice.actions;
export default taskManagementSlice.reducer;
// Selectors
export const taskManagementSelectors = tasksAdapter.getSelectors<RootState>(
(state) => state.taskManagement
state => state.taskManagement
);
// Enhanced selectors for better performance
@@ -617,4 +741,4 @@ export const selectTasksError = (state: RootState) => state.taskManagement.error
// V3 API selectors - no processing needed, data is pre-processed by backend
export const selectTaskGroupsV3 = (state: RootState) => state.taskManagement.groups;
export const selectCurrentGroupingV3 = (state: RootState) => state.taskManagement.grouping;
export const selectCurrentGroupingV3 = (state: RootState) => state.taskManagement.grouping;

View File

@@ -31,7 +31,7 @@ const LOCAL_STORAGE_KEY = 'worklenz.taskManagement.fields';
function loadFields(): TaskListField[] {
const stored = localStorage.getItem(LOCAL_STORAGE_KEY);
if (stored) {
try {
const parsed = JSON.parse(stored);
@@ -40,7 +40,7 @@ function loadFields(): TaskListField[] {
console.warn('Failed to parse stored fields, using defaults:', error);
}
}
return DEFAULT_FIELDS;
}
@@ -84,4 +84,4 @@ if (typeof window !== 'undefined') {
(window as any).forceResetTaskFields = forceResetFields;
}
export default taskListFieldsSlice.reducer;
export default taskListFieldsSlice.reducer;

View File

@@ -261,12 +261,12 @@ export const fetchTaskListColumns = createAsyncThunk(
async (projectId: string, { dispatch }) => {
const [standardColumns, customColumns] = await Promise.all([
tasksApiService.fetchTaskListColumns(projectId),
dispatch(fetchCustomColumns(projectId))
dispatch(fetchCustomColumns(projectId)),
]);
return {
standard: standardColumns.body,
custom: customColumns.payload
custom: customColumns.payload,
};
}
);
@@ -506,11 +506,11 @@ const taskSlice = createSlice({
if (task.parent_task_id) {
const parentTask = group.tasks.find(t => t.id === task.parent_task_id);
// if (parentTask) {
// if (!parentTask.sub_tasks) parentTask.sub_tasks = [];
// parentTask.sub_tasks.push({ ...task });
// parentTask.sub_tasks_count = parentTask.sub_tasks.length; // Update the sub_tasks_count based on the actual length
// Ensure sub-tasks are visible when adding a new one
// parentTask.show_sub_tasks = true;
// if (!parentTask.sub_tasks) parentTask.sub_tasks = [];
// parentTask.sub_tasks.push({ ...task });
// parentTask.sub_tasks_count = parentTask.sub_tasks.length; // Update the sub_tasks_count based on the actual length
// Ensure sub-tasks are visible when adding a new one
// parentTask.show_sub_tasks = true;
// }
} else {
// Handle main task addition
@@ -604,7 +604,7 @@ const taskSlice = createSlice({
task.completed_count = completedCount;
return true;
}
// Check subtasks if they exist
if (task.sub_tasks && task.sub_tasks.length > 0) {
const found = findAndUpdateTask(task.sub_tasks);
@@ -670,7 +670,8 @@ const taskSlice = createSlice({
},
updateTaskStatus: (state, action: PayloadAction<ITaskListStatusChangeResponse>) => {
const { id, status_id, color_code, color_code_dark, complete_ratio, statusCategory } = action.payload;
const { id, status_id, color_code, color_code_dark, complete_ratio, statusCategory } =
action.payload;
// Find the task in any group
const taskInfo = findTaskInGroups(state.taskGroups, id);
@@ -916,11 +917,14 @@ const taskSlice = createSlice({
// Also add to columns array to maintain visibility
state.columns.push({
...action.payload,
pinned: true // New columns are visible by default
pinned: true, // New columns are visible by default
});
},
updateCustomColumn: (state, action: PayloadAction<{ key: string; column: ITaskListColumn }>) => {
updateCustomColumn: (
state,
action: PayloadAction<{ key: string; column: ITaskListColumn }>
) => {
const { key, column } = action.payload;
const index = state.customColumns.findIndex(col => col.key === key);
if (index !== -1) {
@@ -965,7 +969,7 @@ const taskSlice = createSlice({
}>
) => {
const { taskId, columnKey, value } = action.payload;
// Update in task groups
for (const group of state.taskGroups) {
// Check in main tasks
@@ -977,7 +981,7 @@ const taskSlice = createSlice({
group.tasks[taskIndex].custom_column_values[columnKey] = value;
break;
}
// Check in subtasks
for (const parentTask of group.tasks) {
if (parentTask.sub_tasks) {
@@ -992,7 +996,7 @@ const taskSlice = createSlice({
}
}
}
// Also update in the customColumnValues state if needed
if (!state.customColumnValues[taskId]) {
state.customColumnValues[taskId] = {};
@@ -1000,7 +1004,10 @@ const taskSlice = createSlice({
state.customColumnValues[taskId][columnKey] = value;
},
updateCustomColumnPinned: (state, action: PayloadAction<{ columnId: string; isVisible: boolean }>) => {
updateCustomColumnPinned: (
state,
action: PayloadAction<{ columnId: string; isVisible: boolean }>
) => {
const { columnId, isVisible } = action.payload;
const customColumn = state.customColumns.find(col => col.id === columnId);
const column = state.columns.find(col => col.id === columnId);
@@ -1015,13 +1022,13 @@ const taskSlice = createSlice({
},
updateRecurringChange: (state, action: PayloadAction<ITaskRecurringScheduleData>) => {
const {id, schedule_type, task_id} = action.payload;
const taskInfo = findTaskInGroups(state.taskGroups, task_id as string);
const { id, schedule_type, task_id } = action.payload;
const taskInfo = findTaskInGroups(state.taskGroups, task_id as string);
if (!taskInfo) return;
const { task } = taskInfo;
task.schedule_id = id;
}
},
},
extraReducers: builder => {
@@ -1035,7 +1042,8 @@ const taskSlice = createSlice({
state.taskGroups = action.payload && action.payload.groups ? action.payload.groups : [];
state.allTasks = action.payload && action.payload.allTasks ? action.payload.allTasks : [];
state.grouping = action.payload && action.payload.grouping ? action.payload.grouping : '';
state.totalTasks = action.payload && action.payload.totalTasks ? action.payload.totalTasks : 0;
state.totalTasks =
action.payload && action.payload.totalTasks ? action.payload.totalTasks : 0;
})
.addCase(fetchTaskGroups.rejected, (state, action) => {
state.loadingGroups = false;
@@ -1186,7 +1194,7 @@ export const {
updateSubTasks,
updateCustomColumnValue,
updateCustomColumnPinned,
updateRecurringChange
updateRecurringChange,
} = taskSlice.actions;
export default taskSlice.reducer;

View File

@@ -62,7 +62,6 @@ export const editTeamName = createAsyncThunk(
} catch (error) {
logger.error('Edit Team Name', error);
if (error instanceof Error) {
return rejectWithValue(error.message);
}

View File

@@ -16,37 +16,43 @@ const ThemeWrapper = memo(({ children }: ChildrenProp) => {
const configRef = useRef<HTMLDivElement>(null);
// Memoize theme configuration to prevent unnecessary re-renders
const themeConfig = useMemo(() => ({
algorithm: themeMode === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm,
components: {
Layout: {
colorBgLayout: themeMode === 'dark' ? colors.darkGray : colors.white,
headerBg: themeMode === 'dark' ? colors.darkGray : colors.white,
const themeConfig = useMemo(
() => ({
algorithm: themeMode === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm,
components: {
Layout: {
colorBgLayout: themeMode === 'dark' ? colors.darkGray : colors.white,
headerBg: themeMode === 'dark' ? colors.darkGray : colors.white,
},
Menu: {
colorBgContainer: colors.transparent,
},
Table: {
rowHoverBg: themeMode === 'dark' ? '#000' : '#edebf0',
},
Select: {
controlHeight: 32,
},
},
Menu: {
colorBgContainer: colors.transparent,
token: {
borderRadius: 4,
},
Table: {
rowHoverBg: themeMode === 'dark' ? '#000' : '#edebf0',
},
Select: {
controlHeight: 32,
},
},
token: {
borderRadius: 4,
},
}), [themeMode]);
}),
[themeMode]
);
// Memoize the theme class name
const themeClassName = useMemo(() => `theme-${themeMode}`, [themeMode]);
// Memoize the media query change handler
const handleMediaQueryChange = useCallback((e: MediaQueryListEvent) => {
if (!localStorage.getItem('theme')) {
dispatch(initializeTheme());
}
}, [dispatch]);
const handleMediaQueryChange = useCallback(
(e: MediaQueryListEvent) => {
if (!localStorage.getItem('theme')) {
dispatch(initializeTheme());
}
},
[dispatch]
);
// Initialize theme after mount
useEffect(() => {
@@ -72,9 +78,7 @@ const ThemeWrapper = memo(({ children }: ChildrenProp) => {
return (
<div ref={configRef} className={themeClassName}>
<ConfigProvider theme={themeConfig}>
{children}
</ConfigProvider>
<ConfigProvider theme={themeConfig}>{children}</ConfigProvider>
</div>
);
});

View File

@@ -30,5 +30,6 @@ const timeLogSlice = createSlice({
},
});
export const { toggleTimeLogDrawer, setSelectedLabel, setLabelAndToggleDrawer } = timeLogSlice.actions;
export const { toggleTimeLogDrawer, setSelectedLabel, setLabelAndToggleDrawer } =
timeLogSlice.actions;
export default timeLogSlice.reducer;

View File

@@ -30,4 +30,4 @@ const userSlice = createSlice({
});
export const { changeUserName, setUser } = userSlice.actions;
export default userSlice.reducer;
export default userSlice.reducer;