2 Commits

39 changed files with 98 additions and 1218 deletions
-112
View File
@@ -1,112 +0,0 @@
name: Deploy Backend to Production
# Manual production deploy — pulls pre-built backend image from the registry
on:
workflow_dispatch:
inputs:
image_tag:
description: "Docker image tag to deploy (commit SHA or version tag, e.g. v1.0.0)"
required: true
type: string
env:
BACKEND_IMAGE: git.koppkb.com/kopkb/qms-be
DEPLOY_DIR: /home/arrahn/Project/qms
jobs:
deploy:
runs-on: host
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.image_tag }}
- name: Login to Docker registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
docker login git.koppkb.com \
-u "${{ secrets.REGISTRY_USERNAME }}" \
--password-stdin
- name: Verify backend image exists in registry
run: |
set -e
IMAGE_TAG="${{ inputs.image_tag }}"
IMAGE="${BACKEND_IMAGE}:${IMAGE_TAG}"
echo "Checking if image exists: ${IMAGE}"
if ! docker manifest inspect "${IMAGE}" > /dev/null 2>&1; then
echo "ERROR: Image ${IMAGE} does not exist in registry!"
exit 1
fi
echo "✓ ${IMAGE} found"
- name: Pull backend image
run: |
set -e
IMAGE_TAG="${{ inputs.image_tag }}"
docker pull "${BACKEND_IMAGE}:${IMAGE_TAG}"
echo "✓ Image pulled successfully"
- name: Sync compose file and deploy
run: |
set -e
IMAGE_TAG="${{ inputs.image_tag }}"
mkdir -p "${DEPLOY_DIR}"
cp be/prod-compose.yml "${DEPLOY_DIR}/docker-compose.yml"
cd "${DEPLOY_DIR}"
if [ ! -f .env ]; then
echo "ERROR: ${DEPLOY_DIR}/.env is missing on the server"
echo "Create it from be/.env.example (or be/.env.production) before deploying."
exit 1
fi
export IMAGE_TAG
export BACKEND_IMAGE
docker compose -f docker-compose.yml pull backend
docker compose -f docker-compose.yml up -d --remove-orphans
echo "✓ Deployed IMAGE_TAG=${IMAGE_TAG}"
- name: Verify deployment
run: |
set -e
cd "${DEPLOY_DIR}"
echo "Waiting for services to start..."
sleep 15
for SERVICE in mysql backend; do
STATUS=$(docker compose -f docker-compose.yml ps --status running --format '{{.Name}}' "$SERVICE" 2>/dev/null || true)
if [ -z "$STATUS" ]; then
echo "ERROR: ${SERVICE} is not running"
docker compose -f docker-compose.yml ps
docker compose -f docker-compose.yml logs --tail=50 "$SERVICE" || true
exit 1
fi
echo "✓ ${SERVICE} is running (${STATUS})"
done
BACKEND_HOST_PORT=$(grep -E '^BACKEND_HOST_PORT=' .env 2>/dev/null | cut -d= -f2- | tr -d '"' || true)
BACKEND_HOST_PORT=${BACKEND_HOST_PORT:-8080}
# No actuator yet — treat any HTTP response as "server is up"
for i in 1 2 3 4 5 6 7 8; do
CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:${BACKEND_HOST_PORT}/" || echo "000")
if [ "${CODE}" != "000" ]; then
echo "✓ Backend is responding on port ${BACKEND_HOST_PORT} (HTTP ${CODE})"
exit 0
fi
echo "Waiting for backend... attempt ${i}/8"
sleep 5
done
echo "ERROR: Backend is not responding on port ${BACKEND_HOST_PORT}"
docker compose -f docker-compose.yml logs --tail=50 backend || true
exit 1
-187
View File
@@ -1,187 +0,0 @@
name: Build Docker Image
on:
push:
# branches:
# - main
tags:
- "v*"
jobs:
build-backend:
runs-on: docker
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Login to Docker registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
docker login git.koppkb.com \
-u "${{ secrets.REGISTRY_USERNAME }}" \
--password-stdin
- name: Build & push backend image
run: |
set -e
IMAGE="git.koppkb.com/kopkb/qms-be"
TAGS="-t ${IMAGE}:${{ gitea.sha }}"
if [ "${{ gitea.ref_type }}" = "tag" ] && echo "${{ gitea.ref_name }}" | grep -q '^v'; then
TAGS="${TAGS} -t ${IMAGE}:${{ gitea.ref_name }}"
fi
echo "Building and pushing backend: ${TAGS}"
docker buildx build \
--platform linux/amd64 \
--cache-from type=registry,ref=${IMAGE}:buildcache,ignore-error=true \
--cache-to type=registry,ref=${IMAGE}:buildcache,mode=max \
-f be/Dockerfile \
${TAGS} \
--push \
be
build-admin:
runs-on: docker
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Login to Docker registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
docker login git.koppkb.com \
-u "${{ secrets.REGISTRY_USERNAME }}" \
--password-stdin
- name: Build & push admin frontend image
run: |
set -e
IMAGE="git.koppkb.com/kopkb/qms-fe-admin"
TAGS="-t ${IMAGE}:${{ gitea.sha }}"
API_URL="${{ secrets.VITE_API_URL }}"
# Branch QR display URL only (public domain via nginx)
BRANCH_QR_BASE_URL="${{ secrets.VITE_BRANCH_QR_BASE_URL }}"
if [ -z "${BRANCH_QR_BASE_URL}" ]; then
BRANCH_QR_BASE_URL="https://qms-customer.erahn.com.my"
fi
if [ -z "${API_URL}" ]; then
echo "ERROR: Set Gitea secret VITE_API_URL (e.g. http://172.16.6.200:8080)"
exit 1
fi
if [ "${{ gitea.ref_type }}" = "tag" ] && echo "${{ gitea.ref_name }}" | grep -q '^v'; then
TAGS="${TAGS} -t ${IMAGE}:${{ gitea.ref_name }}"
fi
echo "Building and pushing admin: ${TAGS}"
echo "VITE_API_URL=${API_URL}"
echo "VITE_BRANCH_QR_BASE_URL=${BRANCH_QR_BASE_URL}"
docker buildx build \
--platform linux/amd64 \
--build-arg VITE_API_URL="${API_URL}" \
--build-arg VITE_BRANCH_QR_BASE_URL="${BRANCH_QR_BASE_URL}" \
--cache-from type=registry,ref=${IMAGE}:buildcache,ignore-error=true \
--cache-to type=registry,ref=${IMAGE}:buildcache,mode=max \
-f fe/web/admin-app/Dockerfile \
${TAGS} \
--push \
fe/web/admin-app
build-teller:
runs-on: docker
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Login to Docker registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
docker login git.koppkb.com \
-u "${{ secrets.REGISTRY_USERNAME }}" \
--password-stdin
- name: Build & push teller frontend image
run: |
set -e
IMAGE="git.koppkb.com/kopkb/qms-fe-teller"
TAGS="-t ${IMAGE}:${{ gitea.sha }}"
API_URL="${{ secrets.VITE_API_URL }}"
if [ -z "${API_URL}" ]; then
echo "ERROR: Set Gitea secret VITE_API_URL (e.g. http://172.16.6.200:8080)"
exit 1
fi
if [ "${{ gitea.ref_type }}" = "tag" ] && echo "${{ gitea.ref_name }}" | grep -q '^v'; then
TAGS="${TAGS} -t ${IMAGE}:${{ gitea.ref_name }}"
fi
GOLD_PRICE_URL="${{ secrets.VITE_GOLD_PRICE_URL }}"
if [ -z "${GOLD_PRICE_URL}" ]; then
GOLD_PRICE_URL="https://apiujrah.erahn.com.my/api/harga_emas"
fi
echo "Building and pushing teller: ${TAGS}"
docker buildx build \
--platform linux/amd64 \
--build-arg VITE_API_URL="${API_URL}" \
--build-arg VITE_GOLD_PRICE_URL="${GOLD_PRICE_URL}" \
--cache-from type=registry,ref=${IMAGE}:buildcache,ignore-error=true \
--cache-to type=registry,ref=${IMAGE}:buildcache,mode=max \
-f fe/web/teller-app/Dockerfile \
${TAGS} \
--push \
fe/web/teller-app
build-customer:
runs-on: docker
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Login to Docker registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
docker login git.koppkb.com \
-u "${{ secrets.REGISTRY_USERNAME }}" \
--password-stdin
- name: Build & push customer frontend image
run: |
set -e
IMAGE="git.koppkb.com/kopkb/qms-fe-customer"
TAGS="-t ${IMAGE}:${{ gitea.sha }}"
API_URL="${{ secrets.NEXT_PUBLIC_API_URL }}"
if [ -z "${API_URL}" ]; then
API_URL="${{ secrets.VITE_API_URL }}"
fi
if [ -z "${API_URL}" ]; then
echo "ERROR: Set Gitea secret NEXT_PUBLIC_API_URL or VITE_API_URL (e.g. http://172.16.6.200:8080)"
exit 1
fi
if [ "${{ gitea.ref_type }}" = "tag" ] && echo "${{ gitea.ref_name }}" | grep -q '^v'; then
TAGS="${TAGS} -t ${IMAGE}:${{ gitea.ref_name }}"
fi
echo "Building and pushing customer: ${TAGS}"
docker buildx build \
--platform linux/amd64 \
--build-arg NEXT_PUBLIC_API_URL="${API_URL}" \
--cache-from type=registry,ref=${IMAGE}:buildcache,ignore-error=true \
--cache-to type=registry,ref=${IMAGE}:buildcache,mode=max \
-f fe/web/customer-app/Dockerfile \
${TAGS} \
--push \
fe/web/customer-app
-128
View File
@@ -1,128 +0,0 @@
name: Deploy Frontend to Production
# Manual production deploy — pulls pre-built admin + teller + customer images
on:
workflow_dispatch:
inputs:
image_tag:
description: "Docker image tag to deploy (commit SHA or version tag, e.g. v1.0.0)"
required: true
type: string
env:
ADMIN_IMAGE: git.koppkb.com/kopkb/qms-fe-admin
TELLER_IMAGE: git.koppkb.com/kopkb/qms-fe-teller
CUSTOMER_IMAGE: git.koppkb.com/kopkb/qms-fe-customer
DEPLOY_DIR: /home/arrahn/Project/qms-fe
jobs:
deploy:
runs-on: host
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.image_tag }}
- name: Login to Docker registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
docker login git.koppkb.com \
-u "${{ secrets.REGISTRY_USERNAME }}" \
--password-stdin
- name: Verify frontend images exist in registry
run: |
set -e
IMAGE_TAG="${{ inputs.image_tag }}"
for IMAGE in "${ADMIN_IMAGE}:${IMAGE_TAG}" "${TELLER_IMAGE}:${IMAGE_TAG}" "${CUSTOMER_IMAGE}:${IMAGE_TAG}"; do
echo "Checking if image exists: ${IMAGE}"
if ! docker manifest inspect "${IMAGE}" > /dev/null 2>&1; then
echo "ERROR: Image ${IMAGE} does not exist in registry!"
exit 1
fi
echo "✓ ${IMAGE} found"
done
- name: Pull frontend images
run: |
set -e
IMAGE_TAG="${{ inputs.image_tag }}"
docker pull "${ADMIN_IMAGE}:${IMAGE_TAG}"
docker pull "${TELLER_IMAGE}:${IMAGE_TAG}"
docker pull "${CUSTOMER_IMAGE}:${IMAGE_TAG}"
echo "✓ Images pulled successfully"
- name: Sync compose file and deploy
run: |
set -e
IMAGE_TAG="${{ inputs.image_tag }}"
mkdir -p "${DEPLOY_DIR}"
cp fe/web/docker-compose.yml "${DEPLOY_DIR}/docker-compose.yml"
cd "${DEPLOY_DIR}"
if [ ! -f .env ]; then
echo "ERROR: ${DEPLOY_DIR}/.env is missing on the server"
echo "Create it from fe/web/.env.example (or fe/web/.env.production) before deploying."
exit 1
fi
export IMAGE_TAG
export ADMIN_IMAGE
export TELLER_IMAGE
export CUSTOMER_IMAGE
docker compose -f docker-compose.yml pull admin teller customer
docker compose -f docker-compose.yml up -d --remove-orphans
echo "✓ Deployed IMAGE_TAG=${IMAGE_TAG}"
- name: Verify deployment
run: |
set -e
cd "${DEPLOY_DIR}"
echo "Waiting for services to start..."
sleep 8
for SERVICE in admin teller customer; do
STATUS=$(docker compose -f docker-compose.yml ps --status running --format '{{.Name}}' "$SERVICE" 2>/dev/null || true)
if [ -z "$STATUS" ]; then
echo "ERROR: ${SERVICE} is not running"
docker compose -f docker-compose.yml ps
docker compose -f docker-compose.yml logs --tail=50 "$SERVICE" || true
exit 1
fi
echo "✓ ${SERVICE} is running (${STATUS})"
done
ADMIN_HOST_PORT=$(grep -E '^ADMIN_HOST_PORT=' .env 2>/dev/null | cut -d= -f2- | tr -d '"' || true)
TELLER_HOST_PORT=$(grep -E '^TELLER_HOST_PORT=' .env 2>/dev/null | cut -d= -f2- | tr -d '"' || true)
CUSTOMER_HOST_PORT=$(grep -E '^CUSTOMER_HOST_PORT=' .env 2>/dev/null | cut -d= -f2- | tr -d '"' || true)
ADMIN_HOST_PORT=${ADMIN_HOST_PORT:-5000}
TELLER_HOST_PORT=${TELLER_HOST_PORT:-3001}
CUSTOMER_HOST_PORT=${CUSTOMER_HOST_PORT:-3000}
check_http() {
NAME=$1
PORT=$2
for i in 1 2 3 4 5 6; do
CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:${PORT}/" || echo "000")
if [ "${CODE}" != "000" ]; then
echo "✓ ${NAME} is responding on port ${PORT} (HTTP ${CODE})"
return 0
fi
echo "Waiting for ${NAME}... attempt ${i}/6"
sleep 3
done
echo "ERROR: ${NAME} is not responding on port ${PORT}"
return 1
}
check_http "admin" "${ADMIN_HOST_PORT}"
check_http "teller" "${TELLER_HOST_PORT}"
check_http "customer" "${CUSTOMER_HOST_PORT}"
-7
View File
@@ -51,10 +51,3 @@ The backend of the queue management system will be developed using Java's Spring
- [Ahmed Ljubuncic](https://github.com/aljubuncic) - [Ahmed Ljubuncic](https://github.com/aljubuncic)
- [Vedran Mujic](https://github.com/vmujic1) - [Vedran Mujic](https://github.com/vmujic1)
- [Amar Tahirovic](https://github.com/amarderschrecklicher) - [Amar Tahirovic](https://github.com/amarderschrecklicher)
## API ENV GUIDE
VITE_API_URL: API for teller and admin (also customer fallback in CI)
NEXT_PUBLIC_API_URL: API for customer
VITE_BRANCH_QR_BASE_URL: public customer base URL encoded into admin branch QR
Match these in Gitea secrets and in `fe/web/.env` on the deploy host (ports/images only matter at deploy; API URLs matter at image build).
-10
View File
@@ -1,10 +0,0 @@
target/
uploads/
.git/
.gitignore
.idea/
*.iml
.vscode/
*.md
.env
.DS_Store
-14
View File
@@ -1,14 +0,0 @@
# Host ports (change these if they collide on your single server)
BACKEND_HOST_PORT=8080
# Published for DBeaver/tools (container still uses 3306 internally)
MYSQL_HOST_PORT=3307
# MySQL
# Quote if the password contains special chars like ; # $
MYSQL_ROOT_PASSWORD="password"
MYSQL_DATABASE=qms
# App
JWT_SECRET_KEY=change-me-to-a-long-random-secret
GOOGLE_CLIENT_ID=dummy-google-client-id
NOTIFICATIONS_MOCK=true
-13
View File
@@ -1,13 +0,0 @@
# Host ports (change these if they collide on your single server)
BACKEND_HOST_PORT=8080
# Published for DBeaver/tools (container still uses 3306 internally)
MYSQL_HOST_PORT=3307
# MySQL
MYSQL_ROOT_PASSWORD="R8qjjBJDg9NtL2IwcnoFMkn8pktwAZ;wsl"
MYSQL_DATABASE=qms
# App
JWT_SECRET_KEY=a68uiaDQ0V3iLjF4DqMuS13GAVwkut55dlFbGCLyXTF
GOOGLE_CLIENT_ID=dummy-google-client-id
NOTIFICATIONS_MOCK=true
-3
View File
@@ -34,6 +34,3 @@ build/
### Uploads ### ### Uploads ###
uploads/ uploads/
### Docker / env ###
.env
+3 -27
View File
@@ -1,27 +1,3 @@
# Build stage FROM openjdk:17-jdk-alpine
# Non-alpine tags are multi-arch (amd64 + arm64). Alpine Maven often has no Mac/ARM image. COPY target/bbqms-0.0.1-SNAPSHOT.jar ./app.jar
FROM maven:3.9-eclipse-temurin-17 AS build ENTRYPOINT ["java", "-jar", "/app.jar"]
WORKDIR /app
COPY pom.xml .
COPY src ./src
# Use image mvn (not ./mvnw). Official maven image sets MAVEN_CONFIG=/root/.m2,
# which breaks the wrapper with: Unknown lifecycle phase "/root/.m2"
RUN mvn -q -DskipTests package
# Runtime stage
FROM eclipse-temurin:17-jre
WORKDIR /app
RUN groupadd -r qms && useradd -r -g qms qms \
&& mkdir -p /app/uploads/ads \
&& chown -R qms:qms /app
COPY --from=build /app/target/bbqms-0.0.1-SNAPSHOT.jar app.jar
USER qms
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
+4 -33
View File
@@ -1,38 +1,9 @@
## Build instructions (local) ## Build instructions
- Enter your database details in the **src/main/resources/application.yml** file (or use env vars) - Enter your database details in the **src/main/resources/application.yml** file
- Through IntelliJ simply click on the run button - Through IntelliJ simply click on the run button
- Or through console - Or through console
- `./mvnw dependency:resolve` - ./mvnw dependency:resolve
- `./mvnw spring-boot:run` - ./mvnw spring-boot:run
##### **NOTE:** requires Java 17 and MySQL 8 ##### **NOTE:** requires Java 17 and MySQL 8
## Docker deployment (single server)
From the `be/` directory:
```bash
cp .env.example .env
# Edit .env if ports 8080 / 3306 are already in use on the server
docker compose up -d --build
```
Services and default host ports:
| Service | Container | Host port (configurable) |
|----------|-----------|---------------------------|
| Backend | qms-backend | `BACKEND_HOST_PORT`**8080** |
| MySQL | qms-mysql | `MYSQL_HOST_PORT`**3306** |
API base URL on the server: `http://<server-ip>:8080`
Useful commands:
```bash
docker compose ps
docker compose logs -f backend
docker compose down # stop containers (keeps DB volume)
docker compose down -v # stop and delete DB/uploads volumes
```
+5 -50
View File
@@ -2,63 +2,18 @@ services:
mysql: mysql:
image: mysql:8 image: mysql:8
container_name: qms-mysql container_name: qms-mysql
restart: unless-stopped
ports: ports:
- "${MYSQL_HOST_PORT:-3307}:3306" - "3306:3306"
environment: environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-password} MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: ${MYSQL_DATABASE:-qms} MYSQL_DATABASE: qms
volumes: volumes:
- qms-mysql-data:/var/lib/mysql - qms-mysql-data:/var/lib/mysql
healthcheck: healthcheck:
test: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-ppassword"]
[
"CMD",
"mysqladmin",
"ping",
"-h",
"localhost",
"-p${MYSQL_ROOT_PASSWORD:-password}",
]
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 20 retries: 10
start_period: 20s
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: volumes:
qms-mysql-data: qms-mysql-data:
qms-uploads:
networks:
qms-net:
driver: bridge
-64
View File
@@ -1,64 +0,0 @@
services:
mysql:
image: mysql:8
container_name: qms-mysql
restart: unless-stopped
ports:
- "${MYSQL_HOST_PORT:-3307}:3306"
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-password}
MYSQL_DATABASE: ${MYSQL_DATABASE:-qms}
volumes:
- qms-mysql-data:/var/lib/mysql
healthcheck:
test:
[
"CMD",
"mysqladmin",
"ping",
"-h",
"localhost",
"-p${MYSQL_ROOT_PASSWORD:-password}",
]
interval: 5s
timeout: 5s
retries: 20
start_period: 20s
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:
driver: bridge
+7 -10
View File
@@ -1,6 +1,3 @@
server:
port: ${SERVER_PORT:8080}
spring: spring:
application: application:
name: qms name: qms
@@ -9,9 +6,9 @@ spring:
hibernate: hibernate:
ddl-auto: none ddl-auto: none
datasource: datasource:
url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/qms?allowPublicKeyRetrieval=true&useSSL=false} url: jdbc:mysql://localhost:3306/qms
username: ${SPRING_DATASOURCE_USERNAME:root} username: root
password: ${SPRING_DATASOURCE_PASSWORD:password} password: password
servlet: servlet:
multipart: multipart:
max-file-size: 100MB max-file-size: 100MB
@@ -21,14 +18,14 @@ spring:
client: client:
registration: registration:
google: google:
client-id: ${GOOGLE_CLIENT_ID:dummy-google-client-id} client-id: dummy-google-client-id
flyway: flyway:
schemas: qms schemas: qms
jwt: jwt:
header-title: Authorization header-title: Authorization
token-prefix: Bearer token-prefix: Bearer
secret-key: ${JWT_SECRET_KEY:a68uiaDQ0V3iLjF4DqMuS13GAVwkut55dlFbGCLyXTF} secret-key: a68uiaDQ0V3iLjF4DqMuS13GAVwkut55dlFbGCLyXTF
authorities-key: USER_AUTHORITIES authorities-key: USER_AUTHORITIES
token-validity-time: PT30M token-validity-time: PT30M
tfa: tfa:
@@ -38,6 +35,6 @@ tenancy:
default-code: DFLT default-code: DFLT
notifications: notifications:
expo-url: https://exp.host/--/api/v2/push/send expo-url: https://exp.host/--/api/v2/push/send
mock: ${NOTIFICATIONS_MOCK:true} mock: true
ads: ads:
upload-dir: ${ADS_UPLOAD_DIR:uploads/ads} upload-dir: uploads/ads
-15
View File
@@ -1,15 +0,0 @@
# Host ports (nginx terminates TLS on 443 and proxies to these)
ADMIN_HOST_PORT=5000
TELLER_HOST_PORT=3001
CUSTOMER_HOST_PORT=3000
VITE_API_URL=https://qms-api.erahn.com.my
NEXT_PUBLIC_API_URL=https://qms-api.erahn.com.my
VITE_BRANCH_QR_BASE_URL=https://qms-customer.erahn.com.my
QR_TOKEN_SECRET=qms-branch-qr-v1
ADMIN_IMAGE=git.koppkb.com/kopkb/qms-fe-admin
TELLER_IMAGE=git.koppkb.com/kopkb/qms-fe-teller
CUSTOMER_IMAGE=git.koppkb.com/kopkb/qms-fe-customer
IMAGE_TAG=local
-15
View File
@@ -1,15 +0,0 @@
ADMIN_HOST_PORT=5000
TELLER_HOST_PORT=3001
CUSTOMER_HOST_PORT=3000
VITE_API_URL=https://qms-api.erahn.com.my
NEXT_PUBLIC_API_URL=https://qms-api.erahn.com.my
VITE_BRANCH_QR_BASE_URL=https://qms-customer.erahn.com.my
VITE_GOLD_PRICE_URL=https://apiujrah.erahn.com.my/api/harga_emas
QR_TOKEN_SECRET=qms-branch-qr-v1
ADMIN_IMAGE=git.koppkb.com/kopkb/qms-fe-admin
TELLER_IMAGE=git.koppkb.com/kopkb/qms-fe-teller
CUSTOMER_IMAGE=git.koppkb.com/kopkb/qms-fe-customer
IMAGE_TAG=local
-15
View File
@@ -1,15 +0,0 @@
ADMIN_HOST_PORT=5000
TELLER_HOST_PORT=3001
CUSTOMER_HOST_PORT=3000
VITE_API_URL=https://qms-api.erahn.com.my
NEXT_PUBLIC_API_URL=https://qms-api.erahn.com.my
VITE_BRANCH_QR_BASE_URL=https://qms-customer.erahn.com.my
VITE_GOLD_PRICE_URL=https://apiujrah.erahn.com.my/api/harga_emas
QR_TOKEN_SECRET=qms-branch-qr-v1
ADMIN_IMAGE=git.koppkb.com/kopkb/qms-fe-admin
TELLER_IMAGE=git.koppkb.com/kopkb/qms-fe-teller
CUSTOMER_IMAGE=git.koppkb.com/kopkb/qms-fe-customer
IMAGE_TAG=local
-3
View File
@@ -1,3 +0,0 @@
.env
!.env.example
!.env.production
-8
View File
@@ -1,8 +0,0 @@
node_modules
dist
.git
.gitignore
*.md
.env
.env.*
.DS_Store
-2
View File
@@ -1,2 +0,0 @@
VITE_API_URL=http://localhost:8080
VITE_BRANCH_QR_BASE_URL=http://localhost:3000
-2
View File
@@ -1,2 +0,0 @@
VITE_API_URL=https://qms-api.erahn.com.my
VITE_BRANCH_QR_BASE_URL=https://qms-customer.erahn.com.my
-2
View File
@@ -1,2 +0,0 @@
VITE_API_URL=https://qms-api.erahn.com.my
VITE_BRANCH_QR_BASE_URL=https://qms-customer.erahn.com.my
+6 -17
View File
@@ -1,21 +1,10 @@
# Build stage FROM node:21-alpine AS build
FROM node:20-bookworm AS build WORKDIR /admin-app
WORKDIR /app COPY package*.json .
RUN npm install
ARG VITE_API_URL=http://localhost:8080
ARG VITE_BRANCH_QR_BASE_URL=http://localhost:3000
ENV VITE_API_URL=$VITE_API_URL
ENV VITE_BRANCH_QR_BASE_URL=$VITE_BRANCH_QR_BASE_URL
COPY package.json package-lock.json ./
RUN npm ci
COPY . . COPY . .
RUN npm run build RUN npm run build
# Runtime stage — static SPA EXPOSE 5001
FROM nginx:alpine CMD ["npm", "run", "preview"]
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
-10
View File
@@ -1,10 +0,0 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
+3 -304
View File
@@ -12,7 +12,6 @@
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"bootstrap-icons": "^1.11.3", "bootstrap-icons": "^1.11.3",
"formik": "^2.4.5", "formik": "^2.4.5",
"qrcode": "^1.5.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-bootstrap": "^2.10.2", "react-bootstrap": "^2.10.2",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@@ -23,7 +22,6 @@
"yup": "^1.4.0" "yup": "^1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@types/qrcode": "^1.5.6",
"@types/react": "^18.2.64", "@types/react": "^18.2.64",
"@types/react-dom": "^18.2.21", "@types/react-dom": "^18.2.21",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
@@ -1297,31 +1295,11 @@
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
"integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==" "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA=="
}, },
"node_modules/@types/node": {
"version": "26.1.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
"integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~8.3.0"
}
},
"node_modules/@types/prop-types": { "node_modules/@types/prop-types": {
"version": "15.7.11", "version": "15.7.11",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
}, },
"node_modules/@types/qrcode": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
"integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.2.66", "version": "18.2.66",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.66.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.66.tgz",
@@ -1433,6 +1411,7 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -1716,15 +1695,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001597", "version": "1.0.30001597",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz",
@@ -1764,17 +1734,6 @@
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
}, },
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -1904,15 +1863,6 @@
} }
} }
}, },
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/deep-is": { "node_modules/deep-is": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -1969,12 +1919,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT"
},
"node_modules/doctrine": { "node_modules/doctrine": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -2010,12 +1954,6 @@
"integrity": "sha512-iWgEEvREL4GTXXHKohhh33+6Y8XkPI5eHihDmm8zUk5Zo7HICEW+wI/j5kJ2tbuNUCXJ/sNXa03ajW635DiJXA==", "integrity": "sha512-iWgEEvREL4GTXXHKohhh33+6Y8XkPI5eHihDmm8zUk5Zo7HICEW+wI/j5kJ2tbuNUCXJ/sNXa03ajW635DiJXA==",
"dev": true "dev": true
}, },
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/es-abstract": { "node_modules/es-abstract": {
"version": "1.23.0", "version": "1.23.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.0.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.0.tgz",
@@ -2694,15 +2632,6 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
@@ -3113,15 +3042,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-generator-function": { "node_modules/is-generator-function": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
@@ -3711,15 +3631,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -3736,6 +3647,7 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -3770,15 +3682,6 @@
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"dev": true "dev": true
}, },
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/possible-typed-array-names": { "node_modules/possible-typed-array-names": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
@@ -3861,23 +3764,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"license": "MIT",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/queue-microtask": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -4095,21 +3981,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC"
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "2.0.0-next.5", "version": "2.0.0-next.5",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
@@ -4324,12 +4195,6 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/set-function-length": { "node_modules/set-function-length": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -4410,20 +4275,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string.prototype.matchall": { "node_modules/string.prototype.matchall": {
"version": "4.0.10", "version": "4.0.10",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz",
@@ -4493,6 +4344,7 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"
}, },
@@ -4697,13 +4549,6 @@
"react": ">=15.0.0" "react": ">=15.0.0"
} }
}, },
"node_modules/undici-types": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
"dev": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.0.13", "version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
@@ -4889,12 +4734,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
"node_modules/which-typed-array": { "node_modules/which-typed-array": {
"version": "1.1.15", "version": "1.1.15",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz",
@@ -4914,158 +4753,18 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/wrap-ansi/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/wrappy": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true "dev": true
}, },
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true "dev": true
}, },
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/yargs/node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yargs/node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+6 -8
View File
@@ -1,11 +1,9 @@
export const SERVER_URL = export const SERVER_URL = 'http://localhost:8080';
import.meta.env.VITE_API_URL ?? 'http://localhost:8080';
/** Public base URL encoded into branch QR codes (ManageBranchesScreen only). */ export const CUSTOMER_APP_URL =
export const BRANCH_QR_BASE_URL = import.meta.env.VITE_CUSTOMER_APP_URL ?? 'http://localhost:3000';
import.meta.env.VITE_BRANCH_QR_BASE_URL ?? 'http://localhost:3000';
export const ROLES = { export const ROLES = {
ROLE_SUPER_ADMIN: 'ROLE_SUPER_ADMIN', ROLE_SUPER_ADMIN : "ROLE_SUPER_ADMIN",
ROLE_BRANCH_ADMIN: 'ROLE_BRANCH_ADMIN', ROLE_BRANCH_ADMIN : "ROLE_BRANCH_ADMIN"
}; }
+2 -2
View File
@@ -1,5 +1,5 @@
import QRCode from 'qrcode'; import QRCode from 'qrcode';
import { BRANCH_QR_BASE_URL } from '../constants.js'; import { CUSTOMER_APP_URL } from '../constants.js';
const DEFAULT_QR_SECRET = 'qms-branch-qr-v1'; const DEFAULT_QR_SECRET = 'qms-branch-qr-v1';
@@ -54,7 +54,7 @@ export function decodeBranchQrToken(token, secret = getQrSecret()) {
} }
export function getBranchTicketUrl(tenantCode, branchId) { export function getBranchTicketUrl(tenantCode, branchId) {
const base = BRANCH_QR_BASE_URL.replace(/\/$/, ''); const base = CUSTOMER_APP_URL.replace(/\/$/, '');
const token = encodeBranchQrToken(tenantCode, branchId); const token = encodeBranchQrToken(tenantCode, branchId);
return `${base}/q/${token}`; return `${base}/q/${token}`;
} }
-10
View File
@@ -1,10 +0,0 @@
node_modules
.next
.git
.gitignore
*.md
.env
.env.*
!.env.example
.DS_Store
tsconfig.tsbuildinfo
-35
View File
@@ -1,35 +0,0 @@
# Build stage
FROM node:20-bookworm AS build
WORKDIR /app
RUN corepack enable && corepack prepare pnpm@9 --activate
ARG NEXT_PUBLIC_API_URL=http://localhost:8080
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
# Runtime stage
FROM node:20-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs
COPY --from=build /app/public ./public
COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
-1
View File
@@ -2,7 +2,6 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
devIndicators: false, devIndicators: false,
output: "standalone",
}; };
export default nextConfig; export default nextConfig;
-6
View File
@@ -22,11 +22,5 @@
"eslint-config-next": "16.2.10", "eslint-config-next": "16.2.10",
"tailwindcss": "^4", "tailwindcss": "^4",
"typescript": "^5" "typescript": "^5"
},
"pnpm": {
"ignoredBuiltDependencies": [
"sharp",
"unrs-resolver"
]
} }
} }
+3
View File
@@ -0,0 +1,3 @@
ignoredBuiltDependencies:
- sharp
- unrs-resolver
-40
View File
@@ -1,40 +0,0 @@
services:
admin:
image: ${ADMIN_IMAGE:-qms-fe-admin}:${IMAGE_TAG:-local}
build:
context: ./admin-app
dockerfile: Dockerfile
args:
VITE_API_URL: ${VITE_API_URL:-http://localhost:8080}
VITE_BRANCH_QR_BASE_URL: ${VITE_BRANCH_QR_BASE_URL:-http://localhost:3000}
container_name: qms-fe-admin
restart: unless-stopped
ports:
- "${ADMIN_HOST_PORT:-5000}:80"
teller:
image: ${TELLER_IMAGE:-qms-fe-teller}:${IMAGE_TAG:-local}
build:
context: ./teller-app
dockerfile: Dockerfile
args:
VITE_API_URL: ${VITE_API_URL:-http://localhost:8080}
VITE_GOLD_PRICE_URL: ${VITE_GOLD_PRICE_URL:-https://apiujrah.erahn.com.my/api/harga_emas}
container_name: qms-fe-teller
restart: unless-stopped
ports:
- "${TELLER_HOST_PORT:-3001}:80"
customer:
image: ${CUSTOMER_IMAGE:-qms-fe-customer}:${IMAGE_TAG:-local}
build:
context: ./customer-app
dockerfile: Dockerfile
args:
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8080}
container_name: qms-fe-customer
restart: unless-stopped
ports:
- "${CUSTOMER_HOST_PORT:-3000}:3000"
environment:
QR_TOKEN_SECRET: ${QR_TOKEN_SECRET:-qms-branch-qr-v1}
-8
View File
@@ -1,8 +0,0 @@
node_modules
dist
.git
.gitignore
*.md
.env
.env.*
.DS_Store
+6 -18
View File
@@ -1,22 +1,10 @@
# Build stage FROM node:21-alpine AS build
FROM node:20-bookworm AS build WORKDIR /teller-app
WORKDIR /app COPY package*.json .
RUN npm install
ARG VITE_API_URL=http://localhost:8080
ARG VITE_GOLD_PRICE_URL=https://apiujrah.erahn.com.my/api/harga_emas
ENV VITE_API_URL=$VITE_API_URL
ENV VITE_GOLD_PRICE_URL=$VITE_GOLD_PRICE_URL
COPY package.json package-lock.json ./
RUN npm ci
COPY . . COPY . .
RUN npm run build RUN npm run build
# Runtime stage — static SPA EXPOSE 3000
FROM nginx:alpine CMD ["npm", "run", "preview"]
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
-10
View File
@@ -1,10 +0,0 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
+4 -2
View File
@@ -1,6 +1,8 @@
export const SERVER_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080'; export const SERVER_URL = 'http://localhost:8080';
export const GOLD_PRICE_URL = import.meta.env.VITE_GOLD_PRICE_URL || 'https://apiujrah.erahn.com.my/api/harga_emas'; export const GOLD_PRICE_URL =
import.meta.env.VITE_GOLD_PRICE_URL ??
'https://apiujrah.erahn.com.my/api/harga_emas';
export const ROLES = { export const ROLES = {
ROLE_USER: 'ROLE_USER', ROLE_USER: 'ROLE_USER',
@@ -76,41 +76,22 @@ 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 = false; video.muted = true;
video.playsInline = true; video.playsInline = true;
const playPromise = video.play(); const playPromise = video.play();
if (playPromise?.catch) { if (playPromise?.catch) {
playPromise.catch(() => { playPromise.catch(() => {
// Unmuted autoplay blocked — keep video playing muted, then unmute on gesture. // Autoplay blocked — advance after durationSeconds fallback
video.muted = true; clearTimer();
const mutedPlay = video.play(); const durationMs = Math.max(5, Number(currentAd.durationSeconds) || 15) * 1000;
if (mutedPlay?.catch) { timerRef.current = setTimeout(goNext, durationMs);
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]);
@@ -146,6 +127,7 @@ 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
/> />
@@ -32,6 +32,23 @@
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;
@@ -29,6 +29,7 @@ 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);
@@ -191,7 +192,29 @@ export default function BranchDisplayPage() {
); );
return ( return (
<div className="branch-display" onClick={unlockQueueCallSound}> <div
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