feat(qsl): OpsLog QSL received marker + PSE/TNX card stamp + default QSL message
Received flag: new APP_OPSLOG_QSL_RCVD extra, toggled on the QSO edit window
next to QSL Message (immediate targeted write via SetOpsLogQSLReceived so it sets
AND clears reliably), plus a live PSE QSL / TNX indicator and a Recent-QSOs
column mirroring the sent one.
PSE/TNX card stamp: automatic — received → TNX, otherwise PSE QSL — exposed as
the {qso.pse_tnx} token (added to qslVars, so preview and send agree) and placed
in the default QSO-box footer; it can be moved to its own element in the designer.
Default QSL message: new qsl.default_message (QSLEmailTemplates.DefaultMessage),
edited under Settings → E-mail → QSL card e-mail. qslVars falls back to it when
the QSO's own QSLMSG is empty, so a per-QSO message always wins. Single choke
point covers both live preview and send.
This commit is contained in:
+58
-8
@@ -31,13 +31,20 @@ import (
|
||||
const (
|
||||
keyQSLEmailSubject = "qsl.email_subject"
|
||||
keyQSLEmailBody = "qsl.email_body"
|
||||
keyQSLAutoSend = "qsl.auto_send" // "1" → render+send an eQSL on log when an e-mail and default template exist
|
||||
keyQSLAutoSend = "qsl.auto_send" // "1" → render+send an eQSL on log when an e-mail and default template exist
|
||||
keyQSLDefaultMsg = "qsl.default_message" // fallback QSL message printed on the card when the QSO's own QSLMSG is empty
|
||||
)
|
||||
|
||||
// appQSLCardSentField is the ADIF APP_ field stamping when OpsLog e-mailed its
|
||||
// own QSL card. Deliberately NOT eqsl_sent (that's eQSL.cc's, kept independent).
|
||||
const appQSLCardSentField = "APP_OPSLOG_QSL_SENT"
|
||||
|
||||
// appQSLCardRcvdField marks that a QSL was RECEIVED for this QSO (set by the
|
||||
// operator, e.g. when a card arrives by e-mail). It drives the PSE/TNX stamp on
|
||||
// the card: received → "TNX" (thanks for your card), not received → "PSE QSL"
|
||||
// (please send one). Independent of ADIF qsl_rcvd, like the sent field.
|
||||
const appQSLCardRcvdField = "APP_OPSLOG_QSL_RCVD"
|
||||
|
||||
const (
|
||||
defaultQSLEmailSubject = "eQSL — {CALL} de {MYCALL}"
|
||||
defaultQSLEmailBody = "Hi,\n\nThank you for our QSO! Please find attached your eQSL card.\n\n{DATE} · {BAND} · {MODE}\n\n73,\n{MYCALL}"
|
||||
@@ -453,11 +460,37 @@ func (a *App) SendEQSL(qsoID int64, templateID int64, jpegB64 string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// QSLEmailTemplates is the eQSL e-mail subject/body plus the auto-send toggle.
|
||||
// qslDefaultMessage returns the operator's default QSL message (Settings), used
|
||||
// on the card when a QSO has no QSLMSG of its own.
|
||||
func (a *App) qslDefaultMessage() string {
|
||||
if a.settings == nil {
|
||||
return ""
|
||||
}
|
||||
s, _ := a.settings.Get(a.ctx, keyQSLDefaultMsg)
|
||||
return s
|
||||
}
|
||||
|
||||
// SetOpsLogQSLReceived marks (or clears) that a QSL was received for a QSO. This
|
||||
// flips the card's PSE/TNX stamp. Stored as a timestamp in the QSO extras (a
|
||||
// single-key UPDATE, like the sent marker) so it survives concurrent uploads.
|
||||
func (a *App) SetOpsLogQSLReceived(qsoID int64, on bool) error {
|
||||
if a.qso == nil {
|
||||
return fmt.Errorf("db not initialized")
|
||||
}
|
||||
v := ""
|
||||
if on {
|
||||
v = time.Now().UTC().Format(time.RFC3339)
|
||||
}
|
||||
return a.qso.SetExtra(a.ctx, qsoID, appQSLCardRcvdField, v)
|
||||
}
|
||||
|
||||
// QSLEmailTemplates is the eQSL e-mail subject/body, the auto-send toggle, and
|
||||
// the default QSL message printed on the card when a QSO has none of its own.
|
||||
type QSLEmailTemplates struct {
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
AutoSend bool `json:"auto_send"`
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
AutoSend bool `json:"auto_send"`
|
||||
DefaultMessage string `json:"default_message"`
|
||||
}
|
||||
|
||||
// QSLGetEmailTemplates returns the eQSL e-mail templates (with defaults).
|
||||
@@ -466,7 +499,7 @@ func (a *App) QSLGetEmailTemplates() (QSLEmailTemplates, error) {
|
||||
if a.settings == nil {
|
||||
return out, nil
|
||||
}
|
||||
m, err := a.settings.GetMany(a.ctx, keyQSLEmailSubject, keyQSLEmailBody, keyQSLAutoSend)
|
||||
m, err := a.settings.GetMany(a.ctx, keyQSLEmailSubject, keyQSLEmailBody, keyQSLAutoSend, keyQSLDefaultMsg)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
@@ -477,6 +510,7 @@ func (a *App) QSLGetEmailTemplates() (QSLEmailTemplates, error) {
|
||||
out.Body = b
|
||||
}
|
||||
out.AutoSend = m[keyQSLAutoSend] == "1"
|
||||
out.DefaultMessage = m[keyQSLDefaultMsg]
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -495,7 +529,10 @@ func (a *App) QSLSaveEmailTemplates(t QSLEmailTemplates) error {
|
||||
if t.AutoSend {
|
||||
v = "1"
|
||||
}
|
||||
return a.settings.Set(a.ctx, keyQSLAutoSend, v)
|
||||
if err := a.settings.Set(a.ctx, keyQSLAutoSend, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return a.settings.Set(a.ctx, keyQSLDefaultMsg, t.DefaultMessage)
|
||||
}
|
||||
|
||||
// maybeAutoSendEQSL fires an eQSL render+send for a freshly-logged QSO when the
|
||||
@@ -634,6 +671,18 @@ func (a *App) qslVars(q qso.QSO) (map[string]string, qslcard.CountryInfo, error)
|
||||
}
|
||||
return strconv.Itoa(z)
|
||||
}
|
||||
// The QSL message on the card: the QSO's own QSLMSG wins; when it's empty the
|
||||
// operator's default message (Settings) is used instead.
|
||||
msg := q.QSLMsg
|
||||
if strings.TrimSpace(msg) == "" {
|
||||
msg = a.qslDefaultMessage()
|
||||
}
|
||||
// PSE/TNX stamp, chosen automatically by whether a QSL was received for this
|
||||
// QSO (appQSLCardRcvdField): received → thank them, otherwise ask for a card.
|
||||
pseTnx := "PSE QSL"
|
||||
if q.Extras != nil && strings.TrimSpace(q.Extras[appQSLCardRcvdField]) != "" {
|
||||
pseTnx = "TNX"
|
||||
}
|
||||
vars := map[string]string{
|
||||
"profile.callsign": info.Callsign,
|
||||
"profile.operator_name": info.Operator,
|
||||
@@ -650,7 +699,8 @@ func (a *App) qslVars(q qso.QSO) (map[string]string, qslcard.CountryInfo, error)
|
||||
"qso.mode": q.Mode,
|
||||
"qso.submode": q.Submode,
|
||||
"qso.rst_sent": q.RSTSent,
|
||||
"qso.qsl_msg": q.QSLMsg,
|
||||
"qso.qsl_msg": msg,
|
||||
"qso.pse_tnx": pseTnx,
|
||||
"qso.name": q.Name,
|
||||
}
|
||||
vars["qso.my_rig"], vars["qso.my_antenna"] = a.qslRigAntenna(q)
|
||||
|
||||
Reference in New Issue
Block a user