Customer Detail Page and Enhance dynamic breadcrumbs

This commit is contained in:
2025-07-06 17:24:12 +00:00
parent 055d19d201
commit e00142ff81
14 changed files with 934 additions and 69 deletions

View File

@@ -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);
}

View File

@@ -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<Customer | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [openDialog, setOpenDialog] = useState(false);
const [dialogContent, setDialogContent] = useState<React.ReactNode | null>(null);
useEffect(() => {
if (!id) {
setError("Keine Kunden-ID angegeben");
setLoading(false);
return;
}
let isMounted = true;
setLoading(true);
setError(null);
axios
.get<Customer>(`${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 (
<div className="flex flex-col items-end gap-1">
<div className="space-y-1">
<Skeleton className="h-4 w-[250px]"/>
<Skeleton className="h-4 w-[280px]"/>
</div>
</div>
);
}
return (
<div className="flex flex-col items-end gap-1 text-xs text-muted-foreground">
<div>{customerMetadata.createdInfo}</div>
<div>{customerMetadata.lastActivityInfo}</div>
</div>
);
};
return (
<div className="h-full w-full p-6 space-y-4 text-sm">
<div className="flex justify-between items-start">
<Button
variant="ghost"
onClick={() => router.push("/customers")}
disabled={loading}
aria-label="Zurück zur Kundenliste"
>
<ChevronLeft className="w-4 h-4 mr-1"/>
Zurück
</Button>
{renderMetadata()}
</div>
{error ? (
<div className="text-center text-red-500 p-4" role="alert">
{error}
</div>
) : (
<CustomerDetailContent
loading={loading}
customer={customer}
sections={
customer
? [
<CustomerInformationContent
key="customerInformation"
customer={customer}
handleOpenDialog={handleOpenDialog}
/>,
<CustomerPhoneNumberContent
key="customerPhoneNumberInfo"
customer={customer}
handleOpenDialog={handleOpenDialog}
/>,
<CustomerNotesContent
key="customerNotes"
customer={customer}
handleOpenDialog={handleOpenDialog}
/>
]
: []
}
/>
)}
<Dialog open={openDialog} onOpenChange={setOpenDialog}>
{dialogContent}
</Dialog>
</div>
);
}

View File

@@ -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]);