diff --git a/src/app/core/error-handler/README.md b/src/app/core/error-handler/README.md new file mode 100644 index 0000000000..6f1558e56a --- /dev/null +++ b/src/app/core/error-handler/README.md @@ -0,0 +1,384 @@ +# Error Handler Service + +## Overview + +The `ErrorHandlerService` is a centralized error-handling service for the Mifos X Web App. It provides consistent, user-friendly error messaging across the application by converting HTTP errors into meaningful messages and displaying them through Material Design snackbars. + +## How It Works + +### Trigger Mechanism + +The `ErrorHandlerService` is **manually triggered** by components and services using RxJS's `catchError` operator. It works **alongside** (not replacing) the existing `ErrorHandlerInterceptor`: + +#### Existing Interceptor (`error-handler.interceptor.ts`) + +- Automatically intercepts **all** HTTP errors globally +- Shows generic error dialog for unhandled errors +- Cannot provide context-specific messages +- Located at: `src/app/core/http/error-handler.interceptor.ts` + +#### ErrorHandlerService (This Service) + +- **Manually invoked** per API call +- Provides **context-specific** error messages +- Displays user-friendly snackbar notifications +- Extracts Fineract-specific error details +- Gives developers fine-grained control + +### When to Use + +Use `ErrorHandlerService` when you need: + +- Custom error messages for specific operations +- Context-aware error handling (e.g., "User not found", "Loan approval failed") +- Snackbar notifications instead of modal dialogs +- To extract and display Fineract API error messages + +The interceptor still catches errors you don't explicitly handle, providing a safety net. + +## Problem Solved + +**Before:** The app handled API errors inconsistently: + +- Some errors were shown directly from the server +- Others were not displayed clearly to users +- Error handling code was duplicated across components +- Poor user experience with technical error messages + +**After:** Centralized error handling with: + +- Consistent error messaging +- User-friendly error descriptions +- Reusable service across all components +- Better UX with appropriate snackbar notifications + +## Features + +### 1. HTTP Error Handling + +- Automatically converts HTTP status codes into user-friendly messages +- Extracts Fineract-specific error messages +- Handles network/connection errors +- Supports contextual error messages + +### 2. Supported Error Codes + +| Status Code | Title | Behavior | +| ------------- | ------------------- | ----------------------- | +| 400 | Invalid Request | Shows validation errors | +| 401 | Unauthorized | Session expired message | +| 403 | Access Denied | Permission error | +| 404 | Not Found | Resource not found | +| 409 | Conflict | Duplicate resource | +| 500 | Server Error | Generic server error | +| 503 | Service Unavailable | Service down message | +| Network Error | Connection Error | Connection issues | + +### 3. Notification Types + +- **Error Notifications**: Red snackbar, top-center, 5 seconds +- **Success Notifications**: Green snackbar, bottom-center, 3 seconds +- **Info Notifications**: Blue snackbar, bottom-center, 4 seconds + +## Usage + +### Basic Error Handling + +```typescript +import { ErrorHandlerService } from '@core/error-handler/error-handler.service'; +import { catchError } from 'rxjs/operators'; + +export class MyComponent { + constructor(private errorHandler: ErrorHandlerService) {} + + loadData() { + this.dataService + .getData() + .pipe(catchError((error) => this.errorHandler.handleError(error))) + .subscribe((data) => { + // Handle success + }); + } +} +``` + +### Error Handling with Context + +```typescript +loadUser(userId: string) { + this.userService.getUser(userId).pipe( + catchError(error => this.errorHandler.handleError(error, 'User')) + ).subscribe(user => { + // Handle success + }); +} +// Shows: "Not Found: User not found." for 404 errors +``` + +### Success Messages + +```typescript +saveData() { + this.dataService.save(data).subscribe( + response => { + this.errorHandler.showSuccess('Data saved successfully!'); + }, + error => { + this.errorHandler.handleError(error); + } + ); +} +``` + +### Info Messages + +```typescript +loadData() { + this.errorHandler.showInfo('Loading data, please wait...'); + this.dataService.getData().subscribe(/* ... */); +} +``` + +## Advanced Usage + +### Custom Error Handling + +```typescript +import { HttpErrorResponse } from '@angular/common/http'; + +processData() { + this.dataService.process().subscribe( + response => { + this.errorHandler.showSuccess('Processing complete!'); + }, + (error: HttpErrorResponse) => { + if (error.status === 409) { + // Custom handling for conflicts + this.handleConflict(error); + } else { + this.errorHandler.handleError(error, 'Data processing'); + } + } + ); +} +``` + +### Integration with Form Validation + +```typescript +submitForm() { + if (this.form.invalid) { + this.errorHandler.showInfo('Please fill all required fields'); + return; + } + + this.formService.submit(this.form.value).pipe( + catchError(error => this.errorHandler.handleError(error, 'Form submission')) + ).subscribe(response => { + this.errorHandler.showSuccess('Form submitted successfully!'); + }); +} +``` + +## Styling + +The service uses three CSS classes defined in `error-handler.component.scss`: + +```scss +.error-snackbar // Red background (#f44336) for errors +.success-snackbar // Green background (#4caf50) for success +.info-snackbar // Blue background (#2196f3) for info +``` + +These styles are automatically applied based on the notification type. The styles are imported globally in `main.scss` so they work across the entire application. + +## API Reference + +### Methods + +#### `handleError(error: HttpErrorResponse, context?: string): Observable` + +Handles HTTP errors and shows user-friendly messages. + +**Parameters:** + +- `error`: The HTTP error response +- `context` (optional): Additional context for more specific messages + +**Returns:** Observable that throws the original error + +**Example:** + +```typescript +this.errorHandler.handleError(error, 'Client'); +``` + +--- + +#### `showSuccess(message: string, action?: string): void` + +Shows a success message to the user. + +**Parameters:** + +- `message`: The success message to display +- `action` (optional): Button text (defaults to 'OK') + +**Example:** + +```typescript +this.errorHandler.showSuccess('Client created successfully!', 'View'); +``` + +--- + +#### `showInfo(message: string, action?: string): void` + +Shows an informational message to the user. + +**Parameters:** + +- `message`: The info message to display +- `action` (optional): Button text (defaults to 'OK') + +**Example:** + +```typescript +this.errorHandler.showInfo('Loading data...', 'Dismiss'); +``` + +## Fineract Integration + +The service automatically extracts Fineract-specific error messages: + +```typescript +// Fineract error structure +{ + "errors": [{ + "defaultUserMessage": "Client with same name already exists" + }], + "defaultUserMessage": "Validation error" +} +``` + +The service prioritizes: + +1. `errors[0].defaultUserMessage` +2. `defaultUserMessage` +3. Generic fallback message + +## Migration Guide + +### Before (Old Approach) + +```typescript +// Inconsistent error handling +this.clientService.getClient(id).subscribe( + (data) => { + /* success */ + }, + (error) => { + console.error(error); + alert('Error loading client'); // Poor UX + } +); +``` + +### After (Centralized Approach) + +```typescript +// Consistent error handling +this.clientService + .getClient(id) + .pipe(catchError((error) => this.errorHandler.handleError(error, 'Client'))) + .subscribe((data) => { + // Handle success + }); +``` + +## Best Practices + +1. **Always provide context** for better error messages: + + ```typescript + catchError((error) => this.errorHandler.handleError(error, 'Loan Application')); + ``` + +2. **Use appropriate notification types**: + - Errors: Use `handleError()` for API failures + - Success: Use `showSuccess()` for successful operations + - Info: Use `showInfo()` for informational messages + +3. **Don't duplicate error handling**: Let the service handle standard errors + +4. **Custom handling when needed**: Handle specific error cases before using the service + +5. **Combine with loading states**: + ```typescript + this.isLoading = true; + this.service + .getData() + .pipe( + finalize(() => (this.isLoading = false)), + catchError((error) => this.errorHandler.handleError(error)) + ) + .subscribe(); + ``` + +## Testing + +```typescript +import { TestBed } from '@angular/core/testing'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { Router } from '@angular/router'; +import { ErrorHandlerService } from './error-handler.service'; + +describe('ErrorHandlerService', () => { + let service: ErrorHandlerService; + let snackBar: jasmine.SpyObj; + + beforeEach(() => { + const snackBarSpy = jasmine.createSpyObj('MatSnackBar', ['open']); + const routerSpy = jasmine.createSpyObj('Router', ['navigate']); + + TestBed.configureTestingModule({ + providers: [ + ErrorHandlerService, + { provide: MatSnackBar, useValue: snackBarSpy }, + { provide: Router, useValue: routerSpy }] + }); + + service = TestBed.inject(ErrorHandlerService); + snackBar = TestBed.inject(MatSnackBar) as jasmine.SpyObj; + }); + + it('should show success message', () => { + service.showSuccess('Test success'); + expect(snackBar.open).toHaveBeenCalledWith( + 'Test success', + 'OK', + jasmine.objectContaining({ + panelClass: ['success-snackbar'] + }) + ); + }); +}); +``` + +## Related Issues + +- **WEB-429**: Implement centralized error handler service for user-friendly API error messaging + +## Contributing + +When modifying this service: + +1. Ensure all HTTP status codes are handled appropriately +2. Update the documentation with new features +3. Maintain consistent snackbar positioning and duration +4. Test with actual Fineract API responses +5. Follow Angular and TypeScript best practices + +## License + +Licensed under the Apache License, Version 2.0. See the LICENSE file for details. diff --git a/src/app/core/error-handler/error-handler.component.scss b/src/app/core/error-handler/error-handler.component.scss new file mode 100644 index 0000000000..e838b69784 --- /dev/null +++ b/src/app/core/error-handler/error-handler.component.scss @@ -0,0 +1,83 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* === Snackbar Styles for Error Handler Service === */ + +/** + * Error notification snackbar styling + * - Red background for errors + * - White text for contrast + * - Displayed at top-center for 5 seconds + */ +.error-snackbar { + background-color: #f44336 !important; + color: white !important; + + .mat-mdc-snack-bar-label, + .mdc-snackbar__label, + .mat-simple-snackbar-action { + color: white !important; + } + + .mat-mdc-button { + color: white !important; + } +} + +/** + * Success notification snackbar styling + * - Green background for success messages + * - White text for contrast + * - Displayed at bottom-center for 3 seconds + */ +.success-snackbar { + background-color: #4caf50 !important; + color: white !important; + + .mat-mdc-snack-bar-label, + .mdc-snackbar__label, + .mat-simple-snackbar-action { + color: white !important; + } + + .mat-mdc-button { + color: white !important; + } +} + +/** + * Info notification snackbar styling + * - Blue background for informational messages + * - White text for contrast + * - Displayed at bottom-center for 4 seconds + */ +.info-snackbar { + background-color: #2196f3 !important; + color: white !important; + + .mat-mdc-snack-bar-label, + .mdc-snackbar__label, + .mat-simple-snackbar-action { + color: white !important; + } + + .mat-mdc-button { + color: white !important; + } +} diff --git a/src/app/core/error-handler/error-handler.service.ts b/src/app/core/error-handler/error-handler.service.ts new file mode 100644 index 0000000000..0b76b01c70 --- /dev/null +++ b/src/app/core/error-handler/error-handler.service.ts @@ -0,0 +1,182 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Injectable } from '@angular/core'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Observable, throwError } from 'rxjs'; +import { Router } from '@angular/router'; + +export interface ErrorMessage { + title: string; + message: string; + action?: string; +} + +/** + * Centralized error handler service for consistent error messaging + * across the application. Converts HTTP errors into user-friendly + * messages and displays them through snackbar notifications. + */ +@Injectable({ + providedIn: 'root' +}) +export class ErrorHandlerService { + constructor( + private snackBar: MatSnackBar, + private router: Router + ) {} + + /** + * Handle HTTP errors and show user-friendly messages + * @param error The HTTP error response + * @param context Optional context to provide more specific error messages + * @returns Observable that throws the original error + */ + handleError(error: HttpErrorResponse, context?: string): Observable { + const errorMessage = this.getErrorMessage(error, context); + this.showError(errorMessage); + return throwError(() => error); + } + + /** + * Get user-friendly error message based on HTTP status + * @param error The HTTP error response + * @param context Optional context for more specific messages + * @returns ErrorMessage object with title, message, and action + */ + private getErrorMessage(error: HttpErrorResponse, context?: string): ErrorMessage { + if (error.error instanceof ErrorEvent) { + // Client-side or network error + return { + title: 'Connection Error', + message: 'Unable to connect to the server. Please check your internet connection.', + action: 'OK' + }; + } + + // Server-side error - Extract Fineract-specific error messages + const fineractError = error.error?.errors?.[0]?.defaultUserMessage; + const defaultMessage = error.error?.defaultUserMessage; + + switch (error.status) { + case 400: + return { + title: 'Invalid Request', + message: fineractError || defaultMessage || 'Please check your input and try again.', + action: 'OK' + }; + case 401: + return { + title: 'Unauthorized', + message: 'Your session has expired. Please log in again.', + action: 'Login' + }; + case 403: + return { + title: 'Access Denied', + message: fineractError || defaultMessage || 'You do not have permission to perform this action.', + action: 'OK' + }; + case 404: + return { + title: 'Not Found', + message: context + ? `${context} not found.` + : fineractError || defaultMessage || 'The requested resource was not found.', + action: 'OK' + }; + case 409: + return { + title: 'Conflict', + message: fineractError || defaultMessage || 'The resource already exists or there is a conflict.', + action: 'OK' + }; + case 500: + return { + title: 'Server Error', + message: fineractError || defaultMessage || 'An unexpected error occurred. Please try again later.', + action: 'OK' + }; + case 503: + return { + title: 'Service Unavailable', + message: 'The service is temporarily unavailable. Please try again later.', + action: 'OK' + }; + default: + return { + title: 'Error', + message: fineractError || defaultMessage || 'An unexpected error occurred.', + action: 'OK' + }; + } + } + + /** + * Show error message to user via snackbar + * @param errorMessage The error message object to display + */ + private showError(errorMessage: ErrorMessage): void { + const snackBarRef = this.snackBar.open( + `${errorMessage.title}: ${errorMessage.message}`, + errorMessage.action || 'Close', + { + duration: 5000, + horizontalPosition: 'center', + verticalPosition: 'top', + panelClass: ['error-snackbar'] + } + ); + + // Handle action button clicks (e.g., "Login" for 401 errors) + if (errorMessage.action === 'Login') { + snackBarRef.onAction().subscribe(() => { + this.router.navigate(['/login']); + }); + } + } + + /** + * Show success message to user via snackbar + * @param message The success message to display + * @param action Optional action button text (defaults to 'OK') + */ + showSuccess(message: string, action: string = 'OK'): void { + this.snackBar.open(message, action, { + duration: 3000, + horizontalPosition: 'center', + verticalPosition: 'bottom', + panelClass: ['success-snackbar'] + }); + } + + /** + * Show info message to user via snackbar + * @param message The info message to display + * @param action Optional action button text (defaults to 'OK') + */ + showInfo(message: string, action: string = 'OK'): void { + this.snackBar.open(message, action, { + duration: 4000, + horizontalPosition: 'center', + verticalPosition: 'bottom', + panelClass: ['info-snackbar'] + }); + } +} diff --git a/src/main.scss b/src/main.scss index 1050901713..9884d4a4b9 100644 --- a/src/main.scss +++ b/src/main.scss @@ -18,6 +18,9 @@ /* You can add global styles to this file, and also import other style files */ @use 'assets/styles/helper'; +// Error Handler Service Styles +@use 'app/core/error-handler/error-handler.component'; + .scrollbar-styling { scrollbar-width: thin; scrollbar-color: #889 #f1f1f1;