From 9bcb917d3b45853c47644d944a6bd03bd80a8991 Mon Sep 17 00:00:00 2001 From: ahasani194 Date: Mon, 11 May 2026 18:42:05 +0330 Subject: [PATCH] update --- angular.json | 4 ++ ngsw-config.json | 5 +- public-tis/favicon/site.webmanifest | 12 ++++ public/favicon/site.webmanifest | 16 ++++- src/app.component.ts | 7 +- src/app/core/services/auth.service.ts | 3 +- src/app/core/services/index.ts | 1 + src/app/core/services/pwa-install.service.ts | 66 +++++++++++++++++++ .../pos/components/splash.component.html | 16 ++++- .../pos/components/splash.component.ts | 42 +++++++++++- .../main-menu-sidebar.component.html | 10 +-- .../main-menu-sidebar.component.ts | 5 ++ .../saleInvoices/components/list.component.ts | 3 +- src/app/pages/notfound/notfound.component.ts | 5 +- src/environments/environment.tis.ts | 4 +- src/tenants/default/branding.config.ts | 2 + src/tenants/tis/branding.config.ts | 1 + 17 files changed, 185 insertions(+), 17 deletions(-) create mode 100644 src/app/core/services/pwa-install.service.ts diff --git a/angular.json b/angular.json index c235ae2..9c5de5c 100644 --- a/angular.json +++ b/angular.json @@ -162,6 +162,10 @@ ], "extractLicenses": false, "fileReplacements": [ + { + "replace": "src/config.ts", + "with": "src/tenants/tis/config.ts" + }, { "replace": "src/assets/images/brandingAssets.ts", "with": "src/tenants/tis/brandingAssets.ts" diff --git a/ngsw-config.json b/ngsw-config.json index 9ff9b73..171d048 100644 --- a/ngsw-config.json +++ b/ngsw-config.json @@ -1,8 +1,9 @@ { "$schema": "./node_modules/@angular/service-worker/config/schema.json", "appData": { - "appVersion": "0.0.0", - "buildDate": "2026-05-11T10:08:38.776Z" + "appVersion": "0.0.3", + "buildDate": "2026-05-11T13:52:49.314Z", + "buildNumber": 3 }, "assetGroups": [ { diff --git a/public-tis/favicon/site.webmanifest b/public-tis/favicon/site.webmanifest index adb661e..cff6590 100644 --- a/public-tis/favicon/site.webmanifest +++ b/public-tis/favicon/site.webmanifest @@ -2,12 +2,24 @@ "background_color": "#ffffff", "display": "standalone", "icons": [ + { + "purpose": "any", + "sizes": "192x192", + "src": "/favicon/web-app-manifest-192x192.png", + "type": "image/png" + }, { "purpose": "maskable", "sizes": "192x192", "src": "/favicon/web-app-manifest-192x192.png", "type": "image/png" }, + { + "purpose": "any", + "sizes": "512x512", + "src": "/favicon/web-app-manifest-512x512.png", + "type": "image/png" + }, { "purpose": "maskable", "sizes": "512x512", diff --git a/public/favicon/site.webmanifest b/public/favicon/site.webmanifest index bb1f7f2..ac201f4 100644 --- a/public/favicon/site.webmanifest +++ b/public/favicon/site.webmanifest @@ -2,16 +2,28 @@ "background_color": "#ffffff", "display": "standalone", "icons": [ + { + "purpose": "any", + "sizes": "192x192", + "src": "/favicon/web-app-manifest-192x192.png", + "type": "image/png" + }, { "purpose": "maskable", "sizes": "192x192", - "src": "/web-app-manifest-192x192.png", + "src": "/favicon/web-app-manifest-192x192.png", + "type": "image/png" + }, + { + "purpose": "any", + "sizes": "512x512", + "src": "/favicon/web-app-manifest-512x512.png", "type": "image/png" }, { "purpose": "maskable", "sizes": "512x512", - "src": "/web-app-manifest-512x512.png", + "src": "/favicon/web-app-manifest-512x512.png", "type": "image/png" } ], diff --git a/src/app.component.ts b/src/app.component.ts index 5b4b9ae..ce34c4c 100644 --- a/src/app.component.ts +++ b/src/app.component.ts @@ -1,7 +1,9 @@ +import { PwaInstallService } from '@/core/services/pwa-install.service'; import { Component, HostListener } from '@angular/core'; import { RouterModule } from '@angular/router'; import { ConfirmDialog } from 'primeng/confirmdialog'; import { ToastModule } from 'primeng/toast'; +import { brandingConfig } from './app/branding/branding.config'; @Component({ selector: 'app-root', @@ -16,8 +18,11 @@ import { ToastModule } from 'primeng/toast'; export class AppComponent { toastPosition: 'top-center' | 'bottom-right' = 'bottom-right'; - constructor() { + constructor(private readonly pwaInstallService: PwaInstallService) { this.updateToastPosition(); + if (brandingConfig.enableInstallPrompt) { + this.pwaInstallService.init(); + } } @HostListener('window:resize') diff --git a/src/app/core/services/auth.service.ts b/src/app/core/services/auth.service.ts index dc4e693..d0384c8 100644 --- a/src/app/core/services/auth.service.ts +++ b/src/app/core/services/auth.service.ts @@ -8,6 +8,7 @@ import { Router } from '@angular/router'; import { CookieService } from 'ngx-cookie-service'; import { BehaviorSubject, Observable, throwError } from 'rxjs'; import { catchError, finalize, tap } from 'rxjs/operators'; +import config from 'src/config'; import { COOKIE_KEYS, LOCAL_STORAGE_KEYS } from '../../../assets/constants'; import { IAuthAccountResponse, @@ -75,7 +76,7 @@ export class AuthService { return this.http .post( '/api/v1/auth/login', - { username, password }, + { username, password, isPos: config.isPosApplication }, { headers: baseHeaders, }, diff --git a/src/app/core/services/index.ts b/src/app/core/services/index.ts index 4329573..64b0b3b 100644 --- a/src/app/core/services/index.ts +++ b/src/app/core/services/index.ts @@ -4,3 +4,4 @@ export { ErrorHandlerService } from './error-handler.service'; export { ConfigService } from './config.service'; export * from './breadcrumb.service'; export * from './native-bridge.service'; +export * from './pwa-install.service'; diff --git a/src/app/core/services/pwa-install.service.ts b/src/app/core/services/pwa-install.service.ts new file mode 100644 index 0000000..fa233b6 --- /dev/null +++ b/src/app/core/services/pwa-install.service.ts @@ -0,0 +1,66 @@ +import { Injectable, signal } from '@angular/core'; +import { ConfirmationService } from 'primeng/api'; + +@Injectable({ providedIn: 'root' }) +export class PwaInstallService { + private deferredInstallPrompt: any = null; + readonly isInstalled = signal(false); + readonly canInstall = signal(false); + + constructor(private readonly confirmationService: ConfirmationService) { + this.init(); + } + + init() { + if (typeof window === 'undefined') { + return; + } + + const standalone = + window.matchMedia('(display-mode: standalone)').matches || + (window.navigator as any).standalone === true; + + console.log(window.matchMedia('(display-mode: standalone)').matches); + console.log(window.navigator); + + this.isInstalled.set(standalone); + + window.addEventListener('beforeinstallprompt', (event: any) => { + event.preventDefault(); + + this.deferredInstallPrompt = event; + this.canInstall.set(true); + console.log(this.deferredInstallPrompt); + }); + + window.addEventListener('appinstalled', () => { + this.deferredInstallPrompt = null; + this.canInstall.set(false); + this.isInstalled.set(true); + }); + } + + openInstallConfirm() { + if (this.isInstalled() || !this.canInstall() || !this.deferredInstallPrompt) { + return; + } + + console.log('openInstallConfirm'); + + this.confirmationService.confirm({ + header: 'نصب اپلیکیشن', + message: 'برای ادامه می‌بایست نیازمندی‌های نرم‌افزار را نصب کنید', + acceptLabel: 'نصب', + rejectButtonProps: { + outlined: true, + }, + accept: () => { + this.deferredInstallPrompt.prompt(); + this.deferredInstallPrompt.userChoice.finally(() => { + this.deferredInstallPrompt = null; + this.canInstall.set(false); + }); + }, + }); + } +} diff --git a/src/app/domains/pos/components/splash.component.html b/src/app/domains/pos/components/splash.component.html index eee44a3..8ef4a55 100644 --- a/src/app/domains/pos/components/splash.component.html +++ b/src/app/domains/pos/components/splash.component.html @@ -1,4 +1,18 @@
+ {{ requireInstall }} + {{ pwaInstalled() }} - در حال بارگذاری ... + @if (requireInstall && !pwaInstalled()) { +
+ برای ادامه، نصب برنامه الزامی است. + @if (canInstall()) { + + } @else { + منتظر فعال شدن امکان نصب در مرورگر... + } + +
+ } @else { + در حال بارگذاری ... + }
diff --git a/src/app/domains/pos/components/splash.component.ts b/src/app/domains/pos/components/splash.component.ts index 4905a2b..3481b37 100644 --- a/src/app/domains/pos/components/splash.component.ts +++ b/src/app/domains/pos/components/splash.component.ts @@ -1,4 +1,6 @@ +import { PwaInstallService } from '@/core/services'; import { Component, EventEmitter, inject, Output } from '@angular/core'; +import { ButtonDirective } from 'primeng/button'; import { finalize, forkJoin } from 'rxjs'; import images from 'src/assets/images'; import { PosInfoStore, PosProfileStore } from '../store'; @@ -7,6 +9,7 @@ import { DeviceInfoStore } from '../store/device.store'; @Component({ selector: 'pos-splash', templateUrl: 'splash.component.html', + imports: [ButtonDirective], }) export class PosSplashComponent { @Output() onComplete = new EventEmitter(); @@ -14,8 +17,27 @@ export class PosSplashComponent { private readonly posProfileStore = inject(PosProfileStore); private readonly posInfoStore = inject(PosInfoStore); private readonly deviceInfoStore = inject(DeviceInfoStore); + private readonly pwaInstallService = inject(PwaInstallService); logo = images.logo; + requireInstall = false; + // requireInstall = brandingConfig.enableInstallPrompt; + pwaInstalled = this.pwaInstallService.isInstalled; + canInstall = this.pwaInstallService.canInstall; + private installPromptShown = false; + + // constructor() { + // effect(() => { + // if (!this.requireInstall || this.pwaInstalled() || this.installPromptShown) { + // return; + // } + + // if (this.canInstall()) { + // this.installPromptShown = true; + // this.pwaInstallService.openInstallConfirm(); + // } + // }); + // } async ngOnInit() { const data = await this.deviceInfoStore.getData(); @@ -25,9 +47,27 @@ export class PosSplashComponent { forkJoin([profileObs, infoObs]) .pipe( finalize(() => { - this.onComplete.emit(); + if (!this.requireInstall || this.pwaInstalled()) { + this.onComplete.emit(); + return; + } + + if (this.canInstall()) { + this.pwaInstallService.openInstallConfirm(); + this.installPromptShown = true; + } }), ) .subscribe(([profileResult, infoResult]) => {}); } + + installPwa() { + this.pwaInstallService.openInstallConfirm(); + } + + continueAfterInstall() { + if (this.pwaInstalled()) { + this.onComplete.emit(); + } + } } diff --git a/src/app/domains/pos/layouts/mainMenuSidebar/main-menu-sidebar.component.html b/src/app/domains/pos/layouts/mainMenuSidebar/main-menu-sidebar.component.html index 560dae4..67e0132 100644 --- a/src/app/domains/pos/layouts/mainMenuSidebar/main-menu-sidebar.component.html +++ b/src/app/domains/pos/layouts/mainMenuSidebar/main-menu-sidebar.component.html @@ -19,18 +19,18 @@
  • - مشاهده‌ی فاکتورها + فاکتورها
  • @@ -41,7 +41,7 @@
  • @@ -52,7 +52,7 @@
  • diff --git a/src/app/domains/pos/layouts/mainMenuSidebar/main-menu-sidebar.component.ts b/src/app/domains/pos/layouts/mainMenuSidebar/main-menu-sidebar.component.ts index 117c1f4..1a0d762 100644 --- a/src/app/domains/pos/layouts/mainMenuSidebar/main-menu-sidebar.component.ts +++ b/src/app/domains/pos/layouts/mainMenuSidebar/main-menu-sidebar.component.ts @@ -1,3 +1,4 @@ +import { ToastService } from '@/core/services/toast.service'; import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog'; import { Component, computed, inject } from '@angular/core'; import { RouterLink } from '@angular/router'; @@ -16,6 +17,7 @@ import { PosInfoStore } from '../../store'; export class PosMainMenuSidebarComponent extends AbstractDialog { private readonly posInfoStore = inject(PosInfoStore); private readonly swUpdate = inject(SwUpdate); + private readonly toastService = inject(ToastService); readonly posInfo = computed(() => this.posInfoStore.entity()); appVersion = '0.0.0'; @@ -31,12 +33,15 @@ export class PosMainMenuSidebarComponent extends AbstractDialog { ngOnInit() { this.isPwaBuild = this.swUpdate.isEnabled; + console.log(this.swUpdate); + if (!this.isPwaBuild) return; fetch('/ngsw.json') .then((res) => res.json()) .then((data: { appData?: { appVersion?: string } }) => { this.appVersion = data?.appData?.appVersion || this.appVersion; + this.toastService.info({ text: this.appVersion }); console.log('appVersion:', data?.appData); }) .catch((err) => { diff --git a/src/app/domains/pos/modules/saleInvoices/components/list.component.ts b/src/app/domains/pos/modules/saleInvoices/components/list.component.ts index 225c6f3..e953ad2 100644 --- a/src/app/domains/pos/modules/saleInvoices/components/list.component.ts +++ b/src/app/domains/pos/modules/saleInvoices/components/list.component.ts @@ -1,3 +1,4 @@ +import { POS_ROUTES } from '@/domains/pos/routes'; import { InnerPagesHeaderComponent } from '@/shared/components/innerPagesHeader/inner-pages-header.component'; import { Component, computed, inject, signal } from '@angular/core'; import { Router } from '@angular/router'; @@ -70,7 +71,7 @@ export class PosSaleInvoiceListComponent { }); }); activeFilterCount = computed(() => this.activeFilters().length); - backRoute = '/pos'; + backRoute = POS_ROUTES.path || '/'; ngOnInit() { this.getData(); diff --git a/src/app/pages/notfound/notfound.component.ts b/src/app/pages/notfound/notfound.component.ts index 56ce444..f635a81 100644 --- a/src/app/pages/notfound/notfound.component.ts +++ b/src/app/pages/notfound/notfound.component.ts @@ -1,7 +1,8 @@ -import { Component, inject } from '@angular/core'; +import { Component } from '@angular/core'; import { RouterModule } from '@angular/router'; import { ButtonModule } from 'primeng/button'; import images from 'src/assets/images'; +import config from 'src/config'; @Component({ selector: 'app-notfound', @@ -12,6 +13,8 @@ import images from 'src/assets/images'; export class Notfound { readonly logo = images.logo; + isPosApplication = config.isPosApplication; + readonly notFoundListItems = [ { icon: 'pi-table', diff --git a/src/environments/environment.tis.ts b/src/environments/environment.tis.ts index 005ef5f..8718ece 100644 --- a/src/environments/environment.tis.ts +++ b/src/environments/environment.tis.ts @@ -1,8 +1,8 @@ // TIS tenant environment configuration export const environment = { production: true, - apiBaseUrl: 'http://194.59.214.243:5002', - // apiBaseUrl: 'http://192.168.128.73:5002', + // apiBaseUrl: 'http://194.59.214.243:5002', + apiBaseUrl: 'http://192.168.128.73:5002', host: 'localhost', port: 5000, enableLogging: false, diff --git a/src/tenants/default/branding.config.ts b/src/tenants/default/branding.config.ts index a1223a1..1833eb2 100644 --- a/src/tenants/default/branding.config.ts +++ b/src/tenants/default/branding.config.ts @@ -2,10 +2,12 @@ export interface BrandingConfig { appTitle: string; manifestPath: string; themeColor: string; + enableInstallPrompt?: boolean; } export const brandingConfig: BrandingConfig = { appTitle: 'پنل مدیریت صورت‌حساب‌های مالیاتی', manifestPath: '/favicon/site.webmanifest', themeColor: '#ffffff', + enableInstallPrompt: false, }; diff --git a/src/tenants/tis/branding.config.ts b/src/tenants/tis/branding.config.ts index a154735..71d1a71 100644 --- a/src/tenants/tis/branding.config.ts +++ b/src/tenants/tis/branding.config.ts @@ -4,4 +4,5 @@ export const brandingConfig: BrandingConfig = { appTitle: 'تیس - مدیریت صورت‌حساب‌های مالیاتی', manifestPath: '/favicon/site.webmanifest', themeColor: '#ffffff', + enableInstallPrompt: true, };