3 Commits

25 changed files with 344 additions and 851 deletions
-5
View File
@@ -1,10 +1,6 @@
.git
.gitea
node_modules
vendor
.env
.env.*
!.env.example
storage/logs
storage/framework/cache
storage/framework/sessions
@@ -13,4 +9,3 @@ storage/debugbar
npm-debug.log
Dockerfile
docker-compose.yml
docker-compose.staging.yml
-55
View File
@@ -1,55 +0,0 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:O1XZDqK4H6+3GdrODgCaLr2l+OoWvgVhYr0GJ1KizW8=
APP_DEBUG=true
APP_URL=http://localhost
# API_URL=http://api-annurv2.test
API_URL=http://host.docker.internal:8080
# Server-side Http:: from Docker — same rules as API_URL. Defaults in config/api.php use localhost:8000 which fails inside containers.
API_URL_KOOP=http://host.docker.internal:8080
API_URL_PKB=http://host.docker.internal:8080
API_ARRAHNBID=http://lelong.test/api/
LOG_CHANNEL=stack
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=kaunter_erahn
DB_USERNAME=kaunteruser
DB_PASSWORD=password
BROADCAST_DRIVER=pusher
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
PUSHER_APP_ID=1132718
PUSHER_APP_KEY=26b0ec2f297f94b7aa9b
PUSHER_APP_SECRET=421b61fb5a8ecce477a6
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=8085
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
+1 -1
View File
@@ -46,5 +46,5 @@ AWS_BUCKET=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=17321
MYKAD_READER_PORT=8085
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
-58
View File
@@ -1,58 +0,0 @@
APP_NAME=Laravel
APP_ENV=staging
APP_VERSION=v2.2-staging
APP_KEY=base64:O1XZDqK4H6+3GdrODgCaLr2l+OoWvgVhYr0GJ1KizW8=
APP_DEBUG=true
APP_URL=https://staging.erahn.com.my
HTTP_PORT=8091
MYSQL_PUBLISH_PORT=3309
API_URL=https://api-staging.erahn.com.my
API_URL_KOOP=https://api-staging.erahn.com.my
API_URL_PKB=https://api-staging.erahn.com.my
API_ARRAHNBID=http://lelong.test/api/
LOG_CHANNEL=stack
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=kaunter_erahn
DB_USERNAME=kaunteruser
DB_PASSWORD=password
BROADCAST_DRIVER=pusher
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
PUSHER_APP_ID=1132718
PUSHER_APP_KEY=26b0ec2f297f94b7aa9b
PUSHER_APP_SECRET=421b61fb5a8ecce477a6
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=17321
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
-49
View File
@@ -1,49 +0,0 @@
name: Build Docker Image
on:
push:
branches:
- staging
tags:
- "v*"
workflow_dispatch:
jobs:
build-frontend:
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 frontend image
run: |
set -e
IMAGE="git.koppkb.com/kopkb/kaunter"
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
if [ "${{ gitea.ref_name }}" = "staging" ]; then
TAGS="${TAGS} -t ${IMAGE}:staging"
fi
echo "Building and pushing frontend: ${TAGS}"
docker buildx build \
--target staging \
--platform linux/amd64 \
--cache-from type=registry,ref=${IMAGE}:buildcache,ignore-error=true \
--cache-to type=registry,ref=${IMAGE}:buildcache,mode=max \
-f docker/php/Dockerfile \
${TAGS} \
--push .
-123
View File
@@ -1,123 +0,0 @@
name: Deploy to Staging
on:
workflow_dispatch:
inputs:
image_tag:
description: "Docker image tag to deploy (commit SHA, staging, or v*)"
required: true
default: "staging"
type: string
env:
APP_IMAGE: git.koppkb.com/kopkb/kaunter
DEPLOY_DIR: /home/arrahn/Project/kaunter
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 image exists in registry
run: |
set -e
IMAGE_TAG="${{ inputs.image_tag }}"
IMAGE="${APP_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!"
echo "Run the Build Docker Image workflow first."
exit 1
fi
echo "✓ ${IMAGE} found"
- name: Pull Docker image
run: |
set -e
IMAGE_TAG="${{ inputs.image_tag }}"
docker pull "${APP_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}/docker/nginx" "${DEPLOY_DIR}/public"
cp docker-compose.staging.yml "${DEPLOY_DIR}/docker-compose.yml"
cp docker/nginx/default.conf "${DEPLOY_DIR}/docker/nginx/default.conf"
find "${DEPLOY_DIR}/public" -mindepth 1 -delete
cp -R public/. "${DEPLOY_DIR}/public/"
if [ ! -f "${DEPLOY_DIR}/.env.staging" ]; then
if [ -f .env.staging ]; then
cp .env.staging "${DEPLOY_DIR}/.env.staging"
echo "Copied .env.staging from repo (first deploy). Edit ${DEPLOY_DIR}/.env.staging on the server if needed."
else
echo "ERROR: ${DEPLOY_DIR}/.env.staging is missing on the server"
exit 1
fi
fi
cd "${DEPLOY_DIR}"
export APP_IMAGE
export IMAGE_TAG
docker compose --env-file .env.staging -f docker-compose.yml pull app
docker compose --env-file .env.staging -f docker-compose.yml up -d --no-build --force-recreate --remove-orphans app web
echo "✓ Deployed IMAGE_TAG=${IMAGE_TAG}"
- name: Verify deployment
run: |
set -e
cd "${DEPLOY_DIR}"
echo "Waiting for services to start..."
sleep 10
for SERVICE in app web mysql; do
STATUS=$(docker compose --env-file .env.staging -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 --env-file .env.staging -f docker-compose.yml ps
docker compose --env-file .env.staging -f docker-compose.yml logs --tail=50 "$SERVICE" || true
exit 1
fi
echo "✓ ${SERVICE} is running (${STATUS})"
done
HTTP_PORT=$(grep -E '^HTTP_PORT=' .env.staging 2>/dev/null | cut -d= -f2- | tr -d '"' || true)
HTTP_PORT=${HTTP_PORT:-8091}
check_health() {
NAME=$1
URL=$2
for i in 1 2 3 4 5 6; do
if curl -sf "$URL" > /dev/null; then
echo "✓ ${NAME} health check passed (${URL})"
return 0
fi
echo "Waiting for ${NAME}... attempt ${i}/6"
sleep 5
done
echo "ERROR: ${NAME} health check failed (${URL})"
docker compose --env-file .env.staging -f docker-compose.yml logs --tail=50 app web || true
return 1
}
check_health "kaunter" "http://127.0.0.1:${HTTP_PORT}/health"
+1 -1
View File
@@ -12,7 +12,7 @@ class TrustProxies extends Middleware
*
* @var array|string|null
*/
protected $proxies = '*';
protected $proxies;
/**
* The headers that should be used to detect proxies.
+1 -3
View File
@@ -23,8 +23,6 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot()
{
if (strpos((string) config('app.url'), 'https://') === 0) {
\Illuminate\Support\Facades\URL::forceScheme('https');
}
//
}
}
-2
View File
@@ -15,8 +15,6 @@ return [
'name' => env('APP_NAME', 'Laravel'),
'version' => env('APP_VERSION', ''),
/*
|--------------------------------------------------------------------------
| Application Environment
+2 -2
View File
@@ -1,7 +1,7 @@
<?php
$default_port = '17321';
$getReaderEndpoint = fn($x) => 'http://localhost:' . $x . '/read';
$default_port = '8085';
$getReaderEndpoint = fn($x) => 'http://localhost:' . $x . '/?MYKADJSON';
return[
'endpoint' => env('MYKAD_READER_ENDPOINT', $getReaderEndpoint(env('MYKAD_READER_PORT', $default_port))),
-62
View File
@@ -1,62 +0,0 @@
name: kaunter_staging
services:
app:
image: ${APP_IMAGE:-git.koppkb.com/kopkb/kaunter}:${IMAGE_TAG:-staging}
build:
context: .
dockerfile: docker/php/Dockerfile
target: staging
container_name: kaunter_staging_app
working_dir: /var/www/html
volumes:
- storage_staging:/var/www/html/storage
- ./.env.staging:/var/www/html/.env.staging:ro
env_file:
- .env.staging
environment:
APP_ENV: staging
USE_STAGING_ENV: "true"
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
mysql:
condition: service_healthy
restart: unless-stopped
web:
image: nginx:1.25-alpine
container_name: kaunter_staging_web
ports:
- "${HTTP_PORT:-8091}:80"
volumes:
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./public:/var/www/html/public:ro
depends_on:
- app
restart: unless-stopped
mysql:
image: mysql:8.0
container_name: kaunter_staging_mysql
command: --default-authentication-plugin=mysql_native_password
ports:
- "127.0.0.1:${MYSQL_PUBLISH_PORT:-3309}:3306"
environment:
MYSQL_DATABASE: kaunter_erahn
MYSQL_USER: kaunteruser
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: root
volumes:
- mysql_staging_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"]
interval: 5s
timeout: 5s
retries: 30
start_period: 20s
restart: unless-stopped
volumes:
mysql_staging_data:
storage_staging:
-1
View File
@@ -3,7 +3,6 @@ services:
build:
context: .
dockerfile: docker/php/Dockerfile
target: development
working_dir: /var/www/html
volumes:
- ./:/var/www/html:cached
+2 -15
View File
@@ -6,22 +6,9 @@ server {
index index.php index.html;
charset utf-8;
client_max_body_size 50M;
location / {
# Production Apache public/.htaccess 301-strips trailing slashes.
# Nginx ignores .htaccess, so without this /path/ 404s while /path works.
rewrite ^/(.*)/$ /$1 last;
try_files $uri @php;
}
location @php {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_pass app:9000;
fastcgi_index index.php;
fastcgi_read_timeout 120s;
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
@@ -30,10 +17,10 @@ server {
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_pass app:9000;
fastcgi_index index.php;
fastcgi_read_timeout 120s;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
+16 -37
View File
@@ -1,51 +1,30 @@
FROM php:7.4-fpm AS base
FROM php:7.4-fpm
WORKDIR /var/www/html
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
git \
pkg-config \
unzip \
libonig-dev \
libzip-dev \
libpng-dev \
libjpeg62-turbo-dev \
libfreetype6-dev \
git \
pkg-config \
unzip \
libonig-dev \
libzip-dev \
libpng-dev \
libjpeg62-turbo-dev \
libfreetype6-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j"$(nproc)" \
pdo_mysql \
mbstring \
bcmath \
zip \
exif \
gd \
pdo_mysql \
mbstring \
bcmath \
zip \
exif \
gd \
&& rm -rf /var/lib/apt/lists/*
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
COPY docker/php/php.ini /usr/local/etc/php/conf.d/app.ini
COPY docker/php/zz-clear-env.conf /usr/local/etc/php-fpm.d/zz-clear-env.conf
COPY docker/php/entrypoint.sh /usr/local/bin/app-entrypoint.sh
RUN chmod +x /usr/local/bin/app-entrypoint.sh \
&& usermod -u 1000 www-data && groupmod -g 1000 www-data
RUN usermod -u 1000 www-data && groupmod -g 1000 www-data
ENTRYPOINT ["app-entrypoint.sh"]
CMD ["php-fpm"]
FROM base AS staging
COPY --chown=www-data:www-data . /var/www/html
RUN composer install --no-interaction --prefer-dist --no-dev --optimize-autoloader --no-scripts \
&& mkdir -p \
storage/logs \
storage/framework/cache/data \
storage/framework/views \
storage/framework/sessions \
storage/app/public \
bootstrap/cache \
&& chown -R www-data:www-data storage bootstrap/cache vendor
FROM base AS development
-35
View File
@@ -1,35 +0,0 @@
#!/bin/sh
set -e
cd /var/www/html
if [ "${USE_STAGING_ENV:-false}" = "true" ] && [ -f .env.staging ]; then
cp .env.staging .env
fi
if [ -f composer.json ]; then
if [ -f vendor/autoload.php ] && [ "${USE_STAGING_ENV:-false}" = "true" ]; then
: # vendor is baked into the staging image
elif [ "${USE_STAGING_ENV:-false}" = "true" ]; then
composer install --no-interaction --prefer-dist --no-dev --optimize-autoloader --no-scripts
else
composer install --no-interaction --prefer-dist
fi
fi
mkdir -p \
storage/logs \
storage/framework/cache/data \
storage/framework/views \
storage/framework/sessions \
storage/app/public \
bootstrap/cache
if [ -f artisan ]; then
php artisan package:discover --ansi
php artisan storage:link --force >/dev/null 2>&1 || true
fi
chown -R www-data:www-data storage bootstrap/cache
exec docker-php-entrypoint "$@"
+1 -1
View File
@@ -1,4 +1,4 @@
memory_limit=512M
upload_max_filesize=50M
post_max_size=50M
max_execution_time=120
max_execution_time=120
-3
View File
@@ -1,3 +0,0 @@
[www]
; Pass container env vars (from compose env_file) through to PHP workers.
clear_env = no
+254 -332
View File
@@ -1,371 +1,293 @@
let button_read_mykad = document.getElementById("button_read_mykad");
let button_close_modal = document.querySelector("#daftar_pelanggan_mykad_reader button.btn.btn-danger");
let photo_mykad = document.querySelector("#photo-mykad");
let original_photo_loader = photo_mykad ? photo_mykad.src : "";
let original_photo_loader = document.querySelector("#photo-mykad").src;
const MYKAD_READER_TIMEOUT_MS = 45000;
const MYKAD_READER_DEFAULT_ENDPOINT = "http://localhost:17321/read";
let showLoader = () => {
let pageLoader = document.getElementById('page_loader');
if (!pageLoader) {
return;
}
pageLoader.removeAttribute('class');
pageLoader.classList.add('loading');
};
let hideLoader = () => {
let pageLoader = document.getElementById('page_loader');
if (!pageLoader) {
return;
}
pageLoader.removeAttribute('class');
};
function getMyKadReaderEndpoint() {
let endpoint = (typeof mykad_reader_endpoint !== 'undefined' && mykad_reader_endpoint)
? String(mykad_reader_endpoint).trim()
: '';
if (!endpoint || endpoint.includes('MYKADJSON') || endpoint.includes(':8085')) {
return MYKAD_READER_DEFAULT_ENDPOINT;
}
return endpoint;
}
async function parseJsonResponse(response) {
let text = await response.text();
let trimmed = (text || '').trim();
if (!trimmed) {
throw new Error('Pembaca MyKad tidak memulangkan data. Sila cuba lagi.');
}
if (trimmed.charAt(0) === '<') {
throw new Error('Pembaca MyKad memulangkan halaman web, bukan data kad. Sila pastikan perkhidmatan mykad-helper sedang berjalan di localhost:17321.');
}
try {
return JSON.parse(trimmed);
} catch (error) {
throw new Error('Pembaca MyKad memulangkan data yang tidak sah. Sila cuba lagi.');
}
}
function getMyKadNumberWithoutHyphens(nric) {
return String(nric || '').replace(/-/g, '');
}
function titleCase(value) {
return String(value || '')
.toLowerCase()
.split(' ')
.filter(Boolean)
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
function getCodeFromValue(needle, haystack, fallback) {
let normalised = titleCase(needle);
let match = haystack.find(item => item[1].toLowerCase() === normalised.toLowerCase());
return match ? match[0] : fallback;
}
function mapGender(gender) {
let value = String(gender || '').toLowerCase();
if (['male', 'lelaki', 'l', 'm'].includes(value)) {
return 'Lelaki';
}
if (['female', 'perempuan', 'p', 'f'].includes(value)) {
return 'Perempuan';
}
return gender || '';
}
function formatDateOfBirth(dob) {
if (!dob) {
return '';
}
let hyphenated = String(dob).includes('-')
? String(dob)
: String(dob).replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3');
let [year, month, day] = hyphenated.split('-');
if (!year || !month || !day) {
return dob;
}
return day + '/' + month + '/' + year;
}
function getAgeFromDateOfBirth(dob) {
if (!dob) {
return '';
}
let hyphenated = String(dob).includes('-')
? String(dob)
: String(dob).replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3');
let birthDate = new Date(hyphenated);
if (Number.isNaN(birthDate.getTime())) {
return '';
}
let today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
let monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age -= 1;
}
return age;
}
function getBumiputeraStatus(race) {
const non_bumiputera_ethnicity = ['INDIA', 'CINA', 'INDIAN', 'CHINESE'];
return !non_bumiputera_ethnicity.includes(String(race || '').toUpperCase());
}
function joinAddress(data) {
return [data.address1, data.address2, data.address3]
.map(line => String(line || '').trim())
.filter(Boolean)
.join(', ');
}
function mapReaderErrorCode(rawMessage) {
const error_handler_dictionary = {
'0x80100069': 'Tiada MyKad dikesan, sila masukkan MyKad untuk dibaca',
'0x8010000C': 'Tiada MyKad dikesan, sila masukkan MyKad untuk dibaca',
'0x80100066': 'MyKad mungkin dimasukkan terbalik, sila masukkan dengan betul',
'0x80100067': 'MyKad tidak dapat dikesan. Sila cabut dan masukkan semula kad',
'0x80100016': 'Gagal membaca MyKad. Sila pastikan kad dimasukkan dengan betul dan cuba lagi',
'0x8010002E': 'Pembaca MyKad tidak dapat dikesan, sila periksa sambungan',
'0x80100017': 'Pembaca MyKad tidak dapat dikesan, sila periksa sambungan',
let showLoader = () => {
let pageLoader = document.getElementById('page_loader');
pageLoader.removeAttribute('class');
pageLoader.classList.add('loading');
};
let message = String(rawMessage || '');
let codes = message.match(/0x[0-9a-fA-F]+/g) || [];
let matched = codes.reverse().find(code => error_handler_dictionary[code]);
let hideLoader = () => {
let pageLoader = document.getElementById('page_loader');
pageLoader.removeAttribute('class');
};
return matched ? error_handler_dictionary[matched] : (message || null);
}
function getReaderErrorMessage(payload) {
if (!payload) {
return 'Pembaca MyKad tidak dapat membaca kad. Sila cuba lagi.';
function handleMyKadReader()
{
showLoader();
WebSocketManager.init("wss://localhost:5963/mykadwebsocket");
WebSocketManager.listReader(callbackDoneGetListCardReader);
}
let raw = payload.message || payload.error || payload.detail || '';
return mapReaderErrorCode(raw)
|| 'Pembaca MyKad tidak dapat membaca kad. Sila pastikan MyKad dimasukkan dengan betul.';
}
function callbackDoneGetListCardReader(data, respcode)
{
console.log('done get list card reader');
function getFetchErrorTitle(error) {
if (error && error.name === 'AbortError') {
return 'Masa menunggu pembaca MyKad telah tamat. Sila cuba lagi.';
}
return 'Pembaca MyKad tidak dapat dikesan. Sila pastikan perkhidmatan pembaca MyKad sedang berjalan.';
}
function getDisplayError(error) {
if (!error) {
return getFetchErrorTitle(error);
}
if (error.name === 'AbortError') {
return getFetchErrorTitle(error);
}
let message = String(error.message || '');
if (!message || message.startsWith('Failed to fetch') || message.includes('NetworkError')) {
return getFetchErrorTitle(error);
}
return message;
}
async function handleMyKadReader() {
showLoader();
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), MYKAD_READER_TIMEOUT_MS);
try {
const response = await fetch(getMyKadReaderEndpoint(), {
method: 'POST',
headers: {
'Accept': 'application/json',
},
signal: controller.signal,
});
const payload = await parseJsonResponse(response);
if (!response.ok) {
throw new Error(getReaderErrorMessage(payload));
if(respcode === -1){
console.log({'location': 'callbackDoneGetListCardReader', 'data' : data} );
}
if (!payload || payload.ok !== true || !payload.data) {
throw new Error(getReaderErrorMessage(payload));
let isDevicesConnected = Object.keys(data).length > 0;
if(! isDevicesConnected){
hideLoader();
Swal.fire({ icon: 'error', title: 'Pembaca MyKad tidak dapat dikesan, sila periksa sambungan', });
return;
}
await handleMyKadData(payload.data);
} catch (error) {
Swal.fire({
icon: 'error',
title: getDisplayError(error),
});
} finally {
clearTimeout(timeoutId);
let slots = data.toString().split(',');
WebSocketManager.open(doneOpenReaderConnection, slots[0]);
}
function doneOpenReaderConnection()
{
WebSocketManager.readAllText(callBackAfterReadMykad);
}
function handleMyKadReaderError(sw12AndApiName)
{
hideLoader();
let error_handler_dictionary = {
'0x80100069' : 'Tiada MyKad dikesan, sila masukkan Mykad untuk dibaca',
'0x80100066' : 'MyKad mungkin dimasukkan terbalik, sila masukkan dengan betul',
'default' : sw12AndApiName,
};
let error_data = sw12AndApiName.split(" ");
let error_code = error_data.filter(x => x.startsWith('0x')).splice(-1)
let error_message = error_handler_dictionary['default'];
let hasDefinedError = error_handler_dictionary.hasOwnProperty(error_code);
if(hasDefinedError){
error_message = error_handler_dictionary[error_code];
}
Swal.fire({ icon: 'error', title: error_message, });
}
}
function handleMyKadData(data) {
return checkIfMyKadExists(data.nric)
.then(response => {
let isResponseArray = Array.isArray(response);
let resolved_response = isResponseArray ? response.pop() : response;
if (!resolved_response || resolved_response === 'no-data') {
populateDaftarPelangganModal(data);
} else {
let input_search_noic = document.querySelector("form[name='frmsearch'] input[name='noic']");
let form_search_noic = document.querySelector("form[name='frmsearch']");
input_search_noic.value = getMyKadNumberWithoutHyphens(data.nric);
form_search_noic.submit();
}
})
.catch(() => {
populateDaftarPelangganModal(data);
});
}
function checkIfMyKadExists(myKadNumber) {
let myKadNumberWithoutHyphens = getMyKadNumberWithoutHyphens(myKadNumber);
let lookupUrl = new URL(wakil_caricustomer_endpoint, window.location.origin);
lookupUrl.searchParams.set('icno', myKadNumberWithoutHyphens);
const csrfToken = document.querySelector('input[name="_token"]');
return fetch(lookupUrl.toString(), {
method: 'GET',
headers: {
"Accept": "application/json",
"X-Requested-With": "XMLHttpRequest",
"X-CSRF-Token": csrfToken ? csrfToken.value : ""
},
credentials: "same-origin",
}).then(async response => {
let text = await response.text();
let trimmed = (text || '').trim();
if (!trimmed || trimmed.charAt(0) === '<') {
return 'no-data';
function callBackAfterReadMykad(data, respcode, sw12AndApiName)
{
if(respcode !== 0){
handleMyKadReaderError(sw12AndApiName);
return
}
try {
return JSON.parse(trimmed);
} catch (error) {
return 'no-data';
checkIfMyKadExists(data.IDNum)
.then(response => {
let isResponseArray = Array.isArray(response);
resolved_response = isResponseArray ? response.pop() : response;
if (resolved_response === 'no-data') {
populateDaftarPelangganModal(data, handleReadPhoto);
} else {
let input_search_noic = document.getElementById("noic");
let form_search_noic = document.querySelector("form[name='frmsearch']");
let getMyKadNumberWithoutHypens = myKadNumber => myKadNumber.replace(/-/g, '');
input_search_noic.value = getMyKadNumberWithoutHypens(data.IDNum);
form_search_noic.submit();
}
})
.catch(error => {
Swal.fire({
icon: 'error',
title: error,
});
});
hideLoader();
}
function checkIfMyKadExists(myKadNumber) {
let myKadNumberWithoutHyphens = myKadNumber.replace(/-/g, '');
const url = wakil_caricustomer_endpoint + '/?icno=' + myKadNumberWithoutHyphens;
const connectionConfig = {
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"X-Requested-With": "XMLHttpRequest",
"X-CSRF-Token": document.querySelector('input[name="_token"]').value
},
credentials: "same-origin",
};
return fetch(url, connectionConfig)
.then(response => response.json())
}
let populateDaftarPelangganModal = (data, callbackAfterPopulateDaftarPelanggan) => {
$('#daftar_pelanggan_mykad_reader').modal('show');
let modal_form = document.querySelector("#daftar_pelanggan_mykad_reader .modal-body .row.form-group");
const religions = [
['01', 'Islam'],
['02', 'Hindu'],
['03', 'Buddha'],
['04', 'Kristian'],
['05', 'Lain-lain'],
];
const races = [
['M', 'Melayu'],
['C', 'Cina'],
['I', 'India'],
['L', 'Lain-lain'],
];
let getCodeFromValue = (needle, haystack) => {
let title_cased_text = needle.split(' ')
.map(w => w[0].toUpperCase() + w.substring(1).toLowerCase())
.join(' ');
return haystack.filter(x => x[1] == title_cased_text)[0][0];
};
let getMyKadNumberWithoutHypens = (IDNum) => {
return IDNum.replace(/-/g, '');
}
});
}
let populateDaftarPelangganModal = (data) => {
$('#daftar_pelanggan_mykad_reader').modal('show');
let modal_form = document.querySelector("#daftar_pelanggan_mykad_reader .modal-body .row.form-group");
let getFormattedDateOfBirth = (date_of_birth) => {
const religions = [
['01', 'Islam'],
['02', 'Hindu'],
['03', 'Buddha'],
['04', 'Kristian'],
['05', 'Lain-lain'],
];
let date_of_birth_hyphenated = date_of_birth.replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3");
const races = [
['M', 'Melayu'],
['C', 'Cina'],
['I', 'India'],
['L', 'Lain-lain'],
];
const [year, month, day] = date_of_birth_hyphenated.split('-');
return day + '/' + month + '/' + year;
};
let disablePointerEventsOnClickableInputs = () => {
modal_form.querySelector("input[name='bumiputera']").style.pointerEvents = 'none';
modal_form.querySelector("input[name='warganegara']").style.pointerEvents = 'none';
modal_form.querySelector("select[name='bangsa']").style.pointerEvents = 'none';
modal_form.querySelector("select[name='agama']").style.pointerEvents = 'none';
};
let getAgeFromDateOfBirth = (date_of_birth) => {
let mykad_modal = document.querySelector("#daftar_pelanggan_mykad_reader");
let photo_container = mykad_modal.querySelector("#photo-mykad-container");
let photo = mykad_modal.querySelector("#photo-mykad");
let date_of_birth_hyphenated = date_of_birth.replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3");
modal_form.querySelector("textarea[name='name']").value = data.name || data.gmpc_name || '';
modal_form.querySelector("input[name='noic']").value = getMyKadNumberWithoutHyphens(data.nric);
modal_form.querySelector("input[name='tarikh_lahir']").value = formatDateOfBirth(data.dob);
modal_form.querySelector("input[name='negeri_lahir']").value = data.birth_place || '';
modal_form.querySelector("input[name='jantina']").value = mapGender(data.gender);
modal_form.querySelector("input[name='warganegara']").checked = String(data.citizenship || '').toUpperCase() === 'WARGANEGARA';
modal_form.querySelector("input[name='bumiputera']").checked = getBumiputeraStatus(data.race);
modal_form.querySelector("select[name='agama']").value = getCodeFromValue(data.religion, religions, '05');
modal_form.querySelector("textarea[name='alamat']").value = joinAddress(data);
modal_form.querySelector("input[name='poskod']").value = data.postcode || '';
modal_form.querySelector("input[name='daerah']").value = data.city || '';
modal_form.querySelector("input[name='negeri']").value = data.state || '';
modal_form.querySelector("input[name='age']").value = data.age != null && data.age !== ''
? data.age
: getAgeFromDateOfBirth(data.dob);
modal_form.querySelector("select[name='bangsa']").value = getCodeFromValue(data.race, races, 'L');
const [birth_year, birth_month, birth_day] = date_of_birth_hyphenated.split('-');
let formatBirthMonth = (birth_month) => {
let newbirthMonth = birth_month < 9 ? birth_month[1] : birth_month;
return newbirthMonth;
};
const count_leap_year = countLeapYears(birth_year, formatBirthMonth(birth_month), birth_day) - 1;
const start = new Date(date_of_birth_hyphenated),
end = new Date(),
diff = new Date(end - start),
days = ((diff / 1000 / 60 / 60 / 24) - count_leap_year) / 365;
return parseInt(days);
};
let getBumiputeraStatus = (race) => {
const non_bumiputera_ethnicity = ['INDIA', 'CINA'];
let isBumiputera = !non_bumiputera_ethnicity.includes(race);
return isBumiputera;
};
let disablePointerEventsOnClickableInputs = () => {
modal_form.querySelector("input[name='bumiputera']").style.pointerEvents = 'none';
modal_form.querySelector("input[name='warganegara']").style.pointerEvents = 'none';
modal_form.querySelector("select[name='bangsa']").style.pointerEvents = 'none';
modal_form.querySelector("select[name='agama']").style.pointerEvents = 'none';
}
let mykad_modal = document.querySelector("#daftar_pelanggan_mykad_reader");
let photo_container = mykad_modal.querySelector("#photo-mykad-container");
modal_form.querySelector("textarea[name='name']").value = data.OrgName;
modal_form.querySelector("input[name='noic']").value = getMyKadNumberWithoutHypens(data.IDNum);
modal_form.querySelector("input[name='tarikh_lahir']").value = getFormattedDateOfBirth(data.BirthDate);
modal_form.querySelector("input[name='negeri_lahir']").value = data.BirthPlace;
modal_form.querySelector("input[name='jantina']").value = data.Gender == 'LELAKI' ? 'Lelaki' : 'Perempuan';
modal_form.querySelector("input[name='warganegara']").checked = data.Citizenship === 'WARGANEGARA' ? true : false;
modal_form.querySelector("input[name='bumiputera']").checked = getBumiputeraStatus(data.Race);
modal_form.querySelector("select[name='agama']").value = getCodeFromValue(data.Religion, religions);
modal_form.querySelector("textarea[name='alamat']").value = [data.Address1, data.Address2, data.Address3].join(", ");
let getPostCode = (postCodeCity) => postCodeCity.replace(/\D/g, '');
modal_form.querySelector("input[name='poskod']").value = getPostCode(data.PostcodeCity);
let getCityName = (postCodeCity) => postCodeCity.replace(/\d/g, '');
modal_form.querySelector("input[name='daerah']").value = getCityName(data.PostcodeCity).trim();
modal_form.querySelector("input[name='negeri']").value = data.State;
modal_form.querySelector("input[name='age']").value = getAgeFromDateOfBirth(data.BirthDate);
modal_form.querySelector("select[name='bangsa']").value = getCodeFromValue(data.Race, races);
calculatePassportAge();
if (data.has_photo && data.photo_base64) {
let mime = data.photo_mime || 'image/jpeg';
photo.src = 'data:' + mime + ';base64,' + data.photo_base64;
photo_container.classList.remove('d-none');
photo_container.classList.add('d-flex');
} else {
photo.src = original_photo_loader;
photo_container.classList.add('d-none');
photo_container.classList.remove('d-flex');
$('#daftar_pelanggan_mykad_reader').modal('show');
disablePointerEventsOnClickableInputs();
callbackAfterPopulateDaftarPelanggan();
}
function handleReadPhoto()
{
WebSocketManager.init("wss://localhost:5963/mykadwebsocket");
WebSocketManager.listReader(callbackDoneGetListCardReader2);
}
function callbackDoneGetListCardReader2(data, respcode)
{
if(respcode === -1){
console.log({'location': 'callbackDoneGetListCardReader', 'data' : data} );
}
disablePointerEventsOnClickableInputs();
$(modal_form.querySelector("input[name='poskod']")).trigger('change');
};
let slots = data.toString().split(',');
WebSocketManager.open(doneOpenReaderConnection2, slots[0]);
}
function doneOpenReaderConnection2()
{
WebSocketManager.readPhoto(callbackAfterReadPhoto2);
}
function callbackAfterReadPhoto2(data, RespCode, RespSW12AndApiName)
{
let mykad_modal = document.querySelector("#daftar_pelanggan_mykad_reader");
let photo = mykad_modal.querySelector("#photo-mykad");
let isDataEmpty = data
&& Object.keys(data).length === 0
&& Object.getPrototypeOf(data) === Object.prototype;
if(isDataEmpty){
photo.src = original_photo_loader;
return;
}
photo.src = 'data:image/jpeg;base64, ' + data.Base64Photo;
}
let hideMykadPhoto = () => {
WebSocketManager.close(() => console.log('Websocket closed'));
let photo = document.getElementById("photo-mykad");
let photo_container = document.getElementById("photo-mykad-container");
if (photo) {
photo.src = original_photo_loader;
}
photo.src = original_photo_loader;
if (photo_container) {
photo_container.classList.add('d-none');
photo_container.classList.remove('d-flex');
}
photo_container.classList.add('d-none');
photo_container.classList.remove('d-flex');
};
if (button_read_mykad) {
button_read_mykad.addEventListener("click", handleMyKadReader);
}
if (button_close_modal) {
button_close_modal.addEventListener("click", hideMykadPhoto);
}
button_read_mykad.addEventListener("click", handleMyKadReader);
button_close_modal.addEventListener("click", hideMykadPhoto);
+2 -2
View File
@@ -7,7 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" href="{{ asset('img/icon_pkb.png') }}">
<title>arOS {{ config('app.version') }}</title>
<title>arOS v2.2</title>
<style>
@keyframes slide {
@@ -82,7 +82,7 @@
<div class="card-header pb-0 text-left">
<div style="display: flex; justify-content:space-between; align-items:center">
<h1 class="font-weight-bolder text-info text-gradient" style="word-break: break-word; white-space: nowrap;">
Kaunter arOS {{ config('app.version') }}
Kaunter arOS v2.2
</h1>
<img src="{{ asset('img/icon_pkb.png') }}" alt="logo-arrahn" style="width: 85px; margin-left: 30px;">
</div>
+2 -1
View File
@@ -1277,7 +1277,8 @@
let wakil_caricustomer_endpoint = "{{ route('wakil.caricustomer') }}";
let mykad_reader_driver_endpoint = "{{ route('mykadreader.driver') }}";
</script>
<script src="{{ asset('js/mykad-reader-customer.js') }}?v=2"></script>
<script src="{{ asset('js/iris-mykad-websocket-client-api.js') }}"></script>
<script src="{{ asset('js/mykad-reader-customer.js') }}"></script>
<script src="{{ asset('js/postcode-concerns.js') }}"></script>
@endpush
+7 -5
View File
@@ -495,14 +495,15 @@
</div>
<script>
var isAnggota = @json($isAnggota ?? false);
var isPelanggan = @json($isPelanggan ?? false);
var showAssignAnggotaBtn = @json($showAssignAnggotaBtn ?? false);
var showDaftarPelangganBtn = @json($showDaftarPelangganBtn ?? false);
var api_url = "{{ api_url_for_browser() }}";
var search = "<?php echo $search ?>";
var type_id = "<?php echo $idtype ?>";
var blacklist_search = "<?php echo $blacklist_search ?>";
var isAnggota = @json($isAnggota ?? false);
var isPelanggan = @json($isPelanggan ?? false);
var showAssignAnggotaBtn = @json($showAssignAnggotaBtn ?? false);
var showDaftarPelangganBtn = @json($showDaftarPelangganBtn ?? false);
$(document).ready(function(){
// Datepicker
$('#passport_tarikh_lahir').datepicker({
@@ -1417,7 +1418,8 @@ document.getElementsByName('form-store')[0].addEventListener('keypress', functio
let wakil_caricustomer_endpoint = "{{ route('wakil.caricustomer') }}";
let mykad_reader_driver_endpoint = "{{ route('mykadreader.driver') }}";
</script>
<script src="{{ asset('js/mykad-reader-customer.js') }}?v=2"></script>
<script src="{{ asset('js/iris-mykad-websocket-client-api.js') }}"></script>
<script src="{{ asset('js/mykad-reader-customer.js') }}"></script>
<script src="{{ asset('js/postcode-concerns.js') }}"></script>
@endpush
+49 -48
View File
@@ -712,67 +712,68 @@
</div>
</div>
<script>
var api_arrahnbid = "<?php echo config('app.api_arrahnbid') ?>";
var api_url = "{{ api_url_for_browser() }}";
var kodcaw = "{{ $cawangan_info['kodcaw'] }}";
var tagarrahnbid = "{{ $cawangan_info['tagarrahnbid'] }}";
var min_komoditi = "{{ $min_komoditi }}";
$('#daftar_gadai').on('click', function() {
$("#daftar_gadai").prop('disabled', true);
var noic = $('#noic').val();
var iclama = "{{ $customer_info['kplama'] }}";
var icbaru = "{{ $customer_info['kpbaru'] }}";
var outstanding_semasa = "{{ $outstanding_semasa }}";
event.preventDefault();
if (outstanding_semasa >= 300000) {
Swal.fire({
icon: 'error',
title: 'Had Pembiayaan',
text: 'Outstanding Pembiayaan Pelanggan Telah Melebihi Had'
});
$("#daftar_gadai").prop('disabled', false);
<script>
var api_arrahnbid = "<?php echo config('app.api_arrahnbid'); ?>";
var api_url = "{{ api_url_for_browser() }}";
var kodcaw = "{{ $cawangan_info['kodcaw'] }}";
var tagarrahnbid = "{{ $cawangan_info['tagarrahnbid'] }}";
var min_komoditi = "{{ $min_komoditi }}";
$('#daftar_gadai').on('click', function() {
$("#daftar_gadai").prop('disabled', true);
var noic = $('#noic').val();
var iclama = "{{ $customer_info['kplama'] }}";
var icbaru = "{{ $customer_info['kpbaru'] }}";
var outstanding_semasa = "{{ $outstanding_semasa }}";
event.preventDefault();
return false;
} else {
$.ajax({
method: 'get',
url: api_url + '/api/komuditi/check',
success: function(data) {
var check_komuditi = Object.keys(data).length;
if (check_komuditi == 0) {
Swal.fire({
icon: 'error',
title: 'Unit Komoditi Berbaki Minima',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komoditi'
});
$("#daftar_gadai").prop('disabled', false);
event.preventDefault();
return false;
} else {
if (Number(data['unit_komuditi']) <= min_komoditi) {
$("#daftar_gadai").prop('disabled', false);
if (outstanding_semasa >= 300000) {
Swal.fire({
icon: 'error',
title: 'Had Pembiayaan',
text: 'Outstanding Pembiayaan Pelanggan Telah Melebihi Had'
});
$("#daftar_gadai").prop('disabled', false);
event.preventDefault();
return false;
} else {
$.ajax({
method: 'get',
url: api_url + '/api/komuditi/check',
success: function(data) {
var check_komuditi = Object.keys(data).length;
if (check_komuditi == 0) {
Swal.fire({
icon: 'error',
title: 'Unit Komoditi Berbaki Minima',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komoditi'
});
$("#daftar_gadai").prop('disabled', false);
event.preventDefault();
return false;
} else {
if (icbaru.length > 0) {
window.location.href =
"{{ route('gadaian', ['noic' => $customer_info['kpbaru']]) }}";
if (Number(data['unit_komuditi']) <= min_komoditi) {
$("#daftar_gadai").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Unit Komoditi Berbaki Minima',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komoditi'
});
event.preventDefault();
return false;
} else {
window.location.href =
"{{ route('gadaian', ['noic' => $customer_info['kplama']]) }}";
if (icbaru.length > 0) {
window.location.href =
"{{ route('gadaian', ['noic' => $customer_info['kpbaru']]) }}";
} else {
window.location.href =
"{{ route('gadaian', ['noic' => $customer_info['kplama']]) }}";
}
}
}
}
}
});
}
});
});
}
});
$('a[data-toggle="tab"]').on('shown.bs.tab', function(e) {
// here is the new selected tab id
@@ -139,7 +139,7 @@
<a href="{{ route('kakitangan.tukarcawangan',['id'=>$staff->id]) }}" class="btn btn-info" title="Tukar Cawangan"><i class="material-icons">work</i></a>
<a href="{{ route('operasi.kakitangan.edit',['id'=>$staff->id]) }}" class="btn btn-success" title="Edit Maklumat"><i class="material-icons">settings</i></a>
@if(
app()->environment(['local', 'staging'])
app()->environment('local')
&&
in_array($auth_user->roles, ['admin'])
&& $staff->roles != 'admin'
@@ -161,7 +161,7 @@
<a href="{{ route('admin.kakitangan.edit',['id'=>$staff->id]) }}" class="btn btn-success" title="Edit Maklumat"><i class="material-icons">settings</i></a>
<a href="{{ route('admin.kakitangan.resetcredentials',['id'=>$staff->id]) }}" class="btn btn-primary" title="Reset Password"><i class="material-icons">restore</i></a>
@if(
app()->environment(['local', 'staging'])
app()->environment('local')
&&
$staff->roles != 'admin'
&& $staff->id != $auth_user->id
@@ -634,7 +634,7 @@
<tr>
<td style="width:10px">(-) Rebat Untung</td>
<td style="width:10px">: RM
{{ number_format($keuntungan_rebate,2) }}
{{ number_format($totalrebate['onepercent'], 2) }}
</td>
</tr>
<tr>
@@ -644,7 +644,7 @@
<tr>
<td style="width:10px">(-) Rebat Lain-Lain Caj</td>
<td style="width:10px">: RM
{{ number_format($ujrah_rebate, 2) }} </td>
{{ number_format($totalrebate['ujrah'], 2) }} </td>
</tr>
<tr>
<td style="width:10px">Jumlah Keuntungan + Lain-Lain Caj</td>
@@ -867,7 +867,7 @@
<tr>
<td style="width:10px">(-) Rebat Untung</td>
<td style="width:10px">: RM
{{ number_format($keuntungan_rebate, 2) }}
{{ number_format($totalrebate['onepercent'], 2) }}
</td>
</tr>
<tr>
@@ -877,7 +877,7 @@
<tr>
<td style="width:10px">(-) Rebat Lain-Lain Caj</td>
<td style="width:10px">: RM
{{ number_format($ujrah_rebate, 2) }} </td>
{{ number_format($totalrebate['ujrah'], 2) }} </td>
</tr>
<tr>
<td style="width:10px">Jumlah Keuntungan + Lain-Lain Caj</td>
-4
View File
@@ -15,10 +15,6 @@ use Illuminate\Support\Facades\Storage;
|
*/
Route::get('/health', function () {
return response()->json(['status' => 'ok']);
});
Route::get('/',['as'=>'login','uses'=>'HomeController@index']);
Route::get('/testtry',function(ImbangdugaServices $imbangduga){