started implementing stripe

This commit is contained in:
2025-06-26 15:15:16 +02:00
parent 3a0659b63b
commit 9b85f3bb8d
24 changed files with 2025 additions and 103 deletions

View File

@@ -0,0 +1,149 @@
<div class="row">
<div class="col-md-6">
<h6 class="mb-3">Customer Information</h6>
<table class="table table-sm">
<tr>
<td><strong>Name:</strong></td>
<td>{{ customer.name or 'N/A' }}</td>
</tr>
<tr>
<td><strong>Email:</strong></td>
<td>{{ customer.email }}</td>
</tr>
<tr>
<td><strong>Phone:</strong></td>
<td>{{ customer.phone or 'N/A' }}</td>
</tr>
<tr>
<td><strong>Created:</strong></td>
<td>{{ customer.created_at.strftime('%Y-%m-%d %H:%M') if customer.created_at else 'N/A' }}</td>
</tr>
</table>
</div>
<div class="col-md-6">
<h6 class="mb-3">Subscription Information</h6>
<table class="table table-sm">
<tr>
<td><strong>Plan:</strong></td>
<td>
{% if plan %}
<span class="badge bg-primary">{{ plan.name }}</span>
{% else %}
<span class="text-muted">Plan {{ customer.subscription_plan_id or 'N/A' }}</span>
{% endif %}
</td>
</tr>
<tr>
<td><strong>Status:</strong></td>
<td>
{% if customer.subscription_status %}
{% if customer.subscription_status == 'active' %}
<span class="badge bg-success">Active</span>
{% elif customer.subscription_status == 'canceled' %}
<span class="badge bg-danger">Canceled</span>
{% elif customer.subscription_status == 'past_due' %}
<span class="badge bg-warning">Past Due</span>
{% else %}
<span class="badge bg-secondary">{{ customer.subscription_status }}</span>
{% endif %}
{% else %}
<span class="text-muted">No subscription</span>
{% endif %}
</td>
</tr>
<tr>
<td><strong>Billing Cycle:</strong></td>
<td>
{% if customer.subscription_billing_cycle %}
<span class="badge bg-info">{{ customer.subscription_billing_cycle.title() }}</span>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td><strong>Current Period:</strong></td>
<td>
{% if customer.subscription_current_period_start and customer.subscription_current_period_end %}
{{ customer.subscription_current_period_start.strftime('%Y-%m-%d') }} to {{ customer.subscription_current_period_end.strftime('%Y-%m-%d') }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
</table>
</div>
</div>
{% if customer.billing_address_line1 or customer.shipping_address_line1 %}
<div class="row mt-4">
{% if customer.billing_address_line1 %}
<div class="col-md-6">
<h6 class="mb-3">Billing Address</h6>
<address class="mb-0">
{{ customer.billing_address_line1 }}<br>
{% if customer.billing_address_line2 %}{{ customer.billing_address_line2 }}<br>{% endif %}
{% if customer.billing_city %}{{ customer.billing_city }}{% endif %}
{% if customer.billing_state %}, {{ customer.billing_state }}{% endif %}
{% if customer.billing_postal_code %} {{ customer.billing_postal_code }}{% endif %}<br>
{% if customer.billing_country %}{{ customer.billing_country }}{% endif %}
</address>
</div>
{% endif %}
{% if customer.shipping_address_line1 %}
<div class="col-md-6">
<h6 class="mb-3">Shipping Address</h6>
<address class="mb-0">
{{ customer.shipping_address_line1 }}<br>
{% if customer.shipping_address_line2 %}{{ customer.shipping_address_line2 }}<br>{% endif %}
{% if customer.shipping_city %}{{ customer.shipping_city }}{% endif %}
{% if customer.shipping_state %}, {{ customer.shipping_state }}{% endif %}
{% if customer.shipping_postal_code %} {{ customer.shipping_postal_code }}{% endif %}<br>
{% if customer.shipping_country %}{{ customer.shipping_country }}{% endif %}
</address>
</div>
{% endif %}
</div>
{% endif %}
{% if customer.tax_id_type and customer.tax_id_value %}
<div class="row mt-4">
<div class="col-12">
<h6 class="mb-3">Tax Information</h6>
<table class="table table-sm">
<tr>
<td><strong>Tax ID Type:</strong></td>
<td>{{ customer.tax_id_type }}</td>
</tr>
<tr>
<td><strong>Tax ID Value:</strong></td>
<td>{{ customer.tax_id_value }}</td>
</tr>
</table>
</div>
</div>
{% endif %}
{% if customer.stripe_customer_id or customer.stripe_subscription_id %}
<div class="row mt-4">
<div class="col-12">
<h6 class="mb-3">Stripe Information</h6>
<table class="table table-sm">
{% if customer.stripe_customer_id %}
<tr>
<td><strong>Stripe Customer ID:</strong></td>
<td><code>{{ customer.stripe_customer_id }}</code></td>
</tr>
{% endif %}
{% if customer.stripe_subscription_id %}
<tr>
<td><strong>Stripe Subscription ID:</strong></td>
<td><code>{{ customer.stripe_subscription_id }}</code></td>
</tr>
{% endif %}
</table>
</div>
</div>
{% endif %}

View File

@@ -0,0 +1,178 @@
{% extends "common/base.html" %}
{% from "components/header.html" import header %}
{% block title %}Customers - Admin{% endblock %}
{% block content %}
{{ header(
title="Customers",
description="Manage customer information and subscriptions",
icon="fa-users"
) }}
<div class="container-fluid">
{% if customers.items %}
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Plan</th>
<th>Status</th>
<th>Billing Cycle</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for customer in customers.items %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar-sm me-3">
<div class="avatar-title bg-primary rounded-circle">
{{ customer.name[0] if customer.name else customer.email[0] }}
</div>
</div>
<div>
<h6 class="mb-0">{{ customer.name or 'N/A' }}</h6>
</div>
</div>
</td>
<td>{{ customer.email }}</td>
<td>{{ customer.phone or 'N/A' }}</td>
<td>
{% if customer.subscription_plan_id %}
{% set plan = customer.plan %}
{% if plan %}
<span class="badge bg-primary">{{ plan.name }}</span>
{% else %}
<span class="text-muted">Plan {{ customer.subscription_plan_id }}</span>
{% endif %}
{% else %}
<span class="text-muted">No plan</span>
{% endif %}
</td>
<td>
{% if customer.subscription_status %}
{% if customer.subscription_status == 'active' %}
<span class="badge bg-success">Active</span>
{% elif customer.subscription_status == 'canceled' %}
<span class="badge bg-danger">Canceled</span>
{% elif customer.subscription_status == 'past_due' %}
<span class="badge bg-warning">Past Due</span>
{% else %}
<span class="badge bg-secondary">{{ customer.subscription_status }}</span>
{% endif %}
{% else %}
<span class="text-muted">No subscription</span>
{% endif %}
</td>
<td>
{% if customer.subscription_billing_cycle %}
<span class="badge bg-info">{{ customer.subscription_billing_cycle.title() }}</span>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>{{ customer.created_at.strftime('%Y-%m-%d') if customer.created_at else 'N/A' }}</td>
<td>
<button class="btn btn-sm btn-outline-primary"
onclick="viewCustomerDetails({{ customer.id }})">
<i class="fas fa-eye"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if customers.pages > 1 %}
<nav aria-label="Customer pagination">
<ul class="pagination justify-content-center">
{% if customers.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.customers', page=customers.prev_num) }}">Previous</a>
</li>
{% endif %}
{% for page_num in customers.iter_pages() %}
{% if page_num %}
{% if page_num != customers.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.customers', page=page_num) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if customers.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.customers', page=customers.next_num) }}">Next</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
{% else %}
<div class="card">
<div class="card-body text-center py-5">
<i class="fas fa-users fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No customers found</h5>
<p class="text-muted">Customers will appear here once they complete a purchase.</p>
</div>
</div>
{% endif %}
</div>
<!-- Customer Details Modal -->
<div class="modal fade" id="customerDetailsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Customer Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="customerDetailsContent">
<!-- Content will be loaded here -->
</div>
</div>
</div>
</div>
<script>
function viewCustomerDetails(customerId) {
// Load customer details via AJAX
fetch(`/admin/customers/${customerId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('customerDetailsContent').innerHTML = data.html;
new bootstrap.Modal(document.getElementById('customerDetailsModal')).show();
} else {
alert('Failed to load customer details');
}
})
.catch(error => {
console.error('Error:', error);
alert('Failed to load customer details');
});
}
</script>
{% endblock %}

View File

@@ -1,4 +1,5 @@
{% extends "common/base.html" %}
{% from "components/header.html" import header %}
{% block title %}Support Articles - DocuPulse{% endblock %}
@@ -69,18 +70,24 @@
{% endblock %}
{% block content %}
{{ header(
title="Support Articles",
description="Create and manage help articles for users",
icon="fa-life-ring",
buttons=[
{
'text': 'Create New Article',
'url': '#',
'onclick': 'showCreateArticleModal()',
'icon': 'fa-plus',
'class': 'btn-primary'
}
]
) }}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0" style="color: var(--primary-color);">
<i class="fas fa-life-ring me-2"></i>Support Articles
</h1>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createArticleModal">
<i class="fas fa-plus me-2"></i>Create New Article
</button>
</div>
<!-- Articles List -->
<div class="row" id="articlesList">
<!-- Articles will be loaded here via AJAX -->

View File

@@ -0,0 +1,290 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Payment Successful - DocuPulse</title>
<meta name="description" content="Your DocuPulse subscription has been activated successfully. Welcome to the future of document management.">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/colors.css') }}?v={{ 'css/colors.css'|asset_version }}">
<link rel="stylesheet" href="{{ url_for('main.dynamic_colors') }}?v={{ site_settings.updated_at.timestamp() }}" onload="console.log('[CSS] Dynamic colors loaded with version:', '{{ site_settings.updated_at.timestamp() }}')">
<style>
.hero-section {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 120px 0 80px 0;
}
.success-card {
border: none;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
background: var(--white);
overflow: hidden;
}
.success-card .card-header {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
border: none;
padding: 1.5rem;
font-weight: 600;
font-size: 1.1rem;
}
.success-card .card-body {
padding: 2rem;
}
.subscription-detail {
padding: 0.75rem 0;
border-bottom: 1px solid var(--border-color);
}
.subscription-detail:last-child {
border-bottom: none;
}
.subscription-detail strong {
color: var(--text-dark);
font-weight: 600;
}
.next-step-item {
display: flex;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px solid var(--border-color);
}
.next-step-item:last-child {
border-bottom: none;
}
.next-step-item i {
width: 24px;
margin-right: 0.75rem;
color: var(--primary-color);
}
.next-step-item a {
color: var(--text-dark);
text-decoration: none;
transition: color 0.2s ease;
}
.next-step-item a:hover {
color: var(--primary-color);
}
.feature-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
margin-top: 1rem;
}
.feature-item {
text-align: center;
padding: 1.25rem;
background: var(--bg-color);
border-radius: 12px;
}
.feature-icon {
width: 60px;
height: 60px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
margin: 0 auto 1rem;
font-size: 1.25rem;
box-shadow: 0 4px 12px rgba(var(--primary-color-rgb), 0.3);
}
.feature-item h6 {
font-weight: 600;
color: var(--text-dark);
margin-bottom: 0.5rem;
font-size: 0.95rem;
}
.feature-item p {
color: var(--text-muted);
font-size: 0.85rem;
line-height: 1.4;
margin: 0;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border: none;
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
transform: translateY(-2px);
box-shadow: 0 5px 15px var(--primary-opacity-15);
filter: brightness(1.1);
}
.btn-outline-primary {
border: 2px solid var(--primary-color);
color: var(--primary-color);
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-outline-primary:hover {
background: rgba(var(--primary-color-rgb), 0.05);
border-color: var(--primary-color);
color: var(--primary-color);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(var(--primary-color-rgb), 0.1);
}
.section-title {
color: var(--text-dark);
font-weight: 700;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
}
.section-title i {
margin-right: 0.75rem;
color: var(--primary-color);
}
</style>
</head>
<body>
{% include 'components/header_nav.html' %}
<!-- Hero Section -->
<section class="hero-section">
<div class="container">
<div class="row justify-content-center text-center">
<div class="col-lg-8">
<div class="mb-4">
<i class="fas fa-check-circle" style="font-size: 4rem;"></i>
</div>
<h1 class="display-4 fw-bold mb-3">Payment Successful!</h1>
<p class="lead mb-4">Your subscription has been activated and your DocuPulse instance is being set up.</p>
</div>
</div>
</div>
</section>
<!-- Success Details Section -->
<section class="py-5">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="row">
<div class="col-md-6 mb-4">
<div class="card success-card h-100">
<div class="card-header">
<i class="fas fa-check-circle me-2"></i>
Payment Confirmation
</div>
<div class="card-body">
{% if subscription_info %}
<div class="row">
<div class="col-12">
<h5 class="section-title">
<i class="fas fa-receipt"></i>
Subscription Details
</h5>
<div class="subscription-detail">
<strong>Plan:</strong> {{ subscription_info.plan_name }}
</div>
<div class="subscription-detail">
<strong>Billing Cycle:</strong> {{ subscription_info.billing_cycle.title() }}
</div>
<div class="subscription-detail">
<strong>Status:</strong>
<span class="badge bg-success">{{ subscription_info.status.title() }}</span>
</div>
<div class="subscription-detail">
<strong>Amount:</strong> ${{ "%.2f"|format(subscription_info.amount) }} {{ subscription_info.currency.upper() }}
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<h5 class="section-title">
<i class="fas fa-rocket"></i>
Next Steps
</h5>
<div class="next-step-item">
<i class="fas fa-envelope"></i>
<span>Check your email for login credentials</span>
</div>
<div class="next-step-item">
<i class="fas fa-book"></i>
<a href="{{ url_for('public.help_center') }}">Read our getting started guide</a>
</div>
{% if stripe_settings and stripe_settings.customer_portal_url %}
<div class="next-step-item">
<i class="fas fa-credit-card"></i>
<a href="{{ stripe_settings.customer_portal_url }}" target="_blank">Manage your subscription & billing</a>
</div>
{% endif %}
<div class="next-step-item">
<i class="fas fa-headset"></i>
<a href="{{ url_for('public.contact') }}">Contact support if you need help</a>
</div>
</div>
</div>
{% else %}
<div class="text-center">
<h5 class="mb-3">Thank you for your purchase!</h5>
<p class="text-muted">Your payment has been processed successfully.</p>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card success-card h-100">
<div class="card-header">
<i class="fas fa-info-circle me-2"></i>
What happens next?
</div>
<div class="card-body">
<div class="feature-grid">
<div class="feature-item">
<div class="feature-icon">
<i class="fas fa-rocket"></i>
</div>
<h6>Instance Setup</h6>
<p>Your DocuPulse instance will be automatically provisioned within the next few minutes.</p>
</div>
<div class="feature-item">
<div class="feature-icon">
<i class="fas fa-envelope"></i>
</div>
<h6>Welcome Email</h6>
<p>You'll receive an email with your login credentials and setup instructions.</p>
</div>
<div class="feature-item">
<div class="feature-icon">
<i class="fas fa-headset"></i>
</div>
<h6>Support Available</h6>
<p>Our support team is ready to help you get started with DocuPulse.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="text-center mt-4">
<a href="{{ url_for('public.help_center') }}" class="btn btn-primary btn-lg me-3">
<i class="fas fa-question-circle me-2"></i>Get Help
</a>
<a href="{{ url_for('public.contact') }}" class="btn btn-outline-primary btn-lg">
<i class="fas fa-envelope me-2"></i>Contact Support
</a>
</div>
</div>
</div>
</div>
</section>
{% include 'components/footer_nav.html' %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -63,11 +63,9 @@
<li><a class="dropdown-item" href="{{ url_for('main.settings') }}"><i class="fas fa-cog"></i> Settings</a></li>
<li><hr class="dropdown-divider"></li>
{% else %}
{% if not is_master %}
<li><a class="dropdown-item" href="{{ url_for('main.profile') }}"><i class="fas fa-user"></i> Profile</a></li>
{% if current_user.is_admin %}
<li><a class="dropdown-item" href="{{ url_for('main.settings') }}"><i class="fas fa-cog"></i> Settings</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
{% endif %}
{% endif %}
@@ -105,6 +103,11 @@
<i class="fas fa-life-ring"></i> Support Articles
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.customers' %}active{% endif %}" href="{{ url_for('admin.customers') }}">
<i class="fas fa-users"></i> Customers
</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'contacts.contacts_list' %}active{% endif %}" href="{{ url_for('contacts.contacts_list') }}">
@@ -153,6 +156,18 @@
<i class="fas fa-user"></i> Profile
</a>
</li>
{% else %}
<li class="nav-item d-lg-none">
<hr class="my-2">
<a class="nav-link" href="{{ url_for('main.settings') }}">
<i class="fas fa-cog"></i> Settings
</a>
</li>
<li class="nav-item d-lg-none">
<a class="nav-link" href="{{ url_for('main.profile') }}">
<i class="fas fa-user"></i> Profile
</a>
</li>
{% endif %}
<li class="nav-item d-lg-none">
<a class="nav-link" href="{{ url_for('auth.logout') }}">

View File

@@ -8,6 +8,19 @@
{% set pricing_plans = PricingPlan.get_active_plans() %}
{% if pricing_plans %}
<!-- Debug info -->
<div style="display: none;" id="pricing-debug">
<h4>Debug: Pricing Plans Found</h4>
{% for plan in pricing_plans %}
<div>
Plan: {{ plan.name }} (ID: {{ plan.id }})
- Monthly Price ID: {{ plan.stripe_monthly_price_id or 'None' }}
- Annual Price ID: {{ plan.stripe_annual_price_id or 'None' }}
- Is Custom: {{ plan.is_custom }}
</div>
{% endfor %}
</div>
<div class="row g-4 justify-content-center">
{% for plan in pricing_plans %}
<div class="col-md-3">
@@ -53,15 +66,16 @@
{{ plan.button_text }}
</a>
{% else %}
{% if plan.monthly_stripe_link or plan.annual_stripe_link %}
<a href="{{ plan.monthly_stripe_link or plan.annual_stripe_link }}"
class="btn {% if plan.is_popular %}btn-primary{% else %}btn-outline-primary{% endif %} btn-lg w-100 mt-auto px-4 py-3 payment-button"
data-plan-id="{{ plan.id }}"
data-monthly-link="{{ plan.monthly_stripe_link or '' }}"
data-annual-link="{{ plan.annual_stripe_link or '' }}"
target="_blank">
{% if plan.stripe_monthly_price_id or plan.stripe_annual_price_id %}
<button class="btn {% if plan.is_popular %}btn-primary{% else %}btn-outline-primary{% endif %} btn-lg w-100 mt-auto px-4 py-3 checkout-button"
data-plan-id="{{ plan.id }}"
data-monthly-product-id="{{ plan.stripe_monthly_price_id or '' }}"
data-annual-product-id="{{ plan.stripe_annual_price_id or '' }}"
data-plan-name="{{ plan.name }}"
data-monthly-price="{{ plan.monthly_price or 0 }}"
data-annual-price="{{ plan.annual_price or 0 }}">
{{ plan.button_text }}
</a>
</button>
{% else %}
<a href="{{ contact_url }}" class="btn {% if plan.is_popular %}btn-primary{% else %}btn-outline-primary{% endif %} btn-lg w-100 mt-auto px-4 py-3">
{{ plan.button_text }}
@@ -98,7 +112,7 @@
<div class="display-4 fw-bold mb-3" style="color: var(--primary-color);">
<span class="monthly-price">€29</span>
<span class="annual-price" style="display: none;">€23</span>
<span class="fs-6 text-muted">/month</span>
<span class="fs-6 text-muted price-period">/month</span>
</div>
<ul class="list-unstyled mb-4">
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Up to 5 rooms</li>
@@ -125,7 +139,7 @@
<div class="display-4 fw-bold mb-3" style="color: var(--primary-color);">
<span class="monthly-price">€99</span>
<span class="annual-price" style="display: none;">€79</span>
<span class="fs-6 text-muted">/month</span>
<span class="fs-6 text-muted price-period">/month</span>
</div>
<ul class="list-unstyled mb-4">
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Up to 25 rooms</li>
@@ -147,7 +161,7 @@
<div class="display-4 fw-bold mb-3" style="color: var(--primary-color);">
<span class="monthly-price">€299</span>
<span class="annual-price" style="display: none;">€239</span>
<span class="fs-6 text-muted">/month</span>
<span class="fs-6 text-muted price-period">/month</span>
</div>
<ul class="list-unstyled mb-4">
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Up to 100 rooms</li>
@@ -197,11 +211,31 @@
<script>
document.addEventListener('DOMContentLoaded', function() {
// Debug: Log pricing plans info
console.log('=== PRICING DEBUG INFO ===');
const checkoutButtons = document.querySelectorAll('.checkout-button');
console.log('Found checkout buttons:', checkoutButtons.length);
checkoutButtons.forEach((button, index) => {
console.log(`Button ${index + 1}:`, {
planId: button.getAttribute('data-plan-id'),
monthlyProductId: button.getAttribute('data-monthly-product-id'),
annualProductId: button.getAttribute('data-annual-product-id'),
planName: button.getAttribute('data-plan-name'),
monthlyPrice: button.getAttribute('data-monthly-price'),
annualPrice: button.getAttribute('data-annual-price')
});
});
// Show debug info if needed (uncomment to show)
// document.getElementById('pricing-debug').style.display = 'block';
const billingToggle = document.getElementById('annualBilling');
if (!billingToggle) return;
const monthlyPrices = document.querySelectorAll('.monthly-price');
const annualPrices = document.querySelectorAll('.annual-price');
const pricePeriods = document.querySelectorAll('.price-period');
// Add CSS for switch styling
const style = document.createElement('style');
@@ -247,6 +281,89 @@ document.addEventListener('DOMContentLoaded', function() {
requestAnimationFrame(updateNumber);
}
// Function to handle Stripe checkout
async function handleCheckout(planId, billingCycle) {
console.log('handleCheckout called with:', { planId, billingCycle });
try {
const requestBody = {
plan_id: planId,
billing_cycle: billingCycle
};
console.log('Sending request to /api/create-checkout-session with body:', requestBody);
const response = await fetch('/api/create-checkout-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
});
console.log('Response status:', response.status);
console.log('Response ok:', response.ok);
if (!response.ok) {
const errorText = await response.text();
console.error('Response error text:', errorText);
throw new Error(`Failed to create checkout session: ${response.status} ${errorText}`);
}
const data = await response.json();
console.log('Response data:', data);
if (data.checkout_url) {
console.log('Redirecting to checkout URL:', data.checkout_url);
window.location.href = data.checkout_url;
} else {
console.error('No checkout URL received in response');
alert('Failed to create checkout session. Please try again.');
}
} catch (error) {
console.error('Checkout error:', error);
alert('Failed to start checkout. Please try again.');
}
}
// Add click handlers for checkout buttons
document.querySelectorAll('.checkout-button').forEach(button => {
console.log('Adding click handler to checkout button:', button);
button.addEventListener('click', function() {
console.log('Checkout button clicked!');
console.log('Button data attributes:', {
planId: this.getAttribute('data-plan-id'),
monthlyProductId: this.getAttribute('data-monthly-product-id'),
annualProductId: this.getAttribute('data-annual-product-id'),
planName: this.getAttribute('data-plan-name'),
monthlyPrice: this.getAttribute('data-monthly-price'),
annualPrice: this.getAttribute('data-annual-price')
});
const planId = this.getAttribute('data-plan-id');
const monthlyProductId = this.getAttribute('data-monthly-product-id');
const annualProductId = this.getAttribute('data-annual-product-id');
const planName = this.getAttribute('data-plan-name');
const monthlyPrice = parseFloat(this.getAttribute('data-monthly-price'));
const annualPrice = parseFloat(this.getAttribute('data-annual-price'));
// Determine which billing cycle to use based on billing toggle
const isAnnual = billingToggle.checked;
const billingCycle = isAnnual ? 'annual' : 'monthly';
console.log('Billing toggle state:', { isAnnual, billingCycle });
console.log('Plan ID:', planId);
if (planId) {
console.log('Calling handleCheckout with planId and billingCycle');
handleCheckout(planId, billingCycle);
} else {
console.log('No plan ID found, redirecting to contact form');
// Fallback to contact form if no plan configured
window.location.href = '{{ contact_url }}';
}
});
});
billingToggle.addEventListener('change', function() {
if (this.checked) {
// Switch to annual prices with animation
@@ -261,11 +378,10 @@ document.addEventListener('DOMContentLoaded', function() {
animateNumber(price, monthlyValue, annualValue);
});
// Update payment links to annual
document.querySelectorAll('.payment-button').forEach(button => {
const annualLink = button.getAttribute('data-annual-link');
if (annualLink) {
button.href = annualLink;
// Update price periods to show "/year"
document.querySelectorAll('.fs-6.text-muted, .price-period').forEach(period => {
if (period.textContent.includes('/month')) {
period.textContent = '/year';
}
});
} else {
@@ -278,15 +394,13 @@ document.addEventListener('DOMContentLoaded', function() {
animateNumber(price, currentValue, originalMonthlyValue);
});
// Update payment links to monthly
document.querySelectorAll('.payment-button').forEach(button => {
const monthlyLink = button.getAttribute('data-monthly-link');
if (monthlyLink) {
button.href = monthlyLink;
// Update price periods to show "/month"
document.querySelectorAll('.fs-6.text-muted, .price-period').forEach(period => {
if (period.textContent.includes('/year')) {
period.textContent = '/month';
}
});
}
});
});
</script>
</script>

View File

@@ -140,7 +140,7 @@
{% if is_master %}
<!-- Connections Tab -->
<div class="tab-pane fade {% if active_tab == 'connections' %}show active{% endif %}" id="connections" role="tabpanel" aria-labelledby="connections-tab">
{{ connections_tab(portainer_settings, nginx_settings, site_settings, git_settings, cloudflare_settings) }}
{{ connections_tab(portainer_settings, nginx_settings, site_settings, git_settings, cloudflare_settings, stripe_settings) }}
</div>
<!-- Pricing Tab -->

View File

@@ -1,6 +1,6 @@
{% from "settings/components/connection_modals.html" import connection_modals %}
{% macro connections_tab(portainer_settings, nginx_settings, site_settings, git_settings, cloudflare_settings) %}
{% macro connections_tab(portainer_settings, nginx_settings, site_settings, git_settings, cloudflare_settings, stripe_settings) %}
<!-- Meta tags for JavaScript -->
<meta name="management-api-key" content="{{ site_settings.management_api_key }}">
<meta name="git-settings" content="{{ git_settings|tojson|safe }}">
@@ -212,6 +212,67 @@
</div>
</div>
</div>
<!-- Stripe Connection Card -->
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fab fa-stripe me-2"></i>Stripe Connection
</h5>
<button class="btn btn-sm btn-outline-primary" onclick="testStripeConnection()">
<i class="fas fa-plug me-1"></i>Test Connection
</button>
</div>
<div class="card-body">
<form id="stripeForm" onsubmit="saveStripeConnection(event)">
<div class="mb-3">
<label for="stripePublishableKey" class="form-label">Publishable Key</label>
<input type="text" class="form-control" id="stripePublishableKey" name="stripePublishableKey"
placeholder="pk_test_..." required
value="{{ stripe_settings.publishable_key if stripe_settings and stripe_settings.publishable_key else '' }}">
<div class="form-text">Your Stripe publishable key (starts with pk_test_ or pk_live_)</div>
</div>
<div class="mb-3">
<label for="stripeSecretKey" class="form-label">Secret Key</label>
<input type="password" class="form-control" id="stripeSecretKey" name="stripeSecretKey"
placeholder="sk_test_..." required
value="{{ stripe_settings.secret_key if stripe_settings and stripe_settings.secret_key else '' }}">
<div class="form-text">Your Stripe secret key (starts with sk_test_ or sk_live_)</div>
</div>
<div class="mb-3">
<label for="stripeWebhookSecret" class="form-label">Webhook Secret (Optional)</label>
<input type="password" class="form-control" id="stripeWebhookSecret" name="stripeWebhookSecret"
placeholder="whsec_..."
value="{{ stripe_settings.webhook_secret if stripe_settings and stripe_settings.webhook_secret else '' }}">
<div class="form-text">Webhook endpoint secret for secure event handling</div>
</div>
<div class="mb-3">
<label for="stripeCustomerPortalUrl" class="form-label">Customer Portal URL</label>
<input type="url" class="form-control" id="stripeCustomerPortalUrl" name="stripeCustomerPortalUrl"
placeholder="https://billing.stripe.com/p/login/..."
value="{{ stripe_settings.customer_portal_url if stripe_settings and stripe_settings.customer_portal_url else '' }}">
<div class="form-text">URL for customers to manage their subscriptions and billing</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="stripeTestMode" name="stripeTestMode"
{% if stripe_settings and stripe_settings.test_mode %}checked{% endif %}>
<label class="form-check-label" for="stripeTestMode">
Test Mode (Use test keys)
</label>
</div>
<div class="form-text">Enable this to use Stripe test mode for development</div>
</div>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>Save Stripe Settings
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Save Connection Modal -->

View File

@@ -53,28 +53,32 @@
</div>
</div>
<!-- Stripe Payment Links -->
{% if plan.monthly_stripe_link or plan.annual_stripe_link %}
<!-- Stripe Integration Info -->
{% if plan.stripe_product_id or plan.stripe_monthly_price_id or plan.stripe_annual_price_id %}
<div class="mb-3">
<strong>Payment Links:</strong>
<strong>Stripe Integration:</strong>
<div class="mt-2">
{% if plan.monthly_stripe_link %}
{% if plan.stripe_product_id %}
<div class="mb-1">
<small class="text-muted">
<i class="fas fa-credit-card me-1"></i>Monthly:
<a href="{{ plan.monthly_stripe_link }}" target="_blank" class="text-primary">
Stripe Payment Link
</a>
<i class="fas fa-tag me-1"></i>Product ID:
<code class="text-primary">{{ plan.stripe_product_id }}</code>
</small>
</div>
{% endif %}
{% if plan.annual_stripe_link %}
{% if plan.stripe_monthly_price_id %}
<div class="mb-1">
<small class="text-muted">
<i class="fas fa-credit-card me-1"></i>Annual:
<a href="{{ plan.annual_stripe_link }}" target="_blank" class="text-primary">
Stripe Payment Link
</a>
<i class="fas fa-credit-card me-1"></i>Monthly Price ID:
<code class="text-primary">{{ plan.stripe_monthly_price_id }}</code>
</small>
</div>
{% endif %}
{% if plan.stripe_annual_price_id %}
<div class="mb-1">
<small class="text-muted">
<i class="fas fa-credit-card me-1"></i>Annual Price ID:
<code class="text-primary">{{ plan.stripe_annual_price_id }}</code>
</small>
</div>
{% endif %}
@@ -91,10 +95,6 @@
</ul>
</div>
<div class="mb-3">
<strong>Button:</strong> {{ plan.button_text }} → {{ plan.button_url }}
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input plan-popular-toggle" type="checkbox"
@@ -251,22 +251,27 @@
</div>
</div>
<!-- Stripe Payment Links Section -->
<!-- Stripe Product/Price IDs Section -->
<div class="row">
<div class="col-md-6">
<div class="col-md-4">
<div class="mb-3">
<label for="monthlyStripeLink" class="form-label">Monthly Stripe Payment Link</label>
<input type="url" class="form-control" id="monthlyStripeLink" name="monthly_stripe_link"
placeholder="https://buy.stripe.com/...">
<small class="text-muted">Stripe payment link for monthly billing</small>
<label for="stripeProductId" class="form-label">Stripe Product ID</label>
<input type="text" class="form-control" id="stripeProductId" name="stripe_product_id" placeholder="prod_xxx">
<small class="text-muted">The Stripe Product ID for this plan</small>
</div>
</div>
<div class="col-md-6">
<div class="col-md-4">
<div class="mb-3">
<label for="annualStripeLink" class="form-label">Annual Stripe Payment Link</label>
<input type="url" class="form-control" id="annualStripeLink" name="annual_stripe_link"
placeholder="https://buy.stripe.com/...">
<small class="text-muted">Stripe payment link for annual billing</small>
<label for="stripeMonthlyPriceId" class="form-label">Stripe Monthly Price ID</label>
<input type="text" class="form-control" id="stripeMonthlyPriceId" name="stripe_monthly_price_id" placeholder="price_xxx">
<small class="text-muted">The Stripe Price ID for monthly billing</small>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="stripeAnnualPriceId" class="form-label">Stripe Annual Price ID</label>
<input type="text" class="form-control" id="stripeAnnualPriceId" name="stripe_annual_price_id" placeholder="price_xxx">
<small class="text-muted">The Stripe Price ID for annual billing</small>
</div>
</div>
</div>
@@ -411,22 +416,27 @@
</div>
</div>
<!-- Stripe Payment Links Section -->
<!-- Stripe Product/Price IDs Section -->
<div class="row">
<div class="col-md-6">
<div class="col-md-4">
<div class="mb-3">
<label for="editMonthlyStripeLink" class="form-label">Monthly Stripe Payment Link</label>
<input type="url" class="form-control" id="editMonthlyStripeLink" name="monthly_stripe_link"
placeholder="https://buy.stripe.com/...">
<small class="text-muted">Stripe payment link for monthly billing</small>
<label for="stripeProductId" class="form-label">Stripe Product ID</label>
<input type="text" class="form-control" id="stripeProductId" name="stripe_product_id" placeholder="prod_xxx">
<small class="text-muted">The Stripe Product ID for this plan</small>
</div>
</div>
<div class="col-md-6">
<div class="col-md-4">
<div class="mb-3">
<label for="editAnnualStripeLink" class="form-label">Annual Stripe Payment Link</label>
<input type="url" class="form-control" id="editAnnualStripeLink" name="annual_stripe_link"
placeholder="https://buy.stripe.com/...">
<small class="text-muted">Stripe payment link for annual billing</small>
<label for="stripeMonthlyPriceId" class="form-label">Stripe Monthly Price ID</label>
<input type="text" class="form-control" id="stripeMonthlyPriceId" name="stripe_monthly_price_id" placeholder="price_xxx">
<small class="text-muted">The Stripe Price ID for monthly billing</small>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="stripeAnnualPriceId" class="form-label">Stripe Annual Price ID</label>
<input type="text" class="form-control" id="stripeAnnualPriceId" name="stripe_annual_price_id" placeholder="price_xxx">
<small class="text-muted">The Stripe Price ID for annual billing</small>
</div>
</div>
</div>