feat: add feature assignment to admin tier management UI
- Add slide-over panel for assigning features to subscription tiers - Features grouped by category with select all/deselect all - Add puzzle-piece icon button in tier table actions - Add feature management methods to subscription-tiers.js - Fix JS-006 by adding try/catch to init function Documentation: - Update feature-gating-system.md with Admin Tier Management UI section - Update subscription-billing.md with tier management overview - Add new admin user guide: subscription-tier-management.md - Add guide to mkdocs.yml navigation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -144,6 +144,9 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-right">
|
<td class="px-4 py-3 text-right">
|
||||||
<div class="flex items-center justify-end gap-2">
|
<div class="flex items-center justify-end gap-2">
|
||||||
|
<button @click="openFeaturePanel(tier)" class="p-2 text-gray-500 hover:text-blue-600 dark:hover:text-blue-400" title="Edit Features">
|
||||||
|
<span x-html="$icon('puzzle-piece', 'w-4 h-4')"></span>
|
||||||
|
</button>
|
||||||
<button @click="openEditModal(tier)" class="p-2 text-gray-500 hover:text-purple-600 dark:hover:text-purple-400" title="Edit">
|
<button @click="openEditModal(tier)" class="p-2 text-gray-500 hover:text-purple-600 dark:hover:text-purple-400" title="Edit">
|
||||||
<span x-html="$icon('pencil', 'w-4 h-4')"></span>
|
<span x-html="$icon('pencil', 'w-4 h-4')"></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -332,6 +335,143 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Feature Assignment Slide-over Panel -->
|
||||||
|
<div
|
||||||
|
x-show="showFeaturePanel"
|
||||||
|
x-cloak
|
||||||
|
@keydown.escape.window="closeFeaturePanel()"
|
||||||
|
class="fixed inset-0 z-50 overflow-hidden"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
>
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<div
|
||||||
|
x-show="showFeaturePanel"
|
||||||
|
x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0"
|
||||||
|
x-transition:enter-end="opacity-100"
|
||||||
|
x-transition:leave="ease-in duration-200"
|
||||||
|
x-transition:leave-start="opacity-100"
|
||||||
|
x-transition:leave-end="opacity-0"
|
||||||
|
class="fixed inset-0 bg-gray-500/50 dark:bg-gray-900/80 backdrop-blur-sm"
|
||||||
|
@click="closeFeaturePanel()"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Panel -->
|
||||||
|
<div class="fixed inset-y-0 right-0 flex max-w-full pl-10">
|
||||||
|
<div
|
||||||
|
x-show="showFeaturePanel"
|
||||||
|
x-transition:enter="transform transition ease-in-out duration-300"
|
||||||
|
x-transition:enter-start="translate-x-full"
|
||||||
|
x-transition:enter-end="translate-x-0"
|
||||||
|
x-transition:leave="transform transition ease-in-out duration-300"
|
||||||
|
x-transition:leave-start="translate-x-0"
|
||||||
|
x-transition:leave-end="translate-x-full"
|
||||||
|
class="w-screen max-w-lg"
|
||||||
|
>
|
||||||
|
<div class="flex h-full flex-col bg-white dark:bg-gray-800 shadow-xl">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Edit Features</h2>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="selectedTierForFeatures?.name"></p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="closeFeaturePanel()"
|
||||||
|
class="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<span x-html="$icon('x', 'w-5 h-5')"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div class="flex-1 overflow-y-auto px-6 py-4">
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div x-show="loadingFeatures" class="flex items-center justify-center py-8">
|
||||||
|
<span x-html="$icon('refresh', 'w-6 h-6 animate-spin text-purple-600')"></span>
|
||||||
|
<span class="ml-2 text-gray-500 dark:text-gray-400">Loading features...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Feature Categories -->
|
||||||
|
<div x-show="!loadingFeatures" class="space-y-6">
|
||||||
|
<template x-for="category in categories" :key="category">
|
||||||
|
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||||
|
<!-- Category Header -->
|
||||||
|
<div class="flex items-center justify-between px-4 py-3 bg-gray-50 dark:bg-gray-700/50">
|
||||||
|
<h3 class="text-sm font-medium text-gray-900 dark:text-white" x-text="formatCategoryName(category)"></h3>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
@click="selectAllInCategory(category)"
|
||||||
|
class="text-xs text-purple-600 dark:text-purple-400 hover:underline"
|
||||||
|
x-show="!allSelectedInCategory(category)"
|
||||||
|
>
|
||||||
|
Select all
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="deselectAllInCategory(category)"
|
||||||
|
class="text-xs text-gray-500 dark:text-gray-400 hover:underline"
|
||||||
|
x-show="allSelectedInCategory(category)"
|
||||||
|
>
|
||||||
|
Deselect all
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Features List -->
|
||||||
|
<div class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||||
|
<template x-for="feature in featuresGrouped[category]" :key="feature.code">
|
||||||
|
<label class="flex items-start px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/30 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="isFeatureSelected(feature.code)"
|
||||||
|
@change="toggleFeature(feature.code)"
|
||||||
|
class="mt-0.5 rounded border-gray-300 dark:border-gray-600 text-purple-600 focus:ring-purple-500"
|
||||||
|
>
|
||||||
|
<div class="ml-3">
|
||||||
|
<div class="text-sm font-medium text-gray-900 dark:text-white" x-text="feature.name"></div>
|
||||||
|
<div class="text-xs text-gray-500 dark:text-gray-400" x-text="feature.description"></div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div x-show="categories.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||||
|
No features available.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="flex items-center justify-between px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<span x-text="selectedFeatures.length"></span> features selected
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
@click="closeFeaturePanel()"
|
||||||
|
type="button"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="saveFeatures()"
|
||||||
|
:disabled="savingFeatures"
|
||||||
|
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<span x-show="savingFeatures" x-html="$icon('refresh', 'w-4 h-4 mr-2 animate-spin')"></span>
|
||||||
|
<span x-text="savingFeatures ? 'Saving...' : 'Save Features'"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_scripts %}
|
{% block extra_scripts %}
|
||||||
|
|||||||
@@ -137,6 +137,26 @@ tier.features = [
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Admin Tier Management
|
||||||
|
|
||||||
|
Administrators can manage subscription tiers at `/admin/subscription-tiers`:
|
||||||
|
|
||||||
|
**Capabilities:**
|
||||||
|
- View all tiers with stats (total, active, public, MRR)
|
||||||
|
- Create new tiers with custom pricing and limits
|
||||||
|
- Edit tier properties (name, pricing, limits, Stripe IDs)
|
||||||
|
- Activate/deactivate tiers
|
||||||
|
- Assign features to tiers via slide-over panel
|
||||||
|
|
||||||
|
**Feature Assignment:**
|
||||||
|
1. Click the puzzle-piece icon on any tier row
|
||||||
|
2. Features are displayed grouped by category
|
||||||
|
3. Use checkboxes to select/deselect features
|
||||||
|
4. Use "Select all" / "Deselect all" per category
|
||||||
|
5. Click "Save Features" to update
|
||||||
|
|
||||||
|
See [Feature Gating System](../implementation/feature-gating-system.md#admin-tier-management-ui) for technical details.
|
||||||
|
|
||||||
## Limit Enforcement
|
## Limit Enforcement
|
||||||
|
|
||||||
Limits are enforced at the service layer:
|
Limits are enforced at the service layer:
|
||||||
|
|||||||
135
docs/guides/subscription-tier-management.md
Normal file
135
docs/guides/subscription-tier-management.md
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
# Subscription Tier Management
|
||||||
|
|
||||||
|
This guide explains how to manage subscription tiers and assign features to them in the admin panel.
|
||||||
|
|
||||||
|
## Accessing Tier Management
|
||||||
|
|
||||||
|
Navigate to **Admin → Billing & Subscriptions → Subscription Tiers** or go directly to `/admin/subscription-tiers`.
|
||||||
|
|
||||||
|
## Dashboard Overview
|
||||||
|
|
||||||
|
The tier management page displays:
|
||||||
|
|
||||||
|
### Stats Cards
|
||||||
|
- **Total Tiers**: Number of configured subscription tiers
|
||||||
|
- **Active Tiers**: Tiers currently available for subscription
|
||||||
|
- **Public Tiers**: Tiers visible to vendors (excludes enterprise/custom)
|
||||||
|
- **Est. MRR**: Estimated Monthly Recurring Revenue
|
||||||
|
|
||||||
|
### Tier Table
|
||||||
|
|
||||||
|
Each tier shows:
|
||||||
|
|
||||||
|
| Column | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| # | Display order (affects pricing page order) |
|
||||||
|
| Code | Unique identifier (e.g., `essential`, `professional`) |
|
||||||
|
| Name | Display name shown to vendors |
|
||||||
|
| Monthly | Monthly price in EUR |
|
||||||
|
| Annual | Annual price in EUR (or `-` if not set) |
|
||||||
|
| Orders/Mo | Monthly order limit (or `Unlimited`) |
|
||||||
|
| Products | Product limit (or `Unlimited`) |
|
||||||
|
| Team | Team member limit (or `Unlimited`) |
|
||||||
|
| Features | Number of features assigned |
|
||||||
|
| Status | Active, Private, or Inactive |
|
||||||
|
| Actions | Edit Features, Edit, Activate/Deactivate |
|
||||||
|
|
||||||
|
## Managing Tiers
|
||||||
|
|
||||||
|
### Creating a New Tier
|
||||||
|
|
||||||
|
1. Click **Create Tier** button
|
||||||
|
2. Fill in the tier details:
|
||||||
|
- **Code**: Unique lowercase identifier (cannot be changed after creation)
|
||||||
|
- **Name**: Display name for the tier
|
||||||
|
- **Monthly Price**: Price in cents (e.g., 4900 for €49.00)
|
||||||
|
- **Annual Price**: Optional annual price in cents
|
||||||
|
- **Order Limit**: Leave empty for unlimited
|
||||||
|
- **Product Limit**: Leave empty for unlimited
|
||||||
|
- **Team Members**: Leave empty for unlimited
|
||||||
|
- **Display Order**: Controls sort order on pricing pages
|
||||||
|
- **Active**: Whether tier is available
|
||||||
|
- **Public**: Whether tier is visible to vendors
|
||||||
|
3. Click **Create**
|
||||||
|
|
||||||
|
### Editing a Tier
|
||||||
|
|
||||||
|
1. Click the **pencil icon** on the tier row
|
||||||
|
2. Modify the tier properties
|
||||||
|
3. Click **Update**
|
||||||
|
|
||||||
|
Note: The tier code cannot be changed after creation.
|
||||||
|
|
||||||
|
### Activating/Deactivating Tiers
|
||||||
|
|
||||||
|
- Click the **check-circle icon** to activate an inactive tier
|
||||||
|
- Click the **x-circle icon** to deactivate an active tier
|
||||||
|
|
||||||
|
Deactivating a tier:
|
||||||
|
- Does not affect existing subscriptions
|
||||||
|
- Hides the tier from new subscription selection
|
||||||
|
- Can be reactivated at any time
|
||||||
|
|
||||||
|
## Managing Features
|
||||||
|
|
||||||
|
### Assigning Features to a Tier
|
||||||
|
|
||||||
|
1. Click the **puzzle-piece icon** on the tier row
|
||||||
|
2. A slide-over panel opens showing all available features
|
||||||
|
3. Features are grouped by category:
|
||||||
|
- Analytics
|
||||||
|
- Product Management
|
||||||
|
- Order Management
|
||||||
|
- Marketing
|
||||||
|
- Support
|
||||||
|
- Integration
|
||||||
|
- Branding
|
||||||
|
- Team
|
||||||
|
|
||||||
|
4. Check/uncheck features to include in the tier
|
||||||
|
5. Use **Select all** or **Deselect all** per category for bulk actions
|
||||||
|
6. The footer shows the total number of selected features
|
||||||
|
7. Click **Save Features** to apply changes
|
||||||
|
|
||||||
|
### Feature Categories
|
||||||
|
|
||||||
|
| Category | Example Features |
|
||||||
|
|----------|------------------|
|
||||||
|
| Analytics | Basic Analytics, Analytics Dashboard, Custom Reports |
|
||||||
|
| Product Management | Bulk Edit, Variants, Bundles, Inventory Alerts |
|
||||||
|
| Order Management | Order Automation, Advanced Fulfillment, Multi-Warehouse |
|
||||||
|
| Marketing | Discount Codes, Abandoned Cart, Email Marketing, Loyalty |
|
||||||
|
| Support | Email Support, Priority Support, Phone Support, Dedicated Manager |
|
||||||
|
| Integration | Basic API, Advanced API, Webhooks, Custom Integrations |
|
||||||
|
| Branding | Theme Customization, Custom Domain, White Label |
|
||||||
|
| Team | Team Management, Role Permissions, Audit Logs |
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Tier Pricing Strategy
|
||||||
|
|
||||||
|
1. **Essential**: Entry-level with basic features and limits
|
||||||
|
2. **Professional**: Mid-tier with increased limits and key integrations
|
||||||
|
3. **Business**: Full-featured for growing businesses
|
||||||
|
4. **Enterprise**: Custom pricing with unlimited everything
|
||||||
|
|
||||||
|
### Feature Assignment Tips
|
||||||
|
|
||||||
|
- Start with fewer features in lower tiers
|
||||||
|
- Ensure each upgrade tier adds meaningful value
|
||||||
|
- Keep support features as upgrade incentives
|
||||||
|
- API access typically belongs in Business+ tiers
|
||||||
|
|
||||||
|
### Stripe Integration
|
||||||
|
|
||||||
|
For each tier, you can optionally configure:
|
||||||
|
- **Stripe Product ID**: Link to Stripe product
|
||||||
|
- **Stripe Monthly Price ID**: Link to monthly price
|
||||||
|
- **Stripe Annual Price ID**: Link to annual price
|
||||||
|
|
||||||
|
These are required for automated billing via Stripe Checkout.
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Subscription & Billing System](../features/subscription-billing.md) - Complete billing documentation
|
||||||
|
- [Feature Gating System](../implementation/feature-gating-system.md) - Technical feature gating details
|
||||||
@@ -338,6 +338,65 @@ Located at `/admin/features`:
|
|||||||
- Search by name/code
|
- Search by name/code
|
||||||
- View tier requirements
|
- View tier requirements
|
||||||
|
|
||||||
|
## Admin Tier Management UI
|
||||||
|
|
||||||
|
Located at `/admin/subscription-tiers`:
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
The subscription tiers admin page provides full CRUD functionality for managing subscription tiers and their feature assignments.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
1. **Stats Cards**: Display total tiers, active tiers, public tiers, and estimated MRR
|
||||||
|
2. **Tier Table**: Sortable list of all tiers with:
|
||||||
|
- Display order
|
||||||
|
- Code (colored badge by tier)
|
||||||
|
- Name
|
||||||
|
- Monthly/Annual pricing
|
||||||
|
- Limits (orders, products, team members)
|
||||||
|
- Feature count
|
||||||
|
- Status (Active/Private/Inactive)
|
||||||
|
- Actions (Edit Features, Edit, Activate/Deactivate)
|
||||||
|
|
||||||
|
3. **Create/Edit Modal**: Form with all tier fields:
|
||||||
|
- Code and Name
|
||||||
|
- Monthly and Annual pricing (in cents)
|
||||||
|
- Order, Product, and Team member limits
|
||||||
|
- Display order
|
||||||
|
- Stripe IDs (optional)
|
||||||
|
- Description
|
||||||
|
- Active/Public toggles
|
||||||
|
|
||||||
|
4. **Feature Assignment Slide-over Panel**:
|
||||||
|
- Opens when clicking the puzzle-piece icon
|
||||||
|
- Shows all features grouped by category
|
||||||
|
- Checkbox selection with Select all/Deselect all per category
|
||||||
|
- Feature count in footer
|
||||||
|
- Save to update tier's feature assignments
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `app/templates/admin/subscription-tiers.html` | Page template |
|
||||||
|
| `static/admin/js/subscription-tiers.js` | Alpine.js component |
|
||||||
|
| `app/routes/admin_pages.py` | Route registration |
|
||||||
|
|
||||||
|
### API Endpoints Used
|
||||||
|
|
||||||
|
| Action | Method | Endpoint |
|
||||||
|
|--------|--------|----------|
|
||||||
|
| Load tiers | GET | `/api/v1/admin/subscriptions/tiers` |
|
||||||
|
| Load stats | GET | `/api/v1/admin/subscriptions/stats` |
|
||||||
|
| Create tier | POST | `/api/v1/admin/subscriptions/tiers` |
|
||||||
|
| Update tier | PATCH | `/api/v1/admin/subscriptions/tiers/{code}` |
|
||||||
|
| Delete tier | DELETE | `/api/v1/admin/subscriptions/tiers/{code}` |
|
||||||
|
| Load features | GET | `/api/v1/admin/features` |
|
||||||
|
| Load categories | GET | `/api/v1/admin/features/categories` |
|
||||||
|
| Get tier features | GET | `/api/v1/admin/features/tiers/{code}/features` |
|
||||||
|
| Update tier features | PUT | `/api/v1/admin/features/tiers/{code}/features` |
|
||||||
|
|
||||||
## Migration
|
## Migration
|
||||||
|
|
||||||
The features are seeded via Alembic migration:
|
The features are seeded via Alembic migration:
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ nav:
|
|||||||
- User Management: guides/user-management.md
|
- User Management: guides/user-management.md
|
||||||
- Product Management: guides/product-management.md
|
- Product Management: guides/product-management.md
|
||||||
- Inventory Management: guides/inventory-management.md
|
- Inventory Management: guides/inventory-management.md
|
||||||
|
- Subscription Tier Management: guides/subscription-tier-management.md
|
||||||
- Shop Setup: guides/shop-setup.md
|
- Shop Setup: guides/shop-setup.md
|
||||||
- CSV Import: guides/csv-import.md
|
- CSV Import: guides/csv-import.md
|
||||||
- Marketplace Integration: guides/marketplace-integration.md
|
- Marketplace Integration: guides/marketplace-integration.md
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// static/admin/js/subscription-tiers.js
|
// static/admin/js/subscription-tiers.js
|
||||||
// noqa: JS-003 - Uses ...baseData which is data() with safety check
|
// noqa: JS-003 - Uses ...baseData which is data() with safety check
|
||||||
|
|
||||||
const tiersLog = window.LogConfig?.loggers?.subscriptionTiers || console;
|
const tiersLog = window.LogConfig?.loggers?.subscriptionTiers || window.LogConfig?.createLogger?.('subscriptionTiers') || console;
|
||||||
|
|
||||||
function adminSubscriptionTiers() {
|
function adminSubscriptionTiers() {
|
||||||
// Get base data with safety check for standalone usage
|
// Get base data with safety check for standalone usage
|
||||||
@@ -23,6 +23,16 @@ function adminSubscriptionTiers() {
|
|||||||
stats: null,
|
stats: null,
|
||||||
includeInactive: false,
|
includeInactive: false,
|
||||||
|
|
||||||
|
// Feature management
|
||||||
|
features: [],
|
||||||
|
categories: [],
|
||||||
|
featuresGrouped: {},
|
||||||
|
selectedFeatures: [],
|
||||||
|
selectedTierForFeatures: null,
|
||||||
|
showFeaturePanel: false,
|
||||||
|
loadingFeatures: false,
|
||||||
|
savingFeatures: false,
|
||||||
|
|
||||||
// Sorting
|
// Sorting
|
||||||
sortBy: 'display_order',
|
sortBy: 'display_order',
|
||||||
sortOrder: 'asc',
|
sortOrder: 'asc',
|
||||||
@@ -56,8 +66,18 @@ function adminSubscriptionTiers() {
|
|||||||
window._adminSubscriptionTiersInitialized = true;
|
window._adminSubscriptionTiersInitialized = true;
|
||||||
|
|
||||||
tiersLog.info('=== SUBSCRIPTION TIERS PAGE INITIALIZING ===');
|
tiersLog.info('=== SUBSCRIPTION TIERS PAGE INITIALIZING ===');
|
||||||
await this.loadTiers();
|
try {
|
||||||
await this.loadStats();
|
await Promise.all([
|
||||||
|
this.loadTiers(),
|
||||||
|
this.loadStats(),
|
||||||
|
this.loadFeatures(),
|
||||||
|
this.loadCategories()
|
||||||
|
]);
|
||||||
|
tiersLog.info('=== SUBSCRIPTION TIERS PAGE INITIALIZED ===');
|
||||||
|
} catch (error) {
|
||||||
|
tiersLog.error('Failed to initialize subscription tiers page:', error);
|
||||||
|
this.error = 'Failed to load page data. Please refresh.';
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async refresh() {
|
async refresh() {
|
||||||
@@ -207,6 +227,129 @@ function adminSubscriptionTiers() {
|
|||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'EUR'
|
currency: 'EUR'
|
||||||
}).format(cents / 100);
|
}).format(cents / 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==================== FEATURE MANAGEMENT ====================
|
||||||
|
|
||||||
|
async loadFeatures() {
|
||||||
|
try {
|
||||||
|
const data = await apiClient.get('/admin/features');
|
||||||
|
this.features = data.features || [];
|
||||||
|
tiersLog.info(`Loaded ${this.features.length} features`);
|
||||||
|
} catch (error) {
|
||||||
|
tiersLog.error('Failed to load features:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadCategories() {
|
||||||
|
try {
|
||||||
|
const data = await apiClient.get('/admin/features/categories');
|
||||||
|
this.categories = data.categories || [];
|
||||||
|
tiersLog.info(`Loaded ${this.categories.length} categories`);
|
||||||
|
} catch (error) {
|
||||||
|
tiersLog.error('Failed to load categories:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
groupFeaturesByCategory() {
|
||||||
|
this.featuresGrouped = {};
|
||||||
|
for (const category of this.categories) {
|
||||||
|
this.featuresGrouped[category] = this.features.filter(f => f.category === category);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async openFeaturePanel(tier) {
|
||||||
|
tiersLog.info('Opening feature panel for tier:', tier.code);
|
||||||
|
this.selectedTierForFeatures = tier;
|
||||||
|
this.loadingFeatures = true;
|
||||||
|
this.showFeaturePanel = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load tier's current features
|
||||||
|
const data = await apiClient.get(`/admin/features/tiers/${tier.code}/features`);
|
||||||
|
if (data.features) {
|
||||||
|
this.selectedFeatures = data.features.map(f => f.code);
|
||||||
|
} else {
|
||||||
|
this.selectedFeatures = tier.features || [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
tiersLog.error('Failed to load tier features:', error);
|
||||||
|
this.selectedFeatures = tier.features || [];
|
||||||
|
} finally {
|
||||||
|
this.groupFeaturesByCategory();
|
||||||
|
this.loadingFeatures = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
closeFeaturePanel() {
|
||||||
|
this.showFeaturePanel = false;
|
||||||
|
this.selectedTierForFeatures = null;
|
||||||
|
this.selectedFeatures = [];
|
||||||
|
this.featuresGrouped = {};
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleFeature(featureCode) {
|
||||||
|
const index = this.selectedFeatures.indexOf(featureCode);
|
||||||
|
if (index === -1) {
|
||||||
|
this.selectedFeatures.push(featureCode);
|
||||||
|
} else {
|
||||||
|
this.selectedFeatures.splice(index, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isFeatureSelected(featureCode) {
|
||||||
|
return this.selectedFeatures.includes(featureCode);
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveFeatures() {
|
||||||
|
if (!this.selectedTierForFeatures) return;
|
||||||
|
|
||||||
|
tiersLog.info('Saving features for tier:', this.selectedTierForFeatures.code);
|
||||||
|
this.savingFeatures = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiClient.put(
|
||||||
|
`/admin/features/tiers/${this.selectedTierForFeatures.code}/features`,
|
||||||
|
{ feature_codes: this.selectedFeatures }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.successMessage = `Features updated for ${this.selectedTierForFeatures.name}`;
|
||||||
|
this.closeFeaturePanel();
|
||||||
|
await this.loadTiers();
|
||||||
|
} catch (error) {
|
||||||
|
tiersLog.error('Failed to save features:', error);
|
||||||
|
this.error = error.message || 'Failed to save features';
|
||||||
|
} finally {
|
||||||
|
this.savingFeatures = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
selectAllInCategory(category) {
|
||||||
|
const categoryFeatures = this.featuresGrouped[category] || [];
|
||||||
|
for (const feature of categoryFeatures) {
|
||||||
|
if (!this.selectedFeatures.includes(feature.code)) {
|
||||||
|
this.selectedFeatures.push(feature.code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deselectAllInCategory(category) {
|
||||||
|
const categoryFeatures = this.featuresGrouped[category] || [];
|
||||||
|
const codes = categoryFeatures.map(f => f.code);
|
||||||
|
this.selectedFeatures = this.selectedFeatures.filter(c => !codes.includes(c));
|
||||||
|
},
|
||||||
|
|
||||||
|
allSelectedInCategory(category) {
|
||||||
|
const categoryFeatures = this.featuresGrouped[category] || [];
|
||||||
|
if (categoryFeatures.length === 0) return false;
|
||||||
|
return categoryFeatures.every(f => this.selectedFeatures.includes(f.code));
|
||||||
|
},
|
||||||
|
|
||||||
|
formatCategoryName(category) {
|
||||||
|
return category
|
||||||
|
.split('_')
|
||||||
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(' ');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user