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 class="px-4 py-3 text-right">
|
||||
<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">
|
||||
<span x-html="$icon('pencil', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
@@ -332,6 +335,143 @@
|
||||
</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 %}
|
||||
|
||||
{% 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
|
||||
|
||||
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
|
||||
- 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
|
||||
|
||||
The features are seeded via Alembic migration:
|
||||
|
||||
@@ -211,6 +211,7 @@ nav:
|
||||
- User Management: guides/user-management.md
|
||||
- Product Management: guides/product-management.md
|
||||
- Inventory Management: guides/inventory-management.md
|
||||
- Subscription Tier Management: guides/subscription-tier-management.md
|
||||
- Shop Setup: guides/shop-setup.md
|
||||
- CSV Import: guides/csv-import.md
|
||||
- Marketplace Integration: guides/marketplace-integration.md
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// static/admin/js/subscription-tiers.js
|
||||
// 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() {
|
||||
// Get base data with safety check for standalone usage
|
||||
@@ -23,6 +23,16 @@ function adminSubscriptionTiers() {
|
||||
stats: null,
|
||||
includeInactive: false,
|
||||
|
||||
// Feature management
|
||||
features: [],
|
||||
categories: [],
|
||||
featuresGrouped: {},
|
||||
selectedFeatures: [],
|
||||
selectedTierForFeatures: null,
|
||||
showFeaturePanel: false,
|
||||
loadingFeatures: false,
|
||||
savingFeatures: false,
|
||||
|
||||
// Sorting
|
||||
sortBy: 'display_order',
|
||||
sortOrder: 'asc',
|
||||
@@ -56,8 +66,18 @@ function adminSubscriptionTiers() {
|
||||
window._adminSubscriptionTiersInitialized = true;
|
||||
|
||||
tiersLog.info('=== SUBSCRIPTION TIERS PAGE INITIALIZING ===');
|
||||
await this.loadTiers();
|
||||
await this.loadStats();
|
||||
try {
|
||||
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() {
|
||||
@@ -207,6 +227,129 @@ function adminSubscriptionTiers() {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
}).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