feat: add doctor and specialty management modules with full CRUD support and database seeding
This commit is contained in:
@@ -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