import React, { useState, useRef, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; 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, User, Business } from '../types'; import { getOverQuotaServiceIds } from '../utils/quotaUtils'; interface ServiceFormData { name: string; durationMinutes: number; price: number; description: string; photos: string[]; } 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(null); const [formData, setFormData] = useState({ name: '', durationMinutes: 60, price: 0, description: '', photos: [], }); // Photo gallery state const [isDraggingPhoto, setIsDraggingPhoto] = useState(false); const [draggedPhotoIndex, setDraggedPhotoIndex] = useState(null); const [dragOverPhotoIndex, setDragOverPhotoIndex] = useState(null); // Drag and drop state const [draggedId, setDraggedId] = useState(null); const [dragOverId, setDragOverId] = useState(null); const [localServices, setLocalServices] = useState(null); const dragNodeRef = useRef(null); // Use local state during drag, otherwise use fetched data const displayServices = localServices ?? services; // Drag handlers const handleDragStart = (e: React.DragEvent, serviceId: string) => { setDraggedId(serviceId); dragNodeRef.current = e.currentTarget; e.dataTransfer.effectAllowed = 'move'; // Add a slight delay to allow the drag image to be set setTimeout(() => { if (dragNodeRef.current) { dragNodeRef.current.style.opacity = '0.5'; } }, 0); }; const handleDragEnd = () => { if (dragNodeRef.current) { dragNodeRef.current.style.opacity = '1'; } setDraggedId(null); setDragOverId(null); dragNodeRef.current = null; // If we have local changes, save them if (localServices) { const orderedIds = localServices.map(s => s.id); reorderServices.mutate(orderedIds, { onSettled: () => { setLocalServices(null); } }); } }; const handleDragOver = (e: React.DragEvent, serviceId: string) => { e.preventDefault(); if (draggedId === serviceId) return; setDragOverId(serviceId); // Reorder locally for visual feedback const currentServices = localServices ?? services ?? []; const draggedIndex = currentServices.findIndex(s => s.id === draggedId); const targetIndex = currentServices.findIndex(s => s.id === serviceId); if (draggedIndex === -1 || targetIndex === -1 || draggedIndex === targetIndex) return; const newServices = [...currentServices]; const [removed] = newServices.splice(draggedIndex, 1); newServices.splice(targetIndex, 0, removed); setLocalServices(newServices); }; const handleDragLeave = () => { setDragOverId(null); }; // Photo upload handlers const handlePhotoDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDraggingPhoto(false); const files = e.dataTransfer.files; if (files && files.length > 0) { Array.from(files).forEach((file) => { if (file.type.startsWith('image/')) { const reader = new FileReader(); reader.onloadend = () => { setFormData((prev) => ({ ...prev, photos: [...prev.photos, reader.result as string], })); }; reader.readAsDataURL(file); } }); } }; const handlePhotoDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDraggingPhoto(true); }; const handlePhotoDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDraggingPhoto(false); }; const handlePhotoUpload = (e: React.ChangeEvent) => { const files = e.target.files; if (files && files.length > 0) { Array.from(files).forEach((file) => { if (file.type.startsWith('image/')) { const reader = new FileReader(); reader.onloadend = () => { setFormData((prev) => ({ ...prev, photos: [...prev.photos, reader.result as string], })); }; reader.readAsDataURL(file); } }); } // Reset input e.target.value = ''; }; const removePhoto = (index: number) => { setFormData((prev) => ({ ...prev, photos: prev.photos.filter((_, i) => i !== index), })); }; // Photo reorder drag handlers const handlePhotoReorderStart = (e: React.DragEvent, index: number) => { setDraggedPhotoIndex(index); e.dataTransfer.effectAllowed = 'move'; }; const handlePhotoReorderOver = (e: React.DragEvent, index: number) => { e.preventDefault(); if (draggedPhotoIndex === null || draggedPhotoIndex === index) return; setDragOverPhotoIndex(index); // Reorder photos const newPhotos = [...formData.photos]; const [removed] = newPhotos.splice(draggedPhotoIndex, 1); newPhotos.splice(index, 0, removed); setFormData((prev) => ({ ...prev, photos: newPhotos })); setDraggedPhotoIndex(index); }; const handlePhotoReorderEnd = () => { setDraggedPhotoIndex(null); setDragOverPhotoIndex(null); }; const openCreateModal = () => { setEditingService(null); setFormData({ name: '', durationMinutes: 60, price: 0, description: '', photos: [], }); setIsModalOpen(true); }; const openEditModal = (service: Service) => { setEditingService(service); setFormData({ name: service.name, durationMinutes: service.durationMinutes, price: service.price, description: service.description || '', photos: service.photos || [], }); setIsModalOpen(true); }; const closeModal = () => { setIsModalOpen(false); setEditingService(null); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { if (editingService) { await updateService.mutateAsync({ id: editingService.id, updates: formData, }); } else { await createService.mutateAsync(formData); } closeModal(); } catch (error) { console.error('Failed to save service:', error); } }; const handleDelete = async (id: string) => { if (window.confirm(t('services.confirmDelete', 'Are you sure you want to delete this service?'))) { try { await deleteService.mutateAsync(id); } catch (error) { console.error('Failed to delete service:', error); } } }; if (isLoading) { return (
); } if (error) { return (
{t('common.error')}: {error instanceof Error ? error.message : 'Unknown error'}
); } return (

{t('services.title', 'Services')}

{t('services.description', 'Manage the services your business offers')}

{displayServices && displayServices.length === 0 ? (
{t('services.noServices', 'No services yet. Add your first service to get started.')}
) : (
{/* Left Column - Editable Services List */}

{t('services.dragToReorder', 'Drag services to reorder how they appear in menus')}

{displayServices?.map((service) => { const isOverQuota = overQuotaServiceIds.has(service.id); return (
handleDragStart(e, service.id)} onDragEnd={handleDragEnd} onDragOver={(e) => handleDragOver(e, service.id)} onDragLeave={handleDragLeave} 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 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} >
{isOverQuota ? ( ) : ( )}

{service.name} {isOverQuota && ( Over quota )}

{service.description && (

{service.description}

)}
{service.durationMinutes} {t('common.minutes', 'min')} ${service.price.toFixed(2)} {service.photos && service.photos.length > 0 && ( {service.photos.length} )}
); })}
{/* Right Column - Customer Preview Mockup */}

{t('services.customerPreview', 'Customer Preview')}

{/* Mockup Container - styled like a booking widget */}
{/* Mockup Header */}

{t('services.selectService', 'Select a Service')}

{t('services.chooseFromMenu', 'Choose from our available services')}

{/* Services List */}
{displayServices?.map((service) => (
{service.name}
{service.description && (

{service.description}

)}
{service.durationMinutes} min ${service.price.toFixed(2)}
))}
{/* Mockup Footer */}

{t('services.mockupNote', 'Preview only - not clickable')}

)} {/* Modal */} {isModalOpen && (

{editingService ? t('services.editService', 'Edit Service') : t('services.addService', 'Add Service')}

setFormData({ ...formData, name: e.target.value })} required className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-brand-500" placeholder={t('services.namePlaceholder', 'e.g., Haircut, Massage, Consultation')} />
setFormData({ ...formData, durationMinutes: parseInt(e.target.value) || 0 })} required min={5} step={5} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-brand-500" />
setFormData({ ...formData, price: parseFloat(e.target.value) || 0 })} required min={0} step={0.01} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-brand-500" />