c437a3f504
Build Docker Image / build-frontend (push) Successful in 53s
Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local> Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local> Reviewed-on: #24
356 lines
11 KiB
PHP
356 lines
11 KiB
PHP
<style>
|
|
#batch-cutoff-banner {
|
|
position: fixed;
|
|
top: 112px;
|
|
right: 24px;
|
|
z-index: 1080;
|
|
width: min(380px, calc(100vw - 32px));
|
|
margin: 0;
|
|
padding: 0;
|
|
color: #fff;
|
|
background: #b91c1c;
|
|
border-radius: 16px;
|
|
box-shadow: 0 18px 40px rgba(127, 29, 29, 0.35);
|
|
}
|
|
#batch-cutoff-banner.batch-cutoff-warn {
|
|
background: #db2777;
|
|
box-shadow: 0 18px 40px rgba(190, 24, 93, 0.32);
|
|
}
|
|
#batch-cutoff-banner.batch-cutoff-warn .batch-cutoff-link {
|
|
color: #9d174d !important;
|
|
}
|
|
#batch-cutoff-banner.batch-cutoff-urgent {
|
|
background: #991b1b;
|
|
}
|
|
#batch-cutoff-banner .batch-cutoff-inner {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
padding: 14px 16px 16px;
|
|
}
|
|
#batch-cutoff-banner .batch-cutoff-copy {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
min-width: 0;
|
|
}
|
|
#batch-cutoff-banner .batch-cutoff-icon {
|
|
flex-shrink: 0;
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 999px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(255, 255, 255, 0.14);
|
|
font-size: 15px;
|
|
}
|
|
#batch-cutoff-banner .batch-cutoff-title {
|
|
display: block;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
letter-spacing: -0.01em;
|
|
line-height: 1.3;
|
|
}
|
|
#batch-cutoff-banner .batch-cutoff-sub {
|
|
display: block;
|
|
margin-top: 3px;
|
|
font-size: 12px;
|
|
line-height: 1.4;
|
|
color: rgba(255, 255, 255, 0.82);
|
|
}
|
|
#batch-cutoff-banner .batch-cutoff-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding-left: 48px;
|
|
}
|
|
#batch-cutoff-banner .batch-cutoff-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
min-width: 64px;
|
|
justify-content: center;
|
|
padding: 6px 10px;
|
|
border-radius: 999px;
|
|
background: rgba(0, 0, 0, 0.22);
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
font-variant-numeric: tabular-nums;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
#batch-cutoff-banner .batch-cutoff-link,
|
|
#batch-cutoff-banner .batch-cutoff-notify {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 7px 14px;
|
|
border: 0;
|
|
border-radius: 999px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
line-height: 1.2;
|
|
text-decoration: none !important;
|
|
white-space: nowrap;
|
|
cursor: pointer;
|
|
}
|
|
#batch-cutoff-banner .batch-cutoff-link {
|
|
flex: 1;
|
|
color: #991b1b !important;
|
|
background: #fff;
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
|
|
}
|
|
#batch-cutoff-banner .batch-cutoff-link:hover {
|
|
color: #7f1d1d !important;
|
|
background: #f8fafc;
|
|
}
|
|
#batch-cutoff-banner .batch-cutoff-notify {
|
|
color: #fff !important;
|
|
background: transparent;
|
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.55);
|
|
}
|
|
#batch-cutoff-banner .batch-cutoff-notify:hover {
|
|
background: rgba(255, 255, 255, 0.12);
|
|
}
|
|
@media (max-width: 767px) {
|
|
#batch-cutoff-banner {
|
|
top: auto;
|
|
bottom: 16px;
|
|
right: 16px;
|
|
left: 16px;
|
|
width: auto;
|
|
}
|
|
#batch-cutoff-banner .batch-cutoff-actions {
|
|
padding-left: 0;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div id="batch-cutoff-banner" class="d-none" role="alert">
|
|
<div class="batch-cutoff-inner">
|
|
<div class="batch-cutoff-copy">
|
|
<span class="batch-cutoff-icon" aria-hidden="true"><i class="fas fa-clock"></i></span>
|
|
<div>
|
|
<span id="batch-cutoff-title" class="batch-cutoff-title">Hantar senarai pembiayaan online</span>
|
|
<span id="batch-cutoff-body" class="batch-cutoff-sub"></span>
|
|
</div>
|
|
</div>
|
|
<div class="batch-cutoff-actions">
|
|
<span id="batch-cutoff-countdown" class="batch-cutoff-chip">00:00</span>
|
|
<a id="batch-cutoff-link" href="{{ route('pembiayaan.online.pc') }}" class="batch-cutoff-link">Buka senarai</a>
|
|
<button type="button" id="batch-cutoff-enable-notify" class="batch-cutoff-notify d-none">Benarkan notifikasi</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
var STATUS_URL = @json(route('pembiayaan.online.pc.reminder'));
|
|
var POLL_MS = 60000;
|
|
var TICK_MS = 1000;
|
|
var state = null;
|
|
var fetchedAt = 0;
|
|
var serverNowAtFetch = 0;
|
|
var banner = document.getElementById('batch-cutoff-banner');
|
|
var titleEl = document.getElementById('batch-cutoff-title');
|
|
var bodyEl = document.getElementById('batch-cutoff-body');
|
|
var linkEl = document.getElementById('batch-cutoff-link');
|
|
var enableBtn = document.getElementById('batch-cutoff-enable-notify');
|
|
var countdownEl = document.getElementById('batch-cutoff-countdown');
|
|
|
|
if (!banner) {
|
|
return;
|
|
}
|
|
|
|
function todayKey() {
|
|
var d = new Date();
|
|
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
|
|
}
|
|
|
|
function pad(n) {
|
|
return n < 10 ? '0' + n : String(n);
|
|
}
|
|
|
|
function storageKey(kind, level) {
|
|
var slot = (state && state.slot) ? state.slot : 'BATCH';
|
|
return 'batchCutoff:' + kind + ':' + slot + ':' + todayKey() + ':' + level;
|
|
}
|
|
|
|
function was(kind, level) {
|
|
try {
|
|
return sessionStorage.getItem(storageKey(kind, level)) === '1';
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function mark(kind, level) {
|
|
try {
|
|
sessionStorage.setItem(storageKey(kind, level), '1');
|
|
} catch (e) {}
|
|
}
|
|
|
|
function remainingMs() {
|
|
if (!state) {
|
|
return null;
|
|
}
|
|
var cutoffAt = state.cutoff_at ? Date.parse(state.cutoff_at) : NaN;
|
|
if (!isFinite(cutoffAt)) {
|
|
var parts = String(state.cutoff || '').split(':');
|
|
if (parts.length < 2) {
|
|
return null;
|
|
}
|
|
var now = new Date();
|
|
cutoffAt = new Date(now.getFullYear(), now.getMonth(), now.getDate(), parseInt(parts[0], 10), parseInt(parts[1], 10), 0).getTime();
|
|
}
|
|
var elapsed = Date.now() - fetchedAt;
|
|
var nowMs = (serverNowAtFetch || Date.now()) + elapsed;
|
|
return cutoffAt - nowMs;
|
|
}
|
|
|
|
function canNotify() {
|
|
return typeof Notification !== 'undefined' && Notification.permission === 'granted';
|
|
}
|
|
|
|
function notifyDefault() {
|
|
return typeof Notification !== 'undefined' && Notification.permission === 'default';
|
|
}
|
|
|
|
function sendBrowserNotification(level, title, body) {
|
|
if (!canNotify() || was('notify', level)) {
|
|
return;
|
|
}
|
|
mark('notify', level);
|
|
try {
|
|
var n = new Notification(title, {
|
|
body: body,
|
|
tag: 'batch-cutoff-' + ((state && state.slot) || '') + '-' + level,
|
|
requireInteraction: level === 1
|
|
});
|
|
n.onclick = function () {
|
|
window.focus();
|
|
if (state && state.url) {
|
|
window.location.href = state.url;
|
|
}
|
|
n.close();
|
|
};
|
|
} catch (e) {}
|
|
}
|
|
|
|
function hideBanner() {
|
|
banner.classList.add('d-none');
|
|
banner.classList.remove('batch-cutoff-urgent', 'batch-cutoff-warn');
|
|
}
|
|
|
|
function showBanner(level, remaining) {
|
|
var mins = Math.max(0, Math.floor(remaining / 60000));
|
|
var secs = Math.max(0, Math.floor((remaining % 60000) / 1000));
|
|
var slot = (state && state.slot) ? state.slot : 'Batch';
|
|
var cutoff = (state && state.cutoff) ? state.cutoff : '';
|
|
var count = (state && state.bilangan) ? state.bilangan : 0;
|
|
var countdown = pad(mins) + ':' + pad(secs);
|
|
if (countdownEl) {
|
|
countdownEl.textContent = countdown;
|
|
}
|
|
|
|
if (level === 1) {
|
|
titleEl.textContent = 'Sila hantar senarai pembiayaan online sekarang!! 😡 😡';
|
|
bodyEl.textContent = slot + ' ditutup pada ' + cutoff + ' · ' + count + ' rekod belum dihantar';
|
|
banner.classList.add('batch-cutoff-urgent');
|
|
banner.classList.remove('batch-cutoff-warn');
|
|
} else {
|
|
titleEl.textContent = 'Sila hantar senarai pembiayaan online 😇 😇';
|
|
bodyEl.textContent = slot + ' ditutup pada ' + cutoff + ' · ' + count + ' rekod menunggu';
|
|
banner.classList.add('batch-cutoff-warn');
|
|
banner.classList.remove('batch-cutoff-urgent');
|
|
}
|
|
|
|
if (state && state.url) {
|
|
linkEl.href = state.url;
|
|
}
|
|
if (notifyDefault()) {
|
|
enableBtn.classList.remove('d-none');
|
|
} else {
|
|
enableBtn.classList.add('d-none');
|
|
}
|
|
banner.classList.remove('d-none');
|
|
}
|
|
|
|
function tick() {
|
|
if (!state || !state.enabled || state.ditutup || state.lewat || !state.bilangan) {
|
|
hideBanner();
|
|
return;
|
|
}
|
|
|
|
var remaining = remainingMs();
|
|
if (remaining === null || remaining <= 0) {
|
|
hideBanner();
|
|
return;
|
|
}
|
|
|
|
var remindMs = (state.remind_minutes || 10) * 60000;
|
|
var lastMs = (state.last_minute || 1) * 60000;
|
|
if (remaining > remindMs) {
|
|
hideBanner();
|
|
return;
|
|
}
|
|
|
|
var level = remaining <= lastMs ? 1 : 10;
|
|
showBanner(level, remaining);
|
|
sendBrowserNotification(
|
|
level,
|
|
level === 1 ? '1 minit lagi: hantar senarai batch' : 'Hantar senarai pembiayaan online',
|
|
(state.slot || 'Batch') + ' tamat ' + (state.cutoff || '') + '. ' + state.bilangan + ' rekod belum dihantar.'
|
|
);
|
|
}
|
|
|
|
function applyStatus(data) {
|
|
if (!data || !data.enabled) {
|
|
state = null;
|
|
hideBanner();
|
|
return;
|
|
}
|
|
state = data;
|
|
fetchedAt = Date.now();
|
|
serverNowAtFetch = data.server_now ? Date.parse(data.server_now) : Date.now();
|
|
if (!isFinite(serverNowAtFetch)) {
|
|
serverNowAtFetch = Date.now();
|
|
}
|
|
tick();
|
|
}
|
|
|
|
function fetchStatus() {
|
|
fetch(STATUS_URL, {
|
|
credentials: 'same-origin',
|
|
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
|
}).then(function (res) {
|
|
if (!res.ok) {
|
|
throw new Error('reminder status ' + res.status);
|
|
}
|
|
return res.json();
|
|
}).then(applyStatus).catch(function () {});
|
|
}
|
|
|
|
enableBtn.addEventListener('click', function () {
|
|
if (typeof Notification === 'undefined') {
|
|
return;
|
|
}
|
|
Notification.requestPermission().then(function () {
|
|
if (canNotify()) {
|
|
enableBtn.classList.add('d-none');
|
|
var remaining = remainingMs();
|
|
var lastMs = ((state && state.last_minute) || 1) * 60000;
|
|
var level = remaining !== null && remaining <= lastMs ? 1 : 10;
|
|
try { sessionStorage.removeItem(storageKey('notify', level)); } catch (e) {}
|
|
tick();
|
|
} else {
|
|
enableBtn.classList.add('d-none');
|
|
}
|
|
});
|
|
});
|
|
|
|
fetchStatus();
|
|
setInterval(fetchStatus, POLL_MS);
|
|
setInterval(tick, TICK_MS);
|
|
})();
|
|
</script>
|