Initialized Counter login and fixed bugs in the edit products option

This commit is contained in:
Sidharth Prabhu
2026-04-16 10:42:43 +05:30
parent d4cef8d241
commit 9af9f0599a
39 changed files with 1115 additions and 85 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,3 @@
ordering-site/* ordering-site/*
ordering_site/ ordering_site/
counter/ counter-frontend/

View File

@@ -7,8 +7,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController @RestController
@RequestMapping("/api/base-items") @RequestMapping("/api/base-items")
public class BaseItemController { public class BaseItemController {
@@ -18,8 +16,12 @@ public class BaseItemController {
@GetMapping @GetMapping
public org.springframework.data.domain.Page<BaseItem> getAllBaseItems( public org.springframework.data.domain.Page<BaseItem> getAllBaseItems(
@RequestParam(required = false) String search,
@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) { @RequestParam(defaultValue = "10") int size) {
if (search != null && !search.isEmpty()) {
return baseItemRepository.findByNameContainingIgnoreCase(search, org.springframework.data.domain.PageRequest.of(page, size));
}
return baseItemRepository.findAll(org.springframework.data.domain.PageRequest.of(page, size)); return baseItemRepository.findAll(org.springframework.data.domain.PageRequest.of(page, size));
} }

View File

@@ -40,6 +40,7 @@ public class OrderController {
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate,
@RequestParam(required = false) String status, @RequestParam(required = false) String status,
@RequestParam(required = false) String paymentType, @RequestParam(required = false) String paymentType,
@RequestParam(required = false) String orderType,
@RequestParam(required = false) String search, @RequestParam(required = false) String search,
@RequestParam(defaultValue = "false") boolean archived, @RequestParam(defaultValue = "false") boolean archived,
@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "0") int page,
@@ -66,6 +67,9 @@ public class OrderController {
if (paymentType != null && !paymentType.isEmpty()) { if (paymentType != null && !paymentType.isEmpty()) {
predicates.add(cb.equal(root.get("paymentMethod"), paymentType)); predicates.add(cb.equal(root.get("paymentMethod"), paymentType));
} }
if (orderType != null && !orderType.isEmpty()) {
predicates.add(cb.equal(root.get("orderType"), orderType));
}
if (search != null && !search.isEmpty()) { if (search != null && !search.isEmpty()) {
String searchLower = "%" + search.toLowerCase() + "%"; String searchLower = "%" + search.toLowerCase() + "%";
Join<Order, User> userJoin = root.join("user", JoinType.LEFT); Join<Order, User> userJoin = root.join("user", JoinType.LEFT);

View File

@@ -18,8 +18,12 @@ public class ProductController {
@GetMapping @GetMapping
public org.springframework.data.domain.Page<Product> getAllProducts( public org.springframework.data.domain.Page<Product> getAllProducts(
@RequestParam(required = false) String search,
@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) { @RequestParam(defaultValue = "10") int size) {
if (search != null && !search.isEmpty()) {
return productRepository.findByNameOrCategoryContainingIgnoreCase(search, org.springframework.data.domain.PageRequest.of(page, size));
}
return productRepository.findAllWithStalls(org.springframework.data.domain.PageRequest.of(page, size)); return productRepository.findAllWithStalls(org.springframework.data.domain.PageRequest.of(page, size));
} }

View File

@@ -108,9 +108,10 @@ public class UserController {
*/ */
@GetMapping("/users") @GetMapping("/users")
public ResponseEntity<Page<LoginResponse.UserDto>> getAllUsers( public ResponseEntity<Page<LoginResponse.UserDto>> getAllUsers(
@RequestParam(required = false) String search,
@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) { @RequestParam(defaultValue = "10") int size) {
Page<LoginResponse.UserDto> users = userService.getAllUsers(PageRequest.of(page, size)); Page<LoginResponse.UserDto> users = userService.getAllUsers(search, PageRequest.of(page, size));
return ResponseEntity.ok(users); return ResponseEntity.ok(users);
} }

View File

@@ -0,0 +1,53 @@
package com.rit.canteen.sales.counter.config;
import com.rit.canteen.sales.counter.model.CounterCategory;
import com.rit.canteen.sales.counter.model.CounterProduct;
import com.rit.canteen.sales.counter.repository.CounterCategoryRepository;
import com.rit.canteen.sales.counter.repository.CounterProductRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.List;
@Configuration
public class CounterDataLoader {
@Bean
CommandLineRunner initCounterDatabase(CounterCategoryRepository categoryRepository, CounterProductRepository productRepository) {
return args -> {
if (categoryRepository.count() == 0) {
CounterCategory iceCream = categoryRepository.save(new CounterCategory("ICE CREAM"));
CounterCategory biscuit = categoryRepository.save(new CounterCategory("BISCUIT"));
CounterCategory juice = categoryRepository.save(new CounterCategory("READYMADE JUICE"));
CounterCategory chocolate = categoryRepository.save(new CounterCategory("CHOCOLATE"));
CounterCategory snacks = categoryRepository.save(new CounterCategory("SNACKS"));
CounterCategory bakery = categoryRepository.save(new CounterCategory("BAKERY"));
List<CounterProduct> products = Arrays.asList(
new CounterProduct("CM BUTTER SCOTCH CONE", 40.0, 47, iceCream),
new CounterProduct("CM CHOCOLATE CONE", 50.0, 24, iceCream),
new CounterProduct("CM COOKIES AND CREAM", 50.0, 58, iceCream),
new CounterProduct("CM SIPUP CHOCOLATE", 10.0, 111, iceCream),
new CounterProduct("CM SIPUP STRAWBERRY", 10.0, 118, iceCream),
new CounterProduct("CM SIPUP PINEAPPLE", 10.0, 120, iceCream),
new CounterProduct("CM VANILLA CUP", 10.0, 31, iceCream),
new CounterProduct("CM PINE APPLE STICK ICE", 10.0, 25, iceCream),
new CounterProduct("CM MINI VANILLA CONE", 20.0, 128, iceCream),
new CounterProduct("CM PISTA STICK ICE", 20.0, 25, iceCream),
new CounterProduct("CM BALL ICE CREAM", 20.0, 18, iceCream),
new CounterProduct("CM CHOCOBAR WITH POUCH", 30.0, 100, iceCream),
new CounterProduct("CM DILSE BUTTER/CHOCO", 30.0, 18, iceCream),
new CounterProduct("CM DILSE REDVEL/VANILLA", 40.0, 20, iceCream),
new CounterProduct("CM SIPUP PISTA", 10.0, 118, iceCream),
new CounterProduct("COCONUT ICE CREAM", 45.0, 30, iceCream),
new CounterProduct("Coke", 25.0, 100, juice)
);
productRepository.saveAll(products);
System.out.println("Counter Database seeded with initial categories and products.");
}
};
}
}

