diff --git a/go/cmd/server/main.go b/go/cmd/server/main.go index 3786409a..107c586c 100644 --- a/go/cmd/server/main.go +++ b/go/cmd/server/main.go @@ -73,6 +73,7 @@ func main() { // Quote routes goRouter.HandleFunc("/quotes", quoteHandler.QuotesOutstandingView).Methods("GET") + goRouter.HandleFunc("/quotes/send-reminder", quoteHandler.SendManualReminder).Methods("POST") // The following routes are currently disabled: /* diff --git a/go/internal/cmc/db/quotes.sql.go b/go/internal/cmc/db/quotes.sql.go index 0bfccf38..983c3f6d 100644 --- a/go/internal/cmc/db/quotes.sql.go +++ b/go/internal/cmc/db/quotes.sql.go @@ -39,6 +39,7 @@ latest_quote_reminder AS ( SELECT d.id AS document_id, u.username, + u.email as user_email, e.id AS enquiry_id, e.title as enquiry_ref, uu.first_name as customer_name, @@ -67,6 +68,7 @@ ORDER BY q.valid_until type GetExpiringSoonQuotesRow struct { DocumentID int32 `json:"document_id"` Username string `json:"username"` + UserEmail string `json:"user_email"` EnquiryID int32 `json:"enquiry_id"` EnquiryRef string `json:"enquiry_ref"` CustomerName string `json:"customer_name"` @@ -89,6 +91,7 @@ func (q *Queries) GetExpiringSoonQuotes(ctx context.Context, dateADD interface{} if err := rows.Scan( &i.DocumentID, &i.Username, + &i.UserEmail, &i.EnquiryID, &i.EnquiryRef, &i.CustomerName, @@ -139,6 +142,7 @@ latest_quote_reminder AS ( SELECT d.id AS document_id, u.username, + u.email as user_email, e.id AS enquiry_id, e.title as enquiry_ref, uu.first_name as customer_name, @@ -167,6 +171,7 @@ ORDER BY q.valid_until type GetExpiringSoonQuotesOnDayRow struct { DocumentID int32 `json:"document_id"` Username string `json:"username"` + UserEmail string `json:"user_email"` EnquiryID int32 `json:"enquiry_id"` EnquiryRef string `json:"enquiry_ref"` CustomerName string `json:"customer_name"` @@ -189,6 +194,7 @@ func (q *Queries) GetExpiringSoonQuotesOnDay(ctx context.Context, dateADD interf if err := rows.Scan( &i.DocumentID, &i.Username, + &i.UserEmail, &i.EnquiryID, &i.EnquiryRef, &i.CustomerName, @@ -280,6 +286,7 @@ latest_quote_reminder AS ( SELECT d.id AS document_id, u.username, + u.email as user_email, e.id AS enquiry_id, e.title as enquiry_ref, uu.first_name as customer_name, @@ -308,6 +315,7 @@ ORDER BY q.valid_until DESC type GetRecentlyExpiredQuotesRow struct { DocumentID int32 `json:"document_id"` Username string `json:"username"` + UserEmail string `json:"user_email"` EnquiryID int32 `json:"enquiry_id"` EnquiryRef string `json:"enquiry_ref"` CustomerName string `json:"customer_name"` @@ -330,6 +338,7 @@ func (q *Queries) GetRecentlyExpiredQuotes(ctx context.Context, dateSUB interfac if err := rows.Scan( &i.DocumentID, &i.Username, + &i.UserEmail, &i.EnquiryID, &i.EnquiryRef, &i.CustomerName, @@ -380,6 +389,7 @@ latest_quote_reminder AS ( SELECT d.id AS document_id, u.username, + u.email as user_email, e.id AS enquiry_id, e.title as enquiry_ref, uu.first_name as customer_name, @@ -408,6 +418,7 @@ ORDER BY q.valid_until DESC type GetRecentlyExpiredQuotesOnDayRow struct { DocumentID int32 `json:"document_id"` Username string `json:"username"` + UserEmail string `json:"user_email"` EnquiryID int32 `json:"enquiry_id"` EnquiryRef string `json:"enquiry_ref"` CustomerName string `json:"customer_name"` @@ -430,6 +441,7 @@ func (q *Queries) GetRecentlyExpiredQuotesOnDay(ctx context.Context, dateSUB int if err := rows.Scan( &i.DocumentID, &i.Username, + &i.UserEmail, &i.EnquiryID, &i.EnquiryRef, &i.CustomerName, diff --git a/go/internal/cmc/handlers/quotes/quotes.go b/go/internal/cmc/handlers/quotes/quotes.go index ccc8f16c..f3a87c82 100644 --- a/go/internal/cmc/handlers/quotes/quotes.go +++ b/go/internal/cmc/handlers/quotes/quotes.go @@ -72,6 +72,7 @@ func calcExpiryInfo(validUntil time.Time) (string, int, int) { type QuoteRow interface { GetID() int32 GetUsername() string + GetUserEmail() string GetEnquiryID() int32 GetEnquiryRef() string GetDateIssued() time.Time @@ -88,6 +89,7 @@ type ExpiringSoonQuoteRowWrapper struct{ db.GetExpiringSoonQuotesRow } func (q ExpiringSoonQuoteRowWrapper) GetID() int32 { return q.DocumentID } func (q ExpiringSoonQuoteRowWrapper) GetUsername() string { return q.Username } +func (q ExpiringSoonQuoteRowWrapper) GetUserEmail() string { return q.UserEmail } 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 } @@ -101,6 +103,7 @@ type RecentlyExpiredQuoteRowWrapper struct{ db.GetRecentlyExpiredQuotesRow } func (q RecentlyExpiredQuoteRowWrapper) GetID() int32 { return q.DocumentID } func (q RecentlyExpiredQuoteRowWrapper) GetUsername() string { return q.Username } +func (q RecentlyExpiredQuoteRowWrapper) GetUserEmail() string { return q.UserEmail } 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 } @@ -116,6 +119,7 @@ type ExpiringSoonQuoteOnDayRowWrapper struct { func (q ExpiringSoonQuoteOnDayRowWrapper) GetID() int32 { return q.DocumentID } func (q ExpiringSoonQuoteOnDayRowWrapper) GetUsername() string { return q.Username } +func (q ExpiringSoonQuoteOnDayRowWrapper) GetUserEmail() string { return q.UserEmail } 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 } @@ -133,6 +137,7 @@ type RecentlyExpiredQuoteOnDayRowWrapper struct { func (q RecentlyExpiredQuoteOnDayRowWrapper) GetID() int32 { return q.DocumentID } func (q RecentlyExpiredQuoteOnDayRowWrapper) GetUsername() string { return q.Username } +func (q RecentlyExpiredQuoteOnDayRowWrapper) GetUserEmail() string { return q.UserEmail } 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 } @@ -150,6 +155,7 @@ func formatQuoteRow(q QuoteRow) map[string]interface{} { 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()), @@ -295,8 +301,61 @@ func (t QuoteReminderType) String() string { type quoteReminderJob struct { DayOffset int ReminderType QuoteReminderType - Subject string - Template string +} + +// 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 "", "" + } +} + +// 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) @@ -304,9 +363,9 @@ func (h *QuotesHandler) DailyQuoteExpirationCheck() { fmt.Println("Running DailyQuoteExpirationCheck...") reminderJobs := []quoteReminderJob{ - {7, FirstReminder, "Reminder: Quote %s Expires Soon", "first_reminder.html"}, - {-7, SecondReminder, "Follow-Up: Quote %s Expired", "second_reminder.html"}, - {-60, ThirdReminder, "Final Reminder: Quote %s Closed", "final_reminder.html"}, + {7, FirstReminder}, + {-7, SecondReminder}, + {-60, ThirdReminder}, } for _, job := range reminderJobs { @@ -320,38 +379,24 @@ func (h *QuotesHandler) DailyQuoteExpirationCheck() { continue } for _, q := range quotes { - // Format dates as DD/MM/YYYY - var submissionDate, expiryDate string - if dateIssued, ok := q["DateIssued"].(string); ok && dateIssued != "" { - t, err := time.Parse(time.RFC3339, dateIssued) - if err == nil { - submissionDate = t.Format("02/01/2006") - } else { - submissionDate = dateIssued - } - } - if validUntil, ok := q["ValidUntil"].(string); ok && validUntil != "" { - t, err := time.Parse(time.RFC3339, validUntil) - if err == nil { - expiryDate = t.Format("02/01/2006") - } else { - expiryDate = validUntil - } - } - templateData := map[string]interface{}{ - "CustomerName": q["CustomerName"], - "SubmissionDate": submissionDate, - "ExpiryDate": expiryDate, - "QuoteRef": q["EnquiryRef"], - } + 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), - fmt.Sprintf(job.Subject, q["EnquiryRef"]), - job.Template, + subject, + template, templateData, + ccs, nil, ) if err != nil { @@ -364,7 +409,7 @@ func (h *QuotesHandler) DailyQuoteExpirationCheck() { } // 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{}, username *string) error { +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) error { // Safeguard: check for valid recipient if strings.TrimSpace(recipient) == "" { return fmt.Errorf("recipient email is required") @@ -398,7 +443,7 @@ func (h *QuotesHandler) SendQuoteReminderEmail(ctx context.Context, quoteID int3 subject, templateName, templateData, - nil, nil, + ccs, nil, ) if err != nil { return fmt.Errorf("failed to send email: %w", err) @@ -423,6 +468,89 @@ func (h *QuotesHandler) SendQuoteReminderEmail(ctx context.Context, quoteID int3 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 shared function to prepare email data + subject, template, templateData, ccs := prepareReminderEmail( + reminderType, + customerName, + dateIssuedStr, + validUntilStr, + enquiryRef, + userEmail, + ) + + // Get username from request + username := getUsername(r) + usernamePtr := &username + + // Send the reminder + err = h.SendQuoteReminderEmail( + r.Context(), + int32(quoteID), + reminderType, + customerEmail, + subject, + template, + templateData, + ccs, + usernamePtr, + ) + + if err != nil { + http.Error(w, fmt.Sprintf("Failed to send reminder: %v", err), http.StatusInternalServerError) + return + } + + // Redirect back to quotes page + http.Redirect(w, r, "/go/quotes", http.StatusSeeOther) +} + // Helper: get reminder type as string func reminderTypeString(reminderType int) string { switch reminderType { diff --git a/go/internal/cmc/handlers/quotes/quotes_test.go b/go/internal/cmc/handlers/quotes/quotes_test.go index 8085b2e9..a85abf93 100644 --- a/go/internal/cmc/handlers/quotes/quotes_test.go +++ b/go/internal/cmc/handlers/quotes/quotes_test.go @@ -143,7 +143,7 @@ func TestSendQuoteReminderEmail_OnDesignatedDay(t *testing.T) { emailService: me, } // Simulate designated day logic by calling SendQuoteReminderEmail - err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{}, nil) + err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{}, nil, nil) if err != nil { t.Fatalf("Expected no error, got %v", err) } @@ -164,7 +164,7 @@ func TestSendQuoteReminderEmail_AlreadyReminded(t *testing.T) { queries: mq, emailService: me, } - err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil) + err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil, nil) if err == nil { t.Error("Expected error for already sent reminder") } @@ -186,7 +186,7 @@ func TestSendQuoteReminderEmail_OnlyOnce(t *testing.T) { emailService: me, } // First call should succeed - err1 := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil) + err1 := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil, nil) if err1 != nil { t.Fatalf("Expected first call to succeed, got %v", err1) } @@ -197,7 +197,7 @@ func TestSendQuoteReminderEmail_OnlyOnce(t *testing.T) { t.Errorf("Expected 1 reminder recorded in DB, got %d", len(mq.reminders)) } // Second call should fail - err2 := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil) + err2 := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil, nil) if err2 == nil { t.Error("Expected error for already sent reminder on second call") } @@ -215,7 +215,7 @@ func TestSendQuoteReminderEmail_SendsIfNotAlreadySent(t *testing.T) { queries: mq, emailService: me, } - err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{}, nil) + err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{}, nil, nil) if err != nil { t.Fatalf("Expected no error, got %v", err) } @@ -236,7 +236,7 @@ func TestSendQuoteReminderEmail_IgnoresIfAlreadySent(t *testing.T) { queries: mq, emailService: me, } - err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil) + err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil, nil) if err == nil { t.Error("Expected error for already sent reminder") } @@ -252,7 +252,7 @@ func TestSendQuoteReminderEmail_IgnoresIfAlreadySent(t *testing.T) { // Description: Simulates a DB error and expects the handler to return an error. Should fail if DB errors are not handled. func TestSendQuoteReminderEmail_DBError(t *testing.T) { h := &QuotesHandler{queries: &mockQueriesError{}} - err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{}, nil) + err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{}, nil, nil) if err == nil { t.Error("Expected error when DB fails, got nil") } @@ -264,7 +264,7 @@ func TestSendQuoteReminderEmail_DBError(t *testing.T) { func TestSendQuoteReminderEmail_NilRecipient(t *testing.T) { mq := &mockQueries{reminders: []db.QuoteReminder{}} h := &QuotesHandler{queries: mq} - err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "", "Subject", "template", map[string]interface{}{}, nil) + err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "", "Subject", "template", map[string]interface{}{}, nil, nil) if err == nil { t.Error("Expected error for nil recipient, got nil") } @@ -276,7 +276,7 @@ func TestSendQuoteReminderEmail_NilRecipient(t *testing.T) { func TestSendQuoteReminderEmail_MissingTemplateData(t *testing.T) { mq := &mockQueries{reminders: []db.QuoteReminder{}} h := &QuotesHandler{queries: mq} - err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", nil, nil) + err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", nil, nil, nil) if err == nil { t.Error("Expected error for missing template data, got nil") } @@ -288,7 +288,7 @@ func TestSendQuoteReminderEmail_MissingTemplateData(t *testing.T) { func TestSendQuoteReminderEmail_InvalidReminderType(t *testing.T) { mq := &mockQueries{reminders: []db.QuoteReminder{}} h := &QuotesHandler{queries: mq} - err := h.SendQuoteReminderEmail(context.Background(), 123, 99, "test@example.com", "Subject", "template", map[string]interface{}{}, nil) + err := h.SendQuoteReminderEmail(context.Background(), 123, 99, "test@example.com", "Subject", "template", map[string]interface{}{}, nil, nil) if err == nil { t.Error("Expected error for invalid reminder type, got nil") } @@ -302,7 +302,7 @@ func TestSendQuoteReminderEmail_MultipleTypes(t *testing.T) { h := &QuotesHandler{queries: mq, emailService: me} // First reminder type - err1 := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil) + err1 := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil, nil) if err1 != nil { t.Fatalf("Expected first reminder to be sent, got %v", err1) } @@ -314,7 +314,7 @@ func TestSendQuoteReminderEmail_MultipleTypes(t *testing.T) { } // Second reminder type - err2 := h.SendQuoteReminderEmail(context.Background(), 123, 2, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 2}, nil) + err2 := h.SendQuoteReminderEmail(context.Background(), 123, 2, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 2}, nil, nil) if err2 != nil { t.Fatalf("Expected second reminder to be sent, got %v", err2) } @@ -334,7 +334,7 @@ func TestSendQuoteReminderEmail_DifferentQuotes(t *testing.T) { h := &QuotesHandler{queries: mq, emailService: me} // Send reminder for quote 123 - err1 := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil) + err1 := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil, nil) if err1 != nil { t.Fatalf("Expected reminder for quote 123 to be sent, got %v", err1) } @@ -346,7 +346,7 @@ func TestSendQuoteReminderEmail_DifferentQuotes(t *testing.T) { } // Send reminder for quote 456 - err2 := h.SendQuoteReminderEmail(context.Background(), 456, 1, "test2@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 456, "ReminderType": 1}, nil) + err2 := h.SendQuoteReminderEmail(context.Background(), 456, 1, "test2@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 456, "ReminderType": 1}, nil, nil) if err2 != nil { t.Fatalf("Expected reminder for quote 456 to be sent, got %v", err2) } diff --git a/go/sql/queries/quotes.sql b/go/sql/queries/quotes.sql index a8398e64..9f5fc283 100644 --- a/go/sql/queries/quotes.sql +++ b/go/sql/queries/quotes.sql @@ -26,6 +26,7 @@ latest_quote_reminder AS ( SELECT d.id AS document_id, u.username, + u.email as user_email, e.id AS enquiry_id, e.title as enquiry_ref, uu.first_name as customer_name, @@ -78,6 +79,7 @@ latest_quote_reminder AS ( SELECT d.id AS document_id, u.username, + u.email as user_email, e.id AS enquiry_id, e.title as enquiry_ref, uu.first_name as customer_name, @@ -130,6 +132,7 @@ latest_quote_reminder AS ( SELECT d.id AS document_id, u.username, + u.email as user_email, e.id AS enquiry_id, e.title as enquiry_ref, uu.first_name as customer_name, @@ -182,6 +185,7 @@ latest_quote_reminder AS ( SELECT d.id AS document_id, u.username, + u.email as user_email, e.id AS enquiry_id, e.title as enquiry_ref, uu.first_name as customer_name, diff --git a/go/templates/layouts/base.html b/go/templates/layouts/base.html index 3a6438f2..80c9ed59 100644 --- a/go/templates/layouts/base.html +++ b/go/templates/layouts/base.html @@ -184,7 +184,7 @@
-
+
{{block "content" .}}{{end}}
diff --git a/go/templates/quotes/index.html b/go/templates/quotes/index.html index 6b220fc0..0818ec38 100644 --- a/go/templates/quotes/index.html +++ b/go/templates/quotes/index.html @@ -1,9 +1,9 @@ {{define "content"}}

Quotes Expiring

-
+

Expiring Soon

- +
@@ -13,6 +13,7 @@ + @@ -41,15 +42,48 @@ + {{else}} - + {{end}}
QuoteExpires Reminder Reminder SentActions
{{if .LatestReminderSent}}{{.LatestReminderSent}}{{else}}-{{end}} +
+ + + + + + + +
+ {{if eq .LatestReminderType "No Reminder"}} + + + {{else if eq .LatestReminderType "First Reminder"}} + + + {{else if eq .LatestReminderType "Second Reminder"}} + + + {{else}} + + + {{end}} +
+ +
+
No quotes expiring soon.
No quotes expiring soon.

Recently Expired

- +
@@ -59,6 +93,7 @@ + @@ -87,14 +122,141 @@ + {{else}} - + {{end}}
QuoteExpires Reminder Reminder SentActions
{{if .LatestReminderSent}}{{.LatestReminderSent}}{{else}}-{{end}} +
+ + + + + + + +
+ {{if eq .LatestReminderType "No Reminder"}} + + + {{else if eq .LatestReminderType "First Reminder"}} + + + {{else if eq .LatestReminderType "Second Reminder"}} + + + {{else}} + + + {{end}} +
+ +
+
No recently expired quotes.
No recently expired quotes.
+ + + +