feat: add admin subscriptions dashboard and add form
- /admin/subscriptions: shows revenue stats, projected revenue (1mo/3mo/6mo/1yr), upcoming renewal events, and recent subscription history - /admin/subscriptions/add: form to create subscriptions by user email/ID with product selection, frequency override, and initial state - Fix bug in createSubscription() where $status was undefined parameter - Add model methods: getRecent, getUpcoming, getTotalRevenue, getActiveCount, getProjectedRevenue, getSubscriptionProducts - Add nav link on admin dashboard Closes #17
This commit is contained in:
parent
4eb1d59230
commit
be3eafcd9c
6 changed files with 495 additions and 1 deletions
|
|
@ -124,6 +124,8 @@ if (preg_match('/^\/(address(?:\/edit|\/delete)?|transaction|user|order|quote|pr
|
||||||
'/admin/transactions/add' => $defaults['is_admin'] ? admin::transactions_add($defaults) : lost::index($defaults),
|
'/admin/transactions/add' => $defaults['is_admin'] ? admin::transactions_add($defaults) : lost::index($defaults),
|
||||||
'/admin/transactions/reset' => $defaults['is_admin'] ? admin::transactions_reset($defaults) : lost::index($defaults),
|
'/admin/transactions/reset' => $defaults['is_admin'] ? admin::transactions_reset($defaults) : lost::index($defaults),
|
||||||
'/admin/returns' => $defaults['is_admin'] ? admin::returns($defaults) : lost::index($defaults),
|
'/admin/returns' => $defaults['is_admin'] ? admin::returns($defaults) : lost::index($defaults),
|
||||||
|
'/admin/subscriptions' => $defaults['is_admin'] ? admin::subscriptions($defaults) : lost::index($defaults),
|
||||||
|
'/admin/subscriptions/add' => $defaults['is_admin'] ? admin::subscriptions_add($defaults) : lost::index($defaults),
|
||||||
'/magic-link' => magic_link::index(),
|
'/magic-link' => magic_link::index(),
|
||||||
'/checkout/confirmed' => checkout::confirmed($defaults),
|
'/checkout/confirmed' => checkout::confirmed($defaults),
|
||||||
'/checkout/review-pay' => checkout::review_pay($defaults),
|
'/checkout/review-pay' => checkout::review_pay($defaults),
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ namespace app\controllers;
|
||||||
use app\models\categories;
|
use app\models\categories;
|
||||||
use app\models\emails;
|
use app\models\emails;
|
||||||
use app\models\products;
|
use app\models\products;
|
||||||
|
use app\models\subscriptions;
|
||||||
use app\models\transactions;
|
use app\models\transactions;
|
||||||
use app\models\users;
|
use app\models\users;
|
||||||
|
|
||||||
|
|
@ -354,4 +355,111 @@ class admin
|
||||||
header('Location: /admin/transactions/add');
|
header('Location: /admin/transactions/add');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function subscriptions($defaults)
|
||||||
|
{
|
||||||
|
echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
|
||||||
|
'child_template' => 'admin/subscriptions/index.twig',
|
||||||
|
'page_title' => 'Subscriptions',
|
||||||
|
'recent' => subscriptions::getRecent(20),
|
||||||
|
'upcoming' => subscriptions::getUpcoming(20),
|
||||||
|
'revenue' => subscriptions::getTotalRevenue(),
|
||||||
|
'active_count' => subscriptions::getActiveCount(),
|
||||||
|
'projected_revenue' => subscriptions::getProjectedRevenue(),
|
||||||
|
'breadcrumbs' => [
|
||||||
|
[
|
||||||
|
'url' => '/admin',
|
||||||
|
'title' => 'Admin',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'url' => '/admin/subscriptions',
|
||||||
|
'title' => 'Subscriptions',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function subscriptions_add($defaults)
|
||||||
|
{
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
|
$_SESSION['last_post'] = $_POST;
|
||||||
|
|
||||||
|
$user_identifier = $_POST['user_identifier'] ?? null;
|
||||||
|
$product_id = $_POST['product_id'] ?? null;
|
||||||
|
$frequency = $_POST['frequency'] ?? null;
|
||||||
|
$state = $_POST['state'] ?? 'START';
|
||||||
|
|
||||||
|
if (! $user_identifier || ! $product_id) {
|
||||||
|
$_SESSION['error'] = 'User identifier and subscription product are required.';
|
||||||
|
header('Location: /admin/subscriptions/add');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve user
|
||||||
|
$user = null;
|
||||||
|
if (strpos($user_identifier, '@') !== false && strpos($user_identifier, '.') !== false) {
|
||||||
|
$user = users::getByEmail($user_identifier);
|
||||||
|
} elseif (is_numeric($user_identifier)) {
|
||||||
|
$user = users::getById((int) $user_identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
$_SESSION['error'] = 'User not found. Please enter a valid email or user ID.';
|
||||||
|
header('Location: /admin/subscriptions/add');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
$renewAt = self::calculateRenewDate($now, $frequency ?: 'month');
|
||||||
|
|
||||||
|
$subId = subscriptions::createSubscription(
|
||||||
|
$user['id'],
|
||||||
|
(int) $product_id,
|
||||||
|
$state,
|
||||||
|
$now,
|
||||||
|
$renewAt,
|
||||||
|
$now
|
||||||
|
);
|
||||||
|
|
||||||
|
$_SESSION['success'] = 'Subscription created successfully.';
|
||||||
|
unset($_SESSION['last_post']);
|
||||||
|
header('Location: /admin/subscriptions');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
|
||||||
|
'child_template' => 'admin/subscriptions/add.twig',
|
||||||
|
'page_title' => 'Add Subscription',
|
||||||
|
'subscription_products' => subscriptions::getSubscriptionProducts(),
|
||||||
|
'breadcrumbs' => [
|
||||||
|
[
|
||||||
|
'url' => '/admin',
|
||||||
|
'title' => 'Admin',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'url' => '/admin/subscriptions',
|
||||||
|
'title' => 'Subscriptions',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'url' => '/admin/subscriptions/add',
|
||||||
|
'title' => 'Add',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function calculateRenewDate(string $startDate, string $frequency): string
|
||||||
|
{
|
||||||
|
$date = new \DateTime($startDate);
|
||||||
|
switch ($frequency) {
|
||||||
|
case 'minute': $date->modify('+1 minute'); break;
|
||||||
|
case 'hour': $date->modify('+1 hour'); break;
|
||||||
|
case 'day': $date->modify('+1 day'); break;
|
||||||
|
case 'week': $date->modify('+1 week'); break;
|
||||||
|
case 'month': $date->modify('+1 month'); break;
|
||||||
|
case 'year': $date->modify('+1 year'); break;
|
||||||
|
default: $date->modify('+1 month'); break;
|
||||||
|
}
|
||||||
|
return $date->format('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@ class subscriptions
|
||||||
'COMPLETED', 'CANCELED',
|
'COMPLETED', 'CANCELED',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const FREQUENCIES = [
|
||||||
|
'minute', 'hour', 'day', 'week', 'month', 'year',
|
||||||
|
];
|
||||||
|
|
||||||
public static function init()
|
public static function init()
|
||||||
{
|
{
|
||||||
app::$db->exec("CREATE TABLE IF NOT EXISTS subscriptions (
|
app::$db->exec("CREATE TABLE IF NOT EXISTS subscriptions (
|
||||||
|
|
@ -27,7 +31,7 @@ class subscriptions
|
||||||
);");
|
);");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function createSubscription($userId, $productId, $state, $startDate, $renewAt, $invoiceDate)
|
public static function createSubscription($userId, $productId, $state, $startDate, $renewAt, $invoiceDate, $status = 'COMPLETED')
|
||||||
{
|
{
|
||||||
self::validateState($state);
|
self::validateState($state);
|
||||||
self::validateStatus($status);
|
self::validateStatus($status);
|
||||||
|
|
@ -83,6 +87,117 @@ class subscriptions
|
||||||
return $stmt->fetchAll();
|
return $stmt->fetchAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getRecent(int $limit = 20): array
|
||||||
|
{
|
||||||
|
$stmt = app::$db->prepare("
|
||||||
|
SELECT s.*, u.email as user_email, p.title as product_title,
|
||||||
|
p.price_cents, p.price_sats, p.subscription_frequency
|
||||||
|
FROM subscriptions s
|
||||||
|
LEFT JOIN users u ON s.user_id = u.id
|
||||||
|
LEFT JOIN products p ON s.product_id = p.id
|
||||||
|
ORDER BY s.start_date DESC
|
||||||
|
LIMIT :limit
|
||||||
|
");
|
||||||
|
$stmt->bindValue(':limit', $limit, \PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getUpcoming(int $limit = 20): array
|
||||||
|
{
|
||||||
|
$stmt = app::$db->prepare("
|
||||||
|
SELECT s.*, u.email as user_email, p.title as product_title,
|
||||||
|
p.price_cents, p.price_sats, p.subscription_frequency
|
||||||
|
FROM subscriptions s
|
||||||
|
LEFT JOIN users u ON s.user_id = u.id
|
||||||
|
LEFT JOIN products p ON s.product_id = p.id
|
||||||
|
WHERE s.status = 'COMPLETED'
|
||||||
|
AND s.renews_at >= datetime('now')
|
||||||
|
ORDER BY s.renews_at ASC
|
||||||
|
LIMIT :limit
|
||||||
|
");
|
||||||
|
$stmt->bindValue(':limit', $limit, \PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getTotalRevenue(): array
|
||||||
|
{
|
||||||
|
$stmt = app::$db->query("
|
||||||
|
SELECT COALESCE(SUM(p.price_cents), 0) as total_cents,
|
||||||
|
COALESCE(SUM(p.price_sats), 0) as total_sats,
|
||||||
|
COUNT(*) as total_count
|
||||||
|
FROM subscriptions s
|
||||||
|
LEFT JOIN products p ON s.product_id = p.id
|
||||||
|
WHERE s.status = 'COMPLETED'
|
||||||
|
");
|
||||||
|
return $stmt->fetch(\PDO::FETCH_ASSOC) ?: ['total_cents' => 0, 'total_sats' => 0, 'total_count' => 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getActiveCount(): int
|
||||||
|
{
|
||||||
|
$stmt = app::$db->query("
|
||||||
|
SELECT COUNT(*) as cnt
|
||||||
|
FROM subscriptions
|
||||||
|
WHERE status = 'COMPLETED'
|
||||||
|
AND renews_at >= datetime('now')
|
||||||
|
");
|
||||||
|
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
return (int) ($row['cnt'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getProjectedRevenue(): array
|
||||||
|
{
|
||||||
|
$stmt = app::$db->query("
|
||||||
|
SELECT p.price_cents, p.price_sats, p.subscription_frequency
|
||||||
|
FROM subscriptions s
|
||||||
|
LEFT JOIN products p ON s.product_id = p.id
|
||||||
|
WHERE s.status = 'COMPLETED'
|
||||||
|
AND s.renews_at >= datetime('now')
|
||||||
|
");
|
||||||
|
$active = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$projections = [
|
||||||
|
'1mo' => ['cents' => 0, 'sats' => 0],
|
||||||
|
'3mo' => ['cents' => 0, 'sats' => 0],
|
||||||
|
'6mo' => ['cents' => 0, 'sats' => 0],
|
||||||
|
'1yr' => ['cents' => 0, 'sats' => 0],
|
||||||
|
];
|
||||||
|
|
||||||
|
$multipliers = [
|
||||||
|
'minute' => ['1mo' => 43200, '3mo' => 129600, '6mo' => 259200, '1yr' => 525600],
|
||||||
|
'hour' => ['1mo' => 720, '3mo' => 2160, '6mo' => 4320, '1yr' => 8760],
|
||||||
|
'day' => ['1mo' => 30, '3mo' => 90, '6mo' => 180, '1yr' => 365],
|
||||||
|
'week' => ['1mo' => 4, '3mo' => 13, '6mo' => 26, '1yr' => 52],
|
||||||
|
'month' => ['1mo' => 1, '3mo' => 3, '6mo' => 6, '1yr' => 12],
|
||||||
|
'year' => ['1mo' => 0, '3mo' => 0, '6mo' => 0, '1yr' => 1],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($active as $sub) {
|
||||||
|
$freq = $sub['subscription_frequency'] ?? 'month';
|
||||||
|
$mults = $multipliers[$freq] ?? $multipliers['month'];
|
||||||
|
|
||||||
|
foreach ($projections as $period => &$totals) {
|
||||||
|
$totals['cents'] += (int) ($sub['price_cents'] ?? 0) * $mults[$period];
|
||||||
|
$totals['sats'] += (int) ($sub['price_sats'] ?? 0) * $mults[$period];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $projections;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getSubscriptionProducts(): array
|
||||||
|
{
|
||||||
|
$stmt = app::$db->query("
|
||||||
|
SELECT id, title, price_cents, price_sats, subscription_frequency
|
||||||
|
FROM products
|
||||||
|
WHERE is_subscription = 1
|
||||||
|
AND is_enabled = 1
|
||||||
|
ORDER BY title
|
||||||
|
");
|
||||||
|
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
private static function validateState(string $state)
|
private static function validateState(string $state)
|
||||||
{
|
{
|
||||||
if (! in_array($state, self::STATES, true)) {
|
if (! in_array($state, self::STATES, true)) {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@
|
||||||
<a href="/admin/transactions">
|
<a href="/admin/transactions">
|
||||||
Transactions
|
Transactions
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/admin/subscriptions">
|
||||||
|
Subscriptions
|
||||||
|
</a>
|
||||||
|
|
||||||
INDEX
|
INDEX
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
53
src/views/admin/subscriptions/add.twig
Normal file
53
src/views/admin/subscriptions/add.twig
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<section class="flex flex-col gap-4">
|
||||||
|
{% include 'lib/alert.twig' %}
|
||||||
|
<form action="/admin/subscriptions/add" method="post" class="flex flex-col gap-4">
|
||||||
|
{% include 'lib/inputs/text.twig' with {
|
||||||
|
type: 'text',
|
||||||
|
name: 'user_identifier',
|
||||||
|
label: 'User Identifier',
|
||||||
|
placeholder: 'Enter email or user ID',
|
||||||
|
required: true
|
||||||
|
} %}
|
||||||
|
<label for="product_id" class="block text-sm font-medium mt-2">
|
||||||
|
Subscription Product
|
||||||
|
<span class="{{ colors.error.text }} ml-4">*</span>
|
||||||
|
</label>
|
||||||
|
<select id="product_id" name="product_id" class="border rounded-lg p-2 w-full" required>
|
||||||
|
<option value="">-- Select Product --</option>
|
||||||
|
{% for product in subscription_products %}
|
||||||
|
<option value="{{ product.id }}">
|
||||||
|
{{ product.title }} ({{ product.subscription_frequency }} - {{ product.price_cents }} cents / {{ product.price_sats }} sats)
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% include 'lib/inputs/select.twig' with {
|
||||||
|
id: 'frequency',
|
||||||
|
name: 'frequency',
|
||||||
|
label: 'Frequency Override',
|
||||||
|
options: [
|
||||||
|
{ 'label': 'Per Minute', 'value': 'minute' },
|
||||||
|
{ 'label': 'Hourly', 'value': 'hour' },
|
||||||
|
{ 'label': 'Daily', 'value': 'day' },
|
||||||
|
{ 'label': 'Weekly', 'value': 'week' },
|
||||||
|
{ 'label': 'Monthly', 'value': 'month' },
|
||||||
|
{ 'label': 'Yearly', 'value': 'year' }
|
||||||
|
],
|
||||||
|
value: 'month',
|
||||||
|
no_option: { 'label': '-- Use Product Default --', 'value': '' }
|
||||||
|
} %}
|
||||||
|
{% include 'lib/inputs/select.twig' with {
|
||||||
|
id: 'state',
|
||||||
|
name: 'state',
|
||||||
|
label: 'Initial State',
|
||||||
|
options: [
|
||||||
|
{ 'label': 'Trial', 'value': 'TRIAL' },
|
||||||
|
{ 'label': 'Start', 'value': 'START' },
|
||||||
|
{ 'label': 'Renewal', 'value': 'RENEWAL' }
|
||||||
|
],
|
||||||
|
value: 'START'
|
||||||
|
} %}
|
||||||
|
{% include 'lib/buttons/submit.twig' with {
|
||||||
|
label: 'Create Subscription',
|
||||||
|
} %}
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
213
src/views/admin/subscriptions/index.twig
Normal file
213
src/views/admin/subscriptions/index.twig
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
<section class="flex flex-col gap-4">
|
||||||
|
{% include 'lib/alert.twig' %}
|
||||||
|
|
||||||
|
<a href="/admin/subscriptions/add">
|
||||||
|
{% include 'lib/buttons/submit.twig' with {
|
||||||
|
label: 'Create a New Subscription',
|
||||||
|
} %}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<h3 class="text-2xl font-semibold">
|
||||||
|
Revenue
|
||||||
|
</h3>
|
||||||
|
<table class="min-w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="py-2">
|
||||||
|
Metric
|
||||||
|
</th>
|
||||||
|
<th class="py-2">
|
||||||
|
Value
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
Active Subscriptions
|
||||||
|
</td>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
{{ active_count }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
Total Revenue (Cents)
|
||||||
|
</td>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
{{ revenue.total_cents }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
Total Revenue (Sats)
|
||||||
|
</td>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
{{ revenue.total_sats }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
Total Completed
|
||||||
|
</td>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
{{ revenue.total_count }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3 class="text-2xl font-semibold">
|
||||||
|
Projected Revenue (No New Signups, No Cancelations)
|
||||||
|
</h3>
|
||||||
|
<table class="min-w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="py-2">
|
||||||
|
Period
|
||||||
|
</th>
|
||||||
|
<th class="py-2">
|
||||||
|
Cents
|
||||||
|
</th>
|
||||||
|
<th class="py-2">
|
||||||
|
Sats
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for period, totals in projected_revenue %}
|
||||||
|
<tr>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
{{ period }}
|
||||||
|
</td>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
{{ totals.cents }}
|
||||||
|
</td>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
{{ totals.sats }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3 class="text-2xl font-semibold">
|
||||||
|
Upcoming Events
|
||||||
|
</h3>
|
||||||
|
<table class="min-w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="py-2">
|
||||||
|
ID
|
||||||
|
</th>
|
||||||
|
<th class="py-2">
|
||||||
|
User
|
||||||
|
</th>
|
||||||
|
<th class="py-2">
|
||||||
|
Product
|
||||||
|
</th>
|
||||||
|
<th class="py-2">
|
||||||
|
State
|
||||||
|
</th>
|
||||||
|
<th class="py-2">
|
||||||
|
Renews At
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% if upcoming is not empty %}
|
||||||
|
{% for sub in upcoming %}
|
||||||
|
<tr>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
{{ sub.subscription_id }}
|
||||||
|
</td>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
{{ sub.user_email }}
|
||||||
|
</td>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
{{ sub.product_title }}
|
||||||
|
</td>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
{{ sub.state }}
|
||||||
|
</td>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
{{ sub.renews_at }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td class="border px-4 py-2" colspan="5">
|
||||||
|
No upcoming subscription events.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3 class="text-2xl font-semibold">
|
||||||
|
Recent Subscription Events
|
||||||
|
</h3>
|
||||||
|
<table class="min-w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="py-2">
|
||||||
|
ID
|
||||||
|
</th>
|
||||||
|
<th class="py-2">
|
||||||
|
User
|
||||||
|
</th>
|
||||||
|
<th class="py-2">
|
||||||
|
Product
|
||||||
|
</th>
|
||||||
|
<th class="py-2">
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th class="py-2">
|
||||||
|
State
|
||||||
|
</th>
|
||||||
|
<th class="py-2">
|
||||||
|
Start Date
|
||||||
|
</th>
|
||||||
|
<th class="py-2">
|
||||||
|
Renews At
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% if recent is not empty %}
|
||||||
|
{% for sub in recent %}
|
||||||
|
<tr>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
{{ sub.subscription_id }}
|
||||||
|
</td>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
{{ sub.user_email }}
|
||||||
|
</td>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
{{ sub.product_title }}
|
||||||
|
</td>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
{{ sub.status }}
|
||||||
|
</td>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
{{ sub.state }}
|
||||||
|
</td>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
{{ sub.start_date }}
|
||||||
|
</td>
|
||||||
|
<td class="border px-4 py-2">
|
||||||
|
{{ sub.renews_at }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td class="border px-4 py-2" colspan="7">
|
||||||
|
No subscription events yet.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue