From 1b06c0639c383fdf717731a03c3246672c181aef Mon Sep 17 00:00:00 2001 From: Trey t Date: Fri, 5 Dec 2025 14:23:14 -0600 Subject: [PATCH] Add actionable push notifications and fix recurring task completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Features: - Add task action buttons to push notifications (complete, view, cancel, etc.) - Add button types logic for different task states (overdue, in_progress, etc.) - Implement Chain of Responsibility pattern for task categorization - Add comprehensive kanban categorization documentation Fixes: - Reset recurring task status to Pending after completion so tasks appear in correct kanban column (was staying in "In Progress") - Fix PostgreSQL EXTRACT function error in overdue notifications query - Update seed data to properly set next_due_date for recurring tasks Admin: - Add tasks list to residence detail page - Fix task edit page to properly handle all fields πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../(dashboard)/residences/[id]/client.tsx | 82 +- .../app/(dashboard)/tasks/[id]/edit/page.tsx | 19 +- docs/TASK_KANBAN_CATEGORIZATION.md | 308 +++++ internal/admin/handlers/task_handler.go | 49 +- internal/dto/responses/task.go | 49 +- internal/models/task.go | 1 + internal/push/apns.go | 74 ++ internal/push/client.go | 27 + internal/repositories/contractor_repo.go | 3 +- internal/repositories/document_repo.go | 3 +- internal/repositories/residence_repo.go | 3 +- internal/repositories/task_repo.go | 61 +- internal/repositories/task_repo_test.go | 6 +- internal/services/notification_service.go | 87 ++ internal/services/task_button_types.go | 96 ++ internal/services/task_categorization_test.go | 1094 +++++++++++++++++ internal/services/task_service.go | 49 +- internal/services/task_service_test.go | 61 + internal/task/categorization/chain.go | 259 ++++ internal/task/categorization/chain_test.go | 375 ++++++ internal/worker/jobs/handler.go | 2 +- seeds/002_test_data.sql | 149 ++- 22 files changed, 2715 insertions(+), 142 deletions(-) create mode 100644 docs/TASK_KANBAN_CATEGORIZATION.md create mode 100644 internal/services/task_button_types.go create mode 100644 internal/services/task_categorization_test.go create mode 100644 internal/task/categorization/chain.go create mode 100644 internal/task/categorization/chain_test.go diff --git a/admin/src/app/(dashboard)/residences/[id]/client.tsx b/admin/src/app/(dashboard)/residences/[id]/client.tsx index 7822405..efff038 100644 --- a/admin/src/app/(dashboard)/residences/[id]/client.tsx +++ b/admin/src/app/(dashboard)/residences/[id]/client.tsx @@ -3,10 +3,10 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useParams, useRouter } from 'next/navigation'; import Link from 'next/link'; -import { ArrowLeft, Trash2, Pencil } from 'lucide-react'; +import { ArrowLeft, Trash2, Pencil, ExternalLink } from 'lucide-react'; import { toast } from 'sonner'; -import { residencesApi } from '@/lib/api'; +import { residencesApi, tasksApi } from '@/lib/api'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { @@ -17,6 +17,14 @@ import { CardTitle, } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; export function ResidenceDetailClient() { const params = useParams(); @@ -30,6 +38,12 @@ export function ResidenceDetailClient() { enabled: !!residenceId, }); + const { data: tasksData, isLoading: isLoadingTasks } = useQuery({ + queryKey: ['residence-tasks', residenceId], + queryFn: () => tasksApi.list({ residence_id: residenceId, per_page: 100 }), + enabled: !!residenceId, + }); + const deleteMutation = useMutation({ mutationFn: () => residencesApi.delete(residenceId), onSuccess: () => { @@ -217,6 +231,70 @@ export function ResidenceDetailClient() { + + {/* Tasks */} + + + Tasks + All tasks for this property + + + {isLoadingTasks ? ( +
Loading tasks...
+ ) : !tasksData?.data || tasksData.data.length === 0 ? ( +
No tasks for this property
+ ) : ( + + + + Title + Status + Priority + Category + Due Date + Created By + + + + + {tasksData.data.map((task) => ( + + +
+ {task.title} + {task.is_cancelled && ( + Cancelled + )} + {task.is_archived && ( + Archived + )} +
+
+ + {task.status_name || '-'} + + {task.priority_name || '-'} + {task.category_name || '-'} + + {task.due_date + ? new Date(task.due_date).toLocaleDateString() + : '-'} + + {task.created_by_name} + + + +
+ ))} +
+
+ )} +
+
); } diff --git a/admin/src/app/(dashboard)/tasks/[id]/edit/page.tsx b/admin/src/app/(dashboard)/tasks/[id]/edit/page.tsx index 432e5bc..a124c17 100644 --- a/admin/src/app/(dashboard)/tasks/[id]/edit/page.tsx +++ b/admin/src/app/(dashboard)/tasks/[id]/edit/page.tsx @@ -84,8 +84,11 @@ export default function EditTaskPage() { const [formInitialized, setFormInitialized] = useState(false); + // Wait for ALL data including lookups before initializing form + const lookupsLoaded = !categoriesLoading && !prioritiesLoading && !statusesLoading && !frequenciesLoading; + useEffect(() => { - if (task && !formInitialized) { + if (task && lookupsLoaded && !formInitialized) { setFormData({ residence_id: task.residence_id, created_by_id: task.created_by_id, @@ -106,9 +109,9 @@ export default function EditTaskPage() { }); setFormInitialized(true); } - }, [task, formInitialized]); + }, [task, lookupsLoaded, formInitialized]); - const isDataLoading = taskLoading || usersLoading || residencesLoading || categoriesLoading || prioritiesLoading || statusesLoading || frequenciesLoading || !formInitialized; + const isDataLoading = taskLoading || usersLoading || residencesLoading || !lookupsLoaded || !formInitialized; const updateMutation = useMutation({ mutationFn: (data: UpdateTaskRequest) => tasksApi.update(taskId, data), @@ -322,8 +325,14 @@ export default function EditTaskPage() {