303 lines
9.3 KiB
PHP
303 lines
9.3 KiB
PHP
@php
|
|
/**
|
|
* @api-readonly
|
|
* Returns developer project statistics from GitHub
|
|
*/
|
|
use Illuminate\Support\Facades\Http;
|
|
use Carbon\Carbon;
|
|
|
|
// Configuration
|
|
$token = env('GITHUB_TOKEN');
|
|
$org = 'Truncgil-Techology';
|
|
$projectNumber = 2;
|
|
|
|
if (!$token) {
|
|
echo json_encode(['status' => 'error', 'message' => 'GITHUB_TOKEN not set']);
|
|
exit;
|
|
}
|
|
|
|
// GraphQL Query to fetch Project V2 items and fields
|
|
$query = <<<GRAPHQL
|
|
query {
|
|
organization(login: "$org") {
|
|
projectV2(number: $projectNumber) {
|
|
title
|
|
items(first: 100) {
|
|
nodes {
|
|
fieldValues(first: 20) {
|
|
nodes {
|
|
... on ProjectV2ItemFieldSingleSelectValue {
|
|
name
|
|
field {
|
|
... on ProjectV2FieldCommon {
|
|
name
|
|
}
|
|
}
|
|
}
|
|
... on ProjectV2ItemFieldUserValue {
|
|
users(first: 5) {
|
|
nodes {
|
|
login
|
|
avatarUrl
|
|
name
|
|
}
|
|
}
|
|
field {
|
|
... on ProjectV2FieldCommon {
|
|
name
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
content {
|
|
... on Issue {
|
|
title
|
|
url
|
|
state
|
|
number
|
|
createdAt
|
|
updatedAt
|
|
closedAt
|
|
}
|
|
... on DraftIssue {
|
|
title
|
|
createdAt
|
|
updatedAt
|
|
}
|
|
... on PullRequest {
|
|
title
|
|
url
|
|
state
|
|
number
|
|
createdAt
|
|
updatedAt
|
|
closedAt
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL;
|
|
|
|
try {
|
|
$response = Http::withToken($token)->post('https://api.github.com/graphql', [
|
|
'query' => $query
|
|
]);
|
|
|
|
if ($response->failed()) {
|
|
throw new Exception("GitHub API Error: " . $response->body());
|
|
}
|
|
|
|
$data = $response->json();
|
|
|
|
if (isset($data['errors'])) {
|
|
throw new Exception("GraphQL Error: " . json_encode($data['errors']));
|
|
}
|
|
|
|
$project = $data['data']['organization']['projectV2'] ?? null;
|
|
|
|
if (!$project) {
|
|
throw new Exception("Project not found or access denied.");
|
|
}
|
|
|
|
$items = $project['items']['nodes'];
|
|
|
|
// Data structures for advanced analysis
|
|
$developerStats = [];
|
|
$timelineStats = [
|
|
'weekly' => [],
|
|
'monthly' => []
|
|
];
|
|
$detailedItems = []; // Array for DataGrid
|
|
|
|
$statusMapping = [
|
|
'Done' => 'completed',
|
|
'Ready' => 'active',
|
|
'In progress' => 'active',
|
|
'In review' => 'active',
|
|
'Backlog' => 'pending',
|
|
'No Status' => 'pending'
|
|
];
|
|
|
|
foreach ($items as $item) {
|
|
$status = 'No Status';
|
|
$size = 'No Size';
|
|
$assignees = [];
|
|
|
|
// Parse Field Values
|
|
foreach ($item['fieldValues']['nodes'] as $fieldValue) {
|
|
if (!isset($fieldValue['field']['name'])) continue;
|
|
|
|
$fieldName = $fieldValue['field']['name'];
|
|
|
|
if ($fieldName === 'Status') {
|
|
$status = $fieldValue['name'];
|
|
} elseif ($fieldName === 'Size') {
|
|
$size = $fieldValue['name'];
|
|
} elseif ($fieldName === 'Assignees') {
|
|
foreach ($fieldValue['users']['nodes'] as $user) {
|
|
$assignees[] = $user['login'];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (empty($assignees)) {
|
|
$assignees[] = 'Unassigned';
|
|
}
|
|
|
|
// Content details
|
|
$content = $item['content'] ?? [];
|
|
$title = $content['title'] ?? 'Untitled';
|
|
$url = $content['url'] ?? '#';
|
|
$state = $content['state'] ?? 'OPEN';
|
|
|
|
$createdAt = $content['createdAt'] ?? null;
|
|
$updatedAt = $content['updatedAt'] ?? null;
|
|
$closedAt = $content['closedAt'] ?? null;
|
|
|
|
// Determine completion date
|
|
$completionDate = ($state === 'CLOSED' || $state === 'MERGED' || $status === 'Done')
|
|
? ($closedAt ?? $updatedAt)
|
|
: null;
|
|
|
|
$isCompleted = ($completionDate !== null);
|
|
|
|
// Duration Calculation
|
|
$duration = 'N/A';
|
|
$durationSeconds = 0;
|
|
|
|
if ($createdAt) {
|
|
try {
|
|
$cStart = Carbon::parse($createdAt);
|
|
$endDateStr = $isCompleted ? $completionDate : $updatedAt;
|
|
if (!$endDateStr) $endDateStr = $createdAt;
|
|
|
|
$cEnd = Carbon::parse($endDateStr);
|
|
|
|
$diff = $cStart->diff($cEnd);
|
|
$parts = [];
|
|
if ($diff->y > 0) $parts[] = $diff->y . 'y';
|
|
if ($diff->m > 0) $parts[] = $diff->m . 'mo';
|
|
if ($diff->d > 0) $parts[] = $diff->d . 'd';
|
|
if ($diff->h > 0) $parts[] = $diff->h . 'h';
|
|
if (empty($parts) && $diff->i > 0) $parts[] = $diff->i . 'm';
|
|
if (empty($parts)) $parts[] = '0m';
|
|
|
|
$duration = implode(' ', array_slice($parts, 0, 2));
|
|
$durationSeconds = $cStart->diffInSeconds($cEnd);
|
|
|
|
} catch (Exception $e) {
|
|
$duration = 'Error';
|
|
}
|
|
}
|
|
|
|
// Add to detailed items list (For DataGrid)
|
|
$detailedItems[] = [
|
|
'title' => $title,
|
|
'status' => $status,
|
|
'size' => $size,
|
|
'assignees' => implode(', ', $assignees),
|
|
'url' => $url,
|
|
'duration' => $duration,
|
|
'duration_seconds' => $durationSeconds,
|
|
'created_at' => $createdAt ? Carbon::parse($createdAt)->format('Y-m-d H:i') : '-',
|
|
'end_date' => $completionDate ? Carbon::parse($completionDate)->format('Y-m-d H:i') : '-'
|
|
];
|
|
|
|
// Analyze for each assignee (For Charts/Cards)
|
|
foreach ($assignees as $assignee) {
|
|
if (!isset($developerStats[$assignee])) {
|
|
$developerStats[$assignee] = [
|
|
'total_tasks' => 0,
|
|
'completed_tasks' => 0,
|
|
'active_tasks' => 0,
|
|
'pending_tasks' => 0,
|
|
'sizes' => [],
|
|
'completion_history' => []
|
|
];
|
|
}
|
|
|
|
$developerStats[$assignee]['total_tasks']++;
|
|
|
|
$generalStatus = $statusMapping[$status] ?? 'active';
|
|
|
|
if ($isCompleted) {
|
|
$developerStats[$assignee]['completed_tasks']++;
|
|
|
|
if ($completionDate) {
|
|
$cDate = Carbon::parse($completionDate);
|
|
$monthKey = $cDate->format('Y-m');
|
|
|
|
if (!isset($developerStats[$assignee]['completion_history']['monthly'][$monthKey])) {
|
|
$developerStats[$assignee]['completion_history']['monthly'][$monthKey] = 0;
|
|
}
|
|
$developerStats[$assignee]['completion_history']['monthly'][$monthKey]++;
|
|
}
|
|
} else {
|
|
if ($generalStatus === 'active') {
|
|
$developerStats[$assignee]['active_tasks']++;
|
|
} else {
|
|
$developerStats[$assignee]['pending_tasks']++;
|
|
}
|
|
}
|
|
|
|
if (!isset($developerStats[$assignee]['sizes'][$size])) {
|
|
$developerStats[$assignee]['sizes'][$size] = 0;
|
|
}
|
|
$developerStats[$assignee]['sizes'][$size]++;
|
|
}
|
|
}
|
|
|
|
// Format Developer Stats for Frontend
|
|
$performanceData = [];
|
|
foreach ($developerStats as $assignee => $stats) {
|
|
$performanceData[] = [
|
|
'assignee' => $assignee,
|
|
'total' => $stats['total_tasks'],
|
|
'completed' => $stats['completed_tasks'],
|
|
'active' => $stats['active_tasks'],
|
|
'pending' => $stats['pending_tasks'],
|
|
'completion_rate' => $stats['total_tasks'] > 0 ? round(($stats['completed_tasks'] / $stats['total_tasks']) * 100, 1) : 0,
|
|
'sizes' => $stats['sizes'],
|
|
'history' => $stats['completion_history']
|
|
];
|
|
}
|
|
|
|
// Format Timeline Data
|
|
$timelineChartData = [];
|
|
$allMonths = [];
|
|
foreach ($developerStats as $assignee => $stats) {
|
|
if (isset($stats['completion_history']['monthly'])) {
|
|
$allMonths = array_merge($allMonths, array_keys($stats['completion_history']['monthly']));
|
|
}
|
|
}
|
|
$allMonths = array_unique($allMonths);
|
|
sort($allMonths);
|
|
|
|
foreach ($allMonths as $month) {
|
|
$row = ['month' => $month];
|
|
foreach ($developerStats as $assignee => $stats) {
|
|
$row[$assignee] = $stats['completion_history']['monthly'][$month] ?? 0;
|
|
}
|
|
$timelineChartData[] = $row;
|
|
}
|
|
|
|
echo json_encode([
|
|
'status' => 'success',
|
|
'project_title' => $project['title'],
|
|
'developer_performance' => $performanceData,
|
|
'timeline_chart' => $timelineChartData,
|
|
'raw_items' => $detailedItems, // Restored this field for DataGrid
|
|
'meta' => [
|
|
'total_tasks' => count($items)
|
|
]
|
|
], JSON_UNESCAPED_UNICODE);
|
|
|
|
} catch (Exception $e) {
|
|
echo json_encode(['status' => 'error', 'message' => $e->getMessage()], JSON_UNESCAPED_UNICODE);
|
|
}
|
|
@endphp
|