View File

@@ -0,0 +1,31 @@
package com.rit.canteen.sales.counter.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.file.Path;
import java.nio.file.Paths;
@Configuration
public class CounterWebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
Path uploadDir = Paths.get("uploads").toAbsolutePath().normalize();
// Ensure the directory exists
try {
if (!java.nio.file.Files.exists(uploadDir)) {
java.nio.file.Files.createDirectories(uploadDir);
}
} catch (java.io.IOException e) {
System.err.println("Could not create uploads directory: " + e.getMessage());
}
String uploadPath = uploadDir.toUri().toString();
registry.addResourceHandler("/uploads/**")
.addResourceLocations(uploadPath);
}
}

View File

@@ -0,0 +1,23 @@
package com.rit.canteen.sales.counter.controller;
import com.rit.canteen.sales.model.BaseItem;
import com.rit.canteen.sales.repository.BaseItemRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/counter/categories")
public class CounterCategoryController {
@Autowired
private BaseItemRepository baseItemRepository;
@GetMapping
public List<BaseItem> getAllCategories() {
return baseItemRepository.findAll();
}
// Management endpoints are disabled for the counter billing dashboard
}

View File

@@ -0,0 +1,53 @@
package com.rit.canteen.sales.counter.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api/counter/upload")
public class CounterImageUploadController {
private static final String UPLOAD_DIR = "uploads/products";
@PostMapping
public ResponseEntity<?> uploadImage(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "No file selected"));
}
try {
// Create uploads directory if it doesn't exist
Path uploadPath = Paths.get(UPLOAD_DIR).toAbsolutePath().normalize();
Files.createDirectories(uploadPath);
// Generate unique filename to prevent collisions
String originalFilename = file.getOriginalFilename();
String extension = "";
if (originalFilename != null && originalFilename.contains(".")) {
extension = originalFilename.substring(originalFilename.lastIndexOf("."));
}
String uniqueFilename = UUID.randomUUID().toString() + extension;
// Save the file
Path filePath = uploadPath.resolve(uniqueFilename);
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
// Return the URL path that the frontend can use
String imageUrl = "/uploads/products/" + uniqueFilename;
return ResponseEntity.ok(Map.of("imageUrl", imageUrl));
} catch (IOException e) {
return ResponseEntity.internalServerError()
.body(Map.of("error", "Failed to upload image: " + e.getMessage()));
}
}
}

View File

@@ -0,0 +1,47 @@
package com.rit.canteen.sales.counter.controller;
import com.rit.canteen.sales.counter.model.CounterInfo;
import com.rit.canteen.sales.counter.service.CounterInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/counter/counters")
public class CounterInfoController {
@Autowired
private CounterInfoService counterService;
@GetMapping
public List<CounterInfo> getAllCounters() {
return counterService.getAllCounters();
}
@GetMapping("/{id}")
public CounterInfo getCounterById(@PathVariable Long id) {
return counterService.getCounterById(id)
.orElseThrow(() -> new RuntimeException("Counter not found"));
}
@PostMapping
public CounterInfo createCounter(@RequestBody CounterInfo counter) {
return counterService.createCounter(counter);
}
@PutMapping("/{id}")
public CounterInfo updateCounter(@PathVariable Long id, @RequestBody CounterInfo counter) {
return counterService.updateCounter(id, counter);
}
@DeleteMapping("/{id}")
public void deleteCounter(@PathVariable Long id) {
counterService.deleteCounter(id);
}
@PostMapping("/{counterId}/assign-product/{productId}")
public void assignProduct(@PathVariable Long counterId, @PathVariable Long productId) {
counterService.assignProductToCounter(counterId, productId);
}
}

View File

