publication added newly
This commit is contained in:
@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package net.shyshkin.study.fullstack.supportportal.backend.exception.domain;
|
||||||
|
|
||||||
|
public class PublicationNotFoundException extends RuntimeException {
|
||||||
|
|
||||||
|
public PublicationNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -51,7 +51,7 @@ file:
|
|||||||
app:
|
app:
|
||||||
base-url: ${APP_BASE_URL:http://localhost:8080}
|
base-url: ${APP_BASE_URL:http://localhost:8080}
|
||||||
# Fixed public URLs with correct wildcard patterns
|
# 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:
|
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
|
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:
|
jwt:
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import { TestimonialFormComponent } from '../component/testimonial/testimonial-f
|
|||||||
import { TestimonialListComponent } from '../component/testimonial/testimonial-list/testimonial-list.component';
|
import { TestimonialListComponent } from '../component/testimonial/testimonial-list/testimonial-list.component';
|
||||||
import { HeroImageComponent } from '../component/hero-image/hero-image.component';
|
import { HeroImageComponent } from '../component/hero-image/hero-image.component';
|
||||||
import { ServiceTileComponent } from '../component/service-tile/service-tile.component';
|
import { ServiceTileComponent } from '../component/service-tile/service-tile.component';
|
||||||
|
import { PublicationsComponent } from '../component/publications/publications.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', component: HomeComponent },
|
{ path: '', component: HomeComponent },
|
||||||
@ -44,6 +45,7 @@ const routes: Routes = [
|
|||||||
{ path: 'professorManagement', component: ProfessorComponent, canActivate: [AuthenticationGuard] },
|
{ path: 'professorManagement', component: ProfessorComponent, canActivate: [AuthenticationGuard] },
|
||||||
{ path: 'heroImage', component: HeroImageComponent, canActivate: [AuthenticationGuard] },
|
{ path: 'heroImage', component: HeroImageComponent, canActivate: [AuthenticationGuard] },
|
||||||
{ path: 'serviceTiles', component: ServiceTileComponent, 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/list', component: MilestoneListComponent, canActivate: [AuthenticationGuard] },
|
||||||
{ path: 'milestone/create', component: MilestoneFormComponent, canActivate: [AuthenticationGuard] },
|
{ path: 'milestone/create', component: MilestoneFormComponent, canActivate: [AuthenticationGuard] },
|
||||||
{ path: 'milestone/edit/:id', component: MilestoneFormComponent, canActivate: [AuthenticationGuard] },
|
{ path: 'milestone/edit/:id', component: MilestoneFormComponent, canActivate: [AuthenticationGuard] },
|
||||||
|
|||||||
@ -40,6 +40,7 @@ import { TestimonialFormComponent } from '../component/testimonial/testimonial-f
|
|||||||
import { TestimonialListComponent } from '../component/testimonial/testimonial-list/testimonial-list.component';
|
import { TestimonialListComponent } from '../component/testimonial/testimonial-list/testimonial-list.component';
|
||||||
import { HeroImageComponent } from '../component/hero-image/hero-image.component';
|
import { HeroImageComponent } from '../component/hero-image/hero-image.component';
|
||||||
import { ServiceTileComponent } from '../component/service-tile/service-tile.component';
|
import { ServiceTileComponent } from '../component/service-tile/service-tile.component';
|
||||||
|
import { PublicationsComponent } from '../component/publications/publications.component';
|
||||||
// import { PagesModule } from '../pages/pages.module';
|
// import { PagesModule } from '../pages/pages.module';
|
||||||
|
|
||||||
|
|
||||||
@ -72,7 +73,8 @@ import { ServiceTileComponent } from '../component/service-tile/service-tile.com
|
|||||||
TestimonialFormComponent,
|
TestimonialFormComponent,
|
||||||
TestimonialListComponent,
|
TestimonialListComponent,
|
||||||
HeroImageComponent,
|
HeroImageComponent,
|
||||||
ServiceTileComponent
|
ServiceTileComponent,
|
||||||
|
PublicationsComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
|||||||
@ -38,6 +38,7 @@ import { TestimonialFormComponent } from './component/testimonial/testimonial-fo
|
|||||||
import { TestimonialListComponent } from './component/testimonial/testimonial-list/testimonial-list.component';
|
import { TestimonialListComponent } from './component/testimonial/testimonial-list/testimonial-list.component';
|
||||||
import { HeroImageComponent } from './component/hero-image/hero-image.component';
|
import { HeroImageComponent } from './component/hero-image/hero-image.component';
|
||||||
import { ServiceTileComponent } from './component/service-tile/service-tile.component';
|
import { ServiceTileComponent } from './component/service-tile/service-tile.component';
|
||||||
|
import { PublicationsComponent } from './component/publications/publications.component';
|
||||||
// import { PagesModule } from './pages/pages.module';
|
// import { PagesModule } from './pages/pages.module';
|
||||||
|
|
||||||
|
|
||||||
@ -47,6 +48,7 @@ import { ServiceTileComponent } from './component/service-tile/service-tile.comp
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
|
//PublicationsComponent,
|
||||||
//ServiceTileComponent,
|
//ServiceTileComponent,
|
||||||
//HeroImageComponent,
|
//HeroImageComponent,
|
||||||
//TestimonialFormComponent,
|
//TestimonialFormComponent,
|
||||||
|
|||||||
@ -315,7 +315,7 @@
|
|||||||
width: 100px;
|
width: 100px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
margin: 0 auto 24px;
|
margin: 0 auto 24px;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: #3b82f6;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -26,8 +26,7 @@
|
|||||||
<ul class="nav-list">
|
<ul class="nav-list">
|
||||||
<!-- Home -->
|
<!-- Home -->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="/dashboard/home" routerLinkActive="active"
|
<a class="nav-link" routerLink="/dashboard/home" routerLinkActive="active" (click)="changeTitle('Home')">
|
||||||
(click)="changeTitle('Home')">
|
|
||||||
<span class="nav-icon">
|
<span class="nav-icon">
|
||||||
<i class="fa fa-home"></i>
|
<i class="fa fa-home"></i>
|
||||||
</span>
|
</span>
|
||||||
@ -38,7 +37,7 @@
|
|||||||
<!-- Users -->
|
<!-- Users -->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="/dashboard/userManagement" routerLinkActive="active"
|
<a class="nav-link" routerLink="/dashboard/userManagement" routerLinkActive="active"
|
||||||
(click)="changeTitle('Users')">
|
(click)="changeTitle('Users')">
|
||||||
<span class="nav-icon">
|
<span class="nav-icon">
|
||||||
<i class="fa fa-users"></i>
|
<i class="fa fa-users"></i>
|
||||||
</span>
|
</span>
|
||||||
@ -49,7 +48,7 @@
|
|||||||
<!-- Professors -->
|
<!-- Professors -->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="/dashboard/professorManagement" routerLinkActive="active"
|
<a class="nav-link" routerLink="/dashboard/professorManagement" routerLinkActive="active"
|
||||||
(click)="changeTitle('Professors')">
|
(click)="changeTitle('Professors')">
|
||||||
<span class="nav-icon">
|
<span class="nav-icon">
|
||||||
<i class="fa fa-chalkboard-teacher"></i>
|
<i class="fa fa-chalkboard-teacher"></i>
|
||||||
</span>
|
</span>
|
||||||
@ -64,8 +63,7 @@
|
|||||||
|
|
||||||
<!-- Blogs -->
|
<!-- Blogs -->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="/dashboard/blogs" routerLinkActive="active"
|
<a class="nav-link" routerLink="/dashboard/blogs" routerLinkActive="active" (click)="changeTitle('Blogs')">
|
||||||
(click)="changeTitle('Blogs')">
|
|
||||||
<span class="nav-icon">
|
<span class="nav-icon">
|
||||||
<i class="fa fa-blog"></i>
|
<i class="fa fa-blog"></i>
|
||||||
</span>
|
</span>
|
||||||
@ -76,7 +74,7 @@
|
|||||||
<!--Hero Image-->
|
<!--Hero Image-->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="/dashboard/heroImage" routerLinkActive="active"
|
<a class="nav-link" routerLink="/dashboard/heroImage" routerLinkActive="active"
|
||||||
(click)="changeTitle('Hero Image')">
|
(click)="changeTitle('Hero Image')">
|
||||||
<span class="nav-icon">
|
<span class="nav-icon">
|
||||||
<i class="fa fa-home"></i>
|
<i class="fa fa-home"></i>
|
||||||
</span>
|
</span>
|
||||||
@ -87,7 +85,7 @@
|
|||||||
<!-- Service Tiles --> <!-- ← ADD THIS -->
|
<!-- Service Tiles --> <!-- ← ADD THIS -->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="/dashboard/serviceTiles" routerLinkActive="active"
|
<a class="nav-link" routerLink="/dashboard/serviceTiles" routerLinkActive="active"
|
||||||
(click)="changeTitle('Service Tiles')">
|
(click)="changeTitle('Service Tiles')">
|
||||||
<span class="nav-icon">
|
<span class="nav-icon">
|
||||||
<i class="fa fa-th-large"></i>
|
<i class="fa fa-th-large"></i>
|
||||||
</span>
|
</span>
|
||||||
@ -95,10 +93,20 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</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 -->
|
<!-- Events -->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="/dashboard/events" routerLinkActive="active"
|
<a class="nav-link" routerLink="/dashboard/events" routerLinkActive="active" (click)="changeTitle('Events')">
|
||||||
(click)="changeTitle('Events')">
|
|
||||||
<span class="nav-icon">
|
<span class="nav-icon">
|
||||||
<i class="fa fa-calendar"></i>
|
<i class="fa fa-calendar"></i>
|
||||||
</span>
|
</span>
|
||||||
@ -109,7 +117,7 @@
|
|||||||
<!-- Milestones -->
|
<!-- Milestones -->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="/dashboard/milestone/list" routerLinkActive="active"
|
<a class="nav-link" routerLink="/dashboard/milestone/list" routerLinkActive="active"
|
||||||
(click)="changeTitle('Milestones')">
|
(click)="changeTitle('Milestones')">
|
||||||
<span class="nav-icon">
|
<span class="nav-icon">
|
||||||
<i class="fa fa-flag-checkered"></i>
|
<i class="fa fa-flag-checkered"></i>
|
||||||
</span>
|
</span>
|
||||||
@ -120,7 +128,7 @@
|
|||||||
<!-- Testimonials -->
|
<!-- Testimonials -->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="/dashboard/testimonial/list" routerLinkActive="active"
|
<a class="nav-link" routerLink="/dashboard/testimonial/list" routerLinkActive="active"
|
||||||
(click)="changeTitle('Testimonials')">
|
(click)="changeTitle('Testimonials')">
|
||||||
<span class="nav-icon">
|
<span class="nav-icon">
|
||||||
<i class="fa fa-star"></i>
|
<i class="fa fa-star"></i>
|
||||||
</span>
|
</span>
|
||||||
@ -136,7 +144,7 @@
|
|||||||
<!-- Education -->
|
<!-- Education -->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="/dashboard/education" routerLinkActive="active"
|
<a class="nav-link" routerLink="/dashboard/education" routerLinkActive="active"
|
||||||
(click)="changeTitle('Education')">
|
(click)="changeTitle('Education')">
|
||||||
<span class="nav-icon">
|
<span class="nav-icon">
|
||||||
<i class="fa fa-graduation-cap"></i>
|
<i class="fa fa-graduation-cap"></i>
|
||||||
</span>
|
</span>
|
||||||
@ -146,8 +154,7 @@
|
|||||||
|
|
||||||
<!-- Careers -->
|
<!-- Careers -->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="/dashboard/career" routerLinkActive="active"
|
<a class="nav-link" routerLink="/dashboard/career" routerLinkActive="active" (click)="changeTitle('Careers')">
|
||||||
(click)="changeTitle('Careers')">
|
|
||||||
<span class="nav-icon">
|
<span class="nav-icon">
|
||||||
<i class="fa fa-briefcase"></i>
|
<i class="fa fa-briefcase"></i>
|
||||||
</span>
|
</span>
|
||||||
@ -163,7 +170,7 @@
|
|||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="/dashboard/settings" routerLinkActive="active"
|
<a class="nav-link" routerLink="/dashboard/settings" routerLinkActive="active"
|
||||||
(click)="changeTitle('Settings')">
|
(click)="changeTitle('Settings')">
|
||||||
<span class="nav-icon">
|
<span class="nav-icon">
|
||||||
<i class="fa fa-cogs"></i>
|
<i class="fa fa-cogs"></i>
|
||||||
</span>
|
</span>
|
||||||
@ -173,8 +180,7 @@
|
|||||||
|
|
||||||
<!-- Profile -->
|
<!-- Profile -->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="/dashboard/profile" routerLinkActive="active"
|
<a class="nav-link" routerLink="/dashboard/profile" routerLinkActive="active" (click)="changeTitle('Profile')">
|
||||||
(click)="changeTitle('Profile')">
|
|
||||||
<span class="nav-icon">
|
<span class="nav-icon">
|
||||||
<i class="fa fa-user"></i>
|
<i class="fa fa-user"></i>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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) : [];
|
||||||
|
}
|
||||||
|
}
|
||||||
16
support-portal-frontend/src/app/model/publication.model.ts
Normal file
16
support-portal-frontend/src/app/model/publication.model.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -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) : [];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user