Compare commits
5 Commits
c461d9bd5f
...
6231128288
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6231128288 | ||
|
|
fd8e7e1b28 | ||
|
|
000bb78112 | ||
|
|
d3d6632375 | ||
|
|
1e84d67e24 |
@@ -64,6 +64,8 @@
|
||||
@for (i of [1,2,3,4]; track i) { <app-skeleton shape="rect" height="40px" /> }
|
||||
<span class="sr-only">{{ 'common.loading' | translate }}</span>
|
||||
</div>
|
||||
} @else if (error) {
|
||||
<app-empty-state [title]="'common.errorTitle' | translate" [description]="'common.errorDescription' | translate" />
|
||||
} @else if (treeRows.length === 0) {
|
||||
<app-empty-state [title]="'adminCategories.emptyTitle' | translate" [description]="'adminCategories.emptyDescription' | translate">
|
||||
<span slot="actions">
|
||||
|
||||
@@ -51,6 +51,7 @@ export class AdminCategoriesListComponent {
|
||||
@Input() flatCategories: AdminCategory[] = [];
|
||||
@Input() filters!: { search: string; visibility: 'all' | 'visible' | 'hidden'; includeDeleted: boolean };
|
||||
@Input() loading = false;
|
||||
@Input() error: string | null = null;
|
||||
@Input() viewMode: AdminCategoriesViewMode = 'tree';
|
||||
@Input() density: AdminCategoriesDensity = 'comfortable';
|
||||
@Input() visibleColumns: AdminCategoryColumn[] = [...ALL_CATEGORY_COLUMNS];
|
||||
|
||||
@@ -131,11 +131,13 @@ export class AdminCategoriesFacade {
|
||||
readonly filters = signal<AdminCategoryListFilters>({ search: '', visibility: 'all', includeDeleted: false });
|
||||
readonly categories = signal<AdminCategory[]>([]);
|
||||
readonly loading = signal(false);
|
||||
readonly error = signal<string | null>(null);
|
||||
readonly draft = signal<AdminCategory | null>(null);
|
||||
readonly editorMode = signal<AdminCategoryEditorMode>('create');
|
||||
readonly dirty = signal(false);
|
||||
readonly slugTaken = signal(false);
|
||||
readonly slugCheckError = signal(false);
|
||||
readonly mutationError = signal<string | null>(null);
|
||||
private savedSnapshot: string | null = null;
|
||||
private draftStorageKey: string | null = null;
|
||||
|
||||
@@ -174,6 +176,7 @@ export class AdminCategoriesFacade {
|
||||
*/
|
||||
loadList(): void {
|
||||
this.loading.set(true);
|
||||
this.error.set(null);
|
||||
this.gateway.loadCategories({ search: '', visibility: 'all', includeDeleted: true }).pipe(take(1)).subscribe({
|
||||
next: categories => {
|
||||
this.categories.set(categories);
|
||||
@@ -182,6 +185,7 @@ export class AdminCategoriesFacade {
|
||||
error: () => {
|
||||
this.categories.set([]);
|
||||
this.loading.set(false);
|
||||
this.error.set('common.errorDescription');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -323,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);
|
||||
|
||||
@@ -339,7 +344,9 @@ export class AdminCategoriesFacade {
|
||||
this.savedSnapshot = JSON.stringify(saved);
|
||||
this.dirty.set(false);
|
||||
this.loadList();
|
||||
}
|
||||
onSuccess?.();
|
||||
},
|
||||
error: () => this.mutationError.set('common.errorDescription')
|
||||
});
|
||||
}
|
||||
|
||||
@@ -354,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 {
|
||||
|
||||
@@ -19,6 +19,7 @@ import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
||||
[allCategories]="facade.categories()"
|
||||
[filters]="facade.filters()"
|
||||
[loading]="facade.loading()"
|
||||
[error]="facade.error()"
|
||||
[viewMode]="facade.viewMode()"
|
||||
[density]="facade.density()"
|
||||
[visibleColumns]="facade.visibleColumns()"
|
||||
@@ -70,6 +71,14 @@ import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
||||
<app-button variant="primary" (click)="deleteBlockedMessage.set(null)">{{ 'common.confirm' | translate }}</app-button>
|
||||
</div>
|
||||
</app-dialog>
|
||||
}
|
||||
@if (facade.mutationError()) {
|
||||
<app-dialog [open]="true" [titleText]="'common.errorTitle' | translate" size="sm" (closed)="facade.mutationError.set(null)">
|
||||
<p>{{ 'common.errorDescription' | translate }}</p>
|
||||
<div class="app-confirm-dialog__actions">
|
||||
<app-button variant="primary" (click)="facade.mutationError.set(null)">{{ 'common.confirm' | translate }}</app-button>
|
||||
</div>
|
||||
</app-dialog>
|
||||
}`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
|
||||
@@ -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) {<main class="editor-page"><header><h1>{{ title() | translate }}</h1></header><app-admin-category-form [category]="draft" [parentOptions]="parentOptions()" [breadcrumb]="facade.breadcrumbFor(draft.parentId)" [children]="facade.childrenOf(draft.id)" [slugTaken]="facade.slugTaken()" [slugCheckError]="facade.slugCheckError()" [locales]="facade.supportedLocales()" [mode]="facade.editorMode()" [health]="facade.health(draft)" (categoryChange)="facade.updateDraft($event)" (saveDraft)="save(false)" (publish)="save(true)" /></main>} @else {<main class="editor-page"><p>{{ 'common.loading' | translate }}</p></main>}`,
|
||||
imports: [AdminCategoryFormComponent, TranslatePipe, DialogComponent, ButtonComponent],
|
||||
template: `@if (facade.draft(); as draft) {<main class="editor-page"><header><h1>{{ title() | translate }}</h1></header><app-admin-category-form [category]="draft" [parentOptions]="parentOptions()" [breadcrumb]="facade.breadcrumbFor(draft.parentId)" [children]="facade.childrenOf(draft.id)" [slugTaken]="facade.slugTaken()" [slugCheckError]="facade.slugCheckError()" [locales]="facade.supportedLocales()" [mode]="facade.editorMode()" [health]="facade.health(draft)" (categoryChange)="facade.updateDraft($event)" (saveDraft)="save(false)" (publish)="save(true)" /></main>} @else {<main class="editor-page"><p>{{ 'common.loading' | translate }}</p></main>}
|
||||
@if (facade.mutationError()) {
|
||||
<app-dialog [open]="true" [titleText]="'common.errorTitle' | translate" size="sm" (closed)="facade.mutationError.set(null)">
|
||||
<p>{{ 'common.errorDescription' | translate }}</p>
|
||||
<div class="app-confirm-dialog__actions">
|
||||
<app-button variant="primary" (click)="facade.mutationError.set(null)">{{ 'common.confirm' | translate }}</app-button>
|
||||
</div>
|
||||
</app-dialog>
|
||||
}`,
|
||||
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);
|
||||
this.facade.saveDraft(publish, () => {
|
||||
void this.router.navigate([this.languageService.currentLanguage(), 'backoffice', 'categories']);
|
||||
});
|
||||
}
|
||||
|
||||
private descendantIds(id: string): string[] {
|
||||
|
||||
@@ -10,8 +10,11 @@ export class AdminCustomersFacade {
|
||||
|
||||
readonly customers = signal<AdminCustomer[]>([]);
|
||||
readonly loading = signal(false);
|
||||
readonly error = signal<string | null>(null);
|
||||
readonly search = signal('');
|
||||
readonly selected = signal<AdminCustomer | null>(null);
|
||||
readonly selectedLoading = signal(false);
|
||||
readonly selectedError = signal<string | null>(null);
|
||||
|
||||
private buildCustomers(orders: AdminOrder[]): AdminCustomer[] {
|
||||
const byEmail = new Map<string, AdminOrder[]>();
|
||||
@@ -41,6 +44,7 @@ export class AdminCustomersFacade {
|
||||
|
||||
loadList(): void {
|
||||
this.loading.set(true);
|
||||
this.error.set(null);
|
||||
this.ordersGateway.loadOrders({ search: '', status: 'all', page: 1, pageSize: 100000 }).pipe(take(1)).subscribe({
|
||||
next: result => {
|
||||
this.customers.set(this.buildCustomers(result.items));
|
||||
@@ -49,16 +53,24 @@ export class AdminCustomersFacade {
|
||||
error: () => {
|
||||
this.customers.set([]);
|
||||
this.loading.set(false);
|
||||
this.error.set('common.errorDescription');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadDetail(email: string): void {
|
||||
this.selectedLoading.set(true);
|
||||
this.selectedError.set(null);
|
||||
this.ordersGateway.loadOrders({ search: '', status: 'all', page: 1, pageSize: 100000 }).pipe(take(1)).subscribe({
|
||||
next: result => {
|
||||
const decoded = decodeURIComponent(email);
|
||||
const customers = this.buildCustomers(result.items);
|
||||
this.selected.set(customers.find(customer => customer.email === decoded) ?? null);
|
||||
this.selectedLoading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
this.selectedLoading.set(false);
|
||||
this.selectedError.set('common.errorDescription');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,6 +48,12 @@
|
||||
<app-order-timeline [entries]="activity()" [showOrderNumber]="true" />
|
||||
</section>
|
||||
</main>
|
||||
} @else if (facade.selectedError()) {
|
||||
<div class="customer-detail-error" role="alert">
|
||||
<p>{{ 'common.errorTitle' | translate }}</p>
|
||||
<p>{{ 'common.errorDescription' | translate }}</p>
|
||||
<app-button variant="secondary" size="sm" (click)="back()">{{ 'adminCustomers.back' | translate }}</app-button>
|
||||
</div>
|
||||
} @else {
|
||||
<p>{{ 'common.loading' | translate }}</p>
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
@for (i of [1,2,3,4]; track i) { <app-skeleton shape="rect" height="40px" /> }
|
||||
<span class="sr-only">{{ 'common.loading' | translate }}</span>
|
||||
</div>
|
||||
} @else if (facade.error()) {
|
||||
<app-empty-state [title]="'common.errorTitle' | translate" [description]="'common.errorDescription' | translate" />
|
||||
} @else if (facade.filteredCustomers().length === 0) {
|
||||
<app-empty-state [title]="'adminCustomers.emptyTitle' | translate" [description]="'adminCustomers.emptyDescription' | translate" />
|
||||
} @else {
|
||||
|
||||
@@ -37,7 +37,10 @@ export class AdminOrdersFacade {
|
||||
readonly orders = signal<AdminOrder[]>([]);
|
||||
readonly total = signal(0);
|
||||
readonly loading = signal(false);
|
||||
readonly error = signal<string | null>(null);
|
||||
readonly selected = signal<AdminOrder | null>(null);
|
||||
readonly selectedLoading = signal(false);
|
||||
readonly selectedError = signal<string | null>(null);
|
||||
|
||||
readonly viewMode = signal<AdminOrdersViewMode>((this.localStorage.getItem(VIEW_MODE_KEY) as AdminOrdersViewMode) || 'table');
|
||||
readonly density = signal<AdminOrdersDensity>((this.localStorage.getItem(DENSITY_KEY) as AdminOrdersDensity) || 'comfortable');
|
||||
@@ -76,6 +79,7 @@ export class AdminOrdersFacade {
|
||||
|
||||
loadList(): void {
|
||||
this.loading.set(true);
|
||||
this.error.set(null);
|
||||
this.gateway.loadOrders(this.filters()).pipe(take(1)).subscribe({
|
||||
next: result => {
|
||||
this.orders.set(result.items);
|
||||
@@ -86,6 +90,7 @@ export class AdminOrdersFacade {
|
||||
this.orders.set([]);
|
||||
this.total.set(0);
|
||||
this.loading.set(false);
|
||||
this.error.set('common.errorDescription');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -96,7 +101,18 @@ export class AdminOrdersFacade {
|
||||
}
|
||||
|
||||
loadDetail(id: string): void {
|
||||
this.gateway.loadOrder(id).pipe(take(1)).subscribe({ next: order => this.selected.set(order) });
|
||||
this.selectedLoading.set(true);
|
||||
this.selectedError.set(null);
|
||||
this.gateway.loadOrder(id).pipe(take(1)).subscribe({
|
||||
next: order => {
|
||||
this.selected.set(order);
|
||||
this.selectedLoading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
this.selectedLoading.set(false);
|
||||
this.selectedError.set('common.errorDescription');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setStatus(id: string, status: AdminOrderStatus): void {
|
||||
|
||||
@@ -103,6 +103,12 @@
|
||||
(confirmed)="confirmRefund()"
|
||||
(cancelled)="pendingRefundId.set(null)" />
|
||||
</main>
|
||||
} @else if (facade.selectedError()) {
|
||||
<div class="order-detail-error" role="alert">
|
||||
<p>{{ 'common.errorTitle' | translate }}</p>
|
||||
<p>{{ 'common.errorDescription' | translate }}</p>
|
||||
<app-button variant="secondary" size="sm" (click)="back()">{{ 'adminOrders.back' | translate }}</app-button>
|
||||
</div>
|
||||
} @else {
|
||||
<p>{{ 'common.loading' | translate }}</p>
|
||||
}
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
@for (i of [1,2,3,4]; track i) { <app-skeleton shape="rect" height="40px" /> }
|
||||
<span class="sr-only">{{ 'common.loading' | translate }}</span>
|
||||
</div>
|
||||
} @else if (facade.error()) {
|
||||
<app-empty-state [title]="'common.errorTitle' | translate" [description]="'common.errorDescription' | translate" />
|
||||
} @else if (facade.orders().length === 0) {
|
||||
<app-empty-state [title]="'adminOrders.emptyTitle' | translate" [description]="'adminOrders.emptyDescription' | translate" />
|
||||
} @else {
|
||||
|
||||
@@ -78,6 +78,8 @@
|
||||
}
|
||||
<span class="sr-only">{{ 'common.loading' | translate }}</span>
|
||||
</div>
|
||||
} @else if (error) {
|
||||
<app-empty-state [title]="'common.errorTitle' | translate" [description]="'common.errorDescription' | translate" />
|
||||
} @else if (products.length === 0) {
|
||||
<app-empty-state [title]="'adminProducts.emptyTitle' | translate" [description]="'adminProducts.emptyDescription' | translate">
|
||||
<span slot="actions">
|
||||
|
||||
@@ -44,6 +44,7 @@ export class AdminProductsListComponent {
|
||||
@Input() total = 0;
|
||||
@Input() selectedIds: string[] = [];
|
||||
@Input() loading = false;
|
||||
@Input() error: string | null = null;
|
||||
@Input() infiniteScroll = false;
|
||||
@Input() viewMode: AdminProductsViewMode = 'table';
|
||||
@Input() density: AdminProductsDensity = 'comfortable';
|
||||
|
||||
@@ -91,6 +91,8 @@ export class AdminProductsFacade {
|
||||
readonly infiniteScroll = signal(false);
|
||||
readonly categories = signal<AdminProductCategoryOption[]>([]);
|
||||
readonly loading = signal(false);
|
||||
readonly error = signal<string | null>(null);
|
||||
readonly mutationError = signal<string | null>(null);
|
||||
readonly selectedIds = signal<string[]>([]);
|
||||
readonly draft = signal<AdminProduct | null>(null);
|
||||
readonly dirty = signal(false);
|
||||
@@ -100,6 +102,7 @@ export class AdminProductsFacade {
|
||||
|
||||
loadList(): void {
|
||||
this.loading.set(true);
|
||||
this.error.set(null);
|
||||
this.gateway.loadProducts(this.filters()).pipe(take(1)).subscribe({
|
||||
next: result => {
|
||||
this.products.set(result.items);
|
||||
@@ -110,6 +113,7 @@ export class AdminProductsFacade {
|
||||
this.products.set([]);
|
||||
this.total.set(0);
|
||||
this.loading.set(false);
|
||||
this.error.set('common.errorDescription');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -302,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')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {<main class="editor-page"><header><h1>{{ title() | translate }}</h1></header><app-admin-product-form [product]="draft" [categories]="facade.categories()" [allProducts]="facade.products()" [locales]="facade.supportedLocales()" [mode]="facade.editorMode()" [health]="facade.health(draft)" (productChange)="facade.updateDraft($event)" (save)="save()" /></main>} @else {<main class="editor-page"><p>{{ 'common.loading' | translate }}</p></main>}`,
|
||||
imports: [AdminProductFormComponent, TranslatePipe, DialogComponent, ButtonComponent],
|
||||
template: `@if (facade.draft(); as draft) {<main class="editor-page"><header><h1>{{ title() | translate }}</h1></header><app-admin-product-form [product]="draft" [categories]="facade.categories()" [allProducts]="facade.products()" [locales]="facade.supportedLocales()" [mode]="facade.editorMode()" [health]="facade.health(draft)" (productChange)="facade.updateDraft($event)" (save)="save()" /></main>} @else {<main class="editor-page"><p>{{ 'common.loading' | translate }}</p></main>}
|
||||
@if (facade.mutationError()) {
|
||||
<app-dialog [open]="true" [titleText]="'common.errorTitle' | translate" size="sm" (closed)="facade.mutationError.set(null)">
|
||||
<p>{{ 'common.errorDescription' | translate }}</p>
|
||||
<div class="app-confirm-dialog__actions">
|
||||
<app-button variant="primary" (click)="facade.mutationError.set(null)">{{ 'common.confirm' | translate }}</app-button>
|
||||
</div>
|
||||
</app-dialog>
|
||||
}`,
|
||||
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();
|
||||
this.facade.saveDraft(() => {
|
||||
void this.router.navigate([this.languageService.currentLanguage(), 'backoffice', 'products']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: `<app-admin-products-list
|
||||
[products]="facade.products()"
|
||||
[categories]="facade.categories()"
|
||||
@@ -17,6 +19,7 @@ import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
||||
[total]="facade.total()"
|
||||
[selectedIds]="facade.selectedIds()"
|
||||
[loading]="facade.loading()"
|
||||
[error]="facade.error()"
|
||||
[infiniteScroll]="facade.infiniteScroll()"
|
||||
[viewMode]="facade.viewMode()"
|
||||
[density]="facade.density()"
|
||||
@@ -56,7 +59,15 @@ import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
||||
[message]="'adminProducts.confirmBulkDelete' | translate"
|
||||
[destructive]="true"
|
||||
(confirmed)="confirmBulkDelete()"
|
||||
(cancelled)="bulkDeleteConfirmOpen.set(false)" />`,
|
||||
(cancelled)="bulkDeleteConfirmOpen.set(false)" />
|
||||
@if (facade.mutationError()) {
|
||||
<app-dialog [open]="true" [titleText]="'common.errorTitle' | translate" size="sm" (closed)="facade.mutationError.set(null)">
|
||||
<p>{{ 'common.errorDescription' | translate }}</p>
|
||||
<div class="app-confirm-dialog__actions">
|
||||
<app-button variant="primary" (click)="facade.mutationError.set(null)">{{ 'common.confirm' | translate }}</app-button>
|
||||
</div>
|
||||
</app-dialog>
|
||||
}`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class AdminProductsListPageComponent {
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
}
|
||||
<span class="sr-only">{{ 'common.loading' | translate }}</span>
|
||||
</div>
|
||||
} @else if (facade.error()) {
|
||||
<app-empty-state [title]="'common.errorTitle' | translate" [description]="'common.errorDescription' | translate" />
|
||||
} @else {
|
||||
<div class="report-grid">
|
||||
<div class="report-card">
|
||||
|
||||
@@ -3,11 +3,12 @@ import { AdminAnalyticsFacade } from '../../analytics/facade/admin-analytics.fac
|
||||
import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
||||
import { ButtonComponent } from '../../../../shared/ui/button/button.component';
|
||||
import { SkeletonComponent } from '../../../../shared/ui/skeleton/skeleton.component';
|
||||
import { EmptyStateComponent } from '../../../../shared/ui/empty-state/empty-state.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-reports-page',
|
||||
standalone: true,
|
||||
imports: [TranslatePipe, ButtonComponent, SkeletonComponent],
|
||||
imports: [TranslatePipe, ButtonComponent, SkeletonComponent, EmptyStateComponent],
|
||||
templateUrl: './admin-reports-page.component.html',
|
||||
styleUrls: ['./admin-reports-page.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
||||
@@ -11,9 +11,11 @@ export class AdminTransactionsFacade {
|
||||
readonly transactions = signal<AdminTransaction[]>([]);
|
||||
readonly total = signal(0);
|
||||
readonly loading = signal(false);
|
||||
readonly error = signal<string | null>(null);
|
||||
|
||||
loadList(): void {
|
||||
this.loading.set(true);
|
||||
this.error.set(null);
|
||||
this.gateway.loadTransactions(this.filters()).pipe(take(1)).subscribe({
|
||||
next: result => {
|
||||
this.transactions.set(result.items);
|
||||
@@ -24,6 +26,7 @@ export class AdminTransactionsFacade {
|
||||
this.transactions.set([]);
|
||||
this.total.set(0);
|
||||
this.loading.set(false);
|
||||
this.error.set('common.errorDescription');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
@for (i of [1,2,3,4]; track i) { <app-skeleton shape="rect" height="40px" /> }
|
||||
<span class="sr-only">{{ 'common.loading' | translate }}</span>
|
||||
</div>
|
||||
} @else if (facade.error()) {
|
||||
<app-empty-state [title]="'common.errorTitle' | translate" [description]="'common.errorDescription' | translate" />
|
||||
} @else if (facade.transactions().length === 0) {
|
||||
<app-empty-state [title]="'adminTransactions.emptyTitle' | translate" [description]="'adminTransactions.emptyDescription' | translate" />
|
||||
} @else {
|
||||
|
||||
@@ -12,6 +12,7 @@ export class AdminUsersFacade {
|
||||
readonly invitations = signal<AdminInvitation[]>([]);
|
||||
readonly loading = signal(false);
|
||||
readonly error = signal(false);
|
||||
readonly mutationError = signal<string | null>(null);
|
||||
readonly sessionsTarget = signal<AdminUser | null>(null);
|
||||
readonly sessions = signal<AdminSession[]>([]);
|
||||
readonly auditTarget = signal<AdminUser | null>(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 {
|
||||
|
||||
@@ -128,4 +128,13 @@
|
||||
[destructive]="true"
|
||||
(confirmed)="confirmSuspend()"
|
||||
(cancelled)="pendingSuspendUserId.set(null)" />
|
||||
|
||||
@if (facade.mutationError()) {
|
||||
<app-dialog [open]="true" [titleText]="'common.errorTitle' | translate" size="sm" (closed)="facade.mutationError.set(null)">
|
||||
<p>{{ 'common.errorDescription' | translate }}</p>
|
||||
<div class="app-confirm-dialog__actions">
|
||||
<app-button variant="primary" (click)="facade.mutationError.set(null)">{{ 'common.confirm' | translate }}</app-button>
|
||||
</div>
|
||||
</app-dialog>
|
||||
}
|
||||
</section>
|
||||
|
||||
@@ -6,6 +6,14 @@
|
||||
<app-skeleton shape="text" height="16px" />
|
||||
<app-skeleton shape="text" width="70%" height="16px" />
|
||||
</section>
|
||||
} @else if (error()) {
|
||||
<section class="static-page__state">
|
||||
<app-empty-state [title]="'common.errorTitle' | translate" [description]="'common.errorDescription' | translate">
|
||||
<div slot="actions">
|
||||
<app-button variant="primary" [routerLink]="homeRoute()">{{ 'staticPages.backHome' | translate }}</app-button>
|
||||
</div>
|
||||
</app-empty-state>
|
||||
</section>
|
||||
} @else if (notFound()) {
|
||||
<section class="static-page__state">
|
||||
<app-empty-state title="404" [description]="'staticPages.notFound' | translate">
|
||||
|
||||
@@ -29,6 +29,7 @@ export class StaticPageComponent {
|
||||
|
||||
readonly loading = signal(true);
|
||||
readonly notFound = signal(false);
|
||||
readonly error = signal(false);
|
||||
readonly title = signal('');
|
||||
readonly homeRoute = signal('');
|
||||
readonly dir = signal<'ltr' | 'rtl'>('ltr');
|
||||
@@ -76,21 +77,32 @@ export class StaticPageComponent {
|
||||
private loadByKey(key: string): void {
|
||||
this.loading.set(true);
|
||||
this.notFound.set(false);
|
||||
this.error.set(false);
|
||||
|
||||
this.staticPageResolver.resolveByKey(key, this.languageService.currentLanguage()).subscribe(page => {
|
||||
this.applyPage(page?.title ?? '', page?.html ?? '', !page);
|
||||
this.staticPageResolver.resolveByKey(key, this.languageService.currentLanguage()).subscribe({
|
||||
next: page => this.applyPage(page?.title ?? '', page?.html ?? '', !page),
|
||||
error: () => this.applyError()
|
||||
});
|
||||
}
|
||||
|
||||
private loadByPath(path: string): void {
|
||||
this.loading.set(true);
|
||||
this.notFound.set(false);
|
||||
this.error.set(false);
|
||||
|
||||
this.staticPageResolver.resolveByRoute(path, this.languageService.currentLanguage()).subscribe(page => {
|
||||
this.applyPage(page?.title ?? '', page?.html ?? '', !page);
|
||||
this.staticPageResolver.resolveByRoute(path, this.languageService.currentLanguage()).subscribe({
|
||||
next: page => this.applyPage(page?.title ?? '', page?.html ?? '', !page),
|
||||
error: () => this.applyError()
|
||||
});
|
||||
}
|
||||
|
||||
private applyError(): void {
|
||||
this.title.set('');
|
||||
this.safeHtml.set(this.sanitizer.bypassSecurityTrustHtml(''));
|
||||
this.error.set(true);
|
||||
this.loading.set(false);
|
||||
}
|
||||
|
||||
private applyPage(title: string, html: string, notFound: boolean): void {
|
||||
if (notFound) {
|
||||
this.title.set('');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable, of, map, switchMap } from 'rxjs';
|
||||
import { Observable, of, map, switchMap, catchError } from 'rxjs';
|
||||
import { CategoryFacade } from '../../facades/platform/category.facade';
|
||||
import { ProductFacade } from '../../facades/platform/product.facade';
|
||||
import { LanguageService } from '../../services/language.service';
|
||||
@@ -19,7 +19,11 @@ export class DataSourceResolverService {
|
||||
|
||||
resolve(widget: WidgetConfig, section: SectionConfig): Observable<unknown> {
|
||||
return this.widgetManifest.getWidget(widget.type).pipe(
|
||||
switchMap((definition) => this.resolveByDefinition(definition, widget, section))
|
||||
switchMap((definition) => this.resolveByDefinition(definition, widget, section)),
|
||||
catchError((error) => {
|
||||
console.error(`Failed to resolve widget data for ${widget.type}:${widget.id}`, error);
|
||||
return of({ section, settings: {} });
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user