mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-06-03 20:13:00 +00:00
355 lines
9.0 KiB
Go
355 lines
9.0 KiB
Go
package notifications
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"bookra/apps/backend/internal/db"
|
|
)
|
|
|
|
type EmailType string
|
|
|
|
const (
|
|
EmailTypeConfirmation EmailType = "confirmation"
|
|
EmailTypeReminder EmailType = "reminder"
|
|
EmailTypeReschedule EmailType = "reschedule"
|
|
EmailTypeCancellation EmailType = "cancellation"
|
|
EmailTypeBusinessNotify EmailType = "business_notify"
|
|
)
|
|
|
|
type BookingEmailData struct {
|
|
Type EmailType
|
|
TenantName string
|
|
TenantSlug string
|
|
BusinessEmail string
|
|
BusinessPhone string
|
|
BusinessAddress string
|
|
BrandColor string
|
|
CustomerName string
|
|
CustomerEmail string
|
|
Service string
|
|
Location string
|
|
Reference string
|
|
StartsAt time.Time
|
|
EndsAt time.Time
|
|
Timezone string
|
|
Locale string
|
|
Notes string
|
|
ManagementURL string
|
|
AddToCalendarURL string
|
|
}
|
|
|
|
func RenderEmailMessage(data BookingEmailData) EmailMessage {
|
|
subject := renderSubject(data)
|
|
htmlBody := renderHTMLBody(data)
|
|
textBody := renderTextBody(data)
|
|
|
|
return EmailMessage{
|
|
From: data.BusinessEmail,
|
|
To: data.CustomerEmail,
|
|
Subject: subject,
|
|
Text: textBody,
|
|
HTML: htmlBody,
|
|
}
|
|
}
|
|
|
|
func renderSubject(data BookingEmailData) string {
|
|
localizedTime := formatLocalizedTime(data.StartsAt, data.Timezone, data.Locale)
|
|
|
|
switch data.Type {
|
|
case EmailTypeConfirmation:
|
|
if data.Locale == "cs" {
|
|
return fmt.Sprintf("Potvrzení rezervace %s - %s", data.Reference, data.TenantName)
|
|
}
|
|
return fmt.Sprintf("Booking Confirmation %s - %s", data.Reference, data.TenantName)
|
|
case EmailTypeReminder:
|
|
if data.Locale == "cs" {
|
|
return fmt.Sprintf("Připomínka: Máte rezervaci zítra v %s", localizedTime)
|
|
}
|
|
return fmt.Sprintf("Reminder: You have a booking tomorrow at %s", localizedTime)
|
|
case EmailTypeReschedule:
|
|
if data.Locale == "cs" {
|
|
return fmt.Sprintf("Vaše rezervace byla přesunuta - %s", data.Reference)
|
|
}
|
|
return fmt.Sprintf("Your booking has been rescheduled - %s", data.Reference)
|
|
case EmailTypeCancellation:
|
|
if data.Locale == "cs" {
|
|
return fmt.Sprintf("Vaše rezervace byla zrušena - %s", data.Reference)
|
|
}
|
|
return fmt.Sprintf("Your booking has been cancelled - %s", data.Reference)
|
|
case EmailTypeBusinessNotify:
|
|
if data.Locale == "cs" {
|
|
return fmt.Sprintf("Nová rezervace od %s - %s", data.CustomerName, data.Reference)
|
|
}
|
|
return fmt.Sprintf("New booking from %s - %s", data.CustomerName, data.Reference)
|
|
default:
|
|
return "Booking Update"
|
|
}
|
|
}
|
|
|
|
func renderTextBody(data BookingEmailData) string {
|
|
localizedTime := formatLocalizedDateTime(data.StartsAt, data.Timezone, data.Locale)
|
|
|
|
switch data.Type {
|
|
case EmailTypeConfirmation:
|
|
if data.Locale == "cs" {
|
|
return fmt.Sprintf(`Dobrý den %s,
|
|
|
|
vaše rezervace byla potvrzena.
|
|
|
|
Detaily rezervace:
|
|
- Služba: %s
|
|
- Datum a čas: %s
|
|
- Místo: %s
|
|
- Reference: %s
|
|
|
|
Pro správu rezervace navštivte: %s
|
|
|
|
Děkujeme,
|
|
%s
|
|
%s`, data.CustomerName, data.Service, localizedTime, data.Location, data.Reference, data.ManagementURL, data.TenantName, data.BusinessEmail)
|
|
}
|
|
return fmt.Sprintf(`Hello %s,
|
|
|
|
Your booking has been confirmed.
|
|
|
|
Booking Details:
|
|
- Service: %s
|
|
- Date & Time: %s
|
|
- Location: %s
|
|
- Reference: %s
|
|
|
|
Manage your booking at: %s
|
|
|
|
Thank you,
|
|
%s
|
|
%s`, data.CustomerName, data.Service, localizedTime, data.Location, data.Reference, data.ManagementURL, data.TenantName, data.BusinessEmail)
|
|
|
|
case EmailTypeReminder:
|
|
if data.Locale == "cs" {
|
|
return fmt.Sprintf(`Dobrý den %s,
|
|
|
|
připomínáme vám zítřejší rezervaci.
|
|
|
|
- Služba: %s
|
|
- Čas: %s
|
|
- Místo: %s
|
|
- Reference: %s
|
|
|
|
Pro správu rezervace: %s
|
|
|
|
%s`, data.CustomerName, data.Service, localizedTime, data.Location, data.Reference, data.ManagementURL, data.TenantName)
|
|
}
|
|
return fmt.Sprintf(`Hello %s,
|
|
|
|
This is a reminder for your booking tomorrow.
|
|
|
|
- Service: %s
|
|
- Time: %s
|
|
- Location: %s
|
|
- Reference: %s
|
|
|
|
Manage booking: %s
|
|
|
|
%s`, data.CustomerName, data.Service, localizedTime, data.Location, data.Reference, data.ManagementURL, data.TenantName)
|
|
|
|
case EmailTypeReschedule:
|
|
if data.Locale == "cs" {
|
|
return fmt.Sprintf(`Dobrý den %s,
|
|
|
|
vaše rezervace byla přesunuta na nový termín.
|
|
|
|
Nové detaily:
|
|
- Služba: %s
|
|
- Datum a čas: %s
|
|
- Místo: %s
|
|
- Reference: %s
|
|
|
|
Pro správu rezervace: %s
|
|
|
|
%s`, data.CustomerName, data.Service, localizedTime, data.Location, data.Reference, data.ManagementURL, data.TenantName)
|
|
}
|
|
return fmt.Sprintf(`Hello %s,
|
|
|
|
Your booking has been rescheduled.
|
|
|
|
New details:
|
|
- Service: %s
|
|
- Date & Time: %s
|
|
- Location: %s
|
|
- Reference: %s
|
|
|
|
Manage booking: %s
|
|
|
|
%s`, data.CustomerName, data.Service, localizedTime, data.Location, data.Reference, data.ManagementURL, data.TenantName)
|
|
|
|
case EmailTypeCancellation:
|
|
if data.Locale == "cs" {
|
|
return fmt.Sprintf(`Dobrý den %s,
|
|
|
|
vaše rezervace byla zrušena.
|
|
|
|
Zrušená rezervace:
|
|
- Služba: %s
|
|
- Datum a čas: %s
|
|
- Reference: %s
|
|
|
|
Pokud jste rezervaci nezrušili vy, kontaktujte nás: %s
|
|
|
|
%s`, data.CustomerName, data.Service, localizedTime, data.Reference, data.BusinessEmail, data.TenantName)
|
|
}
|
|
return fmt.Sprintf(`Hello %s,
|
|
|
|
Your booking has been cancelled.
|
|
|
|
Cancelled booking:
|
|
- Service: %s
|
|
- Date & Time: %s
|
|
- Reference: %s
|
|
|
|
If you didn't cancel this, please contact us: %s
|
|
|
|
%s`, data.CustomerName, data.Service, localizedTime, data.Reference, data.BusinessEmail, data.TenantName)
|
|
|
|
case EmailTypeBusinessNotify:
|
|
if data.Locale == "cs" {
|
|
return fmt.Sprintf(`Nová rezervace od %s
|
|
|
|
Detaily:
|
|
- Služba: %s
|
|
- Datum a čas: %s
|
|
- Reference: %s
|
|
- Email: %s
|
|
|
|
Spravovat v administraci: https://bookra.eu/dashboard`, data.CustomerName, data.Service, localizedTime, data.Reference, data.CustomerEmail)
|
|
}
|
|
return fmt.Sprintf(`New booking from %s
|
|
|
|
Details:
|
|
- Service: %s
|
|
- Date & Time: %s
|
|
- Reference: %s
|
|
- Email: %s
|
|
|
|
Manage in dashboard: https://bookra.eu/dashboard`, data.CustomerName, data.Service, localizedTime, data.Reference, data.CustomerEmail)
|
|
|
|
default:
|
|
return "Booking update"
|
|
}
|
|
}
|
|
|
|
func renderHTMLBody(data BookingEmailData) string {
|
|
// For now, return simple HTML version. In production, this would use proper HTML templates
|
|
textBody := renderTextBody(data)
|
|
// Simple conversion: wrap paragraphs in <p> tags and preserve line breaks
|
|
html := "<html><body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>"
|
|
html += "<div style='max-width: 600px; margin: 0 auto; padding: 20px;'>"
|
|
html += fmt.Sprintf("<h2 style='color: %s; margin-bottom: 20px;'>%s</h2>", data.BrandColor, data.TenantName)
|
|
|
|
// Convert text to simple HTML
|
|
paragraphs := splitParagraphs(textBody)
|
|
for _, p := range paragraphs {
|
|
if len(p) > 0 {
|
|
html += fmt.Sprintf("<p style='margin-bottom: 10px;'>%s</p>", p)
|
|
}
|
|
}
|
|
|
|
// Add management button
|
|
if data.ManagementURL != "" {
|
|
html += fmt.Sprintf("<div style='margin-top: 30px;'><a href='%s' style='display: inline-block; background: %s; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px;'>Manage Booking</a></div>", data.ManagementURL, data.BrandColor)
|
|
}
|
|
|
|
html += "</div></body></html>"
|
|
return html
|
|
}
|
|
|
|
func formatLocalizedTime(t time.Time, timezone, locale string) string {
|
|
loc, err := time.LoadLocation(timezone)
|
|
if err != nil {
|
|
loc = time.UTC
|
|
}
|
|
localTime := t.In(loc)
|
|
if locale == "cs" {
|
|
return localTime.Format("15:04")
|
|
}
|
|
return localTime.Format("3:04 PM")
|
|
}
|
|
|
|
func formatLocalizedDateTime(t time.Time, timezone, locale string) string {
|
|
loc, err := time.LoadLocation(timezone)
|
|
if err != nil {
|
|
loc = time.UTC
|
|
}
|
|
localTime := t.In(loc)
|
|
if locale == "cs" {
|
|
return localTime.Format("02.01.2006 15:04")
|
|
}
|
|
return localTime.Format("Jan 02, 2006 3:04 PM")
|
|
}
|
|
|
|
func splitParagraphs(text string) []string {
|
|
var paragraphs []string
|
|
current := ""
|
|
for _, line := range splitLines(text) {
|
|
trimmed := trimSpace(line)
|
|
if trimmed == "" {
|
|
if current != "" {
|
|
paragraphs = append(paragraphs, current)
|
|
current = ""
|
|
}
|
|
} else {
|
|
if current != "" {
|
|
current += " "
|
|
}
|
|
current += trimmed
|
|
}
|
|
}
|
|
if current != "" {
|
|
paragraphs = append(paragraphs, current)
|
|
}
|
|
return paragraphs
|
|
}
|
|
|
|
func splitLines(s string) []string {
|
|
var lines []string
|
|
start := 0
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] == '\n' {
|
|
lines = append(lines, s[start:i])
|
|
start = i + 1
|
|
}
|
|
}
|
|
lines = append(lines, s[start:])
|
|
return lines
|
|
}
|
|
|
|
func trimSpace(s string) string {
|
|
start := 0
|
|
end := len(s)
|
|
for start < end && (s[start] == ' ' || s[start] == '\t' || s[start] == '\r' || s[start] == '\n') {
|
|
start++
|
|
}
|
|
for end > start && (s[end-1] == ' ' || s[end-1] == '\t' || s[end-1] == '\r' || s[end-1] == '\n') {
|
|
end--
|
|
}
|
|
return s[start:end]
|
|
}
|
|
|
|
// RenderReminderEmail renders the legacy reminder email from a job record
|
|
func RenderReminderEmail(from string, job db.ReminderJobRecord) EmailMessage {
|
|
data := BookingEmailData{
|
|
Type: EmailTypeReminder,
|
|
TenantName: job.TenantName,
|
|
TenantSlug: "", // Not available in job record
|
|
CustomerName: job.CustomerName,
|
|
CustomerEmail: job.CustomerEmail,
|
|
Reference: job.Reference,
|
|
StartsAt: job.StartsAt,
|
|
Timezone: job.Timezone,
|
|
Locale: job.Locale,
|
|
Service: "Service", // Legacy
|
|
Location: "Location", // Legacy
|
|
}
|
|
return RenderEmailMessage(data)
|
|
}
|