Updated Publications Page

This commit is contained in:
tanmaychinchore
2025-11-22 02:24:30 +05:30
parent 34394e6abf
commit f81f73d614
3 changed files with 638 additions and 55 deletions

View File

@@ -62,6 +62,7 @@ class PublicationsController extends Controller
$year = date('Y', strtotime($validated['start_date']));
$username = $publication->user->name;
$userId = $publication->user->id;
$originalName = $request->file('paper_file')->getClientOriginalName();
$fileName = $username . '_' . $originalName;
@@ -116,7 +117,7 @@ class PublicationsController extends Controller
return response()->json(['success' => 'Publication deleted successfully']);
}
public function getPublicationsResponses()
public function getPublicationsResponses(Request $request)
{
$user = auth()->user();
$isAdmin = $user->role->name === 'Admin';
@@ -134,6 +135,25 @@ class PublicationsController extends Controller
->where('faculty_id', $user->id);
}
// Apply filters
if ($request->has('department') && !empty($request->department)) {
$publications->whereHas('department', function ($query) use ($request) {
$query->where('id', $request->department);
});
}
if ($request->has('is_peer_reviewed') && !empty($request->is_peer_reviewed)) {
$publications->where('is_peer_reviewed', $request->is_peer_reviewed);
}
if ($request->has('dateFrom') && !empty($request->dateFrom)) {
$publications->where('start_date', '>=', $request->dateFrom);
}
if ($request->has('dateTo') && !empty($request->dateTo)) {
$publications->where('end_date', '<=', $request->dateTo);
}
return DataTables::of($publications)
->addColumn('user_name', function ($publication) {
return $publication->user->name ?? 'Unknown';
@@ -163,9 +183,9 @@ class PublicationsController extends Controller
$actions = [];
if ($publication->paper_file) {
$actions[] = '<a href="' . asset('storage/' . $publication->paper_file) . '" target="_blank" class="btn btn-sm btn-primary mr-1">View Paper</a>';
$actions[] = '<a href="' . asset('storage/' . $publication->paper_file) . '" target="_blank" class="btn btn-sm btn-primary mr-1"><i class="fas fa-eye"></i></a>';
} else {
$actions[] = 'No Paper';
$actions[] = '<span class="text-muted"><i class="fas fa-times-circle"></i></span>';
}
$userRole = auth()->user()->role->name;
@@ -177,10 +197,10 @@ class PublicationsController extends Controller
$editRoute = route('faculty.Publications.edit', $publication->id);
}
$actions[] = '<a href="' . $editRoute . '" class="btn btn-sm btn-info mx-1">Edit</a>';
$actions[] = '<a href="' . $editRoute . '" class="btn btn-sm btn-info mx-1"><i class="fas fa-edit"></i></a>';
$deleteRoute = route('publications.destroy', $publication->id);
$actions[] = '<button type="button" class="btn btn-sm btn-danger delete-btn" data-id="' . $publication->id . '" data-url="' . $deleteRoute . '">Delete</button>';
$actions[] = '<button type="button" class="btn btn-sm btn-danger delete-btn" data-id="' . $publication->id . '" data-url="' . $deleteRoute . '"><i class="fas fa-trash"></i></button>';
return implode(' ', $actions);
})

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('profile_image')->nullable()->after('email');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('profile_image');
});
}
};

View File

