From d7f5809771e830c1751fd51c770140f82b66a60c Mon Sep 17 00:00:00 2001 From: Kobe Date: Tue, 24 Jun 2025 12:08:13 +0200 Subject: [PATCH] saving payment data --- routes/__pycache__/admin.cpython-313.pyc | Bin 32398 -> 34619 bytes routes/admin.py | 50 ++++++ static/js/launch_progress.js | 55 ++++++ templates/main/instances.html | 211 ++++++++++++++++++++++- 4 files changed, 314 insertions(+), 2 deletions(-) diff --git a/routes/__pycache__/admin.cpython-313.pyc b/routes/__pycache__/admin.cpython-313.pyc index b96373d259460da77de61de6787517c6c957256f..4af0b3c07545c0149c0c5e4894c0b2e2a673af20 100644 GIT binary patch delta 1422 zcmZ8gTTCNW7(UaPfp&VE(#y1kw$m#Vb}1~2g1|;(4&V2>YPBi7$&JO5#RMeBiZ{K1@T_1ml}#lj*XFzL&Y$8GxR{+wb1^baTh#Bz`>TyhHAM$%WemLyQn~!z}X!!73^6G78`n+_G1~ic9J$p*RgNtvO1WKT%$tfxac#s5ee6dR_v~Ws2!T;wDA6f2VM^u{0RVZWKuJ0Zw@`@eL&DANm$9)v?;W;BsuGZ{*;|y;3@5B zL6#m>m1(tVKvgfU(niF1m6;F=U`CozqqM}4EE*3wZFsH)(W{I~+Yt4oIMRy7`luRb zLsKuPA$G)iRpvmfPh}ipmk{Gfa`v+7xRCQQ^vA45+aPhi`-TIv*L2i>;$f38a{up5 zIvbAQ?xp*PNur^7kRWEJ0MMgrou?ajCA1_pLVK^K(G>!IFhA6&wgNGLZkK}BE^!0} ze@j|R9hWr)jSmQ(95qgbo{y` zU6DVqGlroLL6pvakNKYnP8c;Rl@W^~{G{OaTGm#IspscOskQtQ)b{}GxGy^;F3PMZv0{}?Z_}rC zZku^*b8W|K28V3$NCuArTs~8cwSAWPVr^$lA(1*|9c5}yx2scVr0_!Ca#8lhBwtKv zb(N5%1r~naCi@2@|A3;yO$@O3_9>a~m-v1~j~xTBw3p+1GrKd25o=R(j2SyDuz1Ru zy^Y-s#e#L)3vI>PhE7rh1oz`|XjlpjD?aS-&4=-0C(xW5#7+oUI%|Q592k*Gfsvy(tARIa-Mw=6gw#Dz ziC?dFzf}P_aw{AQc7(j> zFY)#3!2|=287A=(d2Hhn{p4{Efq6fTFdBr^)+6Exe6;0Zo}zaA#TP1vzi-{sXcfv! c#FSSF$banQdWqN>2{bH*SLzg!`EO9~zX&jPwg3PC delta 78 zcmdnp$JFKCmBiU0tf4i>cl diff --git a/routes/admin.py b/routes/admin.py index 4a90084..7f55005 100644 --- a/routes/admin.py +++ b/routes/admin.py @@ -675,4 +675,54 @@ def update_pricing_plan_status(plan_id): except Exception as e: db.session.rollback() + return jsonify({'error': str(e)}), 500 + +@admin.route('/api/admin/pricing-plans', methods=['GET']) +@login_required +def get_pricing_plans(): + """Get all active pricing plans for instance launch""" + if not current_user.is_admin: + return jsonify({'error': 'Unauthorized'}), 403 + + try: + from models import PricingPlan + + # Get all active pricing plans ordered by order_index + plans = PricingPlan.query.filter_by(is_active=True).order_by(PricingPlan.order_index).all() + + plans_data = [] + for plan in plans: + plans_data.append({ + 'id': plan.id, + 'name': plan.name, + 'description': plan.description, + 'monthly_price': plan.monthly_price, + 'annual_price': plan.annual_price, + 'features': plan.features, + 'button_text': plan.button_text, + 'button_url': plan.button_url, + 'is_popular': plan.is_popular, + 'is_custom': plan.is_custom, + 'is_active': plan.is_active, + 'order_index': plan.order_index, + 'room_quota': plan.room_quota, + 'conversation_quota': plan.conversation_quota, + 'storage_quota_gb': plan.storage_quota_gb, + 'manager_quota': plan.manager_quota, + 'admin_quota': plan.admin_quota, + 'format_quota_display': { + 'room_quota': plan.format_quota_display('room_quota'), + 'conversation_quota': plan.format_quota_display('conversation_quota'), + 'storage_quota_gb': plan.format_quota_display('storage_quota_gb'), + 'manager_quota': plan.format_quota_display('manager_quota'), + 'admin_quota': plan.format_quota_display('admin_quota') + } + }) + + return jsonify({ + 'success': True, + 'plans': plans_data + }) + + except Exception as e: return jsonify({'error': str(e)}), 500 \ No newline at end of file diff --git a/static/js/launch_progress.js b/static/js/launch_progress.js index e41bb77..63370be 100644 --- a/static/js/launch_progress.js +++ b/static/js/launch_progress.js @@ -2599,6 +2599,32 @@ async function checkStackExists(stackName) { // Add new function to deploy stack async function deployStack(dockerComposeContent, stackName, port) { try { + // Get the launch data from sessionStorage to access pricing tier info + const launchData = JSON.parse(sessionStorage.getItem('instanceLaunchData')); + + // Fetch the pricing tier details to get the actual quota values + let pricingTierDetails = null; + if (launchData?.pricingTier?.id) { + try { + const pricingResponse = await fetch(`/api/admin/pricing-plans/${launchData.pricingTier.id}`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content + } + }); + + if (pricingResponse.ok) { + const pricingData = await pricingResponse.json(); + if (pricingData.success) { + pricingTierDetails = pricingData.plan; + } + } + } catch (error) { + console.warn('Failed to fetch pricing tier details:', error); + } + } + // First, attempt to deploy the stack const response = await fetch('/api/admin/deploy-stack', { method: 'POST', @@ -2633,6 +2659,35 @@ async function deployStack(dockerComposeContent, stackName, port) { { name: 'DEPLOYED_AT', value: new Date().toISOString() + }, + // Pricing tier environment variables with actual quota values + { + name: 'PRICING_TIER_ID', + value: launchData?.pricingTier?.id?.toString() || '0' + }, + { + name: 'PRICING_TIER_NAME', + value: launchData?.pricingTier?.name || 'Unknown' + }, + { + name: 'ROOM_QUOTA', + value: pricingTierDetails?.room_quota?.toString() || '0' + }, + { + name: 'CONVERSATION_QUOTA', + value: pricingTierDetails?.conversation_quota?.toString() || '0' + }, + { + name: 'STORAGE_QUOTA_GB', + value: pricingTierDetails?.storage_quota_gb?.toString() || '0' + }, + { + name: 'MANAGER_QUOTA', + value: pricingTierDetails?.manager_quota?.toString() || '0' + }, + { + name: 'ADMIN_QUOTA', + value: pricingTierDetails?.admin_quota?.toString() || '0' } ] }) diff --git a/templates/main/instances.html b/templates/main/instances.html index 351985a..c27d685 100644 --- a/templates/main/instances.html +++ b/templates/main/instances.html @@ -49,6 +49,54 @@ .badge.bg-orange:hover { background-color: #e55a00 !important; } + + /* Pricing tier selection styles */ + .pricing-card { + cursor: pointer; + transition: all 0.3s ease; + border: 2px solid transparent; + } + + .pricing-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border-color: var(--primary-color); + } + + .pricing-card.selected { + border-color: var(--primary-color); + background-color: rgba(22, 118, 123, 0.05); + box-shadow: 0 4px 12px rgba(22, 118, 123, 0.2); + } + + .pricing-card.selected::after { + content: '✓'; + position: absolute; + top: 10px; + right: 10px; + background-color: var(--primary-color); + color: white; + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: bold; + } + + .pricing-card.border-primary { + border-color: var(--primary-color) !important; + } + + .quota-info { + font-size: 0.75rem; + } + + .features { + text-align: left; + } {% endblock %} @@ -246,6 +294,10 @@ - +
+
+
+
+
Select Pricing Tier
+

Choose the pricing tier that best fits your needs. This will determine the resource limits for your instance.

+ +
+ +
+
+ Loading pricing tiers... +
+

Loading available pricing tiers...

+
+
+
+
+
+
+ + +

Ready to Launch!

@@ -494,6 +572,7 @@

Repository:

Branch:

Company:

+

Pricing Tier:

Port:

@@ -723,7 +802,7 @@ let launchStepsModal; let currentStep = 1; // Update the total number of steps -const totalSteps = 6; +const totalSteps = 7; document.addEventListener('DOMContentLoaded', function() { addInstanceModal = new bootstrap.Modal(document.getElementById('addInstanceModal')); @@ -1650,6 +1729,11 @@ function updateStepDisplay() { if (currentStep === 4) { getNextAvailablePort(); } + + // If we're on step 6, load pricing tiers + if (currentStep === 6) { + loadPricingTiers(); + } } function nextStep() { @@ -1662,6 +1746,9 @@ function nextStep() { if (currentStep === 4 && !validateStep4()) { return; } + if (currentStep === 6 && !validateStep6()) { + return; + } if (currentStep < totalSteps) { currentStep++; @@ -2066,6 +2153,7 @@ function updateReviewSection() { const webAddresses = Array.from(document.querySelectorAll('.web-address')) .map(input => input.value) .join(', '); + const pricingTier = document.getElementById('selectedPricingTierName').value; // Update the review section document.getElementById('reviewRepo').textContent = repo; @@ -2073,6 +2161,7 @@ function updateReviewSection() { document.getElementById('reviewCompany').textContent = company; document.getElementById('reviewPort').textContent = port; document.getElementById('reviewWebAddresses').textContent = webAddresses; + document.getElementById('reviewPricingTier').textContent = pricingTier || 'Not selected'; } // Function to launch the instance @@ -2099,6 +2188,10 @@ function launchInstance() { colors: { primary: document.getElementById('primaryColor').value, secondary: document.getElementById('secondaryColor').value + }, + pricingTier: { + id: document.getElementById('selectedPricingTierId').value, + name: document.getElementById('selectedPricingTierName').value } }; @@ -2240,5 +2333,119 @@ async function refreshLatestVersion() { console.log('Manual refresh of latest version requested'); await fetchLatestVersion(); } + +// Function to load pricing tiers +async function loadPricingTiers() { + const container = document.getElementById('pricingTiersContainer'); + + try { + const response = await fetch('/api/admin/pricing-plans', { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content + } + }); + + if (!response.ok) { + throw new Error('Failed to load pricing tiers'); + } + + const data = await response.json(); + + if (data.plans && data.plans.length > 0) { + container.innerHTML = data.plans.map(plan => ` +
+ +
+ `).join(''); + } else { + container.innerHTML = ` +
+
+ + No pricing tiers available. Please contact your administrator. +
+
+ `; + } + } catch (error) { + console.error('Error loading pricing tiers:', error); + container.innerHTML = ` +
+
+ + Error loading pricing tiers: ${error.message} +
+
+ `; + } +} + +// Function to select a pricing tier +function selectPricingTier(planId, planName) { + // Remove selection from all cards + document.querySelectorAll('.pricing-card').forEach(card => { + card.classList.remove('selected'); + }); + + // Add selection to clicked card + event.currentTarget.classList.add('selected'); + + // Store the selection + document.getElementById('selectedPricingTierId').value = planId; + document.getElementById('selectedPricingTierName').value = planName; + + // Enable next button if not already enabled + const nextButton = document.querySelector('#launchStepsFooter .btn-primary'); + if (nextButton.disabled) { + nextButton.disabled = false; + } +} + +// Function to validate step 6 (pricing tier selection) +function validateStep6() { + const selectedTierId = document.getElementById('selectedPricingTierId').value; + + if (!selectedTierId) { + alert('Please select a pricing tier before proceeding.'); + return false; + } + + return true; +} {% endblock %} \ No newline at end of file