package handlers import ( "context" "database/sql" "encoding/json" "fmt" "io" "log" "net/http" "os" "strconv" "strings" "time" "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/auth" "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db" "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/templates" ) // getUsername is a wrapper around auth.GetUsername for backwards compatibility func getUsername(r *http.Request) string { return auth.GetUsername(r) } // Helper: returns date string or empty if zero func formatDate(t time.Time) string { if t.IsZero() || t.Year() == 1970 { return "" } return t.UTC().Format(time.RFC3339) } // Helper: checks if a time is a valid DB value (not zero or 1970-01-01) func isValidDBTime(t time.Time) bool { return !t.IsZero() && t.After(time.Date(1971, 1, 1, 0, 0, 0, 0, time.UTC)) } // calcExpiryInfo is a helper to calculate expiry info for a quote func calcExpiryInfo(validUntil time.Time) (string, int, int) { now := time.Now() nowDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) validUntilDate := time.Date(validUntil.Year(), validUntil.Month(), validUntil.Day(), 0, 0, 0, 0, validUntil.Location()) daysUntil := int(validUntilDate.Sub(nowDate).Hours() / 24) daysSince := int(nowDate.Sub(validUntilDate).Hours() / 24) var relative string if validUntilDate.After(nowDate) || validUntilDate.Equal(nowDate) { switch daysUntil { case 0: relative = "expires today" case 1: relative = "expires tomorrow" default: relative = "expires in " + strconv.Itoa(daysUntil) + " days" } } else { switch daysSince { case 0: relative = "expired today" case 1: relative = "expired yesterday" default: relative = "expired " + strconv.Itoa(daysSince) + " days ago" } } return relative, daysUntil, daysSince } // QuoteRow interface for all quote row types // (We use wrapper types since sqlc structs can't be modified directly) type QuoteRow interface { GetID() int32 GetUsername() string GetUserEmail() string GetEnquiryID() int32 GetEnquiryRef() string GetDateIssued() time.Time GetValidUntil() time.Time GetReminderType() int32 GetReminderSent() time.Time GetCustomerName() string GetCustomerEmail() string } // Wrapper types for each DB row struct type ExpiringSoonQuoteRowWrapper struct{ db.GetExpiringSoonQuotesRow } func (q ExpiringSoonQuoteRowWrapper) GetID() int32 { return q.DocumentID } func (q ExpiringSoonQuoteRowWrapper) GetUsername() string { if q.Username.Valid { return q.Username.String } return "Unknown" } func (q ExpiringSoonQuoteRowWrapper) GetUserEmail() string { if q.UserEmail.Valid { return q.UserEmail.String } return "" } func (q ExpiringSoonQuoteRowWrapper) GetEnquiryID() int32 { return q.EnquiryID } func (q ExpiringSoonQuoteRowWrapper) GetEnquiryRef() string { return q.EnquiryRef } func (q ExpiringSoonQuoteRowWrapper) GetDateIssued() time.Time { return q.DateIssued } func (q ExpiringSoonQuoteRowWrapper) GetValidUntil() time.Time { return q.ValidUntil } func (q ExpiringSoonQuoteRowWrapper) GetReminderType() int32 { return q.LatestReminderType } func (q ExpiringSoonQuoteRowWrapper) GetReminderSent() time.Time { return q.LatestReminderSentTime } func (q ExpiringSoonQuoteRowWrapper) GetCustomerName() string { return q.CustomerName } func (q ExpiringSoonQuoteRowWrapper) GetCustomerEmail() string { return q.CustomerEmail } type RecentlyExpiredQuoteRowWrapper struct{ db.GetRecentlyExpiredQuotesRow } func (q RecentlyExpiredQuoteRowWrapper) GetID() int32 { return q.DocumentID } func (q RecentlyExpiredQuoteRowWrapper) GetUsername() string { if q.Username.Valid { return q.Username.String } return "Unknown" } func (q RecentlyExpiredQuoteRowWrapper) GetUserEmail() string { if q.UserEmail.Valid { return q.UserEmail.String } return "" } func (q RecentlyExpiredQuoteRowWrapper) GetEnquiryID() int32 { return q.EnquiryID } func (q RecentlyExpiredQuoteRowWrapper) GetEnquiryRef() string { return q.EnquiryRef } func (q RecentlyExpiredQuoteRowWrapper) GetDateIssued() time.Time { return q.DateIssued } func (q RecentlyExpiredQuoteRowWrapper) GetValidUntil() time.Time { return q.ValidUntil } func (q RecentlyExpiredQuoteRowWrapper) GetReminderType() int32 { return q.LatestReminderType } func (q RecentlyExpiredQuoteRowWrapper) GetReminderSent() time.Time { return q.LatestReminderSentTime } func (q RecentlyExpiredQuoteRowWrapper) GetCustomerName() string { return q.CustomerName } func (q RecentlyExpiredQuoteRowWrapper) GetCustomerEmail() string { return q.CustomerEmail } type ExpiringSoonQuoteOnDayRowWrapper struct { db.GetExpiringSoonQuotesOnDayRow } func (q ExpiringSoonQuoteOnDayRowWrapper) GetID() int32 { return q.DocumentID } func (q ExpiringSoonQuoteOnDayRowWrapper) GetUsername() string { if q.Username.Valid { return q.Username.String } return "Unknown" } func (q ExpiringSoonQuoteOnDayRowWrapper) GetUserEmail() string { if q.UserEmail.Valid { return q.UserEmail.String } return "" } func (q ExpiringSoonQuoteOnDayRowWrapper) GetEnquiryID() int32 { return q.EnquiryID } func (q ExpiringSoonQuoteOnDayRowWrapper) GetEnquiryRef() string { return q.EnquiryRef } func (q ExpiringSoonQuoteOnDayRowWrapper) GetDateIssued() time.Time { return q.DateIssued } func (q ExpiringSoonQuoteOnDayRowWrapper) GetValidUntil() time.Time { return q.ValidUntil } func (q ExpiringSoonQuoteOnDayRowWrapper) GetReminderType() int32 { return q.LatestReminderType } func (q ExpiringSoonQuoteOnDayRowWrapper) GetReminderSent() time.Time { return q.LatestReminderSentTime } func (q ExpiringSoonQuoteOnDayRowWrapper) GetCustomerName() string { return q.CustomerName } func (q ExpiringSoonQuoteOnDayRowWrapper) GetCustomerEmail() string { return q.CustomerEmail } type RecentlyExpiredQuoteOnDayRowWrapper struct { db.GetRecentlyExpiredQuotesOnDayRow } func (q RecentlyExpiredQuoteOnDayRowWrapper) GetID() int32 { return q.DocumentID } func (q RecentlyExpiredQuoteOnDayRowWrapper) GetUsername() string { if q.Username.Valid { return q.Username.String } return "Unknown" } func (q RecentlyExpiredQuoteOnDayRowWrapper) GetUserEmail() string { if q.UserEmail.Valid { return q.UserEmail.String } return "" } func (q RecentlyExpiredQuoteOnDayRowWrapper) GetEnquiryID() int32 { return q.EnquiryID } func (q RecentlyExpiredQuoteOnDayRowWrapper) GetEnquiryRef() string { return q.EnquiryRef } func (q RecentlyExpiredQuoteOnDayRowWrapper) GetDateIssued() time.Time { return q.DateIssued } func (q RecentlyExpiredQuoteOnDayRowWrapper) GetValidUntil() time.Time { return q.ValidUntil } func (q RecentlyExpiredQuoteOnDayRowWrapper) GetReminderType() int32 { return q.LatestReminderType } func (q RecentlyExpiredQuoteOnDayRowWrapper) GetReminderSent() time.Time { return q.LatestReminderSentTime } func (q RecentlyExpiredQuoteOnDayRowWrapper) GetCustomerName() string { return q.CustomerName } func (q RecentlyExpiredQuoteOnDayRowWrapper) GetCustomerEmail() string { return q.CustomerEmail } // Helper: formats a quote row for output (generic) func formatQuoteRow(q QuoteRow) map[string]interface{} { relative, daysUntil, daysSince := calcExpiryInfo(q.GetValidUntil()) return map[string]interface{}{ "ID": q.GetID(), "Username": strings.Title(q.GetUsername()), "UserEmail": q.GetUserEmail(), "EnquiryID": q.GetEnquiryID(), "EnquiryRef": q.GetEnquiryRef(), "CustomerName": strings.TrimSpace(q.GetCustomerName()), "CustomerEmail": q.GetCustomerEmail(), "DateIssued": formatDate(q.GetDateIssued()), "ValidUntil": formatDate(q.GetValidUntil()), "ValidUntilRelative": relative, "DaysUntilExpiry": daysUntil, "DaysSinceExpiry": daysSince, "LatestReminderSent": formatDate(q.GetReminderSent()), "LatestReminderType": reminderTypeString(int(q.GetReminderType())), } } type QuoteQueries interface { GetQuoteRemindersByType(ctx context.Context, params db.GetQuoteRemindersByTypeParams) ([]db.QuoteReminder, error) InsertQuoteReminder(ctx context.Context, params db.InsertQuoteReminderParams) (sql.Result, error) GetExpiringSoonQuotes(ctx context.Context, dateADD interface{}) ([]db.GetExpiringSoonQuotesRow, error) GetRecentlyExpiredQuotes(ctx context.Context, dateSUB interface{}) ([]db.GetRecentlyExpiredQuotesRow, error) GetExpiringSoonQuotesOnDay(ctx context.Context, arg db.GetExpiringSoonQuotesOnDayParams) ([]db.GetExpiringSoonQuotesOnDayRow, error) GetRecentlyExpiredQuotesOnDay(ctx context.Context, dateSUB interface{}) ([]db.GetRecentlyExpiredQuotesOnDayRow, error) DisableQuoteReminders(ctx context.Context, params db.DisableQuoteRemindersParams) (sql.Result, error) EnableQuoteReminders(ctx context.Context, id int32) (sql.Result, error) } type EmailSender interface { SendTemplateEmail(to string, subject string, templateName string, data interface{}, ccs []string, bccs []string) error SendTemplateEmailWithAttachments(to string, subject string, templateName string, data interface{}, ccs []string, bccs []string, attachments []interface{}) error } type QuotesHandler struct { queries QuoteQueries tmpl *templates.TemplateManager emailService EmailSender } func NewQuotesHandler(queries QuoteQueries, tmpl *templates.TemplateManager, emailService EmailSender) *QuotesHandler { return &QuotesHandler{ queries: queries, tmpl: tmpl, emailService: emailService, } } func (h *QuotesHandler) QuotesOutstandingView(w http.ResponseWriter, r *http.Request) { // Days to look ahead and behind for expiring quotes days := 7 // Show all quotes that are expiring in the next 7 days expiringSoonQuotes, err := h.GetOutstandingQuotes(r, days) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Show all quotes that have expired in the last 60 days recentlyExpiredQuotes, err := h.GetOutstandingQuotes(r, -60) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } data := map[string]interface{}{ "RecentlyExpiredQuotes": recentlyExpiredQuotes, "ExpiringSoonQuotes": expiringSoonQuotes, "User": getUsername(r), } if err := h.tmpl.Render(w, "quotes/index.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // GetOutstandingQuotes returns outstanding quotes based on daysUntilExpiry. func (h *QuotesHandler) GetOutstandingQuotes(r *http.Request, daysUntilExpiry int) ([]map[string]interface{}, error) { var rows []map[string]interface{} ctx := r.Context() // If daysUntilExpiry is positive, get quotes expiring soon; if negative, get recently expired quotes if daysUntilExpiry >= 0 { quotes, err := h.queries.GetExpiringSoonQuotes(ctx, daysUntilExpiry) if err != nil { return nil, err } for _, q := range quotes { rows = append(rows, formatQuoteRow(ExpiringSoonQuoteRowWrapper{q})) } } else { days := -daysUntilExpiry quotes, err := h.queries.GetRecentlyExpiredQuotes(ctx, days) if err != nil { return nil, err } for _, q := range quotes { rows = append(rows, formatQuoteRow(RecentlyExpiredQuoteRowWrapper{q})) } } return rows, nil } // GetOutstandingQuotesOnDay returns quotes expiring exactly N days from today (if day >= 0), or exactly N days ago (if day < 0). func (h *QuotesHandler) GetOutstandingQuotesOnDay(r *http.Request, day int) ([]map[string]interface{}, error) { var rows []map[string]interface{} ctx := r.Context() // If day is positive, get quotes expiring on that day; if negative, get recently expired quotes on that day in the past if day >= 0 { quotes, err := h.queries.GetExpiringSoonQuotesOnDay(ctx, db.GetExpiringSoonQuotesOnDayParams{ DATEADD: day, DATEADD_2: day, }) if err != nil { return nil, err } for _, q := range quotes { rows = append(rows, formatQuoteRow(ExpiringSoonQuoteOnDayRowWrapper{q})) } } else { days := -day quotes, err := h.queries.GetRecentlyExpiredQuotesOnDay(ctx, days) if err != nil { return nil, err } for _, q := range quotes { rows = append(rows, formatQuoteRow(RecentlyExpiredQuoteOnDayRowWrapper{q})) } } return rows, nil } type QuoteReminderType int const ( FirstReminder QuoteReminderType = 1 SecondReminder QuoteReminderType = 2 ThirdReminder QuoteReminderType = 3 ) func (t QuoteReminderType) String() string { switch t { case FirstReminder: return "FirstReminder" case SecondReminder: return "SecondReminder" case ThirdReminder: return "ThirdReminder" default: return "UnknownReminder" } } type quoteReminderJob struct { DayOffset int ReminderType QuoteReminderType } // getReminderDetails returns the subject and template for a given reminder type func getReminderDetails(reminderType QuoteReminderType) (subject, template string) { switch reminderType { case FirstReminder: return "Reminder: Quote %s Expires Soon", "first_reminder.html" case SecondReminder: return "Follow-Up: Quote %s Expired", "second_reminder.html" case ThirdReminder: return "Final Reminder: Quote %s Closed", "final_reminder.html" default: return "", "" } } // getReminderDetailsManual returns the subject and template for a manually sent reminder (generic templates without time references) func getReminderDetailsManual(reminderType QuoteReminderType) (subject, template string) { switch reminderType { case FirstReminder: return "Reminder: Quote %s Expires Soon", "manual_first_reminder.html" case SecondReminder: return "Follow-Up: Quote %s Expired", "manual_second_reminder.html" case ThirdReminder: return "Final Reminder: Quote %s Closed", "manual_final_reminder.html" default: return "", "" } } // formatQuoteDates formats ISO date strings to DD/MM/YYYY format func formatQuoteDates(dateIssuedStr, validUntilStr string) (submissionDate, expiryDate string) { if dateIssuedStr != "" { if t, err := time.Parse(time.RFC3339, dateIssuedStr); err == nil { submissionDate = t.Format("02/01/2006") } else { submissionDate = dateIssuedStr } } if validUntilStr != "" { if t, err := time.Parse(time.RFC3339, validUntilStr); err == nil { expiryDate = t.Format("02/01/2006") } else { expiryDate = validUntilStr } } return } // prepareReminderEmail prepares all data needed to send a reminder email func prepareReminderEmail(reminderType QuoteReminderType, customerName, dateIssuedStr, validUntilStr, enquiryRef, userEmail string) (subject, template string, templateData map[string]interface{}, ccs []string) { subject, template = getReminderDetails(reminderType) subject = fmt.Sprintf(subject, enquiryRef) submissionDate, expiryDate := formatQuoteDates(dateIssuedStr, validUntilStr) templateData = map[string]interface{}{ "CustomerName": customerName, "SubmissionDate": submissionDate, "ExpiryDate": expiryDate, "QuoteRef": enquiryRef, } ccs = []string{"sales@cmctechnologies.com.au"} if userEmail != "" { ccs = append(ccs, userEmail) } return } // DailyQuoteExpirationCheck checks quotes for reminders and expiry notices (callable as a cron job from main) func (h *QuotesHandler) DailyQuoteExpirationCheck() { fmt.Println("Running DailyQuoteExpirationCheck...") reminderJobs := []quoteReminderJob{ {7, FirstReminder}, {-7, SecondReminder}, {-60, ThirdReminder}, } for _, job := range reminderJobs { quotes, err := h.GetOutstandingQuotesOnDay((&http.Request{}), job.DayOffset) if err != nil { fmt.Printf("Error getting quotes for day offset %d: %v\n", job.DayOffset, err) continue } if len(quotes) == 0 { continue } for _, q := range quotes { subject, template, templateData, ccs := prepareReminderEmail( job.ReminderType, q["CustomerName"].(string), q["DateIssued"].(string), q["ValidUntil"].(string), q["EnquiryRef"].(string), q["UserEmail"].(string), ) err := h.SendQuoteReminderEmail( context.Background(), q["ID"].(int32), job.ReminderType, q["CustomerEmail"].(string), subject, template, templateData, ccs, nil, false, ) if err != nil { fmt.Printf("Error sending %s for quote %v: %v\n", job.ReminderType.String(), q["ID"], err) } else { fmt.Printf("%s sent and recorded for quote %v\n", job.ReminderType.String(), q["ID"]) } } } } // SendQuoteReminderEmail checks if a reminder of the given type has already been sent for the quote, sends the email if not, and records it. func (h *QuotesHandler) SendQuoteReminderEmail(ctx context.Context, quoteID int32, reminderType QuoteReminderType, recipient string, subject string, templateName string, templateData map[string]interface{}, ccs []string, username *string, allowDuplicate bool) error { // Safeguard: check for valid recipient if strings.TrimSpace(recipient) == "" { return fmt.Errorf("recipient email is required") } // Safeguard: check for valid template data if templateData == nil { return fmt.Errorf("template data is required") } // Safeguard: check for valid reminder type if reminderType != FirstReminder && reminderType != SecondReminder && reminderType != ThirdReminder { return fmt.Errorf("invalid reminder type: %v", reminderType) } // Check if reminder already sent (only if duplicates not allowed) if !allowDuplicate { reminders, err := h.queries.GetQuoteRemindersByType(ctx, db.GetQuoteRemindersByTypeParams{ QuoteID: quoteID, ReminderType: int32(reminderType), }) if err != nil { return fmt.Errorf("failed to check existing reminders: %w", err) } // Exit if the email has already been sent if len(reminders) > 0 { return nil } } // Send the email err := h.emailService.SendTemplateEmail( recipient, subject, templateName, templateData, ccs, nil, ) if err != nil { return fmt.Errorf("failed to send email: %w", err) } // Record the reminder var user sql.NullString if username != nil { user = sql.NullString{String: *username, Valid: true} } else { user = sql.NullString{Valid: false} } _, err = h.queries.InsertQuoteReminder(ctx, db.InsertQuoteReminderParams{ QuoteID: quoteID, ReminderType: int32(reminderType), DateSent: time.Now().UTC(), Username: user, }) if err != nil { return fmt.Errorf("failed to record reminder: %w", err) } return nil } // SendQuoteReminderEmailWithAttachment is like SendQuoteReminderEmail but includes a PDF attachment func (h *QuotesHandler) SendQuoteReminderEmailWithAttachment(ctx context.Context, quoteID int32, reminderType QuoteReminderType, recipient string, subject string, templateName string, templateData map[string]interface{}, ccs []string, username *string, pdfPath string, pdfFilename string, sourceReq *http.Request) error { log.Printf("SendQuoteReminderEmailWithAttachment called for quote %d, recipient: %s, PDF URL: %s", quoteID, recipient, pdfPath) // Safeguard: check for valid recipient if strings.TrimSpace(recipient) == "" { return fmt.Errorf("recipient email is required") } // Safeguard: check for valid template data if templateData == nil { return fmt.Errorf("template data is required") } // Safeguard: check for valid reminder type if reminderType != FirstReminder && reminderType != SecondReminder && reminderType != ThirdReminder { return fmt.Errorf("invalid reminder type: %v", reminderType) } // Download PDF from URL and prepare attachment if available var attachments []interface{} // Create authenticated request for PDF download req, err := auth.NewAuthenticatedRequest("GET", pdfPath, sourceReq) if err != nil { log.Printf("Failed to create PDF download request for quote %d: %v", quoteID, err) } else { client := &http.Client{} resp, err := client.Do(req) if err == nil && resp.StatusCode == 200 { defer resp.Body.Close() // Create temporary file for the PDF tmpFile, err := os.CreateTemp("", "quote_*.pdf") if err == nil { defer os.Remove(tmpFile.Name()) defer tmpFile.Close() // Copy PDF content to temp file if _, err := io.Copy(tmpFile, resp.Body); err == nil { attachments = []interface{}{ struct { Filename string FilePath string }{ Filename: pdfFilename, FilePath: tmpFile.Name(), }, } log.Printf("Successfully downloaded and attached PDF for quote %d: %s (tmpFile: %s)", quoteID, pdfFilename, tmpFile.Name()) } else { log.Printf("Failed to copy PDF content for quote %d: %v", quoteID, err) } } else { log.Printf("Failed to create temporary file for quote %d PDF: %v", quoteID, err) } } else if err != nil { log.Printf("Failed to download PDF from %s for quote %d: %v", pdfPath, quoteID, err) } else if resp != nil { defer resp.Body.Close() log.Printf("PDF download returned status %d for quote %d at %s", resp.StatusCode, quoteID, pdfPath) } } // If PDF download fails, just send email without attachment // Send the email (with or without attachment) log.Printf("Sending email for quote %d with %d attachment(s)", quoteID, len(attachments)) err = h.emailService.SendTemplateEmailWithAttachments( recipient, subject, templateName, templateData, ccs, nil, attachments, ) if err != nil { return fmt.Errorf("failed to send email: %w", err) } // Record the reminder var user sql.NullString if username != nil { user = sql.NullString{String: *username, Valid: true} } else { user = sql.NullString{Valid: false} } _, err = h.queries.InsertQuoteReminder(ctx, db.InsertQuoteReminderParams{ QuoteID: quoteID, ReminderType: int32(reminderType), DateSent: time.Now().UTC(), Username: user, }) if err != nil { return fmt.Errorf("failed to record reminder: %w", err) } return nil } // SendManualReminder handles POST requests to manually send a quote reminder func (h *QuotesHandler) SendManualReminder(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Parse form data if err := r.ParseForm(); err != nil { http.Error(w, "Invalid form data", http.StatusBadRequest) return } // Get parameters quoteIDStr := r.FormValue("quote_id") reminderTypeStr := r.FormValue("reminder_type") customerEmail := r.FormValue("customer_email") userEmail := r.FormValue("user_email") enquiryRef := r.FormValue("enquiry_ref") customerName := r.FormValue("customer_name") dateIssuedStr := r.FormValue("date_issued") validUntilStr := r.FormValue("valid_until") // Validate required fields if quoteIDStr == "" || reminderTypeStr == "" || customerEmail == "" { http.Error(w, "Missing required fields", http.StatusBadRequest) return } quoteID, err := strconv.ParseInt(quoteIDStr, 10, 32) if err != nil { http.Error(w, "Invalid quote ID", http.StatusBadRequest) return } reminderTypeInt, err := strconv.Atoi(reminderTypeStr) if err != nil { http.Error(w, "Invalid reminder type", http.StatusBadRequest) return } reminderType := QuoteReminderType(reminderTypeInt) if reminderType != FirstReminder && reminderType != SecondReminder && reminderType != ThirdReminder { http.Error(w, "Invalid reminder type value", http.StatusBadRequest) return } // Use manual template for manually sent reminders (generic without time references) subject, template := getReminderDetailsManual(reminderType) subject = fmt.Sprintf(subject, enquiryRef) submissionDate, expiryDate := formatQuoteDates(dateIssuedStr, validUntilStr) templateData := map[string]interface{}{ "CustomerName": customerName, "SubmissionDate": submissionDate, "ExpiryDate": expiryDate, "QuoteRef": enquiryRef, } ccs := []string{"sales@cmctechnologies.com.au"} if userEmail != "" { ccs = append(ccs, userEmail) } // Attach PDF quote from URL pdfURL := fmt.Sprintf("https://stg.cmctechnologies.com.au/pdf/%s.pdf", enquiryRef) pdfFilename := fmt.Sprintf("%s.pdf", enquiryRef) // Get username from request username := getUsername(r) usernamePtr := &username // Send the reminder with attachment err = h.SendQuoteReminderEmailWithAttachment( r.Context(), int32(quoteID), reminderType, customerEmail, subject, template, templateData, ccs, usernamePtr, pdfURL, pdfFilename, r, ) if err != nil { log.Printf("Failed to send manual reminder for quote %d: %v", quoteID, err) http.Error(w, fmt.Sprintf("Failed to send reminder: %v", err), http.StatusInternalServerError) return } // Check if this is an AJAX request if r.Header.Get("X-Requested-With") == "XMLHttpRequest" { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Reminder sent successfully", }) return } // Redirect back to quotes page for non-AJAX requests http.Redirect(w, r, "/go/quotes", http.StatusSeeOther) } // DisableReminders handles POST requests to disable automatic reminders for a quote func (h *QuotesHandler) DisableReminders(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Parse form data if err := r.ParseForm(); err != nil { http.Error(w, "Invalid form data", http.StatusBadRequest) return } quoteIDStr := r.FormValue("quote_id") if quoteIDStr == "" { http.Error(w, "Missing quote_id", http.StatusBadRequest) return } quoteID, err := strconv.ParseInt(quoteIDStr, 10, 32) if err != nil { http.Error(w, "Invalid quote ID", http.StatusBadRequest) return } // Get username from request username := getUsername(r) // Update the database to disable reminders _, err = h.queries.DisableQuoteReminders(r.Context(), db.DisableQuoteRemindersParams{ RemindersDisabledBy: sql.NullString{String: username, Valid: true}, ID: int32(quoteID), }) if err != nil { log.Printf("Failed to disable reminders for quote %d: %v", quoteID, err) http.Error(w, fmt.Sprintf("Failed to disable reminders: %v", err), http.StatusInternalServerError) return } // Check if this is an AJAX request if r.Header.Get("X-Requested-With") == "XMLHttpRequest" { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Reminders disabled successfully", }) return } // Redirect back to quotes page for non-AJAX requests http.Redirect(w, r, "/go/quotes", http.StatusSeeOther) } // EnableReminders handles POST requests to re-enable automatic reminders for a quote func (h *QuotesHandler) EnableReminders(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Parse form data if err := r.ParseForm(); err != nil { http.Error(w, "Invalid form data", http.StatusBadRequest) return } quoteIDStr := r.FormValue("quote_id") if quoteIDStr == "" { http.Error(w, "Missing quote_id", http.StatusBadRequest) return } quoteID, err := strconv.ParseInt(quoteIDStr, 10, 32) if err != nil { http.Error(w, "Invalid quote ID", http.StatusBadRequest) return } // Update the database to enable reminders _, err = h.queries.EnableQuoteReminders(r.Context(), int32(quoteID)) if err != nil { log.Printf("Failed to enable reminders for quote %d: %v", quoteID, err) http.Error(w, fmt.Sprintf("Failed to enable reminders: %v", err), http.StatusInternalServerError) return } // Check if this is an AJAX request if r.Header.Get("X-Requested-With") == "XMLHttpRequest" { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Reminders enabled successfully", }) return } // Redirect back to quotes page for non-AJAX requests http.Redirect(w, r, "/go/quotes", http.StatusSeeOther) } // Helper: get reminder type as string func reminderTypeString(reminderType int) string { switch reminderType { case 0: return "No Reminder" case 1: return "First Reminder" case 2: return "Second Reminder" case 3: return "Final Reminder" default: return "" } }