This repository has been archived on 2026-09-04. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
theme-song-booth/templates/admin/request.html

556 lines
26 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Request #{{ req.id }} — Admin</title>
<style>
/*
Operator detail page for a single request.
Two-column layout:
left -> customer info + internal operator notes
right -> Suno prompt, song upload, customer notification,
payment reference, and file delivery.
*/
*{box-sizing:border-box;}
body{
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
background:#111827;
color:#f3f4f6;
margin:0;
padding:1rem;
line-height:1.5;
}
.container{max-width:1200px;margin:0 auto;}
h1,h2{color:#60a5fa;margin-top:0;}
a{color:#93c5fd;}
.section{
background:#1f2937;
padding:1rem;
border-radius:.5rem;
margin-bottom:1rem;
}
label{display:block;margin-top:.8rem;font-weight:600;}
input,textarea{
width:100%;
padding:.6rem;
border-radius:.5rem;
border:1px solid #374151;
background:#111827;
color:#f3f4f6;
box-sizing:border-box;
font:inherit;
}
textarea{min-height:100px;}
button{
padding:.7rem 1rem;
border:none;
border-radius:.5rem;
background:#3b82f6;
color:#fff;
font-weight:700;
cursor:pointer;
margin-top:.5rem;
}
button.secondary{background:#4b5563;}
button.danger{background:#dc2626;}
button.success{background:#10b981;}
button:disabled{background:#374151;color:#9ca3af;cursor:not-allowed;}
.actions{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem;}
.flash{
padding:.8rem;
background:#064e3b;
border-radius:.5rem;
margin-bottom:1rem;
}
.flash.error{background:#450a0a;}
.status-badge{
display:inline-block;
padding:.25rem .6rem;
border-radius:9999px;
font-size:.8rem;
font-weight:600;
background:#374151;
color:#f3f4f6;
margin-right:.3rem;
}
.status-badge.ok{background:#10b981;color:#000;}
.status-badge.missing{background:#ef4444;color:#000;}
.status-badge.sent{background:#60a5fa;color:#000;}
.copy-hint{font-size:.85rem;color:#9ca3af;margin-top:.3rem;}
.revision-note{
margin-top:1rem;
background:#450a0a;
border:1px solid #7f1d1d;
border-radius:.5rem;
padding:1rem;
}
.revision-note h3{
margin-top:0;
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;
}
.song-row{
display:flex;
align-items:flex-start;
gap:.75rem;
margin:.6rem 0;
padding:.4rem 0;
}
.song-row input[type="checkbox"]{
width:auto;
margin:.2rem 0 0 0;
flex-shrink:0;
}
.song-row label{
display:block;
margin:0;
font-weight:400;
line-height:1.4;
}
.old-rev{
font-size:.85rem;
color:#9ca3af;
margin-left:1.8rem;
}
.approved-box{font-size:1.2rem;font-weight:bold;color:#fbbf24;}
.status-row{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;}
.history-list{
margin:0;
padding:0;
list-style:none;
}
.history-list li{
border-bottom:1px solid #374151;
padding:.6rem 0;
}
.history-list li:last-child{border-bottom:none;}
.history-list .meta{
font-size:.8rem;
color:#9ca3af;
}
.history-list .note{
margin:.3rem 0 0 0;
}
/* Two-column layout */
.two-col{
display:grid;
grid-template-columns:1fr 1fr;
gap:1rem;
}
@media(max-width:900px){
.two-col{grid-template-columns:1fr;}
}
</style>
</head>
<body>
<div class="container">
<p><a href="{{ url_for('admin_dashboard') }}">← Dashboard</a></p>
<h1>Request #{{ req.id }} — {{ req.name }}</h1>
<div class="status-row">
<p style="margin:0"><strong>Status:</strong> <span class="status-badge">{{ statuses[req.status] }}</span></p>
{% if req.status != 'cancelled' %}
<form method="POST" onsubmit="return confirm('Are you sure you want to cancel request #{{ req.id }}?')" style="margin:0">
<input type="hidden" name="action" value="cancel_request">
<button type="submit" class="danger" style="margin:0">Cancel Request</button>
</form>
{% endif %}
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="flash {{ category }}">{{ message }}</div>
{% endfor %}
{% endwith %}
<div class="two-col">
<!-- LEFT COLUMN -->
<div class="left">
<!-- Customer Info -->
<div class="section">
<h2>Customer Info</h2>
<p class="copy-hint">Operators can correct customer details here. Click Save when done.</p>
<form method="POST">
<input type="hidden" name="action" value="update_customer_info">
<label for="customer_email">Email address</label>
<input type="email" id="customer_email" name="email" value="{{ req.email }}" required>
<label for="customer_name">Name / persona</label>
<input type="text" id="customer_name" name="name" value="{{ req.name }}" required>
<label for="customer_pronouns">Pronouns <span style="color:#f87171">*</span></label>
<select id="customer_pronouns" name="pronouns" required>
<option value="" {% if not req.pronouns %}selected{% endif %}>— Select pronouns —</option>
<option value="He/Him/His" {% if req.pronouns == 'He/Him/His' %}selected{% endif %}>He/Him/His</option>
<option value="She/Her/Hers" {% if req.pronouns == 'She/Her/Hers' %}selected{% endif %}>She/Her/Hers</option>
<option value="They/Them/Their" {% if req.pronouns == 'They/Them/Their' %}selected{% endif %}>They/Them/Their</option>
</select>
<label for="customer_hobbies">Hobbies &amp; interests</label>
<textarea id="customer_hobbies" name="hobbies" rows="3">{{ req.hobbies or '' }}</textarea>
<label for="customer_notable_facts">Notable things</label>
<textarea id="customer_notable_facts" name="notable_facts" rows="3">{{ req.notable_facts or '' }}</textarea>
<label for="customer_style_genre_decade">Decade / era <span style="color:#f87171">*</span></label>
<select id="customer_style_genre_decade" name="decade" required>
<option value="" {% if not style_parts.decade %}selected{% endif %}>— Select decade —</option>
{% for d in decades %}
<option value="{{ d }}" {% if style_parts.decade == d %}selected{% endif %}>{{ d }}</option>
{% endfor %}
</select>
<label for="customer_style_genre_basic">Basic style <span style="color:#f87171">*</span></label>
<select id="customer_style_genre_basic" name="basic_style" required>
<option value="" {% if not style_parts.basic_style %}selected{% endif %}>— Select style —</option>
{% for g in genres %}
<option value="{{ g }}" {% if style_parts.basic_style == g %}selected{% endif %}>{{ g }}</option>
{% endfor %}
</select>
<label for="customer_style_genre_additional">Additional style (optional)</label>
<select id="customer_style_genre_additional" name="additional_style">
<option value="" {% if not style_parts.additional_style %}selected{% endif %}>— None —</option>
{% for g in genres %}
<option value="{{ g }}" {% if style_parts.additional_style == g %}selected{% endif %}>{{ g }}</option>
{% endfor %}
</select>
<p style="font-size:.85rem;color:#9ca3af;margin-top:.3rem">Stored style: {{ req.style_genre or '—' }}</p>
<label for="customer_vocal_gender">Preferred singer voice / gender</label>
<select id="customer_vocal_gender" name="vocal_gender">
<option value="" {% if not req.vocal_gender %}selected{% endif %}>No preference</option>
<option value="female" {% if req.vocal_gender == 'female' %}selected{% endif %}>Female</option>
<option value="male" {% if req.vocal_gender == 'male' %}selected{% endif %}>Male</option>
<option value="non-binary" {% if req.vocal_gender == 'non-binary' %}selected{% endif %}>Non-binary / Gender-neutral</option>
<option value="androgynous" {% if req.vocal_gender == 'androgynous' %}selected{% endif %}>Androgynous</option>
<option value="other" {% if req.vocal_gender == 'other' %}selected{% endif %}>Other (mention in Extra Requests)</option>
</select>
<label for="customer_extra_requests">Anything else</label>
<textarea id="customer_extra_requests" name="extra_requests" rows="3" maxlength="2000">{{ req.extra_requests or '' }}</textarea>
<label for="customer_stems_interest" style="display:flex;align-items:center;gap:.5rem;margin-top:1rem;cursor:pointer;">
<input type="checkbox" id="customer_stems_interest" name="stems_interest" value="1" {% if req.stems_interest %}checked{% endif %} style="width:auto;margin:0">
Customer is interested in STEMS / multitrack files
</label>
<div class="actions">
<button type="submit" class="secondary">Save Customer Info</button>
</div>
</form>
<p style="margin-top:1rem"><strong>Request #:</strong> {{ req.id }}</p>
{% if req.revision_note %}
<div class="revision-note">
<h3>📝 Revisions Requested{% if req.revision_count %}<span style="float:right">Revision #{{ req.revision_count }}</span>{% endif %}</h3>
<p>{{ req.revision_note }}</p>
<button type="button" onclick="copyRevisionPromptForHermes()">📋 Copy revision prompt for Hermes</button>
</div>
{% endif %}
</div>
<!-- Revision History -->
<div class="section">
<h2>Revision History</h2>
{% if revision_history %}
<ul class="history-list">
{% for h in revision_history %}
<li>
<div class="meta">Revision #{{ h.revision_count }} — {{ h.created_at }}
{% if h.old_song_a_path or h.old_song_b_path %}
<br>Archived:
{% if h.old_song_a_path %}<br>A: {{ basename(h.old_song_a_path) }}{% endif %}
{% if h.old_song_b_path %}<br>B: {{ basename(h.old_song_b_path) }}{% endif %}
{% endif %}
</div>
<p class="note">{{ h.note or 'No note provided.' }}</p>
</li>
{% endfor %}
</ul>
{% else %}
<p class="copy-hint">No revisions recorded yet.</p>
{% endif %}
</div>
<!-- Operator Notes -->
<div class="section">
<h2>Operator Notes</h2>
<p class="copy-hint">Internal notes for the booth team. Customers cannot see this.</p>
<form method="POST">
<input type="hidden" name="action" value="save_operator_notes">
<label for="operator_notes">Notes</label>
<textarea id="operator_notes" name="operator_notes" rows="4" placeholder="e.g. paid cash, VIP friend, follow up...">{{ req.operator_notes or '' }}</textarea>
<div class="actions">
<button type="submit" class="secondary">Save Notes</button>
</div>
</form>
</div>
<!-- Stems / Extras Link -->
<div class="section">
<h2>Stems / Extras Link</h2>
<p class="copy-hint">Paste a self-hosted file share link (e.g. from Pingvin Share). It will appear on the customer player page once delivered.</p>
<form method="POST">
<input type="hidden" name="action" value="save_stems_link">
<label for="stems_link">Share link</label>
<input type="url" id="stems_link" name="stems_link" value="{{ req.stems_link or '' }}" placeholder="https://files.dionysismedia.ca/share/...">
<div class="actions">
<button type="submit" class="secondary">Save Link</button>
</div>
</form>
</div>
</div>
<!-- RIGHT COLUMN -->
<div class="right">
<!-- Generate Suno Prompt -->
<div class="section">
<h2>1. Generate &amp; Save Suno Prompt</h2>
<button type="button" onclick="copyPromptForHermes()">Copy customer info for Hermes</button>
<p class="copy-hint">Paste Hermes' Title, Style, and Lyrics directly into the fields below, then save.</p>
<form method="POST">
<input type="hidden" name="action" value="save_prompt">
<label for="suno_title">Title</label>
<input type="text" id="suno_title" name="suno_title" value="{{ req.suno_title or '' }}">
<label for="suno_style">Style</label>
<textarea id="suno_style" name="suno_style">{{ req.suno_style or '' }}</textarea>
<label for="suno_lyrics">Lyrics (with metatags)</label>
<textarea id="suno_lyrics" name="suno_lyrics" rows="10">{{ req.suno_lyrics or '' }}</textarea>
<div class="actions">
<button type="button" class="secondary" onclick="copyToClipboard('suno_lyrics', 'Lyrics')">Copy Lyrics</button>
<button type="button" class="secondary" onclick="copyToClipboard('suno_style', 'Style')">Copy Style</button>
<button type="button" class="secondary" onclick="copyToClipboard('suno_title', 'Title')">Copy Title</button>
<button type="submit">Save Prompt</button>
</div>
</form>
</div>
<!-- Upload Songs -->
<div class="section">
<h2>2. Upload Songs</h2>
<form method="POST">
<input type="hidden" name="action" value="delete_songs">
<div class="song-row">
<input type="checkbox" id="delete_a" name="delete_song" value="a"
{% if not (req.song_a_path and file_exists(req.song_a_path)) %}disabled{% endif %}>
<label for="delete_a">Version A:
{% if req.song_a_path and file_exists(req.song_a_path) %}
<span class="status-badge ok">✅ Uploaded</span> {{ basename(req.song_a_path) }}
{% else %}
<span class="status-badge missing">❌ Not uploaded</span>
{% endif %}
</label>
</div>
<div class="song-row">
<input type="checkbox" id="delete_b" name="delete_song" value="b"
{% if not (req.song_b_path and file_exists(req.song_b_path)) %}disabled{% endif %}>
<label for="delete_b">Version B:
{% if req.song_b_path and file_exists(req.song_b_path) %}
<span class="status-badge ok">✅ Uploaded</span> {{ basename(req.song_b_path) }}
{% else %}
<span class="status-badge missing">❌ Not uploaded</span>
{% endif %}
</label>
</div>
<div class="actions">
<button type="submit" class="danger" onclick="return confirm('Delete selected song files? This will reset the request to Prompt Ready.')">Delete Selected Songs</button>
</div>
</form>
<hr style="border-color:#374151;margin:1rem 0">
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="action" value="upload_songs">
<label for="song_a">Version A MP3</label>
<input type="file" id="song_a" name="song_a" accept="audio/mpeg,.mp3">
<label for="song_b">Version B MP3</label>
<input type="file" id="song_b" name="song_b" accept="audio/mpeg,.mp3">
<div class="actions">
<button type="submit">Upload Songs</button>
</div>
</form>
</div>
<!-- Notify Customer -->
<div class="section">
<h2>3. Notify Customer</h2>
<p>
Preview email:
{% if req.preview_sent_at %}
<span class="status-badge sent">✅ Sent</span> {{ req.preview_sent_at }}
{% else %}
<span class="status-badge missing">❌ Not sent yet</span>
{% endif %}
</p>
<p>Both songs must be uploaded first.</p>
<form method="POST">
<input type="hidden" name="action" value="notify_customer">
<button type="submit" {% if not (req.song_a_path and req.song_b_path) %}disabled{% endif %}>Send Preview Link</button>
</form>
{% if req.song_a_path and req.song_b_path %}
<p style="margin-top:.8rem">
Operator preview link:
<a href="{{ url_for('play', token=req.player_token) }}" target="_blank" rel="noopener noreferrer">Open customer player in new tab →</a>
</p>
{% endif %}
</div>
<!-- Payment &amp; Delivery -->
<div class="section">
<h2>4. Payment &amp; Delivery</h2>
<p>
Customer approved:
{% if req.customer_approved == 'none' %}
<span class="approved-box">Nothing yet</span>
{% else %}
<span class="approved-box">{{ (req.customer_approved or 'none').upper() }}</span>
{% endif %}
</p>
<p>
Delivery email:
{% if req.delivery_sent_at %}
<span class="status-badge sent">✅ Sent</span> {{ req.delivery_sent_at }}
{% else %}
<span class="status-badge missing">❌ Not sent yet</span>
{% endif %}
</p>
{% if req.square_payment_ref %}
<p><strong>Square Payment Reference:</strong> {{ req.square_payment_ref }}</p>
{% endif %}
<form method="POST">
<input type="hidden" name="action" value="update_payment_ref">
<label for="square_payment_ref_edit">Square Payment Reference</label>
<input type="text" id="square_payment_ref_edit" name="square_payment_ref" value="{{ req.square_payment_ref or '' }}" placeholder="e.g. sq0idp-... or receipt number">
<div class="actions">
<button type="submit" class="secondary">Update Payment Reference</button>
</div>
</form>
<hr style="border-color:#374151;margin:1rem 0">
<form method="POST">
<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 %}
{% 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 %}
{% if req.customer_approved == 'none' %}disabled{% endif %}>
<label for="file_{{ loop.index }}"{% if req.customer_approved == 'none' %} style="opacity:.6"{% endif %}>{{ 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 (required to deliver)</label>
<input type="text" id="square_payment_ref" name="square_payment_ref" value="{{ req.square_payment_ref or '' }}" placeholder="e.g. sq0idp-... or receipt number"
{% if req.customer_approved == 'none' %}disabled{% endif %}>
<div class="actions">
<button type="submit" class="success" {% if req.customer_approved == 'none' %}disabled{% endif %}>Mark Paid &amp; Deliver</button>
</div>
{% if req.customer_approved == 'none' %}
<p style="margin-top:.5rem;color:#f87171;font-weight:600">⚠️ Customer must approve a version before you can mark paid or deliver.</p>
{% endif %}
</form>
</div>
</div>
</div>
<script>
function copyPromptForHermes() {
const callbackUrl = {{ callback_url | tojson }};
const styleGenre = {{ req.style_genre | tojson }};
const styleParts = styleGenre ? styleGenre.split(', ') : [];
const decade = styleParts[0] || '';
const basic = styleParts[1] || '';
const additional = styleParts.slice(2).join(', ') || '';
const styleSentence = [decade && `${decade}-era`, basic, additional && `with ${additional} influences`].filter(Boolean).join(' ');
const data = {
request_id: {{ req.id | tojson }},
email: {{ req.email | tojson }},
name: {{ req.name | tojson }},
pronouns: {{ req.pronouns | tojson }},
hobbies: {{ req.hobbies | tojson }},
notable_facts: {{ req.notable_facts | tojson }},
style_genre: styleGenre,
vocal_gender: {{ req.vocal_gender | tojson }},
extra_requests: {{ req.extra_requests | tojson }},
stems_interest: {{ (req.stems_interest or 0) | tojson }}
};
const text = `Booth request: ${data.request_id}\\nCustomer email: ${data.email}\\nCallback URL: ${callbackUrl}\\n\\nGenerate a Suno Custom Mode prompt for this customer and POST it back to the Callback URL as JSON.\\n\\nCustomer data:\\nName: ${data.name}\\nPronouns: ${data.pronouns || '-'}\\nHobbies: ${data.hobbies || '-'}\\nNotable facts: ${data.notable_facts || '-'}\\nStyle / genre: ${styleSentence || styleGenre || '-'}\\nPreferred singer voice / gender: ${data.vocal_gender || 'No preference'}\\nExtra requests: ${data.extra_requests || '-'}\\nInterested in STEMS: ${data.stems_interest ? 'Yes' : 'No'}\\n\\nExpected JSON response format:\\n{\\n \\"request_id\\": ${data.request_id},\\n \\"email\\": \\"${data.email}\\",\\n \\"suno_title\\": \\"...\\",\\n \\"suno_style\\": \\"...\\",\\n \\"suno_lyrics\\": \\"...\\"\\n}`;
navigator.clipboard.writeText(text).then(() => alert("Copied! Paste into Hermes. Hermes will POST the generated prompt back to the Callback URL."));
}
function copyToClipboard(elementId, label) {
const el = document.getElementById(elementId);
el.select();
el.setSelectionRange(0, 99999);
navigator.clipboard.writeText(el.value).then(() => alert(label + " copied!"));
}
function copyRevisionPromptForHermes() {
const callbackUrl = {{ callback_url | tojson }};
const currentTitle = {{ req.suno_title | tojson }};
const currentStyle = {{ req.suno_style | tojson }};
const currentLyrics = {{ req.suno_lyrics | tojson }};
const revisionNote = {{ req.revision_note | tojson }};
const revisionCount = {{ req.revision_count | tojson }};
const styleGenre = {{ req.style_genre | tojson }};
const styleParts = styleGenre ? styleGenre.split(', ') : [];
const decade = styleParts[0] || '';
const basic = styleParts[1] || '';
const additional = styleParts.slice(2).join(', ') || '';
const styleSentence = [decade && `${decade}-era`, basic, additional && `with ${additional} influences`].filter(Boolean).join(' ');
const data = {
request_id: {{ req.id | tojson }},
email: {{ req.email | tojson }},
name: {{ req.name | tojson }},
pronouns: {{ req.pronouns | tojson }},
hobbies: {{ req.hobbies | tojson }},
notable_facts: {{ req.notable_facts | tojson }},
style_genre: styleGenre,
vocal_gender: {{ req.vocal_gender | tojson }},
extra_requests: {{ req.extra_requests | tojson }},
stems_interest: {{ (req.stems_interest or 0) | tojson }}
};
const text = `Booth request: ${data.request_id}\\nCustomer email: ${data.email}\\nCallback URL: ${callbackUrl}\\n\\nThis is REVISION #${revisionCount || 1} for this customer.\\n\\nGenerate a NEW Suno Custom Mode prompt that addresses the following revision request, while keeping the same overall theme/persona and matching the customer's original brief as closely as possible.\\n\\nOriginal brief:\\nName: ${data.name}\\nPronouns: ${data.pronouns || '-'}\\nHobbies: ${data.hobbies || '-'}\\nNotable facts: ${data.notable_facts || '-'}\\nStyle / genre: ${styleSentence || styleGenre || '-'}\\nPreferred singer voice / gender: ${data.vocal_gender || 'No preference'}\\nExtra requests: ${data.extra_requests || '-'}\\nInterested in STEMS: ${data.stems_interest ? 'Yes' : 'No'}\\n\\nPreviously generated prompt (do not copy verbatim; adapt and improve):\\nTitle: ${currentTitle || '-'}\\nStyle: ${currentStyle || '-'}\\nLyrics:\\n${currentLyrics || '-'}\\n\\nRevision request from customer:\\n${revisionNote || '-'}\\n\\nPOST the new prompt back to the Callback URL as JSON with this shape:\\n{\\n \\"request_id\\": ${data.request_id},\\n \\"email\\": \\"${data.email}\\",\\n \\"suno_title\\": \\"...\\",\\n \\"suno_style\\": \\"...\\",\\n \\"suno_lyrics\\": \\"...\\"\\n}`;
navigator.clipboard.writeText(text).then(() => alert("Revision prompt copied! Paste into Hermes. Hermes will POST the revised prompt back to the Callback URL."));
}
</script>
</div>
</body>
</html>