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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user