feat: add consumer profile management; implement profile form, reset password functionality, and API integration
This commit is contained in:
+24
-2
@@ -1,9 +1,31 @@
|
||||
import { IEnumTranslate } from '@/shared/models/enum_translate.type';
|
||||
|
||||
export interface IConsumerInfoRawResponse {
|
||||
id: string;
|
||||
partner: Partner;
|
||||
type: IEnumTranslate<'INDIVIDUAL' | 'LEGAL'>;
|
||||
status: IEnumTranslate<'ACTIVE' | 'INACTIVE'>;
|
||||
legal?: Legal;
|
||||
individual?: Individual;
|
||||
name: string;
|
||||
}
|
||||
export interface IConsumerInfoResponse extends IConsumerInfoRawResponse {}
|
||||
|
||||
interface Individual {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
mobile_number: string;
|
||||
status: string;
|
||||
national_code: string;
|
||||
fullname: string;
|
||||
}
|
||||
export interface IConsumerInfoResponse extends IConsumerInfoRawResponse {}
|
||||
|
||||
interface Legal {
|
||||
company_name: string;
|
||||
registration_number: string;
|
||||
}
|
||||
|
||||
interface Partner {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<shared-dialog
|
||||
header="ویرایش اطلاعات پروفایل"
|
||||
[(visible)]="visible"
|
||||
[modal]="true"
|
||||
[style]="{ width: '500px' }"
|
||||
[closable]="true"
|
||||
(onHide)="close()"
|
||||
>
|
||||
<form [formGroup]="form" (submit)="submit()">
|
||||
@if (profileType === "LEGAL") {
|
||||
<field-company-name [control]="form.controls.company_name" />
|
||||
<field-registration-number [control]="form.controls.registration_number" />
|
||||
} @else {
|
||||
<field-first-name [control]="form.controls.first_name" />
|
||||
<field-last-name [control]="form.controls.last_name" />
|
||||
<field-mobile-number [control]="form.controls.mobile_number" />
|
||||
<field-national-code [control]="form.controls.national_code" />
|
||||
}
|
||||
<app-form-footer-actions [loading]="submitLoading()" (onCancel)="close()" />
|
||||
</form>
|
||||
</shared-dialog>
|
||||
@@ -0,0 +1,76 @@
|
||||
import { AbstractFormDialog } from '@/shared/abstractClasses';
|
||||
import {
|
||||
CompanyNameComponent,
|
||||
FirstNameComponent,
|
||||
LastNameComponent,
|
||||
MobileNumberComponent,
|
||||
NationalCodeComponent,
|
||||
RegistrationNumberComponent,
|
||||
SharedDialogComponent,
|
||||
} from '@/shared/components';
|
||||
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
|
||||
import { fieldControl } from '@/shared/constants';
|
||||
import { Component, inject, Input } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { IProfileRequestPayload, IProfileResponse } from '../models';
|
||||
import { ProfileService } from '../services/main.service';
|
||||
|
||||
@Component({
|
||||
selector: 'consumer-profile-form',
|
||||
templateUrl: 'form.component.html',
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
FormFooterActionsComponent,
|
||||
FirstNameComponent,
|
||||
LastNameComponent,
|
||||
NationalCodeComponent,
|
||||
MobileNumberComponent,
|
||||
CompanyNameComponent,
|
||||
RegistrationNumberComponent,
|
||||
SharedDialogComponent,
|
||||
],
|
||||
})
|
||||
export class ConsumerProfileFormComponent extends AbstractFormDialog<
|
||||
IProfileRequestPayload,
|
||||
IProfileResponse,
|
||||
IProfileRequestPayload
|
||||
> {
|
||||
@Input({ required: true }) profileType!: 'INDIVIDUAL' | 'LEGAL';
|
||||
|
||||
private readonly service = inject(ProfileService);
|
||||
|
||||
initForm = () => {
|
||||
const form = this.fb.group({
|
||||
first_name: fieldControl.first_name(this.initialValues?.first_name),
|
||||
last_name: fieldControl.last_name(this.initialValues?.last_name),
|
||||
mobile_number: fieldControl.mobile_number(this.initialValues?.mobile_number),
|
||||
national_code: fieldControl.national_code(this.initialValues?.national_code),
|
||||
company_name: fieldControl.company_name(this.initialValues?.company_name),
|
||||
registration_number: fieldControl.registration_number(
|
||||
this.initialValues?.registration_number,
|
||||
),
|
||||
});
|
||||
|
||||
if (this.profileType === 'LEGAL') {
|
||||
// @ts-ignore
|
||||
form.removeControl('first_name');
|
||||
// @ts-ignore
|
||||
form.removeControl('last_name');
|
||||
// @ts-ignore
|
||||
form.removeControl('mobile_number');
|
||||
// @ts-ignore
|
||||
form.removeControl('national_code');
|
||||
} else {
|
||||
// @ts-ignore
|
||||
form.removeControl('company_name');
|
||||
// @ts-ignore
|
||||
form.removeControl('registration_number');
|
||||
}
|
||||
return form;
|
||||
};
|
||||
override form = this.initForm();
|
||||
|
||||
override submitForm(payload: IProfileRequestPayload) {
|
||||
return this.service.updateProfile(payload);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<app-card-data cardTitle="تغییر گذرواژه" [editable]="false">
|
||||
<form [formGroup]="form" (submit)="submit()" class="max-w-lg mx-auto">
|
||||
<shared-password-input
|
||||
[passwordControl]="form.controls.password"
|
||||
[confirmPasswordControl]="form.controls.confirmPassword"
|
||||
/>
|
||||
<button type="submit" pButton [disabled]="form.invalid || loading()" class="w-full max-w-xs mx-auto">
|
||||
تغییر گذرواژه
|
||||
</button>
|
||||
</form>
|
||||
</app-card-data>
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ToastService } from '@/core/services/toast.service';
|
||||
import { MustMatch } from '@/core/validators';
|
||||
import { AppCardComponent, SharedPasswordInputComponent } from '@/shared/components';
|
||||
import { fieldControl } from '@/shared/constants';
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { finalize } from 'rxjs';
|
||||
import { ProfileService } from '../services/main.service';
|
||||
|
||||
@Component({
|
||||
selector: 'consumer-reset-password-card',
|
||||
templateUrl: './reset-password-card.component.html',
|
||||
imports: [ReactiveFormsModule, AppCardComponent, SharedPasswordInputComponent, ButtonDirective],
|
||||
})
|
||||
export class ConsumerResetPasswordCardComponent {
|
||||
private readonly service = inject(ProfileService);
|
||||
private readonly fb = inject(FormBuilder);
|
||||
private readonly toastService = inject(ToastService);
|
||||
|
||||
readonly loading = signal(false);
|
||||
|
||||
form = this.fb.group(
|
||||
{
|
||||
password: fieldControl.password(),
|
||||
confirmPassword: fieldControl.confirmPassword(),
|
||||
},
|
||||
{ validators: [MustMatch('password', 'confirmPassword')] },
|
||||
);
|
||||
|
||||
submit() {
|
||||
if (this.form.invalid) {
|
||||
this.form.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading.set(true);
|
||||
this.service
|
||||
.resetPassword({ password: this.form.value.password as string })
|
||||
.pipe(finalize(() => this.loading.set(false)))
|
||||
.subscribe(() => {
|
||||
this.form.reset();
|
||||
this.toastService.success({ text: 'گذرواژه با موفقیت بهروز شد.' });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const baseUrl = '/api/v1/consumer';
|
||||
|
||||
export const CONSUMER_PROFILE_API_ROUTES = {
|
||||
info: () => `${baseUrl}`,
|
||||
updateInfo: () => `${baseUrl}`,
|
||||
resetPassword: () => `${baseUrl}/update-password`,
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './apiRoutes';
|
||||
export * from './routes';
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NamedRoutes } from '@/core';
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
export type TConsumerProfileRouteNames = 'profile';
|
||||
|
||||
export const consumerProfileNamedRoutes: NamedRoutes<TConsumerProfileRouteNames> = {
|
||||
profile: {
|
||||
path: 'profile',
|
||||
loadComponent: () => import('../../views/single.component').then((m) => m.ConsumerProfileComponent),
|
||||
meta: {
|
||||
title: 'پروفایل',
|
||||
pagePath: () => '/consumer/profile',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CONSUMER_PROFILE_ROUTES: Routes = [consumerProfileNamedRoutes.profile];
|
||||
@@ -0,0 +1 @@
|
||||
export * from './io.d';
|
||||
@@ -0,0 +1,16 @@
|
||||
import { IConsumerInfoResponse } from '@/domains/consumer/models';
|
||||
|
||||
export interface IProfileResponse extends IConsumerInfoResponse {}
|
||||
|
||||
export interface IProfileRequestPayload {
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
mobile_number?: string;
|
||||
national_code?: string;
|
||||
company_name?: string;
|
||||
registration_number?: string;
|
||||
}
|
||||
|
||||
export interface IResetPasswordRequest {
|
||||
password: string;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { CONSUMER_PROFILE_API_ROUTES } from '../constants';
|
||||
import { IProfileRequestPayload, IProfileResponse, IResetPasswordRequest } from '../models';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ProfileService {
|
||||
constructor(private readonly http: HttpClient) {}
|
||||
|
||||
getInfo(): Observable<IProfileResponse> {
|
||||
return this.http.get<IProfileResponse>(CONSUMER_PROFILE_API_ROUTES.info());
|
||||
}
|
||||
|
||||
updateProfile(payload: IProfileRequestPayload): Observable<IProfileResponse> {
|
||||
return this.http.patch<IProfileResponse>(CONSUMER_PROFILE_API_ROUTES.updateInfo(), payload);
|
||||
}
|
||||
|
||||
resetPassword(payload: IResetPasswordRequest): Observable<unknown> {
|
||||
return this.http.put(CONSUMER_PROFILE_API_ROUTES.resetPassword(), payload);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<div class="flex flex-col gap-4">
|
||||
<app-card-data cardTitle="اطلاعات پروفایل" [editable]="true" [(editMode)]="editing">
|
||||
@if (isLegal()) {
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<app-key-value label="نام شرکت" [value]="profile()?.legal?.company_name || '-'" />
|
||||
<app-key-value label="شماره ثبت" [value]="profile()?.legal?.registration_number" />
|
||||
</div>
|
||||
} @else {
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<app-key-value label="نام" [value]="profile()?.individual?.first_name || '-'" />
|
||||
<app-key-value label="نام خانوادگی" [value]="profile()?.individual?.last_name || '-'" />
|
||||
<app-key-value label="کد ملی" [value]="profile()?.individual?.national_code" />
|
||||
<app-key-value label="شماره موبایل" [value]="profile()?.individual?.mobile_number" />
|
||||
</div>
|
||||
}
|
||||
</app-card-data>
|
||||
|
||||
<consumer-reset-password-card />
|
||||
|
||||
@if (editing()) {
|
||||
<consumer-profile-form
|
||||
[(visible)]="editing"
|
||||
[profileType]="profile()!.type!.value"
|
||||
[initialValues]="profile()?.legal || profile()?.individual"
|
||||
(onSubmit)="onEditSuccess()"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ConsumerStore } from '@/domains/consumer/store/main.store';
|
||||
import { AppCardComponent, KeyValueComponent } from '@/shared/components';
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { ConsumerProfileFormComponent } from '../components/form.component';
|
||||
import { ConsumerResetPasswordCardComponent } from '../components/reset-password-card.component';
|
||||
|
||||
@Component({
|
||||
selector: 'consumer-profile',
|
||||
templateUrl: './single.component.html',
|
||||
imports: [
|
||||
AppCardComponent,
|
||||
KeyValueComponent,
|
||||
ConsumerResetPasswordCardComponent,
|
||||
ConsumerProfileFormComponent,
|
||||
],
|
||||
})
|
||||
export class ConsumerProfileComponent {
|
||||
private readonly store = inject(ConsumerStore);
|
||||
|
||||
readonly profile = computed(() => this.store.entity());
|
||||
readonly isLegal = computed(() => this.profile()?.type?.value === 'LEGAL');
|
||||
|
||||
editing = signal(false);
|
||||
|
||||
edit() {
|
||||
this.editing.set(true);
|
||||
}
|
||||
|
||||
onEditSuccess() {
|
||||
this.store.getData();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { CONSUMER_ACCOUNTS_ROUTES } from './modules/accounts/constants';
|
||||
import { CONSUMER_BUSINESS_ACTIVITIES_ROUTES } from './modules/businessActivities/constants';
|
||||
import { CONSUMER_CUSTOMERS_ROUTES } from './modules/customers/constants';
|
||||
import { CONSUMER_POSES_ROUTES } from './modules/poses/constants';
|
||||
import { CONSUMER_PROFILE_ROUTES } from './modules/profile/constants';
|
||||
import { CONSUMER_SALE_INVOICES_ROUTES } from './modules/saleInvoices/constants';
|
||||
|
||||
export const CONSUMER_ROUTES = {
|
||||
@@ -19,5 +20,6 @@ export const CONSUMER_ROUTES = {
|
||||
...CONSUMER_CUSTOMERS_ROUTES,
|
||||
...CONSUMER_SALE_INVOICES_ROUTES,
|
||||
...CONSUMER_POSES_ROUTES,
|
||||
...CONSUMER_PROFILE_ROUTES,
|
||||
],
|
||||
} as Route;
|
||||
|
||||
@@ -7,10 +7,11 @@ import { AbstractForm } from './abstract-form';
|
||||
template: '',
|
||||
imports: [ReactiveFormsModule],
|
||||
})
|
||||
export abstract class AbstractFormDialog<Request, Response> extends AbstractForm<
|
||||
export abstract class AbstractFormDialog<
|
||||
Request,
|
||||
Response
|
||||
> {
|
||||
Response,
|
||||
initialValue = Response,
|
||||
> extends AbstractForm<Request, Response, initialValue> {
|
||||
@Input()
|
||||
set visible(v: boolean) {
|
||||
this.visibleSignal.set(!!v);
|
||||
|
||||
Reference in New Issue
Block a user