feat(layout): enhance topbar with customizable templates and full-page support

- Updated app.layout.component.html to include start, center, and end templates for the topbar.
- Modified app.layout.component.ts to manage new template references and added isFullPage getter.
- Refactored app.topbar.component.html to utilize new templates and improved layout structure.
- Enhanced app.topbar.component.ts to accept new input properties for templates.
- Updated layout.service.ts to manage full-page state and new topbar slots.
- Added new payment bridge services for POS functionality.
- Introduced greater validator for form validation.
- Improved dialog component to support mobile drawer and responsive design.
- Updated styles for better mobile support and layout adjustments.
- Added new main menu sidebar for POS with dynamic content.
This commit is contained in:
2026-04-30 16:27:42 +03:30
parent c89d4027d6
commit 8104f1b7a7
56 changed files with 1130 additions and 434 deletions
@@ -1,8 +1,31 @@
<ng-template #topbarStart>
<p-button (click)="toggleMenu()" icon="pi pi-bars" outlined size="large" />
</ng-template>
<ng-template #topbarCenter>
@if (posInfo()) {
<div class="flex flex-col items-center justify-center gap-2">
<div class="w-8 h-8">
<img
[src]="posInfo()?.partner?.logo_url ?? placeholderLogo"
alt="Logo"
class="w-full h-full object-cover rounded-md bg-surface-card"
/>
</div>
<span class="text-base font-semibold">{{ posInfo()?.partner?.name }}</span>
</div>
}
</ng-template>
<ng-template #topbarEnd>
<p-button (click)="toggleMenu()" icon="pi pi-user" outlined size="large" />
</ng-template>
@if (loading()) {
<shared-page-loading />
} @else {
<div class="w-svw h-svh bg-surface-ground flex flex-col gap-4 p-4">
<div class="w-full flex items-center gap-4 shrink-0 justify-between">
<!-- <div class="w-svw h-svh bg-surface-ground flex flex-col gap-4 p-4"> -->
<!-- <div class="w-full flex items-center gap-4 shrink-0 justify-between">
<div class="flex gap-2 items-center">
@if (posInfo()) {
<div class="w-10 h-10">
@@ -24,27 +47,28 @@
</button>
</div>
</div>
</div>
@if (error()) {
@switch (error()?.status) {
@case (412) {
<pos-choose-cards class="h-full" (onChoose)="onChoosePos()" />
}
@default {
<div class="h-full w-full flex items-center justify-center">
<p-card class="border border-surface-border">
<div class="flex flex-col gap-2 items-center justify-between shrink-0">
<i class="pi pi-exclamation-triangle text-6xl! mb-3 text-error" aria-hidden="true"></i>
<span class="text-xl font-semibold mb-2 text-error block"> عدم دسترسی </span>
<p class="inline-block text-base">متاسفانه امکان دسترسی برای کاربر شما فراهم نیست</p>
<button pButton outlined severity="danger" class="mt-5 w-32" (click)="logout()">خروج</button>
</div>
</p-card>
</div>
}
</div> -->
@if (error()) {
@switch (error()?.status) {
@case (412) {
<pos-choose-cards class="h-full" (onChoose)="onChoosePos()" />
}
@default {
<div class="h-full w-full flex items-center justify-center">
<p-card class="border border-surface-border">
<div class="flex flex-col gap-2 items-center justify-between shrink-0">
<i class="pi pi-exclamation-triangle text-6xl! mb-3 text-error" aria-hidden="true"></i>
<span class="text-xl font-semibold mb-2 text-error block"> عدم دسترسی </span>
<p class="inline-block text-base">متاسفانه امکان دسترسی برای کاربر شما فراهم نیست</p>
<button pButton outlined severity="danger" class="mt-5 w-32" (click)="logout()">خروج</button>
</div>
</p-card>
</div>
}
} @else {
<router-outlet></router-outlet>
}
</div>
} @else {
<pos-main-menu-sidebar [(visible)]="mainMenuVisible" />
<router-outlet></router-outlet>
}
<!-- </div> -->
}
@@ -1,15 +1,25 @@
import { AuthService } from '@/core';
import { LayoutService } from '@/layout/service/layout.service';
import { PageLoadingComponent } from '@/shared/components/page-loading.component';
import { JalaliDateDirective } from '@/shared/directives';
import { Component, computed, inject } from '@angular/core';
import {
AfterViewInit,
Component,
TemplateRef,
ViewChild,
computed,
inject,
signal,
} from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { MenuItem } from 'primeng/api';
import { ButtonDirective } from 'primeng/button';
import { Button, ButtonDirective } from 'primeng/button';
import { Card } from 'primeng/card';
import { Menu } from 'primeng/menu';
import images from 'src/assets/images';
import { PosInfoStore, PosProfileStore } from '../store';
import { PosChooseCardsComponent } from './choose-pos.component';
import { PosMainMenuSidebarComponent } from './mainMenuSidebar/main-menu-sidebar.component';
@Component({
selector: 'pos-layout',
@@ -22,14 +32,22 @@ import { PosChooseCardsComponent } from './choose-pos.component';
ButtonDirective,
Card,
PosChooseCardsComponent,
Button,
PosMainMenuSidebarComponent,
],
})
export class PosLayoutComponent {
export class PosLayoutComponent implements AfterViewInit {
constructor() {}
@ViewChild('topbarStart', { static: true }) topbarStart!: TemplateRef<any>;
@ViewChild('topbarCenter', { static: true }) topbarCenter!: TemplateRef<any>;
@ViewChild('topbarEnd', { static: true }) topbarEnd!: TemplateRef<any>;
private readonly posProfileStore = inject(PosProfileStore);
private readonly posInfoStore = inject(PosInfoStore);
private readonly authService = inject(AuthService);
private readonly layoutService = inject(LayoutService);
mainMenuVisible = signal(false);
readonly posProfileLoading = computed(() => this.posProfileStore.loading());
readonly posProfile = computed(() => this.posProfileStore.entity());
@@ -71,11 +89,28 @@ export class PosLayoutComponent {
});
}
toggleMenu() {
this.mainMenuVisible.update((v) => !v);
}
onChoosePos() {
this.getData();
}
ngOnInit() {
this.layoutService.changeIsFullPage(true);
this.getData();
}
ngAfterViewInit() {
this.layoutService.setTopbarStartSlot(this.topbarStart);
this.layoutService.setTopbarCenterSlot(this.topbarCenter);
this.layoutService.setTopbarEndSlot(this.topbarEnd);
}
ngOnDestroy() {
this.layoutService.setTopbarStartSlot(null);
this.layoutService.setTopbarCenterSlot(null);
this.layoutService.setTopbarEndSlot(null);
}
}
@@ -0,0 +1,67 @@
<p-drawer #drawerRef position="right" [(visible)]="visible">
<ng-template #headless>
<div class="flex flex-col h-full">
<div class="flex items-center justify-between px-6 pt-4 shrink-0">
<div class="flex flex-col gap-2">
<span class="font-semibold text-xl text-primary">{{ posInfo()?.name }}</span>
<span class="font-medium text-md text-muted-color">
{{ posInfo()?.businessActivity?.name }} - {{ posInfo()?.complex?.name }}
</span>
</div>
<span>
<p-button type="button" (click)="drawerRef.close($event)" icon="pi pi-times" outlined="true"></p-button>
</span>
</div>
<hr class="mt-4 mx-4 border-b border-0 border-surface" />
<div class="overflow-y-auto">
<ul class="list-none p-4 m-0">
<li>
<a
pRipple
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>
</a>
</li>
<li>
<a
pRipple
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>
</a>
</li>
<li>
<a
pRipple
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>
</a>
</li>
<li>
<a
pRipple
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>
</a>
</li>
</ul>
</div>
<div class="mt-auto">
<hr class="mb-4 mx-4 border-t border-0 border-surface" />
@if (isPwaBuild) {
<div class="px-6 pb-6 text-sm text-muted-color">
نسخه برنامه : <span class="font-semibold">{{ appVersion }}</span>
</div>
}
</div>
</div>
</ng-template>
</p-drawer>
@@ -0,0 +1,51 @@
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
import { Component, computed, inject } from '@angular/core';
import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';
import { Button } from 'primeng/button';
import { Drawer } from 'primeng/drawer';
import { Ripple } from 'primeng/ripple';
import { filter } from 'rxjs';
import { PosInfoStore } from '../../store';
@Component({
selector: 'pos-main-menu-sidebar',
templateUrl: './main-menu-sidebar.component.html',
imports: [Drawer, Button, Ripple],
})
export class PosMainMenuSidebarComponent extends AbstractDialog {
private readonly posInfoStore = inject(PosInfoStore);
private readonly swUpdate = inject(SwUpdate);
readonly posInfo = computed(() => this.posInfoStore.entity());
appVersion = '0.0.0';
isPwaBuild = false;
readonly posName = computed(() => {
if (this.posInfo()) {
const { name, businessActivity, complex } = this.posInfo()!;
return `${name} (${businessActivity.name} - ${complex.name})`;
}
return '';
});
ngOnInit() {
this.isPwaBuild = this.swUpdate.isEnabled;
if (!this.isPwaBuild) return;
fetch('/ngsw.json')
.then((res) => res.json())
.then((data: { appData?: { appVersion?: string } }) => {
this.appVersion = data?.appData?.appVersion || this.appVersion;
})
.catch(() => {});
this.swUpdate.versionUpdates
.pipe(filter((event): event is VersionReadyEvent => event.type === 'VERSION_READY'))
.subscribe((event) => {
const appData = event.latestVersion.appData as { appVersion?: string } | undefined;
this.appVersion = appData?.appVersion || this.appVersion;
});
this.swUpdate.checkForUpdate().catch(() => {});
}
}