publication added newly

This commit is contained in:
2025-11-06 14:52:45 +05:30
parent 311ca61dea
commit a9cc0a9122
18 changed files with 2118 additions and 21 deletions

View File

@ -0,0 +1,140 @@
package net.shyshkin.study.fullstack.supportportal.backend.resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.shyshkin.study.fullstack.supportportal.backend.domain.HttpResponse;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Publication;
import net.shyshkin.study.fullstack.supportportal.backend.service.PublicationService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/publications")
@RequiredArgsConstructor
@Slf4j
@CrossOrigin
public class PublicationResource {
private final PublicationService publicationService;
@PostMapping
@PreAuthorize("hasAnyAuthority('user:create')")
public ResponseEntity<Publication> addPublication(
@RequestParam String title,
@RequestParam String authors,
@RequestParam Integer year,
@RequestParam String journal,
@RequestParam(required = false) String doi,
@RequestParam(required = false) String category,
@RequestParam(required = false) String abstractText,
@RequestParam(required = false) String publicationDate,
@RequestParam(required = false) String keywords,
@RequestParam(required = false) Integer displayOrder) {
log.info("Adding new publication: {}", title);
Publication publication = publicationService.addPublication(
title, authors, year, journal, doi, category, abstractText, publicationDate, keywords, displayOrder
);
return ResponseEntity.status(HttpStatus.CREATED).body(publication);
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyAuthority('user:update')")
public ResponseEntity<Publication> updatePublication(
@PathVariable Long id,
@RequestParam String title,
@RequestParam String authors,
@RequestParam Integer year,
@RequestParam String journal,
@RequestParam(required = false) String doi,
@RequestParam(required = false) String category,
@RequestParam(required = false) String abstractText,
@RequestParam(required = false) String publicationDate,
@RequestParam(required = false) String keywords,
@RequestParam(required = false) Integer displayOrder) {
log.info("Updating publication with id: {}", id);
Publication publication = publicationService.updatePublication(
id, title, authors, year, journal, doi, category, abstractText, publicationDate, keywords, displayOrder
);
return ResponseEntity.ok(publication);
}
@GetMapping("/active")
public ResponseEntity<List<Publication>> getActivePublications() {
log.info("Getting active publications");
List<Publication> publications = publicationService.getActivePublications();
return ResponseEntity.ok(publications);
}
@GetMapping("/{id}")
public ResponseEntity<Publication> getPublicationById(@PathVariable Long id) {
log.info("Getting publication with id: {}", id);
Publication publication = publicationService.getPublicationById(id);
return ResponseEntity.ok(publication);
}
@GetMapping
@PreAuthorize("hasAnyAuthority('user:read')")
public ResponseEntity<List<Publication>> getAllPublications() {
log.info("Getting all publications");
List<Publication> publications = publicationService.getAllPublications();
return ResponseEntity.ok(publications);
}
@GetMapping("/category/{category}")
public ResponseEntity<List<Publication>> getPublicationsByCategory(@PathVariable String category) {
log.info("Getting publications by category: {}", category);
List<Publication> publications = publicationService.getPublicationsByCategory(category);
return ResponseEntity.ok(publications);
}
@GetMapping("/year/{year}")
public ResponseEntity<List<Publication>> getPublicationsByYear(@PathVariable Integer year) {
log.info("Getting publications by year: {}", year);
List<Publication> publications = publicationService.getPublicationsByYear(year);
return ResponseEntity.ok(publications);
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAnyAuthority('user:delete')")
public ResponseEntity<HttpResponse> deletePublication(@PathVariable Long id) {
log.info("Deleting publication with id: {}", id);
publicationService.deletePublication(id);
return ResponseEntity.ok(
HttpResponse.builder()
.httpStatusCode(HttpStatus.OK.value())
.httpStatus(HttpStatus.OK)
.reason(HttpStatus.OK.getReasonPhrase())
.message("Publication deleted successfully")
.build()
);
}
@PutMapping("/{id}/toggle-active")
@PreAuthorize("hasAnyAuthority('user:update')")
public ResponseEntity<Publication> toggleActiveStatus(@PathVariable Long id) {
log.info("Toggling active status for publication with id: {}", id);
Publication publication = publicationService.toggleActiveStatus(id);
return ResponseEntity.ok(publication);
}
@PutMapping("/reorder")
@PreAuthorize("hasAnyAuthority('user:update')")
public ResponseEntity<HttpResponse> reorderPublications(@RequestBody List<Long> orderedIds) {
log.info("Reordering publications");
publicationService.reorderPublications(orderedIds);
return ResponseEntity.ok(
HttpResponse.builder()
.httpStatusCode(HttpStatus.OK.value())
.httpStatus(HttpStatus.OK)
.reason(HttpStatus.OK.getReasonPhrase())
.message("Publications reordered successfully")
.build()
);
}
}

View File

@ -0,0 +1,74 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "publications")
public class Publication implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false)
private Long id;
@Column(columnDefinition = "TEXT", nullable = false)
private String title;
@Column(columnDefinition = "TEXT")
private String authors; // Stored as comma-separated string
@Column(nullable = false)
private Integer year;
@Column(columnDefinition = "TEXT", nullable = false)
private String journal;
@Column(columnDefinition = "TEXT")
private String doi;
@Column(length = 100)
private String category;
@Column(columnDefinition = "LONGTEXT")
private String abstractText;
@Column(length = 100)
private String publicationDate;
@Column(columnDefinition = "TEXT")
private String keywords; // Stored as comma-separated string
@JsonProperty("isActive")
@Column(name = "is_active")
private boolean isActive;
@Column(name = "display_order")
private Integer displayOrder;
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModified;
@PrePersist
protected void onCreate() {
createdDate = new Date();
lastModified = new Date();
}
@PreUpdate
protected void onUpdate() {
lastModified = new Date();
}
}

View File

@ -0,0 +1,8 @@
package net.shyshkin.study.fullstack.supportportal.backend.exception.domain;
public class PublicationNotFoundException extends RuntimeException {
public PublicationNotFoundException(String message) {
super(message);
}
}

View File

@ -0,0 +1,22 @@
package net.shyshkin.study.fullstack.supportportal.backend.repository;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Publication;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface PublicationRepository extends JpaRepository<Publication, Long> {
@Query("SELECT p FROM Publication p WHERE p.isActive = true ORDER BY p.year DESC, p.displayOrder ASC, p.id DESC")
List<Publication> findByIsActiveTrueOrderByYearDesc();
@Query("SELECT p FROM Publication p ORDER BY p.year DESC, p.displayOrder ASC, p.id DESC")
List<Publication> findAllOrderByYearDesc();
List<Publication> findByCategory(String category);
List<Publication> findByYear(Integer year);
}

View File

