Improve admin dashboard task status overview

- Add all task statuses: Pending, In Progress, On Hold, Completed
- Add active, archived, cancelled, and new_30d task counts
- Fix completed query to filter non-archived/non-cancelled tasks
- Update dashboard UI with better layout showing all statuses
- Show cancelled and archived counts in footer row

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-06 12:09:27 -06:00
parent 3b448abcbd
commit 83384db014
3 changed files with 122 additions and 30 deletions

View File

@@ -23,6 +23,10 @@ import {
AlertTriangle,
CheckCircle,
Clock,
PlayCircle,
XCircle,
Archive,
PauseCircle,
} from 'lucide-react';
export default function DashboardPage() {
@@ -89,7 +93,7 @@ export default function DashboardPage() {
{isLoading ? '-' : stats?.tasks.total.toLocaleString()}
</div>
<p className="text-xs text-muted-foreground">
{isLoading ? '' : `${stats?.tasks.pending} pending, ${stats?.tasks.completed} completed`}
{isLoading ? '' : `${stats?.tasks.active} active, ${stats?.tasks.new_30d} new this month`}
</p>
</CardContent>
</Card>
@@ -178,35 +182,79 @@ export default function DashboardPage() {
<ClipboardList className="h-5 w-5" />
Task Status Overview
</CardTitle>
<CardDescription>Current task distribution</CardDescription>
<CardDescription>
{isLoading ? 'Loading...' : `${stats?.tasks.total} total tasks across all residences`}
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-2 gap-3">
{/* Overdue - Highlighted at top */}
{!isLoading && (stats?.tasks.overdue ?? 0) > 0 && (
<div className="flex items-center gap-3 p-3 bg-red-50 rounded-lg col-span-2 border border-red-200">
<AlertTriangle className="h-8 w-8 text-red-600" />
<div>
<div className="text-2xl font-bold text-red-700">
{stats?.tasks.overdue}
</div>
<div className="text-sm text-red-600">Overdue</div>
</div>
</div>
)}
{/* Active statuses */}
<div className="flex items-center gap-3 p-3 bg-yellow-50 rounded-lg">
<Clock className="h-8 w-8 text-yellow-600" />
<Clock className="h-6 w-6 text-yellow-600" />
<div>
<div className="text-2xl font-bold text-yellow-700">
<div className="text-xl font-bold text-yellow-700">
{isLoading ? '-' : stats?.tasks.pending}
</div>
<div className="text-sm text-yellow-600">Pending</div>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-green-50 rounded-lg">
<CheckCircle className="h-8 w-8 text-green-600" />
<div className="flex items-center gap-3 p-3 bg-blue-50 rounded-lg">
<PlayCircle className="h-6 w-6 text-blue-600" />
<div>
<div className="text-2xl font-bold text-green-700">
<div className="text-xl font-bold text-blue-700">
{isLoading ? '-' : stats?.tasks.in_progress}
</div>
<div className="text-sm text-blue-600">In Progress</div>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-orange-50 rounded-lg">
<PauseCircle className="h-6 w-6 text-orange-600" />
<div>
<div className="text-xl font-bold text-orange-700">
{isLoading ? '-' : stats?.tasks.on_hold}
</div>
<div className="text-sm text-orange-600">On Hold</div>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-green-50 rounded-lg">
<CheckCircle className="h-6 w-6 text-green-600" />
<div>
<div className="text-xl font-bold text-green-700">
{isLoading ? '-' : stats?.tasks.completed}
</div>
<div className="text-sm text-green-600">Completed</div>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-red-50 rounded-lg col-span-2">
<AlertTriangle className="h-8 w-8 text-red-600" />
<div>
<div className="text-2xl font-bold text-red-700">
{isLoading ? '-' : stats?.tasks.overdue}
</div>
<div className="text-sm text-red-600">Overdue Tasks</div>
{/* Archived and Cancelled - smaller at bottom */}
<div className="flex items-center justify-between p-2 bg-gray-100 rounded-lg col-span-2">
<div className="flex items-center gap-2">
<XCircle className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-500">
{isLoading ? '-' : stats?.tasks.cancelled} cancelled
</span>
</div>
<div className="flex items-center gap-2">
<Archive className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-500">
{isLoading ? '-' : stats?.tasks.archived} archived
</span>
</div>
</div>
</div>

View File

@@ -517,9 +517,15 @@ export interface DashboardStats {
};
tasks: {
total: number;
pending: number;
completed: number;
active: number;
archived: number;
cancelled: number;
overdue: number;
new_30d: number;
pending: number;
in_progress: number;
completed: number;
on_hold: number;
};
contractors: {
total: number;

View File

@@ -48,10 +48,24 @@ type ResidenceStats struct {
// TaskStats holds task-related statistics
type TaskStats struct {
Total int64 `json:"total"`
Pending int64 `json:"pending"`
Completed int64 `json:"completed"`
Overdue int64 `json:"overdue"`
Total int64 `json:"total"`
Active int64 `json:"active"` // Non-archived, non-cancelled
Archived int64 `json:"archived"`
Cancelled int64 `json:"cancelled"`
Overdue int64 `json:"overdue"`
New30d int64 `json:"new_30d"`
// By status
Pending int64 `json:"pending"`
InProgress int64 `json:"in_progress"`
Completed int64 `json:"completed"`
OnHold int64 `json:"on_hold"`
}
// TaskStatusCount holds a single status count
type TaskStatusCount struct {
StatusID uint `json:"status_id"`
StatusName string `json:"status_name"`
Count int64 `json:"count"`
}
// ContractorStats holds contractor-related statistics
@@ -103,17 +117,41 @@ func (h *AdminDashboardHandler) GetStats(c *gin.Context) {
// Task stats
h.db.Model(&models.Task{}).Count(&stats.Tasks.Total)
h.db.Model(&models.Task{}).Where("is_cancelled = ? AND is_archived = ?", false, false).
Joins("JOIN task_taskstatus ON task_taskstatus.id = task_task.status_id").
Where("task_taskstatus.name IN ?", []string{"pending", "in_progress"}).
Count(&stats.Tasks.Pending)
h.db.Model(&models.Task{}).Where("is_cancelled = ? AND is_archived = ?", false, false).Count(&stats.Tasks.Active)
h.db.Model(&models.Task{}).Where("is_archived = ?", true).Count(&stats.Tasks.Archived)
h.db.Model(&models.Task{}).Where("is_cancelled = ?", true).Count(&stats.Tasks.Cancelled)
h.db.Model(&models.Task{}).Where("created_at >= ?", thirtyDaysAgo).Count(&stats.Tasks.New30d)
// Task counts by status (using LEFT JOIN to handle tasks with no status)
h.db.Model(&models.Task{}).
Joins("JOIN task_taskstatus ON task_taskstatus.id = task_task.status_id").
Where("task_taskstatus.name = ?", "completed").
Where("is_cancelled = ? AND is_archived = ?", false, false).
Joins("LEFT JOIN task_taskstatus ON task_taskstatus.id = task_task.status_id").
Where("LOWER(task_taskstatus.name) = ? OR task_taskstatus.id IS NULL", "pending").
Count(&stats.Tasks.Pending)
h.db.Model(&models.Task{}).
Where("is_cancelled = ? AND is_archived = ?", false, false).
Joins("LEFT JOIN task_taskstatus ON task_taskstatus.id = task_task.status_id").
Where("LOWER(task_taskstatus.name) = ?", "in progress").
Count(&stats.Tasks.InProgress)
h.db.Model(&models.Task{}).
Where("is_cancelled = ? AND is_archived = ?", false, false).
Joins("LEFT JOIN task_taskstatus ON task_taskstatus.id = task_task.status_id").
Where("LOWER(task_taskstatus.name) = ?", "completed").
Count(&stats.Tasks.Completed)
h.db.Model(&models.Task{}).Where("due_date < ? AND is_cancelled = ? AND is_archived = ?", now, false, false).
Joins("JOIN task_taskstatus ON task_taskstatus.id = task_task.status_id").
Where("task_taskstatus.name NOT IN ?", []string{"completed", "cancelled"}).
h.db.Model(&models.Task{}).
Where("is_cancelled = ? AND is_archived = ?", false, false).
Joins("LEFT JOIN task_taskstatus ON task_taskstatus.id = task_task.status_id").
Where("LOWER(task_taskstatus.name) = ?", "on hold").
Count(&stats.Tasks.OnHold)
// Overdue: past due date, not completed, not cancelled, not archived
h.db.Model(&models.Task{}).
Where("next_due_date < ? AND is_cancelled = ? AND is_archived = ?", now, false, false).
Joins("LEFT JOIN task_taskstatus ON task_taskstatus.id = task_task.status_id").
Where("LOWER(task_taskstatus.name) NOT IN ? OR task_taskstatus.id IS NULL", []string{"completed", "cancelled"}).
Count(&stats.Tasks.Overdue)
// Contractor stats