Further updates on 03-11-2025

This commit is contained in:
2025-11-03 13:46:43 +05:30
parent 91cb073c78
commit 434d95eeaf
87 changed files with 17792 additions and 3461 deletions

View File

@ -62,15 +62,20 @@ public class CourseController {
course.setImageUrl(courseDto.getImageUrl());
course.setEligibility(courseDto.getEligibility());
course.setObjectives(courseDto.getObjectives());
course.setActive(courseDto.isActive());
// FIXED: Use getIsActive() and setIsActive() for both DTO and Entity
// Handle null case - default to true
Boolean isActiveValue = courseDto.getIsActive() != null ? courseDto.getIsActive() : true;
course.setIsActive(isActiveValue);
courseRepository.save(course);
return ResponseEntity.status(HttpStatus.CREATED).build();
Course savedCourse = courseRepository.save(course);
return ResponseEntity.status(HttpStatus.CREATED).body(savedCourse);
}
@PutMapping("/{id}")
public ResponseEntity<?> updateCourse(@PathVariable Long id, @RequestBody CourseDto courseDto) {
Course course = courseRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Course not found"));
Course course = courseRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Course not found"));
course.setTitle(courseDto.getTitle());
course.setDescription(courseDto.getDescription());
@ -84,10 +89,14 @@ public class CourseController {
course.setImageUrl(courseDto.getImageUrl());
course.setEligibility(courseDto.getEligibility());
course.setObjectives(courseDto.getObjectives());
course.setActive(courseDto.isActive());
// FIXED: Use getIsActive() and setIsActive() for both DTO and Entity
// Handle null case - default to true
Boolean isActiveValue = courseDto.getIsActive() != null ? courseDto.getIsActive() : true;
course.setIsActive(isActiveValue);
courseRepository.save(course);
return ResponseEntity.ok().build();
Course updatedCourse = courseRepository.save(course);
return ResponseEntity.ok(updatedCourse);
}
@DeleteMapping("/{id}")

View File

@ -22,7 +22,6 @@ public class JobController {
@Autowired
private JobRepository jobRepository;
// Get all active jobs (for public display)
@GetMapping("/active")
public ResponseEntity<List<Job>> getActiveJobs() {
try {
@ -33,13 +32,11 @@ public class JobController {
}
}
// Get all jobs (for admin)
@GetMapping
public List<Job> getAllJobs() {
return jobRepository.findAll();
}
// Get a single job by ID
@GetMapping("/{id}")
public ResponseEntity<Job> getJobById(@PathVariable Long id) {
return jobRepository.findById(id)
@ -51,8 +48,7 @@ public class JobController {
public ResponseEntity<?> createJob(@RequestBody JobDto jobDto) {
System.out.println("=== BACKEND DEBUG ===");
System.out.println("Received JobDto: " + jobDto);
System.out.println("isActive value: " + jobDto.isActive());
// Remove the .getClass() line since boolean is a primitive
System.out.println("isActive value: " + jobDto.getIsActive());
Job job = new Job();
job.setTitle(jobDto.getTitle());
@ -64,19 +60,22 @@ public class JobController {
job.setDescription(jobDto.getDescription());
job.setRequirements(jobDto.getRequirements());
job.setResponsibilities(jobDto.getResponsibilities());
job.setActive(jobDto.isActive());
Boolean isActiveValue = jobDto.getIsActive() != null ? jobDto.getIsActive() : true;
job.setIsActive(isActiveValue);
System.out.println("Job before save - isActive: " + job.isActive());
System.out.println("Job before save - isActive: " + job.getIsActive());
Job savedJob = jobRepository.save(job);
System.out.println("Job after save - isActive: " + savedJob.isActive());
System.out.println("Job after save - isActive: " + savedJob.getIsActive());
return ResponseEntity.status(HttpStatus.CREATED).build();
return ResponseEntity.status(HttpStatus.CREATED).body(savedJob);
}
@PutMapping("/{id}")
public ResponseEntity<?> updateJob(@PathVariable Long id, @RequestBody JobDto jobDto) {
Job job = jobRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Job not found"));
Job job = jobRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Job not found"));
job.setTitle(jobDto.getTitle());
job.setDepartment(jobDto.getDepartment());
@ -87,10 +86,12 @@ public class JobController {
job.setDescription(jobDto.getDescription());
job.setRequirements(jobDto.getRequirements());
job.setResponsibilities(jobDto.getResponsibilities());
job.setActive(jobDto.isActive());
Boolean isActiveValue = jobDto.getIsActive() != null ? jobDto.getIsActive() : true;
job.setIsActive(isActiveValue);
jobRepository.save(job);
return ResponseEntity.ok().build();
Job updatedJob = jobRepository.save(job);
return ResponseEntity.ok(updatedJob);
}
@DeleteMapping("/{id}")

View File

@ -0,0 +1,63 @@
package net.shyshkin.study.fullstack.supportportal.backend.controller;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Milestone;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.MilestoneDTO;
import net.shyshkin.study.fullstack.supportportal.backend.service.MilestoneService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("/api/milestones")
@RequiredArgsConstructor
@CrossOrigin(origins = {"http://localhost:4200", "http://localhost:3000"})
public class MilestoneController {
private final MilestoneService milestoneService;
// Public endpoint - for user interface
@GetMapping("/public")
public ResponseEntity<List<Milestone>> getActiveMilestones() {
return ResponseEntity.ok(milestoneService.getActiveMilestones());
}
// Admin endpoints - NO SECURITY (matching EventController pattern)
@GetMapping
public ResponseEntity<List<Milestone>> getAllMilestones() {
return ResponseEntity.ok(milestoneService.getAllMilestones());
}
@GetMapping("/{id}")
public ResponseEntity<Milestone> getMilestoneById(@PathVariable Long id) {
return ResponseEntity.ok(milestoneService.getMilestoneById(id));
}
@PostMapping
public ResponseEntity<Milestone> createMilestone(@Valid @RequestBody MilestoneDTO milestoneDTO) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(milestoneService.createMilestone(milestoneDTO));
}
@PutMapping("/{id}")
public ResponseEntity<Milestone> updateMilestone(
@PathVariable Long id,
@Valid @RequestBody MilestoneDTO milestoneDTO) {
return ResponseEntity.ok(milestoneService.updateMilestone(id, milestoneDTO));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteMilestone(@PathVariable Long id) {
milestoneService.deleteMilestone(id);
return ResponseEntity.noContent().build();
}
// @PutMapping("/reorder")
// public ResponseEntity<Void> reorderMilestones(@RequestBody List<Long> orderedIds) {
// milestoneService.reorderMilestones(orderedIds);
// return ResponseEntity.ok().build();
// }
}

View File

@ -0,0 +1,31 @@
package com.shyshkin.study.fullstack.supportportal.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* Simple test controller to verify Spring Boot is working
* Test URL: http://localhost:8080/api/test
*/
@RestController
@RequestMapping("/api")
public class TestController {
@GetMapping("/test")
public Map<String, String> test() {
Map<String, String> response = new HashMap<>();
response.put("status", "success");
response.put("message", "Spring Boot is working!");
response.put("timestamp", String.valueOf(System.currentTimeMillis()));
return response;
}
@GetMapping("/health")
public String health() {
return "Application is healthy!";
}
}

View File

@ -0,0 +1,57 @@
package net.shyshkin.study.fullstack.supportportal.backend.controller;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Testimonial;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.TestimonialDTO;
import net.shyshkin.study.fullstack.supportportal.backend.service.TestimonialService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("/api/testimonials")
@RequiredArgsConstructor
@CrossOrigin(origins = {"http://localhost:4200", "http://localhost:3000"})
public class TestimonialController {
private final TestimonialService testimonialService;
// Public endpoint - for user interface
@GetMapping("/public")
public ResponseEntity<List<Testimonial>> getActiveTestimonials() {
return ResponseEntity.ok(testimonialService.getActiveTestimonials());
}
// Admin endpoints - no security (matching your pattern)
@GetMapping
public ResponseEntity<List<Testimonial>> getAllTestimonials() {
return ResponseEntity.ok(testimonialService.getAllTestimonials());
}
@GetMapping("/{id}")
public ResponseEntity<Testimonial> getTestimonialById(@PathVariable Long id) {
return ResponseEntity.ok(testimonialService.getTestimonialById(id));
}
@PostMapping
public ResponseEntity<Testimonial> createTestimonial(@Valid @RequestBody TestimonialDTO testimonialDTO) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(testimonialService.createTestimonial(testimonialDTO));
}
@PutMapping("/{id}")
public ResponseEntity<Testimonial> updateTestimonial(
@PathVariable Long id,
@Valid @RequestBody TestimonialDTO testimonialDTO) {
return ResponseEntity.ok(testimonialService.updateTestimonial(id, testimonialDTO));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTestimonial(@PathVariable Long id) {
testimonialService.deleteTestimonial(id);
return ResponseEntity.noContent().build();
}
}

View File

@ -1,3 +1,4 @@
// Course.java - Entity for courses
package net.shyshkin.study.fullstack.supportportal.backend.domain;
import javax.persistence.*;
@ -56,6 +57,7 @@ public class Course extends BaseEntity {
@Column(name = "objective", columnDefinition = "TEXT")
private List<String> objectives;
// FIXED: Changed from primitive boolean to Boolean wrapper class
@Column(name = "is_active", nullable = false)
private boolean isActive = true;
private Boolean isActive = true;
}

View File

@ -77,6 +77,10 @@ public class Event {
@Column(name = "is_deleted", nullable = false)
private Boolean isDeleted = false;
// NEW FIELD: Book Seat Link
@Column(name = "book_seat_link", length = 500)
private String bookSeatLink;
@ManyToMany
@JoinTable(
name = "event_professors",

View File

@ -49,6 +49,8 @@ public class Job extends BaseEntity {
@Column(name = "responsibility")
private List<String> responsibilities;
// CRITICAL FIX: Changed from primitive boolean to Boolean wrapper class
// This ensures proper handling of the value from JSON and prevents default false
@Column(name = "is_active", nullable = false)
private boolean isActive = true;
private Boolean isActive = true;
}

View File

@ -0,0 +1,58 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
@Entity
@Table(name = "milestones")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Milestone implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false, length = 500)
private String description;
// @Column(nullable = false)
// private Integer displayOrder;
@Column(nullable = false)
private Boolean isActive = true;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "UTC")
@Temporal(TemporalType.DATE)
private Date milestoneDate;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC")
@Column(nullable = false, updatable = false)
private Date createdAt;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC")
private Date updatedAt;
@PrePersist
protected void onCreate() {
createdAt = new Date();
updatedAt = new Date();
}
@PreUpdate
protected void onUpdate() {
updatedAt = new Date();
}
}

View File

@ -0,0 +1,63 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "testimonials")
public class Testimonial {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(nullable = false)
private Integer age;
@Column(nullable = false, length = 200)
private String title;
@Column(nullable = false, columnDefinition = "TEXT")
private String story;
@Column(nullable = false, columnDefinition = "TEXT")
private String outcome;
@Column(nullable = false, columnDefinition = "TEXT")
private String impact;
@Column(nullable = false, length = 100)
private String category;
@Column(name = "is_active")
private Boolean isActive = true;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}

View File

@ -40,17 +40,10 @@ public class CourseDto {
private String imageUrl;
private List<String> eligibility;
private List<String> objectives;
private boolean active;
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
public void setIsActive(boolean active) {
this.active = active;
}
// FIXED: Changed from primitive boolean to Boolean wrapper
// Changed field name from "active" to "isActive"
// Removed all manual getter/setter methods
// Lombok @Data will generate: getIsActive() and setIsActive()
private Boolean isActive;
}

View File

@ -37,20 +37,5 @@ public class JobDto {
private List<String> requirements;
private List<String> responsibilities;
// Explicit boolean field handling (remove @Data for this field)
private boolean active; // Change from isActive to active
// Explicit getters and setters for the boolean field
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
// For Jackson JSON deserialization
public void setIsActive(boolean active) {
this.active = active;
}
private Boolean isActive;
}

View File

@ -0,0 +1,33 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Date;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MilestoneDTO {
private Long id;
@NotBlank(message = "Title is required")
private String title;
@NotBlank(message = "Description is required")
private String description;
// @NotNull(message = "Display order is required")
// private Integer displayOrder;
@NotNull(message = "Active status is required")
private Boolean isActive;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
private Date milestoneDate;
}

View File

@ -0,0 +1,47 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Min;
import javax.validation.constraints.Max;
import javax.validation.constraints.Size;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TestimonialDTO {
@NotBlank(message = "Name is required")
@Size(max = 100, message = "Name must not exceed 100 characters")
private String name;
@NotNull(message = "Age is required")
@Min(value = 1, message = "Age must be at least 1")
@Max(value = 120, message = "Age must not exceed 120")
private Integer age;
@NotBlank(message = "Title is required")
@Size(max = 200, message = "Title must not exceed 200 characters")
private String title;
@NotBlank(message = "Story is required")
private String story;
@NotBlank(message = "Outcome is required")
private String outcome;
@NotBlank(message = "Impact is required")
private String impact;
@NotBlank(message = "Category is required")
@Size(max = 100, message = "Category must not exceed 100 characters")
private String category;
private Boolean isActive;
}

View File

@ -0,0 +1,21 @@
package net.shyshkin.study.fullstack.supportportal.backend.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.persistence.EntityNotFoundException;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<Map<String, String>> handleEntityNotFound(EntityNotFoundException ex) {
Map<String, String> error = new HashMap<>();
error.put("error", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}

View File

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

View File

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

View File

@ -0,0 +1,16 @@
package net.shyshkin.study.fullstack.supportportal.backend.repository;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Milestone;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface MilestoneRepository extends JpaRepository<Milestone, Long> {
// List<Milestone> findAllByIsActiveTrueOrderByDisplayOrderAsc();
// List<Milestone> findAllByOrderByDisplayOrderAsc();
List<Milestone> findAllByIsActiveTrue();
}

View File

@ -0,0 +1,17 @@
package net.shyshkin.study.fullstack.supportportal.backend.repository;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Testimonial;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface TestimonialRepository extends JpaRepository<Testimonial, Long> {
// Get all active testimonials
List<Testimonial> findAllByIsActiveTrue();
// Get testimonials by category
List<Testimonial> findAllByIsActiveTrueAndCategory(String category);
}

View File

@ -0,0 +1,23 @@
package net.shyshkin.study.fullstack.supportportal.backend.service;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Milestone;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.MilestoneDTO;
import java.util.List;
public interface MilestoneService {
List<Milestone> getAllMilestones();
List<Milestone> getActiveMilestones();
Milestone getMilestoneById(Long id);
Milestone createMilestone(MilestoneDTO milestoneDTO);
Milestone updateMilestone(Long id, MilestoneDTO milestoneDTO);
void deleteMilestone(Long id);
// void reorderMilestones(List<Long> orderedIds);
}

View File

@ -0,0 +1,21 @@
package net.shyshkin.study.fullstack.supportportal.backend.service;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Testimonial;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.TestimonialDTO;
import java.util.List;
public interface TestimonialService {
List<Testimonial> getAllTestimonials();
List<Testimonial> getActiveTestimonials();
Testimonial getTestimonialById(Long id);
Testimonial createTestimonial(TestimonialDTO testimonialDTO);
Testimonial updateTestimonial(Long id, TestimonialDTO testimonialDTO);
void deleteTestimonial(Long id);
}

View File

@ -0,0 +1,141 @@
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.Milestone;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.MilestoneDTO;
import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.MilestoneNotFoundException;
import net.shyshkin.study.fullstack.supportportal.backend.repository.MilestoneRepository;
import net.shyshkin.study.fullstack.supportportal.backend.service.MilestoneService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
public class MilestoneServiceImpl implements MilestoneService {
private final MilestoneRepository milestoneRepository;
@Override
public List<Milestone> getAllMilestones() {
log.info("Fetching all milestones");
List<Milestone> milestones = milestoneRepository.findAll();
return sortMilestonesByMonthYear(milestones);
}
@Override
public List<Milestone> getActiveMilestones() {
log.info("Fetching active milestones");
List<Milestone> milestones = milestoneRepository.findAllByIsActiveTrue();
return sortMilestonesByMonthYear(milestones);
}
@Override
public Milestone getMilestoneById(Long id) {
log.info("Fetching milestone with id: {}", id);
return milestoneRepository.findById(id)
.orElseThrow(() -> new MilestoneNotFoundException("Milestone not found with id: " + id));
}
@Override
@Transactional
public Milestone createMilestone(MilestoneDTO milestoneDTO) {
log.info("Creating new milestone: {}", milestoneDTO.getTitle());
Milestone milestone = Milestone.builder()
.title(milestoneDTO.getTitle())
.description(milestoneDTO.getDescription())
.isActive(milestoneDTO.getIsActive() != null ? milestoneDTO.getIsActive() : true)
.milestoneDate(milestoneDTO.getMilestoneDate())
.build();
Milestone savedMilestone = milestoneRepository.save(milestone);
log.info("Milestone created successfully with id: {}", savedMilestone.getId());
return savedMilestone;
}
@Override
@Transactional
public Milestone updateMilestone(Long id, MilestoneDTO milestoneDTO) {
log.info("Updating milestone with id: {}", id);
Milestone milestone = getMilestoneById(id);
milestone.setTitle(milestoneDTO.getTitle());
milestone.setDescription(milestoneDTO.getDescription());
milestone.setIsActive(milestoneDTO.getIsActive());
milestone.setMilestoneDate(milestoneDTO.getMilestoneDate());
Milestone updatedMilestone = milestoneRepository.save(milestone);
log.info("Milestone updated successfully");
return updatedMilestone;
}
@Override
@Transactional
public void deleteMilestone(Long id) {
log.info("Deleting milestone with id: {}", id);
Milestone milestone = getMilestoneById(id);
milestoneRepository.delete(milestone);
log.info("Milestone deleted successfully");
}
// REMOVE reorderMilestones method
/**
* Sort milestones by year and month extracted from title
* Most recent first (descending order)
*/
private List<Milestone> sortMilestonesByMonthYear(List<Milestone> milestones) {
return milestones.stream()
.sorted(Comparator.comparing(this::extractYearMonth).reversed())
.collect(Collectors.toList());
}
/**
* Extract YearMonth from milestone title
* Supports formats like:
* - "July 2025"
* - "2025"
* - "December 2023"
*/
private YearMonth extractYearMonth(Milestone milestone) {
String title = milestone.getTitle();
try {
// Try parsing "Month Year" format (e.g., "July 2025", "December 2023")
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMMM yyyy", Locale.ENGLISH);
return YearMonth.parse(title, formatter);
} catch (DateTimeParseException e1) {
try {
// Try parsing "Month Year" with short month (e.g., "Jul 2025")
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM yyyy", Locale.ENGLISH);
return YearMonth.parse(title, formatter);
} catch (DateTimeParseException e2) {
try {
// Try parsing just year (e.g., "2025", "2020")
int year = Integer.parseInt(title.trim());
return YearMonth.of(year, 1); // Default to January
} catch (NumberFormatException e3) {
// If all parsing fails, default to a very old date
log.warn("Could not parse date from title: {}. Using default date.", title);
return YearMonth.of(1900, 1);
}
}
}
}
}

View File

@ -0,0 +1,95 @@
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.Testimonial;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.TestimonialDTO;
import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.TestimonialNotFoundException;
import net.shyshkin.study.fullstack.supportportal.backend.repository.TestimonialRepository;
import net.shyshkin.study.fullstack.supportportal.backend.service.TestimonialService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
@Slf4j
public class TestimonialServiceImpl implements TestimonialService {
private final TestimonialRepository testimonialRepository;
@Override
public List<Testimonial> getAllTestimonials() {
log.info("Fetching all testimonials");
return testimonialRepository.findAll();
}
@Override
public List<Testimonial> getActiveTestimonials() {
log.info("Fetching active testimonials");
return testimonialRepository.findAllByIsActiveTrue();
}
@Override
public Testimonial getTestimonialById(Long id) {
log.info("Fetching testimonial with id: {}", id);
return testimonialRepository.findById(id)
.orElseThrow(() -> new TestimonialNotFoundException("Testimonial not found with id: " + id));
}
@Override
@Transactional
public Testimonial createTestimonial(TestimonialDTO testimonialDTO) {
log.info("Creating new testimonial for: {}", testimonialDTO.getName());
Testimonial testimonial = Testimonial.builder()
.name(testimonialDTO.getName())
.age(testimonialDTO.getAge())
.title(testimonialDTO.getTitle())
.story(testimonialDTO.getStory())
.outcome(testimonialDTO.getOutcome())
.impact(testimonialDTO.getImpact())
.category(testimonialDTO.getCategory())
.isActive(testimonialDTO.getIsActive() != null ? testimonialDTO.getIsActive() : true)
.build();
Testimonial savedTestimonial = testimonialRepository.save(testimonial);
log.info("Testimonial created successfully with id: {}", savedTestimonial.getId());
return savedTestimonial;
}
@Override
@Transactional
public Testimonial updateTestimonial(Long id, TestimonialDTO testimonialDTO) {
log.info("Updating testimonial with id: {}", id);
Testimonial testimonial = getTestimonialById(id);
testimonial.setName(testimonialDTO.getName());
testimonial.setAge(testimonialDTO.getAge());
testimonial.setTitle(testimonialDTO.getTitle());
testimonial.setStory(testimonialDTO.getStory());
testimonial.setOutcome(testimonialDTO.getOutcome());
testimonial.setImpact(testimonialDTO.getImpact());
testimonial.setCategory(testimonialDTO.getCategory());
testimonial.setIsActive(testimonialDTO.getIsActive());
Testimonial updatedTestimonial = testimonialRepository.save(testimonial);
log.info("Testimonial updated successfully");
return updatedTestimonial;
}
@Override
@Transactional
public void deleteTestimonial(Long id) {
log.info("Deleting testimonial with id: {}", id);
Testimonial testimonial = getTestimonialById(id);
testimonialRepository.delete(testimonial);
log.info("Testimonial deleted successfully");
}
}

View File

@ -0,0 +1,14 @@
spring:
datasource:
url: jdbc:mysql://localhost:3306/support_portal?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
username: root
password: root
file:
upload:
directory: ${user.home}/support-portal-uploads
app:
base-url: http://localhost:8080
cors:
allowed-origins: http://localhost:4200,http://localhost:3000

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/courses/active,/api/courses/*,/api/course-applications,/api/upcoming-events/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/courses/active,/api/courses/*,/api/course-applications,/api/upcoming-events/active,/api/milestones,/api/milestones/**,/api/testimonials,/api/testimonials/**
cors:
allowed-origins: http://localhost:4200,http://localhost:3000,https://maincmc.rootxwire.com,https://dashboad.cmctrauma.com,https://www.dashboad.cmctrauma.com,https://cmctrauma.com,https://www.cmctrauma.com,https://cmcbackend.rootxwire.com,https://cmcadminfrontend.rootxwire.com
jwt:

View File

@ -37,6 +37,7 @@
],
"scripts": [
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
]
},
@ -127,7 +128,9 @@
"styles": [
"src/styles.css"
],
"scripts": []
"scripts": [
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
]
}
}
}

View File

@ -19,6 +19,7 @@
"@auth0/angular-jwt": "^3.0.1",
"@josipv/angular-editor-k2": "^2.20.0",
"angular-notifier": "^10.0.0",
"bootstrap": "^5.3.8",
"ng-particles": "^2.1.11",
"ngx-typed-js": "^2.0.2",
"rxjs": "~6.6.0",
@ -2806,6 +2807,17 @@
"read-package-json-fast": "^2.0.1"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@schematics/angular": {
"version": "12.2.18",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-12.2.18.tgz",
@ -3856,6 +3868,25 @@
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"dev": true
},
"node_modules/bootstrap": {
"version": "5.3.8",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
"integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"license": "MIT",
"peerDependencies": {
"@popperjs/core": "^2.11.8"
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",

View File

@ -21,6 +21,7 @@
"@auth0/angular-jwt": "^3.0.1",
"@josipv/angular-editor-k2": "^2.20.0",
"angular-notifier": "^10.0.0",
"bootstrap": "^5.3.8",
"ng-particles": "^2.1.11",
"ngx-typed-js": "^2.0.2",
"rxjs": "~6.6.0",

View File

@ -19,6 +19,10 @@ import { BlogComponent } from '../component/blog/blog.component';
import { EventFormComponent } from '../component/event-form/event-form.component';
import { CareerComponent } from '../component/career/career.component';
import { EducationComponent } from '../component/education/education.component';
import { MilestoneListComponent } from '../component/milestone/milestone-list/milestone-list.component';
import { MilestoneFormComponent } from '../component/milestone/milestone-form/milestone-form.component';
import { TestimonialFormComponent } from '../component/testimonial/testimonial-form/testimonial-form.component';
import { TestimonialListComponent } from '../component/testimonial/testimonial-list/testimonial-list.component';
const routes: Routes = [
{ path: '', component: HomeComponent },
@ -36,6 +40,13 @@ const routes: Routes = [
{ path: 'education', component: EducationComponent, canActivate: [AuthenticationGuard] },
{ path: 'userManagement', component: UserComponent, canActivate: [AuthenticationGuard] },
{ path: 'professorManagement', component: ProfessorComponent, canActivate: [AuthenticationGuard] },
{ path: 'milestone/list', component: MilestoneListComponent, canActivate: [AuthenticationGuard] },
{ path: 'milestone/create', component: MilestoneFormComponent, canActivate: [AuthenticationGuard] },
{ path: 'milestone/edit/:id', component: MilestoneFormComponent, canActivate: [AuthenticationGuard] },
{ path: 'testimonial/list', component: TestimonialListComponent, canActivate: [AuthenticationGuard] },
{ path: 'testimonial/create', component: TestimonialFormComponent, canActivate: [AuthenticationGuard] },
{ path: 'testimonial/edit/:id', component: TestimonialFormComponent, canActivate: [AuthenticationGuard] },
{
path: '',
redirectTo: 'login',

View File

@ -34,6 +34,10 @@ import { AdminRoutingModule } from './admin-routing.module';
import { CareerComponent } from '../component/career/career.component';
import { EducationComponent } from '../component/education/education.component';
import { CareerService } from '../service/career.service';
import { MilestoneFormComponent } from '../component/milestone/milestone-form/milestone-form.component';
import { MilestoneListComponent } from '../component/milestone/milestone-list/milestone-list.component';
import { TestimonialFormComponent } from '../component/testimonial/testimonial-form/testimonial-form.component';
import { TestimonialListComponent } from '../component/testimonial/testimonial-list/testimonial-list.component';
// import { PagesModule } from '../pages/pages.module';
@ -60,7 +64,11 @@ import { CareerService } from '../service/career.service';
EventComponent,
EventFormComponent,
CareerComponent,
EducationComponent
EducationComponent,
MilestoneFormComponent,
MilestoneListComponent,
TestimonialFormComponent,
TestimonialListComponent
],
imports: [
CommonModule,

View File

@ -33,6 +33,9 @@ import { NotificationModule } from './notification/notification.module';
import { EventFormComponent } from './component/event-form/event-form.component';
import { CareerComponent } from './component/career/career.component';
import { EducationComponent } from './component/education/education.component';
import { MilestoneFormComponent } from './component/milestone/milestone-form/milestone-form.component';
import { TestimonialFormComponent } from './component/testimonial/testimonial-form/testimonial-form.component';
import { TestimonialListComponent } from './component/testimonial/testimonial-list/testimonial-list.component';
// import { PagesModule } from './pages/pages.module';
@ -42,6 +45,10 @@ import { EducationComponent } from './component/education/education.component';
@NgModule({
declarations: [
AppComponent,
//TestimonialFormComponent,
//TestimonialListComponent,
//MilestoneListComponent,
//MilestoneFormComponent,
// EducationComponent,
// CareerComponent,
// LoginComponent,

View File

@ -1,31 +1,739 @@
/* Add these styles to your component's CSS file or a global stylesheet */
.blog-container {
display: flex;
justify-content: space-between;
/* Blog Layout */
.blog-layout {
display: flex;
min-height: 100vh;
background: #f8f9fa;
}
.blog-list {
flex: 1;
margin-right: 20px; /* Adjust spacing as needed */
.sidebar {
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: 260px;
z-index: 1000;
}
.blog-form {
flex: 2; /* Adjust this if you want the form to be wider or narrower */
.blog-content {
margin-left: 260px;
flex: 1;
padding: 32px;
width: calc(100% - 260px);
max-width: 1400px;
}
.blog-form form {
/* Header Section */
.blog-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
}
.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;
}
/* Buttons */
.btn-primary,
.btn-secondary,
.btn-cancel {
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 {
background: #2563eb;
transform: translateY(-1px);
}
.btn-secondary {
background: white;
color: #374151;
border: 1px solid #e5e7eb;
}
.btn-secondary:hover {
background: #f9fafb;
border-color: #d1d5db;
}
.btn-cancel {
background: #f3f4f6;
color: #6b7280;
}
.btn-cancel:hover {
background: #e5e7eb;
}
/* Form Container */
.form-container {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.form-actions-top {
display: flex;
gap: 12px;
margin-bottom: 24px;
}
/* Form Card */
.form-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 32px;
margin-bottom: 24px;
}
/* Form Groups */
.form-group {
margin-bottom: 24px;
}
.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: 16px;
}
/* Form Inputs */
.form-input,
.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;
}
.form-input:focus,
.form-select:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-input::placeholder {
color: #9ca3af;
}
.form-select {
min-height: 120px;
}
.form-select option {
padding: 8px;
}
/* Form Hints */
.form-hint {
display: block;
font-size: 12px;
color: #6b7280;
margin-top: 6px;
}
/* Error Messages */
.error-message {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
font-size: 13px;
color: #dc2626;
}
.error-message i {
font-size: 14px;
}
/* Image Upload */
.image-upload-wrapper {
position: relative;
}
.file-input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.file-label {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px;
border: 2px dashed #d1d5db;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
background: #fafafa;
}
.file-label:hover {
border-color: #3b82f6;
background: #f0f9ff;
}
.file-label i {
font-size: 32px;
color: #6b7280;
margin-bottom: 12px;
}
.file-label span {
font-size: 14px;
font-weight: 500;
color: #374151;
}
.file-label small {
font-size: 12px;
color: #9ca3af;
margin-top: 4px;
}
/* Image Preview */
.image-preview-container {
margin-top: 16px;
}
.image-preview-wrapper {
position: relative;
display: inline-block;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e5e7eb;
}
.image-preview {
max-width: 400px;
max-height: 300px;
display: block;
}
.btn-remove-image {
position: absolute;
top: 8px;
right: 8px;
width: 32px;
height: 32px;
border-radius: 6px;
background: rgba(220, 38, 38, 0.9);
color: white;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.btn-remove-image:hover {
background: rgba(220, 38, 38, 1);
transform: scale(1.1);
}
/* Upload Progress */
.upload-progress {
margin-top: 16px;
}
.progress-bar {
width: 100%;
height: 8px;
background: #e5e7eb;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
animation: progress 1.5s infinite;
border-radius: 4px;
}
@keyframes progress {
0% {
width: 0%;
}
50% {
width: 70%;
}
100% {
width: 100%;
}
}
.result-container {
border: 1px solid #ddd;
padding: 1rem;
border-radius: 4px;
background-color: #f9f9f9;
.progress-text {
display: block;
font-size: 13px;
color: #6b7280;
margin-top: 8px;
}
.result-container h4 {
margin-bottom: 1rem;
/* Checkbox */
.checkbox-wrapper {
display: flex;
align-items: center;
}
.checkbox-input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
user-select: none;
}
.checkbox-custom {
width: 20px;
height: 20px;
border: 2px solid #d1d5db;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
background: white;
}
.checkbox-input:checked + .checkbox-label .checkbox-custom {
background: #3b82f6;
border-color: #3b82f6;
}
.checkbox-input:checked + .checkbox-label .checkbox-custom::after {
content: '\f00c';
font-family: 'Font Awesome 5 Free';
font-weight: 900;
color: white;
font-size: 12px;
}
.checkbox-text {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 500;
color: #374151;
}
.checkbox-text i {
color: #6b7280;
}
/* Editor Wrapper */
.editor-wrapper {
border: 1px solid #d1d5db;
border-radius: 8px;
overflow: hidden;
}
.editor-wrapper:focus-within {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* Preview Card */
.preview-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
}
.preview-header {
display: flex;
align-items: center;
gap: 10px;
padding: 16px 24px;
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
.preview-header i {
color: #6b7280;
font-size: 18px;
}
.preview-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
}
.preview-content {
padding: 24px;
min-height: 200px;
font-size: 14px;
line-height: 1.6;
color: #374151;
}
.preview-content .empty-preview {
color: #9ca3af;
font-style: italic;
text-align: center;
padding: 40px 20px;
}
/* List Container */
.list-container {
animation: fadeIn 0.3s ease;
}
/* Table Wrapper */
.table-wrapper {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
}
.blog-table {
width: 100%;
border-collapse: collapse;
}
.blog-table thead {
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
.blog-table thead th {
padding: 16px 20px;
text-align: left;
font-size: 13px;
font-weight: 600;
color: #374151;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.blog-table tbody tr {
border-bottom: 1px solid #f3f4f6;
transition: background 0.2s ease;
}
.blog-table tbody tr:last-child {
border-bottom: none;
}
.blog-table tbody tr:hover {
background: #f9fafb;
}
.blog-table tbody td {
padding: 16px 20px;
font-size: 14px;
color: #374151;
}
/* Table Cell Styles */
.blog-title-cell {
display: flex;
align-items: center;
gap: 10px;
font-weight: 500;
color: #1a1a1a;
}
.blog-title-cell i {
color: #6b7280;
font-size: 16px;
}
.authors-cell {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.author-tag {
display: inline-block;
padding: 4px 12px;
background: #eff6ff;
color: #1e40af;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
}
.tags-cell {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.tag {
display: inline-block;
padding: 4px 12px;
background: #f3f4f6;
color: #4b5563;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
}
/* Status Badge */
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
}
.status-published {
background: #d1fae5;
color: #065f46;
}
.status-draft {
background: #fef3c7;
color: #92400e;
}
/* Action Buttons */
.action-buttons {
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-edit:hover {
background: #eff6ff;
border-color: #3b82f6;
color: #3b82f6;
}
.btn-delete:hover {
background: #fef2f2;
border-color: #dc2626;
color: #dc2626;
}
/* Empty State */
.empty-state {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 60px 20px;
text-align: center;
}
.empty-icon {
width: 80px;
height: 80px;
margin: 0 auto 20px;
background: #f3f4f6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.empty-icon i {
font-size: 36px;
color: #9ca3af;
}
.empty-state h3 {
font-size: 20px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 8px 0;
}
.empty-state p {
font-size: 14px;
color: #6b7280;
margin: 0 0 24px 0;
}
/* Responsive Design */
@media (max-width: 992px) {
.blog-content {
margin-left: 0;
width: 100%;
padding: 24px;
}
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0);
}
.blog-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.header-actions {
width: 100%;
}
.header-actions button {
flex: 1;
justify-content: center;
}
.form-card {
padding: 24px;
}
}
@media (max-width: 768px) {
.blog-content {
padding: 20px;
}
.page-title {
font-size: 24px;
}
.form-card {
padding: 20px;
}
.table-wrapper {
overflow-x: auto;
}
.blog-table {
min-width: 600px;
}
.image-preview {
max-width: 100%;
}
}
@media (max-width: 576px) {
.blog-content {
padding: 16px;
}
.form-actions-top {
flex-direction: column;
}
.form-actions-top button {
width: 100%;
justify-content: center;
}
}
/* Print Styles */
@media print {
.sidebar,
.header-actions,
.form-actions-top,
.action-buttons {
display: none;
}
.blog-content {
margin-left: 0;
width: 100%;
}
}

View File

@ -1,151 +1,252 @@
<app-menu></app-menu>
<div class="container mt-4">
<div class="d-flex justify-content-between mb-3">
<!-- Button to Toggle to Blog Form -->
<button *ngIf="!isShowForm" class="btn btn-primary" (click)="showForm()">New Blog</button>
<!-- Button to Toggle to Blog List -->
<button *ngIf="isShowForm" class="btn btn-secondary" (click)="showTable()">Back to List</button>
<div class="blog-layout">
<!-- Sidebar -->
<app-menu class="sidebar"></app-menu>
<!-- Main Content -->
<div class="blog-content">
<!-- Header Section -->
<div class="blog-header">
<div class="header-left">
<h1 class="page-title">Blog Management</h1>
<p class="page-subtitle">Create and manage your blog posts</p>
</div>
<div class="header-actions">
<button *ngIf="!isShowForm" class="btn-primary" (click)="showForm()">
<i class="fa fa-plus"></i>
<span>New Blog</span>
</button>
<button *ngIf="isShowForm" class="btn-secondary" (click)="showTable()">
<i class="fa fa-arrow-left"></i>
<span>Back to List</span>
</button>
</div>
</div>
<!-- Blog Form -->
<div *ngIf="isShowForm" class="mb-4">
<form [formGroup]="blogForm" (ngSubmit)="saveBlog()">
<div class="container">
<button type="submit" class="btn btn-primary mx-2">{{ editing ? 'Update Blog' : 'Create Blog'
}}</button>
<button type="button" class="btn btn-secondary ml-2" (click)="resetForm()">Cancel</button>
</div>
<div class="form-group m-2 ">
<label class="text-primary" for="title">Title</label>
<input type="text" id="title" class="form-control" formControlName="title"
placeholder="Enter blog title">
<div *ngIf="blogForm?.get('title')?.invalid && blogForm.get('title')?.touched" class="text-danger">
Title is required.
</div>
</div>
<div class="form-group m-2">
<label class="text-primary" for="professors">Professors</label>
<select id="professors" formControlName="professors" class="form-control" multiple>
<option *ngFor="let professor of allProfessors" [value]="professor.id">
{{ professor.firstName }}
</option>
</select>
<div *ngIf="blogForm.get('professors')?.invalid && blogForm.get('professors')?.touched"
class="text-danger mt-1">
At least one professor must be selected.
</div>
</div>
<div class="form-group m-2">
<label class="text-primary" for="tags">Tags</label>
<input type="text" id="tags" class="form-control" formControlName="tags"
placeholder="Enter tags separated by commas">
<div *ngIf="blogForm.get('tags')?.invalid && blogForm.get('tags')?.touched" class="text-danger mt-1">
Tags are required.
</div>
</div>
<!-- Add this section after the tags field and before the posted checkbox -->
<div class="form-group m-2">
<label class="text-primary" for="image">Blog Image</label>
<input type="file" id="image" class="form-control" accept="image/*" (change)="onImageSelected($event)">
<!-- Image Preview -->
<div *ngIf="imagePreviewUrl" class="mt-3">
<img [src]="imagePreviewUrl" alt="Image preview" class="img-thumbnail"
style="max-width: 300px; max-height: 200px;">
<button type="button" class="btn btn-sm btn-danger ml-2"
(click)="imagePreviewUrl = null; selectedImage = null;">
Remove Image
</button>
</div>
<!-- Upload Progress -->
<div *ngIf="uploadingImage" class="mt-2">
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 100%">
Uploading...
</div>
</div>
</div>
</div>
<div class="form-group m-2">
<input type="checkbox" class="mx-2" id="posted" formControlName="posted">
<label class="text-primary" for="posted">Posted</label>
</div>
<div class="form-group m-2">
<label class="text-primary m-1" for="content">Content</label>
<angular-editor [config]="editorConfig" [placeholder]="'Enter text here...'"
formControlName="content"></angular-editor>
<div *ngIf="blogForm.get('content')?.invalid && blogForm.get('content')?.touched"
class="text-danger mt-1">
Content is required.
</div>
</div>
<div class="result-container mt-4">
<h4 class="text-primary">Preview</h4>
<div [innerHTML]="blogForm.get('content')?.value || ''"></div>
</div>
</form>
</div>
<!-- Blog List -->
<div *ngIf="!isShowForm">
<div *ngIf="blogs.length > 0">
<table class="table table-striped">
<thead>
<tr>
<th>Title</th>
<!-- <th>Content</th> -->
<th>Authors</th>
<th>Tags</th>
<th>Posted</th> <!-- New column for posted status -->
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let blog of blogs">
<td>{{ blog.title }}</td>
<!-- <td>{{ blog.content | slice:0:50 }}...</td> -->
<td>
<span *ngFor="let professor of blog.professors">{{ professor.firstName }}<br></span>
</td>
<td>
<span *ngFor="let tag of blog.tags">{{ tag }}<br></span>
</td>
<td>
<span *ngIf="blog.posted" class="text-success">&#10003;</span>
<!-- Check mark for posted -->
<span *ngIf="!blog.posted" class="text-danger">&#10007;</span>
<!-- Cross mark for not posted -->
</td>
<td>
<button class="btn btn-info btn-sm mr-2" (click)="editBlog(blog)">
<!-- Added margin-right for spacing -->
Edit
</button>
<button class="btn btn-danger btn-sm" (click)="deleteBlog(blog)">
Delete
</button>
</td>
</tr>
</tbody>
</table>
<!-- Blog Form Section -->
<div *ngIf="isShowForm" class="form-container">
<form [formGroup]="blogForm" (ngSubmit)="saveBlog()">
<!-- Form Actions -->
<div class="form-actions-top">
<button type="submit" class="btn-primary">
<i class="fa fa-save"></i>
<span>{{ editing ? 'Update Blog' : 'Create Blog' }}</span>
</button>
<button type="button" class="btn-cancel" (click)="resetForm()">
<i class="fa fa-times"></i>
<span>Cancel</span>
</button>
</div>
<div *ngIf="blogs.length === 0" class="alert alert-info">
No blogs available. Please create a new blog.
<!-- Form Card -->
<div class="form-card">
<!-- Title Field -->
<div class="form-group">
<label for="title">
<i class="fa fa-heading"></i>
Blog Title
</label>
<input
type="text"
id="title"
class="form-input"
formControlName="title"
placeholder="Enter a compelling title for your blog post">
<div *ngIf="blogForm?.get('title')?.invalid && blogForm.get('title')?.touched" class="error-message">
<i class="fa fa-exclamation-circle"></i>
Title is required.
</div>
</div>
<!-- Professors Field -->
<div class="form-group">
<label for="professors">
<i class="fa fa-user-tie"></i>
Authors (Professors)
</label>
<select id="professors" formControlName="professors" class="form-select" multiple>
<option *ngFor="let professor of allProfessors" [value]="professor.id">
{{ professor.firstName }}
</option>
</select>
<small class="form-hint">Hold Ctrl (Windows) or Cmd (Mac) to select multiple authors</small>
<div *ngIf="blogForm.get('professors')?.invalid && blogForm.get('professors')?.touched" class="error-message">
<i class="fa fa-exclamation-circle"></i>
At least one professor must be selected.
</div>
</div>
<!-- Tags Field -->
<div class="form-group">
<label for="tags">
<i class="fa fa-tags"></i>
Tags
</label>
<input
type="text"
id="tags"
class="form-input"
formControlName="tags"
placeholder="Enter tags separated by commas (e.g., technology, education, innovation)">
<small class="form-hint">Separate multiple tags with commas</small>
<div *ngIf="blogForm.get('tags')?.invalid && blogForm.get('tags')?.touched" class="error-message">
<i class="fa fa-exclamation-circle"></i>
Tags are required.
</div>
</div>
<!-- Image Upload Field -->
<div class="form-group">
<label for="image">
<i class="fa fa-image"></i>
Blog Image
</label>
<div class="image-upload-wrapper">
<input
type="file"
id="image"
class="file-input"
accept="image/*"
(change)="onImageSelected($event)">
<label for="image" class="file-label">
<i class="fa fa-cloud-upload-alt"></i>
<span>Choose an image or drag here</span>
<small>Supports: JPG, PNG, GIF (Max 10MB)</small>
</label>
</div>
<!-- Image Preview -->
<div *ngIf="imagePreviewUrl" class="image-preview-container">
<div class="image-preview-wrapper">
<img [src]="imagePreviewUrl" alt="Image preview" class="image-preview">
<button type="button" class="btn-remove-image" (click)="removeImage()">
<i class="fa fa-times"></i>
</button>
</div>
</div>
<!-- Upload Progress -->
<div *ngIf="uploadingImage" class="upload-progress">
<div class="progress-bar">
<div class="progress-fill"></div>
</div>
<span class="progress-text">Uploading image...</span>
</div>
</div>
<!-- Posted Checkbox -->
<div class="form-group">
<div class="checkbox-wrapper">
<input type="checkbox" id="posted" formControlName="posted" class="checkbox-input">
<label for="posted" class="checkbox-label">
<span class="checkbox-custom"></span>
<span class="checkbox-text">
<i class="fa fa-globe"></i>
Publish this blog post
</span>
</label>
</div>
<small class="form-hint">Check this to make the blog visible to the public</small>
</div>
<!-- Content Editor -->
<div class="form-group">
<label for="content">
<i class="fa fa-pen"></i>
Blog Content
</label>
<div class="editor-wrapper">
<angular-editor
[config]="editorConfig"
[placeholder]="'Write your blog content here...'"
formControlName="content">
</angular-editor>
</div>
<div *ngIf="blogForm.get('content')?.invalid && blogForm.get('content')?.touched" class="error-message">
<i class="fa fa-exclamation-circle"></i>
Content is required.
</div>
</div>
</div>
<!-- Preview Card -->
<div class="preview-card">
<div class="preview-header">
<i class="fa fa-eye"></i>
<h3>Content Preview</h3>
</div>
<div class="preview-content" [innerHTML]="blogForm.get('content')?.value || '<p class=\'empty-preview\'>Start writing to see your content preview here...</p>'"></div>
</div>
</form>
</div>
<!-- Blog List Section -->
<div *ngIf="!isShowForm" class="list-container">
<div *ngIf="blogs.length > 0" class="table-wrapper">
<table class="blog-table">
<thead>
<tr>
<th>Title</th>
<th>Authors</th>
<th>Tags</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let blog of blogs">
<td>
<div class="blog-title-cell">
<i class="fa fa-file-alt"></i>
<span>{{ blog.title }}</span>
</div>
</td>
<td>
<div class="authors-cell">
<span class="author-tag" *ngFor="let professor of blog.professors">
{{ professor.firstName }}
</span>
</div>
</td>
<td>
<div class="tags-cell">
<span class="tag" *ngFor="let tag of blog.tags">
{{ tag }}
</span>
</div>
</td>
<td>
<span class="status-badge" [class.status-published]="blog.posted" [class.status-draft]="!blog.posted">
<i class="fa" [class.fa-check-circle]="blog.posted" [class.fa-clock]="!blog.posted"></i>
{{ blog.posted ? 'Published' : 'Draft' }}
</span>
</td>
<td>
<div class="action-buttons">
<button class="btn-action btn-edit" (click)="editBlog(blog)" title="Edit">
<i class="fa fa-edit"></i>
</button>
<button class="btn-action btn-delete" (click)="deleteBlog(blog)" title="Delete">
<i class="fa fa-trash-alt"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Empty State -->
<div *ngIf="blogs.length === 0" class="empty-state">
<div class="empty-icon">
<i class="fa fa-inbox"></i>
</div>
<h3>No blogs yet</h3>
<p>Get started by creating your first blog post</p>
<button class="btn-primary" (click)="showForm()">
<i class="fa fa-plus"></i>
<span>Create First Blog</span>
</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,921 @@
/* Career Layout */
.career-layout {
display: flex;
min-height: 100vh;
background: #f8f9fa;
}
.sidebar {
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: 260px;
z-index: 1000;
}
.career-content {
margin-left: 260px;
flex: 1;
padding: 32px;
width: calc(100% - 260px);
max-width: 1400px;
}
/* Header Section */
.career-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.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;
}
/* Buttons */
.btn-primary,
.btn-secondary,
.btn-cancel {
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-cancel {
background: #f3f4f6;
color: #6b7280;
}
.btn-cancel:hover {
background: #e5e7eb;
}
/* Stats Bar */
.stats-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-bottom: 32px;
}
.stat-item {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 20px;
display: flex;
align-items: center;
gap: 16px;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.stat-icon-blue {
background: #eff6ff;
color: #3b82f6;
}
.stat-icon-orange {
background: #fff7ed;
color: #f97316;
}
.stat-details {
display: flex;
flex-direction: column;
gap: 4px;
}
.stat-label {
font-size: 13px;
color: #6b7280;
font-weight: 500;
}
.stat-value {
font-size: 28px;
font-weight: 700;
color: #1a1a1a;
line-height: 1;
}
/* Form Container */
.form-container {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Form Card */
.form-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
}
.form-card-header {
padding: 24px 32px;
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
.form-card-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
}
.form-card-body {
padding: 32px;
}
/* Form Sections */
.form-section {
margin-bottom: 32px;
padding-bottom: 32px;
border-bottom: 1px solid #f3f4f6;
}
.form-section:last-of-type {
border-bottom: none;
padding-bottom: 0;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 20px 0;
display: flex;
align-items: center;
gap: 8px;
}
/* Form Rows and Groups */
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.form-row:last-child {
margin-bottom: 0;
}
.form-group {
display: flex;
flex-direction: column;
}
.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 Inputs */
.form-input,
.form-select,
.form-textarea {
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-select:focus,
.form-textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-input::placeholder,
.form-textarea::placeholder {
color: #9ca3af;
}
.form-textarea {
resize: vertical;
min-height: 100px;
}
/* Form Hints */
.form-hint {
display: block;
font-size: 12px;
color: #6b7280;
margin-top: 6px;
}
/* Error Messages */
.error-message {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
font-size: 13px;
color: #dc2626;
}
.error-message i {
font-size: 14px;
}
/* Checkbox */
.checkbox-wrapper {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.checkbox-input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
user-select: none;
}
.checkbox-custom {
width: 20px;
height: 20px;
border: 2px solid #d1d5db;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
background: white;
}
.checkbox-input:checked + .checkbox-label .checkbox-custom {
background: #3b82f6;
border-color: #3b82f6;
}
.checkbox-input:checked + .checkbox-label .checkbox-custom::after {
content: '\f00c';
font-family: 'Font Awesome 5 Free';
font-weight: 900;
color: white;
font-size: 12px;
}
.checkbox-text {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 500;
color: #374151;
}
.checkbox-text i {
color: #6b7280;
}
/* Form Actions */
.form-actions {
display: flex;
gap: 12px;
padding-top: 24px;
}
/* Applications Container */
.applications-container {
animation: fadeIn 0.3s ease;
}
.applications-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
}
.applications-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px 32px;
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
.header-info h3 {
margin: 0 0 4px 0;
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
}
.header-info p {
margin: 0;
font-size: 14px;
color: #6b7280;
}
.application-count {
padding: 8px 16px;
background: #eff6ff;
color: #1e40af;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
}
.applications-body {
padding: 24px 32px;
}
/* Tables */
.table-wrapper {
overflow-x: auto;
border-radius: 8px;
border: 1px solid #e5e7eb;
}
.jobs-table,
.applications-table {
width: 100%;
border-collapse: collapse;
background: white;
}
.jobs-table thead,
.applications-table thead {
background: #f9fafb;
}
.jobs-table thead th,
.applications-table thead th {
padding: 16px 20px;
text-align: left;
font-size: 13px;
font-weight: 600;
color: #374151;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid #e5e7eb;
}
.jobs-table tbody tr,
.applications-table tbody tr {
border-bottom: 1px solid #f3f4f6;
transition: background 0.2s ease;
}
.jobs-table tbody tr:last-child,
.applications-table tbody tr:last-child {
border-bottom: none;
}
.jobs-table tbody tr:hover,
.applications-table tbody tr:hover {
background: #f9fafb;
}
.jobs-table tbody td,
.applications-table tbody td {
padding: 16px 20px;
font-size: 14px;
color: #374151;
vertical-align: middle;
}
/* Job Details Cell */
.job-details h4 {
margin: 0 0 4px 0;
font-size: 15px;
font-weight: 600;
color: #1a1a1a;
}
.job-details p {
margin: 0;
font-size: 13px;
color: #6b7280;
}
/* Department and Location */
.department-text {
font-weight: 500;
color: #374151;
}
.location-cell {
display: flex;
align-items: center;
gap: 8px;
}
.location-cell i {
color: #6b7280;
font-size: 14px;
}
/* Type Badge */
.type-badge {
display: inline-block;
padding: 6px 12px;
background: #f3f4f6;
color: #4b5563;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
}
/* Status Badges */
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
}
.status-active {
background: #d1fae5;
color: #065f46;
}
.status-inactive {
background: #fee2e2;
color: #991b1b;
}
/* Application Status Badge Classes */
.badge-warning {
background: #fef3c7;
color: #92400e;
}
.badge-info {
background: #dbeafe;
color: #1e40af;
}
.badge-primary {
background: #e0e7ff;
color: #3730a3;
}
.badge-secondary {
background: #f3f4f6;
color: #4b5563;
}
.badge-success {
background: #d1fae5;
color: #065f46;
}
.badge-danger {
background: #fee2e2;
color: #991b1b;
}
/* Applications Button */
.btn-applications {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: #eff6ff;
color: #1e40af;
border: 1px solid #bfdbfe;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-applications:hover {
background: #dbeafe;
border-color: #93c5fd;
}
.app-count {
padding: 2px 8px;
background: #1e40af;
color: white;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
/* Action Buttons */
.action-buttons {
display: flex;
gap: 8px;
}
.btn-action {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border-radius: 6px;
border: 1px solid #e5e7eb;
background: white;
color: #6b7280;
font-size: 13px;
font-weight: 500;
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-edit:hover {
background: #eff6ff;
border-color: #3b82f6;
color: #3b82f6;
}
.btn-delete:hover {
background: #fef2f2;
border-color: #dc2626;
color: #dc2626;
}
.btn-status {
min-width: 100px;
justify-content: space-between;
}
/* Applicant Info */
.applicant-info {
display: flex;
align-items: center;
gap: 12px;
}
.applicant-avatar {
width: 40px;
height: 40px;
background: #eff6ff;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #3b82f6;
font-size: 16px;
}
.applicant-name {
font-weight: 600;
color: #1a1a1a;
}
/* Contact Info */
.contact-info {
display: flex;
flex-direction: column;
gap: 6px;
}
.contact-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #6b7280;
}
.contact-item i {
width: 16px;
font-size: 12px;
}
/* Experience Badge */
.experience-badge {
display: inline-block;
padding: 6px 12px;
background: #f0fdf4;
color: #166534;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
}
/* Date Text */
.date-text {
color: #6b7280;
font-size: 13px;
}
/* Dropdown */
.dropdown {
position: relative;
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
margin-top: 4px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
min-width: 180px;
z-index: 10;
display: none;
overflow: hidden;
}
.dropdown:hover .dropdown-menu {
display: block;
}
.dropdown-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: white;
border: none;
width: 100%;
text-align: left;
font-size: 14px;
color: #374151;
cursor: pointer;
transition: background 0.2s ease;
}
.dropdown-item:hover {
background: #f9fafb;
}
.dropdown-item i {
width: 16px;
font-size: 14px;
color: #6b7280;
}
.dropdown-item.item-danger {
color: #dc2626;
}
.dropdown-item.item-danger i {
color: #dc2626;
}
/* Empty State */
.empty-state {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 60px 20px;
text-align: center;
}
.empty-icon {
width: 80px;
height: 80px;
margin: 0 auto 20px;
background: #f3f4f6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.empty-icon i {
font-size: 36px;
color: #9ca3af;
}
.empty-state h3 {
font-size: 20px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 8px 0;
}
.empty-state p {
font-size: 14px;
color: #6b7280;
margin: 0 0 24px 0;
}
/* Jobs List Container */
.jobs-list-container {
animation: fadeIn 0.3s ease;
}
/* Responsive Design */
@media (max-width: 992px) {
.career-content {
margin-left: 0;
width: 100%;
padding: 24px;
}
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0);
}
.career-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.header-actions {
width: 100%;
}
.header-actions button {
flex: 1;
justify-content: center;
}
.stats-bar {
grid-template-columns: 1fr;
}
.form-row {
grid-template-columns: 1fr;
}
.form-card-body {
padding: 24px;
}
.applications-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
}
@media (max-width: 768px) {
.career-content {
padding: 20px;
}
.page-title {
font-size: 24px;
}
.form-card-header,
.applications-header,
.applications-body {
padding: 20px;
}
.table-wrapper {
overflow-x: auto;
}
.jobs-table,
.applications-table {
min-width: 800px;
}
.action-buttons {
flex-direction: column;
}
}
@media (max-width: 576px) {
.career-content {
padding: 16px;
}
.form-actions {
flex-direction: column;
}
.form-actions button {
width: 100%;
justify-content: center;
}
}
/* Print Styles */
@media print {
.sidebar,
.header-actions,
.action-buttons,
.btn-applications {
display: none;
}
.career-content {
margin-left: 0;
width: 100%;
}
}

View File

@ -1,248 +1,445 @@
<app-menu></app-menu>
<div class="container mt-4">
<!-- Header Actions -->
<div class="d-flex justify-content-between mb-3">
<div>
<button *ngIf="!showJobForm && !showApplications" class="btn btn-primary" (click)="showJobFormModal()">
<i class="fa fa-plus"></i> New Job
</button>
<button *ngIf="showJobForm" class="btn btn-secondary" (click)="hideJobForm()">
<i class="fa fa-arrow-left"></i> Back to Jobs
</button>
<button *ngIf="showApplications" class="btn btn-secondary" (click)="hideApplications()">
<i class="fa fa-arrow-left"></i> Back to Jobs
</button>
</div>
<div *ngIf="!showJobForm && !showApplications">
<span class="badge badge-info">Total Jobs: {{ jobs.length }}</span>
<span class="badge badge-warning ml-2">Total Applications: {{ applications.length }}</span>
</div>
<div class="career-layout">
<!-- Sidebar -->
<app-menu class="sidebar"></app-menu>
<!-- Main Content -->
<div class="career-content">
<!-- Header Section -->
<div class="career-header">
<div class="header-left">
<h1 class="page-title">Career Management</h1>
<p class="page-subtitle">Manage job postings and applications</p>
</div>
<div class="header-actions">
<button *ngIf="!showJobForm && !showApplications" class="btn-primary" (click)="showJobFormModal()">
<i class="fa fa-plus"></i>
<span>New Job</span>
</button>
<button *ngIf="showJobForm" class="btn-secondary" (click)="hideJobForm()">
<i class="fa fa-arrow-left"></i>
<span>Back to Jobs</span>
</button>
<button *ngIf="showApplications" class="btn-secondary" (click)="hideApplications()">
<i class="fa fa-arrow-left"></i>
<span>Back to Jobs</span>
</button>
</div>
</div>
<!-- Job Form -->
<div *ngIf="showJobForm" class="card">
<div class="card-header">
<h5 class="mb-0">{{ editing ? 'Edit Job' : 'Create New Job' }}</h5>
<!-- Stats Bar -->
<div *ngIf="!showJobForm && !showApplications" class="stats-bar">
<div class="stat-item">
<div class="stat-icon stat-icon-blue">
<i class="fa fa-briefcase"></i>
</div>
<div class="card-body">
<form [formGroup]="jobForm" (ngSubmit)="saveJob()">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="title" class="text-primary">Job Title *</label>
<input type="text" id="title" class="form-control" formControlName="title" placeholder="Enter job title">
<div *ngIf="jobForm.get('title')?.invalid && jobForm.get('title')?.touched" class="text-danger">
Job title is required.
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="department" class="text-primary">Department *</label>
<input type="text" id="department" class="form-control" formControlName="department" placeholder="Enter department">
<div *ngIf="jobForm.get('department')?.invalid && jobForm.get('department')?.touched" class="text-danger">
Department is required.
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="location" class="text-primary">Location *</label>
<input type="text" id="location" class="form-control" formControlName="location" placeholder="Enter location">
<div *ngIf="jobForm.get('location')?.invalid && jobForm.get('location')?.touched" class="text-danger">
Location is required.
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="type" class="text-primary">Job Type *</label>
<select id="type" class="form-control" formControlName="type">
<option value="">Select job type</option>
<option value="Full-time">Full-time</option>
<option value="Part-time">Part-time</option>
<option value="Contract">Contract</option>
<option value="Rotational">Rotational</option>
<option value="Observership">Observership</option>
</select>
<div *ngIf="jobForm.get('type')?.invalid && jobForm.get('type')?.touched" class="text-danger">
Job type is required.
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="experience" class="text-primary">Experience Required *</label>
<input type="text" id="experience" class="form-control" formControlName="experience" placeholder="e.g., MBBS + MS preferred">
<div *ngIf="jobForm.get('experience')?.invalid && jobForm.get('experience')?.touched" class="text-danger">
Experience requirement is required.
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="salary" class="text-primary">Salary *</label>
<input type="text" id="salary" class="form-control" formControlName="salary" placeholder="e.g., As per hospital norms">
<div *ngIf="jobForm.get('salary')?.invalid && jobForm.get('salary')?.touched" class="text-danger">
Salary information is required.
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="description" class="text-primary">Job Description *</label>
<textarea id="description" class="form-control" formControlName="description" rows="4" placeholder="Enter job description"></textarea>
<div *ngIf="jobForm.get('description')?.invalid && jobForm.get('description')?.touched" class="text-danger">
Job description is required.
</div>
</div>
<div class="form-group">
<label for="requirements" class="text-primary">Requirements</label>
<textarea id="requirements" class="form-control" formControlName="requirements" rows="3" placeholder="Enter requirements separated by commas"></textarea>
<small class="form-text text-muted">Separate multiple requirements with commas</small>
</div>
<div class="form-group">
<label for="responsibilities" class="text-primary">Responsibilities</label>
<textarea id="responsibilities" class="form-control" formControlName="responsibilities" rows="3" placeholder="Enter responsibilities separated by commas"></textarea>
<small class="form-text text-muted">Separate multiple responsibilities with commas</small>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="isActive" formControlName="isActive">
<label class="form-check-label text-primary" for="isActive">Active (visible to applicants)</label>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary" [disabled]="jobForm.invalid">
<i class="fa fa-save"></i> {{ editing ? 'Update Job' : 'Create Job' }}
</button>
<button type="button" class="btn btn-secondary ml-2" (click)="resetJobForm()">
<i class="fa fa-times"></i> Cancel
</button>
</div>
</form>
<div class="stat-details">
<span class="stat-label">Total Jobs</span>
<span class="stat-value">{{ jobs.length }}</span>
</div>
</div>
<div class="stat-item">
<div class="stat-icon stat-icon-orange">
<i class="fa fa-file-alt"></i>
</div>
<div class="stat-details">
<span class="stat-label">Total Applications</span>
<span class="stat-value">{{ applications.length }}</span>
</div>
</div>
</div>
<!-- Job Form Section -->
<div *ngIf="showJobForm" class="form-container">
<div class="form-card">
<div class="form-card-header">
<h3>{{ editing ? 'Edit Job Posting' : 'Create New Job' }}</h3>
</div>
<div class="form-card-body">
<form [formGroup]="jobForm" (ngSubmit)="saveJob()">
<!-- Basic Information -->
<div class="form-section">
<h4 class="section-title">Basic Information</h4>
<div class="form-row">
<div class="form-group">
<label for="title">
<i class="fa fa-briefcase"></i>
Job Title
</label>
<input
type="text"
id="title"
class="form-input"
formControlName="title"
placeholder="e.g., Senior Software Engineer">
<div *ngIf="jobForm.get('title')?.invalid && jobForm.get('title')?.touched" class="error-message">
<i class="fa fa-exclamation-circle"></i>
Job title is required.
</div>
</div>
<div class="form-group">
<label for="department">
<i class="fa fa-building"></i>
Department
</label>
<input
type="text"
id="department"
class="form-input"
formControlName="department"
placeholder="e.g., Engineering">
<div *ngIf="jobForm.get('department')?.invalid && jobForm.get('department')?.touched" class="error-message">
<i class="fa fa-exclamation-circle"></i>
Department is required.
</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="location">
<i class="fa fa-map-marker-alt"></i>
Location
</label>
<input
type="text"
id="location"
class="form-input"
formControlName="location"
placeholder="e.g., New York, NY">
<div *ngIf="jobForm.get('location')?.invalid && jobForm.get('location')?.touched" class="error-message">
<i class="fa fa-exclamation-circle"></i>
Location is required.
</div>
</div>
<div class="form-group">
<label for="type">
<i class="fa fa-clock"></i>
Job Type
</label>
<select id="type" class="form-select" formControlName="type">
<option value="">Select job type</option>
<option value="Full-time">Full-time</option>
<option value="Part-time">Part-time</option>
<option value="Contract">Contract</option>
<option value="Rotational">Rotational</option>
<option value="Observership">Observership</option>
</select>
<div *ngIf="jobForm.get('type')?.invalid && jobForm.get('type')?.touched" class="error-message">
<i class="fa fa-exclamation-circle"></i>
Job type is required.
</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="experience">
<i class="fa fa-user-graduate"></i>
Experience Required
</label>
<input
type="text"
id="experience"
class="form-input"
formControlName="experience"
placeholder="e.g., MBBS + MS preferred, 3-5 years">
<div *ngIf="jobForm.get('experience')?.invalid && jobForm.get('experience')?.touched" class="error-message">
<i class="fa fa-exclamation-circle"></i>
Experience requirement is required.
</div>
</div>
<div class="form-group">
<label for="salary">
<i class="fa fa-dollar-sign"></i>
Salary
</label>
<input
type="text"
id="salary"
class="form-input"
formControlName="salary"
placeholder="e.g., As per hospital norms, $80,000 - $100,000">
<div *ngIf="jobForm.get('salary')?.invalid && jobForm.get('salary')?.touched" class="error-message">
<i class="fa fa-exclamation-circle"></i>
Salary information is required.
</div>
</div>
</div>
</div>
<!-- Job Details -->
<div class="form-section">
<h4 class="section-title">Job Details</h4>
<div class="form-group">
<label for="description">
<i class="fa fa-align-left"></i>
Job Description
</label>
<textarea
id="description"
class="form-textarea"
formControlName="description"
rows="4"
placeholder="Describe the role, responsibilities, and what makes this position unique..."></textarea>
<div *ngIf="jobForm.get('description')?.invalid && jobForm.get('description')?.touched" class="error-message">
<i class="fa fa-exclamation-circle"></i>
Job description is required.
</div>
</div>
<div class="form-group">
<label for="requirements">
<i class="fa fa-check-circle"></i>
Requirements
</label>
<textarea
id="requirements"
class="form-textarea"
formControlName="requirements"
rows="3"
placeholder="Bachelor's degree in relevant field, 3+ years experience, Strong communication skills..."></textarea>
<small class="form-hint">Separate multiple requirements with commas</small>
</div>
<div class="form-group">
<label for="responsibilities">
<i class="fa fa-tasks"></i>
Responsibilities
</label>
<textarea
id="responsibilities"
class="form-textarea"
formControlName="responsibilities"
rows="3"
placeholder="Lead team meetings, Develop strategic plans, Collaborate with stakeholders..."></textarea>
<small class="form-hint">Separate multiple responsibilities with commas</small>
</div>
</div>
<!-- Status -->
<div class="form-section">
<div class="checkbox-wrapper">
<input type="checkbox" id="isActive" formControlName="isActive" class="checkbox-input">
<label for="isActive" class="checkbox-label">
<span class="checkbox-custom"></span>
<span class="checkbox-text">
<i class="fa fa-eye"></i>
Active (visible to job seekers)
</span>
</label>
</div>
<small class="form-hint">Uncheck to hide this job from public view</small>
</div>
<!-- Form Actions -->
<div class="form-actions">
<button type="submit" class="btn-primary" [disabled]="jobForm.invalid">
<i class="fa fa-save"></i>
<span>{{ editing ? 'Update Job' : 'Create Job' }}</span>
</button>
<button type="button" class="btn-cancel" (click)="resetJobForm()">
<i class="fa fa-times"></i>
<span>Cancel</span>
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Applications View -->
<div *ngIf="showApplications && selectedJobForApplications" class="card">
<div class="card-header">
<h5 class="mb-0">Applications for: {{ selectedJobForApplications.title }}</h5>
<div *ngIf="showApplications && selectedJobForApplications" class="applications-container">
<div class="applications-card">
<div class="applications-header">
<div class="header-info">
<h3>Applications</h3>
<p>{{ selectedJobForApplications.title }}</p>
</div>
<span class="application-count">{{ applications.length }} {{ applications.length === 1 ? 'Application' : 'Applications' }}</span>
</div>
<div class="card-body">
<div *ngIf="applications.length > 0">
<table class="table table-striped">
<thead>
<tr>
<th>Applicant</th>
<th>Email</th>
<th>Phone</th>
<th>Experience</th>
<th>Status</th>
<th>Applied Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let application of applications">
<td>{{ application.fullName }}</td>
<td>{{ application.email }}</td>
<td>{{ application.phone }}</td>
<td>{{ application.experience }}</td>
<td>
<span class="badge" [ngClass]="getStatusBadgeClass(application.status || 'pending')">
{{ application.status || 'PENDING' }}
</span>
</td>
<td>{{ application.createdDate | date:'short' }}</td>
<td>
<div class="btn-group" dropdown>
<button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle" data-toggle="dropdown">
Status
</button>
<div class="dropdown-menu">
<a class="dropdown-item" (click)="updateApplicationStatus(application, 'PENDING')">Pending</a>
<a class="dropdown-item" (click)="updateApplicationStatus(application, 'REVIEWED')">Reviewed</a>
<a class="dropdown-item" (click)="updateApplicationStatus(application, 'SHORTLISTED')">Shortlisted</a>
<a class="dropdown-item" (click)="updateApplicationStatus(application, 'INTERVIEWED')">Interviewed</a>
<a class="dropdown-item" (click)="updateApplicationStatus(application, 'HIRED')">Hired</a>
<a class="dropdown-item" (click)="updateApplicationStatus(application, 'REJECTED')">Rejected</a>
</div>
</div>
<button class="btn btn-sm btn-danger ml-1" (click)="deleteApplication(application)">
<i class="fa fa-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div *ngIf="applications.length === 0" class="alert alert-info">
No applications found for this job.
<div class="applications-body">
<div *ngIf="applications.length > 0" class="table-wrapper">
<table class="applications-table">
<thead>
<tr>
<th>Applicant</th>
<th>Contact</th>
<th>Experience</th>
<th>Status</th>
<th>Applied Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let application of applications">
<td>
<div class="applicant-info">
<div class="applicant-avatar">
<i class="fa fa-user"></i>
</div>
<span class="applicant-name">{{ application.fullName }}</span>
</div>
</td>
<td>
<div class="contact-info">
<div class="contact-item">
<i class="fa fa-envelope"></i>
<span>{{ application.email }}</span>
</div>
<div class="contact-item">
<i class="fa fa-phone"></i>
<span>{{ application.phone }}</span>
</div>
</div>
</td>
<td>
<span class="experience-badge">{{ application.experience }}</span>
</td>
<td>
<span class="status-badge" [ngClass]="getStatusBadgeClass(application.status || 'pending')">
{{ application.status || 'PENDING' }}
</span>
</td>
<td>
<span class="date-text">{{ application.createdDate | date:'MMM d, y' }}</span>
</td>
<td>
<div class="action-buttons">
<div class="dropdown">
<button class="btn-action btn-status">
<i class="fa fa-edit"></i>
<span>Status</span>
<i class="fa fa-chevron-down"></i>
</button>
<div class="dropdown-menu">
<button class="dropdown-item" (click)="updateApplicationStatus(application, 'PENDING')">
<i class="fa fa-clock"></i>
Pending
</button>
<button class="dropdown-item" (click)="updateApplicationStatus(application, 'REVIEWED')">
<i class="fa fa-eye"></i>
Reviewed
</button>
<button class="dropdown-item" (click)="updateApplicationStatus(application, 'SHORTLISTED')">
<i class="fa fa-star"></i>
Shortlisted
</button>
<button class="dropdown-item" (click)="updateApplicationStatus(application, 'INTERVIEWED')">
<i class="fa fa-comments"></i>
Interviewed
</button>
<button class="dropdown-item" (click)="updateApplicationStatus(application, 'HIRED')">
<i class="fa fa-check-circle"></i>
Hired
</button>
<button class="dropdown-item item-danger" (click)="updateApplicationStatus(application, 'REJECTED')">
<i class="fa fa-times-circle"></i>
Rejected
</button>
</div>
</div>
<button class="btn-action btn-delete" (click)="deleteApplication(application)">
<i class="fa fa-trash"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Empty State for Applications -->
<div *ngIf="applications.length === 0" class="empty-state">
<div class="empty-icon">
<i class="fa fa-inbox"></i>
</div>
<h3>No applications yet</h3>
<p>This job hasn't received any applications</p>
</div>
</div>
</div>
</div>
<!-- Jobs List -->
<div *ngIf="!showJobForm && !showApplications">
<div *ngIf="jobs.length > 0">
<table class="table table-striped">
<thead>
<tr>
<th>Title</th>
<th>Department</th>
<th>Location</th>
<th>Type</th>
<th>Status</th>
<th>Applications</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let job of jobs">
<td>
<strong>{{ job.title }}</strong>
<br>
<small class="text-muted">{{ job.experience }}</small>
</td>
<td>{{ job.department }}</td>
<td>{{ job.location }}</td>
<td>
<span class="badge badge-secondary">{{ job.type }}</span>
</td>
<td>
<span *ngIf="job.isActive" class="badge badge-success">Active</span>
<span *ngIf="!job.isActive" class="badge badge-danger">Inactive</span>
</td>
<td>
<button class="btn btn-sm btn-info" (click)="viewApplications(job)">
View Applications
<span class="badge badge-light ml-1">{{ getApplicationCount(job.id) }}</span>
</button>
</td>
<td>
<button class="btn btn-info btn-sm mr-2" (click)="editJob(job)">
<i class="fa fa-edit"></i> Edit
</button>
<button class="btn btn-danger btn-sm" (click)="deleteJob(job)">
<i class="fa fa-trash"></i> Delete
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div *ngIf="jobs.length === 0" class="alert alert-info">
No jobs available. Please create a new job.
<div *ngIf="!showJobForm && !showApplications" class="jobs-list-container">
<div *ngIf="jobs.length > 0" class="table-wrapper">
<table class="jobs-table">
<thead>
<tr>
<th>Job Details</th>
<th>Department</th>
<th>Location</th>
<th>Type</th>
<th>Status</th>
<th>Applications</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let job of jobs">
<td>
<div class="job-details">
<h4>{{ job.title }}</h4>
<p>{{ job.experience }}</p>
</div>
</td>
<td>
<span class="department-text">{{ job.department }}</span>
</td>
<td>
<div class="location-cell">
<i class="fa fa-map-marker-alt"></i>
<span>{{ job.location }}</span>
</div>
</td>
<td>
<span class="type-badge">{{ job.type }}</span>
</td>
<td>
<span class="status-badge" [class.status-active]="job.isActive" [class.status-inactive]="!job.isActive">
<i class="fa" [class.fa-check-circle]="job.isActive" [class.fa-times-circle]="!job.isActive"></i>
{{ job.isActive ? 'Active' : 'Inactive' }}
</span>
</td>
<td>
<button class="btn-applications" (click)="viewApplications(job)">
<i class="fa fa-file-alt"></i>
<span>View</span>
<span class="app-count">{{ getApplicationCount(job.id) }}</span>
</button>
</td>
<td>
<div class="action-buttons">
<button class="btn-action btn-edit" (click)="editJob(job)" title="Edit">
<i class="fa fa-edit"></i>
</button>
<button class="btn-action btn-delete" (click)="deleteJob(job)" title="Delete">
<i class="fa fa-trash-alt"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Empty State for Jobs -->
<div *ngIf="jobs.length === 0" class="empty-state">
<div class="empty-icon">
<i class="fa fa-briefcase"></i>
</div>
<h3>No jobs posted yet</h3>
<p>Get started by creating your first job posting</p>
<button class="btn-primary" (click)="showJobFormModal()">
<i class="fa fa-plus"></i>
<span>Create First Job</span>
</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,696 @@
/* Event Form Layout */
.event-form-layout {
display: flex;
min-height: 100vh;
background: #f8f9fa;
}
.sidebar {
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: 260px;
z-index: 1000;
}
.event-form-content {
margin-left: 260px;
flex: 1;
padding: 32px;
width: calc(100% - 260px);
max-width: 1800px;
}
/* Header Section */
.event-form-header {
margin-bottom: 32px;
}
.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;
}
/* Form Grid */
.event-form {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
margin-bottom: 24px;
}
.form-column {
display: flex;
flex-direction: column;
gap: 24px;
}
/* Form Card */
.form-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
gap: 10px;
padding: 20px 24px;
background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%);
border-bottom: 1px solid #e5e7eb;
}
.card-header i {
font-size: 18px;
color: #3b82f6;
}
.card-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
}
.card-body {
padding: 24px;
}
/* Form Elements */
.form-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 16px;
}
.form-row:last-child {
margin-bottom: 0;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
.form-group:last-child {
margin-bottom: 0;
}
.form-group.flex-1 {
flex: 1;
}
.form-group.flex-2 {
flex: 2;
}
.form-group label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
}
.form-group label i {
color: #6b7280;
font-size: 14px;
}
.form-input,
.form-textarea {
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 {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-input::placeholder,
.form-textarea::placeholder {
color: #9ca3af;
}
.form-textarea {
resize: vertical;
min-height: 80px;
}
.form-hint {
font-size: 12px;
color: #6b7280;
margin: 8px 0 0 0;
}
/* Checkbox */
.checkbox-wrapper {
position: relative;
}
.checkbox-input {
position: absolute;
opacity: 0;
cursor: pointer;
}
.checkbox-label {
display: flex;
align-items: start;
gap: 12px;
cursor: pointer;
padding: 16px;
border: 1px solid #e5e7eb;
border-radius: 8px;
transition: all 0.2s ease;
background: white;
}
.checkbox-label:hover {
border-color: #d1d5db;
background: #f9fafb;
}
.checkbox-input:checked + .checkbox-label {
border-color: #3b82f6;
background: #f0f9ff;
}
.checkbox-custom {
position: relative;
width: 20px;
height: 20px;
border: 2px solid #d1d5db;
border-radius: 4px;
flex-shrink: 0;
transition: all 0.2s ease;
background: white;
}
.checkbox-input:checked + .checkbox-label .checkbox-custom {
background: #3b82f6;
border-color: #3b82f6;
}
.checkbox-input:checked + .checkbox-label .checkbox-custom::after {
content: '';
position: absolute;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.checkbox-text {
display: flex;
flex-direction: column;
gap: 2px;
}
.checkbox-text strong {
font-size: 14px;
color: #1a1a1a;
font-weight: 600;
}
.checkbox-text small {
font-size: 12px;
color: #6b7280;
}
/* Upload Section */
.upload-section {
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #e5e7eb;
}
.upload-title {
font-size: 13px;
font-weight: 600;
color: #374151;
margin: 0 0 12px 0;
}
.upload-wrapper {
display: grid;
grid-template-columns: 1fr auto;
gap: 12px;
}
.file-input-wrapper {
position: relative;
}
.file-input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.file-label {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border: 2px dashed #d1d5db;
border-radius: 8px;
background: #f9fafb;
color: #6b7280;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.file-label:hover {
border-color: #3b82f6;
background: #f0f9ff;
color: #3b82f6;
}
.file-label i {
font-size: 18px;
}
.btn-upload {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
background: #3b82f6;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.btn-upload:hover:not(:disabled) {
background: #2563eb;
transform: translateY(-1px);
}
.btn-upload:disabled {
background: #9ca3af;
cursor: not-allowed;
opacity: 0.6;
}
/* URL Section */
.url-section {
margin-bottom: 20px;
}
.url-title {
font-size: 13px;
font-weight: 600;
color: #374151;
margin: 0 0 12px 0;
}
/* Image Preview */
.image-preview {
margin-top: 16px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e5e7eb;
}
.image-preview img {
width: 100%;
max-height: 250px;
object-fit: cover;
display: block;
}
/* Array Items */
.array-item {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
}
.array-item:last-child {
margin-bottom: 0;
}
.array-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #e5e7eb;
}
.array-item-header span {
font-size: 13px;
font-weight: 600;
color: #374151;
}
.array-item-body {
display: flex;
flex-direction: column;
gap: 12px;
}
/* Simple Array Item */
.simple-array-item {
display: flex;
gap: 8px;
align-items: start;
margin-bottom: 12px;
}
.simple-array-item:last-child {
margin-bottom: 0;
}
.simple-array-item .form-input {
flex: 1;
margin-bottom: 0;
}
/* Gallery URL Item */
.gallery-url-item {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
}
.gallery-url-item:last-child {
margin-bottom: 0;
}
.gallery-url-item > div:first-child {
display: flex;
gap: 8px;
}
.gallery-url-item .form-input {
flex: 1;
margin-bottom: 0;
}
.gallery-thumbnail {
width: 100px;
height: 80px;
border-radius: 6px;
overflow: hidden;
border: 1px solid #e5e7eb;
}
.gallery-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Buttons */
.btn-add {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border: 2px dashed #d1d5db;
border-radius: 8px;
background: white;
color: #3b82f6;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
margin-top: 12px;
}
.btn-add:hover {
border-color: #3b82f6;
background: #f0f9ff;
}
.btn-remove {
width: 28px;
height: 28px;
border-radius: 6px;
border: 1px solid #e5e7eb;
background: white;
color: #dc2626;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
font-size: 16px;
}
.btn-remove:hover {
background: #fef2f2;
border-color: #dc2626;
}
.btn-remove-inline {
width: 36px;
height: 36px;
border-radius: 6px;
border: 1px solid #e5e7eb;
background: white;
color: #dc2626;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
font-size: 16px;
flex-shrink: 0;
}
.btn-remove-inline:hover {
background: #fef2f2;
border-color: #dc2626;
}
/* Form Actions */
.form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 24px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
position: sticky;
bottom: 20px;
z-index: 10;
}
.btn-secondary,
.btn-submit {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 32px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.btn-secondary {
background: white;
color: #6b7280;
border: 1px solid #e5e7eb;
}
.btn-secondary:hover {
background: #f9fafb;
border-color: #d1d5db;
}
.btn-submit {
background: #22c55e;
color: white;
}
.btn-submit:hover:not(:disabled) {
background: #16a34a;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.3);
}
.btn-submit:disabled {
background: #9ca3af;
cursor: not-allowed;
opacity: 0.6;
transform: none;
}
/* Spinner */
.spinner-border {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spinner 0.75s linear infinite;
}
.spinner-border-sm {
width: 14px;
height: 14px;
border-width: 2px;
}
@keyframes spinner {
to {
transform: rotate(360deg);
}
}
/* Responsive Design */
@media (max-width: 1200px) {
.form-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 992px) {
.event-form-content {
margin-left: 0;
width: 100%;
padding: 24px;
}
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0);
}
}
@media (max-width: 768px) {
.event-form-content {
padding: 20px;
}
.page-title {
font-size: 24px;
}
.form-row {
grid-template-columns: 1fr;
}
.card-body {
padding: 20px;
}
.upload-wrapper {
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column;
}
.form-actions button {
width: 100%;
justify-content: center;
}
}
@media (max-width: 576px) {
.event-form-content {
padding: 16px;
}
.page-title {
font-size: 20px;
}
.card-header {
padding: 16px 20px;
}
.card-body {
padding: 16px;
}
}
/* Print Styles */
@media print {
.sidebar,
.form-actions {
display: none;
}
.event-form-content {
margin-left: 0;
width: 100%;
}
.form-grid {
grid-template-columns: 1fr;
}
}

View File

@ -1,268 +1,399 @@
<app-menu></app-menu>
<div class="event-form-layout">
<!-- Sidebar -->
<app-menu class="sidebar"></app-menu>
<div class="container mt-4">
<form [formGroup]="eventForm" (ngSubmit)="onSubmit()">
<div class="row">
<div class="col-md-6">
<div class="form-group mb-3">
<label for="code" class="form-label text-primary">Code</label>
<input id="code" formControlName="code" class="form-control" />
</div>
<div class="form-group mb-3">
<label for="year" class="form-label text-primary">Year</label>
<input id="year" formControlName="year" class="form-control" />
</div>
<div class="form-group mb-3">
<label for="subject" class="form-label text-primary">Subject</label>
<input id="subject" formControlName="subject" class="form-control" />
</div>
<div class="form-group mb-3">
<label for="title" class="form-label text-primary">Title</label>
<input id="title" formControlName="title" class="form-control" />
</div>
<div class="form-group mb-3">
<label for="subTitle" class="form-label text-primary">Subtitle</label>
<input id="subTitle" formControlName="subTitle" class="form-control" />
</div>
<div class="form-group mb-3">
<label for="description" class="form-label text-primary">Description</label>
<textarea id="description" formControlName="description" class="form-control" rows="3"
placeholder="Brief description for the event card"></textarea>
</div>
<div class="form-group mb-3">
<label for="detail" class="form-label text-primary">Detail</label>
<textarea id="detail" formControlName="detail" class="form-control" rows="3"
placeholder="Detailed information about the event"></textarea>
</div>
<div class="form-group mb-3">
<label for="date" class="form-label text-primary">Date</label>
<input id="date" formControlName="date" class="form-control" placeholder="MM-DD-YYYY" />
</div>
<div class="form-group mb-3">
<label for="phone" class="form-label text-primary">Phone</label>
<input id="phone" formControlName="phone" class="form-control" />
</div>
<div class="form-group mb-3">
<label for="email" class="form-label text-primary">Email</label>
<input id="email" formControlName="email" class="form-control" />
</div>
<div class="form-group mb-3">
<label for="isActive" class="form-label text-primary">Active</label>
<input id="isActive" type="checkbox" formControlName="isActive" />
</div>
</div>
<div class="col-md-6">
<!-- Main Image Section -->
<div class="form-group mb-3">
<label class="form-label text-primary">Main Image</label>
<!-- Image Upload Section -->
<div class="card mb-2">
<div class="card-body">
<h6 class="card-title">Upload Image File</h6>
<div class="row">
<div class="col-8">
<input type="file"
class="form-control"
accept="image/*"
(change)="onMainImageFileSelected($event)"
#mainImageFile>
</div>
<div class="col-4">
<button type="button"
class="btn btn-primary btn-sm w-100"
(click)="uploadMainImage()"
[disabled]="!selectedMainImageFile || mainImageUploading">
<span *ngIf="mainImageUploading" class="spinner-border spinner-border-sm me-1"></span>
{{ mainImageUploading ? 'Uploading...' : 'Upload' }}
</button>
</div>
</div>
</div>
</div>
<!-- URL Input Section -->
<div class="card mb-2">
<div class="card-body">
<h6 class="card-title">Or Enter Image URL</h6>
<input id="mainImage"
formControlName="mainImage"
class="form-control"
placeholder="https://images.unsplash.com/..." />
</div>
</div>
<!-- Image Preview -->
<div *ngIf="eventForm.get('mainImage')?.value" class="mt-2">
<img [src]="eventForm.get('mainImage')?.value"
alt="Main Image Preview"
class="img-thumbnail"
style="max-width: 200px; max-height: 150px;">
</div>
<small class="form-text text-muted">This will be the primary image displayed for the event</small>
</div>
<!-- Gallery Images Section -->
<div class="form-group mb-3">
<label class="form-label text-primary">Gallery Images</label>
<!-- Gallery Upload Section -->
<div class="card mb-2">
<div class="card-body">
<h6 class="card-title">Upload Gallery Images</h6>
<div class="row">
<div class="col-8">
<input type="file"
class="form-control"
accept="image/*"
multiple
(change)="onGalleryImagesSelected($event)"
#galleryImageFiles>
</div>
<div class="col-4">
<button type="button"
class="btn btn-primary btn-sm w-100"
(click)="uploadGalleryImages()"
[disabled]="!selectedGalleryImageFiles?.length || galleryImagesUploading">
<span *ngIf="galleryImagesUploading" class="spinner-border spinner-border-sm me-1"></span>
{{ galleryImagesUploading ? 'Uploading...' : 'Upload' }}
</button>
</div>
</div>
</div>
</div>
<!-- Manual Gallery URL Entry -->
<div class="card mb-2">
<div class="card-body">
<h6 class="card-title">Or Add Gallery URLs</h6>
<div formArrayName="galleryImages">
<div *ngFor="let image of galleryImages.controls; let i = index" class="row mb-2">
<div class="col-8">
<input [formControlName]="i" class="form-control" placeholder="https://images.unsplash.com/..." />
</div>
<div class="col-4">
<button type="button" class="btn btn-danger btn-sm w-100" (click)="removeGalleryImage(i)">
Remove
</button>
</div>
<!-- Image Preview -->
<div class="col-12" *ngIf="galleryImages.at(i).value">
<img [src]="galleryImages.at(i).value"
alt="Gallery Preview"
class="img-thumbnail mt-2"
style="max-width: 100px; max-height: 80px;">
</div>
</div>
<button type="button" class="btn btn-outline-primary btn-sm" (click)="addGalleryImage()">
Add Gallery URL
</button>
</div>
</div>
</div>
<small class="form-text text-muted">Add up to 4 gallery images for the event grid</small>
</div>
<div class="form-group mb-3">
<label class="form-label text-primary">Venues</label>
<div formArrayName="venues">
<div *ngFor="let venue of venues.controls; let i = index" class="card mb-2">
<div class="card-body">
<div [formGroupName]="i">
<div class="row mb-2">
<div class="col-12">
<input formControlName="title" placeholder="Title" class="form-control" />
</div>
</div>
<div class="row mb-2">
<div class="col-6">
<input formControlName="date" placeholder="MM-DD-YYYY" class="form-control" />
</div>
<div class="col-6">
<input formControlName="address" placeholder="Address" class="form-control" />
</div>
</div>
<div class="row mb-2">
<div class="col-12">
<input formControlName="info" placeholder="Info" class="form-control" />
</div>
</div>
</div>
<button type="button" class="btn btn-danger btn-sm float-end" (click)="removeVenue(i)">
X
</button>
</div>
</div>
<button type="button" class="btn btn-primary" (click)="addVenue()">
<i class="bi bi-plus"></i> Add Venue
</button>
</div>
</div>
<div class="form-group mb-3">
<label class="form-label text-primary">Highlights</label>
<div formArrayName="highlights">
<div *ngFor="let highlight of highlights.controls; let i = index" class="card mb-2">
<div class="card-body">
<input [formControlName]="i" class="form-control" placeholder="Highlight" />
<button type="button" class="btn btn-danger btn-sm float-end mt-2" (click)="removeHighlight(i)">
X
</button>
</div>
</div>
<button type="button" class="btn btn-primary" (click)="addHighlight()">
<i class="bi bi-plus"></i> Add Highlight
</button>
</div>
</div>
<div class="form-group mb-3">
<label class="form-label text-primary">Organisers</label>
<div formArrayName="organisers">
<div *ngFor="let organiser of organisers.controls; let i = index" class="card mb-2">
<div class="card-body">
<input [formControlName]="i" class="form-control" placeholder="Organiser" />
<button type="button" class="btn btn-danger btn-sm float-end mt-2" (click)="removeOrganiser(i)">
X
</button>
</div>
</div>
<button type="button" class="btn btn-primary" (click)="addOrganiser()">
<i class="bi bi-plus"></i> Add Organiser
</button>
</div>
</div>
<div class="form-group mb-3">
<label class="form-label text-primary">Fees</label>
<div formArrayName="fees">
<div *ngFor="let fee of fees.controls; let i = index" class="card mb-2">
<div class="card-body">
<div [formGroupName]="i">
<div class="row mb-2">
<div class="col-8">
<input formControlName="description" placeholder="Description" class="form-control" />
</div>
<div class="col-4">
<input formControlName="cost" placeholder="Cost" type="number" class="form-control" />
</div>
</div>
</div>
<button type="button" class="btn btn-danger btn-sm float-end mt-2" (click)="removeFee(i)">
X
</button>
</div>
</div>
<button type="button" class="btn btn-primary" (click)="addFee()">
<i class="bi bi-plus"></i> Add Fee
</button>
</div>
</div>
<!-- Main Content -->
<div class="event-form-content">
<!-- Header Section -->
<div class="event-form-header">
<div class="header-left">
<h1 class="page-title">{{ eventId ? 'Edit Event' : 'Create New Event' }}</h1>
<p class="page-subtitle">{{ eventId ? 'Update event information and details' : 'Fill in the details to create a new event' }}</p>
</div>
</div>
<button type="submit" class="btn btn-success">Submit</button>
</form>
<!-- Form Container -->
<form [formGroup]="eventForm" (ngSubmit)="onSubmit()" class="event-form">
<div class="form-grid">
<!-- Left Column -->
<div class="form-column">
<!-- Basic Information Section -->
<div class="form-card">
<div class="card-header">
<i class="bi bi-info-circle"></i>
<h3>Basic Information</h3>
</div>
<div class="card-body">
<div class="form-row">
<div class="form-group">
<label for="code">
<i class="bi bi-upc"></i>
Event Code *
</label>
<input id="code" formControlName="code" class="form-input" placeholder="Enter event code">
</div>
<div class="form-group">
<label for="year">
<i class="bi bi-calendar4"></i>
Year *
</label>
<input id="year" formControlName="year" class="form-input" placeholder="2024">
</div>
</div>
<div class="form-group">
<label for="subject">
<i class="bi bi-bookmark"></i>
Subject *
</label>
<input id="subject" formControlName="subject" class="form-input" placeholder="Event subject or category">
</div>
<div class="form-group">
<label for="title">
<i class="bi bi-card-heading"></i>
Event Title *
</label>
<input id="title" formControlName="title" class="form-input" placeholder="Enter event title">
</div>
<div class="form-group">
<label for="subTitle">
<i class="bi bi-card-text"></i>
Subtitle
</label>
<input id="subTitle" formControlName="subTitle" class="form-input" placeholder="Enter subtitle (optional)">
</div>
<div class="form-group">
<label for="description">
<i class="bi bi-file-text"></i>
Description *
</label>
<textarea id="description" formControlName="description" class="form-textarea" rows="3"
placeholder="Brief description for the event card"></textarea>
</div>
<div class="form-group">
<label for="detail">
<i class="bi bi-file-earmark-text"></i>
Detailed Information *
</label>
<textarea id="detail" formControlName="detail" class="form-textarea" rows="4"
placeholder="Detailed information about the event"></textarea>
</div>
</div>
</div>
<!-- Contact Information Section -->
<div class="form-card">
<div class="card-header">
<i class="bi bi-telephone"></i>
<h3>Contact Information</h3>
</div>
<div class="card-body">
<div class="form-row">
<div class="form-group">
<label for="date">
<i class="bi bi-calendar-event"></i>
Event Date *
</label>
<input id="date" formControlName="date" class="form-input" placeholder="MM-DD-YYYY">
</div>
<div class="form-group">
<label for="phone">
<i class="bi bi-phone"></i>
Phone *
</label>
<input id="phone" formControlName="phone" class="form-input" placeholder="Contact phone number">
</div>
</div>
<div class="form-group">
<label for="email">
<i class="bi bi-envelope"></i>
Email *
</label>
<input id="email" formControlName="email" class="form-input" placeholder="contact@example.com">
</div>
<!-- NEW FIELD: Book Seat Link -->
<div class="form-group">
<label for="bookSeatLink">
<i class="bi bi-link-45deg"></i>
Book Seat Registration Link
</label>
<input id="bookSeatLink"
formControlName="bookSeatLink"
class="form-input"
placeholder="https://example.com/register">
<p class="form-hint">Enter the URL where users can register/book seats for this event</p>
</div>
<div class="form-group">
<div class="checkbox-wrapper">
<input id="isActive" type="checkbox" formControlName="isActive" class="checkbox-input">
<label for="isActive" class="checkbox-label">
<span class="checkbox-custom"></span>
<span class="checkbox-text">
<strong>Active Event</strong>
<small>Display this event publicly</small>
</span>
</label>
</div>
</div>
</div>
</div>
<!-- Venues Section -->
<div class="form-card">
<div class="card-header">
<i class="bi bi-geo-alt"></i>
<h3>Venues</h3>
</div>
<div class="card-body">
<div formArrayName="venues">
<div *ngFor="let venue of venues.controls; let i = index" class="array-item">
<div class="array-item-header">
<span>Venue {{ i + 1 }}</span>
<button type="button" class="btn-remove" (click)="removeVenue(i)">
<i class="bi bi-x"></i>
</button>
</div>
<div [formGroupName]="i" class="array-item-body">
<div class="form-group">
<input formControlName="title" placeholder="Venue title" class="form-input">
</div>
<div class="form-row">
<div class="form-group">
<input formControlName="date" placeholder="MM-DD-YYYY" class="form-input">
</div>
<div class="form-group">
<input formControlName="address" placeholder="Address" class="form-input">
</div>
</div>
<div class="form-group">
<input formControlName="info" placeholder="Additional info" class="form-input">
</div>
</div>
</div>
</div>
<button type="button" class="btn-add" (click)="addVenue()">
<i class="bi bi-plus"></i>
<span>Add Venue</span>
</button>
</div>
</div>
<!-- Highlights Section -->
<div class="form-card">
<div class="card-header">
<i class="bi bi-star"></i>
<h3>Event Highlights</h3>
</div>
<div class="card-body">
<div formArrayName="highlights">
<div *ngFor="let highlight of highlights.controls; let i = index" class="simple-array-item">
<input [formControlName]="i" class="form-input" placeholder="Highlight">
<button type="button" class="btn-remove-inline" (click)="removeHighlight(i)">
<i class="bi bi-x"></i>
</button>
</div>
</div>
<button type="button" class="btn-add" (click)="addHighlight()">
<i class="bi bi-plus"></i>
<span>Add Highlight</span>
</button>
</div>
</div>
</div>
<!-- Right Column -->
<div class="form-column">
<!-- Main Image Section -->
<div class="form-card">
<div class="card-header">
<i class="bi bi-image"></i>
<h3>Main Event Image</h3>
</div>
<div class="card-body">
<!-- Upload Section -->
<div class="upload-section">
<h4 class="upload-title">Upload Image File</h4>
<div class="upload-wrapper">
<div class="file-input-wrapper">
<input type="file"
class="file-input"
id="mainImageFile"
accept="image/*"
(change)="onMainImageFileSelected($event)"
#mainImageFile>
<label for="mainImageFile" class="file-label">
<i class="bi bi-cloud-upload"></i>
<span>Choose file</span>
</label>
</div>
<button type="button"
class="btn-upload"
(click)="uploadMainImage()"
[disabled]="!selectedMainImageFile || mainImageUploading">
<i *ngIf="mainImageUploading" class="spinner-border spinner-border-sm"></i>
<i *ngIf="!mainImageUploading" class="bi bi-upload"></i>
<span>{{ mainImageUploading ? 'Uploading...' : 'Upload' }}</span>
</button>
</div>
</div>
<!-- URL Input Section -->
<div class="url-section">
<h4 class="url-title">Or Enter Image URL</h4>
<input id="mainImage"
formControlName="mainImage"
class="form-input"
placeholder="https://images.unsplash.com/...">
</div>
<!-- Image Preview -->
<div *ngIf="eventForm.get('mainImage')?.value" class="image-preview">
<img [src]="eventForm.get('mainImage')?.value"
alt="Main Image Preview">
</div>
<p class="form-hint">This will be the primary image displayed for the event</p>
</div>
</div>
<!-- Gallery Images Section -->
<div class="form-card">
<div class="card-header">
<i class="bi bi-images"></i>
<h3>Gallery Images</h3>
</div>
<div class="card-body">
<!-- Upload Section -->
<div class="upload-section">
<h4 class="upload-title">Upload Gallery Images</h4>
<div class="upload-wrapper">
<div class="file-input-wrapper">
<input type="file"
class="file-input"
id="galleryImageFiles"
accept="image/*"
multiple
(change)="onGalleryImagesSelected($event)"
#galleryImageFiles>
<label for="galleryImageFiles" class="file-label">
<i class="bi bi-cloud-upload"></i>
<span>Choose files</span>
</label>
</div>
<button type="button"
class="btn-upload"
(click)="uploadGalleryImages()"
[disabled]="!selectedGalleryImageFiles?.length || galleryImagesUploading">
<i *ngIf="galleryImagesUploading" class="spinner-border spinner-border-sm"></i>
<i *ngIf="!galleryImagesUploading" class="bi bi-upload"></i>
<span>{{ galleryImagesUploading ? 'Uploading...' : 'Upload' }}</span>
</button>
</div>
</div>
<!-- Manual URL Entry -->
<div class="url-section">
<h4 class="url-title">Or Add Gallery URLs</h4>
<div formArrayName="galleryImages">
<div *ngFor="let image of galleryImages.controls; let i = index" class="gallery-url-item">
<input [formControlName]="i" class="form-input" placeholder="https://images.unsplash.com/...">
<button type="button" class="btn-remove-inline" (click)="removeGalleryImage(i)">
<i class="bi bi-x"></i>
</button>
<!-- Thumbnail Preview -->
<div class="gallery-thumbnail" *ngIf="galleryImages.at(i).value">
<img [src]="galleryImages.at(i).value" alt="Gallery Preview">
</div>
</div>
</div>
<button type="button" class="btn-add" (click)="addGalleryImage()">
<i class="bi bi-plus"></i>
<span>Add Gallery URL</span>
</button>
</div>
<p class="form-hint">Add up to 4 gallery images for the event grid</p>
</div>
</div>
<!-- Organisers Section -->
<div class="form-card">
<div class="card-header">
<i class="bi bi-people"></i>
<h3>Organisers</h3>
</div>
<div class="card-body">
<div formArrayName="organisers">
<div *ngFor="let organiser of organisers.controls; let i = index" class="simple-array-item">
<input [formControlName]="i" class="form-input" placeholder="Organiser name">
<button type="button" class="btn-remove-inline" (click)="removeOrganiser(i)">
<i class="bi bi-x"></i>
</button>
</div>
</div>
<button type="button" class="btn-add" (click)="addOrganiser()">
<i class="bi bi-plus"></i>
<span>Add Organiser</span>
</button>
</div>
</div>
<!-- Fees Section -->
<div class="form-card">
<div class="card-header">
<i class="bi bi-currency-rupee"></i>
<h3>Registration Fees</h3>
</div>
<div class="card-body">
<div formArrayName="fees">
<div *ngFor="let fee of fees.controls; let i = index" class="array-item">
<div class="array-item-header">
<span>Fee {{ i + 1 }}</span>
<button type="button" class="btn-remove" (click)="removeFee(i)">
<i class="bi bi-x"></i>
</button>
</div>
<div [formGroupName]="i" class="array-item-body">
<div class="form-row">
<div class="form-group flex-2">
<input formControlName="description" placeholder="Fee description" class="form-input">
</div>
<div class="form-group flex-1">
<input formControlName="cost" placeholder="Cost" type="number" class="form-input">
</div>
</div>
</div>
</div>
</div>
<button type="button" class="btn-add" (click)="addFee()">
<i class="bi bi-plus"></i>
<span>Add Fee</span>
</button>
</div>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="form-actions">
<button type="button" class="btn-secondary" routerLink="/dashboard/event">
<i class="bi bi-x"></i>
<span>Cancel</span>
</button>
<button type="submit" class="btn-submit" [disabled]="!eventForm.valid">
<i class="bi bi-check-circle"></i>
<span>{{ eventId ? 'Update Event' : 'Create Event' }}</span>
</button>
</div>
</form>
</div>
</div>

View File

@ -44,6 +44,7 @@ export class EventFormComponent implements OnInit {
fees: this.fb.array([]), // Form uses 'fees' (plural)
phone: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
bookSeatLink: [''], // NEW FIELD: Book Seat Link
isActive: [true]
});
@ -146,6 +147,7 @@ export class EventFormComponent implements OnInit {
mainImage: event.mainImage,
phone: event.phone,
email: event.email,
bookSeatLink: event.bookSeatLink, // NEW FIELD
isActive: event.isActive
});
@ -177,7 +179,7 @@ export class EventFormComponent implements OnInit {
}));
} else if (controlName === 'fees') {
array.push(this.fb.group({
description: [value.description || ''], // Changed from 'desc' to 'description'
description: [value.description || ''],
cost: [value.cost || '']
}));
} else {
@ -254,7 +256,7 @@ export class EventFormComponent implements OnInit {
addFee() {
this.fees.push(this.fb.group({
description: [''], // Changed from 'desc' to 'description'
description: [''],
cost: ['']
}));
}

View File

@ -0,0 +1,437 @@
/* Event Layout */
.event-layout {
display: flex;
min-height: 100vh;
background: #f8f9fa;
}
.sidebar {
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: 260px;
z-index: 1000;
}
.event-content {
margin-left: 260px;
flex: 1;
padding: 32px;
width: calc(100% - 260px);
max-width: 1600px;
}
/* Header Section */
.event-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
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;
}
.btn-primary {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
background: #3b82f6;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
}
.btn-primary:hover {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
/* Table Container */
.table-container {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
}
.table-wrapper {
overflow-x: auto;
}
/* Events Table */
.events-table {
width: 100%;
border-collapse: collapse;
}
.events-table thead {
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
.events-table thead th {
padding: 16px 20px;
text-align: left;
font-size: 13px;
font-weight: 600;
color: #374151;
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
}
.events-table tbody tr {
border-bottom: 1px solid #f3f4f6;
transition: background 0.2s ease;
}
.events-table tbody tr:last-child {
border-bottom: none;
}
.events-table tbody tr:hover {
background: #f9fafb;
}
.events-table tbody td {
padding: 16px 20px;
font-size: 14px;
color: #374151;
vertical-align: middle;
}
/* Event Image Cell */
.event-image-cell {
display: flex;
align-items: center;
justify-content: center;
}
.event-thumbnail {
width: 60px;
height: 40px;
object-fit: cover;
border-radius: 6px;
border: 1px solid #e5e7eb;
}
.no-image {
width: 60px;
height: 40px;
background: #f3f4f6;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: #9ca3af;
font-size: 20px;
}
/* Event Code Cell */
.event-code-cell {
display: flex;
flex-direction: column;
gap: 4px;
}
.code-badge {
font-family: 'Courier New', monospace;
font-size: 12px;
font-weight: 600;
color: #3b82f6;
background: #eff6ff;
padding: 4px 8px;
border-radius: 4px;
width: fit-content;
}
.year-text {
font-size: 12px;
color: #6b7280;
}
/* Event Details Cell */
.event-details-cell {
display: flex;
flex-direction: column;
gap: 4px;
max-width: 400px;
}
.event-title {
font-weight: 600;
color: #1a1a1a;
font-size: 14px;
}
.event-subject {
font-size: 12px;
color: #6b7280;
background: #f3f4f6;
padding: 2px 8px;
border-radius: 4px;
width: fit-content;
}
.event-description {
font-size: 13px;
color: #6b7280;
line-height: 1.4;
margin-top: 4px;
}
/* Date Cell */
.date-cell {
display: flex;
align-items: center;
gap: 8px;
color: #6b7280;
font-size: 13px;
white-space: nowrap;
}
.date-cell i {
color: #9ca3af;
font-size: 16px;
}
/* Fees Cell */
.fees-cell {
min-width: 150px;
}
.fee-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.fee-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.fee-desc {
font-size: 12px;
color: #6b7280;
}
.fee-cost {
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
}
.fee-more {
font-size: 11px;
color: #3b82f6;
margin-top: 2px;
}
.no-fees {
font-size: 13px;
color: #9ca3af;
font-style: italic;
}
/* Status Badge */
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
}
.status-active {
background: #d1fae5;
color: #065f46;
}
.status-inactive {
background: #fee2e2;
color: #991b1b;
}
/* Action Buttons */
.action-buttons {
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;
font-size: 14px;
text-decoration: none;
}
.btn-action i {
font-size: 14px;
display: inline-block;
}
.btn-action:hover {
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.btn-edit:hover {
background: #eff6ff;
border-color: #3b82f6;
color: #3b82f6;
}
.btn-edit:hover i {
color: #3b82f6;
}
.btn-delete:hover {
background: #fef2f2;
border-color: #dc2626;
color: #dc2626;
}
.btn-delete:hover i {
color: #dc2626;
}
/* Empty State */
.empty-state {
padding: 80px 20px;
text-align: center;
}
.empty-icon {
width: 80px;
height: 80px;
margin: 0 auto 24px;
background: #f3f4f6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
color: #9ca3af;
}
.empty-state h3 {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 8px 0;
}
.empty-state p {
font-size: 14px;
color: #6b7280;
margin: 0 0 24px 0;
}
/* Responsive Design */
@media (max-width: 992px) {
.event-content {
margin-left: 0;
width: 100%;
padding: 24px;
}
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0);
}
.event-header {
flex-direction: column;
align-items: flex-start;
}
.events-table {
min-width: 1000px;
}
}
@media (max-width: 768px) {
.event-content {
padding: 20px;
}
.page-title {
font-size: 24px;
}
.event-details-cell {
max-width: 250px;
}
}
@media (max-width: 576px) {
.event-content {
padding: 16px;
}
.page-title {
font-size: 20px;
}
}
/* Print Styles */
@media print {
.sidebar,
.header-actions,
.action-buttons {
display: none;
}
.event-content {
margin-left: 0;
width: 100%;
}
}

View File

@ -1,67 +1,128 @@
<app-menu></app-menu>
<div class="event-layout">
<!-- Sidebar -->
<app-menu class="sidebar"></app-menu>
<div class="container mt-4">
<h2 class="mb-4">Events</h2>
<div class="mb-3">
<a routerLink="/dashboard/eventForm" class="btn btn-primary">
<i class="bi bi-plus"></i> Add New Event
</a>
</div>
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th>Code</th>
<th>Year</th>
<th>Subject</th>
<th>Title</th>
<th>Description</th>
<th>Date</th>
<th>Fees</th>
<th>Main Image</th>
<th>Active</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let event of events">
<td>{{ event.code }}</td>
<td>{{ event.year }}</td>
<td>{{ event.subject }}</td>
<td>{{ event.title }}</td>
<td>{{ event.description?.substring(0, 50) }}{{ event.description?.length > 50 ? '...' : '' }}</td>
<td>{{ event.date }}</td>
<td>
<span *ngIf="event.fee && event.fee.length > 0">
{{ event.fee[0].description }}: ₹{{ event.fee[0].cost }}
<span *ngIf="event.fee.length > 1"> (+{{ event.fee.length - 1 }} more)</span>
</span>
<span *ngIf="!event.fee || event.fee.length === 0" class="text-muted">No fees</span>
</td>
<td>
<img *ngIf="event.mainImage"
[src]="event.mainImage"
alt="Event Image"
style="width: 50px; height: 30px; object-fit: cover; border-radius: 4px;"
onerror="this.style.display='none'">
<span *ngIf="!event.mainImage" class="text-muted">No image</span>
</td>
<td>
<span class="badge"
[ngClass]="event.isActive ? 'bg-success' : 'bg-secondary'">
{{ event.isActive ? 'Yes' : 'No' }}
</span>
</td>
<td>
<a [routerLink]="['/dashboard/eventForm', event.id]" class="btn btn-warning btn-sm me-2">
<i class="bi bi-pencil"></i> Edit
</a>
<button (click)="deleteEvent(event.id)" class="btn btn-danger btn-sm">
<i class="bi bi-trash"></i> Delete
</button>
</td>
</tr>
</tbody>
</table>
<!-- Main Content -->
<div class="event-content">
<!-- Header Section -->
<div class="event-header">
<div class="header-left">
<h1 class="page-title">Events Management</h1>
<p class="page-subtitle">Manage and organize all your events</p>
</div>
<div class="header-actions">
<button class="btn-primary" routerLink="/dashboard/eventForm">
<i class="fa fa-plus"></i>
<span>Add New Event</span>
</button>
</div>
</div>
<!-- Events Table -->
<div class="table-container">
<div class="table-wrapper">
<table class="events-table">
<thead>
<tr>
<th>Image</th>
<th>Code</th>
<th>Event Details</th>
<th>Date</th>
<th>Fees</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let event of events">
<td>
<div class="event-image-cell">
<img *ngIf="event.mainImage"
[src]="event.mainImage"
alt="Event Image"
class="event-thumbnail"
onerror="this.style.display='none'">
<div *ngIf="!event.mainImage" class="no-image">
<i class="fa fa-image"></i>
</div>
</div>
</td>
<td>
<div class="event-code-cell">
<span class="code-badge">{{ event.code }}</span>
<span class="year-text">{{ event.year }}</span>
</div>
</td>
<td>
<div class="event-details-cell">
<div class="event-title">{{ event.title }}</div>
<div class="event-subject">{{ event.subject }}</div>
<div class="event-description">
{{ event.description?.substring(0, 80) }}{{ event.description?.length > 80 ? '...' : '' }}
</div>
</div>
</td>
<td>
<div class="date-cell">
<i class="fa fa-calendar"></i>
<span>{{ event.date }}</span>
</div>
</td>
<td>
<div class="fees-cell">
<div *ngIf="event.fee && event.fee.length > 0" class="fee-info">
<div class="fee-item">
<span class="fee-desc">{{ event.fee[0].description }}</span>
<span class="fee-cost">₹{{ event.fee[0].cost }}</span>
</div>
<div *ngIf="event.fee.length > 1" class="fee-more">
+{{ event.fee.length - 1 }} more
</div>
</div>
<span *ngIf="!event.fee || event.fee.length === 0" class="no-fees">No fees</span>
</div>
</td>
<td>
<span class="status-badge"
[class.status-active]="event.isActive"
[class.status-inactive]="!event.isActive">
<i class="fa"
[class.fa-check-circle]="event.isActive"
[class.fa-times-circle]="!event.isActive"></i>
{{ event.isActive ? 'Active' : 'Inactive' }}
</span>
</td>
<td>
<div class="action-buttons">
<button class="btn-action btn-edit"
[routerLink]="['/dashboard/eventForm', event.id]"
title="Edit">
<i class="fa fa-edit"></i>
</button>
<button class="btn-action btn-delete"
(click)="deleteEvent(event.id)"
title="Delete">
<i class="fa fa-trash-alt"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Empty State -->
<div *ngIf="events.length === 0" class="empty-state">
<div class="empty-icon">
<i class="fa fa-calendar-times"></i>
</div>
<h3>No events found</h3>
<p>Get started by creating your first event</p>
<button class="btn-primary" routerLink="/dashboard/eventForm">
<i class="fa fa-plus"></i>
<span>Add New Event</span>
</button>
</div>
</div>
</div>
</div>

View File

@ -1,20 +1,633 @@
/* This ensures that the container takes at least the full viewport height */
.min-vh-100 {
min-height: 100vh;
/* Dashboard Container */
.dashboard-container {
display: flex;
min-height: 100vh;
background: #f8f9fa;
}
/* Sidebar Styling */
.sidebar {
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: 260px;
z-index: 1000;
}
/* Main Content Area */
.main-content {
margin-left: 260px;
flex: 1;
padding: 32px;
width: calc(100% - 260px);
max-width: 1400px;
}
/* Dashboard Header */
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
}
.header-content {
flex: 1;
}
.dashboard-title {
font-size: 28px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 4px 0;
letter-spacing: -0.5px;
}
.dashboard-subtitle {
font-size: 14px;
color: #6b7280;
margin: 0;
font-weight: 400;
}
/* Refresh Button */
.btn-refresh {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
color: #374151;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-refresh:hover:not(:disabled) {
background: #f9fafb;
border-color: #d1d5db;
}
.btn-refresh:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-refresh i.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Loading Container */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
background: white;
border-radius: 12px;
border: 1px solid #e5e7eb;
}
.spinner {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
.spinner > div {
width: 12px;
height: 12px;
background-color: #3b82f6;
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
}
.spinner .bounce1 {
animation-delay: -0.32s;
}
.spinner .bounce2 {
animation-delay: -0.16s;
}
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1.0);
}
/* Optional: Add more custom styles if needed */
.text-center {
text-align: center;
}
.loading-text {
font-size: 14px;
color: #6b7280;
margin: 0;
}
/* Error Alert */
.alert-error {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
color: #991b1b;
margin-bottom: 24px;
font-size: 14px;
}
.alert-error i {
font-size: 18px;
flex-shrink: 0;
}
.btn-close-alert {
margin-left: auto;
background: none;
border: none;
color: #991b1b;
cursor: pointer;
padding: 4px;
font-size: 16px;
opacity: 0.7;
transition: opacity 0.2s;
}
.btn-close-alert:hover {
opacity: 1;
}
/* Dashboard Content */
.dashboard-content {
animation: fadeIn 0.4s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
h1 {
color: #007bff; /* Bootstrap primary color or customize */
font-size: 2.5rem; /* Adjust size as needed */
to {
opacity: 1;
transform: translateY(0);
}
.lead {
color: #6c757d; /* Bootstrap secondary color or customize */
font-size: 1.25rem; /* Adjust size as needed */
}
/* Quick Stats Cards */
.quick-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-bottom: 32px;
}
.stat-card-minimal {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 20px;
display: flex;
align-items: center;
gap: 16px;
transition: all 0.2s ease;
}
.stat-card-minimal:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transform: translateY(-2px);
}
.stat-minimal-icon {
width: 48px;
height: 48px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.stat-icon-blue {
background: #eff6ff;
color: #3b82f6;
}
.stat-icon-purple {
background: #f5f3ff;
color: #8b5cf6;
}
.stat-icon-green {
background: #f0fdf4;
color: #22c55e;
}
.stat-icon-orange {
background: #fff7ed;
color: #f97316;
}
.stat-minimal-content {
flex: 1;
}
.stat-minimal-content h3 {
font-size: 28px;
font-weight: 700;
color: #1a1a1a;
margin: 0 0 4px 0;
line-height: 1;
}
.stat-minimal-content p {
font-size: 13px;
color: #6b7280;
margin: 0;
font-weight: 500;
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 32px;
}
/* Stats Card */
.stats-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
transition: all 0.2s ease;
}
.stats-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transform: translateY(-2px);
}
.card-header {
padding: 20px;
border-bottom: 1px solid #f3f4f6;
}
.card-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 15px;
font-weight: 600;
color: #1a1a1a;
}
.card-title i {
font-size: 18px;
color: #6b7280;
}
.card-body {
padding: 20px;
}
.stat-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f3f4f6;
}
.stat-row:last-child {
border-bottom: none;
padding-bottom: 0;
}
.stat-row:first-child {
padding-top: 0;
}
.stat-label {
font-size: 13px;
color: #6b7280;
font-weight: 500;
}
.stat-value {
font-size: 18px;
font-weight: 700;
color: #1a1a1a;
}
.stat-value-success {
color: #22c55e;
}
.stat-value-info {
color: #3b82f6;
}
.stat-value-warning {
color: #f59e0b;
}
.stat-badge {
padding: 4px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
}
.stat-badge-success {
background: #d1fae5;
color: #065f46;
}
.stat-badge-info {
background: #dbeafe;
color: #1e40af;
}
/* Recent Activities Section */
.recent-section {
margin-top: 32px;
}
.section-header {
margin-bottom: 20px;
}
.section-header h2 {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin: 0;
display: flex;
align-items: center;
gap: 10px;
}
.section-header i {
font-size: 20px;
color: #6b7280;
}
.activities-list {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 8px;
}
.activity-item {
display: flex;
gap: 16px;
padding: 16px;
border-radius: 8px;
transition: background 0.2s ease;
}
.activity-item:hover {
background: #f9fafb;
}
.activity-icon-wrapper {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
.activity-primary {
background: #eff6ff;
color: #3b82f6;
}
.activity-success {
background: #f0fdf4;
color: #22c55e;
}
.activity-details {
flex: 1;
min-width: 0;
}
.activity-details h4 {
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 4px 0;
}
.activity-details p {
font-size: 13px;
color: #6b7280;
margin: 0 0 6px 0;
line-height: 1.5;
}
.activity-time {
font-size: 12px;
color: #9ca3af;
font-style: italic;
}
/* Empty State */
.empty-state {
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
text-align: center;
}
.empty-icon {
width: 64px;
height: 64px;
background: #f3f4f6;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
}
.empty-icon i {
font-size: 28px;
color: #9ca3af;
}
.empty-state p {
color: #9ca3af;
font-size: 14px;
margin: 0;
}
/* Responsive Design */
@media (max-width: 1200px) {
.stats-grid {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
}
@media (max-width: 992px) {
.main-content {
margin-left: 0;
width: 100%;
padding: 24px;
}
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0);
}
.dashboard-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.btn-refresh {
width: 100%;
justify-content: center;
}
.quick-stats {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.main-content {
padding: 20px;
}
.dashboard-title {
font-size: 24px;
}
.dashboard-subtitle {
font-size: 13px;
}
.quick-stats {
grid-template-columns: 1fr;
gap: 16px;
}
.stats-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.stat-minimal-content h3 {
font-size: 24px;
}
}
@media (max-width: 576px) {
.main-content {
padding: 16px;
}
.dashboard-header {
margin-bottom: 24px;
}
.stat-card-minimal {
padding: 16px;
}
.stat-minimal-icon {
width: 40px;
height: 40px;
font-size: 18px;
}
.card-header,
.card-body {
padding: 16px;
}
}
/* Print Styles */
@media print {
.sidebar,
.btn-refresh {
display: none;
}
.main-content {
margin-left: 0;
width: 100%;
}
.stats-card,
.stat-card-minimal {
break-inside: avoid;
}
}
/* Smooth Scrolling */
html {
scroll-behavior: smooth;
}
/* Focus Styles for Accessibility */
.btn-refresh:focus,
.btn-close-alert:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
/* Card Hover Effect Enhancement */
.stats-card::before,
.stat-card-minimal::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 100%);
opacity: 0;
transition: opacity 0.2s ease;
}
.stats-card {
position: relative;
}
.stat-card-minimal {
position: relative;
}
.stats-card:hover::before,
.stat-card-minimal:hover::before {
opacity: 1;
}

