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:
@@ -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 {
|
||||
|
||||
@@ -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, "")
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user