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,341 @@
import { defineStore } from "pinia";
import { useNotificationStore } from "./notifications-primevue";
/**
* Enhanced Error Store with Automatic PrimeVue Toast Notifications
*
* This store automatically creates PrimeVue Toast notifications when errors are set.
* No need to import both error and notification stores - just use this one!
*
* Usage:
* import { useErrorStore } from '@/stores/errors'
* const errorStore = useErrorStore()
*
* // These will automatically show toast notifications:
* errorStore.setGlobalError(new Error("Something went wrong"))
* errorStore.setComponentError("form", new Error("Validation failed"))
* errorStore.setApiError("fetch-users", new Error("Network error"))
*
* // Convenience methods for non-error notifications:
* errorStore.setSuccess("Operation completed!")
* errorStore.setWarning("Please check your input")
* errorStore.setInfo("Loading data...")
*/
export const useErrorStore = defineStore("errors", {
state: () => ({
// Global error state
hasError: false,
lastError: null,
// API-specific errors
apiErrors: new Map(),
// Component-specific errors
componentErrors: {
dataTable: null,
form: null,
clients: null,
jobs: null,
timesheets: null,
warranties: null,
routes: null,
},
// Error history for debugging
errorHistory: [],
// Configuration
maxHistorySize: 50,
autoNotifyErrors: true,
}),
getters: {
// Check if any error exists
hasAnyError: (state) => {
return (
state.hasError ||
state.apiErrors.size > 0 ||
Object.values(state.componentErrors).some((error) => error !== null)
);
},
// Get error for a specific component
getComponentError: (state) => (componentName) => {
return state.componentErrors[componentName];
},
// Get error for a specific API call
getApiError: (state) => (apiKey) => {
return state.apiErrors.get(apiKey);
},
// Get recent errors
getRecentErrors:
(state) =>
(limit = 10) => {
return state.errorHistory.slice(-limit).reverse();
},
},
actions: {
// Set global error
setGlobalError(error, showNotification = true) {
this.hasError = true;
this.lastError = this._normalizeError(error);
this._addToHistory(this.lastError, "global");
if (showNotification && this.autoNotifyErrors) {
const notificationStore = useNotificationStore();
notificationStore.addError(this.lastError.message, "Global Error", {
duration: 5000,
});
}
},
// Set component-specific error
setComponentError(componentName, error, showNotification = true) {
const normalizedError = error ? this._normalizeError(error) : null;
if (this.componentErrors.hasOwnProperty(componentName)) {
this.componentErrors[componentName] = normalizedError;
} else {
this.componentErrors[componentName] = normalizedError;
}
if (normalizedError) {
this._addToHistory(normalizedError, `component:${componentName}`);
if (showNotification && this.autoNotifyErrors) {
const notificationStore = useNotificationStore();
notificationStore.addError(
normalizedError.message,
`${this._formatComponentName(componentName)} Error`,
{ duration: 5000 },
);
}
}
},
// Set API-specific error
setApiError(apiKey, error, showNotification = true) {
if (error) {
const normalizedError = this._normalizeError(error);
this.apiErrors.set(apiKey, normalizedError);
this._addToHistory(normalizedError, `api:${apiKey}`);
if (showNotification && this.autoNotifyErrors) {
const notificationStore = useNotificationStore();
notificationStore.addError(normalizedError.message, "API Error", {
duration: 6000,
});
}
} else {
this.apiErrors.delete(apiKey);
}
},
// Clear specific errors
clearGlobalError() {
this.hasError = false;
this.lastError = null;
},
clearComponentError(componentName) {
if (this.componentErrors.hasOwnProperty(componentName)) {
this.componentErrors[componentName] = null;
}
},
clearApiError(apiKey) {
this.apiErrors.delete(apiKey);
},
// Clear all errors
clearAllErrors() {
this.hasError = false;
this.lastError = null;
this.apiErrors.clear();
Object.keys(this.componentErrors).forEach((key) => {
this.componentErrors[key] = null;
});
},
// Handle API call errors with automatic error management
async handleApiCall(apiKey, apiFunction, options = {}) {
const {
showNotification = true,
retryCount = 0,
retryDelay = 1000,
onSuccess = null,
onError = null,
} = options;
// Clear any existing error for this API
this.clearApiError(apiKey);
let attempt = 0;
while (attempt <= retryCount) {
try {
const result = await apiFunction();
if (onSuccess) {
onSuccess(result);
}
return result;
} catch (error) {
attempt++;
if (attempt <= retryCount) {
// Wait before retry
await new Promise((resolve) => setTimeout(resolve, retryDelay));
continue;
}
// Final attempt failed
this.setApiError(apiKey, error, showNotification);
if (onError) {
onError(error);
}
throw error;
}
}
},
// Convenience method for handling async operations with error management
async withErrorHandling(operationKey, asyncOperation, options = {}) {
const { componentName = null, showNotification = true, rethrow = false } = options;
try {
const result = await asyncOperation();
// Clear any existing errors on success
if (componentName) {
this.clearComponentError(componentName);
}
return result;
} catch (error) {
if (componentName) {
this.setComponentError(componentName, error, showNotification);
} else {
this.setGlobalError(error, showNotification);
}
if (rethrow) {
throw error;
}
return null;
}
},
// Private helper methods
_normalizeError(error) {
if (typeof error === "string") {
return {
message: error,
type: "string_error",
timestamp: new Date().toISOString(),
};
}
if (error instanceof Error) {
return {
message: error.message,
name: error.name,
stack: error.stack,
type: "javascript_error",
timestamp: new Date().toISOString(),
};
}
// Handle API response errors
if (error && error.response) {
return {
message:
error.response.data?.message || error.response.statusText || "API Error",
status: error.response.status,
statusText: error.response.statusText,
data: error.response.data,
type: "api_error",
timestamp: new Date().toISOString(),
};
}
// Handle network errors
if (error && error.request) {
return {
message: "Network error - please check your connection",
type: "network_error",
timestamp: new Date().toISOString(),
};
}
// Fallback for unknown error types
return {
message: error?.message || "An unknown error occurred",
originalError: error,
type: "unknown_error",
timestamp: new Date().toISOString(),
};
},
_addToHistory(normalizedError, source) {
this.errorHistory.push({
...normalizedError,
source,
id: Date.now() + Math.random(),
});
// Trim history if it exceeds max size
if (this.errorHistory.length > this.maxHistorySize) {
this.errorHistory = this.errorHistory.slice(-this.maxHistorySize);
}
},
// Success notifications (convenience methods)
setSuccess(message, title = "Success", options = {}) {
if (this.autoNotifyErrors) {
const notificationStore = useNotificationStore();
notificationStore.addSuccess(message, title, options);
}
},
setWarning(message, title = "Warning", options = {}) {
if (this.autoNotifyErrors) {
const notificationStore = useNotificationStore();
notificationStore.addWarning(message, title, options);
}
},
setInfo(message, title = "Info", options = {}) {
if (this.autoNotifyErrors) {
const notificationStore = useNotificationStore();
notificationStore.addInfo(message, title, options);
}
},
// Configuration methods
setAutoNotifyErrors(enabled) {
this.autoNotifyErrors = enabled;
},
setMaxHistorySize(size) {
this.maxHistorySize = size;
if (this.errorHistory.length > size) {
this.errorHistory = this.errorHistory.slice(-size);
}
},
// Helper method to format component names nicely
_formatComponentName(componentName) {
return componentName
.split(/[-_]/)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
},
},
});

