feat: Dashboard redesign, plan permissions, and help docs improvements

Major updates including:
- Customizable dashboard with drag-and-drop widget grid layout
- Plan-based feature locking for plugins and tasks
- Comprehensive help documentation updates across all pages
- Plugin seeding in deployment process for all tenants
- Permission synchronization system for subscription plans
- QuotaOverageModal component and enhanced UX flows

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-03 13:02:44 -05:00
parent 9444e26924
commit dcb14503a2
66 changed files with 7099 additions and 1467 deletions

View File

@@ -37,6 +37,7 @@ import {
useUpdateSubscriptionPlan,
useDeleteSubscriptionPlan,
useSyncPlansWithStripe,
useSyncPlanToTenants,
SubscriptionPlan,
SubscriptionPlanCreate,
} from '../../hooks/usePlatformSettings';
@@ -527,9 +528,12 @@ const TiersSettingsTab: React.FC = () => {
const updatePlanMutation = useUpdateSubscriptionPlan();
const deletePlanMutation = useDeleteSubscriptionPlan();
const syncMutation = useSyncPlansWithStripe();
const syncTenantsMutation = useSyncPlanToTenants();
const [showModal, setShowModal] = useState(false);
const [editingPlan, setEditingPlan] = useState<SubscriptionPlan | null>(null);
const [showSyncConfirmModal, setShowSyncConfirmModal] = useState(false);
const [savedPlanForSync, setSavedPlanForSync] = useState<SubscriptionPlan | null>(null);
const handleCreatePlan = () => {
setEditingPlan(null);
@@ -550,6 +554,9 @@ const TiersSettingsTab: React.FC = () => {
const handleSavePlan = async (data: SubscriptionPlanCreate) => {
if (editingPlan) {
await updatePlanMutation.mutateAsync({ id: editingPlan.id, ...data });
// After updating an existing plan, ask if they want to sync to tenants
setSavedPlanForSync(editingPlan);
setShowSyncConfirmModal(true);
} else {
await createPlanMutation.mutateAsync(data);
}
@@ -557,6 +564,19 @@ const TiersSettingsTab: React.FC = () => {
setEditingPlan(null);
};
const handleSyncConfirm = async () => {
if (savedPlanForSync) {
await syncTenantsMutation.mutateAsync(savedPlanForSync.id);
}
setShowSyncConfirmModal(false);
setSavedPlanForSync(null);
};
const handleSyncCancel = () => {
setShowSyncConfirmModal(false);
setSavedPlanForSync(null);
};
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
@@ -672,6 +692,48 @@ const TiersSettingsTab: React.FC = () => {
isLoading={createPlanMutation.isPending || updatePlanMutation.isPending}
/>
)}
{/* Sync Confirmation Modal */}
{showSyncConfirmModal && savedPlanForSync && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md mx-4">
<div className="p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Update All Tenants?
</h3>
<p className="text-gray-600 dark:text-gray-400">
Do you want to sync the updated settings to all tenants currently on the "{savedPlanForSync.name}" plan?
</p>
<p className="text-sm text-gray-500 dark:text-gray-500 mt-2">
This will update permissions and limits for all businesses on this tier.
</p>
</div>
<div className="flex justify-end gap-3 px-6 py-4 bg-gray-50 dark:bg-gray-700/50 rounded-b-lg">
<button
onClick={handleSyncCancel}
disabled={syncTenantsMutation.isPending}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50"
>
No, Skip
</button>
<button
onClick={handleSyncConfirm}
disabled={syncTenantsMutation.isPending}
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{syncTenantsMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Syncing...
</>
) : (
'Yes, Update All Tenants'
)}
</button>
</div>
</div>
</div>
)}
</div>
);
};
@@ -780,6 +842,8 @@ const PlanModal: React.FC<PlanModalProps> = ({ plan, onSave, onClose, isLoading
advanced_reporting: false,
priority_support: false,
can_use_custom_domain: false,
can_use_plugins: false,
can_use_tasks: false,
can_create_plugins: false,
can_white_label: false,
can_api_access: false,
@@ -1470,10 +1534,37 @@ const PlanModal: React.FC<PlanModalProps> = ({ plan, onSave, onClose, isLoading
<span className="text-sm text-gray-700 dark:text-gray-300">API Access</span>
</label>
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
<input
type="checkbox"
checked={formData.permissions?.can_use_plugins || false}
onChange={(e) => {
handlePermissionChange('can_use_plugins', e.target.checked);
// If disabling plugins, also disable dependent permissions
if (!e.target.checked) {
handlePermissionChange('can_use_tasks', false);
handlePermissionChange('can_create_plugins', false);
}
}}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Use Plugins</span>
</label>
<label className={`flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer ${formData.permissions?.can_use_plugins ? 'hover:bg-gray-50 dark:hover:bg-gray-700/50' : 'opacity-50 cursor-not-allowed'}`}>
<input
type="checkbox"
checked={formData.permissions?.can_use_tasks || false}
onChange={(e) => handlePermissionChange('can_use_tasks', e.target.checked)}
disabled={!formData.permissions?.can_use_plugins}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Scheduled Tasks</span>
</label>
<label className={`flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer ${formData.permissions?.can_use_plugins ? 'hover:bg-gray-50 dark:hover:bg-gray-700/50' : 'opacity-50 cursor-not-allowed'}`}>
<input
type="checkbox"
checked={formData.permissions?.can_create_plugins || false}
onChange={(e) => handlePermissionChange('can_create_plugins', e.target.checked)}
disabled={!formData.permissions?.can_use_plugins}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Create Plugins</span>