Further updates on 03-11-2025
This commit is contained in:
@ -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}")
|
||||
|
||||
@ -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}")
|
||||
|
||||
@ -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();
|
||||
// }
|
||||
}
|
||||
@ -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!";
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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:
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
support-portal-frontend/package-lock.json
generated
31
support-portal-frontend/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
@ -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">✓</span>
|
||||
<!-- Check mark for posted -->
|
||||
<span *ngIf="!blog.posted" class="text-danger">✗</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>
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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: ['']
|
||||
}));
|
||||
}
|
||||
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
<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>
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
<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>
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
<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>
|
||||
@ -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;
|
||||
}
|
||||
*/
|
||||
@ -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>
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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 '';
|
||||
}
|
||||
}
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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'
|
||||
});
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
<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>
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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) + '...';
|
||||
}
|
||||
}
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
<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>
|
||||
<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">×</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">×</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">×</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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
support-portal-frontend/src/app/model/milestone.model.ts
Normal file
19
support-portal-frontend/src/app/model/milestone.model.ts
Normal 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;
|
||||
}
|
||||
13
support-portal-frontend/src/app/model/testimonial.model.ts
Normal file
13
support-portal-frontend/src/app/model/testimonial.model.ts
Normal 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;
|
||||
}
|
||||
77
support-portal-frontend/src/app/service/dashboard.service.ts
Normal file
77
support-portal-frontend/src/app/service/dashboard.service.ts
Normal 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 }
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
57
support-portal-frontend/src/app/service/milestone.service.ts
Normal file
57
support-portal-frontend/src/app/service/milestone.service.ts
Normal 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() });
|
||||
}
|
||||
}
|
||||
@ -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}`);
|
||||
}
|
||||
}
|
||||
BIN
support-portal-frontend/src/assets/images/cmc/logo.png
Normal file
BIN
support-portal-frontend/src/assets/images/cmc/logo.png
Normal file
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 |
@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user