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 {
if (request.amount <= 10_000) {
const errorMessage = 'برای مقادیر زیر ۱۰۰ هزار ریال، پرداخت ممکن نیست.';
this.toastService.error({ text: errorMessage, life: 3000 });
this.toastService.warn({ text: errorMessage, life: 3000 });
return {
success: false,
error: errorMessage,
};
}
this.toastService.info({ text: 'در حال پردازش پرداخت...', life: 3000 });
this.toastService.info({ text: 'در حال پردازش پرداخت...' });
try {
// @ts-ignore
window.NativeBridge.pay(request.amount, request.id || '');
return { success: true };
} 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 };
}
// const fn = window.NativeBridge.pay(123, 'test');
@@ -99,7 +99,7 @@ export class NativeBridgeService {
// // return { success: true, data: parsed ?? raw };
// } 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 };
// }
}
@@ -109,12 +109,16 @@ export class NativeBridgeService {
}
private invokePrint(payload: INativePrintRequest): INativeBridgeResult {
if (!this.isEnabled() || !this.canPrint()) {
this.toastService.warn({ text: 'متاسفانه ارتباط با چاپگر برقرار نیست.' });
return { success: false, error: 'متاسفانه ارتباط با چاپگر برقرار نیست.' };
}
try {
// @ts-ignore
window.NativeBridge.print(JSON.stringify([payload]));
return { success: true };
} catch (error) {
this.toastService.info({ text: (error as Error).message, life: 30000 });
this.toastService.warn({ text: 'متاسفانه ارتباط با چاپگر برقرار نیست.' });
return { success: false, error: (error as Error).message };
}
}
@@ -8,7 +8,7 @@ export const CONSUMER_MENU_ITEMS = [
routerLink: ['/consumer'],
},
{
label: 'فعالیت‌های اقتصادی',
label: 'فعالیت اقتصادی',
icon: 'pi pi-fw pi-home',
routerLink: ['/consumer/business_activities'],
},
@@ -1,5 +1,5 @@
<app-page-data-list
pageTitle="لیست فعالیت‌های اقتصادی"
pageTitle="لیست فعالیت اقتصادی"
[columns]="columns"
[items]="items()"
[loading]="loading()"
@@ -1,5 +1,5 @@
<app-page-data-list
[pageTitle]="'لیست مشتری‌ها'"
[pageTitle]="'لیست مشتری'"
[columns]="columns"
emptyPlaceholderTitle="مشتری یافت نشد"
[items]="items()"
@@ -1,5 +1,5 @@
<app-page-data-list
pageTitle="مدیریت فعالیت‌های اقتصادی"
pageTitle="مدیریت فعالیت اقتصادی"
[addNewCtaLabel]="'افزودن فعالیت اقتصادی'"
[columns]="columns"
[showAdd]="true"
@@ -14,9 +14,8 @@ export const partnerConsumerBusinessActivitiesNamedRoutes: NamedRoutes<TBusiness
),
// @ts-ignore
meta: {
title: 'فعالیت‌های اقتصادی',
pagePath: (consumerId: string) =>
`/partner/consumers/${consumerId}/business_activities`,
title: 'فعالیت اقتصادی',
pagePath: (consumerId: string) => `/partner/consumers/${consumerId}/business_activities`,
},
},
businessActivity: {
@@ -45,8 +44,7 @@ export const PARTNER_CONSUMER_BUSINESS_ACTIVITIES_ROUTES: Routes = [
children: [
{
path: '',
loadComponent:
partnerConsumerBusinessActivitiesNamedRoutes.businessActivity.loadComponent,
loadComponent: partnerConsumerBusinessActivitiesNamedRoutes.businessActivity.loadComponent,
},
...PARTNER_CONSUMER_COMPLEXES_ROUTES,
],
@@ -1,5 +1,5 @@
<app-page-data-list
pageTitle="لیست مشتری‌ها"
pageTitle="لیست مشتری"
[addNewCtaLabel]="'افزودن مشتری'"
[columns]="columns"
[showAdd]="true"
@@ -1,5 +1,5 @@
<app-page-data-list
[pageTitle]="'لیست مشتری‌ها'"
[pageTitle]="'لیست مشتری'"
[columns]="columns"
emptyPlaceholderTitle="مشتری یافت نشد"
[items]="items()"
@@ -1,7 +1,5 @@
<ng-template #topbarStart>
<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 #topbarCenter>
@@ -26,9 +24,32 @@
</div>
</ng-template>
@if (loading()) {
<div
class="h-full relative"
(touchstart)="onTouchStart($event)"
(touchmove)="onTouchMove($event)"
(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 {
} @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">
@@ -76,4 +97,5 @@
<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 { LayoutService } from '@/layout/service/layout.service';
import { PageLoadingComponent } from '@/shared/components/page-loading.component';
import { CommonModule } from '@angular/common';
import {
AfterViewInit,
Component,
@@ -34,6 +35,7 @@ import { PosMainMenuSidebarComponent } from './mainMenuSidebar/main-menu-sidebar
Button,
PosMainMenuSidebarComponent,
Menu,
CommonModule,
],
})
export class PosLayoutComponent implements AfterViewInit {
@@ -84,6 +86,10 @@ export class PosLayoutComponent implements AfterViewInit {
placeholderLogo = images.placeholders.logo;
now = new Date();
private touchStartY = 0;
private pulling = false;
pullDistance = signal(0);
readonly pullThreshold = 80;
getData() {
this.posProfileStore.getData().subscribe({
@@ -101,24 +107,6 @@ export class PosLayoutComponent implements AfterViewInit {
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() {
this.layoutService.changeIsFullPage(true);
this.getData();
@@ -137,7 +125,33 @@ export class PosLayoutComponent implements AfterViewInit {
this.layoutService.setTopbarEndSlot(null);
}
refresh() {
window.location.reload();
onTouchStart(event: TouchEvent) {
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,4 +1,5 @@
<div class="flex gap-3 overflow-auto">
<div class="overflow-x-auto">
<div class="w-full overflow-visible flex gap-3">
@if (loading()) {
@for (i of [1, 2, 3, 4, 5]; track i) {
<p-skeleton width="6rem" height="2.5rem" />
@@ -7,7 +8,7 @@
@for (category of categories(); track category.id) {
<div
[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' : '')
"
(click)="changeSelectedCategory(category)"
@@ -26,4 +27,5 @@
</div>
}
}
</div>
</div>
@@ -63,4 +63,4 @@
<pos-order-customer-dialog [(visible)]="isVisibleCustomerForm" (onSubmit)="submitCustomer()" />
<!-- <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 { Card } from 'primeng/card';
import images from 'src/assets/images';
import { IPosOrderResponse } from '../../models';
import { PosLandingStore } from '../../store/main.store';
import { PosOrderCustomerDialogComponent } from '../customers/customer-dialog.component';
import { GoldPayloadOrderCardComponent } from '../payloads/gold/order-card.component';
@@ -31,7 +32,7 @@ import { POSOrderPriceInfoCardComponent } from './price-info-card.component';
export class PosOrderSectionComponent {
private readonly store = inject(PosLandingStore);
@Output() onAddMoreGoods = new EventEmitter<void>();
@Output() onSuccess = new EventEmitter<void>();
@Output() onSuccess = new EventEmitter<IPosOrderResponse>();
placeholderImage = images.placeholders.default;
@@ -84,9 +85,7 @@ export class PosOrderSectionComponent {
}
submitCustomer() {}
submitPayment() {
console.log('submitPayment');
this.onSuccess.emit();
submitPayment(invoice: IPosOrderResponse) {
this.onSuccess.emit(invoice);
}
}
@@ -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 { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
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 { ButtonDirective } from 'primeng/button';
import { Select } from 'primeng/select';
import { catchError, throwError } from 'rxjs';
import { IPayment, TOrderPaymentTypes } from '../../models';
import { IPayment, IPosOrderResponse, TOrderPaymentTypes } from '../../models';
import { PosPaymentBridgeAbstract } from '../../services/payment-bridge.abstract';
import { PosPaymentBridgeService } from '../../services/payment-bridge.service';
import { PosLandingStore } from '../../store/main.store';
@@ -32,10 +32,7 @@ import { PosLandingStore } from '../../store/main.store';
],
providers: [{ provide: PosPaymentBridgeAbstract, useExisting: PosPaymentBridgeService }],
})
export class PosPaymentFormDialogComponent
extends AbstractFormDialog<IPayment, IPayment>
implements OnInit, OnDestroy
{
export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment, IPosOrderResponse> {
private readonly store = inject(PosLandingStore);
private readonly paymentBridge = inject(PosPaymentBridgeAbstract);
private readonly nativeBridgeService = inject(NativeBridgeService);
@@ -72,8 +69,8 @@ export class PosPaymentFormDialogComponent
private initForm = () => {
const form = this.fb.group({
set_off: [this.initialValues?.set_off || 0],
cash: [this.initialValues?.cash || 0],
set_off: [0],
cash: [0],
terminals: this.fb.array([this.fb.control(0)]),
});
@@ -237,10 +234,7 @@ export class PosPaymentFormDialogComponent
)
.subscribe((res) => {
this.close();
this.onSubmit.emit();
this.toastServices.success({
text: 'فاکتور شما با موفقیت ایجاد شد',
});
this.onSubmit.emit(res);
});
}
}
@@ -289,7 +283,7 @@ export class PosPaymentFormDialogComponent
// }
}
override onSuccess(response: IPayment): void {}
override onSuccess(response: IPosOrderResponse): void {}
private extractAmountFromPaymentResult(payload: unknown): number | null {
if (!payload || typeof payload !== 'object') return null;
+2 -2
View File
@@ -15,7 +15,7 @@ export interface IPosOrderRequest {
}
export interface IPosOrderRawResponse {
id?: string;
code?: string;
id: string;
code: string;
}
export interface IPosOrderResponse extends IPosOrderRawResponse {}
@@ -1,23 +1,22 @@
@if (loading()) {
<shared-page-loading />
} @else if (pos()) {
<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">
<div class="w-full min-h-[calc(100dvh-5.5rem)] md:overflow-hidden">
<div [class]="`w-full h-full flex max-md:flex-col gap-4 pb-4 pt-0`">
<div class="grow min-h-[calc(100dvh-5.5rem)]">
<pos-goods class="block h-full" />
</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 />
</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!"
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()"
>
@@ -33,7 +32,6 @@
</div>
</button>
}
<shared-dialog
[(visible)]="showInvoiceBottomSheet"
position="bottom"
@@ -41,7 +39,10 @@
[closable]="true"
header="فاکتور"
>
<pos-order-section (onAddMoreGoods)="closeInvoiceBottomSheet()" (onSuccess)="closeInvoiceBottomSheet()" />
<pos-order-section (onAddMoreGoods)="closeInvoiceBottomSheet()" (onSuccess)="onSuccessSubmit($event)" />
</shared-dialog>
</div>
@if (responseInvoice()) {
<pos-order-submitted-dialog [(visible)]="isVisibleSuccessDialog" [invoice]="responseInvoice()!" />
}
}
@@ -1,4 +1,5 @@
// import { CatalogRoleTagComponent } from '@/shared/catalog/roles';
import { Maybe } from '@/core';
import { PosInfoStore } from '@/domains/pos/store/pos.store';
import { SharedDialogComponent } from '@/shared/components';
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 { PosGoodsComponent } from '../components/goods.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';
@Component({
@@ -21,12 +24,16 @@ import { PosLandingStore } from '../store/main.store';
PriceMaskDirective,
SharedDialogComponent,
ButtonDirective,
PosOrderSubmittedDialogComponent,
],
})
export class PosLandingComponent {
private readonly infoStore = inject(PosInfoStore);
private readonly landingStore = inject(PosLandingStore);
isVisibleSuccessDialog = signal(false);
responseInvoice = signal<Maybe<IPosOrderResponse>>(null);
readonly loading = computed(() => this.infoStore.loading());
readonly pos = computed(() => this.infoStore.entity());
readonly error = computed(() => this.infoStore.error());
@@ -45,4 +52,14 @@ export class PosLandingComponent {
closeInvoiceBottomSheet() {
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) {
const { legal, individual } = customer;
let text = 'نوع اول - ';
console.log(customer);
if (legal) {
text += `حقوقی (${legal.company_name})`;
}
@@ -67,7 +66,7 @@ export class SaleInvoiceCardComponent {
sendInvoice() {
this.sendingLoading.set(true);
this.service
.sendFiscal(this.saleInvoice.id)
.sendToTsp(this.saleInvoice.id)
.pipe(finalize(() => this.sendingLoading.set(false)))
.subscribe(() => {
this.toastService.success({ text: 'ارسال فاکتور با موفقیت انجام شد.' });
@@ -88,7 +87,7 @@ export class SaleInvoiceCardComponent {
resendInvoice() {
this.resendingLoading.set(true);
this.service
.retryFiscal(this.saleInvoice.id)
.retrySendToTsp(this.saleInvoice.id)
.pipe(finalize(() => this.resendingLoading.set(false)))
.subscribe(() => {
this.toastService.success({ text: 'ارسال مجدد فاکتور با موفقیت انجام شد.' });
@@ -3,12 +3,12 @@ const baseUrl = () => `/api/v1/pos/sales_invoices`;
export const POS_SALE_INVOICES_API_ROUTES = {
list: () => baseUrl(),
single: (invoiceId: string) => `${baseUrl()}/${invoiceId}`,
fiscal: {
tsp: {
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`,
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`,
},
};
@@ -32,43 +32,41 @@ export class PosSaleInvoicesService {
return this.http.get<ISaleInvoiceFullRawResponse>(this.apiRoutes.single(invoiceId));
}
sendFiscal(invoiceId: string): Observable<IPosSaleInvoiceFiscalActionResponse> {
sendToTsp(invoiceId: string): Observable<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>(
this.apiRoutes.fiscal.retry(invoiceId),
this.apiRoutes.tsp.retry(invoiceId),
{},
);
}
getFiscalStatus(invoiceId: string): Observable<IPosSaleInvoiceFiscalStatusResponse> {
return this.http.get<IPosSaleInvoiceFiscalStatusResponse>(
this.apiRoutes.fiscal.status(invoiceId),
);
return this.http.get<IPosSaleInvoiceFiscalStatusResponse>(this.apiRoutes.tsp.status(invoiceId));
}
getInquiry(invoiceId: string): Observable<IPosSaleInvoiceFiscalStatusResponse> {
return this.http.get<IPosSaleInvoiceFiscalStatusResponse>(
this.apiRoutes.fiscal.getInquiry(invoiceId),
this.apiRoutes.tsp.getInquiry(invoiceId),
{},
);
}
revoke(invoiceId: string): Observable<IPosSaleInvoiceFiscalStatusResponse> {
return this.http.post<IPosSaleInvoiceFiscalStatusResponse>(
this.apiRoutes.fiscal.revoke(invoiceId),
this.apiRoutes.tsp.revoke(invoiceId),
{},
);
}
getFiscalAttempts(invoiceId: string): Observable<IPosSaleInvoiceFiscalAttemptsResponse> {
return this.http.get<IPosSaleInvoiceFiscalAttemptsResponse>(
this.apiRoutes.fiscal.attempts(invoiceId),
this.apiRoutes.tsp.attempts(invoiceId),
);
}
}
@@ -1,5 +1,5 @@
<app-page-data-list
pageTitle="لیست فعالیت‌های اقتصادی"
pageTitle="لیست فعالیت اقتصادی"
[columns]="columns"
emptyPlaceholderTitle="فعالیت اقتصادی‌ای یافت نشد."
[items]="items()"
@@ -14,7 +14,7 @@ export const superAdminConsumerBusinessActivitiesNamedRoutes: NamedRoutes<TBusin
),
// @ts-ignore
meta: {
title: 'فعالیت‌های اقتصادی',
title: 'فعالیت اقتصادی',
pagePath: (consumerId: string) =>
`/super_admin/consumers/${consumerId}/business_activities`,
},
@@ -21,7 +21,7 @@ export class ConsumersComponent extends AbstractList<IAdminConsumerResponse> {
this.columns = [
// { field: 'id', header: 'شناسه', type: 'id' },
{ field: 'name', header: 'نام' },
{ field: 'business_counts', header: 'تعداد فعالیت‌های اقتصادی' },
{ field: 'business_counts', header: 'تعداد فعالیت اقتصادی' },
{
field: 'partner',
header: 'ایجاد شده توسط',
@@ -1,15 +1,15 @@
<app-page-data-list
[pageTitle]="'مدیریت دسته‌بندی‌های کالا'"
[pageTitle]="'مدیریت دسته‌بندی‌ کالا'"
[addNewCtaLabel]="'افزودن دسته‌بندی'"
[columns]="columns"
emptyPlaceholderTitle="دسته‌بندی یافت نشد"
emptyPlaceholderDescription="برای افزودن دسته‌بندی، روی دکمهٔ بالا کلیک کنید."
[items]="items()"
[loading]="loading()"
[showDetails]="true"
[fullHeight]="fullHeight"
[showAdd]="true"
[showEdit]="true"
[fullHeight]="fullHeight"
[allItemsPageRoute]="allItemsPageRoute()"
(onEdit)="onEditClick($event)"
(onAdd)="openAddForm()"
(onRefresh)="refresh()"
@@ -4,7 +4,8 @@ import {
IColumn,
PageDataListComponent,
} 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 { GuildGoodCategoriesService } from '../../services/goodCategories.service';
import { GuildGoodCategoryFormComponent } from './form.component';
@@ -29,6 +30,10 @@ export class GuildGoodCategoriesListComponent extends AbstractList<IGoodCategori
];
service = inject(GuildGoodCategoriesService);
allItemsPageRoute = computed(() =>
guildGoodCategoriesNamedRoutes.goodCategories.meta.pagePath!(this.guildId),
);
override setColumns(): void {
this.columns = this.header;
}
@@ -5,12 +5,13 @@ export type TGuildGoodCategoriesRouteNames = 'goodCategories';
export const guildGoodCategoriesNamedRoutes: NamedRoutes<TGuildGoodCategoriesRouteNames> = {
goodCategories: {
path: 'good_categories',
path: 'good-categories',
loadComponent: () =>
import('../../views/categories/list.component').then((m) => m.GuildGoodCategoriesComponent),
// @ts-ignore
meta: {
title: 'دسته‌بندی‌های کالا',
pagePath: (guildId: string) => `/super_admin/guilds/${guildId}/good-categories`,
},
},
// goodCategory: {
@@ -39,26 +39,28 @@ export class GuildFormComponent extends AbstractFormDialog<IPartnerRequest, IPar
private service = inject(PartnersService);
private initForm() {
const form = this.fb.group({
const formConfig = {
name: fieldControl.name(this.initialValues?.name || ''),
code: fieldControl.code(this.initialValues?.code || ''),
username: fieldControl.username(),
password: fieldControl.password(),
confirmPassword: fieldControl.confirmPassword(),
});
username: [''],
password: [''],
confirmPassword: [''],
};
if (!this.editMode) {
form.controls.username.setValidators([Validators.required, usernameValidator()]);
form.controls.username.updateValueAndValidity({ emitEvent: false });
formConfig.username = fieldControl.username();
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')]);
} else {
// @ts-ignore
form.removeControl('username');
// @ts-ignore
form.removeControl('password');
// @ts-ignore
form.removeControl('confirmPassword');
form.removeValidators([MustMatch('password', 'confirmPassword')]);
}
return form;
@@ -75,15 +77,18 @@ export class GuildFormComponent extends AbstractFormDialog<IPartnerRequest, IPar
}
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) {
throw 'missing some arguments';
}
this.initForm();
}
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) {
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 {
id: string;
name: string;
code: string;
status: string;
status: IEnumTranslate;
created_at: string;
logo_url: string;
licenses_status: {
@@ -38,7 +38,15 @@ export class PartnersComponent extends AbstractList<IPartnerResponse> {
header: 'تعداد لایسنس تمدیدی',
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),
// @ts-ignore
meta: {
title: 'فعالیت‌های اقتصادی',
title: 'فعالیت اقتصادی',
pagePath: (userId: string) => `/super_admin/users/${userId}/accounts`,
},
},
@@ -7,7 +7,7 @@
@for (category of categories(); track category.id) {
<div
[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' : '')
"
(click)="changeSelectedCategory(category)"
@@ -237,8 +237,6 @@ export class InputComponent {
value = this.applyFixed(value);
}
console.log(value);
if (this.preparedMaxLength && value.length > this.preparedMaxLength) {
value = value.slice(0, this.preparedMaxLength);
}
@@ -277,7 +275,6 @@ export class InputComponent {
this.control.setValue(isIdentifierField ? value : value === '' ? '' : numericValue);
const viewValue = isPriceFormat && value !== '' ? numericValue.toLocaleString('en-US') : value;
console.log('viewValue', viewValue);
target.value = viewValue;
if (
@@ -1,4 +1,4 @@
<div class="h-full overflow-auto">
<div class="h-full overflow-auto border border-surface-border cardShadow">
<div
[ngClass]="{
'px-4': captionBox,
@@ -13,14 +13,14 @@
}
<div class="grid grid-cols-1 gap-3" [ngClass]="{ 'pt-4': captionBox }">
@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">
@for (col of columns; track `gridView_${col.field.toString()}_${$index}`) {
@if (col.type !== "index") {
<app-key-value [label]="col.header" [variant]="col.variant">
@if (col.type === "thumbnail") {
<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)"
>
@if (item && col?.field && item[col!.field!]) {
@@ -46,16 +46,33 @@
}
</div>
@if (showEdit || showDelete || showDetails) {
<hr />
<div class="flex justify-center gap-2 mt-2">
<hr class="my-2" />
<div class="flex justify-center gap-2 mt-4">
@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) {
<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) {
<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>
}
@@ -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
[columns]="columns"
[scrollable]="true"
@@ -52,7 +52,7 @@
<td>
@if (col.type === "thumbnail") {
<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)"
>
@if (item[col.field]) {
@@ -40,6 +40,9 @@
@if (showRefresh) {
<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) {
<p-button
label="{{ addNewCtaLabel }}"
@@ -113,15 +116,11 @@
@if (showRefresh) {
<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) {
<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>
<p-button icon="pi pi-plus" size="small" (click)="openAddForm()"></p-button>
}
</div>
}
@@ -14,6 +14,7 @@ import {
signal,
TemplateRef,
} from '@angular/core';
import { RouterLink } from '@angular/router';
import { ButtonModule } from 'primeng/button';
import { DrawerModule } from 'primeng/drawer';
import { PaginatorModule } from 'primeng/paginator';
@@ -85,6 +86,7 @@ export interface IColumn<T = any> {
AppPageDataListTableView,
AppPageDataListGridView,
UikitEmptyStateComponent,
RouterLink,
],
})
export class PageDataListComponent<I = any> {
@@ -117,6 +119,8 @@ export class PageDataListComponent<I = any> {
@Input() expandColumns?: Maybe<IColumn[]> = null;
@Input() dataKey?: string = 'items';
@Input() showIndex?: boolean = true;
@Input() showAll?: boolean = false;
@Input() allItemsPageRoute?: string;
@ContentChild('filter', { static: true }) filter!: TemplateRef<any> | null;
@ContentChild('caption', { static: true }) caption!: TemplateRef<any> | null;
@@ -42,7 +42,6 @@ export class SharedUploadFileComponent implements OnChanges, OnDestroy {
ngAfterViewInit() {
this.revokeObjectUrl();
console.log('first');
if (this.initial_file_url && !this.objectUrl) {
this.filePreview = this.initial_file_url;
+4
View File
@@ -5,3 +5,7 @@
.listKeyValue {
@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;
}