Feature: Admin kann Nachrichten an Kunden senden
- Neues Email-Template für Kundennachrichten - RPC-Funktion sendCustomerMessage für zukünftige Termine - UI: Nachricht-Button und Modal in Admin-Buchungen - Email mit BCC an Admin für Monitoring - HTML-Escaping für sichere Nachrichtenanzeige - Detailliertes Logging für Debugging
This commit is contained in:
@@ -7,6 +7,8 @@ export function AdminBookings() {
|
||||
const [selectedPhoto, setSelectedPhoto] = useState<string>("");
|
||||
const [showPhotoModal, setShowPhotoModal] = useState(false);
|
||||
const [showCancelConfirm, setShowCancelConfirm] = useState<string | null>(null);
|
||||
const [showMessageModal, setShowMessageModal] = useState<string | null>(null);
|
||||
const [messageText, setMessageText] = useState<string>("");
|
||||
const [successMsg, setSuccessMsg] = useState<string>("");
|
||||
const [errorMsg, setErrorMsg] = useState<string>("");
|
||||
|
||||
@@ -49,6 +51,19 @@ export function AdminBookings() {
|
||||
})
|
||||
);
|
||||
|
||||
const { mutate: sendMessage, isPending: isSendingMessage } = useMutation(
|
||||
queryClient.bookings.sendCustomerMessage.mutationOptions({
|
||||
onSuccess: () => {
|
||||
setSuccessMsg("Nachricht wurde erfolgreich gesendet.");
|
||||
setShowMessageModal(null);
|
||||
setMessageText("");
|
||||
},
|
||||
onError: (error: any) => {
|
||||
setErrorMsg(error?.message || "Fehler beim Senden der Nachricht.");
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const getTreatmentName = (treatmentId: string) => {
|
||||
return treatments?.find(t => t.id === treatmentId)?.name || "Unbekannte Behandlung";
|
||||
};
|
||||
@@ -83,6 +98,35 @@ export function AdminBookings() {
|
||||
setSelectedPhoto("");
|
||||
};
|
||||
|
||||
const openMessageModal = (bookingId: string) => {
|
||||
setShowMessageModal(bookingId);
|
||||
setMessageText("");
|
||||
};
|
||||
|
||||
const closeMessageModal = () => {
|
||||
setShowMessageModal(null);
|
||||
setMessageText("");
|
||||
};
|
||||
|
||||
const handleSendMessage = () => {
|
||||
if (!showMessageModal || !messageText.trim()) {
|
||||
setErrorMsg("Bitte gib eine Nachricht ein.");
|
||||
return;
|
||||
}
|
||||
|
||||
sendMessage({
|
||||
sessionId: localStorage.getItem("sessionId") || "",
|
||||
bookingId: showMessageModal,
|
||||
message: messageText.trim(),
|
||||
});
|
||||
};
|
||||
|
||||
// Check if booking is in the future
|
||||
const isFutureBooking = (appointmentDate: string) => {
|
||||
const today = new Date().toISOString().split("T")[0];
|
||||
return appointmentDate >= today;
|
||||
};
|
||||
|
||||
const filteredBookings = bookings?.filter(booking =>
|
||||
selectedDate ? booking.appointmentDate === selectedDate : true
|
||||
).sort((a, b) => {
|
||||
@@ -251,45 +295,57 @@ export function AdminBookings() {
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div className="flex space-x-2">
|
||||
{booking.status === "pending" && (
|
||||
<>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex space-x-2">
|
||||
{booking.status === "pending" && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => updateBookingStatus({ sessionId: localStorage.getItem("sessionId") || "", id: booking.id, status: "confirmed" })}
|
||||
className="text-green-600 hover:text-green-900"
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCancelConfirm(booking.id)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{booking.status === "confirmed" && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => updateBookingStatus({ sessionId: localStorage.getItem("sessionId") || "", id: booking.id, status: "completed" })}
|
||||
className="text-blue-600 hover:text-blue-900"
|
||||
>
|
||||
Complete
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCancelConfirm(booking.id)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{(booking.status === "cancelled" || booking.status === "completed") && (
|
||||
<button
|
||||
onClick={() => updateBookingStatus({ sessionId: localStorage.getItem("sessionId") || "", id: booking.id, status: "confirmed" })}
|
||||
className="text-green-600 hover:text-green-900"
|
||||
>
|
||||
Confirm
|
||||
Reactivate
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCancelConfirm(booking.id)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{booking.status === "confirmed" && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => updateBookingStatus({ sessionId: localStorage.getItem("sessionId") || "", id: booking.id, status: "completed" })}
|
||||
className="text-blue-600 hover:text-blue-900"
|
||||
>
|
||||
Complete
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCancelConfirm(booking.id)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{(booking.status === "cancelled" || booking.status === "completed") && (
|
||||
)}
|
||||
</div>
|
||||
{/* Show message button for future bookings with email */}
|
||||
{isFutureBooking(booking.appointmentDate) && booking.customerEmail && (
|
||||
<button
|
||||
onClick={() => updateBookingStatus({ sessionId: localStorage.getItem("sessionId") || "", id: booking.id, status: "confirmed" })}
|
||||
className="text-green-600 hover:text-green-900"
|
||||
onClick={() => openMessageModal(booking.id)}
|
||||
className="text-pink-600 hover:text-pink-900 text-left"
|
||||
title="Nachricht an Kunden senden"
|
||||
>
|
||||
Reactivate
|
||||
💬 Nachricht
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -369,6 +425,87 @@ export function AdminBookings() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Message Modal */}
|
||||
{showMessageModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 max-w-2xl w-full mx-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">Nachricht an Kunden senden</h3>
|
||||
<button
|
||||
onClick={closeMessageModal}
|
||||
className="text-gray-400 hover:text-gray-600 text-2xl"
|
||||
disabled={isSendingMessage}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(() => {
|
||||
const booking = bookings?.find(b => b.id === showMessageModal);
|
||||
if (!booking) return null;
|
||||
|
||||
return (
|
||||
<div className="mb-4 bg-gray-50 p-4 rounded-md">
|
||||
<p className="text-sm text-gray-700">
|
||||
<strong>Kunde:</strong> {booking.customerName}
|
||||
</p>
|
||||
<p className="text-sm text-gray-700">
|
||||
<strong>E-Mail:</strong> {booking.customerEmail}
|
||||
</p>
|
||||
<p className="text-sm text-gray-700">
|
||||
<strong>Termin:</strong> {new Date(booking.appointmentDate).toLocaleDateString()} um {booking.appointmentTime}
|
||||
</p>
|
||||
<p className="text-sm text-gray-700">
|
||||
<strong>Behandlung:</strong> {getTreatmentName(booking.treatmentId)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Deine Nachricht
|
||||
</label>
|
||||
<textarea
|
||||
value={messageText}
|
||||
onChange={(e) => setMessageText(e.target.value)}
|
||||
placeholder="Schreibe hier deine Nachricht an den Kunden..."
|
||||
rows={6}
|
||||
maxLength={5000}
|
||||
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
|
||||
disabled={isSendingMessage}
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{messageText.length} / 5000 Zeichen
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border-l-4 border-blue-400 p-3 mb-4">
|
||||
<p className="text-sm text-blue-700">
|
||||
💡 <strong>Hinweis:</strong> Der Kunde kann direkt auf diese E-Mail antworten. Die Antwort geht an die in den Einstellungen hinterlegte Admin-E-Mail-Adresse.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
onClick={handleSendMessage}
|
||||
disabled={isSendingMessage || !messageText.trim()}
|
||||
className="flex-1 bg-pink-600 text-white py-2 px-4 rounded-md hover:bg-pink-700 transition-colors disabled:bg-gray-300 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isSendingMessage ? "Wird gesendet..." : "Nachricht senden"}
|
||||
</button>
|
||||
<button
|
||||
onClick={closeMessageModal}
|
||||
disabled={isSendingMessage}
|
||||
className="flex-1 bg-gray-200 text-gray-800 py-2 px-4 rounded-md hover:bg-gray-300 transition-colors disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user