Lock customer page after choice, archive files on revision, add file checkboxes for delivery
This commit is contained in:
parent
51d1b4a4d9
commit
0f6b68cf5d
4 changed files with 144 additions and 48 deletions
59
app.py
59
app.py
|
|
@ -234,11 +234,29 @@ def revise(token):
|
||||||
Customer asked for changes. Store the note and reset status to 'songs_uploaded'
|
Customer asked for changes. Store the note and reset status to 'songs_uploaded'
|
||||||
so the operator sees it in the dashboard queue.
|
so the operator sees it in the dashboard queue.
|
||||||
"""
|
"""
|
||||||
req = get_request_by_token(token)
|
|
||||||
if not req:
|
|
||||||
abort(404)
|
|
||||||
note = request.form.get('revision_note', '').strip()
|
note = request.form.get('revision_note', '').strip()
|
||||||
update_request(req['id'], revision_note=note, status='revisions_requested')
|
# Increment revision counter and archive current files before new versions are uploaded.
|
||||||
|
req = get_request_by_token(token)
|
||||||
|
if req:
|
||||||
|
new_count = (req.get('revision_count') or 0) + 1
|
||||||
|
upload_dir = Path(current_app.config['UPLOAD_FOLDER']) / str(req['id'])
|
||||||
|
if upload_dir.exists():
|
||||||
|
for field, version in (('song_a_path', 'A'), ('song_b_path', 'B')):
|
||||||
|
path = req.get(field)
|
||||||
|
if path and Path(path).exists():
|
||||||
|
old = Path(path)
|
||||||
|
archived = upload_dir / f"Rev{new_count}-{old.name}"
|
||||||
|
try:
|
||||||
|
old.rename(archived)
|
||||||
|
# Keep path pointing to the archived file; operator will upload new files.
|
||||||
|
req[field] = str(archived)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
update_request(req['id'], revision_note=note, status='revisions_requested',
|
||||||
|
song_a_path=req.get('song_a_path'), song_b_path=req.get('song_b_path'),
|
||||||
|
customer_approved='none', revision_count=new_count)
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
# NOTE: No operator email is sent; the dashboard is the single queue.
|
# NOTE: No operator email is sent; the dashboard is the single queue.
|
||||||
flash('Your feedback has been saved. We will regenerate and update you.', 'success')
|
flash('Your feedback has been saved. We will regenerate and update you.', 'success')
|
||||||
|
|
@ -323,6 +341,16 @@ def admin_request(rid):
|
||||||
def basename(path):
|
def basename(path):
|
||||||
return Path(path).name if path else ''
|
return Path(path).name if path else ''
|
||||||
|
|
||||||
|
# Collect any extra MP3 files in the request folder (archived revisions).
|
||||||
|
upload_dir = Path(current_app.config['UPLOAD_FOLDER']) / str(rid)
|
||||||
|
current_paths = {req['song_a_path'], req['song_b_path']}
|
||||||
|
extra_files = []
|
||||||
|
if upload_dir.exists():
|
||||||
|
for f in upload_dir.iterdir():
|
||||||
|
if f.is_file() and f.suffix.lower() == '.mp3' and str(f) not in current_paths:
|
||||||
|
extra_files.append(str(f))
|
||||||
|
extra_files.sort()
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
action = request.form.get('action')
|
action = request.form.get('action')
|
||||||
|
|
||||||
|
|
@ -366,24 +394,25 @@ def admin_request(rid):
|
||||||
|
|
||||||
elif action == 'mark_paid_deliver':
|
elif action == 'mark_paid_deliver':
|
||||||
# Finalize: record Square payment ref, attach approved MP3s, email customer.
|
# Finalize: record Square payment ref, attach approved MP3s, email customer.
|
||||||
if req['customer_approved'] == 'none':
|
|
||||||
flash('Customer has not approved a version yet.', 'error')
|
|
||||||
else:
|
|
||||||
payment_ref = request.form.get('square_payment_ref', '').strip()
|
payment_ref = request.form.get('square_payment_ref', '').strip()
|
||||||
if not payment_ref:
|
if not payment_ref:
|
||||||
flash('Square payment reference is required.', 'error')
|
flash('Square payment reference is required.', 'error')
|
||||||
return redirect(url_for('admin_request', rid=rid))
|
return redirect(url_for('admin_request', rid=rid))
|
||||||
|
|
||||||
|
# Build list of selected files from checkboxes.
|
||||||
|
selected = request.form.getlist('deliver_file')
|
||||||
|
if not selected:
|
||||||
|
flash('Select at least one file to deliver.', 'error')
|
||||||
|
return redirect(url_for('admin_request', rid=rid))
|
||||||
|
|
||||||
attachments = []
|
attachments = []
|
||||||
if req['customer_approved'] in ('a', 'both') and req['song_a_path']:
|
for path in selected:
|
||||||
a_name = Path(req['song_a_path']).name
|
p = Path(path)
|
||||||
attachments.append((req['song_a_path'], a_name))
|
if p.exists():
|
||||||
if req['customer_approved'] in ('b', 'both') and req['song_b_path']:
|
attachments.append((str(p), p.name))
|
||||||
b_name = Path(req['song_b_path']).name
|
|
||||||
attachments.append((req['song_b_path'], b_name))
|
|
||||||
|
|
||||||
player_link = f"{current_app.config['PUBLIC_BASE_URL']}/play/{req['player_token']}"
|
player_link = f"{current_app.config['PUBLIC_BASE_URL']}/play/{req['player_token']}"
|
||||||
body = f"Hi {req['name']},\n\nThanks for your payment! Your approved song is attached to this email.\n\nIf you selected both versions, you'll find two MP3 files.\n\nYou can also keep streaming them here: {player_link}\n\nEnjoy!\n\n— {current_app.config['BOOTH_NAME']}"
|
body = f"Hi {req['name']},\n\nThanks for your payment! Your selected song(s) are attached to this email.\n\nYou can also keep streaming them here: {player_link}\n\nEnjoy!\n\n— {current_app.config['BOOTH_NAME']}"
|
||||||
try:
|
try:
|
||||||
send_email(req['email'], 'Your theme song files are here!', body, attachments=attachments)
|
send_email(req['email'], 'Your theme song files are here!', body, attachments=attachments)
|
||||||
update_request(rid, square_payment_ref=payment_ref, delivery_sent_at=now_utc(), status='delivered')
|
update_request(rid, square_payment_ref=payment_ref, delivery_sent_at=now_utc(), status='delivered')
|
||||||
|
|
@ -393,7 +422,7 @@ def admin_request(rid):
|
||||||
|
|
||||||
return redirect(url_for('admin_request', rid=rid))
|
return redirect(url_for('admin_request', rid=rid))
|
||||||
|
|
||||||
return render_template('admin/request.html', req=req, statuses=STATUS_LABELS, file_exists=file_exists, basename=basename)
|
return render_template('admin/request.html', req=req, statuses=STATUS_LABELS, file_exists=file_exists, basename=basename, extra_files=extra_files)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/admin/request/<int:rid>/delete', methods=['POST'])
|
@app.route('/admin/request/<int:rid>/delete', methods=['POST'])
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ CREATE TABLE IF NOT EXISTS requests (
|
||||||
square_payment_ref TEXT,
|
square_payment_ref TEXT,
|
||||||
admin_alert_email TEXT,
|
admin_alert_email TEXT,
|
||||||
player_token TEXT NOT NULL UNIQUE,
|
player_token TEXT NOT NULL UNIQUE,
|
||||||
|
revision_count INTEGER DEFAULT 0,
|
||||||
revision_note TEXT
|
revision_note TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,26 @@
|
||||||
margin-top:0;
|
margin-top:0;
|
||||||
color:#f87171;
|
color:#f87171;
|
||||||
}
|
}
|
||||||
|
.file-select{
|
||||||
|
background:#111827;
|
||||||
|
padding:.6rem;
|
||||||
|
border-radius:.5rem;
|
||||||
|
margin:.3rem 0;
|
||||||
|
}
|
||||||
|
.file-select input{
|
||||||
|
width:auto;
|
||||||
|
margin-right:.5rem;
|
||||||
|
}
|
||||||
|
.file-select label{
|
||||||
|
display:inline;
|
||||||
|
margin:0;
|
||||||
|
font-weight:400;
|
||||||
|
}
|
||||||
|
.old-rev{
|
||||||
|
font-size:.85rem;
|
||||||
|
color:#9ca3af;
|
||||||
|
margin-left:1.8rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -125,7 +145,7 @@
|
||||||
{% if req.revision_note %}
|
{% if req.revision_note %}
|
||||||
<!-- Revisions requested by the customer -->
|
<!-- Revisions requested by the customer -->
|
||||||
<div class="revision-note">
|
<div class="revision-note">
|
||||||
<h3>📝 Revisions Requested</h3>
|
<h3>📝 Revisions Requested{% if req.revision_count %}<span style="float:right">Revision #{{ req.revision_count }}</span>{% endif %}</h3>
|
||||||
<p>{{ req.revision_note }}</p>
|
<p>{{ req.revision_note }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -238,10 +258,36 @@
|
||||||
</p>
|
</p>
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<input type="hidden" name="action" value="mark_paid_deliver">
|
<input type="hidden" name="action" value="mark_paid_deliver">
|
||||||
|
|
||||||
|
<p style="color:#93c5fd;font-weight:600">Select files to deliver:</p>
|
||||||
|
|
||||||
|
{% set all_files = [] %}
|
||||||
|
{% if req.song_a_path and file_exists(req.song_a_path) %}
|
||||||
|
{% set _ = all_files.append(req.song_a_path) %}
|
||||||
|
{% endif %}
|
||||||
|
{% if req.song_b_path and file_exists(req.song_b_path) %}
|
||||||
|
{% set _ = all_files.append(req.song_b_path) %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- List all MP3 files in the request's upload folder (includes revisions) -->
|
||||||
|
{% set files_to_show = all_files + extra_files %}
|
||||||
|
{% for fpath in files_to_show %}
|
||||||
|
<div class="file-select">
|
||||||
|
<input type="checkbox" id="file_{{ loop.index }}" name="deliver_file" value="{{ fpath }}"
|
||||||
|
{% if fpath in all_files %}checked{% endif %}>
|
||||||
|
<label for="file_{{ loop.index }}">{{ basename(fpath) }}</label>
|
||||||
|
{% if not (fpath == req.song_a_path or fpath == req.song_b_path) %}
|
||||||
|
<div class="old-rev">archived / revision file</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p>No files available. Upload songs first.</p>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
<label for="square_payment_ref">Square Payment Reference</label>
|
<label for="square_payment_ref">Square Payment Reference</label>
|
||||||
<input type="text" id="square_payment_ref" name="square_payment_ref" placeholder="e.g. sq0idp-... or receipt number">
|
<input type="text" id="square_payment_ref" name="square_payment_ref" placeholder="e.g. sq0idp-... or receipt number">
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="submit" class="success" {% if req.customer_approved == 'none' %}disabled{% endif %}>Mark Paid & Deliver</button>
|
<button type="submit" class="success">Mark Paid & Deliver</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@
|
||||||
/*
|
/*
|
||||||
Private customer player page.
|
Private customer player page.
|
||||||
Shows two audio players for Version A and Version B,
|
Shows two audio players for Version A and Version B,
|
||||||
plus approval buttons and a revision note form.
|
plus approval buttons or a revision note form.
|
||||||
|
After the customer makes a choice, the controls are hidden
|
||||||
|
and a confirmation/waiting message is shown instead.
|
||||||
*/
|
*/
|
||||||
body{
|
body{
|
||||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||||
|
|
@ -45,6 +47,7 @@
|
||||||
font-weight:700;
|
font-weight:700;
|
||||||
cursor:pointer;
|
cursor:pointer;
|
||||||
}
|
}
|
||||||
|
button:disabled{background:#374151;color:#9ca3af;cursor:not-allowed;}
|
||||||
button.selected{background:#10b981;}
|
button.selected{background:#10b981;}
|
||||||
button.both{background:#8b5cf6;}
|
button.both{background:#8b5cf6;}
|
||||||
button.revision{background:#f59e0b;color:#000;}
|
button.revision{background:#f59e0b;color:#000;}
|
||||||
|
|
@ -66,6 +69,15 @@
|
||||||
margin-top:1rem;
|
margin-top:1rem;
|
||||||
}
|
}
|
||||||
.status.waiting{background:#3f3f46;}
|
.status.waiting{background:#3f3f46;}
|
||||||
|
.locked{
|
||||||
|
margin-top:1rem;
|
||||||
|
padding:1rem;
|
||||||
|
background:#111827;
|
||||||
|
border-radius:.5rem;
|
||||||
|
border:1px solid #374151;
|
||||||
|
}
|
||||||
|
.locked h2{margin-top:0;color:#fbbf24;}
|
||||||
|
.locked p{margin:.3rem 0;}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -85,14 +97,36 @@
|
||||||
<audio controls src="{{ url_for('audio', token=req.player_token, version='b') }}"></audio>
|
<audio controls src="{{ url_for('audio', token=req.player_token, version='b') }}"></audio>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if req.status in ['songs_uploaded','revisions_requested','awaiting_payment','paid','delivered'] %}
|
{% if req.status == 'revisions_requested' %}
|
||||||
|
<!-- Customer asked for changes; lock the page and tell them to wait -->
|
||||||
|
<div class="locked waiting">
|
||||||
|
<h2>📝 Revision Requested</h2>
|
||||||
|
<p>You asked for changes. We will generate a new version and update this page.</p>
|
||||||
|
<p><strong>Your note:</strong> {{ req.revision_note }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% elif req.status in ['awaiting_payment','paid','delivered'] %}
|
||||||
|
<!-- Customer already picked a version; show their choice and payment instruction -->
|
||||||
|
<div class="locked">
|
||||||
|
<h2>✅ Choice Received</h2>
|
||||||
|
<p>You selected:</span> <strong>{% if req.customer_approved == 'both' %}Both Versions{% else %}Version {{ req.customer_approved.upper() }}{% endif %}</strong></p>
|
||||||
|
{% if req.status == 'awaiting_payment' %}
|
||||||
|
<p>Please return to the booth to finalize payment and collect your files.</p>
|
||||||
|
{% elif req.status == 'paid' %}
|
||||||
|
<p>Payment recorded. Your files are being prepared.</p>
|
||||||
|
{% elif req.status == 'delivered' %}
|
||||||
|
<p>Delivered! Check your email for the MP3 attachment(s).</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
<!-- Approval form: customer picks A, B, or both -->
|
<!-- Approval form: customer picks A, B, or both -->
|
||||||
<form method="POST" action="{{ url_for('approve', token=req.player_token) }}">
|
<form method="POST" action="{{ url_for('approve', token=req.player_token) }}">
|
||||||
<input type="hidden" name="choice" id="choice">
|
<input type="hidden" name="choice" id="choice">
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="submit" class="{% if req.customer_approved == 'a' %}selected{% endif %}" onclick="document.getElementById('choice').value='a'">I want Version A</button>
|
<button type="submit" onclick="document.getElementById('choice').value='a'">I want Version A</button>
|
||||||
<button type="submit" class="{% if req.customer_approved == 'b' %}selected{% endif %}" onclick="document.getElementById('choice').value='b'">I want Version B</button>
|
<button type="submit" onclick="document.getElementById('choice').value='b'">I want Version B</button>
|
||||||
<button type="submit" class="both {% if req.customer_approved == 'both' %}selected{% endif %}" onclick="document.getElementById('choice').value='both'">I want both</button>
|
<button type="submit" class="both" onclick="document.getElementById('choice').value='both'">I want both</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
@ -106,20 +140,6 @@
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if req.status == 'awaiting_payment' %}
|
|
||||||
<!-- Shown after customer approves a version -->
|
|
||||||
<div class="status waiting">
|
|
||||||
<strong>Thanks for choosing {{ req.customer_approved.upper() }}!</strong> Please head to the booth to finalize payment and collect your files.
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if req.status == 'delivered' %}
|
|
||||||
<!-- Shown after operator marks paid and delivers -->
|
|
||||||
<div class="status">
|
|
||||||
<strong>Delivered! ✅</strong> Check your email for the MP3 attachment(s).
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Reference in a new issue