diff --git a/go/cmd/server/main.go b/go/cmd/server/main.go
index 5d073ba6..f2238a62 100644
--- a/go/cmd/server/main.go
+++ b/go/cmd/server/main.go
@@ -75,6 +75,7 @@ func main() {
goRouter.HandleFunc("/quotes", quoteHandler.QuotesOutstandingView).Methods("GET")
goRouter.HandleFunc("/quotes/send-reminder", quoteHandler.SendManualReminder).Methods("POST")
goRouter.HandleFunc("/quotes/disable-reminders", quoteHandler.DisableReminders).Methods("POST")
+ goRouter.HandleFunc("/quotes/enable-reminders", quoteHandler.EnableReminders).Methods("POST")
// The following routes are currently disabled:
/*
diff --git a/go/internal/cmc/db/querier.go b/go/internal/cmc/db/querier.go
index 920fae0f..0c143a4e 100644
--- a/go/internal/cmc/db/querier.go
+++ b/go/internal/cmc/db/querier.go
@@ -44,6 +44,7 @@ type Querier interface {
DeleteState(ctx context.Context, id int32) error
DeleteStatus(ctx context.Context, id int32) error
DisableQuoteReminders(ctx context.Context, arg DisableQuoteRemindersParams) (sql.Result, error)
+ EnableQuoteReminders(ctx context.Context, id int32) (sql.Result, error)
GetAddress(ctx context.Context, id int32) (Address, error)
GetAllCountries(ctx context.Context) ([]Country, error)
GetAllPrinciples(ctx context.Context) ([]Principle, error)
diff --git a/go/internal/cmc/db/quotes.sql.go b/go/internal/cmc/db/quotes.sql.go
index d9452db2..fc5d9bb3 100644
--- a/go/internal/cmc/db/quotes.sql.go
+++ b/go/internal/cmc/db/quotes.sql.go
@@ -28,6 +28,18 @@ func (q *Queries) DisableQuoteReminders(ctx context.Context, arg DisableQuoteRem
return q.db.ExecContext(ctx, disableQuoteReminders, arg.RemindersDisabledBy, arg.ID)
}
+const enableQuoteReminders = `-- name: EnableQuoteReminders :execresult
+UPDATE quotes
+SET reminders_disabled = FALSE,
+ reminders_disabled_at = NULL,
+ reminders_disabled_by = NULL
+WHERE id = ?
+`
+
+func (q *Queries) EnableQuoteReminders(ctx context.Context, id int32) (sql.Result, error) {
+ return q.db.ExecContext(ctx, enableQuoteReminders, id)
+}
+
const getExpiringSoonQuotes = `-- name: GetExpiringSoonQuotes :many
WITH ranked_reminders AS (
SELECT
@@ -64,7 +76,8 @@ SELECT
q.date_issued,
q.valid_until,
COALESCE(lqr.reminder_type, 0) AS latest_reminder_type,
- COALESCE(lqr.date_sent, CAST('1970-01-01 00:00:00' AS DATETIME)) AS latest_reminder_sent_time
+ COALESCE(lqr.date_sent, CAST('1970-01-01 00:00:00' AS DATETIME)) AS latest_reminder_sent_time,
+ COALESCE(q.reminders_disabled, FALSE) AS reminders_disabled
FROM quotes q
JOIN documents d ON d.id = q.document_id
@@ -95,6 +108,7 @@ type GetExpiringSoonQuotesRow struct {
ValidUntil time.Time `json:"valid_until"`
LatestReminderType int32 `json:"latest_reminder_type"`
LatestReminderSentTime time.Time `json:"latest_reminder_sent_time"`
+ RemindersDisabled bool `json:"reminders_disabled"`
}
func (q *Queries) GetExpiringSoonQuotes(ctx context.Context, dateADD interface{}) ([]GetExpiringSoonQuotesRow, error) {
@@ -118,6 +132,7 @@ func (q *Queries) GetExpiringSoonQuotes(ctx context.Context, dateADD interface{}
&i.ValidUntil,
&i.LatestReminderType,
&i.LatestReminderSentTime,
+ &i.RemindersDisabled,
); err != nil {
return nil, err
}
diff --git a/go/internal/cmc/handlers/quotes/quotes.go b/go/internal/cmc/handlers/quotes/quotes.go
index d6ee43d2..2a6a54eb 100644
--- a/go/internal/cmc/handlers/quotes/quotes.go
+++ b/go/internal/cmc/handlers/quotes/quotes.go
@@ -3,6 +3,7 @@ package handlers
import (
"context"
"database/sql"
+ "encoding/json"
"fmt"
"io"
"log"
@@ -176,6 +177,7 @@ type QuoteQueries interface {
GetExpiringSoonQuotesOnDay(ctx context.Context, dateADD interface{}) ([]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 {
@@ -681,7 +683,18 @@ func (h *QuotesHandler) SendManualReminder(w http.ResponseWriter, r *http.Reques
}
log.Printf("Manual reminder sent successfully for quote %d (%s) to %s", quoteID, enquiryRef, customerEmail)
- // Redirect back to quotes page
+
+ // 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)
}
@@ -726,7 +739,68 @@ func (h *QuotesHandler) DisableReminders(w http.ResponseWriter, r *http.Request)
}
log.Printf("Reminders disabled for quote %d by %s", quoteID, username)
- // Redirect back to quotes page
+
+ // 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
+ }
+
+ log.Printf("Reminders enabled for quote %d", quoteID)
+
+ // 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)
}
diff --git a/go/sql/queries/quotes.sql b/go/sql/queries/quotes.sql
index 0acba1f1..1a848366 100644
--- a/go/sql/queries/quotes.sql
+++ b/go/sql/queries/quotes.sql
@@ -34,7 +34,8 @@ SELECT
q.date_issued,
q.valid_until,
COALESCE(lqr.reminder_type, 0) AS latest_reminder_type,
- COALESCE(lqr.date_sent, CAST('1970-01-01 00:00:00' AS DATETIME)) AS latest_reminder_sent_time
+ COALESCE(lqr.date_sent, CAST('1970-01-01 00:00:00' AS DATETIME)) AS latest_reminder_sent_time,
+ COALESCE(q.reminders_disabled, FALSE) AS reminders_disabled
FROM quotes q
JOIN documents d ON d.id = q.document_id
@@ -231,6 +232,13 @@ SET reminders_disabled = TRUE,
reminders_disabled_by = ?
WHERE id = ?;
+-- name: EnableQuoteReminders :execresult
+UPDATE quotes
+SET reminders_disabled = FALSE,
+ reminders_disabled_at = NULL,
+ reminders_disabled_by = NULL
+WHERE id = ?;
+
-- name: GetQuoteRemindersDisabled :one
SELECT reminders_disabled, reminders_disabled_at, reminders_disabled_by
FROM quotes
diff --git a/go/templates/quotes/index.html b/go/templates/quotes/index.html
index e005f584..ffee15b2 100644
--- a/go/templates/quotes/index.html
+++ b/go/templates/quotes/index.html
@@ -13,7 +13,18 @@
Expires |
Reminder |
Reminder Sent |
- Actions |
+
+
+ Actions
+
+
+
+ Manually sent reminders will not specify the number of days until or since expiry
+
+
+
+
+ |
@@ -51,30 +62,34 @@
-
- {{if eq .LatestReminderType "No Reminder"}}
-
-
- {{else if eq .LatestReminderType "First Reminder"}}
-
-
- {{else if eq .LatestReminderType "Second Reminder"}}
-
-
- {{else}}
-
-
- {{end}}
-
-
-
-
-
-
-
-
-
-
+ {{if .RemindersDisabled}}
+
+ {{else}}
+
+ {{if eq .LatestReminderType "No Reminder"}}
+
+
+ {{else if eq .LatestReminderType "First Reminder"}}
+
+
+ {{else if eq .LatestReminderType "Second Reminder"}}
+
+
+ {{else}}
+
+
+ {{end}}
+
+
+
+
+
+
+
+
+
+
+ {{end}}
@@ -95,7 +110,18 @@
Expires |
Reminder |
Reminder Sent |
- Actions |
+
+
+ Actions
+
+
+
+ Manually sent reminders will not specify the number of days until or since expiry
+
+
+
+
+ |
@@ -133,30 +159,34 @@
-
- {{if eq .LatestReminderType "No Reminder"}}
-
-
- {{else if eq .LatestReminderType "First Reminder"}}
-
-
- {{else if eq .LatestReminderType "Second Reminder"}}
-
-
- {{else}}
-
-
- {{end}}
-
-
-
-
-
-
-
-
-
-
+ {{if .RemindersDisabled}}
+
+ {{else}}
+
+ {{if eq .LatestReminderType "No Reminder"}}
+
+
+ {{else if eq .LatestReminderType "First Reminder"}}
+
+
+ {{else if eq .LatestReminderType "Second Reminder"}}
+
+
+ {{else}}
+
+
+ {{end}}
+
+
+
+
+
+
+
+
+
+
+ {{end}}
@@ -167,14 +197,43 @@
-
-
+
+
-
-
+
+
+
+
+
Re-enable reminders?
+
+
+
+ This will allow automatic reminders to be sent again for enquiry .
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
Are you sure?
Send for enquiry to ?
@@ -194,22 +253,24 @@
-
+
-
-
+
-
Are you sure?
- This will prevent future automatic reminders from being sent for enquiry .
+ This will prevent future automatic reminders for quote in enquiry .
-
@@ -226,9 +287,44 @@ function showConfirmModal(button, reminderType, enquiryRef, customerName, remind
currentForm = button.closest('form');
currentReminderType = reminderType;
- document.getElementById('modalReminderType').textContent = reminderTypeName;
- document.getElementById('modalEnquiryRef').textContent = enquiryRef;
- document.getElementById('modalCustomerName').textContent = customerName;
+ // Get the current reminder type from the row
+ const row = button.closest('tr');
+ const reminderCell = row.querySelector('td:nth-child(6)');
+ const currentReminderText = reminderCell.textContent.trim();
+
+ // Check if trying to send same or earlier reminder than what's already been sent
+ const currentLevel = currentReminderText.includes('Final Reminder') ? 3 :
+ currentReminderText.includes('Second Reminder') ? 2 :
+ currentReminderText.includes('First Reminder') ? 1 : 0;
+
+ const modalBody = document.querySelector('#confirmModal .px-7.py-3 p');
+
+ if (currentLevel >= reminderType) {
+ // Warning: trying to send same or earlier reminder
+ const currentReminderName = currentLevel === 3 ? 'Final Reminder' :
+ currentLevel === 2 ? 'Second Reminder' : 'First Reminder';
+ modalBody.innerHTML = `
+
+ Send ${reminderTypeName} for enquiry ${enquiryRef} to ${customerName}?
+
+
+
+
+
+ Warning: The customer has already received their ${currentReminderName}.
+
+
+
+ `;
+ } else {
+ // Normal confirmation
+ modalBody.innerHTML = `
+
+ Send ${reminderTypeName} for enquiry ${enquiryRef} to ${customerName}?
+
+ `;
+ }
+
document.getElementById('confirmModal').classList.remove('hidden');
}
@@ -240,6 +336,7 @@ function hideConfirmModal() {
function showDisableModal(quoteID, enquiryRef) {
currentQuoteID = quoteID;
+ document.getElementById('disableModalQuoteID').textContent = quoteID;
document.getElementById('disableModalEnquiryRef').textContent = enquiryRef;
document.getElementById('disableModal').classList.remove('hidden');
@@ -252,38 +349,109 @@ function hideDisableModal() {
currentQuoteID = null;
}
+function showEnableModal(quoteID, enquiryRef) {
+ currentQuoteID = quoteID;
+ document.getElementById('enableModalEnquiryRef').textContent = enquiryRef;
+ document.getElementById('enableModal').classList.remove('hidden');
+}
+
+function hideEnableModal() {
+ document.getElementById('enableModal').classList.add('hidden');
+ currentQuoteID = null;
+}
+
document.getElementById('modalCancelBtn').addEventListener('click', hideConfirmModal);
-document.getElementById('modalConfirmBtn').addEventListener('click', function() {
+document.getElementById('modalConfirmBtn').addEventListener('click', async 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();
+ const formData = new FormData(currentForm);
+ formData.append('reminder_type', currentReminderType);
+
+ try {
+ const response = await fetch('/go/quotes/send-reminder', {
+ method: 'POST',
+ headers: {
+ 'X-Requested-With': 'XMLHttpRequest'
+ },
+ body: formData
+ });
+
+ if (response.ok) {
+ // Update the row to show the reminder was sent
+ const row = currentForm.closest('tr');
+ const reminderCell = row.querySelector('td:nth-child(6)');
+ const reminderSentCell = row.querySelector('td:nth-child(7)');
+
+ // Update reminder type badge
+ const reminderTypeName = currentReminderType === 1 ? 'First Reminder' :
+ currentReminderType === 2 ? 'Second Reminder' : 'Final Reminder';
+ const colorClass = currentReminderType === 1 ? 'bg-blue-100 text-blue-700 border-blue-200' :
+ currentReminderType === 2 ? 'bg-yellow-100 text-yellow-700 border-yellow-200' :
+ 'bg-red-100 text-red-700 border-red-200';
+ reminderCell.innerHTML = `
${reminderTypeName}`;
+
+ // Update reminder sent time to "just now"
+ reminderSentCell.innerHTML = '
just now';
+
+ // Update the button to show next reminder type
+ const buttonGroup = currentForm.querySelector('.inline-flex');
+ const mainButton = buttonGroup.querySelector('button:first-child');
+ if (currentReminderType === 1) {
+ mainButton.textContent = 'Send Second Reminder';
+ mainButton.setAttribute('onclick', `showConfirmModal(this, 2, '${formData.get('enquiry_ref')}', '${formData.get('customer_name')}', 'Second Reminder')`);
+ } else if (currentReminderType === 2) {
+ mainButton.textContent = 'Send Final Reminder';
+ mainButton.setAttribute('onclick', `showConfirmModal(this, 3, '${formData.get('enquiry_ref')}', '${formData.get('customer_name')}', 'Final Reminder')`);
+ }
+ } else {
+ alert('Failed to send reminder. Please try again.');
+ }
+ } catch (error) {
+ alert('Error sending reminder: ' + error.message);
+ }
}
hideConfirmModal();
});
document.getElementById('disableModalCancelBtn').addEventListener('click', hideDisableModal);
-document.getElementById('disableModalConfirmBtn').addEventListener('click', function() {
+document.getElementById('disableModalConfirmBtn').addEventListener('click', async function() {
if (currentQuoteID) {
- // Create a form to POST the disable request
- const form = document.createElement('form');
- form.method = 'POST';
- form.action = '/go/quotes/disable-reminders';
+ const formData = new FormData();
+ formData.append('quote_id', currentQuoteID);
- const input = document.createElement('input');
- input.type = 'hidden';
- input.name = 'quote_id';
- input.value = currentQuoteID;
-
- form.appendChild(input);
- document.body.appendChild(form);
- form.submit();
+ try {
+ const response = await fetch('/go/quotes/disable-reminders', {
+ method: 'POST',
+ headers: {
+ 'X-Requested-With': 'XMLHttpRequest'
+ },
+ body: formData
+ });
+
+ if (response.ok) {
+ // Find and update all rows for this quote
+ const rows = document.querySelectorAll(`tr`);
+ rows.forEach(row => {
+ const form = row.querySelector('form');
+ if (form && form.querySelector(`input[name="quote_id"][value="${currentQuoteID}"]`)) {
+ // Update reminder badge to show "Reminders Disabled"
+ const reminderCell = row.querySelector('td:nth-child(6)');
+ reminderCell.innerHTML = '
Reminders Disabled';
+
+ // Disable the action buttons
+ const buttonGroup = row.querySelector('.inline-flex');
+ if (buttonGroup) {
+ buttonGroup.innerHTML = '
Disabled';
+ }
+ }
+ });
+ } else {
+ alert('Failed to disable reminders. Please try again.');
+ }
+ } catch (error) {
+ alert('Error disabling reminders: ' + error.message);
+ }
}
hideDisableModal();
});
@@ -301,6 +469,95 @@ document.getElementById('disableModal').addEventListener('click', function(e) {
}
});
+document.getElementById('enableModalCancelBtn').addEventListener('click', hideEnableModal);
+
+document.getElementById('enableModalConfirmBtn').addEventListener('click', async function() {
+ if (currentQuoteID) {
+ const formData = new FormData();
+ formData.append('quote_id', currentQuoteID);
+
+ try {
+ const response = await fetch('/go/quotes/enable-reminders', {
+ method: 'POST',
+ headers: {
+ 'X-Requested-With': 'XMLHttpRequest'
+ },
+ body: formData
+ });
+
+ if (response.ok) {
+ // Find and update all rows for this quote
+ const rows = document.querySelectorAll(`tr`);
+ rows.forEach(row => {
+ const form = row.querySelector('form');
+ if (form && form.querySelector(`input[name="quote_id"][value="${currentQuoteID}"]`)) {
+ // Get the latest reminder type to show appropriate button
+ const reminderCell = row.querySelector('td:nth-child(6)');
+ const reminderText = reminderCell.textContent.trim();
+
+ // Update reminder badge back to current state (remove "Disabled")
+ // Keep the existing reminder type badge
+
+ // Re-enable the action buttons
+ const actionsCell = row.querySelector('td:nth-child(8)');
+ const currentReminderLevel = reminderText.includes('Final') ? 3 :
+ reminderText.includes('Second') ? 2 :
+ reminderText.includes('First') ? 1 : 0;
+
+ const enquiryRef = form.querySelector('input[name="enquiry_ref"]').value;
+ const customerName = form.querySelector('input[name="customer_name"]').value;
+
+ let buttonHTML = '';
+ if (currentReminderLevel === 0) {
+ buttonHTML = `
Send First Reminder`;
+ } else if (currentReminderLevel === 1) {
+ buttonHTML = `
Send Second Reminder`;
+ } else {
+ buttonHTML = `
Send Final Reminder`;
+ }
+
+ const dropdownHTML = `
▼`;
+
+ actionsCell.querySelector('form').innerHTML = `
+
+
+
+
+
+
+
+
+ ${buttonHTML}
+ ${dropdownHTML}
+
+
+
+ - Send First Reminder
+ - Send Second Reminder
+ - Send Final Reminder
+
+ - Disable Future Reminders
+
+
+ `;
+ }
+ });
+ } else {
+ alert('Failed to enable reminders. Please try again.');
+ }
+ } catch (error) {
+ alert('Error enabling reminders: ' + error.message);
+ }
+ }
+ hideEnableModal();
+});
+
+document.getElementById('enableModal').addEventListener('click', function(e) {
+ if (e.target === this) {
+ hideEnableModal();
+ }
+});
+
function toggleDropdown(button) {
const dropdown = button.parentElement.nextElementSibling;
const allDropdowns = document.querySelectorAll('form > div.absolute');