diff --git a/internal_frontend/app/customers/page.tsx b/internal_frontend/app/customers/page.tsx index cd96151..ecdd5b0 100644 --- a/internal_frontend/app/customers/page.tsx +++ b/internal_frontend/app/customers/page.tsx @@ -24,6 +24,7 @@ import {ArrowRight} from "lucide-react"; import {NewCustomerModal} from "@/components/customers/modal/NewCustomerModal"; import {Customer} from "@/services/customers/entities/customer"; import Link from "next/link"; +import {useErrorHandler} from "@/components/error-boundary"; export default function CustomersPage() { const [customers, setCustomers] = useState([]); @@ -31,6 +32,7 @@ export default function CustomersPage() { const [loading, setLoading] = useState(true); const [page, setPage] = useState(1); const pageSize = 15; + const handleError = useErrorHandler(); useEffect(() => { setLoading(true); @@ -45,13 +47,13 @@ export default function CustomersPage() { setCustomers(data); }) .catch((error) => { - console.error('Error fetching customers:', error); + handleError(error); setCustomers([]); }) .finally(() => { setLoading(false); }); - }, []); + }, [handleError]); const filtered = useMemo(() => { if (customers.length === 0) return []; diff --git a/internal_frontend/app/layout.tsx b/internal_frontend/app/layout.tsx index 89f6214..e0cfce2 100644 --- a/internal_frontend/app/layout.tsx +++ b/internal_frontend/app/layout.tsx @@ -9,6 +9,8 @@ import {DynamicBreadcrumb} from "@/components/dynamic-breadcrumb"; import {getServerSession} from "next-auth"; import LoginScreen from "@/components/login-screen"; import {authOptions} from "@/lib/api/auth/authOptions"; +import {ErrorBoundary} from "@/components/error-boundary"; +import {Toaster} from "sonner"; export const metadata: Metadata = { title: "Internal | Rhein Software", @@ -25,42 +27,49 @@ export default async function RootLayout({ return ( - - {session?.accessToken ? ( - - + + + {session?.accessToken ? ( + + - -
-
- - - -
-
- {children} -
-
- ) : ( - - )} -
+ +
+
+ + + +
+
+ + {children} + +
+
+ ) : ( + + + + )} + +
+ ); diff --git a/internal_frontend/components/customers/modal/NewCustomerModal.tsx b/internal_frontend/components/customers/modal/NewCustomerModal.tsx index f474828..c09ddcd 100644 --- a/internal_frontend/components/customers/modal/NewCustomerModal.tsx +++ b/internal_frontend/components/customers/modal/NewCustomerModal.tsx @@ -13,6 +13,7 @@ import {Card, CardContent, CardHeader} from "@/components/ui/card"; import {CreateCustomerDto, NoteDto, PhoneNumberDto} from "@/services/customers/dtos/createCustomer.dto"; import {addCustomer} from "@/services/customers/usecases/addCustomer"; import {validateCustomer} from "@/services/customers/usecases/validateCustomer"; +import {useErrorHandler} from "@/components/error-boundary"; export function NewCustomerModal() { const [step, setStep] = useState(1); @@ -28,6 +29,7 @@ export function NewCustomerModal() { const [matches, setMatches] = useState([]); const [showDetailModal, setShowDetailModal] = useState(false); const [selectedCustomer] = useState(null); + const handleError = useErrorHandler(); type CustomerMatch = { id: string; @@ -52,15 +54,19 @@ export function NewCustomerModal() { const result = await validateCustomer({email, companyName, street, zip, city}); setMatches(result); } catch (err) { - console.error("Validation failed", err); + handleError(err); } }; const handleSubmit = async () => { if (!email || !name || !companyName || !street || !zip || !city) return; - const payload: CreateCustomerDto = {email, name, companyName, street, zip, city, phoneNumbers, notes}; - await addCustomer(payload); - location.reload(); + try { + const payload: CreateCustomerDto = {email, name, companyName, street, zip, city, phoneNumbers, notes}; + await addCustomer(payload); + location.reload(); + } catch (err) { + handleError(err); + } }; const renderFormInput = ( @@ -277,4 +283,4 @@ export function NewCustomerModal() { ); -} \ No newline at end of file +} diff --git a/internal_frontend/components/error-boundary.tsx b/internal_frontend/components/error-boundary.tsx new file mode 100644 index 0000000..063f7c5 --- /dev/null +++ b/internal_frontend/components/error-boundary.tsx @@ -0,0 +1,137 @@ +"use client"; + +import React from "react"; +import { showError } from "@/lib/ui/showError"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; + errorInfo: React.ErrorInfo | null; +} + +interface ErrorBoundaryProps { + children: React.ReactNode; + fallback?: React.ComponentType<{ error: Error; reset: () => void }>; +} + +export class ErrorBoundary extends React.Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { + hasError: false, + error: null, + errorInfo: null, + }; + } + + static getDerivedStateFromError(error: Error): Partial { + return { + hasError: true, + error, + }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error("ErrorBoundary caught an error:", error, errorInfo); + + this.setState({ + error, + errorInfo, + }); + + // Show error toast + showError(error); + } + + handleReset = () => { + this.setState({ + hasError: false, + error: null, + errorInfo: null, + }); + }; + + render() { + if (this.state.hasError) { + if (this.props.fallback) { + const FallbackComponent = this.props.fallback; + return ( + + ); + } + + return ( + + ); + } + + return this.props.children; + } +} + +interface ErrorDialogProps { + error: Error | null; + onReset: () => void; + open: boolean; +} + +function ErrorDialog({ error, onReset, open }: ErrorDialogProps) { + return ( + {}}> + + + Ein Fehler ist aufgetreten + + Es ist ein unerwarteter Fehler aufgetreten. Bitte versuchen Sie es erneut. + + + + {error && ( +
+

+ {error.message} +

+
+ )} + + + + + +
+
+ ); +} + +// Hook for handling async errors +export function useErrorHandler() { + const handleError = React.useCallback((error: unknown) => { + console.error("Async error caught:", error); + showError(error); + }, []); + + return handleError; +} \ No newline at end of file