@ -0,0 +1,33 @@
package net.shyshkin.study.fullstack.supportportal.backend.service;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Publication;
import java.util.List;
public interface PublicationService {
Publication addPublication(String title, String authors, Integer year, String journal,
String doi, String category, String abstractText,
String publicationDate, String keywords, Integer displayOrder);
Publication updatePublication(Long id, String title, String authors, Integer year,
String journal, String doi, String category,
String abstractText, String publicationDate,
String keywords, Integer displayOrder);
List<Publication> getActivePublications();
Publication getPublicationById(Long id);
List<Publication> getAllPublications();
List<Publication> getPublicationsByCategory(String category);
List<Publication> getPublicationsByYear(Integer year);
void deletePublication(Long id);
Publication toggleActiveStatus(Long id);
void reorderPublications(List<Long> orderedIds);
}

View File

@ -0,0 +1,129 @@
package net.shyshkin.study.fullstack.supportportal.backend.service.impl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Publication;
import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.PublicationNotFoundException;
import net.shyshkin.study.fullstack.supportportal.backend.repository.PublicationRepository;
import net.shyshkin.study.fullstack.supportportal.backend.service.PublicationService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Slf4j
@RequiredArgsConstructor
@Transactional
public class PublicationServiceImpl implements PublicationService {
private final PublicationRepository publicationRepository;
@Override
public Publication addPublication(String title, String authors, Integer year, String journal,
String doi, String category, String abstractText,
String publicationDate, String keywords, Integer displayOrder) {
log.info("Adding new publication: {}", title);
Publication publication = new Publication();
publication.setTitle(title);
publication.setAuthors(authors);
publication.setYear(year);
publication.setJournal(journal);
publication.setDoi(doi);
publication.setCategory(category);
publication.setAbstractText(abstractText);
publication.setPublicationDate(publicationDate);
publication.setKeywords(keywords);
publication.setDisplayOrder(displayOrder != null ? displayOrder : 0);
publication.setActive(true);
return publicationRepository.save(publication);
}
@Override
public Publication updatePublication(Long id, String title, String authors, Integer year,
String journal, String doi, String category,
String abstractText, String publicationDate,
String keywords, Integer displayOrder) {
log.info("Updating publication with id: {}", id);
Publication publication = getPublicationById(id);
publication.setTitle(title);
publication.setAuthors(authors);
publication.setYear(year);
publication.setJournal(journal);
publication.setDoi(doi);
publication.setCategory(category);
publication.setAbstractText(abstractText);
publication.setPublicationDate(publicationDate);
publication.setKeywords(keywords);
if (displayOrder != null) {
publication.setDisplayOrder(displayOrder);
}
return publicationRepository.save(publication);
}
@Override
@Transactional(readOnly = true)
public List<Publication> getActivePublications() {
return publicationRepository.findByIsActiveTrueOrderByYearDesc();
}
@Override
@Transactional(readOnly = true)
public Publication getPublicationById(Long id) {
return publicationRepository.findById(id)
.orElseThrow(() -> new PublicationNotFoundException("Publication not found with id: " + id));
}
@Override
@Transactional(readOnly = true)
public List<Publication> getAllPublications() {
return publicationRepository.findAllOrderByYearDesc();
}
@Override
@Transactional(readOnly = true)
public List<Publication> getPublicationsByCategory(String category) {
return publicationRepository.findByCategory(category);
}
@Override
@Transactional(readOnly = true)
public List<Publication> getPublicationsByYear(Integer year) {
return publicationRepository.findByYear(year);
}
@Override
public void deletePublication(Long id) {
log.info("Deleting publication with id: {}", id);
Publication publication = getPublicationById(id);
publicationRepository.delete(publication);
}
@Override
public Publication toggleActiveStatus(Long id) {
log.info("Toggling active status for publication with id: {}", id);
Publication publication = getPublicationById(id);
publication.setActive(!publication.isActive());
return publicationRepository.save(publication);
}
@Override
public void reorderPublications(List<Long> orderedIds) {
log.info("Reordering {} publications", orderedIds.size());
for (int i = 0; i < orderedIds.size(); i++) {
Long id = orderedIds.get(i);
Publication publication = getPublicationById(id);
publication.setDisplayOrder(i);
publicationRepository.save(publication);
}
}
}

View File

