tag-lifecycle: owner close (status closed, gating + page) + move (copy details to new tag, close source) + legacy ?productid= redirect — verified with real tags 6 & 17

This commit is contained in:
2026-08-10 11:36:11 +10:00
parent 77a9f803f2
commit 97cf0974a3
8 changed files with 123 additions and 10 deletions

View File

@@ -103,6 +103,10 @@ func (a *App) shouldAlert(ctx context.Context, tag db.Tag, scan db.Scan, hasLoc
if !tag.Phone.Valid {
return false, ""
}
// Closed or suspended tags never alert (page shows unavailable).
if tag.Status == "closed" || tag.Status == "suspended" {
return false, ""
}
// Customer pause (admin kill-switch): paused owner -> record only.
if tag.OwnerID.Valid {

View File

@@ -24,6 +24,11 @@ type publicData struct {
}
func (a *App) Home(w http.ResponseWriter, r *http.Request) {
// Legacy physical QR URLs: /?x=5&productid=CODE -> /t/CODE
if pid := r.URL.Query().Get("productid"); pid != "" {
http.Redirect(w, r, "/t/"+pid, http.StatusFound)
return
}
a.render(w, r, "index", "Home", nil, "")
}

View File

@@ -231,6 +231,88 @@ func (a *App) UploadPhoto(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `<p class="mt-2 text-xs text-green-700">Photo uploaded ✓ — refresh to see it on the tag page.</p>`)
}
// CloseTag lets an owner close (retire) their tag. Data is kept; the tag page
// shows unavailable and alerts stop (HTMX: returns account-panel).
func (a *App) CloseTag(w http.ResponseWriter, r *http.Request) {
uid, _ := auth.GetUserID(r)
tag, ok := a.loadOwnedTag(w, r, uid)
if !ok {
return
}
if tag.Status == "closed" {
a.renderAccountPanel(w, r, uid, "This tag is already closed.")
return
}
if err := a.Queries.SetTagStatus(r.Context(), db.SetTagStatusParams{ID: tag.ID, Status: "closed"}); err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
a.renderAccountPanel(w, r, uid, "")
}
// MoveTag moves a tag's details to another (unset) tag code, then closes the
// source tag — the broken-tag replacement flow (HTMX: returns account-panel).
func (a *App) MoveTag(w http.ResponseWriter, r *http.Request) {
uid, _ := auth.GetUserID(r)
src, ok := a.loadOwnedTag(w, r, uid)
if !ok {
return
}
targetCode := strings.TrimSpace(r.FormValue("new_tag_code")) // preset IDs are case-sensitive — trim only
if targetCode == "" {
a.renderAccountPanel(w, r, uid, "Enter the new tag's code.")
return
}
if targetCode == src.TagCode {
a.renderAccountPanel(w, r, uid, "The new tag must be different from the current tag.")
return
}
target, err := a.Queries.GetTagByCode(r.Context(), targetCode)
if err != nil {
a.renderAccountPanel(w, r, uid, "That tag code wasn't found. Check the code on the new tag.")
return
}
if target.OwnerID.Valid || target.Status != "unset" {
a.renderAccountPanel(w, r, uid, "That tag is already claimed or not available.")
return
}
// Bind the target to this owner, copy all details, then close the source.
if _, err := a.Queries.BindTag(r.Context(), db.BindTagParams{OwnerID: ownerID(uid), TagCode: targetCode}); err != nil {
a.renderAccountPanel(w, r, uid, "Could not claim the new tag.")
return
}
if _, err := a.Queries.UpdateTagDetails(r.Context(), db.UpdateTagDetailsParams{
ID: target.ID,
ItemType: src.ItemType,
Description: src.Description,
PhotoUrl: src.PhotoUrl,
Phone: src.Phone,
Address: src.Address,
Notes: src.Notes,
SmsEnabled: src.SmsEnabled,
}); err != nil {
a.renderAccountPanel(w, r, uid, "Could not copy the details.")
return
}
if err := a.Queries.SetTagStatus(r.Context(), db.SetTagStatusParams{ID: src.ID, Status: "closed"}); err != nil {
a.renderAccountPanel(w, r, uid, "Details moved, but the old tag couldn't be closed.")
return
}
a.renderAccountPanel(w, r, uid, "")
}
// renderAccountPanel refreshes the My Tags panel (HTMX target) with an optional error.
func (a *App) renderAccountPanel(w http.ResponseWriter, r *http.Request, uid int64, errMsg string) {
tags, err := a.Queries.ListTagsByOwner(r.Context(), ownerID(uid))
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
a.renderPartial(w, "account", "account-panel", accountData{Tags: tags, AddError: errMsg})
}
func writeUploadMsg(w http.ResponseWriter, msg string) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, `<p class="mt-2 text-xs text-red-600">%s</p>`, msg)