Diff -- /var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/TEMPLATES/webstls_admin.html
Diff

/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/TEMPLATES/webstls_admin.html

added on local at 2026-07-01 13:47:56

Added
+270
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to e37a401573fc
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1<!-- =====================================================================
2 Admin Console (company-staff only).
3 - Platform KPI tiles across the top
4 - User search + table; click any row to open /admin_user.cgi
5 The sidebar entry that links here is gated in Wrapper.pm; this
6 page additionally calls Admin->require_admin so a hand-typed URL
7 can't reach it either.
8===================================================================== -->
9<style>
10 .adm-search {
11 display: flex; gap: 8px; align-items: center;
12 margin: 22px 0 14px;
13 }
14 .adm-search input {
15 flex: 1;
16 background: var(--col-surface-1);
17 border: 1px solid var(--col-border);
18 color: var(--col-text);
19 padding: 10px 14px;
20 border-radius: 10px;
21 font-size: 14px;
22 font-family: inherit;
23 }
24 .adm-search input:focus {
25 outline: none; border-color: var(--col-accent);
26 }
27 .adm-search .summary {
28 font-size: 11px; color: var(--col-text-3);
29 letter-spacing: 1.5px; text-transform: uppercase;
30 margin-left: 8px;
31 }
32
33 .adm-utable {
34 width: 100%;
35 border-collapse: separate; border-spacing: 0;
36 background: var(--col-surface-1);
37 border: 1px solid var(--col-border);
38 border-radius: 12px;
39 overflow: hidden;
40 }
41 .adm-utable thead th {
42 background: var(--col-surface-2);
43 padding: 11px 14px;
44 text-align: left;
45 font-size: 10px;
46 letter-spacing: 1.5px;
47 text-transform: uppercase;
48 color: var(--col-text-3);
49 font-weight: 700;
50 border-bottom: 1px solid var(--col-border);
51 }
52 .adm-utable tbody td {
53 padding: 12px 14px;
54 border-bottom: 1px solid var(--col-border);
55 font-size: 13px; color: var(--col-text-2);
56 vertical-align: middle;
57 }
58 .adm-utable tbody tr:last-child td { border-bottom: none; }
59 .adm-utable tbody tr {
60 cursor: pointer; transition: background 0.12s;
61 }
62 .adm-utable tbody tr:hover td { background: rgba(124,58,237,0.08); }
63 .adm-utable a { color: inherit; text-decoration: none; }
64
65 .adm-uname { color: var(--col-text); font-weight: 700; }
66 .adm-uid { color: var(--col-text-3); font-size: 11px; font-family: var(--font-mono, monospace); }
67 .adm-pill {
68 font-size: 9px; font-weight: 700;
69 padding: 3px 8px; border-radius: 6px;
70 letter-spacing: 1px; text-transform: uppercase;
71 }
72 .adm-pill.plan-free { background: rgba(100,116,139,0.20); color: #94a3b8; }
73 .adm-pill.plan-starter { background: rgba(59,130,246,0.18); color: #60a5fa; }
74 .adm-pill.plan-pro { background: rgba(124,58,237,0.18); color: #a78bfa; }
75 .adm-pill.plan-studio { background: rgba(245,158,11,0.18); color: #fbbf24; }
76 .adm-pill.is-admin { background: rgba(34,197,94,0.18); color: #4ade80; }
77 .adm-pill.is-suspended { background: rgba(245,158,11,0.20); color: #fbbf24; }
78 .adm-pill.is-closed { background: rgba(239,68,68,0.20); color: #f87171; }
79
80 .adm-empty {
81 text-align: center;
82 padding: 60px 24px;
83 color: var(--col-text-3);
84 }
85 .adm-empty .lg { font-size: 14px; color: var(--col-text-2); margin-bottom: 6px; }
86 .adm-empty .sm { font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase; }
87</style>
88
89<div class="page-head">
90 <div>
91 <span class="page-eyebrow"><span class="dot"></span> Admin &middot; Company staff only</span>
92 <h1 class="page-title">Operations console</h1>
93 <p class="page-subtitle">Platform-wide view across every seller. Search for a user to drill into their account.</p>
94 </div>
95 <div class="page-actions">
96 [if:$not_tutorial_mode]<a href="/admin.cgi?tutorial=1" class="btn btn-secondary"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>Sample Data View</a>[/if]
97 [if:$tutorial_mode]<a href="/admin.cgi" class="btn btn-secondary"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>Exit sample view</a>[/if]
98 </div>
99</div>
100
101[if:$tutorial_mode]
102<!-- Tutorial banner + intro: example-data walkthrough for staff being
103 onboarded into the admin console. Styles live in site.css. -->
104<div class="tutorial-banner">
105 <div class="tutorial-banner-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></div>
106 <div class="tutorial-banner-body">
107 <div class="tutorial-banner-title">Tutorial &middot; Sample data only</div>
108 <div class="tutorial-banner-text">Every KPI tile, the search results, and the user table below are filled with example data. None of this reflects the real platform &mdash; it is here so staff being onboarded into the Admin Console can see how the page reads on a populated platform.</div>
109 </div>
110 <a href="/admin.cgi" class="btn btn-secondary btn-sm">Exit sample view</a>
111</div>
112
113<div class="tutorial-intro">
114 <div class="tutorial-intro-eyebrow">A walkthrough of the Operations Console</div>
115 <h2 class="tutorial-intro-title">This is what the admin console looks like on a populated platform.</h2>
116 <p class="tutorial-intro-text">The numbers belong to an imaginary deployment with <strong>1,247 users</strong>, <strong>1,089 active accounts</strong>, <strong>86 signups in the last 30 days</strong>, <strong>1,154 active programs</strong>, <strong>8,372 lifetime conversions</strong>, and <strong>\$42,810 in commissions paid</strong> over the past month. The user table beneath the KPI tiles shows a mix of plan tiers (Free / Starter / Pro / Studio), an admin account, and one suspended and one closed account so every row pill and status variant in the UI is visible at once.</p>
117 <p class="tutorial-intro-text">On the live page, the KPI tiles read directly from <code>users</code>, <code>affiliate_programs</code>, and <code>affiliate_conversions</code> &mdash; counts, status filters, and a 30-day sum of commissions on paid conversions. The search box runs a free-text match against email, display name, program name and numeric user id; clicking any row navigates to <code>/admin_user.cgi</code> for that account where staff can adjust plan tier, toggle admin, suspend, close, impersonate, and audit-log. Each KPI tile in the row above also carries a <strong>click-for-detail</strong> popup explaining its data source and how to interpret movement in that number.</p>
118 <p class="tutorial-intro-text tutorial-intro-foot">When you are ready to see real data, click <a href="/admin.cgi" class="text-brand fw-700">Exit sample view</a> to return to the live console.</p>
119</div>
120[/if]
121
122<!-- Platform KPI tiles -->
123<div class="stat-row">
124 <div class="stat" data-kpi-detail='{
125 "eyebrow":"Users . platform-wide total",
126 "description":"Every user account on WebSTLs regardless of status (active, suspended, closed, pending). Active is the slice you can actually transact with; the rest is here for support and admin context.",
127 "methodology":"<code>SELECT COUNT(*) FROM users</code> -- no filter. Active count: <code>WHERE account_status=&#39;active&#39;</code>. New-in-30d: <code>WHERE created_at &gt;= DATE_SUB(NOW(), INTERVAL 30 DAY)</code>. Suspended and closed are filtered separately in the search below.",
128 "kpis":[
129 {"label":"All users","value":"$u_total","sub":"every status"},
130 {"label":"Active","value":"$u_active","sub":"can sign in &amp; transact","color":"#4ade80"},
131 {"label":"New (30d)","value":"$u_new_30d","sub":"signups this month"}
132 ],
133 "interpretation":"User count grows monotonically -- it never shrinks unless we hard-delete. Active is the meaningful number for capacity planning. The new-in-30d rate is your growth pulse; healthy SaaS sees this number stay roughly constant or accelerate. A sudden drop usually means signup is broken (test the /signup.cgi flow) or a marketing channel went dark.",
134 "drilldowns":[
135 {"label":"Search all users","href":"/admin.cgi"},
136 {"label":"Recent signups","href":"/admin.cgi?sort=newest"}
137 ]
138 }'>
139 <div class="stat-label">Users</div>
140 <div class="stat-value">$u_total</div>
141 <div class="stat-meta"><span class="stat-meta-label">$u_active active &middot; $u_new_30d new (30d)</span></div>
142 </div>
143 <div class="stat" data-kpi-detail='{
144 "eyebrow":"Active programs . affiliate programs accepting clicks",
145 "description":"Programs with status=active across every merchant on AffSoft. Draft and paused programs are excluded.",
146 "methodology":"<code>SELECT COUNT(*) FROM affiliate_programs WHERE status=&#39;active&#39;</code>.",
147 "kpis":[
148 {"label":"Active programs","value":"$sf_total","sub":"taking clicks right now"},
149 {"label":"Users","value":"$u_total","sub":"for reference"}
150 ],
151 "interpretation":"This should grow steadily with merchant signups. A sharp drop means merchants are pausing programs en masse -- check for a billing issue or a tracker outage."
152 }'>
153 <div class="stat-label">Active programs</div>
154 <div class="stat-value">$sf_total</div>
155 <div class="stat-meta"><span class="stat-meta-label">taking clicks right now</span></div>
156 </div>
157 <div class="stat" data-kpi-detail='{
158 "eyebrow":"Conversions . platform lifetime total",
159 "description":"Every conversion recorded across every merchant since launch. Pending / approved / paid / rejected all counted.",
160 "methodology":"<code>SELECT COUNT(*) FROM affiliate_conversions</code>. Lifetime, no time filter.",
161 "kpis":[
162 {"label":"Lifetime conversions","value":"$m_total","sub":"all statuses"}
163 ],
164 "interpretation":"This is your headline platform-throughput KPI. Flatlines mean tracking is broken or attribution is misconfigured -- cross-check against affiliate_clicks count for the same window."
165 }'>
166 <div class="stat-label">Conversions</div>
167 <div class="stat-value">$m_total</div>
168 <div class="stat-meta"><span class="stat-meta-label">lifetime total</span></div>
169 </div>
170 <div class="stat" data-kpi-detail='{
171 "eyebrow":"Revenue (30d) . platform-wide gross",
172 "description":"Sum of every paid order across every storefront on the platform in the last 30 days. This is the gross-merchandise-value (GMV) number; WebSTLs revenue is ~10% of this from platform fees.",
173 "methodology":"<code>SUM(orders.total_amount_cents) WHERE status=&#39;paid&#39; AND created_at &gt;= NOW() - INTERVAL 30 DAY</code>, across all storefronts. Marketplace orders are not included -- those route through their own platforms.",
174 "kpis":[
175 {"label":"Commissions paid (30d)","value":"$rev_30d","sub":"total flowed to affiliates","color":"#4ade80"}
176 ],
177 "interpretation":"Commissions paid is the headline throughput KPI for AffSoft. Trending up means more conversions are clearing and getting paid. Sudden drops can indicate a payout-batch failure or a fraud-review backlog -- check Fraud review and Payouts."
178 }'>
179 <div class="stat-label">Commissions paid (30d)</div>
180 <div class="stat-value">$rev_30d</div>
181 <div class="stat-meta"><span class="stat-meta-label">across approved &amp; paid conversions</span></div>
182 </div>
183</div>
184
185<!-- Search -->
186<form method="GET" action="/admin.cgi" class="adm-search">
187 <input type="text" name="q" value="$q" placeholder="Search by email, name, program, or user id..." autofocus>
188 <button class="btn btn-primary btn-sm" type="submit">Search</button>
189 [if:$has_query]<a class="btn btn-secondary btn-sm" href="/admin.cgi">Clear</a>[/if]
190 <span class="summary">$result_count results</span>
191</form>
192
193<!-- Results -->
194[if:$has_results]
195<style>
196 .au-table-wrap { overflow-x: auto; }
197 table.au-table { width: 100%; border-collapse: separate; border-spacing: 0; }
198 table.au-table th { text-align: left; font-size: 10px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--col-text-dim, #94a3b8); padding: 10px 12px; font-weight: 700; border-bottom: 1px solid var(--col-border, #1e293b); }
199 table.au-table td { padding: 12px; border-bottom: 1px solid var(--col-border, #1e293b); color: var(--col-text, #e2e8f0); font-size: 13px; vertical-align: middle; white-space: nowrap; }
200 table.au-table tr:last-child td { border-bottom: none; }
201 table.au-table tr.au-clickable { cursor: pointer; transition: background 0.12s; }
202 table.au-table tr.au-clickable:hover { background: rgba(255,255,255,0.04); }
203 .au-av { width: 32px; height: 32px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: #fff; font-weight: 700; font-size: 12px; flex-shrink: 0; }
204 .au-name-cell { display: flex; align-items: center; gap: 10px; }
205 .au-pill { display: inline-block; padding: 3px 9px; border-radius: 5px; font-size: 11px; font-weight: 600; background: var(--col-bg-3, rgba(255,255,255,0.06)); color: var(--col-text-dim, #94a3b8); border: 1px solid var(--col-border-2, rgba(255,255,255,0.10)); }
206 .au-pill.super { background: rgba(239,68,68,0.18); color: #f87171; border-color: rgba(239,68,68,0.35); }
207 .au-imp-btn { background: #fbbf24; color: #1a1303; border: 1px solid #fbbf24; padding: 6px 12px; border-radius: 6px; font-weight: 700; font-size: 12px; cursor: pointer; }
208 .au-imp-btn:hover { background: #f59e0b; border-color: #f59e0b; }
209</style>
210<div class="au-table-wrap">
211<table class="au-table">
212 <thead>
213 <tr>
214 <th>Member</th>
215 <th>Email</th>
216 <th>Program</th>
217 <th>Role</th>
218 <th>Status</th>
219 <th>Last login</th>
220 <th></th>
221 </tr>
222 </thead>
223 <tbody>
224 [loop:@users]
225 <tr class="au-clickable" data-href="/admin_user.cgi?u=$loop1.id">
226 <td><div class="au-name-cell"><div class="au-av" style="background:$loop1.avatar_color">$loop1.initials</div><div>$loop1.display_name [if:$loop1.is_admin_flag]<span class="au-pill super">ADMIN</span>[/if]</div></div></td>
227 <td>$loop1.email</td>
228 <td>$loop1.storefront_name</td>
229 <td><span class="au-pill">$loop1.plan_tier</span></td>
230 <td><span class="au-pill">$loop1.account_status</span></td>
231 <td style="font-size:12px;color:var(--col-text-dim, #94a3b8)">$loop1.last_login_at</td>
232 <td>
233 [if:!$loop1.is_self]
234 <form method="POST" action="/admin_action.cgi" style="display:inline;margin:0" onclick="event.stopPropagation()">
235 <input type="hidden" name="act" value="impersonate">
236 <input type="hidden" name="u" value="$loop1.id">
237 <button type="submit" class="au-imp-btn">Impersonate</button>
238 </form>
239 [/if]
240 </td>
241 </tr>
242 [/loop]
243 </tbody>
244</table>
245</div>
246<script>
247 document.querySelectorAll('table.au-table tr.au-clickable').forEach(function(tr){
248 tr.addEventListener('click', function(e){
249 if (e.target.closest('a,button,form,input')) return;
250 window.location.href = tr.dataset.href;
251 });
252 });
253</script>
254
255[/if]
256
257[if:!$has_results]
258<div class="adm-empty">
259 [if:$has_query]
260 <div class="lg">No users match "$q".</div>
261 <div class="sm">Try part of an email, display name, program name, or numeric user id.</div>
262 [/if]
263 [if:!$has_query]
264 <div class="lg">No users yet.</div>
265 <div class="sm">Sellers will appear here as they sign up.</div>
266 [/if]
267</div>
268[/if]
269
270<div class="adm-foot-spacer" data-end="admin-console">&nbsp;</div>
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help