Разработаем простое, но быстрое, респонсивное и масштабируемое решение для интернет-магазина, использовав передовые технологии: мощь Java со Spring Boot для бэкенда, Angular для фронтенда, надежную базу данных MySQL и Maven для упрощения управления проектами, а также Jasper Reports для генерирования отчетов в формате PDF. Посмотрим, как из всего этого создается функциональное и готовое к применению в реальных условиях приложение.

С технологиями определились, теперь сделаем надежную схему базы данных для поддержки приложения. Схема тщательно прорабатывается, для эффективного управления данными в нее помещаются ключевые таблицы Customers («Клиенты»), Items («Товары») и Orders («Заказы») с четко определенными отношениями каждой из них. Этой структурой поддерживают надежные CRUD-операции: создание, чтение, изменение, удаление.

Схема базы данных

Разработка бэкенда

Бэкенд приложения разработаем при помощи Spring Boot. Этой частью проекта управляется бизнес-логика, обрабатываются запросы от фронтенда, осуществляется взаимодействие с базой данных, обеспечивается общая функциональность приложения.

1. Инициализация Spring Boot

Разработку бэкенда начинаем с настройки проекта Spring Boot в Spring Initializr. Этим инструментом быстро настраиваются нужные зависимости: Spring Web, Spring Data JPA, MySQL и Lombok. Задав в процессе инициализации структуру проекта, приступаем к созданию ключевых компонентов приложения.

Инициализация Spring

2. Обзор модулей

Сфокусируемся на модуле Order, детали реализации других важных компонентов  —  модулей Customer и Item  —  находятся в репозитории GitHub.

Сущность Order  —  это центр приложения для интернет-магазина. Вот краткое описание каждого компонента:

  • Модель. Классом сущности Order представлены данные заказа: поля orderCode, orderDate, quantity, totalPrice и отношения с сущностями Customer и Item.
  • Объект переноса данных DTO. При помощи OrderDto данные заказа передаются между клиентом и сервером, чем упрощается структура данных: акцент в ней делается на релевантных для взаимодействия с API полях.
  • Репозиторий. Для обработки CRUD-операций сущности Order интерфейсом OrderRepository расширяется JpaRepository, предоставляются методы для взаимодействия с базой данных без пользовательских SQL-запросов.
  • Контроллер. При помощи OrderController управляют конечными точками API для создания, чтения, изменения и удаления заказов. HTTP-запросы им обрабатываются и отображаются на слой служб, возвращаются соответствующие ответы.
  • Служба. В классе OrderService содержится бизнес-логика для управления заказами. Чтобы выполнять операции с базой данных и обрабатывать данные перед их передачей контроллеру, он взаимодействует с репозиторием.

Проиллюстрируем фрагментом сущности Order и соответствующей логикой службы и контроллера:

