Quota Overage System: - Add QuotaOverage model for tracking resource/user quota overages - Implement 30-day grace period with email notifications (immediate, 7-day, 1-day) - Add QuotaWarningBanner component in BusinessLayout - Add QuotaSettings page for managing overages and archiving resources - Add Celery tasks for automated quota checks and expiration handling - Add quota management API endpoints Updated Tier Pricing (Stripe: 2.9% + $0.30): - Free: No payments (requires addon) - Starter: 4% + $0.40 - Professional: 3.5% + $0.35 - Business: 3.25% + $0.32 - Enterprise: 3% + $0.30 New Subscription Addons: - Online Payments ($5/mo + 5% + $0.50) - for Free tier - SMS Notifications ($10/mo) - enables SMS reminders - Masked Calling ($15/mo) - enables anonymous calling BusinessEditModal Improvements: - Increased width to match PlanModal (max-w-3xl) - Added all tier options with auto-update on tier change - Added limits configuration and permissions sections Backend Fixes: - Fixed SubscriptionPlan serializer to include all communication fields - Allow blank business_tier for addon plans - Added migration for business_tier field changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
132 lines
4.7 KiB
TypeScript
132 lines
4.7 KiB
TypeScript
import React from 'react';
|
|
import { AlertTriangle, X, ExternalLink } from 'lucide-react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Link } from 'react-router-dom';
|
|
import { QuotaOverage } from '../api/auth';
|
|
|
|
interface QuotaWarningBannerProps {
|
|
overages: QuotaOverage[];
|
|
onDismiss?: () => void;
|
|
}
|
|
|
|
const QuotaWarningBanner: React.FC<QuotaWarningBannerProps> = ({ overages, onDismiss }) => {
|
|
const { t } = useTranslation();
|
|
|
|
if (!overages || overages.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
// Find the most urgent overage (least days remaining)
|
|
const mostUrgent = overages.reduce((prev, curr) =>
|
|
curr.days_remaining < prev.days_remaining ? curr : prev
|
|
);
|
|
|
|
const isUrgent = mostUrgent.days_remaining <= 7;
|
|
const isCritical = mostUrgent.days_remaining <= 1;
|
|
|
|
const getBannerStyles = () => {
|
|
if (isCritical) {
|
|
return 'bg-red-600 text-white border-red-700';
|
|
}
|
|
if (isUrgent) {
|
|
return 'bg-amber-500 text-white border-amber-600';
|
|
}
|
|
return 'bg-amber-100 text-amber-900 border-amber-300';
|
|
};
|
|
|
|
const getIconColor = () => {
|
|
if (isCritical || isUrgent) {
|
|
return 'text-white';
|
|
}
|
|
return 'text-amber-600';
|
|
};
|
|
|
|
const formatDate = (dateString: string) => {
|
|
return new Date(dateString).toLocaleDateString(undefined, {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className={`border-b ${getBannerStyles()}`}>
|
|
<div className="max-w-7xl mx-auto px-4 py-3 sm:px-6 lg:px-8">
|
|
<div className="flex items-center justify-between flex-wrap gap-2">
|
|
<div className="flex items-center gap-3">
|
|
<AlertTriangle className={`h-5 w-5 flex-shrink-0 ${getIconColor()}`} />
|
|
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
|
|
<span className="font-medium">
|
|
{isCritical
|
|
? t('quota.banner.critical', 'URGENT: Automatic archiving tomorrow!')
|
|
: isUrgent
|
|
? t('quota.banner.urgent', 'Action Required: {{days}} days left', { days: mostUrgent.days_remaining })
|
|
: t('quota.banner.warning', 'Quota exceeded for {{count}} item(s)', { count: overages.length })
|
|
}
|
|
</span>
|
|
<span className="text-sm opacity-90">
|
|
{t('quota.banner.details',
|
|
'You have {{overage}} {{type}} over your plan limit. Grace period ends {{date}}.',
|
|
{
|
|
overage: mostUrgent.overage_amount,
|
|
type: mostUrgent.display_name,
|
|
date: formatDate(mostUrgent.grace_period_ends_at)
|
|
}
|
|
)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Link
|
|
to="/settings/quota"
|
|
className={`inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
|
isCritical || isUrgent
|
|
? 'bg-white/20 hover:bg-white/30 text-white'
|
|
: 'bg-amber-600 hover:bg-amber-700 text-white'
|
|
}`}
|
|
>
|
|
{t('quota.banner.manage', 'Manage Quota')}
|
|
<ExternalLink className="h-4 w-4" />
|
|
</Link>
|
|
{onDismiss && (
|
|
<button
|
|
onClick={onDismiss}
|
|
className={`p-1 rounded-md transition-colors ${
|
|
isCritical || isUrgent
|
|
? 'hover:bg-white/20'
|
|
: 'hover:bg-amber-200'
|
|
}`}
|
|
aria-label={t('common.dismiss', 'Dismiss')}
|
|
>
|
|
<X className="h-5 w-5" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Show additional overages if there are more than one */}
|
|
{overages.length > 1 && (
|
|
<div className="mt-2 text-sm opacity-90">
|
|
<span className="font-medium">{t('quota.banner.allOverages', 'All overages:')}</span>
|
|
<ul className="ml-4 mt-1 space-y-0.5">
|
|
{overages.map((overage) => (
|
|
<li key={overage.id}>
|
|
{overage.display_name}: {overage.current_usage}/{overage.allowed_limit}
|
|
({t('quota.banner.overBy', 'over by {{amount}}', { amount: overage.overage_amount })})
|
|
{' - '}
|
|
{overage.days_remaining <= 0
|
|
? t('quota.banner.expiredToday', 'expires today!')
|
|
: t('quota.banner.daysLeft', '{{days}} days left', { days: overage.days_remaining })
|
|
}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default QuotaWarningBanner;
|