View File

@ -1,9 +1,288 @@
<div class="dashboard-container">
<!-- Sidebar Menu -->
<app-menu class="sidebar"></app-menu>
<app-menu ></app-menu>
<!-- Main Dashboard Content -->
<div class="main-content">
<!-- Dashboard Header -->
<div class="dashboard-header">
<div class="header-content">
<h1 class="dashboard-title">Dashboard Overview</h1>
<p class="dashboard-subtitle">Welcome back! Here's what's happening today.</p>
</div>
<button class="btn-refresh" (click)="refreshDashboard()" [disabled]="loading">
<i class="fa fa-sync-alt" [class.spinning]="loading"></i>
<span>Refresh</span>
</button>
</div>
<!-- Loading Spinner -->
<div *ngIf="loading" class="loading-container">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
<p class="loading-text">Loading dashboard data...</p>
</div>
<!-- Error Message -->
<div *ngIf="error && !loading" class="alert-error">
<i class="fa fa-exclamation-circle"></i>
<span>{{ error }}</span>
<button class="btn-close-alert" (click)="error = null">
<i class="fa fa-times"></i>
</button>
</div>
<!-- Dashboard Content -->
<div *ngIf="!loading && !error" class="dashboard-content">
<!-- Quick Stats Summary Cards -->
<div class="quick-stats">
<div class="stat-card-minimal">
<div class="stat-minimal-icon stat-icon-blue">
<i class="fa fa-calendar-check"></i>
</div>
<div class="stat-minimal-content">
<h3>{{ stats.events.upcoming }}</h3>
<p>Upcoming Events</p>
</div>
</div>
<div class="stat-card-minimal">
<div class="stat-minimal-icon stat-icon-purple">
<i class="fa fa-clock"></i>
</div>
<div class="stat-minimal-content">
<h3>{{ stats.careers.pendingApplications + stats.education.pendingApplications }}</h3>
<p>Pending Reviews</p>
</div>
</div>
<div class="stat-card-minimal">
<div class="stat-minimal-icon stat-icon-green">
<i class="fa fa-briefcase"></i>
</div>
<div class="stat-minimal-content">
<h3>{{ stats.careers.activeJobs }}</h3>
<p>Active Jobs</p>
</div>
</div>
<div class="stat-card-minimal">
<div class="stat-minimal-icon stat-icon-orange">
<i class="fa fa-graduation-cap"></i>
</div>
<div class="stat-minimal-content">
<h3>{{ stats.education.activeCourses }}</h3>
<p>Active Courses</p>
</div>
</div>
</div>
<!-- Main Stats Grid -->
<div class="stats-grid">
<!-- Events Card -->
<div class="stats-card">
<div class="card-header">
<div class="card-title">
<i class="fa fa-calendar-alt"></i>
<span>Events</span>
</div>
</div>
<div class="card-body">
<div class="stat-row">
<span class="stat-label">Total Events</span>
<span class="stat-value">{{ stats.events.total }}</span>
</div>
<div class="stat-row">
<span class="stat-label">Upcoming</span>
<span class="stat-value stat-value-success">{{ stats.events.upcoming }}</span>
</div>
<div class="stat-row">
<span class="stat-label">Active</span>
<span class="stat-value stat-value-info">{{ stats.events.active }}</span>
</div>
</div>
</div>
<!-- Blogs Card -->
<div class="stats-card">
<div class="card-header">
<div class="card-title">
<i class="fa fa-blog"></i>
<span>Blogs</span>
</div>
</div>
<div class="card-body">
<div class="stat-row">
<span class="stat-label">Total Posts</span>
<span class="stat-value">{{ stats.blogs.total }}</span>
</div>
<div class="stat-row">
<span class="stat-label">Published</span>
<span class="stat-value stat-value-success">{{ stats.blogs.published }}</span>
</div>
<div class="stat-row">
<span class="stat-label">Drafts</span>
<span class="stat-value stat-value-warning">{{ stats.blogs.drafts }}</span>
</div>
</div>
</div>
<!-- Careers Card -->
<div class="stats-card">
<div class="card-header">
<div class="card-title">
<i class="fa fa-briefcase"></i>
<span>Careers</span>
</div>
</div>
<div class="card-body">
<div class="stat-row">
<span class="stat-label">Active Jobs</span>
<span class="stat-value">{{ stats.careers.activeJobs }}</span>
</div>
<div class="stat-row">
<span class="stat-label">Applications</span>
<span class="stat-value stat-value-info">{{ stats.careers.totalApplications }}</span>
</div>
<div class="stat-row">
<span class="stat-label">Pending</span>
<span class="stat-value stat-value-warning">{{ stats.careers.pendingApplications }}</span>
</div>
</div>
</div>
<!-- Education Card -->
<div class="stats-card">
<div class="card-header">
<div class="card-title">
<i class="fa fa-graduation-cap"></i>
<span>Education</span>
</div>
</div>
<div class="card-body">
<div class="stat-row">
<span class="stat-label">Active Courses</span>
<span class="stat-value">{{ stats.education.activeCourses }}</span>
</div>
<div class="stat-row">
<span class="stat-label">Enrollments</span>
<span class="stat-value stat-value-info">{{ stats.education.totalApplications }}</span>
</div>
<div class="stat-row">
<span class="stat-label">Pending</span>
<span class="stat-value stat-value-warning">{{ stats.education.pendingApplications }}</span>
</div>
</div>
</div>
<!-- Professors Card -->
<div class="stats-card">
<div class="card-header">
<div class="card-title">
<i class="fa fa-chalkboard-teacher"></i>
<span>Professors</span>
</div>
</div>
<div class="card-body">
<div class="stat-row">
<span class="stat-label">Total</span>
<span class="stat-value">{{ stats.professors.total }}</span>
</div>
<div class="stat-row">
<span class="stat-label">Active</span>
<span class="stat-value stat-value-success">{{ stats.professors.active }}</span>
</div>
<div class="stat-row">
<span class="stat-label">Status</span>
<span class="stat-badge stat-badge-success">Active</span>
</div>
</div>
</div>
<!-- Testimonials Card -->
<div class="stats-card">
<div class="card-header">
<div class="card-title">
<i class="fa fa-quote-left"></i>
<span>Testimonials</span>
</div>
</div>
<div class="card-body">
<div class="stat-row">
<span class="stat-label">Total</span>
<span class="stat-value">{{ stats.testimonials.total }}</span>
</div>
<div class="stat-row">
<span class="stat-label">Active</span>
<span class="stat-value stat-value-success">{{ stats.testimonials.active }}</span>
</div>
<div class="stat-row">
<span class="stat-label">Status</span>
<span class="stat-badge stat-badge-success">Published</span>
</div>
</div>
</div>
<!-- Milestones Card -->
<div class="stats-card">
<div class="card-header">
<div class="card-title">
<i class="fa fa-flag-checkered"></i>
<span>Milestones</span>
</div>
</div>
<div class="card-body">
<div class="stat-row">
<span class="stat-label">Total</span>
<span class="stat-value">{{ stats.milestones.total }}</span>
</div>
<div class="stat-row">
<span class="stat-label">Active</span>
<span class="stat-value stat-value-success">{{ stats.milestones.active }}</span>
</div>
<div class="stat-row">
<span class="stat-label">Status</span>
<span class="stat-badge stat-badge-info">Tracking</span>
</div>
</div>
</div>
</div>
<!-- Recent Activities Section -->
<div class="recent-section" *ngIf="recentActivities.length > 0">
<div class="section-header">
<h2>
<i class="fa fa-clock"></i>
Recent Activities
</h2>
</div>
<div class="activities-list">
<div class="activity-item" *ngFor="let activity of recentActivities">
<div class="activity-icon-wrapper" [ngClass]="'activity-' + activity.color">
<i class="fa fa-{{ activity.icon }}"></i>
</div>
<div class="activity-details">
<h4>{{ activity.type }}</h4>
<p>{{ activity.description }}</p>
<span class="activity-time">{{ getTimeAgo(activity.date) }}</span>
</div>
</div>
<div class="activity-item empty-state" *ngIf="recentActivities.length === 0">
<div class="empty-icon">
<i class="fa fa-inbox"></i>
</div>
<p>No recent activities</p>
</div>
</div>
</div>
<div class="container d-flex justify-content-center align-items-center min-vh-100">
<div class="text-center">
<h1>Welcome, CMC!</h1>
</div>
</div>
</div>