@Entity
@Data
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long orderId;
@Column(name = "order_code", nullable = false)
private String orderCode;
@Column(name = "order_date", nullable = false)
private Date orderDate;
@Column(name = "total_price")
private Integer totalPrice;
@Column(name = "quantity")
private Integer quantity;
@ManyToOne
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
@ManyToOne
@JoinColumn(name = "items_id", nullable = false)
private Item item;
}
@Data
public class OrderDto {
private String orderCode;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date orderDate;
private Integer totalPrice;
private Integer quantity;
private Long customerId;
private Long itemsId;
}
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByOrderId(Long orderId);
}
@RestController
@RequestMapping("/orders")
public class OrderController {
private static final Logger logger = LoggerFactory.getLogger(OrderController.class);
@Autowired
private final OrderService orderService;
private final OrderRepository orderRepository;
private final ReportService reportService;
public OrderController(OrderService orderService, OrderRepository orderRepository, ReportService reportService) {
this.orderService = orderService;
this.orderRepository = orderRepository;
this.reportService = reportService;
}
@PostMapping
public ResponseEntity<Order> addOrder(@RequestBody OrderDto orderDto) {
logger.info("Received OrderDto: {}", orderDto);
Order order = orderService.addOrder(orderDto);
return ResponseEntity.status(HttpStatus.CREATED).body(order);
}
@PutMapping("/{orderId}")
public ResponseEntity<?> updateOrder(@PathVariable Long orderId, @RequestBody OrderDto orderDto) {
try {
Order order = orderService.updateOrder(orderId, orderDto);
return ResponseEntity.ok(order);
} catch (ResponseStatusException e) {
return ResponseEntity.status(e.getStatusCode()).body(e.getReason());
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error occurred");
}
}
@DeleteMapping("/{orderId}")
public ResponseEntity<String> deleteOrder(@PathVariable Long orderId) {
try {
orderService.deleteOrder(orderId);
return ResponseEntity.noContent().build();
} catch (Exception ex) {
logger.error("Error deleting order with ID " + orderId, ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@GetMapping("/{orderId}")
public ResponseEntity<Order> getOrderById(@PathVariable Long orderId) {
Order order = orderService.getOrderById(orderId);
return order != null ? ResponseEntity.ok(order) : ResponseEntity.notFound().build();
}
@GetMapping
public ResponseEntity<Page<Order>> getOrders(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Page<Order> ordersPage = orderService.getAllOrders(page, size);
return ResponseEntity.ok(ordersPage);
}
}
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private CustomerRepository customerRepository;
@Autowired
private ItemRepository itemRepository;
@Autowired
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public Order addOrder(OrderDto orderDto) {
Order order = new Order();
order.setOrderCode(orderDto.getOrderCode());
order.setOrderDate(orderDto.getOrderDate());
order.setQuantity(orderDto.getQuantity());
Customer customer = customerRepository.findById(orderDto.getCustomerId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Customer not found"));
order.setCustomer(customer);
Item item = itemRepository.findById(orderDto.getItemsId())
.orElseThrow(() ->new ResponseStatusException(HttpStatus.NOT_FOUND, "Item not found"));
// Количество товара уменьшается в зависимости от количества в заказе
int updatedQuantity = item.getStock() - orderDto.getQuantity();
if (updatedQuantity < 0) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Insufficient stock of the item");
}
item.setStock(updatedQuantity);
// Исходя из количества и цены товара, высчитывается общая цена
int totalPrice = orderDto.getQuantity() * item.getPrice();
order.setTotalPrice(totalPrice);
itemRepository.save(item);
order.setItem(item);
return orderRepository.save(order);
}
public Order updateOrder(Long orderId, OrderDto orderDto) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Order not found"));
if (orderDto.getOrderCode() != null) {
order.setOrderCode(orderDto.getOrderCode());
}
if (orderDto.getOrderDate() != null) {
order.setOrderDate(orderDto.getOrderDate());
}
if (orderDto.getQuantity() != null) {
// Перед обновлением товаров восстанавливаются старые номенклатурные позиции
Item oldItem = order.getItem();
if (oldItem != null) {
int restoredStock = oldItem.getStock() + order.getQuantity();
oldItem.setStock(restoredStock);
itemRepository.save(oldItem);
}
// Проверяются и обновляются товары, если доступен «itemsId»
if (orderDto.getItemsId() != null) {
Item item = itemRepository.findById(orderDto.getItemsId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Item tidak ditemukan"));
int updatedStock = item.getStock() - orderDto.getQuantity();
if (updatedStock < 0) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Stock item tidak mencukupi");
}
item.setStock(updatedStock);
order.setItem(item);
itemRepository.save(item);
// Пересчитывается общая цена
int totalPrice = orderDto.getQuantity() * item.getPrice();
order.setTotalPrice(totalPrice);
} else {
// Если «itemsId» не указан, общая цена пересчитывается для имеющихся товаров
Item currentItem = order.getItem();
if (currentItem != null) {
int totalPrice = orderDto.getQuantity() * currentItem.getPrice();
order.setTotalPrice(totalPrice);
}
}
// Обновляется количество в заказе
order.setQuantity(orderDto.getQuantity());
}
if (orderDto.getCustomerId() != null) {
Customer customer = customerRepository.findById(orderDto.getCustomerId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Customer tidak ditemukan"));
order.setCustomer(customer);
}
return orderRepository.save(order);
}
public void deleteOrder(Long orderId) {
orderRepository.deleteById(orderId);
}
public Order getOrderById(Long orderId) {
Optional<Order> optionalOrder = orderRepository.findById(orderId);
return optionalOrder.orElse(null);
}
public Page<Order> getAllOrders(int page, int size) {
return orderRepository.findAll(PageRequest.of(page, size));
}}

3. Конфигурация MinIO для локальной разработки

Чтобы управлять файловым хранилищем приложения, интегрируем высокопроизводительную систему хранения объектов MinIO. Сначала устанавливаем и настраиваем MinIO локально, следуя руководству по MinIO.

Вот конфигурация для MinIO в проекте Spring Boot:

@Configuration
public class MinioConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.accessKey}")
private String accessKey;
@Value("${minio.secretKey}")
private String secretKey;
@Bean
public MinioClient minioClient(MinioProp props) {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}

Свойства MinIO minio.url, minio.access.key, minio.secret.key настраиваются в application.properties или в файле application.yml. Вот моя конфигурация MinIO в application.properties:

#MinIO
minio.endpoint=http://localhost:9000
minio.accessKey=minioadmin
minio.secretKey=minioadmin
minio.bucket.name=onlineshop

4. Конфигурация Jasper Reports

При помощи Jasper Reports генерируются сводки по заказам в формате PDF. Начинаем с включения зависимостей в pom.xml:

<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports</artifactId>
<version>6.20.0</version>
</dependency>

Затем создаем шаблон Jasper Report order_report.jrxml и компилируем его в файл .jasper. Отчет генерируется таким кодом:

@Service
public class ReportService {
@Autowired
private ResourceLoader resourceLoader;
@Autowired
private OrderRepository orderRepository;
public byte[] generateOrderReport(Long orderId) throws Exception {
Resource resource = resourceLoader.getResource("classpath:reports/order_report.jrxml");
InputStream inputStream = resource.getInputStream();
JasperReport jasperReport = JasperCompileManager.compileReport(inputStream);
Map<String, Object> parameters = new HashMap<>();
parameters.put("ORDER_ID", orderId);
List<Order> orders = orderRepository.findByOrderId(orderId);
if (orders.isEmpty()) {
throw new RuntimeException("Order not found with ID: " + orderId);
}
JRBeanCollectionDataSource dataSource = new JRBeanCollectionDataSource(orders);
JasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, parameters, dataSource);
return JasperExportManager.exportReportToPdf(jasperPrint);
}
}

