diff --git a/backend/controllers/add_task.go b/backend/controllers/add_task.go index e0510162..e44aa727 100644 --- a/backend/controllers/add_task.go +++ b/backend/controllers/add_task.go @@ -66,9 +66,10 @@ func AddTaskHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) return } - var dueDateStr string - if dueDate != nil && *dueDate != "" { - dueDateStr = *dueDate + dueDateStr, err := utils.ConvertOptionalISOToTaskwarriorFormat(dueDate) + if err != nil { + http.Error(w, fmt.Sprintf("Invalid due date format: %v", err), http.StatusBadRequest) + return } logStore := models.GetLogStore() diff --git a/backend/controllers/controllers_test.go b/backend/controllers/controllers_test.go index 16127919..cd289aa4 100644 --- a/backend/controllers/controllers_test.go +++ b/backend/controllers/controllers_test.go @@ -135,7 +135,7 @@ func Test_AddTaskHandler_WithDueDate(t *testing.T) { "description": "Test task", "project": "TestProject", "priority": "H", - "due": "2025-12-31", + "due": "2025-12-31T23:59:59.000Z", "tags": []string{"test", "important"}, } diff --git a/backend/utils/datetime.go b/backend/utils/datetime.go new file mode 100644 index 00000000..90d148d0 --- /dev/null +++ b/backend/utils/datetime.go @@ -0,0 +1,48 @@ +package utils + +import ( + "fmt" + "time" +) + +func ConvertISOToTaskwarriorFormat(isoDatetime string) (string, error) { + if isoDatetime == "" { + return "", nil + } + + // Try parsing the specific ISO formats we actually receive from frontend + formats := []string{ + "2006-01-02T15:04:05.000Z", // "2025-12-27T14:30:00.000Z" (frontend datetime with milliseconds) + "2006-01-02T15:04:05Z", // "2025-12-27T14:30:00Z" (datetime without milliseconds) + "2006-01-02", // "2025-12-27" (date only) + } + + var parsedTime time.Time + var err error + var isDateOnly bool + + for i, format := range formats { + parsedTime, err = time.Parse(format, isoDatetime) + if err == nil { + // Check if it's date-only format (last format in array) + isDateOnly = (i == 2) // "2006-01-02" format + break + } + } + + if err != nil { + return "", fmt.Errorf("unable to parse datetime '%s': %v", isoDatetime, err) + } + + if isDateOnly { + return parsedTime.Format("2006-01-02"), nil + } else { + return parsedTime.Format("2006-01-02T15:04:05"), nil + } +} +func ConvertOptionalISOToTaskwarriorFormat(isoDatetime *string) (string, error) { + if isoDatetime == nil || *isoDatetime == "" { + return "", nil + } + return ConvertISOToTaskwarriorFormat(*isoDatetime) +} diff --git a/backend/utils/tw/taskwarrior_test.go b/backend/utils/tw/taskwarrior_test.go index 6c69d581..fb15013d 100644 --- a/backend/utils/tw/taskwarrior_test.go +++ b/backend/utils/tw/taskwarrior_test.go @@ -43,7 +43,7 @@ func TestExportTasks(t *testing.T) { } func TestAddTaskToTaskwarrior(t *testing.T) { - err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "", "H", "2025-03-03", "2025-03-01", "2025-03-01", "2025-03-01", "2025-03-03", "daily", nil, []models.Annotation{{Description: "note"}}, []string{}) + err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "", "H", "2025-03-03T10:30:00", "2025-03-01", "2025-03-01", "2025-03-01", "2025-03-03", "daily", []string{}, []models.Annotation{{Description: "note"}}, []string{}) if err != nil { t.Errorf("AddTaskToTaskwarrior failed: %v", err) } else { @@ -52,7 +52,7 @@ func TestAddTaskToTaskwarrior(t *testing.T) { } func TestAddTaskToTaskwarriorWithWaitDate(t *testing.T) { - err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "project", "H", "2025-03-03", "2025-03-04", "2025-03-04", "2025-03-04", "2025-03-04", "", nil, []models.Annotation{}) + err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "project", "H", "2025-03-03T14:00:00", "2025-03-04", "2025-03-04", "2025-03-04", "2025-03-04", "", []string{}, []models.Annotation{}, []string{}) if err != nil { t.Errorf("AddTaskToTaskwarrior with wait date failed: %v", err) } else { @@ -61,7 +61,7 @@ func TestAddTaskToTaskwarriorWithWaitDate(t *testing.T) { } func TestAddTaskToTaskwarriorWithEntryDate(t *testing.T) { - err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "project", "H", "2025-03-05", "2025-03-04", "2025-03-04", "2025-03-04", "2025-03-10", "", nil, nil) + err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "project", "H", "2025-03-05T16:30:00", "2025-03-04", "2025-03-04", "2025-03-04", "2025-03-10", "", []string{}, []models.Annotation{}, []string{}) if err != nil { t.Errorf("AddTaskToTaskwarrior failed: %v", err) } else { @@ -79,7 +79,7 @@ func TestCompleteTaskInTaskwarrior(t *testing.T) { } func TestAddTaskWithTags(t *testing.T) { - err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "", "H", "2025-03-03", "2025-03-01", "2025-03-01", "2025-03-01", "2025-03-03", "daily", []string{"work", "important"}, []models.Annotation{{Description: "note"}}, []string{}) + err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "", "H", "2025-03-03T15:45:00", "2025-03-01", "2025-03-01", "2025-03-01", "2025-03-03", "daily", []string{"work", "important"}, []models.Annotation{{Description: "note"}}, []string{}) if err != nil { t.Errorf("AddTaskToTaskwarrior with tags failed: %v", err) } else { @@ -88,7 +88,7 @@ func TestAddTaskWithTags(t *testing.T) { } func TestAddTaskToTaskwarriorWithEntryDateAndTags(t *testing.T) { - err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "project", "H", "2025-03-05", "2025-03-04", "2025-03-04", "2025-03-04", "2025-03-10", "", []string{"work", "important"}, nil) + err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "project", "H", "2025-03-05T16:00:00", "2025-03-04", "2025-03-04", "2025-03-04", "2025-03-10", "", []string{"work", "important"}, []models.Annotation{}, []string{}) if err != nil { t.Errorf("AddTaskToTaskwarrior with entry date and tags failed: %v", err) } else { @@ -97,7 +97,7 @@ func TestAddTaskToTaskwarriorWithEntryDateAndTags(t *testing.T) { } func TestAddTaskToTaskwarriorWithWaitDateWithTags(t *testing.T) { - err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "project", "H", "2025-03-03", "2025-03-04", "2025-03-04", "2025-03-04", "2025-03-04", "", []string{"work", "important"}, []models.Annotation{}) + err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "project", "H", "2025-03-03T14:30:00", "2025-03-04", "2025-03-04", "2025-03-04", "2025-03-04", "", []string{"work", "important"}, []models.Annotation{}, []string{}) if err != nil { t.Errorf("AddTaskToTaskwarrior with wait date failed: %v", err) } else { diff --git a/backend/utils/utils_test.go b/backend/utils/utils_test.go index b8fe26f3..609bb9c8 100644 --- a/backend/utils/utils_test.go +++ b/backend/utils/utils_test.go @@ -98,3 +98,56 @@ func Test_ValidateDependencies_EmptyList(t *testing.T) { err := ValidateDependencies(depends, currentTaskUUID) assert.NoError(t, err) } + +func TestConvertISOToTaskwarriorFormat(t *testing.T) { + tests := []struct { + name string + input string + expected string + hasError bool + }{ + { + name: "ISO datetime with milliseconds (frontend format)", + input: "2025-12-27T14:30:00.000Z", + expected: "2025-12-27T14:30:00", + hasError: false, + }, + { + name: "ISO datetime at midnight (explicit datetime)", + input: "2025-12-27T00:00:00.000Z", + expected: "2025-12-27T00:00:00", + hasError: false, + }, + { + name: "Date only format", + input: "2025-12-27", + expected: "2025-12-27", + hasError: false, + }, + { + name: "Empty string", + input: "", + expected: "", + hasError: false, + }, + { + name: "Invalid format", + input: "invalid-date", + expected: "", + hasError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := ConvertISOToTaskwarriorFormat(tt.input) + + if tt.hasError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} diff --git a/frontend/src/components/HomeComponents/Tasks/AddTaskDialog.tsx b/frontend/src/components/HomeComponents/Tasks/AddTaskDialog.tsx index 29309864..485f6c5b 100644 --- a/frontend/src/components/HomeComponents/Tasks/AddTaskDialog.tsx +++ b/frontend/src/components/HomeComponents/Tasks/AddTaskDialog.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { DatePicker } from '@/components/ui/date-picker'; +import { DateTimePicker } from '@/components/ui/date-time-picker'; import { Dialog, DialogContent, @@ -255,15 +256,27 @@ export const AddTaskdialog = ({ Due