Dev/v1.0 (#1)

Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local>
Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local>
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2026-07-23 10:51:04 +08:00
parent 3189e3a1e3
commit 85b31e9528
106 changed files with 19492 additions and 401 deletions
@@ -0,0 +1,31 @@
const ACTIVE_STATION_KEY = 'qms_teller_active_station';
export function saveActiveStation({ stationId, stationName, branchId, branchName }) {
sessionStorage.setItem(
ACTIVE_STATION_KEY,
JSON.stringify({
stationId: Number(stationId),
stationName,
branchId: branchId != null ? Number(branchId) : null,
branchName: branchName ?? null,
})
);
}
export function getActiveStation(stationId) {
try {
const raw = sessionStorage.getItem(ACTIVE_STATION_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw);
if (stationId != null && Number(parsed.stationId) !== Number(stationId)) {
return null;
}
return parsed;
} catch {
return null;
}
}
export function clearActiveStation() {
sessionStorage.removeItem(ACTIVE_STATION_KEY);
}
@@ -0,0 +1,89 @@
const CALL_SOUND_URL = '/sounds/header-sound.mp3';
let sharedAudio = null;
let unlocked = false;
function getCallAudio() {
if (!sharedAudio) {
sharedAudio = new Audio(CALL_SOUND_URL);
sharedAudio.preload = 'auto';
}
return sharedAudio;
}
/**
* Call once after a user gesture (click/tap) so later plays are allowed.
*/
export function unlockQueueCallSound() {
if (unlocked) return;
try {
const audio = getCallAudio();
audio.muted = true;
const playPromise = audio.play();
if (playPromise?.then) {
playPromise
.then(() => {
audio.pause();
audio.currentTime = 0;
audio.muted = false;
unlocked = true;
})
.catch(() => {
// Still mark unlocked so we keep trying on real calls.
audio.muted = false;
unlocked = true;
});
} else {
audio.muted = false;
unlocked = true;
}
} catch {
unlocked = true;
}
}
/**
* Play the queue-call notification chime.
* Safe to call repeatedly; restarts from the beginning each time.
*/
export function playQueueCallSound() {
try {
const audio = getCallAudio();
audio.muted = false;
audio.currentTime = 0;
const playPromise = audio.play();
if (playPromise?.catch) {
playPromise.catch((error) => {
console.warn('Could not play queue call sound:', error);
});
}
} catch (error) {
console.warn('Could not play queue call sound:', error);
}
}
/**
* Build a stable map of stationId -> ticket number for currently serving tickets.
*/
export function servingSnapshot(tickets) {
const map = {};
for (const ticket of tickets) {
if (ticket?.station?.id != null && ticket?.number != null) {
map[String(ticket.station.id)] = String(ticket.number);
}
}
return map;
}
/**
* Returns true if any station got a new/different ticket number vs the previous snapshot.
*/
export function hasNewServingCall(previous, next) {
if (!previous) return false;
for (const [stationId, number] of Object.entries(next)) {
if (previous[stationId] !== number) {
return true;
}
}
return false;
}
+87
View File
@@ -0,0 +1,87 @@
const TOKEN_KEY = 'teller_token';
const USER_DATA_KEY = 'teller_userData';
/** Matches backend jwt.token-validity-time (PT30M). */
const SESSION_MAX_AGE_SECONDS = 30 * 60;
function getCookie(name) {
const prefix = `${encodeURIComponent(name)}=`;
const parts = document.cookie ? document.cookie.split('; ') : [];
for (const part of parts) {
if (part.startsWith(prefix)) {
return decodeURIComponent(part.slice(prefix.length));
}
}
return null;
}
function setCookie(name, value, maxAgeSeconds = SESSION_MAX_AGE_SECONDS) {
const secure = window.location.protocol === 'https:' ? '; Secure' : '';
document.cookie = [
`${encodeURIComponent(name)}=${encodeURIComponent(value)}`,
'Path=/',
`Max-Age=${maxAgeSeconds}`,
'SameSite=Lax',
secure,
].join('; ');
}
function removeCookie(name) {
document.cookie = `${encodeURIComponent(name)}=; Path=/; Max-Age=0; SameSite=Lax`;
}
export function getToken() {
return getCookie(TOKEN_KEY);
}
export function setToken(token) {
if (token == null || token === '') {
removeCookie(TOKEN_KEY);
return;
}
setCookie(TOKEN_KEY, token);
}
export function getUserData() {
const raw = getCookie(USER_DATA_KEY) ?? sessionStorage.getItem(USER_DATA_KEY);
if (!raw) {
return null;
}
try {
return JSON.parse(raw);
} catch {
removeCookie(USER_DATA_KEY);
sessionStorage.removeItem(USER_DATA_KEY);
return null;
}
}
export function setUserData(userData) {
if (userData == null) {
removeCookie(USER_DATA_KEY);
sessionStorage.removeItem(USER_DATA_KEY);
return;
}
const serialized = JSON.stringify(userData);
setCookie(USER_DATA_KEY, serialized);
sessionStorage.setItem(USER_DATA_KEY, serialized);
}
export function setSession({ token, userData } = {}) {
if (token !== undefined) {
setToken(token);
}
if (userData !== undefined) {
setUserData(userData);
}
}
export function clearSession() {
removeCookie(TOKEN_KEY);
removeCookie(USER_DATA_KEY);
sessionStorage.removeItem(USER_DATA_KEY);
}