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:
@@ -23,6 +23,10 @@ import {
|
|||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
Clock,
|
Clock,
|
||||||
|
PlayCircle,
|
||||||
|
XCircle,
|
||||||
|
Archive,
|
||||||
|
PauseCircle,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
@@ -89,7 +93,7 @@ export default function DashboardPage() {
|
|||||||
{isLoading ? '-' : stats?.tasks.total.toLocaleString()}
|
{isLoading ? '-' : stats?.tasks.total.toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<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>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -178,35 +182,79 @@ export default function DashboardPage() {
|
|||||||
<ClipboardList className="h-5 w-5" />
|
<ClipboardList className="h-5 w-5" />
|
||||||
Task Status Overview
|
Task Status Overview
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>Current task distribution</CardDescription>
|
<CardDescription>
|
||||||
|
{isLoading ? 'Loading...' : `${stats?.tasks.total} total tasks across all residences`}
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<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">
|
<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>
|
||||||
<div className="text-2xl font-bold text-yellow-700">
|
<div className="text-xl font-bold text-yellow-700">
|
||||||
{isLoading ? '-' : stats?.tasks.pending}
|
{isLoading ? '-' : stats?.tasks.pending}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-yellow-600">Pending</div>
|
<div className="text-sm text-yellow-600">Pending</div>
|
||||||
</div>
|
</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>
|
||||||
<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}
|
{isLoading ? '-' : stats?.tasks.completed}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-green-600">Completed</div>
|
<div className="text-sm text-green-600">Completed</div>
|
||||||
</div>
|
</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" />
|
{/* Archived and Cancelled - smaller at bottom */}
|
||||||
<div>
|
<div className="flex items-center justify-between p-2 bg-gray-100 rounded-lg col-span-2">
|
||||||
<div className="text-2xl font-bold text-red-700">
|
<div className="flex items-center gap-2">
|
||||||
{isLoading ? '-' : stats?.tasks.overdue}
|
<XCircle className="h-4 w-4 text-gray-400" />
|
||||||
</div>
|
<span className="text-sm text-gray-500">
|
||||||
<div className="text-sm text-red-600">Overdue Tasks</div>
|
{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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -517,9 +517,15 @@ export interface DashboardStats {
|
|||||||
};
|
};
|
||||||
tasks: {
|
tasks: {
|
||||||
total: number;
|
total: number;
|
||||||
pending: number;
|
active: number;
|
||||||
completed: number;
|
archived: number;
|
||||||
|
cancelled: number;
|
||||||
overdue: number;
|
overdue: number;
|
||||||
|
new_30d: number;
|
||||||
|
pending: number;
|
||||||
|
in_progress: number;
|
||||||
|
completed: number;
|
||||||
|
on_hold: number;
|
||||||
};
|
};
|
||||||
contractors: {
|
contractors: {
|
||||||
total: number;
|
total: number;
|
||||||
|
|||||||
@@ -48,10 +48,24 @@ type ResidenceStats struct {
|
|||||||
|
|
||||||
// TaskStats holds task-related statistics
|
// TaskStats holds task-related statistics
|
||||||
type TaskStats struct {
|
type TaskStats struct {
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
Pending int64 `json:"pending"`
|
Active int64 `json:"active"` // Non-archived, non-cancelled
|
||||||
Completed int64 `json:"completed"`
|
Archived int64 `json:"archived"`
|
||||||
Overdue int64 `json:"overdue"`
|
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
|
// ContractorStats holds contractor-related statistics
|
||||||
@@ -103,17 +117,41 @@ func (h *AdminDashboardHandler) GetStats(c *gin.Context) {
|
|||||||
|
|
||||||
// Task stats
|
// Task stats
|
||||||
h.db.Model(&models.Task{}).Count(&stats.Tasks.Total)
|
h.db.Model(&models.Task{}).Count(&stats.Tasks.Total)
|
||||||
h.db.Model(&models.Task{}).Where("is_cancelled = ? AND is_archived = ?", false, false).
|
h.db.Model(&models.Task{}).Where("is_cancelled = ? AND is_archived = ?", false, false).Count(&stats.Tasks.Active)
|
||||||
Joins("JOIN task_taskstatus ON task_taskstatus.id = task_task.status_id").
|
h.db.Model(&models.Task{}).Where("is_archived = ?", true).Count(&stats.Tasks.Archived)
|
||||||
Where("task_taskstatus.name IN ?", []string{"pending", "in_progress"}).
|
h.db.Model(&models.Task{}).Where("is_cancelled = ?", true).Count(&stats.Tasks.Cancelled)
|
||||||
Count(&stats.Tasks.Pending)
|
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{}).
|
h.db.Model(&models.Task{}).
|
||||||
Joins("JOIN task_taskstatus ON task_taskstatus.id = task_task.status_id").
|
Where("is_cancelled = ? AND is_archived = ?", false, false).
|
||||||
Where("task_taskstatus.name = ?", "completed").
|
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)
|
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").
|
h.db.Model(&models.Task{}).
|
||||||
Where("task_taskstatus.name NOT IN ?", []string{"completed", "cancelled"}).
|
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)
|
Count(&stats.Tasks.Overdue)
|
||||||
|
|
||||||
// Contractor stats
|
// Contractor stats
|
||||||
|
|||||||
Reference in New Issue
Block a user