add notifiaction handling, error handling

This commit is contained in:
Casey 2025-11-12 15:13:49 -06:00
parent ce708f5209
commit 1af288aa62
21 changed files with 4864 additions and 224 deletions

View file

@ -0,0 +1,131 @@
# 🎉 Integrated Error Store with Automatic Notifications
## What's New
The error store now automatically creates PrimeVue Toast notifications when errors are set. **No need to import both stores anymore!**
## ✅ Benefits
- **Single Import**: Only import `useErrorStore`
- **Automatic Notifications**: Error toasts appear automatically
- **Cleaner Code**: Less boilerplate in components
- **Consistent UI**: All notifications use PrimeVue Toast
- **Better Organization**: All error handling in one place
## 📖 Usage Examples
### Before (Old Way)
```javascript
// Had to import both stores
import { useErrorStore } from "@/stores/errors";
import { useNotificationStore } from "@/stores/notifications-primevue";
const errorStore = useErrorStore();
const notificationStore = useNotificationStore();
// Manual notification creation
errorStore.setGlobalError(new Error("Something failed"));
notificationStore.addError("Something failed"); // Had to do this manually
```
### After (New Way)
```javascript
// Only need one import
import { useErrorStore } from "@/stores/errors";
const errorStore = useErrorStore();
// Automatic notification - toast appears automatically!
errorStore.setGlobalError(new Error("Something failed"));
```
## 🛠️ Available Methods
### Error Methods (Auto-create toasts)
```javascript
// Global errors
errorStore.setGlobalError(new Error("System error"));
// Component-specific errors
errorStore.setComponentError("form", new Error("Validation failed"));
// API errors
errorStore.setApiError("fetch-users", new Error("Network error"));
```
### Convenience Methods (Direct notifications)
```javascript
// Success messages
errorStore.setSuccess("Operation completed!");
// Warnings
errorStore.setWarning("Please check your input");
// Info messages
errorStore.setInfo("Loading data...");
```
### Disable Automatic Notifications
```javascript
// Set errors without showing toasts
errorStore.setGlobalError(new Error("Silent error"), false);
errorStore.setComponentError("form", new Error("Silent error"), false);
```
## 🔄 Migration Guide
### Components Using Both Stores
**Old Code:**
```javascript
import { useErrorStore } from "@/stores/errors";
import { useNotificationStore } from "@/stores/notifications-primevue";
const errorStore = useErrorStore();
const notificationStore = useNotificationStore();
// Show error
errorStore.setGlobalError(error);
notificationStore.addError("Failed to save");
```
**New Code:**
```javascript
import { useErrorStore } from "@/stores/errors";
const errorStore = useErrorStore();
// Error toast shown automatically!
errorStore.setGlobalError(error);
```
### API Wrapper Updates
The `ApiWithToast` wrapper has been updated to use only the error store. All existing usage remains the same, but now it's even simpler internally.
## 🎯 What Changed Internally
1. **Error Store**: Now imports `notifications-primevue` store
2. **Automatic Calls**: Error methods automatically call toast notifications
3. **Formatted Titles**: Component names are nicely formatted (e.g., "demo-component" → "Demo Component Error")
4. **Convenience Methods**: Added `setSuccess()`, `setWarning()`, `setInfo()` methods
5. **ApiWithToast**: Updated to use only error store
6. **Demo Pages**: Updated to show single-store usage
## 🧪 Testing
Visit `/dev/error-handling-demo` to test:
- All buttons now work with single error store
- Automatic toast notifications
- Error history still works
- Component errors formatted nicely
The notifications will appear in the top-right corner using PrimeVue Toast styling!

View file

