fix some bugs from accounting 7

This commit is contained in:
2026-07-28 21:02:44 +03:30
parent 7cd3fac28b
commit a49b20dfd4
20 changed files with 1734 additions and 93 deletions
+165
View File
@@ -0,0 +1,165 @@
/** نوتیف مرورگر بدون Firebase — ترجیحاً با gesture کاربر؛ بعد از لاگین هم تلاش می‌شود. */
export type BrowserNotifState =
| 'unsupported'
| 'needs-pwa'
| 'default'
| 'granted'
| 'denied'
function isStandalonePwa() {
if (typeof window === 'undefined') return false
return (
window.matchMedia('(display-mode: standalone)').matches ||
Boolean((window.navigator as Navigator & { standalone?: boolean }).standalone)
)
}
/** فقط دستگاه‌های واقعی اپل موبایل — نه مک با ترک‌پد */
function isAppleMobileDevice() {
if (typeof navigator === 'undefined') return false
const ua = navigator.userAgent || ''
if (/iPhone|iPod/i.test(ua)) return true
if (/iPad/i.test(ua)) return true
const isTouchMac = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1
if (!isTouchMac) return false
if (/CriOS|FxiOS|EdgiOS/i.test(ua)) return true
if (/Chrome|Edg|Firefox|OPR/i.test(ua) && !/Mobile/i.test(ua)) return false
return true
}
export function getBrowserNotificationState(): BrowserNotifState {
if (typeof window === 'undefined') return 'unsupported'
if (typeof window.isSecureContext === 'boolean' && !window.isSecureContext) {
return 'unsupported'
}
const hasApi = 'Notification' in window
if (isAppleMobileDevice() && !isStandalonePwa() && !hasApi) {
return 'needs-pwa'
}
if (!hasApi) {
return 'unsupported'
}
const permission = Notification.permission
if (permission === 'granted') return 'granted'
if (permission === 'denied') return 'denied'
return 'default'
}
export function canRequestBrowserNotification() {
return getBrowserNotificationState() === 'default'
}
export async function requestBrowserNotificationPermission(): Promise<BrowserNotifState> {
const before = getBrowserNotificationState()
if (before === 'granted' || before === 'denied' || before === 'unsupported' || before === 'needs-pwa') {
return before
}
if (!('Notification' in window)) {
return getBrowserNotificationState()
}
try {
const result = await Notification.requestPermission()
if (result === 'granted') return 'granted'
if (result === 'denied') return 'denied'
return 'default'
} catch {
return getBrowserNotificationState()
}
}
/** لوگوی برند از کانفیگ؛ fallback به آیکون‌های PWA */
function resolveNotifIcon() {
try {
const avatar = String(
(window as Window & { __APP_CONFIG__?: { user?: { avatar?: string } } }).__APP_CONFIG__?.user
?.avatar ?? '',
).trim()
if (avatar) {
return avatar
}
} catch {
// ignore
}
try {
return new URL('/192.png', window.location.origin).href
} catch {
try {
return new URL('/favicon.png', window.location.origin).href
} catch {
return undefined
}
}
}
export type ShowBrowserNotificationOptions = {
title: string
body?: string
tag?: string
url?: string
}
/** نمایش نوتیف سیستم؛ ترجیح با Service Worker (معتبرتر روی Chrome/Android). */
export async function showBrowserNotification(options: ShowBrowserNotificationOptions): Promise<boolean> {
if (getBrowserNotificationState() !== 'granted') {
return false
}
const title = options.title || 'اعلان جدید'
const body = options.body || ''
const tag = options.tag || `app-notification-${Date.now()}`
const url = options.url || '/appNotifications'
const icon = resolveNotifIcon()
try {
if ('serviceWorker' in navigator) {
const reg = await navigator.serviceWorker.ready
if (reg?.showNotification) {
await reg.showNotification(title, {
body,
tag,
icon,
badge: icon,
data: { url },
renotify: true,
})
return true
}
}
} catch (error) {
console.warn('SW showNotification failed, falling back', error)
}
try {
const n = new Notification(title, {
body,
tag,
icon,
dir: 'rtl',
lang: 'fa',
})
n.onclick = () => {
window.focus()
n.close()
if (url) {
void import('@/router').then((mod) => {
mod.default.push(url).catch(() => undefined)
})
}
}
return true
} catch (error) {
console.warn('Notification constructor failed', error)
return false
}
}