@@ -1,7 +1,7 @@
@extends('layouts.app')
@section('content')
<meta name="csrf-token" content="{{ csrf_token() }}">
<!-- <meta name="csrf-token" content="{{ csrf_token() }}"> -->
<div class="container py-4">
<div class="row">
<div class="col-12">
@@ -11,58 +11,139 @@
<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>
<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 pt-0">
<!-- 🔹 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 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 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>
@endif
<!-- Include the reusable send-email component -->
<x-send-email />
</div>
<div class="card-body">
<!-- Filter Controls -->
<div class="row mb-4">
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text bg-light">Department</span>
@if(auth()->user()->role_id == 1)
{{-- Admin: can choose any department --}}
<select id="department-filter" class="form-select">
<option value="">All Departments</option>
@foreach($departments as $department)
<option value="{{ $department->id }}">{{ $department->name }}</option>
@endforeach
</select>
@else
{{-- Coordinator/Faculty: department is fixed --}}
<select id="department-filter" class="form-select" disabled>
<option value="{{ auth()->user()->department_id }}">
{{ auth()->user()->department->name }}
</option>
</select>
@endif
</div>
</div>
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text bg-light">Peer Reviewed</span>
<select id="is_peer_reviewed-filter" class="form-select">
<option value="">All</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
</div>
</div>
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text bg-light">Date Range</span>
<input type="date" id="date-from" class="form-control">
<span class="input-group-text bg-light">to</span>
<input type="date" id="date-to" class="form-control">
</div>
</div>
</div>
<!-- Column Selector -->
<div class="flex justify-between col-md-12 mt-3 mb-4">
<!-- Include the reusable download-proofs component -->
<x-download-proofs :route="route('admin.downloadProofs')" :model="'ActivitiesOrganised'" />
@php
use Illuminate\Support\Str;
$labels = [
'Title',
'First Author',
'Type',
'Journal/Conference',
'Faculty',
'Department',
'Start Date',
'End Date',
'Peer Reviewed',
'Scopus',
'SCI',
];
$columns = [];
foreach ($labels as $i => $label) {
$columns[] = [
'label' => $label,
'id' => 'column-' . Str::slug($label, '-'),
'value' => $i + 2,
'checked' => $label !== 'Peer Reviewed',
];
}
@endphp
<x-column-selector :columns="$columns" />
</div>
@endif
<!-- Table -->
<div class="table-responsive">
<table id="publications-table" class="table table-striped table-hover">
<thead>
<tr>
<th class="select-checkbox"></th>
<th>ID</th>
<th>Title</th>
<th>First Author</th>
@@ -91,14 +172,82 @@
@endsection
@section('scripts')
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/js/all.min.js" integrity="sha512-Tn2m0TIpgVyTzzvmxLNuqbSJH3JP8jm+Cy3hvHrW7ndTDcJ1w5mBiksqDBb8GpE2ksktFvDB/ykZ0mDpsZj20w==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
// 🔹 Global CSRF setup for all fetch() calls
window.csrfToken = document.querySelector('meta[name="csrf-token"]').content;
const downloadProofsRoute = "{{ route('admin.downloadProofs') }}";
const currentModel = "{{ isset($model) ? $model : 'Publications' }}";
const csrf_token = "{{ csrf_token() }}";
$(document).ready(function() {
const sheetName = "Publications";
// Handle "Select All" checkbox for missing proofs modal
$('#selectAll').change(function() {
const isChecked = $(this).prop('checked');
$('input[name="categories[]"]').prop('checked', isChecked);
});
// Update "Select All" when individual checkboxes change
$('input[name="categories[]"]').change(function() {
const totalCheckboxes = $('input[name="categories[]"]').length;
const checkedCheckboxes = $('input[name="categories[]"]:checked').length;
$('#selectAll').prop('checked', totalCheckboxes === checkedCheckboxes);
});
// Form validation for the modal
const form = document.getElementById('missingProofsForm');
if (form) {
form.addEventListener('submit', function(e) {
const checkboxes = form.querySelectorAll('input[type="checkbox"]:checked');
if (checkboxes.length === 0) {
e.preventDefault();
alert('Please select at least one category before sending emails.');
}
});
}
const sheetName = "Publications Report";
let table; // Declare table variable in the outer scope
function exportOptions() {
return {
columns: ':visible',
format: {
body: function(data, row, column, node) {
if ($(node).find('select').length) {
return $(node).find("select option:selected").text();
}
return $(node).text();
}
}
};
}
// Function to toggle column visibility
function toggleColumnVisibility() {
$('.column-checkbox').each(function() {
const columnIndex = $(this).val();
const isChecked = $(this).is(':checked');
// Show or hide the column based on checkbox state
if (table) {
table.column(columnIndex).visible(isChecked);
}
});
// Adjust table layout only if table and responsive are initialized
if (table && table.responsive) {
table.columns.adjust().responsive.recalc();
} else if (table) {
table.columns.adjust();
}
}
var initAjaxRoute = function(route) {
// If table already exists, destroy it before re-initializing
if ($.fn.DataTable.isDataTable('#publications-table')) {
$('#publications-table').DataTable().destroy();
}
table = $("#publications-table").DataTable({
fnDestroy: true,
processing: true,
@@ -106,8 +255,20 @@
responsive: true,
ajax: {
url: route,
data: function(d) {
d.department = $('#department-filter').val();
d.is_peer_reviewed = $('#is_peer_reviewed-filter').val();
d.dateFrom = $('#date-from').val();
d.dateTo = $('#date-to').val();
}
},
columns: [
{
data: null,
defaultContent: '',
orderable: false,
searchable: false
},
{ data: 'id', name: 'id', searchable: false },
{ data: 'title', name: 'title', orderable: true },
{ data: 'first_author_name', name: 'first_author_name', orderable: true },
@@ -123,15 +284,66 @@
{ data: 'action', name: 'action', orderable: false, searchable: false },
],
columnDefs: [
{ targets: '_all', className: 'text-center wrap-text' },
{
targets: 0,
orderable: false,
className: 'select-checkbox',
render: function(data, type, row) {
return row.proof ?
'<input type="checkbox" class="row-checkbox" data-id="' + row.id + '">' :
'<input type="checkbox" disabled>';
}
},
{
targets: '_all',
className: 'text-center wrap-text'
},
{
targets: 13,
render: function(data, type, row) {
return '<div class="btn-group" role="group">' +
data +
'</div>';
}
}
],
dom: '<"d-flex justify-content-between align-items-center mb-3"<"d-flex align-items-center"l><"d-flex"f<"ms-2"B>>>rtip',
buttons: [
{ extend: 'copy', text: '<i class="fas fa-copy me-1"></i> Copy', className: 'btn btn-sm btn-outline-white', title: sheetName },
{ extend: 'csv', text: '<i class="fas fa-file-csv me-1"></i> CSV', className: 'btn btn-sm btn-outline-white', title: sheetName },
{ extend: 'excel', text: '<i class="fas fa-file-excel me-1"></i> Excel', className: 'btn btn-sm btn-outline-white', title: sheetName },
{ extend: 'pdf', text: '<i class="fas fa-file-pdf me-1"></i> PDF', className: 'btn btn-sm btn-outline-white', title: sheetName },
{ extend: 'print', text: '<i class="fas fa-print me-1"></i> Print', className: 'btn btn-sm btn-outline-white', title: sheetName },
{
extend: 'copy',
text: '<i class="fas fa-copy me-1"></i> Copy',
className: 'btn btn-sm btn-outline-white',
title: sheetName,
exportOptions: exportOptions()
},
{
extend: 'csv',
text: '<i class="fas fa-file-csv me-1"></i> CSV',
className: 'btn btn-sm btn-outline-white',
title: sheetName,
exportOptions: exportOptions()
},
{
extend: 'excel',
text: '<i class="fas fa-file-excel me-1"></i> Excel',
className: 'btn btn-sm btn-outline-white',
title: sheetName,
exportOptions: exportOptions()
},
{
extend: 'pdf',
text: '<i class="fas fa-file-pdf me-1"></i> PDF',
className: 'btn btn-sm btn-outline-white',
title: sheetName,
exportOptions: exportOptions()
},
{
extend: 'print',
text: '<i class="fas fa-print me-1"></i> Print',
className: 'btn btn-sm btn-outline-white',
title: sheetName,
exportOptions: exportOptions()
}
],
language: {
search: "<i class='fas fa-search'></i> _INPUT_",
@@ -144,8 +356,160 @@
next: "<i class='fas fa-angle-right'></i>",
previous: "<i class='fas fa-angle-left'></i>"
}
},
// Initialize the column visibility after table is drawn
initComplete: function() {
// Set column visibility
toggleColumnVisibility();
// Add select-all checkbox
addSelectAllCheckbox();
},
drawCallback: function() {
// Ensure select-all checkbox is present on redraw
addSelectAllCheckbox();
}
});
// Apply filters when they change
$('#department-filter, #is_peer_reviewed-filter, #date-from, #date-to').change(function() {
table.ajax.reload();
});
return table;
};
// Delete button handler
$('#publications-table').on('click', '.delete-btn', function() {
if (confirm('Are you sure you want to delete this record?')) {
const id = $(this).data('id');
const url = $(this).data('url');
$.ajax({
url: url,
type: 'DELETE',
data: {
"_token": "{{ csrf_token() }}"
},
success: function(result) {
table.ajax.reload();
showToast('Record deleted successfully', 'success');
},
error: function(error) {
console.error(error);
showToast('Error deleting record', 'danger');
}
});
}
});
// Handle row selection and Download button state
$('#publications-table').on('change', '.row-checkbox', function() {
const selectedRows = $('.row-checkbox:checked').length;
$('#download-proofs').prop('disabled', selectedRows === 0);
});
// Download proofs button handler
$('#download-proofs').on('click', function() {
const selectedIds = [];
$('.row-checkbox:checked').each(function() {
const id = $(this).data('id');
if (id) selectedIds.push(id);
});
if (selectedIds.length > 0) {
// Create a form to submit the request
const form = $('<form></form>')
.attr('method', 'POST')
.attr('action', downloadProofsRoute)
.css('display', 'none');
// Add CSRF token
$('<input>')
.attr('type', 'hidden')
.attr('name', '_token')
.attr('value', csrf_token)
.appendTo(form);
// Add selected IDs
$('<input>')
.attr('type', 'hidden')
.attr('name', 'ids')
.attr('value', JSON.stringify(selectedIds))
.appendTo(form);
// Add model name
$('<input>')
.attr('type', 'hidden')
.attr('name', 'model')
.attr('value', currentModel)
.appendTo(form);
$('body').append(form);
form.submit();
}
});
// Function to add select-all checkbox to the table header
function addSelectAllCheckbox() {
// Only add if it doesn't exist yet
if ($('#select-all-checkbox').length === 0) {
const selectAllCheckbox = $('<input>', {
type: 'checkbox',
id: 'select-all-checkbox',
class: 'form-check-input'
});
// Add to the first header column
$('#publications-table thead th.select-checkbox').html(selectAllCheckbox);
// Handle the select all functionality
$('#select-all-checkbox').on('change', function() {
const isChecked = $(this).prop('checked');
$('.row-checkbox:not(:disabled)').prop('checked', isChecked);
// Update download button state
const selectedRows = $('.row-checkbox:checked').length;
$('#download-proofs').prop('disabled', selectedRows === 0);
});
}
}
// Toast notification function
function showToast(message, type) {
const toastContainer = document.createElement('div');
toastContainer.className = 'position-fixed bottom-0 start-0 p-3';
toastContainer.style.zIndex = '1050';
const toastEl = document.createElement('div');
toastEl.className = `toast align-items-center text-white bg-${type} border-0`;
toastEl.setAttribute('role', 'alert');
toastEl.setAttribute('aria-live', 'assertive');
toastEl.setAttribute('aria-atomic', 'true');
toastEl.innerHTML = `
<div class="d-flex">
<div class="toast-body">
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
toastContainer.appendChild(toastEl);
document.body.appendChild(toastContainer);
const toast = new bootstrap.Toast(toastEl, {
autohide: true,
delay: 3000
});
toast.show();
toastEl.addEventListener('hidden.bs.toast', function() {
document.body.removeChild(toastContainer);
});
};
// Set appropriate route based on user role
@@ -159,7 +523,40 @@
dataRoute = "{{ route('faculty.PublicationsResponses.data') }}";
}
initAjaxRoute(dataRoute);
// Initialize the data table
table = initAjaxRoute(dataRoute);
// Attach change event listener to column visibility checkboxes
$('.column-checkbox').on('change', function() {
toggleColumnVisibility();
});
// Ensure "Actions" column is always visible
$('#column-actions').prop('disabled', true);
// Select all columns button
$('#select-all-columns').click(function() {
$('.column-checkbox').prop('checked', true).trigger('change');
});
// Deselect all columns button
$('#deselect-all-columns').click(function() {
$('.column-checkbox').prop('checked', false).trigger('change');
});
// Prevent dropdown from closing when clicking inside it
$('.dropdown-menu').on('click', function(e) {
e.stopPropagation();
});
// Set department filter from query string if present
$(document).ready(function() {
const urlParams = new URLSearchParams(window.location.search);
const departmentId = urlParams.get('department_id');
if (departmentId) {
$('#department-filter').val(departmentId).trigger('change');
}
});
});
// 🔹 Handle delete button click (for AJAX DataTable)
@@ -286,4 +683,142 @@
}
@endif
</script>
@endsection
<style>
.form-check-input:checked {
background-color: #b7202e;
border-color: #ed1c24;
}
.dropdown-menu {
background-color: #fff;
border-radius: 0.5rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.dropdown-menu .form-check-input:checked {
background-color: #b7202e;
border-color: #ed1c24;
}
.dropdown-menu .form-check-input:focus {
box-shadow: 0 0 0 0.2rem rgba(189, 72, 46, 0.25);
}
.dropdown-menu .form-check-input:checked:focus {
box-shadow: 0 0 0 0.2rem rgba(189, 72, 46, 0.25);
}
.dropdown-menu .form-check-input:focus-visible {
outline: none;
box-shadow: 0 0 0 0.2rem rgba(189, 72, 46, 0.25);
}
.dropdown-menu .form-check-input:checked:focus-visible {
box-shadow: 0 0 0 0.2rem rgba(189, 72, 46, 0.25);
}
.dropdown-menu .form-check-input:focus-visible:checked {
box-shadow: 0 0 0 0.2rem rgba(189, 72, 46, 0.25);
}
.dropdown-menu .form-check-input:focus-visible:not(:checked) {
box-shadow: 0 0 0 0.2rem rgba(189, 72, 46, 0.25);
}
.dropdown-menu .form-check-input:focus-visible:not(:checked):checked {
box-shadow: 0 0 0 0.2rem rgba(189, 72, 46, 0.25);
}
.dropdown-menu .form-check-input:focus-visible:not(:checked):not(:checked) {
box-shadow: 0 0 0 0.2rem rgba(189, 72, 46, 0.25);
}
.dropdown-menu .form-check-input:focus-visible:not(:checked):not(:checked):checked {
box-shadow: 0 0 0 0.2rem rgba(189, 72, 46, 0.25);
}
.dropdown-menu .form-check-input:focus-visible:not(:checked):not(:checked):not(:checked) {
box-shadow: 0 0 0 0.2rem rgba(189, 72, 46, 0.25);
}
.dropdown-menu .form-check-input:focus-visible:not(:checked):not(:checked):not(:checked):checked {
box-shadow: 0 0 0 0.2rem rgba(189, 72, 46, 0.25);
}
.dropdown-menu .form-check-input:focus-visible:not(:checked):not(:checked):not(:checked):not(:checked) {
box-shadow: 0 0 0 0.2rem rgba(189, 72, 46, 0.25);
}
.form-switch .form-check-input {
width: 2.5em;
margin-left: -2.8em;
position: relative;
}
.form-switch .form-check-input:focus {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");
}
.form-switch .form-check-input:checked {
background-position: right center;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");
transition: background-position 0.15s ease-in-out;
}
/* Enhance the toggle appearance */
.form-switch .form-check-input {
background-size: contain;
transition: 0.2s;
}
.dropdown-menu {
max-height: 400px;
overflow-y: auto;
}
/* Ensure the table container is responsive */
.table-responsive {
overflow-x: auto;
white-space: nowrap;
}
/* Ensure the table fits within the container */
#publications-table {
width: 100% !important;
}
.select-checkbox {
width: 30px;
text-align: center;
}
.select-checkbox input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.select-checkbox input[type="checkbox"]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Style the Download button */
#download-proofs {
background-color: #28a745;
border-color: #28a745;
}
#download-proofs:hover:not(:disabled) {
background-color: #218838;
border-color: #218838;
}
#download-proofs:disabled {
opacity: 0.65;
cursor: not-allowed;
}
</style>
@endsection