feat: implement payment result handling; refactor payment components and enhance terminal payment logic

This commit is contained in:
2026-05-17 15:21:06 +03:30
parent 6f1ad20cff
commit 78501b907b
13 changed files with 268 additions and 138 deletions
+2 -3
View File
@@ -1,4 +1,3 @@
# syntax=docker/dockerfile:1.4
# ---------- Build stage ---------- # ---------- Build stage ----------
FROM node:20-alpine AS builder FROM node:20-alpine AS builder
@@ -7,11 +6,11 @@ ARG DIST_DIR=tis
WORKDIR /app WORKDIR /app
RUN npm config set registry "https://hub.megan.ir/npm/"
RUN npm install -g pnpm RUN npm install -g pnpm
COPY package.json pnpm-lock.yaml ./ COPY package.json pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \ RUN pnpm install --frozen-lockfile
pnpm config set store-dir /pnpm/store && pnpm install --frozen-lockfile
COPY . . COPY . .
@@ -1,40 +1,41 @@
<div class="w-full min-h-[60svh] p-4 md:p-6"> @defer {
<section class="mx-auto max-w-3xl overflow-hidden rounded-2xl border border-surface-200 bg-white"> <div class="w-full min-h-[60svh] p-4 md:p-6">
<div class="bg-gradient-to-l from-emerald-50 via-teal-50 to-cyan-50 p-5 md:p-7"> <section class="mx-auto max-w-3xl overflow-hidden rounded-2xl border border-surface-200 bg-white">
<p class="mb-2 inline-flex items-center rounded-full bg-white px-3 py-1 text-xs font-semibold text-emerald-700"> <div class="bg-gradient-to-l from-emerald-50 via-teal-50 to-cyan-50 p-5 md:p-7">
{{ brandingInfo.title }} <p class="mb-2 inline-flex items-center rounded-full bg-white px-3 py-1 text-xs font-semibold text-emerald-700">
</p> {{ brandingInfo.title }}
<span class="block mb-4 text-xl font-bold text-surface-900 md:text-3xl">{{ brandingInfo.fullTitle }}</span> </p>
<p class="mt-3 max-w-2xl text-sm leading-7 text-surface-700 md:text-base"> <span class="block mb-4 text-xl font-bold text-surface-900 md:text-3xl">{{ brandingInfo.fullTitle }}</span>
این نسخه برای استفاده روزانه روی دستگاه های PSP بهینه شده است تا عملیات ثبت و صدور صورتحساب‌‌های مالیاتی با سرعت <p class="mt-3 max-w-2xl text-sm leading-7 text-surface-700 md:text-base">
بالاتر، پایداری بیشتر و تجربه کاربری ساده تر انجام شود. این نسخه برای استفاده روزانه روی دستگاه های PSP بهینه شده است تا عملیات ثبت و صدور صورتحساب‌‌های مالیاتی با
</p> سرعت بالاتر، پایداری بیشتر و تجربه کاربری ساده تر انجام شود.
</div> </p>
</div>
<div class="grid gap-3 p-4 md:grid-cols-3 md:gap-4 md:p-6"> <div class="grid gap-3 p-4 md:grid-cols-3 md:gap-4 md:p-6">
<!-- <article class="rounded-xl border border-surface-200 bg-surface-50 p-4"> <!-- <article class="rounded-xl border border-surface-200 bg-surface-50 p-4">
<h2 class="mb-2 text-sm font-bold text-surface-900">پایداری در وب ویو</h2> <h2 class="mb-2 text-sm font-bold text-surface-900">پایداری در وب ویو</h2>
<p class="text-sm leading-6 text-surface-700"> <p class="text-sm leading-6 text-surface-700">
رفتار رابط کاربری برای محیط WebView تنظیم شده تا روی سخت افزارهای ضعیف تر هم اجرای روان تری داشته باشد. رفتار رابط کاربری برای محیط WebView تنظیم شده تا روی سخت افزارهای ضعیف تر هم اجرای روان تری داشته باشد.
</p> </p>
</article> --> </article> -->
<article class="rounded-xl border border-surface-200 bg-surface-50 p-4"> <article class="rounded-xl border border-surface-200 bg-surface-50 p-4">
<span class="block mb-4 text-xl font-bold text-surface-900">تمرکز بر سرعت عملیات</span> <span class="block mb-4 text-xl font-bold text-surface-900">تمرکز بر سرعت عملیات</span>
<p class="text-sm leading-6 text-surface-700"> <p class="text-sm leading-6 text-surface-700">
مسیرهای کلیدی مانند انتخاب کالا، ورود مقادیر و ثبت نهایی برای کاهش تاخیر و افزایش سرعت کاربر بهینه شده اند. مسیرهای کلیدی مانند انتخاب کالا، ورود مقادیر و ثبت نهایی برای کاهش تاخیر و افزایش سرعت کاربر بهینه شده اند.
</p> </p>
</article> </article>
<article class="rounded-xl border border-surface-200 bg-surface-50 p-4"> <article class="rounded-xl border border-surface-200 bg-surface-50 p-4">
<span class="block mb-4 text-xl font-bold text-surface-900">سازگار با سناریوهای فروش</span> <span class="block mb-4 text-xl font-bold text-surface-900">سازگار با سناریوهای فروش</span>
<p class="text-sm leading-6 text-surface-700"> <p class="text-sm leading-6 text-surface-700">
طراحی این نسخه بر مبنای سناریوهای واقعی فروشگاه انجام شده تا اپراتور با کمترین کلیک فرآیند را تکمیل کند. طراحی این نسخه بر مبنای سناریوهای واقعی فروشگاه انجام شده تا اپراتور با کمترین کلیک فرآیند را تکمیل کند.
</p> </p>
</article> </article>
</div> </div>
<!-- <div class="mx-4 mb-4 rounded-xl border border-surface-200 bg-surface-50 p-4 md:mx-6"> <!-- <div class="mx-4 mb-4 rounded-xl border border-surface-200 bg-surface-50 p-4 md:mx-6">
<div class="flex flex-wrap items-center justify-between gap-3"> <div class="flex flex-wrap items-center justify-between gap-3">
<div class="text-sm text-surface-700"> <div class="text-sm text-surface-700">
نسخه برنامه: نسخه برنامه:
@@ -52,10 +53,11 @@
</div> </div>
</div> --> </div> -->
<div class="border-t border-surface-200 bg-surface-50 px-4 py-4 md:px-6"> <div class="border-t border-surface-200 bg-surface-50 px-4 py-4 md:px-6">
<p class="text-xs leading-6 text-surface-600 md:text-sm"> <p class="text-xs leading-6 text-surface-600 md:text-sm">
در صورت نیاز به راهنمایی بیشتر، به صفحه‌ی راهنمای استفاده مراجعه کنید یا با پشتیبانی تماس بگیرید. در صورت نیاز به راهنمایی بیشتر، به صفحه‌ی راهنمای استفاده مراجعه کنید یا با پشتیبانی تماس بگیرید.
</p> </p>
</div> </div>
</section> </section>
</div> </div>
}
@@ -45,7 +45,6 @@ export class PosConfigPrintFormComponent extends AbstractForm<
override async ngOnInit() { override async ngOnInit() {
this.loading.set(true); this.loading.set(true);
const initialValues = await this.service.get(); const initialValues = await this.service.get();
console.log('initialValues', initialValues);
this.form.patchValue(initialValues); this.form.patchValue(initialValues);
this.loading.set(false); this.loading.set(false);
@@ -16,7 +16,7 @@
<div class="form-group"> <div class="form-group">
<uikit-field label="تعداد مراحل پرداخت با پایانه" name="pay_by_terminal_steps"> <uikit-field label="تعداد مراحل پرداخت با پایانه" name="pay_by_terminal_steps">
<p-select <p-select
[options]="payByTerminalSteps" [options]="payByTerminalSteps()"
[ngModel]="selectedPayByTerminalStep()" [ngModel]="selectedPayByTerminalStep()"
[ngModelOptions]="{ standalone: true }" [ngModelOptions]="{ standalone: true }"
optionLabel="label" optionLabel="label"
@@ -33,13 +33,14 @@
[label]="$index === 0 ? 'پرداخت با پایانه' : 'پرداخت با پایانه - مرحله ' + ($index + 1)" [label]="$index === 0 ? 'پرداخت با پایانه' : 'پرداخت با پایانه - مرحله ' + ($index + 1)"
type="price" type="price"
[max]="terminalsMax()[$index]" [max]="terminalsMax()[$index]"
[disabled]="payByTerminalSteps()[$index].payed"
> >
<ng-template #suffixTemp> <ng-template #suffixTemp>
<button <button
pButton pButton
size="small" size="small"
type="button" type="button"
[disabled]="!remainedAmount()" [disabled]="!remainedAmount() || payByTerminalSteps()[$index].payed"
(click)="fillTerminalRemained($index)" (click)="fillTerminalRemained($index)"
> >
تمامی بدهی باقی‌مانده تمامی بدهی باقی‌مانده
@@ -12,7 +12,13 @@ import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angu
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, IPosOrderResponse, TOrderPaymentTypes } from '../../models'; import {
IPayment,
IPosOrderResponse,
PosPaymentResult,
TerminalSuccessPaymentPayload,
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';
@@ -48,25 +54,57 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
submitOrderLoading = computed(() => this.store.submitOrderLoading()); submitOrderLoading = computed(() => this.store.submitOrderLoading());
payByTerminalSteps = Array.from({ length: 10 }, (_, i) => ({ payByTerminalSteps = signal(
value: i + 1, Array.from({ length: 10 }, (_, i) => ({
label: `${i + 1} مرحله‌ای`, value: i + 1,
})); payed: false,
info: {} as TerminalSuccessPaymentPayload,
label: `${i + 1} مرحله‌ای`,
amount: 0,
})),
);
selectedPayByTerminalStep = signal(1); selectedPayByTerminalStep = signal(1);
payedInTerminal = signal<Array<number>>([]);
private restorePaymentCallback: Maybe<() => void> = null; private restorePaymentCallback: Maybe<() => void> = null;
private readonly injectedPaymentResultHandler = (payload: unknown) => { private readonly injectedPaymentResultHandler = (payload: PosPaymentResult) => {
alert('پرداخت با موفقیت انجام شد'); if (payload.status === 'SUCCESS') {
this.toastServices.success({ text: 'شد شد شد شد شد' }); const paidStepId = Number(payload.id || 0);
// this.handlePaymentResult(payload); this.payByTerminalSteps.update((prev) =>
prev.map((step, index) => {
if (step.value === paidStepId) {
return {
...step,
payed: true,
amount: this.terminalControls[step.value - 1]?.value || 0,
info: {
customer_card_no: payload.data?.customerCardNO || '',
description: payload.message || '',
rrn: payload.data?.rrn || '',
stan: payload.data?.stan || '',
terminal_id: payload.data?.terminalId || '',
transaction_date_time: new Date(payload.data?.transactionDateTime || ''),
},
};
}
return step;
}),
);
const terminalControl = this.terminalControls[paidStepId - 1];
if (terminalControl) {
terminalControl.disable({ onlySelf: true, emitEvent: false });
}
const amount = Number(
terminalControl?.value || this.extractAmountFromPaymentResult(payload) || 0,
);
this.toastServices.success({ text: `پرداخت با موفقیت انجام شد. مبلغ: ${amount}` });
} else {
this.toastServices.error({ text: payload.message || 'پرداخت با دستگاه انجام نشد.' });
}
}; };
ngAfterViewInit() { ngAfterViewInit() {
console.log('ngOnViewInit');
this.restorePaymentCallback = this.paymentBridge.registerPaymentResultListener( this.restorePaymentCallback = this.paymentBridge.registerPaymentResultListener(
this.injectedPaymentResultHandler, this.injectedPaymentResultHandler,
); );
@@ -118,7 +156,11 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
this.terminalsMax.update((max) => { this.terminalsMax.update((max) => {
const terminalMax = remainedAmount + (terminal.value || 0); const terminalMax = remainedAmount + (terminal.value || 0);
terminal.addValidators([Validators.max(terminalMax)]); terminal.addValidators([Validators.max(terminalMax)]);
if (terminalMax) { if (this.isTerminalStepPaid(i + 1)) {
terminal.disable({
onlySelf: true,
});
} else if (terminalMax) {
terminal.enable({ terminal.enable({
onlySelf: true, onlySelf: true,
}); });
@@ -145,7 +187,13 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
} }
changePayByTerminalSteps(steps: number) { changePayByTerminalSteps(steps: number) {
const normalizedSteps = Math.max(1, Number(steps) || 1); const minSteps = this.getMinAllowedSteps();
const normalizedSteps = Math.max(minSteps, Number(steps) || 1);
if ((Number(steps) || 1) < minSteps) {
this.toastServices.warn({
text: `حداقل تعداد مراحل به دلیل پرداخت های انجام شده ${minSteps} است.`,
});
}
this.selectedPayByTerminalStep.set(normalizedSteps); this.selectedPayByTerminalStep.set(normalizedSteps);
const terminals = this.form.controls.terminals; const terminals = this.form.controls.terminals;
@@ -219,10 +267,24 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
} }
const rawPayment = this.form.getRawValue(); const rawPayment = this.form.getRawValue();
const terminalPayments = this.payByTerminalSteps()
.slice(0, this.selectedPayByTerminalStep())
.filter((step) => step.payed)
.map((step) => ({
amount: Number(this.terminalControls[step.value - 1]?.value || step.amount || 0),
terminal_id: step.info.terminal_id || '',
stan: step.info.stan || '',
rrn: step.info.rrn || '',
response_code: '00',
customer_card_no: step.info.customer_card_no || '',
transaction_date_time: step.info.transaction_date_time || new Date(),
description: step.info.description || '',
}));
const payment: IPayment = { const payment: IPayment = {
cash: Number(rawPayment.cash || 0), cash: Number(rawPayment.cash || 0),
set_off: Number(rawPayment.set_off || 0), set_off: Number(rawPayment.set_off || 0),
terminals: (rawPayment.terminals || []).map((value) => Number(value || 0)), terminals: terminalPayments,
}; };
this.store.setPayment(payment); this.store.setPayment(payment);
@@ -230,6 +292,9 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
const terminalPaymentIsDone = this.checkTerminalPayments(); const terminalPaymentIsDone = this.checkTerminalPayments();
if (terminalPaymentIsDone) { if (terminalPaymentIsDone) {
this.toastService.info({
text: `terminalPayments[0].customer_card_no: ${payment.terminals[0]?.customer_card_no || ''}`,
});
this.store this.store
.submitOrder() .submitOrder()
.pipe( .pipe(
@@ -245,23 +310,15 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
} }
private checkTerminalPayments(): boolean { private checkTerminalPayments(): boolean {
this.form.controls.terminals.controls = this.form.controls.terminals.controls.filter( const pendingTerminalPayments = this.getPendingTerminalPayments();
(control) => control.value, if (pendingTerminalPayments.length) {
);
const terminalPayments = this.form.controls.terminals.controls.map(
(terminal) => terminal.value!,
);
this.selectedPayByTerminalStep.set(terminalPayments.length || 1);
if (terminalPayments.length) {
if (this.nativeBridgeService.isEnabled()) { if (this.nativeBridgeService.isEnabled()) {
for (let i in terminalPayments) { const nextPending = pendingTerminalPayments[0];
const terminal = terminalPayments[i]; this.payByTerminal(nextPending.stepId, nextPending.amount);
this.toastServices.info({ text: 'در حال پردازش پرداخت با دستگاه...', life: 3000 }); this.toastServices.info({
this.payByTerminal(parseInt(i), terminal); text: 'پس از تایید پرداخت هر مرحله، برای مرحله بعد دوباره روی ثبت پرداخت بزنید.',
break; life: 3500,
} });
} else { } else {
this.toastServices.error({ this.toastServices.error({
text: 'امکان ارتباط با دستگاه پرداخت میسر نیست.', text: 'امکان ارتباط با دستگاه پرداخت میسر نیست.',
@@ -278,7 +335,7 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
private payByTerminal(id: number, amount: number) { private payByTerminal(id: number, amount: number) {
const payResult = this.nativeBridgeService.pay({ const payResult = this.nativeBridgeService.pay({
amount, amount,
id: id.toString(), id: String(id),
}); });
// if (!payResult.success) { // if (!payResult.success) {
@@ -288,6 +345,27 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
// } // }
} }
private isTerminalStepPaid(stepId: number): boolean {
return this.payByTerminalSteps().some((step) => step.value === stepId && step.payed);
}
private getMinAllowedSteps() {
const paidSteps = this.payByTerminalSteps()
.slice(0, this.selectedPayByTerminalStep())
.filter((step) => step.payed)
.map((step) => step.value);
return paidSteps.length ? Math.max(...paidSteps) : 1;
}
private getPendingTerminalPayments() {
return this.terminalControls
.map((control, index) => ({
stepId: index + 1,
amount: Number(control.value || 0),
}))
.filter(({ stepId, amount }) => amount > 0 && !this.isTerminalStepPaid(stepId));
}
override onSuccess(response: IPosOrderResponse): void {} override onSuccess(response: IPosOrderResponse): void {}
private extractAmountFromPaymentResult(payload: unknown): number | null { private extractAmountFromPaymentResult(payload: unknown): number | null {
@@ -2,4 +2,5 @@ export * from './customer';
export * from './io'; export * from './io';
export * from './payload'; export * from './payload';
export * from './payment'; export * from './payment';
export * from './posPaymentResult';
export * from './types'; export * from './types';
@@ -1,5 +1,5 @@
export interface IPayment { export interface IPayment {
terminals: number[]; terminals: IPaymentTerminal[];
cash: number; cash: number;
set_off: number; set_off: number;
} }
@@ -0,0 +1,22 @@
export interface PosPaymentResult {
id?: string;
status: 'SUCCESS' | 'FAILURE';
message?: string;
data?: {
terminalId: string;
stan: string;
rrn: string;
responseCode: string;
customerCardNO: string;
transactionDateTime: string;
};
}
export interface TerminalSuccessPaymentPayload {
terminal_id: string;
stan: string;
rrn: string;
transaction_date_time: Date;
customer_card_no: string;
description: string;
}
@@ -1,4 +1,6 @@
import { PosPaymentResult } from '../models';
export abstract class PosPaymentBridgeAbstract { export abstract class PosPaymentBridgeAbstract {
abstract registerPaymentResultListener(handler: (payload: unknown) => void): () => void; abstract registerPaymentResultListener(handler: (payload: PosPaymentResult) => void): () => void;
abstract emitPaymentResultForTest(payload: unknown): void; abstract emitPaymentResultForTest(payload: unknown): void;
} }
@@ -1,33 +1,51 @@
import { ToastService } from '@/core/services/toast.service'; import { ToastService } from '@/core/services/toast.service';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { PosPaymentResult } from '../models';
import { PosPaymentBridgeAbstract } from './payment-bridge.abstract'; import { PosPaymentBridgeAbstract } from './payment-bridge.abstract';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class PosPaymentBridgeService extends PosPaymentBridgeAbstract { export class PosPaymentBridgeService extends PosPaymentBridgeAbstract {
private readonly toastServices = inject(ToastService); private readonly toastServices = inject(ToastService);
registerPaymentResultListener(handler: (payload: unknown) => void): () => void { registerPaymentResultListener(handler: (payload: PosPaymentResult) => void): () => void {
// const w = window as unknown as {
// WebV?: {
// onPaymentResult?: (payload: unknown) => void;
// AndroidBridge?: {
// onPaymentResult?: (payload: unknown) => void;
// };
// };
// };
this.toastServices.error({ text: '@@@@@@@@@@@@@' });
// @ts-ignore // @ts-ignore
window.WebV = { window.WebV = {
onPaymentResult: () => { onPaymentResult: (payload: PosPaymentResult) => {
this.toastServices.error({ text: 'asdasdsadassadasdas' }); try {
// const parsedPayload: PosPaymentResult = JSON.parse(payload);
// this.toastServices.info({ text: payload.data?.customerCardNO || '', life: 10000 });
// this.toastServices.info({
// text: payload.data?.responseCode || 'No response code',
// life: 10000,
// });
// this.toastServices.info({ text: payload.data?.rrn || 'No response code', life: 10000 });
// this.toastServices.info({ text: payload.data?.stan || 'No response code', life: 10000 });
// this.toastServices.info({
// text: payload.data?.terminalId || 'No response code',
// life: 10000,
// });
// this.toastServices.info({
// text: payload.data?.transactionDateTime || 'No response code',
// life: 10000,
// });
handler(payload);
} catch (e) {
this.toastServices.error({ text: 'Failed to parse payment result', life: 10000 });
this.toastServices.error({
text: e instanceof Error ? e.message : String(e),
life: 10000,
});
this.toastServices.error({ text: typeof payload, life: 10000 });
}
}, },
}; };
return () => {}; return () => {};
} }
emitPaymentResultForTest(payload: unknown): void { emitPaymentResultForTest(payload: PosPaymentResult): void {
const w = window as unknown as { WebV?: { onPaymentResult?: (payload: unknown) => void } }; const w = window as unknown as {
WebV?: { onPaymentResult?: (payload: PosPaymentResult) => void };
};
w.WebV?.onPaymentResult?.(payload); w.WebV?.onPaymentResult?.(payload);
} }
} }
@@ -1,5 +1,6 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
// import { ICustomerResponse } from '@/modules/customers/models'; // import { ICustomerResponse } from '@/modules/customers/models';
import { ToastService } from '@/core/services/toast.service';
import { IGoodCategoryResponse, IGoodResponse } from '@/domains/pos/models/good.io'; import { IGoodCategoryResponse, IGoodResponse } from '@/domains/pos/models/good.io';
import { CustomerType } from '@/shared/localEnum/constants/customerTypes'; import { CustomerType } from '@/shared/localEnum/constants/customerTypes';
import { createUUID, JALALI_DATE_FORMATS, nowJalali } from '@/utils'; import { createUUID, JALALI_DATE_FORMATS, nowJalali } from '@/utils';
@@ -55,6 +56,7 @@ export const INITIAL_POS_STATE: IPosLandingState = {
@Injectable({ providedIn: 'any' }) @Injectable({ providedIn: 'any' })
export class PosLandingStore { export class PosLandingStore {
private readonly service = inject(PosService); private readonly service = inject(PosService);
private readonly toastServices = inject(ToastService);
private state$ = signal<IPosLandingState>({ ...INITIAL_POS_STATE }); private state$ = signal<IPosLandingState>({ ...INITIAL_POS_STATE });
@@ -238,6 +240,10 @@ export class PosLandingStore {
total_amount: this.orderPricingInfo().totalAmount, total_amount: this.orderPricingInfo().totalAmount,
}; };
this.toastServices.info({
text: `payment submitted: ${this.payments().terminals[0]?.customer_card_no || ''}`,
});
let res: Maybe<IPosOrderResponse> = null; let res: Maybe<IPosOrderResponse> = null;
return this.service.submitOrder(orderPayload).pipe( return this.service.submitOrder(orderPayload).pipe(
@@ -1,49 +1,51 @@
<div class="w-full min-h-[60svh] p-4 md:p-6"> @defer {
<section class="mx-auto max-w-3xl overflow-hidden rounded-2xl border border-surface-200 bg-white"> <div class="w-full min-h-[60svh] p-4 md:p-6">
<div class="bg-gradient-to-l from-sky-50 via-cyan-50 to-teal-50 p-5 md:p-7"> <section class="mx-auto max-w-3xl overflow-hidden rounded-2xl border border-surface-200 bg-white">
<span class="block mb-4 text-xl font-bold text-surface-900 md:text-3xl">پشتیبانی {{ brandingInfo.title }}</span> <div class="bg-gradient-to-l from-sky-50 via-cyan-50 to-teal-50 p-5 md:p-7">
<p class="mt-3 max-w-2xl text-sm leading-7 text-surface-700 md:text-base"> <span class="block mb-4 text-xl font-bold text-surface-900 md:text-3xl">پشتیبانی {{ brandingInfo.title }}</span>
در صورت بروز مشکل در ثبت سفارش، صدور صورتحساب یا تنظیمات دستگاه، تیم پشتیبانی آماده پاسخگویی است. لطفا هنگام <p class="mt-3 max-w-2xl text-sm leading-7 text-surface-700 md:text-base">
تماس، نام پایانه و نام کاربری را همراه داشته باشید. در صورت بروز مشکل در ثبت سفارش، صدور صورتحساب یا تنظیمات دستگاه، تیم پشتیبانی آماده پاسخگویی است. لطفا هنگام
</p> تماس، نام پایانه و نام کاربری را همراه داشته باشید.
</div> </p>
</div>
<div class="grid gap-3 p-4 md:grid-cols-2 md:gap-4 md:p-6"> <div class="grid gap-3 p-4 md:grid-cols-2 md:gap-4 md:p-6">
<article class="rounded-xl border border-surface-200 bg-surface-50 p-4"> <article class="rounded-xl border border-surface-200 bg-surface-50 p-4">
<span class="block mb-4 text-xl font-bold text-surface-900">شماره های تماس مستقیم</span> <span class="block mb-4 text-xl font-bold text-surface-900">شماره های تماس مستقیم</span>
@if (brandingInfo!.support.phones?.length) { @if (brandingInfo!.support.phones?.length) {
<ul class="space-y-2 text-sm text-surface-700"> <ul class="space-y-2 text-sm text-surface-700">
@for (phone of brandingInfo.support.phones!; track $index) { @for (phone of brandingInfo.support.phones!; track $index) {
<li class="flex items-start justify-between gap-2"> <li class="flex items-start justify-between gap-2">
<span>{{ phone.title }}</span> <span>{{ phone.title }}</span>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@for (item of phone.phones; track $index) { @for (item of phone.phones; track $index) {
<a class="font-semibold text-primary" href="tel:{{ item }}" dir="ltr">{{ item }}</a> <a class="font-semibold text-primary" href="tel:{{ item }}" dir="ltr">{{ item }}</a>
} }
</div> </div>
</li> </li>
} }
</ul>
} @else {
<p class="text-sm text-surface-700">در حال حاضر شماره تماس مستقیمی برای پشتیبانی اعلام نشده است.</p>
}
</article>
<article class="rounded-xl border border-surface-200 bg-surface-50 p-4">
<span class="block mb-4 text-xl font-bold text-surface-900">راهنمای سریع قبل از تماس</span>
<ul class="space-y-2 text-sm leading-6 text-surface-700">
<li>اتصال اینترنت دستگاه را بررسی کنید.</li>
<li>یک بار برنامه را ببندید و دوباره اجرا کنید.</li>
<li>در صفحه درباره نرم افزار، نسخه برنامه را اعلام کنید.</li>
<li>در صورت خطا، متن خطا یا تصویر آن را برای پشتیبانی ارسال کنید.</li>
</ul> </ul>
} @else { </article>
<p class="text-sm text-surface-700">در حال حاضر شماره تماس مستقیمی برای پشتیبانی اعلام نشده است.</p> </div>
}
</article>
<article class="rounded-xl border border-surface-200 bg-surface-50 p-4"> <div class="border-t border-surface-200 bg-surface-50 px-4 py-4 md:px-6">
<span class="block mb-4 text-xl font-bold text-surface-900">راهنمای سریع قبل از تماس</span> <p class="text-xs leading-6 text-surface-600 md:text-sm">
<ul class="space-y-2 text-sm leading-6 text-surface-700"> ساعات پاسخگویی: شنبه تا چهارشنبه 8:30 تا 17:30 و پنجشنبه 8:30 تا 13:00
<li>اتصال اینترنت دستگاه را بررسی کنید.</li> </p>
<li>یک بار برنامه را ببندید و دوباره اجرا کنید.</li> </div>
<li>در صفحه درباره نرم افزار، نسخه برنامه را اعلام کنید.</li> </section>
<li>در صورت خطا، متن خطا یا تصویر آن را برای پشتیبانی ارسال کنید.</li> </div>
</ul> }
</article>
</div>
<div class="border-t border-surface-200 bg-surface-50 px-4 py-4 md:px-6">
<p class="text-xs leading-6 text-surface-600 md:text-sm">
ساعات پاسخگویی: شنبه تا چهارشنبه 8:30 تا 17:30 و پنجشنبه 8:30 تا 13:00
</p>
</div>
</section>
</div>
+2 -2
View File
@@ -1,8 +1,8 @@
// TIS tenant environment configuration // TIS tenant environment configuration
export const environment = { export const environment = {
production: true, production: true,
// apiBaseUrl: 'https://psp-api.shift-am.ir', apiBaseUrl: 'https://psp-api.shift-am.ir',
apiBaseUrl: 'http://192.168.128.73:5002', // apiBaseUrl: 'http://192.168.128.73:5002',
host: 'localhost', host: 'localhost',
port: 5000, port: 5000,
enableLogging: false, enableLogging: false,