View File

@ -1,4 +1,46 @@
import { Component, OnInit } from '@angular/core';
import { forkJoin } from 'rxjs';
import { EventService } from '../../service/event.service';
import { BlogService } from '../../service/blog.service';
import { CareerService } from '../../service/career.service';
import { EducationService } from '../../service/education.service';
import { MilestoneService } from '../../service/milestone.service';
import { DashboardService } from '../../service/dashboard.service';
interface DashboardStats {
events: {
total: number;
upcoming: number;
active: number;
};
blogs: {
total: number;
published: number;
drafts: number;
};
careers: {
activeJobs: number;
totalApplications: number;
pendingApplications: number;
};
education: {
activeCourses: number;
totalApplications: number;
pendingApplications: number;
};
professors: {
total: number;
active: number;
};
testimonials: {
total: number;
active: number;
};
milestones: {
total: number;
active: number;
};
}
@Component({
selector: 'app-home',
@ -6,10 +48,166 @@ import { Component, OnInit } from '@angular/core';
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
stats: DashboardStats = {
events: { total: 0, upcoming: 0, active: 0 },
blogs: { total: 0, published: 0, drafts: 0 },
careers: { activeJobs: 0, totalApplications: 0, pendingApplications: 0 },
education: { activeCourses: 0, totalApplications: 0, pendingApplications: 0 },
professors: { total: 0, active: 0 },
testimonials: { total: 0, active: 0 },
milestones: { total: 0, active: 0 }
};
constructor() { }
loading = true;
error: string | null = null;
recentActivities: any[] = [];
constructor(
private eventService: EventService,
private blogService: BlogService,
private careerService: CareerService,
private educationService: EducationService,
private milestoneService: MilestoneService,
private dashboardService: DashboardService
) { }
ngOnInit(): void {
this.loadDashboardData();
}
}
loadDashboardData(): void {
this.loading = true;
this.error = null;
forkJoin({
events: this.eventService.getEvents(),
blogs: this.blogService.getBlogs(),
jobs: this.careerService.getAllJobs(),
jobApplications: this.careerService.getAllApplications(),
courses: this.educationService.getAllCourses(),
courseApplications: this.educationService.getAllApplications(),
milestones: this.milestoneService.getAllMilestones(),
professors: this.dashboardService.getProfessors(),
testimonials: this.dashboardService.getTestimonials()
}).subscribe({
next: (data) => {
this.calculateStats(data);
this.generateRecentActivities(data);
this.loading = false;
},
error: (error) => {
console.error('Error loading dashboard data:', error);
this.error = 'Failed to load dashboard data. Please try again.';
this.loading = false;
}
});
}
calculateStats(data: any): void {
// Events stats
this.stats.events.total = data.events?.length || 0;
const now = new Date();
this.stats.events.upcoming = data.events?.filter((e: any) =>
new Date(e.eventDate) > now
).length || 0;
this.stats.events.active = data.events?.filter((e: any) => e.isActive).length || 0;
// Blogs stats
this.stats.blogs.total = data.blogs?.length || 0;
this.stats.blogs.published = data.blogs?.filter((b: any) => b.posted).length || 0;
this.stats.blogs.drafts = data.blogs?.filter((b: any) => !b.posted).length || 0;
// Careers stats
this.stats.careers.activeJobs = data.jobs?.filter((j: any) => j.isActive).length || 0;
this.stats.careers.totalApplications = data.jobApplications?.length || 0;
this.stats.careers.pendingApplications = data.jobApplications?.filter(
(a: any) => a.status === 'PENDING' || !a.status
).length || 0;
// Education stats
this.stats.education.activeCourses = data.courses?.filter((c: any) => c.isActive).length || 0;
this.stats.education.totalApplications = data.courseApplications?.length || 0;
this.stats.education.pendingApplications = data.courseApplications?.filter(
(a: any) => a.status === 'PENDING' || !a.status
).length || 0;
// Milestones stats
this.stats.milestones.total = data.milestones?.length || 0;
this.stats.milestones.active = data.milestones?.filter((m: any) => m.isActive).length || 0;
// Professors stats
this.stats.professors.total = data.professors?.length || 0;
this.stats.professors.active = data.professors?.filter(
(p: any) => p.status === 'ACTIVE'
).length || 0;
// Testimonials stats
this.stats.testimonials.total = data.testimonials?.length || 0;
this.stats.testimonials.active = data.testimonials?.filter((t: any) => t.isActive).length || 0;
}
generateRecentActivities(data: any): void {
this.recentActivities = [];
// Add recent job applications
if (data.jobApplications && data.jobApplications.length > 0) {
const recentJobApps = data.jobApplications
.sort((a: any, b: any) => new Date(b.createdDate).getTime() - new Date(a.createdDate).getTime())
.slice(0, 3);
recentJobApps.forEach((app: any) => {
this.recentActivities.push({
type: 'Job Application',
description: `New application from ${app.fullName} for ${app.job?.title || 'Unknown Position'}`,
date: app.createdDate,
icon: 'briefcase',
color: 'primary'
});
});
}
// Add recent course applications
if (data.courseApplications && data.courseApplications.length > 0) {
const recentCourseApps = data.courseApplications
.sort((a: any, b: any) => new Date(b.createdDate).getTime() - new Date(a.createdDate).getTime())
.slice(0, 3);
recentCourseApps.forEach((app: any) => {
this.recentActivities.push({
type: 'Course Application',
description: `New enrollment from ${app.fullName} for ${app.course?.title || 'Unknown Course'}`,
date: app.createdDate,
icon: 'book',
color: 'success'
});
});
}
// Sort all activities by date and limit to 5
this.recentActivities = this.recentActivities
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
.slice(0, 5);
}
getTimeAgo(date: string): string {
const now = new Date();
const past = new Date(date);
const diffInMs = now.getTime() - past.getTime();
const diffInMinutes = Math.floor(diffInMs / 60000);
const diffInHours = Math.floor(diffInMs / 3600000);
const diffInDays = Math.floor(diffInMs / 86400000);
if (diffInMinutes < 60) {
return `${diffInMinutes} minute${diffInMinutes !== 1 ? 's' : ''} ago`;
} else if (diffInHours < 24) {
return `${diffInHours} hour${diffInHours !== 1 ? 's' : ''} ago`;
} else {
return `${diffInDays} day${diffInDays !== 1 ? 's' : ''} ago`;
}
}
refreshDashboard(): void {
this.loadDashboardData();
}
}