View file

@ -0,0 +1,186 @@
import { defineStore } from "pinia";
// Global toast instance - will be set during app initialization
let toastInstance = null;
export const useNotificationStore = defineStore("notifications", {
state: () => ({
// Configuration for PrimeVue Toast
defaultLife: 4000,
position: "top-right",
}),
getters: {
// Helper to check if toast is available
isToastAvailable: () => !!toastInstance,
},
actions: {
// Set the toast instance (called from main component)
setToastInstance(toast) {
toastInstance = toast;
},
// Core method to show notifications using PrimeVue Toast
addNotification(notification) {
if (!toastInstance) {
console.warn(
"Toast instance not available. Make sure to call setToastInstance first.",
);
return;
}
const toastMessage = {
severity: this.mapTypesToSeverity(notification.type || "info"),
summary: notification.title || this.getDefaultTitle(notification.type || "info"),
detail: notification.message || "",
life: notification.persistent ? 0 : (notification.duration ?? this.defaultLife),
group: notification.group || "main",
};
toastInstance.add(toastMessage);
},
// Convenience methods for different types of notifications
addSuccess(message, title = "Success", options = {}) {
this.addNotification({
type: "success",
title,
message,
...options,
});
},
addError(message, title = "Error", options = {}) {
this.addNotification({
type: "error",
title,
message,
duration: options.duration ?? 6000, // Errors stay longer by default
...options,
});
},
addWarning(message, title = "Warning", options = {}) {
this.addNotification({
type: "warn",
title,
message,
...options,
});
},
addInfo(message, title = "Info", options = {}) {
this.addNotification({
type: "info",
title,
message,
...options,
});
},
// Show API operation notifications
showApiSuccess(operation, message = null) {
const defaultMessages = {
create: "Item created successfully",
update: "Item updated successfully",
delete: "Item deleted successfully",
fetch: "Data loaded successfully",
};
this.addSuccess(
message || defaultMessages[operation] || "Operation completed successfully",
);
},
showApiError(operation, error, message = null) {
const defaultMessages = {
create: "Failed to create item",
update: "Failed to update item",
delete: "Failed to delete item",
fetch: "Failed to load data",
};
let errorMessage = message;
if (!errorMessage) {
if (typeof error === "string") {
errorMessage = error;
} else if (error?.response?.data?.message) {
errorMessage = error.response.data.message;
} else if (error?.message) {
errorMessage = error.message;
} else {
errorMessage = defaultMessages[operation] || "Operation failed";
}
}
this.addError(errorMessage);
},
// Configuration methods
setPosition(position) {
this.position = position;
},
setDefaultLife(life) {
this.defaultLife = life;
},
// Clear all notifications
clearAll() {
if (toastInstance) {
toastInstance.removeAllGroups();
}
},
// Utility method for handling async operations with notifications
async withNotifications(operation, asyncFunction, options = {}) {
const {
loadingMessage = "Processing...",
successMessage = null,
errorMessage = null,
showLoading = true,
} = options;
try {
if (showLoading) {
this.addInfo(loadingMessage, "Loading", { persistent: true });
}
const result = await asyncFunction();
if (successMessage !== false) {
this.showApiSuccess(operation, successMessage);
}
return result;
} catch (error) {
this.showApiError(operation, error, errorMessage);
throw error;
}
},
// Helper methods
mapTypesToSeverity(type) {
const mapping = {
success: "success",
error: "error",
warn: "warn",
warning: "warn",
info: "info",
};
return mapping[type] || "info";
},
getDefaultTitle(type) {
const titles = {
success: "Success",
error: "Error",
warn: "Warning",
warning: "Warning",
info: "Information",
};
return titles[type] || "Notification";
},
},
});

