Add TenantCustomTier system and fix BusinessEditModal feature loading
Backend: - Add TenantCustomTier model for per-tenant feature overrides - Update EntitlementService to check custom tier before plan features - Add custom_tier action on TenantViewSet (GET/PUT/DELETE) - Add Celery task for grace period management (30-day expiry) Frontend: - Add DynamicFeaturesEditor component for dynamic feature management - Fix BusinessEditModal to load features from plan defaults when no custom tier - Update limits (max_users, max_resources, etc.) to use featureValues - Remove outdated canonical feature check from FeaturePicker (removes warning icons) - Add useBillingPlans hook for accessing billing system data - Add custom tier API functions to platform.ts Features now follow consistent rules: - Load from plan defaults when no custom tier exists - Load from custom tier when one exists - Reset to plan defaults when plan changes - Save to custom tier on edit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,12 +3,11 @@
|
||||
*
|
||||
* A searchable picker for selecting features to include in a plan or version.
|
||||
* Features are grouped by type (boolean capabilities vs integer limits).
|
||||
* Non-canonical features (not in the catalog) are flagged with a warning.
|
||||
* Features are loaded dynamically from the billing API.
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Check, Sliders, Search, X, AlertTriangle } from 'lucide-react';
|
||||
import { isCanonicalFeature } from '../featureCatalog';
|
||||
import { Check, Sliders, Search, X } from 'lucide-react';
|
||||
import type { Feature, PlanFeatureWrite } from '../../hooks/useBillingAdmin';
|
||||
|
||||
export interface FeaturePickerProps {
|
||||
@@ -151,7 +150,6 @@ export const FeaturePicker: React.FC<FeaturePickerProps> = ({
|
||||
<div className={`grid ${compact ? 'grid-cols-1' : 'grid-cols-2'} gap-2`}>
|
||||
{filteredBooleanFeatures.map((feature) => {
|
||||
const selected = isSelected(feature.code);
|
||||
const isCanonical = isCanonicalFeature(feature.code);
|
||||
|
||||
return (
|
||||
<label
|
||||
@@ -170,16 +168,9 @@ export const FeaturePicker: React.FC<FeaturePickerProps> = ({
|
||||
className="mt-0.5 rounded border-gray-300 dark:border-gray-600"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{feature.name}
|
||||
</span>
|
||||
{!isCanonical && (
|
||||
<span title="Not in canonical catalog">
|
||||
<AlertTriangle className="w-3.5 h-3.5 text-amber-500" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{feature.name}
|
||||
</span>
|
||||
{feature.description && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 block mt-0.5">
|
||||
{feature.description}
|
||||
@@ -206,7 +197,6 @@ export const FeaturePicker: React.FC<FeaturePickerProps> = ({
|
||||
{filteredIntegerFeatures.map((feature) => {
|
||||
const selectedFeature = getSelectedFeature(feature.code);
|
||||
const selected = !!selectedFeature;
|
||||
const isCanonical = isCanonicalFeature(feature.code);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -225,18 +215,9 @@ export const FeaturePicker: React.FC<FeaturePickerProps> = ({
|
||||
aria-label={feature.name}
|
||||
className="rounded border-gray-300 dark:border-gray-600"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||
{feature.name}
|
||||
</span>
|
||||
{!isCanonical && (
|
||||
<span title="Not in canonical catalog">
|
||||
<AlertTriangle className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white truncate flex-1 min-w-0">
|
||||
{feature.name}
|
||||
</span>
|
||||
</label>
|
||||
{selected && (
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user