From 0420194541e885f8168d6f345deef398d96dfe71 Mon Sep 17 00:00:00 2001
From: ismailmasseran
Date: Thu, 30 Jul 2026 16:22:51 +0800
Subject: [PATCH] Bugfix/testing (#4)
Co-authored-by: ISMAIL MASSERAN
Co-authored-by: ISMAIL MASSERAN
Reviewed-on: https://git.koppkb.com/KoPKB/QMS/pulls/4
---
TODO.md | 2 +
be/docker-compose.yml | 31 +---
.../implementation/DefaultAdminService.java | 15 +-
.../bbqms/ws/controllers/AdminController.java | 22 +--
.../controllers/AdvertisementController.java | 32 ++---
.../components/CustomerTicketFlow.tsx | 55 ++++++-
fe/web/customer-app/package.json | 2 +-
.../BranchDisplayPage/BranchDisplayPage.css | 135 +++++++++++-------
.../BranchDisplayPage/BranchDisplayPage.jsx | 2 +-
9 files changed, 179 insertions(+), 117 deletions(-)
create mode 100644 TODO.md
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000..026d0cb
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,2 @@
+[x] create qr and teller account
+[x] increase size of gold display
\ No newline at end of file
diff --git a/be/docker-compose.yml b/be/docker-compose.yml
index d56a505..c139798 100644
--- a/be/docker-compose.yml
+++ b/be/docker-compose.yml
@@ -4,7 +4,7 @@ services:
container_name: qms-mysql
restart: unless-stopped
ports:
- - "${MYSQL_HOST_PORT:-3307}:3306"
+ - "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-password}
MYSQL_DATABASE: ${MYSQL_DATABASE:-qms}
@@ -27,37 +27,8 @@ services:
networks:
- qms-net
- backend:
- image: ${BACKEND_IMAGE:-qms-backend}:${IMAGE_TAG:-local}
- build:
- context: .
- dockerfile: Dockerfile
- container_name: qms-backend
- restart: unless-stopped
- ports:
- # Host port can be changed via BACKEND_HOST_PORT in .env (default 8080)
- - "${BACKEND_HOST_PORT:-8080}:8080"
- environment:
- SERVER_PORT: 8080
- # Use Docker service name "mysql", not localhost
- SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/${MYSQL_DATABASE:-qms}?allowPublicKeyRetrieval=true&useSSL=false
- SPRING_DATASOURCE_USERNAME: root
- SPRING_DATASOURCE_PASSWORD: ${MYSQL_ROOT_PASSWORD:-password}
- JWT_SECRET_KEY: ${JWT_SECRET_KEY:-a68uiaDQ0V3iLjF4DqMuS13GAVwkut55dlFbGCLyXTF}
- ADS_UPLOAD_DIR: /app/uploads/ads
- NOTIFICATIONS_MOCK: ${NOTIFICATIONS_MOCK:-true}
- GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-dummy-google-client-id}
- volumes:
- - qms-uploads:/app/uploads
- depends_on:
- mysql:
- condition: service_healthy
- networks:
- - qms-net
-
volumes:
qms-mysql-data:
- qms-uploads:
networks:
qms-net:
diff --git a/be/src/main/java/ba/unsa/etf/si/bbqms/admin_service/implementation/DefaultAdminService.java b/be/src/main/java/ba/unsa/etf/si/bbqms/admin_service/implementation/DefaultAdminService.java
index c10f51d..48d4d99 100644
--- a/be/src/main/java/ba/unsa/etf/si/bbqms/admin_service/implementation/DefaultAdminService.java
+++ b/be/src/main/java/ba/unsa/etf/si/bbqms/admin_service/implementation/DefaultAdminService.java
@@ -33,13 +33,14 @@ public class DefaultAdminService implements AdminService {
private final RoleService roleService;
private final PasswordEncoder passwordEncoder;
private final AuthService authService;
+
public DefaultAdminService(final UserRepository userRepository,
- final UserService userService,
- final TwoFactorService twoFactorService,
- final TenantService tenantService,
- final RoleService roleService,
- final PasswordEncoder passwordEncoder,
- final AuthService authService) {
+ final UserService userService,
+ final TwoFactorService twoFactorService,
+ final TenantService tenantService,
+ final RoleService roleService,
+ final PasswordEncoder passwordEncoder,
+ final AuthService authService) {
this.userRepository = userRepository;
this.userService = userService;
this.twoFactorService = twoFactorService;
@@ -50,7 +51,7 @@ public class DefaultAdminService implements AdminService {
}
@Override
- public List findUsersByCode(final String tenantCode, final String roleName){
+ public List findUsersByCode(final String tenantCode, final String roleName) {
final Set roleNameSet = Set.of(RoleName.valueOf(roleName));
return this.userRepository.findAllByTenant_CodeAndRoles_NameIn(tenantCode, roleNameSet);
}
diff --git a/be/src/main/java/ba/unsa/etf/si/bbqms/ws/controllers/AdminController.java b/be/src/main/java/ba/unsa/etf/si/bbqms/ws/controllers/AdminController.java
index 2c77511..d3303c8 100644
--- a/be/src/main/java/ba/unsa/etf/si/bbqms/ws/controllers/AdminController.java
+++ b/be/src/main/java/ba/unsa/etf/si/bbqms/ws/controllers/AdminController.java
@@ -14,6 +14,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
+import java.util.List;
import java.util.stream.Collectors;
@RestController
@@ -30,7 +31,8 @@ public class AdminController {
@PostMapping("/{code}")
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN', 'ROLE_STAFF_ADMIN')")
- public ResponseEntity getUsers(@RequestBody RoleRequest request, @PathVariable(name = "code") final String tenantCode) {
+ public ResponseEntity> getUsers(@RequestBody RoleRequest request,
+ @PathVariable(name = "code") final String tenantCode) {
RoleName roleName;
try {
roleName = RoleName.valueOf(request.roleName);
@@ -39,7 +41,7 @@ public class AdminController {
return ResponseEntity.badRequest().build();
}
- if(this.authService.canOnlyCRUDUser(roleName)){
+ if (this.authService.canOnlyCRUDUser(roleName)) {
logger.warn("Only super admin can read admins");
return ResponseEntity.badRequest().build();
}
@@ -52,8 +54,7 @@ public class AdminController {
return ResponseEntity.ok().body(
this.adminService.findUsersByCode(tenantCode, request.roleName).stream()
.map(UserDto::fromEntity)
- .collect(Collectors.toList())
- );
+ .collect(Collectors.toList()));
} catch (EntityNotFoundException e) {
return ResponseEntity.badRequest().build();
}
@@ -61,7 +62,8 @@ public class AdminController {
@PostMapping("/{code}/user")
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN', 'ROLE_STAFF_ADMIN')")
- public ResponseEntity addUser(@RequestBody final AdminRequest request, @PathVariable(name = "code") final String tenantCode) throws AuthException {
+ public ResponseEntity addUser(@RequestBody final AdminRequest request,
+ @PathVariable(name = "code") final String tenantCode) throws AuthException {
RoleName roleName;
try {
roleName = RoleName.valueOf(request.roleName);
@@ -70,7 +72,7 @@ public class AdminController {
return ResponseEntity.badRequest().build();
}
- if(this.authService.canOnlyCRUDUser(roleName)){
+ if (this.authService.canOnlyCRUDUser(roleName)) {
logger.warn("Only super admin can add admin");
return ResponseEntity.badRequest().build();
}
@@ -95,7 +97,8 @@ public class AdminController {
@PutMapping("/{code}/user/{userId}")
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN', 'ROLE_STAFF_ADMIN')")
- public ResponseEntity updateAdmin(@RequestBody final UserDto request, @PathVariable(name = "code") final String tenantCode, @PathVariable(name = "userId") final long adminId) {
+ public ResponseEntity updateAdmin(@RequestBody final UserDto request,
+ @PathVariable(name = "code") final String tenantCode, @PathVariable(name = "userId") final long adminId) {
if (!this.authService.canChangeTenant(tenantCode)) {
logger.warn("Admin does not belong to the specified tenant");
return ResponseEntity.badRequest().build();
@@ -112,7 +115,8 @@ public class AdminController {
@DeleteMapping("/{code}/user/{userId}")
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN', 'ROLE_STAFF_ADMIN')")
- public ResponseEntity removeAdmin(@PathVariable(name = "code") final String tenantCode, @PathVariable(name = "userId") final long adminId) {
+ public ResponseEntity removeAdmin(@PathVariable(name = "code") final String tenantCode,
+ @PathVariable(name = "userId") final long adminId) {
if (!this.authService.canChangeTenant(tenantCode)) {
logger.warn("Admin does not belong to the specified tenant");
return ResponseEntity.badRequest().build();
@@ -130,6 +134,6 @@ public class AdminController {
public record AdminRequest(String email, String password, String roleName) {
}
- public record RoleRequest(String roleName){
+ public record RoleRequest(String roleName) {
}
}
diff --git a/be/src/main/java/ba/unsa/etf/si/bbqms/ws/controllers/AdvertisementController.java b/be/src/main/java/ba/unsa/etf/si/bbqms/ws/controllers/AdvertisementController.java
index 4d0afbe..b03a7ec 100644
--- a/be/src/main/java/ba/unsa/etf/si/bbqms/ws/controllers/AdvertisementController.java
+++ b/be/src/main/java/ba/unsa/etf/si/bbqms/ws/controllers/AdvertisementController.java
@@ -30,13 +30,13 @@ public class AdvertisementController {
private final AuthService authService;
public AdvertisementController(final AdvertisementService advertisementService,
- final AuthService authService) {
+ final AuthService authService) {
this.advertisementService = advertisementService;
this.authService = authService;
}
@GetMapping("/media/{adId}")
- public ResponseEntity streamMedia(@PathVariable final long adId) {
+ public ResponseEntity streamMedia(@PathVariable final long adId) {
try {
final Advertisement advertisement = this.advertisementService.findById(adId);
final Resource resource = this.advertisementService.loadMedia(adId);
@@ -64,11 +64,11 @@ public class AdvertisementController {
@PostMapping("/{tenantCode}")
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN')")
public ResponseEntity createAdvertisement(@PathVariable final String tenantCode,
- @RequestParam("file") final MultipartFile file,
- @RequestParam(value = "title", required = false) final String title,
- @RequestParam(value = "durationSeconds", required = false) final Integer durationSeconds,
- @RequestParam(value = "sortOrder", required = false) final Integer sortOrder,
- @RequestParam(value = "active", required = false) final Boolean active) {
+ @RequestParam("file") final MultipartFile file,
+ @RequestParam(value = "title", required = false) final String title,
+ @RequestParam(value = "durationSeconds", required = false) final Integer durationSeconds,
+ @RequestParam(value = "sortOrder", required = false) final Integer sortOrder,
+ @RequestParam(value = "active", required = false) final Boolean active) {
if (!this.authService.canChangeTenant(tenantCode)) {
return ResponseEntity.badRequest().build();
}
@@ -80,8 +80,7 @@ public class AdvertisementController {
title,
durationSeconds,
sortOrder,
- active
- );
+ active);
return ResponseEntity.ok().body(AdvertisementDto.fromEntity(created));
} catch (final Exception exception) {
return ResponseEntity.badRequest().body(new SimpleMessageDto(exception.getMessage()));
@@ -108,8 +107,8 @@ public class AdvertisementController {
@PutMapping("/{tenantCode}/{adId}")
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN')")
public ResponseEntity updateAdvertisement(@PathVariable final String tenantCode,
- @PathVariable final long adId,
- @RequestBody final AdvertisementUpdateRequest request) {
+ @PathVariable final long adId,
+ @RequestBody final AdvertisementUpdateRequest request) {
if (!this.authService.canChangeTenant(tenantCode)) {
return ResponseEntity.badRequest().build();
}
@@ -121,8 +120,7 @@ public class AdvertisementController {
request.title(),
request.durationSeconds(),
request.sortOrder(),
- request.active()
- );
+ request.active());
return ResponseEntity.ok().body(AdvertisementDto.fromEntity(updated));
} catch (final Exception exception) {
return ResponseEntity.badRequest().body(new SimpleMessageDto(exception.getMessage()));
@@ -132,7 +130,7 @@ public class AdvertisementController {
@DeleteMapping("/{tenantCode}/{adId}")
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN')")
public ResponseEntity deleteAdvertisement(@PathVariable final String tenantCode,
- @PathVariable final long adId) {
+ @PathVariable final long adId) {
if (!this.authService.canChangeTenant(tenantCode)) {
return ResponseEntity.badRequest().build();
}
@@ -146,8 +144,8 @@ public class AdvertisementController {
}
public record AdvertisementUpdateRequest(String title,
- Integer durationSeconds,
- Integer sortOrder,
- Boolean active) {
+ Integer durationSeconds,
+ Integer sortOrder,
+ Boolean active) {
}
}
diff --git a/fe/web/customer-app/components/CustomerTicketFlow.tsx b/fe/web/customer-app/components/CustomerTicketFlow.tsx
index 7d13cc4..69eda35 100644
--- a/fe/web/customer-app/components/CustomerTicketFlow.tsx
+++ b/fe/web/customer-app/components/CustomerTicketFlow.tsx
@@ -22,6 +22,9 @@ type Props = {
initialBranchId?: number;
};
+/** Prevents spam of "Dapatkan tiket lain" right after getting a number. */
+const ANOTHER_TICKET_COOLDOWN_MS = 10_000;
+
function pickLatestTicket(
tickets: Ticket[],
branchId?: number
@@ -61,8 +64,27 @@ export default function CustomerTicketFlow({
const [ticket, setTicket] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
+ const [cooldownLeftMs, setCooldownLeftMs] = useState(0);
const bootstrapped = useRef(false);
+ useEffect(() => {
+ if (step !== "ticket" || !ticket) {
+ setCooldownLeftMs(0);
+ return;
+ }
+
+ const tick = () => {
+ const elapsed = Date.now() - new Date(ticket.createdAt).getTime();
+ setCooldownLeftMs(
+ Math.max(0, ANOTHER_TICKET_COOLDOWN_MS - elapsed)
+ );
+ };
+
+ tick();
+ const id = window.setInterval(tick, 250);
+ return () => window.clearInterval(id);
+ }, [step, ticket]);
+
useEffect(() => {
if (bootstrapped.current) return;
bootstrapped.current = true;
@@ -281,6 +303,9 @@ export default function CustomerTicketFlow({
? "Pilih perkhidmatan untuk mendapatkan nombor."
: "Imbas QR code di branch anda, atau masukkan kod syarikat di bawah.";
+ const canGetAnotherTicket = cooldownLeftMs <= 0;
+ const cooldownSeconds = Math.ceil(cooldownLeftMs / 1000);
+
return (
- {ticket?.service?.name ?? 'Waiting for next'}
+ {ticket?.service?.name ?? 'Menunggu nombor giliran berikutnya'}
);