@ -51,7 +51,7 @@ file:
app:
base-url: ${APP_BASE_URL:http://localhost:8080}
# Fixed public URLs with correct wildcard patterns
public-urls: /user/login,/user/register,/user/*/profile-image,/user/*/profile-image/**,/professors,/professors/**,/api/posts,/api/posts/*,/api/posts/posted,/api/posts/tag/*,/api/posts/tags/count,/api/files/**,/uploads/**,/professor/**,/api/events,/api/events/*,/api/public/**,/api/jobs/active,/api/job-applications/**,/api/job-applications/resume/**,/api/courses/active,/api/courses/*,/api/course-applications,/api/upcoming-events/active,/api/milestones,/api/milestones/**,/api/testimonials,/api/testimonials/**,/hero/image/**,/hero/active/**,/hero/**,/service-tiles/active,/service-tiles/active/**
public-urls: /user/login,/user/register,/user/*/profile-image,/user/*/profile-image/**,/professors,/professors/**,/api/posts,/api/posts/*,/api/posts/posted,/api/posts/tag/*,/api/posts/tags/count,/api/files/**,/uploads/**,/professor/**,/api/events,/api/events/*,/api/public/**,/api/jobs/active,/api/job-applications/**,/api/job-applications/resume/**,/api/courses/active,/api/courses/*,/api/course-applications,/api/upcoming-events/active,/api/milestones,/api/milestones/**,/api/testimonials,/api/testimonials/**,/hero/image/**,/hero/active/**,/hero/**,/service-tiles/active,/service-tiles/active/**,/publications/active/**,/publications/*/**,/publications/category/**,/publications/year/**
cors:
allowed-origins: http://localhost:4200,http://localhost:3000,https://maincmc.rootxwire.com,https://dashboard.cmctrauma.com,https://www.dashboard.cmctrauma.com,https://cmctrauma.com,https://www.cmctrauma.com,https://cmcbackend.rootxwire.com,https://cmcadminfrontend.rootxwire.com
jwt:

View File

@ -25,6 +25,7 @@ import { TestimonialFormComponent } from '../component/testimonial/testimonial-f
import { TestimonialListComponent } from '../component/testimonial/testimonial-list/testimonial-list.component';
import { HeroImageComponent } from '../component/hero-image/hero-image.component';
import { ServiceTileComponent } from '../component/service-tile/service-tile.component';
import { PublicationsComponent } from '../component/publications/publications.component';
const routes: Routes = [
{ path: '', component: HomeComponent },
@ -44,6 +45,7 @@ const routes: Routes = [
{ path: 'professorManagement', component: ProfessorComponent, canActivate: [AuthenticationGuard] },
{ path: 'heroImage', component: HeroImageComponent, canActivate: [AuthenticationGuard] },
{ path: 'serviceTiles', component: ServiceTileComponent, canActivate: [AuthenticationGuard] },
{ path: 'publications', component: PublicationsComponent, canActivate: [AuthenticationGuard] }, // ← ADD THIS
{ path: 'milestone/list', component: MilestoneListComponent, canActivate: [AuthenticationGuard] },
{ path: 'milestone/create', component: MilestoneFormComponent, canActivate: [AuthenticationGuard] },
{ path: 'milestone/edit/:id', component: MilestoneFormComponent, canActivate: [AuthenticationGuard] },

View File

@ -40,6 +40,7 @@ import { TestimonialFormComponent } from '../component/testimonial/testimonial-f
import { TestimonialListComponent } from '../component/testimonial/testimonial-list/testimonial-list.component';
import { HeroImageComponent } from '../component/hero-image/hero-image.component';
import { ServiceTileComponent } from '../component/service-tile/service-tile.component';
import { PublicationsComponent } from '../component/publications/publications.component';
// import { PagesModule } from '../pages/pages.module';
@ -72,7 +73,8 @@ import { ServiceTileComponent } from '../component/service-tile/service-tile.com
TestimonialFormComponent,
TestimonialListComponent,
HeroImageComponent,
ServiceTileComponent
ServiceTileComponent,
PublicationsComponent
],
imports: [
CommonModule,

View File

@ -38,6 +38,7 @@ import { TestimonialFormComponent } from './component/testimonial/testimonial-fo
import { TestimonialListComponent } from './component/testimonial/testimonial-list/testimonial-list.component';
import { HeroImageComponent } from './component/hero-image/hero-image.component';
import { ServiceTileComponent } from './component/service-tile/service-tile.component';
import { PublicationsComponent } from './component/publications/publications.component';
// import { PagesModule } from './pages/pages.module';
@ -47,6 +48,7 @@ import { ServiceTileComponent } from './component/service-tile/service-tile.comp
@NgModule({
declarations: [
AppComponent,
//PublicationsComponent,
//ServiceTileComponent,
//HeroImageComponent,
//TestimonialFormComponent,

View File

@ -315,7 +315,7 @@
width: 100px;
height: 100px;
margin: 0 auto 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: #3b82f6;
border-radius: 50%;
display: flex;
align-items: center;

View File

@ -26,8 +26,7 @@
<ul class="nav-list">
<!-- Home -->
<li class="nav-item">
<a class="nav-link" routerLink="/dashboard/home" routerLinkActive="active"
(click)="changeTitle('Home')">
<a class="nav-link" routerLink="/dashboard/home" routerLinkActive="active" (click)="changeTitle('Home')">
<span class="nav-icon">
<i class="fa fa-home"></i>
</span>
@ -64,8 +63,7 @@
<!-- Blogs -->
<li class="nav-item">
<a class="nav-link" routerLink="/dashboard/blogs" routerLinkActive="active"
(click)="changeTitle('Blogs')">
<a class="nav-link" routerLink="/dashboard/blogs" routerLinkActive="active" (click)="changeTitle('Blogs')">
<span class="nav-icon">
<i class="fa fa-blog"></i>
</span>
@ -95,10 +93,20 @@
</a>
</li>
<!-- Publications -->
<li class="nav-item">
<a class="nav-link" routerLink="/dashboard/publications" routerLinkActive="active"
(click)="changeTitle('Publications')">
<span class="nav-icon">
<i class="fa fa-book-open"></i>
</span>
<span class="nav-text">Publications</span>
</a>
</li>
<!-- Events -->
<li class="nav-item">
<a class="nav-link" routerLink="/dashboard/events" routerLinkActive="active"
(click)="changeTitle('Events')">
<a class="nav-link" routerLink="/dashboard/events" routerLinkActive="active" (click)="changeTitle('Events')">
<span class="nav-icon">
<i class="fa fa-calendar"></i>
</span>
@ -146,8 +154,7 @@
<!-- Careers -->
<li class="nav-item">
<a class="nav-link" routerLink="/dashboard/career" routerLinkActive="active"
(click)="changeTitle('Careers')">
<a class="nav-link" routerLink="/dashboard/career" routerLinkActive="active" (click)="changeTitle('Careers')">
<span class="nav-icon">
<i class="fa fa-briefcase"></i>
</span>
@ -173,8 +180,7 @@
<!-- Profile -->
<li class="nav-item">
<a class="nav-link" routerLink="/dashboard/profile" routerLinkActive="active"
(click)="changeTitle('Profile')">
<a class="nav-link" routerLink="/dashboard/profile" routerLinkActive="active" (click)="changeTitle('Profile')">
<span class="nav-icon">
<i class="fa fa-user"></i>
</span>

View File

@ -0,0 +1,742 @@
/* Publication Layout */
.publication-layout {
display: flex;
min-height: 100vh;
background: #f8f9fa;
}
.sidebar {
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: 260px;
z-index: 1000;
}
.publication-content {
margin-left: 260px;
flex: 1;
padding: 32px;
width: calc(100% - 260px);
max-width: 1400px;
}
/* Header Section */
.publication-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
gap: 20px;
}
.header-left {
flex: 1;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 4px 0;
letter-spacing: -0.5px;
}
.page-subtitle {
font-size: 14px;
color: #6b7280;
margin: 0;
}
.header-actions {
display: flex;
gap: 12px;
align-items: center;
}
/* Search Box */
.search-box {
position: relative;
display: flex;
align-items: center;
}
.search-box i {
position: absolute;
left: 16px;
color: #6b7280;
font-size: 14px;
}
.search-input {
padding: 10px 16px 10px 42px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
color: #1a1a1a;
width: 300px;
transition: all 0.2s ease;
}
.search-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* Filters Section */
.filters-section {
display: flex;
align-items: center;
gap: 20px;
padding: 16px 20px;
background: white;
border-radius: 12px;
margin-bottom: 24px;
border: 1px solid #e5e7eb;
}
.filter-group {
display: flex;
align-items: center;
gap: 10px;
}
.filter-group label {
font-size: 14px;
font-weight: 600;
color: #374151;
display: flex;
align-items: center;
gap: 6px;
}
.filter-select {
padding: 8px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 14px;
color: #1a1a1a;
cursor: pointer;
transition: all 0.2s ease;
}
.filter-select:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.results-count {
margin-left: auto;
font-size: 14px;
color: #6b7280;
font-weight: 500;
}
/* Buttons */
.btn-primary,
.btn-secondary,
.btn-refresh {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #2563eb;
transform: translateY(-1px);
}
.btn-primary:disabled {
background: #9ca3af;
cursor: not-allowed;
opacity: 0.6;
}
.btn-secondary {
background: white;
color: #374151;
border: 1px solid #e5e7eb;
}
.btn-secondary:hover {
background: #f9fafb;
border-color: #d1d5db;
}
.btn-refresh {
background: white;
color: #6b7280;
border: 1px solid #e5e7eb;
padding: 10px 16px;
}
.btn-refresh:hover {
background: #f9fafb;
color: #3b82f6;
}
/* Publications List */
.publications-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.publication-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 20px;
display: flex;
align-items: flex-start;
gap: 16px;
transition: all 0.2s ease;
}
.publication-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.publication-card.inactive {
opacity: 0.6;
background: #f9fafb;
}
/* Publication Content */
.publication-content-area {
flex: 1;
cursor: pointer;
}
.publication-header-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.publication-meta {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.year-badge {
padding: 4px 12px;
background: #3b82f6;
color: white;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
}
.category-badge {
padding: 4px 12px;
background: #f3f4f6;
color: #374151;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
}
.category-badge-large {
padding: 6px 16px;
background: #f3f4f6;
color: #374151;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
display: inline-block;
}
.order-badge {
padding: 4px 8px;
background: #f3f4f6;
color: #6b7280;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.publication-title {
margin: 0 0 12px 0;
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
line-height: 1.4;
}
.publication-details {
display: flex;
flex-direction: column;
gap: 8px;
}
.detail-item {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 14px;
color: #6b7280;
}
.detail-item i {
margin-top: 2px;
flex-shrink: 0;
}
.authors-text {
line-height: 1.5;
}
.doi-link {
color: #3b82f6;
text-decoration: none;
word-break: break-all;
}
.doi-link:hover {
text-decoration: underline;
}
.doi-link-large {
color: #3b82f6;
text-decoration: none;
word-break: break-all;
font-size: 14px;
}
.doi-link-large:hover {
text-decoration: underline;
}
/* Status Badge */
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
}
.status-active {
background: #d1fae5;
color: #065f46;
}
.status-inactive {
background: #fee2e2;
color: #991b1b;
}
/* Action Buttons */
.publication-actions {
display: flex;
gap: 8px;
}
.btn-action {
width: 36px;
height: 36px;
border-radius: 6px;
border: 1px solid #e5e7eb;
background: white;
color: #6b7280;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-action:hover {
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.btn-toggle:hover {
background: #fef3c7;
border-color: #f59e0b;
color: #f59e0b;
}
.btn-edit:hover {
background: #eff6ff;
border-color: #3b82f6;
color: #3b82f6;
}
.btn-delete:hover {
background: #fef2f2;
border-color: #dc2626;
color: #dc2626;
}
/* Loading State */
.loading-state {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 60px 20px;
text-align: center;
margin-top: 40px;
}
.loading-spinner {
width: 80px;
height: 80px;
margin: 0 auto 20px;
background: #f3f4f6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.loading-spinner i {
font-size: 36px;
color: #3b82f6;
}
.loading-state p {
font-size: 16px;
color: #6b7280;
margin: 0;
}
/* Empty State */
.empty-state {
background: white;
border: 2px dashed #e5e7eb;
border-radius: 12px;
padding: 80px 20px;
text-align: center;
margin-top: 40px;
}
.empty-icon {
width: 100px;
height: 100px;
margin: 0 auto 24px;
background: #3b82f6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
}
.empty-icon i {
font-size: 48px;
color: white;
}
.empty-state h3 {
font-size: 24px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 12px 0;
}
.empty-state p {
font-size: 16px;
color: #6b7280;
margin: 0 0 24px 0;
}
/* Modal Styles */
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px 32px;
border-bottom: 1px solid #e5e7eb;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
}
.modal-close {
width: 32px;
height: 32px;
border-radius: 6px;
border: none;
background: #f3f4f6;
color: #6b7280;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.modal-close:hover {
background: #e5e7eb;
color: #1a1a1a;
}
.modal-body {
padding: 24px 32px;
max-height: calc(100vh - 200px);
overflow-y: auto;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 20px 32px;
border-top: 1px solid #e5e7eb;
}
/* Publication Detail Card */
.publication-detail-card {
background: #f9fafb;
border-radius: 8px;
padding: 24px;
}
.detail-section {
margin-bottom: 24px;
}
.detail-section:last-child {
margin-bottom: 0;
}
.detail-section h4 {
font-size: 14px;
font-weight: 600;
color: #6b7280;
margin: 0 0 8px 0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.detail-section p {
margin: 0;
color: #1a1a1a;
font-size: 14px;
line-height: 1.6;
}
.abstract-text {
white-space: pre-wrap;
line-height: 1.8;
}
.detail-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-bottom: 24px;
}
.authors-list,
.keywords-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.author-chip,
.keyword-chip {
padding: 6px 12px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 6px;
font-size: 13px;
color: #374151;
}
.keyword-chip {
background: #3b82f6;
color: white;
border: none;
}
/* Form Styles */
.form-section {
margin-bottom: 32px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 20px 0;
padding-bottom: 12px;
border-bottom: 2px solid #e5e7eb;
}
.form-group {
margin-bottom: 20px;
}
.form-group:last-child {
margin-bottom: 0;
}
.form-group label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 8px;
}
.form-group label i {
color: #6b7280;
font-size: 14px;
}
.form-input,
.form-textarea,
.form-select {
width: 100%;
padding: 12px 16px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
color: #1a1a1a;
transition: all 0.2s ease;
background: white;
font-family: inherit;
}
.form-input:focus,
.form-textarea:focus,
.form-select:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-textarea {
resize: vertical;
min-height: 80px;
}
.form-text {
display: block;
margin-top: 6px;
font-size: 12px;
color: #6b7280;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
/* Responsive Design */
@media (max-width: 1200px) {
.publication-content {
max-width: 100%;
}
}
@media (max-width: 992px) {
.publication-content {
margin-left: 0;
width: 100%;
padding: 24px;
}
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0);
}
.publication-header {
flex-direction: column;
align-items: flex-start;
}
.header-actions {
width: 100%;
flex-wrap: wrap;
}
.search-input {
width: 100%;
}
.filters-section {
flex-direction: column;
align-items: flex-start;
}
.results-count {
margin-left: 0;
}
.form-row,
.detail-row {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.publication-content {
padding: 20px;
}
.publication-card {
flex-direction: column;
}
.publication-actions {
width: 100%;
justify-content: flex-end;
}
.modal-header,
.modal-body,
.modal-footer {
padding: 20px;
}
}
@media (max-width: 576px) {
.publication-content {
padding: 16px;
}
.header-actions {
flex-direction: column;
}
.header-actions > * {
width: 100%;
}
}

