From e00142ff8115cb6a44f1d91c11a274a3aadfbd98 Mon Sep 17 00:00:00 2001 From: Thatsaphorn Atchariyaphap Date: Sun, 6 Jul 2025 17:24:12 +0000 Subject: [PATCH] Customer Detail Page and Enhance dynamic breadcrumbs --- .../controller/CustomerController.java | 2 +- .../app/api/customers/[id]/route.ts | 14 ++ internal_frontend/app/customers/[id]/page.tsx | 170 +++++++++++++ internal_frontend/app/customers/page.tsx | 54 ++-- .../details/CustomerDetailContent.tsx | 47 ++++ .../details/sub/ContactInformationContent.tsx | 108 ++++++++ .../details/sub/CustomerNotesContent.tsx | 217 +++++++++++++++++ .../sub/CustomerPhoneNumberContent.tsx | 230 ++++++++++++++++++ .../components/dynamic-breadcrumb.tsx | 101 +++++++- internal_frontend/components/ui/card.tsx | 2 +- internal_frontend/lib/breadcrumb-map.ts | 13 +- .../services/customers/entities/customer.ts | 26 +- .../customers/usecases/validateCustomer.ts | 2 +- internal_frontend/utils/BreadcrumbUtils.ts | 17 +- 14 files changed, 934 insertions(+), 69 deletions(-) create mode 100644 internal_frontend/app/api/customers/[id]/route.ts create mode 100644 internal_frontend/app/customers/[id]/page.tsx create mode 100644 internal_frontend/components/customers/details/CustomerDetailContent.tsx create mode 100644 internal_frontend/components/customers/details/sub/ContactInformationContent.tsx create mode 100644 internal_frontend/components/customers/details/sub/CustomerNotesContent.tsx create mode 100644 internal_frontend/components/customers/details/sub/CustomerPhoneNumberContent.tsx diff --git a/backend/server/src/main/java/dev/rheinsw/server/customer/controller/CustomerController.java b/backend/server/src/main/java/dev/rheinsw/server/customer/controller/CustomerController.java index e5cd9aa..4beccd1 100644 --- a/backend/server/src/main/java/dev/rheinsw/server/customer/controller/CustomerController.java +++ b/backend/server/src/main/java/dev/rheinsw/server/customer/controller/CustomerController.java @@ -53,7 +53,7 @@ public class CustomerController extends AbstractController { } @GetMapping("/{id}") - public ResponseEntity loadById(@PathVariable UUID id) { + public ResponseEntity loadById(@PathVariable("id") UUID id) { return ResponseEntity.ok(loadCustomerQuery.loadById(id)); } diff --git a/internal_frontend/app/api/customers/[id]/route.ts b/internal_frontend/app/api/customers/[id]/route.ts new file mode 100644 index 0000000..b022417 --- /dev/null +++ b/internal_frontend/app/api/customers/[id]/route.ts @@ -0,0 +1,14 @@ +import {NextRequest, NextResponse} from "next/server"; +import {serverCall} from "@/lib/api/serverCall"; + +export async function GET(request: NextRequest) { + const id = request.url.split('/').pop(); + const response = await serverCall(`/customers/${id}`, "GET"); + + if (!response.ok) { + return NextResponse.json({error: "Customer not found"}, {status: 404}); + } + + const customer = await response.json(); + return NextResponse.json(customer); +} \ No newline at end of file diff --git a/internal_frontend/app/customers/[id]/page.tsx b/internal_frontend/app/customers/[id]/page.tsx new file mode 100644 index 0000000..c10068c --- /dev/null +++ b/internal_frontend/app/customers/[id]/page.tsx @@ -0,0 +1,170 @@ +"use client"; + +import React, {useEffect, useState, useMemo} from "react"; +import {useParams, useRouter} from "next/navigation"; +import {ChevronLeft} from "lucide-react"; +import axios, {AxiosError} from "axios"; +import {Button} from "@/components/ui/button"; +import {Dialog} from "@/components/ui/dialog"; +import {Skeleton} from "@/components/ui/skeleton"; +import CustomerDetailContent from "@/components/customers/details/CustomerDetailContent"; +import {Customer} from "@/services/customers/entities/customer"; +import CustomerInformationContent from "@/components/customers/details/sub/ContactInformationContent"; +import CustomerPhoneNumberContent from "@/components/customers/details/sub/CustomerPhoneNumberContent"; +import CustomerNotesContent from "@/components/customers/details/sub/CustomerNotesContent"; + +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || '/api'; + +export default function CustomerDetailPage() { + const router = useRouter(); + const {id} = useParams<{ id: string }>(); + const [customer, setCustomer] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [openDialog, setOpenDialog] = useState(false); + const [dialogContent, setDialogContent] = useState(null); + + useEffect(() => { + if (!id) { + setError("Keine Kunden-ID angegeben"); + setLoading(false); + return; + } + + let isMounted = true; + setLoading(true); + setError(null); + + axios + .get(`${API_BASE_URL}/customers/${id}`) + .then((res) => { + if (isMounted) { + setCustomer(res.data); + } + }) + .catch((error: AxiosError) => { + if (isMounted) { + console.error('Error fetching customer:', error); + setCustomer(null); + setError( + error.response?.status === 404 + ? "Kunde nicht gefunden" + : "Fehler beim Laden der Kundendaten" + ); + } + }) + .finally(() => { + if (isMounted) { + setLoading(false); + } + }); + + return () => { + isMounted = false; + }; + }, [id]); + + const handleOpenDialog = (content: React.ReactNode) => { + setDialogContent(content); + setOpenDialog(true); + }; + + const formatDate = (date: string) => { + try { + const formattedDate = new Date(date).toLocaleDateString('de-DE', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + return formattedDate === 'Invalid Date' ? '-' : formattedDate; + } catch (error) { + console.error('Error formatting date:', error); + return '-'; + } + }; + + const customerMetadata = useMemo(() => ({ + createdInfo: customer + ? `Erstellt von ${customer.createdBy || "-"} am ${formatDate(customer.createdAt)}` + : "Erstellt von - am -", + lastActivityInfo: customer + ? `Letzte Aktivität: ${customer.updatedBy || "-"} am ${formatDate(customer.updatedAt)}` + : "Letzte Aktivität: - am -" + }), [customer]); + + const renderMetadata = () => { + if (loading) { + return ( +
+
+ + +
+
+ ); + } + + return ( +
+
{customerMetadata.createdInfo}
+
{customerMetadata.lastActivityInfo}
+
+ ); + }; + + return ( +
+
+ + + {renderMetadata()} +
+ + {error ? ( +
+ {error} +
+ ) : ( + , + , + + ] + : [] + } + /> + )} + + + {dialogContent} + +
+ ); +} \ No newline at end of file diff --git a/internal_frontend/app/customers/page.tsx b/internal_frontend/app/customers/page.tsx index 15fb04f..741b18d 100644 --- a/internal_frontend/app/customers/page.tsx +++ b/internal_frontend/app/customers/page.tsx @@ -24,36 +24,7 @@ import {motion} from "framer-motion"; import {ArrowRight} from "lucide-react"; import {NewCustomerModal} from "@/components/customers/modal/NewCustomerModal"; import axios from "axios"; - -export interface CustomerPhoneNumber { - number: string; - note: string; - creator: string; - lastModifier: string; - createdAt: string; - updatedAt: string; -} - -export interface CustomerNote { - text: string; - creator: string; - lastModifier: string; - createdAt: string; - updatedAt: string; -} - -export interface Customer { - id: string; - email: string; - name: string; - companyName: string; - phoneNumbers: CustomerPhoneNumber[]; - street: string; - zip: string; - city: string; - notes: CustomerNote[]; - createdAt: string; -} +import {Customer} from "@/services/customers/entities/customer"; export default function CustomersPage() { const router = useRouter(); @@ -64,17 +35,30 @@ export default function CustomersPage() { const pageSize = 15; useEffect(() => { - axios.get("/api/customers").then((res) => { - setCustomers(res.data); - setLoading(false); - }); + setLoading(true); + axios.get("/api/customers") + .then((res) => { + setCustomers(res.data); + }) + .catch((error) => { + console.error('Error fetching customers:', error); + setCustomers([]); + }) + .finally(() => { + setLoading(false); + }); }, []); const filtered = useMemo(() => { return customers.filter( (c) => c.name.toLowerCase().includes(search.toLowerCase()) || - c.email.toLowerCase().includes(search.toLowerCase()) + c.email.toLowerCase().includes(search.toLowerCase()) || + c.companyName.toLowerCase().includes(search.toLowerCase()) || + c.street.toLowerCase().includes(search.toLowerCase()) || + c.zip.toLowerCase().includes(search.toLowerCase()) || + c.city.toLowerCase().includes(search.toLowerCase()) || + c.phoneNumbers?.[0]?.number?.toLowerCase().includes(search.toLowerCase()) ); }, [customers, search]); diff --git a/internal_frontend/components/customers/details/CustomerDetailContent.tsx b/internal_frontend/components/customers/details/CustomerDetailContent.tsx new file mode 100644 index 0000000..69bbca0 --- /dev/null +++ b/internal_frontend/components/customers/details/CustomerDetailContent.tsx @@ -0,0 +1,47 @@ +import {Card} from "@/components/ui/card"; +import {Skeleton} from "@/components/ui/skeleton"; +import {Customer} from "@/services/customers/entities/customer"; +import React from "react"; + +type Props = { + loading: boolean; + customer: Customer | null; + sections: React.ReactNode[]; +}; + +export default function CustomerDetailContent({loading, customer, sections}: Readonly) { + if (loading) { + return ( +
+ + + + + + + + + + + + + + +
+ ); + } + + if (!customer) { + return ( +
+

Keine Kundendaten gefunden.

+
+ ); + } + + return ( +
+ {sections} +
+ ); +} diff --git a/internal_frontend/components/customers/details/sub/ContactInformationContent.tsx b/internal_frontend/components/customers/details/sub/ContactInformationContent.tsx new file mode 100644 index 0000000..53043b2 --- /dev/null +++ b/internal_frontend/components/customers/details/sub/ContactInformationContent.tsx @@ -0,0 +1,108 @@ +import {Button} from "@/components/ui/button"; +import {Mail, Pencil, Building, MapPin, User, Copy} from "lucide-react"; +import {Card} from "@/components/ui/card"; +import React from "react"; +import { + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip"; +import {Customer} from "@/services/customers/entities/customer"; + +interface Props { + customer: Customer, + handleOpenDialog: (content: React.ReactNode) => void; +} + +export default function ContactInformationContent({customer, handleOpenDialog}: Readonly) { + const copyToClipboard = async (text: string) => { + await navigator.clipboard.writeText(text); + }; + + const handleEditClick = () => { + handleOpenDialog( + + + Kontaktinformationen bearbeiten + + Bearbeite hier die Kontaktinformationen für {customer.name}. + + +
+ {/* Add your form fields here */} +

Form fields will go here.

+
+

Name: {customer.name}

+

E-Mail: {customer.email} +

+

Firma: {customer.companyName}

+

+ Adresse: {customer.street}, {customer.zip} {customer.city} +

+
+
+ + + + +
+ ); + }; + + return ( + +
+
+ +

Kontaktinformationen

+
+ +
+
+ {[ + {icon: User, label: "Name", value: customer.name}, + {icon: Mail, label: "E-Mail", value: customer.email, copyable: true}, + {icon: Building, label: "Firma", value: customer.companyName}, + {icon: MapPin, label: "Adresse", value: `${customer.street}, ${customer.zip} ${customer.city}`} + ].map((item, index) => ( +
+ +
+

{item.label}

+
+

{item.value}

+ {item.copyable && ( + + + + + Kopieren + + )} +
+
+
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/internal_frontend/components/customers/details/sub/CustomerNotesContent.tsx b/internal_frontend/components/customers/details/sub/CustomerNotesContent.tsx new file mode 100644 index 0000000..22a7acf --- /dev/null +++ b/internal_frontend/components/customers/details/sub/CustomerNotesContent.tsx @@ -0,0 +1,217 @@ +import React, {useState} from 'react'; +import {motion, AnimatePresence} from "framer-motion"; +import {Card} from "@/components/ui/card"; +import {Button} from "@/components/ui/button"; +import {ChevronDown, ChevronRight, Pencil, Plus, Trash2, Notebook} from "lucide-react"; +import {Customer, CustomerNote} from "@/services/customers/entities/customer"; +import { + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import {Label} from "@/components/ui/label"; +import {Textarea} from "@/components/ui/textarea"; +import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip"; + +type Props = { + customer: Customer; + handleOpenDialog: (content: React.ReactNode) => void; +}; + +export default function CustomerNotesContent({customer, handleOpenDialog}: Readonly) { + const [expandedNotes, setExpandedNotes] = useState([]); + + const toggleNote = (index: number) => { + setExpandedNotes(prev => + prev.includes(index) ? prev.filter(i => i !== index) : [...prev, index] + ); + }; + + const handleAddNote = () => { + handleOpenDialog( + + + Neue Notiz hinzufügen + + Füge eine neue Notiz für {customer.name} hinzu. + + +
+
+ +