Bugfix/testing #4
@@ -2,8 +2,8 @@ name: Build Docker Image
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
# branches:
|
||||||
- main
|
# - main
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "v*"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[x] create qr and teller account
|
||||||
|
[x] increase size of gold display
|
||||||
+1
-30
@@ -4,7 +4,7 @@ services:
|
|||||||
container_name: qms-mysql
|
container_name: qms-mysql
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "${MYSQL_HOST_PORT:-3307}:3306"
|
- "3306:3306"
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-password}
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-password}
|
||||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-qms}
|
MYSQL_DATABASE: ${MYSQL_DATABASE:-qms}
|
||||||
@@ -27,37 +27,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- qms-net
|
- 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:
|
volumes:
|
||||||
qms-mysql-data:
|
qms-mysql-data:
|
||||||
qms-uploads:
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
qms-net:
|
qms-net:
|
||||||
|
|||||||
+8
-7
@@ -33,13 +33,14 @@ public class DefaultAdminService implements AdminService {
|
|||||||
private final RoleService roleService;
|
private final RoleService roleService;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
private final AuthService authService;
|
private final AuthService authService;
|
||||||
|
|
||||||
public DefaultAdminService(final UserRepository userRepository,
|
public DefaultAdminService(final UserRepository userRepository,
|
||||||
final UserService userService,
|
final UserService userService,
|
||||||
final TwoFactorService twoFactorService,
|
final TwoFactorService twoFactorService,
|
||||||
final TenantService tenantService,
|
final TenantService tenantService,
|
||||||
final RoleService roleService,
|
final RoleService roleService,
|
||||||
final PasswordEncoder passwordEncoder,
|
final PasswordEncoder passwordEncoder,
|
||||||
final AuthService authService) {
|
final AuthService authService) {
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
this.twoFactorService = twoFactorService;
|
this.twoFactorService = twoFactorService;
|
||||||
@@ -50,7 +51,7 @@ public class DefaultAdminService implements AdminService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<User> findUsersByCode(final String tenantCode, final String roleName){
|
public List<User> findUsersByCode(final String tenantCode, final String roleName) {
|
||||||
final Set<RoleName> roleNameSet = Set.of(RoleName.valueOf(roleName));
|
final Set<RoleName> roleNameSet = Set.of(RoleName.valueOf(roleName));
|
||||||
return this.userRepository.findAllByTenant_CodeAndRoles_NameIn(tenantCode, roleNameSet);
|
return this.userRepository.findAllByTenant_CodeAndRoles_NameIn(tenantCode, roleNameSet);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -30,7 +31,8 @@ public class AdminController {
|
|||||||
|
|
||||||
@PostMapping("/{code}")
|
@PostMapping("/{code}")
|
||||||
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN', 'ROLE_STAFF_ADMIN')")
|
@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<List<UserDto>> getUsers(@RequestBody RoleRequest request,
|
||||||
|
@PathVariable(name = "code") final String tenantCode) {
|
||||||
RoleName roleName;
|
RoleName roleName;
|
||||||
try {
|
try {
|
||||||
roleName = RoleName.valueOf(request.roleName);
|
roleName = RoleName.valueOf(request.roleName);
|
||||||
@@ -39,7 +41,7 @@ public class AdminController {
|
|||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.authService.canOnlyCRUDUser(roleName)){
|
if (this.authService.canOnlyCRUDUser(roleName)) {
|
||||||
logger.warn("Only super admin can read admins");
|
logger.warn("Only super admin can read admins");
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
@@ -52,8 +54,7 @@ public class AdminController {
|
|||||||
return ResponseEntity.ok().body(
|
return ResponseEntity.ok().body(
|
||||||
this.adminService.findUsersByCode(tenantCode, request.roleName).stream()
|
this.adminService.findUsersByCode(tenantCode, request.roleName).stream()
|
||||||
.map(UserDto::fromEntity)
|
.map(UserDto::fromEntity)
|
||||||
.collect(Collectors.toList())
|
.collect(Collectors.toList()));
|
||||||
);
|
|
||||||
} catch (EntityNotFoundException e) {
|
} catch (EntityNotFoundException e) {
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
@@ -61,7 +62,8 @@ public class AdminController {
|
|||||||
|
|
||||||
@PostMapping("/{code}/user")
|
@PostMapping("/{code}/user")
|
||||||
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN', 'ROLE_STAFF_ADMIN')")
|
@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<UserDto> addUser(@RequestBody final AdminRequest request,
|
||||||
|
@PathVariable(name = "code") final String tenantCode) throws AuthException {
|
||||||
RoleName roleName;
|
RoleName roleName;
|
||||||
try {
|
try {
|
||||||
roleName = RoleName.valueOf(request.roleName);
|
roleName = RoleName.valueOf(request.roleName);
|
||||||
@@ -70,7 +72,7 @@ public class AdminController {
|
|||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.authService.canOnlyCRUDUser(roleName)){
|
if (this.authService.canOnlyCRUDUser(roleName)) {
|
||||||
logger.warn("Only super admin can add admin");
|
logger.warn("Only super admin can add admin");
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
@@ -95,7 +97,8 @@ public class AdminController {
|
|||||||
|
|
||||||
@PutMapping("/{code}/user/{userId}")
|
@PutMapping("/{code}/user/{userId}")
|
||||||
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN', 'ROLE_STAFF_ADMIN')")
|
@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<SimpleMessageDto> updateAdmin(@RequestBody final UserDto request,
|
||||||
|
@PathVariable(name = "code") final String tenantCode, @PathVariable(name = "userId") final long adminId) {
|
||||||
if (!this.authService.canChangeTenant(tenantCode)) {
|
if (!this.authService.canChangeTenant(tenantCode)) {
|
||||||
logger.warn("Admin does not belong to the specified tenant");
|
logger.warn("Admin does not belong to the specified tenant");
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
@@ -112,7 +115,8 @@ public class AdminController {
|
|||||||
|
|
||||||
@DeleteMapping("/{code}/user/{userId}")
|
@DeleteMapping("/{code}/user/{userId}")
|
||||||
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN', 'ROLE_STAFF_ADMIN')")
|
@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<SimpleMessageDto> removeAdmin(@PathVariable(name = "code") final String tenantCode,
|
||||||
|
@PathVariable(name = "userId") final long adminId) {
|
||||||
if (!this.authService.canChangeTenant(tenantCode)) {
|
if (!this.authService.canChangeTenant(tenantCode)) {
|
||||||
logger.warn("Admin does not belong to the specified tenant");
|
logger.warn("Admin does not belong to the specified tenant");
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
@@ -130,6 +134,6 @@ public class AdminController {
|
|||||||
public record AdminRequest(String email, String password, String roleName) {
|
public record AdminRequest(String email, String password, String roleName) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record RoleRequest(String roleName){
|
public record RoleRequest(String roleName) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ public class AdvertisementController {
|
|||||||
private final AuthService authService;
|
private final AuthService authService;
|
||||||
|
|
||||||
public AdvertisementController(final AdvertisementService advertisementService,
|
public AdvertisementController(final AdvertisementService advertisementService,
|
||||||
final AuthService authService) {
|
final AuthService authService) {
|
||||||
this.advertisementService = advertisementService;
|
this.advertisementService = advertisementService;
|
||||||
this.authService = authService;
|
this.authService = authService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/media/{adId}")
|
@GetMapping("/media/{adId}")
|
||||||
public ResponseEntity streamMedia(@PathVariable final long adId) {
|
public ResponseEntity<Resource> streamMedia(@PathVariable final long adId) {
|
||||||
try {
|
try {
|
||||||
final Advertisement advertisement = this.advertisementService.findById(adId);
|
final Advertisement advertisement = this.advertisementService.findById(adId);
|
||||||
final Resource resource = this.advertisementService.loadMedia(adId);
|
final Resource resource = this.advertisementService.loadMedia(adId);
|
||||||
@@ -64,11 +64,11 @@ public class AdvertisementController {
|
|||||||
@PostMapping("/{tenantCode}")
|
@PostMapping("/{tenantCode}")
|
||||||
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN')")
|
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN')")
|
||||||
public ResponseEntity createAdvertisement(@PathVariable final String tenantCode,
|
public ResponseEntity createAdvertisement(@PathVariable final String tenantCode,
|
||||||
@RequestParam("file") final MultipartFile file,
|
@RequestParam("file") final MultipartFile file,
|
||||||
@RequestParam(value = "title", required = false) final String title,
|
@RequestParam(value = "title", required = false) final String title,
|
||||||
@RequestParam(value = "durationSeconds", required = false) final Integer durationSeconds,
|
@RequestParam(value = "durationSeconds", required = false) final Integer durationSeconds,
|
||||||
@RequestParam(value = "sortOrder", required = false) final Integer sortOrder,
|
@RequestParam(value = "sortOrder", required = false) final Integer sortOrder,
|
||||||
@RequestParam(value = "active", required = false) final Boolean active) {
|
@RequestParam(value = "active", required = false) final Boolean active) {
|
||||||
if (!this.authService.canChangeTenant(tenantCode)) {
|
if (!this.authService.canChangeTenant(tenantCode)) {
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
@@ -80,8 +80,7 @@ public class AdvertisementController {
|
|||||||
title,
|
title,
|
||||||
durationSeconds,
|
durationSeconds,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
active
|
active);
|
||||||
);
|
|
||||||
return ResponseEntity.ok().body(AdvertisementDto.fromEntity(created));
|
return ResponseEntity.ok().body(AdvertisementDto.fromEntity(created));
|
||||||
} catch (final Exception exception) {
|
} catch (final Exception exception) {
|
||||||
return ResponseEntity.badRequest().body(new SimpleMessageDto(exception.getMessage()));
|
return ResponseEntity.badRequest().body(new SimpleMessageDto(exception.getMessage()));
|
||||||
@@ -108,8 +107,8 @@ public class AdvertisementController {
|
|||||||
@PutMapping("/{tenantCode}/{adId}")
|
@PutMapping("/{tenantCode}/{adId}")
|
||||||
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN')")
|
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN')")
|
||||||
public ResponseEntity updateAdvertisement(@PathVariable final String tenantCode,
|
public ResponseEntity updateAdvertisement(@PathVariable final String tenantCode,
|
||||||
@PathVariable final long adId,
|
@PathVariable final long adId,
|
||||||
@RequestBody final AdvertisementUpdateRequest request) {
|
@RequestBody final AdvertisementUpdateRequest request) {
|
||||||
if (!this.authService.canChangeTenant(tenantCode)) {
|
if (!this.authService.canChangeTenant(tenantCode)) {
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
@@ -121,8 +120,7 @@ public class AdvertisementController {
|
|||||||
request.title(),
|
request.title(),
|
||||||
request.durationSeconds(),
|
request.durationSeconds(),
|
||||||
request.sortOrder(),
|
request.sortOrder(),
|
||||||
request.active()
|
request.active());
|
||||||
);
|
|
||||||
return ResponseEntity.ok().body(AdvertisementDto.fromEntity(updated));
|
return ResponseEntity.ok().body(AdvertisementDto.fromEntity(updated));
|
||||||
} catch (final Exception exception) {
|
} catch (final Exception exception) {
|
||||||
return ResponseEntity.badRequest().body(new SimpleMessageDto(exception.getMessage()));
|
return ResponseEntity.badRequest().body(new SimpleMessageDto(exception.getMessage()));
|
||||||
@@ -132,7 +130,7 @@ public class AdvertisementController {
|
|||||||
@DeleteMapping("/{tenantCode}/{adId}")
|
@DeleteMapping("/{tenantCode}/{adId}")
|
||||||
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN')")
|
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN')")
|
||||||
public ResponseEntity deleteAdvertisement(@PathVariable final String tenantCode,
|
public ResponseEntity deleteAdvertisement(@PathVariable final String tenantCode,
|
||||||
@PathVariable final long adId) {
|
@PathVariable final long adId) {
|
||||||
if (!this.authService.canChangeTenant(tenantCode)) {
|
if (!this.authService.canChangeTenant(tenantCode)) {
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
@@ -146,8 +144,8 @@ public class AdvertisementController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public record AdvertisementUpdateRequest(String title,
|
public record AdvertisementUpdateRequest(String title,
|
||||||
Integer durationSeconds,
|
Integer durationSeconds,
|
||||||
Integer sortOrder,
|
Integer sortOrder,
|
||||||
Boolean active) {
|
Boolean active) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ type Props = {
|
|||||||
initialBranchId?: number;
|
initialBranchId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Prevents spam of "Dapatkan tiket lain" right after getting a number. */
|
||||||
|
const ANOTHER_TICKET_COOLDOWN_MS = 10_000;
|
||||||
|
|
||||||
function pickLatestTicket(
|
function pickLatestTicket(
|
||||||
tickets: Ticket[],
|
tickets: Ticket[],
|
||||||
branchId?: number
|
branchId?: number
|
||||||
@@ -61,8 +64,27 @@ export default function CustomerTicketFlow({
|
|||||||
const [ticket, setTicket] = useState<Ticket | null>(null);
|
const [ticket, setTicket] = useState<Ticket | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [cooldownLeftMs, setCooldownLeftMs] = useState(0);
|
||||||
const bootstrapped = useRef(false);
|
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(() => {
|
useEffect(() => {
|
||||||
if (bootstrapped.current) return;
|
if (bootstrapped.current) return;
|
||||||
bootstrapped.current = true;
|
bootstrapped.current = true;
|
||||||
@@ -281,6 +303,9 @@ export default function CustomerTicketFlow({
|
|||||||
? "Pilih perkhidmatan untuk mendapatkan nombor."
|
? "Pilih perkhidmatan untuk mendapatkan nombor."
|
||||||
: "Imbas QR code di branch anda, atau masukkan kod syarikat di bawah.";
|
: "Imbas QR code di branch anda, atau masukkan kod syarikat di bawah.";
|
||||||
|
|
||||||
|
const canGetAnotherTicket = cooldownLeftMs <= 0;
|
||||||
|
const cooldownSeconds = Math.ceil(cooldownLeftMs / 1000);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto flex min-h-full w-full max-w-xl flex-col gap-8 px-6 py-12">
|
<main className="mx-auto flex min-h-full w-full max-w-xl flex-col gap-8 px-6 py-12">
|
||||||
<header className="space-y-2">
|
<header className="space-y-2">
|
||||||
@@ -423,7 +448,7 @@ export default function CustomerTicketFlow({
|
|||||||
{step === "ticket" && ticket ? (
|
{step === "ticket" && ticket ? (
|
||||||
<section className="space-y-6 rounded-md border border-zinc-200 bg-white px-6 py-8 text-center">
|
<section className="space-y-6 rounded-md border border-zinc-200 bg-white px-6 py-8 text-center">
|
||||||
<p className="text-sm tracking-wide text-zinc-500 uppercase">
|
<p className="text-sm tracking-wide text-zinc-500 uppercase">
|
||||||
Nombor tiket anda
|
Nombor giliran anda
|
||||||
</p>
|
</p>
|
||||||
<p className="text-6xl font-semibold tracking-tight text-zinc-900">
|
<p className="text-6xl font-semibold tracking-tight text-zinc-900">
|
||||||
{ticket.number}
|
{ticket.number}
|
||||||
@@ -432,12 +457,36 @@ export default function CustomerTicketFlow({
|
|||||||
<p>{ticket.service.name}</p>
|
<p>{ticket.service.name}</p>
|
||||||
<p>{ticket.branch.name}</p>
|
<p>{ticket.branch.name}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="rounded-md border border-amber-300 bg-amber-50 px-4 py-3 text-left text-sm text-amber-950"
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
<p className="font-semibold">Anda sudah mempunyai nombor giliran.</p>
|
||||||
|
<p className="mt-1 text-amber-900">
|
||||||
|
Sila tunggu sehingga nombor anda dipanggil.
|
||||||
|
</p>
|
||||||
|
{!canGetAnotherTicket ? (
|
||||||
|
<p className="mt-2 font-medium tabular-nums text-amber-950">
|
||||||
|
Tiket lain boleh diambil dalam {cooldownSeconds}s
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={resetFlow}
|
onClick={resetFlow}
|
||||||
className="w-full rounded-md bg-zinc-900 px-4 py-3 text-sm font-medium text-white"
|
disabled={!canGetAnotherTicket || loading}
|
||||||
|
aria-disabled={!canGetAnotherTicket || loading}
|
||||||
|
className={
|
||||||
|
canGetAnotherTicket
|
||||||
|
? "w-full rounded-md border border-zinc-300 bg-white px-4 py-3 text-sm font-medium text-zinc-700 transition hover:border-zinc-500 hover:bg-zinc-50 disabled:opacity-60"
|
||||||
|
: "w-full cursor-not-allowed rounded-md bg-zinc-200 px-4 py-3 text-sm font-medium text-zinc-500"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Dapatkan tiket lain
|
{canGetAnotherTicket
|
||||||
|
? "Dapatkan nombor giliran lain"
|
||||||
|
: `Tunggu ${cooldownSeconds}s…`}
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev --webpack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
|
|||||||
@@ -76,22 +76,41 @@ export default function AdCarousel({ tenantCode }) {
|
|||||||
const video = videoRef.current;
|
const video = videoRef.current;
|
||||||
if (!video || currentAd?.mediaType !== 'VIDEO') return undefined;
|
if (!video || currentAd?.mediaType !== 'VIDEO') return undefined;
|
||||||
|
|
||||||
|
let removeUnmuteListener = () => {};
|
||||||
|
|
||||||
const onEnded = () => goNext();
|
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.addEventListener('ended', onEnded);
|
||||||
video.muted = true;
|
video.muted = false;
|
||||||
video.playsInline = true;
|
video.playsInline = true;
|
||||||
|
|
||||||
const playPromise = video.play();
|
const playPromise = video.play();
|
||||||
if (playPromise?.catch) {
|
if (playPromise?.catch) {
|
||||||
playPromise.catch(() => {
|
playPromise.catch(() => {
|
||||||
// Autoplay blocked — advance after durationSeconds fallback
|
// Unmuted autoplay blocked — keep video playing muted, then unmute on gesture.
|
||||||
clearTimer();
|
video.muted = true;
|
||||||
const durationMs = Math.max(5, Number(currentAd.durationSeconds) || 15) * 1000;
|
const mutedPlay = video.play();
|
||||||
timerRef.current = setTimeout(goNext, durationMs);
|
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 () => {
|
return () => {
|
||||||
video.removeEventListener('ended', onEnded);
|
video.removeEventListener('ended', onEnded);
|
||||||
|
removeUnmuteListener();
|
||||||
};
|
};
|
||||||
}, [currentAd, goNext, clearTimer]);
|
}, [currentAd, goNext, clearTimer]);
|
||||||
|
|
||||||
@@ -127,7 +146,6 @@ export default function AdCarousel({ tenantCode }) {
|
|||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
className="branch-display__ad-media"
|
className="branch-display__ad-media"
|
||||||
src={src}
|
src={src}
|
||||||
muted
|
|
||||||
playsInline
|
playsInline
|
||||||
autoPlay
|
autoPlay
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,11 +15,15 @@
|
|||||||
--bd-error-bg: #5c1a1a;
|
--bd-error-bg: #5c1a1a;
|
||||||
--bd-error-text: #fecaca;
|
--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;
|
position: relative;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 1rem 1.5rem 1rem;
|
padding: 0.75em 1em;
|
||||||
background:
|
background:
|
||||||
radial-gradient(ellipse 80% 50% at 10% -10%, rgba(62, 207, 122, 0.12), transparent 55%),
|
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%),
|
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;
|
font-family: system-ui, -apple-system, Segoe UI, sans-serif;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
gap: 0.75rem;
|
gap: 0.6em;
|
||||||
overflow: hidden;
|
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 {
|
.branch-display__header {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 1rem;
|
gap: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-display__brand {
|
.branch-display__brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.85rem;
|
gap: 0.85em;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-display__logo {
|
.branch-display__logo {
|
||||||
height: clamp(2.5rem, 4.5vw, 3.5rem);
|
height: clamp(2em, 1.8em + 1vw, 4em);
|
||||||
width: auto;
|
width: auto;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-radius: 0.35rem;
|
border-radius: 0.35em;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-display__eyebrow {
|
.branch-display__eyebrow {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75em;
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.12em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--bd-accent);
|
color: var(--bd-accent);
|
||||||
@@ -84,7 +71,7 @@
|
|||||||
|
|
||||||
.branch-display__title {
|
.branch-display__title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: clamp(1.4rem, 2.2vw, 2rem);
|
font-size: clamp(1.25em, 2vw, 1.75em);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
color: var(--bd-text);
|
color: var(--bd-text);
|
||||||
@@ -93,17 +80,17 @@
|
|||||||
|
|
||||||
.branch-display__error {
|
.branch-display__error {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5em 0.75em;
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4em;
|
||||||
background: var(--bd-error-bg);
|
background: var(--bd-error-bg);
|
||||||
color: var(--bd-error-text);
|
color: var(--bd-error-text);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-display__main {
|
.branch-display__main {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(340px, 1.05fr);
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
|
||||||
gap: 0.85rem;
|
gap: 0.85em;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -111,6 +98,7 @@
|
|||||||
@media (max-width: 960px) {
|
@media (max-width: 960px) {
|
||||||
.branch-display {
|
.branch-display {
|
||||||
height: auto;
|
height: auto;
|
||||||
|
min-height: 100dvh;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
grid-template-rows: auto;
|
grid-template-rows: auto;
|
||||||
@@ -127,7 +115,7 @@
|
|||||||
|
|
||||||
.branch-display__section--gold {
|
.branch-display__section--gold {
|
||||||
flex: none;
|
flex: none;
|
||||||
min-height: 16rem;
|
min-height: 16em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +124,7 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.65rem;
|
gap: 0.75em;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,12 +133,23 @@
|
|||||||
min-height: 0;
|
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 {
|
.branch-display__section--waiting {
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
max-height: 28%;
|
max-height: 28%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-display__section--gold {
|
.branch-display__section--gold {
|
||||||
@@ -158,11 +157,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-display__section-title {
|
.branch-display__section-title {
|
||||||
margin: 0 0 0.45rem;
|
margin: 0 0 0.45em;
|
||||||
font-size: 0.85rem;
|
font-size: 1.05em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -171,16 +171,16 @@
|
|||||||
|
|
||||||
.branch-display__stations {
|
.branch-display__stations {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 10em), 1fr));
|
||||||
gap: 0.55rem;
|
gap: 0.7em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-display__station-card {
|
.branch-display__station-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.2rem;
|
gap: 0.3em;
|
||||||
padding: 0.65rem 0.5rem;
|
padding: 0.9em 0.65em;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5em;
|
||||||
background: linear-gradient(160deg, var(--bd-surface-raised) 0%, var(--bd-surface) 100%);
|
background: linear-gradient(160deg, var(--bd-surface-raised) 0%, var(--bd-surface) 100%);
|
||||||
border: 1px solid var(--bd-border);
|
border: 1px solid var(--bd-border);
|
||||||
box-shadow: inset 0 1px 0 rgba(62, 207, 122, 0.08);
|
box-shadow: inset 0 1px 0 rgba(62, 207, 122, 0.08);
|
||||||
@@ -189,18 +189,38 @@
|
|||||||
|
|
||||||
.branch-display__station-name {
|
.branch-display__station-name {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.8rem;
|
font-size: 1.1em;
|
||||||
color: var(--bd-muted);
|
color: var(--bd-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-display__ticket-number {
|
.branch-display__ticket-number {
|
||||||
margin: 0;
|
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;
|
font-weight: 700;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
letter-spacing: -0.03em;
|
letter-spacing: -0.03em;
|
||||||
color: var(--bd-accent-bright);
|
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 {
|
.branch-display__ticket-number--idle {
|
||||||
@@ -210,7 +230,7 @@
|
|||||||
|
|
||||||
.branch-display__station-service {
|
.branch-display__station-service {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.75rem;
|
font-size: 1em;
|
||||||
color: var(--bd-muted-strong);
|
color: var(--bd-muted-strong);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -221,8 +241,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.75rem;
|
gap: 0.75em;
|
||||||
margin-bottom: 0.4rem;
|
margin-bottom: 0.4em;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,14 +252,14 @@
|
|||||||
|
|
||||||
.branch-display__waiting-count {
|
.branch-display__waiting-count {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.85rem;
|
font-size: 1.05em;
|
||||||
color: var(--bd-accent);
|
color: var(--bd-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-display__empty {
|
.branch-display__empty {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--bd-muted);
|
color: var(--bd-muted);
|
||||||
font-size: 0.9rem;
|
font-size: 1.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-display__waiting-list {
|
.branch-display__waiting-list {
|
||||||
@@ -248,7 +268,7 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.35rem;
|
gap: 0.45em;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -258,24 +278,24 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.75rem;
|
gap: 0.75em;
|
||||||
padding: 0.4rem 0.7rem;
|
padding: 0.55em 0.85em;
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4em;
|
||||||
background: var(--bd-surface);
|
background: var(--bd-surface);
|
||||||
border: 1px solid var(--bd-border);
|
border: 1px solid var(--bd-border);
|
||||||
border-left: 3px solid var(--bd-green);
|
border-left: 0.2em solid var(--bd-green);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-display__waiting-number {
|
.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;
|
font-weight: 700;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
color: var(--bd-accent);
|
color: var(--bd-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-display__waiting-service {
|
.branch-display__waiting-service {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85em;
|
||||||
color: var(--bd-muted-strong);
|
color: var(--bd-muted-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,8 +303,8 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0.65rem;
|
padding: 0.65em;
|
||||||
border-radius: 0.6rem;
|
border-radius: 0.6em;
|
||||||
background: linear-gradient(160deg, var(--bd-surface-raised) 0%, var(--bd-surface) 100%);
|
background: linear-gradient(160deg, var(--bd-surface-raised) 0%, var(--bd-surface) 100%);
|
||||||
border: 1px solid var(--bd-border);
|
border: 1px solid var(--bd-border);
|
||||||
box-shadow: inset 0 1px 0 rgba(240, 208, 96, 0.06);
|
box-shadow: inset 0 1px 0 rgba(240, 208, 96, 0.06);
|
||||||
@@ -297,8 +317,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.75rem;
|
gap: 0.75em;
|
||||||
margin-bottom: 0.4rem;
|
margin-bottom: 0.4em;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +331,7 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4em;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--bd-bg);
|
background: var(--bd-bg);
|
||||||
border: 1px solid var(--bd-border);
|
border: 1px solid var(--bd-border);
|
||||||
@@ -326,8 +346,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.branch-display__ad-caption {
|
.branch-display__ad-caption {
|
||||||
margin: 0.35rem 0 0;
|
margin: 0.35em 0 0;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8em;
|
||||||
color: var(--bd-muted-strong);
|
color: var(--bd-muted-strong);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -337,7 +357,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.branch-display__section--gold .branch-display__waiting-header {
|
.branch-display__section--gold .branch-display__waiting-header {
|
||||||
margin-bottom: 0.35rem;
|
margin-bottom: 0.35em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-display__gold-table-wrap {
|
.branch-display__gold-table-wrap {
|
||||||
@@ -345,7 +365,7 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5em;
|
||||||
border: 1px solid var(--bd-border);
|
border: 1px solid var(--bd-border);
|
||||||
background: var(--bd-surface);
|
background: var(--bd-surface);
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
@@ -358,19 +378,19 @@
|
|||||||
.branch-display__gold-table {
|
.branch-display__gold-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-size: 0.9rem;
|
font-size: 1.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-display__gold-table th,
|
.branch-display__gold-table th,
|
||||||
.branch-display__gold-table td {
|
.branch-display__gold-table td {
|
||||||
padding: 0.45rem 0.65rem;
|
padding: 0.55em 0.75em;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid var(--bd-border);
|
border-bottom: 1px solid var(--bd-border);
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-display__gold-table th {
|
.branch-display__gold-table th {
|
||||||
font-size: 0.7rem;
|
font-size: 0.72em;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--bd-accent);
|
color: var(--bd-accent);
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ export default function BranchDisplayPage() {
|
|||||||
const [goldUpdatedAt, setGoldUpdatedAt] = useState(null);
|
const [goldUpdatedAt, setGoldUpdatedAt] = useState(null);
|
||||||
const [goldError, setGoldError] = useState(null);
|
const [goldError, setGoldError] = useState(null);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [soundReady, setSoundReady] = useState(false);
|
|
||||||
const goldTableWrapRef = useRef(null);
|
const goldTableWrapRef = useRef(null);
|
||||||
const previousServingRef = useRef(null);
|
const previousServingRef = useRef(null);
|
||||||
|
|
||||||
@@ -192,29 +191,7 @@ export default function BranchDisplayPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="branch-display" onClick={unlockQueueCallSound}>
|
||||||
className="branch-display"
|
|
||||||
onClick={() => {
|
|
||||||
if (!soundReady) {
|
|
||||||
unlockQueueCallSound();
|
|
||||||
setSoundReady(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!soundReady ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="branch-display__sound-enable"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
unlockQueueCallSound();
|
|
||||||
setSoundReady(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Enable call sound
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<header className="branch-display__header">
|
<header className="branch-display__header">
|
||||||
<div className="branch-display__brand">
|
<div className="branch-display__brand">
|
||||||
<img
|
<img
|
||||||
@@ -259,7 +236,7 @@ export default function BranchDisplayPage() {
|
|||||||
{ticket ? ticket.number : '—'}
|
{ticket ? ticket.number : '—'}
|
||||||
</p>
|
</p>
|
||||||
<p className="branch-display__station-service">
|
<p className="branch-display__station-service">
|
||||||
{ticket?.service?.name ?? 'Waiting for next'}
|
{ticket?.service?.name ?? 'Menunggu nombor giliran berikutnya'}
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user