@ -0,0 +1,296 @@
# Simple API Error Handling with PrimeVue Toast
This guide shows how to implement clean, simple error handling using PrimeVue Toast instead of complex custom notification components.
## Overview
The simplified approach provides:
- **Automatic error toasts** using PrimeVue Toast
- **Loading state management** with component-specific tracking
- **Success notifications** for create/update operations
- **Retry logic** with exponential backoff
- **Clean error storage** for debugging and component-specific error handling
## Key Files
### 1. PrimeVue Notification Store
**File:** `/src/stores/notifications-primevue.js`
```javascript
import { ref } from "vue";
import { defineStore } from "pinia";
export const useNotificationStore = defineStore(
"notifications-primevue",
() => {
// Toast instance reference
const toastInstance = ref(null);
// Set the toast instance (called from App.vue)
const setToastInstance = (toast) => {
toastInstance.value = toast;
};
// Convenience methods for different toast types
const addSuccess = (message, life = 4000) => {
if (toastInstance.value) {
toastInstance.value.add({
severity: "success",
summary: "Success",
detail: message,
life,
});
}
};
// ... other methods
},
);
```
### 2. Enhanced API Wrapper
**File:** `/src/api-toast.js`
Provides a wrapper around your existing API calls with automatic:
- Error handling and toast notifications
- Loading state management
- Component-specific error tracking
- Retry logic
- Success messages
```javascript
// Simple usage - automatic error toasts
try {
const result = await ApiWithToast.getClientStatusCounts();
// Success - data loaded
} catch (error) {
// Error toast automatically shown
}
// Create operations with success toasts
await ApiWithToast.createClient(formData);
// Shows: "Client created successfully!"
```
## Usage in Components
### 1. Basic Setup
In your component:
```vue
<script setup>
import ApiWithToast from "@/api-toast";
import { useErrorStore } from "@/stores/errors";
import { useLoadingStore } from "@/stores/loading";
const errorStore = useErrorStore();
const loadingStore = useLoadingStore();
// Simple API call
const loadData = async () => {
try {
const result = await ApiWithToast.getPaginatedClientDetails(
pagination,
filters,
[],
);
// Handle success
} catch (error) {
// Error toast shown automatically
// Component error stored automatically
}
};
</script>
```
### 2. Loading States
The API wrapper automatically manages loading states:
```vue
<template>
<Button
@click="loadClients"
:loading="loadingStore.isComponentLoading('clients')"
label="Load Clients"
/>
</template>
```
### 3. Component-Specific Errors
Access errors for debugging or custom handling:
```vue
<template>
<div v-if="errorStore.getComponentError('clients')" class="error-info">
Error: {{ errorStore.getComponentError("clients").message }}
</div>
</template>
```
## App.vue Integration
Ensure your `App.vue` includes the Toast component and connects it to the store:
```vue
<template>
<div id="app">
<!-- Your app content -->
<router-view />
<!-- PrimeVue Toast for notifications -->
<Toast ref="toast" />
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import Toast from "primevue/toast";
import { useNotificationStore } from "@/stores/notifications-primevue";
const toast = ref();
const notificationStore = useNotificationStore();
onMounted(() => {
// Connect toast instance to the store
notificationStore.setToastInstance(toast.value);
});
</script>
```
## API Wrapper Methods
### Convenience Methods
Pre-configured methods for common operations:
```javascript
// Data fetching (no success toast)
await ApiWithToast.getClientStatusCounts();
await ApiWithToast.getPaginatedClientDetails(pagination, filters, sorting);
await ApiWithToast.getPaginatedJobDetails(pagination, filters, sorting);
await ApiWithToast.getPaginatedWarrantyData(pagination, filters, sorting);
// Create operations (success toast included)
await ApiWithToast.createClient(clientData);
// Utility operations (with retry logic)
await ApiWithToast.getCityStateByZip(zipcode);
```
### Custom API Calls
For custom operations:
```javascript
await ApiWithToast.makeApiCall(() => yourApiFunction(), {
componentName: "myComponent",
showSuccessToast: true,
successMessage: "Operation completed!",
showErrorToast: true,
customErrorMessage: "Custom error message",
retryCount: 3,
retryDelay: 1000,
showLoading: true,
loadingMessage: "Processing...",
});
```
## Configuration Options
| Option | Type | Default | Description |
| -------------------- | ------- | -------------- | ----------------------------------------------- |
| `componentName` | string | null | Component identifier for error/loading tracking |
| `showErrorToast` | boolean | true | Show error toast on failure |
| `showSuccessToast` | boolean | false | Show success toast on completion |
| `showLoading` | boolean | true | Show loading indicator |
| `loadingMessage` | string | 'Loading...' | Loading message to display |
| `successMessage` | string | null | Success message for toast |
| `customErrorMessage` | string | null | Override error message |
| `retryCount` | number | 0 | Number of retry attempts |
| `retryDelay` | number | 1000 | Delay between retries (ms) |
| `operationKey` | string | auto-generated | Unique identifier for operation |
## Demo Pages
### 1. Simple API Demo
**URL:** `/dev/simple-api-demo`
Shows practical usage with real API calls:
- Loading client data
- Creating test clients
- Error handling
- Retry logic
### 2. PrimeVue Toast Demo
**URL:** `/dev/toast-demo`
Demonstrates Toast types and error store integration:
- Different toast severities
- Error store testing
- API simulation
## Migration from Custom Notifications
### Old Approach (Custom NotificationDisplay)
```vue
<!-- Complex setup needed -->
<NotificationDisplay />
<script setup>
import { useNotificationStore } from "@/stores/notifications";
// Manual notification management
</script>
```
### New Approach (PrimeVue Toast)
```vue
<!-- Just use the API wrapper -->
<script setup>
import ApiWithToast from "@/api-toast";
// Automatic toast notifications
</script>
```
## Benefits
1. **Consistency**: Uses PrimeVue components throughout
2. **Simplicity**: No custom notification components needed
3. **Automatic**: Error handling happens automatically
4. **Flexible**: Easy to customize per operation
5. **Maintainable**: Centralized error handling logic
6. **Type Safety**: Clear API with documented options
## Testing
Test the implementation by:
1. Visit `/dev/simple-api-demo`
2. Try different operations:
- Load Clients (success case)
- Create Test Client (success with toast)
- Test Error (error toast)
- Test Retry (retry logic demonstration)
The toasts will appear in the top-right corner using PrimeVue's default styling.
## Next Steps
1. Replace existing API calls with `ApiWithToast` methods
2. Remove custom notification components
3. Update components to use the simplified error handling
4. Test across all your existing workflows
This approach provides cleaner, more maintainable error handling while leveraging your existing PrimeVue setup.

View file

