feat: Quota enforcement UI and various improvements

- Add quota limit warnings to Resources, Services, and OwnerScheduler pages
- Add quotaUtils.ts for checking quota limits
- Update BusinessLayout with quota context
- Improve email receiver logging
- Update serializers

🤖 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 15:47:48 -05:00
parent fd751f02f8
commit 4f515c3710
8 changed files with 281 additions and 36 deletions

View File

@@ -1,8 +1,10 @@
import React, { useState, useRef } from 'react';
import React, { useState, useRef, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Plus, Pencil, Trash2, Clock, DollarSign, X, Loader2, GripVertical, Eye, ChevronRight, Upload, ImagePlus, Image } from 'lucide-react';
import { useOutletContext } from 'react-router-dom';
import { Plus, Pencil, Trash2, Clock, DollarSign, X, Loader2, GripVertical, Eye, ChevronRight, Upload, ImagePlus, Image, AlertTriangle } from 'lucide-react';
import { useServices, useCreateService, useUpdateService, useDeleteService, useReorderServices } from '../hooks/useServices';
import { Service } from '../types';
import { Service, User, Business } from '../types';
import { getOverQuotaServiceIds } from '../utils/quotaUtils';
interface ServiceFormData {
name: string;
@@ -14,12 +16,19 @@ interface ServiceFormData {
const Services: React.FC = () => {
const { t } = useTranslation();
const { user } = useOutletContext<{ user: User, business: Business }>();
const { data: services, isLoading, error } = useServices();
const createService = useCreateService();
const updateService = useUpdateService();
const deleteService = useDeleteService();
const reorderServices = useReorderServices();
// Calculate over-quota services (will be auto-archived when grace period ends)
const overQuotaServiceIds = useMemo(
() => getOverQuotaServiceIds(services || [], user.quota_overages),
[services, user.quota_overages]
);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingService, setEditingService] = useState<Service | null>(null);
const [formData, setFormData] = useState<ServiceFormData>({
@@ -304,7 +313,9 @@ const Services: React.FC = () => {
{t('services.dragToReorder', 'Drag services to reorder how they appear in menus')}
</p>
<div className="space-y-3">
{displayServices?.map((service) => (
{displayServices?.map((service) => {
const isOverQuota = overQuotaServiceIds.has(service.id);
return (
<div
key={service.id}
draggable
@@ -312,20 +323,32 @@ const Services: React.FC = () => {
onDragEnd={handleDragEnd}
onDragOver={(e) => handleDragOver(e, service.id)}
onDragLeave={handleDragLeave}
className={`p-4 bg-white dark:bg-gray-800 border rounded-xl shadow-sm cursor-move transition-all ${
draggedId === service.id
? 'opacity-50 border-brand-500'
className={`p-4 border rounded-xl shadow-sm cursor-move transition-all ${
isOverQuota
? 'bg-amber-50/50 dark:bg-amber-900/10 border-amber-300 dark:border-amber-600 opacity-70'
: draggedId === service.id
? 'opacity-50 border-brand-500 bg-white dark:bg-gray-800'
: dragOverId === service.id
? 'border-brand-500 ring-2 ring-brand-500/50'
: 'border-gray-100 dark:border-gray-700'
? 'border-brand-500 ring-2 ring-brand-500/50 bg-white dark:bg-gray-800'
: 'border-gray-100 dark:border-gray-700 bg-white dark:bg-gray-800'
}`}
title={isOverQuota ? 'Over quota - will be archived if not resolved' : undefined}
>
<div className="flex items-center gap-3">
<GripVertical className="h-5 w-5 text-gray-400 cursor-grab active:cursor-grabbing shrink-0" />
{isOverQuota ? (
<AlertTriangle className="h-5 w-5 text-amber-600 dark:text-amber-400 shrink-0" />
) : (
<GripVertical className="h-5 w-5 text-gray-400 cursor-grab active:cursor-grabbing shrink-0" />
)}
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-gray-900 dark:text-white truncate">
<h3 className={`font-semibold truncate ${isOverQuota ? 'text-amber-800 dark:text-amber-300' : 'text-gray-900 dark:text-white'}`}>
{service.name}
{isOverQuota && (
<span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-amber-100 text-amber-700 dark:bg-amber-800/50 dark:text-amber-400">
Over quota
</span>
)}
</h3>
<div className="flex items-center gap-1 shrink-0 ml-2">
<button
@@ -368,7 +391,8 @@ const Services: React.FC = () => {
</div>
</div>
</div>
))}
);
})}
</div>
</div>