Updated Google Scholar Integration,Updated Admin and HOD Dashboard, Added Google Auth: Login Controller and Routes
This commit is contained in:
@@ -15,7 +15,9 @@ class GoogleController extends Controller
|
||||
*/
|
||||
public function redirectToGoogle()
|
||||
{
|
||||
return Socialite::driver('google')->redirect();
|
||||
// Prefer to hint Google to show accounts from the somaiya.edu domain.
|
||||
// This is only a UI hint — always validate the domain on the callback.
|
||||
return Socialite::driver('google')->with(['hd' => 'somaiya.edu'])->redirect();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,10 +28,20 @@ class GoogleController extends Controller
|
||||
try {
|
||||
$googleUser = Socialite::driver('google')->user();
|
||||
|
||||
// Validate that the user belongs to the somaiya.edu domain.
|
||||
// Google may return a 'hd' (hosted domain) claim; fall back to parsing the email.
|
||||
$email = $googleUser->getEmail();
|
||||
$hostedDomain = data_get($googleUser->user, 'hd');
|
||||
$domain = $hostedDomain ?: (strpos($email, '@') !== false ? substr(strrchr($email, "@"), 1) : null);
|
||||
|
||||
if ($domain !== 'somaiya.edu') {
|
||||
return redirect()->route('login')->withErrors(['error' => 'Please sign in using a somaiya.edu account.']);
|
||||
}
|
||||
|
||||
$user = User::firstOrCreate(
|
||||
['email' => $googleUser->getEmail()],
|
||||
['email' => $email],
|
||||
[
|
||||
'name' => $googleUser->getName(),
|
||||
'name' => $googleUser->getName() ?: $email,
|
||||
'password' => bcrypt(Str::random(16)), // Generate a random password
|
||||
]
|
||||
);
|
||||
|
||||
@@ -2,57 +2,392 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Yajra\DataTables\Facades\DataTables;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Models\ActivitiesAttended;
|
||||
use App\Models\ActivitiesOrganised;
|
||||
use App\Models\BooksPublished;
|
||||
use App\Models\Department;
|
||||
use App\Models\ExternalEngagement;
|
||||
use App\Models\IvOrganised;
|
||||
use App\Models\OnlineCourse;
|
||||
use App\Models\Patent;
|
||||
use App\Models\Publication;
|
||||
use App\Services\ProofDownloadService;
|
||||
use Illuminate\Http\Request;
|
||||
// use Yajra\DataTables\Facades\DataTables;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class CoordinatorController extends Controller
|
||||
{
|
||||
// Coordinator dashboard (optional)
|
||||
public function index()
|
||||
{
|
||||
return view('coordinator.dashboard');
|
||||
$user = Auth::user();
|
||||
$departments = Department::where('id', $user->department_id)->get();
|
||||
|
||||
// Collect distinct years from all relevant date fields
|
||||
$years = collect([
|
||||
ActivitiesAttended::where('department_id', $user->department_id) ->selectRaw('YEAR(end_date) as year')->distinct()->pluck('year'),
|
||||
|
||||
ActivitiesOrganised::where('department_id', $user->department_id) ->selectRaw('YEAR(end_date) as year')->distinct()->pluck('year'),
|
||||
|
||||
BooksPublished::where('department_id', $user->department_id) ->selectRaw('YEAR(date_of_publication) as year')->distinct()->pluck('year'),
|
||||
|
||||
ExternalEngagement::where('department_id', $user->department_id) ->selectRaw('YEAR(end_date) as year')->distinct()->pluck('year'),
|
||||
|
||||
IvOrganised::where('department_id', $user->department_id) ->selectRaw('YEAR(end_date) as year')->distinct()->pluck('year'),
|
||||
|
||||
OnlineCourse::where('department_id', $user->department_id) ->selectRaw('YEAR(end_date) as year')->distinct()->pluck('year'),
|
||||
|
||||
Patent::where('department_id', $user->department_id) ->selectRaw('YEAR(date_of_submission) as year')->distinct()->pluck('year'),
|
||||
|
||||
Publication::where('department_id', $user->department_id) ->selectRaw('YEAR(end_date) as year')->distinct()->pluck('year'),
|
||||
|
||||
])->flatten()->filter()->unique()->sortDesc()->values();
|
||||
|
||||
return view('coordinator.dashboard', compact('departments', 'years'));
|
||||
}
|
||||
|
||||
// View responses submitted by users
|
||||
public function viewActivitiesAttendedResponses()
|
||||
{
|
||||
$departments = Department::all();
|
||||
$user = Auth::user();
|
||||
$departments = Department::where('id', $user->department_id)->get();
|
||||
return view('pages.activities-attended.index', compact('departments'));
|
||||
}
|
||||
|
||||
public function viewActivitiesOrganisedResponses()
|
||||
{
|
||||
return view('pages.activities-organised.index');
|
||||
$user = Auth::user();
|
||||
$departments = Department::where('id', $user->department_id)->get();
|
||||
return view('pages.activities-organised.index', compact('departments'));
|
||||
}
|
||||
|
||||
public function viewIvOrganisedResponses()
|
||||
{
|
||||
return view('pages.iv-organised.index');
|
||||
$user = Auth::user();
|
||||
$departments = Department::where('id', $user->department_id)->get();
|
||||
return view('pages.iv-organised.index', compact('departments'));
|
||||
}
|
||||
|
||||
public function viewPublicationsResponses()
|
||||
{
|
||||
return view('pages.publications.index');
|
||||
$user = Auth::user();
|
||||
$departments = Department::where('id', $user->department_id)->get();
|
||||
return view('pages.publications.index', compact('departments'));
|
||||
}
|
||||
|
||||
public function viewBooksPublishedResponses()
|
||||
{
|
||||
return view('pages.booksPublished.index');
|
||||
$user = Auth::user();
|
||||
$departments = Department::where('id', $user->department_id)->get();
|
||||
return view('pages.booksPublished.index', compact('departments'));
|
||||
}
|
||||
|
||||
public function viewExternalEngagementResponses()
|
||||
{
|
||||
return view('pages.externalEngagement.index');
|
||||
$user = Auth::user();
|
||||
$departments = Department::where('id', $user->department_id)->get();
|
||||
return view('pages.externalEngagement.index', compact('departments'));
|
||||
}
|
||||
|
||||
public function viewOnlineCoursesResponses()
|
||||
{
|
||||
return view('pages.onlineCourses.index');
|
||||
$user = Auth::user();
|
||||
$departments = Department::where('id', $user->department_id)->get();
|
||||
return view('pages.onlineCourses.index', compact('departments'));
|
||||
}
|
||||
|
||||
public function viewPatentsResponses()
|
||||
{
|
||||
return view('pages.patents.index');
|
||||
$user = Auth::user();
|
||||
$departments = Department::where('id', $user->department_id)->get();
|
||||
return view('pages.patents.index', compact('departments'));
|
||||
}
|
||||
|
||||
public function downloadProofs(Request $request, ProofDownloadService $proofDownloadService)
|
||||
{
|
||||
// Validate the request
|
||||
$request->validate([
|
||||
'ids' => 'required|string',
|
||||
'model' => 'sometimes|string',
|
||||
]);
|
||||
|
||||
$ids = json_decode($request->input('ids'));
|
||||
$modelName = $request->input('model', 'ActivitiesAttended');
|
||||
|
||||
// Model mapping
|
||||
$modelMap = [
|
||||
'ActivitiesAttended' => ActivitiesAttended::class,
|
||||
'ActivitiesOrganised' => ActivitiesOrganised::class,
|
||||
'IvOrganised' => IvOrganised::class,
|
||||
'Publication' => Publication::class,
|
||||
'BooksPublished' => BooksPublished::class,
|
||||
'ExternalEngagement' => ExternalEngagement::class,
|
||||
'OnlineCourse' => OnlineCourse::class,
|
||||
'Patent' => Patent::class,
|
||||
];
|
||||
|
||||
// Get the model class from the map or default to ActivitiesAttended
|
||||
$modelClass = $modelMap[$modelName] ?? ActivitiesAttended::class;
|
||||
|
||||
$result = $proofDownloadService->downloadProofs($modelClass, $ids);
|
||||
|
||||
if (isset($result['error'])) {
|
||||
return back()->with('error', $result['error']);
|
||||
}
|
||||
|
||||
// Return the zip file as a download
|
||||
return response()->download($result['filePath'], $result['fileName'])->deleteFileAfterSend(true);
|
||||
}
|
||||
|
||||
public function analyticsActivitiesAttended()
|
||||
{
|
||||
$data = ActivitiesAttended::selectRaw('departments.name as department, COUNT(*) as count')
|
||||
->join('departments', 'activities_attendeds.department_id', '=', 'departments.id')
|
||||
->groupBy('departments.name')
|
||||
->get();
|
||||
|
||||
$total = $data->sum('count');
|
||||
|
||||
return response()->json([
|
||||
'labels' => $data->pluck('department'),
|
||||
'values' => $data->pluck('count'),
|
||||
'total' => $total,
|
||||
]);
|
||||
}
|
||||
|
||||
public function analyticsActivitiesOrganised()
|
||||
{
|
||||
$data = ActivitiesOrganised::selectRaw('departments.name as department, COUNT(*) as count')
|
||||
->join('departments', 'activities_organiseds.department_id', '=', 'departments.id')
|
||||
->groupBy('departments.name')
|
||||
->get();
|
||||
|
||||
$total = $data->sum('count');
|
||||
|
||||
return response()->json([
|
||||
'labels' => $data->pluck('department'),
|
||||
'values' => $data->pluck('count'),
|
||||
'total' => $total,
|
||||
]);
|
||||
}
|
||||
|
||||
public function analyticsIvOrganised()
|
||||
{
|
||||
$data = IvOrganised::selectRaw('departments.name as department, COUNT(*) as count')
|
||||
->join('departments', 'iv_organiseds.department_id', '=', 'departments.id')
|
||||
->groupBy('departments.name')
|
||||
->get();
|
||||
|
||||
$total = $data->sum('count');
|
||||
|
||||
return response()->json([
|
||||
'labels' => $data->pluck('department'),
|
||||
'values' => $data->pluck('count'),
|
||||
'total' => $total,
|
||||
]);
|
||||
}
|
||||
|
||||
public function analyticsPaperPublished()
|
||||
{
|
||||
$data = Publication::selectRaw('departments.name as department, COUNT(*) as count')
|
||||
->join('departments', 'publications.department_id', '=', 'departments.id')
|
||||
->groupBy('departments.name')
|
||||
->get();
|
||||
|
||||
$total = $data->sum('count');
|
||||
|
||||
return response()->json([
|
||||
'labels' => $data->pluck('department'),
|
||||
'values' => $data->pluck('count'),
|
||||
'total' => $total,
|
||||
]);
|
||||
}
|
||||
|
||||
public function analyticsBooksPublished()
|
||||
{
|
||||
$data = BooksPublished::selectRaw('departments.name as department, COUNT(*) as count')
|
||||
->join('departments', 'books_published.department_id', '=', 'departments.id')
|
||||
->groupBy('departments.name')
|
||||
->get();
|
||||
|
||||
$total = $data->sum('count');
|
||||
|
||||
return response()->json([
|
||||
'labels' => $data->pluck('department'),
|
||||
'values' => $data->pluck('count'),
|
||||
'total' => $total,
|
||||
]);
|
||||
}
|
||||
|
||||
public function analyticsExternalEngagement()
|
||||
{
|
||||
$data = ExternalEngagement::selectRaw('departments.name as department, COUNT(*) as count')
|
||||
->join('departments', 'external_engagements.department_id', '=', 'departments.id')
|
||||
->groupBy('departments.name')
|
||||
->get();
|
||||
|
||||
$total = $data->sum('count');
|
||||
|
||||
return response()->json([
|
||||
'labels' => $data->pluck('department'),
|
||||
'values' => $data->pluck('count'),
|
||||
'total' => $total,
|
||||
]);
|
||||
}
|
||||
|
||||
public function analyticsOnlineCourse()
|
||||
{
|
||||
$data = OnlineCourse::selectRaw('departments.name as department, COUNT(*) as count')
|
||||
->join('departments', 'online_courses.department_id', '=', 'departments.id')
|
||||
->groupBy('departments.name')
|
||||
->get();
|
||||
|
||||
$total = $data->sum('count');
|
||||
|
||||
return response()->json([
|
||||
'labels' => $data->pluck('department'),
|
||||
'values' => $data->pluck('count'),
|
||||
'total' => $total,
|
||||
]);
|
||||
}
|
||||
|
||||
public function analyticsPatent()
|
||||
{
|
||||
$data = Patent::selectRaw('departments.name as department, COUNT(*) as count')
|
||||
->join('departments', 'patents.department_id', '=', 'departments.id')
|
||||
->groupBy('departments.name')
|
||||
->get();
|
||||
|
||||
$total = $data->sum('count');
|
||||
|
||||
return response()->json([
|
||||
'labels' => $data->pluck('department'),
|
||||
'values' => $data->pluck('count'),
|
||||
'total' => $total,
|
||||
]);
|
||||
}
|
||||
|
||||
public function analyticsComparison(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
$departmentId = $request->query('department_id');
|
||||
if (empty($departmentId) && $user && $user->role_id == 2) {
|
||||
$departmentId = $user->department_id;
|
||||
}
|
||||
$year = $request->query('year');
|
||||
|
||||
$models = [
|
||||
'ActivitiesAttended' => ['class' => ActivitiesAttended::class, 'date_field' => 'end_date'],
|
||||
'ActivitiesOrganised' => ['class' => ActivitiesOrganised::class, 'date_field' => 'end_date'],
|
||||
'BooksPublished' => ['class' => BooksPublished::class, 'date_field' => 'date_of_publication'],
|
||||
'ExternalEngagement' => ['class' => ExternalEngagement::class, 'date_field' => 'end_date'],
|
||||
'IvOrganised' => ['class' => IvOrganised::class, 'date_field' => 'end_date'],
|
||||
'OnlineCourse' => ['class' => OnlineCourse::class, 'date_field' => 'end_date'],
|
||||
'Patent' => ['class' => Patent::class, 'date_field' => 'date_of_submission'],
|
||||
'Publication' => ['class' => Publication::class, 'date_field' => 'end_date'],
|
||||
];
|
||||
|
||||
$data = [];
|
||||
|
||||
foreach ($models as $label => $modelConfig) {
|
||||
$query = $modelConfig['class']::query();
|
||||
if($departmentId) {
|
||||
$query->where('department_id', $departmentId);
|
||||
}
|
||||
if ($year && $year !== 'all') {
|
||||
$query->whereYear($modelConfig['date_field'], $year);
|
||||
}
|
||||
$count = $query->count();
|
||||
if ($count > 0) { // Only include data with non-zero count
|
||||
$data[] = [
|
||||
'label' => $label,
|
||||
'count' => $count,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$total = array_sum(array_column($data, 'count'));
|
||||
|
||||
return response()->json([
|
||||
'labels' => array_column($data, 'label'),
|
||||
'values' => array_column($data, 'count'),
|
||||
'total' => $total
|
||||
]);
|
||||
}
|
||||
|
||||
public function analyticsContribution(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
$model = $request->query('model', 'all');
|
||||
$year = $request->query('year');
|
||||
$departmentId = $request->query('department_id');
|
||||
if (empty($departmentId) && $user && $user->role_id == 2) {
|
||||
$departmentId = $user->department_id;
|
||||
}
|
||||
|
||||
$models = [
|
||||
'ActivitiesAttended' => ['class' => ActivitiesAttended::class, 'date_field' => 'end_date'],
|
||||
'ActivitiesOrganised' => ['class' => ActivitiesOrganised::class, 'date_field' => 'end_date'],
|
||||
'BooksPublished' => ['class' => BooksPublished::class, 'date_field' => 'date_of_publication'],
|
||||
'ExternalEngagement' => ['class' => ExternalEngagement::class, 'date_field' => 'end_date'],
|
||||
'IvOrganised' => ['class' => IvOrganised::class, 'date_field' => 'end_date'],
|
||||
'OnlineCourse' => ['class' => OnlineCourse::class, 'date_field' => 'end_date'],
|
||||
'Patent' => ['class' => Patent::class, 'date_field' => 'date_of_submission'],
|
||||
'Publication' => ['class' => Publication::class, 'date_field' => 'end_date'],
|
||||
];
|
||||
|
||||
$data = [];
|
||||
|
||||
if ($model === 'all') {
|
||||
$departments = Department::when($departmentId, function ($query) use ($departmentId) {
|
||||
return $query->where('id', $departmentId);
|
||||
})->get();
|
||||
|
||||
foreach ($departments as $department) {
|
||||
$count = 0;
|
||||
foreach ($models as $modelConfig) {
|
||||
$query = $modelConfig['class']::where('department_id', $department->id);
|
||||
if ($year && $year !== 'all') {
|
||||
$query->whereYear($modelConfig['date_field'], $year);
|
||||
}
|
||||
$count += $query->count();
|
||||
}
|
||||
if ($count > 0) { // Only include data with non-zero count
|
||||
$data[] = [
|
||||
'label' => $department->name,
|
||||
'count' => $count,
|
||||
];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$modelConfig = $models[$model];
|
||||
$modelClass = $modelConfig['class'];
|
||||
$table = (new $modelClass)->getTable();
|
||||
|
||||
$query = $modelClass::selectRaw('departments.name as label, COUNT(*) as count')
|
||||
->join('departments', $table . '.department_id', '=', 'departments.id')
|
||||
->groupBy('departments.name');
|
||||
|
||||
if ($year && $year !== 'all') {
|
||||
$query->whereYear($table . '.' . $modelConfig['date_field'], $year);
|
||||
}
|
||||
|
||||
if ($departmentId) {
|
||||
$query->where($table . '.department_id', $departmentId);
|
||||
}
|
||||
|
||||
$data = $query->get()->filter(function ($item) {
|
||||
return $item['count'] > 0; // Filter out data with zero count
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
$total = array_sum(array_column($data, 'count'));
|
||||
|
||||
return response()->json([
|
||||
'labels' => array_column($data, 'label'),
|
||||
'values' => array_column($data, 'count'),
|
||||
'total' => $total
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,25 +201,39 @@ class PublicationsController extends Controller
|
||||
|
||||
if (!$user || !$user->scholar_url) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'No Google Scholar URL found for the logged-in user.'
|
||||
'error' => 'No Google Scholar URL found for the logged-in user.'
|
||||
], 400);
|
||||
}
|
||||
|
||||
$scholarService = new \App\Services\ScholarService();
|
||||
$publications = $scholarService->fetchPublications($user->scholar_url);
|
||||
$result = $scholarService->fetchPublications($user->scholar_url);
|
||||
|
||||
// If the service returned an error payload, forward it to UI
|
||||
if (is_array($result) && array_key_exists('error', $result)) {
|
||||
// Log more details if available
|
||||
if (isset($result['body'])) {
|
||||
\Log::error('ScholarService error body', ['body' => $result['body']]);
|
||||
} elseif (isset($result['raw'])) {
|
||||
\Log::warning('ScholarService returned empty articles', ['raw' => $result['raw']]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => $result['error']
|
||||
], 200);
|
||||
}
|
||||
|
||||
// Expecting successful shape ['data' => [...]]
|
||||
$publications = $result['data'] ?? [];
|
||||
|
||||
if (empty($publications)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'No publications found from the provided Scholar profile.'
|
||||
]);
|
||||
'error' => 'No publications found from the provided Scholar profile.'
|
||||
], 200);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'count' => count($publications),
|
||||
'data' => $publications,
|
||||
'count' => count($publications),
|
||||
]);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -45,7 +45,7 @@ class UserController extends Controller
|
||||
->addColumn('role_name', function ($response) {
|
||||
$roles = [
|
||||
1 => 'Admin',
|
||||
2 => 'Coordinator',
|
||||
2 => 'HOD',
|
||||
3 => 'Faculty',
|
||||
];
|
||||
$options = '';
|
||||
|
||||
@@ -10,20 +10,22 @@ class ScholarService
|
||||
{
|
||||
try {
|
||||
// Extract Google Scholar profile ID (e.g., ssU-uBEAAAAJ)
|
||||
preg_match('/user=([^&]+)/', $url, $match);
|
||||
preg_match('/[?&]user=([^&]+)/', $url, $match);
|
||||
$profileId = $match[1] ?? null;
|
||||
|
||||
if (!$profileId) {
|
||||
\Log::warning('No Google Scholar ID found in URL: ' . $url);
|
||||
return [];
|
||||
$msg = 'No Google Scholar ID found in URL: ' . $url;
|
||||
\Log::warning($msg);
|
||||
return ['error' => $msg];
|
||||
}
|
||||
|
||||
// Load API key from .env
|
||||
$apiKey = env('SERPAPI_KEY');
|
||||
|
||||
if (!$apiKey) {
|
||||
\Log::error('SERPAPI_KEY not found in environment variables');
|
||||
return [];
|
||||
$msg = 'SERPAPI_KEY not found in environment variables';
|
||||
\Log::error($msg);
|
||||
return ['error' => $msg];
|
||||
}
|
||||
|
||||
$endpoint = 'https://serpapi.com/search.json';
|
||||
@@ -34,8 +36,10 @@ class ScholarService
|
||||
]);
|
||||
|
||||
if (!$response->successful()) {
|
||||
\Log::error('Scholar API request failed: ' . $response->status());
|
||||
return [];
|
||||
$status = $response->status();
|
||||
$body = $response->body();
|
||||
\Log::error('Scholar API request failed', ['status' => $status, 'body' => $body]);
|
||||
return ['error' => 'Scholar API request failed: ' . $status, 'body' => $body];
|
||||
}
|
||||
|
||||
$json = $response->json();
|
||||
@@ -46,8 +50,13 @@ class ScholarService
|
||||
?? $json['results']
|
||||
?? [];
|
||||
|
||||
if (empty($articles)) {
|
||||
\Log::warning('Scholar API returned no articles', ['profileId' => $profileId, 'response' => $json]);
|
||||
return ['error' => 'No publications returned by Scholar API', 'raw' => $json];
|
||||
}
|
||||
|
||||
// ✅ Format and return
|
||||
return collect($articles)
|
||||
$formatted = collect($articles)
|
||||
->map(function ($a) {
|
||||
return [
|
||||
'title' => $a['title'] ?? $a['name'] ?? null,
|
||||
@@ -59,6 +68,8 @@ class ScholarService
|
||||
})
|
||||
->toArray();
|
||||
|
||||
return ['data' => $formatted];
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
\Log::error('ScholarService API error: ' . $e->getMessage());
|
||||
return [];
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
"laravel/sail": "^1.26",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.1",
|
||||
"phpunit/phpunit": "^11.0.1"
|
||||
"phpunit/phpunit": "^11.0.1",
|
||||
"symfony/panther": "^2.2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
||||
714
composer.lock
generated
714
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "d9ba03cac29ec7ec3e39de5902752aa9",
|
||||
"content-hash": "907f80ba2043dbb55bff6a61836bb507",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -7755,6 +7755,73 @@
|
||||
},
|
||||
"time": "2025-01-13T16:57:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "masterminds/html5",
|
||||
"version": "2.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Masterminds/html5-php.git",
|
||||
"reference": "fcf91eb64359852f00d921887b219479b4f21251"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251",
|
||||
"reference": "fcf91eb64359852f00d921887b219479b4f21251",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.7-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Masterminds\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Matt Butcher",
|
||||
"email": "technosophos@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Matt Farina",
|
||||
"email": "matt@mattfarina.com"
|
||||
},
|
||||
{
|
||||
"name": "Asmir Mustafic",
|
||||
"email": "goetas@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "An HTML5 parser and serializer.",
|
||||
"homepage": "http://masterminds.github.io/html5-php",
|
||||
"keywords": [
|
||||
"HTML5",
|
||||
"dom",
|
||||
"html",
|
||||
"parser",
|
||||
"querypath",
|
||||
"serializer",
|
||||
"xml"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/Masterminds/html5-php/issues",
|
||||
"source": "https://github.com/Masterminds/html5-php/tree/2.10.0"
|
||||
},
|
||||
"time": "2025-07-25T09:04:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mockery/mockery",
|
||||
"version": "1.6.12",
|
||||
@@ -8114,6 +8181,72 @@
|
||||
},
|
||||
"time": "2022-02-21T01:04:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-webdriver/webdriver",
|
||||
"version": "1.15.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-webdriver/php-webdriver.git",
|
||||
"reference": "998e499b786805568deaf8cbf06f4044f05d91bf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/998e499b786805568deaf8cbf06f4044f05d91bf",
|
||||
"reference": "998e499b786805568deaf8cbf06f4044f05d91bf",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-zip": "*",
|
||||
"php": "^7.3 || ^8.0",
|
||||
"symfony/polyfill-mbstring": "^1.12",
|
||||
"symfony/process": "^5.0 || ^6.0 || ^7.0"
|
||||
},
|
||||
"replace": {
|
||||
"facebook/webdriver": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"ergebnis/composer-normalize": "^2.20.0",
|
||||
"ondram/ci-detector": "^4.0",
|
||||
"php-coveralls/php-coveralls": "^2.4",
|
||||
"php-mock/php-mock-phpunit": "^2.0",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2",
|
||||
"phpunit/phpunit": "^9.3",
|
||||
"squizlabs/php_codesniffer": "^3.5",
|
||||
"symfony/var-dumper": "^5.0 || ^6.0 || ^7.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-SimpleXML": "For Firefox profile creation"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"lib/Exception/TimeoutException.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Facebook\\WebDriver\\": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.",
|
||||
"homepage": "https://github.com/php-webdriver/php-webdriver",
|
||||
"keywords": [
|
||||
"Chromedriver",
|
||||
"geckodriver",
|
||||
"php",
|
||||
"selenium",
|
||||
"webdriver"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-webdriver/php-webdriver/issues",
|
||||
"source": "https://github.com/php-webdriver/php-webdriver/tree/1.15.2"
|
||||
},
|
||||
"time": "2024-11-21T15:12:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
"version": "11.0.8",
|
||||
@@ -9516,6 +9649,581 @@
|
||||
],
|
||||
"time": "2024-10-20T05:08:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/browser-kit",
|
||||
"version": "v7.3.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/browser-kit.git",
|
||||
"reference": "e9a9fd604296b17bf90939c3647069f1f16ef04e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/browser-kit/zipball/e9a9fd604296b17bf90939c3647069f1f16ef04e",
|
||||
"reference": "e9a9fd604296b17bf90939c3647069f1f16ef04e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/dom-crawler": "^6.4|^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/css-selector": "^6.4|^7.0",
|
||||
"symfony/http-client": "^6.4|^7.0",
|
||||
"symfony/mime": "^6.4|^7.0",
|
||||
"symfony/process": "^6.4|^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\BrowserKit\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/browser-kit/tree/v7.3.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-11-05T07:57:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/dependency-injection",
|
||||
"version": "v7.3.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/dependency-injection.git",
|
||||
"reference": "98af8bb46c56aedd9dd5a7f0414fc72bf2dcfe69"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/dependency-injection/zipball/98af8bb46c56aedd9dd5a7f0414fc72bf2dcfe69",
|
||||
"reference": "98af8bb46c56aedd9dd5a7f0414fc72bf2dcfe69",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"psr/container": "^1.1|^2.0",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/service-contracts": "^3.5",
|
||||
"symfony/var-exporter": "^6.4.20|^7.2.5"
|
||||
},
|
||||
"conflict": {
|
||||
"ext-psr": "<1.1|>=2",
|
||||
"symfony/config": "<6.4",
|
||||
"symfony/finder": "<6.4",
|
||||
"symfony/yaml": "<6.4"
|
||||
},
|
||||
"provide": {
|
||||
"psr/container-implementation": "1.1|2.0",
|
||||
"symfony/service-implementation": "1.1|2.0|3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/config": "^6.4|^7.0",
|
||||
"symfony/expression-language": "^6.4|^7.0",
|
||||
"symfony/yaml": "^6.4|^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\DependencyInjection\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Allows you to standardize and centralize the way objects are constructed in your application",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/dependency-injection/tree/v7.3.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-10-31T10:11:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/dom-crawler",
|
||||
"version": "v7.3.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/dom-crawler.git",
|
||||
"reference": "efa076ea0eeff504383ff0dcf827ea5ce15690ba"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/efa076ea0eeff504383ff0dcf827ea5ce15690ba",
|
||||
"reference": "efa076ea0eeff504383ff0dcf827ea5ce15690ba",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"masterminds/html5": "^2.6",
|
||||
"php": ">=8.2",
|
||||
"symfony/polyfill-ctype": "~1.8",
|
||||
"symfony/polyfill-mbstring": "~1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/css-selector": "^6.4|^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\DomCrawler\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Eases DOM navigation for HTML and XML documents",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/dom-crawler/tree/v7.3.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-06T20:13:54+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-client",
|
||||
"version": "v7.3.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-client.git",
|
||||
"reference": "3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-client/zipball/3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de",
|
||||
"reference": "3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"psr/log": "^1|^2|^3",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/http-client-contracts": "~3.4.4|^3.5.2",
|
||||
"symfony/polyfill-php83": "^1.29",
|
||||
"symfony/service-contracts": "^2.5|^3"
|
||||
},
|
||||
"conflict": {
|
||||
"amphp/amp": "<2.5",
|
||||
"amphp/socket": "<1.1",
|
||||
"php-http/discovery": "<1.15",
|
||||
"symfony/http-foundation": "<6.4"
|
||||
},
|
||||
"provide": {
|
||||
"php-http/async-client-implementation": "*",
|
||||
"php-http/client-implementation": "*",
|
||||
"psr/http-client-implementation": "1.0",
|
||||
"symfony/http-client-implementation": "3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"amphp/http-client": "^4.2.1|^5.0",
|
||||
"amphp/http-tunnel": "^1.0|^2.0",
|
||||
"guzzlehttp/promises": "^1.4|^2.0",
|
||||
"nyholm/psr7": "^1.0",
|
||||
"php-http/httplug": "^1.0|^2.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"symfony/amphp-http-client-meta": "^1.0|^2.0",
|
||||
"symfony/dependency-injection": "^6.4|^7.0",
|
||||
"symfony/http-kernel": "^6.4|^7.0",
|
||||
"symfony/messenger": "^6.4|^7.0",
|
||||
"symfony/process": "^6.4|^7.0",
|
||||
"symfony/rate-limiter": "^6.4|^7.0",
|
||||
"symfony/stopwatch": "^6.4|^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\HttpClient\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"http"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/http-client/tree/v7.3.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-11-05T17:41:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-client-contracts",
|
||||
"version": "v3.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-client-contracts.git",
|
||||
"reference": "75d7043853a42837e68111812f4d964b01e5101c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c",
|
||||
"reference": "75d7043853a42837e68111812f4d964b01e5101c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/contracts",
|
||||
"name": "symfony/contracts"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "3.6-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Contracts\\HttpClient\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Test/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Generic abstractions related to HTTP clients",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"abstractions",
|
||||
"contracts",
|
||||
"decoupling",
|
||||
"interfaces",
|
||||
"interoperability",
|
||||
"standards"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-04-29T11:18:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/panther",
|
||||
"version": "v2.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/panther.git",
|
||||
"reference": "b7e0f834c9046918972edb3dde2ecc4a20f6155e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/panther/zipball/b7e0f834c9046918972edb3dde2ecc4a20f6155e",
|
||||
"reference": "b7e0f834c9046918972edb3dde2ecc4a20f6155e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"ext-libxml": "*",
|
||||
"php": ">=8.0",
|
||||
"php-webdriver/webdriver": "^1.8.2",
|
||||
"symfony/browser-kit": "^5.4 || ^6.4 || ^7.0",
|
||||
"symfony/dependency-injection": "^5.4 || ^6.4 || ^7.0",
|
||||
"symfony/deprecation-contracts": "^2.4 || ^3",
|
||||
"symfony/dom-crawler": "^5.4 || ^6.4 || ^7.0",
|
||||
"symfony/http-client": "^6.4 || ^7.0",
|
||||
"symfony/http-kernel": "^5.4 || ^6.4 || ^7.0",
|
||||
"symfony/process": "^5.4 || ^6.4 || ^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/css-selector": "^5.4 || ^6.4 || ^7.0",
|
||||
"symfony/framework-bundle": "^5.4 || ^6.4 || ^7.0",
|
||||
"symfony/mime": "^5.4 || ^6.4 || ^7.0",
|
||||
"symfony/phpunit-bridge": "^7.2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "2.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Panther\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Kévin Dunglas",
|
||||
"email": "dunglas@gmail.com",
|
||||
"homepage": "https://dunglas.fr"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "A browser testing and web scraping library for PHP and Symfony.",
|
||||
"homepage": "https://dunglas.fr",
|
||||
"keywords": [
|
||||
"e2e",
|
||||
"scraping",
|
||||
"selenium",
|
||||
"symfony",
|
||||
"testing",
|
||||
"webdriver"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/symfony/panther/issues",
|
||||
"source": "https://github.com/symfony/panther/tree/v2.2.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://www.panthera.org/donate",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/dunglas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/panther",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-01-30T13:11:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/var-exporter",
|
||||
"version": "v7.3.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/var-exporter.git",
|
||||
"reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/var-exporter/zipball/0f020b544a30a7fe8ba972e53ee48a74c0bc87f4",
|
||||
"reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/property-access": "^6.4|^7.0",
|
||||
"symfony/serializer": "^6.4|^7.0",
|
||||
"symfony/var-dumper": "^6.4|^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\VarExporter\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Allows exporting any serializable PHP data structure to plain PHP code",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"clone",
|
||||
"construct",
|
||||
"export",
|
||||
"hydrate",
|
||||
"instantiate",
|
||||
"lazy-loading",
|
||||
"proxy",
|
||||
"serialize"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/var-exporter/tree/v7.3.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-09-11T10:12:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/yaml",
|
||||
"version": "v7.2.0",
|
||||
@@ -9641,12 +10349,12 @@
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"stability-flags": {},
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": "^8.2"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -1,22 +1,223 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('header')
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
Coordinator Dashboard
|
||||
</h2>
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
Coordinator Dashboard
|
||||
</h2>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 text-gray-900">
|
||||
Welcome, Coordinator! Manage department-related tasks here.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<a href="{{ route('departments.index') }}" class="text-blue-500 underline">View Departments</a><br>
|
||||
<a href="{{ route('users.index') }}" class="text-blue-500 underline">View Faculty</a><br>
|
||||
<a href="{{ route('profile.edit') }}" class="text-blue-500 underline">Edit Profile</a>
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 text-gray-900">
|
||||
Welcome, Coordinator! Manage department-related tasks here.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Graphs Section -->
|
||||
<div class="mt-6">
|
||||
<h3 class="text-lg font-semibold">Analytics</h3>
|
||||
<div id="graphs-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-6 mt-4">
|
||||
<!-- First three graphs -->
|
||||
<div class="bg-white p-4 shadow rounded">
|
||||
<h4 class="text-md font-semibold mb-4">Comparison of All Models by Department</h4>
|
||||
<div class="flex flex-row sm:flex-row sm:items-center sm:justify-between">
|
||||
<select id="departmentSelector" class="mb-4 px-7 border rounded">
|
||||
@foreach($departments as $department)
|
||||
<option value="{{ $department->id }}">{{ $department->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<select id="comparisonYearSelector" class="mb-4 px-7 border rounded">
|
||||
<option value="all">All Years</option>
|
||||
@foreach($years as $year)
|
||||
<option value="{{ $year }}">{{ $year }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<canvas id="comparisonChart"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-4 shadow rounded">
|
||||
<h4 class="text-md font-semibold mb-4">Total Contribution by Department</h4>
|
||||
<div class="flex flex-row sm:flex-row sm:items-center sm:justify-between">
|
||||
<select id="modelSelector" class="mb-4 sm:mb-0 p-2 border rounded">
|
||||
<option value="all">All Categories</option>
|
||||
<option value="ActivitiesAttended">Activities Attended</option>
|
||||
<option value="ActivitiesOrganised">Activities Organised</option>
|
||||
<option value="BooksPublished">Books Published</option>
|
||||
<option value="ExternalEngagement">External Engagement</option>
|
||||
<option value="IvOrganised">IV Organised</option>
|
||||
<option value="OnlineCourse">Online Courses</option>
|
||||
<option value="Patent">Patents</option>
|
||||
<option value="Publication">Publications</option>
|
||||
</select>
|
||||
<select id="contributionYearSelector" class="mb-4 sm:ml-4 px-7 border rounded">
|
||||
<option value="all">All Years</option>
|
||||
@foreach($years as $year)
|
||||
<option value="{{ $year }}">{{ $year }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<canvas id="contributionChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const departmentSelector = document.getElementById('departmentSelector');
|
||||
const comparisonYearSelector = document.getElementById('comparisonYearSelector');
|
||||
const modelSelector = document.getElementById('modelSelector');
|
||||
const contributionYearSelector = document.getElementById('contributionYearSelector');
|
||||
|
||||
// Use coordinator route names (must exist in routes/web.php)
|
||||
const comparisonBaseUrl = "{{ route('coordinator.analytics.comparison') }}";
|
||||
const contributionBaseUrl = "{{ route('coordinator.analytics.contribution') }}";
|
||||
|
||||
function fetchComparisonData(departmentId, year) {
|
||||
const yearParam = year === 'all' ? '' : `&year=${year}`;
|
||||
const url = departmentId
|
||||
? `${comparisonBaseUrl}?department_id=${departmentId}${yearParam}`
|
||||
: `${comparisonBaseUrl}?${yearParam}`;
|
||||
|
||||
|
||||
fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
const ctx = document.getElementById('comparisonChart').getContext('2d');
|
||||
if (window.comparisonChartInstance) window.comparisonChartInstance.destroy();
|
||||
|
||||
const modelPages = [
|
||||
'ActivitiesAttendedResponses',
|
||||
'ActivitiesOrganisedResponses',
|
||||
'BooksPublishedResponses',
|
||||
'ExternalEngagementResponses',
|
||||
'IvOrganisedResponses',
|
||||
'OnlineCoursesResponses',
|
||||
'PatentsResponses',
|
||||
'PublicationsResponses'
|
||||
];
|
||||
|
||||
// Redirect to coordinator pages
|
||||
const barLinks = modelPages.map(page => `/coordinator/${page}`);
|
||||
|
||||
window.comparisonChartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [{
|
||||
label: 'Comparison by Department',
|
||||
data: data.values,
|
||||
}]
|
||||
},
|
||||
plugins: [ChartDataLabels],
|
||||
options: {
|
||||
plugins: { legend: { display: false } },
|
||||
responsive: true,
|
||||
scales: { y: { beginAtZero: true } },
|
||||
onClick: function(evt, elements) {
|
||||
if (elements.length > 0) {
|
||||
const idx = elements[0].index;
|
||||
const deptId = departmentSelector ? departmentSelector.value : departmentId;
|
||||
if (idx < barLinks.length) window.location.href = `${barLinks[idx]}?department_id=${deptId}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// show total
|
||||
let comparisonTotal = document.getElementById('comparisonTotal');
|
||||
if (!comparisonTotal) {
|
||||
comparisonTotal = document.createElement('p');
|
||||
comparisonTotal.id = 'comparisonTotal';
|
||||
comparisonTotal.classList.add('text-center', 'font-bold', 'mt-2');
|
||||
document.getElementById('comparisonChart').parentElement.appendChild(comparisonTotal);
|
||||
}
|
||||
comparisonTotal.textContent = `Total: ${data.total}`;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Comparison fetch error:', err);
|
||||
});
|
||||
}
|
||||
|
||||
function fetchContributionData(model, year) {
|
||||
const yearParam = year === 'all' ? '' : `&year=${year}`;
|
||||
const url = `${contributionBaseUrl}?model=${model}${yearParam}`;
|
||||
|
||||
fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
const ctx = document.getElementById('contributionChart').getContext('2d');
|
||||
if (window.contributionChartInstance) window.contributionChartInstance.destroy();
|
||||
|
||||
// map labels to coordinator pages (adjust if needed)
|
||||
const barLinks = data.labels.map((label, idx) => `/coordinator/contribution/${idx+1}`);
|
||||
|
||||
window.contributionChartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [{ label: 'Total Contribution by Department', data: data.values }]
|
||||
},
|
||||
plugins: [ChartDataLabels],
|
||||
options: {
|
||||
plugins: { legend: { display: false } },
|
||||
responsive: true,
|
||||
scales: { y: { beginAtZero: true } },
|
||||
onClick: function(evt, elements) {
|
||||
if (elements.length > 0) {
|
||||
const idx = elements[0].index;
|
||||
window.location.href = barLinks[idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let contributionTotal = document.getElementById('contributionTotal');
|
||||
if (!contributionTotal) {
|
||||
contributionTotal = document.createElement('p');
|
||||
contributionTotal.id = 'contributionTotal';
|
||||
contributionTotal.classList.add('text-center', 'font-bold', 'mt-2');
|
||||
document.getElementById('contributionChart').parentElement.appendChild(contributionTotal);
|
||||
}
|
||||
contributionTotal.textContent = `Total: ${data.total}`;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Contribution fetch error:', err);
|
||||
});
|
||||
}
|
||||
|
||||
// INITIAL FETCH: use the selected department (not hardcoded 1)
|
||||
const initialDept = departmentSelector ? departmentSelector.value : null;
|
||||
const initialYear = comparisonYearSelector ? comparisonYearSelector.value : 'all';
|
||||
if (initialDept) {
|
||||
fetchComparisonData(initialDept, initialYear);
|
||||
} else {
|
||||
// fallback: let server infer (coordinator filter), call without department_id
|
||||
fetchComparisonData('', initialYear);
|
||||
}
|
||||
|
||||
// initial contribution
|
||||
const initModel = modelSelector ? modelSelector.value : 'all';
|
||||
const initContribYear = contributionYearSelector ? contributionYearSelector.value : 'all';
|
||||
fetchContributionData(initModel, initContribYear);
|
||||
|
||||
// Event listeners
|
||||
if (departmentSelector) departmentSelector.addEventListener('change', () => fetchComparisonData(departmentSelector.value, comparisonYearSelector.value));
|
||||
if (comparisonYearSelector) comparisonYearSelector.addEventListener('change', () => fetchComparisonData(departmentSelector.value, comparisonYearSelector.value));
|
||||
if (modelSelector) modelSelector.addEventListener('change', () => fetchContributionData(modelSelector.value, contributionYearSelector.value));
|
||||
if (contributionYearSelector) contributionYearSelector.addEventListener('change', () => fetchContributionData(modelSelector.value, contributionYearSelector.value));
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
@@ -299,7 +299,23 @@ Route::prefix('admin')->name('admin.')->group(function () {
|
||||
Route::get('analytics/contribution', [AdminController::class, 'analyticsContribution'])->name('analytics.contribution');
|
||||
});
|
||||
|
||||
Route::prefix('coordinator')->name('coordinator.')->group(function () {
|
||||
Route::get('analytics/activitiesAttended', [CoordinatorController::class, 'analyticsActivitiesAttended'])->name('analytics.activitiesAttended');
|
||||
Route::get('analytics/activitiesOrganised', [CoordinatorController::class, 'analyticsActivitiesOrganised'])->name('analytics.activitiesOrganised');
|
||||
Route::get('analytics/ivOrganised', [CoordinatorController::class, 'analyticsIvOrganised'])->name('analytics.ivOrganised');
|
||||
Route::get('analytics/paperPublished', [CoordinatorController::class, 'analyticsPaperPublished'])->name('analytics.paperPublished');
|
||||
Route::get('analytics/booksPublished', [CoordinatorController::class, 'analyticsBooksPublished'])->name('analytics.booksPublished');
|
||||
Route::get('analytics/externalEngagement', [CoordinatorController::class, 'analyticsExternalEngagement'])->name('analytics.externalEngagement');
|
||||
Route::get('analytics/onlineCourse', [CoordinatorController::class, 'analyticsOnlineCourse'])->name('analytics.onlineCourse');
|
||||
Route::get('analytics/patent', [CoordinatorController::class, 'analyticsPatent'])->name('analytics.patent');
|
||||
|
||||
Route::get('analytics/comparison', [CoordinatorController::class, 'analyticsComparison'])->name('analytics.comparison');
|
||||
Route::get('analytics/contribution', [CoordinatorController::class, 'analyticsContribution'])->name('analytics.contribution');
|
||||
});
|
||||
|
||||
|
||||
Route::post('/admin/download-proofs', [AdminController::class, 'downloadProofs'])->name('admin.downloadProofs');
|
||||
Route::post('/admin/download-proofs', [CoordinatorController::class, 'downloadProofs'])->name('admin.downloadProofs');
|
||||
|
||||
// Google Login Route
|
||||
Route::get('/auth/google', [App\Http\Controllers\Auth\GoogleController::class, 'redirectToGoogle'])->name('google.login');
|
||||
|
||||
Reference in New Issue
Block a user