feat: enhance order submission flow and UI improvements

- Updated categories component to improve loading state and category display.
- Modified order section to handle payment submission with event payload.
- Refactored payment form dialog to emit order response on successful payment.
- Adjusted order response model to enforce required fields.
- Improved root component layout and added success dialog for order submission.
- Enhanced sale invoice card component to use new TSP API methods.
- Updated API routes for sale invoices to reflect TSP changes.
- Improved user interface for business activities and partners sections.
- Added card shadow styling for better visual hierarchy.
- Introduced new order submitted dialog component for post-order actions.
- Cleaned up console logs and improved code readability across components.
This commit is contained in:
2026-05-08 18:08:57 +03:30
parent ce40bd8c75
commit a138034c06
40 changed files with 375 additions and 212 deletions
@@ -70,19 +70,19 @@ export class NativeBridgeService {
pay(request: INativePayRequest): any { pay(request: INativePayRequest): any {
if (request.amount <= 10_000) { if (request.amount <= 10_000) {
const errorMessage = 'برای مقادیر زیر ۱۰۰ هزار ریال، پرداخت ممکن نیست.'; const errorMessage = 'برای مقادیر زیر ۱۰۰ هزار ریال، پرداخت ممکن نیست.';
this.toastService.error({ text: errorMessage, life: 3000 }); this.toastService.warn({ text: errorMessage, life: 3000 });
return { return {
success: false, success: false,
error: errorMessage, error: errorMessage,
}; };
} }
this.toastService.info({ text: 'در حال پردازش پرداخت...', life: 3000 }); this.toastService.info({ text: 'در حال پردازش پرداخت...' });
try { try {
// @ts-ignore // @ts-ignore
window.NativeBridge.pay(request.amount, request.id || ''); window.NativeBridge.pay(request.amount, request.id || '');
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
this.toastService.info({ text: (error as Error).message, life: 30000 }); this.toastService.info({ text: (error as Error).message });
return { success: false, error: (error as Error).message }; return { success: false, error: (error as Error).message };
} }
// const fn = window.NativeBridge.pay(123, 'test'); // const fn = window.NativeBridge.pay(123, 'test');
@@ -99,7 +99,7 @@ export class NativeBridgeService {
// // return { success: true, data: parsed ?? raw }; // // return { success: true, data: parsed ?? raw };
// } catch (error) { // } catch (error) {
// this.toastService.info({ text: (error as Error).message, life: 30000 }); // this.toastService.info({ text: (error as Error).message, });
// return { success: false, error: (error as Error).message }; // return { success: false, error: (error as Error).message };
// } // }
} }
@@ -109,12 +109,16 @@ export class NativeBridgeService {
} }
private invokePrint(payload: INativePrintRequest): INativeBridgeResult { private invokePrint(payload: INativePrintRequest): INativeBridgeResult {
if (!this.isEnabled() || !this.canPrint()) {
this.toastService.warn({ text: 'متاسفانه ارتباط با چاپگر برقرار نیست.' });
return { success: false, error: 'متاسفانه ارتباط با چاپگر برقرار نیست.' };
}
try { try {
// @ts-ignore // @ts-ignore
window.NativeBridge.print(JSON.stringify([payload])); window.NativeBridge.print(JSON.stringify([payload]));
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
this.toastService.info({ text: (error as Error).message, life: 30000 }); this.toastService.warn({ text: 'متاسفانه ارتباط با چاپگر برقرار نیست.' });
return { success: false, error: (error as Error).message }; return { success: false, error: (error as Error).message };
} }
} }
@@ -8,7 +8,7 @@ export const CONSUMER_MENU_ITEMS = [
routerLink: ['/consumer'], routerLink: ['/consumer'],
}, },
{ {
label: 'فعالیت‌های اقتصادی', label: 'فعالیت اقتصادی',
icon: 'pi pi-fw pi-home', icon: 'pi pi-fw pi-home',
routerLink: ['/consumer/business_activities'], routerLink: ['/consumer/business_activities'],
}, },
@@ -1,5 +1,5 @@
<app-page-data-list <app-page-data-list
pageTitle="لیست فعالیت‌های اقتصادی" pageTitle="لیست فعالیت اقتصادی"
[columns]="columns" [columns]="columns"
[items]="items()" [items]="items()"
[loading]="loading()" [loading]="loading()"
@@ -1,5 +1,5 @@
<app-page-data-list <app-page-data-list
[pageTitle]="'لیست مشتری‌ها'" [pageTitle]="'لیست مشتری'"
[columns]="columns" [columns]="columns"
emptyPlaceholderTitle="مشتری یافت نشد" emptyPlaceholderTitle="مشتری یافت نشد"
[items]="items()" [items]="items()"
@@ -1,5 +1,5 @@
<app-page-data-list <app-page-data-list
pageTitle="مدیریت فعالیت‌های اقتصادی" pageTitle="مدیریت فعالیت اقتصادی"
[addNewCtaLabel]="'افزودن فعالیت اقتصادی'" [addNewCtaLabel]="'افزودن فعالیت اقتصادی'"
[columns]="columns" [columns]="columns"
[showAdd]="true" [showAdd]="true"
@@ -14,9 +14,8 @@ export const partnerConsumerBusinessActivitiesNamedRoutes: NamedRoutes<TBusiness
), ),
// @ts-ignore // @ts-ignore
meta: { meta: {
title: 'فعالیت‌های اقتصادی', title: 'فعالیت اقتصادی',
pagePath: (consumerId: string) => pagePath: (consumerId: string) => `/partner/consumers/${consumerId}/business_activities`,
`/partner/consumers/${consumerId}/business_activities`,
}, },
}, },
businessActivity: { businessActivity: {
@@ -45,8 +44,7 @@ export const PARTNER_CONSUMER_BUSINESS_ACTIVITIES_ROUTES: Routes = [
children: [ children: [
{ {
path: '', path: '',
loadComponent: loadComponent: partnerConsumerBusinessActivitiesNamedRoutes.businessActivity.loadComponent,
partnerConsumerBusinessActivitiesNamedRoutes.businessActivity.loadComponent,
}, },
...PARTNER_CONSUMER_COMPLEXES_ROUTES, ...PARTNER_CONSUMER_COMPLEXES_ROUTES,
], ],
@@ -1,5 +1,5 @@
<app-page-data-list <app-page-data-list
pageTitle="لیست مشتری‌ها" pageTitle="لیست مشتری"
[addNewCtaLabel]="'افزودن مشتری'" [addNewCtaLabel]="'افزودن مشتری'"
[columns]="columns" [columns]="columns"
[showAdd]="true" [showAdd]="true"
@@ -1,5 +1,5 @@
<app-page-data-list <app-page-data-list
[pageTitle]="'لیست مشتری‌ها'" [pageTitle]="'لیست مشتری'"
[columns]="columns" [columns]="columns"
emptyPlaceholderTitle="مشتری یافت نشد" emptyPlaceholderTitle="مشتری یافت نشد"
[items]="items()" [items]="items()"
@@ -1,7 +1,5 @@
<ng-template #topbarStart> <ng-template #topbarStart>
<p-button (click)="toggleMenu()" icon="pi pi-bars" outlined size="large" /> <p-button (click)="toggleMenu()" icon="pi pi-bars" outlined size="large" />
<p-button (click)="refresh()" icon="pi pi-bars" outlined size="large" />
<p-button (click)="doPay()" icon="pi pi-bars" outlined size="large" />
</ng-template> </ng-template>
<ng-template #topbarCenter> <ng-template #topbarCenter>
@@ -26,11 +24,34 @@
</div> </div>
</ng-template> </ng-template>
@if (loading()) { <div
<shared-page-loading /> class="h-full relative"
} @else { (touchstart)="onTouchStart($event)"
<!-- <div class="w-svw h-svh bg-surface-ground flex flex-col gap-4 p-4"> --> (touchmove)="onTouchMove($event)"
<!-- <div class="w-full flex items-center gap-4 shrink-0 justify-between"> (touchend)="onTouchEnd()"
>
@if (pullDistance() >= 0) {
<div
class="w-full flex justify-center items-center py-2 min-h-10 text-xs text-surface-500 overflow-hidden fixed top-0 inset-x-0 z-1000 translate-y--20px"
[ngStyle]="{ height: pullDistance() + 'px' }"
>
<div
class="inline-flex p-3 rounded-full opacity-0 transition-opacity shadow-xl bg-surface-card"
[ngStyle]="{ opacity: pullDistance() / 100 }"
>
<i
class="pi pi-refresh text-[1.25rem] text-primary"
[ngStyle]="{ fontSize: `${1.25 + pullDistance() * 0.007}rem`, rotate: pullDistance() * 2 + 'deg' }"
></i>
</div>
<!-- [ngStyle]="{ transform: 'scale(1.' + pullDistance() * 10 + ')' }" -->
</div>
}
@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="flex gap-2 items-center"> <div class="flex gap-2 items-center">
@if (posInfo()) { @if (posInfo()) {
<div class="w-10 h-10"> <div class="w-10 h-10">
@@ -53,27 +74,28 @@
</div> </div>
</div> </div>
</div> --> </div> -->
@if (error()) { @if (error()) {
@switch (error()?.status) { @switch (error()?.status) {
@case (412) { @case (412) {
<pos-choose-cards class="h-full" (onChoose)="onChoosePos()" /> <pos-choose-cards class="h-full" (onChoose)="onChoosePos()" />
} }
@default { @default {
<div class="h-full w-full flex items-center justify-center"> <div class="h-full w-full flex items-center justify-center">
<p-card class="border border-surface-border"> <p-card class="border border-surface-border">
<div class="flex flex-col gap-2 items-center justify-between shrink-0"> <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> <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> <span class="text-xl font-semibold mb-2 text-error block"> عدم دسترسی </span>
<p class="inline-block text-base">متاسفانه امکان دسترسی برای کاربر شما فراهم نیست</p> <p class="inline-block text-base">متاسفانه امکان دسترسی برای کاربر شما فراهم نیست</p>
<button pButton outlined severity="danger" class="mt-5 w-32" (click)="logout()">خروج</button> <button pButton outlined severity="danger" class="mt-5 w-32" (click)="logout()">خروج</button>
</div> </div>
</p-card> </p-card>
</div> </div>
}
} }
} @else {
<pos-main-menu-sidebar [(visible)]="mainMenuVisible" />
<router-outlet></router-outlet>
} }
} @else { <!-- </div> -->
<pos-main-menu-sidebar [(visible)]="mainMenuVisible" />
<router-outlet></router-outlet>
} }
<!-- </div> --> </div>
}
+34 -20
View File
@@ -3,6 +3,7 @@ import { NativeBridgeService } from '@/core/services';
import { ToastService } from '@/core/services/toast.service'; import { ToastService } from '@/core/services/toast.service';
import { LayoutService } from '@/layout/service/layout.service'; import { LayoutService } from '@/layout/service/layout.service';
import { PageLoadingComponent } from '@/shared/components/page-loading.component'; import { PageLoadingComponent } from '@/shared/components/page-loading.component';
import { CommonModule } from '@angular/common';
import { import {
AfterViewInit, AfterViewInit,
Component, Component,
@@ -34,6 +35,7 @@ import { PosMainMenuSidebarComponent } from './mainMenuSidebar/main-menu-sidebar
Button, Button,
PosMainMenuSidebarComponent, PosMainMenuSidebarComponent,
Menu, Menu,
CommonModule,
], ],
}) })
export class PosLayoutComponent implements AfterViewInit { export class PosLayoutComponent implements AfterViewInit {
@@ -84,6 +86,10 @@ export class PosLayoutComponent implements AfterViewInit {
placeholderLogo = images.placeholders.logo; placeholderLogo = images.placeholders.logo;
now = new Date(); now = new Date();
private touchStartY = 0;
private pulling = false;
pullDistance = signal(0);
readonly pullThreshold = 80;
getData() { getData() {
this.posProfileStore.getData().subscribe({ this.posProfileStore.getData().subscribe({
@@ -101,24 +107,6 @@ export class PosLayoutComponent implements AfterViewInit {
this.getData(); this.getData();
} }
doPay() {
// this.nativeBridgeService.pay({
// amount: 10_000,
// id: '1',
// });
this.nativeBridgeService.print({
title: 'salam',
items: [
{
label: 'مجموع قیمت',
value: '10_000',
},
],
});
// @ts-ignore
// window.NativeBridge.pay(12312, 'test');
}
ngOnInit() { ngOnInit() {
this.layoutService.changeIsFullPage(true); this.layoutService.changeIsFullPage(true);
this.getData(); this.getData();
@@ -137,7 +125,33 @@ export class PosLayoutComponent implements AfterViewInit {
this.layoutService.setTopbarEndSlot(null); this.layoutService.setTopbarEndSlot(null);
} }
refresh() { onTouchStart(event: TouchEvent) {
window.location.reload(); if (window.scrollY > 0 || event.touches.length !== 1) return;
this.touchStartY = event.touches[0].clientY;
this.pulling = true;
}
onTouchMove(event: TouchEvent) {
if (!this.pulling) return;
const delta = event.touches[0].clientY - this.touchStartY;
if (delta <= 0) {
this.pullDistance.set(0);
return;
}
const damped = Math.min(delta * 0.5, 140);
this.pullDistance.set(damped);
event.preventDefault();
}
onTouchEnd() {
if (!this.pulling) return;
const shouldRefresh = this.pullDistance() >= this.pullThreshold;
this.pulling = false;
this.pullDistance.set(0);
if (shouldRefresh) {
this.layoutService.changeIsFullPage(true);
this.getData();
}
} }
} }
@@ -1,29 +1,31 @@
<div class="flex gap-3 overflow-auto"> <div class="overflow-x-auto">
@if (loading()) { <div class="w-full overflow-visible flex gap-3">
@for (i of [1, 2, 3, 4, 5]; track i) { @if (loading()) {
<p-skeleton width="6rem" height="2.5rem" /> @for (i of [1, 2, 3, 4, 5]; track i) {
} <p-skeleton width="6rem" height="2.5rem" />
} @else { }
@for (category of categories(); track category.id) { } @else {
<div @for (category of categories(); track category.id) {
[class]="
'h-10 rounded-lg px-3 flex items-center gap-3 border border-surface-border cursor-pointer bg-surface-100 hover:bg-surface-200 transition-all' +
(selectedCategory() === category.id ? ' bg-surface-card' : '')
"
(click)="changeSelectedCategory(category)"
>
<span class="text-sm text-muted-color font-bold"> {{ category.name }}</span>
<div <div
[class]=" [class]="
'flex items-center justify-center py-0.5 px-2 rounded-sm bg-surface-300 text-xs text-muted-color' + 'h-10 rounded-lg px-3 flex items-center gap-3 border border-surface-border cursor-pointer bg-surface-100 hover:bg-surface-200 transition-all shrink-0' +
(selectedCategory() === category.id ? ' bg-amber-400! text-white' : '') (selectedCategory() === category.id ? ' bg-surface-card' : '')
" "
(click)="changeSelectedCategory(category)"
> >
<span> <span class="text-sm text-muted-color font-bold"> {{ category.name }}</span>
{{ category.goods_count }} <div
</span> [class]="
'flex items-center justify-center py-0.5 px-2 rounded-sm bg-surface-300 text-xs text-muted-color' +
(selectedCategory() === category.id ? ' bg-amber-400! text-white' : '')
"
>
<span>
{{ category.goods_count }}
</span>
</div>
</div> </div>
</div> }
} }
} </div>
</div> </div>
@@ -63,4 +63,4 @@
<pos-order-customer-dialog [(visible)]="isVisibleCustomerForm" (onSubmit)="submitCustomer()" /> <pos-order-customer-dialog [(visible)]="isVisibleCustomerForm" (onSubmit)="submitCustomer()" />
<!-- <pos-pre-invoice-dialog [(visible)]="isVisiblePreInvoiceForm" /> --> <!-- <pos-pre-invoice-dialog [(visible)]="isVisiblePreInvoiceForm" /> -->
<pos-payment-form-dialog [(visible)]="isVisiblePaymentForm" (onSubmit)="submitPayment()" /> <pos-payment-form-dialog [(visible)]="isVisiblePaymentForm" (onSubmit)="submitPayment($event)" />
@@ -6,6 +6,7 @@ import { FormsModule } from '@angular/forms';
import { ButtonDirective } from 'primeng/button'; import { ButtonDirective } from 'primeng/button';
import { Card } from 'primeng/card'; import { Card } from 'primeng/card';
import images from 'src/assets/images'; import images from 'src/assets/images';
import { IPosOrderResponse } from '../../models';
import { PosLandingStore } from '../../store/main.store'; import { PosLandingStore } from '../../store/main.store';
import { PosOrderCustomerDialogComponent } from '../customers/customer-dialog.component'; import { PosOrderCustomerDialogComponent } from '../customers/customer-dialog.component';
import { GoldPayloadOrderCardComponent } from '../payloads/gold/order-card.component'; import { GoldPayloadOrderCardComponent } from '../payloads/gold/order-card.component';
@@ -31,7 +32,7 @@ import { POSOrderPriceInfoCardComponent } from './price-info-card.component';
export class PosOrderSectionComponent { export class PosOrderSectionComponent {
private readonly store = inject(PosLandingStore); private readonly store = inject(PosLandingStore);
@Output() onAddMoreGoods = new EventEmitter<void>(); @Output() onAddMoreGoods = new EventEmitter<void>();
@Output() onSuccess = new EventEmitter<void>(); @Output() onSuccess = new EventEmitter<IPosOrderResponse>();
placeholderImage = images.placeholders.default; placeholderImage = images.placeholders.default;
@@ -84,9 +85,7 @@ export class PosOrderSectionComponent {
} }
submitCustomer() {} submitCustomer() {}
submitPayment() { submitPayment(invoice: IPosOrderResponse) {
console.log('submitPayment'); this.onSuccess.emit(invoice);
this.onSuccess.emit();
} }
} }
@@ -0,0 +1,18 @@
<shared-dialog
[(visible)]="visible"
header="ثبت موفقیت‌آمیز"
[modal]="true"
[style]="{ width: '420px' }"
[closable]="true"
(onHide)="close()"
>
<div class="text-center pt-10 pb-14 flex flex-col items-center justify-center gap-4">
<i class="" class="pi pi-check-circle text-6xl! text-green-700"></i>
<p class="text-lg font-bold">فاکتور شما با موفقیت ایجاد شد.</p>
</div>
<div class="flex gap-2 justify-center">
<button pButton type="button" outlined (click)="printInvoice()">چاپ</button>
<button pButton type="button" outlined (click)="sendToTsp()">ارسال به سامانه مودیان</button>
<button pButton type="button" outlined (click)="openOrderDetails()">جزئیات</button>
</div>
</shared-dialog>
@@ -0,0 +1,56 @@
import { NativeBridgeService } from '@/core/services';
import { ToastService } from '@/core/services/toast.service';
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
import { SharedDialogComponent } from '@/shared/components';
import { Component, inject, Input, signal } from '@angular/core';
import { ButtonDirective } from 'primeng/button';
import { finalize } from 'rxjs';
import { PosSaleInvoicesService } from '../../../saleInvoices/services/main.service';
import { IPosOrderResponse } from '../../models';
@Component({
selector: 'pos-order-submitted-dialog',
templateUrl: 'order-submitted-dialog.component.html',
imports: [SharedDialogComponent, ButtonDirective],
})
export class PosOrderSubmittedDialogComponent extends AbstractDialog {
@Input({ required: true }) invoice!: IPosOrderResponse;
private readonly nativeBridgeService = inject(NativeBridgeService);
private readonly service = inject(PosSaleInvoicesService);
private readonly toastService = inject(ToastService);
sendingLoading = signal(false);
sended = signal(false);
openOrderDetails() {
this.close();
}
sendInvoice() {
this.sendingLoading.set(true);
this.service
.sendToTsp(this.invoice.id)
.pipe(finalize(() => this.sendingLoading.set(false)))
.subscribe(() => {
this.toastService.success({ text: 'ارسال فاکتور با موفقیت انجام شد.' });
this.sended.set(true);
});
}
printInvoice() {
const printResult = this.nativeBridgeService.print({
title: 'رسید پرداخت',
items: [
{ label: 'مبلغ کل', value: String(this.invoice.code) },
// { label: 'نقدی', value: String(this.invoice.payment.cash || 0) },
// { label: 'تهاتر', value: String(this.invoice.payment.set_off || 0) },
// {
// label: 'پایانه',
// value: String((this.invoice.payment.terminals || []).reduce((acc, item) => acc + item, 0)),
// },
],
});
}
sendToTsp() {}
}
@@ -6,12 +6,12 @@ import { InputComponent, KeyValueComponent } from '@/shared/components';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component'; import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component'; import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { UikitFieldComponent } from '@/uikit'; import { UikitFieldComponent } from '@/uikit';
import { Component, OnDestroy, OnInit, computed, inject, signal } from '@angular/core'; import { Component, computed, inject, signal } from '@angular/core';
import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonDirective } from 'primeng/button'; import { ButtonDirective } from 'primeng/button';
import { Select } from 'primeng/select'; import { Select } from 'primeng/select';
import { catchError, throwError } from 'rxjs'; import { catchError, throwError } from 'rxjs';
import { IPayment, TOrderPaymentTypes } from '../../models'; import { IPayment, IPosOrderResponse, TOrderPaymentTypes } from '../../models';
import { PosPaymentBridgeAbstract } from '../../services/payment-bridge.abstract'; import { PosPaymentBridgeAbstract } from '../../services/payment-bridge.abstract';
import { PosPaymentBridgeService } from '../../services/payment-bridge.service'; import { PosPaymentBridgeService } from '../../services/payment-bridge.service';
import { PosLandingStore } from '../../store/main.store'; import { PosLandingStore } from '../../store/main.store';
@@ -32,10 +32,7 @@ import { PosLandingStore } from '../../store/main.store';
], ],
providers: [{ provide: PosPaymentBridgeAbstract, useExisting: PosPaymentBridgeService }], providers: [{ provide: PosPaymentBridgeAbstract, useExisting: PosPaymentBridgeService }],
}) })
export class PosPaymentFormDialogComponent export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment, IPosOrderResponse> {
extends AbstractFormDialog<IPayment, IPayment>
implements OnInit, OnDestroy
{
private readonly store = inject(PosLandingStore); private readonly store = inject(PosLandingStore);
private readonly paymentBridge = inject(PosPaymentBridgeAbstract); private readonly paymentBridge = inject(PosPaymentBridgeAbstract);
private readonly nativeBridgeService = inject(NativeBridgeService); private readonly nativeBridgeService = inject(NativeBridgeService);
@@ -72,8 +69,8 @@ export class PosPaymentFormDialogComponent
private initForm = () => { private initForm = () => {
const form = this.fb.group({ const form = this.fb.group({
set_off: [this.initialValues?.set_off || 0], set_off: [0],
cash: [this.initialValues?.cash || 0], cash: [0],
terminals: this.fb.array([this.fb.control(0)]), terminals: this.fb.array([this.fb.control(0)]),
}); });
@@ -237,10 +234,7 @@ export class PosPaymentFormDialogComponent
) )
.subscribe((res) => { .subscribe((res) => {
this.close(); this.close();
this.onSubmit.emit(); this.onSubmit.emit(res);
this.toastServices.success({
text: 'فاکتور شما با موفقیت ایجاد شد',
});
}); });
} }
} }
@@ -289,7 +283,7 @@ export class PosPaymentFormDialogComponent
// } // }
} }
override onSuccess(response: IPayment): void {} override onSuccess(response: IPosOrderResponse): void {}
private extractAmountFromPaymentResult(payload: unknown): number | null { private extractAmountFromPaymentResult(payload: unknown): number | null {
if (!payload || typeof payload !== 'object') return null; if (!payload || typeof payload !== 'object') return null;
+2 -2
View File
@@ -15,7 +15,7 @@ export interface IPosOrderRequest {
} }
export interface IPosOrderRawResponse { export interface IPosOrderRawResponse {
id?: string; id: string;
code?: string; code: string;
} }
export interface IPosOrderResponse extends IPosOrderRawResponse {} export interface IPosOrderResponse extends IPosOrderRawResponse {}
@@ -1,47 +1,48 @@
@if (loading()) { @if (loading()) {
<shared-page-loading /> <shared-page-loading />
} @else if (pos()) { } @else if (pos()) {
<div class="w-full h-[calc(100dvh-5.5rem)] overflow-hidden"> <div class="w-full min-h-[calc(100dvh-5.5rem)] md:overflow-hidden">
<div <div [class]="`w-full h-full flex max-md:flex-col gap-4 pb-4 pt-0`">
[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 min-h-[calc(100dvh-5.5rem)]">
>
<div class="grow h-full overflow-auto">
<pos-goods class="block h-full" /> <pos-goods class="block h-full" />
</div> </div>
<div class="md:shrink-0 md:h-full md:block hidden p-4 overflow-auto"> <div class="md:shrink-0 h-full md:block hidden p-4 overflow-auto">
<pos-order-section /> <pos-order-section />
</div> </div>
</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()" (onSuccess)="closeInvoiceBottomSheet()" />
</shared-dialog>
</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()" (onSuccess)="onSuccessSubmit($event)" />
</shared-dialog>
@if (responseInvoice()) {
<pos-order-submitted-dialog [(visible)]="isVisibleSuccessDialog" [invoice]="responseInvoice()!" />
}
} }
@@ -1,4 +1,5 @@
// import { CatalogRoleTagComponent } from '@/shared/catalog/roles'; // import { CatalogRoleTagComponent } from '@/shared/catalog/roles';
import { Maybe } from '@/core';
import { PosInfoStore } from '@/domains/pos/store/pos.store'; import { PosInfoStore } from '@/domains/pos/store/pos.store';
import { SharedDialogComponent } from '@/shared/components'; import { SharedDialogComponent } from '@/shared/components';
import { PageLoadingComponent } from '@/shared/components/page-loading.component'; import { PageLoadingComponent } from '@/shared/components/page-loading.component';
@@ -7,6 +8,8 @@ import { Component, computed, inject, signal } from '@angular/core';
import { ButtonDirective } from 'primeng/button'; import { ButtonDirective } from 'primeng/button';
import { PosGoodsComponent } from '../components/goods.component'; import { PosGoodsComponent } from '../components/goods.component';
import { PosOrderSectionComponent } from '../components/order/order-section.component'; import { PosOrderSectionComponent } from '../components/order/order-section.component';
import { PosOrderSubmittedDialogComponent } from '../components/order/order-submitted-dialog.component';
import { IPosOrderResponse } from '../models';
import { PosLandingStore } from '../store/main.store'; import { PosLandingStore } from '../store/main.store';
@Component({ @Component({
@@ -21,12 +24,16 @@ import { PosLandingStore } from '../store/main.store';
PriceMaskDirective, PriceMaskDirective,
SharedDialogComponent, SharedDialogComponent,
ButtonDirective, ButtonDirective,
PosOrderSubmittedDialogComponent,
], ],
}) })
export class PosLandingComponent { export class PosLandingComponent {
private readonly infoStore = inject(PosInfoStore); private readonly infoStore = inject(PosInfoStore);
private readonly landingStore = inject(PosLandingStore); private readonly landingStore = inject(PosLandingStore);
isVisibleSuccessDialog = signal(false);
responseInvoice = signal<Maybe<IPosOrderResponse>>(null);
readonly loading = computed(() => this.infoStore.loading()); readonly loading = computed(() => this.infoStore.loading());
readonly pos = computed(() => this.infoStore.entity()); readonly pos = computed(() => this.infoStore.entity());
readonly error = computed(() => this.infoStore.error()); readonly error = computed(() => this.infoStore.error());
@@ -45,4 +52,14 @@ export class PosLandingComponent {
closeInvoiceBottomSheet() { closeInvoiceBottomSheet() {
this.showInvoiceBottomSheet.set(false); this.showInvoiceBottomSheet.set(false);
} }
onSuccessSubmit(invoice: IPosOrderResponse) {
this.closeInvoiceBottomSheet();
this.responseInvoice.set(invoice);
this.isVisibleSuccessDialog.set(true);
}
closeSuccessDialog() {
this.isVisibleSuccessDialog.set(false);
}
} }
@@ -52,7 +52,6 @@ export class SaleInvoiceCardComponent {
if (customer) { if (customer) {
const { legal, individual } = customer; const { legal, individual } = customer;
let text = 'نوع اول - '; let text = 'نوع اول - ';
console.log(customer);
if (legal) { if (legal) {
text += `حقوقی (${legal.company_name})`; text += `حقوقی (${legal.company_name})`;
} }
@@ -67,7 +66,7 @@ export class SaleInvoiceCardComponent {
sendInvoice() { sendInvoice() {
this.sendingLoading.set(true); this.sendingLoading.set(true);
this.service this.service
.sendFiscal(this.saleInvoice.id) .sendToTsp(this.saleInvoice.id)
.pipe(finalize(() => this.sendingLoading.set(false))) .pipe(finalize(() => this.sendingLoading.set(false)))
.subscribe(() => { .subscribe(() => {
this.toastService.success({ text: 'ارسال فاکتور با موفقیت انجام شد.' }); this.toastService.success({ text: 'ارسال فاکتور با موفقیت انجام شد.' });
@@ -88,7 +87,7 @@ export class SaleInvoiceCardComponent {
resendInvoice() { resendInvoice() {
this.resendingLoading.set(true); this.resendingLoading.set(true);
this.service this.service
.retryFiscal(this.saleInvoice.id) .retrySendToTsp(this.saleInvoice.id)
.pipe(finalize(() => this.resendingLoading.set(false))) .pipe(finalize(() => this.resendingLoading.set(false)))
.subscribe(() => { .subscribe(() => {
this.toastService.success({ text: 'ارسال مجدد فاکتور با موفقیت انجام شد.' }); this.toastService.success({ text: 'ارسال مجدد فاکتور با موفقیت انجام شد.' });
@@ -3,12 +3,12 @@ const baseUrl = () => `/api/v1/pos/sales_invoices`;
export const POS_SALE_INVOICES_API_ROUTES = { export const POS_SALE_INVOICES_API_ROUTES = {
list: () => baseUrl(), list: () => baseUrl(),
single: (invoiceId: string) => `${baseUrl()}/${invoiceId}`, single: (invoiceId: string) => `${baseUrl()}/${invoiceId}`,
fiscal: { tsp: {
send: (invoiceId: string) => `${baseUrl()}/${invoiceId}/send`, send: (invoiceId: string) => `${baseUrl()}/${invoiceId}/send`,
retry: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/retry`,
status: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/status`,
getInquiry: (invoiceId: string) => `${baseUrl()}/${invoiceId}/inquiry`, getInquiry: (invoiceId: string) => `${baseUrl()}/${invoiceId}/inquiry`,
revoke: (invoiceId: string) => `${baseUrl()}/${invoiceId}/revoke`, revoke: (invoiceId: string) => `${baseUrl()}/${invoiceId}/revoke`,
retry: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/retry`,
status: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/status`,
attempts: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/attempts`, attempts: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/attempts`,
}, },
}; };
@@ -32,43 +32,41 @@ export class PosSaleInvoicesService {
return this.http.get<ISaleInvoiceFullRawResponse>(this.apiRoutes.single(invoiceId)); return this.http.get<ISaleInvoiceFullRawResponse>(this.apiRoutes.single(invoiceId));
} }
sendFiscal(invoiceId: string): Observable<IPosSaleInvoiceFiscalActionResponse> { sendToTsp(invoiceId: string): Observable<IPosSaleInvoiceFiscalActionResponse> {
return this.http.post<IPosSaleInvoiceFiscalActionResponse>( return this.http.post<IPosSaleInvoiceFiscalActionResponse>(
this.apiRoutes.fiscal.send(invoiceId), this.apiRoutes.tsp.send(invoiceId),
{}, {},
); );
} }
retryFiscal(invoiceId: string): Observable<IPosSaleInvoiceFiscalActionResponse> { retrySendToTsp(invoiceId: string): Observable<IPosSaleInvoiceFiscalActionResponse> {
return this.http.post<IPosSaleInvoiceFiscalActionResponse>( return this.http.post<IPosSaleInvoiceFiscalActionResponse>(
this.apiRoutes.fiscal.retry(invoiceId), this.apiRoutes.tsp.retry(invoiceId),
{}, {},
); );
} }
getFiscalStatus(invoiceId: string): Observable<IPosSaleInvoiceFiscalStatusResponse> { getFiscalStatus(invoiceId: string): Observable<IPosSaleInvoiceFiscalStatusResponse> {
return this.http.get<IPosSaleInvoiceFiscalStatusResponse>( return this.http.get<IPosSaleInvoiceFiscalStatusResponse>(this.apiRoutes.tsp.status(invoiceId));
this.apiRoutes.fiscal.status(invoiceId),
);
} }
getInquiry(invoiceId: string): Observable<IPosSaleInvoiceFiscalStatusResponse> { getInquiry(invoiceId: string): Observable<IPosSaleInvoiceFiscalStatusResponse> {
return this.http.get<IPosSaleInvoiceFiscalStatusResponse>( return this.http.get<IPosSaleInvoiceFiscalStatusResponse>(
this.apiRoutes.fiscal.getInquiry(invoiceId), this.apiRoutes.tsp.getInquiry(invoiceId),
{}, {},
); );
} }
revoke(invoiceId: string): Observable<IPosSaleInvoiceFiscalStatusResponse> { revoke(invoiceId: string): Observable<IPosSaleInvoiceFiscalStatusResponse> {
return this.http.post<IPosSaleInvoiceFiscalStatusResponse>( return this.http.post<IPosSaleInvoiceFiscalStatusResponse>(
this.apiRoutes.fiscal.revoke(invoiceId), this.apiRoutes.tsp.revoke(invoiceId),
{}, {},
); );
} }
getFiscalAttempts(invoiceId: string): Observable<IPosSaleInvoiceFiscalAttemptsResponse> { getFiscalAttempts(invoiceId: string): Observable<IPosSaleInvoiceFiscalAttemptsResponse> {
return this.http.get<IPosSaleInvoiceFiscalAttemptsResponse>( return this.http.get<IPosSaleInvoiceFiscalAttemptsResponse>(
this.apiRoutes.fiscal.attempts(invoiceId), this.apiRoutes.tsp.attempts(invoiceId),
); );
} }
} }
@@ -1,5 +1,5 @@
<app-page-data-list <app-page-data-list
pageTitle="لیست فعالیت‌های اقتصادی" pageTitle="لیست فعالیت اقتصادی"
[columns]="columns" [columns]="columns"
emptyPlaceholderTitle="فعالیت اقتصادی‌ای یافت نشد." emptyPlaceholderTitle="فعالیت اقتصادی‌ای یافت نشد."
[items]="items()" [items]="items()"
@@ -14,7 +14,7 @@ export const superAdminConsumerBusinessActivitiesNamedRoutes: NamedRoutes<TBusin
), ),
// @ts-ignore // @ts-ignore
meta: { meta: {
title: 'فعالیت‌های اقتصادی', title: 'فعالیت اقتصادی',
pagePath: (consumerId: string) => pagePath: (consumerId: string) =>
`/super_admin/consumers/${consumerId}/business_activities`, `/super_admin/consumers/${consumerId}/business_activities`,
}, },
@@ -21,7 +21,7 @@ export class ConsumersComponent extends AbstractList<IAdminConsumerResponse> {
this.columns = [ this.columns = [
// { field: 'id', header: 'شناسه', type: 'id' }, // { field: 'id', header: 'شناسه', type: 'id' },
{ field: 'name', header: 'نام' }, { field: 'name', header: 'نام' },
{ field: 'business_counts', header: 'تعداد فعالیت‌های اقتصادی' }, { field: 'business_counts', header: 'تعداد فعالیت اقتصادی' },
{ {
field: 'partner', field: 'partner',
header: 'ایجاد شده توسط', header: 'ایجاد شده توسط',
@@ -1,15 +1,15 @@
<app-page-data-list <app-page-data-list
[pageTitle]="'مدیریت دسته‌بندی‌های کالا'" [pageTitle]="'مدیریت دسته‌بندی‌ کالا'"
[addNewCtaLabel]="'افزودن دسته‌بندی'" [addNewCtaLabel]="'افزودن دسته‌بندی'"
[columns]="columns" [columns]="columns"
emptyPlaceholderTitle="دسته‌بندی یافت نشد" emptyPlaceholderTitle="دسته‌بندی یافت نشد"
emptyPlaceholderDescription="برای افزودن دسته‌بندی، روی دکمهٔ بالا کلیک کنید." emptyPlaceholderDescription="برای افزودن دسته‌بندی، روی دکمهٔ بالا کلیک کنید."
[items]="items()" [items]="items()"
[loading]="loading()" [loading]="loading()"
[showDetails]="true" [fullHeight]="fullHeight"
[showAdd]="true" [showAdd]="true"
[showEdit]="true" [showEdit]="true"
[fullHeight]="fullHeight" [allItemsPageRoute]="allItemsPageRoute()"
(onEdit)="onEditClick($event)" (onEdit)="onEditClick($event)"
(onAdd)="openAddForm()" (onAdd)="openAddForm()"
(onRefresh)="refresh()" (onRefresh)="refresh()"
@@ -4,7 +4,8 @@ import {
IColumn, IColumn,
PageDataListComponent, PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component'; } from '@/shared/components/pageDataList/page-data-list.component';
import { Component, inject, Input } from '@angular/core'; import { Component, computed, inject, Input } from '@angular/core';
import { guildGoodCategoriesNamedRoutes } from '../../constants/routes/goodCategories';
import { IGoodCategoriesResponse } from '../../models'; import { IGoodCategoriesResponse } from '../../models';
import { GuildGoodCategoriesService } from '../../services/goodCategories.service'; import { GuildGoodCategoriesService } from '../../services/goodCategories.service';
import { GuildGoodCategoryFormComponent } from './form.component'; import { GuildGoodCategoryFormComponent } from './form.component';
@@ -29,6 +30,10 @@ export class GuildGoodCategoriesListComponent extends AbstractList<IGoodCategori
]; ];
service = inject(GuildGoodCategoriesService); service = inject(GuildGoodCategoriesService);
allItemsPageRoute = computed(() =>
guildGoodCategoriesNamedRoutes.goodCategories.meta.pagePath!(this.guildId),
);
override setColumns(): void { override setColumns(): void {
this.columns = this.header; this.columns = this.header;
} }
@@ -5,12 +5,13 @@ export type TGuildGoodCategoriesRouteNames = 'goodCategories';
export const guildGoodCategoriesNamedRoutes: NamedRoutes<TGuildGoodCategoriesRouteNames> = { export const guildGoodCategoriesNamedRoutes: NamedRoutes<TGuildGoodCategoriesRouteNames> = {
goodCategories: { goodCategories: {
path: 'good_categories', path: 'good-categories',
loadComponent: () => loadComponent: () =>
import('../../views/categories/list.component').then((m) => m.GuildGoodCategoriesComponent), import('../../views/categories/list.component').then((m) => m.GuildGoodCategoriesComponent),
// @ts-ignore // @ts-ignore
meta: { meta: {
title: 'دسته‌بندی‌های کالا', title: 'دسته‌بندی‌های کالا',
pagePath: (guildId: string) => `/super_admin/guilds/${guildId}/good-categories`,
}, },
}, },
// goodCategory: { // goodCategory: {
@@ -39,26 +39,28 @@ export class GuildFormComponent extends AbstractFormDialog<IPartnerRequest, IPar
private service = inject(PartnersService); private service = inject(PartnersService);
private initForm() { private initForm() {
const form = this.fb.group({ const formConfig = {
name: fieldControl.name(this.initialValues?.name || ''), name: fieldControl.name(this.initialValues?.name || ''),
code: fieldControl.code(this.initialValues?.code || ''), code: fieldControl.code(this.initialValues?.code || ''),
username: fieldControl.username(), username: [''],
password: fieldControl.password(), password: [''],
confirmPassword: fieldControl.confirmPassword(), confirmPassword: [''],
}); };
if (!this.editMode) { if (!this.editMode) {
form.controls.username.setValidators([Validators.required, usernameValidator()]); formConfig.username = fieldControl.username();
form.controls.username.updateValueAndValidity({ emitEvent: false }); formConfig.password = fieldControl.password();
formConfig.confirmPassword = fieldControl.confirmPassword();
}
const form = this.fb.group(formConfig);
if (!this.editMode) {
// @ts-ignore
form.controls['username'].setValidators([Validators.required, usernameValidator()]);
// @ts-ignore
form.controls['username'].updateValueAndValidity({ emitEvent: false });
form.addValidators([MustMatch('password', 'confirmPassword')]); form.addValidators([MustMatch('password', 'confirmPassword')]);
} else {
// @ts-ignore
form.removeControl('username');
// @ts-ignore
form.removeControl('password');
// @ts-ignore
form.removeControl('confirmPassword');
form.removeValidators([MustMatch('password', 'confirmPassword')]);
} }
return form; return form;
@@ -75,15 +77,18 @@ export class GuildFormComponent extends AbstractFormDialog<IPartnerRequest, IPar
} }
override ngOnChanges() { override ngOnChanges() {
this.form.patchValue(this.initialValues ?? {}); this.form = this.initForm();
this.form.patchValue((this.initialValues ?? {}) as Partial<Record<string, unknown>>);
if (this.editMode && !this.partnerId) { if (this.editMode && !this.partnerId) {
throw 'missing some arguments'; throw 'missing some arguments';
} }
this.initForm();
} }
override submitForm(payload: IPartnerRequest) { override submitForm(payload: IPartnerRequest) {
const formData = buildFormData(this.form, { logo: this.logo() }, ['confirmPassword']); const skipFields = this.editMode
? ['confirmPassword', 'username', 'password']
: ['confirmPassword'];
const formData = buildFormData(this.form, { logo: this.logo() }, skipFields);
if (this.editMode) { if (this.editMode) {
return this.service.update(this.partnerId!, formData); return this.service.update(this.partnerId!, formData);
+3 -1
View File
@@ -1,8 +1,10 @@
import { IEnumTranslate } from '@/shared/models/enum_translate.type';
export interface IPartnerRawResponse { export interface IPartnerRawResponse {
id: string; id: string;
name: string; name: string;
code: string; code: string;
status: string; status: IEnumTranslate;
created_at: string; created_at: string;
logo_url: string; logo_url: string;
licenses_status: { licenses_status: {
@@ -38,7 +38,15 @@ export class PartnersComponent extends AbstractList<IPartnerResponse> {
header: 'تعداد لایسنس تمدیدی', header: 'تعداد لایسنس تمدیدی',
customDataModel: this.licensesRenewStatus, customDataModel: this.licensesRenewStatus,
}, },
{ field: 'status', header: 'وضعیت' }, {
field: 'status',
header: 'وضعیت',
type: 'nested',
variant: 'tag',
nestedOption: {
path: 'status.translate',
},
},
]; ];
} }
@@ -10,7 +10,7 @@ export const superAdminUserAccountsNamedRoutes: NamedRoutes<TAccountsRouteNames>
import('../../views/accounts/list.component').then((m) => m.UserAccountsComponent), import('../../views/accounts/list.component').then((m) => m.UserAccountsComponent),
// @ts-ignore // @ts-ignore
meta: { meta: {
title: 'فعالیت‌های اقتصادی', title: 'فعالیت اقتصادی',
pagePath: (userId: string) => `/super_admin/users/${userId}/accounts`, pagePath: (userId: string) => `/super_admin/users/${userId}/accounts`,
}, },
}, },
@@ -7,7 +7,7 @@
@for (category of categories(); track category.id) { @for (category of categories(); track category.id) {
<div <div
[class]=" [class]="
'h-10 rounded-lg px-3 flex items-center gap-3 border border-surface-border cursor-pointer bg-surface-100 hover:bg-surface-200 transition-all' + 'h-10 rounded-lg px-3 flex items-center gap-3 border border-surface-border cursor-pointer bg-surface-100 hover:bg-surface-200 transition-all shrink-0' +
(selectedCategory() === category.id ? ' bg-surface-card' : '') (selectedCategory() === category.id ? ' bg-surface-card' : '')
" "
(click)="changeSelectedCategory(category)" (click)="changeSelectedCategory(category)"
@@ -237,8 +237,6 @@ export class InputComponent {
value = this.applyFixed(value); value = this.applyFixed(value);
} }
console.log(value);
if (this.preparedMaxLength && value.length > this.preparedMaxLength) { if (this.preparedMaxLength && value.length > this.preparedMaxLength) {
value = value.slice(0, this.preparedMaxLength); value = value.slice(0, this.preparedMaxLength);
} }
@@ -277,7 +275,6 @@ export class InputComponent {
this.control.setValue(isIdentifierField ? value : value === '' ? '' : numericValue); this.control.setValue(isIdentifierField ? value : value === '' ? '' : numericValue);
const viewValue = isPriceFormat && value !== '' ? numericValue.toLocaleString('en-US') : value; const viewValue = isPriceFormat && value !== '' ? numericValue.toLocaleString('en-US') : value;
console.log('viewValue', viewValue);
target.value = viewValue; target.value = viewValue;
if ( if (
@@ -1,4 +1,4 @@
<div class="h-full overflow-auto"> <div class="h-full overflow-auto border border-surface-border cardShadow">
<div <div
[ngClass]="{ [ngClass]="{
'px-4': captionBox, 'px-4': captionBox,
@@ -13,14 +13,14 @@
} }
<div class="grid grid-cols-1 gap-3" [ngClass]="{ 'pt-4': captionBox }"> <div class="grid grid-cols-1 gap-3" [ngClass]="{ 'pt-4': captionBox }">
@for (item of items; track `gridView_${$index}`) { @for (item of items; track `gridView_${$index}`) {
<div class="card border border-surface-border bg-surface-50! mb-0! rounded-2xl p-4!"> <div class="card border border-surface-border bg-surface-0! mb-0! rounded-2xl p-4!">
<div class="listKeyValue"> <div class="listKeyValue">
@for (col of columns; track `gridView_${col.field.toString()}_${$index}`) { @for (col of columns; track `gridView_${col.field.toString()}_${$index}`) {
@if (col.type !== "index") { @if (col.type !== "index") {
<app-key-value [label]="col.header" [variant]="col.variant"> <app-key-value [label]="col.header" [variant]="col.variant">
@if (col.type === "thumbnail") { @if (col.type === "thumbnail") {
<div <div
class="w-20 h-20 rounded-2xl overflow-hidden bg-surface-50 cursor-pointer" class="w-20 h-20 rounded-2xl overflow-hidden bg-surface-100 cursor-pointer"
(click)="openThumbnailModal(item)" (click)="openThumbnailModal(item)"
> >
@if (item && col?.field && item[col!.field!]) { @if (item && col?.field && item[col!.field!]) {
@@ -46,16 +46,33 @@
} }
</div> </div>
@if (showEdit || showDelete || showDetails) { @if (showEdit || showDelete || showDetails) {
<hr /> <hr class="my-2" />
<div class="flex justify-center gap-2 mt-2"> <div class="flex justify-center gap-2 mt-4">
@if (showEdit) { @if (showEdit) {
<p-button icon="pi pi-pencil" label="ویرایش" size="small" outlined (click)="edit(item)"> </p-button> <p-button
icon="pi pi-pencil"
label="ویرایش"
size="small"
outlined
severity="secondary"
(click)="edit(item)"
>
</p-button>
} }
@if (showDelete) { @if (showDelete) {
<p-button icon="pi pi-trash" label="حذف" size="small" outlined (click)="remove(item)"> </p-button> <p-button icon="pi pi-trash" label="حذف" size="small" outlined severity="danger" (click)="remove(item)">
</p-button>
} }
@if (showDetails) { @if (showDetails) {
<p-button icon="pi pi-eye" label="مشاهده" size="small" outlined (click)="details(item)"> </p-button> <p-button
icon="pi pi-eye"
label="مشاهده"
size="small"
outlined
severity="primary"
(click)="details(item)"
>
</p-button>
} }
</div> </div>
} }
@@ -1,4 +1,4 @@
<div class="h-full flex flex-col gap-4"> <div class="h-full flex flex-col gap-4 cardShadow">
<p-table <p-table
[columns]="columns" [columns]="columns"
[scrollable]="true" [scrollable]="true"
@@ -52,7 +52,7 @@
<td> <td>
@if (col.type === "thumbnail") { @if (col.type === "thumbnail") {
<div <div
class="w-20 h-20 rounded-2xl overflow-hidden bg-surface-50 cursor-pointer" class="w-20 h-20 rounded-2xl overflow-hidden bg-surface-100 cursor-pointer"
(click)="openThumbnailModal(item)" (click)="openThumbnailModal(item)"
> >
@if (item[col.field]) { @if (item[col.field]) {
@@ -40,6 +40,9 @@
@if (showRefresh) { @if (showRefresh) {
<p-button icon="pi pi-refresh" outlined size="small" (click)="refresh()"></p-button> <p-button icon="pi pi-refresh" outlined size="small" (click)="refresh()"></p-button>
} }
@if (showAll && allItemsPageRoute) {
<a routerLink pButton [routerLink]="allItemsPageRoute" outlined size="small">نمایش همه</a>
}
@if (showAdd) { @if (showAdd) {
<p-button <p-button
label="{{ addNewCtaLabel }}" label="{{ addNewCtaLabel }}"
@@ -113,15 +116,11 @@
@if (showRefresh) { @if (showRefresh) {
<p-button icon="pi pi-refresh" outlined size="small" (click)="refresh()"></p-button> <p-button icon="pi pi-refresh" outlined size="small" (click)="refresh()"></p-button>
} }
@if (showAll && allItemsPageRoute) {
<a routerLink pButton [routerLink]="allItemsPageRoute" icon="pi pi-eye" outlined size="small"></a>
}
@if (showAdd) { @if (showAdd) {
<p-button <p-button icon="pi pi-plus" size="small" (click)="openAddForm()"></p-button>
label="{{ addNewCtaLabel }}"
icon="pi pi-plus"
size="small"
class="max-sm:hidden"
(click)="openAddForm()"
></p-button>
<p-button icon="pi pi-plus" size="small" class="sm:hidden" (click)="openAddForm()"></p-button>
} }
</div> </div>
} }
@@ -14,6 +14,7 @@ import {
signal, signal,
TemplateRef, TemplateRef,
} from '@angular/core'; } from '@angular/core';
import { RouterLink } from '@angular/router';
import { ButtonModule } from 'primeng/button'; import { ButtonModule } from 'primeng/button';
import { DrawerModule } from 'primeng/drawer'; import { DrawerModule } from 'primeng/drawer';
import { PaginatorModule } from 'primeng/paginator'; import { PaginatorModule } from 'primeng/paginator';
@@ -85,6 +86,7 @@ export interface IColumn<T = any> {
AppPageDataListTableView, AppPageDataListTableView,
AppPageDataListGridView, AppPageDataListGridView,
UikitEmptyStateComponent, UikitEmptyStateComponent,
RouterLink,
], ],
}) })
export class PageDataListComponent<I = any> { export class PageDataListComponent<I = any> {
@@ -117,6 +119,8 @@ export class PageDataListComponent<I = any> {
@Input() expandColumns?: Maybe<IColumn[]> = null; @Input() expandColumns?: Maybe<IColumn[]> = null;
@Input() dataKey?: string = 'items'; @Input() dataKey?: string = 'items';
@Input() showIndex?: boolean = true; @Input() showIndex?: boolean = true;
@Input() showAll?: boolean = false;
@Input() allItemsPageRoute?: string;
@ContentChild('filter', { static: true }) filter!: TemplateRef<any> | null; @ContentChild('filter', { static: true }) filter!: TemplateRef<any> | null;
@ContentChild('caption', { static: true }) caption!: TemplateRef<any> | null; @ContentChild('caption', { static: true }) caption!: TemplateRef<any> | null;
@@ -42,7 +42,6 @@ export class SharedUploadFileComponent implements OnChanges, OnDestroy {
ngAfterViewInit() { ngAfterViewInit() {
this.revokeObjectUrl(); this.revokeObjectUrl();
console.log('first');
if (this.initial_file_url && !this.objectUrl) { if (this.initial_file_url && !this.objectUrl) {
this.filePreview = this.initial_file_url; this.filePreview = this.initial_file_url;
+4
View File
@@ -5,3 +5,7 @@
.listKeyValue { .listKeyValue {
@apply grid md:grid-cols-3 sm:grid-cols-2 sm:gap-4 gap-3 items-center; @apply grid md:grid-cols-3 sm:grid-cols-2 sm:gap-4 gap-3 items-center;
} }
.cardShadow {
box-shadow: var(--p-card-shadow) !important;
}