feat: implement change password and send to fiscal activation modules with forms and services

This commit is contained in:
2026-05-24 10:44:35 +03:30
parent 12752f37d5
commit f18d7a1f04
20 changed files with 247 additions and 117 deletions
@@ -43,12 +43,12 @@ export class PosMainMenuSidebarComponent extends AbstractDialog {
icon: 'pi pi-fw pi-home', icon: 'pi pi-fw pi-home',
}, },
{ {
label: 'فروش', label: 'ایجاد صورت‌حساب',
routerLink: PosShopNamedRoutes.shop.meta.pagePath!(), routerLink: PosShopNamedRoutes.shop.meta.pagePath!(),
icon: 'pi pi-fw pi-cart-arrow-down', icon: 'pi pi-fw pi-cart-arrow-down',
}, },
{ {
label: 'فاکتورها', label: 'صورت‌حساب‌ها',
routerLink: posSaleInvoicesNamedRoutes.saleInvoices.meta.pagePath!(), routerLink: posSaleInvoicesNamedRoutes.saleInvoices.meta.pagePath!(),
icon: 'pi pi-fw pi-receipt', icon: 'pi pi-fw pi-receipt',
}, },
@@ -0,0 +1,11 @@
<app-card-data cardTitle="تغییر گذرواژه" class="w-full">
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<shared-password-input
[passwordControl]="form.controls.password"
[confirmPasswordControl]="form.controls.confirmPassword" />
<div class="flex w-full justify-end">
<button pButton label="ذخیره گذرواژه" [loading]="submitLoading()" class="w-full max-w-40"></button>
</div>
</form>
</app-card-data>
@@ -0,0 +1,51 @@
import { MustMatch } from '@/core/validators';
import { AbstractForm } from '@/shared/abstractClasses';
import { AppCardComponent } from '@/shared/components';
import { SharedPasswordInputComponent } from '@/shared/components/passwordInput/password-input.component';
import { fieldControl } from '@/shared/constants';
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { ButtonDirective } from 'primeng/button';
import { tap } from 'rxjs';
import { IChangePasswordSubmitPayload } from './model';
import { PosChangePasswordService } from './services/main.service';
@Component({
selector: 'pos-change-password-form',
templateUrl: './form.component.html',
imports: [
ReactiveFormsModule,
SharedPasswordInputComponent,
CommonModule,
ButtonDirective,
AppCardComponent,
],
})
export class PosChangePasswordFormDialogComponent extends AbstractForm<
IChangePasswordSubmitPayload,
any
> {
private readonly changePasswordService = inject(PosChangePasswordService);
form = this.fb.group(
{
password: fieldControl.password(),
confirmPassword: fieldControl.confirmPassword(),
},
{
validators: [MustMatch('password', 'confirmPassword')],
}
);
override submitForm() {
const payload = this.form.getRawValue().password!;
return this.changePasswordService.changePassword({ password: payload }).pipe(
tap({
next: (res) => {
this.form.reset();
},
})
);
}
}
@@ -0,0 +1,3 @@
export interface IChangePasswordSubmitPayload {
password: string;
}
@@ -0,0 +1,15 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { IChangePasswordSubmitPayload } from '../model';
@Injectable({
providedIn: 'root',
})
export class PosChangePasswordService {
constructor(private readonly http: HttpClient) {}
changePassword(payload: IChangePasswordSubmitPayload): Observable<unknown> {
return this.http.put('/api/v1/pos/update-password', payload);
}
}
@@ -1,9 +1,7 @@
<app-card-data cardTitle="قیمت پیش‌فرض هر گرم طلا" class="w-full"> <app-input
<p-message variant="text" severity="info" icon="pi pi-info-circle"> [control]="goldPrice"
در این بخش می‌توانید قیمت پیش‌فرض هر گرم طلا را وارد کنید. label="قیمت پیش‌فرض هر گرم طلا"
</p-message> name="goldBasePrice"
<form [formGroup]="form" class="mt-6" (submit)="submit()"> type="price"
<field-unit-price [control]="form.controls.price" /> [min]="0"
<app-form-footer-actions (onCancel)="close()" /> hint="در این بخش می‌توانید قیمت پیش‌فرض هر گرم طلا را وارد کنید." />
</form>
</app-card-data>
@@ -1,53 +1,31 @@
import { AbstractForm } from '@/shared/abstractClasses'; import { InputComponent } from '@/shared/components';
import { AppCardComponent, UnitPriceComponent } from '@/shared/components'; import { Component, inject } from '@angular/core';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component'; import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { fieldControl } from '@/shared/constants'; import { debounce, timer } from 'rxjs';
import { Component, inject, signal } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { Message } from 'primeng/message';
import { IPosConfigGoldPricePayload, IPosConfigGoldPriceResponse } from './models';
import { PosConfigGoldPriceService } from './services/main.service'; import { PosConfigGoldPriceService } from './services/main.service';
@Component({ @Component({
selector: 'pos-config-gold-price-form', selector: 'pos-config-gold-price-form',
templateUrl: 'form.component.html', templateUrl: 'form.component.html',
imports: [ imports: [ReactiveFormsModule, InputComponent],
ReactiveFormsModule,
FormFooterActionsComponent,
AppCardComponent,
Message,
UnitPriceComponent,
],
}) })
export class PosConfigGoldPriceFormComponent extends AbstractForm< export class PosConfigGoldPriceFormComponent {
IPosConfigGoldPricePayload,
IPosConfigGoldPriceResponse
> {
private readonly service = inject(PosConfigGoldPriceService); private readonly service = inject(PosConfigGoldPriceService);
private readonly fb = inject(FormBuilder);
loading = signal(true); goldPrice = this.fb.control(0, { nonNullable: true });
initForm = () => { ngOnInit() {
const form = this.fb.group({ const initialValues = this.service.get();
price: fieldControl.unit_price(), this.goldPrice.setValue(initialValues);
this.goldPrice.valueChanges.pipe(debounce(() => timer(300))).subscribe((value) => {
this.submit();
}); });
return form;
};
form = this.initForm();
override async ngOnInit() {
this.loading.set(true);
const initialValues = await this.service.get();
this.form.controls.price.setValue((initialValues || 0) + '');
this.loading.set(false);
} }
override submitForm() { submit() {
const formValue = this.form.value.price as IPosConfigGoldPricePayload; const formValue = this.goldPrice.value;
this.toastService.success({ text: 'تغییرات با موفقیت اعمال شد.' });
return this.service.submit(formValue); return this.service.submit(formValue);
} }
} }
@@ -1,12 +1,9 @@
<app-card-data cardTitle="ثبت سریع صورت‌حساب" class="w-full"> <div class="gap-4">
<p-message variant="text" severity="info" icon="pi pi-info-circle"> <div class="flex items-center justify-between">
در صورتی که فروش شما تک محصولی / خدماتی است، این قابلیت را فعال کنید.
</p-message>
<form [formGroup]="form" class="mt-6" (submit)="submit()">
<div class="flex items-center gap-4">
<uikit-label name="rapidInvoice"> ثبت سریع صورت‌حساب </uikit-label> <uikit-label name="rapidInvoice"> ثبت سریع صورت‌حساب </uikit-label>
<p-toggleSwitch [formControl]="form.controls.rapidInvoice" /> <p-toggleSwitch [formControl]="rapidInvoice" />
</div> </div>
<app-form-footer-actions (onCancel)="close()" /> <small id="rapidInvoice-help" class="text-muted-color mt-2 block">
</form> در صورتی که فروش شما تک محصولی / خدماتی است، این قابلیت را فعال کنید.
</app-card-data> </small>
</div>
@@ -1,55 +1,33 @@
import { AbstractForm } from '@/shared/abstractClasses';
import { AppCardComponent } from '@/shared/components';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { UikitLabelComponent } from '@/uikit'; import { UikitLabelComponent } from '@/uikit';
import { Component, inject, signal } from '@angular/core'; import { Component, inject } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms'; import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { Message } from 'primeng/message';
import { ToggleSwitch } from 'primeng/toggleswitch'; import { ToggleSwitch } from 'primeng/toggleswitch';
import { IPosConfigRapidInvoicePayload, IPosConfigRapidInvoiceResponse } from './models'; import { debounce, timer } from 'rxjs';
import { PosConfigRapidInvoiceService } from './services/main.service'; import { PosConfigRapidInvoiceService } from './services/main.service';
@Component({ @Component({
selector: 'pos-config-rapid-invoice-form', selector: 'pos-config-rapid-invoice-form',
templateUrl: 'form.component.html', templateUrl: 'form.component.html',
imports: [ imports: [ReactiveFormsModule, UikitLabelComponent, ToggleSwitch],
ReactiveFormsModule,
FormFooterActionsComponent,
AppCardComponent,
Message,
UikitLabelComponent,
ToggleSwitch,
],
}) })
export class PosConfigRapidInvoiceFormComponent extends AbstractForm< export class PosConfigRapidInvoiceFormComponent {
IPosConfigRapidInvoicePayload,
IPosConfigRapidInvoiceResponse
> {
private readonly service = inject(PosConfigRapidInvoiceService); private readonly service = inject(PosConfigRapidInvoiceService);
loading = signal(true); private readonly fb = inject(FormBuilder);
initForm = () => { rapidInvoice = this.fb.control(false, { nonNullable: true });
const form = this.fb.group({
rapidInvoice: [false], ngOnInit() {
const initialValues = this.service.get();
this.rapidInvoice.setValue(initialValues);
this.rapidInvoice.valueChanges.pipe(debounce(() => timer(300))).subscribe((value) => {
this.submit();
}); });
return form;
};
form = this.initForm();
override async ngOnInit() {
this.loading.set(true);
const initialValues = await this.service.get();
this.form.controls.rapidInvoice.setValue(initialValues || false);
this.loading.set(false);
} }
override submitForm() { submit() {
const formValue = this.form.value.rapidInvoice as IPosConfigRapidInvoicePayload; const formValue = this.rapidInvoice.value;
this.toastService.success({ text: 'تغییرات با موفقیت اعمال شد.' });
return this.service.submit(formValue); return this.service.submit(formValue);
} }
} }
@@ -0,0 +1,9 @@
<div class="gap-4">
<div class="flex items-center justify-between">
<uikit-label name="sendToFiscalActivation"> ارسال سریع صورت‌حساب به مالیات </uikit-label>
<p-toggleSwitch [formControl]="sendToFiscalActivation" />
</div>
<small id="sendToFiscalActivation-help" class="text-muted-color mt-2 block">
در صورتی که می‌خوا‌هید همزمان با ساخت صورت‌حساب، اطلاعات آن به سازمان مالیاتی ارسال شود این قابلیت را فعال کنید.
</small>
</div>
@@ -0,0 +1,33 @@
import { UikitLabelComponent } from '@/uikit';
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { ToggleSwitch } from 'primeng/toggleswitch';
import { debounce, timer } from 'rxjs';
import { PosConfigSendToFiscalActivationService } from './services/main.service';
@Component({
selector: 'pos-config-send-to-fiscal-activation-form',
templateUrl: 'form.component.html',
imports: [ReactiveFormsModule, UikitLabelComponent, ToggleSwitch],
})
export class PosConfigSendToFiscalActivationFormComponent {
private readonly service = inject(PosConfigSendToFiscalActivationService);
private readonly fb = inject(FormBuilder);
sendToFiscalActivation = this.fb.control(false, { nonNullable: true });
ngOnInit() {
const initialValues = this.service.get();
this.sendToFiscalActivation.setValue(initialValues);
this.sendToFiscalActivation.valueChanges.pipe(debounce(() => timer(300))).subscribe((value) => {
this.submit();
});
}
submit() {
const formValue = this.sendToFiscalActivation.value;
return this.service.submit(formValue);
}
}
@@ -0,0 +1,3 @@
export type IPosConfigSendToFiscalActivationResponse = boolean;
export type IPosConfigSendToFiscalActivationPayload = boolean;
@@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import { LOCAL_STORAGE_KEYS } from 'src/assets/constants';
import {
IPosConfigSendToFiscalActivationPayload,
IPosConfigSendToFiscalActivationResponse,
} from '../models';
@Injectable({ providedIn: 'root' })
export class PosConfigSendToFiscalActivationService {
get(): IPosConfigSendToFiscalActivationResponse {
const defaultPrice = Boolean(
window.localStorage.getItem(LOCAL_STORAGE_KEYS.POS_CONFIG_SEND_TO_FISCAL_ACTIVATION) ===
'true'
);
this.submit(defaultPrice);
return defaultPrice;
}
submit(data: IPosConfigSendToFiscalActivationPayload) {
window.localStorage.setItem(
LOCAL_STORAGE_KEYS.POS_CONFIG_SEND_TO_FISCAL_ACTIVATION,
data.toString()
);
}
}
@@ -1,9 +1,17 @@
<div class="flex h-full w-full flex-col items-center justify-center gap-6 p-4"> <div class="flex h-full w-full flex-col items-center justify-center gap-6 p-4">
<app-card-data cardTitle="تنظیمات صدور صورت‌حساب" class="w-full">
<div class="flex w-full flex-col gap-4">
@if (info()?.guild!.code.toLocaleLowerCase() === 'gold') { @if (info()?.guild!.code.toLocaleLowerCase() === 'gold') {
<pos-config-gold-price-form class="w-full" (onClose)="returnToMainPage()" /> <pos-config-gold-price-form class="w-full" />
<hr />
} }
<pos-config-rapid-invoice-form class="w-full" (onClose)="returnToMainPage()" /> <pos-config-rapid-invoice-form class="w-full" />
<app-card-data cardTitle="تنظیمات فاکتور" class="w-full"> <hr />
<pos-config-send-to-fiscal-activation-form class="w-full" />
</div>
</app-card-data>
<pos-change-password-form class="w-full" />
<app-card-data cardTitle="تنظیمات پرینت صورت‌حساب" class="w-full">
<p-message variant="text" severity="info" icon="pi pi-info-circle"> <p-message variant="text" severity="info" icon="pi pi-info-circle">
هریک از موارد زیر را که می‌خواهید در چاپ فاکتور نمایش داده‌ شوند را انتخاب کنید هریک از موارد زیر را که می‌خواهید در چاپ فاکتور نمایش داده‌ شوند را انتخاب کنید
</p-message> </p-message>
@@ -3,9 +3,11 @@ import { AppCardComponent } from '@/shared/components';
import { Component, computed, inject } from '@angular/core'; import { Component, computed, inject } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Message } from 'primeng/message'; import { Message } from 'primeng/message';
import { PosChangePasswordFormDialogComponent } from '../components/changePassword/form.component';
import { PosConfigGoldPriceFormComponent } from '../components/goldPrice/form.component'; import { PosConfigGoldPriceFormComponent } from '../components/goldPrice/form.component';
import { PosConfigPrintFormComponent } from '../components/print/form.component'; import { PosConfigPrintFormComponent } from '../components/print/form.component';
import { PosConfigRapidInvoiceFormComponent } from '../components/rapidInvoice/form.component'; import { PosConfigRapidInvoiceFormComponent } from '../components/rapidInvoice/form.component';
import { PosConfigSendToFiscalActivationFormComponent } from '../components/sendToFiscalActivation/form.component';
@Component({ @Component({
selector: 'pos-config-page', selector: 'pos-config-page',
@@ -16,6 +18,8 @@ import { PosConfigRapidInvoiceFormComponent } from '../components/rapidInvoice/f
Message, Message,
PosConfigGoldPriceFormComponent, PosConfigGoldPriceFormComponent,
PosConfigRapidInvoiceFormComponent, PosConfigRapidInvoiceFormComponent,
PosConfigSendToFiscalActivationFormComponent,
PosChangePasswordFormDialogComponent,
], ],
}) })
export class PosConfigPageComponent { export class PosConfigPageComponent {
@@ -4,10 +4,17 @@ import { InputComponent } from '../input/input.component';
@Component({ @Component({
selector: 'field-unit-price', selector: 'field-unit-price',
template: `<app-input label="قیمت پایه" [control]="control" [name]="name" type="price" />`, template: `<app-input
label="قیمت پایه"
[control]="control"
[name]="name"
[hint]="hint"
type="price"
/>`,
imports: [ReactiveFormsModule, InputComponent], imports: [ReactiveFormsModule, InputComponent],
}) })
export class UnitPriceComponent { export class UnitPriceComponent {
@Input({ required: true }) control = new FormControl<string>(''); @Input({ required: true }) control = new FormControl<string>('');
@Input() name = 'unit_price'; @Input() name = 'unit_price';
@Input() hint = '';
} }
@@ -1,7 +1,7 @@
<uikit-field [label]="label" [name]="name" [control]="control" [showLabel]="!!label" [showErrors]="showErrors"> <uikit-field [label]="label" [name]="name" [control]="control" [showLabel]="!!label" [showErrors]="showErrors">
@if (labelSuffix) { @if (labelSuffix) {
<ng-template #labelView> <ng-template #labelView>
<div class="flex gap-1 items-center"> <div class="flex items-center gap-1">
<uikit-label [name]="name"> <uikit-label [name]="name">
@if (labelTextView) { @if (labelTextView) {
<ng-container [ngTemplateOutlet]="labelTextView"></ng-container> <ng-container [ngTemplateOutlet]="labelTextView"></ng-container>
@@ -13,12 +13,12 @@
</div> </div>
</ng-template> </ng-template>
} }
@if (type === "switch") { @if (type === 'switch') {
<p-toggleSwitch [attr.disabled]="disabled" [formControl]="control" /> <p-toggleSwitch [attr.disabled]="disabled" [formControl]="control" />
} @else { } @else {
<p-inputgroup> <p-inputgroup>
<!-- [minlength]="minlength || null" --> <!-- [minlength]="minlength || null" -->
@if (type === "price") { @if (type === 'price') {
<p-inputnumber <p-inputnumber
[attr.id]="name" [attr.id]="name"
[formControl]="control" [formControl]="control"
@@ -28,12 +28,11 @@
[attr.type]="htmlType" [attr.type]="htmlType"
[attr.autocomplete]="autocomplete" [attr.autocomplete]="autocomplete"
[attr.disabled]="disabled" [attr.disabled]="disabled"
[class]="` w-full ${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`" [class]="`w-full ${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`"
[required]="isRequired" [required]="isRequired"
[invalid]="control.invalid && (control.touched || control.dirty)" [invalid]="control.invalid && (control.touched || control.dirty)"
(onInput)="onPriceInput($event)" (onInput)="onPriceInput($event)"
(onBlur)="blur.emit()" (onBlur)="blur.emit()" />
/>
<!-- (input)="onInput($event)" --> <!-- (input)="onInput($event)" -->
} @else { } @else {
<input <input
@@ -58,8 +57,7 @@
[required]="isRequired" [required]="isRequired"
[invalid]="control.invalid && (control.touched || control.dirty)" [invalid]="control.invalid && (control.touched || control.dirty)"
(onBlur)="blur.emit()" (onBlur)="blur.emit()"
(input)="onInput($event)" (input)="onInput($event)" />
/>
} }
@if (suffixTemp) { @if (suffixTemp) {
<ng-container [ngTemplateOutlet]="suffixTemp" /> <ng-container [ngTemplateOutlet]="suffixTemp" />
@@ -69,6 +67,6 @@
</p-inputgroup> </p-inputgroup>
} }
@if (hint) { @if (hint) {
<small [attr.id]="name + '-help'">{{ hint }}</small> <small [attr.id]="name + '-help'" class="text-muted-color">{{ hint }}</small>
} }
</uikit-field> </uikit-field>
@@ -5,6 +5,7 @@ export enum LOCAL_STORAGE_KEYS {
LAST_LOGIN_TIME = 'lastLoginTime', LAST_LOGIN_TIME = 'lastLoginTime',
LOGIN_ATTEMPTS = 'loginAttempts', LOGIN_ATTEMPTS = 'loginAttempts',
POS_CONFIG_RAPID_INVOICE = 'posConfigRapidInvoice', POS_CONFIG_RAPID_INVOICE = 'posConfigRapidInvoice',
POS_CONFIG_SEND_TO_FISCAL_ACTIVATION = 'posConfigSendToFiscalActivation',
POS_CONFIG_PRINT = 'posConfigPrint', POS_CONFIG_PRINT = 'posConfigPrint',
POS_CONFIG_GOLD_PRICE = 'posConfigGoldPrice', POS_CONFIG_GOLD_PRICE = 'posConfigGoldPrice',
} }
+13 -3
View File
@@ -15,17 +15,27 @@ form,
gap: 1rem; gap: 1rem;
} }
body.tenant-tis .p-drawer { body.application-pos .p-drawer {
will-change: transform; will-change: transform;
transform: translateZ(0); transform: translateZ(0);
backface-visibility: hidden; backface-visibility: hidden;
box-shadow: none !important;
} }
body.tenant-tis .p-drawer-mask { body.application-pos .p-drawer-mask {
will-change: opacity; will-change: opacity;
} }
body.tenant-tis .p-drawer .p-drawer-content { body.application-pos .p-drawer .p-drawer-content {
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
overscroll-behavior: contain; overscroll-behavior: contain;
} }
body.application-pos .p-card {
box-shadow: none !important;
border: 1px solid var(--surface-border) !important;
}
body.application-pos .p-select {
box-shadow: none !important;
}
+1 -1
View File
@@ -32,7 +32,7 @@ if (themeMeta) {
} }
if (config.isPosApplication) { if (config.isPosApplication) {
document.body.classList.add('tenant-tis'); document.body.classList.add('application-pos');
} }
bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)); bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err));