@ -0,0 +1,357 @@
# NotificationDisplay Component
The `NotificationDisplay` component provides a global notification system for displaying toast-style messages to users. It integrates seamlessly with the notification store to show success, error, warning, and info messages with optional action buttons.
## Overview
- **Location**: `src/components/common/NotificationDisplay.vue`
- **Type**: Global Component
- **Dependencies**: `@/stores/notifications`
- **Integration**: Added to `App.vue` for global usage
## Features
- ✅ **Multiple notification types** (success, error, warning, info)
- ✅ **Configurable positioning** (6 different positions)
- ✅ **Auto-dismiss with progress bars**
- ✅ **Persistent notifications**
- ✅ **Action buttons** with custom handlers
- ✅ **Smooth animations** (slide-in/out effects)
- ✅ **Responsive design**
- ✅ **Accessibility features**
## Usage
### Basic Integration
The component is automatically included in your application via `App.vue`:
```vue
<template>
<div id="app">
<!-- Your app content -->
<!-- Global Notifications -->
<NotificationDisplay />
</div>
</template>
```
### Triggering Notifications
Use the notification store to display notifications:
```javascript
import { useNotificationStore } from "@/stores/notifications";
const notificationStore = useNotificationStore();
// Simple success notification
notificationStore.addSuccess("Data saved successfully!");
// Error notification
notificationStore.addError("Failed to save data", "Save Error");
// Custom notification with options
notificationStore.addNotification({
type: "warning",
title: "Unsaved Changes",
message: "You have unsaved changes. What would you like to do?",
persistent: true,
actions: [
{
label: "Save",
variant: "primary",
handler: () => saveData(),
},
{
label: "Discard",
variant: "danger",
handler: () => discardChanges(),
},
],
});
```
## Notification Types
### Success
- **Color**: Green (#10b981)
- **Icon**: Check circle
- **Use case**: Successful operations, confirmations
### Error
- **Color**: Red (#ef4444)
- **Icon**: Alert circle
- **Default duration**: 6000ms (longer than others)
- **Use case**: Errors, failures, critical issues
### Warning
- **Color**: Orange (#f59e0b)
- **Icon**: Alert triangle
- **Use case**: Warnings, potential issues, confirmations needed
### Info
- **Color**: Blue (#3b82f6)
- **Icon**: Information circle
- **Use case**: General information, tips, status updates
## Positioning Options
The notification container can be positioned in 6 different locations:
```javascript
// Set position via notification store
notificationStore.setPosition("top-right"); // Default
// Available positions:
// - 'top-right'
// - 'top-left'
// - 'top-center'
// - 'bottom-right'
// - 'bottom-left'
// - 'bottom-center'
```
## Action Buttons
Notifications can include action buttons for user interaction:
```javascript
notificationStore.addNotification({
type: "info",
title: "File Upload",
message: "File uploaded successfully. What would you like to do next?",
actions: [
{
label: "View File",
variant: "primary",
icon: "mdi mdi-eye",
handler: (notification) => {
// Custom action logic
console.log("Viewing file from notification:", notification);
},
},
{
label: "Share",
variant: "secondary",
icon: "mdi mdi-share",
handler: () => shareFile(),
dismissAfter: false, // Don't auto-dismiss after action
},
],
});
```
### Action Button Variants
- **primary**: Blue background
- **danger**: Red background
- **secondary**: Gray background (default)
## Configuration Options
### Global Configuration
```javascript
const notificationStore = useNotificationStore();
// Set default duration (milliseconds)
notificationStore.setDefaultDuration(5000);
// Set maximum number of notifications
notificationStore.setMaxNotifications(3);
// Set position
notificationStore.setPosition("top-center");
```
### Per-Notification Options
```javascript
notificationStore.addNotification({
type: 'success',
title: 'Custom Notification',
message: 'This notification has custom settings',
// Duration (0 = no auto-dismiss)
duration: 8000,
// Persistent (won't auto-dismiss regardless of duration)
persistent: false,
// Custom actions
actions: [...],
// Additional data for handlers
data: { userId: 123, action: 'update' }
});
```
## Responsive Behavior
The component automatically adapts to different screen sizes:
- **Desktop**: Fixed width (320px minimum, 400px maximum)
- **Mobile**: Full width with adjusted padding
- **Positioning**: Center positions become full-width on mobile
## Animations
The component includes smooth CSS transitions:
- **Enter**: Slide in from the appropriate direction
- **Leave**: Slide out in the same direction
- **Duration**: 300ms ease-out/ease-in
- **Progress Bar**: Animated countdown for timed notifications
## Accessibility Features
- **Keyboard Navigation**: Buttons are focusable and keyboard accessible
- **Screen Readers**: Proper ARIA labels and semantic HTML
- **Color Contrast**: High contrast colors for readability
- **Focus Management**: Proper focus indicators
## Styling
The component uses scoped CSS with CSS custom properties for easy customization:
```css
/* Custom styling example */
.notification-container {
/* Override default styles */
--notification-success-color: #059669;
--notification-error-color: #dc2626;
--notification-warning-color: #d97706;
--notification-info-color: #2563eb;
}
```
## Best Practices
### Do's
- ✅ Use appropriate notification types for different scenarios
- ✅ Keep messages concise and actionable
- ✅ Use action buttons for common follow-up actions
- ✅ Set appropriate durations (longer for errors, shorter for success)
- ✅ Use persistent notifications for critical actions requiring user input
### Don'ts
- ❌ Don't show too many notifications at once (overwhelming)
- ❌ Don't use persistent notifications for simple confirmations
- ❌ Don't make notification messages too long
- ❌ Don't use error notifications for non-critical issues
## Integration with Error Store
The NotificationDisplay component works seamlessly with the Error Store:
```javascript
import { useErrorStore } from "@/stores/errors";
const errorStore = useErrorStore();
// Errors automatically trigger notifications
await errorStore.withErrorHandling(
"api-call",
async () => {
// Your async operation
},
{
componentName: "myComponent",
showNotification: true, // Automatically shows error notifications
},
);
```
## Examples
### Basic Usage Examples
```javascript
const notificationStore = useNotificationStore();
// Simple notifications
notificationStore.addSuccess("Changes saved!");
notificationStore.addError("Network connection failed");
notificationStore.addWarning("Unsaved changes detected");
notificationStore.addInfo("New feature available");
// Advanced notification with multiple actions
notificationStore.addNotification({
type: "warning",
title: "Confirm Deletion",
message: "This action cannot be undone. Are you sure?",
persistent: true,
actions: [
{
label: "Delete",
variant: "danger",
handler: () => {
performDeletion();
notificationStore.addSuccess("Item deleted successfully");
},
},
{
label: "Cancel",
variant: "secondary",
},
],
});
```
### API Integration Examples
```javascript
// Show loading notification that updates on completion
const loadingId =
notificationStore.showLoadingNotification("Uploading file...");
try {
await uploadFile();
notificationStore.updateToSuccess(loadingId, "File uploaded successfully!");
} catch (error) {
notificationStore.updateToError(loadingId, "Upload failed: " + error.message);
}
```
## Troubleshooting
### Common Issues
1. **Notifications not appearing**
- Ensure NotificationDisplay is included in App.vue
- Check z-index conflicts with other components
- Verify notification store is properly imported
2. **Notifications appearing in wrong position**
- Check the position setting in the store
- Verify CSS is not being overridden
3. **Action buttons not working**
- Ensure handler functions are properly defined
- Check for JavaScript errors in handlers
### Debug Mode
Enable debug logging in development:
```javascript
// In your main.js or component
if (process.env.NODE_ENV === "development") {
const notificationStore = useNotificationStore();
// Watch for notification changes
watch(
() => notificationStore.notifications,
(notifications) => {
console.log("Notifications updated:", notifications);
},
);
}
```

View file

@ -0,0 +1,699 @@
# Errors Store
The errors store provides comprehensive error handling and management for the entire application. It centralizes error tracking, automatic retry logic, and integration with the notification system to provide users with consistent error feedback.
## Overview
- **Location**: `src/stores/errors.js`
- **Type**: Pinia Store
- **Purpose**: Centralized error state management and handling
- **Integration**: Works with notification store for user feedback
## Installation & Setup
```javascript
// Import in your component
import { useErrorStore } from "@/stores/errors";
// Use in component
const errorStore = useErrorStore();
```
## State Structure
### Core State Properties
```javascript
state: {
hasError: false, // Global error flag
lastError: null, // Most recent global error
apiErrors: new Map(), // API-specific errors by key
componentErrors: { // Component-specific errors
dataTable: null,
form: null,
clients: null,
jobs: null,
timesheets: null,
warranties: null,
routes: null
},
errorHistory: [], // Historical error log
maxHistorySize: 50, // Maximum history entries
autoNotifyErrors: true // Auto-show notifications for errors
}
```
### Error Object Structure
```javascript
{
message: 'Error description', // Human-readable error message
type: 'api_error', // Error classification
timestamp: '2025-11-12T10:30:00Z', // When error occurred
name: 'ValidationError', // Error name (if available)
stack: 'Error stack trace...', // Stack trace (if available)
status: 404, // HTTP status (for API errors)
statusText: 'Not Found', // HTTP status text
data: {...} // Additional error data
}
```
## Getters
### `hasAnyError`
Check if there are any errors in the application.
```javascript
const hasErrors = errorStore.hasAnyError;
```
### `getComponentError(componentName)`
Get error for a specific component.
```javascript
const formError = errorStore.getComponentError("form");
```
### `getApiError(apiKey)`
Get error for a specific API operation.
```javascript
const loginError = errorStore.getApiError("user-login");
```
### `getRecentErrors(limit)`
Get recent errors from history.
```javascript
const recentErrors = errorStore.getRecentErrors(10);
```
## Actions
### Global Error Management
#### `setGlobalError(error, showNotification)`
Set a global application error.
```javascript
errorStore.setGlobalError(
new Error("Critical system failure"),
true, // Show notification
);
// With custom error object
errorStore.setGlobalError({
message: "Database connection lost",
type: "connection_error",
recoverable: true,
});
```
#### `clearGlobalError()`
Clear the global error state.
```javascript
errorStore.clearGlobalError();
```
### Component-Specific Error Management
#### `setComponentError(componentName, error, showNotification)`
Set an error for a specific component.
```javascript
// Set error for form component
errorStore.setComponentError("form", new Error("Validation failed"), true);
// Clear error (pass null)
errorStore.setComponentError("form", null);
```
#### `clearComponentError(componentName)`
Clear error for a specific component.
```javascript
errorStore.clearComponentError("form");
```
### API Error Management
#### `setApiError(apiKey, error, showNotification)`
Set an error for a specific API operation.
```javascript
// Set API error
errorStore.setApiError("user-login", apiError, true);
// Clear API error (pass null)
errorStore.setApiError("user-login", null);
```
#### `clearApiError(apiKey)`
Clear error for a specific API operation.
```javascript
errorStore.clearApiError("user-login");
```
### Bulk Operations
#### `clearAllErrors()`
Clear all errors from the store.
```javascript
errorStore.clearAllErrors();
```
### Advanced Error Handling
#### `handleApiCall(apiKey, apiFunction, options)`
Handle an API call with automatic error management and retry logic.
```javascript
const result = await errorStore.handleApiCall(
"fetch-users",
async () => {
return await api.getUsers();
},
{
showNotification: true,
retryCount: 2,
retryDelay: 1000,
onSuccess: (result) => console.log("Success:", result),
onError: (error) => console.log("Failed:", error),
},
);
```
#### `withErrorHandling(operationKey, asyncOperation, options)`
Wrap an async operation with comprehensive error handling.
```javascript
const result = await errorStore.withErrorHandling(
"save-data",
async () => {
return await saveUserData();
},
{
componentName: "userForm",
showNotification: true,
rethrow: false, // Don't re-throw errors
},
);
```
## Error Types
The store automatically categorizes errors into different types:
### `string_error`
Simple string errors.
```javascript
errorStore.setGlobalError("Something went wrong");
```
### `javascript_error`
Standard JavaScript Error objects.
```javascript
errorStore.setGlobalError(new Error("Validation failed"));
```
### `api_error`
HTTP/API response errors with status codes.
```javascript
// Automatically detected from axios-style error objects
{
message: 'Not Found',
status: 404,
statusText: 'Not Found',
type: 'api_error',
data: {...}
}
```
### `network_error`
Network connectivity errors.
```javascript
{
message: 'Network error - please check your connection',
type: 'network_error'
}
```
### `unknown_error`
Unrecognized error formats.
```javascript
{
message: 'An unknown error occurred',
type: 'unknown_error',
originalError: {...}
}
```
## Configuration Methods
### `setAutoNotifyErrors(enabled)`
Enable/disable automatic error notifications.
```javascript
errorStore.setAutoNotifyErrors(false); // Disable auto-notifications
```
### `setMaxHistorySize(size)`
Set maximum number of errors to keep in history.
```javascript
errorStore.setMaxHistorySize(100);
```
## Usage Patterns
### Basic Error Handling
```javascript
const errorStore = useErrorStore();
// Simple error setting
try {
await riskyOperation();
} catch (error) {
errorStore.setGlobalError(error);
}
// Component-specific error
try {
await validateForm();
} catch (validationError) {
errorStore.setComponentError("form", validationError);
}
```
### API Error Handling with Retry
```javascript
// Automatic retry logic
const userData = await errorStore.handleApiCall(
"fetch-user-data",
async () => {
const response = await fetch("/api/users");
if (!response.ok) throw new Error("Failed to fetch users");
return response.json();
},
{
retryCount: 3,
retryDelay: 1000,
showNotification: true,
},
);
```
### Comprehensive Operation Wrapping
```javascript
// Wrap complex operations
const result = await errorStore.withErrorHandling(
"complex-workflow",
async () => {
// Step 1: Validate data
await validateInputData();
// Step 2: Save to database
const saveResult = await saveToDatabase();
// Step 3: Send notification email
await sendNotificationEmail();
return saveResult;
},
{
componentName: "workflow",
showNotification: true,
rethrow: false,
},
);
if (result) {
// Success - result contains the return value
console.log("Workflow completed:", result);
} else {
// Error occurred - check component error for details
const workflowError = errorStore.getComponentError("workflow");
console.log("Workflow failed:", workflowError?.message);
}
```
### Integration with Vue Components
```javascript
// In a Vue component
import { useErrorStore } from "@/stores/errors";
import { useNotificationStore } from "@/stores/notifications";
export default {
setup() {
const errorStore = useErrorStore();
const notificationStore = useNotificationStore();
const submitForm = async (formData) => {
// Clear any previous errors
errorStore.clearComponentError("form");
try {
await errorStore.withErrorHandling(
"form-submit",
async () => {
const result = await api.submitForm(formData);
notificationStore.addSuccess("Form submitted successfully!");
return result;
},
{
componentName: "form",
showNotification: true,
},
);
// Reset form on success
resetForm();
} catch (error) {
// Error handling is automatic, but you can add custom logic here
console.log("Form submission failed");
}
};
return {
submitForm,
formError: computed(() => errorStore.getComponentError("form")),
};
},
};
```
## Integration with Enhanced API
The errors store works seamlessly with the enhanced API wrapper:
```javascript
import { ApiWithErrorHandling } from "@/api-enhanced";
// The enhanced API automatically uses the error store
try {
const clients = await ApiWithErrorHandling.getPaginatedClientDetails(
pagination,
filters,
[],
{
componentName: "clients",
retryCount: 2,
showErrorNotifications: true,
},
);
} catch (error) {
// Error is automatically handled by the error store
// Check component error for details
const clientError = errorStore.getComponentError("clients");
}
```
## Error Recovery Patterns
### Graceful Degradation
```javascript
const loadCriticalData = async () => {
let primaryData = null;
let fallbackData = null;
// Try primary data source
try {
primaryData = await errorStore.withErrorHandling(
"primary-data",
() => api.getPrimaryData(),
{ showNotification: false, rethrow: true },
);
} catch (error) {
console.log("Primary data failed, trying fallback...");
// Try fallback data source
try {
fallbackData = await errorStore.withErrorHandling(
"fallback-data",
() => api.getFallbackData(),
{ showNotification: false, rethrow: true },
);
notificationStore.addWarning(
"Using cached data due to connectivity issues",
);
} catch (fallbackError) {
errorStore.setGlobalError("Unable to load data from any source");
return null;
}
}
return primaryData || fallbackData;
};
```
### User-Driven Error Recovery
```javascript
const handleApiError = async (operation) => {
try {
return await operation();
} catch (error) {
// Show error with recovery options
notificationStore.addNotification({
type: "error",
title: "Operation Failed",
message: "Would you like to try again or continue with cached data?",
persistent: true,
actions: [
{
label: "Retry",
variant: "primary",
handler: () => handleApiError(operation), // Recursive retry
},
{
label: "Use Cached Data",
variant: "secondary",
handler: () => loadCachedData(),
},
{
label: "Cancel",
variant: "secondary",
},
],
});
throw error; // Let caller handle as needed
}
};
```
## Debugging & Monitoring
### Error History Access
```javascript
// Get all error history for debugging
const allErrors = errorStore.errorHistory;
// Get recent errors with details
const recent = errorStore.getRecentErrors(5);
recent.forEach((error) => {
console.log(`[${error.timestamp}] ${error.source}: ${error.message}`);
});
// Filter errors by type
const apiErrors = errorStore.errorHistory.filter((e) => e.type === "api_error");
```
### Error Reporting
```javascript
// Send error reports to monitoring service
const reportErrors = () => {
const recentErrors = errorStore.getRecentErrors(10);
recentErrors.forEach((error) => {
if (error.type === "api_error" && error.status >= 500) {
// Report server errors
analyticsService.reportError({
message: error.message,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: error.timestamp,
stack: error.stack,
});
}
});
};
```
## Testing
### Unit Testing
```javascript
import { setActivePinia, createPinia } from "pinia";
import { useErrorStore } from "@/stores/errors";
describe("Errors Store", () => {
beforeEach(() => {
setActivePinia(createPinia());
});
it("sets and clears global errors", () => {
const store = useErrorStore();
store.setGlobalError("Test error");
expect(store.hasError).toBe(true);
expect(store.lastError.message).toBe("Test error");
store.clearGlobalError();
expect(store.hasError).toBe(false);
});
it("handles component errors", () => {
const store = useErrorStore();
store.setComponentError("form", new Error("Validation failed"));
expect(store.getComponentError("form").message).toBe("Validation failed");
store.clearComponentError("form");
expect(store.getComponentError("form")).toBeNull();
});
it("tracks error history", () => {
const store = useErrorStore();
store.setGlobalError("Error 1");
store.setGlobalError("Error 2");
expect(store.errorHistory).toHaveLength(2);
expect(store.getRecentErrors(1)[0].message).toBe("Error 2");
});
});
```
### Integration Testing
```javascript
// Test error handling with API calls
it("handles API errors correctly", async () => {
const store = useErrorStore();
const mockApi = jest.fn().mockRejectedValue(new Error("API Error"));
const result = await store.withErrorHandling("test-api", mockApi, {
componentName: "test",
showNotification: false,
rethrow: false,
});
expect(result).toBeNull();
expect(store.getComponentError("test").message).toBe("API Error");
});
```
## Best Practices
### Do's
- ✅ Use component-specific errors for UI validation
- ✅ Use API-specific errors for network operations
- ✅ Enable auto-notifications for user-facing errors
- ✅ Use retry logic for transient failures
- ✅ Clear errors when operations succeed
- ✅ Keep error messages user-friendly and actionable
### Don'ts
- ❌ Don't set global errors for minor validation issues
- ❌ Don't ignore error context (component/API source)
- ❌ Don't let error history grow indefinitely
- ❌ Don't show technical stack traces to end users
- ❌ Don't retry operations that will consistently fail
### Performance Considerations
- Error history is automatically trimmed to prevent memory leaks
- Use component-specific errors to isolate issues
- Clear errors promptly when no longer relevant
- Consider disabling auto-notifications for high-frequency operations
## Common Patterns
### Form Validation Errors
```javascript
const validateAndSubmit = async (formData) => {
errorStore.clearComponentError("form");
try {
await errorStore.withErrorHandling(
"form-validation",
async () => {
validateFormData(formData);
await submitForm(formData);
},
{
componentName: "form",
showNotification: true,
},
);
} catch (error) {
// Validation errors are now stored in component error
// and automatically displayed to user
}
};
```
### Background Task Monitoring
```javascript
const monitorBackgroundTask = async (taskId) => {
await errorStore.handleApiCall(
`task-${taskId}`,
async () => {
const status = await api.getTaskStatus(taskId);
if (status.failed) {
throw new Error(`Task failed: ${status.error}`);
}
return status;
},
{
retryCount: 5,
retryDelay: 2000,
showNotification: status.failed, // Only notify on final failure
},
);
};
```

View file

@ -0,0 +1,609 @@
# Notifications Store
The notifications store provides centralized state management for application-wide notifications. It handles the creation, management, and lifecycle of toast-style notifications with support for multiple types, positions, and interactive actions.
## Overview
- **Location**: `src/stores/notifications.js`
- **Type**: Pinia Store
- **Purpose**: Global notification state management
- **Integration**: Used by NotificationDisplay component
## Installation & Setup
```javascript
// Import in your component
import { useNotificationStore } from "@/stores/notifications";
// Use in component
const notificationStore = useNotificationStore();
```
## State Structure
### Core State Properties
```javascript
state: {
notifications: [], // Array of active notifications
defaultDuration: 4000, // Default auto-dismiss time (ms)
maxNotifications: 5, // Maximum concurrent notifications
position: 'top-right', // Default position
nextId: 1 // Auto-incrementing ID counter
}
```
### Notification Object Structure
```javascript
{
id: 1, // Unique identifier
type: 'success', // 'success' | 'error' | 'warning' | 'info'
title: 'Operation Complete', // Notification title
message: 'Data saved successfully', // Main message
duration: 4000, // Auto-dismiss time (0 = no auto-dismiss)
persistent: false, // If true, won't auto-dismiss
actions: [], // Array of action buttons
data: null, // Additional data for handlers
timestamp: '2025-11-12T10:30:00Z', // Creation timestamp
dismissed: false, // Whether notification is dismissed
seen: false // Whether user has interacted with it
}
```
## Getters
### `getNotificationsByType(type)`
Get all notifications of a specific type.
```javascript
const errorNotifications = notificationStore.getNotificationsByType("error");
```
### `activeNotifications`
Get all non-dismissed notifications.
```javascript
const active = notificationStore.activeNotifications;
```
### `activeCount`
Get count of active notifications.
```javascript
const count = notificationStore.activeCount;
```
### `hasErrorNotifications`
Check if there are any active error notifications.
```javascript
const hasErrors = notificationStore.hasErrorNotifications;
```
### `hasSuccessNotifications`
Check if there are any active success notifications.
```javascript
const hasSuccess = notificationStore.hasSuccessNotifications;
```
## Actions
### Core Notification Methods
#### `addNotification(notification)`
Add a new notification with full configuration options.
```javascript
const notificationId = notificationStore.addNotification({
type: "warning",
title: "Confirm Action",
message: "This will permanently delete the item.",
persistent: true,
actions: [
{
label: "Delete",
variant: "danger",
handler: () => performDelete(),
},
{
label: "Cancel",
variant: "secondary",
},
],
data: { itemId: 123 },
});
```
#### Convenience Methods
```javascript
// Quick success notification
notificationStore.addSuccess("Operation completed!");
notificationStore.addSuccess("Custom message", "Custom Title", {
duration: 6000,
});
// Quick error notification
notificationStore.addError("Something went wrong!");
notificationStore.addError("Custom error", "Error Title", { persistent: true });
// Quick warning notification
notificationStore.addWarning("Please confirm this action");
// Quick info notification
notificationStore.addInfo("New feature available");
```
### Notification Management
#### `dismissNotification(id)`
Mark a notification as dismissed (hides it but keeps in history).
```javascript
notificationStore.dismissNotification(notificationId);
```
#### `removeNotification(id)`
Completely remove a notification from the store.
```javascript
notificationStore.removeNotification(notificationId);
```
#### `markAsSeen(id)`
Mark a notification as seen (user has interacted with it).
```javascript
notificationStore.markAsSeen(notificationId);
```
#### `updateNotification(id, updates)`
Update an existing notification's properties.
```javascript
notificationStore.updateNotification(notificationId, {
type: "success",
message: "Updated message",
persistent: false,
});
```
### Bulk Operations
#### `clearType(type)`
Remove all notifications of a specific type.
```javascript
notificationStore.clearType("error"); // Remove all error notifications
```
#### `clearAll()`
Remove all notifications.
```javascript
notificationStore.clearAll();
```
#### `clearDismissed()`
Remove all dismissed notifications from history.
```javascript
notificationStore.clearDismissed();
```
### Loading Notifications
#### `showLoadingNotification(message, title)`
Show a persistent loading notification that can be updated later.
```javascript
const loadingId = notificationStore.showLoadingNotification(
"Uploading file...",
"Please Wait",
);
```
#### `updateToSuccess(id, message, title)`
Update a loading notification to success and auto-dismiss.
```javascript
notificationStore.updateToSuccess(
loadingId,
"File uploaded successfully!",
"Upload Complete",
);
```
#### `updateToError(id, message, title)`
Update a loading notification to error and auto-dismiss.
```javascript
notificationStore.updateToError(
loadingId,
"Upload failed. Please try again.",
"Upload Failed",
);
```
### API Integration Helpers
#### `showApiSuccess(operation, customMessage)`
Show standardized success notifications for API operations.
```javascript
// Uses default messages based on operation
notificationStore.showApiSuccess("create"); // "Item created successfully"
notificationStore.showApiSuccess("update"); // "Item updated successfully"
notificationStore.showApiSuccess("delete"); // "Item deleted successfully"
notificationStore.showApiSuccess("fetch"); // "Data loaded successfully"
// With custom message
notificationStore.showApiSuccess("create", "New client added successfully!");
```
#### `showApiError(operation, error, customMessage)`
Show standardized error notifications for API operations.
```javascript
// Automatically extracts error message from different error formats
notificationStore.showApiError("create", apiError);
notificationStore.showApiError("update", "Network timeout occurred");
notificationStore.showApiError("delete", errorObject, "Custom error message");
```
### Configuration Methods
#### `setPosition(position)`
Set the global notification position.
```javascript
// Available positions:
notificationStore.setPosition("top-right"); // Default
notificationStore.setPosition("top-left");
notificationStore.setPosition("top-center");
notificationStore.setPosition("bottom-right");
notificationStore.setPosition("bottom-left");
notificationStore.setPosition("bottom-center");
```
#### `setDefaultDuration(duration)`
Set the default auto-dismiss duration (milliseconds).
```javascript
notificationStore.setDefaultDuration(5000); // 5 seconds
```
#### `setMaxNotifications(max)`
Set maximum number of concurrent notifications.
```javascript
notificationStore.setMaxNotifications(3);
```
### Advanced Workflow Helper
#### `withNotifications(operation, asyncFunction, options)`
Wrap an async operation with automatic loading/success/error notifications.
```javascript
const result = await notificationStore.withNotifications(
"save",
async () => {
return await saveDataToApi();
},
{
loadingMessage: "Saving changes...",
successMessage: "Changes saved successfully!",
errorMessage: null, // Use default error handling
showLoading: true,
},
);
```
## Action Button Configuration
### Action Object Structure
```javascript
{
label: 'Button Text', // Required: Button label
variant: 'primary', // Optional: 'primary' | 'danger' | 'secondary'
icon: 'mdi mdi-check', // Optional: Icon class
handler: (notification) => {}, // Optional: Click handler function
dismissAfter: true // Optional: Auto-dismiss after click (default: true)
}
```
### Action Examples
```javascript
notificationStore.addNotification({
type: "warning",
title: "Unsaved Changes",
message: "You have unsaved changes. What would you like to do?",
persistent: true,
actions: [
{
label: "Save",
variant: "primary",
icon: "mdi mdi-content-save",
handler: (notification) => {
saveChanges();
// Access notification data if needed
console.log("Saving from notification:", notification.data);
},
},
{
label: "Discard",
variant: "danger",
icon: "mdi mdi-delete",
handler: () => {
discardChanges();
},
},
{
label: "Keep Editing",
variant: "secondary",
dismissAfter: false, // Don't dismiss, let user continue editing
},
],
});
```
## Usage Patterns
### Basic Notifications
```javascript
const notificationStore = useNotificationStore();
// Simple success
notificationStore.addSuccess("Data saved successfully!");
// Simple error
notificationStore.addError("Failed to connect to server");
// Custom duration
notificationStore.addWarning("Session expires in 5 minutes", "Warning", {
duration: 8000,
});
```
### API Operation Notifications
```javascript
// Method 1: Manual handling
try {
await apiCall();
notificationStore.addSuccess("Operation completed successfully!");
} catch (error) {
notificationStore.addError(error.message, "Operation Failed");
}
// Method 2: Using API helpers
try {
await apiCall();
notificationStore.showApiSuccess("create");
} catch (error) {
notificationStore.showApiError("create", error);
}
// Method 3: Using workflow helper
await notificationStore.withNotifications("create", async () => {
return await apiCall();
});
```
### Loading States
```javascript
// Show loading notification
const loadingId = notificationStore.showLoadingNotification("Processing...");
try {
const result = await longRunningOperation();
notificationStore.updateToSuccess(loadingId, "Operation completed!");
} catch (error) {
notificationStore.updateToError(loadingId, "Operation failed");
}
```
### Interactive Notifications
```javascript
// Confirmation dialog
notificationStore.addNotification({
type: "warning",
title: "Delete Confirmation",
message: `Are you sure you want to delete "${itemName}"?`,
persistent: true,
actions: [
{
label: "Yes, Delete",
variant: "danger",
handler: async () => {
const deleteId =
notificationStore.showLoadingNotification("Deleting...");
try {
await deleteItem(itemId);
notificationStore.updateToSuccess(
deleteId,
"Item deleted successfully",
);
refreshData();
} catch (error) {
notificationStore.updateToError(deleteId, "Failed to delete item");
}
},
},
{
label: "Cancel",
variant: "secondary",
},
],
});
// Multi-step workflow
notificationStore.addNotification({
type: "info",
title: "Export Complete",
message: "Your data has been exported successfully.",
actions: [
{
label: "Download",
variant: "primary",
icon: "mdi mdi-download",
handler: () => downloadFile(),
},
{
label: "Email",
variant: "secondary",
icon: "mdi mdi-email",
handler: () => emailFile(),
},
{
label: "View",
variant: "secondary",
icon: "mdi mdi-eye",
handler: () => viewFile(),
},
],
});
```
## Integration with Vue Components
### In Composition API
```javascript
import { useNotificationStore } from "@/stores/notifications";
export default {
setup() {
const notificationStore = useNotificationStore();
const handleSubmit = async () => {
try {
await submitForm();
notificationStore.addSuccess("Form submitted successfully!");
} catch (error) {
notificationStore.addError("Failed to submit form", "Submission Error");
}
};
return {
handleSubmit,
};
},
};
```
### In Options API
```javascript
import { mapActions, mapGetters } from "pinia";
import { useNotificationStore } from "@/stores/notifications";
export default {
computed: {
...mapGetters(useNotificationStore, [
"activeCount",
"hasErrorNotifications",
]),
},
methods: {
...mapActions(useNotificationStore, ["addSuccess", "addError", "clearAll"]),
async saveData() {
try {
await this.apiCall();
this.addSuccess("Data saved!");
} catch (error) {
this.addError(error.message);
}
},
},
};
```
## Best Practices
### Do's
- ✅ Use appropriate notification types for different scenarios
- ✅ Keep messages concise and actionable
- ✅ Use loading notifications for long-running operations
- ✅ Provide clear action buttons for next steps
- ✅ Set appropriate durations (longer for errors, shorter for success)
- ✅ Use persistent notifications for critical actions requiring user input
### Don'ts
- ❌ Don't overwhelm users with too many notifications
- ❌ Don't use persistent notifications for simple confirmations
- ❌ Don't make notification messages too long
- ❌ Don't forget to handle loading state cleanup
- ❌ Don't use success notifications for every small action
### Performance Considerations
- The store automatically limits concurrent notifications
- Dismissed notifications are kept in history for debugging
- Use `clearDismissed()` periodically to prevent memory leaks
- Action handlers should be lightweight to avoid blocking the UI
## Testing
### Unit Testing
```javascript
import { setActivePinia, createPinia } from "pinia";
import { useNotificationStore } from "@/stores/notifications";
describe("Notifications Store", () => {
beforeEach(() => {
setActivePinia(createPinia());
});
it("adds notifications correctly", () => {
const store = useNotificationStore();
const id = store.addSuccess("Test message");
expect(store.activeNotifications).toHaveLength(1);
expect(store.activeNotifications[0].message).toBe("Test message");
expect(store.activeNotifications[0].type).toBe("success");
});
it("dismisses notifications", () => {
const store = useNotificationStore();
const id = store.addInfo("Test");
store.dismissNotification(id);
expect(store.activeNotifications).toHaveLength(0);
});
});
```