diff --git a/src/app/features/admin/categories/facade/admin-categories.facade.ts b/src/app/features/admin/categories/facade/admin-categories.facade.ts index ff9347f..0101102 100644 --- a/src/app/features/admin/categories/facade/admin-categories.facade.ts +++ b/src/app/features/admin/categories/facade/admin-categories.facade.ts @@ -137,6 +137,7 @@ export class AdminCategoriesFacade { readonly dirty = signal(false); readonly slugTaken = signal(false); readonly slugCheckError = signal(false); + readonly mutationError = signal(null); private savedSnapshot: string | null = null; private draftStorageKey: string | null = null; @@ -326,10 +327,11 @@ export class AdminCategoriesFacade { }); } - saveDraft(publish: boolean): void { + saveDraft(publish: boolean, onSuccess?: () => void): void { const draft = this.draft(); if (!draft || this.slugTaken() || this.slugCheckError()) return; + this.mutationError.set(null); const toSave: AdminCategory = { ...draft, status: publish ? 'published' : 'draft', updatedAt: new Date().toISOString() }; const request = this.editorMode() === 'create' ? this.gateway.createCategory(toSave) : this.gateway.updateCategory(toSave); @@ -342,7 +344,9 @@ export class AdminCategoriesFacade { this.savedSnapshot = JSON.stringify(saved); this.dirty.set(false); this.loadList(); - } + onSuccess?.(); + }, + error: () => this.mutationError.set('common.errorDescription') }); } @@ -357,7 +361,11 @@ export class AdminCategoriesFacade { } deleteOne(id: string): void { - this.gateway.deleteCategory(id).pipe(take(1)).subscribe({ next: () => { this.loadList(); this.loadDashboardStats(); } }); + this.mutationError.set(null); + this.gateway.deleteCategory(id).pipe(take(1)).subscribe({ + next: () => { this.loadList(); this.loadDashboardStats(); }, + error: () => this.mutationError.set('common.errorDescription') + }); } restoreOne(id: string): void { diff --git a/src/app/features/admin/categories/pages/admin-categories-list-page.component.ts b/src/app/features/admin/categories/pages/admin-categories-list-page.component.ts index 7306184..7ac61c3 100644 --- a/src/app/features/admin/categories/pages/admin-categories-list-page.component.ts +++ b/src/app/features/admin/categories/pages/admin-categories-list-page.component.ts @@ -71,6 +71,14 @@ import { TranslatePipe } from '../../../../i18n/translate.pipe'; {{ 'common.confirm' | translate }} + } + @if (facade.mutationError()) { + +

{{ 'common.errorDescription' | translate }}

+
+ {{ 'common.confirm' | translate }} +
+
}`, changeDetection: ChangeDetectionStrategy.OnPush }) diff --git a/src/app/features/admin/categories/pages/admin-category-editor-page.component.ts b/src/app/features/admin/categories/pages/admin-category-editor-page.component.ts index 8ce2499..725ec56 100644 --- a/src/app/features/admin/categories/pages/admin-category-editor-page.component.ts +++ b/src/app/features/admin/categories/pages/admin-category-editor-page.component.ts @@ -4,12 +4,22 @@ import { AdminCategoriesFacade } from '../facade/admin-categories.facade'; import { AdminCategoryFormComponent } from '../components/admin-category-form.component'; import { TranslatePipe } from '../../../../i18n/translate.pipe'; import { LanguageService } from '../../../../services/language.service'; +import { DialogComponent } from '../../../../shared/ui/dialog/dialog.component'; +import { ButtonComponent } from '../../../../shared/ui/button/button.component'; @Component({ selector: 'app-admin-category-editor-page', standalone: true, - imports: [AdminCategoryFormComponent, TranslatePipe], - template: `@if (facade.draft(); as draft) {

{{ title() | translate }}

} @else {

{{ 'common.loading' | translate }}

}`, + imports: [AdminCategoryFormComponent, TranslatePipe, DialogComponent, ButtonComponent], + template: `@if (facade.draft(); as draft) {

{{ title() | translate }}

} @else {

{{ 'common.loading' | translate }}

} + @if (facade.mutationError()) { + +

{{ 'common.errorDescription' | translate }}

+
+ {{ 'common.confirm' | translate }} +
+
+ }`, styles: [`.editor-page { max-width: 1120px; margin: 0 auto; padding: 24px; display: grid; gap: 16px; } .editor-page h1, .editor-page p { margin: 0; }`], changeDetection: ChangeDetectionStrategy.OnPush }) @@ -41,8 +51,9 @@ export class AdminCategoryEditorPageComponent { } save(publish: boolean): void { - this.facade.saveDraft(publish); - void this.router.navigate([this.languageService.currentLanguage(), 'backoffice', 'categories']); + this.facade.saveDraft(publish, () => { + void this.router.navigate([this.languageService.currentLanguage(), 'backoffice', 'categories']); + }); } private descendantIds(id: string): string[] { diff --git a/src/app/features/admin/products/facade/admin-products.facade.ts b/src/app/features/admin/products/facade/admin-products.facade.ts index f7bc50b..abffd3c 100644 --- a/src/app/features/admin/products/facade/admin-products.facade.ts +++ b/src/app/features/admin/products/facade/admin-products.facade.ts @@ -92,6 +92,7 @@ export class AdminProductsFacade { readonly categories = signal([]); readonly loading = signal(false); readonly error = signal(null); + readonly mutationError = signal(null); readonly selectedIds = signal([]); readonly draft = signal(null); readonly dirty = signal(false); @@ -305,18 +306,26 @@ export class AdminProductsFacade { this.dirty.set(true); } - saveDraft(): void { + saveDraft(onSuccess?: () => void): void { const draft = this.draft(); if (!draft) return; + this.mutationError.set(null); const request = this.editorMode() === 'create' ? this.gateway.createProduct(draft) : this.gateway.updateProduct(draft); - request.pipe(take(1)).subscribe({ next: () => { this.dirty.set(false); this.loadList(); } }); + request.pipe(take(1)).subscribe({ + next: () => { this.dirty.set(false); this.loadList(); onSuccess?.(); }, + error: () => this.mutationError.set('common.errorDescription') + }); } deleteOne(id: string): void { - this.gateway.deleteProduct(id).pipe(take(1)).subscribe({ next: () => this.loadList() }); + this.mutationError.set(null); + this.gateway.deleteProduct(id).pipe(take(1)).subscribe({ + next: () => this.loadList(), + error: () => this.mutationError.set('common.errorDescription') + }); } } diff --git a/src/app/features/admin/products/pages/admin-product-editor-page.component.ts b/src/app/features/admin/products/pages/admin-product-editor-page.component.ts index 643a7fe..5b5c3fb 100644 --- a/src/app/features/admin/products/pages/admin-product-editor-page.component.ts +++ b/src/app/features/admin/products/pages/admin-product-editor-page.component.ts @@ -4,12 +4,22 @@ import { AdminProductsFacade } from '../facade/admin-products.facade'; import { AdminProductFormComponent } from '../components/admin-product-form.component'; import { TranslatePipe } from '../../../../i18n/translate.pipe'; import { LanguageService } from '../../../../services/language.service'; +import { DialogComponent } from '../../../../shared/ui/dialog/dialog.component'; +import { ButtonComponent } from '../../../../shared/ui/button/button.component'; @Component({ selector: 'app-admin-product-editor-page', standalone: true, - imports: [AdminProductFormComponent, TranslatePipe], - template: `@if (facade.draft(); as draft) {

{{ title() | translate }}

} @else {

{{ 'common.loading' | translate }}

}`, + imports: [AdminProductFormComponent, TranslatePipe, DialogComponent, ButtonComponent], + template: `@if (facade.draft(); as draft) {

{{ title() | translate }}

} @else {

{{ 'common.loading' | translate }}

} + @if (facade.mutationError()) { + +

{{ 'common.errorDescription' | translate }}

+
+ {{ 'common.confirm' | translate }} +
+
+ }`, styles: [`.editor-page { max-width: 1120px; margin: 0 auto; padding: 24px; display: grid; gap: 16px; } .editor-page h1, .editor-page p { margin: 0; }`], changeDetection: ChangeDetectionStrategy.OnPush }) @@ -33,7 +43,8 @@ export class AdminProductEditorPageComponent { } save(): void { - this.facade.saveDraft(); - void this.router.navigate([this.languageService.currentLanguage(), 'backoffice', 'products']); + this.facade.saveDraft(() => { + void this.router.navigate([this.languageService.currentLanguage(), 'backoffice', 'products']); + }); } } diff --git a/src/app/features/admin/products/pages/admin-products-list-page.component.ts b/src/app/features/admin/products/pages/admin-products-list-page.component.ts index 3676f3c..334b10f 100644 --- a/src/app/features/admin/products/pages/admin-products-list-page.component.ts +++ b/src/app/features/admin/products/pages/admin-products-list-page.component.ts @@ -5,11 +5,13 @@ import { AdminProductsListComponent } from '../components/admin-products-list.co import { LanguageService } from '../../../../services/language.service'; import { ConfirmDialogComponent } from '../../../../shared/ui/confirm-dialog/confirm-dialog.component'; import { TranslatePipe } from '../../../../i18n/translate.pipe'; +import { DialogComponent } from '../../../../shared/ui/dialog/dialog.component'; +import { ButtonComponent } from '../../../../shared/ui/button/button.component'; @Component({ selector: 'app-admin-products-list-page', standalone: true, - imports: [AdminProductsListComponent, ConfirmDialogComponent, TranslatePipe], + imports: [AdminProductsListComponent, ConfirmDialogComponent, TranslatePipe, DialogComponent, ButtonComponent], template: ``, + (cancelled)="bulkDeleteConfirmOpen.set(false)" /> + @if (facade.mutationError()) { + +

{{ 'common.errorDescription' | translate }}

+
+ {{ 'common.confirm' | translate }} +
+
+ }`, changeDetection: ChangeDetectionStrategy.OnPush }) export class AdminProductsListPageComponent { diff --git a/src/app/features/admin/users/facade/admin-users.facade.ts b/src/app/features/admin/users/facade/admin-users.facade.ts index 3b95261..aeb89d2 100644 --- a/src/app/features/admin/users/facade/admin-users.facade.ts +++ b/src/app/features/admin/users/facade/admin-users.facade.ts @@ -12,6 +12,7 @@ export class AdminUsersFacade { readonly invitations = signal([]); readonly loading = signal(false); readonly error = signal(false); + readonly mutationError = signal(null); readonly sessionsTarget = signal(null); readonly sessions = signal([]); readonly auditTarget = signal(null); @@ -33,16 +34,28 @@ export class AdminUsersFacade { } setRole(userId: string, roleId: string): void { - this.gateway.setUserRole(userId, roleId).pipe(take(1)).subscribe({ next: () => this.loadAll() }); + this.mutationError.set(null); + this.gateway.setUserRole(userId, roleId).pipe(take(1)).subscribe({ + next: () => this.loadAll(), + error: () => this.mutationError.set('common.errorDescription') + }); } setStatus(userId: string, status: AdminUserStatus): void { - this.gateway.setUserStatus(userId, status).pipe(take(1)).subscribe({ next: () => this.loadAll() }); + this.mutationError.set(null); + this.gateway.setUserStatus(userId, status).pipe(take(1)).subscribe({ + next: () => this.loadAll(), + error: () => this.mutationError.set('common.errorDescription') + }); } invite(email: string, roleId: string, scope: AdminUserScope): void { if (!email.trim()) return; - this.gateway.inviteUser(email.trim(), roleId, scope).pipe(take(1)).subscribe({ next: () => this.loadAll() }); + this.mutationError.set(null); + this.gateway.inviteUser(email.trim(), roleId, scope).pipe(take(1)).subscribe({ + next: () => this.loadAll(), + error: () => this.mutationError.set('common.errorDescription') + }); } revokeInvitation(id: string): void { diff --git a/src/app/features/admin/users/pages/admin-users-page.component.html b/src/app/features/admin/users/pages/admin-users-page.component.html index 0672c20..42b4ca1 100644 --- a/src/app/features/admin/users/pages/admin-users-page.component.html +++ b/src/app/features/admin/users/pages/admin-users-page.component.html @@ -128,4 +128,13 @@ [destructive]="true" (confirmed)="confirmSuspend()" (cancelled)="pendingSuspendUserId.set(null)" /> + + @if (facade.mutationError()) { + +

{{ 'common.errorDescription' | translate }}

+
+ {{ 'common.confirm' | translate }} +
+
+ }