Compare commits

...

2 Commits

Author SHA1 Message Date
ahasani 9bcb917d3b update 2026-05-11 18:42:05 +03:30
ahasani 88adb566eb feat: streamline routing and layout initialization for POS module 2026-05-11 16:24:57 +03:30
21 changed files with 198 additions and 31 deletions
+4
View File
@@ -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"
+3 -2
View File
@@ -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": [
{
+12
View File
@@ -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",
+14 -2
View File
@@ -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"
}
],
+6 -1
View File
@@ -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')
+7 -7
View File
@@ -10,13 +10,13 @@ export const appRoutes: Routes = [
{
path: '',
loadComponent: () => import('@/layout/default/app.layout.component').then((m) => m.AppLayout),
children: [
SUPER_ADMIN_ROUTES,
CONSUMER_ROUTES,
PROVIDER_ROUTES,
PARTNER_ROUTES,
...POS_ROUTES.children!,
],
children: [SUPER_ADMIN_ROUTES, CONSUMER_ROUTES, PROVIDER_ROUTES, PARTNER_ROUTES],
},
{
path: 'pos',
loadComponent: () =>
import('@/domains/pos/layouts/layout.component').then((m) => m.PosLayoutComponent),
children: [POS_ROUTES],
},
{
+2 -1
View File
@@ -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<IAuthResponse>(
'/api/v1/auth/login',
{ username, password },
{ username, password, isPos: config.isPosApplication },
{
headers: baseHeaders,
},
+1
View File
@@ -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';
@@ -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);
});
},
});
}
}
@@ -1,4 +1,18 @@
<div class="h-svh w-svw flex flex-col items-center justify-center">
{{ requireInstall }}
{{ pwaInstalled() }}
<img [src]="logo" class="w-32 animate-pulse" />
<span class="text-xl font-medium text-center text-muted-color mt-4"> در حال بارگذاری ... </span>
@if (requireInstall && !pwaInstalled()) {
<div class="mt-6 flex flex-col items-center gap-3">
<span class="text-base font-medium text-center text-muted-color">برای ادامه، نصب برنامه الزامی است.</span>
@if (canInstall()) {
<button pButton type="button" size="small" (click)="installPwa()">نصب برنامه</button>
} @else {
<span class="text-sm text-center text-muted-color">منتظر فعال شدن امکان نصب در مرورگر...</span>
}
<button pButton type="button" outlined size="small" (click)="continueAfterInstall()">ادامه</button>
</div>
} @else {
<span class="text-xl font-medium text-center text-muted-color mt-4"> در حال بارگذاری ... </span>
}
</div>
@@ -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<void>();
@@ -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(() => {
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();
}
}
}
@@ -25,7 +25,6 @@
<!-- [ngStyle]="{ transform: 'scale(1.' + pullDistance() * 10 + ')' }" -->
</div>
}
{{ isAuth() }}
@if (isAuth()) {
@defer (when isAuth()) {
<router-outlet></router-outlet>
@@ -19,18 +19,18 @@
<li>
<a
pRipple
routerLink="/pos/sale_invoices"
routerLink="/sale_invoices"
(click)="drawerRef.close($event)"
class="flex items-center cursor-pointer p-4 rounded-border text-surface-700 dark:text-surface-100 hover:bg-surface-100 dark:hover:bg-surface-700 duration-150 transition-colors p-ripple"
>
<i class="pi pi-home me-2"></i>
<span class="font-medium">مشاهده‌ی فاکتورها</span>
<span class="font-medium">فاکتورها</span>
</a>
</li>
<li>
<a
pRipple
routerLink="/pos"
routerLink="/"
(click)="drawerRef.close($event)"
class="flex items-center cursor-pointer p-4 rounded-border text-surface-700 dark:text-surface-100 hover:bg-surface-100 dark:hover:bg-surface-700 duration-150 transition-colors p-ripple"
>
@@ -41,7 +41,7 @@
<li>
<a
pRipple
routerLink="/pos/about"
routerLink="/about"
(click)="drawerRef.close($event)"
class="flex items-center cursor-pointer p-4 rounded-border text-surface-700 dark:text-surface-100 hover:bg-surface-100 dark:hover:bg-surface-700 duration-150 transition-colors p-ripple"
>
@@ -52,7 +52,7 @@
<li>
<a
pRipple
routerLink="/pos/support"
routerLink="/support"
(click)="drawerRef.close($event)"
class="flex items-center cursor-pointer p-4 rounded-border text-surface-700 dark:text-surface-100 hover:bg-surface-100 dark:hover:bg-surface-700 duration-150 transition-colors p-ripple"
>
@@ -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) => {
@@ -116,6 +116,10 @@ export class PosPagesLayoutComponent implements AfterViewInit {
this.getData();
}
ngOnInit() {
this.layoutService.changeIsFullPage(true);
}
ngAfterViewInit() {
this.layoutService.setTopbarStartSlot(this.topbarStart);
this.layoutService.setTopbarCenterSlot(this.topbarCenter);
@@ -61,10 +61,6 @@
</div>
</div>
@defer (when isVisibleCustomerForm()) {
<pos-order-customer-dialog [(visible)]="isVisibleCustomerForm" (onSubmit)="submitCustomer()" />
}
<!-- <pos-pre-invoice-dialog [(visible)]="isVisiblePreInvoiceForm" /> -->
@defer (when isVisiblePaymentForm()) {
<pos-payment-form-dialog [(visible)]="isVisiblePaymentForm" (onSubmit)="submitPayment($event)" />
}
@@ -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();
+4 -1
View File
@@ -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',
+2 -2
View File
@@ -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,
+2
View File
@@ -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,
};
+1
View File
@@ -4,4 +4,5 @@ export const brandingConfig: BrandingConfig = {
appTitle: 'تیس - مدیریت صورت‌حساب‌های مالیاتی',
manifestPath: '/favicon/site.webmanifest',
themeColor: '#ffffff',
enableInstallPrompt: true,
};