feat: add /status order lookup page linked from request form
This commit is contained in:
parent
5e26650b90
commit
2ac99fde8b
6 changed files with 226 additions and 2 deletions
|
|
@ -8,6 +8,7 @@ A Flask web app for a convention booth where visitors request a custom AI-genera
|
|||
|
||||
- **Request form** (`/request`) — visitors enter name, email, hobbies, notable facts, preferred style/genre, vocal gender preference, and extra requests. A branded banner image is shown. The email field is validated to reduce delivery problems. If the operator marks the booth as closed, this page shows a closed banner and message instead.
|
||||
- **Closed page** (`/request`) — when the booth is marked closed from `/admin/settings`, visitors see the closed banner and a friendly "goblin engineers are on a break" message.
|
||||
- **Order status lookup** (`/status`) — customers enter their email to see all their requests, current status, and the private player link once songs are uploaded.
|
||||
- **Confirmation page** (`/thanks/<id>`) — shows the request number after submission.
|
||||
- **FAQ page** (`/faq`) — answers common customer questions.
|
||||
- **Private player page** (`/play/<token>`) — customer receives an email with a unique link. They can stream Version A and Version B, pick one (or both), or request a limited number of revisions.
|
||||
|
|
@ -63,6 +64,7 @@ pending → prompt_ready → songs_uploaded → awaiting_payment → paid → de
|
|||
| `templates/closed.html` | Message shown on `/request` when the booth is marked closed. |
|
||||
| `templates/request.html` | Customer request form. |
|
||||
| `templates/thanks.html` | Post-submission confirmation. |
|
||||
| `templates/status.html` | Customer order status lookup. |
|
||||
| `templates/faq.html` | Customer FAQ page. |
|
||||
| `templates/player.html` | Customer audio player, approval, and revision form. |
|
||||
| `templates/admin/login.html` | Admin login page. |
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ Flask app that lets convention attendees request custom AI-generated theme songs
|
|||
| `templates/admin/dashboard.html` | Queue table + filters + auto-refresh + topbar Reset System button. |
|
||||
| `templates/admin/settings.html` | SMTP config, MP3 metadata defaults, DB backup/restore, health check, reset. |
|
||||
| `templates/faq.html` | Customer FAQ page. |
|
||||
| `templates/status.html` | Customer order status lookup. |
|
||||
| `templates/closed.html` | Message shown on `/request` when the booth is marked closed. |
|
||||
| `docker-compose.yml` | No `env_file`; variables come from Portainer. |
|
||||
|
||||
|
|
|
|||
22
app.py
22
app.py
|
|
@ -54,7 +54,7 @@ from werkzeug.utils import secure_filename
|
|||
# Project imports
|
||||
from config import Config
|
||||
from models import init_db, close_db, create_request, get_request_by_id, get_request_by_token, list_requests, update_request, now_utc, delete_request, reset_all_requests, get_db
|
||||
from models import SCHEMA
|
||||
from models import SCHEMA, get_requests_by_email
|
||||
|
||||
# Audio metadata import
|
||||
from mutagen.mp3 import MP3
|
||||
|
|
@ -392,6 +392,26 @@ def thanks(rid):
|
|||
return render_template('thanks.html', req=req)
|
||||
|
||||
|
||||
@app.route('/status', methods=['GET', 'POST'])
|
||||
def status_lookup():
|
||||
"""
|
||||
Public order status lookup page.
|
||||
Customers enter their email to see all their requests and their current
|
||||
statuses, plus the private player link once songs have been uploaded.
|
||||
"""
|
||||
requests_list = []
|
||||
email = ''
|
||||
searched = False
|
||||
if request.method == 'POST':
|
||||
email = request.form.get('email', '').strip().lower()
|
||||
if not is_valid_email(email):
|
||||
flash('Please enter a valid email address.', 'error')
|
||||
else:
|
||||
requests_list = get_requests_by_email(email)
|
||||
searched = True
|
||||
return render_template('status.html', email=email, requests=requests_list, searched=searched, statuses=STATUS_LABELS)
|
||||
|
||||
|
||||
@app.route('/faq')
|
||||
def faq():
|
||||
"""Customer-facing frequently asked questions page."""
|
||||
|
|
|
|||
11
models.py
11
models.py
|
|
@ -129,6 +129,17 @@ def list_requests(status=None):
|
|||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def get_requests_by_email(email):
|
||||
"""Fetch all requests for a given email address, newest first.
|
||||
Email is compared case-insensitively and stripped of whitespace."""
|
||||
db = get_db()
|
||||
rows = db.execute(
|
||||
"SELECT * FROM requests WHERE LOWER(TRIM(email)) = LOWER(TRIM(?)) ORDER BY created_at DESC",
|
||||
(email,)
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def update_request(request_id, **fields):
|
||||
"""
|
||||
Update arbitrary columns for a request.
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@
|
|||
<div class="card">
|
||||
<h1><span class="sparkle">🎵</span> Get Your Custom Theme Song <span class="sparkle">🎶</span></h1>
|
||||
<p class="subtitle">Tell us about yourself and we'll craft a one-of-a-kind song just for you.</p>
|
||||
<p class="subtitle"><a href="{{ url_for('faq') }}" target="_blank" rel="noopener noreferrer">Questions? Read our FAQ →</a></p>
|
||||
<p class="subtitle"><a href="{{ url_for('faq') }}" target="_blank" rel="noopener noreferrer">Questions? Read our FAQ →</a> · <a href="{{ url_for('status_lookup') }}" target="_blank" rel="noopener noreferrer">Already ordered? Check status →</a></p>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
|
|
|
|||
190
templates/status.html
Normal file
190
templates/status.html
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Check Your Order Status — Trollgorithm Theme Song Booth</title>
|
||||
<style>
|
||||
/*
|
||||
Public order status lookup page.
|
||||
Customers enter their email to see request status and the private player link.
|
||||
*/
|
||||
*{box-sizing:border-box;}
|
||||
body{
|
||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||
background:linear-gradient(135deg,#111827 0%,#1e3a8a 50%,#111827 100%);
|
||||
color:#f3f4f6;
|
||||
margin:0;
|
||||
padding:1rem;
|
||||
line-height:1.5;
|
||||
min-height:100vh;
|
||||
}
|
||||
.container{max-width:680px;margin:0 auto;}
|
||||
.banner{
|
||||
width:100%;
|
||||
border-radius:1rem;
|
||||
box-shadow:0 10px 40px rgba(0,0,0,.4);
|
||||
margin-bottom:1.5rem;
|
||||
}
|
||||
.card{
|
||||
background:rgba(31,41,55,.9);
|
||||
padding:1.5rem;
|
||||
border-radius:1rem;
|
||||
box-shadow:0 10px 25px rgba(0,0,0,.3);
|
||||
border:1px solid rgba(96,165,250,.2);
|
||||
}
|
||||
h1{
|
||||
margin-top:0;
|
||||
color:#60a5fa;
|
||||
text-align:center;
|
||||
font-size:1.7rem;
|
||||
}
|
||||
.subtitle{
|
||||
text-align:center;
|
||||
color:#9ca3af;
|
||||
margin-bottom:1.5rem;
|
||||
}
|
||||
label{
|
||||
display:block;
|
||||
margin-top:1rem;
|
||||
font-weight:600;
|
||||
color:#93c5fd;
|
||||
}
|
||||
input{
|
||||
width:100%;
|
||||
padding:.7rem;
|
||||
border-radius:.5rem;
|
||||
border:1px solid #374151;
|
||||
background:#111827;
|
||||
color:#f3f4f6;
|
||||
font:inherit;
|
||||
margin-top:.25rem;
|
||||
}
|
||||
input:focus{
|
||||
outline:none;
|
||||
border-color:#60a5fa;
|
||||
box-shadow:0 0 0 3px rgba(96,165,250,.2);
|
||||
}
|
||||
button{
|
||||
margin-top:1.5rem;
|
||||
width:100%;
|
||||
padding:1rem;
|
||||
border:none;
|
||||
border-radius:.5rem;
|
||||
background:linear-gradient(90deg,#3b82f6,#8b5cf6);
|
||||
color:#fff;
|
||||
font-weight:700;
|
||||
font-size:1.1rem;
|
||||
cursor:pointer;
|
||||
box-shadow:0 4px 15px rgba(59,130,246,.4);
|
||||
transition:transform .1s,box-shadow .1s;
|
||||
}
|
||||
button:hover{
|
||||
transform:translateY(-2px);
|
||||
box-shadow:0 6px 20px rgba(139,92,246,.5);
|
||||
}
|
||||
.flash{
|
||||
padding:.8rem;
|
||||
background:#064e3b;
|
||||
border-radius:.5rem;
|
||||
margin-bottom:1rem;
|
||||
}
|
||||
.flash.error{background:#450a0a;}
|
||||
.result{
|
||||
margin-top:1.5rem;
|
||||
}
|
||||
.request{
|
||||
background:#111827;
|
||||
border:1px solid #374151;
|
||||
border-radius:.75rem;
|
||||
padding:1rem;
|
||||
margin-bottom:1rem;
|
||||
}
|
||||
.request h3{margin-top:0;color:#93c5fd;}
|
||||
.status{
|
||||
display:inline-block;
|
||||
padding:.25rem .6rem;
|
||||
border-radius:9999px;
|
||||
font-size:.85rem;
|
||||
font-weight:600;
|
||||
background:#374151;
|
||||
color:#f3f4f6;
|
||||
margin:.3rem 0;
|
||||
}
|
||||
.status.pending,.status.prompt_ready{background:#60a5fa;color:#000;}
|
||||
.status.songs_uploaded{background:#a78bfa;color:#000;}
|
||||
.status.awaiting_payment{background:#f59e0b;color:#000;}
|
||||
.status.paid,.status.delivered{background:#10b981;color:#000;}
|
||||
.status.revisions_requested{background:#f87171;color:#000;}
|
||||
.link-button{
|
||||
display:inline-block;
|
||||
margin-top:.5rem;
|
||||
padding:.6rem 1rem;
|
||||
background:#3b82f6;
|
||||
border-radius:.5rem;
|
||||
color:#fff;
|
||||
text-decoration:none;
|
||||
font-weight:600;
|
||||
}
|
||||
.approved{font-weight:600;color:#fbbf24;}
|
||||
.back{
|
||||
display:block;
|
||||
text-align:center;
|
||||
margin-top:1.5rem;
|
||||
color:#93c5fd;
|
||||
text-decoration:none;
|
||||
}
|
||||
.back:hover{text-decoration:underline;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src="{{ url_for('static', filename='Trollgorithm_booth.jpg') }}" alt="Trollgorithm Theme Song Booth" class="banner">
|
||||
|
||||
<div class="card">
|
||||
<h1>🔍 Check Your Order Status</h1>
|
||||
<p class="subtitle">Enter the email address you used when you requested your song.</p>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="flash {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST">
|
||||
<label for="email">Email address</label>
|
||||
<input type="email" id="email" name="email" value="{{ email }}" required>
|
||||
<button type="submit">Look Up My Song</button>
|
||||
</form>
|
||||
|
||||
{% if searched %}
|
||||
<div class="result">
|
||||
{% if requests %}
|
||||
{% for req in requests %}
|
||||
<div class="request">
|
||||
<h3>Request #{{ req.id }} — {{ req.name }}</h3>
|
||||
<p><strong>Status:</strong> <span class="status {{ req.status }}">{{ statuses[req.status] }}</span></p>
|
||||
|
||||
{% if req.customer_approved != 'none' %}
|
||||
<p class="approved">You selected: {% if req.customer_approved == 'both' %}Both Versions{% else %}Version {{ req.customer_approved.upper() }}{% endif %}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if req.song_a_path and req.song_b_path %}
|
||||
<p><strong>Your preview link:</strong></p>
|
||||
<a class="link-button" href="{{ url_for('play', token=req.player_token) }}" target="_blank" rel="noopener noreferrer">🎧 Listen & Pick Your Version</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>No requests found for that email address. Make sure you used the same email you gave us at the booth.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<a class="back" href="{{ url_for('request_form') }}">← Back to the request form</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in a new issue