expand sub tasks
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -16,7 +16,6 @@ const initialState: IProjectDrawerState = {
|
||||
project: null,
|
||||
};
|
||||
|
||||
|
||||
export const fetchProjectData = createAsyncThunk(
|
||||
'project/fetchProjectData',
|
||||
async (projectId: string, { rejectWithValue, dispatch }) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -39,4 +39,4 @@ export const updateProject = (payload: UpdateProjectPayload) => async (dispatch:
|
||||
}
|
||||
};
|
||||
|
||||
export default projectsSlice.reducer;
|
||||
export default projectsSlice.reducer;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -75,7 +75,7 @@ const PhaseDrawer = () => {
|
||||
|
||||
const handleAddOptions = async () => {
|
||||
if (!projectId) return;
|
||||
|
||||
|
||||
await dispatch(addPhaseOption({ projectId: projectId }));
|
||||
await dispatch(fetchPhasesByProjectId(projectId));
|
||||
await refreshTasks();
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 ?? '';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -62,7 +62,6 @@ export const editTeamName = createAsyncThunk(
|
||||
} catch (error) {
|
||||
logger.error('Edit Team Name', error);
|
||||
|
||||
|
||||
if (error instanceof Error) {
|
||||
return rejectWithValue(error.message);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -30,4 +30,4 @@ const userSlice = createSlice({
|
||||
});
|
||||
|
||||
export const { changeUserName, setUser } = userSlice.actions;
|
||||
export default userSlice.reducer;
|
||||
export default userSlice.reducer;
|
||||
|
||||
Reference in New Issue
Block a user