View File

@ -0,0 +1,506 @@
<div class="publication-layout">
<!-- Sidebar -->
<app-menu class="sidebar"></app-menu>
<!-- Main Content -->
<div class="publication-content">
<!-- Header Section -->
<div class="publication-header">
<div class="header-left">
<h1 class="page-title">Publications Management</h1>
<p class="page-subtitle">Manage academic publications and research papers</p>
</div>
<div class="header-actions">
<div class="search-box">
<i class="fa fa-search"></i>
<input
name="searchTerm"
#searchTerm="ngModel"
class="search-input"
type="search"
placeholder="Search publications..."
ngModel
(ngModelChange)="searchPublications(searchTerm.value)">
</div>
<button *ngIf="isManager" class="btn-primary" data-bs-toggle="modal" data-bs-target="#addPublicationModal">
<i class="fa fa-plus"></i>
<span>New Publication</span>
</button>
<button class="btn-refresh" (click)="getPublications(true)">
<i class="fas fa-sync" [ngClass]="{'fa-spin': refreshing }"></i>
</button>
</div>
</div>
<!-- Filters -->
<div class="filters-section">
<div class="filter-group">
<label><i class="fa fa-tag"></i> Category:</label>
<select [(ngModel)]="selectedCategory" (change)="onCategoryChange()" class="filter-select">
<option *ngFor="let cat of categories" [value]="cat">{{cat}}</option>
</select>
</div>
<div class="filter-group">
<label><i class="fa fa-calendar"></i> Year:</label>
<select [(ngModel)]="selectedYear" (change)="onYearChange()" class="filter-select">
<option value="All">All Years</option>
<option *ngFor="let year of years" [value]="year">{{year}}</option>
</select>
</div>
<div class="results-count">
Showing {{publications.length}} publication{{publications.length !== 1 ? 's' : ''}}
</div>
</div>
<!-- Publications List -->
<div class="publications-list" *ngIf="publications && publications.length > 0">
<div *ngFor="let publication of publications"
class="publication-card"
[class.inactive]="!publication?.isActive">
<div class="publication-content-area" (click)="publication && onSelectPublication(publication)">
<div class="publication-header-row">
<div class="publication-meta">
<span class="year-badge">{{publication?.year}}</span>
<span class="category-badge" *ngIf="publication?.category">{{publication.category}}</span>
<span class="status-badge"
[class.status-active]="publication?.isActive"
[class.status-inactive]="!publication?.isActive">
<i class="fa"
[class.fa-check-circle]="publication?.isActive"
[class.fa-times-circle]="!publication?.isActive"></i>
{{publication?.isActive ? 'Active' : 'Inactive'}}
</span>
</div>
<div class="order-badge">Order: {{publication?.displayOrder}}</div>
</div>
<h3 class="publication-title">{{publication?.title}}</h3>
<div class="publication-details">
<div class="detail-item">
<i class="fa fa-users"></i>
<span class="authors-text">{{publication?.authors}}</span>
</div>
<div class="detail-item">
<i class="fa fa-book"></i>
<span>{{publication?.journal}}</span>
</div>
<div class="detail-item" *ngIf="publication?.doi">
<i class="fa fa-link"></i>
<a [href]="publication.doi" target="_blank" class="doi-link" (click)="$event.stopPropagation()">
{{publication.doi}}
</a>
</div>
</div>
</div>
<div class="publication-actions">
<button class="btn-action btn-toggle"
(click)="onToggleActive(publication); $event.stopPropagation()"
[title]="publication?.isActive ? 'Set Inactive' : 'Set Active'">
<i class="fas" [class.fa-toggle-on]="publication?.isActive" [class.fa-toggle-off]="!publication?.isActive"></i>
</button>
<button class="btn-action btn-edit"
(click)="onEditPublication(publication); $event.stopPropagation()"
title="Edit">
<i class="fas fa-edit"></i>
</button>
<button *ngIf="isAdmin"
class="btn-action btn-delete"
(click)="onDeletePublication(publication); $event.stopPropagation()"
title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
<!-- Loading State -->
<div *ngIf="refreshing" class="loading-state">
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin"></i>
</div>
<p>Loading publications...</p>
</div>
<!-- Empty State -->
<div *ngIf="!refreshing && (!publications || publications.length === 0)" class="empty-state">
<div class="empty-icon">
<i class="fa fa-book-open"></i>
</div>
<h3>No publications yet</h3>
<p>Get started by creating your first publication</p>
<button *ngIf="isManager" class="btn-primary" data-bs-toggle="modal" data-bs-target="#addPublicationModal">
<i class="fa fa-plus"></i>
<span>Create Your First Publication</span>
</button>
</div>
<!-- Hidden Modal Triggers -->
<button [hidden]="true" type="button" id="openPublicationInfo" data-bs-toggle="modal" data-bs-target="#viewPublicationModal"></button>
<button [hidden]="true" type="button" id="openPublicationEdit" data-bs-toggle="modal" data-bs-target="#editPublicationModal"></button>
<!-- View Publication Modal -->
<div class="modal fade" id="viewPublicationModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content" *ngIf="selectedPublication">
<div class="modal-header">
<h3>Publication Details</h3>
<button class="modal-close" data-bs-dismiss="modal">
<i class="fa fa-times"></i>
</button>
</div>
<div class="modal-body">
<div class="publication-detail-card">
<!-- Title -->
<div class="detail-section">
<h4>Title</h4>
<p>{{selectedPublication.title}}</p>
</div>
<!-- Authors -->
<div class="detail-section">
<h4>Authors</h4>
<div class="authors-list">
<span *ngFor="let author of getAuthorsList(selectedPublication.authors)" class="author-chip">
{{author}}
</span>
</div>
</div>
<!-- Year & Journal -->
<div class="detail-row">
<div class="detail-section">
<h4>Year</h4>
<p>{{selectedPublication.year}}</p>
</div>
<div class="detail-section">
<h4>Journal</h4>
<p>{{selectedPublication.journal}}</p>
</div>
</div>
<!-- DOI -->
<div class="detail-section" *ngIf="selectedPublication.doi">
<h4>DOI</h4>
<a [href]="selectedPublication.doi" target="_blank" class="doi-link-large">
{{selectedPublication.doi}}
</a>
</div>
<!-- Category & Publication Date -->
<div class="detail-row">
<div class="detail-section" *ngIf="selectedPublication.category">
<h4>Category</h4>
<span class="category-badge-large">{{selectedPublication.category}}</span>
</div>
<div class="detail-section" *ngIf="selectedPublication.publicationDate">
<h4>Publication Date</h4>
<p>{{selectedPublication.publicationDate}}</p>
</div>
</div>
<!-- Abstract -->
<div class="detail-section" *ngIf="selectedPublication.abstractText">
<h4>Abstract</h4>
<p class="abstract-text">{{selectedPublication.abstractText}}</p>
</div>
<!-- Keywords -->
<div class="detail-section" *ngIf="selectedPublication.keywords">
<h4>Keywords</h4>
<div class="keywords-list">
<span *ngFor="let keyword of getKeywordsList(selectedPublication.keywords)" class="keyword-chip">
{{keyword}}
</span>
</div>
</div>
<!-- Status & Order -->
<div class="detail-row">
<div class="detail-section">
<h4>Status</h4>
<span class="status-badge"
[class.status-active]="selectedPublication.isActive"
[class.status-inactive]="!selectedPublication.isActive">
{{selectedPublication.isActive ? 'Active' : 'Inactive'}}
</span>
</div>
<div class="detail-section">
<h4>Display Order</h4>
<p>{{selectedPublication.displayOrder}}</p>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Add Publication Modal -->
<div *ngIf="isManager" class="modal fade" id="addPublicationModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h3>Add New Publication</h3>
<button class="modal-close" data-bs-dismiss="modal">
<i class="fa fa-times"></i>
</button>
</div>
<div class="modal-body">
<form #newPublicationForm="ngForm" (ngSubmit)="onAddNewPublication(newPublicationForm)">
<div class="form-section">
<h4 class="section-title">Basic Information</h4>
<div class="form-group">
<label for="title">
<i class="fa fa-heading"></i>
Title *
</label>
<textarea id="title" name="title" class="form-textarea" rows="3" ngModel required placeholder="Enter publication title"></textarea>
</div>
<div class="form-group">
<label for="authors">
<i class="fa fa-users"></i>
Authors * (comma-separated)
</label>
<textarea id="authors" name="authors" class="form-textarea" rows="2" ngModel required placeholder="Smith, J., Doe, A., Johnson, B."></textarea>
<small class="form-text">Separate multiple authors with commas</small>
</div>
<div class="form-row">
<div class="form-group">
<label for="year">
<i class="fa fa-calendar"></i>
Year *
</label>
<input type="number" id="year" name="year" class="form-input" ngModel required min="1900" max="2100" placeholder="2024">
</div>
<div class="form-group">
<label for="journal">
<i class="fa fa-book"></i>
Journal *
</label>
<input type="text" id="journal" name="journal" class="form-input" ngModel required placeholder="Journal name">
</div>
</div>
</div>
<div class="form-section">
<h4 class="section-title">Additional Details</h4>
<div class="form-group">
<label for="doi">
<i class="fa fa-link"></i>
DOI
</label>
<input type="text" id="doi" name="doi" class="form-input" ngModel placeholder="https://doi.org/10.1234/example">
</div>
<div class="form-row">
<div class="form-group">
<label for="category">
<i class="fa fa-tag"></i>
Category
</label>
<select id="category" name="category" class="form-select" ngModel>
<option value="">Select category...</option>
<option value="Clinical Research">Clinical Research</option>
<option value="Case Reports">Case Reports</option>
<option value="Review">Review</option>
<option value="Healthcare Systems">Healthcare Systems</option>
<option value="Meta-Analysis">Meta-Analysis</option>
<option value="Editorial">Editorial</option>
</select>
</div>
<div class="form-group">
<label for="publicationDate">
<i class="fa fa-calendar-alt"></i>
Publication Date
</label>
<input type="text" id="publicationDate" name="publicationDate" class="form-input" ngModel placeholder="January 2024">
</div>
</div>
<div class="form-group">
<label for="abstractText">
<i class="fa fa-align-left"></i>
Abstract
</label>
<textarea id="abstractText" name="abstractText" class="form-textarea" rows="6" ngModel placeholder="Enter abstract text"></textarea>
</div>
<div class="form-group">
<label for="keywords">
<i class="fa fa-tags"></i>
Keywords (comma-separated)
</label>
<textarea id="keywords" name="keywords" class="form-textarea" rows="2" ngModel placeholder="trauma, surgery, emergency"></textarea>
<small class="form-text">Separate multiple keywords with commas</small>
</div>
<div class="form-group">
<label for="displayOrder">
<i class="fa fa-sort-numeric-up"></i>
Display Order
</label>
<input type="number" id="displayOrder" name="displayOrder" class="form-input" ngModel value="0" placeholder="0">
<small class="form-text">Lower numbers appear first within the same year</small>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button class="btn-primary"
type="button"
(click)="newPublicationForm.ngSubmit.emit()"
[disabled]="newPublicationForm.invalid">
<i class="fa fa-save"></i>
Create Publication
</button>
</div>
</div>
</div>
</div>
<!-- Edit Publication Modal -->
<div class="modal fade" id="editPublicationModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content" *ngIf="selectedPublication">
<div class="modal-header">
<h3>Edit Publication</h3>
<button class="modal-close" data-bs-dismiss="modal">
<i class="fa fa-times"></i>
</button>
</div>
<div class="modal-body">
<form #editPublicationForm="ngForm" (ngSubmit)="onUpdatePublication(editPublicationForm)">
<div class="form-section">
<h4 class="section-title">Basic Information</h4>
<div class="form-group">
<label for="editTitle">
<i class="fa fa-heading"></i>
Title *
</label>
<textarea id="editTitle" name="title" class="form-textarea" rows="3" [(ngModel)]="selectedPublication.title" required></textarea>
</div>
<div class="form-group">
<label for="editAuthors">
<i class="fa fa-users"></i>
Authors * (comma-separated)
</label>
<textarea id="editAuthors" name="authors" class="form-textarea" rows="2" [(ngModel)]="selectedPublication.authors" required></textarea>
<small class="form-text">Separate multiple authors with commas</small>
</div>
<div class="form-row">
<div class="form-group">
<label for="editYear">
<i class="fa fa-calendar"></i>
Year *
</label>
<input type="number" id="editYear" name="year" class="form-input" [(ngModel)]="selectedPublication.year" required min="1900" max="2100">
</div>
<div class="form-group">
<label for="editJournal">
<i class="fa fa-book"></i>
Journal *
</label>
<input type="text" id="editJournal" name="journal" class="form-input" [(ngModel)]="selectedPublication.journal" required>
</div>
</div>
</div>
<div class="form-section">
<h4 class="section-title">Additional Details</h4>
<div class="form-group">
<label for="editDoi">
<i class="fa fa-link"></i>
DOI
</label>
<input type="text" id="editDoi" name="doi" class="form-input" [(ngModel)]="selectedPublication.doi">
</div>
<div class="form-row">
<div class="form-group">
<label for="editCategory">
<i class="fa fa-tag"></i>
Category
</label>
<select id="editCategory" name="category" class="form-select" [(ngModel)]="selectedPublication.category">
<option value="">Select category...</option>
<option value="Clinical Research">Clinical Research</option>
<option value="Case Reports">Case Reports</option>
<option value="Review">Review</option>
<option value="Healthcare Systems">Healthcare Systems</option>
<option value="Meta-Analysis">Meta-Analysis</option>
<option value="Editorial">Editorial</option>
</select>
</div>
<div class="form-group">
<label for="editPublicationDate">
<i class="fa fa-calendar-alt"></i>
Publication Date
</label>
<input type="text" id="editPublicationDate" name="publicationDate" class="form-input" [(ngModel)]="selectedPublication.publicationDate">
</div>
</div>
<div class="form-group">
<label for="editAbstractText">
<i class="fa fa-align-left"></i>
Abstract
</label>
<textarea id="editAbstractText" name="abstractText" class="form-textarea" rows="6" [(ngModel)]="selectedPublication.abstractText"></textarea>
</div>
<div class="form-group">
<label for="editKeywords">
<i class="fa fa-tags"></i>
Keywords (comma-separated)
</label>
<textarea id="editKeywords" name="keywords" class="form-textarea" rows="2" [(ngModel)]="selectedPublication.keywords"></textarea>
<small class="form-text">Separate multiple keywords with commas</small>
</div>
<div class="form-group">
<label for="editDisplayOrder">
<i class="fa fa-sort-numeric-up"></i>
Display Order
</label>
<input type="number" id="editDisplayOrder" name="displayOrder" class="form-input" [(ngModel)]="selectedPublication.displayOrder">
<small class="form-text">Lower numbers appear first within the same year</small>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button class="btn-primary" (click)="editPublicationForm.ngSubmit.emit()" [disabled]="editPublicationForm.invalid">
<i class="fa fa-save"></i>
Save Changes
</button>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PublicationsComponent } from './publications.component';
describe('PublicationsComponent', () => {
let component: PublicationsComponent;
let fixture: ComponentFixture<PublicationsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ PublicationsComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PublicationsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,300 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { Publication } from '../../model/publication.model';
import { NotificationService } from '../../service/notification.service';
import { NotificationType } from '../../notification/notification-type';
import { HttpErrorResponse } from '@angular/common/http';
import { NgForm } from '@angular/forms';
import { CustomHttpResponse } from '../../dto/custom-http-response';
import { SubSink } from 'subsink';
import { User } from 'src/app/model/user';
import { Role } from 'src/app/enum/role.enum';
import { AuthenticationService } from 'src/app/service/authentication.service';
import { PublicationService } from 'src/app/service/publication.service';
@Component({
selector: 'app-publication',
templateUrl: './publications.component.html',
styleUrls: ['./publications.component.css']
})
export class PublicationsComponent implements OnInit, OnDestroy {
private titleSubject = new BehaviorSubject<string>('Publications');
public titleAction$ = this.titleSubject.asObservable();
public loggedInUser: User;
public publications: Publication[] = [];
public selectedPublication: Publication | null = null;
public refreshing: boolean = false;
private subs = new SubSink();
// Filter options
public categories: string[] = ['All', 'Clinical Research', 'Case Reports', 'Review', 'Healthcare Systems', 'Meta-Analysis', 'Editorial'];
public selectedCategory: string = 'All';
public years: number[] = [];
public selectedYear: string = 'All';
constructor(
private publicationService: PublicationService,
private notificationService: NotificationService,
private authenticationService: AuthenticationService,
) {}
ngOnInit(): void {
document.addEventListener('hidden.bs.modal', () => {
document.body.classList.remove('modal-open');
const backdrops = document.querySelectorAll('.modal-backdrop');
backdrops.forEach(b => b.remove());
});
this.getPublications(false);
this.loggedInUser = this.authenticationService.getUserFromLocalStorage();
this.setupModalEventListeners();
}
ngOnDestroy(): void {
this.subs.unsubscribe();
}
private setupModalEventListeners(): void {
const editModal = document.getElementById('editPublicationModal');
if (editModal) {
editModal.addEventListener('hidden.bs.modal', () => {
this.clearEditPublicationData();
});
}
const addModal = document.getElementById('addPublicationModal');
if (addModal) {
addModal.addEventListener('hidden.bs.modal', () => {
this.clearNewPublicationData();
});
}
const viewModal = document.getElementById('viewPublicationModal');
if (viewModal) {
viewModal.addEventListener('hidden.bs.modal', () => {
this.selectedPublication = null;
});
}
}
private clearNewPublicationData(): void {
// Clear any form data if needed
}
private clearEditPublicationData(): void {
this.selectedPublication = null;
}
public getPublications(showNotification: boolean): void {
this.refreshing = true;
this.subs.sink = this.publicationService.getAllPublications().subscribe(
publications => {
this.publications = (publications || []).filter(pub => pub !== null && pub !== undefined);
this.publications.sort((a, b) => {
// Sort by year DESC, then by displayOrder ASC
if (b.year !== a.year) {
return b.year - a.year;
}
return (a.displayOrder || 0) - (b.displayOrder || 0);
});
this.publicationService.addPublicationsToLocalStorage(this.publications);
this.extractYears();
if (showNotification && this.publications.length > 0) {
this.notificationService.notify(NotificationType.SUCCESS, `Loaded ${this.publications.length} publication${this.publications.length > 1 ? 's' : ''}`);
}
this.refreshing = false;
},
(errorResponse: HttpErrorResponse) => {
this.sendErrorNotification(errorResponse.error?.message || 'Failed to load publications');
this.refreshing = false;
this.publications = [];
}
);
}
private extractYears(): void {
const yearSet = new Set<number>();
this.publications.forEach(pub => {
if (pub.year) {
yearSet.add(pub.year);
}
});
this.years = Array.from(yearSet).sort((a, b) => b - a);
}
public onSelectPublication(publication: Publication): void {
this.selectedPublication = JSON.parse(JSON.stringify(publication));
this.clickButton('openPublicationInfo');
}
private sendErrorNotification(message: string): void {
this.sendNotification(NotificationType.ERROR, message);
}
private sendNotification(type: NotificationType, message: string): void {
this.notificationService.notify(type, message ? message : 'An error occurred. Please try again');
}
public onAddNewPublication(publicationForm: NgForm): void {
const formData = this.createPublicationFormData(publicationForm.value);
this.subs.sink = this.publicationService.addPublication(formData).subscribe(
(publication: Publication) => {
this.getPublications(false);
this.notificationService.notify(NotificationType.SUCCESS, 'Publication added successfully');
publicationForm.reset();
this.clearNewPublicationData();
const modalElement = document.getElementById('addPublicationModal');
if (modalElement) {
const modal = (window as any).bootstrap.Modal.getInstance(modalElement);
if (modal) {
modal.hide();
}
}
},
(errorResponse: HttpErrorResponse) => {
this.sendErrorNotification(errorResponse.error.message);
}
);
}
public searchPublications(searchTerm: string): void {
if (!searchTerm) {
this.publications = this.publicationService.getPublicationsFromLocalStorage();
this.applyFilters();
return;
}
const matchPublications: Publication[] = [];
searchTerm = searchTerm.toLowerCase();
for (const publication of this.publicationService.getPublicationsFromLocalStorage()) {
if (
(publication.title && publication.title.toLowerCase().includes(searchTerm)) ||
(publication.authors && publication.authors.toLowerCase().includes(searchTerm)) ||
(publication.journal && publication.journal.toLowerCase().includes(searchTerm)) ||
(publication.category && publication.category.toLowerCase().includes(searchTerm))
) {
matchPublications.push(publication);
}
}
this.publications = matchPublications;
this.applyFilters();
}
public onCategoryChange(): void {
this.applyFilters();
}
public onYearChange(): void {
this.applyFilters();
}
private applyFilters(): void {
let filtered = this.publicationService.getPublicationsFromLocalStorage();
// Apply category filter
if (this.selectedCategory !== 'All') {
filtered = filtered.filter(pub => pub.category === this.selectedCategory);
}
// Apply year filter
if (this.selectedYear !== 'All') {
const year = parseInt(this.selectedYear);
filtered = filtered.filter(pub => pub.year === year);
}
this.publications = filtered;
}
private clickButton(buttonId: string): void {
document.getElementById(buttonId)?.click();
}
public onEditPublication(publication: Publication): void {
this.selectedPublication = JSON.parse(JSON.stringify(publication));
this.clickButton('openPublicationEdit');
}
public onUpdatePublication(form: NgForm): void {
if (form.invalid || !this.selectedPublication) {
this.notificationService.notify(NotificationType.ERROR, 'Please fill out all required fields.');
return;
}
const formData = this.createPublicationFormData(this.selectedPublication);
this.subs.add(this.publicationService.updatePublication(this.selectedPublication.id!, formData).subscribe(
(publication: Publication) => {
this.getPublications(false);
this.notificationService.notify(NotificationType.SUCCESS, 'Publication updated successfully');
const modalElement = document.getElementById('editPublicationModal');
if (modalElement) {
const modal = (window as any).bootstrap.Modal.getInstance(modalElement);
if (modal) {
modal.hide();
}
}
},
(errorResponse: HttpErrorResponse) => {
this.sendErrorNotification(errorResponse.error.message);
}
));
}
private createPublicationFormData(publication: any): FormData {
const formData = new FormData();
formData.append('title', publication.title || '');
formData.append('authors', publication.authors || '');
formData.append('year', (publication.year || new Date().getFullYear()).toString());
formData.append('journal', publication.journal || '');
formData.append('doi', publication.doi || '');
formData.append('category', publication.category || '');
formData.append('abstractText', publication.abstractText || '');
formData.append('publicationDate', publication.publicationDate || '');
formData.append('keywords', publication.keywords || '');
formData.append('displayOrder', (publication.displayOrder || 0).toString());
return formData;
}
public onDeletePublication(publication: Publication): void {
if (confirm(`Are you sure you want to delete "${publication.title}"?`)) {
this.subs.sink = this.publicationService.deletePublication(publication.id!).subscribe(
(response: CustomHttpResponse) => {
this.getPublications(false);
this.notificationService.notify(NotificationType.SUCCESS, response.message);
},
(errorResponse: HttpErrorResponse) => {
this.sendErrorNotification(errorResponse.error.message);
}
);
}
}
public onToggleActive(publication: Publication): void {
this.subs.sink = this.publicationService.toggleActiveStatus(publication.id!).subscribe(
(response: Publication) => {
this.getPublications(false);
this.notificationService.notify(NotificationType.SUCCESS, `Publication ${response.isActive ? 'activated' : 'deactivated'} successfully`);
},
(errorResponse: HttpErrorResponse) => {
this.sendErrorNotification(errorResponse.error.message);
}
);
}
public get isAdmin(): boolean {
return this.loggedInUser && (this.loggedInUser.role === Role.ADMIN || this.loggedInUser.role === Role.SUPER_ADMIN);
}
public get isManager(): boolean {
return this.isAdmin || (this.loggedInUser && this.loggedInUser.role === Role.MANAGER);
}
public getAuthorsList(authors: string): string[] {
return authors ? authors.split(',').map(a => a.trim()).filter(a => a.length > 0) : [];
}
public getKeywordsList(keywords: string): string[] {
return keywords ? keywords.split(',').map(k => k.trim()).filter(k => k.length > 0) : [];
}
}

View File

@ -0,0 +1,16 @@
export interface Publication {
id?: number;
title: string;
authors: string; // Comma-separated string
year: number;
journal: string;
doi?: string;
category?: string;
abstractText?: string;
publicationDate?: string;
keywords?: string; // Comma-separated string
isActive: boolean;
displayOrder?: number;
createdDate?: Date;
lastModified?: Date;
}

View File

@ -0,0 +1,90 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { environment } from '../../environments/environment';
import { Publication } from '../model/publication.model';
import { CustomHttpResponse } from '../dto/custom-http-response';
@Injectable({
providedIn: 'root'
})
export class PublicationService {
private host = environment.apiUrl;
constructor(private http: HttpClient) {}
public addPublication(formData: FormData): Observable<Publication> {
return this.http.post<any>(`${this.host}/publications`, formData).pipe(
map(pub => this.transformPublication(pub))
);
}
public updatePublication(id: number, formData: FormData): Observable<Publication> {
return this.http.put<any>(`${this.host}/publications/${id}`, formData).pipe(
map(pub => this.transformPublication(pub))
);
}
public getActivePublications(): Observable<Publication[]> {
return this.http.get<any[]>(`${this.host}/publications/active`).pipe(
map(pubs => pubs.map(pub => this.transformPublication(pub)))
);
}
public getPublicationById(id: number): Observable<Publication> {
return this.http.get<any>(`${this.host}/publications/${id}`).pipe(
map(pub => this.transformPublication(pub))
);
}
public getAllPublications(): Observable<Publication[]> {
return this.http.get<any[]>(`${this.host}/publications`).pipe(
map(pubs => pubs.map(pub => this.transformPublication(pub)))
);
}
public getPublicationsByCategory(category: string): Observable<Publication[]> {
return this.http.get<any[]>(`${this.host}/publications/category/${category}`).pipe(
map(pubs => pubs.map(pub => this.transformPublication(pub)))
);
}
public getPublicationsByYear(year: number): Observable<Publication[]> {
return this.http.get<any[]>(`${this.host}/publications/year/${year}`).pipe(
map(pubs => pubs.map(pub => this.transformPublication(pub)))
);
}
public deletePublication(id: number): Observable<CustomHttpResponse> {
return this.http.delete<CustomHttpResponse>(`${this.host}/publications/${id}`);
}
public toggleActiveStatus(id: number): Observable<Publication> {
return this.http.put<any>(`${this.host}/publications/${id}/toggle-active`, {}).pipe(
map(pub => this.transformPublication(pub))
);
}
public reorderPublications(orderedIds: number[]): Observable<CustomHttpResponse> {
return this.http.put<CustomHttpResponse>(`${this.host}/publications/reorder`, orderedIds);
}
// Transform backend response to match frontend model
private transformPublication(pub: any): Publication {
return {
...pub,
isActive: pub.isActive !== undefined ? pub.isActive : pub.active
};
}
// Local storage methods
public addPublicationsToLocalStorage(publications: Publication[]): void {
localStorage.setItem('publications', JSON.stringify(publications));
}
public getPublicationsFromLocalStorage(): Publication[] {
const publications = localStorage.getItem('publications');
return publications ? JSON.parse(publications) : [];
}
}