Adding ability to manually send reminders
This commit is contained in:
parent
c496e7232f
commit
1d8753764f
|
|
@ -73,6 +73,7 @@ func main() {
|
||||||
|
|
||||||
// Quote routes
|
// Quote routes
|
||||||
goRouter.HandleFunc("/quotes", quoteHandler.QuotesOutstandingView).Methods("GET")
|
goRouter.HandleFunc("/quotes", quoteHandler.QuotesOutstandingView).Methods("GET")
|
||||||
|
goRouter.HandleFunc("/quotes/send-reminder", quoteHandler.SendManualReminder).Methods("POST")
|
||||||
|
|
||||||
// The following routes are currently disabled:
|
// The following routes are currently disabled:
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ latest_quote_reminder AS (
|
||||||
SELECT
|
SELECT
|
||||||
d.id AS document_id,
|
d.id AS document_id,
|
||||||
u.username,
|
u.username,
|
||||||
|
u.email as user_email,
|
||||||
e.id AS enquiry_id,
|
e.id AS enquiry_id,
|
||||||
e.title as enquiry_ref,
|
e.title as enquiry_ref,
|
||||||
uu.first_name as customer_name,
|
uu.first_name as customer_name,
|
||||||
|
|
@ -67,6 +68,7 @@ ORDER BY q.valid_until
|
||||||
type GetExpiringSoonQuotesRow struct {
|
type GetExpiringSoonQuotesRow struct {
|
||||||
DocumentID int32 `json:"document_id"`
|
DocumentID int32 `json:"document_id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
|
UserEmail string `json:"user_email"`
|
||||||
EnquiryID int32 `json:"enquiry_id"`
|
EnquiryID int32 `json:"enquiry_id"`
|
||||||
EnquiryRef string `json:"enquiry_ref"`
|
EnquiryRef string `json:"enquiry_ref"`
|
||||||
CustomerName string `json:"customer_name"`
|
CustomerName string `json:"customer_name"`
|
||||||
|
|
@ -89,6 +91,7 @@ func (q *Queries) GetExpiringSoonQuotes(ctx context.Context, dateADD interface{}
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.DocumentID,
|
&i.DocumentID,
|
||||||
&i.Username,
|
&i.Username,
|
||||||
|
&i.UserEmail,
|
||||||
&i.EnquiryID,
|
&i.EnquiryID,
|
||||||
&i.EnquiryRef,
|
&i.EnquiryRef,
|
||||||
&i.CustomerName,
|
&i.CustomerName,
|
||||||
|
|
@ -139,6 +142,7 @@ latest_quote_reminder AS (
|
||||||
SELECT
|
SELECT
|
||||||
d.id AS document_id,
|
d.id AS document_id,
|
||||||
u.username,
|
u.username,
|
||||||
|
u.email as user_email,
|
||||||
e.id AS enquiry_id,
|
e.id AS enquiry_id,
|
||||||
e.title as enquiry_ref,
|
e.title as enquiry_ref,
|
||||||
uu.first_name as customer_name,
|
uu.first_name as customer_name,
|
||||||
|
|
@ -167,6 +171,7 @@ ORDER BY q.valid_until
|
||||||
type GetExpiringSoonQuotesOnDayRow struct {
|
type GetExpiringSoonQuotesOnDayRow struct {
|
||||||
DocumentID int32 `json:"document_id"`
|
DocumentID int32 `json:"document_id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
|
UserEmail string `json:"user_email"`
|
||||||
EnquiryID int32 `json:"enquiry_id"`
|
EnquiryID int32 `json:"enquiry_id"`
|
||||||
EnquiryRef string `json:"enquiry_ref"`
|
EnquiryRef string `json:"enquiry_ref"`
|
||||||
CustomerName string `json:"customer_name"`
|
CustomerName string `json:"customer_name"`
|
||||||
|
|
@ -189,6 +194,7 @@ func (q *Queries) GetExpiringSoonQuotesOnDay(ctx context.Context, dateADD interf
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.DocumentID,
|
&i.DocumentID,
|
||||||
&i.Username,
|
&i.Username,
|
||||||
|
&i.UserEmail,
|
||||||
&i.EnquiryID,
|
&i.EnquiryID,
|
||||||
&i.EnquiryRef,
|
&i.EnquiryRef,
|
||||||
&i.CustomerName,
|
&i.CustomerName,
|
||||||
|
|
@ -280,6 +286,7 @@ latest_quote_reminder AS (
|
||||||
SELECT
|
SELECT
|
||||||
d.id AS document_id,
|
d.id AS document_id,
|
||||||
u.username,
|
u.username,
|
||||||
|
u.email as user_email,
|
||||||
e.id AS enquiry_id,
|
e.id AS enquiry_id,
|
||||||
e.title as enquiry_ref,
|
e.title as enquiry_ref,
|
||||||
uu.first_name as customer_name,
|
uu.first_name as customer_name,
|
||||||
|
|
@ -308,6 +315,7 @@ ORDER BY q.valid_until DESC
|
||||||
type GetRecentlyExpiredQuotesRow struct {
|
type GetRecentlyExpiredQuotesRow struct {
|
||||||
DocumentID int32 `json:"document_id"`
|
DocumentID int32 `json:"document_id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
|
UserEmail string `json:"user_email"`
|
||||||
EnquiryID int32 `json:"enquiry_id"`
|
EnquiryID int32 `json:"enquiry_id"`
|
||||||
EnquiryRef string `json:"enquiry_ref"`
|
EnquiryRef string `json:"enquiry_ref"`
|
||||||
CustomerName string `json:"customer_name"`
|
CustomerName string `json:"customer_name"`
|
||||||
|
|
@ -330,6 +338,7 @@ func (q *Queries) GetRecentlyExpiredQuotes(ctx context.Context, dateSUB interfac
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.DocumentID,
|
&i.DocumentID,
|
||||||
&i.Username,
|
&i.Username,
|
||||||
|
&i.UserEmail,
|
||||||
&i.EnquiryID,
|
&i.EnquiryID,
|
||||||
&i.EnquiryRef,
|
&i.EnquiryRef,
|
||||||
&i.CustomerName,
|
&i.CustomerName,
|
||||||
|
|
@ -380,6 +389,7 @@ latest_quote_reminder AS (
|
||||||
SELECT
|
SELECT
|
||||||
d.id AS document_id,
|
d.id AS document_id,
|
||||||
u.username,
|
u.username,
|
||||||
|
u.email as user_email,
|
||||||
e.id AS enquiry_id,
|
e.id AS enquiry_id,
|
||||||
e.title as enquiry_ref,
|
e.title as enquiry_ref,
|
||||||
uu.first_name as customer_name,
|
uu.first_name as customer_name,
|
||||||
|
|
@ -408,6 +418,7 @@ ORDER BY q.valid_until DESC
|
||||||
type GetRecentlyExpiredQuotesOnDayRow struct {
|
type GetRecentlyExpiredQuotesOnDayRow struct {
|
||||||
DocumentID int32 `json:"document_id"`
|
DocumentID int32 `json:"document_id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
|
UserEmail string `json:"user_email"`
|
||||||
EnquiryID int32 `json:"enquiry_id"`
|
EnquiryID int32 `json:"enquiry_id"`
|
||||||
EnquiryRef string `json:"enquiry_ref"`
|
EnquiryRef string `json:"enquiry_ref"`
|
||||||
CustomerName string `json:"customer_name"`
|
CustomerName string `json:"customer_name"`
|
||||||
|
|
@ -430,6 +441,7 @@ func (q *Queries) GetRecentlyExpiredQuotesOnDay(ctx context.Context, dateSUB int
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.DocumentID,
|
&i.DocumentID,
|
||||||
&i.Username,
|
&i.Username,
|
||||||
|
&i.UserEmail,
|
||||||
&i.EnquiryID,
|
&i.EnquiryID,
|
||||||
&i.EnquiryRef,
|
&i.EnquiryRef,
|
||||||
&i.CustomerName,
|
&i.CustomerName,
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ func calcExpiryInfo(validUntil time.Time) (string, int, int) {
|
||||||
type QuoteRow interface {
|
type QuoteRow interface {
|
||||||
GetID() int32
|
GetID() int32
|
||||||
GetUsername() string
|
GetUsername() string
|
||||||
|
GetUserEmail() string
|
||||||
GetEnquiryID() int32
|
GetEnquiryID() int32
|
||||||
GetEnquiryRef() string
|
GetEnquiryRef() string
|
||||||
GetDateIssued() time.Time
|
GetDateIssued() time.Time
|
||||||
|
|
@ -88,6 +89,7 @@ type ExpiringSoonQuoteRowWrapper struct{ db.GetExpiringSoonQuotesRow }
|
||||||
|
|
||||||
func (q ExpiringSoonQuoteRowWrapper) GetID() int32 { return q.DocumentID }
|
func (q ExpiringSoonQuoteRowWrapper) GetID() int32 { return q.DocumentID }
|
||||||
func (q ExpiringSoonQuoteRowWrapper) GetUsername() string { return q.Username }
|
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) GetEnquiryID() int32 { return q.EnquiryID }
|
||||||
func (q ExpiringSoonQuoteRowWrapper) GetEnquiryRef() string { return q.EnquiryRef }
|
func (q ExpiringSoonQuoteRowWrapper) GetEnquiryRef() string { return q.EnquiryRef }
|
||||||
func (q ExpiringSoonQuoteRowWrapper) GetDateIssued() time.Time { return q.DateIssued }
|
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) GetID() int32 { return q.DocumentID }
|
||||||
func (q RecentlyExpiredQuoteRowWrapper) GetUsername() string { return q.Username }
|
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) GetEnquiryID() int32 { return q.EnquiryID }
|
||||||
func (q RecentlyExpiredQuoteRowWrapper) GetEnquiryRef() string { return q.EnquiryRef }
|
func (q RecentlyExpiredQuoteRowWrapper) GetEnquiryRef() string { return q.EnquiryRef }
|
||||||
func (q RecentlyExpiredQuoteRowWrapper) GetDateIssued() time.Time { return q.DateIssued }
|
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) GetID() int32 { return q.DocumentID }
|
||||||
func (q ExpiringSoonQuoteOnDayRowWrapper) GetUsername() string { return q.Username }
|
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) GetEnquiryID() int32 { return q.EnquiryID }
|
||||||
func (q ExpiringSoonQuoteOnDayRowWrapper) GetEnquiryRef() string { return q.EnquiryRef }
|
func (q ExpiringSoonQuoteOnDayRowWrapper) GetEnquiryRef() string { return q.EnquiryRef }
|
||||||
func (q ExpiringSoonQuoteOnDayRowWrapper) GetDateIssued() time.Time { return q.DateIssued }
|
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) GetID() int32 { return q.DocumentID }
|
||||||
func (q RecentlyExpiredQuoteOnDayRowWrapper) GetUsername() string { return q.Username }
|
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) GetEnquiryID() int32 { return q.EnquiryID }
|
||||||
func (q RecentlyExpiredQuoteOnDayRowWrapper) GetEnquiryRef() string { return q.EnquiryRef }
|
func (q RecentlyExpiredQuoteOnDayRowWrapper) GetEnquiryRef() string { return q.EnquiryRef }
|
||||||
func (q RecentlyExpiredQuoteOnDayRowWrapper) GetDateIssued() time.Time { return q.DateIssued }
|
func (q RecentlyExpiredQuoteOnDayRowWrapper) GetDateIssued() time.Time { return q.DateIssued }
|
||||||
|
|
@ -150,6 +155,7 @@ func formatQuoteRow(q QuoteRow) map[string]interface{} {
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"ID": q.GetID(),
|
"ID": q.GetID(),
|
||||||
"Username": strings.Title(q.GetUsername()),
|
"Username": strings.Title(q.GetUsername()),
|
||||||
|
"UserEmail": q.GetUserEmail(),
|
||||||
"EnquiryID": q.GetEnquiryID(),
|
"EnquiryID": q.GetEnquiryID(),
|
||||||
"EnquiryRef": q.GetEnquiryRef(),
|
"EnquiryRef": q.GetEnquiryRef(),
|
||||||
"CustomerName": strings.TrimSpace(q.GetCustomerName()),
|
"CustomerName": strings.TrimSpace(q.GetCustomerName()),
|
||||||
|
|
@ -295,8 +301,61 @@ func (t QuoteReminderType) String() string {
|
||||||
type quoteReminderJob struct {
|
type quoteReminderJob struct {
|
||||||
DayOffset int
|
DayOffset int
|
||||||
ReminderType QuoteReminderType
|
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)
|
// 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...")
|
fmt.Println("Running DailyQuoteExpirationCheck...")
|
||||||
|
|
||||||
reminderJobs := []quoteReminderJob{
|
reminderJobs := []quoteReminderJob{
|
||||||
{7, FirstReminder, "Reminder: Quote %s Expires Soon", "first_reminder.html"},
|
{7, FirstReminder},
|
||||||
{-7, SecondReminder, "Follow-Up: Quote %s Expired", "second_reminder.html"},
|
{-7, SecondReminder},
|
||||||
{-60, ThirdReminder, "Final Reminder: Quote %s Closed", "final_reminder.html"},
|
{-60, ThirdReminder},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, job := range reminderJobs {
|
for _, job := range reminderJobs {
|
||||||
|
|
@ -320,38 +379,24 @@ func (h *QuotesHandler) DailyQuoteExpirationCheck() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, q := range quotes {
|
for _, q := range quotes {
|
||||||
// Format dates as DD/MM/YYYY
|
subject, template, templateData, ccs := prepareReminderEmail(
|
||||||
var submissionDate, expiryDate string
|
job.ReminderType,
|
||||||
if dateIssued, ok := q["DateIssued"].(string); ok && dateIssued != "" {
|
q["CustomerName"].(string),
|
||||||
t, err := time.Parse(time.RFC3339, dateIssued)
|
q["DateIssued"].(string),
|
||||||
if err == nil {
|
q["ValidUntil"].(string),
|
||||||
submissionDate = t.Format("02/01/2006")
|
q["EnquiryRef"].(string),
|
||||||
} else {
|
q["UserEmail"].(string),
|
||||||
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"],
|
|
||||||
}
|
|
||||||
err := h.SendQuoteReminderEmail(
|
err := h.SendQuoteReminderEmail(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
q["ID"].(int32),
|
q["ID"].(int32),
|
||||||
job.ReminderType,
|
job.ReminderType,
|
||||||
q["CustomerEmail"].(string),
|
q["CustomerEmail"].(string),
|
||||||
fmt.Sprintf(job.Subject, q["EnquiryRef"]),
|
subject,
|
||||||
job.Template,
|
template,
|
||||||
templateData,
|
templateData,
|
||||||
|
ccs,
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
if err != 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.
|
// 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
|
// Safeguard: check for valid recipient
|
||||||
if strings.TrimSpace(recipient) == "" {
|
if strings.TrimSpace(recipient) == "" {
|
||||||
return fmt.Errorf("recipient email is required")
|
return fmt.Errorf("recipient email is required")
|
||||||
|
|
@ -398,7 +443,7 @@ func (h *QuotesHandler) SendQuoteReminderEmail(ctx context.Context, quoteID int3
|
||||||
subject,
|
subject,
|
||||||
templateName,
|
templateName,
|
||||||
templateData,
|
templateData,
|
||||||
nil, nil,
|
ccs, nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to send email: %w", err)
|
return fmt.Errorf("failed to send email: %w", err)
|
||||||
|
|
@ -423,6 +468,89 @@ func (h *QuotesHandler) SendQuoteReminderEmail(ctx context.Context, quoteID int3
|
||||||
return nil
|
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
|
// Helper: get reminder type as string
|
||||||
func reminderTypeString(reminderType int) string {
|
func reminderTypeString(reminderType int) string {
|
||||||
switch reminderType {
|
switch reminderType {
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ func TestSendQuoteReminderEmail_OnDesignatedDay(t *testing.T) {
|
||||||
emailService: me,
|
emailService: me,
|
||||||
}
|
}
|
||||||
// Simulate designated day logic by calling SendQuoteReminderEmail
|
// 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 {
|
if err != nil {
|
||||||
t.Fatalf("Expected no error, got %v", err)
|
t.Fatalf("Expected no error, got %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -164,7 +164,7 @@ func TestSendQuoteReminderEmail_AlreadyReminded(t *testing.T) {
|
||||||
queries: mq,
|
queries: mq,
|
||||||
emailService: me,
|
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 {
|
if err == nil {
|
||||||
t.Error("Expected error for already sent reminder")
|
t.Error("Expected error for already sent reminder")
|
||||||
}
|
}
|
||||||
|
|
@ -186,7 +186,7 @@ func TestSendQuoteReminderEmail_OnlyOnce(t *testing.T) {
|
||||||
emailService: me,
|
emailService: me,
|
||||||
}
|
}
|
||||||
// First call should succeed
|
// 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 {
|
if err1 != nil {
|
||||||
t.Fatalf("Expected first call to succeed, got %v", err1)
|
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))
|
t.Errorf("Expected 1 reminder recorded in DB, got %d", len(mq.reminders))
|
||||||
}
|
}
|
||||||
// Second call should fail
|
// 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 {
|
if err2 == nil {
|
||||||
t.Error("Expected error for already sent reminder on second call")
|
t.Error("Expected error for already sent reminder on second call")
|
||||||
}
|
}
|
||||||
|
|
@ -215,7 +215,7 @@ func TestSendQuoteReminderEmail_SendsIfNotAlreadySent(t *testing.T) {
|
||||||
queries: mq,
|
queries: mq,
|
||||||
emailService: me,
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Expected no error, got %v", err)
|
t.Fatalf("Expected no error, got %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -236,7 +236,7 @@ func TestSendQuoteReminderEmail_IgnoresIfAlreadySent(t *testing.T) {
|
||||||
queries: mq,
|
queries: mq,
|
||||||
emailService: me,
|
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 {
|
if err == nil {
|
||||||
t.Error("Expected error for already sent reminder")
|
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.
|
// 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) {
|
func TestSendQuoteReminderEmail_DBError(t *testing.T) {
|
||||||
h := &QuotesHandler{queries: &mockQueriesError{}}
|
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 {
|
if err == nil {
|
||||||
t.Error("Expected error when DB fails, got 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) {
|
func TestSendQuoteReminderEmail_NilRecipient(t *testing.T) {
|
||||||
mq := &mockQueries{reminders: []db.QuoteReminder{}}
|
mq := &mockQueries{reminders: []db.QuoteReminder{}}
|
||||||
h := &QuotesHandler{queries: mq}
|
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 {
|
if err == nil {
|
||||||
t.Error("Expected error for nil recipient, got 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) {
|
func TestSendQuoteReminderEmail_MissingTemplateData(t *testing.T) {
|
||||||
mq := &mockQueries{reminders: []db.QuoteReminder{}}
|
mq := &mockQueries{reminders: []db.QuoteReminder{}}
|
||||||
h := &QuotesHandler{queries: mq}
|
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 {
|
if err == nil {
|
||||||
t.Error("Expected error for missing template data, got 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) {
|
func TestSendQuoteReminderEmail_InvalidReminderType(t *testing.T) {
|
||||||
mq := &mockQueries{reminders: []db.QuoteReminder{}}
|
mq := &mockQueries{reminders: []db.QuoteReminder{}}
|
||||||
h := &QuotesHandler{queries: mq}
|
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 {
|
if err == nil {
|
||||||
t.Error("Expected error for invalid reminder type, got 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}
|
h := &QuotesHandler{queries: mq, emailService: me}
|
||||||
|
|
||||||
// First reminder type
|
// 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 {
|
if err1 != nil {
|
||||||
t.Fatalf("Expected first reminder to be sent, got %v", err1)
|
t.Fatalf("Expected first reminder to be sent, got %v", err1)
|
||||||
}
|
}
|
||||||
|
|
@ -314,7 +314,7 @@ func TestSendQuoteReminderEmail_MultipleTypes(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second reminder type
|
// 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 {
|
if err2 != nil {
|
||||||
t.Fatalf("Expected second reminder to be sent, got %v", err2)
|
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}
|
h := &QuotesHandler{queries: mq, emailService: me}
|
||||||
|
|
||||||
// Send reminder for quote 123
|
// 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 {
|
if err1 != nil {
|
||||||
t.Fatalf("Expected reminder for quote 123 to be sent, got %v", err1)
|
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
|
// 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 {
|
if err2 != nil {
|
||||||
t.Fatalf("Expected reminder for quote 456 to be sent, got %v", err2)
|
t.Fatalf("Expected reminder for quote 456 to be sent, got %v", err2)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ latest_quote_reminder AS (
|
||||||
SELECT
|
SELECT
|
||||||
d.id AS document_id,
|
d.id AS document_id,
|
||||||
u.username,
|
u.username,
|
||||||
|
u.email as user_email,
|
||||||
e.id AS enquiry_id,
|
e.id AS enquiry_id,
|
||||||
e.title as enquiry_ref,
|
e.title as enquiry_ref,
|
||||||
uu.first_name as customer_name,
|
uu.first_name as customer_name,
|
||||||
|
|
@ -78,6 +79,7 @@ latest_quote_reminder AS (
|
||||||
SELECT
|
SELECT
|
||||||
d.id AS document_id,
|
d.id AS document_id,
|
||||||
u.username,
|
u.username,
|
||||||
|
u.email as user_email,
|
||||||
e.id AS enquiry_id,
|
e.id AS enquiry_id,
|
||||||
e.title as enquiry_ref,
|
e.title as enquiry_ref,
|
||||||
uu.first_name as customer_name,
|
uu.first_name as customer_name,
|
||||||
|
|
@ -130,6 +132,7 @@ latest_quote_reminder AS (
|
||||||
SELECT
|
SELECT
|
||||||
d.id AS document_id,
|
d.id AS document_id,
|
||||||
u.username,
|
u.username,
|
||||||
|
u.email as user_email,
|
||||||
e.id AS enquiry_id,
|
e.id AS enquiry_id,
|
||||||
e.title as enquiry_ref,
|
e.title as enquiry_ref,
|
||||||
uu.first_name as customer_name,
|
uu.first_name as customer_name,
|
||||||
|
|
@ -182,6 +185,7 @@ latest_quote_reminder AS (
|
||||||
SELECT
|
SELECT
|
||||||
d.id AS document_id,
|
d.id AS document_id,
|
||||||
u.username,
|
u.username,
|
||||||
|
u.email as user_email,
|
||||||
e.id AS enquiry_id,
|
e.id AS enquiry_id,
|
||||||
e.title as enquiry_ref,
|
e.title as enquiry_ref,
|
||||||
uu.first_name as customer_name,
|
uu.first_name as customer_name,
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,7 @@
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<section class="section flex-1">
|
<section class="section flex-1">
|
||||||
<div class="container p-4">
|
<div class="p-4">
|
||||||
{{block "content" .}}{{end}}
|
{{block "content" .}}{{end}}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<h1 class="text-3xl font-bold mb-4 text-gray-800">Quotes Expiring</h1>
|
<h1 class="text-3xl font-bold mb-4 text-gray-800">Quotes Expiring</h1>
|
||||||
<div class="pl-4">
|
<div class="px-8">
|
||||||
<!-- Expiring Soon Section -->
|
<!-- Expiring Soon Section -->
|
||||||
<h2 class="text-xl font-semibold mt-6 mb-2">Expiring Soon</h2>
|
<h2 class="text-xl font-semibold mt-6 mb-2">Expiring Soon</h2>
|
||||||
<table class="min-w-full border text-center align-middle mt-1 ml-4">
|
<table class="w-full border text-center align-middle mt-1">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Quote</th>
|
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Quote</th>
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Expires</th>
|
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Expires</th>
|
||||||
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Reminder</th>
|
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Reminder</th>
|
||||||
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Reminder Sent</th>
|
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Reminder Sent</th>
|
||||||
|
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
@ -41,15 +42,48 @@
|
||||||
<td class="px-4 py-2 border align-middle">
|
<td class="px-4 py-2 border align-middle">
|
||||||
{{if .LatestReminderSent}}<span class="localdatetime">{{.LatestReminderSent}}</span>{{else}}-{{end}}
|
{{if .LatestReminderSent}}<span class="localdatetime">{{.LatestReminderSent}}</span>{{else}}-{{end}}
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-4 py-2 border align-middle">
|
||||||
|
<form method="POST" action="/go/quotes/send-reminder" style="display:inline;">
|
||||||
|
<input type="hidden" name="quote_id" value="{{.ID}}">
|
||||||
|
<input type="hidden" name="customer_email" value="{{.CustomerEmail}}">
|
||||||
|
<input type="hidden" name="user_email" value="{{.UserEmail}}">
|
||||||
|
<input type="hidden" name="enquiry_ref" value="{{.EnquiryRef}}">
|
||||||
|
<input type="hidden" name="customer_name" value="{{.CustomerName}}">
|
||||||
|
<input type="hidden" name="date_issued" value="{{.DateIssued}}">
|
||||||
|
<input type="hidden" name="valid_until" value="{{.ValidUntil}}">
|
||||||
|
<div class="inline-flex rounded-md shadow-sm" role="group">
|
||||||
|
{{if eq .LatestReminderType "No Reminder"}}
|
||||||
|
<button type="button" onclick="showConfirmModal(this, 1, '{{.EnquiryRef}}', '{{.CustomerName}}', 'First Reminder')" class="w-44 px-3 py-1.5 text-xs font-medium text-white bg-cmcblue rounded-l-md hover:bg-cmcblue/90 focus:z-10">Send First Reminder</button>
|
||||||
|
<button type="button" onclick="toggleDropdown(this)" class="px-2 py-1.5 text-xs font-medium text-white bg-cmcblue border-l border-cmcblue/80 rounded-r-md hover:bg-cmcblue/90 focus:z-10">▼</button>
|
||||||
|
{{else if eq .LatestReminderType "First Reminder"}}
|
||||||
|
<button type="button" onclick="showConfirmModal(this, 2, '{{.EnquiryRef}}', '{{.CustomerName}}', 'Second Reminder')" class="w-44 px-3 py-1.5 text-xs font-medium text-white bg-cmcblue rounded-l-md hover:bg-cmcblue/90 focus:z-10">Send Second Reminder</button>
|
||||||
|
<button type="button" onclick="toggleDropdown(this)" class="px-2 py-1.5 text-xs font-medium text-white bg-cmcblue border-l border-cmcblue/80 rounded-r-md hover:bg-cmcblue/90 focus:z-10">▼</button>
|
||||||
|
{{else if eq .LatestReminderType "Second Reminder"}}
|
||||||
|
<button type="button" onclick="showConfirmModal(this, 3, '{{.EnquiryRef}}', '{{.CustomerName}}', 'Final Reminder')" class="w-44 px-3 py-1.5 text-xs font-medium text-white bg-cmcblue rounded-l-md hover:bg-cmcblue/90 focus:z-10">Send Final Reminder</button>
|
||||||
|
<button type="button" onclick="toggleDropdown(this)" class="px-2 py-1.5 text-xs font-medium text-white bg-cmcblue border-l border-cmcblue/80 rounded-r-md hover:bg-cmcblue/90 focus:z-10">▼</button>
|
||||||
|
{{else}}
|
||||||
|
<button type="button" onclick="showConfirmModal(this, 3, '{{.EnquiryRef}}', '{{.CustomerName}}', 'Final Reminder')" class="w-44 px-3 py-1.5 text-xs font-medium text-white bg-cmcblue rounded-l-md hover:bg-cmcblue/90 focus:z-10">Send Final Reminder</button>
|
||||||
|
<button type="button" onclick="toggleDropdown(this)" class="px-2 py-1.5 text-xs font-medium text-white bg-cmcblue border-l border-cmcblue/80 rounded-r-md hover:bg-cmcblue/90 focus:z-10">▼</button>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="hidden absolute z-10 mt-1 bg-white divide-y divide-gray-100 rounded-md shadow-lg ring-1 ring-black ring-opacity-5" style="min-width: 150px;">
|
||||||
|
<ul class="py-1 text-xs text-gray-700">
|
||||||
|
<li><button type="button" onclick="showConfirmModal(this, 1, '{{.EnquiryRef}}', '{{.CustomerName}}', 'First Reminder')" class="block w-full text-left px-4 py-2 hover:bg-gray-100">Send First Reminder</button></li>
|
||||||
|
<li><button type="button" onclick="showConfirmModal(this, 2, '{{.EnquiryRef}}', '{{.CustomerName}}', 'Second Reminder')" class="block w-full text-left px-4 py-2 hover:bg-gray-100">Send Second Reminder</button></li>
|
||||||
|
<li><button type="button" onclick="showConfirmModal(this, 3, '{{.EnquiryRef}}', '{{.CustomerName}}', 'Final Reminder')" class="block w-full text-left px-4 py-2 hover:bg-gray-100">Send Final Reminder</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{else}}
|
{{else}}
|
||||||
<tr><td colspan="7" class="px-4 py-2 border text-center align-middle">No quotes expiring soon.</td></tr>
|
<tr><td colspan="8" class="px-4 py-2 border text-center align-middle">No quotes expiring soon.</td></tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<!-- Recently Expired Quotes Section -->
|
<!-- Recently Expired Quotes Section -->
|
||||||
<h2 class="text-xl font-semibold mt-6 mb-2">Recently Expired</h2>
|
<h2 class="text-xl font-semibold mt-6 mb-2">Recently Expired</h2>
|
||||||
<table class="min-w-full border mb-6 text-center align-middle mt-1 ml-4">
|
<table class="w-full border mb-6 text-center align-middle mt-1">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Quote</th>
|
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Quote</th>
|
||||||
|
|
@ -59,6 +93,7 @@
|
||||||
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Expires</th>
|
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Expires</th>
|
||||||
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Reminder</th>
|
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Reminder</th>
|
||||||
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Reminder Sent</th>
|
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Reminder Sent</th>
|
||||||
|
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
@ -87,14 +122,141 @@
|
||||||
<td class="px-4 py-2 border align-middle">
|
<td class="px-4 py-2 border align-middle">
|
||||||
{{if .LatestReminderSent}}<span class="localdatetime">{{.LatestReminderSent}}</span>{{else}}-{{end}}
|
{{if .LatestReminderSent}}<span class="localdatetime">{{.LatestReminderSent}}</span>{{else}}-{{end}}
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-4 py-2 border align-middle">
|
||||||
|
<form method="POST" action="/go/quotes/send-reminder" style="display:inline;">
|
||||||
|
<input type="hidden" name="quote_id" value="{{.ID}}">
|
||||||
|
<input type="hidden" name="customer_email" value="{{.CustomerEmail}}">
|
||||||
|
<input type="hidden" name="user_email" value="{{.UserEmail}}">
|
||||||
|
<input type="hidden" name="enquiry_ref" value="{{.EnquiryRef}}">
|
||||||
|
<input type="hidden" name="customer_name" value="{{.CustomerName}}">
|
||||||
|
<input type="hidden" name="date_issued" value="{{.DateIssued}}">
|
||||||
|
<input type="hidden" name="valid_until" value="{{.ValidUntil}}">
|
||||||
|
<div class="inline-flex rounded-md shadow-sm" role="group">
|
||||||
|
{{if eq .LatestReminderType "No Reminder"}}
|
||||||
|
<button type="button" onclick="showConfirmModal(this, 1, '{{.EnquiryRef}}', '{{.CustomerName}}', 'First Reminder')" class="w-44 px-3 py-1.5 text-xs font-medium text-white bg-cmcblue rounded-l-md hover:bg-cmcblue/90 focus:z-10">Send First Reminder</button>
|
||||||
|
<button type="button" onclick="toggleDropdown(this)" class="px-2 py-1.5 text-xs font-medium text-white bg-cmcblue border-l border-cmcblue/80 rounded-r-md hover:bg-cmcblue/90 focus:z-10">▼</button>
|
||||||
|
{{else if eq .LatestReminderType "First Reminder"}}
|
||||||
|
<button type="button" onclick="showConfirmModal(this, 2, '{{.EnquiryRef}}', '{{.CustomerName}}', 'Second Reminder')" class="w-44 px-3 py-1.5 text-xs font-medium text-white bg-cmcblue rounded-l-md hover:bg-cmcblue/90 focus:z-10">Send Second Reminder</button>
|
||||||
|
<button type="button" onclick="toggleDropdown(this)" class="px-2 py-1.5 text-xs font-medium text-white bg-cmcblue border-l border-cmcblue/80 rounded-r-md hover:bg-cmcblue/90 focus:z-10">▼</button>
|
||||||
|
{{else if eq .LatestReminderType "Second Reminder"}}
|
||||||
|
<button type="button" onclick="showConfirmModal(this, 3, '{{.EnquiryRef}}', '{{.CustomerName}}', 'Final Reminder')" class="w-44 px-3 py-1.5 text-xs font-medium text-white bg-cmcblue rounded-l-md hover:bg-cmcblue/90 focus:z-10">Send Final Reminder</button>
|
||||||
|
<button type="button" onclick="toggleDropdown(this)" class="px-2 py-1.5 text-xs font-medium text-white bg-cmcblue border-l border-cmcblue/80 rounded-r-md hover:bg-cmcblue/90 focus:z-10">▼</button>
|
||||||
|
{{else}}
|
||||||
|
<button type="button" onclick="showConfirmModal(this, 3, '{{.EnquiryRef}}', '{{.CustomerName}}', 'Final Reminder')" class="w-44 px-3 py-1.5 text-xs font-medium text-white bg-cmcblue rounded-l-md hover:bg-cmcblue/90 focus:z-10">Send Final Reminder</button>
|
||||||
|
<button type="button" onclick="toggleDropdown(this)" class="px-2 py-1.5 text-xs font-medium text-white bg-cmcblue border-l border-cmcblue/80 rounded-r-md hover:bg-cmcblue/90 focus:z-10">▼</button>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="hidden absolute z-10 mt-1 bg-white divide-y divide-gray-100 rounded-md shadow-lg ring-1 ring-black ring-opacity-5" style="min-width: 150px;">
|
||||||
|
<ul class="py-1 text-xs text-gray-700">
|
||||||
|
<li><button type="button" onclick="showConfirmModal(this, 1, '{{.EnquiryRef}}', '{{.CustomerName}}', 'First Reminder')" class="block w-full text-left px-4 py-2 hover:bg-gray-100">Send First Reminder</button></li>
|
||||||
|
<li><button type="button" onclick="showConfirmModal(this, 2, '{{.EnquiryRef}}', '{{.CustomerName}}', 'Second Reminder')" class="block w-full text-left px-4 py-2 hover:bg-gray-100">Send Second Reminder</button></li>
|
||||||
|
<li><button type="button" onclick="showConfirmModal(this, 3, '{{.EnquiryRef}}', '{{.CustomerName}}', 'Final Reminder')" class="block w-full text-left px-4 py-2 hover:bg-gray-100">Send Final Reminder</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{else}}
|
{{else}}
|
||||||
<tr><td colspan="7" class="px-4 py-2 border text-center align-middle">No recently expired quotes.</td></tr>
|
<tr><td colspan="8" class="px-4 py-2 border text-center align-middle">No recently expired quotes.</td></tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirmation Modal -->
|
||||||
|
<div id="confirmModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||||
|
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="flex items-center justify-center w-12 h-12 mx-auto bg-cmcblue rounded-full">
|
||||||
|
<i class="fas fa-envelope text-white text-xl"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900 text-center mt-4">Send Email Reminder</h3>
|
||||||
|
<div class="mt-2 px-7 py-3">
|
||||||
|
<p class="text-sm text-gray-600 text-center">
|
||||||
|
Send <strong id="modalReminderType"></strong> for enquiry <strong id="modalEnquiryRef"></strong> to <strong id="modalCustomerName"></strong>?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 px-4 py-3">
|
||||||
|
<button id="modalCancelBtn" class="flex-1 px-4 py-2 bg-gray-200 text-gray-800 text-sm font-medium rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-300">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button id="modalConfirmBtn" class="flex-1 px-4 py-2 bg-cmcblue text-white text-sm font-medium rounded-md hover:bg-cmcblue/90 focus:outline-none focus:ring-2 focus:ring-cmcblue focus:ring-offset-2">
|
||||||
|
Send Reminder
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
let currentForm = null;
|
||||||
|
let currentReminderType = null;
|
||||||
|
|
||||||
|
function showConfirmModal(button, reminderType, enquiryRef, customerName, reminderTypeName) {
|
||||||
|
currentForm = button.closest('form');
|
||||||
|
currentReminderType = reminderType;
|
||||||
|
|
||||||
|
document.getElementById('modalReminderType').textContent = reminderTypeName;
|
||||||
|
document.getElementById('modalEnquiryRef').textContent = enquiryRef;
|
||||||
|
document.getElementById('modalCustomerName').textContent = customerName;
|
||||||
|
document.getElementById('confirmModal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideConfirmModal() {
|
||||||
|
document.getElementById('confirmModal').classList.add('hidden');
|
||||||
|
currentForm = null;
|
||||||
|
currentReminderType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('modalCancelBtn').addEventListener('click', hideConfirmModal);
|
||||||
|
|
||||||
|
document.getElementById('modalConfirmBtn').addEventListener('click', function() {
|
||||||
|
if (currentForm && currentReminderType) {
|
||||||
|
// Create a hidden input for reminder_type and submit the form
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'hidden';
|
||||||
|
input.name = 'reminder_type';
|
||||||
|
input.value = currentReminderType;
|
||||||
|
currentForm.appendChild(input);
|
||||||
|
currentForm.submit();
|
||||||
|
}
|
||||||
|
hideConfirmModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal when clicking outside
|
||||||
|
document.getElementById('confirmModal').addEventListener('click', function(e) {
|
||||||
|
if (e.target === this) {
|
||||||
|
hideConfirmModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleDropdown(button) {
|
||||||
|
const dropdown = button.parentElement.nextElementSibling;
|
||||||
|
const allDropdowns = document.querySelectorAll('form > div.absolute');
|
||||||
|
|
||||||
|
// Close all other dropdowns
|
||||||
|
allDropdowns.forEach(d => {
|
||||||
|
if (d !== dropdown) {
|
||||||
|
d.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle current dropdown
|
||||||
|
dropdown.classList.toggle('hidden');
|
||||||
|
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
if (!dropdown.classList.contains('hidden')) {
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', function closeDropdown(e) {
|
||||||
|
if (!dropdown.contains(e.target) && !button.contains(e.target)) {
|
||||||
|
dropdown.classList.add('hidden');
|
||||||
|
document.removeEventListener('click', closeDropdown);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Convert .localdate to browser local date, .localdatetime to browser local date+time (no offset)
|
// Convert .localdate to browser local date, .localdatetime to browser local date+time (no offset)
|
||||||
function formatLocalDate(isoString) {
|
function formatLocalDate(isoString) {
|
||||||
if (!isoString) return '';
|
if (!isoString) return '';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue