Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion apiserver/internal/apis/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,16 @@ func (h *TasksAPIHandler) completeTask(c *gin.Context) {
return
}

status, response := h.tService.CompleteTask(c, currentIdentity.UserID, id)
endRecurrenceStr := c.DefaultQuery("endRecurrence", "false")
endRecurrence, err := strconv.ParseBool(endRecurrenceStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid endRecurrence value",
})
return
}

status, response := h.tService.CompleteTask(c, currentIdentity.UserID, id, endRecurrence)
c.JSON(status, response)
}

Expand Down
11 changes: 7 additions & 4 deletions apiserver/internal/services/tasks/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,16 +157,19 @@ func (h *TasksMessageHandler) updateDueDate(ctx context.Context, userID int, msg
}

func (h *TasksMessageHandler) completeTask(ctx context.Context, userID int, msg ws.WSMessage) *ws.WSResponse {
var id int
if err := json.Unmarshal(msg.Data, &id); err != nil {
var req struct {
ID int `json:"id"`
EndRecurrence bool `json:"endRecurrence"`
}
if err := json.Unmarshal(msg.Data, &req); err != nil {
return &ws.WSResponse{
Status: http.StatusBadRequest,
Data: gin.H{
"error": "Invalid task ID",
"error": "Invalid request data",
},
}
}
status, response := h.ts.CompleteTask(ctx, userID, id)
status, response := h.ts.CompleteTask(ctx, userID, req.ID, req.EndRecurrence)
return &ws.WSResponse{
Status: status,
Data: response,
Expand Down
17 changes: 10 additions & 7 deletions apiserver/internal/services/tasks/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ func (s *TaskService) UpdateDueDate(ctx context.Context, userID, taskID int, req
}
}

func (s *TaskService) CompleteTask(ctx context.Context, userID, taskID int) (int, interface{}) {
func (s *TaskService) CompleteTask(ctx context.Context, userID, taskID int, endRecurrence bool) (int, interface{}) {
log := logging.FromContext(ctx)

task, err := s.t.GetTask(ctx, taskID)
Expand All @@ -394,12 +394,15 @@ func (s *TaskService) CompleteTask(ctx context.Context, userID, taskID int) (int
}

var completedDate time.Time = time.Now().UTC()
var nextDueDate *time.Time
nextDueDate, err = tRepo.ScheduleNextDueDate(task, completedDate)
if err != nil {
log.Errorf("error scheduling next due date: %s", err.Error())
return http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("Error scheduling next due date: %s", err),
var nextDueDate *time.Time = nil

if !endRecurrence {
nextDueDate, err = tRepo.ScheduleNextDueDate(task, completedDate)
if err != nil {
log.Errorf("error scheduling next due date: %s", err.Error())
return http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("Error scheduling next due date: %s", err),
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/api/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export const GetTasks = async (): Promise<TasksResponse> =>
export const GetCompletedTasks = async (): Promise<TasksResponse> =>
await Request<TasksResponse>(`/tasks/completed`)

export const MarkTaskComplete = async (id: number): Promise<SingleTaskResponse> =>
await Request<SingleTaskResponse>(`/tasks/${id}/do`, 'POST')
export const MarkTaskComplete = async (id: number, endRecurrence: boolean): Promise<SingleTaskResponse> =>
await Request<SingleTaskResponse>(`/tasks/${id}/do?endRecurrence=${endRecurrence}`, 'POST')
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The boolean parameter endRecurrence should be URL-encoded to prevent potential issues. Boolean values in query strings can be misinterpreted if not properly encoded. Consider using encodeURIComponent() or a URL building library to ensure proper encoding:

export const MarkTaskComplete = async (id: number, endRecurrence: boolean): Promise<SingleTaskResponse> =>
    await Request<SingleTaskResponse>(`/tasks/${id}/do?endRecurrence=${encodeURIComponent(endRecurrence)}`, 'POST')
Suggested change
await Request<SingleTaskResponse>(`/tasks/${id}/do?endRecurrence=${endRecurrence}`, 'POST')
await Request<SingleTaskResponse>(`/tasks/${id}/do?endRecurrence=${encodeURIComponent(endRecurrence)}`, 'POST')

Copilot uses AI. Check for mistakes.

export const SkipTask = async (id: number): Promise<SingleTaskResponse> =>
await Request<SingleTaskResponse>(`/tasks/${id}/skip`, 'POST')
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/store/tasksSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ export const fetchCompletedTasks = createAsyncThunk(

export const completeTask = createAsyncThunk(
'tasks/completeTask',
async (taskId: number) => {
const response = await MarkTaskComplete(taskId)
async (req: { taskId: number, endRecurrence: boolean }) => {
const response = await MarkTaskComplete(req.taskId, req.endRecurrence)
return response.task
},
)
Expand Down
31 changes: 26 additions & 5 deletions frontend/src/views/Tasks/MyTasks.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { skipTask, deleteTask, updateDueDate, setGroupBy, toggleGroup, toggleShowCompleted, fetchCompletedTasks } from '@/store/tasksSlice'
import { skipTask, deleteTask, updateDueDate, setGroupBy, toggleGroup, toggleShowCompleted, fetchCompletedTasks, completeTask } from '@/store/tasksSlice'
import { Task, TASK_UPDATE_EVENT } from '@/models/task'
import {
ExpandCircleDown,
Expand All @@ -12,6 +12,7 @@ import {
SwitchAccessShortcut,
Visibility,
VisibilityOff,
PublishedWithChanges,
} from '@mui/icons-material'
import {
Container,
Expand Down Expand Up @@ -54,6 +55,7 @@ type MyTasksProps = {
toggleShowCompleted: () => void
fetchCompletedTasks: () => Promise<any>

completeTask: (taskId: number, endRecurrence: boolean) => Promise<any>
deleteTask: (taskId: number) => Promise<any>
skipTask: (taskId: number) => Promise<any>
updateDueDate: (taskId: number, dueDate: string) => Promise<any>
Expand Down Expand Up @@ -187,6 +189,18 @@ class MyTasksImpl extends React.Component<MyTasksProps, MyTasksState> {
this.props.navigate(NavigationPaths.TaskHistory(task.id))
}

private onCompleteAndStopRecurrenceClicked = async () => {
const { contextMenuTask: task } = this.state
if (task === null) {
throw new Error('Attempted to complete and stop recurrence without a reference')
}

await this.props.completeTask(task.id, true)

this.dismissMoreMenu()
this.onEvent('completed')
}

private onSkipTaskClicked = async () => {
const { contextMenuTask: task } = this.state
if (task === null) {
Expand Down Expand Up @@ -384,10 +398,16 @@ class MyTasksImpl extends React.Component<MyTasksProps, MyTasksState> {
ref={this.menuRef}
>
{contextMenuTask && contextMenuTask.frequency.type !== 'once' && (
<MenuItem onClick={this.onSkipTaskClicked}>
<SwitchAccessShortcut />
Skip to next due date
</MenuItem>
<>
<MenuItem onClick={this.onCompleteAndStopRecurrenceClicked}>
<PublishedWithChanges />
Complete and end recurrence
</MenuItem>
<MenuItem onClick={this.onSkipTaskClicked}>
<SwitchAccessShortcut />
Skip to next due date
</MenuItem>
</>
)}
<MenuItem onClick={this.onChangeDueDateClicked}>
<MoreTime />
Expand Down Expand Up @@ -511,6 +531,7 @@ const mapDispatchToProps = (dispatch: AppDispatch) => ({
toggleShowCompleted: () => dispatch(toggleShowCompleted()),
fetchCompletedTasks: () => dispatch(fetchCompletedTasks()),

completeTask: (taskId: number, endRecurrence: boolean) => dispatch(completeTask({ taskId, endRecurrence })),
deleteTask: (taskId: number) => dispatch(deleteTask(taskId)),
skipTask: (taskId: number) => dispatch(skipTask(taskId)),
updateDueDate: (taskId: number, dueDate: string) => dispatch(updateDueDate({ taskId, dueDate })),
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/views/Tasks/TaskCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { TaskUI } from '@/utils/marshalling'
type TaskCardProps = WithNavigate & {
task: TaskUI

completeTask: (taskId: number) => Promise<any>
completeTask: (taskId: number, endRecurrence: boolean) => Promise<any>
onTaskUpdate: (event: TASK_UPDATE_EVENT) => void
onContextMenu: (event: React.MouseEvent<HTMLDivElement>, task: TaskUI) => void
}
Expand All @@ -46,7 +46,7 @@ class TaskCardImpl extends React.Component<TaskCardProps> {

private handleTaskCompletion = async () => {
const { task, onTaskUpdate } = this.props
await this.props.completeTask(task.id)
await this.props.completeTask(task.id, false)

// Play the task completion sound
playSound(SoundEffect.TaskComplete)
Expand Down Expand Up @@ -234,7 +234,7 @@ class TaskCardImpl extends React.Component<TaskCardProps> {
}

const mapDispatchToProps = (dispatch: AppDispatch) => ({
completeTask: (taskId: number) => dispatch(completeTask(taskId)),
completeTask: (taskId: number, endRecurrence: boolean) => dispatch(completeTask({ taskId, endRecurrence })),
})

export const TaskCard = connect(
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/views/Tasks/TasksOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ type TasksOverviewProps = {
filterTasks: (searchQuery: string) => void
toggleShowCompleted: () => void
fetchCompletedTasks: () => Promise<any>
completeTask: (taskId: number) => Promise<any>
completeTask: (taskId: number, endRecurrence: boolean) => Promise<any>
deleteTask: (taskId: number) => Promise<any>
updateDueDate: (taskId: number, dueDate: string) => Promise<any>
pushStatus: (status: Status) => void
Expand Down Expand Up @@ -91,7 +91,7 @@ class TasksOverviewImpl extends React.Component<TasksOverviewProps> {
}

private onCompleteTaskClicked = (task: TaskUI) => async () => {
await this.props.completeTask(task.id)
await this.props.completeTask(task.id, false)

playSound(SoundEffect.TaskComplete)
this.props.pushStatus({
Expand Down Expand Up @@ -445,7 +445,7 @@ const mapDispatchToProps = (dispatch: AppDispatch) => ({
filterTasks: (searchQuery: string) => dispatch(filterTasks(searchQuery)),
toggleShowCompleted: () => dispatch(toggleShowCompleted()),
fetchCompletedTasks: () => dispatch(fetchCompletedTasks()),
completeTask: (taskId: number) => dispatch(completeTask(taskId)),
completeTask: (taskId: number, endRecurrence: boolean) => dispatch(completeTask({ taskId, endRecurrence })),
deleteTask: (taskId: number) => dispatch(deleteTask(taskId)),
updateDueDate: (taskId: number, dueDate: string) => dispatch(updateDueDate({ taskId, dueDate })),
pushStatus: (status: Status) => dispatch(pushStatus(status)),
Expand Down
Loading