Merge branch 'main' into AttachmentsToChat

This commit is contained in:
Alex
2026-04-07 16:24:17 -06:00
48 changed files with 1650 additions and 542 deletions

View File

@@ -16,13 +16,11 @@ import retrofit2.http.Query;
public interface InventoryApi {
// GET /api/v1/inventory?q=...&page=...&size=...&category=...&storeId=...&sort=...
@GET("api/v1/inventory")
Call<PageResponse<InventoryDTO>> getAllInventory(
@Query("page") int page,
@Query("size") int size,
@Query("q") String query,
@Query("category") String category,
@Query("storeId") Long storeId,
@Query("sort") String sort);

View File

@@ -3,14 +3,10 @@ package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.MessageDTO;
import com.example.petstoremobile.dtos.SendMessageRequest;
import java.util.List;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.Multipart;
import retrofit2.http.POST;
import retrofit2.http.Part;
import retrofit2.http.Path;
//api calls to get and send messages
@@ -21,12 +17,4 @@ public interface MessageApi {
@POST("api/v1/chat/conversations/{id}/messages")
Call<MessageDTO> sendMessage(@Path("id") Long conversationId, @Body SendMessageRequest request);
@Multipart
@POST("api/v1/chat/conversations/{id}/messages/attachment")
Call<MessageDTO> sendMessageWithAttachment(
@Path("id") Long conversationId,
@Part("content") RequestBody content,
@Part MultipartBody.Part file
);
}

View File

@@ -13,7 +13,7 @@ public interface PurchaseOrderApi {
Call<PageResponse<PurchaseOrderDTO>> getAllPurchaseOrders(
@Query("page") int page,
@Query("size") int size,
@Query("query") String query,
@Query("q") String query,
@Query("storeId") Long storeId,
@Query("sort") String sort);

View File

@@ -5,10 +5,8 @@ import com.example.petstoremobile.dtos.SaleDTO;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Path;
import retrofit2.http.Query;
@@ -24,10 +22,4 @@ public interface SaleApi {
@POST("api/v1/sales")
Call<SaleDTO> createSale(@Body SaleDTO sale);
@PUT("api/v1/sales/{id}")
Call<SaleDTO> updateSale(@Path("id") Long id, @Body SaleDTO sale);
@DELETE("api/v1/sales/{id}")
Call<Void> deleteSale(@Path("id") Long id);
}

View File

@@ -1,6 +1,7 @@
package com.example.petstoremobile.api.auth;
import com.example.petstoremobile.dtos.AuthDTO;
import com.example.petstoremobile.dtos.AvatarUploadResponse;
import com.example.petstoremobile.dtos.UserDTO;
import java.util.Map;
@@ -36,7 +37,7 @@ public interface AuthApi {
//upload avatar endpoint
@Multipart
@POST("api/v1/auth/me/avatar")
Call<UserDTO> uploadAvatar(@Part MultipartBody.Part avatar);
Call<AvatarUploadResponse> uploadAvatar(@Part MultipartBody.Part avatar);
//delete avatar endpoint
@DELETE("api/v1/auth/me/avatar")

View File

@@ -0,0 +1,25 @@
package com.example.petstoremobile.dtos;
public class AvatarUploadResponse {
private String avatarUrl;
private String message;
public AvatarUploadResponse() {
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

View File

@@ -9,6 +9,7 @@ import android.provider.OpenableColumns;
import android.util.Log;
import android.view.*;
import android.view.inputmethod.EditorInfo;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
@@ -28,21 +29,16 @@ import com.example.petstoremobile.dtos.SendMessageRequest;
import com.example.petstoremobile.models.Chat;
import com.example.petstoremobile.models.Message;
import com.example.petstoremobile.services.ChatNotificationService;
import com.example.petstoremobile.utils.FileUtils;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.viewmodels.ChatViewModel;
import com.example.petstoremobile.websocket.StompChatManager;
import java.io.File;
import java.util.*;
import javax.inject.Inject;
import javax.inject.Named;
import dagger.hilt.android.AndroidEntryPoint;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
@AndroidEntryPoint
public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickListener, StompChatManager.MessageListener,
@@ -361,26 +357,10 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
binding.etMessage.setText("");
removeAttachment();
try {
File file = FileUtils.getFileFromUri(requireContext(), uri);
if (file == null) return;
String mimeType = requireContext().getContentResolver().getType(uri);
RequestBody requestFile = RequestBody.create(file, MediaType.parse(mimeType != null ? mimeType : "application/octet-stream"));
MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), requestFile);
RequestBody contentPart = RequestBody.create(text, MediaType.parse("text/plain"));
viewModel.sendMessageWithAttachment(activeConversationId, contentPart, filePart).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
messageList.add(dtoToModel(resource.data));
messageAdapter.notifyItemInserted(messageList.size() - 1);
scrollToBottom();
loadConversations();
}
});
} catch (Exception e) {
Log.e(TAG, "Error sending message with attachment", e);
if (!text.isEmpty()) {
binding.etMessage.setText(text);
}
Toast.makeText(requireContext(), "File attachments are not supported", Toast.LENGTH_SHORT).show();
}
/**

View File

@@ -230,9 +230,7 @@ public class ProfileFragment extends Fragment {
viewModel.uploadAvatar(body).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS) {
currentUser = resource.data;
Toast.makeText(getContext(), "Avatar updated successfully", Toast.LENGTH_SHORT).show();
// Reload image after successful upload
loadProfileData();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Upload failed: " + resource.message, Toast.LENGTH_SHORT).show();

View File

@@ -224,7 +224,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
}
//Load all inventory items from the backend using viewModel
viewModel.getAllInventory(query, null, storeId, currentPage, PAGE_SIZE, "product.prodName").observe(getViewLifecycleOwner(), resource -> {
viewModel.getAllInventory(query, storeId, currentPage, PAGE_SIZE, "product.prodName").observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
// Check the status to see if the resource is loaded and display the data

View File

@@ -7,6 +7,7 @@ import androidx.lifecycle.MutableLiveData;
import com.example.petstoremobile.api.auth.AuthApi;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.dtos.AuthDTO;
import com.example.petstoremobile.dtos.AvatarUploadResponse;
import com.example.petstoremobile.dtos.UserDTO;
import com.example.petstoremobile.utils.ErrorUtils;
import com.example.petstoremobile.utils.Resource;
@@ -83,7 +84,7 @@ public class AuthRepository extends BaseRepository {
/**
* Uploads a multipart image to be used as the current user's avatar.
*/
public LiveData<Resource<UserDTO>> uploadAvatar(MultipartBody.Part avatar) {
public LiveData<Resource<AvatarUploadResponse>> uploadAvatar(MultipartBody.Part avatar) {
return executeCall(authApi.uploadAvatar(avatar));
}

View File

@@ -17,9 +17,6 @@ import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
/**
* Repository for handling chat-related data operations.
*/
@@ -58,13 +55,6 @@ public class ChatRepository extends BaseRepository {
return executeCall(messageApi.sendMessage(conversationId, request));
}
/**
* Sends a message with a file attachment to a conversation.
*/
public LiveData<Resource<MessageDTO>> sendMessageWithAttachment(Long conversationId, RequestBody content, MultipartBody.Part file) {
return executeCall(messageApi.sendMessageWithAttachment(conversationId, content, file));
}
/**
* Fetches a paginated list of customers.
*/

View File

@@ -24,8 +24,8 @@ public class InventoryRepository extends BaseRepository {
/**
* Retrieves a paginated list of inventory items from the API with optional search, category, storeId and sort.
*/
public LiveData<Resource<PageResponse<InventoryDTO>>> getAllInventory(String query, String category, Long storeId, int page, int size, String sort) {
return executeCall(inventoryApi.getAllInventory(page, size, query, category, storeId, sort));
public LiveData<Resource<PageResponse<InventoryDTO>>> getAllInventory(String query, Long storeId, int page, int size, String sort) {
return executeCall(inventoryApi.getAllInventory(page, size, query, storeId, sort));
}
/**

View File

@@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.AuthDTO;
import com.example.petstoremobile.dtos.AvatarUploadResponse;
import com.example.petstoremobile.dtos.UserDTO;
import com.example.petstoremobile.repositories.AuthRepository;
import com.example.petstoremobile.utils.Resource;
@@ -48,7 +49,7 @@ public class AuthViewModel extends ViewModel {
/**
* Uploads a new avatar image for the current user.
*/
public LiveData<Resource<UserDTO>> uploadAvatar(MultipartBody.Part avatar) {
public LiveData<Resource<AvatarUploadResponse>> uploadAvatar(MultipartBody.Part avatar) {
return repository.uploadAvatar(avatar);
}

View File

@@ -16,9 +16,6 @@ import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
/**
* ViewModel for managing chat-related UI state and data operations.
*/
@@ -52,13 +49,6 @@ public class ChatViewModel extends ViewModel {
return repository.sendMessage(conversationId, request);
}
/**
* Sends a message with a file attachment to a conversation.
*/
public LiveData<Resource<MessageDTO>> sendMessageWithAttachment(Long conversationId, RequestBody content, MultipartBody.Part file) {
return repository.sendMessageWithAttachment(conversationId, content, file);
}
/**
* Fetches a paginated list of customers.
*/

View File

@@ -35,8 +35,8 @@ public class InventoryViewModel extends ViewModel {
/**
* Retrieves a paginated list of inventory items, with optional filtering and sorting.
*/
public LiveData<Resource<PageResponse<InventoryDTO>>> getAllInventory(String query, String category, Long storeId, int page, int size, String sort) {
return inventoryRepository.getAllInventory(query, category, storeId, page, size, sort);
public LiveData<Resource<PageResponse<InventoryDTO>>> getAllInventory(String query, Long storeId, int page, int size, String sort) {
return inventoryRepository.getAllInventory(query, storeId, page, size, sort);
}
/**

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,7 @@ import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.security.JwtUtil;
import com.petshop.backend.service.AvatarStorageService;
import com.petshop.backend.util.AuthenticationHelper;
import com.petshop.backend.util.PhoneUtils;
import jakarta.validation.Valid;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
@@ -53,19 +54,23 @@ public class AuthController {
@PostMapping("/register")
public ResponseEntity<?> register(@Valid @RequestBody RegisterRequest request) {
if (userRepository.findByUsername(request.getUsername()).isPresent()) {
String username = trimToNull(request.getUsername());
String email = trimToNull(request.getEmail());
NameParts nameParts = splitFullName(request.getFullName());
String phone = normalizePhone(request.getPhone());
if (userRepository.findByUsername(username).isPresent()) {
Map<String, String> error = new HashMap<>();
error.put("message", "Username already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
if (userRepository.findByEmail(request.getEmail()).isPresent()) {
if (userRepository.findByEmail(email).isPresent()) {
Map<String, String> error = new HashMap<>();
error.put("message", "Email already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
String phone = trimToNull(request.getPhone());
if (phone != null && userRepository.findByPhone(phone).isPresent()) {
Map<String, String> error = new HashMap<>();
error.put("message", "Phone already exists");
@@ -73,10 +78,12 @@ public class AuthController {
}
User user = new User();
user.setUsername(request.getUsername());
user.setUsername(username);
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setEmail(request.getEmail());
user.setFullName(request.getFullName());
user.setEmail(email);
user.setFirstName(nameParts.firstName());
user.setLastName(nameParts.lastName());
user.setFullName(nameParts.fullName());
user.setPhone(phone);
user.setRole(User.Role.CUSTOMER);
user.setActive(true);
@@ -143,31 +150,36 @@ public class AuthController {
User user = getAuthenticatedUser();
boolean invalidateToken = false;
if (request.getUsername() != null && !request.getUsername().equals(user.getUsername())) {
if (userRepository.findByUsername(request.getUsername()).isPresent()) {
String username = trimToNull(request.getUsername());
if (username != null && !username.equals(user.getUsername())) {
if (userRepository.findByUsername(username).isPresent()) {
Map<String, String> error = new HashMap<>();
error.put("message", "Username already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
user.setUsername(request.getUsername());
user.setUsername(username);
invalidateToken = true;
}
if (request.getEmail() != null && !request.getEmail().equals(user.getEmail())) {
if (userRepository.findByEmail(request.getEmail()).isPresent()) {
String email = trimToNull(request.getEmail());
if (email != null && !email.equals(user.getEmail())) {
if (userRepository.findByEmail(email).isPresent()) {
Map<String, String> error = new HashMap<>();
error.put("message", "Email already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
user.setEmail(request.getEmail());
user.setEmail(email);
}
if (request.getFullName() != null) {
user.setFullName(request.getFullName());
NameParts nameParts = splitFullName(request.getFullName());
user.setFirstName(nameParts.firstName());
user.setLastName(nameParts.lastName());
user.setFullName(nameParts.fullName());
}
if (request.getPhone() != null) {
String phone = trimToNull(request.getPhone());
String phone = normalizePhone(request.getPhone());
if (!java.util.Objects.equals(phone, user.getPhone())) {
if (phone != null && userRepository.findByPhone(phone)
.filter(existing -> !existing.getId().equals(user.getId()))
@@ -196,11 +208,15 @@ public class AuthController {
private UserInfoResponse toUserInfoResponse(User user) {
StoreLocation primaryStore = user.getPrimaryStore();
Long customerId = user.getRole() == User.Role.CUSTOMER ? user.getId() : null;
String fullName = user.getFullName();
if (fullName == null || fullName.isBlank()) {
fullName = joinFullName(user.getFirstName(), user.getLastName());
}
return new UserInfoResponse(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getFullName(),
fullName,
user.getPhone(),
avatarStorageService.toOwnerAvatarUrl(user),
user.getRole().name(),
@@ -218,6 +234,36 @@ public class AuthController {
return trimmed.isEmpty() ? null : trimmed;
}
private String normalizePhone(String value) {
return trimToNull(PhoneUtils.normalize(trimToNull(value)));
}
private NameParts splitFullName(String value) {
String normalized = trimToNull(value);
if (normalized == null) {
throw new IllegalArgumentException("Full name is required");
}
String[] parts = normalized.split("\\s+", 2);
String firstName = parts[0];
String lastName = parts.length > 1 ? parts[1] : "";
return new NameParts(firstName, lastName, joinFullName(firstName, lastName));
}
private String joinFullName(String firstName, String lastName) {
String first = trimToNull(firstName);
String last = trimToNull(lastName);
if (first == null) {
return last == null ? null : last;
}
if (last == null) {
return first;
}
return first + " " + last;
}
private record NameParts(String firstName, String lastName, String fullName) {
}
@PostMapping("/me/avatar")
public ResponseEntity<?> uploadAvatar(@RequestParam("avatar") MultipartFile file) {
User user = getAuthenticatedUser();

View File

@@ -153,6 +153,16 @@ public class DropdownController {
);
}
@GetMapping("/customers/{customerId}/pets")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<List<DropdownOption>> getCustomerPets(@PathVariable Long customerId) {
return ResponseEntity.ok(
petRepository.findAllByOwner_IdOrderByPetNameAsc(customerId).stream()
.map(p -> new DropdownOption(p.getPetId(), p.getPetName()))
.collect(Collectors.toList())
);
}
@GetMapping("/suppliers")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<List<DropdownOption>> getSuppliers() {

View File

@@ -0,0 +1,76 @@
package com.petshop.backend.controller;
import com.petshop.backend.dto.pet.MyPetRequest;
import com.petshop.backend.dto.pet.MyPetResponse;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.service.PetService;
import com.petshop.backend.util.AuthenticationHelper;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/my-pets")
@PreAuthorize("isAuthenticated()")
public class MyPetController {
private final PetService petService;
private final UserRepository userRepository;
public MyPetController(PetService petService, UserRepository userRepository) {
this.petService = petService;
this.userRepository = userRepository;
}
@GetMapping
public ResponseEntity<List<MyPetResponse>> getMyPets() {
return ResponseEntity.ok(petService.getMyPets(currentUserId()));
}
@PostMapping
public ResponseEntity<MyPetResponse> createMyPet(@Valid @RequestBody MyPetRequest request) {
return ResponseEntity.ok(petService.createMyPet(currentUserId(), request));
}
@PutMapping("/{id}")
public ResponseEntity<MyPetResponse> updateMyPet(@PathVariable Long id, @Valid @RequestBody MyPetRequest request) {
return ResponseEntity.ok(petService.updateMyPet(currentUserId(), id, request));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteMyPet(@PathVariable Long id) {
petService.deleteMyPet(currentUserId(), id);
return ResponseEntity.noContent().build();
}
@PostMapping("/{id}/image")
public ResponseEntity<?> uploadMyPetImage(@PathVariable Long id, @RequestParam("image") MultipartFile image) {
try {
return ResponseEntity.ok(petService.uploadMyPetImage(currentUserId(), id, image));
} catch (IllegalArgumentException ex) {
return ResponseEntity.badRequest().body(Map.of("message", ex.getMessage()));
} catch (IOException ex) {
return ResponseEntity.badRequest().body(Map.of("message", "Failed to upload pet image: " + ex.getMessage()));
}
}
private Long currentUserId() {
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
return user.getId();
}
}

View File

@@ -48,12 +48,8 @@ public class PetImageController {
@GetMapping("/{id}/image")
public ResponseEntity<Resource> getPetImage(@PathVariable Long id) {
try {
PetService.ImagePayload payload = petService.loadPetImage(id, currentUserId(), currentUserRole());
return ResponseEntity.ok().contentType(payload.mediaType()).body(payload.resource());
} catch (PetService.ForbiddenImageAccessException ex) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
}
@DeleteMapping("/{id}/image")

View File

@@ -15,9 +15,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/refunds")
@@ -33,8 +31,7 @@ public class RefundController {
@PostMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<?> createRefund(@Valid @RequestBody RefundRequest request) {
try {
public ResponseEntity<RefundResponse> createRefund(@Valid @RequestBody RefundRequest request) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
@@ -47,13 +44,7 @@ public class RefundController {
customerId = user.getId();
}
RefundResponse refund = refundService.createRefund(request, customerId);
return ResponseEntity.status(HttpStatus.CREATED).body(refund);
} catch (RuntimeException e) {
Map<String, String> error = new HashMap<>();
error.put("message", e.getMessage());
return ResponseEntity.badRequest().body(error);
}
return ResponseEntity.status(HttpStatus.CREATED).body(refundService.createRefund(request, customerId));
}
@GetMapping
@@ -77,8 +68,7 @@ public class RefundController {
@GetMapping("/{id}")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<?> getRefundById(@PathVariable Long id) {
try {
public ResponseEntity<RefundResponse> getRefundById(@PathVariable Long id) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
@@ -91,40 +81,19 @@ public class RefundController {
customerId = user.getId();
}
RefundResponse refund = refundService.getRefundById(id, customerId);
return ResponseEntity.ok(refund);
} catch (RuntimeException e) {
Map<String, String> error = new HashMap<>();
error.put("message", e.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
return ResponseEntity.ok(refundService.getRefundById(id, customerId));
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<?> updateRefund(@PathVariable Long id, @Valid @RequestBody RefundUpdateRequest request) {
try {
RefundResponse refund = refundService.updateRefundStatus(id, request.getStatus());
return ResponseEntity.ok(refund);
} catch (RuntimeException e) {
Map<String, String> error = new HashMap<>();
error.put("message", e.getMessage());
return ResponseEntity.badRequest().body(error);
}
public ResponseEntity<RefundResponse> updateRefund(@PathVariable Long id, @Valid @RequestBody RefundUpdateRequest request) {
return ResponseEntity.ok(refundService.updateRefundStatus(id, request.getStatus()));
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> deleteRefund(@PathVariable Long id) {
try {
public ResponseEntity<Void> deleteRefund(@PathVariable Long id) {
refundService.deleteRefund(id);
Map<String, String> response = new HashMap<>();
response.put("message", "Refund deleted successfully");
return ResponseEntity.ok(response);
} catch (RuntimeException e) {
Map<String, String> error = new HashMap<>();
error.put("message", e.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
return ResponseEntity.noContent().build();
}
}

View File

@@ -0,0 +1,42 @@
package com.petshop.backend.dto.pet;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class MyPetRequest {
@NotBlank(message = "Pet name is required")
@Size(max = 50, message = "Pet name must not exceed 50 characters")
private String petName;
@NotBlank(message = "Species is required")
@Size(max = 50, message = "Species must not exceed 50 characters")
private String species;
@Size(max = 50, message = "Breed must not exceed 50 characters")
private String breed;
public String getPetName() {
return petName;
}
public void setPetName(String petName) {
this.petName = petName;
}
public String getSpecies() {
return species;
}
public void setSpecies(String species) {
this.species = species;
}
public String getBreed() {
return breed;
}
public void setBreed(String breed) {
this.breed = breed;
}
}

View File

@@ -0,0 +1,61 @@
package com.petshop.backend.dto.pet;
public class MyPetResponse {
private Long customerPetId;
private String petName;
private String species;
private String breed;
private String imageUrl;
public MyPetResponse() {
}
public MyPetResponse(Long customerPetId, String petName, String species, String breed, String imageUrl) {
this.customerPetId = customerPetId;
this.petName = petName;
this.species = species;
this.breed = breed;
this.imageUrl = imageUrl;
}
public Long getCustomerPetId() {
return customerPetId;
}
public void setCustomerPetId(Long customerPetId) {
this.customerPetId = customerPetId;
}
public String getPetName() {
return petName;
}
public void setPetName(String petName) {
this.petName = petName;
}
public String getSpecies() {
return species;
}
public void setSpecies(String species) {
this.species = species;
}
public String getBreed() {
return breed;
}
public void setBreed(String breed) {
this.breed = breed;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
}

View File

@@ -1,6 +1,7 @@
package com.petshop.backend.exception;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
@@ -13,7 +14,10 @@ import java.time.LocalDateTime;
@Component
public class ApiErrorResponder {
private final ObjectMapper objectMapper = JsonMapper.builder().findAndAddModules().build();
private final ObjectMapper objectMapper = JsonMapper.builder()
.findAndAddModules()
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.build();
public void write(HttpServletResponse response, HttpStatus status, String message, String details, String path) throws IOException {
response.setStatus(status.value());

View File

@@ -1,15 +1,19 @@
package com.petshop.backend.exception;
import com.petshop.backend.service.PetService;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.core.PropertyReferenceException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import java.time.LocalDateTime;
import java.util.HashMap;
@@ -78,6 +82,26 @@ public class GlobalExceptionHandler {
return buildErrorResponse(HttpStatus.valueOf(ex.getStatusCode().value()), message, ex, request);
}
@ExceptionHandler(NoResourceFoundException.class)
public ResponseEntity<ApiErrorResponse> handleNoResourceFound(NoResourceFoundException ex, HttpServletRequest request) {
return buildErrorResponse(HttpStatus.NOT_FOUND, "Route not found", ex, request);
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<ApiErrorResponse> handleMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpServletRequest request) {
return buildErrorResponse(HttpStatus.METHOD_NOT_ALLOWED, ex.getMessage(), ex, request);
}
@ExceptionHandler(PropertyReferenceException.class)
public ResponseEntity<ApiErrorResponse> handleBadSortProperty(PropertyReferenceException ex, HttpServletRequest request) {
return buildErrorResponse(HttpStatus.BAD_REQUEST, "Invalid sort field: " + ex.getPropertyName(), ex, request);
}
@ExceptionHandler(PetService.ForbiddenImageAccessException.class)
public ResponseEntity<ApiErrorResponse> handleForbiddenImageAccess(PetService.ForbiddenImageAccessException ex, HttpServletRequest request) {
return buildErrorResponse(HttpStatus.FORBIDDEN, "Access to this pet image is not allowed", ex, request);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiErrorResponse> handleGenericException(Exception ex, HttpServletRequest request) {
String message = ex.getMessage() == null || ex.getMessage().isBlank()

View File

@@ -9,11 +9,14 @@ import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface PetRepository extends JpaRepository<Pet, Long> {
List<Pet> findAllByPetStatusIgnoreCaseOrderByPetNameAsc(String petStatus);
List<Pet> findAllByOwner_IdOrderByPetNameAsc(Long ownerId);
Optional<Pet> findByIdAndOwner_Id(Long id, Long ownerId);
@Query("SELECT p FROM Pet p WHERE " +
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.petBreed, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +

View File

@@ -1,6 +1,8 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.dto.pet.MyPetRequest;
import com.petshop.backend.dto.pet.MyPetResponse;
import com.petshop.backend.dto.pet.PetRequest;
import com.petshop.backend.dto.pet.PetResponse;
import com.petshop.backend.entity.Adoption;
@@ -25,6 +27,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
@Service
@@ -82,6 +85,50 @@ public class PetService {
return mapToResponse(pet);
}
@Transactional(readOnly = true)
public List<MyPetResponse> getMyPets(Long ownerUserId) {
return petRepository.findAllByOwner_IdOrderByPetNameAsc(ownerUserId).stream()
.map(this::mapToMyPetResponse)
.toList();
}
@Transactional
public MyPetResponse createMyPet(Long ownerUserId, MyPetRequest request) {
User owner = userRepository.findById(ownerUserId)
.orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + ownerUserId));
Pet pet = new Pet();
pet.setOwner(owner);
pet.setStore(null);
pet.setPetStatus("Owned");
applyMyPetRequest(pet, request);
return mapToMyPetResponse(petRepository.save(pet));
}
@Transactional
public MyPetResponse updateMyPet(Long ownerUserId, Long petId, MyPetRequest request) {
Pet pet = findOwnedPet(ownerUserId, petId);
pet.setPetStatus("Owned");
pet.setStore(null);
applyMyPetRequest(pet, request);
return mapToMyPetResponse(petRepository.save(pet));
}
@Transactional
public void deleteMyPet(Long ownerUserId, Long petId) {
Pet pet = findOwnedPet(ownerUserId, petId);
deleteStoredImageIfPresent(pet.getImageUrl());
petRepository.delete(pet);
}
@Transactional
public MyPetResponse uploadMyPetImage(Long ownerUserId, Long petId, MultipartFile file) throws IOException {
validateImageFile(file);
Pet pet = findOwnedPet(ownerUserId, petId);
deleteStoredImageIfPresent(pet.getImageUrl());
pet.setImageUrl(catalogImageStorageService.storePetImage(file));
return mapToMyPetResponse(petRepository.save(pet));
}
@Transactional
public PetResponse createPet(PetRequest request) {
Pet pet = new Pet();
@@ -225,6 +272,11 @@ public class PetService {
}
}
private Pet findOwnedPet(Long ownerUserId, Long petId) {
return petRepository.findByIdAndOwner_Id(petId, ownerUserId)
.orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + petId));
}
private void deleteStoredImageIfPresent(String storedImagePath) {
if (storedImagePath == null || storedImagePath.isBlank()) {
return;
@@ -276,6 +328,32 @@ public class PetService {
);
}
private MyPetResponse mapToMyPetResponse(Pet pet) {
return new MyPetResponse(
pet.getPetId(),
pet.getPetName(),
pet.getPetSpecies(),
pet.getPetBreed(),
pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null
);
}
private void applyMyPetRequest(Pet pet, MyPetRequest request) {
pet.setPetName(request.getPetName().trim());
pet.setPetSpecies(request.getSpecies().trim());
pet.setPetBreed(normalizeOptional(request.getBreed()));
pet.setPetAge(null);
pet.setPetPrice(null);
}
private String normalizeOptional(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private void applyOwnerAndStore(Pet pet, PetRequest request) {
if ("owned".equalsIgnoreCase(request.getPetStatus())) {
if (request.getCustomerId() != null) {

View File

@@ -8,6 +8,8 @@ import com.petshop.backend.entity.Product;
import com.petshop.backend.entity.Refund;
import com.petshop.backend.entity.RefundItem;
import com.petshop.backend.entity.Sale;
import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.ProductRepository;
import com.petshop.backend.repository.RefundRepository;
import com.petshop.backend.repository.SaleRepository;
@@ -40,14 +42,14 @@ public class RefundService {
@Transactional
public RefundResponse createRefund(RefundRequest request, Long customerId) {
Sale sale = saleRepository.findById(request.getSaleId())
.orElseThrow(() -> new RuntimeException("Sale not found"));
.orElseThrow(() -> new BusinessException("Sale not found"));
if (sale.getCustomer() == null) {
throw new RuntimeException("Sale has no associated customer");
throw new BusinessException("Sale has no associated customer");
}
if (customerId != null && !sale.getCustomer().getId().equals(customerId)) {
throw new RuntimeException("You can only create refunds for your own purchases");
throw new BusinessException("You can only create refunds for your own purchases");
}
Refund refund = new Refund();
@@ -59,13 +61,13 @@ public class RefundService {
BigDecimal totalAmount = BigDecimal.ZERO;
for (var itemRequest : request.getItems()) {
Product product = productRepository.findById(itemRequest.getProdId())
.orElseThrow(() -> new RuntimeException("Product not found: " + itemRequest.getProdId()));
.orElseThrow(() -> new BusinessException("Product not found: " + itemRequest.getProdId()));
BigDecimal unitPrice = sale.getItems().stream()
.filter(item -> item.getProduct().getProdId().equals(itemRequest.getProdId()))
.findFirst()
.map(item -> item.getUnitPrice())
.orElseThrow(() -> new RuntimeException("Product " + itemRequest.getProdId() + " was not in original sale"));
.orElseThrow(() -> new BusinessException("Product " + itemRequest.getProdId() + " was not in original sale"));
RefundItem refundItem = new RefundItem();
refundItem.setProduct(product);
@@ -84,10 +86,10 @@ public class RefundService {
@Transactional(readOnly = true)
public RefundResponse getRefundById(Long id, Long customerId) {
Refund refund = refundRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Refund not found"));
.orElseThrow(() -> new ResourceNotFoundException("Refund not found"));
if (customerId != null && !refund.getCustomerId().equals(customerId)) {
throw new RuntimeException("You can only view your own refunds");
throw new ResourceNotFoundException("You can only view your own refunds");
}
return toResponse(refund);
@@ -111,18 +113,18 @@ public class RefundService {
@Transactional
public RefundResponse updateRefundStatus(Long id, String status) {
Refund refund = refundRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Refund not found"));
.orElseThrow(() -> new ResourceNotFoundException("Refund not found"));
Refund.RefundStatus newStatus;
try {
newStatus = Refund.RefundStatus.valueOf(status.toUpperCase());
} catch (IllegalArgumentException e) {
throw new RuntimeException("Invalid status: " + status);
throw new BusinessException("Invalid status: " + status);
}
if (refund.getStatus() == Refund.RefundStatus.PENDING && newStatus == Refund.RefundStatus.APPROVED) {
Sale originalSale = saleRepository.findById(refund.getSaleId())
.orElseThrow(() -> new RuntimeException("Original sale not found"));
.orElseThrow(() -> new ResourceNotFoundException("Original sale not found"));
SaleRequest saleRequest = new SaleRequest();
saleRequest.setStoreId(originalSale.getStore().getStoreId());
@@ -150,7 +152,7 @@ public class RefundService {
@Transactional
public void deleteRefund(Long id) {
if (!refundRepository.existsById(id)) {
throw new RuntimeException("Refund not found");
throw new ResourceNotFoundException("Refund not found");
}
refundRepository.deleteById(id);
}

View File

@@ -6,6 +6,7 @@ public class AdoptionRequest {
private Long petId;
private Long customerId;
private Long employeeId;
private Long sourceStoreId;
private LocalDate adoptionDate;
private String adoptionStatus;
@@ -36,6 +37,14 @@ public class AdoptionRequest {
this.employeeId = employeeId;
}
public Long getSourceStoreId() {
return sourceStoreId;
}
public void setSourceStoreId(Long sourceStoreId) {
this.sourceStoreId = sourceStoreId;
}
public LocalDate getAdoptionDate() {
return adoptionDate;
}

View File

@@ -2,11 +2,8 @@ package org.example.petshopdesktop.api.dto.appointment;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;
public class AppointmentRequest {
private List<Long> petIds;
private List<Long> customerPetIds;
private Long customerId;
private Long storeId;
private Long serviceId;
@@ -14,26 +11,11 @@ public class AppointmentRequest {
private LocalDate appointmentDate;
private LocalTime appointmentTime;
private String appointmentStatus;
private Long petId;
public AppointmentRequest() {
}
public List<Long> getPetIds() {
return petIds;
}
public void setPetIds(List<Long> petIds) {
this.petIds = petIds;
}
public List<Long> getCustomerPetIds() {
return customerPetIds;
}
public void setCustomerPetIds(List<Long> customerPetIds) {
this.customerPetIds = customerPetIds;
}
public Long getCustomerId() {
return customerId;
}
@@ -89,4 +71,12 @@ public class AppointmentRequest {
public void setAppointmentStatus(String appointmentStatus) {
this.appointmentStatus = appointmentStatus;
}
public Long getPetId() {
return petId;
}
public void setPetId(Long petId) {
this.petId = petId;
}
}

View File

@@ -10,16 +10,14 @@ public class AppointmentResponse {
private Long storeId;
private String storeName;
private Long serviceId;
private java.util.List<String> petNames;
private java.util.List<Long> petIds;
private java.util.List<String> customerPetNames;
private java.util.List<Long> customerPetIds;
private String serviceName;
private Long employeeId;
private String employeeName;
private LocalDate appointmentDate;
private LocalTime appointmentTime;
private String appointmentStatus;
private String petName;
private Long petId;
public AppointmentResponse() {
}
@@ -72,36 +70,20 @@ public class AppointmentResponse {
this.serviceId = serviceId;
}
public java.util.List<String> getPetNames() {
return petNames;
public String getPetName() {
return petName;
}
public void setPetNames(java.util.List<String> petNames) {
this.petNames = petNames;
public void setPetName(String petName) {
this.petName = petName;
}
public java.util.List<Long> getPetIds() {
return petIds;
public Long getPetId() {
return petId;
}
public void setPetIds(java.util.List<Long> petIds) {
this.petIds = petIds;
}
public java.util.List<String> getCustomerPetNames() {
return customerPetNames;
}
public void setCustomerPetNames(java.util.List<String> customerPetNames) {
this.customerPetNames = customerPetNames;
}
public java.util.List<Long> getCustomerPetIds() {
return customerPetIds;
}
public void setCustomerPetIds(java.util.List<Long> customerPetIds) {
this.customerPetIds = customerPetIds;
public void setPetId(Long petId) {
this.petId = petId;
}
public String getServiceName() {

View File

@@ -2,6 +2,10 @@ package org.example.petshopdesktop.api.dto.chat;
public class MessageRequest {
private String content;
private String attachmentUrl;
private String attachmentName;
private String attachmentMimeType;
private Long attachmentSizeBytes;
public MessageRequest() {
}
@@ -17,4 +21,36 @@ public class MessageRequest {
public void setContent(String content) {
this.content = content;
}
public String getAttachmentUrl() {
return attachmentUrl;
}
public void setAttachmentUrl(String attachmentUrl) {
this.attachmentUrl = attachmentUrl;
}
public String getAttachmentName() {
return attachmentName;
}
public void setAttachmentName(String attachmentName) {
this.attachmentName = attachmentName;
}
public String getAttachmentMimeType() {
return attachmentMimeType;
}
public void setAttachmentMimeType(String attachmentMimeType) {
this.attachmentMimeType = attachmentMimeType;
}
public Long getAttachmentSizeBytes() {
return attachmentSizeBytes;
}
public void setAttachmentSizeBytes(Long attachmentSizeBytes) {
this.attachmentSizeBytes = attachmentSizeBytes;
}
}

View File

@@ -9,6 +9,10 @@ public class MessageResponse {
private String content;
private LocalDateTime timestamp;
private Boolean isRead;
private String attachmentUrl;
private String attachmentName;
private String attachmentMimeType;
private Long attachmentSizeBytes;
public MessageResponse() {
}
@@ -60,4 +64,36 @@ public class MessageResponse {
public void setIsRead(Boolean isRead) {
this.isRead = isRead;
}
public String getAttachmentUrl() {
return attachmentUrl;
}
public void setAttachmentUrl(String attachmentUrl) {
this.attachmentUrl = attachmentUrl;
}
public String getAttachmentName() {
return attachmentName;
}
public void setAttachmentName(String attachmentName) {
this.attachmentName = attachmentName;
}
public String getAttachmentMimeType() {
return attachmentMimeType;
}
public void setAttachmentMimeType(String attachmentMimeType) {
this.attachmentMimeType = attachmentMimeType;
}
public Long getAttachmentSizeBytes() {
return attachmentSizeBytes;
}
public void setAttachmentSizeBytes(Long attachmentSizeBytes) {
this.attachmentSizeBytes = attachmentSizeBytes;
}
}

View File

@@ -5,9 +5,12 @@ public class EmployeeRequest {
private String password;
private String firstName;
private String lastName;
private String fullName;
private String email;
private String phone;
private String role;
private String staffRole;
private Long primaryStoreId;
private Boolean active;
public String getUsername() { return username; }
@@ -18,12 +21,18 @@ public class EmployeeRequest {
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public String getFullName() { return fullName; }
public void setFullName(String fullName) { this.fullName = fullName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
public String getStaffRole() { return staffRole; }
public void setStaffRole(String staffRole) { this.staffRole = staffRole; }
public Long getPrimaryStoreId() { return primaryStoreId; }
public void setPrimaryStoreId(Long primaryStoreId) { this.primaryStoreId = primaryStoreId; }
public Boolean getActive() { return active; }
public void setActive(Boolean active) { this.active = active; }
}

View File

@@ -3,6 +3,7 @@ package org.example.petshopdesktop.api.dto.employee;
import java.time.LocalDateTime;
public class EmployeeResponse {
private Long id;
private Long employeeId;
private Long userId;
private String username;
@@ -12,13 +13,17 @@ public class EmployeeResponse {
private String email;
private String phone;
private String role;
private String staffRole;
private Long primaryStoreId;
private Boolean active;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public Long getEmployeeId() { return employeeId; }
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getEmployeeId() { return employeeId != null ? employeeId : id; }
public void setEmployeeId(Long employeeId) { this.employeeId = employeeId; }
public Long getUserId() { return userId; }
public Long getUserId() { return userId != null ? userId : id; }
public void setUserId(Long userId) { this.userId = userId; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
@@ -34,6 +39,10 @@ public class EmployeeResponse {
public void setPhone(String phone) { this.phone = phone; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
public String getStaffRole() { return staffRole; }
public void setStaffRole(String staffRole) { this.staffRole = staffRole; }
public Long getPrimaryStoreId() { return primaryStoreId; }
public void setPrimaryStoreId(Long primaryStoreId) { this.primaryStoreId = primaryStoreId; }
public Boolean getActive() { return active; }
public void setActive(Boolean active) { this.active = active; }
public LocalDateTime getCreatedAt() { return createdAt; }

View File

@@ -3,6 +3,7 @@ package org.example.petshopdesktop.api.dto.inventory;
public class InventoryRequest {
private Long prodId;
private Integer quantity;
private Long storeId;
public InventoryRequest() {
}
@@ -22,4 +23,12 @@ public class InventoryRequest {
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
public Long getStoreId() {
return storeId;
}
public void setStoreId(Long storeId) {
this.storeId = storeId;
}
}

View File

@@ -7,6 +7,8 @@ public class InventoryResponse {
private Long prodId;
private String productName;
private String categoryName;
private Long storeId;
private String storeName;
private Integer quantity;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@@ -46,6 +48,22 @@ public class InventoryResponse {
this.categoryName = categoryName;
}
public Long getStoreId() {
return storeId;
}
public void setStoreId(Long storeId) {
this.storeId = storeId;
}
public String getStoreName() {
return storeName;
}
public void setStoreName(String storeName) {
this.storeName = storeName;
}
public Integer getQuantity() {
return quantity;
}

View File

@@ -231,18 +231,12 @@ public class AppointmentController {
}
private AppointmentDTO mapToAppointmentDTO(AppointmentResponse response) {
Long petId = response.getCustomerPetIds() != null && !response.getCustomerPetIds().isEmpty()
? response.getCustomerPetIds().get(0)
: response.getPetIds() != null && !response.getPetIds().isEmpty() ? response.getPetIds().get(0) : null;
String petName = response.getCustomerPetNames() != null && !response.getCustomerPetNames().isEmpty()
? String.join(", ", response.getCustomerPetNames())
: String.join(", ", response.getPetNames());
return new AppointmentDTO(
response.getAppointmentId().intValue(),
response.getCustomerId() != null ? response.getCustomerId().intValue() : 0,
response.getCustomerName(),
petId != null ? petId.intValue() : 0,
petName,
response.getPetId() != null ? response.getPetId().intValue() : 0,
response.getPetName(),
response.getServiceId() != null ? response.getServiceId().intValue() : 0,
response.getServiceName(),
response.getEmployeeId() != null ? response.getEmployeeId().intValue() : 0,

View File

@@ -123,6 +123,7 @@ public class ChatController {
@FXML
void btnSendClicked() {
try {
if (selectedConversation == null) {
lblChatStatus.setText("Select a conversation");
return;
@@ -138,6 +139,13 @@ public class ChatController {
if (!sent) {
sendMessageFallback(selectedConversation.getId(), content);
}
} catch (Exception e) {
ActivityLogger.getInstance().logException(
"ChatController.btnSendClicked",
e,
"Sending chat message");
lblChatStatus.setText("Chat send failed");
}
}
private void loadCustomers() {
@@ -223,17 +231,31 @@ public class ChatController {
private void renderMessages(List<MessageResponse> messages) {
vbMessages.getChildren().clear();
for (MessageResponse message : messages) {
try {
vbMessages.getChildren().add(createMessageBubble(message));
} catch (Exception e) {
ActivityLogger.getInstance().logException(
"ChatController.renderMessages",
e,
"Rendering chat message");
}
}
scrollMessagesToBottom();
}
private void appendMessageIfSelected(MessageResponse message) {
try {
upsertConversationForMessage(message);
if (selectedConversation != null && selectedConversation.getId().equals(message.getConversationId())) {
vbMessages.getChildren().add(createMessageBubble(message));
scrollMessagesToBottom();
}
} catch (Exception e) {
ActivityLogger.getInstance().logException(
"ChatController.appendMessageIfSelected",
e,
"Appending chat message");
}
}
private void upsertConversation(ConversationResponse conversation) {
@@ -284,15 +306,34 @@ public class ChatController {
Label author = new Label(resolveAuthorLabel(message));
author.setStyle("-fx-font-weight: bold; -fx-text-fill: " + (mine ? "#ffffff" : "#1f2937") + ";");
Label content = new Label(message.getContent());
content.setWrapText(true);
content.setStyle("-fx-text-fill: " + (mine ? "#ffffff" : "#1f2937") + ";");
String timestampText = message.getTimestamp() == null ? "" : TIME_FORMATTER.format(message.getTimestamp());
Label timestamp = new Label(timestampText);
timestamp.setStyle("-fx-text-fill: " + (mine ? "#dbeafe" : "#94a3b8") + "; -fx-font-size: 11px;");
VBox bubble = new VBox(4, author, content, timestamp);
VBox bubble = new VBox(4, author);
String contentText = message.getContent() == null ? "" : message.getContent();
if (!contentText.isBlank()) {
Label content = new Label(contentText);
content.setWrapText(true);
content.setStyle("-fx-text-fill: " + (mine ? "#ffffff" : "#1f2937") + ";");
bubble.getChildren().add(content);
}
if (message.getAttachmentUrl() != null && !message.getAttachmentUrl().isBlank()) {
String attachmentLabel = message.getAttachmentName();
if (attachmentLabel == null || attachmentLabel.isBlank()) {
attachmentLabel = "Attachment";
}
if (message.getAttachmentSizeBytes() != null && message.getAttachmentSizeBytes() > 0) {
attachmentLabel = attachmentLabel + " (" + formatSize(message.getAttachmentSizeBytes()) + ")";
}
Label attachment = new Label(attachmentLabel);
attachment.setWrapText(true);
attachment.setStyle("-fx-text-fill: " + (mine ? "#dbeafe" : "#0f766e") + "; -fx-underline: true;");
bubble.getChildren().add(attachment);
}
bubble.getChildren().add(timestamp);
bubble.setMaxWidth(420);
bubble.setStyle(mine
? "-fx-background-color: #0f766e; -fx-background-radius: 14; -fx-padding: 12;"
@@ -347,4 +388,17 @@ public class ChatController {
private void scrollMessagesToBottom() {
Platform.runLater(() -> spMessages.setVvalue(1.0));
}
private String formatSize(Long bytes) {
if (bytes == null || bytes <= 0) {
return "";
}
double size = bytes;
String[] units = {"B", "KB", "MB", "GB"};
int unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size = size / 1024;
unitIndex++;
}
return unitIndex == 0 ? String.format("%.0f %s", size, units[unitIndex]) : String.format("%.1f %s", size, units[unitIndex]);
}
}

View File

@@ -235,8 +235,8 @@ public class InventoryController {
response.getProdId() != null ? response.getProdId().intValue() : 0,
response.getProductName(),
response.getCategoryName() != null ? response.getCategoryName() : "",
0,
"N/A",
response.getStoreId() != null ? response.getStoreId().intValue() : 0,
response.getStoreName() != null ? response.getStoreName() : "",
response.getQuantity() != null ? response.getQuantity() : 0,
0
);

View File

@@ -186,10 +186,14 @@ public class StaffAccountsController {
long userId = employee.getUserId() != null ? employee.getUserId() : 0L;
long employeeId = employee.getEmployeeId() != null ? employee.getEmployeeId() : 0L;
String username = employee.getUsername();
String firstName = employee.getFirstName() != null ? employee.getFirstName() : "";
String lastName = employee.getLastName() != null ? employee.getLastName() : "";
if (firstName.isBlank() && lastName.isBlank()) {
String fullName = employee.getFullName() != null ? employee.getFullName() : "";
String[] names = splitFullName(fullName);
String firstName = names[0];
String lastName = names[1];
firstName = names[0];
lastName = names[1];
}
String email = employee.getEmail() != null ? employee.getEmail() : "";
String phone = employee.getPhone() != null ? employee.getPhone() : "";
String role = employee.getRole() != null ? employee.getRole() : "STAFF";

View File

@@ -190,10 +190,16 @@ public class AdoptionDialogController {
if (errorMsg.isEmpty()) {
try {
Long storeId = UserSession.getInstance().getStoreId();
if (storeId == null || storeId <= 0) {
throw new IllegalStateException("Store is not set for this account");
}
AdoptionRequest request = new AdoptionRequest();
request.setPetId(cbPet.getSelectionModel().getSelectedItem().getId());
request.setCustomerId(cbCustomer.getSelectionModel().getSelectedItem().getId());
request.setEmployeeId(cbEmployee.getSelectionModel().getSelectedItem().getId());
request.setSourceStoreId(storeId);
request.setAdoptionDate(dpAdoptionDate.getValue());
request.setAdoptionStatus(cbAdoptionStatus.getValue());

View File

@@ -21,7 +21,6 @@ import org.example.petshopdesktop.util.ActivityLogger;
import java.time.LocalTime;
import java.time.LocalDate;
import java.util.List;
import java.util.Collections;
import java.util.Objects;
public class AppointmentDialogController {
@@ -215,7 +214,7 @@ public class AppointmentDialogController {
}
AppointmentRequest request = new AppointmentRequest();
request.setCustomerPetIds(Collections.singletonList(cbPet.getValue().getId()));
request.setPetId(cbPet.getValue().getId());
request.setCustomerId(cbCustomer.getValue().getId());
request.setStoreId(storeId);
request.setServiceId(cbService.getValue().getId());

View File

@@ -20,6 +20,7 @@ import org.example.petshopdesktop.api.dto.inventory.InventoryResponse;
import org.example.petshopdesktop.api.dto.product.ProductResponse;
import org.example.petshopdesktop.api.endpoints.InventoryApi;
import org.example.petshopdesktop.api.endpoints.ProductApi;
import org.example.petshopdesktop.auth.UserSession;
import org.example.petshopdesktop.models.Product;
import org.example.petshopdesktop.util.ActivityLogger;
@@ -127,6 +128,10 @@ public class InventoryDialogController {
try {
InventoryRequest request = new InventoryRequest();
Product selectedProduct = cbProduct.getSelectionModel().getSelectedItem();
Long storeId = UserSession.getInstance().getStoreId();
if (storeId == null || storeId <= 0) {
throw new IllegalStateException("Store is not set for this account");
}
request.setProdId((long) selectedProduct.getProdId());
int quantity;
try {
@@ -135,6 +140,7 @@ public class InventoryDialogController {
throw new IllegalArgumentException("Invalid quantity format");
}
request.setQuantity(quantity);
request.setStoreId(storeId);
if (mode.equals("Add")) {
InventoryApi.getInstance().createInventory(request);

View File

@@ -11,6 +11,7 @@ import javafx.stage.Stage;
import org.example.petshopdesktop.Validator;
import org.example.petshopdesktop.api.dto.employee.EmployeeRequest;
import org.example.petshopdesktop.api.endpoints.EmployeeApi;
import org.example.petshopdesktop.auth.UserSession;
import org.example.petshopdesktop.models.StaffAccount;
import org.example.petshopdesktop.util.ActivityLogger;
@@ -104,14 +105,18 @@ public class StaffEditDialogController {
new Thread(() -> {
try {
Long storeId = UserSession.getInstance().getStoreId();
EmployeeRequest request = new EmployeeRequest();
request.setUsername(username);
request.setPassword(password.isEmpty() ? null : password);
request.setFirstName(firstName);
request.setLastName(lastName);
request.setFullName(firstName + " " + lastName);
request.setEmail(email);
request.setPhone(phone);
request.setRole(staffAccount.getRole());
request.setStaffRole("Staff");
request.setPrimaryStoreId(storeId);
request.setActive(staffAccount.isActive());
EmployeeApi.getInstance().updateEmployee(staffAccount.getEmployeeId(), request);

View File

@@ -11,6 +11,7 @@ import javafx.scene.control.TextField;
import javafx.stage.Stage;
import org.example.petshopdesktop.api.dto.employee.EmployeeRequest;
import org.example.petshopdesktop.api.endpoints.EmployeeApi;
import org.example.petshopdesktop.auth.UserSession;
import org.example.petshopdesktop.Validator;
import org.example.petshopdesktop.util.ActivityLogger;
@@ -89,14 +90,18 @@ public class StaffRegisterDialogController {
new Thread(() -> {
try {
Long storeId = UserSession.getInstance().getStoreId();
EmployeeRequest request = new EmployeeRequest();
request.setUsername(username);
request.setPassword(password);
request.setFirstName(firstName);
request.setLastName(lastName);
request.setFullName(firstName + " " + lastName);
request.setEmail(email);
request.setPhone(phone);
request.setRole("STAFF");
request.setStaffRole("Staff");
request.setPrimaryStoreId(storeId);
request.setActive(true);
EmployeeApi.getInstance().createEmployee(request);

114
web/package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "threaded-pets",
"version": "0.1.0",
"dependencies": {
"next": "16.1.6",
"next": "^16.2.2",
"react": "19.2.3",
"react-dom": "19.2.3"
},
@@ -1032,9 +1032,9 @@
}
},
"node_modules/@next/env": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz",
"integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==",
"version": "16.2.2",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.2.tgz",
"integrity": "sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -1048,9 +1048,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz",
"integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==",
"version": "16.2.2",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.2.tgz",
"integrity": "sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg==",
"cpu": [
"arm64"
],
@@ -1064,9 +1064,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz",
"integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==",
"version": "16.2.2",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.2.tgz",
"integrity": "sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw==",
"cpu": [
"x64"
],
@@ -1080,9 +1080,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz",
"integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==",
"version": "16.2.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.2.tgz",
"integrity": "sha512-c3m8kBHMziMgo2fICOP/cd/5YlrxDU5YYjAJeQLyFsCqVF8xjOTH/QYG4a2u48CvvZZSj1eHQfBCbyh7kBr30Q==",
"cpu": [
"arm64"
],
@@ -1096,9 +1096,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz",
"integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==",
"version": "16.2.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.2.tgz",
"integrity": "sha512-VKLuscm0P/mIfzt+SDdn2+8TNNJ7f0qfEkA+az7OqQbjzKdBxAHs0UvuiVoCtbwX+dqMEL9U54b5wQ/aN3dHeg==",
"cpu": [
"arm64"
],
@@ -1112,9 +1112,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz",
"integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==",
"version": "16.2.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.2.tgz",
"integrity": "sha512-kU3OPHJq6sBUjOk7wc5zJ7/lipn8yGldMoAv4z67j6ov6Xo/JvzA7L7LCsyzzsXmgLEhk3Qkpwqaq/1+XpNR3g==",
"cpu": [
"x64"
],
@@ -1128,9 +1128,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz",
"integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==",
"version": "16.2.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.2.tgz",
"integrity": "sha512-CKXRILyErMtUftp+coGcZ38ZwE/Aqq45VMCcRLr2I4OXKrgxIBDXHnBgeX/UMil0S09i2JXaDL3Q+TN8D/cKmg==",
"cpu": [
"x64"
],
@@ -1144,9 +1144,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz",
"integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==",
"version": "16.2.2",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.2.tgz",
"integrity": "sha512-sS/jSk5VUoShUqINJFvNjVT7JfR5ORYj/+/ZpOYbbIohv/lQfduWnGAycq2wlknbOql2xOR0DoV0s6Xfcy49+g==",
"cpu": [
"arm64"
],
@@ -1160,9 +1160,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz",
"integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==",
"version": "16.2.2",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.2.tgz",
"integrity": "sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA==",
"cpu": [
"x64"
],
@@ -1741,9 +1741,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2422,9 +2422,9 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3568,9 +3568,9 @@
}
},
"node_modules/flatted": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz",
"integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
"license": "ISC"
},
@@ -4954,14 +4954,14 @@
"license": "MIT"
},
"node_modules/next": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz",
"integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==",
"version": "16.2.2",
"resolved": "https://registry.npmjs.org/next/-/next-16.2.2.tgz",
"integrity": "sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A==",
"license": "MIT",
"dependencies": {
"@next/env": "16.1.6",
"@next/env": "16.2.2",
"@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.8.3",
"baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
@@ -4973,15 +4973,15 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "16.1.6",
"@next/swc-darwin-x64": "16.1.6",
"@next/swc-linux-arm64-gnu": "16.1.6",
"@next/swc-linux-arm64-musl": "16.1.6",
"@next/swc-linux-x64-gnu": "16.1.6",
"@next/swc-linux-x64-musl": "16.1.6",
"@next/swc-win32-arm64-msvc": "16.1.6",
"@next/swc-win32-x64-msvc": "16.1.6",
"sharp": "^0.34.4"
"@next/swc-darwin-arm64": "16.2.2",
"@next/swc-darwin-x64": "16.2.2",
"@next/swc-linux-arm64-gnu": "16.2.2",
"@next/swc-linux-arm64-musl": "16.2.2",
"@next/swc-linux-x64-gnu": "16.2.2",
"@next/swc-linux-x64-musl": "16.2.2",
"@next/swc-win32-arm64-msvc": "16.2.2",
"@next/swc-win32-x64-msvc": "16.2.2",
"sharp": "^0.34.5"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
@@ -5298,9 +5298,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -6099,9 +6099,9 @@
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {

View File

@@ -9,7 +9,7 @@
"lint": "eslint"
},
"dependencies": {
"next": "16.1.6",
"next": "^16.2.2",
"react": "19.2.3",
"react-dom": "19.2.3"
},