@@ -0,0 +1,50 @@
package com.rit.canteen.sales.counter.controller;
import com.rit.canteen.sales.model.Order;
import com.rit.canteen.sales.counter.service.CounterOrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/counter/orders")
public class CounterOrderController {
@Autowired
private CounterOrderService orderService;
@GetMapping
public List<Order> getAllOrders() {
return orderService.getAllOrders();
}
@GetMapping("/{id}")
public ResponseEntity<Order> getOrderById(@PathVariable Long id) {
return orderService.getOrderById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public Order createOrder(@RequestBody Order order) {
return orderService.createOrder(order);
}
@PutMapping("/{id}")
public ResponseEntity<Order> updateOrder(@PathVariable Long id, @RequestBody Order orderDetails) {
try {
Order updatedOrder = orderService.updateOrder(id, orderDetails);
return ResponseEntity.ok(updatedOrder);
} catch (RuntimeException e) {
return ResponseEntity.notFound().build();
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteOrder(@PathVariable Long id) {
orderService.deleteOrder(id);
return ResponseEntity.ok().build();
}
}

View File

@@ -0,0 +1,28 @@
package com.rit.canteen.sales.counter.controller;
import com.rit.canteen.sales.model.Product;
import com.rit.canteen.sales.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/counter/products")
public class CounterProductController {
@Autowired
private ProductRepository productRepository;
@GetMapping
public List<Product> getAllProducts() {
return productRepository.findAll();
}
@GetMapping("/category/{categoryName}")
public List<Product> getProductsByCategory(@PathVariable String categoryName) {
return productRepository.findByCategory(categoryName);
}
// Management endpoints are disabled for the counter billing dashboard
}

View File

@@ -0,0 +1,51 @@
package com.rit.canteen.sales.counter.model;
import jakarta.persistence.*;
import java.util.List;
@Entity
@Table(name = "counter_categories")
public class CounterCategory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String name;
@OneToMany(mappedBy = "category", cascade = CascadeType.ALL)
@com.fasterxml.jackson.annotation.JsonIgnore
private List<CounterProduct> products;
public CounterCategory() {
}
public CounterCategory(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<CounterProduct> getProducts() {
return products;
}
public void setProducts(List<CounterProduct> products) {
this.products = products;
}
}

View File

@@ -0,0 +1,63 @@
package com.rit.canteen.sales.counter.model;
import jakarta.persistence.*;
import java.util.List;
@Entity
@Table(name = "counter_info")
public class CounterInfo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String name;
@Column
private String location;
@OneToMany(mappedBy = "counter")
@com.fasterxml.jackson.annotation.JsonIgnore
private List<CounterProduct> products;
public CounterInfo() {
}
public CounterInfo(String name, String location) {
this.name = name;
this.location = location;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public List<CounterProduct> getProducts() {
return products;
}
public void setProducts(List<CounterProduct> products) {
this.products = products;
}
}

View File

@@ -0,0 +1,71 @@
package com.rit.canteen.sales.counter.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "counter_orders")
public class CounterOrder {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "order_id")
private List<CounterOrderItem> items;
@Column(nullable = false)
private Double totalAmount;
@Column(nullable = false)
private String status;
@Column(name = "created_at")
private LocalDateTime createdAt;
public CounterOrder() {
this.createdAt = LocalDateTime.now();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public List<CounterOrderItem> getItems() {
return items;
}
public void setItems(List<CounterOrderItem> items) {
this.items = items;
}
public Double getTotalAmount() {
return totalAmount;
}
public void setTotalAmount(Double totalAmount) {
this.totalAmount = totalAmount;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
}

View File

@@ -0,0 +1,78 @@
package com.rit.canteen.sales.counter.model;
import jakarta.persistence.*;
@Entity
@Table(name = "counter_order_items")
public class CounterOrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "product_name", nullable = false)
private String productName;
@Column(nullable = false)
private Integer quantity;
@Column(name = "unit_price", nullable = false)
private Double unitPrice;
@Column(name = "total_price", nullable = false)
private Double totalPrice;
@Column(name = "product_id")
private Long productId;
public CounterOrderItem() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getProductId() {
return productId;
}
public void setProductId(Long productId) {
this.productId = productId;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
public Double getUnitPrice() {
return unitPrice;
}
public void setUnitPrice(Double unitPrice) {
this.unitPrice = unitPrice;
}
public Double getTotalPrice() {
return totalPrice;
}
public void setTotalPrice(Double totalPrice) {
this.totalPrice = totalPrice;
}
}

View File

@@ -0,0 +1,109 @@
package com.rit.canteen.sales.counter.model;
import jakarta.persistence.*;
@Entity
@Table(name = "counter_products")
public class CounterProduct {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Double price;
@Column(nullable = false)
private Integer stock;
@Column(columnDefinition = "TEXT")
private String description;
@Column
private String imageUrl;
@ManyToOne
@JoinColumn(name = "category_id")
private CounterCategory category;
@ManyToOne
@JoinColumn(name = "counter_id")
private CounterInfo counter;
public CounterProduct() {
}
public CounterProduct(String name, Double price, Integer stock, CounterCategory category) {
this.name = name;
this.price = price;
this.stock = stock;
this.category = category;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public Integer getStock() {
return stock;
}
public void setStock(Integer stock) {
this.stock = stock;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public CounterCategory getCategory() {
return category;
}
public void setCategory(CounterCategory category) {
this.category = category;
}
public CounterInfo getCounter() {
return counter;
}
public void setCounter(CounterInfo counter) {
this.counter = counter;
}
}

View File

@@ -0,0 +1,12 @@
package com.rit.canteen.sales.counter.repository;
import com.rit.canteen.sales.counter.model.CounterCategory;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface CounterCategoryRepository extends JpaRepository<CounterCategory, Long> {
Optional<CounterCategory> findByName(String name);
}

View File

@@ -0,0 +1,12 @@
package com.rit.canteen.sales.counter.repository;
import com.rit.canteen.sales.counter.model.CounterInfo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface CounterInfoRepository extends JpaRepository<CounterInfo, Long> {
Optional<CounterInfo> findByName(String name);
}

View File

@@ -0,0 +1,13 @@
package com.rit.canteen.sales.counter.repository;
import com.rit.canteen.sales.counter.model.CounterOrder;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface CounterOrderRepository extends JpaRepository<CounterOrder, Long> {
List<CounterOrder> findByStatus(String status);
List<CounterOrder> findAllByOrderByCreatedAtDesc();
}

View File

@@ -0,0 +1,14 @@
package com.rit.canteen.sales.counter.repository;
import com.rit.canteen.sales.counter.model.CounterCategory;
import com.rit.canteen.sales.counter.model.CounterProduct;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface CounterProductRepository extends JpaRepository<CounterProduct, Long> {
List<CounterProduct> findByCategoryName(String categoryName);
List<CounterProduct> findByCategory(CounterCategory category);
}

View File

@@ -0,0 +1,39 @@
package com.rit.canteen.sales.counter.service;
import com.rit.canteen.sales.counter.model.CounterCategory;
import com.rit.canteen.sales.counter.repository.CounterCategoryRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class CounterCategoryService {
@Autowired
private CounterCategoryRepository categoryRepository;
public List<CounterCategory> getAllCategories() {
return categoryRepository.findAll();
}
public Optional<CounterCategory> getCategoryByName(String name) {
return categoryRepository.findByName(name);
}
public CounterCategory createCategory(CounterCategory category) {
return categoryRepository.save(category);
}
public CounterCategory updateCategory(Long id, CounterCategory categoryDetails) {
CounterCategory category = categoryRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Category not found"));
category.setName(categoryDetails.getName());
return categoryRepository.save(category);
}
public void deleteCategory(Long id) {
categoryRepository.deleteById(id);
}
}

View File

@@ -0,0 +1,55 @@
package com.rit.canteen.sales.counter.service;
import com.rit.canteen.sales.counter.model.CounterInfo;
import com.rit.canteen.sales.counter.model.CounterProduct;
import com.rit.canteen.sales.counter.repository.CounterInfoRepository;
import com.rit.canteen.sales.counter.repository.CounterProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class CounterInfoService {
@Autowired
private CounterInfoRepository counterRepository;
@Autowired
private CounterProductRepository productRepository;
public List<CounterInfo> getAllCounters() {
return counterRepository.findAll();
}
public Optional<CounterInfo> getCounterById(Long id) {
return counterRepository.findById(id);
}
public CounterInfo createCounter(CounterInfo counter) {
return counterRepository.save(counter);
}
public CounterInfo updateCounter(Long id, CounterInfo counterDetails) {
CounterInfo counter = counterRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Counter not found"));
counter.setName(counterDetails.getName());
counter.setLocation(counterDetails.getLocation());
return counterRepository.save(counter);
}
public void deleteCounter(Long id) {
counterRepository.deleteById(id);
}
public void assignProductToCounter(Long counterId, Long productId) {
CounterInfo counter = counterRepository.findById(counterId)
.orElseThrow(() -> new RuntimeException("Counter not found"));
CounterProduct product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));
product.setCounter(counter);
productRepository.save(product);
}
}

View File

@@ -0,0 +1,64 @@
package com.rit.canteen.sales.counter.service;
import com.rit.canteen.sales.model.Order;
import com.rit.canteen.sales.model.OrderItem;
import com.rit.canteen.sales.repository.OrderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Service
public class CounterOrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private CounterProductService productService;
public List<Order> getAllOrders() {
return orderRepository.findAll();
}
public Optional<Order> getOrderById(Long id) {
return orderRepository.findById(id);
}
public Order createOrder(Order order) {
order.setCreatedAt(LocalDateTime.now());
if (order.getStatus() == null) {
order.setStatus("COMPLETED");
}
// Ensure bidirectional relationship for items
if (order.getItems() != null) {
for (OrderItem item : order.getItems()) {
item.setOrder(order);
// Update stock if product ID is present
if (item.getProductId() != null) {
productService.updateStock(item.getProductId(), item.getQuantity());
}
}
}
return orderRepository.save(order);
}
public Order updateOrder(Long id, Order orderDetails) {
Order order = orderRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Order not found"));
order.setItems(orderDetails.getItems());
order.setTotalAmount(orderDetails.getTotalAmount());
order.setStatus(orderDetails.getStatus());
return orderRepository.save(order);
}
public void deleteOrder(Long id) {
orderRepository.deleteById(id);
}
}

View File

@@ -0,0 +1,58 @@
package com.rit.canteen.sales.counter.service;
import com.rit.canteen.sales.counter.model.CounterProduct;
import com.rit.canteen.sales.counter.repository.CounterProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class CounterProductService {
@Autowired
private CounterProductRepository productRepository;
public List<CounterProduct> getAllProducts() {
return productRepository.findAll();
}
public List<CounterProduct> getProductsByCategory(String categoryName) {
return productRepository.findByCategoryName(categoryName);
}
public Optional<CounterProduct> getProductById(Long id) {
return productRepository.findById(id);
}
public CounterProduct createProduct(CounterProduct product) {
return productRepository.save(product);
}
public CounterProduct updateProduct(Long id, CounterProduct productDetails) {
CounterProduct product = productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Product not found"));
product.setName(productDetails.getName());
product.setPrice(productDetails.getPrice());
product.setStock(productDetails.getStock());
product.setDescription(productDetails.getDescription());
product.setImageUrl(productDetails.getImageUrl());
product.setCategory(productDetails.getCategory());
product.setCounter(productDetails.getCounter());
return productRepository.save(product);
}
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
public CounterProduct updateStock(Long productId, Integer quantity) {
CounterProduct product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));
product.setStock(product.getStock() - quantity);
return productRepository.save(product);
}
}

View File

@@ -1,10 +1,6 @@
package com.rit.canteen.sales.model; package com.rit.canteen.sales.model;
import jakarta.persistence.Entity; import jakarta.persistence.*;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
@Entity @Entity
@@ -18,9 +14,11 @@ public class BaseItem {
@NotBlank(message = "Item name is required") @NotBlank(message = "Item name is required")
private String name; private String name;
@Column(columnDefinition = "TEXT")
private String description; private String description;
private boolean active; @Column(nullable = false)
private boolean active = true;
public BaseItem() {} public BaseItem() {}
@@ -30,35 +28,15 @@ public class BaseItem {
this.active = active; this.active = active;
} }
public Long getId() { public Long getId() { return id; }
return id; public void setId(Long id) { this.id = id; }
}
public void setId(Long id) { public String getName() { return name; }
this.id = id; public void setName(String name) { this.name = name; }
}
public String getName() { public String getDescription() { return description; }
return name; public void setDescription(String description) { this.description = description; }
}
public void setName(String name) { public boolean isActive() { return active; }
this.name = name; public void setActive(boolean active) { this.active = active; }
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
} }

View File

@@ -4,16 +4,17 @@ public class DashboardStats {
private long totalSales; private long totalSales;
private int activeOrders; private int activeOrders;
private int dailyCustomers; private int dailyCustomers;
private double revenueGrowth; private double growth;
public DashboardStats(long totalSales, int activeOrders, int dailyCustomers, double revenueGrowth) { public DashboardStats() {}
public DashboardStats(long totalSales, int activeOrders, int dailyCustomers, double growth) {
this.totalSales = totalSales; this.totalSales = totalSales;
this.activeOrders = activeOrders; this.activeOrders = activeOrders;
this.dailyCustomers = dailyCustomers; this.dailyCustomers = dailyCustomers;
this.revenueGrowth = revenueGrowth; this.growth = growth;
} }
// Getters and Setters
public long getTotalSales() { return totalSales; } public long getTotalSales() { return totalSales; }
public void setTotalSales(long totalSales) { this.totalSales = totalSales; } public void setTotalSales(long totalSales) { this.totalSales = totalSales; }
@@ -23,6 +24,6 @@ public class DashboardStats {
public int getDailyCustomers() { return dailyCustomers; } public int getDailyCustomers() { return dailyCustomers; }
public void setDailyCustomers(int dailyCustomers) { this.dailyCustomers = dailyCustomers; } public void setDailyCustomers(int dailyCustomers) { this.dailyCustomers = dailyCustomers; }
public double getRevenueGrowth() { return revenueGrowth; } public double getGrowth() { return growth; }
public void setRevenueGrowth(double revenueGrowth) { this.revenueGrowth = revenueGrowth; } public void setGrowth(double growth) { this.growth = growth; }
} }

