Adding ability to disable automatic reminders

This commit is contained in:
Finley Ghosh 2025-12-07 12:44:30 +11:00
parent 57047e3b32
commit bbdd035d04
8 changed files with 177 additions and 13 deletions

View file

@ -342,17 +342,20 @@ type Quote struct {
// limited at 5 digits. Really, you're not going to have more revisions of a single quote than that // limited at 5 digits. Really, you're not going to have more revisions of a single quote than that
Revision int32 `json:"revision"` Revision int32 `json:"revision"`
// estimated delivery time for quote // estimated delivery time for quote
DeliveryTime string `json:"delivery_time"` DeliveryTime string `json:"delivery_time"`
DeliveryTimeFrame string `json:"delivery_time_frame"` DeliveryTimeFrame string `json:"delivery_time_frame"`
PaymentTerms string `json:"payment_terms"` PaymentTerms string `json:"payment_terms"`
DaysValid int32 `json:"days_valid"` DaysValid int32 `json:"days_valid"`
DateIssued time.Time `json:"date_issued"` DateIssued time.Time `json:"date_issued"`
ValidUntil time.Time `json:"valid_until"` ValidUntil time.Time `json:"valid_until"`
DeliveryPoint string `json:"delivery_point"` RemindersDisabled sql.NullBool `json:"reminders_disabled"`
ExchangeRate string `json:"exchange_rate"` RemindersDisabledAt sql.NullTime `json:"reminders_disabled_at"`
CustomsDuty string `json:"customs_duty"` RemindersDisabledBy sql.NullString `json:"reminders_disabled_by"`
DocumentID int32 `json:"document_id"` DeliveryPoint string `json:"delivery_point"`
CommercialComments sql.NullString `json:"commercial_comments"` ExchangeRate string `json:"exchange_rate"`
CustomsDuty string `json:"customs_duty"`
DocumentID int32 `json:"document_id"`
CommercialComments sql.NullString `json:"commercial_comments"`
} }
type QuoteReminder struct { type QuoteReminder struct {

View file

@ -43,6 +43,7 @@ type Querier interface {
DeletePurchaseOrder(ctx context.Context, id int32) error DeletePurchaseOrder(ctx context.Context, id int32) error
DeleteState(ctx context.Context, id int32) error DeleteState(ctx context.Context, id int32) error
DeleteStatus(ctx context.Context, id int32) error DeleteStatus(ctx context.Context, id int32) error
DisableQuoteReminders(ctx context.Context, arg DisableQuoteRemindersParams) (sql.Result, error)
GetAddress(ctx context.Context, id int32) (Address, error) GetAddress(ctx context.Context, id int32) (Address, error)
GetAllCountries(ctx context.Context) ([]Country, error) GetAllCountries(ctx context.Context) ([]Country, error)
GetAllPrinciples(ctx context.Context) ([]Principle, error) GetAllPrinciples(ctx context.Context) ([]Principle, error)
@ -76,6 +77,7 @@ type Querier interface {
GetPurchaseOrderRevisions(ctx context.Context, parentPurchaseOrderID int32) ([]PurchaseOrder, error) GetPurchaseOrderRevisions(ctx context.Context, parentPurchaseOrderID int32) ([]PurchaseOrder, error)
GetPurchaseOrdersByPrinciple(ctx context.Context, arg GetPurchaseOrdersByPrincipleParams) ([]PurchaseOrder, error) GetPurchaseOrdersByPrinciple(ctx context.Context, arg GetPurchaseOrdersByPrincipleParams) ([]PurchaseOrder, error)
GetQuoteRemindersByType(ctx context.Context, arg GetQuoteRemindersByTypeParams) ([]QuoteReminder, error) GetQuoteRemindersByType(ctx context.Context, arg GetQuoteRemindersByTypeParams) ([]QuoteReminder, error)
GetQuoteRemindersDisabled(ctx context.Context, id int32) (GetQuoteRemindersDisabledRow, error)
GetRecentDocuments(ctx context.Context, limit int32) ([]GetRecentDocumentsRow, error) GetRecentDocuments(ctx context.Context, limit int32) ([]GetRecentDocumentsRow, error)
GetRecentlyExpiredQuotes(ctx context.Context, dateSUB interface{}) ([]GetRecentlyExpiredQuotesRow, error) GetRecentlyExpiredQuotes(ctx context.Context, dateSUB interface{}) ([]GetRecentlyExpiredQuotesRow, error)
GetRecentlyExpiredQuotesOnDay(ctx context.Context, dateSUB interface{}) ([]GetRecentlyExpiredQuotesOnDayRow, error) GetRecentlyExpiredQuotesOnDay(ctx context.Context, dateSUB interface{}) ([]GetRecentlyExpiredQuotesOnDayRow, error)

View file

@ -11,6 +11,23 @@ import (
"time" "time"
) )
const disableQuoteReminders = `-- name: DisableQuoteReminders :execresult
UPDATE quotes
SET reminders_disabled = TRUE,
reminders_disabled_at = NOW(),
reminders_disabled_by = ?
WHERE id = ?
`
type DisableQuoteRemindersParams struct {
RemindersDisabledBy sql.NullString `json:"reminders_disabled_by"`
ID int32 `json:"id"`
}
func (q *Queries) DisableQuoteReminders(ctx context.Context, arg DisableQuoteRemindersParams) (sql.Result, error) {
return q.db.ExecContext(ctx, disableQuoteReminders, arg.RemindersDisabledBy, arg.ID)
}
const getExpiringSoonQuotes = `-- name: GetExpiringSoonQuotes :many const getExpiringSoonQuotes = `-- name: GetExpiringSoonQuotes :many
WITH ranked_reminders AS ( WITH ranked_reminders AS (
SELECT SELECT
@ -61,6 +78,7 @@ WHERE
q.valid_until >= CURRENT_DATE q.valid_until >= CURRENT_DATE
AND q.valid_until <= DATE_ADD(CURRENT_DATE, INTERVAL ? DAY) AND q.valid_until <= DATE_ADD(CURRENT_DATE, INTERVAL ? DAY)
AND e.status_id = 5 AND e.status_id = 5
AND (q.reminders_disabled IS NULL OR q.reminders_disabled = FALSE)
ORDER BY q.valid_until ORDER BY q.valid_until
` `
@ -164,6 +182,7 @@ WHERE
q.valid_until >= CURRENT_DATE q.valid_until >= CURRENT_DATE
AND q.valid_until = DATE_ADD(CURRENT_DATE, INTERVAL ? DAY) AND q.valid_until = DATE_ADD(CURRENT_DATE, INTERVAL ? DAY)
AND e.status_id = 5 AND e.status_id = 5
AND (q.reminders_disabled IS NULL OR q.reminders_disabled = FALSE)
ORDER BY q.valid_until ORDER BY q.valid_until
` `
@ -258,6 +277,25 @@ func (q *Queries) GetQuoteRemindersByType(ctx context.Context, arg GetQuoteRemin
return items, nil return items, nil
} }
const getQuoteRemindersDisabled = `-- name: GetQuoteRemindersDisabled :one
SELECT reminders_disabled, reminders_disabled_at, reminders_disabled_by
FROM quotes
WHERE id = ?
`
type GetQuoteRemindersDisabledRow struct {
RemindersDisabled sql.NullBool `json:"reminders_disabled"`
RemindersDisabledAt sql.NullTime `json:"reminders_disabled_at"`
RemindersDisabledBy sql.NullString `json:"reminders_disabled_by"`
}
func (q *Queries) GetQuoteRemindersDisabled(ctx context.Context, id int32) (GetQuoteRemindersDisabledRow, error) {
row := q.db.QueryRowContext(ctx, getQuoteRemindersDisabled, id)
var i GetQuoteRemindersDisabledRow
err := row.Scan(&i.RemindersDisabled, &i.RemindersDisabledAt, &i.RemindersDisabledBy)
return i, err
}
const getRecentlyExpiredQuotes = `-- name: GetRecentlyExpiredQuotes :many const getRecentlyExpiredQuotes = `-- name: GetRecentlyExpiredQuotes :many
WITH ranked_reminders AS ( WITH ranked_reminders AS (
SELECT SELECT
@ -308,6 +346,7 @@ WHERE
q.valid_until < CURRENT_DATE q.valid_until < CURRENT_DATE
AND valid_until >= DATE_SUB(CURRENT_DATE, INTERVAL ? DAY) AND valid_until >= DATE_SUB(CURRENT_DATE, INTERVAL ? DAY)
AND e.status_id = 5 AND e.status_id = 5
AND (q.reminders_disabled IS NULL OR q.reminders_disabled = FALSE)
ORDER BY q.valid_until DESC ORDER BY q.valid_until DESC
` `
@ -411,6 +450,7 @@ WHERE
q.valid_until < CURRENT_DATE q.valid_until < CURRENT_DATE
AND valid_until = DATE_SUB(CURRENT_DATE, INTERVAL ? DAY) AND valid_until = DATE_SUB(CURRENT_DATE, INTERVAL ? DAY)
AND e.status_id = 5 AND e.status_id = 5
AND (q.reminders_disabled IS NULL OR q.reminders_disabled = FALSE)
ORDER BY q.valid_until DESC ORDER BY q.valid_until DESC
` `

View file

@ -65,13 +65,20 @@ func (es *EmailService) SendTemplateEmail(to string, subject string, templateNam
func (es *EmailService) SendTemplateEmailWithAttachments(to string, subject string, templateName string, data interface{}, ccs []string, bccs []string, attachments []interface{}) error { func (es *EmailService) SendTemplateEmailWithAttachments(to string, subject string, templateName string, data interface{}, ccs []string, bccs []string, attachments []interface{}) error {
// Convert interface{} attachments to []Attachment // Convert interface{} attachments to []Attachment
var typedAttachments []Attachment var typedAttachments []Attachment
for _, att := range attachments { fmt.Printf("DEBUG: Received %d attachments to convert\n", len(attachments))
for i, att := range attachments {
fmt.Printf("DEBUG: Attachment %d type: %T\n", i, att)
if a, ok := att.(Attachment); ok { if a, ok := att.(Attachment); ok {
fmt.Printf("DEBUG: Converted to Attachment type: %s -> %s\n", a.Filename, a.FilePath)
typedAttachments = append(typedAttachments, a) typedAttachments = append(typedAttachments, a)
} else if a, ok := att.(struct{ Filename, FilePath string }); ok { } else if a, ok := att.(struct{ Filename, FilePath string }); ok {
fmt.Printf("DEBUG: Converted from anonymous struct: %s -> %s\n", a.Filename, a.FilePath)
typedAttachments = append(typedAttachments, Attachment{Filename: a.Filename, FilePath: a.FilePath}) typedAttachments = append(typedAttachments, Attachment{Filename: a.Filename, FilePath: a.FilePath})
} else {
fmt.Printf("DEBUG: Failed to convert attachment type %T\n", att)
} }
} }
fmt.Printf("DEBUG: Final typed attachments count: %d\n", len(typedAttachments))
defaultBccs := []string{"carpis@cmctechnologies.com.au"} defaultBccs := []string{"carpis@cmctechnologies.com.au"}
bccs = append(defaultBccs, bccs...) bccs = append(defaultBccs, bccs...)

View file

@ -0,0 +1,22 @@
-- +goose Up
-- Add reminders_disabled field to quotes table
ALTER TABLE quotes
ADD COLUMN reminders_disabled BOOLEAN DEFAULT FALSE AFTER valid_until,
ADD COLUMN reminders_disabled_at DATETIME DEFAULT NULL AFTER reminders_disabled,
ADD COLUMN reminders_disabled_by VARCHAR(100) DEFAULT NULL AFTER reminders_disabled_at;
-- +goose StatementBegin
CREATE INDEX idx_reminders_disabled ON quotes(reminders_disabled);
-- +goose StatementEnd
-- +goose Down
-- Remove index
-- +goose StatementBegin
DROP INDEX idx_reminders_disabled ON quotes;
-- +goose StatementEnd
-- Remove columns from quotes
ALTER TABLE quotes
DROP COLUMN reminders_disabled_by,
DROP COLUMN reminders_disabled_at,
DROP COLUMN reminders_disabled;

View file

@ -48,6 +48,7 @@ WHERE
q.valid_until >= CURRENT_DATE q.valid_until >= CURRENT_DATE
AND q.valid_until <= DATE_ADD(CURRENT_DATE, INTERVAL ? DAY) AND q.valid_until <= DATE_ADD(CURRENT_DATE, INTERVAL ? DAY)
AND e.status_id = 5 AND e.status_id = 5
AND (q.reminders_disabled IS NULL OR q.reminders_disabled = FALSE)
ORDER BY q.valid_until; ORDER BY q.valid_until;
@ -101,6 +102,7 @@ WHERE
q.valid_until >= CURRENT_DATE q.valid_until >= CURRENT_DATE
AND q.valid_until = DATE_ADD(CURRENT_DATE, INTERVAL ? DAY) AND q.valid_until = DATE_ADD(CURRENT_DATE, INTERVAL ? DAY)
AND e.status_id = 5 AND e.status_id = 5
AND (q.reminders_disabled IS NULL OR q.reminders_disabled = FALSE)
ORDER BY q.valid_until; ORDER BY q.valid_until;
@ -154,6 +156,7 @@ WHERE
q.valid_until < CURRENT_DATE q.valid_until < CURRENT_DATE
AND valid_until >= DATE_SUB(CURRENT_DATE, INTERVAL ? DAY) AND valid_until >= DATE_SUB(CURRENT_DATE, INTERVAL ? DAY)
AND e.status_id = 5 AND e.status_id = 5
AND (q.reminders_disabled IS NULL OR q.reminders_disabled = FALSE)
ORDER BY q.valid_until DESC; ORDER BY q.valid_until DESC;
@ -207,6 +210,7 @@ WHERE
q.valid_until < CURRENT_DATE q.valid_until < CURRENT_DATE
AND valid_until = DATE_SUB(CURRENT_DATE, INTERVAL ? DAY) AND valid_until = DATE_SUB(CURRENT_DATE, INTERVAL ? DAY)
AND e.status_id = 5 AND e.status_id = 5
AND (q.reminders_disabled IS NULL OR q.reminders_disabled = FALSE)
ORDER BY q.valid_until DESC; ORDER BY q.valid_until DESC;
@ -218,4 +222,16 @@ ORDER BY date_sent;
-- name: InsertQuoteReminder :execresult -- name: InsertQuoteReminder :execresult
INSERT INTO quote_reminders (quote_id, reminder_type, date_sent, username) INSERT INTO quote_reminders (quote_id, reminder_type, date_sent, username)
VALUES (?, ?, ?, ?); VALUES (?, ?, ?, ?);
-- name: DisableQuoteReminders :execresult
UPDATE quotes
SET reminders_disabled = TRUE,
reminders_disabled_at = NOW(),
reminders_disabled_by = ?
WHERE id = ?;
-- name: GetQuoteRemindersDisabled :one
SELECT reminders_disabled, reminders_disabled_at, reminders_disabled_by
FROM quotes
WHERE id = ?;

View file

@ -13,6 +13,9 @@ CREATE TABLE IF NOT EXISTS `quotes` (
`days_valid` int(3) NOT NULL, `days_valid` int(3) NOT NULL,
`date_issued` date NOT NULL, `date_issued` date NOT NULL,
`valid_until` date NOT NULL, `valid_until` date NOT NULL,
`reminders_disabled` tinyint(1) DEFAULT 0,
`reminders_disabled_at` datetime DEFAULT NULL,
`reminders_disabled_by` varchar(100) DEFAULT NULL,
`delivery_point` varchar(400) NOT NULL, `delivery_point` varchar(400) NOT NULL,
`exchange_rate` varchar(255) NOT NULL, `exchange_rate` varchar(255) NOT NULL,
`customs_duty` varchar(255) NOT NULL, `customs_duty` varchar(255) NOT NULL,

View file

@ -71,6 +71,8 @@
<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, 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, 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> <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>
<li class="border-t border-gray-200"></li>
<li><button type="button" onclick="showDisableModal('{{.ID}}', '{{.EnquiryRef}}')" class="block w-full text-left px-4 py-2 hover:bg-gray-100 text-red-600">Disable Future Reminders</button></li>
</ul> </ul>
</div> </div>
</form> </form>
@ -151,6 +153,8 @@
<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, 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, 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> <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>
<li class="border-t border-gray-200"></li>
<li><button type="button" onclick="showDisableModal('{{.ID}}', '{{.EnquiryRef}}')" class="block w-full text-left px-4 py-2 hover:bg-gray-100 text-red-600">Disable Future Reminders</button></li>
</ul> </ul>
</div> </div>
</form> </form>
@ -188,9 +192,35 @@
</div> </div>
</div> </div>
<!-- Disable Reminders Modal -->
<div id="disableModal" 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-red-600 rounded-full">
<i class="fas fa-ban text-white text-xl"></i>
</div>
<h3 class="text-lg leading-6 font-large font-bold text-gray-900 text-center mt-4">Are you sure?</h3>
<div class="px-7 py-3">
<p class="text-sm text-gray-600 text-center">
This will prevent future automatic reminders from being sent for enquiry <strong id="disableModalEnquiryRef"></strong>.
</p>
</div>
<div class="flex gap-3 px-4 py-3">
<button id="disableModalCancelBtn" 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="disableModalConfirmBtn" class="flex-1 px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-600 focus:ring-offset-2">
Disable Reminders
</button>
</div>
</div>
</div>
</div>
<script> <script>
let currentForm = null; let currentForm = null;
let currentReminderType = null; let currentReminderType = null;
let currentQuoteID = null;
function showConfirmModal(button, reminderType, enquiryRef, customerName, reminderTypeName) { function showConfirmModal(button, reminderType, enquiryRef, customerName, reminderTypeName) {
currentForm = button.closest('form'); currentForm = button.closest('form');
@ -208,6 +238,20 @@ function hideConfirmModal() {
currentReminderType = null; currentReminderType = null;
} }
function showDisableModal(quoteID, enquiryRef) {
currentQuoteID = quoteID;
document.getElementById('disableModalEnquiryRef').textContent = enquiryRef;
document.getElementById('disableModal').classList.remove('hidden');
// Close any open dropdowns
document.querySelectorAll('form > div.absolute').forEach(d => d.classList.add('hidden'));
}
function hideDisableModal() {
document.getElementById('disableModal').classList.add('hidden');
currentQuoteID = null;
}
document.getElementById('modalCancelBtn').addEventListener('click', hideConfirmModal); document.getElementById('modalCancelBtn').addEventListener('click', hideConfirmModal);
document.getElementById('modalConfirmBtn').addEventListener('click', function() { document.getElementById('modalConfirmBtn').addEventListener('click', function() {
@ -223,6 +267,27 @@ document.getElementById('modalConfirmBtn').addEventListener('click', function()
hideConfirmModal(); hideConfirmModal();
}); });
document.getElementById('disableModalCancelBtn').addEventListener('click', hideDisableModal);
document.getElementById('disableModalConfirmBtn').addEventListener('click', 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 input = document.createElement('input');
input.type = 'hidden';
input.name = 'quote_id';
input.value = currentQuoteID;
form.appendChild(input);
document.body.appendChild(form);
form.submit();
}
hideDisableModal();
});
// Close modal when clicking outside // Close modal when clicking outside
document.getElementById('confirmModal').addEventListener('click', function(e) { document.getElementById('confirmModal').addEventListener('click', function(e) {
if (e.target === this) { if (e.target === this) {
@ -230,6 +295,12 @@ document.getElementById('confirmModal').addEventListener('click', function(e) {
} }
}); });
document.getElementById('disableModal').addEventListener('click', function(e) {
if (e.target === this) {
hideDisableModal();
}
});
function toggleDropdown(button) { function toggleDropdown(button) {
const dropdown = button.parentElement.nextElementSibling; const dropdown = button.parentElement.nextElementSibling;
const allDropdowns = document.querySelectorAll('form > div.absolute'); const allDropdowns = document.querySelectorAll('form > div.absolute');