Files
kaunter/resources/views/layouts/partials/batch_cutoff_banner.blade.php
T

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>