06/16 Phase 6 (continue)

This commit is contained in:
2026-06-17 17:51:03 -04:00
parent 68a842d6b6
commit d1383a9835
11 changed files with 846 additions and 5 deletions
+4
View File
@@ -5,7 +5,11 @@
<a href="{{ url_for('admin.reports') }}" class="{{ 'on' if request.endpoint == 'admin.reports' else '' }}">{{ _('Reports') }}</a>
<a href="{{ url_for('admin.categories') }}" class="{{ 'on' if request.endpoint in ('admin.categories', 'admin.toggle_category', 'admin.category_schema') else '' }}">{{ _('Categories') }}</a>
<a href="{{ url_for('admin.plans') }}" class="{{ 'on' if request.endpoint in ('admin.plans', 'admin.plan_edit') else '' }}">{{ _('Plans') }}</a>
<a href="{{ url_for('admin.admin_ads') }}" class="{{ 'on' if request.endpoint in ('admin.admin_ads', 'admin.admin_ad_new', 'admin.admin_ad_edit') else '' }}">{{ _('Ads') }}</a>
<a href="{{ url_for('admin.admin_sponsors') }}" class="{{ 'on' if request.endpoint in ('admin.admin_sponsors', 'admin.admin_sponsor_new', 'admin.admin_sponsor_edit') else '' }}">{{ _('Sponsors') }}</a>
<a href="{{ url_for('admin.admin_promoted_keywords') }}" class="{{ 'on' if request.endpoint in ('admin.admin_promoted_keywords', 'admin.admin_promoted_keyword_new') else '' }}">{{ _('Keywords') }}</a>
<a href="{{ url_for('admin.transactions') }}" class="{{ 'on' if request.endpoint == 'admin.transactions' else '' }}">{{ _('Transactions') }}</a>
<a href="{{ url_for('admin.analytics') }}" class="{{ 'on' if request.endpoint == 'admin.analytics' else '' }}">{{ _('Analytics') }}</a>
<a href="{{ url_for('admin.audit_log') }}" class="{{ 'on' if request.endpoint == 'admin.audit_log' else '' }}">{{ _('Audit log') }}</a>
<a href="{{ url_for('admin.settings') }}" class="{{ 'on' if request.endpoint == 'admin.settings' else '' }}">{{ _('Settings') }}</a>
</nav>
+76
View File
@@ -0,0 +1,76 @@
{% extends "base.html" %}
{% block title %}{{ _('Admin · %(t)s', t=_('Edit ad') if ad else _('New ad')) }}{% endblock %}
{% block content %}
{% include "admin/_nav.html" %}
<div class="card">
<h2>{{ _('Edit ad') if ad else _('New ad') }}</h2>
{% if error %}<p class="alert danger">{{ error }}</p>{% endif %}
<form method="post" class="settings-panel">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="field">
<label>{{ _('Advertiser name') }} *</label>
<input class="input" name="advertiser_name" required
value="{{ ad.advertiser_name if ad else '' }}">
</div>
<div class="field">
<label>{{ _('Slot') }} *</label>
<select class="input" name="slot" required>
{% for s in ['header', 'sidebar', 'inline', 'footer'] %}
<option value="{{ s }}" {{ 'selected' if ad and ad.slot == s }}>{{ s }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<label>{{ _('Target URL') }} *</label>
<input class="input" name="target_url" required
value="{{ ad.target_url if ad else '' }}">
</div>
<div class="field">
<label>{{ _('Creative path (relative under media/ads/)') }}</label>
<input class="input" name="creative_path"
value="{{ ad.creative_path or '' if ad else '' }}"
placeholder="{{ _('e.g. banner.jpg — leave blank for text-only ad') }}">
</div>
<div class="field">
<label>{{ _('Alt text') }}</label>
<input class="input" name="alt_text" value="{{ ad.alt_text or '' if ad else '' }}">
</div>
<div class="field">
<label>{{ _('Language targeting (blank = all)') }}</label>
<select class="input" name="lang">
<option value="">{{ _('All') }}</option>
{% for l in ['en', 'vi', 'es'] %}
<option value="{{ l }}" {{ 'selected' if ad and ad.lang == l }}>{{ l }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<label>{{ _('State targeting (2-letter, blank = all)') }}</label>
<input class="input" name="geo_state" maxlength="2"
value="{{ ad.geo_state or '' if ad else '' }}" placeholder="CA">
</div>
<div class="field">
<label>{{ _('Starts at (YYYY-MM-DD)') }}</label>
<input class="input" name="starts_at" type="date"
value="{{ ad.starts_at.strftime('%Y-%m-%d') if ad else '' }}">
</div>
<div class="field">
<label>{{ _('Ends at (YYYY-MM-DD)') }}</label>
<input class="input" name="ends_at" type="date"
value="{{ ad.ends_at.strftime('%Y-%m-%d') if ad else '' }}">
</div>
<div class="check">
<input type="checkbox" name="is_active" id="is_active"
{{ 'checked' if (not ad or ad.is_active) }}>
<label for="is_active">{{ _('Active') }}</label>
</div>
<div class="billing-actions">
<button class="btn" type="submit">{{ _('Save') }}</button>
<a class="btn ghost" href="{{ url_for('admin.admin_ads') }}">{{ _('Cancel') }}</a>
</div>
</form>
</div>
{% endblock %}
+64
View File
@@ -0,0 +1,64 @@
{% extends "base.html" %}
{% block title %}{{ _('Admin · Ads') }}{% endblock %}
{% block content %}
{% include "admin/_nav.html" %}
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center">
<h2>{{ _('Ads') }}</h2>
<a class="btn" href="{{ url_for('admin.admin_ad_new') }}">{{ _('+ New ad') }}</a>
</div>
{% if not ad_list %}<p class="muted">{{ _('No ads yet.') }}</p>{% endif %}
<table class="list">
<thead><tr>
<th>{{ _('Advertiser') }}</th>
<th>{{ _('Slot') }}</th>
<th>{{ _('Targeting') }}</th>
<th>{{ _('Schedule') }}</th>
<th>{{ _('Imp.') }}</th>
<th>{{ _('Clicks') }}</th>
<th>{{ _('CTR') }}</th>
<th>{{ _('Status') }}</th>
<th></th>
</tr></thead>
{% for ad in ad_list %}
<tr>
<td><a href="{{ url_for('admin.admin_ad_edit', ad_id=ad.id) }}">{{ ad.advertiser_name }}</a></td>
<td><span class="badge cat">{{ ad.slot }}</span></td>
<td class="muted small">
{% if ad.lang %}lang={{ ad.lang }}{% endif %}
{% if ad.geo_state %} state={{ ad.geo_state }}{% endif %}
{% if not ad.lang and not ad.geo_state %}—{% endif %}
</td>
<td class="muted small">
{{ ad.starts_at.strftime('%Y-%m-%d') }}
{{ ad.ends_at.strftime('%Y-%m-%d') }}
</td>
<td>{{ ad.impressions }}</td>
<td>{{ ad.clicks }}</td>
<td>{{ ad.ctr }}%</td>
<td>
{% if ad.is_running %}
<span class="badge ok">{{ _('Live') }}</span>
{% elif ad.is_active %}
<span class="badge cat">{{ _('Scheduled') }}</span>
{% else %}
<span class="badge warn">{{ _('Off') }}</span>
{% endif %}
</td>
<td>
<form method="post" action="{{ url_for('admin.admin_ad_toggle', ad_id=ad.id) }}" style="display:inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn ghost tiny" type="submit">{{ _('Toggle') }}</button>
</form>
<form method="post" action="{{ url_for('admin.admin_ad_delete', ad_id=ad.id) }}" style="display:inline"
onsubmit="return confirm('{{ _('Delete this ad?') }}');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn danger tiny" type="submit">{{ _('Del') }}</button>
</form>
</td>
</tr>
{% endfor %}
</table>
</div>
{% endblock %}
+108
View File
@@ -0,0 +1,108 @@
{% extends "base.html" %}
{% block title %}{{ _('Admin · Analytics') }}{% endblock %}
{% block content %}
{% include "admin/_nav.html" %}
<div class="card">
<h2>{{ _('Analytics — last 30 days') }}</h2>
<div class="kpi-grid">
<div class="card kpi-card">
<div class="kpi-label">{{ _('New signups (30d)') }}</div>
<div class="kpi-value">{{ signups|sum(attribute='n') }}</div>
</div>
<div class="card kpi-card">
<div class="kpi-label">{{ _('Listings posted (30d)') }}</div>
<div class="kpi-value">{{ listings_chart|sum(attribute='n') }}</div>
</div>
<div class="card kpi-card">
<div class="kpi-label">{{ _('Revenue (30d)') }}</div>
<div class="kpi-value">${{ '%.2f'|format((revenue_chart|sum(attribute='cents') or 0) / 100) }}</div>
</div>
<div class="card kpi-card">
<div class="kpi-label">{{ _('Ad impressions (active ads)') }}</div>
<div class="kpi-value">{{ ad_stats.total_impressions or 0 }}</div>
</div>
<div class="card kpi-card">
<div class="kpi-label">{{ _('Ad clicks (active ads)') }}</div>
<div class="kpi-value">{{ ad_stats.total_clicks or 0 }}</div>
</div>
<div class="card kpi-card">
<div class="kpi-label">{{ _('Overall CTR') }}</div>
<div class="kpi-value">
{% if ad_stats.total_impressions %}
{{ '%.2f'|format(ad_stats.total_clicks / ad_stats.total_impressions * 100) }}%
{% else %}—{% endif %}
</div>
</div>
</div>
</div>
<div class="card">
<h3>{{ _('Signups per day') }}</h3>
{% if signups %}
<table class="list">
<thead><tr><th>{{ _('Date') }}</th><th>{{ _('Signups') }}</th></tr></thead>
{% for row in signups %}
<tr>
<td class="muted small">{{ row.day }}</td>
<td>{{ row.n }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="muted">{{ _('No signups in the last 30 days.') }}</p>
{% endif %}
</div>
<div class="card">
<h3>{{ _('Listings posted per day') }}</h3>
{% if listings_chart %}
<table class="list">
<thead><tr><th>{{ _('Date') }}</th><th>{{ _('Listings') }}</th></tr></thead>
{% for row in listings_chart %}
<tr>
<td class="muted small">{{ row.day }}</td>
<td>{{ row.n }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="muted">{{ _('No listings posted in the last 30 days.') }}</p>
{% endif %}
</div>
<div class="card">
<h3>{{ _('Revenue per day') }}</h3>
{% if revenue_chart %}
<table class="list">
<thead><tr><th>{{ _('Date') }}</th><th>{{ _('Revenue') }}</th></tr></thead>
{% for row in revenue_chart %}
<tr>
<td class="muted small">{{ row.day }}</td>
<td>${{ '%.2f'|format((row.cents or 0) / 100) }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="muted">{{ _('No revenue in the last 30 days.') }}</p>
{% endif %}
</div>
<div class="card">
<h3>{{ _('Top categories by active listings') }}</h3>
{% if top_cats %}
<table class="list">
<thead><tr><th>{{ _('Category') }}</th><th>{{ _('Active listings') }}</th></tr></thead>
{% for name, n in top_cats %}
<tr>
<td>{{ name }}</td>
<td>{{ n }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="muted">{{ _('No listings.') }}</p>
{% endif %}
</div>
{% endblock %}
@@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block title %}{{ _('Admin · Assign keyword') }}{% endblock %}
{% block content %}
{% include "admin/_nav.html" %}
<div class="card">
<h2>{{ _('Assign promoted keyword') }}</h2>
{% if error %}<p class="alert danger">{{ error }}</p>{% endif %}
<p class="muted">
{{ _('The listing will appear at the top of search results when the keyword is matched (accent-insensitive).') }}
</p>
<form method="post" class="settings-panel">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="field">
<label>{{ _('Listing ID') }} *</label>
<input class="input" name="listing_id" type="number" required min="1"
placeholder="{{ _('Enter the numeric listing ID') }}">
</div>
<div class="field">
<label>{{ _('Keyword') }} *</label>
<input class="input" name="keyword" required maxlength="80"
placeholder="{{ _('e.g. pho, cleaning service') }}">
</div>
<div class="field">
<label>{{ _('Priority (higher = shown first)') }}</label>
<input class="input" name="priority" type="number" value="0">
</div>
<div class="field">
<label>{{ _('Expires at (YYYY-MM-DD)') }}</label>
<input class="input" name="expires_at" type="date">
</div>
<div class="billing-actions">
<button class="btn" type="submit">{{ _('Save') }}</button>
<a class="btn ghost" href="{{ url_for('admin.admin_promoted_keywords') }}">{{ _('Cancel') }}</a>
</div>
</form>
</div>
{% endblock %}
@@ -0,0 +1,52 @@
{% extends "base.html" %}
{% block title %}{{ _('Admin · Promoted Keywords') }}{% endblock %}
{% block content %}
{% include "admin/_nav.html" %}
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center">
<h2>{{ _('Promoted keywords') }}</h2>
<a class="btn" href="{{ url_for('admin.admin_promoted_keyword_new') }}">{{ _('+ Assign keyword') }}</a>
</div>
{% if not pks %}<p class="muted">{{ _('No promoted keywords.') }}</p>{% endif %}
<table class="list">
<thead><tr>
<th>{{ _('Keyword') }}</th>
<th>{{ _('Listing') }}</th>
<th>{{ _('Priority') }}</th>
<th>{{ _('Expires') }}</th>
<th>{{ _('Status') }}</th>
<th></th>
</tr></thead>
{% for pk in pks %}
<tr>
<td><code>{{ pk.keyword }}</code></td>
<td>
{% if pk.listing %}
<a href="{{ url_for('listings.detail', listing_id=pk.listing_id) }}">
{{ pk.listing.title[:60] }}
</a>
{% else %}<span class="muted"></span>{% endif %}
</td>
<td>{{ pk.priority }}</td>
<td class="muted small">{{ pk.expires_at.strftime('%Y-%m-%d') }}</td>
<td>
{% if pk.is_active %}
<span class="badge ok">{{ _('Active') }}</span>
{% else %}
<span class="badge warn">{{ _('Expired') }}</span>
{% endif %}
</td>
<td>
<form method="post"
action="{{ url_for('admin.admin_promoted_keyword_delete', pk_id=pk.id) }}"
style="display:inline" onsubmit="return confirm('{{ _('Remove?') }}');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn danger tiny" type="submit">{{ _('Remove') }}</button>
</form>
</td>
</tr>
{% endfor %}
</table>
</div>
{% endblock %}
+75
View File
@@ -0,0 +1,75 @@
{% extends "base.html" %}
{% block title %}{{ _('Admin · %(t)s', t=_('Edit sponsor') if sponsor else _('New sponsor')) }}{% endblock %}
{% block content %}
{% include "admin/_nav.html" %}
<div class="card">
<h2>{{ _('Edit sponsor') if sponsor else _('New sponsor') }}</h2>
{% if error %}<p class="alert danger">{{ error }}</p>{% endif %}
<form method="post" class="settings-panel">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="field">
<label>{{ _('Name') }} *</label>
<input class="input" name="name" required
value="{{ sponsor.name if sponsor else '' }}">
</div>
<div class="field">
<label>{{ _('URL') }} *</label>
<input class="input" name="url" required
value="{{ sponsor.url if sponsor else '' }}">
</div>
<div class="field">
<label>{{ _('Tagline') }}</label>
<input class="input" name="tagline"
value="{{ sponsor.tagline or '' if sponsor else '' }}">
</div>
<div class="field">
<label>{{ _('Logo path (relative under media/)') }}</label>
<input class="input" name="logo_path"
value="{{ sponsor.logo_path or '' if sponsor else '' }}"
placeholder="{{ _('e.g. sponsors/logo.png') }}">
</div>
<div class="field">
<label>{{ _('Tier') }}</label>
<select class="input" name="tier">
{% for t in ['directory', 'category'] %}
<option value="{{ t }}" {{ 'selected' if sponsor and sponsor.tier == t }}>{{ t }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<label>{{ _('Category (for category-tier sponsors)') }}</label>
<select class="input" name="category_id">
<option value="">{{ _('None') }}</option>
{% for cat in categories %}
<option value="{{ cat.id }}"
{{ 'selected' if sponsor and sponsor.category_id == cat.id }}>
{{ cat.name }}
</option>
{% endfor %}
</select>
</div>
<div class="field">
<label>{{ _('Starts at (YYYY-MM-DD)') }}</label>
<input class="input" name="starts_at" type="date"
value="{{ sponsor.starts_at.strftime('%Y-%m-%d') if sponsor else '' }}">
</div>
<div class="field">
<label>{{ _('Ends at (YYYY-MM-DD)') }}</label>
<input class="input" name="ends_at" type="date"
value="{{ sponsor.ends_at.strftime('%Y-%m-%d') if sponsor else '' }}">
</div>
<div class="check">
<input type="checkbox" name="is_active" id="is_active"
{{ 'checked' if (not sponsor or sponsor.is_active) }}>
<label for="is_active">{{ _('Active') }}</label>
</div>
<div class="billing-actions">
<button class="btn" type="submit">{{ _('Save') }}</button>
<a class="btn ghost" href="{{ url_for('admin.admin_sponsors') }}">{{ _('Cancel') }}</a>
</div>
</form>
</div>
{% endblock %}
+50
View File
@@ -0,0 +1,50 @@
{% extends "base.html" %}
{% block title %}{{ _('Admin · Sponsors') }}{% endblock %}
{% block content %}
{% include "admin/_nav.html" %}
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center">
<h2>{{ _('Sponsors') }}</h2>
<a class="btn" href="{{ url_for('admin.admin_sponsor_new') }}">{{ _('+ New sponsor') }}</a>
</div>
{% if not sponsors %}<p class="muted">{{ _('No sponsors yet.') }}</p>{% endif %}
<table class="list">
<thead><tr>
<th>{{ _('Name') }}</th>
<th>{{ _('Tier') }}</th>
<th>{{ _('Category') }}</th>
<th>{{ _('Schedule') }}</th>
<th>{{ _('Status') }}</th>
<th></th>
</tr></thead>
{% for sp in sponsors %}
<tr>
<td><a href="{{ url_for('admin.admin_sponsor_edit', sponsor_id=sp.id) }}">{{ sp.name }}</a></td>
<td><span class="badge cat">{{ sp.tier }}</span></td>
<td class="muted small">{{ sp.category.name if sp.category else '—' }}</td>
<td class="muted small">
{{ sp.starts_at.strftime('%Y-%m-%d') }}
{{ sp.ends_at.strftime('%Y-%m-%d') }}
</td>
<td>
{% if sp.is_running %}
<span class="badge ok">{{ _('Live') }}</span>
{% elif sp.is_active %}
<span class="badge cat">{{ _('Scheduled') }}</span>
{% else %}
<span class="badge warn">{{ _('Off') }}</span>
{% endif %}
</td>
<td>
<form method="post" action="{{ url_for('admin.admin_sponsor_delete', sponsor_id=sp.id) }}"
style="display:inline" onsubmit="return confirm('{{ _('Delete?') }}');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn danger tiny" type="submit">{{ _('Del') }}</button>
</form>
</td>
</tr>
{% endfor %}
</table>
</div>
{% endblock %}
+11
View File
@@ -37,6 +37,7 @@
<th>{{ _('Amount') }}</th>
<th>{{ _('Status') }}</th>
<th>{{ _('Stripe ID') }}</th>
<th></th>
</tr></thead>
{% for txn in pagination.items %}
<tr>
@@ -46,6 +47,16 @@
<td>${{ '%.2f'|format(txn.amount_cents / 100) }}</td>
<td><span class="badge {{ 'ok' if txn.status == 'succeeded' else 'warn' }}">{{ txn.status }}</span></td>
<td class="muted small">{{ txn.stripe_object_id or '—' }}</td>
<td>
{% if txn.status == 'succeeded' and txn.type != 'refund' %}
<form method="post"
action="{{ url_for('admin.refund_transaction', txn_id=txn.id) }}"
onsubmit="return confirm('{{ _('Issue refund for $%(a)s?', a='%.2f'|format(txn.amount_cents/100)) }}');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn ghost tiny" type="submit">{{ _('Refund') }}</button>
</form>
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="6" class="muted">{{ _('No transactions.') }}</td></tr>