View file

@ -0,0 +1,272 @@
import { defineStore } from "pinia";
// Global toast instance - will be set during app initialization
let toastInstance = null;
export const useNotificationStore = defineStore("notifications", {
state: () => ({
// Configuration for PrimeVue Toast
defaultLife: 4000,
position: "top-right",
}),
getters: {
// Helper to check if toast is available
isToastAvailable: () => !!toastInstance,
},
actions: {
// Add a new notification
addNotification(notification) {
const newNotification = {
id: this.nextId++,
type: notification.type || "info", // info, success, warning, error
title: notification.title || "",
message: notification.message || "",
duration: notification.duration ?? this.defaultDuration,
persistent: notification.persistent || false, // If true, won't auto-dismiss
actions: notification.actions || [], // Array of action buttons
data: notification.data || null, // Any additional data
timestamp: new Date().toISOString(),
dismissed: false,
seen: false,
};
// Add to beginning of array (newest first)
this.notifications.unshift(newNotification);
// Trim notifications if we exceed max count
if (this.notifications.length > this.maxNotifications * 2) {
// Keep twice the max to maintain some history
this.notifications = this.notifications.slice(0, this.maxNotifications * 2);
}
// Auto-dismiss if not persistent
if (!newNotification.persistent && newNotification.duration > 0) {
setTimeout(() => {
this.dismissNotification(newNotification.id);
}, newNotification.duration);
}
return newNotification.id;
},
// Convenience methods for different types of notifications
addSuccess(message, title = "Success", options = {}) {
return this.addNotification({
type: "success",
title,
message,
...options,
});
},
addError(message, title = "Error", options = {}) {
return this.addNotification({
type: "error",
title,
message,
duration: options.duration ?? 6000, // Errors stay longer by default
...options,
});
},
addWarning(message, title = "Warning", options = {}) {
return this.addNotification({
type: "warning",
title,
message,
...options,
});
},
addInfo(message, title = "Info", options = {}) {
return this.addNotification({
type: "info",
title,
message,
...options,
});
},
// Dismiss a specific notification
dismissNotification(id) {
const notification = this.notifications.find((n) => n.id === id);
if (notification) {
notification.dismissed = true;
}
},
// Remove a notification completely
removeNotification(id) {
const index = this.notifications.findIndex((n) => n.id === id);
if (index !== -1) {
this.notifications.splice(index, 1);
}
},
// Mark notification as seen
markAsSeen(id) {
const notification = this.notifications.find((n) => n.id === id);
if (notification) {
notification.seen = true;
}
},
// Clear all notifications of a specific type
clearType(type) {
this.notifications = this.notifications.filter((n) => n.type !== type);
},
// Clear all notifications
clearAll() {
this.notifications = [];
},
// Clear all dismissed notifications
clearDismissed() {
this.notifications = this.notifications.filter((n) => !n.dismissed);
},
// Update notification content
updateNotification(id, updates) {
const notification = this.notifications.find((n) => n.id === id);
if (notification) {
Object.assign(notification, updates);
}
},
// Show a loading notification that can be updated
showLoadingNotification(message, title = "Loading...") {
return this.addNotification({
type: "info",
title,
message,
persistent: true,
actions: [],
});
},
// Update a loading notification to success
updateToSuccess(id, message, title = "Success") {
this.updateNotification(id, {
type: "success",
title,
message,
persistent: false,
duration: this.defaultDuration,
});
// Auto-dismiss after updating
setTimeout(() => {
this.dismissNotification(id);
}, this.defaultDuration);
},
// Update a loading notification to error
updateToError(id, message, title = "Error") {
this.updateNotification(id, {
type: "error",
title,
message,
persistent: false,
duration: 6000,
});
// Auto-dismiss after updating
setTimeout(() => {
this.dismissNotification(id);
}, 6000);
},
// Show API operation notifications
showApiSuccess(operation, message = null) {
const defaultMessages = {
create: "Item created successfully",
update: "Item updated successfully",
delete: "Item deleted successfully",
fetch: "Data loaded successfully",
};
return this.addSuccess(
message || defaultMessages[operation] || "Operation completed successfully",
"Success",
);
},
showApiError(operation, error, message = null) {
const defaultMessages = {
create: "Failed to create item",
update: "Failed to update item",
delete: "Failed to delete item",
fetch: "Failed to load data",
};
let errorMessage = message;
if (!errorMessage) {
if (typeof error === "string") {
errorMessage = error;
} else if (error?.response?.data?.message) {
errorMessage = error.response.data.message;
} else if (error?.message) {
errorMessage = error.message;
} else {
errorMessage = defaultMessages[operation] || "Operation failed";
}
}
return this.addError(errorMessage, "Operation Failed");
},
// Configuration methods
setPosition(position) {
this.position = position;
},
setDefaultDuration(duration) {
this.defaultDuration = duration;
},
setMaxNotifications(max) {
this.maxNotifications = max;
},
// Utility method for handling async operations with notifications
async withNotifications(operation, asyncFunction, options = {}) {
const {
loadingMessage = "Processing...",
successMessage = null,
errorMessage = null,
showLoading = true,
} = options;
let loadingId = null;
try {
if (showLoading) {
loadingId = this.showLoadingNotification(loadingMessage);
}
const result = await asyncFunction();
if (loadingId) {
this.updateToSuccess(
loadingId,
successMessage || `${operation} completed successfully`,
);
} else if (successMessage !== false) {
this.showApiSuccess(operation, successMessage);
}
return result;
} catch (error) {
if (loadingId) {
this.updateToError(loadingId, errorMessage || error.message);
} else {
this.showApiError(operation, error, errorMessage);
}
throw error;
}
},
},
});