View File

@ -1,69 +1,330 @@
body,
html {
margin: 0;
padding: 0;
height: 100%;
background: #60a3bc !important;
}
.user_card {
height: 500px;
width: 600px;
margin-top: auto;
margin-bottom: auto;
background: #afbfd8;
position: relative;
/* Login Layout */
.login-layout {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 10px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
-webkit-box-shadow: 0 4px 8px 0 rgba(0, 0, 0a, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
-moz-box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
border-radius: 5px;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
padding: 20px;
position: relative;
overflow: hidden;
}
}
.brand_logo_container {
.login-layout::before {
content: '';
position: absolute;
height: 170px;
width: 170px;
top: -75px;
top: -50%;
right: -20%;
width: 600px;
height: 600px;
background: radial-gradient(circle, rgba(230, 72, 56, 0.05) 0%, transparent 70%);
border-radius: 50%;
background: #60a3bc;
padding: 10px;
text-align: center;
pointer-events: none;
}
.brand_logo {
height: 150px;
width: 150px;
.login-layout::after {
content: '';
position: absolute;
bottom: -50%;
left: -20%;
width: 500px;
height: 500px;
background: radial-gradient(circle, rgba(1, 32, 104, 0.04) 0%, transparent 70%);
border-radius: 50%;
border: 2px solid white;
pointer-events: none;
}
.form_container {
margin-top: 100px;
}
.login_btn {
/* Login Container */
.login-container {
position: relative;
z-index: 10;
width: 100%;
background: #1B4F72!important;
color: white !important;
max-width: 380px;
animation: fadeInUp 0.6s ease;
}
.login_btn:focus {
box-shadow: none !important;
outline: 0px !important;
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.login_container {
padding: 0 2rem;
/* Login Card */
.login-card {
background: #ffffff;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
overflow: hidden;
border: 1px solid #e5e7eb;
}
.input-group-text {
background: #1B4F72!important;
color: white !important;
border: 0 !important;
border-radius: 0.25rem 0 0 0.25rem !important;
/* Login Header */
.login-header {
text-align: center;
padding: 32px 32px 20px;
background: #ffffff;
border-bottom: 3px solid #e64838;
}
.input_user,
.input_pass:focus {
box-shadow: none !important;
outline: 0px !important;
.logo-wrapper {
margin-bottom: 16px;
}
.custom-checkbox .custom-control-input:checked~.custom-control-label::before {
background-color: #1B4F72!important;
.logo-image {
width: 60px;
height: 60px;
object-fit: contain;
filter: drop-shadow(0 2px 8px rgba(1, 32, 104, 0.15));
}
.login-title {
font-size: 22px;
font-weight: 700;
color: #012068;
margin: 0 0 6px 0;
letter-spacing: -0.5px;
}
.login-subtitle {
font-size: 14px;
color: #6b7280;
margin: 0;
}
/* Login Form */
.login-form {
padding: 24px 32px 28px;
background: #ffffff;
}
/* Form Group */
.form-group {
margin-bottom: 18px;
}
.form-group:last-of-type {
margin-bottom: 24px;
}
.form-group label {
display: block;
font-size: 13px;
font-weight: 600;
color: #012068;
margin-bottom: 6px;
}
/* Input Wrapper */
.input-wrapper {
position: relative;
}
.form-input {
width: 100%;
padding: 11px 14px 11px 38px;
border: 1.5px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
color: #1a1a1a;
transition: all 0.3s ease;
background: #f9fafb;
}
.form-input:focus {
outline: none;
border-color: #012068;
box-shadow: 0 0 0 3px rgba(1, 32, 104, 0.1);
background: #ffffff;
}
.form-input::placeholder {
color: #9ca3af;
}
.input-icon {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: #9ca3af;
font-size: 14px;
pointer-events: none;
}
.form-input:focus + .input-icon {
color: #012068;
}
.input-error {
border-color: #e64838;
}
.input-error:focus {
border-color: #e64838;
box-shadow: 0 0 0 3px rgba(230, 72, 56, 0.1);
}
/* Error Message */
.error-message {
display: flex;
align-items: center;
gap: 6px;
margin-top: 6px;
font-size: 12px;
color: #e64838;
}
.error-message i {
font-size: 12px;
flex-shrink: 0;
}
/* Login Button */
.btn-login {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 20px;
background: linear-gradient(135deg, #e64838 0%, #d63527 100%);
color: #ffffff;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(230, 72, 56, 0.25);
}
.btn-login:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(230, 72, 56, 0.35);
background: linear-gradient(135deg, #d63527 0%, #c62e1f 100%);
}
.btn-login:active:not(:disabled) {
transform: translateY(0);
}
.btn-login:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-login i {
font-size: 16px;
}
/* Login Footer */
.login-footer {
padding: 20px 32px 24px;
text-align: center;
background: #f9fafb;
border-top: 1px solid #e5e7eb;
}
.footer-text {
font-size: 13px;
color: #6b7280;
margin: 0;
}
.footer-link {
color: #e64838;
font-weight: 600;
text-decoration: none;
margin-left: 4px;
transition: color 0.2s ease;
}
.footer-link:hover {
color: #d63527;
text-decoration: underline;
}
/* Spinner Animation */
.fa-spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Responsive Design */
@media (max-width: 576px) {
.login-container {
max-width: 100%;
}
.login-header {
padding: 28px 24px 18px;
}
.logo-image {
width: 55px;
height: 55px;
}
.login-title {
font-size: 20px;
}
.login-subtitle {
font-size: 13px;
}
.login-form,
.login-footer {
padding-left: 24px;
padding-right: 24px;
}
.form-input {
padding: 10px 12px 10px 36px;
}
.btn-login {
padding: 11px 20px;
}
}
@media (max-width: 400px) {
.login-card {
border-radius: 14px;
}
.logo-image {
width: 50px;
height: 50px;
}
.login-title {
font-size: 19px;
}
}
/* Print Styles */
@media print {
.login-layout {
background: #ffffff;
}
.login-layout::before,
.login-layout::after {
display: none;
}
}

View File

@ -1,46 +1,80 @@
<div class="container" style="margin-top: 100px;">
<div class="d-flex justify-content-center h-50">
<div class="user_card">
<div class="d-flex justify-content-center">
<div style="margin-top: 10px;margin-bottom:-90px;">
<h3>User Management Portal</h3>
<div class="login-layout">
<div class="login-container">
<!-- Login Card -->
<div class="login-card">
<!-- Logo/Header Section -->
<div class="login-header">
<div class="logo-wrapper">
<img src="assets/images/cmc/logo.png" alt="Logo" class="logo-image">
</div>
<h1 class="login-title">Welcome Back</h1>
<p class="login-subtitle">Sign in to continue</p>
</div>
<div class="d-flex justify-content-center form_container">
<form #loginForm="ngForm" (ngSubmit)="onLogin(loginForm.value)">
<div class="input-group mb-3">
<div class="input-group-append">
<span class="input-group-text"><i class="fas fa-user"></i></span>
</div>
<input type="text" class="form-control" name="username" placeholder="Username"
ngModel #usernameInput="ngModel" required>
<!-- Login Form -->
<form #loginForm="ngForm" (ngSubmit)="onLogin(loginForm.value)" class="login-form">
<!-- Username Field -->
<div class="form-group">
<label for="username">Username</label>
<div class="input-wrapper">
<input
type="text"
id="username"
class="form-input"
name="username"
placeholder="Enter your username"
ngModel
#usernameInput="ngModel"
required
[class.input-error]="usernameInput.invalid && usernameInput.touched">
<i class="fas fa-user input-icon"></i>
</div>
<span class="help-block" style="color:red;" *ngIf="usernameInput.invalid && usernameInput.touched">
Please enter a username</span>
<div class="input-group mb-2">
<div class="input-group-append">
<span class="input-group-text"><i class="fas fa-key"></i></span>
</div>
<input type="password" class="form-control" name="password" placeholder="Password"
ngModel #passwordInput="ngModel" required>
<div class="error-message" *ngIf="usernameInput.invalid && usernameInput.touched">
<i class="fas fa-exclamation-circle"></i>
Please enter a username
</div>
<span class="help-block" style="color:red;" *ngIf="passwordInput.invalid && passwordInput.touched"
>Please enter a password.</span>
<div class="d-flex justify-content-center mt-3 login_container">
<button type="submit" [disabled]="loginForm.invalid || showLoading" name="button" class="btn login_btn">
<i class="fas fa-spinner fa-spin" *ngIf="showLoading"></i>&nbsp;&nbsp;
<span>{{showLoading ? 'Loading...' : 'Login'}}</span>
</button>
</div>
</form>
</div>
<div class="mt-4">
<div class="d-flex justify-content-center links">
Don't have an account? <a routerLink="/register" class="ml-2" style="color: #2C3E50;">Sign Up</a>
</div>
<!-- Password Field -->
<div class="form-group">
<label for="password">Password</label>
<div class="input-wrapper">
<input
type="password"
id="password"
class="form-input"
name="password"
placeholder="Enter your password"
ngModel
#passwordInput="ngModel"
required
[class.input-error]="passwordInput.invalid && passwordInput.touched">
<i class="fas fa-lock input-icon"></i>
</div>
<div class="error-message" *ngIf="passwordInput.invalid && passwordInput.touched">
<i class="fas fa-exclamation-circle"></i>
Please enter a password
</div>
</div>
<!-- Login Button -->
<button
type="submit"
class="btn-login"
[disabled]="loginForm.invalid || showLoading">
<i class="fas fa-spinner fa-spin" *ngIf="showLoading"></i>
<i class="fas fa-sign-in-alt" *ngIf="!showLoading"></i>
<span>{{ showLoading ? 'Signing in...' : 'Sign In' }}</span>
</button>
</form>
<!-- Register Link -->
<div class="login-footer">
<p class="footer-text">
Don't have an account?
<a routerLink="/register" class="footer-link">Sign Up</a>
</p>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,297 @@
/* Management Layout */
.management-layout {
display: flex;
min-height: 100vh;
background: #f8f9fa;
}
.sidebar {
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: 260px;
z-index: 1000;
}
.management-content {
margin-left: 260px;
flex: 1;
width: calc(100% - 260px);
display: flex;
flex-direction: column;
}
/* Header Section */
.management-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32px 32px 24px 32px;
background: white;
border-bottom: 1px solid #e5e7eb;
}
.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-right {
display: flex;
align-items: center;
}
.user-welcome {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.welcome-text {
font-size: 13px;
color: #6b7280;
}
.user-name {
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
}
.user-avatar-small {
width: 32px;
height: 32px;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 14px;
}
/* Navigation Tabs */
.management-nav {
background: white;
border-bottom: 1px solid #e5e7eb;
padding: 0 32px;
}
.nav-tabs {
display: flex;
gap: 4px;
}
.nav-tab {
display: flex;
align-items: center;
gap: 8px;
padding: 16px 24px;
font-size: 14px;
font-weight: 500;
color: #6b7280;
text-decoration: none;
border-bottom: 2px solid transparent;
transition: all 0.2s ease;
cursor: pointer;
position: relative;
}
.nav-tab:hover {
color: #3b82f6;
background: #f9fafb;
}
.nav-tab.active {
color: #3b82f6;
border-bottom-color: #3b82f6;
background: linear-gradient(to bottom, rgba(59, 130, 246, 0.05) 0%, transparent 100%);
}
.nav-tab i {
font-size: 16px;
}
/* Management Body */
.management-body {
flex: 1;
padding: 32px;
overflow-y: auto;
}
/* Responsive Design */
@media (max-width: 992px) {
.management-content {
margin-left: 0;
width: 100%;
}
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0);
}
.management-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
padding: 24px;
}
.header-right {
width: 100%;
justify-content: flex-start;
}
.management-nav {
padding: 0 24px;
}
.management-body {
padding: 24px;
}
}
@media (max-width: 768px) {
.management-header {
padding: 20px;
}
.page-title {
font-size: 24px;
}
.management-nav {
padding: 0 20px;
overflow-x: auto;
}
.nav-tabs {
min-width: max-content;
}
.nav-tab {
padding: 14px 20px;
white-space: nowrap;
}
.management-body {
padding: 20px;
}
.user-welcome {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}
@media (max-width: 576px) {
.management-header {
padding: 16px;
}
.page-title {
font-size: 20px;
}
.management-nav {
padding: 0 16px;
}
.nav-tab {
padding: 12px 16px;
font-size: 13px;
}
.nav-tab i {
font-size: 14px;
}
.nav-tab span {
display: none;
}
.management-body {
padding: 16px;
}
.user-welcome {
padding: 6px 12px;
}
.welcome-text {
font-size: 12px;
}
.user-name {
font-size: 13px;
}
.user-avatar-small {
width: 28px;
height: 28px;
font-size: 12px;
}
}
/* Print Styles */
@media print {
.sidebar,
.management-nav,
.header-right {
display: none;
}
.management-content {
margin-left: 0;
width: 100%;
}
.management-header {
border-bottom: none;
padding: 0;
}
.management-body {
padding: 0;
}
}
/* Animation */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.management-body > * {
animation: fadeIn 0.3s ease;
}

View File

@ -1,54 +1,59 @@
<div class="container">
<div class="row mb-2 mt-2 text-center">
<div class="col-md-4">
</div>
<div class="col-md-4">
<h5>User Management Portal</h5>
<small *ngIf="titleAction$ | async as title">{{title}}</small>
</div>
<div class="col-md-4">
</div>
</div>
<div class="management-layout">
<!-- Sidebar -->
<app-menu class="sidebar"></app-menu>
<!-- nav bar -->
<nav class="navbar navbar-expand-md breadcrumb">
<div class="collapse navbar-collapse" id="navbarCollapse">
<div class="nav nav-pills">
<a
class="nav-item nav-link ml-1"
routerLinkActive="active"
(click)="changeTitle('Users')"
data-bs-toggle="tab"
[routerLink]="['users']">
<!-- Main Content -->
<div class="management-content">
<!-- Header Section -->
<div class="management-header">
<div class="header-left">
<h1 class="page-title">User Management Portal</h1>
<p class="page-subtitle" *ngIf="titleAction$ | async as title">{{ title }}</p>
</div>
<div class="header-right">
<div class="user-welcome">
<span class="welcome-text">Welcome,</span>
<span class="user-name">{{ loggedInUser.firstName }} {{ loggedInUser.lastName }}</span>
<div class="user-avatar-small">
<i class="fa fa-user"></i>
</div>
</div>
</div>
</div>
<!-- Navigation Tabs -->
<nav class="management-nav">
<div class="nav-tabs">
<a class="nav-tab"
routerLinkActive="active"
(click)="changeTitle('Users')"
[routerLink]="['users']">
<i class="fa fa-users"></i>
Users
<span>Users</span>
</a>
<a *ngIf="isAdmin"
class="nav-item nav-link ml-3"
class="nav-tab"
routerLinkActive="active"
(click)="changeTitle('Settings')"
data-bs-toggle="tab"
[routerLink]="['settings']">
<i class="fa fa-cogs"></i>
Settings
<span>Settings</span>
</a>
<a
class="nav-item nav-link move-right mr-3"
routerLinkActive="active"
(click)="changeTitle('Profile')"
data-bs-toggle="tab"
[routerLink]="['profile']">
Welcome, {{loggedInUser.firstName}} {{loggedInUser.lastName}}
<a class="nav-tab"
routerLinkActive="active"
(click)="changeTitle('Profile')"
[routerLink]="['profile']">
<i class="fa fa-user"></i>
<span>Profile</span>
</a>
</div>
</nav>
<!-- Router Content -->
<div class="management-body">
<router-outlet></router-outlet>
</div>
</nav>
<!-- main content -->
<div class="tab-content mt-3" id="myTabContent">
<router-outlet></router-outlet>
</div>
</div>
</div>

View File

@ -0,0 +1,661 @@
/* Profile Layout */
.profile-layout {
display: flex;
min-height: 100vh;
background: #f8f9fa;
}
.sidebar {
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: 260px;
z-index: 1000;
}
.profile-content {
margin-left: 260px;
flex: 1;
padding: 32px;
width: calc(100% - 260px);
max-width: 1400px;
}
/* Header Section */
.profile-header {
margin-bottom: 32px;
}
.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;
}
/* Profile Container */
.profile-container {
display: grid;
grid-template-columns: 1fr 350px;
gap: 24px;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Profile Main Card */
.profile-main {
min-width: 0;
}
.profile-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
}
/* Profile Card Header */
.profile-card-header {
display: flex;
gap: 24px;
padding: 32px;
background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%);
border-bottom: 1px solid #e5e7eb;
}
.profile-avatar-section {
flex-shrink: 0;
}
.profile-avatar-wrapper {
position: relative;
width: 120px;
height: 120px;
}
.profile-avatar {
width: 120px;
height: 120px;
border-radius: 12px;
object-fit: cover;
border: 3px solid white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.avatar-edit-btn {
position: absolute;
bottom: 4px;
right: 4px;
width: 36px;
height: 36px;
border-radius: 8px;
background: #3b82f6;
color: white;
border: 2px solid white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.avatar-edit-btn:hover {
background: #2563eb;
transform: scale(1.05);
}
.upload-progress {
margin-top: 12px;
height: 6px;
background: #e5e7eb;
border-radius: 3px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #3b82f6 0%, #2563eb 100%);
transition: width 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.progress-text {
font-size: 10px;
color: white;
font-weight: 600;
}
/* Profile Info */
.profile-info {
flex: 1;
min-width: 0;
}
.profile-name {
font-size: 24px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 4px 0;
}
.profile-username {
font-size: 14px;
color: #6b7280;
margin: 0 0 16px 0;
}
.profile-meta {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.meta-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #6b7280;
}
.meta-item i {
color: #9ca3af;
font-size: 14px;
}
/* Profile Card Body */
.profile-card-body {
padding: 32px;
}
/* Profile Form */
.profile-form {
display: flex;
flex-direction: column;
gap: 32px;
}
/* Form Section */
.form-section {
display: flex;
flex-direction: column;
gap: 20px;
}
.section-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
margin: 0;
padding-bottom: 12px;
border-bottom: 2px solid #f3f4f6;
}
.section-title i {
color: #3b82f6;
font-size: 18px;
}
/* Form Row */
.form-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
/* Form Group */
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
}
.form-group label i {
color: #6b7280;
font-size: 14px;
}
.readonly-badge {
margin-left: 8px;
padding: 2px 8px;
background: #fef3c7;
color: #92400e;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.form-input,
.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-select:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-input:disabled,
.form-select:disabled {
background: #f3f4f6;
color: #9ca3af;
cursor: not-allowed;
}
.form-input::placeholder {
color: #9ca3af;
}
/* Checkbox Group */
.checkbox-group {
display: flex;
flex-direction: column;
gap: 16px;
}
.checkbox-wrapper {
position: relative;
}
.checkbox-input {
position: absolute;
opacity: 0;
cursor: pointer;
}
.checkbox-label {
display: flex;
align-items: start;
gap: 12px;
cursor: pointer;
padding: 16px;
border: 1px solid #e5e7eb;
border-radius: 8px;
transition: all 0.2s ease;
background: white;
}
.checkbox-label:hover {
border-color: #d1d5db;
background: #f9fafb;
}
.checkbox-input:checked + .checkbox-label {
border-color: #3b82f6;
background: #f0f9ff;
}
.checkbox-input:disabled + .checkbox-label {
opacity: 0.5;
cursor: not-allowed;
}
.checkbox-custom {
position: relative;
width: 20px;
height: 20px;
border: 2px solid #d1d5db;
border-radius: 4px;
flex-shrink: 0;
transition: all 0.2s ease;
background: white;
}
.checkbox-input:checked + .checkbox-label .checkbox-custom {
background: #3b82f6;
border-color: #3b82f6;
}
.checkbox-input:checked + .checkbox-label .checkbox-custom::after {
content: '';
position: absolute;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.checkbox-text {
display: flex;
flex-direction: column;
gap: 2px;
}
.checkbox-text strong {
font-size: 14px;
color: #1a1a1a;
font-weight: 600;
}
.checkbox-text small {
font-size: 12px;
color: #6b7280;
}
/* Form Actions */
.form-actions {
display: flex;
justify-content: flex-end;
padding-top: 8px;
}
.btn-primary {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 32px;
background: #3b82f6;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary:hover:not(:disabled) {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.btn-primary:disabled {
background: #9ca3af;
cursor: not-allowed;
opacity: 0.6;
transform: none;
}
/* Profile Sidebar */
.profile-sidebar {
display: flex;
flex-direction: column;
gap: 20px;
}
.sidebar-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
}
/* Logout Card */
.logout-card {
text-align: center;
background: linear-gradient(135deg, #fef2f2 0%, #ffffff 100%);
border-color: #fecaca;
}
.logout-card .card-icon {
width: 56px;
height: 56px;
background: #dc2626;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
margin: 0 auto 16px;
}
.logout-card h4 {
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 8px 0;
}
.logout-card p {
font-size: 13px;
color: #6b7280;
margin: 0 0 20px 0;
}
.btn-logout {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 24px;
background: #dc2626;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-logout:hover {
background: #b91c1c;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3);
}
/* Permissions Card */
.permissions-card {
background: linear-gradient(135deg, #f0f9ff 0%, #ffffff 100%);
border-color: #bae6fd;
}
.card-header-small {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #e0f2fe;
}
.card-header-small i {
color: #3b82f6;
font-size: 18px;
}
.card-header-small h4 {
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
margin: 0;
}
.permissions-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.permission-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: white;
border: 1px solid #dbeafe;
border-radius: 6px;
font-size: 13px;
color: #374151;
}
.permission-item i {
color: #22c55e;
font-size: 14px;
flex-shrink: 0;
}
/* Responsive Design */
@media (max-width: 992px) {
.profile-content {
margin-left: 0;
width: 100%;
padding: 24px;
}
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0);
}
.profile-container {
grid-template-columns: 1fr;
}
.profile-sidebar {
order: -1;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
}
@media (max-width: 768px) {
.profile-content {
padding: 20px;
}
.page-title {
font-size: 24px;
}
.profile-card-header {
flex-direction: column;
align-items: center;
text-align: center;
padding: 24px;
}
.profile-avatar-wrapper {
margin: 0 auto;
}
.profile-meta {
justify-content: center;
}
.profile-card-body {
padding: 24px;
}
.form-row {
grid-template-columns: 1fr;
}
.profile-sidebar {
grid-template-columns: 1fr;
}
}
@media (max-width: 576px) {
.profile-content {
padding: 16px;
}
.page-title {
font-size: 20px;
}
.profile-card-header {
padding: 20px;
}
.profile-avatar,
.profile-avatar-wrapper {
width: 100px;
height: 100px;
}
.profile-name {
font-size: 20px;
}
.profile-card-body {
padding: 20px;
}
.form-actions {
justify-content: stretch;
}
.btn-primary {
width: 100%;
justify-content: center;
}
}
/* Print Styles */
@media print {
.sidebar,
.profile-sidebar,
.avatar-edit-btn,
.form-actions {
display: none;
}
.profile-content {
margin-left: 0;
width: 100%;
}
.profile-container {
grid-template-columns: 1fr;
}
}

View File

@ -1,162 +1,241 @@
<!-- user profile -->
<div class="profile-layout">
<!-- Sidebar -->
<app-menu class="sidebar"></app-menu>
<app-menu></app-menu>
<!-- Main Content -->
<div class="profile-content">
<!-- Header Section -->
<div class="profile-header">
<div class="header-left">
<h1 class="page-title">My Profile</h1>
<p class="page-subtitle">Manage your account settings and preferences</p>
</div>
</div>
<div class="tab-pane fade show active" id="profile">
<div class="container">
<div class="row flex-lg-nowrap">
<div class="col">
<div class="row">
<div class="col mb-3">
<div class="card">
<div class="card-body">
<div class="e-profile">
<div class="row">
<div class="col-12 col-sm-auto">
<div class="mx-auto" style="width: 120px;">
<div class="d-flex justify-content-center align-items-center rounded">
<img class="rounded" height="135" width="135" src="{{loggedInUser?.profileImageUrl}}"
alt="">
</div>
<div *ngIf="fileUploadStatus?.status==='progress'" class="progress mt-1">
<div class="progress-bar bg-info" role="progressbar"
[style.width.%]="fileUploadStatus?.percentage" aria-valuenow="0" aria-valuemin="0"
aria-valuemax="100">{{fileUploadStatus?.percentage}}%
</div>
</div>
</div>
</div>
<div class="col d-flex flex-column flex-sm-row justify-content-between mb-3">
<div class="text-center text-sm-left mb-2 mb-sm-0">
<h4
class="pt-sm-2 pb-1 mb-0 text-nowrap">{{loggedInUser?.firstName}} {{loggedInUser?.lastName}}</h4>
<p class="mb-0">{{loggedInUser?.username}}</p>
<div *ngIf="loggedInUser?.lastLoginDateDisplay !== null" class="text-muted"><small>Last
login:
{{loggedInUser?.lastLoginDateDisplay | date:'medium'}}</small></div>
<div class="mt-2">
<button (click)="updateProfileImage()" class="btn btn-primary" type="button">
<i class="fa fa-fw fa-camera"></i>
<span>Change Photo</span>
</button>
</div>
</div>
<div class="text-center text-sm-right">
<div class="text-muted"><small>Joined {{loggedInUser?.joinDate | date:'mediumDate'}}</small>
</div>
</div>
</div>
</div>
<div class="tab-content pt-3">
<div class="tab-pane active">
<form #profileUserForm="ngForm" (ngSubmit)="onUpdateCurrentUser(profileUserForm.value)"
class="form" novalidate>
<div class="row">
<div class="col">
<div class="row">
<div class="col">
<div class="form-group">
<label>First Name</label>
<input type="text" name="firstName" required [(ngModel)]="loggedInUser.firstName"
class="form-control">
</div>
</div>
<div class="col">
<div class="form-group">
<label>Last Name</label>
<input type="text" name="lastName" required [(ngModel)]="loggedInUser.lastName"
class="form-control">
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="form-group">
<label>Username</label>
<input type="text" name="username" required [(ngModel)]="loggedInUser.username"
class="form-control">
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="form-group">
<label>Email</label>
<input type="text" name="email" required [(ngModel)]="loggedInUser.email"
class="form-control">
</div>
</div>
</div>
<div class="row">
<div class="col mb-3">
<div class="form-group">
<label>Role</label><small [hidden]="isAdmin">(read only)</small>
<select [disabled]="!isAdmin" name="role" required [(ngModel)]="loggedInUser.role"
class="form-control">
<option value="ROLE_USER">USER</option>
<option value="ROLE_HR">HR</option>
<option value="ROLE_MANAGER">MANAGER</option>
<option value="ROLE_ADMIN">ADMIN</option>
<option value="ROLE_SUPER_ADMIN">SUPER ADMIN</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12 col-sm-5 offset-sm-1 mb-3">
<div class="mb-2"><b>Account Settings</b></div>
<div class="row">
<div class="col">
<div class="custom-controls-stacked px-2">
<div class="custom-control custom-checkbox">
<input [disabled]="!isAdmin" name="active" type="checkbox"
[(ngModel)]="loggedInUser.active"
class="custom-control-input">
<label class="custom-control-label">Active</label>
</div>
<div class="custom-control custom-checkbox">
<input [disabled]="!isAdmin" name="notLocked" type="checkbox"
[(ngModel)]="loggedInUser.notLocked" class="custom-control-input">
<label class="custom-control-label">Unlocked</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col d-flex justify-content-end">
<button class="btn btn-primary" type="submit">
<i *ngIf="refreshing" class="fas fa-spinner fa-spin"></i>&nbsp;&nbsp;
<span>{{refreshing ? 'Loading...' : 'Save Changes'}}</span>
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Profile Container -->
<div class="profile-container">
<!-- Main Profile Card -->
<div class="profile-main">
<div class="profile-card">
<!-- Profile Header -->
<div class="profile-card-header">
<div class="profile-avatar-section">
<div class="profile-avatar-wrapper">
<img class="profile-avatar" [src]="loggedInUser?.profileImageUrl" [alt]="loggedInUser?.firstName">
<button class="avatar-edit-btn" (click)="updateProfileImage()" title="Change Photo">
<i class="fa fa-camera"></i>
</button>
</div>
<div *ngIf="fileUploadStatus?.status==='progress'" class="upload-progress">
<div class="progress-bar" [style.width.%]="fileUploadStatus?.percentage">
<span class="progress-text">{{ fileUploadStatus?.percentage }}%</span>
</div>
</div>
</div>
<div class="profile-info">
<h2 class="profile-name">{{ loggedInUser?.firstName }} {{ loggedInUser?.lastName }}</h2>
<p class="profile-username">@{{ loggedInUser?.username }}</p>
<div class="profile-meta">
<div class="meta-item" *ngIf="loggedInUser?.lastLoginDateDisplay !== null">
<i class="fa fa-clock"></i>
<span>Last login: {{ loggedInUser?.lastLoginDateDisplay | date:'medium' }}</span>
</div>
<div class="meta-item">
<i class="fa fa-calendar"></i>
<span>Joined {{ loggedInUser?.joinDate | date:'mediumDate' }}</span>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-3 mb-3">
<div class="card mb-3">
<div class="card-body">
<div class="px-xl-3">
<button (click)="onLogOut()" class="btn btn-block btn-secondary">
<span>Logout</span>
<i class="fas fa-sign-in-alt ml-1"></i>
</button>
<!-- Profile Form -->
<div class="profile-card-body">
<form #profileUserForm="ngForm" (ngSubmit)="onUpdateCurrentUser(profileUserForm.value)" class="profile-form">
<!-- Personal Information Section -->
<div class="form-section">
<h3 class="section-title">
<i class="fa fa-user"></i>
Personal Information
</h3>
<div class="form-row">
<div class="form-group">
<label for="firstName">
<i class="fa fa-id-card"></i>
First Name
</label>
<input
type="text"
id="firstName"
name="firstName"
required
[(ngModel)]="loggedInUser.firstName"
class="form-input"
placeholder="Enter first name">
</div>
<div class="form-group">
<label for="lastName">
<i class="fa fa-id-card"></i>
Last Name
</label>
<input
type="text"
id="lastName"
name="lastName"
required
[(ngModel)]="loggedInUser.lastName"
class="form-input"
placeholder="Enter last name">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="username">
<i class="fa fa-at"></i>
Username
</label>
<input
type="text"
id="username"
name="username"
required
[(ngModel)]="loggedInUser.username"
class="form-input"
placeholder="Enter username">
</div>
<div class="form-group">
<label for="email">
<i class="fa fa-envelope"></i>
Email Address
</label>
<input
type="email"
id="email"
name="email"
required
[(ngModel)]="loggedInUser.email"
class="form-input"
placeholder="Enter email address">
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<h6 class="card-title font-weight-bold">Permissions From Role</h6>
<h6 *ngFor="let authority of loggedInUser?.authorities" class="card-text">{{authority}}</h6>
<!-- Role & Permissions Section -->
<div class="form-section">
<h3 class="section-title">
<i class="fa fa-shield-alt"></i>
Role & Permissions
</h3>
<div class="form-group">
<label for="role">
<i class="fa fa-user-tag"></i>
User Role
<small *ngIf="!isAdmin" class="readonly-badge">(read only)</small>
</label>
<select
id="role"
[disabled]="!isAdmin"
name="role"
required
[(ngModel)]="loggedInUser.role"
class="form-select">
<option value="ROLE_USER">USER</option>
<option value="ROLE_HR">HR</option>
<option value="ROLE_MANAGER">MANAGER</option>
<option value="ROLE_ADMIN">ADMIN</option>
<option value="ROLE_SUPER_ADMIN">SUPER ADMIN</option>
</select>
</div>
</div>
<!-- Account Settings Section -->
<div class="form-section">
<h3 class="section-title">
<i class="fa fa-cog"></i>
Account Settings
</h3>
<div class="checkbox-group">
<div class="checkbox-wrapper">
<input
[disabled]="!isAdmin"
name="active"
type="checkbox"
id="active"
[(ngModel)]="loggedInUser.active"
class="checkbox-input">
<label for="active" class="checkbox-label">
<span class="checkbox-custom"></span>
<span class="checkbox-text">
<strong>Active Account</strong>
<small>User can access the system</small>
</span>
</label>
</div>
<div class="checkbox-wrapper">
<input
[disabled]="!isAdmin"
name="notLocked"
type="checkbox"
id="notLocked"
[(ngModel)]="loggedInUser.notLocked"
class="checkbox-input">
<label for="notLocked" class="checkbox-label">
<span class="checkbox-custom"></span>
<span class="checkbox-text">
<strong>Unlocked Account</strong>
<small>User is not locked out</small>
</span>
</label>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="form-actions">
<button class="btn-primary" type="submit" [disabled]="refreshing">
<i *ngIf="refreshing" class="fas fa-spinner fa-spin"></i>
<i *ngIf="!refreshing" class="fa fa-save"></i>
<span>{{ refreshing ? 'Saving...' : 'Save Changes' }}</span>
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="profile-sidebar">
<!-- Logout Card -->
<div class="sidebar-card logout-card">
<div class="card-icon">
<i class="fa fa-sign-out-alt"></i>
</div>
<h4>Session Management</h4>
<p>Sign out of your account securely</p>
<button (click)="onLogOut()" class="btn-logout">
<i class="fas fa-sign-out-alt"></i>
<span>Logout</span>
</button>
</div>
<!-- Permissions Card -->
<div class="sidebar-card permissions-card">
<div class="card-header-small">
<i class="fa fa-shield-alt"></i>
<h4>Permissions</h4>
</div>
<div class="permissions-list">
<div *ngFor="let authority of loggedInUser?.authorities" class="permission-item">
<i class="fa fa-check-circle"></i>
<span>{{ authority }}</span>
</div>
</div>
</div>
@ -165,9 +244,12 @@
</div>
</div>
<!-- profile image change form -->
<!-- Profile Image Change Form (Hidden) -->
<form enctype="multipart/form-data" style="display:none;">
<input type="file"
(change)="onProfileImageChange($any($event).target.files); onUpdateProfileImage()"
name="profile-image-input" id="profile-image-input" placeholder="file" accept="image/*"/>
</form>
<input
type="file"
(change)="onProfileImageChange($any($event).target.files); onUpdateProfileImage()"
name="profile-image-input"
id="profile-image-input"
accept="image/*"/>
</form>

View File

@ -0,0 +1,430 @@
/* Settings Layout */
.settings-layout {
display: flex;
min-height: 100vh;
background: #f8f9fa;
}
.sidebar {
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: 260px;
z-index: 1000;
}
.settings-content {
margin-left: 260px;
flex: 1;
padding: 32px;
width: calc(100% - 260px);
max-width: 1200px;
}
/* Header Section */
.settings-header {
margin-bottom: 32px;
}
.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;
}
/* Settings Container */
.settings-container {
display: grid;
grid-template-columns: 1fr 400px;
gap: 24px;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Settings Card */
.settings-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
}
.card-header {
display: flex;
gap: 16px;
padding: 24px;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: white;
}
.header-icon {
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.header-text {
flex: 1;
}
.header-text h3 {
margin: 0 0 4px 0;
font-size: 18px;
font-weight: 600;
}
.header-text p {
margin: 0;
font-size: 13px;
opacity: 0.9;
}
.card-body {
padding: 32px;
}
/* Form Group */
.form-group {
margin-bottom: 24px;
}
.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 {
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 {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-input:disabled {
background: #f3f4f6;
color: #9ca3af;
cursor: not-allowed;
}
.form-input::placeholder {
color: #9ca3af;
}
.form-hint {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #6b7280;
margin-top: 8px;
}
.form-hint i {
font-size: 12px;
color: #3b82f6;
}
/* Form Actions */
.form-actions {
display: flex;
gap: 12px;
padding-top: 8px;
}
.btn-primary,
.btn-secondary {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.btn-primary {
background: #3b82f6;
color: white;
flex: 1;
}
.btn-primary:hover:not(:disabled) {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.btn-primary:disabled {
background: #9ca3af;
cursor: not-allowed;
opacity: 0.6;
transform: none;
}
.btn-secondary {
background: white;
color: #6b7280;
border: 1px solid #e5e7eb;
}
.btn-secondary:hover:not(:disabled) {
background: #f9fafb;
border-color: #d1d5db;
}
.btn-secondary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Info Card */
.info-card {
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border: 1px solid #bae6fd;
border-radius: 12px;
padding: 24px;
height: fit-content;
}
.info-icon {
width: 48px;
height: 48px;
background: #3b82f6;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: white;
margin-bottom: 16px;
}
.info-content h4 {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
}
.info-content p {
margin: 0 0 16px 0;
font-size: 14px;
color: #6b7280;
line-height: 1.6;
}
.info-list {
list-style: none;
padding: 0;
margin: 0;
}
.info-list li {
display: flex;
align-items: start;
gap: 10px;
margin-bottom: 12px;
font-size: 13px;
color: #374151;
}
.info-list li:last-child {
margin-bottom: 0;
}
.info-list li i {
color: #22c55e;
font-size: 14px;
margin-top: 2px;
flex-shrink: 0;
}
.info-list li span {
flex: 1;
line-height: 1.5;
}
/* No Access Container */
.no-access-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.no-access-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 48px;
text-align: center;
max-width: 500px;
}
.no-access-icon {
width: 80px;
height: 80px;
margin: 0 auto 24px;
background: #fee2e2;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.no-access-icon i {
font-size: 36px;
color: #dc2626;
}
.no-access-card h3 {
font-size: 20px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 8px 0;
}
.no-access-card p {
font-size: 14px;
color: #6b7280;
margin: 0;
line-height: 1.6;
}
/* Responsive Design */
@media (max-width: 992px) {
.settings-content {
margin-left: 0;
width: 100%;
padding: 24px;
}
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0);
}
.settings-container {
grid-template-columns: 1fr;
}
.info-card {
order: -1;
}
}
@media (max-width: 768px) {
.settings-content {
padding: 20px;
}
.page-title {
font-size: 24px;
}
.card-body {
padding: 24px;
}
.card-header {
padding: 20px;
}
.form-actions {
flex-direction: column;
}
.form-actions button {
width: 100%;
justify-content: center;
}
}
@media (max-width: 576px) {
.settings-content {
padding: 16px;
}
.card-header {
flex-direction: column;
text-align: center;
}
.header-icon {
margin: 0 auto;
}
.no-access-card {
padding: 32px 24px;
}
}
/* Print Styles */
@media print {
.sidebar,
.form-actions {
display: none;
}
.settings-content {
margin-left: 0;
width: 100%;
}
}

View File

@ -1,27 +1,109 @@
<!-- change password -->
<div class="settings-layout">
<!-- Sidebar -->
<app-menu class="sidebar"></app-menu>
<app-menu></app-menu>
<div class="container">
<div *ngIf="isAdmin" class="tab-pane fade show active" id="reset-password">
<form #resetPasswordForm="ngForm" (ngSubmit)="onResetPassword(resetPasswordForm)">
<fieldset>
<legend>User Password Management</legend>
<div class="form-group">
<label for="email">Email address</label>
<input type="email" name="reset-password-email" required ngModel class="form-control"
id="email"
placeholder="Enter email (example@email.com)">
<small class="form-text text-muted">We'll never share your email with anyone else.</small>
<!-- Main Content -->
<div class="settings-content">
<!-- Header Section -->
<div class="settings-header">
<div class="header-left">
<h1 class="page-title">Settings</h1>
<p class="page-subtitle">Manage system settings and user password management</p>
</div>
<button type="submit" [disabled]="resetPasswordForm.invalid" class="btn btn-primary">
<i *ngIf="refreshing" class="fas fa-spinner fa-spin"></i>&nbsp;&nbsp;
<span>{{refreshing ? 'Loading...' : 'Reset Password'}}</span>
</button>
</fieldset>
</form>
</div>
</div>
</div>
<!-- Password Reset Section -->
<div *ngIf="isAdmin" class="settings-container">
<div class="settings-card">
<div class="card-header">
<div class="header-icon">
<i class="fa fa-lock"></i>
</div>
<div class="header-text">
<h3>User Password Management</h3>
<p>Reset password for any user in the system</p>
</div>
</div>
<div class="card-body">
<form #resetPasswordForm="ngForm" (ngSubmit)="onResetPassword(resetPasswordForm)">
<div class="form-group">
<label for="email">
<i class="fa fa-envelope"></i>
User Email Address
</label>
<input
type="email"
name="reset-password-email"
required
ngModel
class="form-input"
id="email"
placeholder="Enter user email (e.g., user@example.com)"
[disabled]="refreshing">
<small class="form-hint">
<i class="fa fa-info-circle"></i>
A password reset link will be sent to this email address
</small>
</div>
<div class="form-actions">
<button
type="submit"
[disabled]="resetPasswordForm.invalid || refreshing"
class="btn-primary">
<i *ngIf="refreshing" class="fas fa-spinner fa-spin"></i>
<i *ngIf="!refreshing" class="fa fa-key"></i>
<span>{{ refreshing ? 'Sending Reset Link...' : 'Reset Password' }}</span>
</button>
<button
type="button"
class="btn-secondary"
(click)="resetPasswordForm.reset()"
[disabled]="refreshing">
<i class="fa fa-times"></i>
<span>Clear</span>
</button>
</div>
</form>
</div>
</div>
<!-- Info Card -->
<div class="info-card">
<div class="info-icon">
<i class="fa fa-shield-alt"></i>
</div>
<div class="info-content">
<h4>Security Notice</h4>
<p>When you reset a user's password, they will receive an email with instructions to create a new password. The reset link will expire after 24 hours for security purposes.</p>
<ul class="info-list">
<li>
<i class="fa fa-check-circle"></i>
<span>User will receive an email with reset instructions</span>
</li>
<li>
<i class="fa fa-check-circle"></i>
<span>Reset link expires in 24 hours</span>
</li>
<li>
<i class="fa fa-check-circle"></i>
<span>Old password remains valid until new one is set</span>
</li>
</ul>
</div>
</div>
</div>
<!-- No Admin Access -->
<div *ngIf="!isAdmin" class="no-access-container">
<div class="no-access-card">
<div class="no-access-icon">
<i class="fa fa-lock"></i>
</div>
<h3>Access Restricted</h3>
<p>You don't have permission to access settings. Administrator privileges are required.</p>
</div>
</div>
</div>
</div>

View File

@ -1,19 +1,419 @@
.navbar {
background-color: #f8f9fa;
padding: 0.5rem 1rem;
/* ===================================
Modern Minimal Sidebar Styles
=================================== */
/* Sidebar Container */
.sidebar {
width: 250px;
height: 100vh;
background: #ffffff;
border-right: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
position: fixed;
left: 0;
top: 0;
overflow-y: auto;
overflow-x: hidden;
z-index: 1000;
transition: all 0.3s ease;
}
/* Custom Scrollbar */
.sidebar::-webkit-scrollbar {
width: 4px;
}
.sidebar::-webkit-scrollbar-track {
background: transparent;
}
.sidebar::-webkit-scrollbar-thumb {
background: #d1d5db;
border-radius: 4px;
}
.sidebar::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
/* ===================================
Sidebar Header / Brand
=================================== */
.sidebar-header {
padding: 24px 20px;
border-bottom: 1px solid #f3f4f6;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
}
.brand-icon {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
flex-shrink: 0;
}
.brand-title {
font-size: 18px;
font-weight: 700;
color: #111827;
margin: 0;
letter-spacing: -0.5px;
}
/* ===================================
User Profile Section
=================================== */
.user-profile {
padding: 20px;
display: flex;
align-items: center;
gap: 12px;
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
.user-avatar {
width: 44px;
height: 44px;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
flex-shrink: 0;
}
.user-info {
flex: 1;
min-width: 0;
}
.user-name {
font-size: 14px;
font-weight: 600;
color: #111827;
margin: 0 0 2px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-role {
font-size: 12px;
color: #6b7280;
font-weight: 400;
}
/* ===================================
Navigation Menu
=================================== */
.sidebar-nav {
flex: 1;
padding: 12px 0;
overflow-y: auto;
}
.nav-list {
list-style: none;
padding: 0;
margin: 0;
}
/* Navigation Items */
.nav-item {
margin: 0;
padding: 0 12px;
}
.nav-link {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
color: #6b7280;
text-decoration: none;
border-radius: 8px;
transition: all 0.2s ease;
font-size: 14px;
font-weight: 500;
position: relative;
margin-bottom: 2px;
}
.nav-link:hover {
background: #f3f4f6;
color: #3b82f6;
text-decoration: none;
}
/* Active State */
.nav-link.active {
background: #eff6ff;
color: #3b82f6;
font-weight: 600;
}
.nav-link.active::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 24px;
background: #3b82f6;
border-radius: 0 4px 4px 0;
}
/* Navigation Icon */
.nav-icon {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
.nav-text {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ===================================
Navigation Dividers
=================================== */
.nav-divider {
padding: 16px 28px 8px 28px;
margin-top: 8px;
}
.nav-divider span {
font-size: 11px;
font-weight: 700;
color: #9ca3af;
text-transform: uppercase;
letter-spacing: 0.8px;
}
/* ===================================
Sidebar Footer / Logout
=================================== */
.sidebar-footer {
padding: 16px 20px;
border-top: 1px solid #e5e7eb;
margin-top: auto;
}
.logout-btn {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 12px 16px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
color: #6b7280;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.logout-btn:hover {
background: #fef2f2;
border-color: #fecaca;
color: #dc2626;
}
.logout-btn i {
font-size: 16px;
}
/* ===================================
Responsive Design
=================================== */
@media (max-width: 992px) {
.sidebar {
transform: translateX(-100%);
}
.nav-pills .nav-link {
border-radius: 0;
margin-right: 0.5rem;
.sidebar.open {
transform: translateX(0);
box-shadow: 4px 0 12px rgba(0, 0, 0, 0.1);
}
.nav-pills .nav-link.active {
background-color: #007bff;
color: white;
}
@media (max-width: 768px) {
.sidebar {
width: 280px;
}
.move-right {
margin-left: auto;
.brand-title {
font-size: 16px;
}
.user-name {
font-size: 13px;
}
.nav-link {
font-size: 13px;
}
}
/* ===================================
Print Styles
=================================== */
@media print {
.sidebar {
display: none;
}
}
/* ===================================
Accessibility
=================================== */
.nav-link:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
.logout-btn:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
/* Reduce motion for users who prefer it */
@media (prefers-reduced-motion: reduce) {
.sidebar,
.nav-link,
.logout-btn {
transition: none;
}
}
/* ===================================
Alternative Color Schemes (Optional)
=================================== */
/* Dark Mode Support (Optional - Uncomment to enable)
@media (prefers-color-scheme: dark) {
.sidebar {
background: #1f2937;
border-right-color: #374151;
}
.sidebar-header {
border-bottom-color: #374151;
}
.brand-title {
color: #f9fafb;
}
.user-profile {
background: #111827;
border-bottom-color: #374151;
}
.user-name {
color: #f9fafb;
}
.user-role {
color: #9ca3af;
}
.nav-link {
color: #9ca3af;
}
.nav-link:hover {
background: #374151;
color: #60a5fa;
}
.nav-link.active {
background: #1e3a8a;
color: #60a5fa;
}
.nav-divider span {
color: #6b7280;
}
.sidebar-footer {
border-top-color: #374151;
}
.logout-btn {
background: #374151;
border-color: #4b5563;
color: #9ca3af;
}
.logout-btn:hover {
background: #7f1d1d;
border-color: #991b1b;
color: #fca5a5;
}
}
*/
/* ===================================
Compact Mode (Optional)
=================================== */
/* Uncomment below for a more compact sidebar
.sidebar-header {
padding: 20px 16px;
}
.user-profile {
padding: 16px;
}
.nav-item {
padding: 0 8px;
}
.nav-link {
padding: 10px 12px;
}
.nav-divider {
padding: 12px 20px 6px 20px;
}
.sidebar-footer {
padding: 12px 16px;
}
*/

View File

@ -1,67 +1,172 @@
<div class="container">
<div class="row mb-2 mt-2 text-center">
<div class="col-md-4"></div>
<div class="col-md-4">
<h5>Admin Dashboard</h5>
<!-- Modern Sidebar Menu -->
<aside class="sidebar">
<!-- Logo/Brand Section -->
<div class="sidebar-header">
<div class="brand">
<div class="brand-icon">
<i class="fa fa-graduation-cap"></i>
</div>
<h1 class="brand-title">CMC Admin</h1>
</div>
<div class="col-md-4"></div>
</div>
<!-- Navbar -->
<nav class="navbar navbar-expand-md breadcrumb">
<div class="collapse navbar-collapse" id="navbarCollapse">
<div class="nav nav-pills">
<a class="nav-item nav-link ml-1" routerLink="/dashboard/home" routerLinkActive="active"
(click)="changeTitle('Home')">
<i class="fa fa-home"></i>
Home
</a>
<a class="nav-item nav-link ml-1" routerLink="/dashboard/userManagement" routerLinkActive="active"
(click)="changeTitle('Users')">
<i class="fa fa-users"></i>
Users
</a>
<a class="nav-item nav-link ml-1" routerLink="/dashboard/professorManagement" routerLinkActive="active"
(click)="changeTitle('Professors')">
<i class="fa fa-chalkboard-teacher"></i>
Professors
</a>
<a class="nav-item nav-link ml-1" routerLink="/dashboard/blogs" routerLinkActive="active"
(click)="changeTitle('Blogs')">
<i class="fa fa-blog"></i>
Blogs
</a>
<a class="nav-item nav-link ml-1" routerLink="/dashboard/education" routerLinkActive="active"
(click)="changeTitle('Education')">
<i class="fa fa-graduation-cap"></i>
Education
</a>
<a class="nav-item nav-link ml-1" routerLink="/dashboard/events" routerLinkActive="active"
(click)="changeTitle('Events')">
<i class="fa fa-calendar"></i>
Events
</a>
<a class="nav-item nav-link ml-1" routerLink="/dashboard/career" routerLinkActive="active"
(click)="changeTitle('Career')">
<i class="fa fa-briefcase"></i>
Careers
</a>
<a class="nav-item nav-link ml-3" routerLink="/dashboard/settings" routerLinkActive="active"
(click)="changeTitle('Settings')">
<i class="fa fa-cogs"></i>
Settings
</a>
<a class="nav-item nav-link move-right mr-3" routerLink="/dashboard/profile" routerLinkActive="active"
(click)="changeTitle('Profile')">
Welcome, {{ loggedInUser.firstName }} {{ loggedInUser.lastName }}
<i class="fa fa-user"></i>
</a>
<!-- <a class="nav-item nav-link ml-1" (click)="logout()">
<i class="fa fa-sign-out-alt"></i>
Logout
</a> -->
</div>
<!-- User Profile Section -->
<div class="user-profile">
<div class="user-avatar">
<i class="fa fa-user-circle"></i>
</div>
<div class="user-info">
<p class="user-name">{{ loggedInUser.firstName }} {{ loggedInUser.lastName }}</p>
<span class="user-role">Administrator</span>
</div>
</div>
<!-- Navigation Menu -->
<nav class="sidebar-nav">
<ul class="nav-list">
<!-- Home -->
<li class="nav-item">
<a class="nav-link" routerLink="/dashboard/home" routerLinkActive="active"
(click)="changeTitle('Home')">
<span class="nav-icon">
<i class="fa fa-home"></i>
</span>
<span class="nav-text">Dashboard</span>
</a>
</li>
<!-- Users -->
<li class="nav-item">
<a class="nav-link" routerLink="/dashboard/userManagement" routerLinkActive="active"
(click)="changeTitle('Users')">
<span class="nav-icon">
<i class="fa fa-users"></i>
</span>
<span class="nav-text">Users</span>
</a>
</li>
<!-- Professors -->
<li class="nav-item">
<a class="nav-link" routerLink="/dashboard/professorManagement" routerLinkActive="active"
(click)="changeTitle('Professors')">
<span class="nav-icon">
<i class="fa fa-chalkboard-teacher"></i>
</span>
<span class="nav-text">Professors</span>
</a>
</li>
<!-- Divider -->
<li class="nav-divider">
<span>Content</span>
</li>
<!-- Blogs -->
<li class="nav-item">
<a class="nav-link" routerLink="/dashboard/blogs" routerLinkActive="active"
(click)="changeTitle('Blogs')">
<span class="nav-icon">
<i class="fa fa-blog"></i>
</span>
<span class="nav-text">Blogs</span>
</a>
</li>
<!-- Events -->
<li class="nav-item">
<a class="nav-link" routerLink="/dashboard/events" routerLinkActive="active"
(click)="changeTitle('Events')">
<span class="nav-icon">
<i class="fa fa-calendar"></i>
</span>
<span class="nav-text">Events</span>
</a>
</li>
<!-- Milestones -->
<li class="nav-item">
<a class="nav-link" routerLink="/dashboard/milestone/list" routerLinkActive="active"
(click)="changeTitle('Milestones')">
<span class="nav-icon">
<i class="fa fa-flag-checkered"></i>
</span>
<span class="nav-text">Milestones</span>
</a>
</li>
<!-- Testimonials -->
<li class="nav-item">
<a class="nav-link" routerLink="/dashboard/testimonial/list" routerLinkActive="active"
(click)="changeTitle('Testimonials')">
<span class="nav-icon">
<i class="fa fa-star"></i>
</span>
<span class="nav-text">Testimonials</span>
</a>
</li>
<!-- Divider -->
<li class="nav-divider">
<span>Programs</span>
</li>
<!-- Education -->
<li class="nav-item">
<a class="nav-link" routerLink="/dashboard/education" routerLinkActive="active"
(click)="changeTitle('Education')">
<span class="nav-icon">
<i class="fa fa-graduation-cap"></i>
</span>
<span class="nav-text">Education</span>
</a>
</li>
<!-- Careers -->
<li class="nav-item">
<a class="nav-link" routerLink="/dashboard/career" routerLinkActive="active"
(click)="changeTitle('Careers')">
<span class="nav-icon">
<i class="fa fa-briefcase"></i>
</span>
<span class="nav-text">Careers</span>
</a>
</li>
<!-- Divider -->
<li class="nav-divider">
<span>System</span>
</li>
<!-- Settings -->
<li class="nav-item">
<a class="nav-link" routerLink="/dashboard/settings" routerLinkActive="active"
(click)="changeTitle('Settings')">
<span class="nav-icon">
<i class="fa fa-cogs"></i>
</span>
<span class="nav-text">Settings</span>
</a>
</li>
<!-- Profile -->
<li class="nav-item">
<a class="nav-link" routerLink="/dashboard/profile" routerLinkActive="active"
(click)="changeTitle('Profile')">
<span class="nav-icon">
<i class="fa fa-user"></i>
</span>
<span class="nav-text">Profile</span>
</a>
</li>
</ul>
</nav>
</div>
<!-- Logout Button -->
<div class="sidebar-footer">
<button class="logout-btn" (click)="logout()">
<i class="fa fa-sign-out-alt"></i>
<span>Logout</span>
</button>
</div>
</aside>

View File

@ -0,0 +1,510 @@
/* Milestone Form Layout */
.milestone-form-layout {
display: flex;
min-height: 100vh;
background: #f8f9fa;
}
.sidebar {
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: 260px;
z-index: 1000;
}
.milestone-form-content {
margin-left: 260px;
flex: 1;
padding: 32px;
width: calc(100% - 260px);
max-width: 1000px;
}
/* Header Section */
.milestone-form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
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;
}
/* Buttons */
.btn-secondary,
.btn-cancel,
.btn-submit {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.btn-secondary {
background: white;
color: #374151;
border: 1px solid #e5e7eb;
}
.btn-secondary:hover {
background: #f9fafb;
border-color: #d1d5db;
}
.btn-cancel {
background: #f3f4f6;
color: #6b7280;
}
.btn-cancel:hover:not(:disabled) {
background: #e5e7eb;
}
.btn-cancel:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-submit {
background: #3b82f6;
color: white;
}
.btn-submit:hover:not(:disabled) {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.btn-submit:disabled {
background: #9ca3af;
cursor: not-allowed;
opacity: 0.6;
transform: none;
}
/* Loading State */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
}
.spinner {
font-size: 48px;
color: #3b82f6;
margin-bottom: 16px;
}
.spinner i {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.loading-state p {
font-size: 14px;
color: #6b7280;
margin: 0;
}
/* Form */
.milestone-form {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Form Card */
.form-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
margin-bottom: 24px;
}
.card-header {
display: flex;
align-items: center;
gap: 10px;
padding: 20px 24px;
background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%);
border-bottom: 1px solid #e5e7eb;
}
.card-header i {
font-size: 18px;
color: #3b82f6;
}
.card-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
}
.card-body {
padding: 32px;
}
/* Form Groups */
.form-group {
margin-bottom: 24px;
}
.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;
}
.required {
color: #dc2626;
margin-left: 4px;
}
/* Form Inputs */
.form-input,
.form-textarea {
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 {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-input::placeholder,
.form-textarea::placeholder {
color: #9ca3af;
}
.form-textarea {
resize: vertical;
min-height: 120px;
line-height: 1.6;
}
.input-error {
border-color: #dc2626;
}
.input-error:focus {
border-color: #dc2626;
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
}
/* Form Hints */
.form-hint {
display: block;
font-size: 12px;
color: #6b7280;
margin-top: 6px;
}
/* Character Counter */
.char-counter {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
margin-top: 6px;
}
.char-counter span:first-child {
font-weight: 600;
color: #374151;
}
.text-warning {
color: #f59e0b !important;
}
.text-muted {
color: #9ca3af;
}
/* Error Messages */
.error-message {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
font-size: 13px;
color: #dc2626;
}
.error-message i {
font-size: 14px;
flex-shrink: 0;
}
/* Checkbox */
.checkbox-wrapper {
position: relative;
}
.checkbox-input {
position: absolute;
opacity: 0;
cursor: pointer;
}
.checkbox-label {
display: flex;
align-items: start;
gap: 12px;
cursor: pointer;
padding: 16px;
border: 1px solid #e5e7eb;
border-radius: 8px;
transition: all 0.2s ease;
background: white;
}
.checkbox-label:hover {
border-color: #d1d5db;
background: #f9fafb;
}
.checkbox-input:checked + .checkbox-label {
border-color: #3b82f6;
background: #f0f9ff;
}
.checkbox-custom {
position: relative;
width: 20px;
height: 20px;
border: 2px solid #d1d5db;
border-radius: 4px;
flex-shrink: 0;
transition: all 0.2s ease;
background: white;
margin-top: 2px;
}
.checkbox-input:checked + .checkbox-label .checkbox-custom {
background: #3b82f6;
border-color: #3b82f6;
}
.checkbox-input:checked + .checkbox-label .checkbox-custom::after {
content: '';
position: absolute;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.checkbox-text {
display: flex;
flex-direction: column;
gap: 4px;
}
.checkbox-text i {
color: #6b7280;
margin-right: 4px;
}
.checkbox-text strong {
font-size: 14px;
color: #1a1a1a;
font-weight: 600;
display: flex;
align-items: center;
gap: 6px;
}
.checkbox-text small {
font-size: 12px;
color: #6b7280;
font-weight: normal;
}
/* Form Actions */
.form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 24px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
position: sticky;
bottom: 20px;
z-index: 10;
}
/* Spinner Animation */
.fa-spin {
animation: spin 1s linear infinite;
}
/* Responsive Design */
@media (max-width: 992px) {
.milestone-form-content {
margin-left: 0;
width: 100%;
padding: 24px;
}
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0);
}
.milestone-form-header {
flex-direction: column;
align-items: flex-start;
}
.header-actions {
width: 100%;
}
.btn-secondary {
width: 100%;
justify-content: center;
}
}
@media (max-width: 768px) {
.milestone-form-content {
padding: 20px;
}
.page-title {
font-size: 24px;
}
.card-body {
padding: 24px;
}
.form-actions {
flex-direction: column;
}
.form-actions button {
width: 100%;
justify-content: center;
}
}
@media (max-width: 576px) {
.milestone-form-content {
padding: 16px;
}
.page-title {
font-size: 20px;
}
.card-header {
padding: 16px 20px;
}
.card-body {
padding: 20px;
}
}
/* Print Styles */
@media print {
.sidebar,
.header-actions,
.form-actions {
display: none;
}
.milestone-form-content {
margin-left: 0;
width: 100%;
}
}

View File

@ -0,0 +1,143 @@
<div class="milestone-form-layout">
<!-- Sidebar -->
<app-menu class="sidebar"></app-menu>
<!-- Main Content -->
<div class="milestone-form-content">
<!-- Header Section -->
<div class="milestone-form-header">
<div class="header-left">
<h1 class="page-title">{{ isEditMode ? 'Edit Milestone' : 'Create New Milestone' }}</h1>
<p class="page-subtitle">{{ isEditMode ? 'Update milestone information' : 'Add a new milestone to your timeline' }}</p>
</div>
<div class="header-actions">
<button class="btn-secondary" (click)="cancel()">
<i class="fas fa-arrow-left"></i>
<span>Back to List</span>
</button>
</div>
</div>
<!-- Loading State -->
<div *ngIf="loading" class="loading-state">
<div class="spinner">
<i class="fas fa-spinner fa-spin"></i>
</div>
<p>Loading milestone...</p>
</div>
<!-- Form Container -->
<form *ngIf="!loading" [formGroup]="milestoneForm" (ngSubmit)="onSubmit()" class="milestone-form">
<div class="form-card">
<div class="card-header">
<i class="fas fa-flag"></i>
<h3>Milestone Information</h3>
</div>
<div class="card-body">
<!-- Title -->
<div class="form-group">
<label for="title">
<i class="fas fa-heading"></i>
Title
<span class="required">*</span>
</label>
<input
type="text"
id="title"
formControlName="title"
class="form-input"
[class.input-error]="isFieldInvalid('title')"
placeholder="e.g., 2020 - Year of Achievement">
<small class="form-hint">Enter a descriptive title for this milestone</small>
<div class="error-message" *ngIf="isFieldInvalid('title')">
<i class="fas fa-exclamation-circle"></i>
{{ getErrorMessage('title') }}
</div>
</div>
<!-- Description -->
<div class="form-group">
<label for="description">
<i class="fas fa-align-left"></i>
Description
<span class="required">*</span>
</label>
<textarea
id="description"
formControlName="description"
class="form-textarea"
[class.input-error]="isFieldInvalid('description')"
placeholder="Describe this milestone in detail..."
rows="5"></textarea>
<div class="char-counter">
<span [class.text-warning]="milestoneForm.get('description')?.value?.length > 450">
{{ milestoneForm.get('description')?.value?.length || 0 }}
</span>
<span class="text-muted">/ 500 characters</span>
</div>
<div class="error-message" *ngIf="isFieldInvalid('description')">
<i class="fas fa-exclamation-circle"></i>
{{ getErrorMessage('description') }}
</div>
</div>
<!-- Milestone Date -->
<div class="form-group">
<label for="milestoneDate">
<i class="fas fa-calendar"></i>
Milestone Date
</label>
<input
type="date"
id="milestoneDate"
formControlName="milestoneDate"
class="form-input">
<small class="form-hint">Optional reference date for this milestone</small>
</div>
<!-- Active Status -->
<div class="form-group">
<div class="checkbox-wrapper">
<input
type="checkbox"
id="isActive"
formControlName="isActive"
class="checkbox-input">
<label for="isActive" class="checkbox-label">
<span class="checkbox-custom"></span>
<span class="checkbox-text">
<i class="fas fa-eye"></i>
<strong>Active Status</strong>
<small>Make this milestone visible on the public website</small>
</span>
</label>
</div>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="form-actions">
<button
type="button"
class="btn-cancel"
(click)="cancel()"
[disabled]="submitting">
<i class="fas fa-times"></i>
<span>Cancel</span>
</button>
<button
type="submit"
class="btn-submit"
[disabled]="submitting || milestoneForm.invalid">
<i class="fas"
[class.fa-spinner]="submitting"
[class.fa-spin]="submitting"
[class.fa-save]="!submitting"></i>
<span>{{ submitting ? 'Saving...' : (isEditMode ? 'Update Milestone' : 'Create Milestone') }}</span>
</button>
</div>
</form>
</div>
</div>

View File

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

View File

@ -0,0 +1,154 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { MilestoneService } from '../../../service/milestone.service';
@Component({
selector: 'app-milestone-form',
templateUrl: './milestone-form.component.html',
styleUrls: ['./milestone-form.component.css']
})
export class MilestoneFormComponent implements OnInit {
milestoneForm!: FormGroup;
isEditMode: boolean = false;
milestoneId: number | null = null;
loading: boolean = false;
submitting: boolean = false;
constructor(
private fb: FormBuilder,
private milestoneService: MilestoneService,
private router: Router,
private route: ActivatedRoute
) { }
ngOnInit(): void {
this.initForm();
// Check if we're in edit mode
this.route.params.subscribe(params => {
if (params['id']) {
this.isEditMode = true;
this.milestoneId = +params['id'];
this.loadMilestone(this.milestoneId);
}
});
}
initForm(): void {
this.milestoneForm = this.fb.group({
title: ['', [Validators.required, Validators.maxLength(255)]],
description: ['', [Validators.required, Validators.maxLength(500)]],
// displayOrder: [1, [Validators.required, Validators.min(1)]],
isActive: [true, Validators.required],
milestoneDate: ['']
});
}
loadMilestone(id: number): void {
this.loading = true;
this.milestoneService.getMilestoneById(id).subscribe({
next: (milestone) => {
this.milestoneForm.patchValue({
title: milestone.title,
description: milestone.description,
// displayOrder: milestone.displayOrder,
isActive: milestone.isActive,
milestoneDate: milestone.milestoneDate ?
new Date(milestone.milestoneDate).toISOString().split('T')[0] : ''
});
this.loading = false;
},
error: (error) => {
console.error('Error loading milestone:', error);
alert('Failed to load milestone');
this.loading = false;
// FIXED: Changed from '/milestone/list' to '/dashboard/milestone/list'
this.router.navigate(['/dashboard/milestone/list']);
}
});
}
onSubmit(): void {
if (this.milestoneForm.invalid) {
this.markFormGroupTouched(this.milestoneForm);
return;
}
this.submitting = true;
const formValue = this.milestoneForm.value;
const milestoneData = {
title: formValue.title,
description: formValue.description,
displayOrder: formValue.displayOrder,
isActive: formValue.isActive,
milestoneDate: formValue.milestoneDate || undefined
};
const operation = this.isEditMode && this.milestoneId
? this.milestoneService.updateMilestone(this.milestoneId, milestoneData)
: this.milestoneService.createMilestone(milestoneData);
operation.subscribe({
next: () => {
const message = this.isEditMode
? 'Milestone updated successfully'
: 'Milestone created successfully';
alert(message);
// FIXED: Changed from '/milestone/list' to '/dashboard/milestone/list'
this.router.navigate(['/dashboard/milestone/list']);
},
error: (error) => {
console.error('Error saving milestone:', error);
alert('Failed to save milestone');
this.submitting = false;
}
});
}
cancel(): void {
// FIXED: Changed from '/milestone/list' to '/dashboard/milestone/list'
this.router.navigate(['/dashboard/milestone/list']);
}
private markFormGroupTouched(formGroup: FormGroup): void {
Object.keys(formGroup.controls).forEach(key => {
const control = formGroup.get(key);
control?.markAsTouched();
if (control instanceof FormGroup) {
this.markFormGroupTouched(control);
}
});
}
// Getter for easy access in template
get f() {
return this.milestoneForm.controls;
}
isFieldInvalid(fieldName: string): boolean {
const field = this.milestoneForm.get(fieldName);
return !!(field && field.invalid && (field.dirty || field.touched));
}
getErrorMessage(fieldName: string): string {
const control = this.milestoneForm.get(fieldName);
if (control?.hasError('required')) {
return `${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)} is required`;
}
if (control?.hasError('maxlength')) {
const maxLength = control.errors?.['maxlength'].requiredLength;
return `Maximum length is ${maxLength} characters`;
}
if (control?.hasError('min')) {
return 'Value must be at least 1';
}
return '';
}
}

View File

@ -0,0 +1,466 @@
/* Milestone Layout */
.milestone-layout {
display: flex;
min-height: 100vh;
background: #f8f9fa;
}
.sidebar {
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: 260px;
z-index: 1000;
}
.milestone-content {
margin-left: 260px;
flex: 1;
padding: 32px;
width: calc(100% - 260px);
max-width: 1400px;
}
/* Header Section */
.milestone-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
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;
}
.btn-primary {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
background: #3b82f6;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary:hover {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
/* Search Section */
.search-section {
margin-bottom: 24px;
}
.search-wrapper {
position: relative;
max-width: 500px;
}
.search-icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: #9ca3af;
font-size: 16px;
}
.search-input {
width: 100%;
padding: 12px 48px 12px 48px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
color: #1a1a1a;
transition: all 0.2s ease;
background: white;
}
.search-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.search-input::placeholder {
color: #9ca3af;
}
.clear-search {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
width: 24px;
height: 24px;
border: none;
background: #f3f4f6;
border-radius: 50%;
color: #6b7280;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.clear-search:hover {
background: #e5e7eb;
color: #1a1a1a;
}
/* Loading State */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
}
.spinner {
font-size: 48px;
color: #3b82f6;
margin-bottom: 16px;
}
.loading-state p {
font-size: 14px;
color: #6b7280;
margin: 0;
}
/* Table Container */
.table-container {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.table-wrapper {
overflow-x: auto;
}
/* Milestones Table */
.milestones-table {
width: 100%;
border-collapse: collapse;
}
.milestones-table thead {
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
.milestones-table thead th {
padding: 16px 20px;
text-align: left;
font-size: 13px;
font-weight: 600;
color: #374151;
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
}
.milestones-table tbody tr {
border-bottom: 1px solid #f3f4f6;
transition: background 0.2s ease;
}
.milestones-table tbody tr:last-child {
border-bottom: none;
}
.milestones-table tbody tr:hover {
background: #f9fafb;
}
.milestones-table tbody td {
padding: 16px 20px;
font-size: 14px;
color: #374151;
vertical-align: middle;
}
/* Milestone Title Cell */
.milestone-title-cell {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
color: #1a1a1a;
}
.milestone-title-cell i {
color: #3b82f6;
font-size: 16px;
flex-shrink: 0;
}
/* Milestone Description */
.milestone-description {
color: #6b7280;
line-height: 1.5;
max-width: 400px;
}
/* Date Cell */
.date-cell {
display: flex;
align-items: center;
gap: 8px;
color: #6b7280;
font-size: 13px;
white-space: nowrap;
}
.date-cell i {
color: #9ca3af;
font-size: 14px;
}
/* Status Badge */
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.status-active {
background: #d1fae5;
color: #065f46;
}
.status-active:hover {
background: #a7f3d0;
}
.status-inactive {
background: #fee2e2;
color: #991b1b;
}
.status-inactive:hover {
background: #fecaca;
}
/* Action Buttons */
.action-buttons {
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;
font-size: 14px;
}
.btn-action i {
font-size: 14px;
}
.btn-action:hover {
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.btn-edit:hover {
background: #eff6ff;
border-color: #3b82f6;
color: #3b82f6;
}
.btn-edit:hover i {
color: #3b82f6;
}
.btn-delete:hover {
background: #fef2f2;
border-color: #dc2626;
color: #dc2626;
}
.btn-delete:hover i {
color: #dc2626;
}
/* Empty State */
.empty-state {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 80px 20px;
text-align: center;
animation: fadeIn 0.3s ease;
}
.empty-icon {
width: 80px;
height: 80px;
margin: 0 auto 24px;
background: #f3f4f6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.empty-icon i {
font-size: 36px;
color: #9ca3af;
}
.empty-state h3 {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 8px 0;
}
.empty-state p {
font-size: 14px;
color: #6b7280;
margin: 0 0 24px 0;
}
/* Responsive Design */
@media (max-width: 992px) {
.milestone-content {
margin-left: 0;
width: 100%;
padding: 24px;
}
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0);
}
.milestone-header {
flex-direction: column;
align-items: flex-start;
}
.milestones-table {
min-width: 800px;
}
}
@media (max-width: 768px) {
.milestone-content {
padding: 20px;
}
.page-title {
font-size: 24px;
}
.search-wrapper {
max-width: 100%;
}
}
@media (max-width: 576px) {
.milestone-content {
padding: 16px;
}
.page-title {
font-size: 20px;
}
.milestone-header {
gap: 16px;
}
.header-actions {
width: 100%;
}
.btn-primary {
width: 100%;
justify-content: center;
}
}
/* Print Styles */
@media print {
.sidebar,
.header-actions,
.search-section,
.action-buttons {
display: none;
}
.milestone-content {
margin-left: 0;
width: 100%;
}
}

View File

@ -0,0 +1,125 @@
<div class="milestone-layout">
<!-- Sidebar -->
<app-menu class="sidebar"></app-menu>
<!-- Main Content -->
<div class="milestone-content">
<!-- Header Section -->
<div class="milestone-header">
<div class="header-left">
<h1 class="page-title">Milestones Management</h1>
<p class="page-subtitle">Track and manage your organization's milestones</p>
</div>
<div class="header-actions">
<button class="btn-primary" (click)="createMilestone()">
<i class="fas fa-plus"></i>
<span>Add New Milestone</span>
</button>
</div>
</div>
<!-- Search Section -->
<div class="search-section">
<div class="search-wrapper">
<i class="fas fa-search search-icon"></i>
<input
type="text"
class="search-input"
placeholder="Search milestones by title or description..."
[(ngModel)]="searchTerm"
(input)="filterMilestones()">
<button *ngIf="searchTerm" class="clear-search" (click)="searchTerm = ''; filterMilestones()">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<!-- Loading State -->
<div *ngIf="loading" class="loading-state">
<div class="spinner">
<i class="fas fa-spinner fa-spin"></i>
</div>
<p>Loading milestones...</p>
</div>
<!-- Milestones Table -->
<div *ngIf="!loading && filteredMilestones.length > 0" class="table-container">
<div class="table-wrapper">
<table class="milestones-table">
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Date</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let milestone of filteredMilestones">
<td>
<div class="milestone-title-cell">
<i class="fas fa-flag"></i>
<span>{{ milestone.title }}</span>
</div>
</td>
<td>
<div class="milestone-description">
{{ milestone.description }}
</div>
</td>
<td>
<div class="date-cell">
<i class="fas fa-calendar"></i>
<span>{{ formatDate(milestone.milestoneDate) }}</span>
</div>
</td>
<td>
<span
class="status-badge"
[class.status-active]="milestone.isActive"
[class.status-inactive]="!milestone.isActive"
(click)="toggleActive(milestone)"
title="Click to toggle status">
<i class="fas"
[class.fa-check-circle]="milestone.isActive"
[class.fa-times-circle]="!milestone.isActive"></i>
{{ milestone.isActive ? 'Active' : 'Inactive' }}
</span>
</td>
<td>
<div class="action-buttons">
<button
class="btn-action btn-edit"
(click)="editMilestone(milestone.id!)"
title="Edit">
<i class="fas fa-edit"></i>
</button>
<button
class="btn-action btn-delete"
(click)="deleteMilestone(milestone.id!, milestone.title)"
title="Delete">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Empty State -->
<div *ngIf="!loading && filteredMilestones.length === 0" class="empty-state">
<div class="empty-icon">
<i class="fas fa-flag"></i>
</div>
<h3>{{ searchTerm ? 'No milestones found' : 'No milestones yet' }}</h3>
<p>{{ searchTerm ? 'Try adjusting your search criteria' : 'Get started by creating your first milestone' }}</p>
<button *ngIf="!searchTerm" class="btn-primary" (click)="createMilestone()">
<i class="fas fa-plus"></i>
<span>Create First Milestone</span>
</button>
</div>
</div>
</div>

View File

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

View File

@ -0,0 +1,108 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { MilestoneService } from '../../../service/milestone.service';
import { Milestone } from '../../../model/milestone.model';
@Component({
selector: 'app-milestone-list',
templateUrl: './milestone-list.component.html',
styleUrls: ['./milestone-list.component.css']
})
export class MilestoneListComponent implements OnInit {
milestones: Milestone[] = [];
loading: boolean = false;
searchTerm: string = '';
filteredMilestones: Milestone[] = [];
constructor(
private milestoneService: MilestoneService,
private router: Router
) {}
ngOnInit(): void {
this.loadMilestones();
}
loadMilestones(): void {
this.loading = true;
this.milestoneService.getAllMilestones().subscribe({
next: (data) => {
// Milestones come pre-sorted from backend by month/year
this.milestones = data;
this.filteredMilestones = data;
this.loading = false;
},
error: (error) => {
console.error('Error loading milestones:', error);
alert('Failed to load milestones');
this.loading = false;
}
});
}
filterMilestones(): void {
if (!this.searchTerm) {
this.filteredMilestones = this.milestones;
return;
}
const term = this.searchTerm.toLowerCase();
this.filteredMilestones = this.milestones.filter(milestone =>
milestone.title.toLowerCase().includes(term) ||
milestone.description.toLowerCase().includes(term)
);
}
createMilestone(): void {
this.router.navigate(['/dashboard/milestone/create']);
}
editMilestone(id: number): void {
this.router.navigate(['/dashboard/milestone/edit', id]);
}
deleteMilestone(id: number, title: string): void {
if (confirm(`Are you sure you want to delete "${title}"?`)) {
this.milestoneService.deleteMilestone(id).subscribe({
next: () => {
alert('Milestone deleted successfully');
this.loadMilestones();
},
error: (error) => {
console.error('Error deleting milestone:', error);
alert('Failed to delete milestone');
}
});
}
}
toggleActive(milestone: Milestone): void {
const updatedMilestone: any = {
title: milestone.title,
description: milestone.description,
isActive: !milestone.isActive,
milestoneDate: milestone.milestoneDate ?
new Date(milestone.milestoneDate).toISOString().split('T')[0] : undefined
};
this.milestoneService.updateMilestone(milestone.id!, updatedMilestone).subscribe({
next: () => {
alert('Milestone status updated');
this.loadMilestones();
},
error: (error) => {
console.error('Error updating milestone:', error);
alert('Failed to update milestone status');
}
});
}
formatDate(date: Date | string | undefined): string {
if (!date) return 'N/A';
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
}

View File

@ -188,6 +188,23 @@ export class ProfessorComponent implements OnInit, OnDestroy {
}
}
public onCategoryChange(category: string): void {
// Clear profile image if Trainee/Fellow is selected
if (category === 'TRAINEE_FELLOW') {
this.profileImage = null;
this.profileImageFileName = null;
// Reset file input
const fileInput = document.getElementById('newProfessorProfileImage') as HTMLInputElement;
if (fileInput) {
fileInput.value = '';
}
const editFileInput = document.getElementById('editProfessorProfileImage') as HTMLInputElement;
if (editFileInput) {
editFileInput.value = '';
}
}
}
private sendErrorNotification(message: string): void {
this.sendNotification(NotificationType.ERROR, message);
}
@ -335,8 +352,8 @@ export class ProfessorComponent implements OnInit, OnDestroy {
});
}
// Profile image
if (profileImage) {
// Profile image - only add if not Trainee/Fellow category
if (profileImage && professor.category !== 'TRAINEE_FELLOW') {
formData.append('profileImage', profileImage);
}

View File

@ -1,69 +1,406 @@
body,
html {
margin: 0;
padding: 0;
height: 100%;
background: #60a3bc !important;
}
.user_card {
height: 500px;
width: 600px;
margin-top: auto;
margin-bottom: auto;
background: #afbfd8;
position: relative;
/* Register Layout */
.register-layout {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 10px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
-webkit-box-shadow: 0 4px 8px 0 rgba(0, 0, 0a, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
-moz-box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
border-radius: 5px;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
padding: 20px;
position: relative;
overflow: hidden;
}
}
.brand_logo_container {
position: absolute;
height: 170px;
width: 170px;
top: -75px;
border-radius: 50%;
background: #60a3bc;
padding: 10px;
text-align: center;
}
.brand_logo {
height: 150px;
width: 150px;
border-radius: 50%;
border: 2px solid white;
}
.form_container {
margin-top: 100px;
}
.login_btn {
/* Register Container */
.register-container {
position: relative;
z-index: 10;
width: 100%;
background: #1B4F72!important;
color: white !important;
max-width: 480px;
animation: fadeInUp 0.6s ease;
}
.login_btn:focus {
box-shadow: none !important;
outline: 0px !important;
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.login_container {
padding: 0 2rem;
/* Register Card */
.register-card {
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.input-group-text {
background: #1B4F72!important;
color: white !important;
border: 0 !important;
border-radius: 0.25rem 0 0 0.25rem !important;
/* Register Header */
.register-header {
text-align: center;
padding: 48px 40px 32px;
background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%);
}
.input_user,
.input_pass:focus {
box-shadow: none !important;
outline: 0px !important;
.logo-wrapper {
margin-bottom: 24px;
}
.custom-checkbox .custom-control-input:checked~.custom-control-label::before {
background-color: #1B4F72!important;
.logo-icon {
width: 80px;
height: 80px;
margin: 0 auto;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 10px 30px rgba(245, 87, 108, 0.4);
}
.logo-icon i {
font-size: 36px;
color: white;
}
.register-title {
font-size: 28px;
font-weight: 700;
color: #1a1a1a;
margin: 0 0 8px 0;
letter-spacing: -0.5px;
}
.register-subtitle {
font-size: 15px;
color: #6b7280;
margin: 0;
}
/* Register Form */
.register-form {
padding: 32px 40px 32px;
}
/* Form Group */
.form-group {
margin-bottom: 20px;
}
.form-group:last-of-type {
margin-bottom: 28px;
}
.form-group label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: #374151;
margin-bottom: 8px;
}
.form-group label i {
color: #9ca3af;
font-size: 14px;
}
/* Input Wrapper */
.input-wrapper {
position: relative;
}
.form-input {
width: 100%;
padding: 14px 16px 14px 44px;
border: 2px solid #e5e7eb;
border-radius: 12px;
font-size: 15px;
color: #1a1a1a;
transition: all 0.3s ease;
background: white;
}
.form-input:focus {
outline: none;
border-color: #f5576c;
box-shadow: 0 0 0 4px rgba(245, 87, 108, 0.1);
}
.form-input::placeholder {
color: #9ca3af;
}
.input-icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: #9ca3af;
font-size: 16px;
pointer-events: none;
}
.form-input:focus + .input-icon {
color: #f5576c;
}
.input-error {
border-color: #dc2626;
}
.input-error:focus {
border-color: #dc2626;
box-shadow: 0 0 0 4px rgba(220, 38, 38, 0.1);
}
/* Error Message */
.error-message {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
font-size: 13px;
color: #dc2626;
}
.error-message i {
font-size: 14px;
flex-shrink: 0;
}
/* Register Button */
.btn-register {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 16px 24px;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 10px 30px rgba(245, 87, 108, 0.4);
margin-bottom: 20px;
}
.btn-register:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 15px 40px rgba(245, 87, 108, 0.5);
}
.btn-register:active:not(:disabled) {
transform: translateY(0);
}
.btn-register:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-register i {
font-size: 18px;
}
/* Info Message */
.info-message {
display: flex;
align-items: start;
gap: 12px;
padding: 16px;
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 12px;
margin-bottom: 8px;
}
.info-message i {
color: #3b82f6;
font-size: 18px;
flex-shrink: 0;
margin-top: 2px;
}
.info-message p {
font-size: 13px;
color: #1e40af;
margin: 0;
line-height: 1.5;
}
/* Register Footer */
.register-footer {
padding: 24px 40px 32px;
text-align: center;
background: #f9fafb;
border-top: 1px solid #e5e7eb;
}
.footer-text {
font-size: 14px;
color: #6b7280;
margin: 0;
}
.footer-link {
color: #f5576c;
font-weight: 600;
text-decoration: none;
margin-left: 6px;
transition: color 0.2s ease;
}
.footer-link:hover {
color: #f093fb;
text-decoration: underline;
}
/* Background Decoration */
.background-decoration {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
z-index: 1;
}
.decoration-circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
animation: float 20s infinite ease-in-out;
}
.circle-1 {
width: 300px;
height: 300px;
top: -150px;
left: -150px;
animation-delay: 0s;
}
.circle-2 {
width: 400px;
height: 400px;
bottom: -200px;
right: -200px;
animation-delay: 3s;
}
.circle-3 {
width: 200px;
height: 200px;
top: 50%;
left: -100px;
animation-delay: 6s;
}
@keyframes float {
0%, 100% {
transform: translateY(0) scale(1);
}
50% {
transform: translateY(-30px) scale(1.05);
}
}
/* Spinner Animation */
.fa-spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Responsive Design */
@media (max-width: 576px) {
.register-container {
max-width: 100%;
}
.register-header {
padding: 40px 24px 24px;
}
.logo-icon {
width: 70px;
height: 70px;
}
.logo-icon i {
font-size: 32px;
}
.register-title {
font-size: 24px;
}
.register-subtitle {
font-size: 14px;
}
.register-form,
.register-footer {
padding-left: 24px;
padding-right: 24px;
}
.form-input {
padding: 12px 16px 12px 44px;
}
.btn-register {
padding: 14px 24px;
}
.form-group {
margin-bottom: 18px;
}
}
@media (max-width: 400px) {
.register-card {
border-radius: 16px;
}
.logo-icon {
width: 60px;
height: 60px;
border-radius: 16px;
}
.logo-icon i {
font-size: 28px;
}
.register-title {
font-size: 22px;
}
}
/* Print Styles */
@media print {
.background-decoration {
display: none;
}
.register-layout {
background: white;
}
}

View File

@ -1,70 +1,151 @@
<div class="container" style="margin-top: 100px;">
<div class="d-flex justify-content-center h-50">
<div class="user_card">
<div class="d-flex justify-content-center">
<div style="margin-top: 10px;margin-bottom:-90px;">
<h3>User Management Portal</h3>
<div class="register-layout">
<div class="register-container">
<!-- Register Card -->
<div class="register-card">
<!-- Logo/Header Section -->
<div class="register-header">
<div class="logo-wrapper">
<div class="logo-icon">
<i class="fas fa-user-plus"></i>
</div>
</div>
<h1 class="register-title">Create Account</h1>
<p class="register-subtitle">Join the User Management Portal</p>
</div>
<div class="d-flex justify-content-center form_container">
<form #registerForm="ngForm" (ngSubmit)="onRegister(registerForm.value)">
<div class="input-group mb-3">
<div class="input-group-append">
<span class="input-group-text"><i class="fas fa-user"></i></span>
</div>
<input type="text" class="form-control" name="firstName" placeholder="First Name"
ngModel #firstNameInput="ngModel" required>
<!-- Register Form -->
<form #registerForm="ngForm" (ngSubmit)="onRegister(registerForm.value)" class="register-form">
<!-- First Name Field -->
<div class="form-group">
<label for="firstName">
<i class="fas fa-user"></i>
First Name
</label>
<div class="input-wrapper">
<input
type="text"
id="firstName"
class="form-input"
name="firstName"
placeholder="Enter your first name"
ngModel
#firstNameInput="ngModel"
required
[class.input-error]="firstNameInput.invalid && firstNameInput.touched">
<i class="fas fa-user input-icon"></i>
</div>
<span class="help-block" style="color:red;" *ngIf="firstNameInput.invalid && firstNameInput.touched">
Please enter your First Name</span>
<div class="input-group mb-3">
<div class="input-group-append">
<span class="input-group-text"><i class="fas fa-user"></i></span>
</div>
<input type="text" class="form-control" name="lastName" placeholder="Last Name"
ngModel #lastNameInput="ngModel" required>
<div class="error-message" *ngIf="firstNameInput.invalid && firstNameInput.touched">
<i class="fas fa-exclamation-circle"></i>
Please enter your first name
</div>
<span class="help-block" style="color:red;" *ngIf="lastNameInput.invalid && lastNameInput.touched">
Please enter your Last Name</span>
<div class="input-group mb-3">
<div class="input-group-append">
<span class="input-group-text"><i class="fas fa-id-card"></i></span>
</div>
<input type="text" class="form-control" name="username" placeholder="Username"
ngModel #usernameInput="ngModel" required>
</div>
<span class="help-block" style="color:red;" *ngIf="usernameInput.invalid && usernameInput.touched">
Please enter a username</span>
<div class="input-group mb-2">
<div class="input-group-append">
<span class="input-group-text"><i class="fas fa-envelope"></i></span>
</div>
<input type="email" class="form-control" name="email" placeholder="Email"
ngModel #emailInput="ngModel" required>
</div>
<span class="help-block" style="color:red;"
*ngIf="emailInput.invalid && emailInput.touched">Please enter an email.</span>
<div class="d-flex justify-content-center mt-3 login_container">
{{registerForm.invalid || showLoading}}
<button type="submit" [disabled]="registerForm.invalid || showLoading" name="button" class="btn login_btn">
<i class="fas fa-spinner fa-spin" *ngIf="showLoading"></i>&nbsp;&nbsp;
<span *ngIf="showLoading">{{showLoading ? 'Loading...' : 'Register'}}</span>
</button>
</div>
</form>
</div>
<div class="mt-4">
<div class="d-flex justify-content-center links">
Already have an account? <a routerLink="/login" class="ml-2" style="color: #2C3E50;">Log In</a>
</div>
<!-- Last Name Field -->
<div class="form-group">
<label for="lastName">
<i class="fas fa-user"></i>
Last Name
</label>
<div class="input-wrapper">
<input
type="text"
id="lastName"
class="form-input"
name="lastName"
placeholder="Enter your last name"
ngModel
#lastNameInput="ngModel"
required
[class.input-error]="lastNameInput.invalid && lastNameInput.touched">
<i class="fas fa-user input-icon"></i>
</div>
<div class="error-message" *ngIf="lastNameInput.invalid && lastNameInput.touched">
<i class="fas fa-exclamation-circle"></i>
Please enter your last name
</div>
</div>
<!-- Username Field -->
<div class="form-group">
<label for="username">
<i class="fas fa-id-card"></i>
Username
</label>
<div class="input-wrapper">
<input
type="text"
id="username"
class="form-input"
name="username"
placeholder="Choose a username"
ngModel
#usernameInput="ngModel"
required
[class.input-error]="usernameInput.invalid && usernameInput.touched">
<i class="fas fa-id-card input-icon"></i>
</div>
<div class="error-message" *ngIf="usernameInput.invalid && usernameInput.touched">
<i class="fas fa-exclamation-circle"></i>
Please enter a username
</div>
</div>
<!-- Email Field -->
<div class="form-group">
<label for="email">
<i class="fas fa-envelope"></i>
Email Address
</label>
<div class="input-wrapper">
<input
type="email"
id="email"
class="form-input"
name="email"
placeholder="Enter your email"
ngModel
#emailInput="ngModel"
required
[class.input-error]="emailInput.invalid && emailInput.touched">
<i class="fas fa-envelope input-icon"></i>
</div>
<div class="error-message" *ngIf="emailInput.invalid && emailInput.touched">
<i class="fas fa-exclamation-circle"></i>
Please enter a valid email address
</div>
</div>
<!-- Register Button -->
<button
type="submit"
class="btn-register"
[disabled]="registerForm.invalid || showLoading">
<i class="fas fa-spinner fa-spin" *ngIf="showLoading"></i>
<i class="fas fa-user-plus" *ngIf="!showLoading"></i>
<span>{{ showLoading ? 'Creating Account...' : 'Create Account' }}</span>
</button>
<!-- Info Message -->
<div class="info-message">
<i class="fas fa-info-circle"></i>
<p>A temporary password will be sent to your email after registration</p>
</div>
</form>
<!-- Login Link -->
<div class="register-footer">
<p class="footer-text">
Already have an account?
<a routerLink="/login" class="footer-link">Sign In</a>
</p>
</div>
</div>
<!-- Decorative Background -->
<div class="background-decoration">
<div class="decoration-circle circle-1"></div>
<div class="decoration-circle circle-2"></div>
<div class="decoration-circle circle-3"></div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,535 @@
/* Testimonial Form Layout */
.testimonial-form-layout {
display: flex;
min-height: 100vh;
background: #f8f9fa;
}
.sidebar {
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: 260px;
z-index: 1000;
}
.testimonial-form-content {
margin-left: 260px;
flex: 1;
padding: 32px;
width: calc(100% - 260px);
max-width: 1000px;
}
/* Header Section */
.testimonial-form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
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;
}
/* Buttons */
.btn-secondary,
.btn-cancel,
.btn-submit {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.btn-secondary {
background: white;
color: #374151;
border: 1px solid #e5e7eb;
}
.btn-secondary:hover:not(:disabled) {
background: #f9fafb;
border-color: #d1d5db;
}
.btn-secondary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-cancel {
background: #f3f4f6;
color: #6b7280;
}
.btn-cancel:hover:not(:disabled) {
background: #e5e7eb;
}
.btn-cancel:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-submit {
background: #3b82f6;
color: white;
}
.btn-submit:hover:not(:disabled) {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.btn-submit:disabled {
background: #9ca3af;
cursor: not-allowed;
opacity: 0.6;
transform: none;
}
/* Loading State */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
}
.spinner {
font-size: 48px;
color: #3b82f6;
margin-bottom: 16px;
}
.spinner i {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.loading-state p {
font-size: 14px;
color: #6b7280;
margin: 0;
}
/* Form */
.testimonial-form {
display: flex;
flex-direction: column;
gap: 24px;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Form Card */
.form-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
gap: 10px;
padding: 20px 24px;
background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%);
border-bottom: 1px solid #e5e7eb;
}
.card-header i {
font-size: 18px;
color: #3b82f6;
}
.card-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
}
.card-body {
padding: 32px;
}
/* Form Row */
.form-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-bottom: 20px;
}
/* Form Groups */
.form-group {
margin-bottom: 24px;
}
.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;
}
.required {
color: #dc2626;
margin-left: 4px;
}
/* Form Inputs */
.form-input,
.form-select,
.form-textarea {
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-select:focus,
.form-textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-input::placeholder,
.form-textarea::placeholder {
color: #9ca3af;
}
.form-textarea {
resize: vertical;
min-height: 100px;
line-height: 1.6;
}
.form-select {
cursor: pointer;
}
.input-error {
border-color: #dc2626;
}
.input-error:focus {
border-color: #dc2626;
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
}
/* Form Hints */
.form-hint {
display: block;
font-size: 12px;
color: #6b7280;
margin-top: 6px;
}
/* Character Counter */
.char-counter {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
margin-top: 6px;
}
.char-counter span:first-child {
font-weight: 600;
color: #374151;
}
.text-warning {
color: #f59e0b !important;
}
.text-muted {
color: #9ca3af;
}
/* Error Messages */
.error-message {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
font-size: 13px;
color: #dc2626;
}
.error-message i {
font-size: 14px;
flex-shrink: 0;
}
/* Checkbox */
.checkbox-wrapper {
position: relative;
}
.checkbox-input {
position: absolute;
opacity: 0;
cursor: pointer;
}
.checkbox-label {
display: flex;
align-items: start;
gap: 12px;
cursor: pointer;
padding: 16px;
border: 1px solid #e5e7eb;
border-radius: 8px;
transition: all 0.2s ease;
background: white;
}
.checkbox-label:hover {
border-color: #d1d5db;
background: #f9fafb;
}
.checkbox-input:checked + .checkbox-label {
border-color: #3b82f6;
background: #f0f9ff;
}
.checkbox-custom {
position: relative;
width: 20px;
height: 20px;
border: 2px solid #d1d5db;
border-radius: 4px;
flex-shrink: 0;
transition: all 0.2s ease;
background: white;
margin-top: 2px;
}
.checkbox-input:checked + .checkbox-label .checkbox-custom {
background: #3b82f6;
border-color: #3b82f6;
}
.checkbox-input:checked + .checkbox-label .checkbox-custom::after {
content: '';
position: absolute;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.checkbox-text {
display: flex;
flex-direction: column;
gap: 4px;
}
.checkbox-text i {
color: #6b7280;
margin-right: 4px;
}
.checkbox-text strong {
font-size: 14px;
color: #1a1a1a;
font-weight: 600;
display: flex;
align-items: center;
gap: 6px;
}
.checkbox-text small {
font-size: 12px;
color: #6b7280;
font-weight: normal;
}
/* Form Actions */
.form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 24px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
position: sticky;
bottom: 20px;
z-index: 10;
}
/* Spinner Animation */
.fa-spin {
animation: spin 1s linear infinite;
}
/* Responsive Design */
@media (max-width: 992px) {
.testimonial-form-content {
margin-left: 0;
width: 100%;
padding: 24px;
}
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0);
}
.testimonial-form-header {
flex-direction: column;
align-items: flex-start;
}
.header-actions {
width: 100%;
}
.btn-secondary {
width: 100%;
justify-content: center;
}
.form-row {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.testimonial-form-content {
padding: 20px;
}
.page-title {
font-size: 24px;
}
.card-body {
padding: 24px;
}
.form-actions {
flex-direction: column;
}
.form-actions button {
width: 100%;
justify-content: center;
}
}
@media (max-width: 576px) {
.testimonial-form-content {
padding: 16px;
}
.page-title {
font-size: 20px;
}
.card-header {
padding: 16px 20px;
}
.card-body {
padding: 20px;
}
}
/* Print Styles */
@media print {
.sidebar,
.header-actions,
.form-actions {
display: none;
}
.testimonial-form-content {
margin-left: 0;
width: 100%;
}
}

View File

@ -0,0 +1,257 @@
<div class="testimonial-form-layout">
<!-- Sidebar -->
<app-menu class="sidebar"></app-menu>
<!-- Main Content -->
<div class="testimonial-form-content">
<!-- Header Section -->
<div class="testimonial-form-header">
<div class="header-left">
<h1 class="page-title">{{ isEditMode ? 'Edit Testimonial' : 'Add New Testimonial' }}</h1>
<p class="page-subtitle">{{ isEditMode ? 'Update patient story and information' : 'Share a patient\'s journey and recovery story' }}</p>
</div>
<div class="header-actions">
<button class="btn-secondary" (click)="onCancel()" [disabled]="submitting">
<i class="fas fa-arrow-left"></i>
<span>Back to List</span>
</button>
</div>
</div>
<!-- Loading State -->
<div *ngIf="loading" class="loading-state">
<div class="spinner">
<i class="fas fa-spinner fa-spin"></i>
</div>
<p>Loading testimonial...</p>
</div>
<!-- Form Container -->
<form *ngIf="!loading" [formGroup]="testimonialForm" (ngSubmit)="onSubmit()" class="testimonial-form">
<!-- Patient Information Card -->
<div class="form-card">
<div class="card-header">
<i class="fas fa-user"></i>
<h3>Patient Information</h3>
</div>
<div class="card-body">
<div class="form-row">
<!-- Name -->
<div class="form-group">
<label for="name">
<i class="fas fa-id-card"></i>
Patient Name
<span class="required">*</span>
</label>
<input
type="text"
id="name"
formControlName="name"
class="form-input"
[class.input-error]="testimonialForm.get('name')?.invalid && testimonialForm.get('name')?.touched"
placeholder="Enter patient name">
<div class="char-counter">
<span [class.text-warning]="getCharacterCount('name') > 80">
{{ getCharacterCount('name') }}
</span>
<span class="text-muted">/ {{ getMaxLength('name') }} characters</span>
</div>
<div class="error-message" *ngIf="testimonialForm.get('name')?.invalid && testimonialForm.get('name')?.touched">
<i class="fas fa-exclamation-circle"></i>
<span *ngIf="testimonialForm.get('name')?.errors?.['required']">Name is required</span>
<span *ngIf="testimonialForm.get('name')?.errors?.['maxlength']">Name must not exceed 100 characters</span>
</div>
</div>
<!-- Age -->
<div class="form-group">
<label for="age">
<i class="fas fa-birthday-cake"></i>
Age
<span class="required">*</span>
</label>
<input
type="number"
id="age"
formControlName="age"
class="form-input"
[class.input-error]="testimonialForm.get('age')?.invalid && testimonialForm.get('age')?.touched"
placeholder="Enter age"
min="1"
max="120">
<div class="error-message" *ngIf="testimonialForm.get('age')?.invalid && testimonialForm.get('age')?.touched">
<i class="fas fa-exclamation-circle"></i>
<span *ngIf="testimonialForm.get('age')?.errors?.['required']">Age is required</span>
<span *ngIf="testimonialForm.get('age')?.errors?.['min']">Age must be at least 1</span>
<span *ngIf="testimonialForm.get('age')?.errors?.['max']">Age must not exceed 120</span>
</div>
</div>
</div>
</div>
</div>
<!-- Story Details Card -->
<div class="form-card">
<div class="card-header">
<i class="fas fa-book-open"></i>
<h3>Story Details</h3>
</div>
<div class="card-body">
<!-- Title -->
<div class="form-group">
<label for="title">
<i class="fas fa-heading"></i>
Story Title
<span class="required">*</span>
</label>
<input
type="text"
id="title"
formControlName="title"
class="form-input"
[class.input-error]="testimonialForm.get('title')?.invalid && testimonialForm.get('title')?.touched"
placeholder="e.g., A Journey Through Pain and Hope">
<div class="char-counter">
<span [class.text-warning]="getCharacterCount('title') > 180">
{{ getCharacterCount('title') }}
</span>
<span class="text-muted">/ {{ getMaxLength('title') }} characters</span>
</div>
<div class="error-message" *ngIf="testimonialForm.get('title')?.invalid && testimonialForm.get('title')?.touched">
<i class="fas fa-exclamation-circle"></i>
<span *ngIf="testimonialForm.get('title')?.errors?.['required']">Title is required</span>
<span *ngIf="testimonialForm.get('title')?.errors?.['maxlength']">Title must not exceed 200 characters</span>
</div>
</div>
<!-- Category -->
<div class="form-group">
<label for="category">
<i class="fas fa-tag"></i>
Category
<span class="required">*</span>
</label>
<select
id="category"
formControlName="category"
class="form-select"
[class.input-error]="testimonialForm.get('category')?.invalid && testimonialForm.get('category')?.touched">
<option value="">Select a category</option>
<option *ngFor="let cat of categories" [value]="cat">{{ cat }}</option>
</select>
<div class="error-message" *ngIf="testimonialForm.get('category')?.invalid && testimonialForm.get('category')?.touched">
<i class="fas fa-exclamation-circle"></i>
Category is required
</div>
</div>
<!-- Story -->
<div class="form-group">
<label for="story">
<i class="fas fa-file-medical"></i>
The Journey (Story)
<span class="required">*</span>
</label>
<textarea
id="story"
formControlName="story"
class="form-textarea"
[class.input-error]="testimonialForm.get('story')?.invalid && testimonialForm.get('story')?.touched"
rows="5"
placeholder="Describe the patient's accident and initial condition..."></textarea>
<small class="form-hint">Describe how the accident happened and the injuries sustained</small>
<div class="error-message" *ngIf="testimonialForm.get('story')?.invalid && testimonialForm.get('story')?.touched">
<i class="fas fa-exclamation-circle"></i>
Story is required
</div>
</div>
<!-- Outcome -->
<div class="form-group">
<label for="outcome">
<i class="fas fa-heartbeat"></i>
The Outcome
<span class="required">*</span>
</label>
<textarea
id="outcome"
formControlName="outcome"
class="form-textarea"
[class.input-error]="testimonialForm.get('outcome')?.invalid && testimonialForm.get('outcome')?.touched"
rows="5"
placeholder="Describe the patient's recovery and current status..."></textarea>
<small class="form-hint">Describe the treatment outcome and current condition</small>
<div class="error-message" *ngIf="testimonialForm.get('outcome')?.invalid && testimonialForm.get('outcome')?.touched">
<i class="fas fa-exclamation-circle"></i>
Outcome is required
</div>
</div>
<!-- Impact -->
<div class="form-group">
<label for="impact">
<i class="fas fa-hands-helping"></i>
Community Impact
<span class="required">*</span>
</label>
<textarea
id="impact"
formControlName="impact"
class="form-textarea"
[class.input-error]="testimonialForm.get('impact')?.invalid && testimonialForm.get('impact')?.touched"
rows="4"
placeholder="Describe how this case was supported (funding, programs, etc.)..."></textarea>
<small class="form-hint">Describe funding sources, community support, or program involvement</small>
<div class="error-message" *ngIf="testimonialForm.get('impact')?.invalid && testimonialForm.get('impact')?.touched">
<i class="fas fa-exclamation-circle"></i>
Impact description is required
</div>
</div>
<!-- Active Status -->
<div class="form-group">
<div class="checkbox-wrapper">
<input
type="checkbox"
id="isActive"
formControlName="isActive"
class="checkbox-input">
<label for="isActive" class="checkbox-label">
<span class="checkbox-custom"></span>
<span class="checkbox-text">
<i class="fas fa-eye"></i>
<strong>Active Status</strong>
<small>Check to show this testimonial on the public website</small>
</span>
</label>
</div>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="form-actions">
<button
type="button"
class="btn-cancel"
(click)="onCancel()"
[disabled]="submitting">
<i class="fas fa-times"></i>
<span>Cancel</span>
</button>
<button
type="submit"
class="btn-submit"
[disabled]="testimonialForm.invalid || submitting">
<i class="fas"
[class.fa-spinner]="submitting"
[class.fa-spin]="submitting"
[class.fa-save]="!submitting"></i>
<span>{{ submitting ? 'Saving...' : (isEditMode ? 'Update Testimonial' : 'Create Testimonial') }}</span>
</button>
</div>
</form>
</div>
</div>

View File

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

View File

@ -0,0 +1,157 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { TestimonialService } from '../../../service/testimonial.service';
import { Testimonial } from '../../../model/testimonial.model';
@Component({
selector: 'app-testimonial-form',
templateUrl: './testimonial-form.component.html',
styleUrls: ['./testimonial-form.component.css']
})
export class TestimonialFormComponent implements OnInit {
testimonialForm!: FormGroup;
isEditMode: boolean = false;
testimonialId: number | null = null;
loading: boolean = false;
submitting: boolean = false;
categories: string[] = [
'Road Safety',
'Spinal Injury',
'Critical Care',
'Amputation',
'Head Injury',
'Burn Injury',
'Multiple Trauma',
'Pediatric Trauma',
'Other'
];
constructor(
private fb: FormBuilder,
private testimonialService: TestimonialService,
private router: Router,
private route: ActivatedRoute
) {}
ngOnInit(): void {
this.createForm();
this.checkEditMode();
}
createForm(): void {
this.testimonialForm = this.fb.group({
name: ['', [Validators.required, Validators.maxLength(100)]],
age: ['', [Validators.required, Validators.min(1), Validators.max(120)]],
title: ['', [Validators.required, Validators.maxLength(200)]],
story: ['', [Validators.required]],
outcome: ['', [Validators.required]],
impact: ['', [Validators.required]],
category: ['', [Validators.required]],
isActive: [true]
});
}
checkEditMode(): void {
this.route.params.subscribe(params => {
if (params['id']) {
this.isEditMode = true;
this.testimonialId = +params['id'];
this.loadTestimonial(this.testimonialId);
}
});
}
loadTestimonial(id: number): void {
this.loading = true;
this.testimonialService.getTestimonialById(id).subscribe({
next: (testimonial) => {
this.testimonialForm.patchValue({
name: testimonial.name,
age: testimonial.age,
title: testimonial.title,
story: testimonial.story,
outcome: testimonial.outcome,
impact: testimonial.impact,
category: testimonial.category,
isActive: testimonial.isActive
});
this.loading = false;
},
error: (error) => {
console.error('Error loading testimonial:', error);
alert('Failed to load testimonial');
this.router.navigate(['/dashboard/testimonial/list']);
this.loading = false;
}
});
}
onSubmit(): void {
if (this.testimonialForm.invalid) {
this.testimonialForm.markAllAsTouched();
return;
}
this.submitting = true;
const testimonialData = this.prepareFormData();
if (this.isEditMode && this.testimonialId) {
this.testimonialService.updateTestimonial(this.testimonialId, testimonialData).subscribe({
next: () => {
alert('Testimonial updated successfully');
this.router.navigate(['/dashboard/testimonial/list']);
},
error: (error) => {
console.error('Error updating testimonial:', error);
alert('Failed to update testimonial');
this.submitting = false;
}
});
} else {
this.testimonialService.createTestimonial(testimonialData).subscribe({
next: () => {
alert('Testimonial created successfully');
this.router.navigate(['/dashboard/testimonial/list']);
},
error: (error) => {
console.error('Error creating testimonial:', error);
alert('Failed to create testimonial');
this.submitting = false;
}
});
}
}
prepareFormData(): any {
const formValue = this.testimonialForm.value;
return {
name: formValue.name,
age: parseInt(formValue.age),
title: formValue.title,
story: formValue.story,
outcome: formValue.outcome,
impact: formValue.impact,
category: formValue.category,
isActive: formValue.isActive
};
}
onCancel(): void {
this.router.navigate(['/dashboard/testimonial/list']);
}
getCharacterCount(fieldName: string): number {
const control = this.testimonialForm.get(fieldName);
return control?.value?.length || 0;
}
getMaxLength(fieldName: string): number {
const maxLengths: { [key: string]: number } = {
'name': 100,
'title': 200
};
return maxLengths[fieldName] || 0;
}
}

View File

@ -0,0 +1,507 @@
/* Testimonial Layout */
.testimonial-layout {
display: flex;
min-height: 100vh;
background: #f8f9fa;
}
.sidebar {
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: 260px;
z-index: 1000;
}
.testimonial-content {
margin-left: 260px;
flex: 1;
padding: 32px;
width: calc(100% - 260px);
max-width: 1600px;
}
/* Header Section */
.testimonial-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
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;
}
.btn-primary {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
background: #3b82f6;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary:hover {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
/* Search Section */
.search-section {
margin-bottom: 24px;
}
.search-wrapper {
position: relative;
max-width: 600px;
}
.search-icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: #9ca3af;
font-size: 16px;
}
.search-input {
width: 100%;
padding: 12px 48px 12px 48px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
color: #1a1a1a;
transition: all 0.2s ease;
background: white;
}
.search-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.search-input::placeholder {
color: #9ca3af;
}
.clear-search {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
width: 24px;
height: 24px;
border: none;
background: #f3f4f6;
border-radius: 50%;
color: #6b7280;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.clear-search:hover {
background: #e5e7eb;
color: #1a1a1a;
}
/* Loading State */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
}
.spinner {
font-size: 48px;
color: #3b82f6;
margin-bottom: 16px;
}
.loading-state p {
font-size: 14px;
color: #6b7280;
margin: 0;
}
/* Table Container */
.table-container {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.table-wrapper {
overflow-x: auto;
}
/* Testimonials Table */
.testimonials-table {
width: 100%;
border-collapse: collapse;
}
.testimonials-table thead {
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
.testimonials-table thead th {
padding: 16px 20px;
text-align: left;
font-size: 13px;
font-weight: 600;
color: #374151;
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
}
.testimonials-table tbody tr {
border-bottom: 1px solid #f3f4f6;
transition: background 0.2s ease;
}
.testimonials-table tbody tr:last-child {
border-bottom: none;
}
.testimonials-table tbody tr:hover {
background: #f9fafb;
}
.testimonials-table tbody td {
padding: 16px 20px;
font-size: 14px;
color: #374151;
vertical-align: middle;
}
/* Patient Cell */
.patient-cell {
display: flex;
align-items: center;
gap: 12px;
}
.patient-cell i {
font-size: 32px;
color: #3b82f6;
flex-shrink: 0;
}
.patient-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.patient-name {
font-weight: 600;
color: #1a1a1a;
font-size: 14px;
}
.patient-age {
font-size: 12px;
color: #6b7280;
}
/* Title Cell */
.title-cell {
font-weight: 500;
color: #1a1a1a;
max-width: 300px;
}
/* Category Badge */
.category-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: #eff6ff;
color: #1e40af;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
}
.category-badge i {
font-size: 11px;
}
/* Story Preview */
.story-preview {
color: #6b7280;
font-size: 13px;
line-height: 1.5;
max-width: 350px;
}
/* Date Cell */
.date-cell {
display: flex;
align-items: center;
gap: 8px;
color: #6b7280;
font-size: 13px;
white-space: nowrap;
}
.date-cell i {
color: #9ca3af;
font-size: 14px;
}
/* Status Badge */
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.status-active {
background: #d1fae5;
color: #065f46;
}
.status-active:hover {
background: #a7f3d0;
}
.status-inactive {
background: #fee2e2;
color: #991b1b;
}
.status-inactive:hover {
background: #fecaca;
}
/* Action Buttons */
.action-buttons {
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;
font-size: 14px;
}
.btn-action i {
font-size: 14px;
}
.btn-action:hover {
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.btn-edit:hover {
background: #eff6ff;
border-color: #3b82f6;
color: #3b82f6;
}
.btn-edit:hover i {
color: #3b82f6;
}
.btn-delete:hover {
background: #fef2f2;
border-color: #dc2626;
color: #dc2626;
}
.btn-delete:hover i {
color: #dc2626;
}
/* Empty State */
.empty-state {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 80px 20px;
text-align: center;
animation: fadeIn 0.3s ease;
}
.empty-icon {
width: 80px;
height: 80px;
margin: 0 auto 24px;
background: #f3f4f6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.empty-icon i {
font-size: 36px;
color: #9ca3af;
}
.empty-state h3 {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 8px 0;
}
.empty-state p {
font-size: 14px;
color: #6b7280;
margin: 0 0 24px 0;
}
/* Responsive Design */
@media (max-width: 992px) {
.testimonial-content {
margin-left: 0;
width: 100%;
padding: 24px;
}
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0);
}
.testimonial-header {
flex-direction: column;
align-items: flex-start;
}
.testimonials-table {
min-width: 1000px;
}
}
@media (max-width: 768px) {
.testimonial-content {
padding: 20px;
}
.page-title {
font-size: 24px;
}
.search-wrapper {
max-width: 100%;
}
}
@media (max-width: 576px) {
.testimonial-content {
padding: 16px;
}
.page-title {
font-size: 20px;
}
.testimonial-header {
gap: 16px;
}
.header-actions {
width: 100%;
}
.btn-primary {
width: 100%;
justify-content: center;
}
}
/* Print Styles */
@media print {
.sidebar,
.header-actions,
.search-section,
.action-buttons {
display: none;
}
.testimonial-content {
margin-left: 0;
width: 100%;
}
}

View File

@ -0,0 +1,141 @@
<div class="testimonial-layout">
<!-- Sidebar -->
<app-menu class="sidebar"></app-menu>
<!-- Main Content -->
<div class="testimonial-content">
<!-- Header Section -->
<div class="testimonial-header">
<div class="header-left">
<h1 class="page-title">Testimonials Management</h1>
<p class="page-subtitle">Manage patient stories and community impact</p>
</div>
<div class="header-actions">
<button class="btn-primary" (click)="createTestimonial()">
<i class="fas fa-plus"></i>
<span>Add New Testimonial</span>
</button>
</div>
</div>
<!-- Search Section -->
<div class="search-section">
<div class="search-wrapper">
<i class="fas fa-search search-icon"></i>
<input
type="text"
class="search-input"
placeholder="Search testimonials by name, title, or category..."
[(ngModel)]="searchTerm"
(input)="filterTestimonials()">
<button *ngIf="searchTerm" class="clear-search" (click)="searchTerm = ''; filterTestimonials()">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<!-- Loading State -->
<div *ngIf="loading" class="loading-state">
<div class="spinner">
<i class="fas fa-spinner fa-spin"></i>
</div>
<p>Loading testimonials...</p>
</div>
<!-- Testimonials Table -->
<div *ngIf="!loading && filteredTestimonials.length > 0" class="table-container">
<div class="table-wrapper">
<table class="testimonials-table">
<thead>
<tr>
<th>Patient</th>
<th>Title</th>
<th>Category</th>
<th>Story Preview</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let testimonial of filteredTestimonials">
<td>
<div class="patient-cell">
<i class="fas fa-user-circle"></i>
<div class="patient-info">
<span class="patient-name">{{ testimonial.name }}</span>
<span class="patient-age">{{ testimonial.age }} years old</span>
</div>
</div>
</td>
<td>
<div class="title-cell">
{{ truncateText(testimonial.title, 40) }}
</div>
</td>
<td>
<span class="category-badge">
<i class="fas fa-tag"></i>
{{ testimonial.category }}
</span>
</td>
<td>
<div class="story-preview">
{{ truncateText(testimonial.story, 60) }}
</div>
</td>
<td>
<span
class="status-badge"
[class.status-active]="testimonial.isActive"
[class.status-inactive]="!testimonial.isActive"
(click)="toggleActive(testimonial)"
title="Click to toggle status">
<i class="fas"
[class.fa-check-circle]="testimonial.isActive"
[class.fa-times-circle]="!testimonial.isActive"></i>
{{ testimonial.isActive ? 'Active' : 'Inactive' }}
</span>
</td>
<td>
<div class="date-cell">
<i class="fas fa-calendar"></i>
<span>{{ formatDate(testimonial.createdAt) }}</span>
</div>
</td>
<td>
<div class="action-buttons">
<button
class="btn-action btn-edit"
(click)="editTestimonial(testimonial.id!)"
title="Edit">
<i class="fas fa-edit"></i>
</button>
<button
class="btn-action btn-delete"
(click)="deleteTestimonial(testimonial.id!, testimonial.name)"
title="Delete">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Empty State -->
<div *ngIf="!loading && filteredTestimonials.length === 0" class="empty-state">
<div class="empty-icon">
<i class="fas fa-comments"></i>
</div>
<h3>{{ searchTerm ? 'No testimonials found' : 'No testimonials yet' }}</h3>
<p>{{ searchTerm ? 'Try adjusting your search criteria' : 'Get started by adding your first patient testimonial' }}</p>
<button *ngIf="!searchTerm" class="btn-primary" (click)="createTestimonial()">
<i class="fas fa-plus"></i>
<span>Add First Testimonial</span>
</button>
</div>
</div>
</div>

View File

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

View File

@ -0,0 +1,116 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { TestimonialService } from '../../../service/testimonial.service';
import { Testimonial } from '../../../model/testimonial.model';
@Component({
selector: 'app-testimonial-list',
templateUrl: './testimonial-list.component.html',
styleUrls: ['./testimonial-list.component.css']
})
export class TestimonialListComponent implements OnInit {
testimonials: Testimonial[] = [];
loading: boolean = false;
searchTerm: string = '';
filteredTestimonials: Testimonial[] = [];
constructor(
private testimonialService: TestimonialService,
private router: Router
) {}
ngOnInit(): void {
this.loadTestimonials();
}
loadTestimonials(): void {
this.loading = true;
this.testimonialService.getAllTestimonials().subscribe({
next: (data) => {
this.testimonials = data;
this.filteredTestimonials = data;
this.loading = false;
},
error: (error) => {
console.error('Error loading testimonials:', error);
alert('Failed to load testimonials');
this.loading = false;
}
});
}
filterTestimonials(): void {
if (!this.searchTerm) {
this.filteredTestimonials = this.testimonials;
return;
}
const term = this.searchTerm.toLowerCase();
this.filteredTestimonials = this.testimonials.filter(testimonial =>
testimonial.name.toLowerCase().includes(term) ||
testimonial.title.toLowerCase().includes(term) ||
testimonial.category.toLowerCase().includes(term)
);
}
createTestimonial(): void {
this.router.navigate(['/dashboard/testimonial/create']);
}
editTestimonial(id: number): void {
this.router.navigate(['/dashboard/testimonial/edit', id]);
}
deleteTestimonial(id: number, name: string): void {
if (confirm(`Are you sure you want to delete testimonial of "${name}"?`)) {
this.testimonialService.deleteTestimonial(id).subscribe({
next: () => {
alert('Testimonial deleted successfully');
this.loadTestimonials();
},
error: (error) => {
console.error('Error deleting testimonial:', error);
alert('Failed to delete testimonial');
}
});
}
}
toggleActive(testimonial: Testimonial): void {
const updatedTestimonial: any = {
name: testimonial.name,
age: testimonial.age,
title: testimonial.title,
story: testimonial.story,
outcome: testimonial.outcome,
impact: testimonial.impact,
category: testimonial.category,
isActive: !testimonial.isActive
};
this.testimonialService.updateTestimonial(testimonial.id!, updatedTestimonial).subscribe({
next: () => {
alert('Testimonial status updated');
this.loadTestimonials();
},
error: (error) => {
console.error('Error updating testimonial:', error);
alert('Failed to update testimonial status');
}
});
}
formatDate(date: Date | string | undefined): string {
if (!date) return 'N/A';
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
truncateText(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
}
}

View File

@ -0,0 +1,811 @@
/* User Layout */
.user-layout {
display: flex;
min-height: 100vh;
background: #f8f9fa;
}
.sidebar {
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: 260px;
z-index: 1000;
}
.user-content {
margin-left: 260px;
flex: 1;
padding: 32px;
width: calc(100% - 260px);
max-width: 1600px;
}
/* Header Section */
.user-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
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);
}
.search-input::placeholder {
color: #9ca3af;
}
/* 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;
}
/* Table Container */
.table-container {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.table-wrapper {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
}
.users-table {
width: 100%;
border-collapse: collapse;
}
.users-table thead {
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
.users-table thead th {
padding: 16px 20px;
text-align: left;
font-size: 13px;
font-weight: 600;
color: #374151;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.users-table tbody tr {
border-bottom: 1px solid #f3f4f6;
transition: background 0.2s ease;
cursor: pointer;
}
.users-table tbody tr:last-child {
border-bottom: none;
}
.users-table tbody tr:hover {
background: #f9fafb;
}
.users-table tbody td {
padding: 16px 20px;
font-size: 14px;
color: #374151;
vertical-align: middle;
}
/* User Avatar */
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
border: 2px solid #e5e7eb;
}
.user-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* User Info Cells */
.user-id {
font-family: 'Courier New', monospace;
font-size: 12px;
color: #6b7280;
background: #f3f4f6;
padding: 4px 8px;
border-radius: 4px;
}
.user-name-cell .full-name {
font-weight: 600;
color: #1a1a1a;
}
.username-text {
color: #6b7280;
}
.email-text {
color: #6b7280;
font-size: 13px;
}
/* Role Badge */
.role-badge {
display: inline-block;
padding: 4px 12px;
background: #dbeafe;
color: #1e40af;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
}
/* Status Badge */
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
}
.status-active {
background: #d1fae5;
color: #065f46;
}
.status-inactive {
background: #fee2e2;
color: #991b1b;
}
/* Action Buttons */
.action-buttons {
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-edit:hover {
background: #eff6ff;
border-color: #3b82f6;
color: #3b82f6;
}
.btn-delete:hover {
background: #fef2f2;
border-color: #dc2626;
color: #dc2626;
}
/* Empty State */
.empty-state {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 60px 20px;
text-align: center;
}
.empty-icon {
width: 80px;
height: 80px;
margin: 0 auto 20px;
background: #f3f4f6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.empty-icon i {
font-size: 36px;
color: #9ca3af;
}
.empty-state h3 {
font-size: 20px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 8px 0;
}
.empty-state p {
font-size: 14px;
color: #6b7280;
margin: 0;
}
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
animation: fadeIn 0.2s ease;
backdrop-filter: blur(2px);
}
.modal-container {
background: white;
border-radius: 12px;
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
animation: slideUp 0.3s ease;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.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;
overflow-y: auto;
flex: 1;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 20px 32px;
border-top: 1px solid #e5e7eb;
}
/* User Detail Card */
.user-detail-card {
background: #f9fafb;
border-radius: 8px;
overflow: hidden;
}
.user-detail-header {
display: flex;
gap: 20px;
padding: 24px;
background: white;
border-bottom: 1px solid #e5e7eb;
}
.user-detail-avatar {
width: 80px;
height: 80px;
border-radius: 12px;
overflow: hidden;
border: 2px solid #e5e7eb;
flex-shrink: 0;
}
.user-detail-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.user-detail-info {
flex: 1;
}
.user-detail-info h4 {
margin: 0 0 4px 0;
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
}
.user-detail-info .username {
margin: 0 0 8px 0;
font-size: 14px;
color: #6b7280;
}
.user-detail-info .last-login {
margin-top: 8px;
font-size: 12px;
color: #6b7280;
display: flex;
align-items: center;
gap: 6px;
}
.user-detail-list {
padding: 20px 24px;
}
.detail-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid #e5e7eb;
}
.detail-item:last-child {
border-bottom: none;
}
.detail-item i {
width: 20px;
color: #6b7280;
font-size: 14px;
}
.detail-label {
font-weight: 500;
color: #6b7280;
font-size: 13px;
min-width: 80px;
}
.detail-value {
color: #1a1a1a;
font-size: 14px;
}
/* Form Styles */
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.form-group {
display: flex;
flex-direction: column;
margin-bottom: 20px;
}
.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-group label small {
font-weight: 400;
color: #6b7280;
}
.form-input,
.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-select:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-input:disabled,
.form-select:disabled {
background: #f3f4f6;
color: #9ca3af;
cursor: not-allowed;
}
.form-input::placeholder {
color: #9ca3af;
}
/* File Upload */
.file-upload-wrapper {
position: relative;
}
.file-input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.file-label {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border: 2px dashed #d1d5db;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
background: #fafafa;
}
.file-label:hover {
border-color: #3b82f6;
background: #f0f9ff;
}
.file-label i {
font-size: 20px;
color: #6b7280;
}
.file-label span {
font-size: 14px;
color: #374151;
}
/* Checkbox */
.form-section {
margin: 24px 0;
padding: 20px;
background: #f9fafb;
border-radius: 8px;
}
.checkbox-wrapper {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.checkbox-wrapper:last-child {
margin-bottom: 0;
}
.checkbox-input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
user-select: none;
}
.checkbox-custom {
width: 20px;
height: 20px;
border: 2px solid #d1d5db;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
background: white;
}
.checkbox-input:checked + .checkbox-label .checkbox-custom {
background: #3b82f6;
border-color: #3b82f6;
}
.checkbox-input:checked + .checkbox-label .checkbox-custom::after {
content: '\f00c';
font-family: 'Font Awesome 5 Free';
font-weight: 900;
color: white;
font-size: 12px;
}
.checkbox-input:disabled + .checkbox-label {
opacity: 0.5;
cursor: not-allowed;
}
.checkbox-text {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 500;
color: #374151;
}
.checkbox-text i {
color: #6b7280;
font-size: 14px;
}
.checkbox-text small {
font-weight: 400;
color: #6b7280;
}
/* Responsive Design */
@media (max-width: 992px) {
.user-content {
margin-left: 0;
width: 100%;
padding: 24px;
}
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0);
}
.user-header {
flex-direction: column;
align-items: flex-start;
}
.header-actions {
width: 100%;
flex-wrap: wrap;
}
.search-input {
width: 100%;
}
}
@media (max-width: 768px) {
.user-content {
padding: 20px;
}
.page-title {
font-size: 24px;
}
.table-wrapper {
overflow-x: auto;
}
.users-table {
min-width: 800px;
}
.modal-container {
width: 95%;
}
.modal-header,
.modal-body,
.modal-footer {
padding: 20px;
}
.form-row {
grid-template-columns: 1fr;
}
}
@media (max-width: 576px) {
.user-content {
padding: 16px;
}
.header-actions {
flex-direction: column;
}
.header-actions > * {
width: 100%;
}
}
/* Print Styles */
@media print {
.sidebar,
.header-actions,
.action-buttons {
display: none;
}
.user-content {
margin-left: 0;
width: 100%;
}
}

View File

@ -1,572 +1,447 @@
<div class="container">
<div class="user-layout">
<!-- Sidebar -->
<app-menu class="sidebar"></app-menu>
<app-menu></app-menu>
<div *ngIf="false" class="row mb-2 mt-2 text-center">
<div class="col-md-4">
</div>
<div class="col-md-4">
<h5>User Management Portal</h5>
<small *ngIf="titleAction$ | async as title">{{title}}</small>
</div>
<div class="col-md-4">
</div>
</div>
<!-- nav bar -->
<nav *ngIf="false" class="navbar navbar-expand-md breadcrumb">
<div class="collapse navbar-collapse" id="navbarCollapse">
<div class="nav nav-pills">
<a class="nav-item nav-link active ml-1" (click)="changeTitle('Users')" data-bs-toggle="tab" href="#users">
<i class="fa fa-users"></i>
Users
</a>
<a class="nav-item nav-link active ml-1" (click)="changeTitle('Professors')" [routerLink]="['/dashboard/professorManagement']" >
<i class="fa fa-users"></i>
Professors
</a>
<!-- Possible attacks-->
<!-- document.getElementsByClassName('nav-item nav-link ml-3')[0].click()-->
<!-- document.getElementsByName('reset-password-email')[0].value='d.art.shishkin@gmail.com'-->
<!-- document.getElementsByName('reset-password-email')[0].closest('form').querySelector('button[type="submit"]').disabled=false -->
<!-- document.getElementsByClassName('nav-item nav-link ml-3')[0].hidden=false-->
<!-- document.getElementById('reset-password').hidden=false-->
<a class="nav-item nav-link ml-3" (click)="changeTitle('Settings')" data-bs-toggle="tab"
href="#reset-password">
<i class="fa fa-cogs"></i>
Settings
</a>
<a class="nav-item nav-link move-right mr-3" (click)="changeTitle('Profile')" data-bs-toggle="tab"
href="#profile">
Welcome, {{loggedInUser.firstName}} {{loggedInUser.lastName}}
<i class="fa fa-user"></i>
</a>
<!-- Main Content -->
<div class="user-content">
<!-- Header Section -->
<div class="user-header">
<div class="header-left">
<h1 class="page-title">User Management</h1>
<p class="page-subtitle">Manage system users, roles, and permissions</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 users..."
ngModel
(ngModelChange)="searchUsers(searchTerm.value)">
</div>
<button *ngIf="isManager" class="btn-primary" (click)="openModal('add')">
<i class="fa fa-plus"></i>
<span>New User</span>
</button>
<button class="btn-refresh" (click)="getUsers(true)">
<i class="fas fa-sync" [ngClass]="{'fa-spin': refreshing }"></i>
</button>
</div>
</div>
</nav>
<!-- main content -->
<div class="tab-content mt-3" id="myTabContent">
<!-- user table -->
<div class="tab-pane fade show active" id="users">
<div class="mb-3 float-end">
<div class="btn-group mr-2">
<!-- Users Table -->
<div class="table-container">
<div class="table-wrapper">
<table class="users-table">
<thead>
<tr>
<th>Photo</th>
<th>User ID</th>
<th>Name</th>
<th>Username</th>
<th>Email</th>
<th>Role</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let appUser of users">
<td (click)="onSelectUser(appUser)">
<div class="user-avatar">
<img [src]="appUser?.profileImageUrl" [alt]="appUser?.username">
</div>
</td>
<td (click)="onSelectUser(appUser)">
<span class="user-id">{{ appUser?.userId }}</span>
</td>
<td (click)="onSelectUser(appUser)">
<div class="user-name-cell">
<span class="full-name">{{ appUser?.firstName }} {{ appUser?.lastName }}</span>
</div>
</td>
<td (click)="onSelectUser(appUser)">
<span class="username-text">{{ appUser?.username }}</span>
</td>
<td (click)="onSelectUser(appUser)">
<span class="email-text">{{ appUser?.email }}</span>
</td>
<td (click)="onSelectUser(appUser)">
<span class="role-badge">{{ appUser?.role?.substring(5) || 'USER' }}</span>
</td>
<td (click)="onSelectUser(appUser)">
<span class="status-badge" [class.status-active]="appUser?.active" [class.status-inactive]="!appUser?.active">
<i class="fa" [class.fa-check-circle]="appUser?.active" [class.fa-times-circle]="!appUser?.active"></i>
{{ appUser?.active ? 'Active' : 'Inactive' }}
</span>
</td>
<td>
<div class="action-buttons">
<button class="btn-action btn-edit" (click)="onEditUser(appUser)" title="Edit">
<i class="fas fa-edit"></i>
</button>
<button *ngIf="isAdmin" class="btn-action btn-delete" (click)="onDeleteUser(appUser)" title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<form class="form-inline my-2 my-lg-0 justify-content-center">
<input name="searchTerm" #searchTerm="ngModel" class="form-control mr-sm-2" type="search"
placeholder="Search users..."
ngModel (ngModelChange)="searchUsers(searchTerm.value)">
<!-- Empty State -->
<div *ngIf="users.length === 0 && !refreshing" class="empty-state">
<div class="empty-icon">
<i class="fa fa-users"></i>
</div>
<h3>No users found</h3>
<p>Try adjusting your search or add a new user</p>
</div>
</div>
<!-- View User Modal -->
<div *ngIf="selectedUser && showViewUserModal" class="modal-overlay" (click)="closeModal('view')">
<div class="modal-container" (click)="$event.stopPropagation()">
<div class="modal-header">
<h3>User Details</h3>
<button class="modal-close" (click)="closeModal('view')">
<i class="fa fa-times"></i>
</button>
</div>
<div class="modal-body">
<div class="user-detail-card">
<div class="user-detail-header">
<div class="user-detail-avatar">
<img [src]="selectedUser.profileImageUrl" [alt]="selectedUser.username">
</div>
<div class="user-detail-info">
<h4>{{ selectedUser.firstName }} {{ selectedUser.lastName }}</h4>
<p class="username">@{{ selectedUser.username }}</p>
<span class="status-badge" [class.status-active]="selectedUser.active" [class.status-inactive]="!selectedUser.active">
<i class="fa" [class.fa-check-circle]="selectedUser.active" [class.fa-times-circle]="!selectedUser.active"></i>
{{ selectedUser.active ? 'Active' : 'Inactive' }}
</span>
<div *ngIf="selectedUser.lastLoginDateDisplay" class="last-login">
<i class="fa fa-clock"></i>
Last login: {{ selectedUser.lastLoginDateDisplay | date:'medium' }}
</div>
</div>
</div>
<div class="user-detail-list">
<div class="detail-item">
<i class="fa fa-id-badge"></i>
<span class="detail-label">User ID:</span>
<span class="detail-value">{{ selectedUser.userId }}</span>
</div>
<div class="detail-item">
<i class="fa fa-envelope"></i>
<span class="detail-label">Email:</span>
<span class="detail-value">{{ selectedUser.email }}</span>
</div>
<div class="detail-item">
<i class="fas fa-shield-alt"></i>
<span class="detail-label">Role:</span>
<span class="detail-value">{{ selectedUser?.role?.substring(5) }}</span>
</div>
<div class="detail-item">
<i class="fa fa-calendar"></i>
<span class="detail-label">Joined:</span>
<span class="detail-value">{{ selectedUser.joinDate | date:'medium' }}</span>
</div>
<div class="detail-item">
<i class="fa" [class.fa-unlock]="selectedUser?.notLocked" [class.fa-lock]="!selectedUser?.notLocked"></i>
<span class="detail-label">Account:</span>
<span class="detail-value">{{ selectedUser?.notLocked ? 'Unlocked' : 'Locked' }}</span>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" (click)="closeModal('view')">Close</button>
</div>
</div>
</div>
<!-- Add User Modal -->
<div *ngIf="showAddUserModal && isManager" class="modal-overlay" (click)="closeModal('add')">
<div class="modal-container" (click)="$event.stopPropagation()">
<div class="modal-header">
<h3>Add New User</h3>
<button class="modal-close" (click)="closeModal('add')">
<i class="fa fa-times"></i>
</button>
</div>
<div class="modal-body">
<form #newUserForm="ngForm" (ngSubmit)="onAddNewUser(newUserForm)">
<div class="form-row">
<div class="form-group">
<label for="firstName">
<i class="fa fa-user"></i>
First Name
</label>
<input type="text" name="firstName" required ngModel class="form-input" placeholder="Enter first name">
</div>
<div class="form-group">
<label for="lastName">
<i class="fa fa-user"></i>
Last Name
</label>
<input type="text" name="lastName" required ngModel class="form-input" placeholder="Enter last name">
</div>
</div>
<div class="form-group">
<label for="username">
<i class="fa fa-at"></i>
Username
</label>
<input type="text" name="username" required ngModel class="form-input" placeholder="Enter username">
</div>
<div class="form-group">
<label for="email">
<i class="fa fa-envelope"></i>
Email
</label>
<input type="email" name="email" required ngModel class="form-input" placeholder="user@example.com">
</div>
<div class="form-group">
<label for="role">
<i class="fa fa-shield-alt"></i>
Role
</label>
<select *ngIf="isAdmin" name="role" required ngModel="ROLE_USER" class="form-select">
<option value="ROLE_USER">USER</option>
<option value="ROLE_HR">HR</option>
<option value="ROLE_MANAGER">MANAGER</option>
<option value="ROLE_ADMIN">ADMIN</option>
<option value="ROLE_SUPER_ADMIN">SUPER ADMIN</option>
</select>
<input *ngIf="!isAdmin" type="text" name="role" required ngModel="ROLE_USER" readonly class="form-input">
</div>
<div class="form-group">
<label>
<i class="fa fa-image"></i>
Profile Picture
</label>
<div class="file-upload-wrapper">
<input type="file"
id="newUserProfileImage"
accept="image/*"
name="profileImage"
(change)="onProfileImageChange($any($event).target.files)"
class="file-input">
<label for="newUserProfileImage" class="file-label">
<i class="fa fa-cloud-upload-alt"></i>
<span>{{ profileImageFileName || 'Choose profile picture' }}</span>
</label>
</div>
</div>
<div class="form-section">
<div class="checkbox-wrapper">
<input type="checkbox" id="newUserActive" name="active" ngModel class="checkbox-input">
<label for="newUserActive" class="checkbox-label">
<span class="checkbox-custom"></span>
<span class="checkbox-text">
<i class="fa fa-check-circle"></i>
Active
</span>
</label>
</div>
<div class="checkbox-wrapper">
<input type="checkbox" id="newUserUnlocked" name="notLocked" ngModel class="checkbox-input">
<label for="newUserUnlocked" class="checkbox-label">
<span class="checkbox-custom"></span>
<span class="checkbox-text">
<i class="fa fa-unlock"></i>
Unlocked
</span>
</label>
</div>
</div>
<button type="submit" style="display: none;" id="new-user-save"></button>
</form>
<button *ngIf="isManager" type="button" class="btn btn-info" data-bs-toggle="modal"
data-bs-target="#addUserModal">
<i class="fa fa-plus"></i>New User
</button>
</div>
<div class="btn-group">
<button type="button" class="btn btn-info" (click)="getUsers(true)">
<i class="fas fa-sync" [ngClass]="{'fa-spin': refreshing }"></i>
<div class="modal-footer">
<button class="btn-secondary" (click)="closeModal('add')">Cancel</button>
<button class="btn-primary" (click)="saveNewUser()" [disabled]="newUserForm.invalid">
<i class="fa fa-save"></i>
Create User
</button>
</div>
</div>
<table class="table table-hover">
<thead class="table-borderless">
<tr class="text-center">
<th>Photo</th>
<th>User ID</th>
<th>First Name</th>
<th>Last Name</th>
<th>Username</th>
<th>Email</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr class="text-center" *ngFor="let appUser of users">
<td (click)="onSelectUser(appUser)">
<img height="40" width="40" src="{{appUser?.profileImageUrl}}"
class="rounded-circle img-fluid img-thumbnail" alt=""/>
</td>
<td (click)="onSelectUser(appUser)">{{appUser?.userId}}</td>
<td (click)="onSelectUser(appUser)">{{appUser?.firstName}}</td>
<td (click)="onSelectUser(appUser)">{{appUser?.lastName}}</td>
<td (click)="onSelectUser(appUser)">{{appUser?.username}}</td>
<td (click)="onSelectUser(appUser)">{{appUser?.email}}</td>
<td (click)="onSelectUser(appUser)">
<span class="badge" [ngClass]="{ 'bg-success': appUser?.active, 'bg-danger': !appUser?.active }">
{{appUser?.active ? 'Active' : 'Inactive'}}
</span>
</td>
<td class="">
<div class="btn-group">
<button class="btn btn-outline-info" (click)="onEditUser(appUser)"><i class="fas fa-edit"></i></button>
<button *ngIf="isAdmin" class="btn btn-outline-danger" (click)="onDeleteUser(appUser)"><i
class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<button [hidden]="true" type="button" id="openUserInfo" data-bs-toggle="modal" data-bs-target="#viewUserModal">
</button>
<button [hidden]="true" type="button" id="openUserEdit" data-bs-toggle="modal" data-bs-target="#editUserModal">
</button>
<!-- change password -->
<div *ngIf="isAdmin" class="tab-pane fade" id="reset-password">
<form #resetPasswordForm="ngForm" (ngSubmit)="onResetPassword(resetPasswordForm)">
<fieldset>
<legend>User Password Management</legend>
<div class="form-group">
<label for="exampleInputEmail1">Email address</label>
<input type="email" name="reset-password-email" required ngModel class="form-control"
placeholder="Enter email (example@email.com)">
<small class="form-text text-muted">We'll never share your email with anyone else.</small>
</div>
<button type="submit" [disabled]="resetPasswordForm.invalid" class="btn btn-primary">
<i *ngIf="refreshing" class="fas fa-spinner fa-spin"></i>&nbsp;&nbsp;
<span>{{refreshing ? 'Loading...' : 'Reset Password'}}</span>
<!-- Edit User Modal -->
<div *ngIf="showEditUserModal" class="modal-overlay" (click)="closeModal('edit')">
<div class="modal-container" (click)="$event.stopPropagation()">
<div class="modal-header">
<h3>Edit User {{ editUser.firstName }} {{ editUser.lastName }}</h3>
<button class="modal-close" (click)="closeModal('edit')">
<i class="fa fa-times"></i>
</button>
</fieldset>
</form>
</div>
<!-- user profile -->
<div class="tab-pane fade" id="profile">
<div class="container">
<div class="row flex-lg-nowrap">
<div class="col">
<div class="row">
<div class="col mb-3">
<div class="card">
<div class="card-body">
<div class="e-profile">
<div class="row">
<div class="col-12 col-sm-auto">
<div class="mx-auto" style="width: 120px;">
<div class="d-flex justify-content-center align-items-center rounded">
<img class="rounded" height="135" width="135" src="{{loggedInUser?.profileImageUrl}}"
alt="">
</div>
<div *ngIf="fileUploadStatus?.status==='progress'" class="progress mt-1">
<div class="progress-bar bg-info" role="progressbar"
[style.width.%]="fileUploadStatus?.percentage" aria-valuenow="0" aria-valuemin="0"
aria-valuemax="100">{{fileUploadStatus?.percentage}}%
</div>
</div>
</div>
</div>
<div class="col d-flex flex-column flex-sm-row justify-content-between mb-3">
<div class="text-center text-sm-left mb-2 mb-sm-0">
<h4
class="pt-sm-2 pb-1 mb-0 text-nowrap">{{loggedInUser?.firstName}} {{loggedInUser?.lastName}}</h4>
<p class="mb-0">{{loggedInUser?.username}}</p>
<div *ngIf="loggedInUser?.lastLoginDateDisplay !== null" class="text-muted"><small>Last
login:
{{loggedInUser?.lastLoginDateDisplay | date:'medium'}}</small></div>
<div class="mt-2">
<button (click)="updateProfileImage()" class="btn btn-primary" type="button">
<i class="fa fa-fw fa-camera"></i>
<span>Change Photo</span>
</button>
</div>
</div>
<div class="text-center text-sm-right">
<div class="text-muted"><small>Joined {{loggedInUser?.joinDate | date:'mediumDate'}}</small>
</div>
</div>
</div>
</div>
<div class="tab-content pt-3">
<div class="tab-pane active">
<form #profileUserForm="ngForm" (ngSubmit)="onUpdateCurrentUser(profileUserForm.value)"
class="form" novalidate>
<div class="row">
<div class="col">
<div class="row">
<div class="col">
<div class="form-group">
<label>First Name</label>
<input type="text" name="firstName" required [(ngModel)]="loggedInUser.firstName"
class="form-control">
</div>
</div>
<div class="col">
<div class="form-group">
<label>Last Name</label>
<input type="text" name="lastName" required [(ngModel)]="loggedInUser.lastName"
class="form-control">
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="form-group">
<label>Username</label>
<input type="text" name="username" required [(ngModel)]="loggedInUser.username"
class="form-control">
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="form-group">
<label>Email</label>
<input type="text" name="email" required [(ngModel)]="loggedInUser.email"
class="form-control">
</div>
</div>
</div>
<div class="row">
<div class="col mb-3">
<div class="form-group">
<label>Role</label><small [hidden]="isAdmin">(read only)</small>
<select [disabled]="!isAdmin" name="role" required [(ngModel)]="loggedInUser.role"
class="form-control">
<option value="ROLE_USER">USER</option>
<option value="ROLE_HR">HR</option>
<option value="ROLE_MANAGER">MANAGER</option>
<option value="ROLE_ADMIN">ADMIN</option>
<option value="ROLE_SUPER_ADMIN">SUPER ADMIN</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12 col-sm-5 offset-sm-1 mb-3">
<div class="mb-2"><b>Account Settings</b></div>
<div class="row">
<div class="col">
<div class="custom-controls-stacked px-2">
<div class="custom-control custom-checkbox">
<input [disabled]="!isAdmin" name="active" type="checkbox"
[(ngModel)]="loggedInUser.active"
class="custom-control-input">
<label class="custom-control-label">Active</label>
</div>
<div class="custom-control custom-checkbox">
<input [disabled]="!isAdmin" name="notLocked" type="checkbox"
[(ngModel)]="loggedInUser.notLocked" class="custom-control-input">
<label class="custom-control-label">Unlocked</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col d-flex justify-content-end">
<button class="btn btn-primary" type="submit">
<i *ngIf="refreshing" class="fas fa-spinner fa-spin"></i>&nbsp;&nbsp;
<span>{{refreshing ? 'Loading...' : 'Save Changes'}}</span>
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-body">
<form #editUserForm="ngForm">
<div class="form-row">
<div class="form-group">
<label for="editFirstName">
<i class="fa fa-user"></i>
First Name
</label>
<input type="text"
name="firstName"
required
[(ngModel)]="editUser.firstName"
class="form-input"
[disabled]="!isManager">
</div>
<div class="col-12 col-md-3 mb-3">
<div class="card mb-3">
<div class="card-body">
<div class="px-xl-3">
<button (click)="onLogOut()" class="btn btn-block btn-secondary">
<span>Logout</span>
<i class="fas fa-sign-in-alt ml-1"></i>
</button>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<h6 class="card-title font-weight-bold">Permissions From Role</h6>
<h6 *ngFor="let authority of loggedInUser?.authorities" class="card-text">{{authority}}</h6>
</div>
</div>
<div class="form-group">
<label for="editLastName">
<i class="fa fa-user"></i>
Last Name
</label>
<input type="text"
name="lastName"
required
[(ngModel)]="editUser.lastName"
class="form-input"
[disabled]="!isManager">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- modal user info -->
<div *ngIf="selectedUser" class="modal fade bd-example-modal-lg" id="viewUserModal" tabindex="-1" role="dialog"
aria-labelledby=""
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title text-center"
id="exampleModalLongTitle">{{selectedUser.firstName}} {{selectedUser.lastName}}</h5>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div>
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-12 col-sm-auto">
<div class="mx-auto" style="width: 120px;">
<div class="d-flex justify-content-center align-items-center rounded">
<img class="rounded" height="120" width="120" src="{{selectedUser.profileImageUrl}}"
alt="{{selectedUser.username}}">
</div>
</div>
</div>
<div class="col d-flex flex-column flex-sm-row justify-content-between">
<div class="text-center text-sm-left mb-sm-0">
<h6
class="pt-sm-2 pb-1 mb-0 text-nowrap">{{selectedUser.firstName}} {{selectedUser.lastName}}</h6>
<p class="mb-1">{{selectedUser.username}}</p>
<div class="">Status:
<span class="badge"
[ngClass]="{'bg-success':selectedUser.active,'bg-danger':!selectedUser.active}">
{{selectedUser.active ? 'Active' : 'Inactive'}}
</span>
</div>
<div *ngIf="selectedUser.lastLoginDateDisplay" class="text-muted">
<small>Last Login: {{selectedUser.lastLoginDateDisplay | date: 'medium' }}</small>
</div>
</div>
<div class="text-center text-sm-right">
<div class="text-muted"><small>Joined {{selectedUser.joinDate | date: 'medium' }}</small>
</div>
</div>
</div>
</div>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item"></li>
<li class="list-group-item"><i class="fa fa-id-badge float-end"></i>{{selectedUser.userId}}
</li>
<li class="list-group-item"><i class="fa fa-envelope float-end"></i>{{selectedUser.email}}
</li>
<li class="list-group-item"><i class="fas fa-shield-alt float-end"></i>
{{selectedUser?.role?.substring(5)}}
<li *ngIf="selectedUser.lastLoginDateDisplay" class="list-group-item">
<i class="fas fa-sign-in-alt float-end"></i>
{{ selectedUser.lastLoginDateDisplay | date: 'medium' }}
</li>
<li class="list-group-item">
<span>
<i class="fa float-end"
[ngClass]="{'fa-unlock':selectedUser?.notLocked, 'fa-lock':!selectedUser?.notLocked}"
[ngStyle]="{color: selectedUser?.notLocked ? 'green' : 'red'}"></i>
Account {{selectedUser?.notLocked ? 'Unlocked' : 'Locked'}}
</span>
</li>
</ul>
<div class="form-group">
<label for="editUsername">
<i class="fa fa-at"></i>
Username
</label>
<input type="text"
name="username"
required
[(ngModel)]="editUser.username"
class="form-input"
[disabled]="!isManager">
</div>
<div class="form-group">
<label for="editEmail">
<i class="fa fa-envelope"></i>
Email
</label>
<input type="email"
name="email"
required
[(ngModel)]="editUser.email"
class="form-input"
[disabled]="!isManager">
</div>
<div class="form-group">
<label for="editRole">
<i class="fa fa-shield-alt"></i>
Role <small *ngIf="!isAdmin">(read only)</small>
</label>
<select name="role"
required
[(ngModel)]="editUser.role"
class="form-select"
[disabled]="!isAdmin">
<option value="ROLE_USER">USER</option>
<option value="ROLE_HR">HR</option>
<option value="ROLE_MANAGER">MANAGER</option>
<option value="ROLE_ADMIN">ADMIN</option>
<option value="ROLE_SUPER_ADMIN">SUPER ADMIN</option>
</select>
</div>
<div class="form-group">
<label>
<i class="fa fa-image"></i>
Profile Picture
</label>
<div class="file-upload-wrapper">
<input type="file"
id="editUserProfileImage"
accept="image/*"
name="profileImage"
(change)="onProfileImageChange($any($event).target.files)"
class="file-input"
[disabled]="!isManager">
<label for="editUserProfileImage" class="file-label">
<i class="fa fa-cloud-upload-alt"></i>
<span>{{ profileImageFileName || 'Choose profile picture' }}</span>
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- modal add user -->
<div *ngIf="isManager" class="modal draggable fade bd-example-modal-lg" id="addUserModal" tabindex="-1"
role="dialog"
aria-labelledby="exampleModalCenterTitle" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title text-center">New User</h5>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div>
<form #newUserForm="ngForm" (ngSubmit)="onAddNewUser(newUserForm)">
<div class="form-group">
<label for="firstName">First Name</label>
<input type="text" name="firstName" required ngModel class="form-control">
</div>
<div class="form-group">
<label for="lastName">Last Name</label>
<input type="text" name="lastName" required ngModel class="form-control">
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" name="username" required ngModel class="form-control">
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" name="email" required ngModel class="form-control">
</div>
<div *ngIf="isAdmin" class="form-group">
<label for="authority">Role</label>
<select name="role" required ngModel="ROLE_USER" class="form-control">
<option value="ROLE_USER">USER</option>
<option value="ROLE_HR">HR</option>
<option value="ROLE_MANAGER">MANAGER</option>
<option value="ROLE_ADMIN">ADMIN</option>
<option value="ROLE_SUPER_ADMIN">SUPER ADMIN</option>
</select>
</div>
<div *ngIf="!isAdmin" class="form-group">
<label for="authority">Role</label>
<input type="text" name="role" required ngModel="USER" readonly class="form-control">
</div>
<div class="input-group mb-2">
<div class="input-group-prepend">
<span class="input-group-text">Profile Picture </span>
</div>
<div class="custom-file">
<input type="file" accept="image/*" name="profileImage"
(change)="onProfileImageChange($any($event).target.files)"
class="custom-file-input">
<label class="custom-file-label">
<span>{{profileImageFileName ? profileImageFileName : 'Choose File'}}</span>
</label>
</div>
</div>
<fieldset class="form-group">
<div class="form-check">
<label class="form-check-label">
<input type="checkbox" name="active" ngModel class="form-check-input">
Active
</label>
</div>
<div class="form-check disabled">
<label class="form-check-label">
<input type="checkbox" name="notLocked" ngModel class="form-check-input">
Unlocked
</label>
</div>
</fieldset>
<button type="submit" style="display: none;" id="new-user-save"></button>
</form>
<div class="form-section">
<div class="checkbox-wrapper">
<input type="checkbox"
id="editUserActive"
name="active"
[(ngModel)]="editUser.active"
class="checkbox-input"
[disabled]="!isManager">
<label for="editUserActive" class="checkbox-label">
<span class="checkbox-custom"></span>
<span class="checkbox-text">
<i class="fa fa-check-circle"></i>
Active <small *ngIf="!isManager">(read only)</small>
</span>
</label>
</div>
<div class="checkbox-wrapper">
<input type="checkbox"
id="editUserUnlocked"
name="notLocked"
[(ngModel)]="editUser.notLocked"
class="checkbox-input"
[disabled]="!isManager">
<label for="editUserUnlocked" class="checkbox-label">
<span class="checkbox-custom"></span>
<span class="checkbox-text">
<i class="fa fa-unlock"></i>
Unlocked <small *ngIf="!isManager">(read only)</small>
</span>
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="new-user-close">Close</button>
<button type="button" class="btn btn-primary" (click)="saveNewUser()" [disabled]="newUserForm.invalid">Save
changes
</button>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn-secondary" (click)="closeModal('edit')">Cancel</button>
<button *ngIf="isManager"
class="btn-primary"
(click)="onUpdateUser()"
[disabled]="editUserForm.invalid">
<i class="fa fa-save"></i>
Save Changes
</button>
</div>
</div>
</div>
<!-- modal edit user -->
<div class="modal draggable fade bd-example-modal-lg" id="editUserModal" tabindex="-1" role="dialog"
aria-labelledby="exampleModalCenterTitle" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title text-center">Edit {{editUser.firstName}} {{editUser.lastName}}
<small [hidden]="isManager"> (read only)</small>
</h5>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div>
<form #editUserForm="ngForm">
<div class="form-group">
<label for="firstName">First Name</label>
<input type="text" name="firstName" required [(ngModel)]="editUser.firstName" class="form-control"
[disabled]="!isManager">
</div>
<div class="form-group">
<label for="lastName">Last Name</label>
<input type="text" name="lastName" required [(ngModel)]="editUser.lastName" class="form-control"
[disabled]="!isManager">
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" name="username" required [(ngModel)]="editUser.username" class="form-control"
[disabled]="!isManager">
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" name="email" required [(ngModel)]="editUser.email" class="form-control"
[disabled]="!isManager">
</div>
<div class="form-group">
<label for="authority">Role<small [hidden]="isAdmin"> (read only)</small></label>
<select name="role" required [(ngModel)]="editUser.role" class="form-control"
[disabled]="!isAdmin">
<option value="ROLE_USER">USER</option>
<option value="ROLE_HR">HR</option>
<option value="ROLE_MANAGER">MANAGER</option>
<option value="ROLE_ADMIN">ADMIN</option>
<option value="ROLE_SUPER_ADMIN">SUPER ADMIN</option>
</select>
</div>
<div class="input-group mb-2">
<div class="input-group-prepend">
<span class="input-group-text">Profile Picture </span>
</div>
<div class="custom-file">
<input type="file" accept="image/*" name="profileImage"
(change)="onProfileImageChange($any($event).target.files)"
class="custom-file-input" [disabled]="!isManager">
<label class="custom-file-label">
<span>{{profileImageFileName ? profileImageFileName : 'Choose File'}}</span>
</label>
</div>
</div>
<fieldset class="form-group">
<div class="form-check">
<label class="form-check-label">
<input type="checkbox" name="active" [(ngModel)]="editUser.active" class="form-check-input"
[disabled]="!isManager">
Active<small [hidden]="isManager"> (read only)</small>
</label>
</div>
<div class="form-check disabled">
<label class="form-check-label">
<input type="checkbox" name="notLocked" [(ngModel)]="editUser.notLocked" class="form-check-input"
[disabled]="!isManager">
Unlocked<small [hidden]="isManager"> (read only)</small>
</label>
</div>
</fieldset>
</form>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="closeEditUserButton">Close
</button>
<button *ngIf="isManager" type="button" class="btn btn-primary" (click)="onUpdateUser()"
[disabled]="editUserForm.invalid">
Save changes
</button>
</div>
</div>
</div>
</div>
<!-- profile image change form -->
<!-- Profile Image Change Form (Hidden) -->
<form enctype="multipart/form-data" style="display:none;">
<input type="file"
(change)="onProfileImageChange($any($event).target.files); onUpdateProfileImage()"
name="profile-image-input" id="profile-image-input" placeholder="file" accept="image/*"/>
name="profile-image-input"
id="profile-image-input"
accept="image/*"/>
</form>
</div>
</div>
</div>

View File

@ -1,21 +1,17 @@
import {Component, ElementRef, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {BehaviorSubject} from "rxjs";
import {User} from "../../model/user";
import {UserService} from "../../service/user.service";
import {NotificationService} from "../../service/notification.service";
import {NotificationType} from "../../notification/notification-type";
import {HttpErrorResponse, HttpEvent, HttpEventType} from "@angular/common/http";
import {NgForm} from "@angular/forms";
import {CustomHttpResponse} from "../../dto/custom-http-response";
import { Component, ElementRef, OnDestroy, OnInit, ViewChild, Renderer2 } from '@angular/core';
import { BehaviorSubject } from "rxjs";
import { User } from "../../model/user";
import { UserService } from "../../service/user.service";
import { NotificationService } from "../../service/notification.service";
import { NotificationType } from "../../notification/notification-type";
import { HttpErrorResponse, HttpEvent, HttpEventType } from "@angular/common/http";
import { NgForm } from "@angular/forms";
import { CustomHttpResponse } from "../../dto/custom-http-response";
import { AuthenticationService } from 'src/app/service/authentication.service';
;
import {Router} from "@angular/router";
import {FileUploadStatus} from "../../model/file-upload.status";
import {Role} from "../../enum/role.enum";
import {SubSink} from "subsink";
import { Router } from "@angular/router";
import { FileUploadStatus } from "../../model/file-upload.status";
import { Role } from "../../enum/role.enum";
import { SubSink } from "subsink";
@Component({
selector: 'app-user',
@ -27,24 +23,28 @@ export class UserComponent implements OnInit, OnDestroy {
private titleSubject = new BehaviorSubject<string>('Users');
public titleAction$ = this.titleSubject.asObservable();
public users: User[] = [];
public loggedInUser: User;
public refreshing: boolean;
private subs = new SubSink();
public selectedUser: User;
public profileImageFileName: string | null;
public profileImage: File | null;
public editUser: User = new User();
public fileUploadStatus: FileUploadStatus = new FileUploadStatus();
constructor(private userService: UserService,
private notificationService: NotificationService,
private authenticationService: AuthenticationService,
private router: Router) {
// Modal visibility flags
public showViewUserModal = false;
public showAddUserModal = false;
public showEditUserModal = false;
constructor(
private userService: UserService,
private notificationService: NotificationService,
private authenticationService: AuthenticationService,
private router: Router,
private renderer: Renderer2
) {
}
ngOnInit(): void {
@ -64,7 +64,6 @@ export class UserComponent implements OnInit, OnDestroy {
this.titleSubject.next(title);
}
public getUsers(showNotification: boolean) {
this.refreshing = true;
@ -84,12 +83,11 @@ export class UserComponent implements OnInit, OnDestroy {
this.refreshing = false;
}
);
}
public onSelectUser(selectedUser: User): void {
this.selectedUser = selectedUser;
this.clickButton('openUserInfo');
this.openModal('view');
}
public onProfileImageChange(fileList: FileList): void {
@ -110,7 +108,7 @@ export class UserComponent implements OnInit, OnDestroy {
this.subs.sink = this.userService.addUser(formData)
.subscribe(
(user: User) => {
this.clickButton('new-user-close');
this.closeModal('add');
this.getUsers(false);
this.invalidateVariables();
userForm.reset();
@ -128,11 +126,14 @@ export class UserComponent implements OnInit, OnDestroy {
}
public saveNewUser(): void {
this.clickButton('new-user-save');
// This will trigger form submission
const saveButton = document.getElementById('new-user-save');
if (saveButton) {
saveButton.click();
}
}
public searchUsers(searchTerm: string): void {
if (!searchTerm) {
this.users = this.userService.getUsersFromLocalStorage();
return;
@ -150,16 +151,11 @@ export class UserComponent implements OnInit, OnDestroy {
}
}
this.users = matchUsers;
}
private clickButton(buttonId: string): void {
document.getElementById(buttonId)?.click();
}
public onEditUser(user: User): void {
this.editUser = user;
this.clickButton('openUserEdit');
this.editUser = { ...user }; // Create a copy to avoid mutating original
this.openModal('edit');
}
public onUpdateUser(): void {
@ -167,7 +163,7 @@ export class UserComponent implements OnInit, OnDestroy {
this.subs.sink = this.userService.updateUser(this.editUser.userId, formData)
.subscribe(
(user: User) => {
this.clickButton('closeEditUserButton');
this.closeModal('edit');
this.getUsers(false);
this.invalidateVariables();
this.notificationService.notify(NotificationType.SUCCESS, `User ${user.username} updated successfully`);
@ -243,7 +239,10 @@ export class UserComponent implements OnInit, OnDestroy {
}
public updateProfileImage(): void {
this.clickButton('profile-image-input');
const input = document.getElementById('profile-image-input');
if (input) {
input.click();
}
}
public onUpdateProfileImage(): void {
@ -270,7 +269,6 @@ export class UserComponent implements OnInit, OnDestroy {
}
private reportUploadProgress(event: HttpEvent<any>): void {
switch (event.type) {
case HttpEventType.UploadProgress:
this.fileUploadStatus.percentage = Math.round(100 * event.loaded / event.total!);
@ -288,23 +286,45 @@ export class UserComponent implements OnInit, OnDestroy {
break;
default:
this.fileUploadStatus.status = 'default';
}
}
@ViewChild('addUserModal') addUserModal!: ElementRef;
openAddUserModal() {
console.log("clicked")
if (this.addUserModal) {
// const modalInstance = new Modal(this.addUserModal.nativeElement);
// modalInstance.show();
// Modal control methods
public openModal(type: 'view' | 'add' | 'edit'): void {
switch (type) {
case 'view':
this.showViewUserModal = true;
break;
case 'add':
this.showAddUserModal = true;
break;
case 'edit':
this.showEditUserModal = true;
break;
}
// Prevent body scroll when modal is open
document.body.style.overflow = 'hidden';
}
public closeModal(type: 'view' | 'add' | 'edit'): void {
switch (type) {
case 'view':
this.showViewUserModal = false;
break;
case 'add':
this.showAddUserModal = false;
this.profileImageFileName = null;
this.profileImage = null;
break;
case 'edit':
this.showEditUserModal = false;
this.profileImageFileName = null;
this.profileImage = null;
break;
}
// Restore body scroll
document.body.style.overflow = 'auto';
}
public get isAdmin(): boolean {
return this.loggedInUser.role === Role.ADMIN || this.loggedInUser.role === Role.SUPER_ADMIN;
@ -313,4 +333,4 @@ export class UserComponent implements OnInit, OnDestroy {
public get isManager(): boolean {
return this.isAdmin || this.loggedInUser.role === Role.MANAGER;
}
}
}

View File

@ -0,0 +1,19 @@
export interface Milestone {
id?: number;
title: string;
description: string;
// displayOrder: number;
isActive: boolean;
milestoneDate?: Date | string;
createdAt?: Date | string;
updatedAt?: Date | string;
}
export interface MilestoneDTO {
id?: number;
title: string;
description: string;
displayOrder: number;
isActive: boolean;
milestoneDate?: string;
}

View File

@ -0,0 +1,13 @@
export interface Testimonial {
id?: number;
name: string;
age: number;
title: string;
story: string;
outcome: string;
impact: string;
category: string;
isActive: boolean;
createdAt?: Date;
updatedAt?: Date;
}

View File

@ -0,0 +1,77 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, forkJoin, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { environment } from 'src/environments/environment';
export interface Professor {
id?: number;
firstName: string;
lastName: string;
status: string;
category: string;
}
export interface Testimonial {
id?: number;
name: string;
isActive: boolean;
}
@Injectable({
providedIn: 'root'
})
export class DashboardService {
private apiUrl = environment.apiUrl;
constructor(private http: HttpClient) {}
// Get professors data
getProfessors(): Observable<Professor[]> {
return this.http.get<any>(`${this.apiUrl}/professor`) // Changed from /api/professors
.pipe(
map(response => response.content || []), // Extract content from Page
catchError(error => {
console.error('Error fetching professors:', error);
return of([]);
})
);
}
// Get testimonials data
getTestimonials(): Observable<Testimonial[]> {
return this.http.get<Testimonial[]>(`${this.apiUrl}/api/testimonials`)
.pipe(
catchError(error => {
console.error('Error fetching testimonials:', error);
return of([]);
})
);
}
// Get all dashboard data
getAllDashboardData(): Observable<any> {
return forkJoin({
professors: this.getProfessors(),
testimonials: this.getTestimonials()
}).pipe(
map(data => ({
professors: {
total: data.professors.length,
active: data.professors.filter(p => p.status === 'ACTIVE').length
},
testimonials: {
total: data.testimonials.length,
active: data.testimonials.filter(t => t.isActive).length
}
})),
catchError(error => {
console.error('Error fetching dashboard data:', error);
return of({
professors: { total: 0, active: 0 },
testimonials: { total: 0, active: 0 }
});
})
);
}
}

View File

@ -0,0 +1,57 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
import { Milestone, MilestoneDTO } from '../model/milestone.model';
@Injectable({
providedIn: 'root'
})
export class MilestoneService {
private apiUrl = `${environment.apiUrl}/api/milestones`;
constructor(private http: HttpClient) { }
private getHeaders(): HttpHeaders {
const token = localStorage.getItem('token');
return new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
});
}
// Get all milestones (admin)
getAllMilestones(): Observable<Milestone[]> {
return this.http.get<Milestone[]>(this.apiUrl, { headers: this.getHeaders() });
}
// Get active milestones (public)
getActiveMilestones(): Observable<Milestone[]> {
return this.http.get<Milestone[]>(`${this.apiUrl}/public`);
}
// Get milestone by ID
getMilestoneById(id: number): Observable<Milestone> {
return this.http.get<Milestone>(`${this.apiUrl}/${id}`, { headers: this.getHeaders() });
}
// Create milestone
createMilestone(milestone: MilestoneDTO): Observable<Milestone> {
return this.http.post<Milestone>(this.apiUrl, milestone, { headers: this.getHeaders() });
}
// Update milestone
updateMilestone(id: number, milestone: MilestoneDTO): Observable<Milestone> {
return this.http.put<Milestone>(`${this.apiUrl}/${id}`, milestone, { headers: this.getHeaders() });
}
// Delete milestone
deleteMilestone(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`, { headers: this.getHeaders() });
}
// Reorder milestones
reorderMilestones(orderedIds: number[]): Observable<void> {
return this.http.put<void>(`${this.apiUrl}/reorder`, orderedIds, { headers: this.getHeaders() });
}
}

View File

@ -0,0 +1,38 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Testimonial } from '../model/testimonial.model';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class TestimonialService {
private apiUrl = `${environment.apiUrl}/api/testimonials`;
constructor(private http: HttpClient) {}
getAllTestimonials(): Observable<Testimonial[]> {
return this.http.get<Testimonial[]>(this.apiUrl);
}
getActiveTestimonials(): Observable<Testimonial[]> {
return this.http.get<Testimonial[]>(`${this.apiUrl}/public`);
}
getTestimonialById(id: number): Observable<Testimonial> {
return this.http.get<Testimonial>(`${this.apiUrl}/${id}`);
}
createTestimonial(testimonial: Testimonial): Observable<Testimonial> {
return this.http.post<Testimonial>(this.apiUrl, testimonial);
}
updateTestimonial(id: number, testimonial: Testimonial): Observable<Testimonial> {
return this.http.put<Testimonial>(`${this.apiUrl}/${id}`, testimonial);
}
deleteTestimonial(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 948 B

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<base href="/" />
<title>CMC - College</title>
<title>CMC - Dashboard</title>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />