добавил стату по стоимости заказа

This commit is contained in:
2025-11-07 18:03:57 +04:00
parent aa66e43e67
commit 760d3cc709
72 changed files with 268 additions and 164 deletions

Binary file not shown.

View File

@@ -1,4 +1,4 @@
-- Тестовые клиенты
INSERT INTO customers (id, name, email, created_at, updated_at)
VALUES
(1, 'Иван Иванов', 'ivan@mail.ru', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
@@ -7,7 +7,6 @@ VALUES
(4, 'Алексей Козлов', 'alex@mail.ru', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(5, 'Ольга Новикова', 'olga@mail.ru', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
-- Тестовые доставки (опционально)
INSERT INTO deliveries (id, tracking_number, destination, status, customer_name, created_at, updated_at)
VALUES
(1, 'IVN777777', 'гоголя 34', 'В пути', 'Иван Иванов', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),

View File

@@ -4,7 +4,8 @@ function OrderForm({ customers, deliveries, onSubmit, editOrder }) {
const [formData, setFormData] = useState({
customerId: '',
deliveryId: '',
status: 'В пути'
status: 'В пути',
orderAmount: ''
});
useEffect(() => {
@@ -12,17 +13,40 @@ function OrderForm({ customers, deliveries, onSubmit, editOrder }) {
setFormData({
customerId: editOrder.customer?.id || '',
deliveryId: editOrder.delivery?.id || '',
status: editOrder.status || 'В пути'
status: editOrder.status || 'В пути',
orderAmount: editOrder.orderAmount || ''
});
}
}, [editOrder]);
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(formData);
if (!editOrder) {
setFormData({ customerId: '', deliveryId: '', status: 'В пути' });
const submitData = {
...formData,
customerId: formData.customerId ? parseInt(formData.customerId) : null,
deliveryId: formData.deliveryId ? parseInt(formData.deliveryId) : null,
orderAmount: parseFloat(formData.orderAmount) || 0
};
if (!submitData.customerId || !submitData.deliveryId) {
alert("Пожалуйста, выберите клиента и доставку!");
return;
}
console.log("Отправляемые данные:", submitData);
onSubmit(submitData);
if (!editOrder) {
setFormData({ customerId: '', deliveryId: '', status: 'В пути', orderAmount: '' });
}
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const statusOptions = ['В пути', 'Доставлено', 'Обработка', 'Отменен'];
@@ -34,8 +58,9 @@ function OrderForm({ customers, deliveries, onSubmit, editOrder }) {
<div style={{ marginBottom: '10px' }}>
<label>Клиент: </label>
<select
name="customerId"
value={formData.customerId}
onChange={(e) => setFormData({...formData, customerId: e.target.value})}
onChange={handleChange}
required
>
<option value="">Выберите клиента</option>
@@ -50,8 +75,9 @@ function OrderForm({ customers, deliveries, onSubmit, editOrder }) {
<div style={{ marginBottom: '10px' }}>
<label>Доставка: </label>
<select
name="deliveryId"
value={formData.deliveryId}
onChange={(e) => setFormData({...formData, deliveryId: e.target.value})}
onChange={handleChange}
required
>
<option value="">Выберите доставку</option>
@@ -66,8 +92,9 @@ function OrderForm({ customers, deliveries, onSubmit, editOrder }) {
<div style={{ marginBottom: '10px' }}>
<label>Статус: </label>
<select
name="status"
value={formData.status}
onChange={(e) => setFormData({...formData, status: e.target.value})}
onChange={handleChange}
>
{statusOptions.map(status => (
<option key={status} value={status}>{status}</option>
@@ -75,6 +102,21 @@ function OrderForm({ customers, deliveries, onSubmit, editOrder }) {
</select>
</div>
<div style={{ marginBottom: '10px' }}>
<label>Стоимость заказа (руб): </label>
<input
type="number"
name="orderAmount"
value={formData.orderAmount}
onChange={handleChange}
placeholder="Введите сумму"
min="0"
step="0.01"
required
style={{ marginLeft: '10px', padding: '5px' }}
/>
</div>
<button type="submit" style={{ padding: '10px 20px' }}>
{editOrder ? 'Обновить заказ' : 'Создать заказ'}
</button>

View File

@@ -6,21 +6,31 @@ function OrderItem({ order, onEdit, onDelete }) {
margin: '10px 0',
borderRadius: '5px'
}}>
<h4>Заказ #{order.id}</h4>
<h3>Заказ #{order.id}</h3>
<p><strong>Клиент:</strong> {order.customer?.name} ({order.customer?.email})</p>
<p><strong>Доставка:</strong> {order.delivery?.trackingNumber} - {order.delivery?.destination}</p>
<p><strong>Статус:</strong>
<span style={{
color: order.status === 'Доставлено' ? 'green' :
order.status === 'В пути' ? 'orange' : 'gray',
fontWeight: 'bold',
marginLeft: '10px'
}}>
{order.status}
</span>
</p>
<p><strong>Статус:</strong> {order.status}</p>
{}
<p><strong>Сумма заказа:</strong> {order.orderAmount ? `${order.orderAmount} руб.` : 'Не указана'}</p>
<div style={{ marginTop: '10px' }}>
<button
onClick={() => onEdit(order)}
style={{ marginRight: '10px', padding: '5px 10px' }}
>
Редактировать
</button>
<button
onClick={() => onDelete(order.id)}
style={{ padding: '5px 10px', backgroundColor: '#ff4444', color: 'white' }}
>
Удалить
</button>
</div>
</div>
);
}

View File

@@ -62,11 +62,11 @@ function OrderList() {
const handleCreateOrder = async (orderData) => {
try {
const orderRq = {
customerId: orderData.customerId,
deliveryId: orderData.deliveryId,
status: orderData.status
status: orderData.status,
orderAmount: orderData.orderAmount
};
const response = await fetch('http://localhost:8080/api/orders', {
@@ -96,7 +96,8 @@ function OrderList() {
const orderRq = {
customerId: orderData.customerId,
deliveryId: orderData.deliveryId,
status: orderData.status
status: orderData.status,
orderAmount: orderData.orderAmount
};
const response = await fetch(`http://localhost:8080/api/orders/${editingOrder.id}`, {

View File

@@ -9,8 +9,9 @@ import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
import java.util.Map;
@CrossOrigin(origins = "http://localhost:5173")
@RestController
@@ -33,44 +34,23 @@ public class OrderController {
@GetMapping("/{id}")
@Operation(summary = "Получить заказ по ID")
public ResponseEntity<OrderRs> getOne(@PathVariable Long id) {
Optional<OrderRs> order = orderService.findById(id);
return order.map(ResponseEntity::ok)
return orderService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/customer/{customerId}")
@Operation(summary = "Получить заказы по ID клиента")
public List<OrderRs> getByCustomer(@PathVariable Long customerId) {
return orderService.findByCustomerId(customerId);
}
@GetMapping("/status/{status}")
@Operation(summary = "Получить заказы по статусу")
public List<OrderRs> getByStatus(@PathVariable String status) {
return orderService.findByStatus(status);
}
@PostMapping
@Operation(summary = "Создать новый заказ")
public ResponseEntity<?> create(@Valid @RequestBody OrderRq orderRq) {
try {
OrderRs created = orderService.create(orderRq);
return ResponseEntity.ok(created);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
public OrderRs create(@Valid @RequestBody OrderRq orderRq) {
return orderService.create(orderRq);
}
@PutMapping("/{id}")
@Operation(summary = "Обновить заказ")
public ResponseEntity<?> update(@PathVariable Long id, @Valid @RequestBody OrderRq orderRq) {
try {
Optional<OrderRs> updated = orderService.update(id, orderRq);
return updated.map(ResponseEntity::ok)
@Operation(summary = "Обновить данные заказа")
public ResponseEntity<OrderRs> update(@PathVariable Long id, @Valid @RequestBody OrderRq orderRq) {
return orderService.update(id, orderRq)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
@DeleteMapping("/{id}")
@@ -80,8 +60,26 @@ public class OrderController {
return deleted ? ResponseEntity.ok().build() : ResponseEntity.notFound().build();
}
@GetMapping("/stats/customer-spending")
@Operation(summary = "Получить статистику трат клиентов")
public List<OrderService.CustomerSpendingStats> getCustomerSpendingStats() {
return orderService.getCustomerSpendingStats();
}
@GetMapping("/stats/total-revenue")
@Operation(summary = "Получить общую выручку")
public Map<String, BigDecimal> getTotalRevenue() {
return Map.of("totalRevenue", orderService.getTotalRevenue());
}
@GetMapping("/stats/average-order-value")
@Operation(summary = "Получить средний чек")
public Map<String, BigDecimal> getAverageOrderValue() {
return orderService.getAverageOrderValue();
}
@GetMapping("/stats/monthly")
@Operation(summary = "Получить месячную статистику заказов")
@Operation(summary = "Получить статистику заказов по месяцам")
public List<OrderService.OrderMonthlyStats> getOrderMonthlyStats() {
return orderService.getOrderMonthlyStats();
}

View File

@@ -1,6 +1,7 @@
package com.example.dto;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
public class OrderRq {
@NotNull(message = "ID клиента обязателен")
@@ -12,16 +13,18 @@ public class OrderRq {
@NotNull(message = "Статус обязателен")
private String status;
@NotNull(message = "Стоимость заказа обязательна")
private BigDecimal orderAmount;
public OrderRq() {}
public OrderRq(Long customerId, Long deliveryId, String status) {
public OrderRq(Long customerId, Long deliveryId, String status, BigDecimal orderAmount) {
this.customerId = customerId;
this.deliveryId = deliveryId;
this.status = status;
this.orderAmount = orderAmount;
}
public Long getCustomerId() { return customerId; }
public void setCustomerId(Long customerId) { this.customerId = customerId; }
@@ -30,4 +33,7 @@ public class OrderRq {
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public BigDecimal getOrderAmount() { return orderAmount; }
public void setOrderAmount(BigDecimal orderAmount) { this.orderAmount = orderAmount; }
}

View File

@@ -1,5 +1,6 @@
package com.example.dto;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public class OrderRs {
@@ -7,10 +8,10 @@ public class OrderRs {
private CustomerRs customer;
private DeliveryRs delivery;
private String status;
private BigDecimal orderAmount;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
@@ -23,6 +24,9 @@ public class OrderRs {
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public BigDecimal getOrderAmount() { return orderAmount; }
public void setOrderAmount(BigDecimal orderAmount) { this.orderAmount = orderAmount; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }

View File

@@ -1,17 +1,16 @@
package com.example.entity;
import jakarta.persistence.*;
import java.math.BigDecimal;
@Entity
@Table(name = "orders")
public class Order extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "delivery_id", nullable = false)
private Delivery delivery;
@@ -19,70 +18,37 @@ public class Order extends BaseEntity {
@Column(name = "status", nullable = false)
private String status;
@Column(name = "order_amount", nullable = false)
private BigDecimal orderAmount;
public Order() {}
public Order(Customer customer, Delivery delivery, String status) {
public Order(Customer customer, Delivery delivery, String status, BigDecimal orderAmount) {
this.customer = customer;
this.delivery = delivery;
this.status = status;
this.orderAmount = orderAmount;
}
public Customer getCustomer() {
return customer;
}
public Customer getCustomer() { return customer; }
public void setCustomer(Customer customer) {
this.customer = customer;
if (customer != null && !customer.getOrders().contains(this)) {
customer.getOrders().add(this);
}
}
public Delivery getDelivery() {
return delivery;
}
public Delivery getDelivery() { return delivery; }
public void setDelivery(Delivery delivery) {
this.delivery = delivery;
if (delivery != null && !delivery.getOrders().contains(this)) {
delivery.getOrders().add(this);
}
}
public String getStatus() {
return status;
}
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public void setStatus(String status) {
this.status = status;
}
@Override
public String toString() {
return "Order{" +
"id=" + getId() +
", customer=" + (customer != null ? customer.getName() : "null") +
", delivery=" + (delivery != null ? delivery.getTrackingNumber() : "null") +
", status='" + status + '\'' +
", createdAt=" + getCreatedAt() +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Order)) return false;
Order order = (Order) o;
return getId() != null && getId().equals(order.getId());
}
@Override
public int hashCode() {
return getClass().hashCode();
}
public BigDecimal getOrderAmount() { return orderAmount; }
public void setOrderAmount(BigDecimal orderAmount) { this.orderAmount = orderAmount; }
}

View File

@@ -13,6 +13,7 @@ public class OrderMapper {
public Order toEntity(OrderRq orderRq) {
Order order = new Order();
order.setStatus(orderRq.getStatus());
order.setOrderAmount(orderRq.getOrderAmount());
return order;
}
@@ -20,10 +21,10 @@ public class OrderMapper {
OrderRs orderRs = new OrderRs();
orderRs.setId(order.getId());
orderRs.setStatus(order.getStatus());
orderRs.setOrderAmount(order.getOrderAmount());
orderRs.setCreatedAt(order.getCreatedAt());
orderRs.setUpdatedAt(order.getUpdatedAt());
if (order.getCustomer() != null) {
CustomerRs customerRs = new CustomerRs();
customerRs.setId(order.getCustomer().getId());

View File

@@ -3,47 +3,36 @@ package com.example.repository;
import com.example.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
import java.util.List;
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByCustomerId(Long customerId);
List<Order> findByDeliveryId(Long deliveryId);
List<Order> findByStatus(String status);
@Query("SELECT MONTH(o.createdAt) as month, " +
"COUNT(o.id) as orderCount " +
"FROM Order o " +
"GROUP BY MONTH(o.createdAt) " +
"ORDER BY month")
List<OrderMonthlyStats> findOrderMonthlyStats();
interface OrderMonthlyStats {
Integer getMonth();
Long getOrderCount();
}
@Query("SELECT o.status, COUNT(o) FROM Order o GROUP BY o.status")
List<Object[]> findOrderStatusStats();
@Query("SELECT o.status as status, " +
"COUNT(o.id) as total " +
"FROM Order o " +
"GROUP BY o.status")
List<OrderStatusStats> findOrderStatusStats();
@Query("SELECT MONTH(o.createdAt), COUNT(o) FROM Order o GROUP BY MONTH(o.createdAt)")
List<Object[]> findOrderMonthlyStats();
interface OrderStatusStats {
String getStatus();
Long getTotal();
}
@Query("SELECT o.customer.id, o.customer.name, SUM(o.orderAmount), COUNT(o) " +
"FROM Order o GROUP BY o.customer.id, o.customer.name ORDER BY SUM(o.orderAmount) DESC")
List<Object[]> findCustomerSpendingStats();
@Query("SELECT SUM(o.orderAmount) FROM Order o")
BigDecimal getTotalRevenue();
@Query("SELECT o FROM Order o WHERE o.customer.id = :customerId AND o.status = :status")
List<Order> findCustomerOrdersByStatus(@Param("customerId") Long customerId,
@Param("status") String status);
List<Order> findCustomerOrdersByStatus(Long customerId, String status);
}

View File

@@ -12,7 +12,10 @@ import com.example.repository.OrderRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Service
@@ -95,6 +98,7 @@ public class OrderService {
order.setCustomer(customer);
order.setDelivery(delivery);
order.setStatus(orderRq.getStatus());
order.setOrderAmount(orderRq.getOrderAmount());
Order updated = orderRepository.save(order);
return orderMapper.toResponse(updated);
@@ -109,31 +113,116 @@ public class OrderService {
return false;
}
@Transactional(readOnly = true)
public List<OrderMonthlyStats> getOrderMonthlyStats() {
List<OrderRepository.OrderMonthlyStats> repoStats = orderRepository.findOrderMonthlyStats();
return repoStats.stream()
.map(stat -> new OrderMonthlyStats(stat.getMonth(), stat.getOrderCount()))
@Transactional(readOnly = true)
public List<CustomerSpendingStats> getCustomerSpendingStats() {
List<Order> allOrders = orderRepository.findAll();
Map<Customer, List<Order>> ordersByCustomer = allOrders.stream()
.collect(java.util.stream.Collectors.groupingBy(Order::getCustomer));
return ordersByCustomer.entrySet().stream()
.map(entry -> {
Customer customer = entry.getKey();
List<Order> customerOrders = entry.getValue();
BigDecimal totalSpent = customerOrders.stream()
.map(Order::getOrderAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
return new CustomerSpendingStats(
customer.getId(),
customer.getName(),
totalSpent,
(long) customerOrders.size()
);
})
.sorted((a, b) -> b.totalSpent().compareTo(a.totalSpent()))
.toList();
}
public record OrderMonthlyStats(Integer month, Long orderCount) {}
public record CustomerSpendingStats(Long customerId, String customerName, BigDecimal totalSpent, Long orderCount) {}
@Transactional(readOnly = true)
public List<OrderStatusStats> getOrderStatusStats() {
List<OrderRepository.OrderStatusStats> repoStats = orderRepository.findOrderStatusStats();
List<Order> allOrders = orderRepository.findAll();
return repoStats.stream()
.map(stat -> new OrderStatusStats(stat.getStatus(), stat.getTotal()))
Map<String, List<Order>> ordersByStatus = allOrders.stream()
.collect(java.util.stream.Collectors.groupingBy(Order::getStatus));
return ordersByStatus.entrySet().stream()
.map(entry -> new OrderStatusStats(
entry.getKey(),
(long) entry.getValue().size()
))
.toList();
}
public record OrderStatusStats(String status, Long total) {}
@Transactional(readOnly = true)
public List<Order> getCustomerOrdersByStatus(Long customerId, String status) {
return orderRepository.findCustomerOrdersByStatus(customerId, status);
public List<OrderMonthlyStats> getOrderMonthlyStats() {
List<Order> allOrders = orderRepository.findAll();
Map<Integer, List<Order>> ordersByMonth = allOrders.stream()
.collect(java.util.stream.Collectors.groupingBy(order ->
order.getCreatedAt().getMonthValue()
));
return ordersByMonth.entrySet().stream()
.map(entry -> new OrderMonthlyStats(
entry.getKey(),
(long) entry.getValue().size()
))
.sorted((a, b) -> a.month().compareTo(b.month()))
.toList();
}
public record OrderMonthlyStats(Integer month, Long orderCount) {}
@Transactional(readOnly = true)
public BigDecimal getTotalRevenue() {
List<Order> allOrders = orderRepository.findAll();
return allOrders.stream()
.map(Order::getOrderAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
@Transactional(readOnly = true)
public Map<String, BigDecimal> getAverageOrderValue() {
List<Order> allOrders = orderRepository.findAll();
if (allOrders.isEmpty()) {
return Map.of("averageOrderValue", BigDecimal.ZERO);
}
BigDecimal totalRevenue = getTotalRevenue();
BigDecimal average = totalRevenue.divide(BigDecimal.valueOf(allOrders.size()), 2, RoundingMode.HALF_UP);
return Map.of("averageOrderValue", average);
}
@Transactional(readOnly = true)
public Map<String, Object> getBusinessSummary() {
long totalCustomers = customerRepository.count();
long totalDeliveries = deliveryRepository.count();
long totalOrders = orderRepository.count();
BigDecimal totalRevenue = getTotalRevenue();
List<Order> activeOrders = orderRepository.findByStatus("В пути");
long activeOrdersCount = activeOrders.size();
return Map.of(
"totalCustomers", totalCustomers,
"totalDeliveries", totalDeliveries,
"totalOrders", totalOrders,
"totalRevenue", totalRevenue,
"activeOrders", activeOrdersCount
);
}
@Transactional(readOnly = true)

View File

@@ -1,4 +1,4 @@
-- Тестовые клиенты
INSERT INTO customers (id, name, email, created_at, updated_at)
VALUES
(1, 'Иван Иванов', 'ivan@mail.ru', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
@@ -7,7 +7,6 @@ VALUES
(4, 'Алексей Козлов', 'alex@mail.ru', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(5, 'Ольга Новикова', 'olga@mail.ru', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
-- Тестовые доставки (опционально)
INSERT INTO deliveries (id, tracking_number, destination, status, customer_name, created_at, updated_at)
VALUES
(1, 'IVN777777', 'гоголя 34', 'В пути', 'Иван Иванов', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),