Чтобы сгенерированный отчет загружался пользователями, добавляем в OrderController такой метод:

@GetMapping("/report/{orderId}")
public ResponseEntity<byte[]> downloadOrderReport(@PathVariable Long orderId) {
try {
byte[] pdfBytes = reportService.generateOrderReport(orderId);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDispositionFormData("attachment", "order_report_" + orderId + ".pdf");
return new ResponseEntity<>(pdfBytes, headers, HttpStatus.OK);
} catch (Exception e) {
e.printStackTrace();
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}

Этим методом downloadOrderReport отчет о заказе загружается пользователями в PDF-файле. На то, что в ответе содержится PDF-файл, указывается соответствующими HTTP-заголовками, на основе orderId файлу присваивается название.

5. Тестирование с Postman

Наконец, с помощью Postman протестируем конечные точки API и генерирование отчетов. Начинаем с создания заказа, используя конечную точку POST /http://localhost:8080/orders.

Тестирование создания заказа в Postman

Затем тестируем другие конечные точки для редактирования, удаления, просмотра и загрузки отчета.

Вот так, шаг за шагом, получаются полнофункциональный бэкенд со Spring Boot, интегрированный с MinIO для хранения файлов, и Jasper Reports для генерирования отчетов о заказах.

Разработка фронтенда

Фронтенд приложения разработаем при помощи Angular. В этой части проекта создается адаптивный и удобный интерфейс для управления заказами. Благодаря Angular строится динамичный, интерактивный пользовательский интерфейс с усовершенствованным пользовательским взаимодействием.

1. Описание моделей, компонентов и служб

Модели:

  • Модель заказа. Это данные заказа на фронтенде с полями вроде orderId, orderCode, orderDate, totalPrice, quantity, customer и item. Такой моделью в различных компонентах обрабатываются и отображаются данные заказа.

Новая модель в Angular создается так:

ng generate model <model-name>

Компоненты:

  • Компонент списка заказов. Им отображается постраничный список заказов, где пользователи просматривают, ищут и фильтруют заказы. Чтобы извлекать и отображать заказы, этот компонент взаимодействует с API бэкенда.
  • Компонент данных заказа. Им дается детализированное представление о конкретном заказе со всеми соответствующими данными: код заказа, дата, общая цена, сведения о клиенте и товаре.
  • Компонент формы заказа. В нем добавляются и редактируются заказы, имеется форма для ввода деталей заказа. Для сохранения изменений этот компонент взаимодействует с бэкендом.

Новый компонент в Angular создается так:

ng generate component <component-name>

Службы:

  • Служба заказов. Ею управляются взаимодействия с API для операций, связанных с заказами. Имеются методы для создания, изменения, удаления и извлечения заказов. Эта служба внедряется в компоненты для обработки операций с данными.

Новая служба в Angular создается так:

ng generate service <service-name>

2. Реализация кода

Модель заказа order.model.ts:

export interface Order {
orderId: number;
orderCode: string;
orderDate: Date;
totalPrice: number;
quantity: number;
customer: Customer;
item: Item;
}

Служба заказов order.service.ts:

@Injectable({
providedIn: 'root'
})
export class OrderService {
private baseUrl = 'http://localhost:8080/orders';
constructor(private http: HttpClient) { }
getAllOrders(page: number, size: number): Observable<any> {
let params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString());
return this.http.get<any>(this.baseUrl, { params }).pipe(
map(response => {
return {
orders: response.content,
totalElements: response.totalElements
};
})
);
}
getOrderById(orderId: number): Observable<Order> {
return this.http.get<Order>(`${this.baseUrl}/${orderId}`).pipe(
catchError(error => {
console.error('Error fetching order by ID:', error);
return throwError(error);
})
);
}
addOrder(order: Order): Observable<Order> {
return this.http.post<Order>(`${this.baseUrl}`, order);
}
updateOrder(orderId: number, order: Order): Observable<Order> {
return this.http.put<Order>(`${this.baseUrl}/${orderId}`, order);
}
deleteOrder(orderId: number): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/${orderId}`).pipe(
catchError(error => {
console.error('Error deleting order:', error);
return throwError(error);
})
);
}
downloadOrderReport(orderId: number): Observable<Blob> {
const url = `${this.baseUrl}/report/${orderId}`;
return this.http.get(url, { responseType: 'blob' }).pipe(
catchError(error => {
console.error('Error downloading order report:', error);
return throwError(error);
})
);
}
}

Компонент списка заказов order-list.component.ts:

@Component({
selector: 'app-order-list',
standalone: true,
imports: [CommonModule, MatTableModule, MatButtonModule, MatIconModule, MatProgressSpinnerModule, MatCardModule, MatPaginatorModule, MatSortModule, MatFormFieldModule, MatInputModule],
templateUrl: './order-list.component.html',
styleUrl: './order-list.component.css'
})
export class OrderListComponent implements OnInit {
orders: Order[] = [];
displayedOrders: any [] = [];
isLoading: boolean = false;
error: string | null = null;
displayedColumns = ['id', 'code', 'customer', 'date', 'item', 'quantity', 'totalPrice', 'actions'];
dataSource = new MatTableDataSource<Order>();
totalOrders = 0;
pageSize = 5;
currentPage = 0;
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
constructor(private orderService: OrderService, public dialog: MatDialog, private snackBar: MatSnackBar) {}
ngOnInit(): void {
this.getOrders();
}
getOrders(): void {
this.isLoading = true;
this.orderService.getAllOrders(this.currentPage, this.pageSize).subscribe(
(data: any) => {
const orders = data.orders;
const simplifiedOrders = orders.map((order: Order) => ({
orderId: order.orderId,
orderCode: order.orderCode,
customer: order.customer.customerName,
orderDate: order.orderDate,
item: order.item.itemsName,
quantity: order.quantity,
totalPrice: order.totalPrice
}));
this.dataSource.data = simplifiedOrders;
this.totalOrders = data.totalElements;
this.isLoading = false;
},
error => {
console.error('Error loading orders', error);
this.error = 'Error loading orders';
this.isLoading = false;
}
);
}
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value;
this.dataSource.filter = filterValue.trim().toLowerCase();
}
onPageChange(event: any): void {
this.currentPage = event.pageIndex;
this.pageSize = event.pageSize;
this.getOrders();
}
viewOrder(order: Order) {
this.dialog.open(OrderDetailComponent, {
data: order,
width: '600px'
});
openAddOrderDialog(): void {
const dialogRef = this.dialog.open(OrderFormComponent, {
width: '500px',
data: { action: 'Add' }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.getOrders();
}
});
}
openEditOrderDialog(order: Order): void {
const dialogRef = this.dialog.open(OrderFormComponent, {
width: '500px',
data: { action: 'Edit', order: order }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.getOrders();
}
});
}
deleteOrder(order: Order) {
if (confirm(`Are you sure you want to delete this order: ${order.orderCode}?`)) {
this.orderService.deleteOrder(order.orderId).subscribe({
next: () => {
this.snackBar.open('Order successfully deleted', 'Close', { duration: 3000 });
this.getOrders();
},
error: (err) => {
this.snackBar.open('Order cannot be deleted: ' + err.message, 'Close', { duration: 3000 });
console.error('Error deleting order:', err);
}
});
}
}
}

Шаблон списка заказов order-list.component.html:

<div *ngIf="isLoading">
<mat-progress-spinner mode="indeterminate" class="loading-container"></mat-progress-spinner>
</div>

<div *ngIf="error" class="error-message">
Error loading orders: {{ error }}
</div>

<div *ngIf="dataSource.data.length > 0 && !isLoading && !error">
<mat-card class="order-card">
<mat-card-header>
<mat-card-title>Order List</mat-card-title>
<div class="button-container">
<button mat-raised-button class="custom-add-order-btn" (click)="openAddOrderDialog()">Add Order</button>
</div>
</mat-card-header>
<mat-card-content>
<mat-form-field appearance="fill">
<mat-label>Filter</mat-label>
<input matInput (keyup)="applyFilter($event)" placeholder="Ex. Order Code">
</mat-form-field>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8 order-table">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef> ID </th>
<td mat-cell *matCellDef="let element"> {{element.orderId}} </td>
</ng-container>

<ng-container matColumnDef="code">
<th mat-header-cell *matHeaderCellDef> Code </th>
<td mat-cell *matCellDef="let element"> {{element.orderCode}} </td>
</ng-container>
<ng-container matColumnDef="customer">
<th mat-header-cell *matHeaderCellDef> Customer Name </th>
<td mat-cell *matCellDef="let element"> {{element.customer}} </td>
</ng-container>

<ng-container matColumnDef="date">
<th mat-header-cell *matHeaderCellDef> Date </th>
<td mat-cell *matCellDef="let element"> {{element.orderDate | date: 'yyyy-MM-dd'}} </td>
</ng-container>
<ng-container matColumnDef="item">
<th mat-header-cell *matHeaderCellDef> Item Name </th>
<td mat-cell *matCellDef="let element"> {{element.item}} </td>
</ng-container>

<ng-container matColumnDef="quantity">
<th mat-header-cell *matHeaderCellDef> Quantity </th>
<td mat-cell *matCellDef="let element"> {{element.quantity}} </td>
</ng-container>
<ng-container matColumnDef="totalPrice">
<th mat-header-cell *matHeaderCellDef> Total Price </th>
<td mat-cell *matCellDef="let element"> {{element.totalPrice | currency:'IDR':'symbol':'1.0-0'}} </td>
</ng-container>

<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> Actions </th>
<td mat-cell *matCellDef="let element">
<button mat-icon-button class="button-actions" (click)="openEditOrderDialog(element)">
<mat-icon>edit</mat-icon >
</button>
<button mat-icon-button class="button-actions" (click)="deleteOrder(element)">
<mat-icon>delete</mat-icon>
</button>
<button mat-icon-button class="button-actions" (click)="viewOrder(element)">
<mat-icon>visibility</mat-icon>
</button>
</td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="totalOrders"
[pageSize]="pageSize"
[pageIndex]="currentPage"
[pageSizeOptions]="[5, 10, 25, 100]"
(page)="onPageChange($event)">
</mat-paginator>
</mat-card-content>
</mat-card>
</div>

<div *ngIf="dataSource.data.length === 0 && !isLoading && !error">
No orders found.
</div>

Компонент данных заказа order-detail.component.ts:

@Component({
selector: 'app-order-detail',
standalone: true,
imports: [CommonModule, MatCardModule],
templateUrl: './order-detail.component.html',
styleUrl: './order-detail.component.css'
})
export class OrderDetailComponent {
constructor(
private dialogRef: MatDialogRef<OrderDetailComponent>,
@Inject(MAT_DIALOG_DATA) public data: Order,
private orderService: OrderService
) { }
downloadReport(): void {
this.orderService.downloadOrderReport(this.data.orderId).subscribe(
(blob: Blob) => {
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `order-report-${this.data.orderCode}.pdf`;
link.click();
window.URL.revokeObjectURL(url);
},
error => {
console.error('Error downloading report:', error);
}
);
}
close(): void {
this.dialogRef.close();
}
}

Шаблон данных заказа order-detail.component.html:

<mat-card>
<mat-card-header>
<mat-card-title>Order Details</mat-card-title>
</mat-card-header>
<mat-card-content>
<p><strong>Code:</strong> {{ data.orderCode }}</p>
<p><strong>Customer Name:</strong> {{data.customer}}</p>
<p><strong>Date:</strong> {{ data.orderDate | date:'yyyy-MM-dd' }}</p>
<p><strong>Item Name:</strong> {{data.item}}</p>
<p><strong>Quantity:</strong> {{ data.quantity }}</p>
<p><strong>Total Price:</strong> {{ data.totalPrice | currency:'IDR':'symbol':'1.0-0' }}</p>
</mat-card-content>
<mat-card-actions>
<button mat-button class="custom-button" (click)="downloadReport()">Download Report</button>
<button mat-button class="custom-button" (click)="close()">Close</button>
</mat-card-actions>
</mat-card>

Вся реализация и дополнительные детали  —  в репозитории GitHub.

2. Запуск приложения

Приложение запускается из каталога проекта:

ng serve

Этой командой запускается сервер разработки Angular, и приложение компилируется. Запущенное приложение просматриваем в браузере, вводя http://localhost:4200.

Посмотрим на приложение для интернет-магазина в действии:

Экран со списком клиентов
Экран добавления клиента
Экран данных клиента
Экран редактирования клиента
Экран списка товаров
Экран добавления товаров
Экран информации о товаре
Экран редактирования товара
Экран списка заказов
Экран добавления заказа
Экран информации о заказе

Вот так, шаг за шагом, создается полнофункциональный фронтенд, который легко взаимодействует с бэкендом для удобного и эффективного управления заказами в интернет-магазине.

Подводя итоги

При разработке приложения для интернет-магазина были интегрированы фронтенд- и бэкенд-технологии создания динамичной, адаптивной, полнофункциональной платформы электронной коммерции. Использовав Angular для фронтенда, мы обеспечили беспроблемное интерактивное пользовательское взаимодействие, а со Spring Boot на бэкенде получили прочную основу для управления бизнес-логикой, сохранения данных и файловой обработки.

Пишите код со вкусом, и результат порадует глаз.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Fiora Berliana Putri: Developing a Simple Online Shop with Spring Boot, Angular, MySQL, and Jasper Reports

Предыдущая статьяJava: оператор try-with-resources
Следующая статья5 концепций JavaScript, которые должен знать каждый разработчик