Files
rheinsw-mono-repo/internal_frontend/components/customers/modal/NewCustomerModal.tsx
Thatsaphorn Atchariyaphap 6aae06635d Reset form state on dialog close in NewCustomerModal
- Add `resetForm` utility to restore initial modal state.
- Automatically reset form state when dialog closes or a new customer is created to improve UX.
2025-07-11 20:36:51 +02:00

323 lines
13 KiB
TypeScript

"use client";
import {useState} from "react";
import {motion} from "framer-motion";
import {Trash2} from "lucide-react";
import {Button} from "@/components/ui/button";
import {Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger,} from "@/components/ui/dialog";
import {Label} from "@/components/ui/label";
import {Input} from "@/components/ui/input";
import {Textarea} from "@/components/ui/textarea";
import {Progress} from "@/components/ui/progress";
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";
import {showInfoToast, showSuccessToast} from "@/lib/ui/showToast";
interface NewCustomerModalProps {
onCustomerCreated?: () => void;
}
export function NewCustomerModal({onCustomerCreated}: Readonly<NewCustomerModalProps>) {
const [step, setStep] = useState(1);
const [open, setOpen] = useState(false);
const [email, setEmail] = useState("");
const [name, setName] = useState("");
const [companyName, setCompanyName] = useState("");
const [phoneNumbers, setPhoneNumbers] = useState<PhoneNumberDto[]>([{note: "", number: ""}]);
const [notes, setNotes] = useState<NoteDto[]>([{text: ""}]);
const [street, setStreet] = useState("");
const [zip, setZip] = useState("");
const [city, setCity] = useState("");
const [matches, setMatches] = useState<CustomerMatch[]>([]);
const [showDetailModal, setShowDetailModal] = useState(false);
const [selectedCustomer] = useState<CustomerMatch | null>(null);
const handleError = useErrorHandler();
type CustomerMatch = {
id: string;
name: string;
email: string;
companyName: string;
street: string;
zip: string;
city: string;
};
const emailExists = Array.isArray(matches) && matches.some(m => m.email.toLowerCase() === email.toLowerCase());
const companyExists = Array.isArray(matches) && matches.some(m => m.companyName.toLowerCase() === companyName.toLowerCase());
const addressExists = Array.isArray(matches) && matches.some(m =>
m.street.toLowerCase() === street.toLowerCase() &&
m.zip.toLowerCase() === zip.toLowerCase() &&
m.city.toLowerCase() === city.toLowerCase()
);
const validateField = async () => {
try {
const result = await validateCustomer({email, companyName, street, zip, city});
setMatches(result || []);
showInfoToast("Datenvalidierung abgeschlossen");
} catch (err) {
setMatches([]); // Ensure matches is always an array
handleError(err);
}
};
const handleSubmit = async () => {
if (!email || !name || !companyName || !street || !zip || !city) return;
try {
const payload: CreateCustomerDto = {email, name, companyName, street, zip, city, phoneNumbers, notes};
await addCustomer(payload);
showSuccessToast("Kunde erfolgreich erstellt");
setOpen(false);
if (onCustomerCreated) {
onCustomerCreated();
}
resetForm();
} catch (err) {
handleError(err);
}
};
const renderFormInput = (
label: string,
value: string,
onChange: (value: string) => void,
error?: boolean,
className?: string
) => (
<div className="space-y-2">
<Label>{label} *</Label>
<Input
value={value}
onChange={(e) => onChange(e.target.value)}
onBlur={validateField}
className={error ? "border border-red-500" : className}
/>
</div>
);
const renderCustomerInfo = (customer: CustomerMatch) => (
<div className="space-y-1 text-sm">
<div><strong>Name:</strong> {customer.name}</div>
<div><strong>Firma:</strong> {customer.companyName}</div>
<div><strong>E-Mail:</strong> {customer.email}</div>
<div><strong>Adresse:</strong> {customer.street}, {customer.zip} {customer.city}</div>
</div>
);
const updatePhoneNumber = (i: number, key: keyof PhoneNumberDto, value: string) => {
const updated = [...phoneNumbers];
updated[i][key] = value;
setPhoneNumbers(updated);
};
const removePhoneNumber = (i: number) => {
setPhoneNumbers(phoneNumbers.filter((_, idx) => idx !== i));
};
const updateNote = (i: number, value: string) => {
const updated = [...notes];
updated[i].text = value;
setNotes(updated);
};
const resetForm = () => {
setStep(1); // Reset to step 1
setEmail("");
setName("");
setCompanyName("");
setPhoneNumbers([{note: "", number: ""}]);
setNotes([{text: ""}]);
setStreet("");
setZip("");
setCity("");
setMatches([]);
setShowDetailModal(false);
};
const renderStepOne = () => (
<div className="space-y-4">
<div className="space-y-2">
{renderFormInput("Name", name, setName)}
{renderFormInput("Firma", companyName, setCompanyName, companyExists)}
{renderFormInput("E-Mail", email, setEmail, emailExists)}
{emailExists && (
<div className="text-red-500 text-sm">Ein Kunde mit dieser E-Mail existiert bereits.</div>
)}
</div>
<div className="space-y-2">
<Label>Telefonnummern *</Label>
{phoneNumbers.map((p, i) => (
<div key={i} className="grid grid-cols-[2fr_3fr_auto] gap-2 items-center w-full">
<Input
placeholder="Nummer"
value={p.number}
onChange={(e) => updatePhoneNumber(i, "number", e.target.value)}
/>
<Input
placeholder="Notiz"
value={p.note}
onChange={(e) => updatePhoneNumber(i, "note", e.target.value)}
/>
<Button variant="ghost" size="icon" onClick={() => removePhoneNumber(i)}>
<Trash2 className="w-4 h-4"/>
</Button>
</div>
))}
<Button
variant="outline"
size="sm"
onClick={() => setPhoneNumbers([...phoneNumbers, {note: "", number: ""}])}
>
+ Telefonnummer hinzufügen
</Button>
</div>
</div>
);
const renderStepTwo = () => (
<div className="space-y-4">
<div className="grid grid-cols-12 gap-4">
<div className="col-span-6">
{renderFormInput("Straße", street, setStreet, addressExists)}
</div>
<div className="col-span-3">
{renderFormInput("PLZ", zip, setZip, addressExists)}
</div>
<div className="col-span-3">
{renderFormInput("Ort", city, setCity, addressExists)}
</div>
</div>
<div className="space-y-2">
<Label>Notizen</Label>
{notes.map((note, i) => (
<div key={i} className="flex items-start gap-2">
<Textarea
placeholder={`Notiz ${i + 1}`}
value={note.text}
rows={3}
className="w-full resize-y"
onChange={(e) => updateNote(i, e.target.value)}
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setNotes(notes.filter((_, idx) => idx !== i))}
>
<Trash2 className="w-4 h-4"/>
</Button>
</div>
))}
<Button
type="button"
onClick={() => setNotes([...notes, {text: ""}])}
variant="outline"
size="sm"
>
+ Notiz hinzufügen
</Button>
</div>
</div>
);
const renderDuplicationCard = () => (
<motion.div initial={{opacity: 0}} animate={{opacity: 1}} transition={{duration: 0.3}} className="mt-6">
<Card
className={emailExists ? "border-red-500 bg-red-100 dark:bg-red-900/20" : "border-yellow-500 bg-yellow-100 dark:bg-yellow-900/20"}>
<CardHeader className="text-sm font-semibold">
{matches.length} mögliche Duplikate gefunden
</CardHeader>
<CardContent>
<Button variant="outline" size="sm" className="w-full"
onClick={() => setShowDetailModal(true)}>Details</Button>
</CardContent>
</Card>
</motion.div>
);
return (
<>
<Dialog open={open} onOpenChange={() => {
if (!open) {
resetForm();
}
// Prevent closing on backdrop click - modal can only be closed explicitly
}}>
<DialogTrigger asChild>
<Button onClick={() => {
setOpen(true);
setStep(1);
}}>Neue Kunde</Button>
</DialogTrigger>
<DialogContent className="w-full max-w-5xl max-h-[95vh] overflow-y-auto" showCloseButton={false}>
<DialogHeader><DialogTitle>Neuen Kunden anlegen</DialogTitle></DialogHeader>
<div className="mb-4">
<div className="text-xs font-semibold mb-2">Schritt {step} von 2</div>
<Progress value={step === 1 ? 50 : 100}/>
</div>
{step === 1 ? renderStepOne() : renderStepTwo()}
{matches.length > 0 && renderDuplicationCard()}
<div className="flex justify-between mt-6">
<div className="flex gap-2">
<Button variant="outline" onClick={() => setOpen(false)}>
Abbrechen
</Button>
<Button variant="secondary" disabled={step === 1} onClick={() => setStep(step - 1)}>
Zurück
</Button>
</div>
{step === 1 ? (
<Button
onClick={() => setStep(2)}
disabled={!email || !name || !companyName || phoneNumbers.length === 0 || !phoneNumbers.some(p => p.number.trim()) || emailExists}
>
Weiter
</Button>
) : (
<Button onClick={handleSubmit} disabled={!street || !zip || !city}>
Anlegen
</Button>
)}
</div>
</DialogContent>
</Dialog>
<Dialog open={showDetailModal} onOpenChange={setShowDetailModal}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto space-y-4">
<DialogHeader>
<DialogTitle>Kundendetails</DialogTitle>
</DialogHeader>
{matches.length === 1 ? (
renderCustomerInfo(matches[0])
) : (
<div className="space-y-4">
{matches.map((customer) => (
<Card key={customer.id}>
<CardContent className="pt-4">
{renderCustomerInfo(customer)}
</CardContent>
</Card>
))}
{selectedCustomer && (
<motion.div
initial={{opacity: 0, y: 10}}
animate={{opacity: 1, y: 0}}
className="p-4 border rounded-md bg-muted"
>
{renderCustomerInfo(selectedCustomer)}
</motion.div>
)}
</div>
)}
</DialogContent>
</Dialog>
</>
);
}