Skip to content

Commit 348e05b

Browse files
committed
Merge branch 'dev' into dev2
2 parents a783735 + 08fd7ff commit 348e05b

File tree

92 files changed

+7848
-4279
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

92 files changed

+7848
-4279
lines changed

.github/workflows/ci.yml

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main, dev, shift]
6+
pull_request:
7+
branches: [main, dev]
8+
9+
# Cancel redundant runs when a new push is made to the same branch
10+
concurrency:
11+
group: ${{ github.workflow }}-${{ github.ref }}
12+
cancel-in-progress: true
13+
14+
jobs:
15+
frontend:
16+
name: Frontend (Lint & Build)
17+
runs-on: ubuntu-latest
18+
19+
defaults:
20+
run:
21+
working-directory: frontend
22+
23+
steps:
24+
- name: Checkout code
25+
uses: actions/checkout@v4
26+
27+
- name: Setup Node.js 20
28+
uses: actions/setup-node@v4
29+
with:
30+
node-version: "20"
31+
cache: "npm"
32+
cache-dependency-path: frontend/package-lock.json
33+
34+
- name: Install dependencies
35+
run: npm ci
36+
37+
- name: Lint
38+
run: npm run lint:ci
39+
40+
- name: Build
41+
run: npm run build
42+
43+
- name: Upload build artifact
44+
uses: actions/upload-artifact@v4
45+
with:
46+
name: frontend-dist
47+
path: frontend/dist
48+
retention-days: 7
49+
50+
backend:
51+
name: Backend (Build & Test)
52+
runs-on: ubuntu-latest
53+
54+
defaults:
55+
run:
56+
working-directory: backend
57+
58+
services:
59+
mysql:
60+
image: mysql:8.0
61+
env:
62+
MYSQL_ROOT_PASSWORD: test1234
63+
MYSQL_DATABASE: smalltrend_test
64+
ports:
65+
- 3306:3306
66+
options: >-
67+
--health-cmd="mysqladmin ping -h localhost"
68+
--health-interval=10s
69+
--health-timeout=5s
70+
--health-retries=5
71+
72+
env:
73+
DB_URL: jdbc:mysql://localhost:3306/smalltrend_test?allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC&createDatabaseIfNotExist=true
74+
DB_USERNAME: root
75+
DB_PASSWORD: test1234
76+
JWT_SECRET: 5367566B59703373367639792F423F4528482B4D6251655468576D5A71347437
77+
CLOUDINARY_CLOUD_NAME: ci_dummy
78+
CLOUDINARY_API_KEY: "000000000000000"
79+
CLOUDINARY_API_SECRET: ci_dummy_secret
80+
SPRING_SQL_INIT_MODE: never
81+
SPRING_JPA_DDL_AUTO: create-drop
82+
83+
steps:
84+
- name: Checkout code
85+
uses: actions/checkout@v4
86+
87+
- name: Setup Java 17
88+
uses: actions/setup-java@v4
89+
with:
90+
java-version: "17"
91+
distribution: "temurin"
92+
cache: "maven"
93+
94+
- name: Make Maven wrapper executable
95+
run: chmod +x mvnw
96+
97+
- name: Compile (skip tests)
98+
run: ./mvnw -B compile -DskipTests
99+
100+
- name: Run tests + JaCoCo coverage
101+
run: ./mvnw -B verify
102+
103+
- name: Upload JaCoCo coverage report
104+
if: always()
105+
uses: actions/upload-artifact@v4
106+
with:
107+
name: jacoco-report
108+
path: backend/target/site/jacoco
109+
retention-days: 7
110+
111+
- name: Upload JUnit test results
112+
if: always()
113+
uses: actions/upload-artifact@v4
114+
with:
115+
name: junit-test-results
116+
path: backend/target/surefire-reports
117+
retention-days: 7

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22

33
Hệ thống POS (Point of Sale) hiện đại giúp quản lý toàn diện các hoạt động của cửa hàng tạp hóa/siêu thị mini, từ bán hàng, quản lý kho, nhân sự, khách hàng đến báo cáo thống kê.
44

