diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index affa413..b5340fa 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -2,8 +2,8 @@ name: Build Docker Image on: push: - branches: - - main + # branches: + # - main tags: - "v*" 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 (
@@ -423,7 +448,7 @@ export default function CustomerTicketFlow({ {step === "ticket" && ticket ? (

- Nombor tiket anda + Nombor giliran anda

{ticket.number} @@ -432,12 +457,36 @@ export default function CustomerTicketFlow({

{ticket.service.name}

{ticket.branch.name}

+ +
+

Anda sudah mempunyai nombor giliran.

+

+ Sila tunggu sehingga nombor anda dipanggil. +

+ {!canGetAnotherTicket ? ( +

+ Tiket lain boleh diambil dalam {cooldownSeconds}s +

+ ) : null} +
+
) : null} diff --git a/fe/web/customer-app/package.json b/fe/web/customer-app/package.json index 71e94ca..4a010de 100644 --- a/fe/web/customer-app/package.json +++ b/fe/web/customer-app/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev --webpack", "build": "next build", "start": "next start", "lint": "eslint" diff --git a/fe/web/teller-app/src/pages/BranchDisplayPage/AdCarousel.jsx b/fe/web/teller-app/src/pages/BranchDisplayPage/AdCarousel.jsx index be8ee24..64a7101 100644 --- a/fe/web/teller-app/src/pages/BranchDisplayPage/AdCarousel.jsx +++ b/fe/web/teller-app/src/pages/BranchDisplayPage/AdCarousel.jsx @@ -76,22 +76,41 @@ export default function AdCarousel({ tenantCode }) { const video = videoRef.current; if (!video || currentAd?.mediaType !== 'VIDEO') return undefined; + let removeUnmuteListener = () => {}; + const onEnded = () => goNext(); + const scheduleFallbackAdvance = () => { + clearTimer(); + const durationMs = Math.max(5, Number(currentAd.durationSeconds) || 15) * 1000; + timerRef.current = setTimeout(goNext, durationMs); + }; + video.addEventListener('ended', onEnded); - video.muted = true; + video.muted = false; video.playsInline = true; + const playPromise = video.play(); if (playPromise?.catch) { playPromise.catch(() => { - // Autoplay blocked — advance after durationSeconds fallback - clearTimer(); - const durationMs = Math.max(5, Number(currentAd.durationSeconds) || 15) * 1000; - timerRef.current = setTimeout(goNext, durationMs); + // Unmuted autoplay blocked — keep video playing muted, then unmute on gesture. + video.muted = true; + const mutedPlay = video.play(); + if (mutedPlay?.catch) { + mutedPlay.catch(scheduleFallbackAdvance); + } + + const unmute = () => { + video.muted = false; + video.play()?.catch(() => {}); + }; + window.addEventListener('pointerdown', unmute, { once: true }); + removeUnmuteListener = () => window.removeEventListener('pointerdown', unmute); }); } return () => { video.removeEventListener('ended', onEnded); + removeUnmuteListener(); }; }, [currentAd, goNext, clearTimer]); @@ -127,7 +146,6 @@ export default function AdCarousel({ tenantCode }) { ref={videoRef} className="branch-display__ad-media" src={src} - muted playsInline autoPlay /> diff --git a/fe/web/teller-app/src/pages/BranchDisplayPage/BranchDisplayPage.css b/fe/web/teller-app/src/pages/BranchDisplayPage/BranchDisplayPage.css index 44aa694..8286614 100644 --- a/fe/web/teller-app/src/pages/BranchDisplayPage/BranchDisplayPage.css +++ b/fe/web/teller-app/src/pages/BranchDisplayPage/BranchDisplayPage.css @@ -15,11 +15,15 @@ --bd-error-bg: #5c1a1a; --bd-error-text: #fecaca; + /* Fluid root: scales 720p → 4K; descendants use em so they follow */ + font-size: clamp(12px, 0.75vw + 0.55vh, 28px); + position: relative; height: 100vh; + height: 100dvh; width: 100%; box-sizing: border-box; - padding: 1rem 1.5rem 1rem; + padding: 0.75em 1em; background: radial-gradient(ellipse 80% 50% at 10% -10%, rgba(62, 207, 122, 0.12), transparent 55%), radial-gradient(ellipse 60% 40% at 95% 5%, rgba(240, 208, 96, 0.1), transparent 50%), @@ -28,54 +32,37 @@ font-family: system-ui, -apple-system, Segoe UI, sans-serif; display: grid; grid-template-rows: auto minmax(0, 1fr); - gap: 0.75rem; + gap: 0.6em; overflow: hidden; } -.branch-display__sound-enable { - position: absolute; - top: 0.75rem; - left: 50%; - transform: translateX(-50%); - z-index: 20; - border: 1px solid var(--bd-accent-deep); - background: linear-gradient(180deg, var(--bd-accent-bright) 0%, var(--bd-accent) 100%); - color: #1a2e14; - border-radius: 999px; - padding: 0.45rem 1rem; - font-size: 0.85rem; - font-weight: 600; - cursor: pointer; - box-shadow: 0 2px 12px rgba(240, 208, 96, 0.35); -} - .branch-display__header { margin: 0; display: flex; align-items: center; justify-content: space-between; - gap: 1rem; + gap: 1em; } .branch-display__brand { display: flex; align-items: center; - gap: 0.85rem; + gap: 0.85em; min-width: 0; } .branch-display__logo { - height: clamp(2.5rem, 4.5vw, 3.5rem); + height: clamp(2em, 1.8em + 1vw, 4em); width: auto; flex-shrink: 0; - border-radius: 0.35rem; + border-radius: 0.35em; object-fit: contain; background: #fff; } .branch-display__eyebrow { margin: 0; - font-size: 0.75rem; + font-size: 0.75em; letter-spacing: 0.12em; text-transform: uppercase; color: var(--bd-accent); @@ -84,7 +71,7 @@ .branch-display__title { margin: 0; - font-size: clamp(1.4rem, 2.2vw, 2rem); + font-size: clamp(1.25em, 2vw, 1.75em); font-weight: 700; letter-spacing: -0.02em; color: var(--bd-text); @@ -93,17 +80,17 @@ .branch-display__error { margin: 0; - padding: 0.5rem 0.75rem; - border-radius: 0.4rem; + padding: 0.5em 0.75em; + border-radius: 0.4em; background: var(--bd-error-bg); color: var(--bd-error-text); - font-size: 0.9rem; + font-size: 0.9em; } .branch-display__main { display: grid; - grid-template-columns: minmax(0, 1fr) minmax(340px, 1.05fr); - gap: 0.85rem; + grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); + gap: 0.85em; min-height: 0; overflow: hidden; } @@ -111,6 +98,7 @@ @media (max-width: 960px) { .branch-display { height: auto; + min-height: 100dvh; min-height: 100vh; overflow: auto; grid-template-rows: auto; @@ -127,7 +115,7 @@ .branch-display__section--gold { flex: none; - min-height: 16rem; + min-height: 16em; } } @@ -136,7 +124,7 @@ min-height: 0; display: flex; flex-direction: column; - gap: 0.65rem; + gap: 0.75em; overflow: hidden; } @@ -145,12 +133,23 @@ min-height: 0; } +/* Serving block must keep its content height — otherwise cards spill into Giliran */ +.branch-display__queue-column > .branch-display__section:first-of-type { + flex: 0 0 auto; + min-height: auto; + overflow: hidden; +} + .branch-display__section--waiting { flex: 0 1 auto; max-height: 28%; display: flex; flex-direction: column; min-height: 0; + overflow: hidden; + position: relative; + z-index: 1; + background: transparent; } .branch-display__section--gold { @@ -158,11 +157,12 @@ display: flex; flex-direction: column; min-height: 0; + overflow: hidden; } .branch-display__section-title { - margin: 0 0 0.45rem; - font-size: 0.85rem; + margin: 0 0 0.45em; + font-size: 1.05em; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; @@ -171,16 +171,16 @@ .branch-display__stations { display: grid; - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); - gap: 0.55rem; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 10em), 1fr)); + gap: 0.7em; } .branch-display__station-card { display: flex; flex-direction: column; - gap: 0.2rem; - padding: 0.65rem 0.5rem; - border-radius: 0.5rem; + gap: 0.3em; + padding: 0.9em 0.65em; + border-radius: 0.5em; background: linear-gradient(160deg, var(--bd-surface-raised) 0%, var(--bd-surface) 100%); border: 1px solid var(--bd-border); box-shadow: inset 0 1px 0 rgba(62, 207, 122, 0.08); @@ -189,18 +189,38 @@ .branch-display__station-name { margin: 0; - font-size: 0.8rem; + font-size: 1.1em; color: var(--bd-muted); } .branch-display__ticket-number { margin: 0; - font-size: clamp(1.75rem, 3.2vw, 2.75rem); + font-size: clamp(1.75em, min(4.2vw, 8vh), 5.5em); font-weight: 700; line-height: 1; letter-spacing: -0.03em; color: var(--bd-accent-bright); - text-shadow: 0 0 24px rgba(255, 229, 102, 0.25); + text-shadow: 0 0 1.5em rgba(255, 229, 102, 0.25); +} + +/* Short TVs (e.g. 720p): tighten serving cards so sections don't collide */ +@media (max-height: 800px) { + .branch-display__station-card { + padding: 0.55em 0.5em; + gap: 0.15em; + } + + .branch-display__ticket-number { + font-size: clamp(1.5em, min(3.6vw, 7vh), 3.25em); + } + + .branch-display__section-title { + margin-bottom: 0.3em; + } + + .branch-display__section--waiting { + max-height: 24%; + } } .branch-display__ticket-number--idle { @@ -210,7 +230,7 @@ .branch-display__station-service { margin: 0; - font-size: 0.75rem; + font-size: 1em; color: var(--bd-muted-strong); white-space: nowrap; overflow: hidden; @@ -221,8 +241,8 @@ display: flex; align-items: baseline; justify-content: space-between; - gap: 0.75rem; - margin-bottom: 0.4rem; + gap: 0.75em; + margin-bottom: 0.4em; flex-shrink: 0; } @@ -232,14 +252,14 @@ .branch-display__waiting-count { margin: 0; - font-size: 0.85rem; + font-size: 1.05em; color: var(--bd-accent); } .branch-display__empty { margin: 0; color: var(--bd-muted); - font-size: 0.9rem; + font-size: 1.05em; } .branch-display__waiting-list { @@ -248,7 +268,7 @@ padding: 0; display: flex; flex-direction: column; - gap: 0.35rem; + gap: 0.45em; flex: 1; min-height: 0; overflow: auto; @@ -258,24 +278,24 @@ display: flex; align-items: center; justify-content: space-between; - gap: 0.75rem; - padding: 0.4rem 0.7rem; - border-radius: 0.4rem; + gap: 0.75em; + padding: 0.55em 0.85em; + border-radius: 0.4em; background: var(--bd-surface); border: 1px solid var(--bd-border); - border-left: 3px solid var(--bd-green); + border-left: 0.2em solid var(--bd-green); flex-shrink: 0; } .branch-display__waiting-number { - font-size: clamp(1.1rem, 1.8vw, 1.5rem); + font-size: clamp(1.35em, 2.4vw, 2.5em); font-weight: 700; letter-spacing: -0.02em; color: var(--bd-accent); } .branch-display__waiting-service { - font-size: 0.85rem; + font-size: 0.85em; color: var(--bd-muted-strong); } @@ -283,8 +303,8 @@ min-width: 0; min-height: 0; height: 100%; - padding: 0.65rem; - border-radius: 0.6rem; + padding: 0.65em; + border-radius: 0.6em; background: linear-gradient(160deg, var(--bd-surface-raised) 0%, var(--bd-surface) 100%); border: 1px solid var(--bd-border); box-shadow: inset 0 1px 0 rgba(240, 208, 96, 0.06); @@ -297,8 +317,8 @@ display: flex; align-items: baseline; justify-content: space-between; - gap: 0.75rem; - margin-bottom: 0.4rem; + gap: 0.75em; + margin-bottom: 0.4em; flex-shrink: 0; } @@ -311,7 +331,7 @@ flex: 1; min-height: 0; width: 100%; - border-radius: 0.4rem; + border-radius: 0.4em; overflow: hidden; background: var(--bd-bg); border: 1px solid var(--bd-border); @@ -326,8 +346,8 @@ } .branch-display__ad-caption { - margin: 0.35rem 0 0; - font-size: 0.8rem; + margin: 0.35em 0 0; + font-size: 0.8em; color: var(--bd-muted-strong); text-align: center; flex-shrink: 0; @@ -337,7 +357,7 @@ } .branch-display__section--gold .branch-display__waiting-header { - margin-bottom: 0.35rem; + margin-bottom: 0.35em; } .branch-display__gold-table-wrap { @@ -345,7 +365,7 @@ min-height: 0; overflow-y: auto; overflow-x: hidden; - border-radius: 0.5rem; + border-radius: 0.5em; border: 1px solid var(--bd-border); background: var(--bd-surface); scrollbar-width: none; @@ -358,19 +378,19 @@ .branch-display__gold-table { width: 100%; border-collapse: collapse; - font-size: 0.9rem; + font-size: 1.25em; } .branch-display__gold-table th, .branch-display__gold-table td { - padding: 0.45rem 0.65rem; + padding: 0.55em 0.75em; text-align: left; border-bottom: 1px solid var(--bd-border); line-height: 1.3; } .branch-display__gold-table th { - font-size: 0.7rem; + font-size: 0.72em; letter-spacing: 0.06em; text-transform: uppercase; color: var(--bd-accent); diff --git a/fe/web/teller-app/src/pages/BranchDisplayPage/BranchDisplayPage.jsx b/fe/web/teller-app/src/pages/BranchDisplayPage/BranchDisplayPage.jsx index 44e7b0d..1ab24e3 100644 --- a/fe/web/teller-app/src/pages/BranchDisplayPage/BranchDisplayPage.jsx +++ b/fe/web/teller-app/src/pages/BranchDisplayPage/BranchDisplayPage.jsx @@ -29,7 +29,6 @@ export default function BranchDisplayPage() { const [goldUpdatedAt, setGoldUpdatedAt] = useState(null); const [goldError, setGoldError] = useState(null); const [error, setError] = useState(null); - const [soundReady, setSoundReady] = useState(false); const goldTableWrapRef = useRef(null); const previousServingRef = useRef(null); @@ -192,29 +191,7 @@ export default function BranchDisplayPage() { ); return ( -
{ - if (!soundReady) { - unlockQueueCallSound(); - setSoundReady(true); - } - }} - > - {!soundReady ? ( - - ) : null} - +

- {ticket?.service?.name ?? 'Waiting for next'} + {ticket?.service?.name ?? 'Menunggu nombor giliran berikutnya'}

);