admin-insights-pages: dashboard customers+revenue (orders.amount), users->tags relation, pause customer (gates alerts), About/What-is-this/Contact pages
This commit is contained in:
@@ -14,6 +14,7 @@ type Order struct {
|
||||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
Amount pgtype.Numeric `json:"amount"`
|
||||
}
|
||||
|
||||
type Product struct {
|
||||
@@ -74,4 +75,5 @@ type User struct {
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
RememberToken pgtype.Text `json:"remember_token"`
|
||||
Paused bool `json:"paused"`
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ func (q *Queries) CountTagsByOwner(ctx context.Context, ownerID pgtype.Int8) (in
|
||||
const createUser = `-- name: CreateUser :one
|
||||
INSERT INTO users (email, password_hash, name, phone)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, email, password_hash, name, phone, created_at, is_admin, remember_token
|
||||
RETURNING id, email, password_hash, name, phone, created_at, is_admin, remember_token, paused
|
||||
`
|
||||
|
||||
type CreateUserParams struct {
|
||||
@@ -166,6 +166,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
|
||||
&i.CreatedAt,
|
||||
&i.IsAdmin,
|
||||
&i.RememberToken,
|
||||
&i.Paused,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -221,7 +222,7 @@ func (q *Queries) GetLatestScanByTag(ctx context.Context, tagID int64) (Scan, er
|
||||
}
|
||||
|
||||
const getOrderByID = `-- name: GetOrderByID :one
|
||||
SELECT id, account_id, status, created_at, updated_at FROM orders WHERE id = $1
|
||||
SELECT id, account_id, status, created_at, updated_at, amount FROM orders WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetOrderByID(ctx context.Context, id int64) (Order, error) {
|
||||
@@ -233,6 +234,7 @@ func (q *Queries) GetOrderByID(ctx context.Context, id int64) (Order, error) {
|
||||
&i.Status,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Amount,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -361,7 +363,7 @@ func (q *Queries) GetTagByID(ctx context.Context, id int64) (Tag, error) {
|
||||
}
|
||||
|
||||
const getUserByEmail = `-- name: GetUserByEmail :one
|
||||
SELECT id, email, password_hash, name, phone, created_at, is_admin, remember_token FROM users WHERE email = $1
|
||||
SELECT id, email, password_hash, name, phone, created_at, is_admin, remember_token, paused FROM users WHERE email = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) {
|
||||
@@ -376,12 +378,13 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error
|
||||
&i.CreatedAt,
|
||||
&i.IsAdmin,
|
||||
&i.RememberToken,
|
||||
&i.Paused,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByID = `-- name: GetUserByID :one
|
||||
SELECT id, email, password_hash, name, phone, created_at, is_admin, remember_token FROM users WHERE id = $1
|
||||
SELECT id, email, password_hash, name, phone, created_at, is_admin, remember_token, paused FROM users WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
||||
@@ -396,6 +399,7 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
||||
&i.CreatedAt,
|
||||
&i.IsAdmin,
|
||||
&i.RememberToken,
|
||||
&i.Paused,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -52,6 +52,9 @@ func LoadTemplates() (Templates, error) {
|
||||
base := dir + "/base.html"
|
||||
pages := map[string][]string{
|
||||
"index": {dir + "/index.html"},
|
||||
"about": {dir + "/about.html"},
|
||||
"what": {dir + "/what.html"},
|
||||
"contact": {dir + "/contact.html"},
|
||||
"register": {dir + "/register.html"},
|
||||
"login": {dir + "/login.html"},
|
||||
"account": {dir + "/account.html", dir + "/account-panel.html", dir + "/tag-list.html", dir + "/tag-edit-inline.html"},
|
||||
|
||||
18
frontend/internal/handlers/pages.go
Normal file
18
frontend/internal/handlers/pages.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package handlers
|
||||
|
||||
import "net/http"
|
||||
|
||||
// About renders the About page.
|
||||
func (a *App) About(w http.ResponseWriter, r *http.Request) {
|
||||
a.render(w, r, "about", "About", nil, "")
|
||||
}
|
||||
|
||||
// WhatIsThis renders the "what is this?" explainer page.
|
||||
func (a *App) WhatIsThis(w http.ResponseWriter, r *http.Request) {
|
||||
a.render(w, r, "what", "What is this?", nil, "")
|
||||
}
|
||||
|
||||
// Contact renders the contact page.
|
||||
func (a *App) Contact(w http.ResponseWriter, r *http.Request) {
|
||||
a.render(w, r, "contact", "Contact", nil, "")
|
||||
}
|
||||
@@ -98,6 +98,13 @@ func (a *App) shouldAlert(ctx context.Context, tag db.Tag, scan db.Scan, hasLoc
|
||||
return false
|
||||
}
|
||||
|
||||
// Customer pause (admin kill-switch): paused owner -> record only.
|
||||
if tag.OwnerID.Valid {
|
||||
if owner, err := a.Queries.GetUserByID(ctx, tag.OwnerID.Int64); err == nil && owner.Paused {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 24 h per-device block: same fingerprint seen recently (excluding this scan) => record only.
|
||||
if fp := scan.Fingerprint.String; fp != "" {
|
||||
if _, err := a.Queries.GetRecentScanByFingerprint(ctx, db.GetRecentScanByFingerprintParams{Fingerprint: pgtype.Text{String: fp, Valid: true}, ID: scan.ID}); err == nil {
|
||||
@@ -279,6 +286,12 @@ func (a *App) FinderContact(w http.ResponseWriter, r *http.Request) {
|
||||
// Should we notify? Skip when the same phone was alerted within the window,
|
||||
// or when this device (fingerprint) has already alerted in the last 24 h.
|
||||
notify := tag.SmsEnabled && tag.Phone.Valid && a.Sender != nil
|
||||
// Customer pause: paused owner -> store only.
|
||||
if notify && tag.OwnerID.Valid {
|
||||
if owner, err := a.Queries.GetUserByID(r.Context(), tag.OwnerID.Int64); err == nil && owner.Paused {
|
||||
notify = false
|
||||
}
|
||||
}
|
||||
// Metering: metered tags must have credits remaining.
|
||||
if notify && tag.SmsAllocated > 0 && tag.SmsUsed >= tag.SmsAllocated {
|
||||
notify = false
|
||||
|
||||
@@ -65,6 +65,9 @@ func main() {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
||||
mux.HandleFunc("GET /{$}", app.Home)
|
||||
mux.HandleFunc("GET /about", app.About)
|
||||
mux.HandleFunc("GET /what-is-this", app.WhatIsThis)
|
||||
mux.HandleFunc("GET /contact", app.Contact)
|
||||
mux.HandleFunc("/{path...}", http.NotFound)
|
||||
mux.HandleFunc("GET /t/{tag_code}", app.PublicTag)
|
||||
mux.HandleFunc("GET /s/{code}", app.ServeShort)
|
||||
|
||||
21
frontend/templates/about.html
Normal file
21
frontend/templates/about.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{{define "content"}}
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<h1 class="text-3xl font-extrabold tracking-tight">About Where Woof</h1>
|
||||
<p class="mt-4 text-stone-700">
|
||||
Where Woof makes it easy to get lost things — and lost dogs — home. Each Where Woof tag carries a
|
||||
unique code. If your dog, baggage or gear goes missing, a finder scans the tag and sees exactly how
|
||||
to return it: who to call, where to bring it, and how to reach you.
|
||||
</p>
|
||||
<p class="mt-3 text-stone-700">
|
||||
When a finder scans your tag and shares their location, you get an instant SMS with a link to a map
|
||||
showing where it was found. No app required — it just works from any phone.
|
||||
</p>
|
||||
<h2 class="mt-8 text-xl font-bold">How it works</h2>
|
||||
<ul class="mt-3 space-y-2 text-stone-700">
|
||||
<li>🏷️ Put the tag on your dog, luggage, skis — anything you care about.</li>
|
||||
<li>📱 Claim it and set up your return details (description, photo, phone, address).</li>
|
||||
<li>🔍 A finder scans the QR or taps the NFC tag and sees your details.</li>
|
||||
<li>📍 If they share their location, you get an SMS with a map link.</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
11
frontend/templates/contact.html
Normal file
11
frontend/templates/contact.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{{define "content"}}
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<h1 class="text-3xl font-extrabold tracking-tight">Get in touch</h1>
|
||||
<p class="mt-4 text-stone-700">Questions, lost-tag help, or bulk orders? We'd love to hear from you.</p>
|
||||
<div class="mt-6 space-y-3 rounded-xl border border-stone-200 bg-white p-6 text-sm shadow-sm">
|
||||
<p><strong>Email:</strong> <a href="mailto:info@where-woof.com" class="text-amber-600 hover:underline">info@where-woof.com</a></p>
|
||||
<p><strong>Website:</strong> <a href="/" class="text-amber-600 hover:underline">where-woof.com</a></p>
|
||||
<p class="text-stone-500">For bulk tags (parks, trails, campgrounds) mention your use case and volume.</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -17,8 +17,14 @@
|
||||
<div class="text-2xl">🔍</div>
|
||||
<h2 class="mt-2 font-bold">I found something</h2>
|
||||
<p class="mt-1 text-sm text-stone-600">Scan the tag you found — the page shows the owner's return details.</p>
|
||||
<a href="/account" class="mt-4 inline-block rounded bg-amber-500 px-4 py-2 text-sm font-semibold text-stone-900 hover:bg-amber-400">My account</a>
|
||||
<a href="/what-is-this" class="mt-4 inline-block rounded bg-amber-500 px-4 py-2 text-sm font-semibold text-stone-900 hover:bg-amber-400">What is this?</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-10 text-center text-sm text-stone-500">
|
||||
<a href="/about" class="hover:underline">About</a> ·
|
||||
<a href="/what-is-this" class="hover:underline">What is this?</a> ·
|
||||
<a href="/contact" class="hover:underline">Get in touch</a>
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
20
frontend/templates/what.html
Normal file
20
frontend/templates/what.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{{define "content"}}
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<h1 class="text-3xl font-extrabold tracking-tight">What is this?</h1>
|
||||
<p class="mt-4 text-stone-700">
|
||||
You've scanned a Where Woof tag. This page belongs to the item's owner and shows how to return it.
|
||||
</p>
|
||||
<div class="mt-6 rounded-xl border border-stone-200 bg-white p-6 shadow-sm">
|
||||
<h2 class="font-bold">If you found this item</h2>
|
||||
<ul class="mt-3 space-y-2 text-sm text-stone-700">
|
||||
<li>📞 <strong>Call or text the owner</strong> using the details on this page.</li>
|
||||
<li>📍 <strong>Share your location</strong> (the button on the page) — the owner gets an instant map link.</li>
|
||||
<li>💬 <strong>Leave your number</strong> so the owner can contact you.</li>
|
||||
</ul>
|
||||
<p class="mt-4 text-sm text-stone-500">Thank you for helping bring something (or someone) home!</p>
|
||||
</div>
|
||||
<p class="mt-6 text-sm text-stone-500">
|
||||
This tag belongs to a <a href="/about" class="text-amber-600 hover:underline">Where Woof</a> customer.
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user