Enhance NewCustomerModal with callback support and toast notifications

- Add `onCustomerCreated` callback to refresh customer list after creation.
- Integrate `showInfoToast` and `showSuccessToast` for validation and creation feedback.
- Prevent closing modal on backdrop click; add explicit cancel button.
- Refactor `addCustomer` to use `callApi` and centralized routes.
- Simplify customer fetching logic in `CustomersPage` with reusable function.
This commit is contained in:
2025-07-11 19:53:52 +02:00
parent 644d907b45
commit 86be1e8920
4 changed files with 51 additions and 41 deletions

View File

@@ -16,7 +16,6 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;

View File

@@ -35,28 +35,28 @@ export default function CustomersPage() {
const pageSize = 15; const pageSize = 15;
const handleError = useErrorHandler(); const handleError = useErrorHandler();
useEffect(() => { const loadCustomers = async () => {
setLoading(true); setLoading(true);
fetch('/api/customers') try {
.then(async (response) => { const response = await fetch('/api/customers');
if (!response.ok) { if (!response.ok) {
showError("Failed to fetch customers data") showError("Failed to fetch customers data");
throw new Error(`Failed to fetch customers: ${response.statusText}`); throw new Error(`Failed to fetch customers: ${response.statusText}`);
} }
return response.json(); const data = await response.json();
}) showInfoToast("Customers data loaded");
.then((data) => { setCustomers(data);
showInfoToast("Customers data loaded") } catch (error) {
setCustomers(data); showError("Failed to fetch customers data (1)");
}) handleError(error);
.catch((error) => { setCustomers([]);
showError("Failed to fetch customers data (1)") } finally {
handleError(error); setLoading(false);
setCustomers([]); }
}) };
.finally(() => {
setLoading(false); useEffect(() => {
}); loadCustomers();
}, [handleError]); }, [handleError]);
const filtered = useMemo(() => { const filtered = useMemo(() => {
@@ -114,7 +114,7 @@ export default function CustomersPage() {
<CardContent className="space-y-6"> <CardContent className="space-y-6">
<div className="flex flex-col md:flex-row justify-between items-center gap-4"> <div className="flex flex-col md:flex-row justify-between items-center gap-4">
<Input placeholder="Suche" value={search} onChange={(e) => setSearch(e.target.value)}/> <Input placeholder="Suche" value={search} onChange={(e) => setSearch(e.target.value)}/>
<NewCustomerModal/> <NewCustomerModal onCustomerCreated={loadCustomers}/>
</div> </div>
{customers.length === 0 && loading ? ( {customers.length === 0 && loading ? (

View File

@@ -14,8 +14,13 @@ import {CreateCustomerDto, NoteDto, PhoneNumberDto} from "@/services/customers/d
import {addCustomer} from "@/services/customers/usecases/addCustomer"; import {addCustomer} from "@/services/customers/usecases/addCustomer";
import {validateCustomer} from "@/services/customers/usecases/validateCustomer"; import {validateCustomer} from "@/services/customers/usecases/validateCustomer";
import {useErrorHandler} from "@/components/error-boundary"; import {useErrorHandler} from "@/components/error-boundary";
import {showInfoToast, showSuccessToast} from "@/lib/ui/showToast";
export function NewCustomerModal() { interface NewCustomerModalProps {
onCustomerCreated?: () => void;
}
export function NewCustomerModal({ onCustomerCreated }: NewCustomerModalProps) {
const [step, setStep] = useState(1); const [step, setStep] = useState(1);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
@@ -53,6 +58,7 @@ export function NewCustomerModal() {
try { try {
const result = await validateCustomer({email, companyName, street, zip, city}); const result = await validateCustomer({email, companyName, street, zip, city});
setMatches(result); setMatches(result);
showInfoToast("Datenvalidierung abgeschlossen");
} catch (err) { } catch (err) {
handleError(err); handleError(err);
} }
@@ -63,7 +69,11 @@ export function NewCustomerModal() {
try { try {
const payload: CreateCustomerDto = {email, name, companyName, street, zip, city, phoneNumbers, notes}; const payload: CreateCustomerDto = {email, name, companyName, street, zip, city, phoneNumbers, notes};
await addCustomer(payload); await addCustomer(payload);
location.reload(); showSuccessToast("Kunde erfolgreich erstellt");
setOpen(false);
if (onCustomerCreated) {
onCustomerCreated();
}
} catch (err) { } catch (err) {
handleError(err); handleError(err);
} }
@@ -215,14 +225,16 @@ export function NewCustomerModal() {
return ( return (
<> <>
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={() => {
// Prevent closing on backdrop click - modal can only be closed explicitly
}}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button onClick={() => { <Button onClick={() => {
setOpen(true); setOpen(true);
setStep(1); setStep(1);
}}>Neue Kunde</Button> }}>Neue Kunde</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="w-full max-w-5xl max-h-[95vh] overflow-y-auto"> <DialogContent className="w-full max-w-5xl max-h-[95vh] overflow-y-auto" showCloseButton={false}>
<DialogHeader><DialogTitle>Neuen Kunden anlegen</DialogTitle></DialogHeader> <DialogHeader><DialogTitle>Neuen Kunden anlegen</DialogTitle></DialogHeader>
<div className="mb-4"> <div className="mb-4">
<div className="text-xs font-semibold mb-2">Schritt {step} von 2</div> <div className="text-xs font-semibold mb-2">Schritt {step} von 2</div>
@@ -233,9 +245,14 @@ export function NewCustomerModal() {
{matches.length > 0 && renderDuplicationCard()} {matches.length > 0 && renderDuplicationCard()}
<div className="flex justify-between mt-6"> <div className="flex justify-between mt-6">
<Button variant="secondary" disabled={step === 1} onClick={() => setStep(step - 1)}> <div className="flex gap-2">
Zurück <Button variant="outline" onClick={() => setOpen(false)}>
</Button> Abbrechen
</Button>
<Button variant="secondary" disabled={step === 1} onClick={() => setStep(step - 1)}>
Zurück
</Button>
</div>
{step === 1 ? ( {step === 1 ? (
<Button <Button
onClick={() => setStep(2)} onClick={() => setStep(2)}

View File

@@ -1,6 +1,9 @@
"use server"; "use server";
import {CreateCustomerDto} from "@/services/customers/dtos/createCustomer.dto"; import {CreateCustomerDto} from "@/services/customers/dtos/createCustomer.dto";
import {customerRoutes} from "@/app/api/customers/customerRoutes";
import {callApi} from "@/lib/api/callApi";
import {UUID} from "node:crypto";
export async function addCustomer(params: CreateCustomerDto): Promise<void> { export async function addCustomer(params: CreateCustomerDto): Promise<void> {
const {email, name, companyName, street, zip, city, phoneNumbers, notes} = params; const {email, name, companyName, street, zip, city, phoneNumbers, notes} = params;
@@ -16,15 +19,6 @@ export async function addCustomer(params: CreateCustomerDto): Promise<void> {
notes: notes.map(({text}) => ({text})), notes: notes.map(({text}) => ({text})),
}; };
const response = await fetch('/api/customers', { const response = await callApi<UUID>(customerRoutes.create, "POST", payload);
method: 'POST', console.log(response);
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`Failed to create customer: ${response.statusText}`);
}
} }