feat(profile,publications): integrate working Google Scholar import flow and unified faculty profile system
This commit is contained in:
14
resources/views/faculty/profile.blade.php
Normal file
14
resources/views/faculty/profile.blade.php
Normal file
@@ -0,0 +1,14 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="container py-5">
|
||||
<h1 class="mb-4">Faculty Profile</h1>
|
||||
|
||||
<div class="card p-4 shadow-sm">
|
||||
<p><strong>Name:</strong> {{ $profile->name }}</p>
|
||||
<p><strong>Email:</strong> {{ $profile->email }}</p>
|
||||
<p><strong>Phone Number:</strong> {{ $profile->phone_number ?: 'N/A' }}</p>
|
||||
<p><strong>Department:</strong> {{ $profile->department_name ?: 'N/A' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -1,4 +1,6 @@
|
||||
<link rel="stylesheet" href="{{ asset('assets/frontend/css/app.css') }}">
|
||||
<!-- Add Font Awesome for icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
.navbar-profile-picture {
|
||||
width: 40px;
|
||||
@@ -12,6 +14,16 @@
|
||||
body {
|
||||
padding-top: 70px;
|
||||
}
|
||||
|
||||
/* Style for dropdown toggle without underline */
|
||||
.dropdown-toggle:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Ensure dropdown menu appears above other content */
|
||||
.dropdown-menu {
|
||||
z-index: 1030;
|
||||
}
|
||||
</style>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bgGrad text-white fixed-top">
|
||||
<div class="container-fluid">
|
||||
@@ -29,13 +41,35 @@
|
||||
@if (!Auth::guest())
|
||||
<ul class="navbar-nav me-md-5">
|
||||
<div class="d-flex align-items-center bgRightShade">
|
||||
<div class="dropdown ms-2">
|
||||
<a class="d-flex align-items-center text-white text-decoration-none dropdown-toggle"
|
||||
href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<img src="{{ asset('assets/frontend/images/user.png') }}"
|
||||
alt="profile"
|
||||
class="navbar-profile-picture me-2">
|
||||
<span>{{ Auth::user()->name }}</span>
|
||||
</a>
|
||||
|
||||
<div class="d-none d-md-inline ms-2">
|
||||
{{ auth()->user()->name }}
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ route('faculty.profile.index') }}">
|
||||
<i class="fa-solid fa-id-card me-2 text-primary"></i> Profile
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button type="submit" class="dropdown-item text-danger">
|
||||
<i class="fa-solid fa-right-from-bracket me-2"></i> Logout
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</ul>
|
||||
@endif
|
||||
@yield('login')
|
||||
</div>
|
||||
</nav>
|
||||
</nav>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<div class="container py-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@@ -9,8 +10,54 @@
|
||||
<h3 class="page-title m-0">
|
||||
<i class="fas fa-book-open me-2 text-primary"></i>All Publications
|
||||
</h3>
|
||||
@if(auth()->user()->role->name === 'Faculty')
|
||||
<input type="hidden" id="scholar-url" value="{{ $scholarUrl ?? '' }}">
|
||||
<button id="btn-fetch-scholar" class="btn btn-outline-primary">
|
||||
<i class="fab fa-google me-1"></i> Import from Google Scholar
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- 🔹 Google Scholar Modal -->
|
||||
@if(auth()->user()->role->name === 'Faculty')
|
||||
<div class="modal fade" id="scholarModal" tabindex="-1" aria-labelledby="scholarModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="scholarModalLabel">Select Publications to Import</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="scholar-loading" class="text-center my-4 d-none">
|
||||
<div class="spinner-border text-primary"></div>
|
||||
<p class="mt-2">Fetching from Google Scholar...</p>
|
||||
</div>
|
||||
|
||||
<div id="scholar-table-wrapper" class="table-responsive d-none">
|
||||
<table class="table table-bordered align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="select-all"></th>
|
||||
<th>Title</th>
|
||||
<th>Authors</th>
|
||||
<th>Journal</th>
|
||||
<th>Year</th>
|
||||
<th>Link</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="scholar-table-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button id="btn-import-selected" class="btn btn-success" disabled>Import Selected</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Table -->
|
||||
<div class="table-responsive">
|
||||
<table id="publications-table" class="table table-striped table-hover">
|
||||
@@ -45,6 +92,9 @@
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
// 🔹 Global CSRF setup for all fetch() calls
|
||||
window.csrfToken = document.querySelector('meta[name="csrf-token"]').content;
|
||||
|
||||
$(document).ready(function() {
|
||||
const sheetName = "Publications";
|
||||
|
||||
@@ -111,5 +161,129 @@
|
||||
|
||||
initAjaxRoute(dataRoute);
|
||||
});
|
||||
|
||||
// 🔹 Handle delete button click (for AJAX DataTable)
|
||||
$(document).on('click', '.delete-btn', function () {
|
||||
const button = $(this);
|
||||
const id = button.data('id');
|
||||
const url = button.data('url');
|
||||
|
||||
if (!confirm('Are you sure you want to delete this publication?')) return;
|
||||
|
||||
fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': window.csrfToken,
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
alert(data.success || 'Deleted successfully');
|
||||
$('#publications-table').DataTable().ajax.reload(null, false);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
alert('Failed to delete publication.');
|
||||
});
|
||||
});
|
||||
|
||||
// 🔹 Google Scholar Integration Script (Faculty Only)
|
||||
@if(auth()->user()->role->name === 'Faculty')
|
||||
const fetchBtn = document.getElementById('btn-fetch-scholar');
|
||||
if (fetchBtn) {
|
||||
const modal = new bootstrap.Modal(document.getElementById('scholarModal'));
|
||||
const body = document.getElementById('scholar-table-body');
|
||||
const loading = document.getElementById('scholar-loading');
|
||||
const tableWrap = document.getElementById('scholar-table-wrapper');
|
||||
const importBtn = document.getElementById('btn-import-selected');
|
||||
const scholarUrlInput = document.getElementById('scholar-url');
|
||||
|
||||
fetchBtn.addEventListener('click', () => {
|
||||
const scholarUrl = scholarUrlInput.value.trim();
|
||||
|
||||
if (!scholarUrl) {
|
||||
alert('Please set your Google Scholar profile URL in your profile settings first.');
|
||||
return;
|
||||
}
|
||||
|
||||
modal.show();
|
||||
loading.classList.remove('d-none');
|
||||
tableWrap.classList.add('d-none');
|
||||
body.innerHTML = '';
|
||||
|
||||
// ✅ No need to redeclare scholarUrl here again
|
||||
fetch("{{ route('faculty.publications.fetchScholar') }}", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-TOKEN": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ url: scholarUrl })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
loading.classList.add('d-none');
|
||||
if (data.error) {
|
||||
body.innerHTML = `<tr><td colspan='6' class='text-danger text-center'>${data.error}</td></tr>`;
|
||||
} else {
|
||||
const pubs = data.data;
|
||||
pubs.forEach((p, i) => {
|
||||
body.innerHTML += `
|
||||
<tr>
|
||||
<td><input type="checkbox" class="pub-check" data-index="${i}"></td>
|
||||
<td>${p.title}</td>
|
||||
<td>${p.authors || '-'}</td>
|
||||
<td>${p.journal || '-'}</td>
|
||||
<td>${p.year || '-'}</td>
|
||||
<td><a href="${p.link}" target="_blank">View</a></td>
|
||||
</tr>`;
|
||||
});
|
||||
tableWrap.classList.remove('d-none');
|
||||
importBtn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
loading.classList.add('d-none');
|
||||
body.innerHTML = `<tr><td colspan='6' class='text-danger text-center'>Failed to fetch data.</td></tr>`;
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('select-all').addEventListener('change', function() {
|
||||
document.querySelectorAll('.pub-check').forEach(cb => cb.checked = this.checked);
|
||||
});
|
||||
|
||||
importBtn.addEventListener('click', () => {
|
||||
const selected = Array.from(document.querySelectorAll('.pub-check:checked')).map(cb => {
|
||||
const row = cb.closest('tr').children;
|
||||
return {
|
||||
title: row[1].innerText.trim(),
|
||||
authors: row[2].innerText.trim(),
|
||||
journal: row[3].innerText.trim(),
|
||||
year: row[4].innerText.trim(),
|
||||
link: row[5].querySelector('a')?.href || null
|
||||
};
|
||||
});
|
||||
|
||||
if (selected.length === 0) return alert("Please select at least one publication.");
|
||||
|
||||
fetch("{{ route('faculty.publications.importScholar') }}", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-TOKEN": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ publications: selected })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
alert(data.message);
|
||||
modal.hide();
|
||||
location.reload();
|
||||
})
|
||||
.catch(() => alert("Import failed!"));
|
||||
});
|
||||
}
|
||||
@endif
|
||||
</script>
|
||||
@endsection
|
||||
@endsection
|
||||
|
||||
@@ -1,29 +1,105 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
{{ __('Profile') }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
@extends('layouts.app')
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
||||
<div class="max-w-xl">
|
||||
@include('profile.partials.update-profile-information-form')
|
||||
</div>
|
||||
@section('content')
|
||||
<div class="container mt-4">
|
||||
<h4 class="mb-4">Edit Faculty Profile</h4>
|
||||
|
||||
<form action="{{ route('faculty.profile.update') }}" method="POST" enctype="multipart/form-data" class="card shadow-sm p-4">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Full Name</label>
|
||||
<input type="text" name="name" class="form-control" value="{{ old('name', $profile->name) }}" required>
|
||||
</div>
|
||||
|
||||
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
||||
<div class="max-w-xl">
|
||||
@include('profile.partials.update-password-form')
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Email</label>
|
||||
<input type="email" name="email" class="form-control" value="{{ old('email', $profile->email) }}" required>
|
||||
</div>
|
||||
|
||||
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
||||
<div class="max-w-xl">
|
||||
@include('profile.partials.delete-user-form')
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Phone Number</label>
|
||||
<input type="text" name="phone_number" class="form-control" value="{{ old('phone_number', $profile->phone_number) }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Employee ID</label>
|
||||
<input type="text" name="employee_id" class="form-control" value="{{ old('employee_id', $profile->employee_id) }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Department</label>
|
||||
<input type="text" name="department_name" class="form-control" value="{{ old('department_name', $profile->department_name) }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Programme</label>
|
||||
<input type="text" name="programme_name" class="form-control" value="{{ old('programme_name', $profile->programme_name) }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-semibold">Salutation</label>
|
||||
<input type="text" name="salutation" class="form-control" value="{{ old('salutation', $profile->salutation) }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-semibold">Designation</label>
|
||||
<input type="text" name="designation" class="form-control" value="{{ old('designation', $profile->designation) }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-semibold">Date of Joining</label>
|
||||
<input type="date" name="date_of_joining" class="form-control" value="{{ old('date_of_joining', $profile->date_of_joining?->format('Y-m-d')) }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Google Scholar ID</label>
|
||||
<input type="text" class="form-control" id="google_scholar_id" name="google_scholar_id"
|
||||
value="{{ old('google_scholar_id', $profile->google_scholar_id) }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Vidhwan ID</label>
|
||||
<input type="text" class="form-control" id="vidhwan_id" name="vidhwan_id"
|
||||
value="{{ old('vidhwan_id', $profile->vidhwan_id) }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">LinkedIn Profile</label>
|
||||
<input type="text" class="form-control" id="linkedin_id" name="linkedin_id"
|
||||
value="{{ old('linkedin_id', $profile->linkedin_id) }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label fw-semibold">Qualification</label>
|
||||
<textarea name="qualification" class="form-control" rows="2">{{ is_array($profile->qualification) ? implode(', ', $profile->qualification) : $profile->qualification }}</textarea>
|
||||
<small class="text-muted">Separate multiple qualifications with commas (e.g., B.Tech, M.Tech, Ph.D)</small>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label fw-semibold">Address</label>
|
||||
<textarea name="address" class="form-control" rows="2">{{ old('address', $profile->address) }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Profile Photo</label>
|
||||
<input type="file" name="profile_photo" class="form-control">
|
||||
|
||||
@if ($profile->profile_photo_path)
|
||||
<div class="mt-2">
|
||||
<img src="{{ Storage::url($profile->profile_photo_path) }}" alt="Profile Photo" width="100" class="rounded shadow-sm">
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
|
||||
<div class="text-end mt-3">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fa fa-save me-1"></i> Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
89
resources/views/profile/index.blade.php
Normal file
89
resources/views/profile/index.blade.php
Normal file
@@ -0,0 +1,89 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="container mt-4">
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
{{ session('success') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<h4 class="mb-4">Faculty Profile</h4>
|
||||
|
||||
<div class="card shadow-sm p-4">
|
||||
<div class="row align-items-center mb-3">
|
||||
<div class="col-md-2 text-center">
|
||||
@if($profile->profile_photo_path)
|
||||
<img src="{{ Storage::url($profile->profile_photo_path) }}"
|
||||
alt="Profile Photo"
|
||||
class="rounded-circle shadow-sm"
|
||||
width="120" height="120">
|
||||
@else
|
||||
<img src="{{ asset('assets/frontend/images/default-avatar.png') }}"
|
||||
alt="Default Photo"
|
||||
class="rounded-circle shadow-sm"
|
||||
width="120" height="120">
|
||||
@endif
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<h5 class="fw-bold mb-1">{{ $profile->salutation }} {{ $profile->name }}</h5>
|
||||
<p class="mb-0"><strong>{{ $profile->designation }}</strong></p>
|
||||
<p class="text-muted mb-0">{{ $profile->department_name }} — {{ $profile->programme_name }}</p>
|
||||
|
||||
{{-- Compact links row --}}
|
||||
<div class="mt-2 text-md-start text-center">
|
||||
@if($profile->google_scholar_id)
|
||||
<a href="{{ $profile->google_scholar_id }}" target="_blank" rel="noopener noreferrer"
|
||||
class="btn btn-outline-primary btn-sm me-2">
|
||||
<i class="fa fa-graduation-cap me-1"></i> Scholar
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if($profile->vidhwan_id)
|
||||
<a href="https://vidwan.inflibnet.ac.in/profile/{{ $profile->vidhwan_id }}"
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
class="btn btn-outline-secondary btn-sm me-2">
|
||||
<i class="fa fa-id-card me-1"></i> Vidhwan
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if($profile->linkedin_id)
|
||||
<a href="{{ $profile->linkedin_id }}" target="_blank" rel="noopener noreferrer"
|
||||
class="btn btn-outline-info btn-sm">
|
||||
<i class="fa fa-linkedin"></i> LinkedIn
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>Email:</strong> {{ $profile->email }}</p>
|
||||
<p><strong>Phone:</strong> {{ $profile->phone_number ?: 'N/A' }}</p>
|
||||
<p><strong>Employee ID:</strong> {{ $profile->employee_id ?: 'N/A' }}</p>
|
||||
<p><strong>Date of Joining:</strong>
|
||||
{{ $profile->date_of_joining ? $profile->date_of_joining->format('d M Y') : 'N/A' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<p><strong>Qualification:</strong>
|
||||
{{ is_array($profile->qualification) ? implode(', ', $profile->qualification) : ($profile->qualification ?: 'N/A') }}
|
||||
</p>
|
||||
<p><strong>Address:</strong> {{ $profile->address ?: 'N/A' }}</p>
|
||||
<p><strong>Vidhwan ID:</strong> {{ $profile->vidhwan_id ?: 'N/A' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-end mt-3">
|
||||
<a href="{{ route('faculty.profile.edit') }}" class="btn btn-primary">
|
||||
<i class="fa fa-pen me-1"></i> Edit Profile
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
Reference in New Issue
Block a user