修復稽核日誌與任務報表頁面,並將「問題」改名為「任務」 ## Changes 變更內容 ### Bug Fixes 錯誤修復 1. Fixed audit logs page 500 error - Added missing $auditableTypes variable to controller - Changed $events to $actions in view - Added description and ip_address columns to audit_logs table - Updated AuditLog model fillable array 2. Fixed issue reports page SQLite compatibility errors - Replaced MySQL NOW() function with Laravel now() helper - Replaced TIMESTAMPDIFF() with PHP-based date calculation - Fixed request->date() default value handling ### Feature Changes 功能變更 3. Renamed "Issues" terminology to "Tasks" throughout the system - Updated navigation menus (Admin: Issues → Admin: Tasks) - Updated all issue-related views to use task terminology - Changed Chinese labels from "問題" to "任務" - Updated dashboard, issue tracker, and reports pages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
138 lines
5.0 KiB
PHP
138 lines
5.0 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\Issue;
|
|
use App\Models\User;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class IssueReportsController extends Controller
|
|
{
|
|
public function index(Request $request)
|
|
{
|
|
// Date range filter (default: last 30 days)
|
|
$startDate = $request->date('start_date') ?? now()->subDays(30);
|
|
$endDate = $request->date('end_date') ?? now();
|
|
|
|
// Overview Statistics
|
|
$stats = [
|
|
'total_issues' => Issue::count(),
|
|
'open_issues' => Issue::open()->count(),
|
|
'closed_issues' => Issue::closed()->count(),
|
|
'overdue_issues' => Issue::overdue()->count(),
|
|
];
|
|
|
|
// Issues by Status
|
|
$issuesByStatus = Issue::select('status', DB::raw('count(*) as count'))
|
|
->groupBy('status')
|
|
->get()
|
|
->mapWithKeys(fn($item) => [$item->status => $item->count]);
|
|
|
|
// Issues by Priority
|
|
$issuesByPriority = Issue::select('priority', DB::raw('count(*) as count'))
|
|
->groupBy('priority')
|
|
->get()
|
|
->mapWithKeys(fn($item) => [$item->priority => $item->count]);
|
|
|
|
// Issues by Type
|
|
$issuesByType = Issue::select('issue_type', DB::raw('count(*) as count'))
|
|
->groupBy('issue_type')
|
|
->get()
|
|
->mapWithKeys(fn($item) => [$item->issue_type => $item->count]);
|
|
|
|
// Issues Created Over Time (last 30 days)
|
|
$issuesCreatedOverTime = Issue::select(
|
|
DB::raw('DATE(created_at) as date'),
|
|
DB::raw('count(*) as count')
|
|
)
|
|
->whereBetween('created_at', [$startDate, $endDate])
|
|
->groupBy('date')
|
|
->orderBy('date')
|
|
->get();
|
|
|
|
// Issues Closed Over Time (last 30 days)
|
|
$issuesClosedOverTime = Issue::select(
|
|
DB::raw('DATE(closed_at) as date'),
|
|
DB::raw('count(*) as count')
|
|
)
|
|
->whereNotNull('closed_at')
|
|
->whereBetween('closed_at', [$startDate, $endDate])
|
|
->groupBy('date')
|
|
->orderBy('date')
|
|
->get();
|
|
|
|
// Assignee Performance
|
|
$now = now();
|
|
$assigneePerformance = User::select('users.id', 'users.name')
|
|
->leftJoin('issues', 'users.id', '=', 'issues.assigned_to_user_id')
|
|
->selectRaw('count(issues.id) as total_assigned')
|
|
->selectRaw('sum(case when issues.status = ? then 1 else 0 end) as completed', [Issue::STATUS_CLOSED])
|
|
->selectRaw('sum(case when issues.due_date < ? and issues.status != ? then 1 else 0 end) as overdue', [$now, Issue::STATUS_CLOSED])
|
|
->groupBy('users.id', 'users.name')
|
|
->having('total_assigned', '>', 0)
|
|
->orderByDesc('total_assigned')
|
|
->limit(10)
|
|
->get()
|
|
->map(function ($user) {
|
|
$user->completion_rate = $user->total_assigned > 0
|
|
? round(($user->completed / $user->total_assigned) * 100, 1)
|
|
: 0;
|
|
return $user;
|
|
});
|
|
|
|
// Time Tracking Metrics
|
|
$timeTrackingMetrics = Issue::selectRaw('
|
|
sum(estimated_hours) as total_estimated,
|
|
sum(actual_hours) as total_actual,
|
|
avg(estimated_hours) as avg_estimated,
|
|
avg(actual_hours) as avg_actual
|
|
')
|
|
->whereNotNull('estimated_hours')
|
|
->first();
|
|
|
|
// Top Labels Used
|
|
$topLabels = DB::table('issue_labels')
|
|
->select('issue_labels.id', 'issue_labels.name', 'issue_labels.color', DB::raw('count(issue_label_pivot.issue_id) as usage_count'))
|
|
->leftJoin('issue_label_pivot', 'issue_labels.id', '=', 'issue_label_pivot.issue_label_id')
|
|
->groupBy('issue_labels.id', 'issue_labels.name', 'issue_labels.color')
|
|
->having('usage_count', '>', 0)
|
|
->orderByDesc('usage_count')
|
|
->limit(10)
|
|
->get();
|
|
|
|
// Average Resolution Time (days)
|
|
$closedIssues = Issue::whereNotNull('closed_at')
|
|
->select('created_at', 'closed_at')
|
|
->get();
|
|
|
|
$avgResolutionTime = $closedIssues->isNotEmpty()
|
|
? $closedIssues->avg(function ($issue) {
|
|
return $issue->created_at->diffInDays($issue->closed_at);
|
|
})
|
|
: null;
|
|
|
|
// Recent Activity (last 10 issues)
|
|
$recentIssues = Issue::with(['creator', 'assignee'])
|
|
->latest()
|
|
->limit(10)
|
|
->get();
|
|
|
|
return view('admin.issue-reports.index', compact(
|
|
'stats',
|
|
'issuesByStatus',
|
|
'issuesByPriority',
|
|
'issuesByType',
|
|
'issuesCreatedOverTime',
|
|
'issuesClosedOverTime',
|
|
'assigneePerformance',
|
|
'timeTrackingMetrics',
|
|
'topLabels',
|
|
'avgResolutionTime',
|
|
'recentIssues',
|
|
'startDate',
|
|
'endDate'
|
|
));
|
|
}
|
|
}
|