Phase 6 - Database-driven tiers: - Update subscription_service to query database first with legacy fallback - Add get_tier_info() db parameter and _get_tier_from_legacy() method Phase 7 - Platform health integration: - Add get_subscription_capacity() for theoretical vs actual capacity - Include subscription capacity in full health report Phase 8 - Background subscription tasks: - Add reset_period_counters() for billing period resets - Add check_trial_expirations() for trial management - Add sync_stripe_status() for Stripe synchronization - Add cleanup_stale_subscriptions() for maintenance - Add capture_capacity_snapshot() for daily metrics Phase 10 - Capacity planning & forecasting: - Add CapacitySnapshot model for historical tracking - Create capacity_forecast_service with growth trends - Add /subscription-capacity, /trends, /recommendations endpoints - Add /snapshot endpoint for manual captures Also includes billing API enhancements from phase 4: - Add upcoming-invoice, change-tier, addon purchase/cancel endpoints - Add UsageSummary schema for billing page - Enhance billing.js with addon management functions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
452 lines
26 KiB
HTML
452 lines
26 KiB
HTML
{# app/templates/vendor/billing.html #}
|
|
{% extends "vendor/base.html" %}
|
|
|
|
{% block title %}Billing & Subscription{% endblock %}
|
|
|
|
{% block alpine_data %}billingData(){% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="flex items-center justify-between my-6">
|
|
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
|
|
Billing & Subscription
|
|
</h2>
|
|
</div>
|
|
|
|
<!-- Success/Cancel Messages -->
|
|
<template x-if="showSuccessMessage">
|
|
<div class="mb-6 p-4 bg-green-100 border border-green-400 text-green-700 rounded-lg flex items-center justify-between">
|
|
<div class="flex items-center">
|
|
<span x-html="$icon('check-circle', 'w-5 h-5 mr-2')"></span>
|
|
<span>Your subscription has been updated successfully!</span>
|
|
</div>
|
|
<button @click="showSuccessMessage = false" class="text-green-700 hover:text-green-900">
|
|
<span x-html="$icon('x-mark', 'w-5 h-5')"></span>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="showCancelMessage">
|
|
<div class="mb-6 p-4 bg-yellow-100 border border-yellow-400 text-yellow-700 rounded-lg flex items-center justify-between">
|
|
<div class="flex items-center">
|
|
<span x-html="$icon('exclamation-triangle', 'w-5 h-5 mr-2')"></span>
|
|
<span>Checkout was cancelled. No changes were made to your subscription.</span>
|
|
</div>
|
|
<button @click="showCancelMessage = false" class="text-yellow-700 hover:text-yellow-900">
|
|
<span x-html="$icon('x-mark', 'w-5 h-5')"></span>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="showAddonSuccessMessage">
|
|
<div class="mb-6 p-4 bg-green-100 border border-green-400 text-green-700 rounded-lg flex items-center justify-between">
|
|
<div class="flex items-center">
|
|
<span x-html="$icon('check-circle', 'w-5 h-5 mr-2')"></span>
|
|
<span>Add-on purchased successfully!</span>
|
|
</div>
|
|
<button @click="showAddonSuccessMessage = false" class="text-green-700 hover:text-green-900">
|
|
<span x-html="$icon('x-mark', 'w-5 h-5')"></span>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Loading State -->
|
|
<template x-if="loading">
|
|
<div class="flex justify-center items-center py-12">
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-purple-600"></div>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="!loading">
|
|
<div class="grid gap-6 md:grid-cols-2 xl:grid-cols-3">
|
|
<!-- Current Plan Card -->
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Current Plan</h3>
|
|
<span :class="{
|
|
'bg-green-100 text-green-800': subscription?.status === 'active',
|
|
'bg-yellow-100 text-yellow-800': subscription?.status === 'trial',
|
|
'bg-red-100 text-red-800': subscription?.status === 'past_due' || subscription?.status === 'cancelled',
|
|
'bg-gray-100 text-gray-800': !['active', 'trial', 'past_due', 'cancelled'].includes(subscription?.status)
|
|
}" class="px-2 py-1 text-xs font-semibold rounded-full">
|
|
<span x-text="subscription?.status?.replace('_', ' ')?.toUpperCase() || 'INACTIVE'"></span>
|
|
</span>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<div class="text-3xl font-bold text-purple-600 dark:text-purple-400" x-text="subscription?.tier_name || 'No Plan'"></div>
|
|
<template x-if="subscription?.is_trial">
|
|
<p class="text-sm text-yellow-600 dark:text-yellow-400 mt-1">
|
|
Trial ends <span x-text="formatDate(subscription?.trial_ends_at)"></span>
|
|
</p>
|
|
</template>
|
|
<template x-if="subscription?.cancelled_at">
|
|
<p class="text-sm text-red-600 dark:text-red-400 mt-1">
|
|
Cancels on <span x-text="formatDate(subscription?.period_end)"></span>
|
|
</p>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
|
<template x-if="subscription?.period_end && !subscription?.cancelled_at">
|
|
<p>
|
|
Next billing: <span class="font-medium text-gray-800 dark:text-gray-200" x-text="formatDate(subscription?.period_end)"></span>
|
|
</p>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="mt-6 space-y-2">
|
|
<template x-if="subscription?.stripe_customer_id">
|
|
<button @click="openPortal()"
|
|
class="w-full px-4 py-2 text-sm font-medium text-purple-600 bg-purple-100 rounded-lg hover:bg-purple-200 dark:bg-purple-900 dark:text-purple-300">
|
|
Manage Payment Method
|
|
</button>
|
|
</template>
|
|
<template x-if="subscription?.cancelled_at">
|
|
<button @click="reactivate()"
|
|
class="w-full px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700">
|
|
Reactivate Subscription
|
|
</button>
|
|
</template>
|
|
<template x-if="!subscription?.cancelled_at && subscription?.status === 'active'">
|
|
<button @click="showCancelModal = true"
|
|
class="w-full px-4 py-2 text-sm font-medium text-red-600 bg-red-100 rounded-lg hover:bg-red-200 dark:bg-red-900 dark:text-red-300">
|
|
Cancel Subscription
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Usage Summary Card -->
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
|
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-4">Usage This Period</h3>
|
|
|
|
<!-- Orders Usage -->
|
|
<div class="mb-4">
|
|
<div class="flex justify-between text-sm mb-1">
|
|
<span class="text-gray-600 dark:text-gray-400">Orders</span>
|
|
<span class="font-medium text-gray-800 dark:text-gray-200">
|
|
<span x-text="subscription?.orders_this_period || 0"></span>
|
|
<span x-text="subscription?.orders_limit ? ` / ${subscription.orders_limit}` : ' (Unlimited)'"></span>
|
|
</span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
|
|
<div class="bg-purple-600 h-2 rounded-full transition-all duration-300"
|
|
:style="`width: ${subscription?.orders_limit ? Math.min(100, (subscription.orders_this_period / subscription.orders_limit) * 100) : 0}%`"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Products Usage -->
|
|
<div class="mb-4">
|
|
<div class="flex justify-between text-sm mb-1">
|
|
<span class="text-gray-600 dark:text-gray-400">Products</span>
|
|
<span class="font-medium text-gray-800 dark:text-gray-200">
|
|
<span x-text="subscription?.products_count || 0"></span>
|
|
<span x-text="subscription?.products_limit ? ` / ${subscription.products_limit}` : ' (Unlimited)'"></span>
|
|
</span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
|
|
<div class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
|
:style="`width: ${subscription?.products_limit ? Math.min(100, (subscription.products_count / subscription.products_limit) * 100) : 0}%`"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Team Usage -->
|
|
<div class="mb-4">
|
|
<div class="flex justify-between text-sm mb-1">
|
|
<span class="text-gray-600 dark:text-gray-400">Team Members</span>
|
|
<span class="font-medium text-gray-800 dark:text-gray-200">
|
|
<span x-text="subscription?.team_count || 0"></span>
|
|
<span x-text="subscription?.team_limit ? ` / ${subscription.team_limit}` : ' (Unlimited)'"></span>
|
|
</span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
|
|
<div class="bg-green-600 h-2 rounded-full transition-all duration-300"
|
|
:style="`width: ${subscription?.team_limit ? Math.min(100, (subscription.team_count / subscription.team_limit) * 100) : 0}%`"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<template x-if="subscription?.last_payment_error">
|
|
<div class="mt-4 p-3 bg-red-100 dark:bg-red-900 rounded-lg">
|
|
<p class="text-sm text-red-700 dark:text-red-300">
|
|
<span x-html="$icon('exclamation-circle', 'w-4 h-4 inline mr-1')"></span>
|
|
Payment issue: <span x-text="subscription.last_payment_error"></span>
|
|
</p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Quick Actions Card -->
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
|
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-4">Quick Actions</h3>
|
|
|
|
<div class="space-y-3">
|
|
<button @click="showTiersModal = true"
|
|
class="w-full flex items-center justify-between px-4 py-3 text-left bg-gray-50 dark:bg-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors">
|
|
<div class="flex items-center">
|
|
<span x-html="$icon('arrow-trending-up', 'w-5 h-5 text-purple-600 mr-3')"></span>
|
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">Change Plan</span>
|
|
</div>
|
|
<span x-html="$icon('chevron-right', 'w-5 h-5 text-gray-400')"></span>
|
|
</button>
|
|
|
|
<button @click="showAddonsModal = true"
|
|
class="w-full flex items-center justify-between px-4 py-3 text-left bg-gray-50 dark:bg-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors">
|
|
<div class="flex items-center">
|
|
<span x-html="$icon('puzzle-piece', 'w-5 h-5 text-blue-600 mr-3')"></span>
|
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">Add-ons</span>
|
|
</div>
|
|
<span x-html="$icon('chevron-right', 'w-5 h-5 text-gray-400')"></span>
|
|
</button>
|
|
|
|
<a :href="`/vendor/${vendorCode}/invoices`"
|
|
class="w-full flex items-center justify-between px-4 py-3 text-left bg-gray-50 dark:bg-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors">
|
|
<div class="flex items-center">
|
|
<span x-html="$icon('document-text', 'w-5 h-5 text-green-600 mr-3')"></span>
|
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">View Invoices</span>
|
|
</div>
|
|
<span x-html="$icon('chevron-right', 'w-5 h-5 text-gray-400')"></span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Invoice History Section -->
|
|
<div class="mt-8 bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
|
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-4">Recent Invoices</h3>
|
|
|
|
<template x-if="invoices.length === 0">
|
|
<p class="text-gray-500 dark:text-gray-400 text-center py-8">No invoices yet</p>
|
|
</template>
|
|
|
|
<template x-if="invoices.length > 0">
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full whitespace-nowrap">
|
|
<thead>
|
|
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
|
|
<th class="px-4 py-3">Invoice</th>
|
|
<th class="px-4 py-3">Date</th>
|
|
<th class="px-4 py-3">Amount</th>
|
|
<th class="px-4 py-3">Status</th>
|
|
<th class="px-4 py-3">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
|
<template x-for="invoice in invoices.slice(0, 5)" :key="invoice.id">
|
|
<tr class="text-gray-700 dark:text-gray-400">
|
|
<td class="px-4 py-3 text-sm font-medium" x-text="invoice.invoice_number || `#${invoice.id}`"></td>
|
|
<td class="px-4 py-3 text-sm" x-text="formatDate(invoice.invoice_date)"></td>
|
|
<td class="px-4 py-3 text-sm font-medium" x-text="formatCurrency(invoice.total_cents, invoice.currency)"></td>
|
|
<td class="px-4 py-3 text-sm">
|
|
<span :class="{
|
|
'bg-green-100 text-green-800': invoice.status === 'paid',
|
|
'bg-yellow-100 text-yellow-800': invoice.status === 'open',
|
|
'bg-red-100 text-red-800': invoice.status === 'uncollectible'
|
|
}" class="px-2 py-1 text-xs font-semibold rounded-full" x-text="invoice.status.toUpperCase()"></span>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm">
|
|
<template x-if="invoice.pdf_url">
|
|
<a :href="invoice.pdf_url" target="_blank" class="text-purple-600 hover:text-purple-800">
|
|
<span x-html="$icon('arrow-down-tray', 'w-5 h-5')"></span>
|
|
</a>
|
|
</template>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Tiers Modal -->
|
|
<div x-show="showTiersModal"
|
|
x-transition:enter="transition ease-out duration-150"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
x-transition:leave="transition ease-in duration-150"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0"
|
|
class="fixed inset-0 z-30 flex items-center justify-center overflow-auto bg-black bg-opacity-50"
|
|
@click.self="showTiersModal = false">
|
|
<div class="w-full max-w-4xl mx-4 bg-white dark:bg-gray-800 rounded-lg shadow-xl">
|
|
<div class="flex items-center justify-between px-6 py-4 border-b dark:border-gray-700">
|
|
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Choose Your Plan</h3>
|
|
<button @click="showTiersModal = false" class="text-gray-400 hover:text-gray-600">
|
|
<span x-html="$icon('x-mark', 'w-6 h-6')"></span>
|
|
</button>
|
|
</div>
|
|
<div class="p-6">
|
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
<template x-for="tier in tiers" :key="tier.code">
|
|
<div :class="{'ring-2 ring-purple-600': tier.is_current}"
|
|
class="relative p-6 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
<template x-if="tier.is_current">
|
|
<span class="absolute top-0 right-0 px-2 py-1 text-xs font-semibold text-white bg-purple-600 rounded-bl-lg rounded-tr-lg">Current</span>
|
|
</template>
|
|
<h4 class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="tier.name"></h4>
|
|
<p class="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
|
|
<span x-text="formatCurrency(tier.price_monthly_cents, 'EUR')"></span>
|
|
<span class="text-sm font-normal text-gray-500">/mo</span>
|
|
</p>
|
|
<ul class="mt-4 space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
|
<li class="flex items-center">
|
|
<span x-html="$icon('check', 'w-4 h-4 text-green-500 mr-2')"></span>
|
|
<span x-text="tier.orders_per_month ? `${tier.orders_per_month} orders/mo` : 'Unlimited orders'"></span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span x-html="$icon('check', 'w-4 h-4 text-green-500 mr-2')"></span>
|
|
<span x-text="tier.products_limit ? `${tier.products_limit} products` : 'Unlimited products'"></span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span x-html="$icon('check', 'w-4 h-4 text-green-500 mr-2')"></span>
|
|
<span x-text="tier.team_members ? `${tier.team_members} team members` : 'Unlimited team'"></span>
|
|
</li>
|
|
</ul>
|
|
<button @click="selectTier(tier)"
|
|
:disabled="tier.is_current"
|
|
:class="tier.is_current ? 'bg-gray-300 cursor-not-allowed' : 'bg-purple-600 hover:bg-purple-700'"
|
|
class="w-full mt-4 px-4 py-2 text-sm font-medium text-white rounded-lg transition-colors">
|
|
<span x-text="tier.is_current ? 'Current Plan' : (tier.can_upgrade ? 'Upgrade' : 'Downgrade')"></span>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add-ons Modal -->
|
|
<div x-show="showAddonsModal"
|
|
x-transition:enter="transition ease-out duration-150"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
x-transition:leave="transition ease-in duration-150"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0"
|
|
class="fixed inset-0 z-30 flex items-center justify-center overflow-auto bg-black bg-opacity-50"
|
|
@click.self="showAddonsModal = false">
|
|
<div class="w-full max-w-2xl mx-4 bg-white dark:bg-gray-800 rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col">
|
|
<div class="flex items-center justify-between px-6 py-4 border-b dark:border-gray-700">
|
|
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Add-ons</h3>
|
|
<button @click="showAddonsModal = false" class="text-gray-400 hover:text-gray-600">
|
|
<span x-html="$icon('x-mark', 'w-6 h-6')"></span>
|
|
</button>
|
|
</div>
|
|
<div class="p-6 overflow-y-auto">
|
|
<!-- My Active Add-ons -->
|
|
<template x-if="myAddons.length > 0">
|
|
<div class="mb-6">
|
|
<h4 class="text-sm font-semibold text-gray-600 dark:text-gray-400 mb-3 uppercase tracking-wide">Your Active Add-ons</h4>
|
|
<div class="space-y-3">
|
|
<template x-for="addon in myAddons.filter(a => a.status === 'active')" :key="addon.id">
|
|
<div class="flex items-center justify-between p-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-800 rounded-lg">
|
|
<div>
|
|
<h4 class="font-medium text-gray-700 dark:text-gray-200" x-text="addon.addon_name"></h4>
|
|
<template x-if="addon.domain_name">
|
|
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="addon.domain_name"></p>
|
|
</template>
|
|
<p class="text-xs text-gray-400 mt-1">
|
|
<span x-text="addon.period_end ? `Renews ${formatDate(addon.period_end)}` : 'Active'"></span>
|
|
</p>
|
|
</div>
|
|
<button @click="cancelAddon(addon)"
|
|
class="px-3 py-1 text-sm font-medium text-red-600 bg-red-100 rounded-lg hover:bg-red-200 dark:bg-red-900/50 dark:text-red-400">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Available Add-ons -->
|
|
<h4 class="text-sm font-semibold text-gray-600 dark:text-gray-400 mb-3 uppercase tracking-wide">Available Add-ons</h4>
|
|
<template x-if="addons.length === 0">
|
|
<p class="text-gray-500 text-center py-8">No add-ons available</p>
|
|
</template>
|
|
<div class="space-y-3">
|
|
<template x-for="addon in addons" :key="addon.id">
|
|
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
<div>
|
|
<h4 class="font-medium text-gray-700 dark:text-gray-200" x-text="addon.name"></h4>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="addon.description"></p>
|
|
<p class="text-sm font-medium text-purple-600 mt-1">
|
|
<span x-text="formatCurrency(addon.price_cents, 'EUR')"></span>
|
|
<span x-text="`/${addon.billing_period}`"></span>
|
|
</p>
|
|
</div>
|
|
<button @click="purchaseAddon(addon)"
|
|
:disabled="isAddonPurchased(addon.code) || purchasingAddon === addon.code"
|
|
:class="isAddonPurchased(addon.code) ? 'bg-gray-200 text-gray-500 cursor-not-allowed' : 'bg-purple-100 text-purple-600 hover:bg-purple-200'"
|
|
class="px-4 py-2 text-sm font-medium rounded-lg transition-colors">
|
|
<template x-if="purchasingAddon === addon.code">
|
|
<span class="flex items-center">
|
|
<svg class="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
Processing...
|
|
</span>
|
|
</template>
|
|
<template x-if="purchasingAddon !== addon.code">
|
|
<span x-text="isAddonPurchased(addon.code) ? 'Active' : 'Add'"></span>
|
|
</template>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cancel Subscription Modal -->
|
|
<div x-show="showCancelModal"
|
|
x-transition:enter="transition ease-out duration-150"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
x-transition:leave="transition ease-in duration-150"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0"
|
|
class="fixed inset-0 z-30 flex items-center justify-center overflow-auto bg-black bg-opacity-50"
|
|
@click.self="showCancelModal = false">
|
|
<div class="w-full max-w-md mx-4 bg-white dark:bg-gray-800 rounded-lg shadow-xl">
|
|
<div class="flex items-center justify-between px-6 py-4 border-b dark:border-gray-700">
|
|
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Cancel Subscription</h3>
|
|
<button @click="showCancelModal = false" class="text-gray-400 hover:text-gray-600">
|
|
<span x-html="$icon('x-mark', 'w-6 h-6')"></span>
|
|
</button>
|
|
</div>
|
|
<div class="p-6">
|
|
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
|
Are you sure you want to cancel your subscription? You'll continue to have access until the end of your current billing period.
|
|
</p>
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Reason for cancelling (optional)
|
|
</label>
|
|
<textarea x-model="cancelReason"
|
|
rows="3"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500"
|
|
placeholder="Tell us why you're leaving..."></textarea>
|
|
</div>
|
|
<div class="flex justify-end space-x-3">
|
|
<button @click="showCancelModal = false"
|
|
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300">
|
|
Keep Subscription
|
|
</button>
|
|
<button @click="cancelSubscription()"
|
|
class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700">
|
|
Cancel Subscription
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script src="/static/vendor/js/billing.js"></script>
|
|
{% endblock %}
|