View File

@@ -9,6 +9,8 @@ public class GeneralDashboardData {
private List<Map<String, Object>> hourlySales; private List<Map<String, Object>> hourlySales;
private List<String> insights; private List<String> insights;
public GeneralDashboardData() {}
public GeneralDashboardData(DashboardStats stats, List<Map<String, Object>> storeOverview, List<Map<String, Object>> hourlySales, List<String> insights) { public GeneralDashboardData(DashboardStats stats, List<Map<String, Object>> storeOverview, List<Map<String, Object>> hourlySales, List<String> insights) {
this.stats = stats; this.stats = stats;
this.storeOverview = storeOverview; this.storeOverview = storeOverview;
@@ -16,9 +18,15 @@ public class GeneralDashboardData {
this.insights = insights; this.insights = insights;
} }
// Getters
public DashboardStats getStats() { return stats; } public DashboardStats getStats() { return stats; }
public void setStats(DashboardStats stats) { this.stats = stats; }
public List<Map<String, Object>> getStoreOverview() { return storeOverview; } public List<Map<String, Object>> getStoreOverview() { return storeOverview; }
public void setStoreOverview(List<Map<String, Object>> storeOverview) { this.storeOverview = storeOverview; }
public List<Map<String, Object>> getHourlySales() { return hourlySales; } public List<Map<String, Object>> getHourlySales() { return hourlySales; }
public void setHourlySales(List<Map<String, Object>> hourlySales) { this.hourlySales = hourlySales; }
public List<String> getInsights() { return insights; } public List<String> getInsights() { return insights; }
public void setInsights(List<String> insights) { this.insights = insights; }
} }

View File

@@ -41,6 +41,9 @@ public class Order {
@Column(nullable = false) @Column(nullable = false)
private LocalDateTime createdAt; private LocalDateTime createdAt;
@Column(nullable = false)
private String orderType = "STORE_ORDER"; // STORE_ORDER, QR_ORDER, MY_ORDER, etc.
@Column(name = "is_archived", nullable = false) @Column(name = "is_archived", nullable = false)
private boolean isArchived = false; private boolean isArchived = false;
@@ -97,6 +100,9 @@ public class Order {
public boolean isArchived() { return isArchived; } public boolean isArchived() { return isArchived; }
public void setArchived(boolean archived) { isArchived = archived; } public void setArchived(boolean archived) { isArchived = archived; }
public String getOrderType() { return orderType; }
public void setOrderType(String orderType) { this.orderType = orderType; }
public List<OrderItem> getItems() { return items; } public List<OrderItem> getItems() { return items; }
public void setItems(List<OrderItem> items) { this.items = items; } public void setItems(List<OrderItem> items) { this.items = items; }
} }

View File

@@ -1,10 +1,13 @@
package com.rit.canteen.sales.repository; package com.rit.canteen.sales.repository;
import com.rit.canteen.sales.model.BaseItem; import com.rit.canteen.sales.model.BaseItem;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@Repository @Repository
public interface BaseItemRepository extends JpaRepository<BaseItem, Long> { public interface BaseItemRepository extends JpaRepository<BaseItem, Long> {
boolean existsByName(String name); boolean existsByName(String name);
Page<BaseItem> findByNameContainingIgnoreCase(String name, Pageable pageable);
} }

