Add optional cancellation message for notifications

This commit is contained in:
2026-01-12 21:42:52 +01:00
parent 62e1f5f1b4
commit cc2687dad8
5 changed files with 67 additions and 7 deletions

View File

@@ -51,6 +51,9 @@ export function PlanDashboard({ plan, dict, settingsDict, lang }: PlanDashboardP
const [sitterName, setSitterName] = useState("")
const [bookingType, setBookingType] = useState<"SITTER" | "OWNER_HOME">("SITTER")
const [isDialogOpen, setIsDialogOpen] = useState(false)
const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false)
const [bookingToCancel, setBookingToCancel] = useState<number | null>(null)
const [cancelReason, setCancelReason] = useState("")
const [isSubmitting, setIsSubmitting] = useState(false)
const dateLocale = lang === "de" ? de : enUS
@@ -92,14 +95,25 @@ export function PlanDashboard({ plan, dict, settingsDict, lang }: PlanDashboardP
}
}
const handleCancel = async (bookingId: number) => {
if (!confirm(dict.cancelConfirm)) return
const handleCancelClick = (bookingId: number) => {
setBookingToCancel(bookingId)
setCancelReason("")
setIsCancelDialogOpen(true)
}
const handleConfirmCancel = async () => {
if (!bookingToCancel) return
setIsSubmitting(true)
try {
await deleteBooking(bookingId, plan.id, lang)
await deleteBooking(bookingToCancel, plan.id, lang, cancelReason)
toast.success(dict.cancelSuccess)
setIsCancelDialogOpen(false)
setBookingToCancel(null)
} catch {
toast.error(dict.cancelError)
} finally {
setIsSubmitting(false)
}
}
@@ -168,7 +182,7 @@ export function PlanDashboard({ plan, dict, settingsDict, lang }: PlanDashboardP
variant="ghost"
size="icon"
className="h-6 w-6 -mr-2 -mt-2 opacity-50 hover:opacity-100 text-destructive"
onClick={() => handleCancel(booking.id)}
onClick={() => handleCancelClick(booking.id)}
>
<X className="w-4 h-4" />
<span className="sr-only">Remove</span>
@@ -245,6 +259,40 @@ export function PlanDashboard({ plan, dict, settingsDict, lang }: PlanDashboardP
)
})}
</div>
{/* Cancellation Dialog */}
<Dialog open={isCancelDialogOpen} onOpenChange={setIsCancelDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>{dict.cancelTitle}</DialogTitle>
<DialogDescription>
{dict.cancelConfirm}
</DialogDescription>
</DialogHeader>
{plan.webhookUrl && (
<div className="grid gap-2 py-4">
<Label htmlFor="reason">{dict.cancelMessageLabel}</Label>
<Input
id="reason"
value={cancelReason}
onChange={(e) => setCancelReason(e.target.value)}
placeholder={dict.cancelMessagePlaceholder}
autoFocus
/>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setIsCancelDialogOpen(false)} disabled={isSubmitting}>
{dict.cancel}
</Button>
<Button variant="destructive" onClick={handleConfirmCancel} disabled={isSubmitting}>
{isSubmitting ? dict.saving : dict.cancelSubmit}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -50,7 +50,7 @@ export async function createBooking(planId: string, date: Date, name: string, ty
revalidatePath(`/${lang}/dashboard/${planId}`)
}
export async function deleteBooking(bookingId: number, planId: string, lang: string = "en") {
export async function deleteBooking(bookingId: number, planId: string, lang: string = "en", reason?: string) {
const dict = await getDictionary(lang as any)
const booking = await prisma.booking.findUnique({
@@ -70,10 +70,12 @@ export async function deleteBooking(bookingId: number, planId: string, lang: str
const planUrl = `${protocol}://${host}/${lang}/dashboard/${planId}`
const dateStr = booking.date.toLocaleDateString(lang)
const messageDisplay = reason ? `\nMessage: ${reason}` : ""
const message = dict.notifications.cancellation
.replace("{name}", booking.sitterName || "Someone")
.replace("{date}", dateStr)
.replace("{url}", planUrl)
.replace("{message}", messageDisplay)
await sendNotification(booking.plan.webhookUrl, message)
}

View File

@@ -36,6 +36,11 @@
"bookedSuccess": "Termin gebucht!",
"bookError": "Buchung fehlgeschlagen. Vielleicht war jemand schneller?",
"cancelConfirm": "Bist du sicher, dass du diesen Eintrag entfernen möchtest?",
"cancelTitle": "Termin stornieren",
"cancelMessageLabel": "Nachricht (optional)",
"cancelMessagePlaceholder": "Ich kann leider doch nicht...",
"cancelSubmit": "Stornierung bestätigen",
"cancel": "Abbrechen",
"cancelSuccess": "Eintrag entfernt",
"cancelError": "Entfernen fehlgeschlagen",
"noInstructions": "Noch keine spezifischen Instruktionen hinterlegt."
@@ -64,7 +69,7 @@
"notifications": {
"ownerHome": "🏠 BESITZER ZU HAUSE: Markiert für den {date}.\nPlan: {url}",
"newBooking": "✅ NEUE BUCHUNG: {name} sittet am {date}.\nPlan: {url}",
"cancellation": "🚨 ABSAGE: {name} hat die Buchung für den {date} gelöscht.\nPlan: {url}",
"cancellation": "🚨 ABSAGE: {name} hat die Buchung für den {date} gelöscht.{message}\nPlan: {url}",
"instructionsUpdated": "📝 UPDATE: Die Katzen-Instruktionen wurden geändert.\nPlan: {url}"
}
}

View File

@@ -36,6 +36,11 @@
"bookedSuccess": "Spot booked!",
"bookError": "Failed to book spot. Maybe it was just taken?",
"cancelConfirm": "Are you sure you want to remove this entry?",
"cancelTitle": "Cancel Booking",
"cancelMessageLabel": "Message (optional)",
"cancelMessagePlaceholder": "I can't make it because...",
"cancelSubmit": "Confirm Cancellation",
"cancel": "Cancel",
"cancelSuccess": "Entry removed",
"cancelError": "Failed to remove entry",
"noInstructions": "No specific instructions provided yet."
@@ -64,7 +69,7 @@
"notifications": {
"ownerHome": "🏠 OWNER HOME: Marked for {date}.\nPlan: {url}",
"newBooking": "✅ NEW BOOKING: {name} is sitting on {date}.\nPlan: {url}",
"cancellation": "🚨 CANCELLATION: {name} removed their booking for {date}.\nPlan: {url}",
"cancellation": "🚨 CANCELLATION: {name} removed their booking for {date}.{message}\nPlan: {url}",
"instructionsUpdated": "📝 UPDATED: Cat instructions have been modified.\nPlan: {url}"
}
}

Binary file not shown.