Add actionable push notifications and fix recurring task completion
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tasks */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Tasks</CardTitle>
|
||||
<CardDescription>All tasks for this property</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoadingTasks ? (
|
||||
<div className="text-muted-foreground">Loading tasks...</div>
|
||||
) : !tasksData?.data || tasksData.data.length === 0 ? (
|
||||
<div className="text-muted-foreground">No tasks for this property</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Title</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Priority</TableHead>
|
||||
<TableHead>Category</TableHead>
|
||||
<TableHead>Due Date</TableHead>
|
||||
<TableHead>Created By</TableHead>
|
||||
<TableHead className="w-[50px]"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{tasksData.data.map((task) => (
|
||||
<TableRow key={task.id}>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
{task.title}
|
||||
{task.is_cancelled && (
|
||||
<Badge variant="destructive" className="text-xs">Cancelled</Badge>
|
||||
)}
|
||||
{task.is_archived && (
|
||||
<Badge variant="secondary" className="text-xs">Archived</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{task.status_name || '-'}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{task.priority_name || '-'}</TableCell>
|
||||
<TableCell>{task.category_name || '-'}</TableCell>
|
||||
<TableCell>
|
||||
{task.due_date
|
||||
? new Date(task.due_date).toLocaleDateString()
|
||||
: '-'}
|
||||
</TableCell>
|
||||
<TableCell>{task.created_by_name}</TableCell>
|
||||
<TableCell>
|
||||
<Button variant="ghost" size="icon" asChild>
|
||||
<Link href={`/tasks/${task.id}`}>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="status_id">Status</Label>
|
||||
<Select
|
||||
value={formData.status_id?.toString() || 'none'}
|
||||
onValueChange={(value) => updateField('status_id', value === 'none' ? undefined : Number(value))}
|
||||
value={formData.status_id !== undefined ? formData.status_id.toString() : 'none'}
|
||||
onValueChange={(value) => {
|
||||
const newValue = value === 'none' ? undefined : Number(value);
|
||||
// Only update if actually different (prevents spurious triggers)
|
||||
if (newValue !== formData.status_id) {
|
||||
updateField('status_id', newValue);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select status" />
|
||||
|
||||
Reference in New Issue
Block a user