cmc-sales/go/internal/cmc/handlers/quotes/quotes.go

856 lines
28 KiB
Go

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 "-"
}
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 "-"
}
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 "-"
}
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 ""
}
}