feat: add doctor and specialty management modules with full CRUD support and database seeding
This commit is contained in:
+12
-3
@@ -34,12 +34,17 @@ public class PageController {
|
||||
private final ObjectMapper objectMapper;
|
||||
private final HookManager hookManager;
|
||||
private final com.sisvietnamvn.web.service.HtmlSnippetService snippetService;
|
||||
private final com.sisvietnamvn.web.service.DoctorService doctorService;
|
||||
private final com.sisvietnamvn.web.service.SpecialtyService specialtyService;
|
||||
|
||||
public PageController(PageService pageService, ObjectMapper objectMapper, HookManager hookManager, com.sisvietnamvn.web.service.HtmlSnippetService snippetService) {
|
||||
public PageController(PageService pageService, ObjectMapper objectMapper, HookManager hookManager, com.sisvietnamvn.web.service.HtmlSnippetService snippetService,
|
||||
com.sisvietnamvn.web.service.DoctorService doctorService, com.sisvietnamvn.web.service.SpecialtyService specialtyService) {
|
||||
this.pageService = pageService;
|
||||
this.objectMapper = objectMapper;
|
||||
this.hookManager = hookManager;
|
||||
this.snippetService = snippetService;
|
||||
this.doctorService = doctorService;
|
||||
this.specialtyService = specialtyService;
|
||||
}
|
||||
|
||||
@GetMapping("/page/{slug}")
|
||||
@@ -68,7 +73,11 @@ public class PageController {
|
||||
public String getSpecialty(Model model) { return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.SPECIALTY), model); }
|
||||
|
||||
@GetMapping("/doctor")
|
||||
public String getDoctor(Model model) { return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.DOCTOR), model); }
|
||||
public String getDoctor(Model model) {
|
||||
model.addAttribute("doctors", doctorService.findAll());
|
||||
model.addAttribute("specialties", specialtyService.findAll());
|
||||
return "doctor";
|
||||
}
|
||||
|
||||
@GetMapping("/service")
|
||||
public String getService(Model model) { return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.SERVICE), model); }
|
||||
@@ -100,7 +109,7 @@ public class PageController {
|
||||
}
|
||||
Page page = pageOpt.get();
|
||||
// Security Check: If Draft, only Admins can view
|
||||
if ("DRAFT".equals(page.getStatus()) && !SecurityUtils.hasCurrentUserThisAuthority(AuthoritiesConstants.ADMIN)) {
|
||||
if (com.sisvietnamvn.web.domain.PageStatus.DRAFT.equals(page.getStatus()) && !SecurityUtils.hasCurrentUserThisAuthority(AuthoritiesConstants.ADMIN)) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Page not found");
|
||||
}
|
||||
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package com.sisvietnamvn.web.controller.manage;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Doctor;
|
||||
import com.sisvietnamvn.web.service.DoctorService;
|
||||
import com.sisvietnamvn.web.service.SpecialtyService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
import com.sisvietnamvn.web.security.AuthoritiesConstants;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/manage/doctors")
|
||||
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\")")
|
||||
public class ManageDoctorController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ManageDoctorController.class);
|
||||
|
||||
private final DoctorService doctorService;
|
||||
private final SpecialtyService specialtyService;
|
||||
|
||||
public ManageDoctorController(DoctorService doctorService, SpecialtyService specialtyService) {
|
||||
this.doctorService = doctorService;
|
||||
this.specialtyService = specialtyService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String listDoctors(Model model) {
|
||||
LOG.debug("Request to list all doctors");
|
||||
populateModel(model, new Doctor(), true);
|
||||
return "manage/doctors/index";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String createDoctor(@Valid @ModelAttribute("doctor") Doctor doctor,
|
||||
BindingResult bindingResult,
|
||||
@RequestParam(value = "specialtyId", required = false) Long specialtyId,
|
||||
Model model,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to create Doctor : {}", doctor);
|
||||
if (bindingResult.hasErrors()) {
|
||||
populateModel(model, doctor, true);
|
||||
return "manage/doctors/index";
|
||||
}
|
||||
if (specialtyId != null) {
|
||||
specialtyService.findById(specialtyId).ifPresent(doctor::setSpecialty);
|
||||
}
|
||||
doctorService.save(doctor);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Doctor created successfully!");
|
||||
return "redirect:/manage/doctors";
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/edit")
|
||||
public String showEditForm(@PathVariable Long id, Model model, RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to show edit form for Doctor : {}", id);
|
||||
Optional<Doctor> doctorOptional = doctorService.findById(id);
|
||||
if (doctorOptional.isEmpty()) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "Doctor not found.");
|
||||
return "redirect:/manage/doctors";
|
||||
}
|
||||
populateModel(model, doctorOptional.get(), false);
|
||||
return "manage/doctors/index";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}")
|
||||
public String updateDoctor(@PathVariable Long id,
|
||||
@Valid @ModelAttribute("doctor") Doctor doctor,
|
||||
BindingResult bindingResult,
|
||||
@RequestParam(value = "specialtyId", required = false) Long specialtyId,
|
||||
Model model,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to update Doctor : {}", id);
|
||||
if (bindingResult.hasErrors()) {
|
||||
populateModel(model, doctor, false);
|
||||
return "manage/doctors/index";
|
||||
}
|
||||
doctor.setId(id);
|
||||
if (specialtyId != null) {
|
||||
specialtyService.findById(specialtyId).ifPresent(doctor::setSpecialty);
|
||||
} else {
|
||||
doctor.setSpecialty(null);
|
||||
}
|
||||
doctorService.save(doctor);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Doctor updated successfully!");
|
||||
return "redirect:/manage/doctors";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/delete")
|
||||
public String deleteDoctor(@PathVariable Long id, RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to delete Doctor : {}", id);
|
||||
try {
|
||||
doctorService.deleteById(id);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Doctor deleted successfully!");
|
||||
} catch (Exception e) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "Failed to delete doctor: " + e.getMessage());
|
||||
}
|
||||
return "redirect:/manage/doctors";
|
||||
}
|
||||
|
||||
private void populateModel(Model model, Doctor doctor, boolean isNew) {
|
||||
model.addAttribute("doctor", doctor);
|
||||
model.addAttribute("isNew", isNew);
|
||||
model.addAttribute("allDoctors", doctorService.findAll());
|
||||
model.addAttribute("allSpecialties", specialtyService.findAll());
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package com.sisvietnamvn.web.controller.manage;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Specialty;
|
||||
import com.sisvietnamvn.web.service.SpecialtyService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
import com.sisvietnamvn.web.security.AuthoritiesConstants;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/manage/specialties")
|
||||
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\")")
|
||||
public class ManageSpecialtyController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ManageSpecialtyController.class);
|
||||
|
||||
private final SpecialtyService specialtyService;
|
||||
|
||||
public ManageSpecialtyController(SpecialtyService specialtyService) {
|
||||
this.specialtyService = specialtyService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String listSpecialties(Model model) {
|
||||
LOG.debug("Request to list all specialties");
|
||||
populateModel(model, new Specialty(), true);
|
||||
return "manage/specialties/index";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String createSpecialty(@Valid @ModelAttribute("specialty") Specialty specialty,
|
||||
BindingResult bindingResult,
|
||||
Model model,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to create Specialty : {}", specialty);
|
||||
if (bindingResult.hasErrors()) {
|
||||
populateModel(model, specialty, true);
|
||||
return "manage/specialties/index";
|
||||
}
|
||||
specialtyService.save(specialty);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Specialty created successfully!");
|
||||
return "redirect:/manage/specialties";
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/edit")
|
||||
public String showEditForm(@PathVariable Long id, Model model, RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to show edit form for Specialty : {}", id);
|
||||
Optional<Specialty> specialtyOptional = specialtyService.findById(id);
|
||||
if (specialtyOptional.isEmpty()) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "Specialty not found.");
|
||||
return "redirect:/manage/specialties";
|
||||
}
|
||||
populateModel(model, specialtyOptional.get(), false);
|
||||
return "manage/specialties/index";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}")
|
||||
public String updateSpecialty(@PathVariable Long id,
|
||||
@Valid @ModelAttribute("specialty") Specialty specialty,
|
||||
BindingResult bindingResult,
|
||||
Model model,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to update Specialty : {}", id);
|
||||
if (bindingResult.hasErrors()) {
|
||||
populateModel(model, specialty, false);
|
||||
return "manage/specialties/index";
|
||||
}
|
||||
specialty.setId(id);
|
||||
specialtyService.save(specialty);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Specialty updated successfully!");
|
||||
return "redirect:/manage/specialties";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/delete")
|
||||
public String deleteSpecialty(@PathVariable Long id, RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to delete Specialty : {}", id);
|
||||
try {
|
||||
specialtyService.deleteById(id);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Specialty deleted successfully!");
|
||||
} catch (Exception e) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "Failed to delete specialty: " + e.getMessage());
|
||||
}
|
||||
return "redirect:/manage/specialties";
|
||||
}
|
||||
|
||||
private void populateModel(Model model, Specialty specialty, boolean isNew) {
|
||||
model.addAttribute("specialty", specialty);
|
||||
model.addAttribute("isNew", isNew);
|
||||
model.addAttribute("allSpecialties", specialtyService.findAll());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.sisvietnamvn.web.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* A Doctor entity.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "sis_doctor")
|
||||
public class Doctor extends AbstractAuditingEntity<Long> {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||
@SequenceGenerator(name = "sequenceGenerator")
|
||||
private Long id;
|
||||
|
||||
@NotNull
|
||||
@Size(max = 255)
|
||||
@Column(name = "name", length = 255, nullable = false)
|
||||
private String name;
|
||||
|
||||
@Size(max = 100)
|
||||
@Column(name = "title", length = 100)
|
||||
private String title;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "specialty_id")
|
||||
private Specialty specialty;
|
||||
|
||||
@Size(max = 500)
|
||||
@Column(name = "avatar_url", length = 500)
|
||||
private String avatarUrl;
|
||||
|
||||
@Size(max = 500)
|
||||
@Column(name = "booking_url", length = 500)
|
||||
private String bookingUrl;
|
||||
|
||||
// --- Getters and Setters ---
|
||||
|
||||
@Override
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public Specialty getSpecialty() {
|
||||
return specialty;
|
||||
}
|
||||
|
||||
public void setSpecialty(Specialty specialty) {
|
||||
this.specialty = specialty;
|
||||
}
|
||||
|
||||
public String getAvatarUrl() {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public void setAvatarUrl(String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
public String getBookingUrl() {
|
||||
return bookingUrl;
|
||||
}
|
||||
|
||||
public void setBookingUrl(String bookingUrl) {
|
||||
this.bookingUrl = bookingUrl;
|
||||
}
|
||||
|
||||
// --- equals, hashCode, toString ---
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Doctor doctor)) return false;
|
||||
return id != null && id.equals(doctor.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Doctor{" +
|
||||
"id=" + getId() +
|
||||
", name='" + getName() + "'" +
|
||||
", title='" + getTitle() + "'" +
|
||||
", avatarUrl='" + getAvatarUrl() + "'" +
|
||||
", bookingUrl='" + getBookingUrl() + "'" +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.sisvietnamvn.web.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* A Specialty entity representing a medical department.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "sis_specialty")
|
||||
public class Specialty extends AbstractAuditingEntity<Long> {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||
@SequenceGenerator(name = "sequenceGenerator")
|
||||
private Long id;
|
||||
|
||||
@NotNull
|
||||
@Size(max = 255)
|
||||
@Column(name = "name", length = 255, nullable = false)
|
||||
private String name;
|
||||
|
||||
@NotNull
|
||||
@Size(max = 255)
|
||||
@Column(name = "slug", length = 255, nullable = false, unique = true)
|
||||
private String slug;
|
||||
|
||||
@Size(max = 500)
|
||||
@Column(name = "icon_url", length = 500)
|
||||
private String iconUrl;
|
||||
|
||||
// --- Getters and Setters ---
|
||||
|
||||
@Override
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getSlug() {
|
||||
return slug;
|
||||
}
|
||||
|
||||
public void setSlug(String slug) {
|
||||
this.slug = slug;
|
||||
}
|
||||
|
||||
public String getIconUrl() {
|
||||
return iconUrl;
|
||||
}
|
||||
|
||||
public void setIconUrl(String iconUrl) {
|
||||
this.iconUrl = iconUrl;
|
||||
}
|
||||
|
||||
// --- equals, hashCode, toString ---
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Specialty specialty)) return false;
|
||||
return id != null && id.equals(specialty.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Specialty{" +
|
||||
"id=" + getId() +
|
||||
", name='" + getName() + "'" +
|
||||
", slug='" + getSlug() + "'" +
|
||||
", iconUrl='" + getIconUrl() + "'" +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sisvietnamvn.web.repository;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Doctor;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface DoctorRepository extends JpaRepository<Doctor, Long> {
|
||||
@org.springframework.data.jpa.repository.EntityGraph(attributePaths = {"specialty"})
|
||||
List<Doctor> findAllByOrderByCreatedDateDesc();
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.sisvietnamvn.web.repository;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Specialty;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface SpecialtyRepository extends JpaRepository<Specialty, Long> {
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.sisvietnamvn.web.service;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Doctor;
|
||||
import com.sisvietnamvn.web.repository.DoctorRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class DoctorService {
|
||||
|
||||
private final DoctorRepository doctorRepository;
|
||||
|
||||
public DoctorService(DoctorRepository doctorRepository) {
|
||||
this.doctorRepository = doctorRepository;
|
||||
}
|
||||
|
||||
public List<Doctor> findAll() {
|
||||
return doctorRepository.findAllByOrderByCreatedDateDesc();
|
||||
}
|
||||
|
||||
public Optional<Doctor> findById(Long id) {
|
||||
return doctorRepository.findById(id);
|
||||
}
|
||||
|
||||
public Doctor save(Doctor doctor) {
|
||||
return doctorRepository.save(doctor);
|
||||
}
|
||||
|
||||
public void deleteById(Long id) {
|
||||
doctorRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.sisvietnamvn.web.service;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Specialty;
|
||||
import com.sisvietnamvn.web.repository.SpecialtyRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class SpecialtyService {
|
||||
|
||||
private final SpecialtyRepository specialtyRepository;
|
||||
|
||||
public SpecialtyService(SpecialtyRepository specialtyRepository) {
|
||||
this.specialtyRepository = specialtyRepository;
|
||||
}
|
||||
|
||||
public List<Specialty> findAll() {
|
||||
return specialtyRepository.findAll();
|
||||
}
|
||||
|
||||
public Optional<Specialty> findById(Long id) {
|
||||
return specialtyRepository.findById(id);
|
||||
}
|
||||
|
||||
public Specialty save(Specialty specialty) {
|
||||
return specialtyRepository.save(specialty);
|
||||
}
|
||||
|
||||
public void deleteById(Long id) {
|
||||
specialtyRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
+1
@@ -6,6 +6,7 @@
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.20.xsd">
|
||||
|
||||
<changeSet id="20260714080000-1" author="antigravity">
|
||||
<validCheckSum>ANY</validCheckSum>
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM sis_component_template WHERE slug = 'news-grid'</sqlCheck>
|
||||
</preConditions>
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||
|
||||
<changeSet id="20260718100000-1" author="antigravity">
|
||||
<createTable tableName="sis_specialty">
|
||||
<column name="id" type="bigint">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<column name="name" type="varchar(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="slug" type="varchar(255)">
|
||||
<constraints nullable="false" unique="true"/>
|
||||
</column>
|
||||
<column name="icon_url" type="varchar(500)"/>
|
||||
<column name="created_by" type="varchar(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_date" type="timestamp"/>
|
||||
<column name="last_modified_by" type="varchar(50)"/>
|
||||
<column name="last_modified_date" type="timestamp"/>
|
||||
</createTable>
|
||||
|
||||
<createTable tableName="sis_doctor">
|
||||
<column name="id" type="bigint">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<column name="name" type="varchar(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="title" type="varchar(100)"/>
|
||||
<column name="specialty_id" type="bigint">
|
||||
<constraints nullable="true"/>
|
||||
</column>
|
||||
<column name="avatar_url" type="varchar(500)"/>
|
||||
<column name="booking_url" type="varchar(500)"/>
|
||||
<column name="created_by" type="varchar(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_date" type="timestamp"/>
|
||||
<column name="last_modified_by" type="varchar(50)"/>
|
||||
<column name="last_modified_date" type="timestamp"/>
|
||||
</createTable>
|
||||
|
||||
<addForeignKeyConstraint baseColumnNames="specialty_id"
|
||||
baseTableName="sis_doctor"
|
||||
constraintName="fk_doctor_specialty_id"
|
||||
referencedColumnNames="id"
|
||||
referencedTableName="sis_specialty"/>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||
|
||||
<changeSet id="20260718110000-1" author="antigravity" context="dev, faker">
|
||||
<insert tableName="sis_specialty">
|
||||
<column name="id" value="1"/>
|
||||
<column name="name" value="Khoa Tim mạch"/>
|
||||
<column name="slug" value="khoa-tim-mach"/>
|
||||
<column name="icon_url" value="https://cdn-icons-png.flaticon.com/512/883/883360.png"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_specialty">
|
||||
<column name="id" value="2"/>
|
||||
<column name="name" value="Khoa Thần kinh"/>
|
||||
<column name="slug" value="khoa-than-kinh"/>
|
||||
<column name="icon_url" value="https://cdn-icons-png.flaticon.com/512/2093/2093153.png"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_specialty">
|
||||
<column name="id" value="3"/>
|
||||
<column name="name" value="Khoa Tiêu hóa"/>
|
||||
<column name="slug" value="khoa-tieu-hoa"/>
|
||||
<column name="icon_url" value="https://cdn-icons-png.flaticon.com/512/3063/3063200.png"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_specialty">
|
||||
<column name="id" value="4"/>
|
||||
<column name="name" value="Khoa Nhi"/>
|
||||
<column name="slug" value="khoa-nhi"/>
|
||||
<column name="icon_url" value="https://cdn-icons-png.flaticon.com/512/2966/2966327.png"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_doctor">
|
||||
<column name="id" value="1"/>
|
||||
<column name="name" value="Ngô Tùng T"/>
|
||||
<column name="title" value="ThS. BS."/>
|
||||
<column name="specialty_id" value="2"/>
|
||||
<column name="avatar_url" value="https://i.pravatar.cc/300?img=11"/>
|
||||
<column name="booking_url" value="https://booking.bvdaihoc.com.vn/"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_doctor">
|
||||
<column name="id" value="2"/>
|
||||
<column name="name" value="Ngô Anh L"/>
|
||||
<column name="title" value="TS. BS."/>
|
||||
<column name="specialty_id" value="1"/>
|
||||
<column name="avatar_url" value="https://i.pravatar.cc/300?img=12"/>
|
||||
<column name="booking_url" value="https://booking.bvdaihoc.com.vn/"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_doctor">
|
||||
<column name="id" value="3"/>
|
||||
<column name="name" value="Huỳnh Thị B"/>
|
||||
<column name="title" value="TS. BS."/>
|
||||
<column name="specialty_id" value="4"/>
|
||||
<column name="avatar_url" value="https://i.pravatar.cc/300?img=13"/>
|
||||
<column name="booking_url" value="https://booking.bvdaihoc.com.vn/"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_doctor">
|
||||
<column name="id" value="4"/>
|
||||
<column name="name" value="Hoàng Thị B"/>
|
||||
<column name="title" value="ThS. BS."/>
|
||||
<column name="specialty_id" value="2"/>
|
||||
<column name="avatar_url" value="https://i.pravatar.cc/300?img=14"/>
|
||||
<column name="booking_url" value="https://booking.bvdaihoc.com.vn/"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_doctor">
|
||||
<column name="id" value="5"/>
|
||||
<column name="name" value="Lê Anh L"/>
|
||||
<column name="title" value="PGS. TS. BS."/>
|
||||
<column name="specialty_id" value="1"/>
|
||||
<column name="avatar_url" value="https://i.pravatar.cc/300?img=15"/>
|
||||
<column name="booking_url" value="https://booking.bvdaihoc.com.vn/"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_doctor">
|
||||
<column name="id" value="6"/>
|
||||
<column name="name" value="Võ Thị B"/>
|
||||
<column name="title" value="PGS. TS. BS."/>
|
||||
<column name="specialty_id" value="3"/>
|
||||
<column name="avatar_url" value="https://i.pravatar.cc/300?img=16"/>
|
||||
<column name="booking_url" value="https://booking.bvdaihoc.com.vn/"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_doctor">
|
||||
<column name="id" value="7"/>
|
||||
<column name="name" value="Lê Ngọc I"/>
|
||||
<column name="title" value="BS. CKII."/>
|
||||
<column name="specialty_id" value="1"/>
|
||||
<column name="avatar_url" value="https://i.pravatar.cc/300?img=17"/>
|
||||
<column name="booking_url" value="https://booking.bvdaihoc.com.vn/"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_doctor">
|
||||
<column name="id" value="8"/>
|
||||
<column name="name" value="Trần Anh L"/>
|
||||
<column name="title" value="BS. CKI."/>
|
||||
<column name="specialty_id" value="2"/>
|
||||
<column name="avatar_url" value="https://i.pravatar.cc/300?img=18"/>
|
||||
<column name="booking_url" value="https://booking.bvdaihoc.com.vn/"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_doctor">
|
||||
<column name="id" value="9"/>
|
||||
<column name="name" value="Dương Sơn S"/>
|
||||
<column name="title" value="BS. CKI."/>
|
||||
<column name="specialty_id" value="3"/>
|
||||
<column name="avatar_url" value="https://i.pravatar.cc/300?img=19"/>
|
||||
<column name="booking_url" value="https://booking.bvdaihoc.com.vn/"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_doctor">
|
||||
<column name="id" value="10"/>
|
||||
<column name="name" value="Võ Minh D"/>
|
||||
<column name="title" value="ThS. BS."/>
|
||||
<column name="specialty_id" value="3"/>
|
||||
<column name="avatar_url" value="https://i.pravatar.cc/300?img=20"/>
|
||||
<column name="booking_url" value="https://booking.bvdaihoc.com.vn/"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_doctor">
|
||||
<column name="id" value="11"/>
|
||||
<column name="name" value="Ngô Tuấn M"/>
|
||||
<column name="title" value="BS. CKI."/>
|
||||
<column name="specialty_id" value="4"/>
|
||||
<column name="avatar_url" value="https://i.pravatar.cc/300?img=21"/>
|
||||
<column name="booking_url" value="https://booking.bvdaihoc.com.vn/"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_doctor">
|
||||
<column name="id" value="12"/>
|
||||
<column name="name" value="Hoàng Văn A"/>
|
||||
<column name="title" value="ThS. BS."/>
|
||||
<column name="specialty_id" value="2"/>
|
||||
<column name="avatar_url" value="https://i.pravatar.cc/300?img=22"/>
|
||||
<column name="booking_url" value="https://booking.bvdaihoc.com.vn/"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_doctor">
|
||||
<column name="id" value="13"/>
|
||||
<column name="name" value="Hồ Minh D"/>
|
||||
<column name="title" value="BS. CKII."/>
|
||||
<column name="specialty_id" value="3"/>
|
||||
<column name="avatar_url" value="https://i.pravatar.cc/300?img=23"/>
|
||||
<column name="booking_url" value="https://booking.bvdaihoc.com.vn/"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_doctor">
|
||||
<column name="id" value="14"/>
|
||||
<column name="name" value="Trần Tuấn M"/>
|
||||
<column name="title" value="ThS. BS."/>
|
||||
<column name="specialty_id" value="2"/>
|
||||
<column name="avatar_url" value="https://i.pravatar.cc/300?img=24"/>
|
||||
<column name="booking_url" value="https://booking.bvdaihoc.com.vn/"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_doctor">
|
||||
<column name="id" value="15"/>
|
||||
<column name="name" value="Dương Quốc C"/>
|
||||
<column name="title" value="BS. CKI."/>
|
||||
<column name="specialty_id" value="1"/>
|
||||
<column name="avatar_url" value="https://i.pravatar.cc/300?img=25"/>
|
||||
<column name="booking_url" value="https://booking.bvdaihoc.com.vn/"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_doctor">
|
||||
<column name="id" value="16"/>
|
||||
<column name="name" value="Đỗ Hữu E"/>
|
||||
<column name="title" value="ThS. BS."/>
|
||||
<column name="specialty_id" value="3"/>
|
||||
<column name="avatar_url" value="https://i.pravatar.cc/300?img=26"/>
|
||||
<column name="booking_url" value="https://booking.bvdaihoc.com.vn/"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_doctor">
|
||||
<column name="id" value="17"/>
|
||||
<column name="name" value="Dương Thị B"/>
|
||||
<column name="title" value="PGS. TS. BS."/>
|
||||
<column name="specialty_id" value="1"/>
|
||||
<column name="avatar_url" value="https://i.pravatar.cc/300?img=27"/>
|
||||
<column name="booking_url" value="https://booking.bvdaihoc.com.vn/"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_doctor">
|
||||
<column name="id" value="18"/>
|
||||
<column name="name" value="Ngô Thị B"/>
|
||||
<column name="title" value="PGS. TS. BS."/>
|
||||
<column name="specialty_id" value="1"/>
|
||||
<column name="avatar_url" value="https://i.pravatar.cc/300?img=28"/>
|
||||
<column name="booking_url" value="https://booking.bvdaihoc.com.vn/"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_doctor">
|
||||
<column name="id" value="19"/>
|
||||
<column name="name" value="Ngô Quốc C"/>
|
||||
<column name="title" value="PGS. TS. BS."/>
|
||||
<column name="specialty_id" value="1"/>
|
||||
<column name="avatar_url" value="https://i.pravatar.cc/300?img=29"/>
|
||||
<column name="booking_url" value="https://booking.bvdaihoc.com.vn/"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
<insert tableName="sis_doctor">
|
||||
<column name="id" value="20"/>
|
||||
<column name="name" value="Phạm Thu P"/>
|
||||
<column name="title" value="PGS. TS. BS."/>
|
||||
<column name="specialty_id" value="2"/>
|
||||
<column name="avatar_url" value="https://i.pravatar.cc/300?img=30"/>
|
||||
<column name="booking_url" value="https://booking.bvdaihoc.com.vn/"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="2026-07-18T00:00:00"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
@@ -39,4 +39,6 @@
|
||||
<include file="config/liquibase/changelog/20260714080000_seed_news_events_components.xml" relativeToChangelogFile="false"/>
|
||||
<include file="config/liquibase/changelog/20260714112000_add_location_to_post.xml" relativeToChangelogFile="false"/>
|
||||
<include file="config/liquibase/changelog/20260714113000_update_events_list.xml" relativeToChangelogFile="false"/>
|
||||
<include file="config/liquibase/changelog/20260718100000_add_doctor_and_specialty.xml" relativeToChangelogFile="false"/>
|
||||
<include file="config/liquibase/changelog/20260718110000_seed_sample_doctors.xml" relativeToChangelogFile="false"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
[vaul-drawer]{touch-action:none;will-change:transform;transition:transform .5s cubic-bezier(.32,.72,0,1)}[vaul-drawer][vaul-drawer-direction=bottom]{transform:translate3d(0,100%,0)}[vaul-drawer][vaul-drawer-direction=top]{transform:translate3d(0,-100%,0)}[vaul-drawer][vaul-drawer-direction=left]{transform:translate3d(-100%,0,0)}[vaul-drawer][vaul-drawer-direction=right]{transform:translate3d(100%,0,0)}.vaul-dragging .vaul-scrollable [vault-drawer-direction=top],.vaul-dragging .vaul-scrollable [vault-drawer-direction=bottom]{overflow-y:hidden!important}.vaul-dragging .vaul-scrollable [vault-drawer-direction=left],.vaul-dragging .vaul-scrollable [vault-drawer-direction=right]{overflow-x:hidden!important}[vaul-drawer][vaul-drawer-visible=true][vaul-drawer-direction=top],[vaul-drawer][vaul-drawer-visible=true][vaul-drawer-direction=bottom]{transform:translate3d(0,var(--snap-point-height, 0),0)}[vaul-drawer][vaul-drawer-visible=true][vaul-drawer-direction=left],[vaul-drawer][vaul-drawer-visible=true][vaul-drawer-direction=right]{transform:translate3d(var(--snap-point-height, 0),0,0)}[vaul-overlay]{opacity:0;transition:opacity .5s cubic-bezier(.32,.72,0,1)}[vaul-overlay][vaul-drawer-visible=true]{opacity:1}[vaul-drawer]:after{content:"";position:absolute;background:inherit;background-color:inherit}[vaul-drawer][vaul-drawer-direction=top]:after{top:initial;bottom:100%;left:0;right:0;height:200%}[vaul-drawer][vaul-drawer-direction=bottom]:after{top:100%;bottom:initial;left:0;right:0;height:200%}[vaul-drawer][vaul-drawer-direction=left]:after{left:initial;right:100%;top:0;bottom:0;width:200%}[vaul-drawer][vaul-drawer-direction=right]:after{left:100%;right:initial;top:0;bottom:0;width:200%}[vaul-overlay][vaul-snap-points=true]:not([vaul-snap-points-overlay=true]):not([data-state=closed]){opacity:0}[vaul-overlay][vaul-snap-points-overlay=true]:not([vaul-drawer-visible=false]){opacity:1}@keyframes fake-animation{}@media (hover: hover) and (pointer: fine){[vaul-drawer]{-webkit-user-select:none;user-select:none}}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,202 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{fragments/layout}">
|
||||
|
||||
<head>
|
||||
<th:block layout:fragment="head">
|
||||
<title>Đội ngũ bác sĩ | UMC</title>
|
||||
<style>
|
||||
:root {
|
||||
--spacing: 0.25rem;
|
||||
}
|
||||
/* Fix umass.css overriding layered Tailwind classes */
|
||||
h1.mb-4 { margin-bottom: calc(var(--spacing) * 4) !important; }
|
||||
@media (min-width: 768px) {
|
||||
h1.md\:mb-6 { margin-bottom: calc(var(--spacing) * 6) !important; }
|
||||
}
|
||||
@media (min-width: 1280px) {
|
||||
h1.xl\:mb-8 { margin-bottom: calc(var(--spacing) * 8) !important; }
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" th:href="@{/css/umc_doctor.css}" />
|
||||
<link rel="stylesheet" th:href="@{/css/umc_doctor_2.css}" />
|
||||
<link rel="stylesheet" th:href="@{/css/umc_doctor_3.css}" />
|
||||
</th:block>
|
||||
</head>
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<main class=""><!--$--><!--/$--><!--$--><!--/$--><section id="doctor-content" class="xl:py-12 md:py-8 py-6 bg-[#f6f6f6]"><div class="container"><div class="flex flex-col md:flex-row md:items-center md:justify-between xl:mb-8 md:mb-6 mb-4 gap-4"><div class="flex gap-4 w-full xl:flex-row flex-col"><div class="relative xl:w-2/4 w-full flex-shrink-0"><input id="search-input" onkeyup="filterDoctors()" placeholder="Tìm kiếm bác sĩ..." class="w-full shadow rounded-lg border border-gray-200 px-4 py-3 label-3 bg-white pr-10 focus:outline-none focus:border-primary-600 lg:hover:border-primary-600 lg:duration-150 placeholder:text-gray-500" type="text" value=""><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 w-5 h-5"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.3-4.3"></path></svg></div><div class="flex gap-4 md:flex-row flex-col md:w-full"><div class="relative w-full "><button type="button" class="w-full group rounded-lg cursor-pointer border label-3 bg-white flex justify-between items-center border-gray-200 lg:px-5 px-4 py-3 shadow lg:hover:border-primary-600 lg:duration-150" id="btn-specialty" aria-haspopup="listbox" aria-expanded="false" onclick="toggleDropdown('list-specialty')" style="height: 100%; background-color: var(--color-white);"><span id="text-specialty" class="text-gray-900 lg:group-hover:text-primary-600 lg:duration-150">--Theo chuyên khoa--</span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down ml-2 duration-150 md:size-5 size-4 rotate-0 text-gray-900 lg:group-hover:text-primary-600 duration-150"><path d="m6 9 6 6 6-6"></path></svg></button><ul id="list-specialty" class="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" style="display: none;">
|
||||
<li class="text-gray-900 cursor-pointer select-none relative py-2 pl-3 pr-9 hover:bg-gray-100" onclick="selectSpecialty('')">--Tất cả chuyên khoa--</li>
|
||||
<li th:each="spec : ${specialties}" th:text="${spec.name}" th:data-specialty="${spec.name}" onclick="selectSpecialty(this.getAttribute('data-specialty'))" class="text-gray-900 cursor-pointer select-none relative py-2 pl-3 pr-9 hover:bg-gray-100"></li>
|
||||
</ul></div><div class="relative w-full "><button type="button" class="w-full group rounded-lg cursor-pointer border label-3 bg-white flex justify-between items-center border-gray-200 lg:px-5 px-4 py-3 shadow lg:hover:border-primary-600 lg:duration-150" id="btn-gender" aria-haspopup="listbox" aria-expanded="false" onclick="toggleDropdown('list-gender')" style="height: 100%; background-color: var(--color-white);"><span id="text-gender" class="text-gray-900 lg:group-hover:text-primary-600 lg:duration-150">--Theo giới tính--</span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down ml-2 duration-150 md:size-5 size-4 rotate-0 text-gray-900 lg:group-hover:text-primary-600 duration-150"><path d="m6 9 6 6 6-6"></path></svg></button><ul id="list-gender" class="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" style="display: none;"><li class="text-gray-900 cursor-pointer select-none relative py-2 pl-3 pr-9 hover:bg-gray-100" onclick="selectGender('')">--Tất cả giới tính--</li><li class="text-gray-900 cursor-pointer select-none relative py-2 pl-3 pr-9 hover:bg-gray-100" onclick="selectGender('Nam')">Nam</li><li class="text-gray-900 cursor-pointer select-none relative py-2 pl-3 pr-9 hover:bg-gray-100" onclick="selectGender('Nữ')">Nữ</li></ul></div></div></div></div><h1 class="display-7 text-primary-500 xl:mb-8 md:mb-6 mb-4">Danh sách bác sĩ</h1><div id="doctors-grid" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 xl:gap-4 md:gap-3 gap-2">
|
||||
<!-- Thymeleaf Loop over doctors -->
|
||||
<div th:each="doc : ${doctors}" class="bg-white rounded-xl overview-wrapper-shadow flex flex-col justify-between h-full doctor-card" th:data-specialty="${doc.specialty != null ? doc.specialty.name : ''}" data-gender="">
|
||||
<div>
|
||||
<div class="flex gap-4 items-center xl:px-6 px-4 xl:pt-6 pt-4 mb-3 relative">
|
||||
<a class="flex-shrink-0 relative aspect-square overflow-hidden rounded border border-primary-400 bg-gradient-to-tl from-primary-75 to-primary-100 xl:size-[150px] size-[100px]" th:href="${doc.bookingUrl != null and !doc.bookingUrl.isEmpty() ? doc.bookingUrl : '#'}">
|
||||
<img loading="lazy" decoding="async" th:src="${doc.avatarUrl != null and !doc.avatarUrl.isEmpty() ? doc.avatarUrl : '/images/default-avatar.png'}" class="lg:hover:scale-105 scale-100 lg:duration-150" style="position: absolute; height: 100%; width: 100%; inset: 0px; object-fit: contain;">
|
||||
</a>
|
||||
<div class="flex-1">
|
||||
<div class="text-primary-300 label-3 max-md:!text-[14px] !leading-[150%]" th:text="${doc.title}"></div>
|
||||
<a class="text-[16px] leading-[150%] font-display font-bold text-[#1E293B] lg:hover:text-primary-600 lg:duration-150 mb-1 line-clamp-2" th:href="${doc.bookingUrl != null and !doc.bookingUrl.isEmpty() ? doc.bookingUrl : '#'}" th:text="${doc.name}"></a>
|
||||
<div class="text-primary-300 label-3 !leading-[150%]" th:text="${doc.specialty != null ? doc.specialty.name : ''}"></div>
|
||||
</div>
|
||||
<div class="absolute xl:top-4 top-4 xl:right-4 right-4 size-[40px] rounded-lg bg-white card-icon-shadow p-1" th:if="${doc.specialty != null and doc.specialty.iconUrl != null and !doc.specialty.iconUrl.isEmpty()}">
|
||||
<img loading="lazy" width="40" height="38" th:src="${doc.specialty.iconUrl}" style="object-fit: cover;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="xl:px-6 px-4 xl:pb-6 pb-4 xl:mt-3 mt-2 flex items-center justify-between gap-x-2">
|
||||
<a class="btn btn-outline-primary !inline-flex w-[149px] text-center label-2 !px-0 group items-center justify-center gap-1.5" th:href="${doc.bookingUrl != null and !doc.bookingUrl.isEmpty() ? doc.bookingUrl : '#'}">
|
||||
<div>Xem hồ sơ</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m10 8 4 4-4 4"/></svg>
|
||||
</a>
|
||||
<button type="button" class="btn text-center label-2 !px-0 w-[149px] text-white flex items-center justify-center gap-1.5 rounded-lg transition" style="background-color: var(--color-brand, #0054a6); height: 100%;" th:data-url="${doc.bookingUrl != null and !doc.bookingUrl.isEmpty() ? doc.bookingUrl : '#'}" onclick="window.location.href=this.getAttribute('data-url')">
|
||||
<div>Đặt lịch khám</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="4" rx="2" ry="2"/><line x1="16" x2="16" y1="2" y2="6"/><line x1="8" x2="8" y1="2" y2="6"/><line x1="3" x2="21" y1="10" y2="10"/><path d="M8 14h.01"/><path d="M12 14h.01"/><path d="M16 14h.01"/><path d="M8 18h.01"/><path d="M12 18h.01"/><path d="M16 18h.01"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pagination-controls" class="flex justify-center items-center space-x-2 mt-8"></div>
|
||||
</section></main>
|
||||
|
||||
<script th:inline="javascript">
|
||||
/*<![CDATA[*/
|
||||
let currentSpecialty = '';
|
||||
let currentGender = '';
|
||||
|
||||
function toggleDropdown(id) {
|
||||
const el = document.getElementById(id);
|
||||
el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function selectSpecialty(spec) {
|
||||
currentSpecialty = spec;
|
||||
document.getElementById('text-specialty').innerText = spec || '--Theo chuyên khoa--';
|
||||
document.getElementById('list-specialty').style.display = 'none';
|
||||
filterDoctors();
|
||||
}
|
||||
|
||||
function selectGender(gender) {
|
||||
currentGender = gender;
|
||||
document.getElementById('text-gender').innerText = gender || '--Theo giới tính--';
|
||||
document.getElementById('list-gender').style.display = 'none';
|
||||
filterDoctors();
|
||||
}
|
||||
|
||||
let currentPage = 1;
|
||||
const itemsPerPage = 12;
|
||||
let filteredCards = [];
|
||||
|
||||
function filterDoctors() {
|
||||
const searchText = document.getElementById('search-input').value.toLowerCase();
|
||||
const cards = document.querySelectorAll('#doctors-grid > div.doctor-card');
|
||||
|
||||
filteredCards = [];
|
||||
|
||||
cards.forEach(card => {
|
||||
const textContent = card.innerText || '';
|
||||
const textLower = textContent.toLowerCase();
|
||||
|
||||
let matchSearch = searchText === '' || textLower.includes(searchText);
|
||||
let matchSpecialty = currentSpecialty === '' || textContent.includes(currentSpecialty);
|
||||
|
||||
if (matchSearch && matchSpecialty) {
|
||||
filteredCards.push(card);
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
currentPage = 1;
|
||||
renderPagination();
|
||||
}
|
||||
|
||||
function renderPagination() {
|
||||
const totalPages = Math.ceil(filteredCards.length / itemsPerPage);
|
||||
const paginationContainer = document.getElementById('pagination-controls');
|
||||
paginationContainer.innerHTML = '';
|
||||
|
||||
if (totalPages <= 1) {
|
||||
filteredCards.forEach(card => card.style.display = '');
|
||||
return;
|
||||
}
|
||||
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
|
||||
filteredCards.forEach((card, index) => {
|
||||
if (index >= startIndex && index < endIndex) {
|
||||
card.style.display = '';
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
const createButton = (content, onClick, disabled, isActive) => {
|
||||
const btn = document.createElement('button');
|
||||
if (isActive) {
|
||||
btn.className = 'lg:size-10 md:size-9 size-8 flex items-center justify-center rounded-full border body-1 cursor-pointer bg-primary-600 text-white border-primary-600';
|
||||
} else {
|
||||
btn.className = 'lg:size-10 md:size-9 size-8 flex items-center justify-center rounded-full border border-gray-300 bg-white text-black disabled:opacity-50 disabled:!bg-white disabled:!text-black cursor-pointer lg:hover:bg-primary-600 lg:hover:text-white lg:duration-150 transition-colors';
|
||||
}
|
||||
btn.innerHTML = content;
|
||||
if (disabled) btn.disabled = true;
|
||||
if (!disabled && !isActive) {
|
||||
btn.onclick = () => {
|
||||
onClick();
|
||||
renderPagination();
|
||||
window.scrollTo(0, document.getElementById('doctor-content').offsetTop - 50);
|
||||
};
|
||||
}
|
||||
return btn;
|
||||
};
|
||||
|
||||
const iconFirst = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevrons-left size-4"><path d="m11 17-5-5 5-5"></path><path d="m18 17-5-5 5-5"></path></svg>';
|
||||
const iconPrev = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-up size-4 -rotate-90"><path d="m18 15-6-6-6 6"></path></svg>';
|
||||
const iconNext = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down size-4 -rotate-90"><path d="m6 9 6 6 6-6"></path></svg>';
|
||||
const iconLast = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevrons-right size-4"><path d="m6 17 5-5-5-5"></path><path d="m13 17 5-5-5-5"></path></svg>';
|
||||
|
||||
paginationContainer.appendChild(createButton(iconFirst, () => currentPage = 1, currentPage === 1, false));
|
||||
paginationContainer.appendChild(createButton(iconPrev, () => currentPage--, currentPage === 1, false));
|
||||
|
||||
let startPage = Math.max(1, currentPage - 1);
|
||||
let endPage = Math.min(totalPages, currentPage + 1);
|
||||
|
||||
if (currentPage === 1 && totalPages > 2) endPage = 3;
|
||||
if (currentPage === totalPages && totalPages > 2) startPage = totalPages - 2;
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
paginationContainer.appendChild(createButton(i, () => currentPage = i, false, i === currentPage));
|
||||
}
|
||||
|
||||
paginationContainer.appendChild(createButton(iconNext, () => currentPage++, currentPage === totalPages, false));
|
||||
paginationContainer.appendChild(createButton(iconLast, () => currentPage = totalPages, currentPage === totalPages, false));
|
||||
}
|
||||
|
||||
// Initialize pagination on load
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
filterDoctors();
|
||||
});
|
||||
|
||||
// Close dropdowns when clicking outside
|
||||
document.addEventListener('click', function(event) {
|
||||
const specBtn = document.getElementById('btn-specialty');
|
||||
const specList = document.getElementById('list-specialty');
|
||||
if (specBtn && !specBtn.contains(event.target) && !specList.contains(event.target)) {
|
||||
specList.style.display = 'none';
|
||||
}
|
||||
|
||||
const genderBtn = document.getElementById('btn-gender');
|
||||
const genderList = document.getElementById('list-gender');
|
||||
if (genderBtn && !genderBtn.contains(event.target) && !genderList.contains(event.target)) {
|
||||
genderList.style.display = 'none';
|
||||
}
|
||||
});
|
||||
/*]]>*/
|
||||
</script>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -121,6 +121,22 @@
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Nav Item - Medical Collapse Menu -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseMedical"
|
||||
aria-expanded="true" aria-controls="collapseMedical">
|
||||
<i class="fas fa-fw fa-user-md"></i>
|
||||
<span>Medical</span>
|
||||
</a>
|
||||
<div id="collapseMedical" class="collapse" aria-labelledby="headingMedical" data-parent="#accordionSidebar">
|
||||
<div class="bg-white py-2 collapse-inner rounded">
|
||||
<h6 class="collapse-header">Medical Team:</h6>
|
||||
<a class="collapse-item" th:href="@{/manage/doctors}">Doctors</a>
|
||||
<a class="collapse-item" th:href="@{/manage/specialties}">Specialties</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Nav Item - Appearance Collapse Menu -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseAppearance"
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/manage-layout}">
|
||||
|
||||
<head>
|
||||
<title>Doctors</title>
|
||||
<!-- DataTables CSS (from CDN) -->
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap4.min.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div layout:fragment="content">
|
||||
<!-- Page Heading -->
|
||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">Doctors</h1>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
<div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<span th:text="${successMessage}"></span>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<span th:text="${errorMessage}"></span>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Left Column: Add/Edit Doctor Form -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary"
|
||||
th:text="${isNew} ? 'Add New Doctor' : 'Edit Doctor'">Form</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form th:action="${isNew} ? @{/manage/doctors} : @{/manage/doctors/{id}(id=${doctor.id})}"
|
||||
th:object="${doctor}" method="post">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
|
||||
<!-- Validation errors -->
|
||||
<div th:if="${#fields.hasErrors('*')}" class="alert alert-danger alert-sm">
|
||||
<ul class="mb-0 small">
|
||||
<li th:each="err : ${#fields.errors('*')}" th:text="${err}"></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div class="form-group">
|
||||
<label for="doctorName" class="font-weight-bold">Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="doctorName" th:field="*{name}"
|
||||
th:classappend="${#fields.hasErrors('name')} ? 'is-invalid' : ''"
|
||||
placeholder="e.g. Nguyễn Văn A" required>
|
||||
<div class="invalid-feedback" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></div>
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<div class="form-group">
|
||||
<label for="doctorTitle" class="font-weight-bold">Title</label>
|
||||
<input type="text" class="form-control" id="doctorTitle" th:field="*{title}"
|
||||
placeholder="e.g. BS. CKII">
|
||||
</div>
|
||||
|
||||
<!-- Specialty -->
|
||||
<div class="form-group">
|
||||
<label for="specialtyId" class="font-weight-bold">Specialty</label>
|
||||
<select class="form-control" id="specialtyId" name="specialtyId">
|
||||
<option value="">— Select Specialty —</option>
|
||||
<option th:each="spec : ${allSpecialties}" th:value="${spec.id}" th:text="${spec.name}"
|
||||
th:selected="${doctor.specialty != null && doctor.specialty.id == spec.id}"></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Avatar URL -->
|
||||
<div class="form-group">
|
||||
<label for="doctorAvatar" class="font-weight-bold">Avatar URL</label>
|
||||
<input type="text" class="form-control" id="doctorAvatar" th:field="*{avatarUrl}"
|
||||
placeholder="/uploads/images/bac-si/avatar.png">
|
||||
</div>
|
||||
|
||||
<!-- Booking URL -->
|
||||
<div class="form-group">
|
||||
<label for="doctorBooking" class="font-weight-bold">External Booking URL</label>
|
||||
<input type="text" class="form-control" id="doctorBooking" th:field="*{bookingUrl}"
|
||||
placeholder="https://booking.bvdaihoc.com.vn/...">
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a th:if="${!isNew}" th:href="@{/manage/doctors}" class="btn btn-secondary btn-sm">
|
||||
<i class="fas fa-times"></i> Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary btn-sm"
|
||||
th:classappend="${isNew} ? 'btn-block' : ''">
|
||||
<i class="fas fa-save"></i>
|
||||
<span th:text="${isNew} ? 'Add New Doctor' : 'Update Doctor'">Save</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Doctors Table -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Existing Doctors</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover" id="doctorsTable" width="100%" cellspacing="0">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th width="30%">Name</th>
|
||||
<th width="15%">Title</th>
|
||||
<th width="25%">Specialty</th>
|
||||
<th width="15%">Booking</th>
|
||||
<th width="15%">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="doc : ${allDoctors}">
|
||||
<td>
|
||||
<strong th:text="${doc.name}"></strong>
|
||||
</td>
|
||||
<td>
|
||||
<span th:text="${doc.title}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span th:if="${doc.specialty != null}" th:text="${doc.specialty.name}" class="badge badge-info"></span>
|
||||
<span th:if="${doc.specialty == null}" class="text-muted">—</span>
|
||||
</td>
|
||||
<td>
|
||||
<a th:if="${doc.bookingUrl != null && !doc.bookingUrl.isEmpty()}" th:href="${doc.bookingUrl}" target="_blank" class="badge badge-success">Link</a>
|
||||
<span th:if="${doc.bookingUrl == null || doc.bookingUrl.isEmpty()}" class="text-muted">—</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a th:href="@{/manage/doctors/{id}/edit(id=${doc.id})}"
|
||||
class="btn btn-sm btn-outline-info mr-1" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form th:action="@{/manage/doctors/{id}/delete(id=${doc.id})}" method="post"
|
||||
style="display:inline;"
|
||||
onsubmit="return confirm('Are you sure you want to delete this doctor?');">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(allDoctors)}">
|
||||
<td colspan="5" class="text-center text-muted py-4">
|
||||
<i class="fas fa-user-md fa-2x mb-2 d-block"></i>
|
||||
No doctors yet. Use the form on the left to create one.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section layout:fragment="scripts">
|
||||
<!-- Page level plugins (from CDN) -->
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap4.min.js"></script>
|
||||
|
||||
<!-- Page level custom scripts -->
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#doctorsTable').DataTable({
|
||||
"order": [],
|
||||
"pageLength": 10
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,150 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/manage-layout}">
|
||||
|
||||
<head>
|
||||
<title>Specialties</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div layout:fragment="content">
|
||||
<!-- Page Heading -->
|
||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">Specialties</h1>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
<div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<span th:text="${successMessage}"></span>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<span th:text="${errorMessage}"></span>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Left Column: Add/Edit Specialty Form -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary"
|
||||
th:text="${isNew} ? 'Add New Specialty' : 'Edit Specialty'">Form</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form th:action="${isNew} ? @{/manage/specialties} : @{/manage/specialties/{id}(id=${specialty.id})}"
|
||||
th:object="${specialty}" method="post">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
|
||||
<!-- Validation errors -->
|
||||
<div th:if="${#fields.hasErrors('*')}" class="alert alert-danger alert-sm">
|
||||
<ul class="mb-0 small">
|
||||
<li th:each="err : ${#fields.errors('*')}" th:text="${err}"></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div class="form-group">
|
||||
<label for="specialtyName" class="font-weight-bold">Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="specialtyName" th:field="*{name}"
|
||||
th:classappend="${#fields.hasErrors('name')} ? 'is-invalid' : ''"
|
||||
placeholder="e.g. Khoa Tim mạch" required>
|
||||
<div class="invalid-feedback" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></div>
|
||||
</div>
|
||||
|
||||
<!-- Slug -->
|
||||
<div class="form-group">
|
||||
<label for="specialtySlug" class="font-weight-bold">Slug <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="specialtySlug" th:field="*{slug}"
|
||||
placeholder="e.g. khoa-tim-mach" required>
|
||||
<div class="invalid-feedback" th:if="${#fields.hasErrors('slug')}" th:errors="*{slug}"></div>
|
||||
</div>
|
||||
|
||||
<!-- Icon URL -->
|
||||
<div class="form-group">
|
||||
<label for="specialtyIcon" class="font-weight-bold">Icon URL</label>
|
||||
<input type="text" class="form-control" id="specialtyIcon" th:field="*{iconUrl}"
|
||||
placeholder="/uploads/chuyen-khoa/icon.png">
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<a th:if="${!isNew}" th:href="@{/manage/specialties}" class="btn btn-secondary btn-sm">
|
||||
<i class="fas fa-times"></i> Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary btn-sm"
|
||||
th:classappend="${isNew} ? 'btn-block' : ''">
|
||||
<i class="fas fa-save"></i>
|
||||
<span th:text="${isNew} ? 'Add New Specialty' : 'Update Specialty'">Save</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Specialties Table -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Existing Specialties</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover" id="specialtiesTable" width="100%" cellspacing="0">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th width="35%">Name</th>
|
||||
<th width="30%">Slug</th>
|
||||
<th width="20%">Icon URL</th>
|
||||
<th width="15%">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="spec : ${allSpecialties}">
|
||||
<td>
|
||||
<strong th:text="${spec.name}"></strong>
|
||||
</td>
|
||||
<td>
|
||||
<code th:text="${spec.slug}"></code>
|
||||
</td>
|
||||
<td>
|
||||
<small th:text="${spec.iconUrl}" class="text-muted"></small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a th:href="@{/manage/specialties/{id}/edit(id=${spec.id})}"
|
||||
class="btn btn-sm btn-outline-info mr-1" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form th:action="@{/manage/specialties/{id}/delete(id=${spec.id})}" method="post"
|
||||
style="display:inline;"
|
||||
onsubmit="return confirm('Are you sure you want to delete this specialty?');">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(allSpecialties)}">
|
||||
<td colspan="4" class="text-center text-muted py-4">
|
||||
<i class="fas fa-folder-open fa-2x mb-2 d-block"></i>
|
||||
No specialties yet. Use the form on the left to create one.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
<head>
|
||||
<title>Users Management</title>
|
||||
<!-- DataTables CSS (from CDN) -->
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap4.min.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -113,6 +115,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page level plugins (from CDN) -->
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap4.min.js"></script>
|
||||
|
||||
<!-- Page level custom scripts -->
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#dataTable').DataTable();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function openDeleteModal(login) {
|
||||
document.getElementById('deleteUserLoginDisplay').textContent = login;
|
||||
|
||||
Reference in New Issue
Block a user