diff --git a/pom.xml b/pom.xml index 873c239..e217d5d 100644 --- a/pom.xml +++ b/pom.xml @@ -113,6 +113,11 @@ org.thymeleaf.extras thymeleaf-extras-springsecurity6 + + + org.springframework.boot + spring-boot-starter-oauth2-client + diff --git a/src/main/java/com/bookingtour/sun/config/OAuth2LoginSuccessHandler.java b/src/main/java/com/bookingtour/sun/config/OAuth2LoginSuccessHandler.java new file mode 100644 index 0000000..05b20e6 --- /dev/null +++ b/src/main/java/com/bookingtour/sun/config/OAuth2LoginSuccessHandler.java @@ -0,0 +1,62 @@ +package com.bookingtour.sun.config; + +import com.bookingtour.sun.entity.User; +import com.bookingtour.sun.enums.LoginProvider; +import com.bookingtour.sun.enums.UserRole; +import com.bookingtour.sun.enums.UserStatus; +import com.bookingtour.sun.repository.UserRepository; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class OAuth2LoginSuccessHandler + extends SavedRequestAwareAuthenticationSuccessHandler { + + @Autowired + UserRepository userRepository; + + @Override + public void onAuthenticationSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) + throws IOException, ServletException { + + OAuth2User user = (OAuth2User) authentication.getPrincipal(); + + String email = user.getAttribute("email"); + + String name = user.getAttribute("name"); + + + User dbUser = userRepository.findByEmail(email) + .orElseGet(() -> { + User u = new User(); + u.setEmail(email); + u.setFullName(name); + String username = name != null ? name : email; + if (username != null && username.length() > 50) { + username = username.substring(0, 50); + } + u.setUsername(username); + u.setRole(UserRole.USER); + u.setStatus(UserStatus.ACTIVE); + u.setPasswordDigest(""); + u.setLoginProvider(LoginProvider.GOOGLE); + return userRepository.save(u); + }); + + super.onAuthenticationSuccess( + request, + response, + authentication); + } +} diff --git a/src/main/java/com/bookingtour/sun/config/SecurityConfig.java b/src/main/java/com/bookingtour/sun/config/SecurityConfig.java index 83f8858..b3d5510 100644 --- a/src/main/java/com/bookingtour/sun/config/SecurityConfig.java +++ b/src/main/java/com/bookingtour/sun/config/SecurityConfig.java @@ -1,34 +1,48 @@ package com.bookingtour.sun.config; import com.bookingtour.sun.enums.UserRole; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration +@RequiredArgsConstructor public class SecurityConfig { - + private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth - + // public pages .requestMatchers( + "/tours", "/login", "/register", + "/css/**", "/js/**", - "/images/**" + "/images/**", + "/uploads/**", + + "/login/oauth2/**", + "/oauth2/**" ) .permitAll() + .requestMatchers(HttpMethod.GET, + "/", + "/tours/**" + ).permitAll() // admin page .requestMatchers("/admin/**") .hasAnyRole(UserRole.ADMIN.name()) // client page - .requestMatchers("/") - .hasAnyRole(UserRole.ADMIN.name(), UserRole.USER.name()) + // booking phải login + .requestMatchers("/bookings/**") + .authenticated() .anyRequest() .authenticated() @@ -46,6 +60,12 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .invalidateHttpSession(true) .deleteCookies("JSESSIONID") .permitAll() + ) + + .oauth2Login(oauth -> oauth + .loginPage("/login") + .defaultSuccessUrl("/") + .successHandler(oAuth2LoginSuccessHandler) ); return http.build(); diff --git a/src/main/java/com/bookingtour/sun/controller/BookingController.java b/src/main/java/com/bookingtour/sun/controller/BookingController.java index 20606ea..8d7125f 100644 --- a/src/main/java/com/bookingtour/sun/controller/BookingController.java +++ b/src/main/java/com/bookingtour/sun/controller/BookingController.java @@ -5,6 +5,7 @@ import com.bookingtour.sun.dto.request.PageResponse; import com.bookingtour.sun.dto.response.BookingReponse; import com.bookingtour.sun.services.BookingService; +import com.bookingtour.sun.services.CurrentUserService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -13,9 +14,7 @@ import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.support.RedirectAttributes; @Controller @@ -24,12 +23,13 @@ @Slf4j public class BookingController { private final BookingService bookingService; + private final CurrentUserService currentUserService; @GetMapping("") - public String listBooking(@AuthenticationPrincipal UserDetails userDetails, - Model model, + public String listBooking(Model model, BookingSearchRequest request) { - String email = userDetails.getUsername(); + String email = currentUserService.getCurrentUserEmail(); + PageResponse bookings = bookingService.getMyBooking(email, request); model.addAttribute("bookings", bookings.getContent()); @@ -51,8 +51,8 @@ public String createBooking(@AuthenticationPrincipal UserDetails userDetails, redirectAttributes.addFlashAttribute("errorMessage", "Thông tin đặt tour không hợp lệ"); return "redirect:/tours/" + request.getTourId(); } - String email = userDetails.getUsername(); try { + String email = currentUserService.getCurrentUserEmail(); bookingService.createBooking(email, request); redirectAttributes.addFlashAttribute("successMessage", "Đặt tour thành công"); return "redirect:/bookings"; @@ -62,4 +62,21 @@ public String createBooking(@AuthenticationPrincipal UserDetails userDetails, return "redirect:/tours/" + request.getTourId(); } } + + @PostMapping("/{id}/cancel") + public String cancelBooking( + @PathVariable Long id, + RedirectAttributes redirectAttributes + ) { + try { + String email = currentUserService.getCurrentUserEmail(); + bookingService.cancelBooking(id, email); + redirectAttributes.addFlashAttribute("successMessage", "Hủy đặt tour thành công"); + + } catch (Exception e) { + redirectAttributes.addFlashAttribute("errorMessage", e.getMessage()); + } + + return "redirect:/bookings"; + } } diff --git a/src/main/java/com/bookingtour/sun/controller/ClientController.java b/src/main/java/com/bookingtour/sun/controller/ClientController.java index 19dc611..9fe4ccd 100644 --- a/src/main/java/com/bookingtour/sun/controller/ClientController.java +++ b/src/main/java/com/bookingtour/sun/controller/ClientController.java @@ -1,10 +1,16 @@ package com.bookingtour.sun.controller; +import com.bookingtour.sun.dto.request.RegisterRequest; import com.bookingtour.sun.dto.response.TourResponse; import com.bookingtour.sun.services.TourService; +import com.bookingtour.sun.services.UserService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; import java.util.List; @@ -12,6 +18,7 @@ @RequiredArgsConstructor public class ClientController { private final TourService tourService; + private final UserService userService; @GetMapping("/") public String home(Model model) { @@ -26,4 +33,31 @@ public String login(Model model) { model.addAttribute("title", "Login - Tour Booking"); return "client/login"; } + + @GetMapping("/register") + public String register(Model model) { + model.addAttribute("title", "Register - Tour Booking"); + model.addAttribute("user", new RegisterRequest()); + return "client/register"; + } + + @PostMapping("/register") + public String register( + @Valid @ModelAttribute("user") RegisterRequest request, + BindingResult result, + Model model) { + + if (!request.getPassword().equals(request.getConfirmPassword())) { + model.addAttribute("passwordError", "Passwords do not match."); + return "client/register"; + } + + if (result.hasErrors()) { + return "client/register"; + } + + userService.register(request); + + return "redirect:/login?registered"; + } } diff --git a/src/main/java/com/bookingtour/sun/controller/PaymentController.java b/src/main/java/com/bookingtour/sun/controller/PaymentController.java new file mode 100644 index 0000000..1447e40 --- /dev/null +++ b/src/main/java/com/bookingtour/sun/controller/PaymentController.java @@ -0,0 +1,73 @@ +package com.bookingtour.sun.controller; + +import com.bookingtour.sun.entity.Payment; +import com.bookingtour.sun.exception.ResourceNotFoundException; +import com.bookingtour.sun.exception.UnauthorizedException; +import com.bookingtour.sun.repository.PaymentRepository; +import com.bookingtour.sun.services.CurrentUserService; +import com.bookingtour.sun.services.PaymentService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +@Controller +@RequestMapping("/payments") +@RequiredArgsConstructor +public class PaymentController { + private final PaymentService paymentService; + private final CurrentUserService currentUserService; + private final PaymentRepository paymentRepository; + + + @PostMapping("/{bookingId}/create") + public String createPayment(@PathVariable Long bookingId, RedirectAttributes redirectAttributes) + { + try { + String email = currentUserService.getCurrentUserEmail(); + String url = paymentService.createPayment(bookingId, email); + return "redirect:" + url; + } catch (Exception e) { + redirectAttributes.addFlashAttribute("errorMessage", "Thanh toán thất bại"); + return "redirect:/bookings"; + } + } + + @GetMapping("/mock/{id}") + public String mockPayment( + @PathVariable Long id, + Model model, + RedirectAttributes redirectAttributes + ){ + try { + String email = currentUserService.getCurrentUserEmail(); + Payment payment = paymentRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Payment not found with id: " + id)); + if(!payment.getBooking().getUser().getEmail().equals(email)) { + throw new UnauthorizedException("Invalid booking"); + } + model.addAttribute("paymentId", id); + return "client/payment/mock"; + } catch (Exception e) { + redirectAttributes.addFlashAttribute("errorMessage", "Thanh toán thất bại"); + return "redirect:/bookings"; + } + } + + @PostMapping("/{id}/success") + public String success(@PathVariable Long id, RedirectAttributes redirectAttributes) + { + try { + String email = currentUserService.getCurrentUserEmail(); + paymentService.success(id, email); + redirectAttributes.addFlashAttribute("successMessage", "Thanh toán thành công"); + } catch (Exception e) { + redirectAttributes.addFlashAttribute("errorMessage", "Thanh toán thất bại"); + } + + return "redirect:/bookings"; + } +} diff --git a/src/main/java/com/bookingtour/sun/controller/TourController.java b/src/main/java/com/bookingtour/sun/controller/TourController.java index 8d89fd8..4e6feb1 100644 --- a/src/main/java/com/bookingtour/sun/controller/TourController.java +++ b/src/main/java/com/bookingtour/sun/controller/TourController.java @@ -3,17 +3,17 @@ import com.bookingtour.sun.dto.request.ClientTourSearchRequest; import com.bookingtour.sun.dto.request.EditTourRequest; import com.bookingtour.sun.dto.request.PageResponse; +import com.bookingtour.sun.dto.request.ReviewRequest; import com.bookingtour.sun.dto.response.TourResponse; import com.bookingtour.sun.services.CategoryService; +import com.bookingtour.sun.services.CurrentUserService; +import com.bookingtour.sun.services.TourReviewService; import com.bookingtour.sun.services.TourService; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.support.RedirectAttributes; @Controller @@ -23,6 +23,8 @@ public class TourController { private final CategoryService categoryService; private final TourService tourService; + private final TourReviewService reviewService; + private final CurrentUserService currentUserService; @GetMapping("") public String listTour(@ModelAttribute ClientTourSearchRequest searchForm, Model model) { @@ -52,6 +54,7 @@ public String showTour(@PathVariable Long id, Model model, RedirectAttributes re model.addAttribute("tour", request); model.addAttribute("tourImages", tourService.getTourImages(id)); model.addAttribute("tourItineraries", tourService.getTourItineraries(id)); + model.addAttribute("reviews", reviewService.getReviews(id)); return "client/tours/show"; } catch (Exception e) { @@ -60,4 +63,20 @@ public String showTour(@PathVariable Long id, Model model, RedirectAttributes re return "redirect:/tours"; } } + + @PostMapping("/{id}/reviews") + public String review( + @PathVariable Long id, + @ModelAttribute ReviewRequest request, + RedirectAttributes redirectAttributes){ + + try { + String email = currentUserService.getCurrentUserEmail(); + reviewService.createReview(id, email, request); + } catch (Exception e) { + redirectAttributes.addFlashAttribute("errorMessage", "Failed to review tour for viewing: " + e.getMessage()); + } + + return "redirect:/tours/"+id; + } } diff --git a/src/main/java/com/bookingtour/sun/controller/admin/AdminBookingController.java b/src/main/java/com/bookingtour/sun/controller/admin/AdminBookingController.java index e22a34c..72b4cfd 100644 --- a/src/main/java/com/bookingtour/sun/controller/admin/AdminBookingController.java +++ b/src/main/java/com/bookingtour/sun/controller/admin/AdminBookingController.java @@ -81,4 +81,19 @@ public String updateBooking(@PathVariable Long id, @Valid @ModelAttribute("booki return "admin/booking/edit"; } } + + @DeleteMapping("/{id}") + public String deleteBooking( + @PathVariable Long id, + RedirectAttributes redirectAttributes) { + + try { + bookingService.deleteBooking(id); + redirectAttributes.addFlashAttribute("successMessage", "Delete booking successfully"); + + } catch (Exception e) { + redirectAttributes.addFlashAttribute("errorMessage", e.getMessage()); + } + return "redirect:/admin/bookings"; + } } diff --git a/src/main/java/com/bookingtour/sun/controller/admin/AdminCategoryController.java b/src/main/java/com/bookingtour/sun/controller/admin/AdminCategoryController.java index 8c49781..ee5abcb 100644 --- a/src/main/java/com/bookingtour/sun/controller/admin/AdminCategoryController.java +++ b/src/main/java/com/bookingtour/sun/controller/admin/AdminCategoryController.java @@ -1,16 +1,18 @@ package com.bookingtour.sun.controller.admin; +import com.bookingtour.sun.dto.request.AdminCreateCategoryRequest; import com.bookingtour.sun.dto.request.CategorySearchRequest; import com.bookingtour.sun.dto.request.PageResponse; import com.bookingtour.sun.dto.response.CategoryResponse; import com.bookingtour.sun.services.CategoryService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; @Controller @RequestMapping("/admin/categories") @@ -23,16 +25,112 @@ public class AdminCategoryController { public String listCategory(@ModelAttribute CategorySearchRequest request, Model model) { model.addAttribute("title", "Manage Category"); model.addAttribute("activeMenu", "categories"); + loadCategoryPage(request, model); + model.addAttribute("category", new AdminCreateCategoryRequest()); + model.addAttribute("editCategory", new AdminCreateCategoryRequest()); + return "admin/categories/index"; + } + + @PostMapping + public String create( + @Valid @ModelAttribute("category") AdminCreateCategoryRequest request, + BindingResult result, + Model model, + RedirectAttributes redirectAttributes) { + CategorySearchRequest searchRequest = new CategorySearchRequest(); + if (result.hasErrors()) { + model.addAttribute("category", request); + model.addAttribute("editCategory", new AdminCreateCategoryRequest()); + loadCategoryPage(searchRequest, model); + model.addAttribute("openCreateModal", true); + return "admin/categories/index"; + } + + categoryService.create(request); + + redirectAttributes.addFlashAttribute("successMessage", "Category created successfully."); + + return "redirect:/admin/categories"; + } + + @PutMapping("/{id}") + public String updateCategory( + @PathVariable Long id, + @Valid + @ModelAttribute("editCategory") + AdminCreateCategoryRequest request, + BindingResult result, + Model model, + RedirectAttributes redirectAttributes + ){ + CategorySearchRequest searchRequest = new CategorySearchRequest(); + if(result.hasErrors()) { + loadCategoryPage(searchRequest, model); + model.addAttribute("editId", id); + model.addAttribute("editCategory", request); + model.addAttribute("category", new AdminCreateCategoryRequest()); + + model.addAttribute("openEditModal", true); + return "admin/categories/index"; + } + + try { + categoryService.update(id, request); + + redirectAttributes.addFlashAttribute( + "successMessage", + "Update category successfully" + ); + + } catch(Exception e){ + redirectAttributes.addFlashAttribute( + "errorMessage", + e.getMessage() + ); + + } + + return "redirect:/admin/categories"; + } + + @DeleteMapping("/{id}") + public String deleteUser( + @PathVariable Long id, + RedirectAttributes redirectAttributes) { + + try { + categoryService.deleteCategory(id); + + redirectAttributes.addFlashAttribute( + "successMessage", + "Category deleted successfully"); + } catch (Exception e) { + redirectAttributes.addFlashAttribute( + "errorMessage", + "Delete failed: " + e.getMessage()); + } + + return "redirect:/admin/categories"; + } + + private void loadCategoryPage(CategorySearchRequest request, Model model) { + + model.addAttribute("title", "Manage Category"); + model.addAttribute("activeMenu", "categories"); + PageResponse pageResponse = categoryService.searchCategory(request); model.addAttribute("categories", pageResponse.getContent()); - // Pagination and filter info for template + model.addAttribute("pageNumber", pageResponse.getPage() + 1); model.addAttribute("pageSize", pageResponse.getSize()); model.addAttribute("totalPages", pageResponse.getTotalPages()); model.addAttribute("totalElements", pageResponse.getTotalElements()); - model.addAttribute("currentName", request.getName() != null ? request.getName() : ""); - return "admin/categories/index"; + model.addAttribute( + "currentName", + request.getName() == null ? "" : request.getName() + ); } + } diff --git a/src/main/java/com/bookingtour/sun/controller/admin/AdminUserController.java b/src/main/java/com/bookingtour/sun/controller/admin/AdminUserController.java index f6392ce..6ab8a13 100644 --- a/src/main/java/com/bookingtour/sun/controller/admin/AdminUserController.java +++ b/src/main/java/com/bookingtour/sun/controller/admin/AdminUserController.java @@ -1,18 +1,20 @@ package com.bookingtour.sun.controller.admin; -import com.bookingtour.sun.dto.request.PageResponse; -import com.bookingtour.sun.dto.request.UserSearchRequest; +import com.bookingtour.sun.dto.request.*; import com.bookingtour.sun.dto.response.UserResponse; import com.bookingtour.sun.enums.UserRole; import com.bookingtour.sun.enums.UserStatus; +import com.bookingtour.sun.services.CurrentUserService; import com.bookingtour.sun.services.UserService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.flywaydb.core.internal.util.StringUtils; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; @Controller @RequestMapping("/admin/users") @@ -20,6 +22,7 @@ @Slf4j public class AdminUserController { private final UserService userService; + private final CurrentUserService currentUserService; @GetMapping() public String listUser(@ModelAttribute UserSearchRequest request, Model model) { @@ -40,4 +43,127 @@ public String listUser(@ModelAttribute UserSearchRequest request, Model model) { model.addAttribute("currentRole", request.getRole() != null ? request.getRole() : ""); return "admin/users/index"; } + + @GetMapping("/new") + public String newUser(Model model) { + model.addAttribute("activeMenu", "users"); + model.addAttribute("statuses", UserStatus.values()); + model.addAttribute("roles", UserRole.values()); + model.addAttribute("user", new AdminCreateUserRequest()); + return "admin/users/new"; + } + + @PostMapping() + public String createUser( + @Valid @ModelAttribute("user") AdminCreateUserRequest request, + BindingResult result, + Model model, RedirectAttributes redirectAttributes) { + + if (!request.getPassword().equals(request.getConfirmPassword())) { + model.addAttribute("passwordError", "Passwords do not match."); + model.addAttribute("statuses", UserStatus.values()); + model.addAttribute("roles", UserRole.values()); + return "admin/users/new"; + } + + if (result.hasErrors()) { + model.addAttribute("statuses", UserStatus.values()); + model.addAttribute("roles", UserRole.values()); + return "admin/users/new"; + } + + userService.createUser(request); + redirectAttributes.addFlashAttribute("successMessage", "User created successfully!"); + return "redirect:/admin/users"; + } + + @GetMapping("/{id}/edit") + public String editUser(@PathVariable Long id, Model model, RedirectAttributes redirectAttributes) { + model.addAttribute("activeMenu", "users"); + try { + AdminUpdateUserRequeset request = userService.getForUpdate(id); + model.addAttribute("statuses", UserStatus.values()); + model.addAttribute("roles", UserRole.values()); + model.addAttribute("user", request); + return "admin/users/edit"; + } catch (Exception e) { + log.error("Error occurred while fetching user for edit: {}", e.getMessage()); + redirectAttributes.addFlashAttribute("errorMessage", "Failed to update user: " + e.getMessage()); + return "redirect:/admin/users"; + } + } + + @PutMapping("/{id}") + public String update( + @PathVariable Long id, + @Valid @ModelAttribute("user") AdminUpdateUserRequeset request, + BindingResult result, + Model model, + RedirectAttributes redirectAttributes) { + + if (!StringUtils.hasText(request.getPassword())) { + + request.setPassword(null); + request.setConfirmPassword(null); + + } else { + if (request.getPassword().length() < 8 + || request.getPassword().length() > 100) { + + result.rejectValue( + "password", + "password.length", + "Password must be between 8 and 100 characters." + ); + } + if (!StringUtils.hasText(request.getConfirmPassword())) { + + result.rejectValue( + "confirmPassword", + "confirmPassword.required", + "Please confirm your password." + ); + } else if (!request.getPassword().equals(request.getConfirmPassword())) { + + result.rejectValue( + "confirmPassword", + "password.mismatch", + "Password confirmation does not match." + ); + } + } + + if (result.hasErrors()) { + + model.addAttribute("roles", UserRole.values()); + model.addAttribute("statuses", UserStatus.values()); + + return "admin/users/edit"; + } + + userService.update(id, request); + redirectAttributes.addFlashAttribute("successMessage", "User updated successfully!"); + return "redirect:/admin/users"; + } + + @DeleteMapping("/{id}") + public String deleteUser( + @PathVariable Long id, + RedirectAttributes redirectAttributes) { + + try { + String email = currentUserService.getCurrentUserEmail(); + userService.deleteUser(id, email); + + redirectAttributes.addFlashAttribute( + "successMessage", + "User deleted successfully"); + } catch (Exception e) { + redirectAttributes.addFlashAttribute( + "errorMessage", + "Delete failed: " + e.getMessage()); + } + + return "redirect:/admin/users"; + } } diff --git a/src/main/java/com/bookingtour/sun/dto/request/AdminCreateCategoryRequest.java b/src/main/java/com/bookingtour/sun/dto/request/AdminCreateCategoryRequest.java new file mode 100644 index 0000000..55caa19 --- /dev/null +++ b/src/main/java/com/bookingtour/sun/dto/request/AdminCreateCategoryRequest.java @@ -0,0 +1,18 @@ +package com.bookingtour.sun.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class AdminCreateCategoryRequest { + @NotBlank(message = "Name is required") + @Size(min = 2, max = 255, message = "Name must be between 2 and 255 characters") + private String name; + + @NotBlank(message = "Description is required") + @Size(min = 10, message = "Description must be at least 10 characters") + private String description; +} diff --git a/src/main/java/com/bookingtour/sun/dto/request/AdminCreateUserRequest.java b/src/main/java/com/bookingtour/sun/dto/request/AdminCreateUserRequest.java new file mode 100644 index 0000000..628b3dd --- /dev/null +++ b/src/main/java/com/bookingtour/sun/dto/request/AdminCreateUserRequest.java @@ -0,0 +1,39 @@ +package com.bookingtour.sun.dto.request; +import com.bookingtour.sun.enums.UserRole; +import com.bookingtour.sun.enums.UserStatus; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class AdminCreateUserRequest { + @NotBlank(message = "Username is required.") + private String username; + + @NotBlank(message = "Full name is required.") + private String fullName; + + @NotBlank(message = "Email is required.") + @Email(message = "Please enter a valid email address.") + private String email; + + private String phone; + + @NotBlank(message = "Password is required.") + @Size(min = 8, max = 100, + message = "Password must be between 8 and 100 characters.") + private String password; + + @NotBlank(message = "Please confirm your password.") + private String confirmPassword; + + @NotNull(message = "Please select a role.") + private UserRole role; + + @NotNull(message = "Please select a status.") + private UserStatus status; +} diff --git a/src/main/java/com/bookingtour/sun/dto/request/AdminUpdateUserRequeset.java b/src/main/java/com/bookingtour/sun/dto/request/AdminUpdateUserRequeset.java new file mode 100644 index 0000000..a887976 --- /dev/null +++ b/src/main/java/com/bookingtour/sun/dto/request/AdminUpdateUserRequeset.java @@ -0,0 +1,37 @@ +package com.bookingtour.sun.dto.request; + +import com.bookingtour.sun.enums.UserRole; +import com.bookingtour.sun.enums.UserStatus; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class AdminUpdateUserRequeset { + private Long id; + + @NotBlank(message = "Username is required.") + private String username; + + @NotBlank(message = "Full name is required.") + private String fullName; + + @NotBlank(message = "Email is required.") + @Email(message = "Please enter a valid email address.") + private String email; + + private String phone; + + private String password; + + private String confirmPassword; + + @NotNull(message = "Please select a role.") + private UserRole role; + + @NotNull(message = "Please select a status.") + private UserStatus status; +} diff --git a/src/main/java/com/bookingtour/sun/dto/request/CreateTourRequest.java b/src/main/java/com/bookingtour/sun/dto/request/CreateTourRequest.java index d162bc4..40b58cd 100644 --- a/src/main/java/com/bookingtour/sun/dto/request/CreateTourRequest.java +++ b/src/main/java/com/bookingtour/sun/dto/request/CreateTourRequest.java @@ -26,10 +26,7 @@ public class CreateTourRequest { @Size(min = 10, message = "Description must be at least 10 characters") private String description; - private String departureLocation; - - private String destination; @Min(value = 1, message = "Duration must be a positive number") diff --git a/src/main/java/com/bookingtour/sun/dto/request/EditTourRequest.java b/src/main/java/com/bookingtour/sun/dto/request/EditTourRequest.java index ad99185..75ed8ea 100644 --- a/src/main/java/com/bookingtour/sun/dto/request/EditTourRequest.java +++ b/src/main/java/com/bookingtour/sun/dto/request/EditTourRequest.java @@ -6,6 +6,7 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.*; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.multipart.MultipartFile; import java.math.BigDecimal; @@ -28,10 +29,8 @@ public class EditTourRequest { @Size(min = 10, message = "Description must be at least 10 characters") private String description; - private String departureLocation; - private String destination; @Min(value = 1, message = "Duration must be a positive number") @@ -42,8 +41,12 @@ public class EditTourRequest { private BigDecimal price; + @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate startDate; + + @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate endDate; + private Long categoryId; private TourStatus status; private String categoryName; @@ -54,4 +57,6 @@ public class EditTourRequest { @Builder.Default @Valid private List itineraries = new ArrayList<>(); + private Double averageRating; + private Long reviewCount; } diff --git a/src/main/java/com/bookingtour/sun/dto/request/RatingSummary.java b/src/main/java/com/bookingtour/sun/dto/request/RatingSummary.java new file mode 100644 index 0000000..a8a9cd0 --- /dev/null +++ b/src/main/java/com/bookingtour/sun/dto/request/RatingSummary.java @@ -0,0 +1,14 @@ +package com.bookingtour.sun.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class RatingSummary { + private Double averageRating; + private Long reviewCount; +} diff --git a/src/main/java/com/bookingtour/sun/dto/request/RegisterRequest.java b/src/main/java/com/bookingtour/sun/dto/request/RegisterRequest.java new file mode 100644 index 0000000..ba0edf1 --- /dev/null +++ b/src/main/java/com/bookingtour/sun/dto/request/RegisterRequest.java @@ -0,0 +1,31 @@ +package com.bookingtour.sun.dto.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RegisterRequest { + @NotBlank(message = "Username is required.") + private String username; + + @NotBlank(message = "Full name is required.") + private String fullName; + + @Email(message = "Please enter a valid email address.") + @NotBlank(message = "Email is required.") + private String email; + + private String phone; + + @NotBlank(message = "Password is required.") + @Size(min = 8, max = 100, + message = "Password must be between 8 and 100 characters.") + private String password; + + @NotBlank(message = "Please confirm your password.") + private String confirmPassword; +} diff --git a/src/main/java/com/bookingtour/sun/dto/request/ReviewRequest.java b/src/main/java/com/bookingtour/sun/dto/request/ReviewRequest.java new file mode 100644 index 0000000..2f4ee45 --- /dev/null +++ b/src/main/java/com/bookingtour/sun/dto/request/ReviewRequest.java @@ -0,0 +1,9 @@ +package com.bookingtour.sun.dto.request; + +import lombok.Data; + +@Data +public class ReviewRequest { + private Integer rating; + private String comment; +} diff --git a/src/main/java/com/bookingtour/sun/dto/response/ReviewResponse.java b/src/main/java/com/bookingtour/sun/dto/response/ReviewResponse.java new file mode 100644 index 0000000..590a665 --- /dev/null +++ b/src/main/java/com/bookingtour/sun/dto/response/ReviewResponse.java @@ -0,0 +1,21 @@ +package com.bookingtour.sun.dto.response; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@Builder +public class ReviewResponse { + + private String username; + + private String avatar; + + private Integer rating; + + private String comment; + + private LocalDateTime createdAt; +} diff --git a/src/main/java/com/bookingtour/sun/entity/Booking.java b/src/main/java/com/bookingtour/sun/entity/Booking.java index 5fe35ce..7e4b892 100644 --- a/src/main/java/com/bookingtour/sun/entity/Booking.java +++ b/src/main/java/com/bookingtour/sun/entity/Booking.java @@ -52,6 +52,10 @@ public class Booking extends BaseEntity { @Column(columnDefinition = "TEXT") private String note; + @OneToOne(mappedBy = "booking", + cascade = CascadeType.ALL) + private Payment payment; + @PrePersist public void prePersist() { if (bookingDate == null) { diff --git a/src/main/java/com/bookingtour/sun/entity/Payment.java b/src/main/java/com/bookingtour/sun/entity/Payment.java new file mode 100644 index 0000000..8f45418 --- /dev/null +++ b/src/main/java/com/bookingtour/sun/entity/Payment.java @@ -0,0 +1,35 @@ +package com.bookingtour.sun.entity; + +import com.bookingtour.sun.enums.PaymentStatus; +import jakarta.persistence.*; +import lombok.*; + +import java.math.BigDecimal; + +@Entity +@Table(name = "payments") +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Payment extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne + @JoinColumn(name = "booking_id", nullable = false) + private Booking booking; + + @Column(nullable = false) + private BigDecimal amount; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private PaymentStatus status; + + private String paymentMethod; + + private String transactionId; +} diff --git a/src/main/java/com/bookingtour/sun/entity/Tour.java b/src/main/java/com/bookingtour/sun/entity/Tour.java index 11b1fdf..1910f97 100644 --- a/src/main/java/com/bookingtour/sun/entity/Tour.java +++ b/src/main/java/com/bookingtour/sun/entity/Tour.java @@ -5,7 +5,6 @@ import jakarta.persistence.Table; import jakarta.persistence.*; import lombok.*; -import com.bookingtour.sun.entity.Category; import java.math.BigDecimal; import java.time.LocalDate; diff --git a/src/main/java/com/bookingtour/sun/entity/TourReview.java b/src/main/java/com/bookingtour/sun/entity/TourReview.java new file mode 100644 index 0000000..697244a --- /dev/null +++ b/src/main/java/com/bookingtour/sun/entity/TourReview.java @@ -0,0 +1,41 @@ +package com.bookingtour.sun.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name="tour_reviews", + uniqueConstraints = { + @UniqueConstraint( + name="uk_user_tour_review", + columnNames={"tour_id","user_id"} + ) + }) +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TourReview extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="tour_id") + private Tour tour; + + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="user_id") + private User user; + + + @Column(nullable=false) + private Integer rating; + + + @Column(columnDefinition="TEXT") + private String comment; +} diff --git a/src/main/java/com/bookingtour/sun/entity/User.java b/src/main/java/com/bookingtour/sun/entity/User.java index 105d01d..9c81719 100644 --- a/src/main/java/com/bookingtour/sun/entity/User.java +++ b/src/main/java/com/bookingtour/sun/entity/User.java @@ -1,10 +1,14 @@ package com.bookingtour.sun.entity; +import com.bookingtour.sun.enums.LoginProvider; import com.bookingtour.sun.enums.UserRole; import com.bookingtour.sun.enums.UserStatus; import jakarta.persistence.*; import lombok.*; +import java.util.ArrayList; +import java.util.List; + @Entity @Table(name = "users") @@ -24,7 +28,7 @@ public class User extends BaseEntity { @Column(nullable = false, unique = true, length = 100) private String email; - @Column(name = "password_digest", nullable = false) + @Column(name = "password_digest") private String passwordDigest; @Column(name = "full_name", length = 100) @@ -43,6 +47,16 @@ public class User extends BaseEntity { @Enumerated(EnumType.STRING) private UserStatus status; + @Enumerated(EnumType.STRING) + @Column(name = "login_provider", nullable = false) + private LoginProvider loginProvider; + + @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) + private List bookings = new ArrayList<>();; + + @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) + private List reviews = new ArrayList<>(); + @PrePersist protected void onCreate() { if (role == null) { @@ -51,5 +65,9 @@ protected void onCreate() { if (status == null) { status = UserStatus.ACTIVE; // Default status } + + if (loginProvider == null) { + loginProvider = LoginProvider.LOCAL; + } } } diff --git a/src/main/java/com/bookingtour/sun/enums/LoginProvider.java b/src/main/java/com/bookingtour/sun/enums/LoginProvider.java new file mode 100644 index 0000000..3bc6e0b --- /dev/null +++ b/src/main/java/com/bookingtour/sun/enums/LoginProvider.java @@ -0,0 +1,6 @@ +package com.bookingtour.sun.enums; + +public enum LoginProvider { + LOCAL, + GOOGLE +} diff --git a/src/main/java/com/bookingtour/sun/enums/PaymentStatus.java b/src/main/java/com/bookingtour/sun/enums/PaymentStatus.java new file mode 100644 index 0000000..8b7e02a --- /dev/null +++ b/src/main/java/com/bookingtour/sun/enums/PaymentStatus.java @@ -0,0 +1,8 @@ +package com.bookingtour.sun.enums; + +public enum PaymentStatus { + PENDING, + SUCCESS, + FAILED, + REFUNDED +} diff --git a/src/main/java/com/bookingtour/sun/exception/DuplicateResourceException.java b/src/main/java/com/bookingtour/sun/exception/DuplicateResourceException.java new file mode 100644 index 0000000..c48acec --- /dev/null +++ b/src/main/java/com/bookingtour/sun/exception/DuplicateResourceException.java @@ -0,0 +1,5 @@ +package com.bookingtour.sun.exception; + +public class DuplicateResourceException extends RuntimeException { + public DuplicateResourceException(String message) {super(message);} +} diff --git a/src/main/java/com/bookingtour/sun/exception/UnauthorizedException.java b/src/main/java/com/bookingtour/sun/exception/UnauthorizedException.java new file mode 100644 index 0000000..368f085 --- /dev/null +++ b/src/main/java/com/bookingtour/sun/exception/UnauthorizedException.java @@ -0,0 +1,5 @@ +package com.bookingtour.sun.exception; + +public class UnauthorizedException extends RuntimeException { + public UnauthorizedException(String message) {super(message);} +} diff --git a/src/main/java/com/bookingtour/sun/repository/BookingRepository.java b/src/main/java/com/bookingtour/sun/repository/BookingRepository.java index c8adbfb..ab2e6f6 100644 --- a/src/main/java/com/bookingtour/sun/repository/BookingRepository.java +++ b/src/main/java/com/bookingtour/sun/repository/BookingRepository.java @@ -17,5 +17,7 @@ SELECT COALESCE(SUM(b.totalAmount),0) WHERE b.status IN ('PAID', 'COMPLETED', 'CONFIRMED') """) BigDecimal getRevenue(); + + boolean existsByTourId(Long tourId); } diff --git a/src/main/java/com/bookingtour/sun/repository/PaymentRepository.java b/src/main/java/com/bookingtour/sun/repository/PaymentRepository.java new file mode 100644 index 0000000..36aa5f3 --- /dev/null +++ b/src/main/java/com/bookingtour/sun/repository/PaymentRepository.java @@ -0,0 +1,11 @@ +package com.bookingtour.sun.repository; + +import com.bookingtour.sun.entity.Payment; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface PaymentRepository + extends JpaRepository { + Optional findByBookingId(Long bookingId); +} diff --git a/src/main/java/com/bookingtour/sun/repository/TourRepository.java b/src/main/java/com/bookingtour/sun/repository/TourRepository.java index a4664b3..a687c2d 100644 --- a/src/main/java/com/bookingtour/sun/repository/TourRepository.java +++ b/src/main/java/com/bookingtour/sun/repository/TourRepository.java @@ -13,4 +13,6 @@ List findTop4ByStatusOrderByAverageRatingDesc( ); long count(); + + boolean existsByCategoryId(Long categoryId); } diff --git a/src/main/java/com/bookingtour/sun/repository/TourReviewRepository.java b/src/main/java/com/bookingtour/sun/repository/TourReviewRepository.java new file mode 100644 index 0000000..8681d66 --- /dev/null +++ b/src/main/java/com/bookingtour/sun/repository/TourReviewRepository.java @@ -0,0 +1,29 @@ +package com.bookingtour.sun.repository; + +import com.bookingtour.sun.dto.request.RatingSummary; +import com.bookingtour.sun.entity.TourReview; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface TourReviewRepository + extends JpaRepository { + + List findByTourIdOrderByCreatedAtDesc(Long tourId); + + boolean existsByTourIdAndUserId( + Long tourId, + Long userId + ); + + @Query(""" + SELECT + AVG(r.rating), + COUNT(r.id) + FROM TourReview r + WHERE r.tour.id = :tourId + """) + RatingSummary getRatingSummary(@Param("tourId") Long tourId); +} diff --git a/src/main/java/com/bookingtour/sun/repository/UserRepository.java b/src/main/java/com/bookingtour/sun/repository/UserRepository.java index 723c1c9..feed57c 100644 --- a/src/main/java/com/bookingtour/sun/repository/UserRepository.java +++ b/src/main/java/com/bookingtour/sun/repository/UserRepository.java @@ -12,4 +12,7 @@ public interface UserRepository extends JpaRepository, JpaSpecificat @Query("SELECT COUNT(u) FROM User u WHERE u.role = 'USER'") long countUsers(); + + boolean existsByEmailAndIdNot(String email, Long id); + boolean existsByEmail(String email); } diff --git a/src/main/java/com/bookingtour/sun/services/BookingService.java b/src/main/java/com/bookingtour/sun/services/BookingService.java index 77e898f..f8b8726 100644 --- a/src/main/java/com/bookingtour/sun/services/BookingService.java +++ b/src/main/java/com/bookingtour/sun/services/BookingService.java @@ -6,12 +6,15 @@ import com.bookingtour.sun.entity.Tour; import com.bookingtour.sun.entity.TourImage; import com.bookingtour.sun.entity.User; +import com.bookingtour.sun.enums.BookingStatus; import com.bookingtour.sun.exception.ResourceNotFoundException; +import com.bookingtour.sun.exception.UnauthorizedException; import com.bookingtour.sun.repository.BookingRepository; import com.bookingtour.sun.repository.TourRepository; import com.bookingtour.sun.repository.UserRepository; import com.bookingtour.sun.specification.AdminBookingSpecification; import com.bookingtour.sun.specification.BookingSpecification; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -60,7 +63,7 @@ public PageResponse getMyBooking(String email, BookingSearchRequ request.getSize(), Sort.by( Sort.Direction.DESC, - "bookingDate") + "createdAt") ); Page bookings = @@ -137,6 +140,38 @@ public void updateBooking(Long id, AdminEditBookingRequest request) { bookingRepository.save(booking); } + public void cancelBooking(Long bookingId, String email) { + Booking booking = bookingRepository.findById(bookingId) + .orElseThrow(() -> new ResourceNotFoundException("Booking not found")); + + // check booking owner + if(!booking.getUser().getEmail().equals(email)) { + throw new UnauthorizedException("You cannot cancel this booking"); + } + + // chỉ pending payment mới được cancel + if(booking.getStatus() != BookingStatus.PENDING_PAYMENT){ + throw new IllegalStateException("Only pending payment booking can be cancelled"); + } + booking.setStatus(BookingStatus.CANCELLED); + bookingRepository.save(booking); + } + + @Transactional + public void deleteBooking(Long id){ + Booking booking = bookingRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Booking not found")); + if(isNotAllowDelete(booking.getStatus())){ + throw new IllegalStateException("Cannot delete confirmed booking"); + } + bookingRepository.delete(booking); + } + + private boolean isNotAllowDelete(BookingStatus status) { + return status == BookingStatus.CONFIRMED + || status == BookingStatus.PAID + || status == BookingStatus.COMPLETED; + } + private BookingReponse toBookingResponse( Booking booking) { diff --git a/src/main/java/com/bookingtour/sun/services/CategoryService.java b/src/main/java/com/bookingtour/sun/services/CategoryService.java index d028c41..220d7b4 100644 --- a/src/main/java/com/bookingtour/sun/services/CategoryService.java +++ b/src/main/java/com/bookingtour/sun/services/CategoryService.java @@ -1,10 +1,14 @@ package com.bookingtour.sun.services; +import com.bookingtour.sun.dto.request.AdminCreateCategoryRequest; import com.bookingtour.sun.dto.request.CategorySearchRequest; import com.bookingtour.sun.dto.request.PageResponse; import com.bookingtour.sun.dto.response.CategoryResponse; import com.bookingtour.sun.entity.Category; +import com.bookingtour.sun.exception.ResourceNotFoundException; import com.bookingtour.sun.repository.CategoryRepository; +import com.bookingtour.sun.repository.TourRepository; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -18,6 +22,7 @@ @RequiredArgsConstructor public class CategoryService { private final CategoryRepository categoryRepository; + private final TourRepository tourRepository; public List getAllCategories() { return categoryRepository.findAll().stream() @@ -60,6 +65,31 @@ public PageResponse searchCategory(CategorySearchRequest reque .build(); } + public void create(AdminCreateCategoryRequest request) { + Category category = Category.builder() + .name(request.getName()) + .description(request.getDescription()) + .build(); + categoryRepository.save(category); + } + + @Transactional + public void update(Long id, AdminCreateCategoryRequest request) { + Category category = categoryRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Category not found with id: " + id)); + category.setName(request.getName()); + category.setDescription(request.getDescription()); + } + + @Transactional + public void deleteCategory(Long id) { + Category category = categoryRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Category not found with id: " + id)); + if (tourRepository.existsByCategoryId(id)) { + throw new IllegalStateException("Không thể xóa danh mục này vì đang có tour sử dụng"); + } + categoryRepository.delete(category); + } + + private CategoryResponse mapToResponse(Category category) { return CategoryResponse.builder() .id(category.getId()) diff --git a/src/main/java/com/bookingtour/sun/services/CurrentUserService.java b/src/main/java/com/bookingtour/sun/services/CurrentUserService.java new file mode 100644 index 0000000..f9aa561 --- /dev/null +++ b/src/main/java/com/bookingtour/sun/services/CurrentUserService.java @@ -0,0 +1,33 @@ +package com.bookingtour.sun.services; + +import com.bookingtour.sun.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CurrentUserService { + + private final UserRepository userRepository; + + public String getCurrentUserEmail() { + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + Object principal = authentication.getPrincipal(); + + if (principal instanceof UserDetails userDetails) { + return userDetails.getUsername(); + } + + if (principal instanceof OAuth2User oAuth2User) { + return oAuth2User.getAttribute("email"); + } + + throw new IllegalStateException("User is not authenticated."); + } +} diff --git a/src/main/java/com/bookingtour/sun/services/PaymentService.java b/src/main/java/com/bookingtour/sun/services/PaymentService.java new file mode 100644 index 0000000..30b3743 --- /dev/null +++ b/src/main/java/com/bookingtour/sun/services/PaymentService.java @@ -0,0 +1,61 @@ +package com.bookingtour.sun.services; + +import com.bookingtour.sun.entity.Booking; +import com.bookingtour.sun.entity.Payment; +import com.bookingtour.sun.enums.BookingStatus; +import com.bookingtour.sun.enums.PaymentStatus; +import com.bookingtour.sun.exception.ResourceNotFoundException; +import com.bookingtour.sun.exception.UnauthorizedException; +import com.bookingtour.sun.repository.BookingRepository; +import com.bookingtour.sun.repository.PaymentRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class PaymentService { + + private final BookingRepository bookingRepository; + private final PaymentRepository paymentRepository; + + @Transactional + public String createPayment(Long bookingId, String email){ + Booking booking = bookingRepository.findById(bookingId) + .orElseThrow(() -> new ResourceNotFoundException("Booking not found with id: " + bookingId)); + + if(!booking.getUser().getEmail().equals(email)){ + throw new UnauthorizedException("Invalid booking"); + } + if(booking.getStatus() != BookingStatus.PENDING_PAYMENT){ + throw new IllegalStateException("Booking cannot be paid"); + } + Payment payment = paymentRepository + .findByBookingId(bookingId) + .orElse(null); + if (payment == null) { + payment = Payment.builder() + .booking(booking) + .amount(booking.getTotalAmount()) + .status(PaymentStatus.PENDING) + .paymentMethod("ONLINE") + .build(); + } + + paymentRepository.save(payment); + + // gia lap url + return "/payments/mock/" + payment.getId(); + } + + @Transactional + public void success(Long paymentId, String email){ + Payment payment = paymentRepository.findById(paymentId) + .orElseThrow(() -> new ResourceNotFoundException("Payment not found with id: " + paymentId)); + if(!payment.getBooking().getUser().getEmail().equals(email)){ + throw new UnauthorizedException("Invalid booking"); + } + payment.setStatus(PaymentStatus.SUCCESS); + payment.getBooking().setStatus(BookingStatus.PAID); + } +} diff --git a/src/main/java/com/bookingtour/sun/services/TourReviewService.java b/src/main/java/com/bookingtour/sun/services/TourReviewService.java new file mode 100644 index 0000000..fd4e489 --- /dev/null +++ b/src/main/java/com/bookingtour/sun/services/TourReviewService.java @@ -0,0 +1,52 @@ +package com.bookingtour.sun.services; + +import com.bookingtour.sun.dto.request.ReviewRequest; +import com.bookingtour.sun.entity.Tour; +import com.bookingtour.sun.entity.TourReview; +import com.bookingtour.sun.entity.User; +import com.bookingtour.sun.exception.DuplicateResourceException; +import com.bookingtour.sun.exception.ResourceNotFoundException; +import com.bookingtour.sun.repository.TourRepository; +import com.bookingtour.sun.repository.TourReviewRepository; +import com.bookingtour.sun.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class TourReviewService { + private final TourReviewRepository reviewRepository; + private final TourRepository tourRepository; + private final UserRepository userRepository; + + public void createReview( + Long tourId, + String email, + ReviewRequest request + ){ + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new ResourceNotFoundException("User not found with email: " + email)); + + if(reviewRepository.existsByTourIdAndUserId(tourId, user.getId())){ + throw new DuplicateResourceException("You already reviewed this tour"); + } + Tour tour = tourRepository.findById(tourId) + .orElseThrow(() -> new ResourceNotFoundException("Tour not found with id: " + tourId)); + + TourReview review = TourReview.builder() + .tour(tour) + .user(user) + .rating(request.getRating()) + .comment(request.getComment()) + .build(); + + reviewRepository.save(review); + } + + + public List getReviews(Long tourId){ + return reviewRepository.findByTourIdOrderByCreatedAtDesc(tourId); + } +} diff --git a/src/main/java/com/bookingtour/sun/services/TourService.java b/src/main/java/com/bookingtour/sun/services/TourService.java index e497880..ab1fb23 100644 --- a/src/main/java/com/bookingtour/sun/services/TourService.java +++ b/src/main/java/com/bookingtour/sun/services/TourService.java @@ -10,9 +10,7 @@ import com.bookingtour.sun.entity.TourItinerary; import com.bookingtour.sun.enums.TourStatus; import com.bookingtour.sun.exception.ResourceNotFoundException; -import com.bookingtour.sun.repository.TourImageRepository; -import com.bookingtour.sun.repository.TourItineraryRepository; -import com.bookingtour.sun.repository.TourRepository; +import com.bookingtour.sun.repository.*; import com.bookingtour.sun.specification.TourSpecification; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; @@ -36,6 +34,8 @@ public class TourService { private final TourImageRepository tourImageRepository; private final FileStorageService fileStorageService; private final TourItineraryRepository tourItineraryRepository; + private final TourReviewRepository tourReviewRepository; + private final BookingRepository bookingRepository; @Transactional public TourResponse createTour(CreateTourRequest request) { @@ -194,6 +194,15 @@ public EditTourRequest getTourForEdit(Long id) { .build()) .toList(); request.setItineraries(itineraries); + RatingSummary rating = tourReviewRepository.getRatingSummary(id); + if(rating != null){ + request.setAverageRating(rating.getAverageRating()); + request.setReviewCount(rating.getReviewCount()); + } + else { + request.setAverageRating(0D); + request.setReviewCount(0L); + } return request; } @@ -297,6 +306,9 @@ public void deleteTour(Long id) { Tour tour = tourRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Tour not found with id: " + id)); + if(bookingRepository.existsByTourId(id)){ + throw new IllegalStateException("Cannot delete tour because it has bookings"); + } List images = tourImageRepository.findByTourId(id); diff --git a/src/main/java/com/bookingtour/sun/services/UserService.java b/src/main/java/com/bookingtour/sun/services/UserService.java index b11becf..4aa63c0 100644 --- a/src/main/java/com/bookingtour/sun/services/UserService.java +++ b/src/main/java/com/bookingtour/sun/services/UserService.java @@ -1,18 +1,22 @@ package com.bookingtour.sun.services; -import com.bookingtour.sun.dto.request.PageResponse; -import com.bookingtour.sun.dto.request.UserSearchRequest; +import com.bookingtour.sun.dto.request.*; import com.bookingtour.sun.dto.response.UserResponse; import com.bookingtour.sun.entity.User; +import com.bookingtour.sun.exception.DuplicateResourceException; +import com.bookingtour.sun.exception.ResourceNotFoundException; import com.bookingtour.sun.repository.UserRepository; import com.bookingtour.sun.specification.UserSpecification; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.flywaydb.core.internal.util.StringUtils; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.List; @@ -22,6 +26,7 @@ @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; public PageResponse searchUsers(UserSearchRequest request) { Pageable pageable = PageRequest.of( @@ -50,6 +55,83 @@ public PageResponse searchUsers(UserSearchRequest request) { .build(); } + public void register(RegisterRequest request) { + if (userRepository.existsByEmail(request.getEmail())) { + throw new DuplicateResourceException("Email already exists."); + } + User user = User.builder() + .username(request.getUsername()) + .fullName(request.getFullName()) + .email(request.getEmail()) + .phone(request.getPhone()) + .passwordDigest(passwordEncoder.encode(request.getPassword())) + .build(); + userRepository.save(user); + } + + public void createUser(AdminCreateUserRequest request) { + User user = User.builder() + .username(request.getUsername()) + .fullName(request.getFullName()) + .email(request.getEmail()) + .phone(request.getPhone()) + .passwordDigest(passwordEncoder.encode(request.getPassword())) + .role(request.getRole()) + .status(request.getStatus()) + .build(); + userRepository.save(user); + } + + public AdminUpdateUserRequeset getForUpdate(Long id) { + User user = userRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id)); + AdminUpdateUserRequeset request = new AdminUpdateUserRequeset(); + request.setId(id); + request.setUsername(user.getUsername()); + request.setFullName(user.getFullName()); + request.setEmail(user.getEmail()); + request.setStatus(user.getStatus()); + request.setRole(user.getRole()); + request.setPhone(user.getPhone()); + return request; + } + + @Transactional + public void update(Long id, AdminUpdateUserRequeset request) { + User user = userRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id)); + if (userRepository.existsByEmailAndIdNot(request.getEmail(), id)) { + throw new DuplicateResourceException("Email already exists."); + } + user.setUsername(request.getUsername()); + user.setFullName(request.getFullName()); + user.setEmail(request.getEmail()); + user.setPhone(request.getPhone()); + user.setRole(request.getRole()); + user.setStatus(request.getStatus()); + if (StringUtils.hasText(request.getPassword())) { + user.setPasswordDigest( + passwordEncoder.encode(request.getPassword()) + ); + } + userRepository.save(user); + } + + @Transactional + public void deleteUser(Long id, String email) { + User user = userRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id)); + User currentUser = userRepository.findByEmail(email).orElseThrow(() -> new ResourceNotFoundException("User not found with email: " + email)); + if (currentUser.getId().equals(id)) { + throw new IllegalStateException("You cannot delete your own account."); + } + if (!user.getBookings().isEmpty()) { + throw new IllegalStateException("Cannot delete user because they have bookings."); + } + if (!user.getReviews().isEmpty()) { + throw new IllegalStateException("Cannot delete user because they have submitted reviews."); + } + + userRepository.delete(user); + } + private UserResponse mapToResponse(User user) { return UserResponse.builder() .id(user.getId()) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f2ff644..0992425 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -6,6 +6,16 @@ spring: enabled: true locations: classpath:db/migration baseline-on-migrate: true + security: + oauth2: + client: + registration: + google: + client-id: ${GOOGLE_CLIENT_ID} + client-secret: ${GOOGLE_CLIENT_SECRET} + scope: + - email + - profile # Turn on hiddenhttpmethodfilter to support HTTP methods like PUT and DELETE in forms mvc: diff --git a/src/main/resources/db/migration/V202606291151__alter_users.sql b/src/main/resources/db/migration/V202606291151__alter_users.sql new file mode 100644 index 0000000..43fe2b4 --- /dev/null +++ b/src/main/resources/db/migration/V202606291151__alter_users.sql @@ -0,0 +1,4 @@ +ALTER TABLE users + MODIFY COLUMN password_digest VARCHAR(255) NULL; +ALTER TABLE users + ADD COLUMN login_provider VARCHAR(20) NOT NULL DEFAULT 'LOCAL'; \ No newline at end of file diff --git a/src/main/resources/db/migration/V202606291317__create_tour_reviews.sql b/src/main/resources/db/migration/V202606291317__create_tour_reviews.sql new file mode 100644 index 0000000..ca74990 --- /dev/null +++ b/src/main/resources/db/migration/V202606291317__create_tour_reviews.sql @@ -0,0 +1,27 @@ +CREATE TABLE tour_reviews ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + + tour_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + + rating INT NOT NULL, + comment TEXT, + + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ON UPDATE CURRENT_TIMESTAMP, + + + CONSTRAINT fk_review_tour + FOREIGN KEY (tour_id) + REFERENCES tours(id) + ON DELETE CASCADE, + + CONSTRAINT fk_review_user + FOREIGN KEY (user_id) + REFERENCES users(id) + ON DELETE CASCADE, + + CONSTRAINT uk_user_tour_review + UNIQUE(tour_id, user_id) +); \ No newline at end of file diff --git a/src/main/resources/db/migration/V202606291424__create_payments.sql b/src/main/resources/db/migration/V202606291424__create_payments.sql new file mode 100644 index 0000000..9cd3dce --- /dev/null +++ b/src/main/resources/db/migration/V202606291424__create_payments.sql @@ -0,0 +1,16 @@ +CREATE TABLE payments ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + booking_id BIGINT NOT NULL UNIQUE, + amount DECIMAL(12,2) NOT NULL, + status VARCHAR(50) NOT NULL, + payment_method VARCHAR(50), + transaction_id VARCHAR(255), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ON UPDATE CURRENT_TIMESTAMP, + + CONSTRAINT fk_payment_booking + FOREIGN KEY (booking_id) + REFERENCES bookings(id) + ON DELETE CASCADE +); \ No newline at end of file diff --git a/src/main/resources/db/migration/V202607010806__alter_tour_itineraries.sql b/src/main/resources/db/migration/V202607010806__alter_tour_itineraries.sql new file mode 100644 index 0000000..5ebeeb0 --- /dev/null +++ b/src/main/resources/db/migration/V202607010806__alter_tour_itineraries.sql @@ -0,0 +1,3 @@ +CREATE INDEX idx_tour_itineraries_tour_id + ON tour_itineraries(tour_id); +ALTER TABLE tour_itineraries DROP CONSTRAINT uk_tour_itinerary_day; \ No newline at end of file diff --git a/src/main/resources/static/js/admin-category.js b/src/main/resources/static/js/admin-category.js new file mode 100644 index 0000000..802edd1 --- /dev/null +++ b/src/main/resources/static/js/admin-category.js @@ -0,0 +1,34 @@ +$(document).ready(function () { + $(".btn-edit").each(function () { + $(this).on("click", function () { + let id = $(this).data("id"); + let name = $(this).data("name"); + let description = $(this).data("description"); + + $("#editName").val(name); + $("#editDescription").val(description); + + $("#editCategoryForm").attr( + "action", + "/admin/categories/" + id + ); + }); + }); + + let createModal = $("#createCategoryModal"); + + if(createModal.length && window.openCreateModal){ + bootstrap.Modal + .getOrCreateInstance(createModal[0]) + .show(); + + } + + let editModal = $("#editCategoryModal"); + + if(editModal.length && window.openEditModal){ + bootstrap.Modal + .getOrCreateInstance(editModal[0]) + .show(); + } +}); diff --git a/src/main/resources/static/js/client-tour-details.js b/src/main/resources/static/js/client-tour-details.js new file mode 100644 index 0000000..dc30837 --- /dev/null +++ b/src/main/resources/static/js/client-tour-details.js @@ -0,0 +1,23 @@ +$(document).ready(function () { + function updateTotalPrice() { + let price = Number($("#totalPrice").data("price")); + let people = parseInt($("#adults").val()) || 1; + + if (people < 1) { + people = 1; + $("#adults").val(1); + } + + let total = price * people; + + $("#totalPrice").text(total.toLocaleString('en-US', { style: 'currency', currency: 'USD' })); + } + + // Hiển thị khi load trang + updateTotalPrice(); + + // Cập nhật khi thay đổi số người + $("#adults").on("input", function () { + updateTotalPrice(); + }); +}); \ No newline at end of file diff --git a/src/main/resources/static/js/jquery-4.0.0.min.js b/src/main/resources/static/js/jquery-4.0.0.min.js new file mode 100644 index 0000000..3b89a1c --- /dev/null +++ b/src/main/resources/static/js/jquery-4.0.0.min.js @@ -0,0 +1,2 @@ +/*! jQuery v4.0.0 | (c) OpenJS Foundation and other contributors | jquery.com/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=t(e,!0):t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";if(!e.document)throw Error("jQuery requires a window with a document");var n=[],r=Object.getPrototypeOf,i=n.slice,o=n.flat?function(e){return n.flat.call(e)}:function(e){return n.concat.apply([],e)},a=n.push,s=n.indexOf,u={},l=u.toString,c=u.hasOwnProperty,f=c.toString,p=f.call(Object),d={};function h(e){return null==e?e+"":"object"==typeof e?u[l.call(e)]||"object":typeof e}function g(e){return null!=e&&e===e.window}function v(e){var t=!!e&&e.length,n=h(e);return!("function"==typeof e||g(e))&&("array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e)}var y=e.document,m={type:!0,src:!0,nonce:!0,noModule:!0};function x(e,t,n){var r,i=(n=n||y).createElement("script");for(r in i.text=e,m)t&&t[r]&&(i[r]=t[r]);n.head.appendChild(i).parentNode&&i.parentNode.removeChild(i)}var b="4.0.0",w=/HTML$/i,T=function(e,t){return new T.fn.init(e,t)};function C(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}T.fn=T.prototype={jquery:b,constructor:T,length:0,toArray:function(){return i.call(this)},get:function(e){return null==e?i.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=T.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return T.each(this,e)},map:function(e){return this.pushStack(T.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(i.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(T.grep(this,function(e,t){return(t+1)%2}))},odd:function(){return this.pushStack(T.grep(this,function(e,t){return t%2}))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n+~]|"+E+")"+E+"*"),q=RegExp(E+"|>"),O=/[+~]/,L=y.documentElement,H=L.matches||L.msMatchesSelector;function P(){var e=[];function t(n,r){return e.push(n+" ")>T.expr.cacheLength&&delete t[e.shift()],t[n+" "]=r}return t}function R(e){return e&&void 0!==e.getElementsByTagName&&e}var M="\\["+E+"*("+A+")(?:"+E+"*([*^$|!~]?=)"+E+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+A+"))|)"+E+"*\\]",W=":("+A+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+M+")*)|.*)\\)|)",$={ID:RegExp("^#("+A+")"),CLASS:RegExp("^\\.("+A+")"),TAG:RegExp("^("+A+"|[*])"),ATTR:RegExp("^"+M),PSEUDO:RegExp("^"+W),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+E+"*(even|odd|(([+-]|)(\\d*)n|)"+E+"*(?:([+-]|)"+E+"*(\\d+)|))"+E+"*\\)|)","i")},I=new RegExp(W),F=RegExp("\\\\[\\da-fA-F]{1,6}"+E+"?|\\\\([^\\r\\n\\f])","g"),B=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))};function _(e){return e.replace(F,B)}function U(e){T.error("Syntax error, unrecognized expression: "+e)}var X=RegExp("^"+E+"*,"+E+"*"),z=P();function Y(e,t){var n,r,i,o,a,s,u,l=z[e+" "];if(l)return t?0:l.slice(0);a=e,s=[],u=T.expr.preFilter;while(a){for(o in(!n||(r=X.exec(a)))&&(r&&(a=a.slice(r[0].length)||a),s.push(i=[])),n=!1,(r=N.exec(a))&&(n=r.shift(),i.push({value:n,type:r[0].replace(D," ")}),a=a.slice(n.length)),$)(r=T.expr.match[o].exec(a))&&(!u[o]||(r=u[o](r)))&&(n=r.shift(),i.push({value:n,type:o,matches:r}),a=a.slice(n.length));if(!n)break}return t?a.length:a?U(e):z(e,s).slice(0)}function G(e){for(var t=0,n=e.length,r="";t1)},removeAttr:function(e){return this.each(function(){T.removeAttr(this,e)})}}),T.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return void 0===e.getAttribute?T.prop(e,t,n):(1===o&&T.isXMLDoc(e)||(i=T.attrHooks[t.toLowerCase()]),void 0!==n)?null===n||!1===n&&0!==t.toLowerCase().indexOf("aria-")?void T.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=e.getAttribute(t))?void 0:r},attrHooks:{},removeAttr:function(e,t){var n,r=0,i=t&&t.match(Q);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),k&&(T.attrHooks.type={set:function(e,t){if("radio"===t&&C(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}});var J=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g;function K(e,t){return t?"\0"===e?"\uFFFD":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e}T.escapeSelector=function(e){return(e+"").replace(J,K)};var Z=n.sort,ee=n.splice;function et(e,t){if(e===t)return en=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n?n:1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)?e==y||e.ownerDocument==y&&T.contains(y,e)?-1:t==y||t.ownerDocument==y&&T.contains(y,t)?1:0:4&n?-1:1}T.uniqueSort=function(e){var t,n=[],r=0,i=0;if(en=!1,Z.call(e,et),en){while(t=e[i++])t===e[i]&&(r=n.push(i));while(r--)ee.call(e,n[r],1)}return e},T.fn.uniqueSort=function(){return this.pushStack(T.uniqueSort(i.apply(this)))};var en,er,ei,eo,ea,es,eu=0,el=0,ec=P(),ef=P(),ep=P(),ed=RegExp(E+"+","g"),eh=RegExp("^"+A+"$"),eg=T.extend({needsContext:RegExp("^"+E+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+E+"*((?:-\\d)?\\d*)"+E+"*\\)|)(?=[^-]|$)","i")},$),ev=/^(?:input|select|textarea|button)$/i,ey=/^h\d$/i,em=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ex=function(){eE()},eb=eS(function(e){return!0===e.disabled&&C(e,"fieldset")},{dir:"parentNode",next:"legend"});function ew(e,t,n,r){var i,o,s,u,l,c,f,p=t&&t.ownerDocument,d=t?t.nodeType:9;if(n=n||[],"string"!=typeof e||!e||1!==d&&9!==d&&11!==d)return n;if(!r&&(eE(t),t=t||eo,es)){if(11!==d&&(l=em.exec(e))){if(i=l[1]){if(9===d)return(s=t.getElementById(i))&&a.call(n,s),n;else if(p&&(s=p.getElementById(i))&&T.contains(t,s))return a.call(n,s),n}else if(l[2])return a.apply(n,t.getElementsByTagName(e)),n;else if((i=l[3])&&t.getElementsByClassName)return a.apply(n,t.getElementsByClassName(i)),n}if(!ep[e+" "]&&(!S||!S.test(e))){if(f=e,p=t,1===d&&(q.test(e)||N.test(e))){((p=O.test(e)&&R(t.parentNode)||t)!=t||k)&&((u=t.getAttribute("id"))?u=T.escapeSelector(u):t.setAttribute("id",u=T.expando)),o=(c=Y(e)).length;while(o--)c[o]=(u?"#"+u:":scope")+" "+G(c[o]);f=c.join(",")}try{return a.apply(n,p.querySelectorAll(f)),n}catch(t){ep(e,!0)}finally{u===T.expando&&t.removeAttribute("id")}}}return eq(e.replace(D,"$1"),t,n,r)}function eT(e){return e[T.expando]=!0,e}function eC(e){return function(t){if("form"in t){if(t.parentNode&&!1===t.disabled){if("label"in t)if("label"in t.parentNode)return t.parentNode.disabled===e;else return t.disabled===e;return t.isDisabled===e||!e!==t.isDisabled&&eb(t)===e}return t.disabled===e}return"label"in t&&t.disabled===e}}function ej(e){return eT(function(t){return t*=1,eT(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function eE(e){var t,n=e?e.ownerDocument||e:y;n!=eo&&9===n.nodeType&&(ea=(eo=n).documentElement,es=!T.isXMLDoc(eo),k&&y!=eo&&(t=eo.defaultView)&&t.top!==t&&t.addEventListener("unload",ex))}for(er in ew.matches=function(e,t){return ew(e,null,null,t)},ew.matchesSelector=function(e,t){if(eE(e),es&&!ep[t+" "]&&(!S||!S.test(t)))try{return H.call(e,t)}catch(e){ep(t,!0)}return ew(t,eo,null,[e]).length>0},T.expr={cacheLength:50,createPseudo:eT,match:eg,find:{ID:function(e,t){if(void 0!==t.getElementById&&es){var n=t.getElementById(e);return n?[n]:[]}},TAG:function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},CLASS:function(e,t){if(void 0!==t.getElementsByClassName&&es)return t.getElementsByClassName(e)}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=_(e[1]),e[3]=_(e[3]||e[4]||e[5]||""),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||U(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&U(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return $.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&I.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{ID:function(e){var t=_(e);return function(e){return e.getAttribute("id")===t}},TAG:function(e){var t=_(e).toLowerCase();return"*"===e?function(){return!0}:function(e){return C(e,t)}},CLASS:function(e){var t=ec[e+" "];return t||(t=RegExp("(^|"+E+")"+e+"("+E+"|$)"))&&ec(e,function(e){return t.test("string"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=T.attr(r,e);return null==i?"!="===t:!t||((i+="","="===t)?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace(ed," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h=o!==a?"nextSibling":"previousSibling",g=t.parentNode,v=s&&t.nodeName.toLowerCase(),y=!u&&!s,m=!1;if(g){if(o){while(h){f=t;while(f=f[h])if(s?C(f,v):1===f.nodeType)return!1;d=h="only"===e&&!d&&"nextSibling"}return!0}if(d=[a?g.firstChild:g.lastChild],a&&y){m=(p=(l=(c=g[T.expando]||(g[T.expando]={}))[e]||[])[0]===eu&&l[1])&&l[2],f=p&&g.childNodes[p];while(f=++p&&f&&f[h]||(m=p=0)||d.pop())if(1===f.nodeType&&++m&&f===t){c[e]=[eu,p,m];break}}else if(y&&(m=p=(l=(c=t[T.expando]||(t[T.expando]={}))[e]||[])[0]===eu&&l[1]),!1===m){while(f=++p&&f&&f[h]||(m=p=0)||d.pop())if((s?C(f,v):1===f.nodeType)&&++m&&(y&&((c=f[T.expando]||(f[T.expando]={}))[e]=[eu,m]),f===t))break}return(m-=i)===r||m%r==0&&m/r>=0}}},PSEUDO:function(e,t){var n=T.expr.pseudos[e]||T.expr.setFilters[e.toLowerCase()]||U("unsupported pseudo: "+e);return n[T.expando]?n(t):n}},pseudos:{not:eT(function(e){var t=[],n=[],r=eN(e.replace(D,"$1"));return r[T.expando]?eT(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:eT(function(e){return function(t){return ew(e,t).length>0}}),contains:eT(function(e){return e=_(e),function(t){return(t.textContent||T.text(t)).indexOf(e)>-1}}),lang:eT(function(e){return eh.test(e||"")||U("unsupported lang: "+e),e=_(e).toLowerCase(),function(t){var n;do if(n=es?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===ea},focus:function(e){return e===eo.activeElement&&eo.hasFocus()&&!!(e.type||e.href||~e.tabIndex)},enabled:eC(!1),disabled:eC(!0),checked:function(e){return C(e,"input")&&!!e.checked||C(e,"option")&&!!e.selected},selected:function(e){return k&&e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!T.expr.pseudos.empty(e)},header:function(e){return ey.test(e.nodeName)},input:function(e){return ev.test(e.nodeName)},button:function(e){return C(e,"input")&&"button"===e.type||C(e,"button")},text:function(e){return C(e,"input")&&"text"===e.type},first:ej(function(){return[0]}),last:ej(function(e,t){return[t-1]}),eq:ej(function(e,t,n){return[n<0?n+t:n]}),even:ej(function(e,t){for(var n=0;nt?t:n;--r>=0;)e.push(r);return e}),gt:ej(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function eA(e,t,n,r,i){for(var o,a=[],s=0,u=e.length,l=null!=t;s-1},l,!0),d=[function(e,t,r){var i=!u&&(r||t!=ei)||((n=t).nodeType?f(e,t,r):p(e,t,r));return n=null,i}];c-1&&(e[f]=!(u[f]=d))}}else h=eA(h===u?h.splice(y,h.length):h),o?o(null,u,h,c):a.apply(u,h)})}(c>1&&eD(d),c>1&&G(t.slice(0,c-1).concat({value:" "===t[c-2].type?"*":""})).replace(D,"$1"),r,c0,r=l.length>0,i=function(e,t,i,o,s){var c,f,p,d=0,h="0",g=e&&[],v=[],y=ei,m=e||r&&T.expr.find.TAG("*",s),x=eu+=null==y?1:Math.random()||.1;for(s&&(ei=t==eo||t||s);null!=(c=m[h]);h++){if(r&&c){f=0,t||c.ownerDocument==eo||(eE(c),i=!es);while(p=l[f++])if(p(c,t||eo,i)){a.call(o,c);break}s&&(eu=x)}n&&((c=!p&&c)&&d--,e&&g.push(c))}if(d+=h,n&&h!==d){f=0;while(p=u[f++])p(g,v,t,i);if(e){if(d>0)while(h--)g[h]||v[h]||(v[h]=j.call(o));v=eA(v)}a.apply(o,v),s&&!e&&v.length>0&&d+u.length>1&&T.uniqueSort(o)}return s&&(eu=x,ei=y),g},n?eT(i):i))).selector=e}return c}function eq(e,t,n,r){var i,o,s,u,l,c="function"==typeof e&&e,f=!r&&Y(e=c.selector||e);if(n=n||[],1===f.length){if((o=f[0]=f[0].slice(0)).length>2&&"ID"===(s=o[0]).type&&9===t.nodeType&&es&&T.expr.relative[o[1].type]){if(!(t=(T.expr.find.ID(_(s.matches[0]),t)||[])[0]))return n;c&&(t=t.parentNode),e=e.slice(o.shift().value.length)}i=eg.needsContext.test(e)?0:o.length;while(i--){if(s=o[i],T.expr.relative[u=s.type])break;if((l=T.expr.find[u])&&(r=l(_(s.matches[0]),O.test(o[0].type)&&R(t.parentNode)||t))){if(o.splice(i,1),!(e=r.length&&G(o)))return a.apply(n,r),n;break}}}return(c||eN(e,f))(r,t,!es,n,!t||O.test(e)&&R(t.parentNode)||t),n}function eO(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&T(e).is(n))break;r.push(e)}return r}function eL(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}ek.prototype=T.expr.pseudos,T.expr.setFilters=new ek,eE(),T.find=ew,ew.compile=eN,ew.select=eq,ew.setDocument=eE,ew.tokenize=Y;var eH=T.expr.match.needsContext,eP=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function eR(e){return"<"===e[0]&&">"===e[e.length-1]&&e.length>=3}function eM(e,t,n){return"function"==typeof t?T.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?T.grep(e,function(e){return e===t!==n}):"string"!=typeof t?T.grep(e,function(e){return s.call(t,e)>-1!==n}):T.filter(t,e,n)}T.filter=function(e,t,n){var r=t[0];return(n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType)?T.find.matchesSelector(r,e)?[r]:[]:T.find.matches(e,T.grep(t,function(e){return 1===e.nodeType}))},T.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(T(e).filter(function(){for(t=0;t1?T.uniqueSort(n):n},filter:function(e){return this.pushStack(eM(this,e||[],!1))},not:function(e){return this.pushStack(eM(this,e||[],!0))},is:function(e){return!!eM(this,"string"==typeof e&&eH.test(e)?T(e):e||[],!1).length}});var eW,e$=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(T.fn.init=function(e,t){var n,r;if(!e)return this;if(e.nodeType)return this[0]=e,this.length=1,this;if("function"==typeof e)return void 0!==eW.ready?eW.ready(e):e(T);if(eR(n=e+""))n=[null,e,null];else{if("string"!=typeof e)return T.makeArray(e,this);n=e$.exec(e)}if(n&&(n[1]||!t))if(!n[1])return(r=y.getElementById(n[2]))&&(this[0]=r,this.length=1),this;else{if(t=t instanceof T?t[0]:t,T.merge(this,T.parseHTML(n[1],t&&t.nodeType?t.ownerDocument||t:y,!0)),eP.test(n[1])&&T.isPlainObject(t))for(n in t)"function"==typeof this[n]?this[n](t[n]):this.attr(n,t[n]);return this}return!t||t.jquery?(t||eW).find(e):this.constructor(t).find(e)}).prototype=T.fn,eW=T(y);var eI=/^(?:parents|prev(?:Until|All))/,eF={children:!0,contents:!0,next:!0,prev:!0};function eB(e,t){while((e=e[t])&&1!==e.nodeType);return e}function e_(e){return e}function eU(e){throw e}function eX(e,t,n,r){var i;try{e&&"function"==typeof(i=e.promise)?i.call(e).done(t).fail(n):e&&"function"==typeof(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n(e)}}T.fn.extend({has:function(e){var t=T(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&T.find.matchesSelector(n,e))){o.push(n);break}}return this.pushStack(o.length>1?T.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?s.call(T(e),this[0]):s.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(T.uniqueSort(T.merge(this.get(),T(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),T.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return eO(e,"parentNode")},parentsUntil:function(e,t,n){return eO(e,"parentNode",n)},next:function(e){return eB(e,"nextSibling")},prev:function(e){return eB(e,"previousSibling")},nextAll:function(e){return eO(e,"nextSibling")},prevAll:function(e){return eO(e,"previousSibling")},nextUntil:function(e,t,n){return eO(e,"nextSibling",n)},prevUntil:function(e,t,n){return eO(e,"previousSibling",n)},siblings:function(e){return eL((e.parentNode||{}).firstChild,e)},children:function(e){return eL(e.firstChild)},contents:function(e){return null!=e.contentDocument&&r(e.contentDocument)?e.contentDocument:(C(e,"template")&&(e=e.content||e),T.merge([],e.childNodes))}},function(e,t){T.fn[e]=function(n,r){var i=T.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=T.filter(r,i)),this.length>1&&(eF[e]||T.uniqueSort(i),eI.test(e)&&i.reverse()),this.pushStack(i)}}),T.Callbacks=function(e){e="string"==typeof e?(t=e,n={},T.each(t.match(Q)||[],function(e,t){n[t]=!0}),n):T.extend({},e);var t,n,r,i,o,a,s=[],u=[],l=-1,c=function(){for(a=a||e.once,o=r=!0;u.length;l=-1){i=u.shift();while(++l-1)s.splice(n,1),n<=l&&l--}),this},has:function(e){return e?T.inArray(e,s)>-1:s.length>0},empty:function(){return s&&(s=[]),this},disable:function(){return a=u=[],s=i="",this},disabled:function(){return!s},lock:function(){return a=u=[],i||r||(s=i=""),this},locked:function(){return!!a},fireWith:function(e,t){return!a&&(t=[e,(t=t||[]).slice?t.slice():t],u.push(t),r||c()),this},fire:function(){return f.fireWith(this,arguments),this},fired:function(){return!!o}};return f},T.extend({Deferred:function(t){var n=[["notify","progress",T.Callbacks("memory"),T.Callbacks("memory"),2],["resolve","done",T.Callbacks("once memory"),T.Callbacks("once memory"),0,"resolved"],["reject","fail",T.Callbacks("once memory"),T.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},catch:function(e){return i.then(null,e)},pipe:function(){var e=arguments;return T.Deferred(function(t){T.each(n,function(n,r){var i="function"==typeof e[r[4]]&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&"function"==typeof e.promise?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==eU&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(T.Deferred.getErrorHook&&(c.error=T.Deferred.getErrorHook()),e.setTimeout(c))}}return T.Deferred(function(e){n[0][3].add(a(0,e,"function"==typeof i?i:e_,e.notifyWith)),n[1][3].add(a(0,e,"function"==typeof t?t:e_)),n[2][3].add(a(0,e,"function"==typeof r?r:eU))}).promise()},promise:function(e){return null!=e?T.extend(e,i):i}},o={};return T.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),o=i.call(arguments),a=T.Deferred(),s=function(e){return function(n){r[e]=this,o[e]=arguments.length>1?i.call(arguments):n,--t||a.resolveWith(r,o)}};if(t<=1&&(eX(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||"function"==typeof(o[n]&&o[n].then)))return a.then();while(n--)eX(o[n],s(n),a.reject);return a.promise()}});var ez=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;T.Deferred.exceptionHook=function(t,n){t&&ez.test(t.name)&&e.console.warn("jQuery.Deferred exception",t,n)},T.readyException=function(t){e.setTimeout(function(){throw t})};var eY=T.Deferred();function eG(){y.removeEventListener("DOMContentLoaded",eG),e.removeEventListener("load",eG),T.ready()}T.fn.ready=function(e){return eY.then(e).catch(function(e){T.readyException(e)}),this},T.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--T.readyWait:T.isReady)||(T.isReady=!0,!0!==e&&--T.readyWait>0||eY.resolveWith(y,[T]))}}),T.ready.then=eY.then,"loading"!==y.readyState?e.setTimeout(T.ready):(y.addEventListener("DOMContentLoaded",eG),e.addEventListener("load",eG));var eV=/-([a-z])/g;function eQ(e,t){return t.toUpperCase()}function eJ(e){return e.replace(eV,eQ)}function eK(e){return 1===e.nodeType||9===e.nodeType||!+e.nodeType}function eZ(){this.expando=T.expando+eZ.uid++}eZ.uid=1,eZ.prototype={cache:function(e){var t=e[this.expando];return!t&&(t=Object.create(null),eK(e)&&(e.nodeType?e[this.expando]=t:Object.defineProperty(e,this.expando,{value:t,configurable:!0}))),t},set:function(e,t,n){var r,i=this.cache(e);if("string"==typeof t)i[eJ(t)]=n;else for(r in t)i[eJ(r)]=t[r];return n},get:function(e,t){return void 0===t?this.cache(e):e[this.expando]&&e[this.expando][eJ(t)]},access:function(e,t,n){return void 0===t||t&&"string"==typeof t&&void 0===n?this.get(e,t):(this.set(e,t,n),void 0!==n?n:t)},remove:function(e,t){var n,r=e[this.expando];if(void 0!==r){if(void 0!==t){n=(t=Array.isArray(t)?t.map(eJ):(t=eJ(t))in r?[t]:t.match(Q)||[]).length;while(n--)delete r[t[n]]}(void 0===t||T.isEmptyObject(r))&&(e.nodeType?e[this.expando]=void 0:delete e[this.expando])}},hasData:function(e){var t=e[this.expando];return void 0!==t&&!T.isEmptyObject(t)}};var e0=new eZ,e1=new eZ,e2=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,e3=/[A-Z]/g;function e4(e,t,n){var r,i;if(void 0===n&&1===e.nodeType)if(r="data-"+t.replace(e3,"-$&").toLowerCase(),"string"==typeof(n=e.getAttribute(r))){try{i=n,n="true"===i||"false"!==i&&("null"===i?null:i===+i+""?+i:e2.test(i)?JSON.parse(i):i)}catch(e){}e1.set(e,t,n)}else n=void 0;return n}T.extend({hasData:function(e){return e1.hasData(e)||e0.hasData(e)},data:function(e,t,n){return e1.access(e,t,n)},removeData:function(e,t){e1.remove(e,t)},_data:function(e,t,n){return e0.access(e,t,n)},_removeData:function(e,t){e0.remove(e,t)}}),T.fn.extend({data:function(e,t){var n,r,i,o=this[0],a=o&&o.attributes;if(void 0===e){if(this.length&&(i=e1.get(o),1===o.nodeType&&!e0.get(o,"hasDataAttrs"))){n=a.length;while(n--)a[n]&&0===(r=a[n].name).indexOf("data-")&&e4(o,r=eJ(r.slice(5)),i[r]);e0.set(o,"hasDataAttrs",!0)}return i}return"object"==typeof e?this.each(function(){e1.set(this,e)}):V(this,function(t){var n;if(o&&void 0===t)return void 0!==(n=e1.get(o,e))||void 0!==(n=e4(o,e))?n:void 0;this.each(function(){e1.set(this,e,t)})},null,t,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){e1.remove(this,e)})}}),T.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=e0.get(e,t),n&&(!r||Array.isArray(n)?r=e0.set(e,t,T.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=T.queue(e,t),r=n.length,i=n.shift(),o=T._queueHooks(e,t);"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,function(){T.dequeue(e,t)},o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return e0.get(e,n)||e0.set(e,n,{empty:T.Callbacks("once memory").add(function(){e0.remove(e,[t+"queue",n])})})}}),T.fn.extend({queue:function(e,t){var n=2;return("string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]*)/i,tc={thead:["table"],col:["colgroup","table"],tr:["tbody","table"],td:["tr","tbody","table"]};function tf(e,t){var r;return(r=void 0!==e.getElementsByTagName?n.slice.call(e.getElementsByTagName(t||"*")):void 0!==e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&C(e,t))?T.merge([e],r):r}tc.tbody=tc.tfoot=tc.colgroup=tc.caption=tc.thead,tc.th=tc.td;var tp=/^$|^module$|\/(?:java|ecma)script/i;function td(e,t){for(var n=0,r=e.length;n-1)s=s.appendChild(t.createElement(u[c]));s.innerHTML=T.htmlPrefilter(a),T.merge(p,s.childNodes),(s=f.firstChild).textContent=""}else p.push(t.createTextNode(a));f.textContent="",d=0;while(a=p[d++]){if(i&&T.inArray(a,i)>-1){o&&o.push(a);continue}if(l=ts(a),s=tf(f.appendChild(a),"script"),l&&td(s),r){c=0;while(a=s[c++])tp.test(a.type||"")&&r.push(a)}}return f}function tv(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function ty(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function tm(e,t,n,r){t=o(t);var i,a,s,u,l,c,f=0,p=e.length,d=p-1,h=t[0];if("function"==typeof h)return e.each(function(i){var o=e.eq(i);t[0]=h.call(this,i,o.html()),tm(o,t,n,r)});if(p&&(a=(i=tg(t,e[0].ownerDocument,!1,e,r)).firstChild,1===i.childNodes.length&&(i=a),a||r)){for(u=(s=T.map(tf(i,"script"),tv)).length;f=1)){for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(n=0,o=[],a={};n-1:T.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}}return l=this,u0&&td(a,!u&&tf(e,"script")),s},cleanData:function(e){for(var t,n,r,i=T.event.special,o=0;void 0!==(n=e[o]);o++)if(eK(n)){if(t=n[e0.expando]){if(t.events)for(r in t.events)i[r]?T.event.remove(n,r):T.removeEvent(n,r,t.handle);n[e0.expando]=void 0}n[e1.expando]&&(n[e1.expando]=void 0)}}}),T.fn.extend({detach:function(e){return tD(this,e,!0)},remove:function(e){return tD(this,e)},text:function(e){return V(this,function(e){return void 0===e?T.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=e)})},null,e,arguments.length)},append:function(){return tm(this,arguments,function(e){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&tk(this,e).appendChild(e)})},prepend:function(){return tm(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=tk(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return tm(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return tm(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(T.cleanData(tf(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return T.clone(this,e,t)})},html:function(e){return V(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!tE.test(e)&&!tc[(tl.exec(e)||["",""])[1].toLowerCase()]){e=T.htmlPrefilter(e);try{for(;nT.inArray(this,e)&&(T.cleanData(tf(this)),n&&n.replaceChild(t,this))},e)}}),T.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){T.fn[e]=function(e){for(var n,r=[],i=T(e),o=i.length-1,s=0;s<=o;s++)n=s===o?this:this.clone(!0),T(i[s])[t](n),a.apply(r,n);return this.pushStack(r)}});var tA=RegExp("^("+e5+")(?!px)[a-z%]+$","i"),tN=/^--/;function tq(t){var n=t.ownerDocument.defaultView;return n||(n=e),n.getComputedStyle(t)}function tO(e,t,n){var r,i=tN.test(t);return(n=n||tq(e))&&(r=n.getPropertyValue(t)||n[t],i&&r&&(r=r.replace(D,"$1")||void 0),""!==r||ts(e)||(r=T.style(e,t))),void 0!==r?r+"":r}var tL=["Webkit","Moz","ms"],tH=y.createElement("div").style;function tP(e){return e in tH?e:function(e){var t=e[0].toUpperCase()+e.slice(1),n=tL.length;while(n--)if((e=tL[n]+t)in tH)return e}(e)||e}var tR,tM,tW=y.createElement("table");function t$(){if(tW&&tW.style){var t,n=y.createElement("col"),r=y.createElement("tr"),i=y.createElement("td");if(tW.style.cssText="position:absolute;left:-11111px;border-collapse:separate;border-spacing:0",r.style.cssText="box-sizing:content-box;border:1px solid;height:1px",i.style.cssText="height:9px;width:9px;padding:0",n.span=2,L.appendChild(tW).appendChild(n).parentNode.appendChild(r).appendChild(i).parentNode.appendChild(i.cloneNode(!0)),0===tW.offsetWidth)return void L.removeChild(tW);t=e.getComputedStyle(r),tM=k||18===Math.round(parseFloat(e.getComputedStyle(n).width)),tR=Math.round(parseFloat(t.height)+parseFloat(t.borderTopWidth)+parseFloat(t.borderBottomWidth))===r.offsetHeight,L.removeChild(tW),tW=null}}T.extend(d,{reliableTrDimensions:function(){return t$(),tR},reliableColDimensions:function(){return t$(),tM}});var tI={position:"absolute",visibility:"hidden",display:"block"},tF={letterSpacing:"0",fontWeight:"400"};function tB(e,t,n){var r=e9.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||"px"):t}function t_(e,t,n,r,i,o){var a=+("width"===t),s=0,u=0,l=0;if(n===(r?"border":"content"))return 0;for(;a<4;a+=2)"margin"===n&&(l+=T.css(e,n+e6[a],!0,i)),r?("content"===n&&(u-=T.css(e,"padding"+e6[a],!0,i)),"margin"!==n&&(u-=T.css(e,"border"+e6[a]+"Width",!0,i))):(u+=T.css(e,"padding"+e6[a],!0,i),"padding"!==n?u+=T.css(e,"border"+e6[a]+"Width",!0,i):s+=T.css(e,"border"+e6[a]+"Width",!0,i));return!r&&o>=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))||0),u+l}function tU(e,t,n){var r=tq(e),i=(k||n)&&"border-box"===T.css(e,"boxSizing",!1,r),o=i,a=tO(e,t,r),s="offset"+t[0].toUpperCase()+t.slice(1);if(tA.test(a)){if(!n)return a;a="auto"}return("auto"===a||k&&i||!d.reliableColDimensions()&&C(e,"col")||!d.reliableTrDimensions()&&C(e,"tr"))&&e.getClientRects().length&&(i="border-box"===T.css(e,"boxSizing",!1,r),(o=s in e)&&(a=e[s])),(a=parseFloat(a)||0)+t_(e,t,n||(i?"border":"content"),o,r,a)+"px"}function tX(e,t,n,r,i){return new tX.prototype.init(e,t,n,r,i)}T.extend({cssHooks:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=ti(t),u=tN.test(t),l=e.style;if(u||(t=tP(s)),a=T.cssHooks[t]||T.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];if("string"==(o=typeof n)&&(i=e9.exec(n))&&i[1]&&(n=tn(e,t,i),o="number"),null!=n&&n==n)"number"===o&&(n+=i&&i[3]||(tt(s)?"px":"")),k&&""===n&&0===t.indexOf("background")&&(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n)}},css:function(e,t,n,r){var i,o,a,s=ti(t);return(tN.test(t)||(t=tP(s)),(a=T.cssHooks[t]||T.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=tO(e,t,r)),"normal"===i&&t in tF&&(i=tF[t]),""===n||n)?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),T.each(["height","width"],function(e,t){T.cssHooks[t]={get:function(e,n,r){if(n)return"none"===T.css(e,"display")?function(e,t,n){var r,i,o={};for(i in t)o[i]=e.style[i],e.style[i]=t[i];for(i in r=n.call(e),t)e.style[i]=o[i];return r}(e,tI,function(){return tU(e,t,r)}):tU(e,t,r)},set:function(e,n,r){var i,o=tq(e),a=r&&"border-box"===T.css(e,"boxSizing",!1,o),s=r?t_(e,t,r,a,o):0;return s&&(i=e9.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=T.css(e,t)),tB(e,n,s)}}}),T.each({margin:"",padding:"",border:"Width"},function(e,t){T.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+e6[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(T.cssHooks[e+t].set=tB)}),T.fn.extend({css:function(e,t){return V(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=tq(e),i=t.length;a1)}}),T.Tween=tX,tX.prototype={constructor:tX,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||T.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(tt(n)?"px":"")},cur:function(){var e=tX.propHooks[this.prop];return e&&e.get?e.get(this):tX.propHooks._default.get(this)},run:function(e){var t,n=tX.propHooks[this.prop];return this.options.duration?this.pos=t=T.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tX.propHooks._default.set(this),this}},tX.prototype.init.prototype=tX.prototype,tX.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=T.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){T.fx.step[e.prop]?T.fx.step[e.prop](e):1===e.elem.nodeType&&(T.cssHooks[e.prop]||null!=e.elem.style[tP(e.prop)])?T.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},T.easing={linear:function(e){return e},swing:function(e){return .5-Math.cos(e*Math.PI)/2},_default:"swing"},T.fx=tX.prototype.init,T.fx.step={};var tz,tY,tG=/^(?:toggle|show|hide)$/,tV=/queueHooks$/;function tQ(){return e.setTimeout(function(){tz=void 0}),tz=Date.now()}function tJ(e,t){var n,r=0,i={height:e};for(t=+!!t;r<4;r+=2-t)i["margin"+(n=e6[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function tK(e,t,n){for(var r,i=(tZ.tweeners[t]||[]).concat(tZ.tweeners["*"]),o=0,a=i.length;o1)},removeProp:function(e){return this.each(function(){delete this[T.propFix[e]||e]})}}),T.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return(1===o&&T.isXMLDoc(e)||(t=T.propFix[t]||t,i=T.propHooks[t]),void 0!==n)?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=e.getAttribute("tabindex");return t?parseInt(t,10):t0.test(e.nodeName)||t1.test(e.nodeName)&&e.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),k&&(T.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),T.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){T.propFix[this.toLowerCase()]=this}),T.fn.extend({addClass:function(e){var t,n,r,i,o,a;return"function"==typeof e?this.each(function(t){T(this).addClass(e.call(this,t,t3(this)))}):(t=t4(e)).length?this.each(function(){if(r=t3(this),n=1===this.nodeType&&" "+t2(r)+" "){for(o=0;on.indexOf(" "+i+" ")&&(n+=i+" ");r!==(a=t2(n))&&this.setAttribute("class",a)}}):this},removeClass:function(e){var t,n,r,i,o,a;return"function"==typeof e?this.each(function(t){T(this).removeClass(e.call(this,t,t3(this)))}):arguments.length?(t=t4(e)).length?this.each(function(){if(r=t3(this),n=1===this.nodeType&&" "+t2(r)+" "){for(o=0;o-1)n=n.replace(" "+i+" "," ")}r!==(a=t2(n))&&this.setAttribute("class",a)}}):this:this.attr("class","")},toggleClass:function(e,t){var n,r,i,o;return"function"==typeof e?this.each(function(n){T(this).toggleClass(e.call(this,n,t3(this),t),t)}):"boolean"==typeof t?t?this.addClass(e):this.removeClass(e):(n=t4(e)).length?this.each(function(){for(i=0,o=T(this);i-1)return!0;return!1}}),T.fn.extend({val:function(e){var t,n,r,i=this[0];if(!arguments.length)return i?(t=T.valHooks[i.type]||T.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:null==(n=i.value)?"":n:void 0;return r="function"==typeof e,this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,T(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=T.map(i,function(e){return null==e?"":e+""})),(t=T.valHooks[this.type]||T.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))})}}),T.extend({valHooks:{select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),k&&(T.valHooks.option={get:function(e){var t=e.getAttribute("value");return null!=t?t:t2(T.text(e))}}),T.each(["radio","checkbox"],function(){T.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=T.inArray(T(e).val(),t)>-1}}});var t5=/^(?:focusinfocus|focusoutblur)$/,t9=function(e){e.stopPropagation()};T.extend(T.event,{trigger:function(t,n,r,i){var o,a,s,u,l,f,p,d,h=[r||y],v=c.call(t,"type")?t.type:t,m=c.call(t,"namespace")?t.namespace.split("."):[];if((a=d=s=r=r||y,!(3===r.nodeType||8===r.nodeType||t5.test(v+T.event.triggered)))&&(v.indexOf(".")>-1&&(v=(m=v.split(".")).shift(),m.sort()),l=0>v.indexOf(":")&&"on"+v,(t=t[T.expando]?t:new T.Event(v,"object"==typeof t&&t)).isTrigger=i?2:3,t.namespace=m.join("."),t.rnamespace=t.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=r),n=null==n?[t]:T.makeArray(n,[t]),p=T.event.special[v]||{},i||!p.trigger||!1!==p.trigger.apply(r,n))){if(!i&&!p.noBubble&&!g(r)){for(u=p.delegateType||v,!t5.test(u+v)&&(a=a.parentNode);a;a=a.parentNode)h.push(a),s=a;s===(r.ownerDocument||y)&&h.push(s.defaultView||s.parentWindow||e)}o=0;while((a=h[o++])&&!t.isPropagationStopped())d=a,t.type=o>1?u:p.bindType||v,(f=(e0.get(a,"events")||Object.create(null))[t.type]&&e0.get(a,"handle"))&&f.apply(a,n),(f=l&&a[l])&&f.apply&&eK(a)&&(t.result=f.apply(a,n),!1===t.result&&t.preventDefault());return t.type=v,!i&&!t.isDefaultPrevented()&&(!p._default||!1===p._default.apply(h.pop(),n))&&eK(r)&&l&&"function"==typeof r[v]&&!g(r)&&((s=r[l])&&(r[l]=null),T.event.triggered=v,t.isPropagationStopped()&&d.addEventListener(v,t9),r[v](),t.isPropagationStopped()&&d.removeEventListener(v,t9),T.event.triggered=void 0,s&&(r[l]=s)),t.result}},simulate:function(e,t,n){var r=T.extend(new T.Event,n,{type:e,isSimulated:!0});T.event.trigger(r,null,t)}}),T.fn.extend({trigger:function(e,t){return this.each(function(){T.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return T.event.trigger(e,t,n,!0)}});var t6=e.location,t8={guid:Date.now()},t7=/\?/;T.parseXML=function(t){var n,r;if(!t||"string"!=typeof t)return null;try{n=new e.DOMParser().parseFromString(t,"text/xml")}catch(e){}return r=n&&n.getElementsByTagName("parsererror")[0],(!n||r)&&T.error("Invalid XML: "+(r?T.map(r.childNodes,function(e){return e.textContent}).join("\n"):t)),n};var ne=/\[\]$/,nt=/\r?\n/g,nn=/^(?:submit|button|image|reset|file)$/i,nr=/^(?:input|select|textarea|keygen)/i;T.param=function(e,t){var n,r=[],i=function(e,t){var n="function"==typeof t?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(null==e)return"";if(Array.isArray(e)||e.jquery&&!T.isPlainObject(e))T.each(e,function(){i(this.name,this.value)});else for(n in e)!function e(t,n,r,i){var o;if(Array.isArray(n))T.each(n,function(n,o){r||ne.test(t)?i(t,o):e(t+"["+("object"==typeof o&&null!=o?n:"")+"]",o,r,i)});else if(r||"object"!==h(n))i(t,n);else for(o in n)e(t+"["+o+"]",n[o],r,i)}(n,e[n],t,i);return r.join("&")},T.fn.extend({serialize:function(){return T.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=T.prop(this,"elements");return e?T.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!T(this).is(":disabled")&&nr.test(this.nodeName)&&!nn.test(e)&&(this.checked||!tx.test(e))}).map(function(e,t){var n=T(this).val();return null==n?null:Array.isArray(n)?T.map(n,function(e){return{name:t.name,value:e.replace(nt,"\r\n")}}):{name:t.name,value:n.replace(nt,"\r\n")}}).get()}});var ni=/%20/g,no=/#.*$/,na=/([?&])_=[^&]*/,ns=/^(.*?):[ \t]*([^\r\n]*)$/mg,nu=/^(?:GET|HEAD)$/,nl=/^\/\//,nc={},nf={},np="*/".concat("*"),nd=y.createElement("a");function nh(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(Q)||[];if("function"==typeof n)while(r=o[i++])"+"===r[0]?(e[r=r.slice(1)||"*"]=e[r]||[]).unshift(n):(e[r]=e[r]||[]).push(n)}}function ng(e,t,n,r){var i={},o=e===nf;function a(s){var u;return i[s]=!0,T.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function nv(e,t){var n,r,i=T.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&T.extend(!0,e,r),e}nd.href=t6.href,T.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:t6.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(t6.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":np,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":T.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?nv(nv(e,T.ajaxSettings),t):nv(T.ajaxSettings,e)},ajaxPrefilter:nh(nc),ajaxTransport:nh(nf),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var r,i,o,a,s,u,l,c,f,p,d=T.ajaxSetup({},n),h=d.context||d,g=d.context&&(h.nodeType||h.jquery)?T(h):T.event,v=T.Deferred(),m=T.Callbacks("once memory"),x=d.statusCode||{},b={},w={},C="canceled",j={readyState:0,getResponseHeader:function(e){var t;if(l){if(!a){a={};while(t=ns.exec(o))a[t[1].toLowerCase()+" "]=(a[t[1].toLowerCase()+" "]||[]).concat(t[2])}t=a[e.toLowerCase()+" "]}return null==t?null:t.join(", ")},getAllResponseHeaders:function(){return l?o:null},setRequestHeader:function(e,t){return null==l&&(b[e=w[e.toLowerCase()]=w[e.toLowerCase()]||e]=t),this},overrideMimeType:function(e){return null==l&&(d.mimeType=e),this},statusCode:function(e){var t;if(e)if(l)j.always(e[j.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return r&&r.abort(t),E(0,t),this}};if(v.promise(j),d.url=((t||d.url||t6.href)+"").replace(nl,t6.protocol+"//"),d.type=n.method||n.type||d.method||d.type,d.dataTypes=(d.dataType||"*").toLowerCase().match(Q)||[""],null==d.crossDomain){u=y.createElement("a");try{u.href=d.url,u.href=u.href,d.crossDomain=nd.protocol+"//"+nd.host!=u.protocol+"//"+u.host}catch(e){d.crossDomain=!0}}if(ng(nc,d,n,j),d.data&&d.processData&&"string"!=typeof d.data&&(d.data=T.param(d.data,d.traditional)),l)return j;for(f in(c=T.event&&d.global)&&0==T.active++&&T.event.trigger("ajaxStart"),d.type=d.type.toUpperCase(),d.hasContent=!nu.test(d.type),i=d.url.replace(no,""),d.hasContent?d.data&&d.processData&&0===(d.contentType||"").indexOf("application/x-www-form-urlencoded")&&(d.data=d.data.replace(ni,"+")):(p=d.url.slice(i.length),d.data&&(d.processData||"string"==typeof d.data)&&(i+=(t7.test(i)?"&":"?")+d.data,delete d.data),!1===d.cache&&(i=i.replace(na,"$1"),p=(t7.test(i)?"&":"?")+"_="+t8.guid+++p),d.url=i+p),d.ifModified&&(T.lastModified[i]&&j.setRequestHeader("If-Modified-Since",T.lastModified[i]),T.etag[i]&&j.setRequestHeader("If-None-Match",T.etag[i])),(d.data&&d.hasContent&&!1!==d.contentType||n.contentType)&&j.setRequestHeader("Content-Type",d.contentType),j.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+("*"!==d.dataTypes[0]?", "+np+"; q=0.01":""):d.accepts["*"]),d.headers)j.setRequestHeader(f,d.headers[f]);if(d.beforeSend&&(!1===d.beforeSend.call(h,j,d)||l))return j.abort();if(C="abort",m.add(d.complete),j.done(d.success),j.fail(d.error),r=ng(nf,d,n,j)){if(j.readyState=1,c&&g.trigger("ajaxSend",[j,d]),l)return j;d.async&&d.timeout>0&&(s=e.setTimeout(function(){j.abort("timeout")},d.timeout));try{l=!1,r.send(b,E)}catch(e){if(l)throw e;E(-1,e)}}else E(-1,"No Transport");function E(t,n,a,u){var f,p,y,b,w,C=n;!l&&(l=!0,s&&e.clearTimeout(s),r=void 0,o=u||"",j.readyState=4*(t>0),f=t>=200&&t<300||304===t,a&&(b=function(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r){for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}(d,j,a)),!f&&T.inArray("script",d.dataTypes)>-1&&0>T.inArray("json",d.dataTypes)&&(d.converters["text script"]=function(){}),b=function(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift()){if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o])){for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}}if(!0!==a)if(a&&e.throws)t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}}return{state:"success",data:t}}(d,b,j,f),f?(d.ifModified&&((w=j.getResponseHeader("Last-Modified"))&&(T.lastModified[i]=w),(w=j.getResponseHeader("etag"))&&(T.etag[i]=w)),204===t||"HEAD"===d.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,f=!(y=b.error))):(y=C,(t||!C)&&(C="error",t<0&&(t=0))),j.status=t,j.statusText=(n||C)+"",f?v.resolveWith(h,[p,C,j]):v.rejectWith(h,[j,C,y]),j.statusCode(x),x=void 0,c&&g.trigger(f?"ajaxSuccess":"ajaxError",[j,d,f?p:y]),m.fireWith(h,[j,C]),c&&(g.trigger("ajaxComplete",[j,d]),--T.active||T.event.trigger("ajaxStop")))}return j},getJSON:function(e,t,n){return T.get(e,t,n,"json")},getScript:function(e,t){return T.get(e,void 0,t,"script")}}),T.each(["get","post"],function(e,t){T[t]=function(e,n,r,i){return("function"==typeof n||null===n)&&(i=i||r,r=n,n=void 0),T.ajax(T.extend({url:e,type:t,dataType:i,data:n,success:r},T.isPlainObject(e)&&e))}}),T.ajaxPrefilter(function(e){var t;for(t in e.headers)"content-type"===t.toLowerCase()&&(e.contentType=e.headers[t]||"")}),T._evalUrl=function(e,t,n){return T.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,scriptAttrs:t.crossOrigin?{crossOrigin:t.crossOrigin}:void 0,converters:{"text script":function(){}},dataFilter:function(e){T.globalEval(e,t,n)}})},T.fn.extend({wrapAll:function(e){var t;return this[0]&&("function"==typeof e&&(e=e.call(this[0])),t=T(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return"function"==typeof e?this.each(function(t){T(this).wrapInner(e.call(this,t))}):this.each(function(){var t=T(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t="function"==typeof e;return this.each(function(n){T(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){T(this).replaceWith(this.childNodes)}),this}}),T.expr.pseudos.hidden=function(e){return!T.expr.pseudos.visible(e)},T.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},T.ajaxSettings.xhr=function(){return new e.XMLHttpRequest};var ny={0:200};function nm(e){return e.scriptAttrs||!e.headers&&(e.crossDomain||e.async&&0>T.inArray("json",e.dataTypes))}T.ajaxTransport(function(e){var t;return{send:function(n,r){var i,o=e.xhr();if(o.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(i in e.xhrFields)o[i]=e.xhrFields[i];for(i in e.mimeType&&o.overrideMimeType&&o.overrideMimeType(e.mimeType),e.crossDomain||n["X-Requested-With"]||(n["X-Requested-With"]="XMLHttpRequest"),n)o.setRequestHeader(i,n[i]);t=function(e){return function(){t&&(t=o.onload=o.onerror=o.onabort=o.ontimeout=null,"abort"===e?o.abort():"error"===e?r(o.status,o.statusText):r(ny[o.status]||o.status,o.statusText,"text"===(o.responseType||"text")?{text:o.responseText}:{binary:o.response},o.getAllResponseHeaders()))}},o.onload=t(),o.onabort=o.onerror=o.ontimeout=t("error"),t=t("abort");try{o.send(e.hasContent&&e.data||null)}catch(e){if(t)throw e}},abort:function(){t&&t()}}}),T.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},converters:{"text script":function(e){return T.globalEval(e),e}}}),T.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),nm(e)&&(e.type="GET")}),T.ajaxTransport("script",function(e){if(nm(e)){var t,n;return{send:function(r,i){t=T(" + + - - - + diff --git a/src/main/resources/templates/admin/layouts/admin-layout.html b/src/main/resources/templates/admin/layouts/admin-layout.html index 10aa33c..e33d14e 100644 --- a/src/main/resources/templates/admin/layouts/admin-layout.html +++ b/src/main/resources/templates/admin/layouts/admin-layout.html @@ -24,7 +24,6 @@ - diff --git a/src/main/resources/templates/admin/tours/edit.html b/src/main/resources/templates/admin/tours/edit.html index 669b0b8..6950882 100644 --- a/src/main/resources/templates/admin/tours/edit.html +++ b/src/main/resources/templates/admin/tours/edit.html @@ -143,12 +143,12 @@
- +
- +
diff --git a/src/main/resources/templates/admin/users/edit.html b/src/main/resources/templates/admin/users/edit.html new file mode 100644 index 0000000..91b68e6 --- /dev/null +++ b/src/main/resources/templates/admin/users/edit.html @@ -0,0 +1,197 @@ + + + + +
+ + + +

Update User

+ +
+ + + + + +
+ Please fix the following errors: + +
    +
  • +
  • +
+
+ +
+ + +
+ + +
+ + + + +
+
+
+ + +
+ + + + +
+
+
+ + +
+ + + + +
+
+
+ + +
+ + + + +
+
+
+ +
+ + +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+ +
+ +
+ + +
+ +
+ + +
+ + + + +
+
+
+ + +
+ + + + +
+
+
+ +
+ +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/admin/users/index.html b/src/main/resources/templates/admin/users/index.html index 8406308..4c4674c 100644 --- a/src/main/resources/templates/admin/users/index.html +++ b/src/main/resources/templates/admin/users/index.html @@ -88,6 +88,7 @@

Users

+ diff --git a/src/main/resources/templates/admin/users/new.html b/src/main/resources/templates/admin/users/new.html new file mode 100644 index 0000000..d4939e0 --- /dev/null +++ b/src/main/resources/templates/admin/users/new.html @@ -0,0 +1,198 @@ + + + + +
+ + + +

Create User

+ +
+ + + + +
+ Please fix the following errors: + +
    +
  • +
  • +
+
+ +
+ + +
+ + +
+ + + + +
+
+
+ + +
+ + + + +
+
+
+ + +
+ + + + +
+
+
+ + +
+ + + + +
+
+
+ +
+ + +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+ +
+ +
+ + +
+ +
+ + +
+ + + + +
+
+
+ + +
+ + + + +
+
+
+ +
+ +
+
+
+
+
+
+ + diff --git a/src/main/resources/templates/client/booking/index.html b/src/main/resources/templates/client/booking/index.html index cc6a24e..f114e3b 100644 --- a/src/main/resources/templates/client/booking/index.html +++ b/src/main/resources/templates/client/booking/index.html @@ -12,6 +12,19 @@

Lịch Sử Đặt Tour

+
+ + +
+
+ +
+
@@ -81,12 +94,23 @@

-
- - +
+ + +
+
+ + +
-
diff --git a/src/main/resources/templates/client/layouts/main.html b/src/main/resources/templates/client/layouts/main.html index eccd1b3..355c688 100644 --- a/src/main/resources/templates/client/layouts/main.html +++ b/src/main/resources/templates/client/layouts/main.html @@ -13,6 +13,7 @@ + diff --git a/src/main/resources/templates/client/login.html b/src/main/resources/templates/client/login.html index a574d9f..2416b94 100644 --- a/src/main/resources/templates/client/login.html +++ b/src/main/resources/templates/client/login.html @@ -8,6 +8,10 @@
+
+ + Registration successful! Please sign in. +