Adding ability to disable automatic reminders
This commit is contained in:
parent
57047e3b32
commit
bbdd035d04
|
|
@ -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
|
||||
Revision int32 `json:"revision"`
|
||||
// estimated delivery time for quote
|
||||
DeliveryTime string `json:"delivery_time"`
|
||||
DeliveryTimeFrame string `json:"delivery_time_frame"`
|
||||
PaymentTerms string `json:"payment_terms"`
|
||||
DaysValid int32 `json:"days_valid"`
|
||||
DateIssued time.Time `json:"date_issued"`
|
||||
ValidUntil time.Time `json:"valid_until"`
|
||||
DeliveryPoint string `json:"delivery_point"`
|
||||
ExchangeRate string `json:"exchange_rate"`
|
||||
CustomsDuty string `json:"customs_duty"`
|
||||
DocumentID int32 `json:"document_id"`
|
||||
CommercialComments sql.NullString `json:"commercial_comments"`
|
||||
DeliveryTime string `json:"delivery_time"`
|
||||
DeliveryTimeFrame string `json:"delivery_time_frame"`
|
||||
PaymentTerms string `json:"payment_terms"`
|
||||
DaysValid int32 `json:"days_valid"`
|
||||
DateIssued time.Time `json:"date_issued"`
|
||||
ValidUntil time.Time `json:"valid_until"`
|
||||
RemindersDisabled sql.NullBool `json:"reminders_disabled"`
|
||||
RemindersDisabledAt sql.NullTime `json:"reminders_disabled_at"`
|
||||
RemindersDisabledBy sql.NullString `json:"reminders_disabled_by"`
|
||||
DeliveryPoint string `json:"delivery_point"`
|
||||
ExchangeRate string `json:"exchange_rate"`
|
||||
CustomsDuty string `json:"customs_duty"`
|
||||
DocumentID int32 `json:"document_id"`
|
||||
CommercialComments sql.NullString `json:"commercial_comments"`
|
||||
}
|
||||
|
||||
type QuoteReminder struct {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ type Querier interface {
|
|||
DeletePurchaseOrder(ctx context.Context, id int32) error
|
||||
DeleteState(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)
|
||||
GetAllCountries(ctx context.Context) ([]Country, error)
|
||||
GetAllPrinciples(ctx context.Context) ([]Principle, error)
|
||||
|
|
@ -76,6 +77,7 @@ type Querier interface {
|
|||
GetPurchaseOrderRevisions(ctx context.Context, parentPurchaseOrderID int32) ([]PurchaseOrder, error)
|
||||
GetPurchaseOrdersByPrinciple(ctx context.Context, arg GetPurchaseOrdersByPrincipleParams) ([]PurchaseOrder, 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)
|
||||
GetRecentlyExpiredQuotes(ctx context.Context, dateSUB interface{}) ([]GetRecentlyExpiredQuotesRow, error)
|
||||
GetRecentlyExpiredQuotesOnDay(ctx context.Context, dateSUB interface{}) ([]GetRecentlyExpiredQuotesOnDayRow, error)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,23 @@ import (
|
|||
"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
|
||||
WITH ranked_reminders AS (
|
||||
SELECT
|
||||
|
|
@ -61,6 +78,7 @@ WHERE
|
|||
q.valid_until >= CURRENT_DATE
|
||||
AND q.valid_until <= DATE_ADD(CURRENT_DATE, INTERVAL ? DAY)
|
||||
AND e.status_id = 5
|
||||
AND (q.reminders_disabled IS NULL OR q.reminders_disabled = FALSE)
|
||||
|
||||
ORDER BY q.valid_until
|
||||
`
|
||||
|
|
@ -164,6 +182,7 @@ WHERE
|
|||
q.valid_until >= CURRENT_DATE
|
||||
AND q.valid_until = DATE_ADD(CURRENT_DATE, INTERVAL ? DAY)
|
||||
AND e.status_id = 5
|
||||
AND (q.reminders_disabled IS NULL OR q.reminders_disabled = FALSE)
|
||||
|
||||
ORDER BY q.valid_until
|
||||
`
|
||||
|
|
@ -258,6 +277,25 @@ func (q *Queries) GetQuoteRemindersByType(ctx context.Context, arg GetQuoteRemin
|
|||
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
|
||||
WITH ranked_reminders AS (
|
||||
SELECT
|
||||
|
|
@ -308,6 +346,7 @@ WHERE
|
|||
q.valid_until < CURRENT_DATE
|
||||
AND valid_until >= DATE_SUB(CURRENT_DATE, INTERVAL ? DAY)
|
||||
AND e.status_id = 5
|
||||
AND (q.reminders_disabled IS NULL OR q.reminders_disabled = FALSE)
|
||||
|
||||
ORDER BY q.valid_until DESC
|
||||
`
|
||||
|
|
@ -411,6 +450,7 @@ WHERE
|
|||
q.valid_until < CURRENT_DATE
|
||||
AND valid_until = DATE_SUB(CURRENT_DATE, INTERVAL ? DAY)
|
||||
AND e.status_id = 5
|
||||
AND (q.reminders_disabled IS NULL OR q.reminders_disabled = FALSE)
|
||||
|
||||
ORDER BY q.valid_until DESC
|
||||
`
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
// Convert interface{} attachments to []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 {
|
||||
fmt.Printf("DEBUG: Converted to Attachment type: %s -> %s\n", a.Filename, a.FilePath)
|
||||
typedAttachments = append(typedAttachments, a)
|
||||
} 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})
|
||||
} 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"}
|
||||
bccs = append(defaultBccs, bccs...)
|
||||
|
||||
|
|
|
|||
22
go/sql/migrations/002_add_reminders_disabled_to_quotes.sql
Normal file
22
go/sql/migrations/002_add_reminders_disabled_to_quotes.sql
Normal 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;
|
||||
|
|
@ -48,6 +48,7 @@ WHERE
|
|||
q.valid_until >= CURRENT_DATE
|
||||
AND q.valid_until <= DATE_ADD(CURRENT_DATE, INTERVAL ? DAY)
|
||||
AND e.status_id = 5
|
||||
AND (q.reminders_disabled IS NULL OR q.reminders_disabled = FALSE)
|
||||
|
||||
ORDER BY q.valid_until;
|
||||
|
||||
|
|
@ -101,6 +102,7 @@ WHERE
|
|||
q.valid_until >= CURRENT_DATE
|
||||
AND q.valid_until = DATE_ADD(CURRENT_DATE, INTERVAL ? DAY)
|
||||
AND e.status_id = 5
|
||||
AND (q.reminders_disabled IS NULL OR q.reminders_disabled = FALSE)
|
||||
|
||||
ORDER BY q.valid_until;
|
||||
|
||||
|
|
@ -154,6 +156,7 @@ WHERE
|
|||
q.valid_until < CURRENT_DATE
|
||||
AND valid_until >= DATE_SUB(CURRENT_DATE, INTERVAL ? DAY)
|
||||
AND e.status_id = 5
|
||||
AND (q.reminders_disabled IS NULL OR q.reminders_disabled = FALSE)
|
||||
|
||||
ORDER BY q.valid_until DESC;
|
||||
|
||||
|
|
@ -207,6 +210,7 @@ WHERE
|
|||
q.valid_until < CURRENT_DATE
|
||||
AND valid_until = DATE_SUB(CURRENT_DATE, INTERVAL ? DAY)
|
||||
AND e.status_id = 5
|
||||
AND (q.reminders_disabled IS NULL OR q.reminders_disabled = FALSE)
|
||||
|
||||
ORDER BY q.valid_until DESC;
|
||||
|
||||
|
|
@ -218,4 +222,16 @@ ORDER BY date_sent;
|
|||
|
||||
-- name: InsertQuoteReminder :execresult
|
||||
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 = ?;
|
||||
|
|
@ -13,6 +13,9 @@ CREATE TABLE IF NOT EXISTS `quotes` (
|
|||
`days_valid` int(3) NOT NULL,
|
||||
`date_issued` 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,
|
||||
`exchange_rate` varchar(255) NOT NULL,
|
||||
`customs_duty` varchar(255) NOT NULL,
|
||||
|
|
|
|||
|
|
@ -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, 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 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>
|
||||
</div>
|
||||
</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, 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 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>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -188,9 +192,35 @@
|
|||
</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>
|
||||
let currentForm = null;
|
||||
let currentReminderType = null;
|
||||
let currentQuoteID = null;
|
||||
|
||||
function showConfirmModal(button, reminderType, enquiryRef, customerName, reminderTypeName) {
|
||||
currentForm = button.closest('form');
|
||||
|
|
@ -208,6 +238,20 @@ function hideConfirmModal() {
|
|||
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('modalConfirmBtn').addEventListener('click', function() {
|
||||
|
|
@ -223,6 +267,27 @@ document.getElementById('modalConfirmBtn').addEventListener('click', function()
|
|||
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
|
||||
document.getElementById('confirmModal').addEventListener('click', function(e) {
|
||||
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) {
|
||||
const dropdown = button.parentElement.nextElementSibling;
|
||||
const allDropdowns = document.querySelectorAll('form > div.absolute');
|
||||
|
|
|
|||
Loading…
Reference in a new issue