5-
---
5+
- **CI Status**: [![CI Workflow](https://github.com/touchmegit1/SmallTrend/actions/workflows/ci.yml/badge.svg?branch=dev)](https://github.com/touchmegit1/SmallTrend/actions/workflows/ci.yml?query=branch%3Adev)
66

7-
---
87

98
## 🚀 Quick Start - Chạy Dự Án Nhanh Chóng
109

@@ -28,6 +27,12 @@ Hệ thống POS (Point of Sale) hiện đại giúp quản lý toàn diện cá
2827

2928
### 📦 Setup Backend (3 bước đơn giản)
3029

30+
---
31+
32+
## 📋 CI/CD Pipeline
33+
34+
Dự án sử dụng **GitHub Actions** để tự động test & build. Xem chi tiết tại: [CI_CD_SETUP.md](./CI_CD_SETUP.md)
35+
3136
#### Bước 1: Tạo Database
3237
```sql
3338
CREATE DATABASE smalltrend CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

backend/Dump.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import java.sql.*;
2+
public class Dump {
3+
public static void main(String[] args) throws Exception {
4+
Connection c = DriverManager.getConnection("jdbc:mysql://localhost:3306/smalltrend", "root", "");
5+
ResultSet rs = c.createStatement().executeQuery("SELECT phone FROM customers");
6+
while(rs.next()) System.out.println("PHONE_IN_DB: " + rs.getString(1));
7+
}
8+
}

backend/src/main/java/com/smalltrend/controller/CRM/AdvertisementController.java

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,60 +5,83 @@
55
import com.smalltrend.service.CRM.AdvertisementService;
66
import lombok.RequiredArgsConstructor;
77
import org.springframework.http.ResponseEntity;
8+
import org.springframework.security.access.prepost.PreAuthorize;
89
import org.springframework.web.bind.annotation.*;
910

1011
import java.util.List;
1112
import java.util.Map;
1213

1314
@RestController
14-
@RequestMapping("/api/crm/ads")
15+
@RequestMapping("/api/advertisements")
1516
@RequiredArgsConstructor
1617
public class AdvertisementController {
1718

18-
private final AdvertisementService adService;
19+
private final AdvertisementService advertisementService;
1920

20-
/** GET /api/crm/ads — toàn bộ danh sách (admin) */
21-
@GetMapping
22-
public ResponseEntity<List<AdvertisementResponse>> getAll() {
23-
return ResponseEntity.ok(adService.getAll());
21+
/**
22+
* Lấy quảng cáo đang active (for homepage)
23+
* Không cần authentication
24+
*/
25+
@GetMapping("/active")
26+
public ResponseEntity<Map<String, AdvertisementResponse>> getActiveAdvertisements() {
27+
return ResponseEntity.ok(advertisementService.getActiveAds());
2428
}
2529

26-
/** GET /api/crm/ads/active — 2 quảng cáo đang active (LEFT + RIGHT) */
27-
@GetMapping("/active")
28-
public ResponseEntity<Map<String, AdvertisementResponse>> getActive() {
29-
return ResponseEntity.ok(adService.getActiveAds());
30+
/**
31+
* Lấy tất cả quảng cáo (admin only)
32+
*/
33+
@GetMapping
34+
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
35+
public ResponseEntity<List<AdvertisementResponse>> getAllAdvertisements() {
36+
return ResponseEntity.ok(advertisementService.getAll());
3037
}
3138

32-
/** GET /api/crm/ads/stats — báo cáo thống kê hợp đồng */
39+
/**
40+
* Lấy stats của quảng cáo
41+
*/
3342
@GetMapping("/stats")
34-
public ResponseEntity<Map<String, Object>> getStats() {
35-
return ResponseEntity.ok(adService.getStats());
43+
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
44+
public ResponseEntity<Map<String, Object>> getAdvertisementStats() {
45+
return ResponseEntity.ok(advertisementService.getStats());
3646
}
3747

38-
/** POST /api/crm/ads — tạo mới */
39-
@PostMapping
40-
public ResponseEntity<AdvertisementResponse> create(@RequestBody SaveAdvertisementRequest req) {
41-
return ResponseEntity.ok(adService.save(null, req));
48+
/**
49+
* Tạo hoặc cập nhật quảng cáo
50+
*/
51+
@PostMapping("/{id}")
52+
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
53+
public ResponseEntity<AdvertisementResponse> saveAdvertisement(
54+
@PathVariable(required = false) Long id,
55+
@RequestBody SaveAdvertisementRequest request) {
56+
return ResponseEntity.ok(advertisementService.save(id, request));
4257
}
4358

44-
/** PUT /api/crm/ads/{id} — cập nhật */
45-
@PutMapping("/{id}")
46-
public ResponseEntity<AdvertisementResponse> update(
47-
@PathVariable Long id,
48-
@RequestBody SaveAdvertisementRequest req) {
49-
return ResponseEntity.ok(adService.save(id, req));
59+
/**
60+
* Tạo quảng cáo mới
61+
*/
62+
@PostMapping
63+
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
64+
public ResponseEntity<AdvertisementResponse> createAdvertisement(
65+
@RequestBody SaveAdvertisementRequest request) {
66+
return ResponseEntity.ok(advertisementService.save(null, request));
5067
}
5168

52-
/** PATCH /api/crm/ads/{id}/toggle — bật/tắt hiển thị */
53-
@PatchMapping("/{id}/toggle")
54-
public ResponseEntity<AdvertisementResponse> toggle(@PathVariable Long id) {
55-
return ResponseEntity.ok(adService.toggleActive(id));
69+
/**
70+
* Bật / tắt quảng cáo
71+
*/
72+
@PutMapping("/{id}/toggle")
73+
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
74+
public ResponseEntity<AdvertisementResponse> toggleActiveAdvertisement(@PathVariable Long id) {
75+
return ResponseEntity.ok(advertisementService.toggleActive(id));
5676
}
5777

58-
/** DELETE /api/crm/ads/{id} */
78+
/**
79+
* Xoá quảng cáo
80+
*/
5981
@DeleteMapping("/{id}")
60-
public ResponseEntity<Void> delete(@PathVariable Long id) {
61-
adService.delete(id);
82+
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
83+
public ResponseEntity<Void> deleteAdvertisement(@PathVariable Long id) {
84+
advertisementService.delete(id);
6285
return ResponseEntity.noContent().build();
6386
}
6487
}

backend/src/main/java/com/smalltrend/controller/CRM/CouponController.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,20 @@ public ResponseEntity<List<CouponResponse>> getAllCoupons() {
2525
return ResponseEntity.ok(couponService.getAllCoupons());
2626
}
2727

28+
@GetMapping("/validate")
29+
public ResponseEntity<?> validateCoupon(
30+
@RequestParam String code,
31+
@RequestParam(required = false) Integer customerId) {
32+
try {
33+
CouponResponse coupon = couponService.validateCoupon(code, customerId);
34+
return ResponseEntity.ok(coupon);
35+
} catch (Exception e) {
36+
Map<String, String> error = new HashMap<>();
37+
error.put("message", e.getMessage());
38+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
39+
}
40+
}
41+
2842
@PostMapping
2943
public ResponseEntity<?> createCoupon(@RequestBody CreateCouponRequest request) {
3044
try {

backend/src/main/java/com/smalltrend/controller/CRM/CustomerController.java

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,34 +17,34 @@
1717
@RequiredArgsConstructor
1818
@CrossOrigin(origins = { "http://localhost:5173", "http://localhost:5174", "http://localhost:5175", "http://localhost:3000" })
1919
public class CustomerController {
20-
20+
2121
private final CustomerService customerService;
22-
22+
2323
@GetMapping("/customers")
2424
public ResponseEntity<List<CustomerResponse>> getAllCustomers() {
2525
List<CustomerResponse> customers = customerService.getAllCustomers();
2626
return ResponseEntity.ok(customers);
2727
}
2828

2929
@GetMapping("/customers/{id}")
30-
public ResponseEntity<CustomerResponse> getCustomerById(@PathVariable Integer id) {
30+
public ResponseEntity<CustomerResponse> getCustomerById(@PathVariable("id") Integer id) {
3131
CustomerResponse customer = customerService.getCustomerById(id);
3232
return ResponseEntity.ok(customer);
3333
}
3434

3535
@GetMapping("/customers/phone/{phone}")
36-
public ResponseEntity<?> getCustomerByPhone(@PathVariable String phone) {
36+
public ResponseEntity<?> getCustomerByPhone(@PathVariable("phone") String phone) {
3737
try {
3838
CustomerResponse customer = customerService.getCustomerByPhone(phone);
3939
return ResponseEntity.ok(customer);
4040
} catch (Exception e) {
4141
return ResponseEntity.status(HttpStatus.NOT_FOUND)
42-
.body(new com.smalltrend.dto.common.MessageResponse(e.getMessage()));
42+
.body(new com.smalltrend.dto.common.MessageResponse(e.getMessage()));
4343
}
4444
}
4545

4646
@GetMapping("/customers/search")
47-
public ResponseEntity<CustomerResponse> searchCustomerByPhone(@RequestParam String phone) {
47+
public ResponseEntity<CustomerResponse> searchCustomerByPhone(@RequestParam("phone") String phone) {
4848
CustomerResponse customer = customerService.getCustomerByPhone(phone);
4949
return ResponseEntity.ok(customer);
5050
}
@@ -57,19 +57,19 @@ public ResponseEntity<CustomerResponse> createCustomer(@RequestBody CreateCustom
5757

5858
@PutMapping("/customers/{id}")
5959
public ResponseEntity<CustomerResponse> updateCustomer(
60-
@PathVariable Integer id,
60+
@PathVariable("id") Integer id,
6161
@RequestBody UpdateCustomerRequest request) {
6262
CustomerResponse customer = customerService.updateCustomer(
63-
id,
64-
request.getName(),
65-
request.getPhone(),
66-
request.getLoyaltyPoints()
67-
);
63+
id,
64+
request.getName(),
65+
request.getPhone(),
66+
request.getLoyaltyPoints(),
67+
request.getSpentAmount());
6868
return ResponseEntity.ok(customer);
6969
}
7070

7171
@DeleteMapping("/customers/{id}")
72-
public ResponseEntity<Void> deleteCustomer(@PathVariable Integer id) {
72+
public ResponseEntity<Void> deleteCustomer(@PathVariable("id") Integer id) {
7373
customerService.deleteCustomer(id);
7474
return ResponseEntity.noContent().build();
7575
}

backend/src/main/java/com/smalltrend/controller/CRM/TicketController.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public ResponseEntity<List<TicketResponse>> getAllTickets() {
3232
}
3333

3434
@GetMapping("/tickets/{id}")
35-
public ResponseEntity<TicketResponse> getTicketById(@PathVariable Long id) {
35+
public ResponseEntity<TicketResponse> getTicketById(@PathVariable("id") Long id) {
3636
TicketResponse ticket = ticketService.getTicketById(id);
3737
return ResponseEntity.ok(ticket);
3838
}
@@ -51,14 +51,14 @@ public ResponseEntity<?> createTicket(@RequestBody CreateTicketRequest request)
5151

5252
@PutMapping("/tickets/{id}")
5353
public ResponseEntity<TicketResponse> updateTicket(
54-
@PathVariable Long id,
54+
@PathVariable("id") Long id,
5555
@RequestBody UpdateTicketRequest request) {
5656
TicketResponse ticket = ticketService.updateTicket(id, request);
5757
return ResponseEntity.ok(ticket);
5858
}
5959

6060
@DeleteMapping("/tickets/{id}")
61-
public ResponseEntity<Void> deleteTicket(@PathVariable Long id) {
61+
public ResponseEntity<Void> deleteTicket(@PathVariable("id") Long id) {
6262
ticketService.deleteTicket(id);
6363
return ResponseEntity.noContent().build();
6464
}
@@ -67,7 +67,7 @@ public ResponseEntity<Void> deleteTicket(@PathVariable Long id) {
6767
* Lookup users by role ID — for assigning tickets
6868
*/
6969
@GetMapping("/tickets/lookup/users-by-role/{roleId}")
70-
public ResponseEntity<List<Map<String, Object>>> getUsersByRole(@PathVariable Integer roleId) {
70+
public ResponseEntity<List<Map<String, Object>>> getUsersByRole(@PathVariable("roleId") Integer roleId) {
7171
List<User> users = userRepository.findByRoleId(roleId);
7272
List<Map<String, Object>> result = users.stream().map(u -> {
7373
Map<String, Object> map = new HashMap<>();
@@ -86,7 +86,7 @@ public ResponseEntity<List<Map<String, Object>>> getUsersByRole(@PathVariable In
8686
* Delegates to service to keep transaction open for lazy-loaded collections.
8787
*/
8888
@GetMapping("/tickets/lookup/variant-by-sku")
89-
public ResponseEntity<List<Map<String, Object>>> getVariantBySku(@RequestParam String sku) {
89+
public ResponseEntity<List<Map<String, Object>>> getVariantBySku(@RequestParam("sku") String sku) {
9090
List<Map<String, Object>> result = ticketService.lookupVariantBySku(sku);
9191
return ResponseEntity.ok(result);
9292
}

0 commit comments

Comments
 (0)