This commit is contained in:
2026-03-10 13:36:45 +03:30
parent 45d48f79dc
commit b379787002
208 changed files with 2328 additions and 2510 deletions
+13 -13
View File
@@ -7,20 +7,20 @@
class="flex flex-col items-center justify-center px-12 py-10 h-auto bg-surface-card border border-surface-border rounded-lg shadow w-full max-w-md"
>
<div class="flex flex-col gap-4 text-center mb-10 items-center justify-center">
<img [src]="logo" alt="صندوق فروشگاهی" class="w-20 h-auto" />
<span class="text-lg font-bold"> به پنل صندوق فروشگاهی خوش آمدید. </span>
<img [src]="logo" alt="مدیریت کسب‌وکار" class="w-20 h-auto" />
<span class="text-lg font-bold"> به پنل مدیریت کسب‌وکار خوش آمدید. </span>
</div>
@if (activeStep() === "login") {
<app-login
[redirectUrl]="redirectUrl"
[loginApiUrl]="loginApiUrl"
[defaultRole]="role"
class="w-full"
(onSuccessfullySubmit)="onLoggedIn($event)"
(onToSignup)="toSignUp()"
/>
} @else if (activeStep() === "signup") {
<!-- @if (activeStep() === "login") { -->
<app-login
[redirectUrl]="redirectUrl"
[loginApiUrl]="loginApiUrl"
[defaultRole]="role"
class="w-full"
(onSuccessfullySubmit)="onLoggedIn($event)"
(onToSignup)="toSignUp()"
/>
<!-- } @else if (activeStep() === "signup") {
<auth-signup role="TEACHER" class="w-full" />
} @else if (activeStep() === "otp") {
<app-otp class="w-full" />
@@ -31,7 +31,7 @@
(onSubmit)="onModifyLoginInfo()"
class="w-full"
/>
}
} -->
</div>
</div>
</div>
+12 -15
View File
@@ -4,16 +4,13 @@ import { Component, EventEmitter, inject, Input, Output, signal } from '@angular
import { Router } from '@angular/router';
import images from 'src/assets/images';
import { LoginComponent } from './login/login.component';
import { ModifyLoginInfoComponent } from './modifyLoginInfo/modify-login-info.component';
import { OTPComponent } from './otp/otp.component';
import { SignupComponent } from './signup/signup.component';
type TSteps = 'login' | 'otp' | 'modifyLoginInfo' | 'signup';
@Component({
selector: 'app-auth',
templateUrl: './auth.component.html',
imports: [LoginComponent, OTPComponent, ModifyLoginInfoComponent, SignupComponent],
imports: [LoginComponent],
})
export class AuthComponent {
@Input() redirectUrl!: string;
@@ -39,11 +36,11 @@ export class AuthComponent {
onLoggedIn = (data: IAuthResponse) => {
this.toastService.success({ text: 'شما با موفقیت وارد شدید.' });
this.selectedRole.set(data.role.toUpperCase() as TRoles);
if (data.mustChangePassword) {
this.activeStep.set('modifyLoginInfo');
return;
}
this.selectedRole.set(data.account.type.toUpperCase() as TRoles);
// if (data.mustChangePassword) {
// this.activeStep.set('modifyLoginInfo');
// return;
// }
if (this.submit.observed) {
return this.submit.emit(Promise.resolve(true));
@@ -63,17 +60,17 @@ export class AuthComponent {
if (!redirectUrl) {
switch (this.selectedRole()) {
case 'SCHOOL':
redirectUrl = '/schools';
case 'SUPERADMIN':
redirectUrl = '/super_admin';
break;
case 'ADMIN':
redirectUrl = '/admin';
break;
case 'TEACHER':
redirectUrl = '/teachers';
case 'POS':
redirectUrl = '/poss';
break;
case 'STUDENTS':
redirectUrl = '/students';
case 'PARTNER':
redirectUrl = '/partner';
break;
}
}
@@ -46,12 +46,12 @@
</span>
</div> -->
<div class="mb-6 flex flex-col items-center gap-4 w-[16rem] mx-auto">
<div
class="relative bg-surface-ground rounded-xl overflow-hidden shadow shrink-0 border border-[var(--p-inputtext-border-color)] w-full aspect-[2]"
<!-- <div
class="relative bg-surface-ground rounded-xl overflow-hidden shadow shrink-0 border border-(--p-inputtext-border-color) w-full aspect-[2]"
>
<img p-image id="captchaImage" [src]="captchaImageSrc()" class="w-full object-contain" />
@if (isCaptchaLoading()) {
<div class="!absolute inset-0 bg-surface-hover/50 flex items-center justify-center">
<div class="absolute! inset-0 bg-surface-hover/50 flex items-center justify-center">
<p-progress-spinner
strokeWidth="4"
fill="transparent"
@@ -61,14 +61,14 @@
</div>
} @else if (isCaptchaExpired()) {
<div
class="!absolute inset-0 bg-surface-hover/70 flex items-center justify-center text-center p-2 text-sm font-medium cursor-pointer"
class="absolute! inset-0 bg-surface-hover/70 flex items-center justify-center text-center p-2 text-sm font-medium cursor-pointer"
(click)="onRefreshCaptcha()"
>
<i class="pi pi-refresh !text-3xl text-gray-500"></i>
<i class="pi pi-refresh text-3xl! text-gray-500"></i>
</div>
}
</div>
<p-inputgroup>
</div> -->
<!-- <p-inputgroup>
<input
pInputText
id="captcha"
@@ -81,14 +81,14 @@
<p-inputgroup-addon>
<p-button icon="pi pi-refresh" severity="secondary" variant="text" (click)="onRefreshCaptchaImage()" />
</p-inputgroup-addon>
</p-inputgroup>
</p-inputgroup> -->
</div>
<div class="grid grid-cols-1 gap-3">
@if (loginForm.controls.role.value === "TEACHER") {
<!-- @if (loginForm.controls.role.value === "TEACHER") {
<span class="w-full text-center text-sm select-none">
برای ثبت نام در سامانه <a class="text-primary cursor-pointer" (click)="toSignup()">اینجا</a> کلیک کنید.
</span>
}
} -->
<p-button label="ورود" styleClass="w-full" size="large" [loading]="isLoading()" type="submit"></p-button>
</div>
</form>
@@ -14,7 +14,6 @@ import { InputText } from 'primeng/inputtext';
import { Password } from 'primeng/password';
import { ProgressSpinnerModule } from 'primeng/progressspinner';
import { RadioButtonModule } from 'primeng/radiobutton';
import { CaptchaService } from 'src/app/core/services/captcha.service';
// import { CENTRAL_AUTH_ROLES } from '../../constants/central-auth-roles.const';
@Component({
@@ -45,48 +44,47 @@ export class LoginComponent {
private readonly authService = inject(AuthService);
private readonly router = inject(Router);
private readonly fb = inject(FormBuilder);
private readonly captchaService = inject(CaptchaService);
private readonly toastService = inject(ToastService);
readonly isLoading = computed(() => this.authService.isLoading());
readonly errorMessage = signal<string>('');
readonly hidePassword = signal<boolean>(true);
readonly isCaptchaExpired = this.captchaService.captchaIsExpired;
readonly isCaptchaLoading = this.captchaService.loading;
readonly captchaImageSrc = this.captchaService.captchaImageSrc;
// readonly isCaptchaExpired = this.captchaService.captchaIsExpired;
// readonly isCaptchaLoading = this.captchaService.loading;
// readonly captchaImageSrc = this.captchaService.captchaImageSrc;
// readonly centralAuthRoles = CENTRAL_AUTH_ROLES;
readonly loginForm = this.fb.group({
username: ['', [Validators.required]],
password: ['', [Validators.required]],
rememberMe: [false],
captcha: ['', [Validators.required]],
role: [this.defaultRole, [Validators.required]],
// captcha: ['', [Validators.required]],
// role: [this.defaultRole, [Validators.required]],
});
selectedRole = signal<TRoles>('SCHOOL');
selectedRole = signal<TRoles>('SUPERADMIN');
ngOnInit() {
this.captchaService.requestNewCaptcha();
// this.captchaService.requestNewCaptcha();
}
togglePasswordVisibility(): void {
this.hidePassword.set(!this.hidePassword());
}
resetCaptcha(): void {
this.fb.control('captcha').setValue('');
this.captchaService.renewCaptcha();
}
// resetCaptcha(): void {
// this.fb.control('captcha').setValue('');
// this.captchaService.renewCaptcha();
// }
onRefreshCaptcha(): void {
this.resetCaptcha();
}
onRefreshCaptchaImage(): void {
this.resetCaptcha();
// document
// .getElementById('captchaImage')
// ?.setAttribute('src', this.captchaService.captchaImageSrc()! + `&${new Date().getTime()}`);
}
// onRefreshCaptcha(): void {
// this.resetCaptcha();
// }
// onRefreshCaptchaImage(): void {
// this.resetCaptcha();
// // document
// // .getElementById('captchaImage')
// // ?.setAttribute('src', this.captchaService.captchaImageSrc()! + `&${new Date().getTime()}`);
// }
toSignup() {
this.onToSignup.emit();
@@ -95,27 +93,25 @@ export class LoginComponent {
submit(): void {
this.loginForm.markAllAsTouched();
if (this.loginForm.invalid) return;
if (!this.captchaService.captchaId()) {
this.toastService.error({ text: 'مقدار کپچا را وارد کنید' });
}
// if (!this.captchaService.captchaId()) {
// this.toastService.error({ text: 'مقدار کپچا را وارد کنید' });
// }
const credentials: LoginCredentials = this.loginForm.value as LoginCredentials;
this.authService
.login(credentials, (this.defaultRole || this.loginForm.value.role)!)
.subscribe({
next: (data) => {
this.errorMessage.set('');
if (this.redirectUrl) {
this.router.navigateByUrl(this.redirectUrl.replace(/\/[^/]*$/, ''));
} else {
this.onSuccessfullySubmit.emit(data);
}
},
error: (error) => {
this.resetCaptcha();
this.errorMessage.set('نام کاربری یا رمز عبور اشتباه است');
console.error('Login failed:', error);
},
});
this.authService.login(credentials).subscribe({
next: (data) => {
this.errorMessage.set('');
if (this.redirectUrl) {
this.router.navigateByUrl(this.redirectUrl.replace(/\/[^/]*$/, ''));
} else {
this.onSuccessfullySubmit.emit(data);
}
},
error: (error) => {
// this.resetCaptcha();
this.errorMessage.set('نام کاربری یا رمز عبور اشتباه است');
console.error('Login failed:', error);
},
});
}
}
@@ -1,7 +1,7 @@
<form [formGroup]="form" class="w-full flex flex-col gap-4" (ngSubmit)="submit()">
<app-input label="نام" name="firstName" size="large" [control]="form.controls.firstName" />
<app-input label="نام خانوادگی" name="lastName" size="large" [control]="form.controls.lastName" />
<app-gender-select [control]="form.controls.gender" size="large" />
<!-- <app-gender-select [control]="form.controls.gender" size="large" /> -->
<app-input
label="شماره تلفن همراه"
name="mobile"
@@ -3,7 +3,7 @@ import { ISignupRequestPayload, IUserLoginInfo, Maybe, TRoles } from '@/core/mod
import { ToastService } from '@/core/services/toast.service';
import { mobileValidator, MustMatch } from '@/core/validators';
import { password } from '@/core/validators/password.validator';
import { GenderSelectComponent } from '@/shared/catalog';
// import { GenderSelectComponent } from '@/shared/catalog';
import { InputComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit/uikit-field.component';
import { CommonModule } from '@angular/common';
@@ -33,7 +33,7 @@ import { RadioButtonModule } from 'primeng/radiobutton';
InputGroupAddonModule,
UikitFieldComponent,
InputComponent,
GenderSelectComponent,
// GenderSelectComponent,
],
})
export class SignupComponent {
-406
View File
@@ -1,406 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { computed, inject, Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, of, tap } from 'rxjs';
import { LOCAL_STORAGE_KEYS, ROLES } from 'src/assets/constants';
import { IAuthResponse, LoginCredentials, Maybe, TRoles, User } from '../../../core/models';
import { BaseState, BaseStore } from '../../../core/state/base-store';
/**
* Authentication state
*/
export interface AuthState extends BaseState {
user: Maybe<User>;
token: Maybe<string>;
refreshToken: Maybe<string>;
isAuthenticated: boolean;
permissions: string[];
loginAttempts: number;
lastLoginTime: Maybe<number>;
sessionExpiry: Maybe<number>;
}
/**
* Login state for UI feedback
*/
export interface LoginState {
isLoggingIn: boolean;
isRefreshing: boolean;
loginError: Maybe<string>;
}
/**
* Authentication state store
*/
@Injectable({
providedIn: 'root',
})
export class AuthStore extends BaseStore<AuthState> {
private readonly http = inject(HttpClient);
private readonly router = inject(Router);
// Login-specific state
private readonly _loginState = {
isLoggingIn: false,
isRefreshing: false,
loginError: null as Maybe<string>,
};
constructor() {
super({
initialized: false,
loading: false,
error: null,
user: null,
token: null,
refreshToken: null,
isAuthenticated: false,
permissions: [],
loginAttempts: 0,
lastLoginTime: null,
sessionExpiry: null,
});
this.initializeAuth();
}
// Computed selectors
readonly user = computed(() => this._state().user);
readonly token = computed(() => this._state().token);
readonly refreshToken = computed(() => this._state().refreshToken);
readonly isAuthenticated = computed(() => this._state().isAuthenticated);
readonly userRole = computed(() => this._state().user?.role);
readonly permissions = computed(() => this._state().permissions);
readonly loginAttempts = computed(() => this._state().loginAttempts);
readonly lastLoginTime = computed(() => this._state().lastLoginTime);
readonly sessionExpiry = computed(() => this._state().sessionExpiry);
// Login state selectors
readonly isLoggingIn = computed(() => this._loginState.isLoggingIn);
readonly isRefreshing = computed(() => this._loginState.isRefreshing);
readonly loginError = computed(() => this._loginState.loginError);
/**
* Initialize authentication from stored data
*/
private initializeAuth(): void {
const token = localStorage.getItem(LOCAL_STORAGE_KEYS.AUTH_TOKEN);
const refreshToken = localStorage.getItem(LOCAL_STORAGE_KEYS.REFRESH_TOKEN);
const userData = localStorage.getItem(LOCAL_STORAGE_KEYS.USER_DATA);
const loginAttempts = parseInt(localStorage.getItem(LOCAL_STORAGE_KEYS.LOGIN_ATTEMPTS) || '0');
const lastLoginTime = parseInt(localStorage.getItem(LOCAL_STORAGE_KEYS.LAST_LOGIN_TIME) || '0');
if (token && refreshToken && userData) {
try {
const user = JSON.parse(userData) as User;
const sessionExpiry = this.getTokenExpiry(token);
this.patchState({
user,
token,
refreshToken,
isAuthenticated: true,
permissions: [],
// permissions: user.permissions?.map((p) => `${p.resource}:${p.action}`) || [],
loginAttempts,
lastLoginTime: lastLoginTime || null,
sessionExpiry,
});
// Check if token is expired
if (this.isTokenExpired()) {
this.refreshAuthToken().subscribe();
}
} catch (error) {
console.error('Error parsing stored user data:', error);
this.logout();
}
}
}
/**
* Login with credentials
*/
login(credentials: LoginCredentials) {
this._loginState.isLoggingIn = true;
this._loginState.loginError = null;
this.setLoading(true);
return this.http.post<IAuthResponse>('/api/auth/login', credentials).pipe(
tap((response) => {
this.handleAuthSuccess(response);
this._loginState.isLoggingIn = false;
}),
catchError((error) => {
this.handleAuthError(error);
return of(null);
}),
);
}
/**
* Logout user
*/
logout(): void {
// Clear stored data
localStorage.removeItem(LOCAL_STORAGE_KEYS.AUTH_TOKEN);
localStorage.removeItem(LOCAL_STORAGE_KEYS.REFRESH_TOKEN);
localStorage.removeItem(LOCAL_STORAGE_KEYS.USER_DATA);
localStorage.removeItem(LOCAL_STORAGE_KEYS.LAST_LOGIN_TIME);
// Reset state
this.patchState({
user: null,
token: null,
refreshToken: null,
isAuthenticated: false,
permissions: [],
lastLoginTime: null,
sessionExpiry: null,
loading: false,
error: null,
});
// Reset login state
this._loginState.isLoggingIn = false;
this._loginState.isRefreshing = false;
this._loginState.loginError = null;
// Navigate to login
this.router.navigate(['/auth']);
}
/**
* Refresh authentication token
*/
refreshAuthToken() {
const refreshToken = this._state().refreshToken;
if (!refreshToken) {
this.logout();
return of(null);
}
this._loginState.isRefreshing = true;
this.setLoading(true);
return this.http.post<IAuthResponse>('/api/auth/refresh', { refreshToken }).pipe(
tap((response) => {
this.handleAuthSuccess(response);
this._loginState.isRefreshing = false;
}),
catchError((error) => {
console.error('Token refresh failed:', error);
this.logout();
return of(null);
}),
);
}
/**
* Check if user has specific role
*/
hasRole(role: TRoles): boolean {
return this._state().user?.role === role;
}
/**
* Check if user has any of the specified roles
*/
hasAnyRole(roles: TRoles[]): boolean {
const userRole = this._state().user?.role;
return userRole ? roles.includes(userRole) : false;
}
/**
* Check if user has specific permission
*/
hasPermission(permission: string): boolean {
const permissions = this._state().permissions;
return permissions.includes(permission);
}
/**
* Check if user has all specified permissions
*/
hasAllPermissions(permissions: string[]): boolean {
const userPermissions = this._state().permissions;
return permissions.every((permission) => userPermissions.includes(permission));
}
/**
* Check if user has any of the specified permissions
*/
hasAnyPermission(permissions: string[]): boolean {
const userPermissions = this._state().permissions;
return permissions.some((permission) => userPermissions.includes(permission));
}
/**
* Check if user can access resource
*/
canAccess(requiredRoles?: TRoles[], requiredPermissions?: string[]): boolean {
if (!this._state().isAuthenticated) return false;
if (requiredRoles && !this.hasAnyRole(requiredRoles)) {
return false;
}
if (requiredPermissions && !this.hasAllPermissions(requiredPermissions)) {
return false;
}
return true;
}
/**
* Get current access token
*/
getToken(): string | null {
return this._state().token;
}
/**
* Get current refresh token
*/
getRefreshToken(): string | null {
return this._state().refreshToken;
}
/**
* Check if token is expired
*/
isTokenExpired(): boolean {
const sessionExpiry = this._state().sessionExpiry;
if (!sessionExpiry) return true;
return Date.now() > sessionExpiry;
}
/**
* Get time until token expires (in minutes)
*/
getTimeUntilExpiry(): number {
const sessionExpiry = this._state().sessionExpiry;
if (!sessionExpiry) return 0;
return Math.max(0, Math.floor((sessionExpiry - Date.now()) / 60000));
}
/**
* Update user profile
*/
updateUserProfile(updates: Partial<User>): void {
const currentUser = this._state().user;
if (!currentUser) return;
const updatedUser = { ...currentUser, ...updates };
this.patchState({ user: updatedUser });
localStorage.setItem(LOCAL_STORAGE_KEYS.USER_DATA, JSON.stringify(updatedUser));
}
/**
* Handle successful authentication
*/
private handleAuthSuccess(response: IAuthResponse): void {
const sessionExpiry = this.getTokenExpiry(response.token);
const currentTime = Date.now();
// Store in localStorage
localStorage.setItem(LOCAL_STORAGE_KEYS.AUTH_TOKEN, response.token);
localStorage.setItem(LOCAL_STORAGE_KEYS.REFRESH_TOKEN, response.refreshToken);
// localStorage.setItem(LOCAL_STORAGE_KEYS.USER_DATA, JSON.stringify(response.user));
localStorage.setItem(LOCAL_STORAGE_KEYS.LAST_LOGIN_TIME, currentTime.toString());
// Update state
this.patchState({
user: { fullName: response.fullName, role: response.role as TRoles },
token: response.token,
refreshToken: response.refreshToken,
isAuthenticated: true,
// permissions: response.user.permissions?.map((p) => `${p.resource}:${p.action}`) || [],
lastLoginTime: currentTime,
sessionExpiry,
loading: false,
error: null,
});
// Reset login attempts on successful login
localStorage.removeItem(LOCAL_STORAGE_KEYS.LOGIN_ATTEMPTS);
// Navigate based on user role
// this.navigateByRole(response.user.role);
}
/**
* Handle authentication error
*/
private handleAuthError(error: any): void {
console.error('Authentication error:', error);
// Increment login attempts
const attempts = this._state().loginAttempts + 1;
this.patchState({ loginAttempts: attempts });
localStorage.setItem(LOCAL_STORAGE_KEYS.LOGIN_ATTEMPTS, attempts.toString());
// Set login error
this._loginState.isLoggingIn = false;
this._loginState.loginError = error.error?.message || 'خطا در ورود به سیستم';
if (this._loginState.loginError) {
this.setError(this._loginState.loginError);
}
}
/**
* Navigate user based on their role
*/
private navigateByRole(role: TRoles): void {
const roleRoutes = {
[ROLES.ADMIN]: '/admin',
[ROLES.SCHOOL]: '/schools',
[ROLES.TEACHER]: '/teachers',
[ROLES.STUDENTS]: '/students',
[ROLES.GRADER]: '/grader',
[ROLES.SUPERADMIN]: '/superadmin',
};
this.router.navigate([roleRoutes[role]]);
}
/**
* Extract token expiry from JWT
*/
private getTokenExpiry(token: string): number | null {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
return payload.exp * 1000; // Convert to milliseconds
} catch (error) {
console.error('Error parsing token:', error);
return null;
}
}
/**
* Reset state to initial values
*/
reset(): void {
this.setState({
initialized: false,
loading: false,
error: null,
user: null,
token: null,
refreshToken: null,
isAuthenticated: false,
permissions: [],
loginAttempts: 0,
lastLoginTime: null,
sessionExpiry: null,
});
this._loginState.isLoggingIn = false;
this._loginState.isRefreshing = false;
this._loginState.loginError = null;
}
}
-5
View File
@@ -1,5 +0,0 @@
// Authentication state management
export * from './auth.store';
// Types
export type { AuthState, LoginState } from './auth.store';
@@ -1,7 +1,7 @@
<div class="">
<div class="grid grid-cols-2 gap-4">
<app-statistics-shared-top-alert-stocks class="block h-[300px]" />
<!-- <app-statistics-shared-top-alert-stocks class="block h-[300px]" />
<app-statistics-shared-top-sales class="block h-[300px]" />
<app-statistics-shared-top-supplier-debts class="block h-[300px]" />
<app-statistics-shared-top-supplier-debts class="block h-[300px]" /> -->
</div>
</div>
@@ -1,6 +1,3 @@
import { StatisticsSharedTopAlertStocksComponent } from '@/modules/statistics/shared/components/top-alert-stocks.component';
import { StatisticsSharedTopSalesComponent } from '@/modules/statistics/shared/components/top-sales.component';
import { StatisticsSharedTopSupplierDebtsComponent } from '@/modules/statistics/shared/components/top-supplier-debts.component';
import { Component } from '@angular/core';
import images from 'src/assets/images';
@@ -8,9 +5,9 @@ import images from 'src/assets/images';
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
imports: [
StatisticsSharedTopAlertStocksComponent,
StatisticsSharedTopSalesComponent,
StatisticsSharedTopSupplierDebtsComponent,
// StatisticsSharedTopAlertStocksComponent,
// StatisticsSharedTopSalesComponent,
// StatisticsSharedTopSupplierDebtsComponent,
],
})
export class DashboardComponent {
@@ -4,12 +4,12 @@
</div>
<hr />
<div class="shrink-0 sticky top-0">
<customers-select-field
<!-- <customers-select-field
[canInsert]="true"
[value]="selectedCustomer()"
(valueChange)="updateCustomer($event)"
[isFullDataOptionValue]="true"
/>
/> -->
</div>
<hr />
<div class="grow overflow-auto flex flex-col">
@@ -1,9 +1,8 @@
import { Maybe } from '@/core';
import { CustomersSelectComponent } from '@/modules/customers/components/select/select.component';
import { ICustomerResponse } from '@/modules/customers/models';
// import { CustomersSelectComponent } from '@/modules/customers/components/select/select.component';
// import { ICustomerResponse } from '@/modules/customers/models';
import { PriceMaskDirective } from '@/shared/directives';
import { UikitCounterComponent } from '@/uikit';
import { Component, computed, inject, signal } from '@angular/core';
import { Component, computed, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ButtonDirective } from 'primeng/button';
import { InplaceModule } from 'primeng/inplace';
@@ -17,7 +16,7 @@ import { POSOrderPriceInfoCardComponent } from './price-info-card.component';
selector: 'pos-order-card',
templateUrl: './order-card.component.html',
imports: [
CustomersSelectComponent,
// CustomersSelectComponent,
ButtonDirective,
PriceMaskDirective,
UikitCounterComponent,
@@ -32,7 +31,7 @@ export class PosOrderCardComponent {
private store = inject(POSStore);
constructor() {
this.selectedCustomer.set(this.store.selectedCustomer());
// this.selectedCustomer.set(this.store.selectedCustomer());
}
posId = this.store.posId;
@@ -41,7 +40,7 @@ export class PosOrderCardComponent {
inOrderProducts = computed(() => this.store.inOrderProducts());
selectedCustomer = signal<Maybe<ICustomerResponse>>(null);
// selectedCustomer = signal<Maybe<ICustomerResponse>>(null);
removeProductFromOrder(productId: number) {
this.store.removeFromInOrderProducts(productId);
@@ -63,9 +62,9 @@ export class PosOrderCardComponent {
this.store.resetInOrderProducts();
}
updateCustomer(customer: Maybe<ICustomerResponse>) {
this.store.setSelectedCustomer(customer);
}
// updateCustomer(customer: Maybe<ICustomerResponse>) {
// this.store.setSelectedCustomer(customer);
// }
submit() {
this.store.submitOrder();
+3 -3
View File
@@ -1,5 +1,5 @@
import { Maybe } from '@/core';
import { ICustomerResponse } from '@/modules/customers/models';
// import { ICustomerResponse } from '@/modules/customers/models';
import { computed, Inject, Injectable, InjectionToken, signal } from '@angular/core';
import { map, of } from 'rxjs';
import { IPosInfoResponse, IPosProductCategoriesResponse, IPosStockResponse } from '../models';
@@ -19,7 +19,7 @@ interface IPosState {
productCategories: Maybe<IPosProductCategoriesResponse[]>;
activeProductCategory: Maybe<number>;
inOrderProducts: IPosInOrderProduct[];
selectedCustomer: Maybe<ICustomerResponse>;
selectedCustomer: Maybe<any>;
viewType: TViewType;
searchQuery: string;
}
@@ -245,7 +245,7 @@ export class POSStore {
this.setState({ inOrderProducts: [] });
}
setSelectedCustomer(customer: Maybe<ICustomerResponse>) {
setSelectedCustomer(customer: Maybe<any>) {
this.setState({ selectedCustomer: customer });
}
@@ -1,37 +0,0 @@
<p-dialog header="فرم کاربر" [(visible)]="visible" [modal]="true" [style]="{ width: '500px' }" [closable]="true">
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<app-input label="نام" [control]="form.controls.firstName" name="firstName" />
<app-input label="نام خانوادگی" [control]="form.controls.lastName" name="lastName" />
<app-input label="شماره موبایل" [control]="form.controls.mobileNumber" name="mobileNumber" type="mobile" />
<catalog-roles-select [control]="form.controls.roleId" />
<uikit-field label="رمز عبور" class="" [control]="form.get('password')">
<p-password
id="password1"
name="password"
formControlName="password"
autocomplete="password"
[toggleMask]="true"
[fluid]="true"
[feedback]="false"
[invalid]="form.get('password')?.touched && form.get('password')?.invalid"
/>
<span class="text-xs mt-2 text-muted-color">
رمز باید حداقل ۸ کاراکتر بوده و شامل حداقل یک حرف بزرگ، یک حرف کوچک، یک عدد و یک کاراکتر ویژه باشد.
</span>
</uikit-field>
<uikit-field label="تکرار رمز عبور" class="" [control]="form.get('confirmPassword')">
<p-password
id="confirmPassword"
name="confirmPassword"
formControlName="confirmPassword"
autocomplete="new-password"
[toggleMask]="true"
[fluid]="true"
[feedback]="false"
[invalid]="form.get('confirmPassword')?.touched && form.get('confirmPassword')?.invalid"
/>
</uikit-field>
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
</form>
</p-dialog>
@@ -1,101 +0,0 @@
import { ToastService } from '@/core/services/toast.service';
import { mobileValidator, MustMatch, password } from '@/core/validators';
import { CatalogRolesComponent } from '@/shared/catalog/roles';
import { InputComponent } from '@/shared/components';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { UikitFieldComponent } from '@/uikit';
import { Component, EventEmitter, inject, Input, Output, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { Dialog } from 'primeng/dialog';
import { Password } from 'primeng/password';
import { IUserRequest, IUserResponse } from '../models';
import { UsersService } from '../services/main.service';
@Component({
selector: 'user-form',
templateUrl: './form.component.html',
imports: [
ReactiveFormsModule,
Dialog,
InputComponent,
FormFooterActionsComponent,
CatalogRolesComponent,
UikitFieldComponent,
Password,
],
})
export class UserFormComponent {
@Input() initialValues?: IUserResponse;
@Input()
set visible(v: boolean) {
this.visibleSignal.set(!!v);
}
get visible() {
return this.visibleSignal();
}
private visibleSignal = signal(false);
@Output() visibleChange = new EventEmitter<boolean>();
@Output() onSubmit = new EventEmitter<IUserResponse>();
private fb = inject(FormBuilder);
constructor(
private service: UsersService,
private toastService: ToastService,
) {
// effect(() => {
// const v = this.visibleSignal();
// // this.visibleChange.emit(v);
// if (!v) this.form.reset();
// });
}
form = this.fb.group(
{
firstName: [this.initialValues?.firstName || '', [Validators.required]],
lastName: [this.initialValues?.lastName || '', [Validators.required]],
mobileNumber: [
this.initialValues?.mobileNumber || '',
[Validators.required, mobileValidator()],
],
roleId: [this.initialValues?.role.id || null, [Validators.required]],
password: ['', [Validators.required, password()]],
confirmPassword: ['', [Validators.required]],
},
{
validators: [MustMatch('password', 'confirmPassword')],
},
);
submitLoading = signal(false);
submit() {
this.form.markAllAsTouched();
if (this.form.valid) {
this.form.disable();
this.submitLoading.set(true);
const { confirmPassword, ...rest } = this.form.value;
this.service.create(rest as IUserRequest).subscribe({
next: (res) => {
this.toastService.success({
text: `کاربر ${this.form.value.firstName} با موفقیت ایجاد شد`,
});
this.close();
this.submitLoading.set(false);
this.form.enable();
this.form.reset();
this.onSubmit.emit(res);
},
error: () => {
this.submitLoading.set(false);
this.form.enable();
},
});
}
}
close() {
this.visibleChange.emit(false);
}
}
@@ -1,6 +0,0 @@
const baseUrl = '/api/v1/users';
export const USERS_API_ROUTES = {
list: () => `${baseUrl}`,
single: (userId: string) => `${baseUrl}/${userId}`,
};
-2
View File
@@ -1,2 +0,0 @@
export * from './apiRoutes';
export * from './routes';
@@ -1,25 +0,0 @@
import { NamedRoutes } from '@/core';
import { Routes } from '@angular/router';
export type TUsersRouteNames = 'users' | 'user';
export const usersNamedRoutes: NamedRoutes<TUsersRouteNames> = {
users: {
path: 'users',
loadComponent: () => import('../../views/list.component').then((m) => m.UsersComponent),
meta: {
title: 'کاربران',
pagePath: () => '/users',
},
},
user: {
path: 'users/:userId',
loadComponent: () => import('../../views/single.component').then((m) => m.UserComponent),
meta: {
title: 'کاربر',
pagePath: () => '/users/:userId',
},
},
};
export const USERS_ROUTES: Routes = Object.values(usersNamedRoutes);
-1
View File
@@ -1 +0,0 @@
export * from './io';
-17
View File
@@ -1,17 +0,0 @@
import { IRoleResponse } from '@/shared/catalog/roles';
export interface IUserRawResponse {
mobileNumber: string;
firstName: string;
lastName: string;
id: number;
role: IRoleResponse;
}
export interface IUserResponse extends IUserRawResponse {}
export interface IUserRequest {
firstName: string;
lastName: string;
mobileNumber: string;
roleId: number;
}
@@ -1,28 +0,0 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { USERS_API_ROUTES } from '../constants';
import { IUserRawResponse, IUserRequest, IUserResponse } from '../models';
@Injectable({ providedIn: 'root' })
export class UsersService {
constructor(private http: HttpClient) {}
private apiRoutes = USERS_API_ROUTES;
getAll(): Observable<IPaginatedResponse<IUserResponse>> {
return this.http.get<IPaginatedResponse<IUserRawResponse>>(this.apiRoutes.list());
}
getSingle(userId: string): Observable<IUserResponse> {
return this.http.get<IUserRawResponse>(this.apiRoutes.single(userId));
}
create(userData: IUserRequest): Observable<IUserResponse> {
return this.http.post<IUserResponse>(this.apiRoutes.list(), userData);
}
update(userId: string, userData: IUserRequest): Observable<IUserResponse> {
return this.http.patch<IUserResponse>(this.apiRoutes.single(userId), userData);
}
}
-2
View File
@@ -1,2 +0,0 @@
export * from './list.component';
export * from './single.component';
@@ -1,18 +0,0 @@
<app-page-data-list
[pageTitle]="'مدیریت کاربران'"
[addNewCtaLabel]="'افزودن کاربر جدید'"
[columns]="columns"
[showAdd]="true"
emptyPlaceholderTitle="کاربری یافت نشد"
emptyPlaceholderDescription="برای افزودن کاربر جدید، روی دکمهٔ بالا کلیک کنید."
[items]="items()"
[loading]="loading()"
[showDetails]="true"
[showAdd]="true"
(onAdd)="openAddForm()"
>
<ng-template #role let-data>
<catalog-role-tag [role]="data.role" />
</ng-template>
</app-page-data-list>
<user-form [(visible)]="visibleForm" (onSubmit)="refresh()" />
@@ -1,54 +0,0 @@
import { CatalogRoleTagComponent } from '@/shared/catalog/roles';
import {
IColumn,
PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component';
import { Component, signal, TemplateRef, ViewChild } from '@angular/core';
import { UserFormComponent } from '../components/form.component';
import { IUserResponse } from '../models';
import { UsersService } from '../services/main.service';
@Component({
selector: 'app-users',
templateUrl: './list.component.html',
imports: [PageDataListComponent, UserFormComponent, CatalogRoleTagComponent],
})
export class UsersComponent {
constructor(private userService: UsersService) {
this.getData();
}
@ViewChild('role', { static: true }) roleTpl!: TemplateRef<any>;
columns = [
{ field: 'id', header: 'شناسه' },
{ field: 'firstName', header: 'نام' },
{ field: 'lastName', header: 'نام خانوادگی' },
{ field: 'mobileNumber', header: 'شماره موبایل' },
// { field: 'role', header: 'نقش', customDataModel: this.roleTpl },
{
field: 'role',
header: 'نقش',
customDataModel: (item: IUserResponse) => item.role.name ?? '-',
},
] as IColumn[];
loading = signal(false);
items = signal<IUserResponse[]>([]);
visibleForm = signal(false);
refresh() {
this.getData();
}
getData() {
this.loading.set(true);
this.userService.getAll().subscribe((res) => {
this.loading.set(false);
this.items.set(res.data);
});
}
openAddForm() {
this.visibleForm.set(true);
}
}
@@ -1 +0,0 @@
<div class=""></div>
@@ -1,9 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-user',
templateUrl: './single.component.html',
})
export class UserComponent {
constructor() {}
}