View File

@@ -9,6 +9,11 @@ import java.util.List;
@Repository @Repository
public interface ProductRepository extends JpaRepository<Product, Long> { public interface ProductRepository extends JpaRepository<Product, Long> {
@Query("SELECT DISTINCT p FROM Product p LEFT JOIN FETCH p.stalls " +
"WHERE LOWER(p.name) LIKE LOWER(CONCAT('%', :search, '%')) " +
"OR LOWER(p.category) LIKE LOWER(CONCAT('%', :search, '%'))")
org.springframework.data.domain.Page<Product> findByNameOrCategoryContainingIgnoreCase(String search, org.springframework.data.domain.Pageable pageable);
@Query(value = "SELECT DISTINCT p FROM Product p LEFT JOIN FETCH p.stalls", @Query(value = "SELECT DISTINCT p FROM Product p LEFT JOIN FETCH p.stalls",
countQuery = "SELECT count(p) FROM Product p") countQuery = "SELECT count(p) FROM Product p")
org.springframework.data.domain.Page<Product> findAllWithStalls(org.springframework.data.domain.Pageable pageable); org.springframework.data.domain.Page<Product> findAllWithStalls(org.springframework.data.domain.Pageable pageable);

View File

@@ -2,6 +2,7 @@ package com.rit.canteen.sales.repository;
import com.rit.canteen.sales.model.User; import com.rit.canteen.sales.model.User;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.Optional; import java.util.Optional;
@@ -10,4 +11,8 @@ import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> { public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByMobileNumber(String mobileNumber); Optional<User> findByMobileNumber(String mobileNumber);
boolean existsByMobileNumber(String mobileNumber); boolean existsByMobileNumber(String mobileNumber);
@Query("SELECT u FROM User u WHERE LOWER(u.name) LIKE LOWER(CONCAT('%', :search, '%')) " +
"OR u.mobileNumber LIKE CONCAT('%', :search, '%')")
org.springframework.data.domain.Page<User> findByNameOrMobileContainingIgnoreCase(String search, org.springframework.data.domain.Pageable pageable);
} }

View File

@@ -50,7 +50,7 @@ public class DashboardService {
hourMap.put("value", 0); hourMap.put("value", 0);
for (Object[] row : hourlyData) { for (Object[] row : hourlyData) {
int h = (int) row[0]; int h = ((Number) row[0]).intValue();
if (h >= i && h < i + 2) { if (h >= i && h < i + 2) {
BigDecimal val = (BigDecimal) row[1]; BigDecimal val = (BigDecimal) row[1];
hourMap.put("value", ((Number) hourMap.get("value")).doubleValue() + val.doubleValue()); hourMap.put("value", ((Number) hourMap.get("value")).doubleValue() + val.doubleValue());

View File

@@ -158,11 +158,17 @@ public class UserService {
} }
/** /**
* Get all registered users for administration dashboard (Paginated). * Get all registered users for administration dashboard (Paginated with Search).
*/ */
public Page<LoginResponse.UserDto> getAllUsers(Pageable pageable) { public Page<LoginResponse.UserDto> getAllUsers(String search, Pageable pageable) {
return userRepository.findAll(pageable) Page<User> users;
.map(user -> new LoginResponse.UserDto( if (search != null && !search.isEmpty()) {
users = userRepository.findByNameOrMobileContainingIgnoreCase(search, pageable);
} else {
users = userRepository.findAll(pageable);
}
return users.map(user -> new LoginResponse.UserDto(
user.getId(), user.getId(),
user.getMobileNumber(), user.getMobileNumber(),
user.getName(), user.getName(),

View File

@@ -30,6 +30,7 @@ const BaseMenu = () => {
const [currentPage, setCurrentPage] = useState(0); const [currentPage, setCurrentPage] = useState(0);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const [totalElements, setTotalElements] = useState(0); const [totalElements, setTotalElements] = useState(0);
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
// Associated Products State // Associated Products State
const [selectedBaseItem, setSelectedBaseItem] = useState<BaseItem | null>(null); const [selectedBaseItem, setSelectedBaseItem] = useState<BaseItem | null>(null);
@@ -39,9 +40,24 @@ const BaseMenu = () => {
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
// Debounce search term
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchTerm(searchTerm);
}, 500);
return () => clearTimeout(timer);
}, [searchTerm]);
// Reset to first page when search changes
useEffect(() => {
setCurrentPage(0);
}, [debouncedSearchTerm]);
useEffect(() => { useEffect(() => {
fetchItems(); fetchItems();
}, [currentPage, pageSize, debouncedSearchTerm]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) { if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setOpenMenuId(null); setOpenMenuId(null);
@@ -49,13 +65,20 @@ const BaseMenu = () => {
}; };
document.addEventListener('mousedown', handleClickOutside); document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside);
}, [currentPage, pageSize]); }, []);
const fetchItems = async () => { const fetchItems = async () => {
setLoading(true); setLoading(true);
try { try {
const host = window.location.hostname; const host = window.location.hostname;
const response = await fetch(`http://${host}:8080/api/base-items?page=${currentPage}&size=${pageSize}`); const params = new URLSearchParams();
params.append('page', currentPage.toString());
params.append('size', pageSize.toString());
if (debouncedSearchTerm) {
params.append('search', debouncedSearchTerm);
}
const response = await fetch(`http://${host}:8080/api/base-items?${params.toString()}`);
const data = await response.json(); const data = await response.json();
if (data && data.content) { if (data && data.content) {
setItems(data.content); setItems(data.content);
@@ -200,17 +223,15 @@ const BaseMenu = () => {
</div> </div>
</td> </td>
</tr> </tr>
) : items.filter(item => item.name.toLowerCase().includes(searchTerm.toLowerCase())).length === 0 ? ( ) : items.length === 0 ? (
<tr> <tr>
<td colSpan={6} className="px-6 py-12 text-center text-[#64748b]"> <td colSpan={6} className="px-6 py-12 text-center text-[#64748b]">
<p className="text-lg font-medium mb-1">No items found</p> <p className="text-lg font-medium mb-1">No items found</p>
<p className="text-sm">Click "Add New Item" to create your first item.</p> <p className="text-sm">Try adjusting your search or add a new item.</p>
</td> </td>
</tr> </tr>
) : ( ) : (
items items.map((item) => (
.filter(item => item.name.toLowerCase().includes(searchTerm.toLowerCase()))
.map((item) => (
<tr key={item.id} className="hover:bg-gray-50/50 transition-all group"> <tr key={item.id} className="hover:bg-gray-50/50 transition-all group">
<td className="px-6 py-4 text-sm font-medium text-[#64748b]">#{item.id}</td> <td className="px-6 py-4 text-sm font-medium text-[#64748b]">#{item.id}</td>
<td className="px-6 py-4"> <td className="px-6 py-4">

View File

@@ -19,6 +19,7 @@ const Customers: React.FC = () => {
const [currentPage, setCurrentPage] = useState(0); const [currentPage, setCurrentPage] = useState(0);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const [totalElements, setTotalElements] = useState(0); const [totalElements, setTotalElements] = useState(0);
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
// Edit Modal State // Edit Modal State
const [showEditModal, setShowEditModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false);
@@ -34,15 +35,33 @@ const Customers: React.FC = () => {
// Action Menu State // Action Menu State
const [openMenuId, setOpenMenuId] = useState<number | null>(null); const [openMenuId, setOpenMenuId] = useState<number | null>(null);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchTerm(searchTerm);
}, 500);
return () => clearTimeout(timer);
}, [searchTerm]);
useEffect(() => {
setCurrentPage(0);
}, [debouncedSearchTerm]);
useEffect(() => { useEffect(() => {
fetchUsers(); fetchUsers();
}, [currentPage, pageSize]); }, [currentPage, pageSize, debouncedSearchTerm]);
const fetchUsers = async () => { const fetchUsers = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
const host = window.location.hostname; const host = window.location.hostname;
const response = await fetch(`http://${host}:8080/api/auth/users?page=${currentPage}&size=${pageSize}`); const params = new URLSearchParams();
params.append('page', currentPage.toString());
params.append('size', pageSize.toString());
if (debouncedSearchTerm) {
params.append('search', debouncedSearchTerm);
}
const response = await fetch(`http://${host}:8080/api/auth/users?${params.toString()}`);
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
if (data && data.content) { if (data && data.content) {
@@ -146,11 +165,6 @@ const Customers: React.FC = () => {
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2); return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
}; };
const filteredUsers = users.filter(user =>
user.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.mobileNumber?.includes(searchTerm)
);
return ( return (
<> <>
{/* Toast Notification - Moved outside animation trapping */} {/* Toast Notification - Moved outside animation trapping */}
@@ -232,8 +246,8 @@ const Customers: React.FC = () => {
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-[#e2e8f0]"> <tbody className="divide-y divide-[#e2e8f0]">
{filteredUsers.length > 0 ? ( {users.length > 0 ? (
filteredUsers.map((user) => ( users.map((user) => (
<tr key={user.id} className="hover:bg-gray-50/50 transition-all"> <tr key={user.id} className="hover:bg-gray-50/50 transition-all">
<td className="px-6 py-4"> <td className="px-6 py-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -300,7 +314,7 @@ const Customers: React.FC = () => {
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 p-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 p-6">
{filteredUsers.map(user => ( {users.map(user => (
<div key={user.id} className="group relative p-6 bg-white border border-[#e2e8f0] rounded-xl flex flex-col items-center text-center transition-all hover:shadow-lg hover:-translate-y-1"> <div key={user.id} className="group relative p-6 bg-white border border-[#e2e8f0] rounded-xl flex flex-col items-center text-center transition-all hover:shadow-lg hover:-translate-y-1">
<div className="absolute top-4 right-4"> <div className="absolute top-4 right-4">
<button onClick={() => setOpenMenuId(openMenuId === user.id ? null : user.id)} className="p-1.5 text-[#94a3b8] hover:text-[#231651] rounded-lg"> <button onClick={() => setOpenMenuId(openMenuId === user.id ? null : user.id)} className="p-1.5 text-[#94a3b8] hover:text-[#231651] rounded-lg">

View File

@@ -66,6 +66,7 @@ const Orders: React.FC = () => {
const [currentPage, setCurrentPage] = useState(0); const [currentPage, setCurrentPage] = useState(0);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const [totalElements, setTotalElements] = useState(0); const [totalElements, setTotalElements] = useState(0);
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('');
// Action Menu & Edit Modal States // Action Menu & Edit Modal States
const [showActionMenu, setShowActionMenu] = useState(false); const [showActionMenu, setShowActionMenu] = useState(false);
@@ -75,10 +76,26 @@ const Orders: React.FC = () => {
const [editingItems, setEditingItems] = useState<OrderItem[]>([]); const [editingItems, setEditingItems] = useState<OrderItem[]>([]);
const [isUpdatingOrder, setIsUpdatingOrder] = useState(false); const [isUpdatingOrder, setIsUpdatingOrder] = useState(false);
// Debounce search query
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchQuery(searchQuery);
}, 500);
return () => clearTimeout(timer);
}, [searchQuery]);
// Reset to first page when search changes
useEffect(() => {
setCurrentPage(0);
}, [debouncedSearchQuery]);
useEffect(() => { useEffect(() => {
fetchOrders(); fetchOrders();
}, [startDate, endDate, statusFilter, paymentFilter, currentPage, pageSize, debouncedSearchQuery]);
useEffect(() => {
fetchProducts(); fetchProducts();
}, [startDate, endDate, statusFilter, paymentFilter, currentPage, pageSize]); }, []);
const fetchProducts = async () => { const fetchProducts = async () => {
try { try {
@@ -102,7 +119,7 @@ const Orders: React.FC = () => {
if (endDate) params.append('endDate', `${endDate}T23:59:59`); if (endDate) params.append('endDate', `${endDate}T23:59:59`);
if (statusFilter) params.append('status', statusFilter); if (statusFilter) params.append('status', statusFilter);
if (paymentFilter) params.append('paymentType', paymentFilter); if (paymentFilter) params.append('paymentType', paymentFilter);
if (searchQuery) params.append('search', searchQuery); if (debouncedSearchQuery) params.append('search', debouncedSearchQuery);
params.append('page', currentPage.toString()); params.append('page', currentPage.toString());
params.append('size', pageSize.toString()); params.append('size', pageSize.toString());

View File

@@ -94,14 +94,32 @@ const Products = () => {
const [currentPage, setCurrentPage] = useState(0); const [currentPage, setCurrentPage] = useState(0);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const [totalElements, setTotalElements] = useState(0); const [totalElements, setTotalElements] = useState(0);
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
// Debounce search term
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchTerm(searchTerm);
}, 500);
return () => clearTimeout(timer);
}, [searchTerm]);
// Reset to first page when search changes
useEffect(() => {
setCurrentPage(0);
}, [debouncedSearchTerm]);
useEffect(() => { useEffect(() => {
fetchProducts(); fetchProducts();
}, [currentPage, pageSize, debouncedSearchTerm]);
useEffect(() => {
fetchBaseItems(); fetchBaseItems();
fetchAllStalls(); fetchAllStalls();
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) { if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setOpenMenuId(null); setOpenMenuId(null);
@@ -109,13 +127,20 @@ const Products = () => {
}; };
document.addEventListener('mousedown', handleClickOutside); document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside);
}, [currentPage, pageSize]); }, []);
const fetchProducts = async () => { const fetchProducts = async () => {
setLoading(true); setLoading(true);
try { try {
const host = window.location.hostname; const host = window.location.hostname;
const response = await fetch(`http://${host}:8080/api/products?page=${currentPage}&size=${pageSize}`); const params = new URLSearchParams();
params.append('page', currentPage.toString());
params.append('size', pageSize.toString());
if (debouncedSearchTerm) {
params.append('search', debouncedSearchTerm);
}
const response = await fetch(`http://${host}:8080/api/products?${params.toString()}`);
const data = await response.json(); const data = await response.json();
if (data && data.content) { if (data && data.content) {
setProducts(data.content); setProducts(data.content);
@@ -133,9 +158,9 @@ const Products = () => {
const fetchBaseItems = async () => { const fetchBaseItems = async () => {
try { try {
const response = await fetch('http://localhost:8080/api/base-items'); const response = await fetch('http://localhost:8080/api/base-items?size=100');
const data = await response.json(); const data = await response.json();
setBaseItems(data); setBaseItems(data.content || data);
} catch (error) { } catch (error) {
console.error('Error fetching base items:', error); console.error('Error fetching base items:', error);
} }
@@ -303,9 +328,9 @@ const Products = () => {
<tbody className="divide-y divide-[#e2e8f0]"> <tbody className="divide-y divide-[#e2e8f0]">
{loading ? ( {loading ? (
<tr><td colSpan={6} className="px-6 py-12 text-center text-[#64748b]"><RefreshCw className="animate-spin inline mr-2" />Loading...</td></tr> <tr><td colSpan={6} className="px-6 py-12 text-center text-[#64748b]"><RefreshCw className="animate-spin inline mr-2" />Loading...</td></tr>
) : products.filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()) || p.category.toLowerCase().includes(searchTerm.toLowerCase())).length === 0 ? ( ) : products.length === 0 ? (
<tr><td colSpan={6} className="px-6 py-12 text-center text-[#64748b]">No products found</td></tr> <tr><td colSpan={6} className="px-6 py-12 text-center text-[#64748b]">No products found</td></tr>
) : products.filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()) || p.category.toLowerCase().includes(searchTerm.toLowerCase())).map((product) => ( ) : products.map((product) => (
<tr key={product.id} className="hover:bg-gray-50/50 transition-all"> <tr key={product.id} className="hover:bg-gray-50/50 transition-all">
<td className="px-6 py-4"> <td className="px-6 py-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">

View File

@@ -194,12 +194,18 @@ const Stalls: React.FC = () => {
try { try {
const host = window.location.hostname; const host = window.location.hostname;
const [prodRes, baseRes] = await Promise.all([ const [prodRes, baseRes] = await Promise.all([
fetch(`http://${host}:8080/api/products`), fetch(`http://${host}:8080/api/products?size=1000`),
fetch(`http://${host}:8080/api/base-items`) fetch(`http://${host}:8080/api/base-items?size=100`)
]); ]);
if (prodRes.ok) setAllProducts(await prodRes.json()); if (prodRes.ok) {
if (baseRes.ok) setAllBaseItems(await baseRes.json()); const data = await prodRes.json();
setAllProducts(data.content || data);
}
if (baseRes.ok) {
const data = await baseRes.json();
setAllBaseItems(data.content || data);
}
} catch (error) { } catch (error) {
console.error('Error fetching available items:', error); console.error('Error fetching available items:', error);
} }