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:
+19
-3
@@ -1,4 +1,4 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, HostListener } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { ConfirmDialog } from 'primeng/confirmdialog';
|
||||
import { ToastModule } from 'primeng/toast';
|
||||
@@ -8,9 +8,25 @@ import { ToastModule } from 'primeng/toast';
|
||||
standalone: true,
|
||||
imports: [RouterModule, ToastModule, ConfirmDialog],
|
||||
template: `
|
||||
<p-toast position="bottom-right" />
|
||||
<p-toast [position]="toastPosition" />
|
||||
<p-confirmDialog />
|
||||
<router-outlet />
|
||||
`,
|
||||
})
|
||||
export class AppComponent {}
|
||||
export class AppComponent {
|
||||
toastPosition: 'top-center' | 'bottom-right' = 'bottom-right';
|
||||
|
||||
constructor() {
|
||||
this.updateToastPosition();
|
||||
}
|
||||
|
||||
@HostListener('window:resize')
|
||||
onResize() {
|
||||
this.updateToastPosition();
|
||||
}
|
||||
|
||||
private updateToastPosition() {
|
||||
this.toastPosition =
|
||||
typeof window !== 'undefined' && window.innerWidth <= 768 ? 'top-center' : 'bottom-right';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { Maybe } from '../models';
|
||||
|
||||
interface INativeBridgeHost {
|
||||
pay?: (payload: string) => unknown;
|
||||
@@ -8,8 +9,7 @@ interface INativeBridgeHost {
|
||||
|
||||
export interface INativePayRequest {
|
||||
amount: number;
|
||||
totalAmount: number;
|
||||
invoiceDate: string;
|
||||
id: Maybe<string>;
|
||||
}
|
||||
|
||||
export interface INativePrintRequest {
|
||||
@@ -55,9 +55,9 @@ export class NativeBridgeService {
|
||||
}
|
||||
|
||||
private invoke(method: 'pay' | 'print', payload: object): INativeBridgeResult {
|
||||
if (!this.isEnabled()) {
|
||||
return { success: false, error: 'Native bridge is disabled for this tenant.' };
|
||||
}
|
||||
// if (!this.isEnabled()) {
|
||||
// return { success: false, error: 'Native bridge is disabled for this tenant.' };
|
||||
// }
|
||||
|
||||
const fn = this.host?.[method];
|
||||
if (typeof fn !== 'function') {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
|
||||
|
||||
/**
|
||||
* Strictly checks numeric value is greater than `minValue` (not equal).
|
||||
* Empty values are treated as valid; combine with `Validators.required` when needed.
|
||||
*/
|
||||
export function greaterThanValidator(minValue: number): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
const v = control.value;
|
||||
if (v === null || v === undefined || v === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const numericValue = typeof v === 'number' ? v : Number(v);
|
||||
if (Number.isNaN(numericValue)) {
|
||||
return { greaterThan: 'مقدار باید عددی باشد' };
|
||||
}
|
||||
|
||||
return numericValue > minValue
|
||||
? null
|
||||
: {
|
||||
greaterThan: `مقدار باید بیشتر از ${minValue} باشد`,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './fiscal-code.validator';
|
||||
export * from './greater.validator';
|
||||
export * from './iban.validator';
|
||||
export * from './mobile.validator';
|
||||
export * from './must-match.validator';
|
||||
|
||||
@@ -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(() => {});
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,11 @@ import ISummary from '@/core/models/summary';
|
||||
|
||||
export interface IPosInfoRawResponse {
|
||||
name: string;
|
||||
complex: {
|
||||
id: string;
|
||||
name: string;
|
||||
branch_code: string;
|
||||
};
|
||||
businessActivity: ISummary;
|
||||
guild: ISummary;
|
||||
complex: Complex;
|
||||
businessActivity: BusinessActivity;
|
||||
guild: Guild;
|
||||
license_info: LicenseInfo;
|
||||
partner: Partner;
|
||||
}
|
||||
|
||||
export interface IPosInfoResponse extends IPosInfoRawResponse {}
|
||||
@@ -21,3 +19,25 @@ export interface IPosAccessibleResponse extends IPosAccessibleRawResponse {}
|
||||
interface Complex extends ISummary {
|
||||
business_activity: ISummary;
|
||||
}
|
||||
|
||||
interface LicenseInfo {
|
||||
expires_at: string;
|
||||
license_id: string;
|
||||
}
|
||||
|
||||
interface Guild extends ISummary {
|
||||
code: string;
|
||||
}
|
||||
|
||||
interface Partner extends ISummary {
|
||||
code: string;
|
||||
logo_url?: string;
|
||||
}
|
||||
|
||||
interface BusinessActivity extends ISummary {
|
||||
economic_code: string;
|
||||
}
|
||||
|
||||
interface Complex extends ISummary {
|
||||
branch_code: string;
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,4 +1,5 @@
|
||||
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
|
||||
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
|
||||
import { CustomerType } from '@/shared/localEnum/constants/customerTypes';
|
||||
import { Component, EventEmitter, inject, Output, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
@@ -7,7 +8,6 @@ import { PosLandingStore } from '../../store/main.store';
|
||||
import { CustomerIndividualFormComponent } from './individual/form.component';
|
||||
import { CustomerLegalFormComponent } from './legal/form.component';
|
||||
import { CustomerUnknownFormComponent } from './unknown/form.component';
|
||||
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'pos-order-customer-dialog',
|
||||
@@ -28,15 +28,15 @@ export class PosOrderCustomerDialogComponent extends AbstractDialog {
|
||||
|
||||
customerTypes = [
|
||||
{
|
||||
label: 'بدون اطلاعات خریدار (نوع دوم)',
|
||||
label: 'بدون اطلاعات (نوع دوم)',
|
||||
value: 'UNKNOWN',
|
||||
},
|
||||
{
|
||||
label: 'خریدار حقیقی (نوع اول)',
|
||||
label: 'حقیقی (نوع اول)',
|
||||
value: 'INDIVIDUAL',
|
||||
},
|
||||
{
|
||||
label: 'خریدار حقوقی (نوع اول)',
|
||||
label: 'حقوقی (نوع اول)',
|
||||
value: 'LEGAL',
|
||||
},
|
||||
] as {
|
||||
|
||||
@@ -1,29 +1,40 @@
|
||||
<div class="flex flex-col">
|
||||
<div class="sticky top-0 bg-surface-ground z-10 pb-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 text-muted-color">
|
||||
<div class="flex flex-col min-h-full">
|
||||
<div class="bg-surface-ground z-10 p-4 border-b border-surface-border shadow-[0_-4px_16px_rgba(0,0,0,0.08)]">
|
||||
<div class="flex items-center justify-end mb-4">
|
||||
<!-- <div class="flex items-center gap-2 text-muted-color">
|
||||
<i class="pi pi-list"></i>
|
||||
<span class="text-base font-bold">لیست کالاها</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<app-search-input (search)="onSearch($event)"></app-search-input>
|
||||
|
||||
<div class="card p-2! flex justify-center">
|
||||
<p-selectbutton [options]="viewOptions" [(ngModel)]="viewType" optionValue="value">
|
||||
<ng-template #item let-item>
|
||||
<i [class]="item.icon"></i>
|
||||
</ng-template>
|
||||
</p-selectbutton>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
<app-search-input class="sm:w-auto w-full max-w-xs" (search)="onSearch($event)"></app-search-input>
|
||||
<p-divider layout="vertical" />
|
||||
<p-selectbutton
|
||||
[options]="viewOptions"
|
||||
[(ngModel)]="viewType"
|
||||
optionValue="value"
|
||||
[allowEmpty]="false"
|
||||
class="shrink-0 border border-surface-border bg-surface-ground"
|
||||
>
|
||||
<ng-template #item let-item>
|
||||
<i [class]="item.icon"></i>
|
||||
</ng-template>
|
||||
</p-selectbutton>
|
||||
</div>
|
||||
</div>
|
||||
<pos-good-categories (categoryChange)="onCategoryChange($event)" />
|
||||
</div>
|
||||
@if (viewType === "grid") {
|
||||
<pos-good-grid-view (onAdd)="addGood($event)" />
|
||||
} @else {
|
||||
<pos-goods-list-view (onAdd)="addGood($event)" />
|
||||
}
|
||||
<div [class]="`p-4 ${!loading() && !goods()?.length ? ' grow flex items-center justify-center' : ''}`">
|
||||
@if (!loading() && !goods()?.length) {
|
||||
<div class="flex flex-col items-center gap-4 mt-10">
|
||||
<i class="pi pi-box text-6xl! text-muted-color"></i>
|
||||
<span class="text-xl font-semibold text-muted-color">کالایی برای نمایش وجود ندارد.</span>
|
||||
</div>
|
||||
} @else if (viewType === "grid") {
|
||||
<pos-good-grid-view (onAdd)="addGood($event)" />
|
||||
} @else {
|
||||
<pos-goods-list-view (onAdd)="addGood($event)" />
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (selectedGoodToAdd()) {
|
||||
<pos-payload-form-dialog
|
||||
|
||||
@@ -3,6 +3,7 @@ import { IGoodResponse } from '@/domains/pos/models/good.io';
|
||||
import { SearchInputComponent } from '@/shared/components/search/search-input.component';
|
||||
import { Component, computed, inject, Input, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Divider } from 'primeng/divider';
|
||||
import { SelectButton } from 'primeng/selectbutton';
|
||||
import { PosLandingStore, TViewType } from '../store/main.store';
|
||||
import { PosGoodCategoriesComponent } from './categories.component';
|
||||
@@ -21,6 +22,7 @@ import { PayloadFormDialogComponent } from './payloads';
|
||||
PosGoodsGridViewComponent,
|
||||
SearchInputComponent,
|
||||
PayloadFormDialogComponent,
|
||||
Divider,
|
||||
],
|
||||
})
|
||||
export class PosGoodsComponent {
|
||||
@@ -42,6 +44,10 @@ export class PosGoodsComponent {
|
||||
|
||||
readonly activatedCategoryProducts = computed(() => this.store.activatedCategoryGoods());
|
||||
readonly stock = computed(() => this.store.goods());
|
||||
readonly inOrderGoods = computed(() => this.store.inOrderGoods());
|
||||
readonly priceInfo = computed(() => this.store.orderPricingInfo());
|
||||
readonly goods = computed(() => this.store.filteredGoods());
|
||||
readonly loading = computed(() => this.store.getGoodsLoading());
|
||||
|
||||
get viewType() {
|
||||
return this.store.viewType();
|
||||
@@ -52,7 +58,7 @@ export class PosGoodsComponent {
|
||||
|
||||
onSearch(searchTerm: string) {
|
||||
this.store.updateSearchQuery(searchTerm);
|
||||
this.store.getGoods();
|
||||
this.store.filterGoods();
|
||||
}
|
||||
|
||||
onCategoryChange(categoryId: string) {
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
}
|
||||
} @else {
|
||||
@for (good of goods(); track good.id) {
|
||||
<div class="flex-1 bg-surface-card rounded-lg p-1 shadow-xs">
|
||||
<div class="flex-1 bg-surface-card rounded-lg p-1 shadow-xs" (click)="addProduct(good)">
|
||||
<img [src]="good.image_url || goodPlaceholder" class="w-full aspect-[1.3] object-cover rounded-md" />
|
||||
<div class="mt-2">
|
||||
<span class="text-lg font-bold">
|
||||
<div class="mt-2 text-center">
|
||||
<span class="text-lg font-bold text-center">
|
||||
{{ good.name }}
|
||||
@if (!good.is_default_guild_good) {
|
||||
<small class="text-xs text-muted-color">*</small>
|
||||
@@ -18,9 +18,7 @@
|
||||
</span>
|
||||
<div class="flex items-center justify-between mt-1 w-full px-2 my-2">
|
||||
<!-- <span [appPriceMask]="good.salePrice" class="text-base font-bold"></span> -->
|
||||
<button pButton type="button" icon="pi pi-plus" size="small" class="w-full!" (click)="addProduct(good)">
|
||||
انتخاب
|
||||
</button>
|
||||
<button pButton type="button" icon="pi pi-plus" class="w-full!">انتخاب</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ export class PosGoodsGridViewComponent {
|
||||
private readonly store = inject(PosLandingStore);
|
||||
@Output() onAdd = new EventEmitter<IGoodResponse>();
|
||||
|
||||
goods = computed(() => this.store.activatedCategoryGoods());
|
||||
goods = computed(() => this.store.filteredGoods());
|
||||
loading = computed(() => this.store.getGoodsLoading());
|
||||
|
||||
goodPlaceholder = images.placeholders.default;
|
||||
|
||||
@@ -14,7 +14,7 @@ export class PosGoodsListViewComponent {
|
||||
private readonly store = inject(PosLandingStore);
|
||||
@Output() onAdd = new EventEmitter<IGoodResponse>();
|
||||
|
||||
goods = computed(() => this.store.activatedCategoryGoods());
|
||||
goods = computed(() => this.store.filteredGoods());
|
||||
loading = computed(() => this.store.getGoodsLoading());
|
||||
|
||||
goodPlaceholder = images.placeholders.default;
|
||||
|
||||
-3
@@ -12,9 +12,6 @@
|
||||
<span class="font-bold text-lg text-ellipsis text-nowrap overflow-hidden">
|
||||
{{ inOrderGood.good.name }}
|
||||
</span>
|
||||
<div class="grow">
|
||||
<span class="text-sm text-muted-color">({{ inOrderGood.good.sku }})</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-sm text-ellipsis text-nowrap overflow-hidden">
|
||||
{{ inOrderGood.quantity }} {{ enumTranslator(inOrderGood.good.unit_type) }}
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
<div class="bg-surface-card w-md shrink-0 h-full overflow-hidden rounded-xl p-4 pb-0 flex flex-col">
|
||||
<div class="bg-surface-card shrink-0 h-full overflow-hidden md:rounded-xl md:p-4 md:w-md pb-0 flex flex-col">
|
||||
<div class="grow flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-between shrink-0">
|
||||
<div class="flex items-center gap-2 text-muted-color">
|
||||
<i class="pi pi-shopping-cart"></i>
|
||||
<span class="text-base font-bold">سفارشها</span>
|
||||
</div>
|
||||
<button pButton type="button" icon="pi pi-refresh" outlined size="small" (click)="clearOrderList()">
|
||||
پاک کردن سفارشها
|
||||
</button>
|
||||
<div class="flex gap-2 shrink-0">
|
||||
<button pButton type="button" icon="pi pi-plus" outlined size="small" (click)="addMoreGoods()">
|
||||
افزودن کالا
|
||||
</button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-refresh"
|
||||
outlined
|
||||
severity="danger"
|
||||
size="small"
|
||||
(click)="clearOrderList()"
|
||||
>
|
||||
پاک کردن سفارشها
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (!inOrderGoods().length) {
|
||||
<div class="grow flex flex-col gap-5 justify-center items-center text-center text-muted-color">
|
||||
<div class="grow flex flex-col gap-5 justify-center items-center text-center text-muted-color py-5">
|
||||
<i class="pi pi-inbox text-6xl!"></i>
|
||||
<span class="text-lg text-muted-color">هیچ سفارشی ثبت نشده است.</span>
|
||||
</div>
|
||||
@@ -42,7 +55,7 @@
|
||||
severity="primary"
|
||||
icon="pi pi-check"
|
||||
class="w-full"
|
||||
[attr.disabled]="inOrderGoods().length === 0 ? true : null"
|
||||
size="large"
|
||||
(click)="submitAndPay()"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// import { CustomersSelectComponent } from '@/modules/customers/components/select/select.component';
|
||||
// import { ICustomerResponse } from '@/modules/customers/models';
|
||||
import { KeyValueComponent } from '@/shared/components';
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { Component, computed, EventEmitter, inject, Output, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { Card } from 'primeng/card';
|
||||
@@ -30,6 +30,7 @@ import { POSOrderPriceInfoCardComponent } from './price-info-card.component';
|
||||
})
|
||||
export class PosOrderSectionComponent {
|
||||
private readonly store = inject(PosLandingStore);
|
||||
@Output() onAddMoreGoods = new EventEmitter<void>();
|
||||
|
||||
placeholderImage = images.placeholders.default;
|
||||
|
||||
@@ -77,6 +78,10 @@ export class PosOrderSectionComponent {
|
||||
this.isVisiblePaymentForm.set(true);
|
||||
}
|
||||
|
||||
addMoreGoods() {
|
||||
this.onAddMoreGoods.emit();
|
||||
}
|
||||
|
||||
submitCustomer() {}
|
||||
submitPayment() {}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
>
|
||||
@if (good) {
|
||||
@if (isGoldMode()) {
|
||||
<pos-gold-payload-form [initialValues]="goldPayload()" (onSubmit)="submit($event)" />
|
||||
<pos-gold-payload-form [initialValues]="goldPayload()" [editMode]="editMode()" (onSubmit)="submit($event)" />
|
||||
} @else if (isStandardMode()) {
|
||||
<pos-standard-payload-form
|
||||
[initialValues]="standardPayload()"
|
||||
[unitType]="good.unit_type"
|
||||
[editMode]="editMode()"
|
||||
(onSubmit)="submit($event)"
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { IGoodResponse } from '@/domains/pos/models/good.io';
|
||||
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
|
||||
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
|
||||
import { Component, computed, EventEmitter, inject, Input, Output, signal } from '@angular/core';
|
||||
import { IGoldPayload, IPosOrderItem, IStandardPayload, TPosOrderGoodPayload } from '../models';
|
||||
import { PosLandingStore } from '../store/main.store';
|
||||
import { PosGoldPayloadFormComponent } from './payloads/gold/form.component';
|
||||
import { PosStandardPayloadFormComponent } from './payloads/standard/form.component';
|
||||
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'pos-payload-form-dialog',
|
||||
|
||||
@@ -1,35 +1,37 @@
|
||||
<form [formGroup]="form">
|
||||
<app-input [control]="form.controls.unit_price" name="unit_price" label="قیمت پایه هر گرم" type="price" />
|
||||
<app-input [control]="form.controls.quantity" name="quantity" label="مقدار" type="number" suffix="گرم" />
|
||||
<app-enum-select [control]="form.controls.payload.controls.karat" type="goldKarat" name="karat" />
|
||||
<div class="form-group">
|
||||
<form [formGroup]="form">
|
||||
<app-input [control]="form.controls.unit_price" name="unit_price" label="قیمت پایه هر گرم" type="price" />
|
||||
<app-input [control]="form.controls.quantity" name="quantity" label="مقدار" type="number" suffix="گرم" />
|
||||
<app-enum-select [control]="form.controls.payload.controls.karat" type="goldKarat" name="karat" />
|
||||
|
||||
<app-amount-percentage-input
|
||||
[percentageControl]="form.controls.payload.controls.wages_percentage"
|
||||
[amountControl]="form.controls.payload.controls.wages_amount"
|
||||
[baseAmount]="unitWithQuantity()"
|
||||
[minAmount]="0"
|
||||
[maxAmount]="unitWithQuantity()"
|
||||
name="wages"
|
||||
label="اجرت"
|
||||
/>
|
||||
<app-amount-percentage-input
|
||||
[percentageControl]="form.controls.payload.controls.commission_percentage"
|
||||
[amountControl]="form.controls.payload.controls.commission_amount"
|
||||
[baseAmount]="unitWithQuantity()"
|
||||
[minAmount]="0"
|
||||
[maxAmount]="unitWithQuantity()"
|
||||
name="commission"
|
||||
label="حقالعمل"
|
||||
/>
|
||||
<app-amount-percentage-input
|
||||
[percentageControl]="form.controls.payload.controls.profit_percentage"
|
||||
[amountControl]="form.controls.payload.controls.profit_amount"
|
||||
[baseAmount]="totalAmountBeforeProfit()"
|
||||
[minAmount]="0"
|
||||
[maxAmount]="totalAmountBeforeProfit()"
|
||||
name="profit"
|
||||
label="سود"
|
||||
/>
|
||||
<app-amount-percentage-input
|
||||
[percentageControl]="form.controls.payload.controls.wages_percentage"
|
||||
[amountControl]="form.controls.payload.controls.wages_amount"
|
||||
[baseAmount]="unitWithQuantity()"
|
||||
[minAmount]="0"
|
||||
[maxAmount]="unitWithQuantity()"
|
||||
name="wages"
|
||||
label="اجرت"
|
||||
/>
|
||||
<app-amount-percentage-input
|
||||
[percentageControl]="form.controls.payload.controls.commission_percentage"
|
||||
[amountControl]="form.controls.payload.controls.commission_amount"
|
||||
[baseAmount]="unitWithQuantity()"
|
||||
[minAmount]="0"
|
||||
[maxAmount]="unitWithQuantity()"
|
||||
name="commission"
|
||||
label="حقالعمل"
|
||||
/>
|
||||
<app-amount-percentage-input
|
||||
[percentageControl]="form.controls.payload.controls.profit_percentage"
|
||||
[amountControl]="form.controls.payload.controls.profit_amount"
|
||||
[baseAmount]="totalAmountBeforeProfit()"
|
||||
[minAmount]="0"
|
||||
[maxAmount]="totalAmountBeforeProfit()"
|
||||
name="profit"
|
||||
label="سود"
|
||||
/>
|
||||
</form>
|
||||
|
||||
<app-amount-percentage-input
|
||||
[percentageControl]="form.controls.discount_percentage"
|
||||
@@ -43,6 +45,8 @@
|
||||
<ng-template #labelSuffix>
|
||||
<p-selectButton
|
||||
[options]="discountTypeItems"
|
||||
[(ngModel)]="discountType"
|
||||
[allowEmpty]="false"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
(onChange)="changeDiscountCalculation($event)"
|
||||
@@ -58,6 +62,6 @@
|
||||
[discountAmount]="form.controls.discount_amount.value || 0"
|
||||
[taxAmount]="taxAmount()"
|
||||
/>
|
||||
<p-button (onClick)="submit()"> افزودن به لیست خرید </p-button>
|
||||
<button pButton class="sm:w-auto w-full" (click)="submit()">{{ preparedCTAText() }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { greaterThanValidator } from '@/core/validators/greater.validator';
|
||||
import { AbstractForm } from '@/shared/abstractClasses';
|
||||
import { EnumSelectComponent } from '@/shared/apiEnum/select.component';
|
||||
import { InputComponent } from '@/shared/components';
|
||||
import { AmountPercentageInputComponent } from '@/shared/components/amountPercentageInput/amount-percentage-input.component';
|
||||
import { TGoldKarat } from '@/shared/localEnum/constants/goldKarat';
|
||||
import { formatWithCurrency } from '@/utils';
|
||||
import { Component, EventEmitter, Output, signal } from '@angular/core';
|
||||
import { ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { Button } from 'primeng/button';
|
||||
import { Component, computed, EventEmitter, Output, signal } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { SelectButton, SelectButtonChangeEvent } from 'primeng/selectbutton';
|
||||
import { IGoldPayload, IPosOrderItem } from '../../../models';
|
||||
import { PosFormDialogAmountCardTemplateComponent } from '../form-dialog-amount-card-template.component';
|
||||
@@ -15,13 +16,14 @@ import { PosFormDialogAmountCardTemplateComponent } from '../form-dialog-amount-
|
||||
selector: 'pos-gold-payload-form',
|
||||
templateUrl: './form.component.html',
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
InputComponent,
|
||||
EnumSelectComponent,
|
||||
Button,
|
||||
PosFormDialogAmountCardTemplateComponent,
|
||||
SelectButton,
|
||||
AmountPercentageInputComponent,
|
||||
ButtonDirective,
|
||||
],
|
||||
})
|
||||
export class PosGoldPayloadFormComponent extends AbstractForm<
|
||||
@@ -50,10 +52,12 @@ export class PosGoldPayloadFormComponent extends AbstractForm<
|
||||
taxAmount = signal<number>(0);
|
||||
totalAmount = signal<number>(0);
|
||||
|
||||
preparedCTAText = computed(() => (this.editMode ? 'اعمال ویرایش' : 'افزودن به لیست خرید'));
|
||||
|
||||
private readonly initialForm = () => {
|
||||
const form = this.fb.group({
|
||||
unit_price: [this.initialValues?.unit_price || 0, [Validators.required]],
|
||||
quantity: [this.initialValues?.quantity || 0, [Validators.required]],
|
||||
quantity: [this.initialValues?.quantity || 0, [Validators.required, greaterThanValidator(0)]],
|
||||
discount_amount: [this.initialValues?.discount || 0],
|
||||
discount_percentage: [0, [Validators.max(100), Validators.min(0)]],
|
||||
payload: this.fb.group({
|
||||
|
||||
+1
-1
@@ -27,6 +27,6 @@
|
||||
[discountAmount]="discountAmount()"
|
||||
[taxAmount]="taxAmount()"
|
||||
/>
|
||||
<p-button (onClick)="submit()"> افزودن به لیست خرید </p-button>
|
||||
<p-button (onClick)="submit()"> {{ preparedCTAText() }} </p-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -59,6 +59,8 @@ export class PosStandardPayloadFormComponent extends AbstractForm<
|
||||
discountAmount = signal<number>(0);
|
||||
taxAmount = signal<number>(0);
|
||||
|
||||
preparedCTAText = computed(() => (this.editMode ? 'اعمال ویرایش' : 'افزودن به لیست خرید'));
|
||||
|
||||
toPriceFormat(amount: number) {
|
||||
if (!amount) {
|
||||
return '';
|
||||
|
||||
@@ -6,43 +6,69 @@
|
||||
[closable]="true"
|
||||
(onHide)="close()"
|
||||
>
|
||||
<form [formGroup]="form" (submit)="submit()">
|
||||
<app-key-value label="مبلغ قابل پرداخت" [value]="totalAmount().toString()" type="price" />
|
||||
<app-key-value label="مبلغ باقیمانده" [value]="remainedAmount().toString()" type="price" />
|
||||
<hr />
|
||||
<div class="form-group">
|
||||
<form [formGroup]="form" (submit)="submit()">
|
||||
<app-key-value label="مبلغ قابل پرداخت" [value]="totalAmount().toString()" type="price" />
|
||||
<app-key-value label="مبلغ باقیمانده" [value]="remainedAmount().toString()" type="price" />
|
||||
<hr />
|
||||
</form>
|
||||
|
||||
<app-input
|
||||
[control]="form.controls.terminal"
|
||||
name="terminal"
|
||||
label="پرداخت با پایانه"
|
||||
type="price"
|
||||
[max]="terminalMax()"
|
||||
>
|
||||
<ng-template #suffixTemp>
|
||||
<button pButton size="small" outlined type="button" class="border-s-0!" (click)="fillRemained('TERMINAL')">
|
||||
تمامی بدهی باقیمانده
|
||||
</button>
|
||||
</ng-template>
|
||||
</app-input>
|
||||
<app-input [control]="form.controls.cash" name="cash" label="نقدی" type="price" [max]="cashMax()">
|
||||
<ng-template #suffixTemp>
|
||||
<button pButton size="small" outlined type="button" class="border-s-0!" (click)="fillRemained('CASH')">
|
||||
تمامی بدهی باقیمانده
|
||||
</button>
|
||||
</ng-template>
|
||||
</app-input>
|
||||
<app-input [control]="form.controls.set_off" name="setOff" label="تهاتر" type="price" [max]="setOffMax()">
|
||||
<ng-template #suffixTemp>
|
||||
<button pButton size="small" outlined type="button" class="border-s-0!" (click)="fillRemained('SET_OFF')">
|
||||
تمامی بدهی باقیمانده
|
||||
</button>
|
||||
</ng-template>
|
||||
</app-input>
|
||||
<div class="form-group">
|
||||
<uikit-field label="تعداد مراحل پرداخت با پایانه" name="pay_by_terminal_steps">
|
||||
<p-select
|
||||
[options]="payByTerminalSteps"
|
||||
[ngModel]="selectedPayByTerminalStep()"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
[disabled]="!remainedAmount()"
|
||||
class="w-full"
|
||||
(onChange)="changePayByTerminalSteps($event.value)"
|
||||
/>
|
||||
</uikit-field>
|
||||
@for (terminalControl of terminalControls; track $index) {
|
||||
<app-input
|
||||
[control]="terminalControl"
|
||||
[name]="'terminal_' + $index"
|
||||
[label]="$index === 0 ? 'پرداخت با پایانه' : 'پرداخت با پایانه - مرحله ' + ($index + 1)"
|
||||
type="price"
|
||||
[max]="terminalsMax()[$index]"
|
||||
>
|
||||
<ng-template #suffixTemp>
|
||||
<button
|
||||
pButton
|
||||
size="small"
|
||||
type="button"
|
||||
[disabled]="!remainedAmount()"
|
||||
(click)="fillTerminalRemained($index)"
|
||||
>
|
||||
تمامی بدهی باقیمانده
|
||||
</button>
|
||||
</ng-template>
|
||||
</app-input>
|
||||
}
|
||||
</div>
|
||||
<form [formGroup]="form" (submit)="submit()">
|
||||
<app-input [control]="form.controls.cash" name="cash" label="نقدی" type="price" [max]="cashMax()">
|
||||
<ng-template #suffixTemp>
|
||||
<button pButton size="small" type="button" [disabled]="!remainedAmount()" (click)="fillRemained('CASH')">
|
||||
تمامی بدهی باقیمانده
|
||||
</button>
|
||||
</ng-template>
|
||||
</app-input>
|
||||
<app-input [control]="form.controls.set_off" name="setOff" label="تهاتر" type="price" [max]="setOffMax()">
|
||||
<ng-template #suffixTemp>
|
||||
<button pButton size="small" type="button" [disabled]="!remainedAmount()" (click)="fillRemained('SET_OFF')">
|
||||
تمامی بدهی باقیمانده
|
||||
</button>
|
||||
</ng-template>
|
||||
</app-input>
|
||||
|
||||
<app-form-footer-actions
|
||||
submitLabel="ثبت اطلاعات پرداخت و ادامه"
|
||||
[loading]="submitOrderLoading()"
|
||||
(onCancel)="close()"
|
||||
/>
|
||||
</form>
|
||||
<app-form-footer-actions
|
||||
submitLabel="ثبت اطلاعات پرداخت و ادامه"
|
||||
[loading]="submitOrderLoading()"
|
||||
(onCancel)="close()"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</shared-dialog>
|
||||
|
||||
@@ -1,31 +1,37 @@
|
||||
import { NativeBridgeService } from '@/core/services';
|
||||
import { ToastService } from '@/core/services/toast.service';
|
||||
import { AbstractFormDialog } from '@/shared/abstractClasses';
|
||||
import { InputComponent, KeyValueComponent } from '@/shared/components';
|
||||
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { catchError, throwError } from 'rxjs';
|
||||
import { IPayment, TOrderPaymentTypes } from '../../models';
|
||||
import { PosLandingStore } from '../../store/main.store';
|
||||
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
|
||||
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
|
||||
import { UikitFieldComponent } from '@/uikit';
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { Select } from 'primeng/select';
|
||||
import { IPayment, TOrderPaymentTypes } from '../../models';
|
||||
import { PosPaymentBridgeAbstract } from '../../services/payment-bridge.abstract';
|
||||
import { PosPaymentBridgeService } from '../../services/payment-bridge.service';
|
||||
import { PosLandingStore } from '../../store/main.store';
|
||||
|
||||
@Component({
|
||||
selector: 'pos-payment-form-dialog',
|
||||
templateUrl: './form-dialog.component.html',
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
InputComponent,
|
||||
FormFooterActionsComponent,
|
||||
KeyValueComponent,
|
||||
ButtonDirective,
|
||||
SharedDialogComponent,
|
||||
Select,
|
||||
UikitFieldComponent,
|
||||
],
|
||||
providers: [{ provide: PosPaymentBridgeAbstract, useExisting: PosPaymentBridgeService }],
|
||||
})
|
||||
export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment, IPayment> {
|
||||
private readonly store = inject(PosLandingStore);
|
||||
private readonly nativeBridge = inject(NativeBridgeService);
|
||||
private readonly paymentBridge = inject(PosPaymentBridgeAbstract);
|
||||
private readonly toastServices = inject(ToastService);
|
||||
|
||||
readonly totalAmount = computed(() => this.store.orderPricingInfo().totalAmount);
|
||||
@@ -33,35 +39,79 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
|
||||
readonly remainedAmount = signal(this.totalAmount());
|
||||
setOffMax = signal(this.remainedAmount());
|
||||
cashMax = signal(this.remainedAmount());
|
||||
terminalMax = signal(this.remainedAmount());
|
||||
terminalsMax = signal([this.remainedAmount()]);
|
||||
|
||||
submitOrderLoading = computed(() => this.store.submitOrderLoading());
|
||||
|
||||
payByTerminalSteps = Array.from({ length: 10 }, (_, i) => ({
|
||||
value: i + 1,
|
||||
label: `${i + 1} مرحلهای`,
|
||||
}));
|
||||
|
||||
selectedPayByTerminalStep = signal(1);
|
||||
payedInTerminal = signal<Array<number>>([]);
|
||||
|
||||
private initForm = () => {
|
||||
const form = this.fb.group({
|
||||
set_off: [this.initialValues?.set_off || 0],
|
||||
cash: [this.initialValues?.cash || 0],
|
||||
terminal: [this.initialValues?.terminal || 0],
|
||||
terminals: this.fb.array([this.fb.control(0)]),
|
||||
});
|
||||
|
||||
form.valueChanges.subscribe((value) => {
|
||||
const totalValue = (value.cash || 0) + (value.set_off || 0) + (value.terminal || 0);
|
||||
const terminalTotal =
|
||||
(value.terminals || []).reduce((acc, curr) => (acc || 0) + (curr || 0), 0) || 0;
|
||||
|
||||
const totalValue = (value.cash || 0) + (value.set_off || 0) + terminalTotal;
|
||||
const remainedAmount = this.totalAmount() - totalValue;
|
||||
|
||||
const setOffMax = remainedAmount + (value.set_off || 0);
|
||||
const cashMax = remainedAmount + (value.cash || 0);
|
||||
const terminalMax = remainedAmount + (value.terminal || 0);
|
||||
|
||||
form.controls.set_off.clearValidators();
|
||||
form.controls.set_off.addValidators([Validators.max(setOffMax)]);
|
||||
if (!setOffMax) {
|
||||
form.controls.set_off.disable({
|
||||
onlySelf: true,
|
||||
});
|
||||
} else {
|
||||
form.controls.set_off.enable({
|
||||
onlySelf: true,
|
||||
});
|
||||
}
|
||||
|
||||
form.controls.cash.clearValidators();
|
||||
form.controls.cash.addValidators([Validators.max(cashMax)]);
|
||||
form.controls.terminal.clearValidators();
|
||||
form.controls.terminal.addValidators([Validators.max(terminalMax)]);
|
||||
if (!cashMax) {
|
||||
form.controls.cash.disable({
|
||||
onlySelf: true,
|
||||
});
|
||||
} else {
|
||||
form.controls.cash.enable({
|
||||
onlySelf: true,
|
||||
});
|
||||
}
|
||||
|
||||
form.controls.terminals.controls.forEach((terminal, i) => {
|
||||
terminal.clearValidators();
|
||||
this.terminalsMax.update((max) => {
|
||||
const terminalMax = remainedAmount + (terminal.value || 0);
|
||||
terminal.addValidators([Validators.max(terminalMax)]);
|
||||
if (terminalMax) {
|
||||
terminal.enable({
|
||||
onlySelf: true,
|
||||
});
|
||||
} else {
|
||||
terminal.disable({
|
||||
onlySelf: true,
|
||||
});
|
||||
}
|
||||
return max.map((m, index) => (index === i ? terminalMax : m));
|
||||
});
|
||||
});
|
||||
|
||||
this.remainedAmount.set(remainedAmount);
|
||||
this.cashMax.set(cashMax);
|
||||
this.terminalMax.set(terminalMax);
|
||||
this.setOffMax.set(setOffMax);
|
||||
});
|
||||
|
||||
@@ -69,13 +119,55 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
|
||||
};
|
||||
|
||||
form = this.initForm();
|
||||
get terminalControls() {
|
||||
return this.form.controls.terminals.controls as FormControl<number | null>[];
|
||||
}
|
||||
|
||||
fillRemained(type: TOrderPaymentTypes) {
|
||||
changePayByTerminalSteps(steps: number) {
|
||||
const normalizedSteps = Math.max(1, Number(steps) || 1);
|
||||
this.selectedPayByTerminalStep.set(normalizedSteps);
|
||||
|
||||
const terminals = this.form.controls.terminals;
|
||||
const currentLength = terminals.length;
|
||||
|
||||
if (normalizedSteps > currentLength) {
|
||||
for (let i = currentLength; i < normalizedSteps; i += 1) {
|
||||
terminals.push(this.fb.control(0));
|
||||
const terminalMax = this.remainedAmount();
|
||||
this.form.controls.terminals.controls[i].addValidators([Validators.max(terminalMax)]);
|
||||
this.terminalsMax.update((max) => [...max, terminalMax]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalizedSteps < currentLength) {
|
||||
const removedSum = terminals.controls
|
||||
.slice(normalizedSteps)
|
||||
.reduce((acc, control) => acc + (control.value || 0), 0);
|
||||
|
||||
this.remainedAmount.update((value) => value + removedSum);
|
||||
|
||||
while (terminals.length > normalizedSteps) {
|
||||
terminals.removeAt(terminals.length - 1);
|
||||
}
|
||||
|
||||
// terminals.at(0)?.setValue(firstValue + removedSum);
|
||||
}
|
||||
}
|
||||
|
||||
fillTerminalRemained(index: number) {
|
||||
this.fillRemained('TERMINAL', index);
|
||||
}
|
||||
|
||||
fillRemained(type: TOrderPaymentTypes, _terminalIndex?: number) {
|
||||
switch (type) {
|
||||
case 'TERMINAL':
|
||||
this.form.controls.terminal.setValue(
|
||||
(this.form.controls.terminal.value || 0) + this.remainedAmount(),
|
||||
);
|
||||
const terminalIndex = _terminalIndex ?? 0;
|
||||
this.form.controls.terminals
|
||||
.at(terminalIndex)
|
||||
?.setValue(
|
||||
(this.form.controls.terminals.at(terminalIndex)?.value || 0) + this.remainedAmount(),
|
||||
);
|
||||
break;
|
||||
|
||||
case 'CASH':
|
||||
@@ -101,43 +193,50 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
|
||||
});
|
||||
}
|
||||
|
||||
const payment = this.form.value as IPayment;
|
||||
const rawPayment = this.form.getRawValue();
|
||||
const payment: IPayment = {
|
||||
cash: Number(rawPayment.cash || 0),
|
||||
set_off: Number(rawPayment.set_off || 0),
|
||||
terminals: (rawPayment.terminals || []).map((value) => Number(value || 0)),
|
||||
};
|
||||
|
||||
if (payment.terminal > 0 && this.nativeBridge.isEnabled()) {
|
||||
const payResult = this.nativeBridge.pay({
|
||||
amount: payment.terminal,
|
||||
totalAmount: this.totalAmount(),
|
||||
invoiceDate: new Date().toISOString(),
|
||||
});
|
||||
|
||||
if (!payResult.success) {
|
||||
return this.toastServices.warn({
|
||||
text: payResult.error || 'پرداخت با دستگاه انجام نشد. لطفا دوباره تلاش کنید.',
|
||||
if (payment.terminals.some((terminal) => terminal > 0) && this.paymentBridge.isEnabled()) {
|
||||
for (let terminal of payment.terminals) {
|
||||
if (terminal <= 0) continue;
|
||||
const payResult = this.paymentBridge.pay({
|
||||
amount: terminal,
|
||||
id: '0',
|
||||
});
|
||||
|
||||
if (!payResult.success) {
|
||||
return this.toastServices.warn({
|
||||
text: payResult.error || 'پرداخت با دستگاه انجام نشد. لطفا دوباره تلاش کنید.',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.store.setPayment(payment);
|
||||
// this.store.setPayment(payment);
|
||||
|
||||
this.store
|
||||
.submitOrder()
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
return throwError(() => err);
|
||||
}),
|
||||
)
|
||||
.subscribe((res) => {
|
||||
if (this.nativeBridge.isEnabled()) {
|
||||
this.nativeBridge.print({
|
||||
invoiceId: res?.id,
|
||||
code: res?.code,
|
||||
});
|
||||
}
|
||||
this.close();
|
||||
this.toastServices.success({
|
||||
text: 'فاکتور شما با موفقیت ایجاد شد',
|
||||
});
|
||||
});
|
||||
// this.store
|
||||
// .submitOrder()
|
||||
// .pipe(
|
||||
// catchError((err) => {
|
||||
// return throwError(() => err);
|
||||
// }),
|
||||
// )
|
||||
// .subscribe((res) => {
|
||||
// if (this.paymentBridge.isEnabled()) {
|
||||
// this.paymentBridge.print({
|
||||
// invoiceId: res?.id,
|
||||
// code: res?.code,
|
||||
// });
|
||||
// }
|
||||
// this.close();
|
||||
// this.toastServices.success({
|
||||
// text: 'فاکتور شما با موفقیت ایجاد شد',
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
override onSuccess(response: IPayment): void {}
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
export interface IPayment {
|
||||
terminal: number;
|
||||
terminals: number[];
|
||||
cash: number;
|
||||
set_off: number;
|
||||
}
|
||||
|
||||
export type TOrderPaymentTypes = 'TERMINAL' | 'CASH' | 'SET_OFF';
|
||||
|
||||
export interface IPaymentTerminal {
|
||||
amount: number;
|
||||
terminal_id: string;
|
||||
stan: string;
|
||||
rrn: string;
|
||||
response_code: string;
|
||||
customer_card_no: string;
|
||||
transaction_date_time: Date;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,9 @@ export class PosService {
|
||||
}
|
||||
|
||||
getGoods(searchQuery: string): Observable<IListingResponse<IGoodResponse>> {
|
||||
return this.http.get<IListingResponse<IGoodRawResponse>>(this.apiRoutes.goods());
|
||||
return this.http.get<IListingResponse<IGoodRawResponse>>(this.apiRoutes.goods(), {
|
||||
params: { search: searchQuery },
|
||||
});
|
||||
}
|
||||
|
||||
getGoodCategories(): Observable<IListingResponse<IGoodCategoryResponse>> {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { INativeBridgeResult, INativePayRequest, INativePrintRequest } from '@/core/services/native-bridge.service';
|
||||
|
||||
export abstract class PosPaymentBridgeAbstract {
|
||||
abstract isEnabled(): boolean;
|
||||
abstract canPay(): boolean;
|
||||
abstract canPrint(): boolean;
|
||||
abstract pay(request: INativePayRequest): INativeBridgeResult;
|
||||
abstract print(request: INativePrintRequest): INativeBridgeResult;
|
||||
abstract registerPaymentResultListener(handler: (payload: unknown) => void): () => void;
|
||||
abstract emitPaymentResultForTest(payload: unknown): void;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { NativeBridgeService } from '@/core/services';
|
||||
import {
|
||||
INativeBridgeResult,
|
||||
INativePayRequest,
|
||||
INativePrintRequest,
|
||||
} from '@/core/services/native-bridge.service';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { PosPaymentBridgeAbstract } from './payment-bridge.abstract';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PosPaymentBridgeService extends PosPaymentBridgeAbstract {
|
||||
private readonly nativeBridge = inject(NativeBridgeService);
|
||||
|
||||
isEnabled(): boolean {
|
||||
return this.nativeBridge.isEnabled();
|
||||
}
|
||||
|
||||
canPay(): boolean {
|
||||
return this.nativeBridge.canPay();
|
||||
}
|
||||
|
||||
canPrint(): boolean {
|
||||
return this.nativeBridge.canPrint();
|
||||
}
|
||||
|
||||
pay(request: INativePayRequest): INativeBridgeResult {
|
||||
return this.nativeBridge.pay(request);
|
||||
}
|
||||
|
||||
print(request: INativePrintRequest): INativeBridgeResult {
|
||||
return this.nativeBridge.print(request);
|
||||
}
|
||||
|
||||
registerPaymentResultListener(handler: (payload: unknown) => void): () => void {
|
||||
const w = window as unknown as {
|
||||
onPaymentResult?: (payload: unknown) => void;
|
||||
AndroidPSP?: {
|
||||
onPaymentResult?: (payload: unknown) => void;
|
||||
};
|
||||
};
|
||||
|
||||
const previousGlobal = w.onPaymentResult;
|
||||
const previousBridge = w.AndroidPSP?.onPaymentResult;
|
||||
|
||||
w.onPaymentResult = handler;
|
||||
if (w.AndroidPSP) {
|
||||
w.AndroidPSP.onPaymentResult = handler;
|
||||
}
|
||||
|
||||
return () => {
|
||||
w.onPaymentResult = previousGlobal;
|
||||
if (w.AndroidPSP) {
|
||||
w.AndroidPSP.onPaymentResult = previousBridge;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
emitPaymentResultForTest(payload: unknown): void {
|
||||
const w = window as unknown as { onPaymentResult?: (payload: unknown) => void };
|
||||
w.onPaymentResult?.(payload);
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export const INITIAL_POS_STATE: IPosLandingState = {
|
||||
payments: {
|
||||
cash: 0,
|
||||
set_off: 0,
|
||||
terminal: 0,
|
||||
terminals: [],
|
||||
},
|
||||
submitOrderLoading: false,
|
||||
};
|
||||
@@ -59,6 +59,7 @@ export class PosLandingStore {
|
||||
private state$ = signal<IPosLandingState>({ ...INITIAL_POS_STATE });
|
||||
|
||||
readonly goods = computed(() => this.state$().goods);
|
||||
|
||||
readonly getGoodsLoading = computed(() => this.state$().getGoodsLoading);
|
||||
readonly inOrderGoods = computed(() => this.state$().inOrderGoods);
|
||||
readonly goodCategories = computed(() => this.state$().goodCategories);
|
||||
@@ -73,6 +74,9 @@ export class PosLandingStore {
|
||||
}
|
||||
return this.state$().goods?.filter((s) => s.category.id === this.state$().activeGoodCategory);
|
||||
});
|
||||
readonly filteredGoods = computed(() =>
|
||||
this.activatedCategoryGoods()?.filter((good) => good.name.includes(this.state$().searchQuery)),
|
||||
);
|
||||
|
||||
readonly customer = computed(() => this.state$().customerDetails);
|
||||
readonly invoiceDate = computed(() => this.state$().invoiceDate);
|
||||
@@ -194,6 +198,8 @@ export class PosLandingStore {
|
||||
this.setState({ searchQuery });
|
||||
}
|
||||
|
||||
filterGoods() {}
|
||||
|
||||
resetInOrderGoods() {
|
||||
this.setState({ inOrderGoods: [] });
|
||||
}
|
||||
@@ -241,7 +247,7 @@ export class PosLandingStore {
|
||||
payments: {
|
||||
cash: 0,
|
||||
set_off: 0,
|
||||
terminal: 0,
|
||||
terminals: [],
|
||||
},
|
||||
orderNote: '',
|
||||
});
|
||||
|
||||
@@ -1,13 +1,47 @@
|
||||
@if (loading()) {
|
||||
<shared-page-loading />
|
||||
|
||||
} @else if (pos()) {
|
||||
<div class="flex gap-4 grow overflow-hidden h-full">
|
||||
<div class="grow h-full overflow-auto">
|
||||
<pos-goods />
|
||||
</div>
|
||||
<div class="shrink-0 h-full">
|
||||
<pos-order-section />
|
||||
<div class="w-full h-[calc(100dvh-5.5rem)] overflow-hidden">
|
||||
<div
|
||||
[class]="`w-full h-full flex max-md:flex-col gap-4 pb-4 pt-0 overflow-hidden ${inOrderGoods().length > 0 ? 'pb-18!' : ''}`"
|
||||
>
|
||||
<div class="grow h-full overflow-auto">
|
||||
<pos-goods class="block h-full" />
|
||||
</div>
|
||||
<div class="md:shrink-0 md:h-full md:block hidden p-4 overflow-auto">
|
||||
<pos-order-section />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (inOrderGoods().length > 0) {
|
||||
<button
|
||||
type="button"
|
||||
pButton
|
||||
class="sticky! bottom-0 inset-x-0 w-full right-0 z-1200 border-t border-surface-border bg-surface-0 px-4 py-3 shadow-[0_-4px_16px_rgba(0,0,0,0.08)] md:hidden rounded-b-none!"
|
||||
[style.paddingBottom]="'calc(0.75rem + env(safe-area-inset-bottom))'"
|
||||
(click)="openInvoiceBottomSheet()"
|
||||
>
|
||||
<div class="mx-auto flex w-full max-w-3xl items-center justify-between">
|
||||
<div class="flex flex-col items-start gap-1">
|
||||
<span class="text-base">مبلغ کل فاکتور</span>
|
||||
<span class="text-xl font-semibold" [appPriceMask]="priceInfo().totalAmount"></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-base font-medium">مشاهده فاکتور</span>
|
||||
<i class="pi pi-chevron-up"></i>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
|
||||
<shared-dialog
|
||||
[(visible)]="showInvoiceBottomSheet"
|
||||
position="bottom"
|
||||
[modal]="true"
|
||||
[closable]="true"
|
||||
header="فاکتور"
|
||||
>
|
||||
<pos-order-section (onAddMoreGoods)="closeInvoiceBottomSheet()" />
|
||||
</shared-dialog>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,25 +1,48 @@
|
||||
// import { CatalogRoleTagComponent } from '@/shared/catalog/roles';
|
||||
import { PosInfoStore } from '@/domains/pos/store/pos.store';
|
||||
import { SharedDialogComponent } from '@/shared/components';
|
||||
import { PageLoadingComponent } from '@/shared/components/page-loading.component';
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { PriceMaskDirective } from '@/shared/directives';
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { PosGoodsComponent } from '../components/goods.component';
|
||||
import { PosOrderSectionComponent } from '../components/order/order-section.component';
|
||||
import { PosLandingStore } from '../store/main.store';
|
||||
|
||||
@Component({
|
||||
selector: 'pos-landing',
|
||||
templateUrl: './root.component.html',
|
||||
host: { class: 'grow overflow-hidden' },
|
||||
|
||||
imports: [PosGoodsComponent, PosOrderSectionComponent, PageLoadingComponent],
|
||||
imports: [
|
||||
PosGoodsComponent,
|
||||
PosOrderSectionComponent,
|
||||
PageLoadingComponent,
|
||||
PriceMaskDirective,
|
||||
SharedDialogComponent,
|
||||
ButtonDirective,
|
||||
],
|
||||
})
|
||||
export class PosLandingComponent {
|
||||
private readonly store = inject(PosInfoStore);
|
||||
private readonly infoStore = inject(PosInfoStore);
|
||||
private readonly landingStore = inject(PosLandingStore);
|
||||
|
||||
readonly loading = computed(() => this.store.loading());
|
||||
readonly pos = computed(() => this.store.entity());
|
||||
readonly error = computed(() => this.store.error());
|
||||
readonly loading = computed(() => this.infoStore.loading());
|
||||
readonly pos = computed(() => this.infoStore.entity());
|
||||
readonly error = computed(() => this.infoStore.error());
|
||||
readonly inOrderGoods = computed(() => this.landingStore.inOrderGoods());
|
||||
readonly priceInfo = computed(() => this.landingStore.orderPricingInfo());
|
||||
readonly showInvoiceBottomSheet = signal(false);
|
||||
|
||||
getData() {
|
||||
this.store.getData().subscribe();
|
||||
this.infoStore.getData().subscribe();
|
||||
}
|
||||
|
||||
openInvoiceBottomSheet() {
|
||||
this.showInvoiceBottomSheet.set(true);
|
||||
}
|
||||
|
||||
closeInvoiceBottomSheet() {
|
||||
this.showInvoiceBottomSheet.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<div class="layout-wrapper" [ngClass]="containerClass">
|
||||
<app-topbar [showMenu]="showMenu()">
|
||||
<ng-container [ngTemplateOutlet]="topBarMoreAction"></ng-container>
|
||||
</app-topbar>
|
||||
<app-topbar
|
||||
[showMenu]="showMenu()"
|
||||
[startTemplate]="topBarStartAction"
|
||||
[centerTemplate]="topBarCenterAction"
|
||||
[endTemplate]="topBarEndAction || topBarMoreAction"
|
||||
/>
|
||||
@if (fullLoading) {
|
||||
<div class="flex justify-center align-items-center grow items-center">
|
||||
<p-progressSpinner></p-progressSpinner>
|
||||
@@ -10,12 +13,12 @@
|
||||
@if (showMenu()) {
|
||||
<app-sidebar></app-sidebar>
|
||||
}
|
||||
<div [class]="`layout-main-container ${!showMenu() ? 'hideMenu' : ''} grow`">
|
||||
<div [class]="`layout-main-container ${!showMenu() ? 'hideMenu' : ''} grow ${isFullPage ? 'isFullPage' : ''}`">
|
||||
<div class="layout-main flex flex-col gap-4">
|
||||
@if (showBreadcrumb) {
|
||||
<app-breadcrumb class="rounded-md overflow-hidden shrink-0" />
|
||||
}
|
||||
<div [class]="`rounded-md flex flex-col grow ${isFixedContentSize ? 'overflow-auto' : 'overflow-hidden'}`">
|
||||
<div [class]="`rounded-md flex flex-col grow`">
|
||||
<!-- style="container-type: size" -->
|
||||
<div class="h-full">
|
||||
@if (content) {
|
||||
|
||||
@@ -47,6 +47,9 @@ export class AppLayout {
|
||||
private authService = inject(AuthService);
|
||||
|
||||
topBarMoreAction?: Maybe<TemplateRef<any>> = null;
|
||||
topBarStartAction?: Maybe<TemplateRef<any>> = null;
|
||||
topBarCenterAction?: Maybe<TemplateRef<any>> = null;
|
||||
topBarEndAction?: Maybe<TemplateRef<any>> = null;
|
||||
|
||||
constructor(
|
||||
public layoutService: LayoutService,
|
||||
@@ -137,6 +140,9 @@ export class AppLayout {
|
||||
get isFixedContentSize(): boolean {
|
||||
return this.layoutService.isFixedContentSize() || false;
|
||||
}
|
||||
get isFullPage(): boolean {
|
||||
return this.layoutService.isFullPage() || false;
|
||||
}
|
||||
|
||||
showMenu = computed(() => {
|
||||
return this.layoutService.menuItems().length > 0;
|
||||
@@ -144,6 +150,9 @@ export class AppLayout {
|
||||
|
||||
ngOnInit() {
|
||||
this.layoutService.headerSlot$.subscribe((tpl) => (this.topBarMoreAction = tpl));
|
||||
this.layoutService.topbarStartSlot$.subscribe((tpl) => (this.topBarStartAction = tpl));
|
||||
this.layoutService.topbarCenterSlot$.subscribe((tpl) => (this.topBarCenterAction = tpl));
|
||||
this.layoutService.topbarEndSlot$.subscribe((tpl) => (this.topBarEndAction = tpl));
|
||||
|
||||
if (window.location.pathname === '/') {
|
||||
if (!this.authService.currentAccount()) {
|
||||
|
||||
@@ -1,43 +1,44 @@
|
||||
<div class="layout-topbar shrink-0">
|
||||
<div class="layout-topbar-logo-container">
|
||||
@if (showMenu) {
|
||||
<button class="layout-menu-button layout-topbar-action" (click)="layoutService.onMenuToggle()">
|
||||
<i class="pi pi-bars"></i>
|
||||
</button>
|
||||
}
|
||||
<div class="layout-topbar-logo">
|
||||
<img [src]="logo" width="32" />
|
||||
<span class="text-xl">{{ panelTitle() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-topbar-actions items-center">
|
||||
<ng-content></ng-content>
|
||||
<div class="layout-config-menu">
|
||||
<button type="button" class="layout-topbar-action" (click)="toggleDarkMode()">
|
||||
<i
|
||||
[ngClass]="{ 'pi ': true, 'pi-moon': layoutService.isDarkTheme(), 'pi-sun': !layoutService.isDarkTheme() }"
|
||||
></i>
|
||||
</button>
|
||||
<!-- <div class="relative">
|
||||
<button
|
||||
class="layout-topbar-action layout-topbar-action-highlight"
|
||||
pStyleClass="@next"
|
||||
enterFromClass="hidden"
|
||||
enterActiveClass="animate-scalein"
|
||||
leaveToClass="hidden"
|
||||
leaveActiveClass="animate-fadeout"
|
||||
[hideOnOutsideClick]="true"
|
||||
>
|
||||
<i class="pi pi-palette"></i>
|
||||
<p-toolbar>
|
||||
<ng-template #start>
|
||||
@if (startTemplate) {
|
||||
<ng-container [ngTemplateOutlet]="startTemplate"></ng-container>
|
||||
} @else {
|
||||
@if (showMenu) {
|
||||
<button class="layout-menu-button layout-topbar-action" (click)="layoutService.onMenuToggle()">
|
||||
<i class="pi pi-bars"></i>
|
||||
</button>
|
||||
<app-configurator></app-configurator>
|
||||
</div> -->
|
||||
</div>
|
||||
}
|
||||
<div class="flex items-center gap-2">
|
||||
<img [src]="logo" width="32" />
|
||||
<span class="text-xl">{{ panelTitle() }}</span>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template #center>
|
||||
@if (centerTemplate) {
|
||||
<ng-container [ngTemplateOutlet]="centerTemplate"></ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template #end>
|
||||
@if (endTemplate) {
|
||||
<ng-container [ngTemplateOutlet]="endTemplate"></ng-container>
|
||||
} @else {
|
||||
<div class="flex items-center gap-2">
|
||||
<ng-content></ng-content>
|
||||
<p-button
|
||||
type="button"
|
||||
[icon]="`pi ${layoutService.isDarkTheme() ? 'pi-moon' : 'pi-sun'}`"
|
||||
text
|
||||
(click)="toggleDarkMode()"
|
||||
>
|
||||
<i [ngClass]="{}"></i>
|
||||
</p-button>
|
||||
|
||||
<div class="">
|
||||
<p-menu #menu [model]="profileMenuItems" [popup]="true" />
|
||||
<p-button (click)="menu.toggle($event)" icon="pi pi-user" text severity="contrast" size="large" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="">
|
||||
<p-menu #menu [model]="profileMenuItems" [popup]="true" />
|
||||
<p-button (click)="menu.toggle($event)" icon="pi pi-user" text severity="contrast" size="large" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</p-toolbar>
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import { AuthService } from '@/core/services/auth.service';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, computed, inject, Input } from '@angular/core';
|
||||
import { Component, computed, inject, Input, TemplateRef } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { MenuItem } from 'primeng/api';
|
||||
import { Button } from 'primeng/button';
|
||||
import { Button, ButtonIcon } from 'primeng/button';
|
||||
import { MenuModule } from 'primeng/menu';
|
||||
import { StyleClassModule } from 'primeng/styleclass';
|
||||
import { Toolbar } from 'primeng/toolbar';
|
||||
import images from 'src/assets/images';
|
||||
import { LayoutService } from '../service/layout.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-topbar',
|
||||
standalone: true,
|
||||
imports: [RouterModule, CommonModule, StyleClassModule, MenuModule, Button],
|
||||
imports: [RouterModule, CommonModule, StyleClassModule, MenuModule, Button, Toolbar, ButtonIcon],
|
||||
templateUrl: './app.topbar.component.html',
|
||||
})
|
||||
export class AppTopbar {
|
||||
@Input() showMenu: boolean = false;
|
||||
@Input() startTemplate?: TemplateRef<any> | null = null;
|
||||
@Input() centerTemplate?: TemplateRef<any> | null = null;
|
||||
@Input() endTemplate?: TemplateRef<any> | null = null;
|
||||
|
||||
constructor(public layoutService: LayoutService) {}
|
||||
private authService: AuthService = inject(AuthService);
|
||||
|
||||
@@ -21,6 +21,7 @@ interface LayoutState {
|
||||
staticMenuMobileActive?: boolean;
|
||||
menuHoverActive?: boolean;
|
||||
isFixedContentSize?: boolean;
|
||||
isFullPage?: boolean;
|
||||
}
|
||||
|
||||
interface MenuChangeEvent {
|
||||
@@ -47,6 +48,7 @@ export class LayoutService {
|
||||
staticMenuMobileActive: false,
|
||||
menuHoverActive: false,
|
||||
isFixedContentSize: true,
|
||||
isFullPage: false,
|
||||
};
|
||||
|
||||
layoutConfig = signal<layoutConfig>(this._config);
|
||||
@@ -90,6 +92,7 @@ export class LayoutService {
|
||||
isOverlay = computed(() => this.layoutConfig().menuMode === 'overlay');
|
||||
|
||||
isFixedContentSize = computed(() => this.layoutState().isFixedContentSize);
|
||||
isFullPage = computed(() => this.layoutState().isFullPage);
|
||||
|
||||
transitionComplete = signal<boolean>(false);
|
||||
|
||||
@@ -144,6 +147,13 @@ export class LayoutService {
|
||||
}));
|
||||
}
|
||||
|
||||
changeIsFullPage(status: boolean) {
|
||||
this.layoutState.update((prev) => ({
|
||||
...prev,
|
||||
isFullPage: status,
|
||||
}));
|
||||
}
|
||||
|
||||
toggleDarkMode(config?: layoutConfig): void {
|
||||
const _config = config || this.layoutConfig();
|
||||
localStorage.setItem('isDarkTheme', JSON.stringify(_config.darkTheme));
|
||||
@@ -221,8 +231,28 @@ export class LayoutService {
|
||||
|
||||
private headerSlot = new BehaviorSubject<TemplateRef<any> | null>(null);
|
||||
headerSlot$ = this.headerSlot.asObservable();
|
||||
private topbarStartSlot = new BehaviorSubject<TemplateRef<any> | null>(null);
|
||||
topbarStartSlot$ = this.topbarStartSlot.asObservable();
|
||||
private topbarCenterSlot = new BehaviorSubject<TemplateRef<any> | null>(null);
|
||||
topbarCenterSlot$ = this.topbarCenterSlot.asObservable();
|
||||
private topbarEndSlot = new BehaviorSubject<TemplateRef<any> | null>(null);
|
||||
topbarEndSlot$ = this.topbarEndSlot.asObservable();
|
||||
|
||||
setHeaderSlot(tpl: TemplateRef<any> | null) {
|
||||
// Backward-compatible alias for topbar end slot.
|
||||
this.setTopbarEndSlot(tpl);
|
||||
this.headerSlot.next(tpl);
|
||||
}
|
||||
|
||||
setTopbarStartSlot(tpl: TemplateRef<any> | null) {
|
||||
this.topbarStartSlot.next(tpl);
|
||||
}
|
||||
|
||||
setTopbarCenterSlot(tpl: TemplateRef<any> | null) {
|
||||
this.topbarCenterSlot.next(tpl);
|
||||
}
|
||||
|
||||
setTopbarEndSlot(tpl: TemplateRef<any> | null) {
|
||||
this.topbarEndSlot.next(tpl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IAuthResponse, Maybe, TRoles } from '@/core';
|
||||
import { ToastService } from '@/core/services/toast.service';
|
||||
import { PosPaymentBridgeAbstract } from '@/domains/pos/modules/landing/services/payment-bridge.abstract';
|
||||
import { PosPaymentBridgeService } from '@/domains/pos/modules/landing/services/payment-bridge.service';
|
||||
import { Component, EventEmitter, OnDestroy, OnInit, inject, Input, Output, signal } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
@@ -12,6 +14,7 @@ type TSteps = 'login' | 'otp' | 'modifyLoginInfo' | 'signup';
|
||||
selector: 'app-auth',
|
||||
templateUrl: './auth.component.html',
|
||||
imports: [LoginComponent, ButtonDirective],
|
||||
providers: [{ provide: PosPaymentBridgeAbstract, useExisting: PosPaymentBridgeService }],
|
||||
})
|
||||
export class AuthComponent implements OnInit, OnDestroy {
|
||||
@Input() redirectUrl!: string;
|
||||
@@ -23,6 +26,7 @@ export class AuthComponent implements OnInit, OnDestroy {
|
||||
|
||||
private readonly toastService = inject(ToastService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly paymentBridge = inject(PosPaymentBridgeAbstract);
|
||||
|
||||
readonly logo = images.logo;
|
||||
readonly authVector = images.login;
|
||||
@@ -92,33 +96,15 @@ export class AuthComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
|
||||
ngOnInit() {
|
||||
this.listenAndroidPaymentResultCallback();
|
||||
this.restorePaymentCallback = this.paymentBridge.registerPaymentResultListener(
|
||||
this.injectedPaymentResultHandler,
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.restorePaymentCallback?.();
|
||||
}
|
||||
|
||||
private listenAndroidPaymentResultCallback() {
|
||||
const w = window as any;
|
||||
const previousGlobal = w.onPaymentResult;
|
||||
// Android app can call this function directly via evaluateJavascript.
|
||||
w.onPaymentResult = this.injectedPaymentResultHandler;
|
||||
|
||||
const bridge = w.AndroidPSP;
|
||||
const previousBridge = bridge?.onPaymentResult;
|
||||
if (bridge) {
|
||||
bridge.onPaymentResult = this.injectedPaymentResultHandler;
|
||||
}
|
||||
|
||||
this.restorePaymentCallback = () => {
|
||||
w.onPaymentResult = previousGlobal;
|
||||
if (bridge) {
|
||||
bridge.onPaymentResult = previousBridge;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private handlePaymentResult(result: unknown) {
|
||||
const value = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
||||
this.text.set(value);
|
||||
@@ -145,6 +131,6 @@ export class AuthComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
mockPaymentResult() {
|
||||
this.injectedPaymentResultHandler(`test-result-${Date.now()}`);
|
||||
this.paymentBridge.emitPaymentResultForTest(`test-result-${Date.now()}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ export abstract class AbstractForm<
|
||||
submitLoading = signal(false);
|
||||
|
||||
submit() {
|
||||
console.log('first');
|
||||
|
||||
this.form.markAllAsTouched();
|
||||
|
||||
if (this.form.valid) {
|
||||
|
||||
+2
-2
@@ -166,9 +166,9 @@ export class AmountPercentageInputComponent {
|
||||
|
||||
private prepareLabel(isPercentageType: boolean) {
|
||||
if (isPercentageType) {
|
||||
return `${this.label} (${formatWithCurrency(this.amountControl.value || 0)})`;
|
||||
return `${this.label} (معادل ${formatWithCurrency(this.amountControl.value || 0)})`;
|
||||
}
|
||||
return `${this.label} (${this.percentageControl.value || 0} درصد)`;
|
||||
return `${this.label} (معادل ${this.percentageControl.value || 0} درصد)`;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<ng-template #dialogContent>
|
||||
<ng-content></ng-content>
|
||||
</ng-template>
|
||||
|
||||
@if (mobileDrawer && isMobile) {
|
||||
<p-drawer
|
||||
[header]="header"
|
||||
[visible]="visible"
|
||||
position="bottom"
|
||||
[modal]="modal"
|
||||
[closable]="closable"
|
||||
[style]="{ 'max-height': mobileDrawerHeight, height: 'auto' }"
|
||||
styleClass="shared-mobile-drawer"
|
||||
(visibleChange)="onVisibilityChange($event)"
|
||||
(onHide)="onHide.emit($event)"
|
||||
>
|
||||
<ng-container [ngTemplateOutlet]="dialogContent"></ng-container>
|
||||
</p-drawer>
|
||||
} @else {
|
||||
<p-dialog
|
||||
[header]="header"
|
||||
[visible]="visible"
|
||||
[modal]="modal"
|
||||
[style]="style"
|
||||
[closable]="closable"
|
||||
[draggable]="draggable"
|
||||
[breakpoints]="breakpoints"
|
||||
(visibleChange)="onVisibilityChange($event)"
|
||||
(onHide)="onHide.emit($event)"
|
||||
>
|
||||
<ng-container [ngTemplateOutlet]="dialogContent"></ng-container>
|
||||
</p-dialog>
|
||||
}
|
||||
@@ -1,25 +1,25 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { NgTemplateOutlet } from '@angular/common';
|
||||
import { Component, EventEmitter, HostListener, Input, OnInit, Output } from '@angular/core';
|
||||
import { Dialog } from 'primeng/dialog';
|
||||
import { Drawer } from 'primeng/drawer';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-dialog',
|
||||
template: `
|
||||
<p-dialog
|
||||
[header]="header"
|
||||
[(visible)]="visible"
|
||||
[modal]="modal"
|
||||
[style]="style"
|
||||
[closable]="closable"
|
||||
[draggable]="draggable"
|
||||
[breakpoints]="breakpoints"
|
||||
(onHide)="onHide.emit($event)"
|
||||
>
|
||||
<ng-content />
|
||||
</p-dialog>
|
||||
`,
|
||||
imports: [Dialog],
|
||||
templateUrl: './dialog.component.html',
|
||||
imports: [Dialog, Drawer, NgTemplateOutlet],
|
||||
styles: [
|
||||
`
|
||||
:host ::ng-deep .shared-mobile-drawer.p-drawer-bottom {
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
}
|
||||
:host ::ng-deep .shared-mobile-drawer .p-drawer-content {
|
||||
padding-bottom: calc(1rem + env(safe-area-inset-bottom));
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
export class SharedDialogComponent {
|
||||
export class SharedDialogComponent implements OnInit {
|
||||
@Input() header = '';
|
||||
@Input() visible = false;
|
||||
@Output() visibleChange = new EventEmitter<boolean>();
|
||||
@@ -29,6 +29,29 @@ export class SharedDialogComponent {
|
||||
@Input() draggable = false;
|
||||
@Input() style: Record<string, string> | undefined;
|
||||
@Input() breakpoints: Record<string, string> | undefined;
|
||||
@Input() mobileBreakpoint = 768;
|
||||
@Input() mobileDrawer = true;
|
||||
@Input() mobileDrawerHeight = '90svh';
|
||||
|
||||
@Output() onHide = new EventEmitter<any>();
|
||||
|
||||
isMobile = false;
|
||||
|
||||
ngOnInit() {
|
||||
this.updateViewportMode();
|
||||
}
|
||||
|
||||
@HostListener('window:resize')
|
||||
onResize() {
|
||||
this.updateViewportMode();
|
||||
}
|
||||
|
||||
onVisibilityChange(nextValue: boolean) {
|
||||
this.visible = nextValue;
|
||||
this.visibleChange.emit(nextValue);
|
||||
}
|
||||
|
||||
private updateViewportMode() {
|
||||
this.isMobile = typeof window !== 'undefined' && window.innerWidth <= this.mobileBreakpoint;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="flex justify-end gap-2">
|
||||
<p-button [label]="cancelLabel" severity="secondary" (click)="cancel()" />
|
||||
<p-button [label]="submitLabel" type="submit" [loading]="loading" (onClick)="submit()" />
|
||||
<p-button [label]="submitLabel" type="submit" [disabled]="disabled" [loading]="loading" (onClick)="submit()" />
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ export class FormFooterActionsComponent {
|
||||
@Input() submitLabel = 'تایید';
|
||||
@Input() cancelLabel = 'لغو';
|
||||
@Input() loading = false;
|
||||
@Input() disabled = false;
|
||||
@Output() onSubmit = new EventEmitter<void>();
|
||||
@Output() onCancel = new EventEmitter<void>();
|
||||
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
<uikit-field [label]="label" [name]="name" [control]="control" [showLabel]="!!label" [showErrors]="showErrors">
|
||||
@if (labelSuffix) {
|
||||
<ng-template #labelView>
|
||||
<div class="flex gap-1 items-center">
|
||||
<uikit-label [name]="name">
|
||||
@if (labelTextView) {
|
||||
<ng-container [ngTemplateOutlet]="labelTextView"></ng-container>
|
||||
} @else {
|
||||
{{ label }}
|
||||
}
|
||||
</uikit-label>
|
||||
<ng-container [ngTemplateOutlet]="labelSuffix"></ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
@if (type === "switch") {
|
||||
<p-toggleSwitch [formControl]="control" />
|
||||
<p-toggleSwitch [attr.disabled]="disabled" [formControl]="control" />
|
||||
} @else {
|
||||
<p-inputgroup>
|
||||
<!-- [minlength]="minlength || null" -->
|
||||
@@ -13,6 +27,7 @@
|
||||
[attr.placeholder]="preparedPlaceholder"
|
||||
[attr.type]="htmlType"
|
||||
[attr.autocomplete]="autocomplete"
|
||||
[attr.disabled]="disabled"
|
||||
[class]="` w-full ${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`"
|
||||
[required]="isRequired"
|
||||
[invalid]="control.invalid && (control.touched || control.dirty)"
|
||||
@@ -30,6 +45,7 @@
|
||||
[attr.inputmode]="inputMode"
|
||||
[attr.maxlength]="preparedMaxLength || null"
|
||||
[attr.type]="htmlType"
|
||||
[attr.disabled]="disabled"
|
||||
[attr.autocomplete]="autocomplete"
|
||||
[classList]="
|
||||
[
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Maybe } from '@/core';
|
||||
import { ToastService } from '@/core/services/toast.service';
|
||||
import { UikitLabelComponent } from '@/uikit';
|
||||
import { UikitFieldComponent } from '@/uikit/uikit-field.component';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
@@ -35,6 +36,7 @@ import { ToggleSwitch } from 'primeng/toggleswitch';
|
||||
InputNumberModule,
|
||||
KeyFilterModule,
|
||||
InputMaskModule,
|
||||
UikitLabelComponent,
|
||||
],
|
||||
templateUrl: './input.component.html',
|
||||
})
|
||||
@@ -69,6 +71,8 @@ export class InputComponent {
|
||||
@Output() blur = new EventEmitter<void>();
|
||||
|
||||
@ContentChild('suffixTemp', { static: true }) suffixTemp!: TemplateRef<any> | null;
|
||||
@ContentChild('labelSuffix', { static: false }) labelSuffix!: TemplateRef<any> | null;
|
||||
@ContentChild('labelTextView', { static: false }) labelTextView!: TemplateRef<any> | null;
|
||||
|
||||
private readonly toastService = inject(ToastService);
|
||||
|
||||
@@ -202,14 +206,14 @@ export class InputComponent {
|
||||
const minValidator = (this.min || this.min === 0) && parseFloat(value) < this.min!;
|
||||
const maxValidator = (this.max || this.max === 0) && parseFloat(value) > this.max;
|
||||
if (minValidator || maxValidator) {
|
||||
if (minValidator) {
|
||||
this.toastService.warn({
|
||||
text: `مقدار ${this.label} نمیتواند بیشتر از ${this.min} باشد.`,
|
||||
});
|
||||
}
|
||||
if (maxValidator) {
|
||||
this.toastService.warn({
|
||||
text: `مقدار ${this.label} نمیتواند کمتر از ${this.max} باشد.`,
|
||||
text: `مقدار ${this.label} نمیتواند بیشتر از ${this.max} باشد.`,
|
||||
});
|
||||
}
|
||||
if (minValidator) {
|
||||
this.toastService.warn({
|
||||
text: `مقدار ${this.label} نمیتواند کمتر از ${this.min} باشد.`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -233,8 +237,12 @@ export class InputComponent {
|
||||
}
|
||||
|
||||
if (replacedTargetValue !== null) {
|
||||
console.log(replacedTargetValue);
|
||||
|
||||
// @ts-ignore
|
||||
this.$event.value = replacedTargetValue;
|
||||
$event.value = replacedTargetValue;
|
||||
// @ts-ignore
|
||||
$event.target.value = replacedTargetValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<input pInputText type="text" placeholder="جستجو..." (input)="onSearchInput($event.target.value)" />
|
||||
<input pInputText type="text" placeholder="جستجو..." class="w-full" (input)="onSearchInput($event.target.value)" />
|
||||
|
||||
@@ -19,6 +19,7 @@ export class UikitFieldComponent {
|
||||
@Input() pSize: PSize = 'normal';
|
||||
@Input() className?: string;
|
||||
@Input() invalid?: boolean = false;
|
||||
@Input() disabled?: boolean = false;
|
||||
// accept a FormControl or AbstractControl to display errors for
|
||||
@Input() control?: AbstractControl | null;
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
:root {
|
||||
--p-drawer-header-padding: 0.5rem 0.875rem !important;
|
||||
}
|
||||
@@ -26,6 +26,6 @@ html {
|
||||
|
||||
@media (max-width: 640px) {
|
||||
html {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,13 @@
|
||||
transform: translateX(0) !important;
|
||||
padding-inline-start: 2rem !important;
|
||||
}
|
||||
&.isFullPage {
|
||||
padding-inline-start: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-main {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
// overflow: auto;
|
||||
}
|
||||
|
||||
@@ -1,110 +1,122 @@
|
||||
@media screen and (min-width: 1960px) {
|
||||
.layout-main,
|
||||
.landing-wrapper {
|
||||
width: 1504px;
|
||||
margin-right: auto !important;
|
||||
margin-left: auto !important;
|
||||
}
|
||||
.layout-main,
|
||||
.landing-wrapper {
|
||||
width: 1504px;
|
||||
margin-right: auto !important;
|
||||
margin-left: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.layout-wrapper {
|
||||
&.layout-overlay {
|
||||
.layout-main-container {
|
||||
margin-inline-start: 0;
|
||||
padding-inline-start: 2rem;
|
||||
}
|
||||
|
||||
.layout-sidebar {
|
||||
transform: translateX(100%);
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-inline-end: 1px solid var(--surface-border);
|
||||
transition:
|
||||
transform 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99),
|
||||
right 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99);
|
||||
box-shadow:
|
||||
0px 3px 5px rgba(0, 0, 0, 0.02),
|
||||
0px 0px 2px rgba(0, 0, 0, 0.05),
|
||||
0px 1px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
&.layout-overlay-active {
|
||||
.layout-sidebar {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.layout-static {
|
||||
.layout-main-container {
|
||||
margin-inline-start: 22rem;
|
||||
}
|
||||
|
||||
&.layout-static-inactive {
|
||||
.layout-sidebar {
|
||||
transform: translateX(100%);
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.layout-main-container {
|
||||
margin-inline-start: 0;
|
||||
padding-inline-start: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-mask {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 620px) {
|
||||
.layout-main-container {
|
||||
padding: 1rem 0.5rem 1rem 1rem;
|
||||
&.hideMenu {
|
||||
padding-inline-start: 1rem !important;
|
||||
}
|
||||
&.isFullPage {
|
||||
padding-inline-start: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.layout-wrapper {
|
||||
&.layout-overlay {
|
||||
.layout-main-container {
|
||||
margin-inline-start: 0;
|
||||
padding-inline-start: 2rem;
|
||||
}
|
||||
|
||||
.layout-sidebar {
|
||||
transform: translateX(100%);
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-inline-end: 1px solid var(--surface-border);
|
||||
transition:
|
||||
transform 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99),
|
||||
right 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99);
|
||||
box-shadow:
|
||||
0px 3px 5px rgba(0, 0, 0, 0.02),
|
||||
0px 0px 2px rgba(0, 0, 0, 0.05),
|
||||
0px 1px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
&.layout-overlay-active {
|
||||
.layout-sidebar {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.layout-static {
|
||||
.layout-main-container {
|
||||
margin-inline-start: 22rem;
|
||||
}
|
||||
|
||||
&.layout-static-inactive {
|
||||
.layout-sidebar {
|
||||
transform: translateX(100%);
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.layout-main-container {
|
||||
margin-inline-start: 0;
|
||||
padding-inline-start: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-mask {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.blocked-scroll {
|
||||
overflow: hidden;
|
||||
.blocked-scroll {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout-wrapper {
|
||||
.layout-main-container {
|
||||
margin-inline-start: 0;
|
||||
padding-inline-start: 2rem;
|
||||
}
|
||||
|
||||
.layout-wrapper {
|
||||
.layout-main-container {
|
||||
margin-inline-start: 0;
|
||||
padding-inline-start: 2rem;
|
||||
}
|
||||
|
||||
.layout-sidebar {
|
||||
transform: translateX(100%);
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
transition:
|
||||
transform 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99),
|
||||
left 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99);
|
||||
}
|
||||
|
||||
.layout-mask {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 998;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--maskbg);
|
||||
}
|
||||
|
||||
&.layout-mobile-active {
|
||||
.layout-sidebar {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.layout-mask {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.layout-sidebar {
|
||||
transform: translateX(100%);
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
transition:
|
||||
transform 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99),
|
||||
left 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99);
|
||||
}
|
||||
|
||||
.layout-mask {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 998;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--maskbg);
|
||||
}
|
||||
|
||||
&.layout-mobile-active {
|
||||
.layout-sidebar {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.layout-mask {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,3 +66,7 @@ input.p-inputtext {
|
||||
.p-avatar-image {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.p-drawer {
|
||||
border-radius: 1rem 1rem 0 0 !important;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
@use "primeicons/primeicons.css";
|
||||
@use "./demo/demo.scss";
|
||||
@use "./rtlSupport.scss";
|
||||
@use "./customize.scss";
|
||||
|
||||
form {
|
||||
form,
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
@@ -19,13 +19,13 @@ export const appRoutes: Routes = [
|
||||
CONSUMER_ROUTES,
|
||||
PROVIDER_ROUTES,
|
||||
PARTNER_ROUTES,
|
||||
POS_ROUTES,
|
||||
{ path: 'ng', component: Dashboard },
|
||||
{ path: 'uikit', loadChildren: () => import('@/pages/uikit/uikit.routes') },
|
||||
{ path: 'documentation', component: Documentation },
|
||||
{ path: 'pages', loadChildren: () => import('@/pages/pages.routes') },
|
||||
],
|
||||
},
|
||||
POS_ROUTES,
|
||||
{
|
||||
path: 'auth',
|
||||
component: AuthComponent,
|
||||
|
||||
Reference in New Issue
Block a user