Files
gh-christianlouis-docuelevate/frontend/templates/subscription.html
T
copilot-swe-agent[bot] 699a93e193 chore: merge main into branch, resolve conflict in models.py
Both sets of UserProfile columns are retained:
- is_complimentary (from main, migration 019_add_is_complimentary)
- subscription_change_pending_tier / subscription_change_pending_date
  (our branch, renamed to migration 020_add_subscription_change_pending
   with down_revision updated to chain after 019_add_is_complimentary)
2026-03-07 20:53:42 +00:00

355 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}My Subscription DocuElevate{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8 max-w-4xl">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 flex items-center gap-3">
<i class="fas fa-layer-group text-indigo-500" aria-hidden="true"></i>
My Subscription
</h1>
<p class="text-gray-500 text-sm mt-1">Your current plan, usage and available upgrades.</p>
</div>
<!-- Flash messages -->
<div id="flash-container" aria-live="polite" aria-atomic="true"></div>
{% if not multi_user_enabled %}
<!-- Single-user notice -->
<div class="bg-blue-50 border border-blue-200 rounded-xl p-6 flex items-start gap-4">
<i class="fas fa-info-circle text-blue-500 text-2xl flex-shrink-0 mt-0.5" aria-hidden="true"></i>
<div>
<p class="font-semibold text-blue-800">Multi-user mode is not enabled.</p>
<p class="text-blue-700 text-sm mt-1">
Subscription tiers apply when multi-user mode is active. Your instance currently runs in
single-user mode with no processing limits.
An admin can enable multi-user mode via <a href="/settings" class="underline hover:text-blue-900">Settings</a>.
</p>
</div>
</div>
{% else %}
<!-- Pending-change banner -->
{% if pending_tier_id %}
<div class="bg-amber-50 border border-amber-300 rounded-xl p-4 mb-6 flex items-start gap-3">
<i class="fas fa-clock text-amber-500 text-xl flex-shrink-0 mt-0.5" aria-hidden="true"></i>
<div class="flex-1">
<p class="font-semibold text-amber-800">Pending plan change scheduled</p>
<p class="text-amber-700 text-sm mt-1">
Your plan will change to <strong>{{ pending_tier.name }}</strong>
on <strong>{% if pending_date %}{{ pending_date.strftime('%B') }} {{ pending_date.day }}, {{ pending_date.year }}{% endif %}</strong>.
You keep all current plan benefits until then.
</p>
</div>
<button type="button" onclick="cancelPendingChange()"
class="ml-auto flex-shrink-0 text-sm text-amber-700 underline hover:text-amber-900 font-medium"
aria-label="Cancel pending plan change">
Cancel change
</button>
</div>
{% endif %}
<!-- Current plan card -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<!-- Plan badge -->
<div class="bg-white rounded-xl shadow p-6 flex flex-col items-center text-center
{% if tier_id == 'professional' %}ring-2 ring-indigo-500{% endif %}">
<div class="h-14 w-14 rounded-full
{% if tier_id == 'free' %}bg-gray-100{% elif tier_id == 'starter' %}bg-blue-100{% elif tier_id == 'professional' %}bg-indigo-100{% else %}bg-purple-100{% endif %}
flex items-center justify-center mb-3">
<i class="fas
{% if tier_id == 'free' %}fa-seedling text-gray-500
{% elif tier_id == 'starter' %}fa-rocket text-blue-600
{% elif tier_id == 'professional' %}fa-star text-indigo-600
{% else %}fa-bolt text-purple-600{% endif %}
text-2xl" aria-hidden="true"></i>
</div>
<p class="text-xs font-semibold text-gray-400 uppercase tracking-widest">Current Plan</p>
<p class="text-2xl font-extrabold text-gray-900 mt-1">{{ tier.name }}</p>
<p class="text-gray-500 text-sm mt-1">{{ tier.tagline }}</p>
{% if tier.price_monthly > 0 %}
<p class="mt-3 text-lg font-bold text-indigo-600">${{ tier.price_monthly }}<span class="text-sm font-normal text-gray-400">/month</span></p>
{% else %}
<p class="mt-3 text-lg font-bold text-gray-500">Free</p>
{% endif %}
{% if period_start %}
<p class="text-xs text-gray-400 mt-2">
<i class="fas fa-calendar-alt mr-1" aria-hidden="true"></i>
Since {{ period_start.strftime('%b') }} {{ period_start.day }}, {{ period_start.year }}
</p>
{% endif %}
</div>
<!-- Usage this period -->
{% if usage %}
<div class="md:col-span-2 bg-white rounded-xl shadow p-6">
<h2 class="text-sm font-semibold text-gray-500 uppercase tracking-widest mb-4">Usage</h2>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<!-- Lifetime -->
<div class="text-center">
<p class="text-3xl font-extrabold text-gray-900">{{ usage.lifetime }}</p>
<p class="text-xs text-gray-500 mt-1">Total files (lifetime)</p>
{% if tier.lifetime_file_limit > 0 %}
<div class="mt-2">
{% set lifetime_pct = ((usage.lifetime / tier.lifetime_file_limit) * 100) | int %}
<div class="w-full bg-gray-200 rounded-full h-1.5" role="progressbar" aria-valuemin="0"
aria-valuenow="{{ usage.lifetime }}" aria-valuemax="{{ tier.lifetime_file_limit }}">
<div class="h-1.5 rounded-full
{% if lifetime_pct >= 90 %}bg-red-500{% elif lifetime_pct >= 70 %}bg-yellow-500{% else %}bg-green-500{% endif %}"
style="width: {{ [lifetime_pct, 100] | min }}%"></div>
</div>
<p class="text-xs text-gray-400 mt-1">{{ usage.lifetime }} / {{ tier.lifetime_file_limit }}</p>
</div>
{% else %}
<p class="text-xs text-green-600 mt-1">Unlimited</p>
{% endif %}
</div>
<!-- Today -->
<div class="text-center">
<p class="text-3xl font-extrabold text-gray-900">{{ usage.today }}</p>
<p class="text-xs text-gray-500 mt-1">Files today</p>
{% if tier.daily_upload_limit > 0 %}
<div class="mt-2">
{% set today_pct = ((usage.today / tier.daily_upload_limit) * 100) | int %}
<div class="w-full bg-gray-200 rounded-full h-1.5" role="progressbar" aria-valuemin="0"
aria-valuenow="{{ usage.today }}" aria-valuemax="{{ tier.daily_upload_limit }}">
<div class="h-1.5 rounded-full
{% if today_pct >= 90 %}bg-red-500{% elif today_pct >= 70 %}bg-yellow-500{% else %}bg-blue-500{% endif %}"
style="width: {{ [today_pct, 100] | min }}%"></div>
</div>
<p class="text-xs text-gray-400 mt-1">{{ usage.today }} / {{ tier.daily_upload_limit }} today</p>
</div>
{% else %}
<p class="text-xs text-green-600 mt-1">Unlimited</p>
{% endif %}
</div>
<!-- This month -->
<div class="text-center">
<p class="text-3xl font-extrabold text-gray-900">{{ usage.month }}</p>
<p class="text-xs text-gray-500 mt-1">Files this month</p>
{% if tier.monthly_upload_limit > 0 %}
<div class="mt-2">
{% set month_pct = ((usage.month / tier.monthly_upload_limit) * 100) | int %}
<div class="w-full bg-gray-200 rounded-full h-1.5" role="progressbar" aria-valuemin="0"
aria-valuenow="{{ usage.month }}" aria-valuemax="{{ tier.monthly_upload_limit }}">
<div class="h-1.5 rounded-full
{% if month_pct >= 90 %}bg-red-500{% elif month_pct >= 70 %}bg-yellow-500{% else %}bg-indigo-500{% endif %}"
style="width: {{ [month_pct, 100] | min }}%"></div>
</div>
<p class="text-xs text-gray-400 mt-1">{{ usage.month }} / {{ tier.monthly_upload_limit }} this month</p>
</div>
{% else %}
<p class="text-xs text-green-600 mt-1">Unlimited</p>
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>
<!-- Plan features included -->
<div class="bg-white rounded-xl shadow p-6 mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<i class="fas fa-list-check text-green-500" aria-hidden="true"></i>
What's included in your plan
</h2>
<ul class="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm text-gray-600">
{% for feature in tier.features %}
<li class="flex items-start gap-2">
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0" aria-hidden="true"></i>
<span>{{ feature }}</span>
</li>
{% endfor %}
</ul>
</div>
<!-- All available plans -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<i class="fas fa-layer-group text-indigo-500" aria-hidden="true"></i>
Available Plans
</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{% for t in all_tiers %}
{% set t_rank = tier_order.index(t.id) %}
{% set current_rank = tier_order.index(tier_id) %}
{% set is_current = (t.id == tier_id) %}
{% set is_pending = (t.id == pending_tier_id) %}
{% set is_upgrade = (t_rank > current_rank) %}
{% set is_downgrade = (t_rank < current_rank) %}
<div class="bg-white rounded-xl border-2
{% if is_current %}border-indigo-500 ring-2 ring-indigo-200{% elif t.highlight %}border-indigo-300{% else %}border-gray-200{% endif %}
p-5 flex flex-col justify-between hover:shadow-md transition relative">
{% if is_current %}
<span class="absolute top-3 right-3 text-xs bg-indigo-100 text-indigo-700 font-semibold rounded-full px-2 py-0.5">
Current
</span>
{% elif is_pending %}
<span class="absolute top-3 right-3 text-xs bg-amber-100 text-amber-700 font-semibold rounded-full px-2 py-0.5">
Pending
</span>
{% elif t.badge %}
<span class="absolute top-3 right-3 text-xs bg-indigo-100 text-indigo-700 font-semibold rounded-full px-2 py-0.5">
{{ t.badge }}
</span>
{% endif %}
<div>
<p class="font-bold text-gray-900 pr-16">{{ t.name }}</p>
<p class="text-gray-500 text-xs mt-1 mb-3">{{ t.tagline }}</p>
{% if t.price_monthly > 0 %}
<p class="text-xl font-extrabold text-gray-900">${{ t.price_monthly }}<span class="text-sm font-normal text-gray-400">/mo</span></p>
{% else %}
<p class="text-xl font-extrabold text-gray-500">Free</p>
{% endif %}
<ul class="mt-3 space-y-1 text-xs text-gray-600">
{% for feature in t.features[:3] %}
<li class="flex items-start gap-1.5">
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0" aria-hidden="true"></i>
{{ feature }}
</li>
{% endfor %}
{% if t.features | length > 3 %}
<li class="text-indigo-500 font-medium">+ {{ (t.features | length) - 3 }} more</li>
{% endif %}
</ul>
</div>
<div class="mt-4">
{% if is_current and not pending_tier_id %}
<span class="block text-center py-2 px-4 rounded-lg bg-gray-100 text-gray-400 text-sm font-semibold cursor-default">
Your current plan
</span>
{% elif is_current and pending_tier_id %}
<!-- Keep current plan (cancel pending downgrade) -->
<button type="button"
onclick="changePlan('{{ t.id }}')"
class="block w-full text-center py-2 px-4 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold transition">
Keep {{ t.name }}
</button>
{% elif is_upgrade %}
{% if t.id == 'business' %}
<a href="mailto:sales@docuelevate.io?subject=Business+Plan+Enquiry"
class="block text-center py-2 px-4 rounded-lg bg-gray-800 hover:bg-gray-700 text-white text-sm font-semibold transition">
Contact Sales
</a>
{% else %}
<button type="button"
onclick="changePlan('{{ t.id }}')"
class="block w-full text-center py-2 px-4 rounded-lg bg-green-600 hover:bg-green-700 text-white text-sm font-semibold transition">
Upgrade to {{ t.name }}
</button>
{% endif %}
{% elif is_pending %}
<button type="button"
onclick="cancelPendingChange()"
class="block w-full text-center py-2 px-4 rounded-lg bg-amber-100 hover:bg-amber-200 text-amber-800 text-sm font-semibold transition">
Cancel scheduled change
</button>
{% else %}
<!-- Downgrade -->
<button type="button"
onclick="changePlan('{{ t.id }}')"
class="block w-full text-center py-2 px-4 rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-700 text-sm font-semibold transition">
Downgrade to {{ t.name }}
</button>
{% endif %}
</div>
</div>
{% endfor %}
</div>
<p class="text-xs text-gray-400 mt-3">
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i>
Upgrades take effect immediately. Downgrades are scheduled for the end of your current billing period.
</p>
</div>
{% endif %}
<!-- Back link -->
<div class="mt-8">
<a href="/" class="text-sm text-indigo-600 hover:text-indigo-800 font-medium flex items-center gap-1">
<i class="fas fa-arrow-left text-xs" aria-hidden="true"></i> Back to Dashboard
</a>
</div>
</div>
<script>
const CSRF_TOKEN = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
function showFlash(message, type) {
const container = document.getElementById('flash-container');
let bgClass, borderClass, iconClass, textClass;
if (type === 'success') {
bgClass = 'bg-green-50'; borderClass = 'border-green-300';
iconClass = 'fa-check-circle text-green-500'; textClass = 'text-green-800';
} else if (type === 'warning') {
bgClass = 'bg-amber-50'; borderClass = 'border-amber-300';
iconClass = 'fa-clock text-amber-500'; textClass = 'text-amber-800';
} else {
bgClass = 'bg-red-50'; borderClass = 'border-red-300';
iconClass = 'fa-exclamation-circle text-red-500'; textClass = 'text-red-800';
}
container.innerHTML = `
<div class="${bgClass} border ${borderClass} rounded-xl p-4 mb-6 flex items-start gap-3" role="alert">
<i class="fas ${iconClass} text-xl flex-shrink-0 mt-0.5" aria-hidden="true"></i>
<p class="${textClass} text-sm">${message}</p>
</div>`;
container.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
async function changePlan(planId) {
try {
const res = await fetch('/api/subscriptions/change', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': CSRF_TOKEN,
},
body: JSON.stringify({ plan_id: planId, billing_cycle: 'monthly' }),
});
const data = await res.json();
if (!res.ok) {
showFlash(data.detail || 'An error occurred. Please try again.', 'error');
return;
}
const msgType = data.immediate ? 'success' : 'warning';
showFlash(data.message, msgType);
// Reload after a short delay to reflect the change
setTimeout(() => window.location.reload(), 1800);
} catch (err) {
showFlash('Network error. Please try again.', 'error');
}
}
async function cancelPendingChange() {
try {
const res = await fetch('/api/subscriptions/change', {
method: 'DELETE',
headers: { 'X-CSRF-Token': CSRF_TOKEN },
});
const data = await res.json();
if (!res.ok) {
showFlash(data.detail || 'An error occurred. Please try again.', 'error');
return;
}
showFlash(data.message, 'success');
setTimeout(() => window.location.reload(), 1800);
} catch (err) {
showFlash('Network error. Please try again.', 'error');
}
}
</script>
{% endblock %}