Add service addons and manual scheduling features
Service Addons: - Add ServiceAddon model with optional resource assignment - Create AddonSelection component for booking flow - Add ServiceAddonManager for service configuration - Include addon API endpoints and serializers Manual Scheduling: - Add requires_manual_scheduling and capture_preferred_time to Service model - Add preferred_datetime and preferred_time_notes to Event model - Create ManualSchedulingRequest component for booking callback flow - Auto-open pending sidebar when requests exist or arrive via websocket - Show preferred times on pending items with detail modal popup - Add interactive UnscheduledBookingDemo component for help docs Scheduler Improvements: - Consolidate Create/EditAppointmentModal into single AppointmentModal - Update pending sidebar to show preferred schedule info - Add modal for pending request details with Schedule Now action Documentation: - Add Manual Scheduling section to HelpScheduler with interactive demo - Add Manual Scheduling section to HelpServices with interactive demo 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useRef, useMemo, useEffect } 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, Users, Bell, Mail, MessageSquare, Heart, Check } from 'lucide-react';
|
||||
import { Plus, Pencil, Trash2, Clock, DollarSign, X, Loader2, GripVertical, Eye, ChevronRight, Upload, ImagePlus, Image, AlertTriangle, Users, Bell, Mail, MessageSquare, Heart, Check, Phone, Calendar } from 'lucide-react';
|
||||
import { useServices, useCreateService, useUpdateService, useDeleteService, useReorderServices } from '../hooks/useServices';
|
||||
import { useResources } from '../hooks/useResources';
|
||||
import { useUpdateBusiness } from '../hooks/useBusiness';
|
||||
@@ -9,6 +9,7 @@ import { Service, User, Business } from '../types';
|
||||
import { getOverQuotaServiceIds } from '../utils/quotaUtils';
|
||||
import { CurrencyInput } from '../components/ui';
|
||||
import CustomerPreview from '../components/services/CustomerPreview';
|
||||
import ServiceAddonManager from '../components/services/ServiceAddonManager';
|
||||
|
||||
interface ServiceFormData {
|
||||
name: string;
|
||||
@@ -25,6 +26,9 @@ interface ServiceFormData {
|
||||
// Resource assignment fields
|
||||
all_resources: boolean;
|
||||
resource_ids: string[];
|
||||
// Manual scheduling (unscheduled booking)
|
||||
requires_manual_scheduling: boolean;
|
||||
capture_preferred_time: boolean;
|
||||
// Timing fields
|
||||
prep_time: number;
|
||||
takedown_time: number;
|
||||
@@ -283,6 +287,8 @@ const Services: React.FC = () => {
|
||||
deposit_percent: null,
|
||||
all_resources: true,
|
||||
resource_ids: [],
|
||||
requires_manual_scheduling: false,
|
||||
capture_preferred_time: true,
|
||||
prep_time: 0,
|
||||
takedown_time: 0,
|
||||
reminder_enabled: false,
|
||||
@@ -314,6 +320,8 @@ const Services: React.FC = () => {
|
||||
deposit_percent: service.deposit_percent || null,
|
||||
all_resources: service.all_resources ?? true,
|
||||
resource_ids: service.resource_ids || [],
|
||||
requires_manual_scheduling: service.requires_manual_scheduling || false,
|
||||
capture_preferred_time: service.capture_preferred_time ?? true,
|
||||
prep_time: service.prep_time || 0,
|
||||
takedown_time: service.takedown_time || 0,
|
||||
reminder_enabled: service.reminder_enabled || false,
|
||||
@@ -351,6 +359,9 @@ const Services: React.FC = () => {
|
||||
// Resource assignment
|
||||
all_resources: formData.all_resources,
|
||||
resource_ids: formData.all_resources ? [] : formData.resource_ids,
|
||||
// Manual scheduling (unscheduled booking)
|
||||
requires_manual_scheduling: formData.requires_manual_scheduling,
|
||||
capture_preferred_time: formData.requires_manual_scheduling ? formData.capture_preferred_time : true,
|
||||
// Timing fields
|
||||
prep_time: formData.prep_time,
|
||||
takedown_time: formData.takedown_time,
|
||||
@@ -1053,6 +1064,77 @@ const Services: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Manual Scheduling (Unscheduled Booking) */}
|
||||
<div className="p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg border border-orange-200 dark:border-orange-700">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Phone className="h-4 w-4 text-orange-600 dark:text-orange-400" />
|
||||
<div>
|
||||
<label className="text-sm font-medium text-orange-900 dark:text-orange-100">
|
||||
{t('services.requiresManualScheduling', 'Requires Manual Scheduling')}
|
||||
</label>
|
||||
<p className="text-xs text-orange-600 dark:text-orange-300">
|
||||
{t('services.manualSchedulingDescription', 'Online bookings go to Pending Requests for staff to call and schedule')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFormData({ ...formData, requires_manual_scheduling: !formData.requires_manual_scheduling })}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
formData.requires_manual_scheduling ? 'bg-orange-600' : 'bg-gray-300 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
formData.requires_manual_scheduling ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Nested option - Capture Preferred Time */}
|
||||
{formData.requires_manual_scheduling && (
|
||||
<div className="mt-3 pt-3 border-t border-orange-200 dark:border-orange-600">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4 text-orange-500 dark:text-orange-400" />
|
||||
<div>
|
||||
<span className="text-sm text-orange-800 dark:text-orange-200">
|
||||
{t('services.capturePreferredTime', 'Ask for Preferred Time')}
|
||||
</span>
|
||||
<p className="text-xs text-orange-600 dark:text-orange-300">
|
||||
{t('services.capturePreferredTimeDescription', 'Let customers indicate when they prefer to be scheduled')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFormData({ ...formData, capture_preferred_time: !formData.capture_preferred_time })}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
formData.capture_preferred_time ? 'bg-orange-500' : 'bg-gray-300 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
formData.capture_preferred_time ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info box when enabled */}
|
||||
{formData.requires_manual_scheduling && (
|
||||
<div className="mt-3 p-2 bg-orange-100 dark:bg-orange-800/30 rounded-lg">
|
||||
<p className="text-xs text-orange-800 dark:text-orange-200">
|
||||
{t('services.manualSchedulingNote', 'When a customer books this service, they won\'t select a time slot. The booking goes to Pending Requests for staff to call and schedule manually.')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Prep Time and Takedown Time */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
@@ -1313,6 +1395,14 @@ const Services: React.FC = () => {
|
||||
{t('services.photosHint', 'Drag photos to reorder. First photo is the primary image.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Service Addons - Only show when editing an existing service */}
|
||||
{editingService && (
|
||||
<ServiceAddonManager
|
||||
serviceId={parseInt(editingService.id)}
|
||||
serviceName={editingService.name}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 p-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50 shrink-0">
|
||||
|
||||
Reference in New Issue
Block a user