diff --git a/backend/src/modules/admin-settings/presentation/admin-settings.controller.ts b/backend/src/modules/admin-settings/presentation/admin-settings.controller.ts index a7116e1..75cb9ff 100644 --- a/backend/src/modules/admin-settings/presentation/admin-settings.controller.ts +++ b/backend/src/modules/admin-settings/presentation/admin-settings.controller.ts @@ -1,18 +1,22 @@ +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { Controller, Get, Body, Put, Post, Delete } from '@nestjs/common'; import { AdminSettingsService } from '../application/admin-settings.service'; import { AdminSettings } from '../domain/admin-settings.entity'; import { UpdateAdminSettingsDto } from './dto/update-admin-settings.dto'; import { CreateAdminSettingsDto } from './dto/create-admin-settings.dto'; +@ApiTags('Admin Settings') @Controller('admin-settings') export class AdminSettingsController { constructor(private readonly adminSettingsService: AdminSettingsService) {} + @ApiOperation({ summary: 'Get all' }) @ApiResponse({ status: 200 }) @Get() async getSettings(): Promise { return this.adminSettingsService.getSettings(); } + @ApiOperation({ summary: 'Create' }) @ApiResponse({ status: 201 }) @Post() async createSettings( @Body() createAdminSettingsDto: CreateAdminSettingsDto, @@ -24,6 +28,7 @@ export class AdminSettingsController { ); } + @ApiOperation({ summary: 'Update' }) @ApiResponse({ status: 200 }) @Put() async updateSettings( @Body() updateAdminSettingsDto: UpdateAdminSettingsDto, @@ -37,6 +42,7 @@ export class AdminSettingsController { return this.adminSettingsService.updateSettings(settings); } + @ApiOperation({ summary: 'Delete' }) @ApiResponse({ status: 200 }) @Delete() async deleteSettings(): Promise { return this.adminSettingsService.deleteSettings(); diff --git a/backend/src/modules/admin-settings/presentation/dto/create-admin-settings.dto.ts b/backend/src/modules/admin-settings/presentation/dto/create-admin-settings.dto.ts index 5cc6541..fb1e29b 100644 --- a/backend/src/modules/admin-settings/presentation/dto/create-admin-settings.dto.ts +++ b/backend/src/modules/admin-settings/presentation/dto/create-admin-settings.dto.ts @@ -1,4 +1,5 @@ import { Type } from 'class-transformer'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsString, IsNotEmpty, @@ -14,80 +15,80 @@ import { } from '../../domain/interfaces/admin-settings.interface'; class LocationDto implements IAdminLocation { - @IsString() + @IsString() @ApiProperty() @IsNotEmpty() address: string; - @IsNumber() + @IsNumber() @ApiProperty() latitude: number; - @IsNumber() + @IsNumber() @ApiProperty() longitude: number; } class OwnerInfoDto implements IOwnerInfo { - @IsString() + @IsString() @ApiProperty() @IsNotEmpty() name: string; - @IsString() + @IsString() @ApiProperty() @IsNotEmpty() phoneNumber: string; } export class CreateAdminSettingsDto { - @ValidateNested() + @ValidateNested() @ApiProperty() @Type(() => LocationDto) @IsNotEmpty() location: LocationDto; - @IsObject() + @IsObject() @ApiPropertyOptional() @IsOptional() socialLinks: Record; - @IsObject() + @IsObject() @ApiPropertyOptional() @IsOptional() workHours: Record; - @ValidateNested() + @ValidateNested() @ApiProperty() @Type(() => OwnerInfoDto) @IsNotEmpty() ownerInfo: OwnerInfoDto; - @IsString() + @IsString() @ApiProperty() @IsOptional() biography: string; - @IsString() + @IsString() @ApiProperty() @IsOptional() philosophy: string; - @IsArray() + @IsArray() @ApiPropertyOptional() @IsString({ each: true }) @IsOptional() galleryCategories: string[]; - @IsArray() + @IsArray() @ApiPropertyOptional() @IsString({ each: true }) @IsOptional() treatmentCategories: string[]; - @IsArray() + @IsArray() @ApiPropertyOptional() @IsString({ each: true }) @IsOptional() veilSilhouettes: string[]; - @IsArray() + @IsArray() @ApiPropertyOptional() @IsString({ each: true }) @IsOptional() veilFabrics: string[]; - @IsArray() + @IsArray() @ApiPropertyOptional() @IsString({ each: true }) @IsOptional() veilTrainLengths: string[]; - @IsArray() + @IsArray() @ApiPropertyOptional() @IsString({ each: true }) @IsOptional() veilNecklines: string[]; diff --git a/backend/src/modules/admin-settings/presentation/dto/update-admin-settings.dto.ts b/backend/src/modules/admin-settings/presentation/dto/update-admin-settings.dto.ts index 8e81fe9..2953c51 100644 --- a/backend/src/modules/admin-settings/presentation/dto/update-admin-settings.dto.ts +++ b/backend/src/modules/admin-settings/presentation/dto/update-admin-settings.dto.ts @@ -1,4 +1,4 @@ -import { PartialType } from '@nestjs/mapped-types'; +import { PartialType } from '@nestjs/swagger'; import { CreateAdminSettingsDto } from './create-admin-settings.dto'; export class UpdateAdminSettingsDto extends PartialType( diff --git a/backend/src/modules/gallery/presentation/gallery.controller.ts b/backend/src/modules/gallery/presentation/gallery.controller.ts index 370c0b7..3916a73 100644 --- a/backend/src/modules/gallery/presentation/gallery.controller.ts +++ b/backend/src/modules/gallery/presentation/gallery.controller.ts @@ -1,3 +1,4 @@ +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { Controller, Get, @@ -17,20 +18,24 @@ import { Gallery } from '../domain/gallery.entity'; import { CreateGalleryDto } from './dto/create-gallery.dto'; import { UpdateGalleryDto } from './dto/update-gallery.dto'; +@ApiTags('Gallery') @Controller('gallery') export class GalleryController { constructor(private readonly galleryService: GalleryService) {} + @ApiOperation({ summary: 'Get all' }) @ApiResponse({ status: 200 }) @Get() async findAll(): Promise { return this.galleryService.findAll(); } + @ApiOperation({ summary: 'Get one' }) @ApiResponse({ status: 200 }) @Get(':id') async findOne(@Param('id') id: string): Promise { return this.galleryService.findOne(id); } + @ApiOperation({ summary: 'Create' }) @ApiResponse({ status: 201 }) @Post() @UseInterceptors( FilesInterceptor('files', 10, { @@ -64,6 +69,7 @@ export class GalleryController { return this.galleryService.create(gallery); } + @ApiOperation({ summary: 'Update' }) @ApiResponse({ status: 200 }) @Put(':id') @UseInterceptors( FilesInterceptor('files', 10, { @@ -98,6 +104,7 @@ export class GalleryController { ); } + @ApiOperation({ summary: 'Delete' }) @ApiResponse({ status: 200 }) @Delete(':id') async remove(@Param('id') id: string): Promise { return this.galleryService.remove(id); diff --git a/backend/src/modules/treatments/presentation/dto/create-treatments.dto.ts b/backend/src/modules/treatments/presentation/dto/create-treatments.dto.ts index 220b820..071d160 100644 --- a/backend/src/modules/treatments/presentation/dto/create-treatments.dto.ts +++ b/backend/src/modules/treatments/presentation/dto/create-treatments.dto.ts @@ -1,4 +1,5 @@ import { Type } from 'class-transformer'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsBoolean, IsNotEmpty, @@ -21,35 +22,35 @@ export enum TreatmentCategory { export class CreateServiceDto { @IsNotEmpty() - @IsString() + @IsString() @ApiProperty() name: string; @IsNotEmpty() - @IsString() + @IsString() @ApiProperty() description: string; @IsNotEmpty() @Type(() => Number) - @IsNumber() + @IsNumber() @ApiProperty() @Min(1) price: number; @IsOptional() @Type(() => Boolean) - @IsBoolean() + @IsBoolean() @ApiPropertyOptional() active: boolean; @IsNotEmpty() @Type(() => Number) - @IsNumber() + @IsNumber() @ApiProperty() @Min(1) duration: number; @IsNotEmpty() - @IsString() + @IsString() @ApiProperty() category: string; @IsOptional() - @IsString() + @IsString() @ApiProperty() imageUrl: string; } diff --git a/backend/src/modules/treatments/presentation/dto/update-treatments.dto.ts b/backend/src/modules/treatments/presentation/dto/update-treatments.dto.ts index de6c534..d0e8913 100644 --- a/backend/src/modules/treatments/presentation/dto/update-treatments.dto.ts +++ b/backend/src/modules/treatments/presentation/dto/update-treatments.dto.ts @@ -1,4 +1,4 @@ -import { PartialType } from '@nestjs/mapped-types'; +import { PartialType } from '@nestjs/swagger'; import { CreateServiceDto } from './create-treatments.dto'; export class UpdateServiceDto extends PartialType(CreateServiceDto) {} diff --git a/backend/src/modules/treatments/presentation/treatments.controller.ts b/backend/src/modules/treatments/presentation/treatments.controller.ts index 90c0058..b6e89f3 100644 --- a/backend/src/modules/treatments/presentation/treatments.controller.ts +++ b/backend/src/modules/treatments/presentation/treatments.controller.ts @@ -1,3 +1,4 @@ +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { CreateServiceDto as CreateTreatmentDto, Treatments, @@ -19,20 +20,24 @@ import { FilesInterceptor } from '@nestjs/platform-express'; import { diskStorage } from 'multer'; import { extname } from 'path'; +@ApiTags('Treatments') @Controller('treatments') export class TreatmentsController { constructor(private readonly treatmentsService: TreatmentsService) {} + @ApiOperation({ summary: 'Get all' }) @ApiResponse({ status: 200 }) @Get() async findAll(): Promise { return this.treatmentsService.findAll(); } + @ApiOperation({ summary: 'Get one' }) @ApiResponse({ status: 200 }) @Get(':id') async findOne(@Param('id') id: string): Promise { return this.treatmentsService.findOne(id); } + @ApiOperation({ summary: 'Create' }) @ApiResponse({ status: 201 }) @Post() @UseInterceptors( FilesInterceptor('image', 10, { @@ -63,6 +68,7 @@ export class TreatmentsController { ); } + @ApiOperation({ summary: 'Update' }) @ApiResponse({ status: 200 }) @Put(':id') @UseInterceptors( FilesInterceptor('image', 10, { @@ -97,6 +103,7 @@ export class TreatmentsController { ); } + @ApiOperation({ summary: 'Delete' }) @ApiResponse({ status: 200 }) @Delete(':id') async remove(@Param('id') id: string): Promise { return this.treatmentsService.remove(id); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7730b09..c438eb4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5636,6 +5636,19 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "17.4.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz", + "integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6caf5b5..22d3eef 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,9 @@ "version": "0.0.0", "type": "module", "scripts": { + "predev": "node scripts/setenv.cjs", "dev": "ng serve --configuration=development", + "prebuild": "node scripts/setenv.cjs", "build": "ng build", "preview": "ng serve --configuration=production", "test": "vitest" diff --git a/frontend/scripts/setenv.cjs b/frontend/scripts/setenv.cjs new file mode 100644 index 0000000..fdd8ce7 --- /dev/null +++ b/frontend/scripts/setenv.cjs @@ -0,0 +1,17 @@ +const fs = require('fs'); +const path = require('path'); +require('dotenv').config({ path: path.resolve(__dirname, '../../.env') }); + +const envConfigFile = `export const environment = { + production: ${process.env.NODE_ENV === 'production'}, + apiUrl: "${process.env.FRONTEND_URL || 'http://localhost:3000'}", + telegramBotName: "${process.env.TELEGRAM_BOT_NAME || 'test_bot'}", +}; +`; + +const targetPath = path.resolve(__dirname, '../src/environments/environment.ts'); +const targetProdPath = path.resolve(__dirname, '../src/environments/environment.prod.ts'); + +fs.writeFileSync(targetPath, envConfigFile); +fs.writeFileSync(targetProdPath, envConfigFile); +console.log(`Environment files generated at ${targetPath}`); diff --git a/frontend/src/core/constants/api-endpoints.ts b/frontend/src/core/constants/api-endpoints.ts index 74a6615..61b139a 100644 --- a/frontend/src/core/constants/api-endpoints.ts +++ b/frontend/src/core/constants/api-endpoints.ts @@ -18,16 +18,16 @@ export const API_ENDPOINTS = { URL_BY_ID: (id: string) => linkServerConvert(API_ENDPOINTS.GALLERY.URL, id), }, AUTH: { - LOGIN: "/auth/login", - REGISTER: "/auth/register", + LOGIN: linkServerConvert("auth", "login"), + REGISTER: linkServerConvert("auth", "register"), }, USER: { - PROFILE: "/user/profile", - UPDATE: "/user/update", + PROFILE: linkServerConvert("user", "profile"), + UPDATE: linkServerConvert("user", "update"), }, ADMIN: { - SETTINGS: "/admin-settings", - ANALYTICS: "/admin/analytics", + SETTINGS: linkServerConvert("admin-settings"), + ANALYTICS: linkServerConvert("admin", "analytics"), }, } as const; diff --git a/frontend/src/core/interceptors/error.interceptor.ts b/frontend/src/core/interceptors/error.interceptor.ts deleted file mode 100644 index 64a39d0..0000000 --- a/frontend/src/core/interceptors/error.interceptor.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { HttpErrorResponse, HttpInterceptorFn } from "@angular/common/http"; -import { inject } from "@angular/core"; -import { catchError, throwError } from "rxjs"; -import { ErrorService } from "@shared/services"; - -export const errorInterceptor: HttpInterceptorFn = (req, next) => { - const errorService = inject(ErrorService); - - return next(req).pipe( - catchError((error: HttpErrorResponse) => { - let message = "An unexpected error occurred"; - - if (error.error instanceof ErrorEvent) { - // Client-side error - message = `Client Error: ${error.error.message}`; - } else { - // Backend error - if (error.status === 0) { - message = "Network error. Please check your internet connection."; - } else if (error.status === 400) { - // Check for Veil/NestJS validation array - if (Array.isArray(error.error.message)) { - message = error.error.message.join("\n"); - } else if (typeof error.error.message === "string") { - message = error.error.message; - } else { - message = "Bad Request"; - } - } else if (error.status === 401) { - message = "Unauthorized. Please login again."; - } else if (error.status === 403) { - message = - "Forbidden. You do not have permission to access this resource."; - } else if (error.status === 404) { - message = "Resource not found."; - } else if (error.status >= 500) { - message = "Server error. Please try again later."; - } - } - - errorService.showError(message, error.status); - return throwError(() => error); - }), - ); -}; diff --git a/frontend/src/core/interceptors/global-error.interceptor.ts b/frontend/src/core/interceptors/global-error.interceptor.ts index fdb3bad..875d5de 100644 --- a/frontend/src/core/interceptors/global-error.interceptor.ts +++ b/frontend/src/core/interceptors/global-error.interceptor.ts @@ -1,30 +1,45 @@ -import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http'; -import { inject } from '@angular/core'; -import { catchError, throwError } from 'rxjs'; -// Assuming a hypothetical ErrorService exists. Alternatively, use a UI library's Toast service. -import { ErrorService } from '@shared/services/error.service'; +import { HttpErrorResponse, HttpInterceptorFn } from "@angular/common/http"; +import { inject } from "@angular/core"; +import { catchError, throwError } from "rxjs"; +import { ErrorService } from "@shared/services"; export const globalErrorInterceptor: HttpInterceptorFn = (req, next) => { const errorService = inject(ErrorService); return next(req).pipe( catchError((error: HttpErrorResponse) => { - let errorMessage = 'An unknown error occurred!'; + let message = "An unexpected error occurred"; if (error.error instanceof ErrorEvent) { - // Client-side or network error - errorMessage = `Error: ${error.error.message}`; + // Client-side error + message = `Client Error: ${error.error.message}`; } else { - // Backend returns an unsuccessful response code - if (error.status >= 400 && error.status < 500) { - errorMessage = `Client Error (${error.status}): ${error.message}`; + // Backend error + if (error.status === 0) { + message = "Network error. Please check your internet connection."; + } else if (error.status === 400) { + // Check for Veil/NestJS validation array + if (Array.isArray(error.error.message)) { + message = error.error.message.join("\n"); + } else if (typeof error.error.message === "string") { + message = error.error.message; + } else { + message = "Bad Request"; + } + } else if (error.status === 401) { + message = "Unauthorized. Please login again."; + } else if (error.status === 403) { + message = + "Forbidden. You do not have permission to access this resource."; + } else if (error.status === 404) { + message = "Resource not found."; } else if (error.status >= 500) { - errorMessage = `Server Error (${error.status}): ${error.message}`; + message = "Server error. Please try again later."; } } - errorService.showError(errorMessage); // Assuming showError method shows a Toast + errorService.showError(message, error.status); return throwError(() => error); - }) + }), ); }; diff --git a/frontend/src/entities/admin-settings/admin-settings.service.ts b/frontend/src/entities/admin-settings/admin-settings.service.ts index 2cd0e46..574148c 100644 --- a/frontend/src/entities/admin-settings/admin-settings.service.ts +++ b/frontend/src/entities/admin-settings/admin-settings.service.ts @@ -1,7 +1,7 @@ import { Injectable, inject, signal } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable, tap } from "rxjs"; -import { API_ENDPOINTS } from "@core/constants/api-endpoints"; +import { API_ENDPOINTS } from "@core/constants"; import { AdminSettings } from "@shared/models/admin-settings.model"; @Injectable({ diff --git a/frontend/src/entities/gallery/gallery.service.ts b/frontend/src/entities/gallery/gallery.service.ts index d0edae2..6dbc34c 100644 --- a/frontend/src/entities/gallery/gallery.service.ts +++ b/frontend/src/entities/gallery/gallery.service.ts @@ -2,13 +2,14 @@ import { Injectable, inject, signal } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable, tap } from "rxjs"; import { Gallery } from "@shared/models"; +import { API_ENDPOINTS } from "@core/constants"; @Injectable({ providedIn: "root", }) export class GalleryService { private http = inject(HttpClient); - private apiUrl = "/gallery"; + private apiUrl = API_ENDPOINTS.GALLERY.BASE; // State private _images = signal([]); @@ -21,7 +22,7 @@ export class GalleryService { } getImage(id: string): Observable { - return this.http.get(`${this.apiUrl}/${id}`); + return this.http.get(API_ENDPOINTS.GALLERY.URL_BY_ID(id)); } // Use this for both create and update if sending full object, or create specific methods @@ -35,7 +36,7 @@ export class GalleryService { updateImage(id: string, formData: FormData): Observable { return this.http - .put(`${this.apiUrl}/${id}`, formData) + .put(API_ENDPOINTS.GALLERY.URL_BY_ID(id), formData) .pipe( tap((updatedImage) => this._images.update((imgs) => @@ -47,7 +48,7 @@ export class GalleryService { deleteImage(id: string): Observable { return this.http - .delete(`${this.apiUrl}/${id}`) + .delete(API_ENDPOINTS.GALLERY.URL_BY_ID(id)) .pipe( tap(() => this._images.update((imgs) => imgs.filter((img) => img.id !== id)), diff --git a/frontend/src/entities/treatments/treatments.service.ts b/frontend/src/entities/treatments/treatments.service.ts index e313d09..0ff969b 100644 --- a/frontend/src/entities/treatments/treatments.service.ts +++ b/frontend/src/entities/treatments/treatments.service.ts @@ -2,7 +2,7 @@ import { Injectable, inject, signal } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable, tap } from "rxjs"; import { TreatmentItem } from "@features/treatments"; -import { deleteArrayItemById, deleteProperties, excludeFormDataProperties, formDataExcludeProperty } from "@shared/lib"; +import { deleteArrayItemById, formDataExcludeProperty } from "@shared/lib"; import { API_ENDPOINTS } from "@core/constants"; @Injectable() diff --git a/frontend/src/entities/veil/veil.service.ts b/frontend/src/entities/veil/veil.service.ts index c79adc1..70bf355 100644 --- a/frontend/src/entities/veil/veil.service.ts +++ b/frontend/src/entities/veil/veil.service.ts @@ -1,11 +1,13 @@ -import { HttpClient } from "@angular/common/http"; import { Injectable, inject, signal } from "@angular/core"; -import { API_ENDPOINTS } from "@core/constants"; -import { Veil } from "@features/veil"; -import { deleteArrayItemById, formDataExcludeProperty } from "@shared/lib"; +import { HttpClient } from "@angular/common/http"; import { Observable, tap } from "rxjs"; +import { Veil } from "@features/veil"; +import { deleteArrayItemById, objectExcludePropety } from "@shared/lib"; +import { API_ENDPOINTS } from "@core/constants"; -@Injectable() +@Injectable({ + providedIn: "root", +}) export class VeilService { private http = inject(HttpClient); @@ -23,20 +25,17 @@ export class VeilService { return this.http.get(API_ENDPOINTS.VEILS.URL_BY_ID(id)); } - createVeil(veil: Omit | FormData): Observable { + createVeil(veil: FormData): Observable { return this.http .post(API_ENDPOINTS.VEILS.BASE, veil) .pipe( - tap((newVeil) => this._veils.update((veils) => [...veils, newVeil])), + tap((newVeil) => + this._veils.update((veils) => [...veils, newVeil]), + ), ); } updateVeil(id: string, veil: FormData): Observable { - const updatedVeil = formDataExcludeProperty(veil, [ - "id", - "createdAt", - "updatedAt", - ]); return this.http .put(API_ENDPOINTS.VEILS.URL_BY_ID(id), veil) .pipe( diff --git a/frontend/src/environments/environment.prod.ts b/frontend/src/environments/environment.prod.ts index ed7a9d6..6b5164d 100644 --- a/frontend/src/environments/environment.prod.ts +++ b/frontend/src/environments/environment.prod.ts @@ -1,5 +1,5 @@ export const environment = { - production: true, - apiUrl: 'http://localhost:4100', - telegramBotName: 'MavludaBeautyBot' + production: false, + apiUrl: "http://localhost:4200", + telegramBotName: "test_bot", }; diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts index 4898281..6b5164d 100644 --- a/frontend/src/environments/environment.ts +++ b/frontend/src/environments/environment.ts @@ -1,11 +1,5 @@ -export const environment: Environment = { +export const environment = { production: false, - apiUrl: "http://localhost:4100", + apiUrl: "http://localhost:4200", telegramBotName: "test_bot", }; - -export interface Environment { - production: boolean; - apiUrl: string; - telegramBotName: string; -} diff --git a/frontend/src/shared/models/service.model.ts b/frontend/src/shared/models/service.model.ts index f44503c..e260917 100644 --- a/frontend/src/shared/models/service.model.ts +++ b/frontend/src/shared/models/service.model.ts @@ -3,7 +3,7 @@ export interface Service { name: string; description: string; price: number; - durationMinutes: number; + duration: number; category: 'medical' | 'beauty'; createdAt: Date; }