feat: Add customer appointment details modal and ATM-style currency input
- Add appointment detail modal to CustomerDashboard with payment info display - Shows service, date/time, duration, status, and notes - Displays payment summary: service price, deposit paid, payment made, amount due - Print receipt functionality with secure DOM manipulation - Cancel appointment button for upcoming appointments - Add CurrencyInput component for ATM-style price entry - Digits entered as cents, shift left as more digits added (e.g., "1234" → $12.34) - Robust input validation: handles keyboard, mobile, paste, drop, IME - Only allows integer digits (0-9) - Update useAppointments hook to map payment fields from backend - Converts amounts from cents to dollars for display - Update Services page to use CurrencyInput for price and deposit fields 🤖 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,29 +1,50 @@
|
||||
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 { Plus, Pencil, Trash2, Clock, DollarSign, X, Loader2, GripVertical, Eye, ChevronRight, Upload, ImagePlus, Image, AlertTriangle, Users, Bell, Mail, MessageSquare, Heart } from 'lucide-react';
|
||||
import { useServices, useCreateService, useUpdateService, useDeleteService, useReorderServices } from '../hooks/useServices';
|
||||
import { useResources } from '../hooks/useResources';
|
||||
import { Service, User, Business } from '../types';
|
||||
import { getOverQuotaServiceIds } from '../utils/quotaUtils';
|
||||
import CurrencyInput from '../components/CurrencyInput';
|
||||
|
||||
interface ServiceFormData {
|
||||
name: string;
|
||||
durationMinutes: number;
|
||||
price: number;
|
||||
price_cents: number; // Price in cents (e.g., 5000 = $50.00)
|
||||
description: string;
|
||||
photos: string[];
|
||||
// Pricing fields
|
||||
variable_pricing: boolean;
|
||||
deposit_enabled: boolean;
|
||||
deposit_type: 'amount' | 'percent';
|
||||
deposit_amount: number | null;
|
||||
deposit_amount_cents: number | null; // Deposit in cents (e.g., 2500 = $25.00)
|
||||
deposit_percent: number | null;
|
||||
// Resource assignment fields
|
||||
all_resources: boolean;
|
||||
resource_ids: string[];
|
||||
// Timing fields
|
||||
prep_time: number;
|
||||
takedown_time: number;
|
||||
// Reminder notification fields
|
||||
reminder_enabled: boolean;
|
||||
reminder_hours_before: number;
|
||||
reminder_email: boolean;
|
||||
reminder_sms: boolean;
|
||||
// Thank you email
|
||||
thank_you_email_enabled: boolean;
|
||||
}
|
||||
|
||||
// Helper to format cents as dollars for display
|
||||
const formatCentsAsDollars = (cents: number): string => {
|
||||
return (cents / 100).toFixed(2);
|
||||
};
|
||||
|
||||
const Services: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useOutletContext<{ user: User, business: Business }>();
|
||||
const { data: services, isLoading, error } = useServices();
|
||||
const { data: resources } = useResources({ type: 'STAFF' }); // Only STAFF resources for services
|
||||
const createService = useCreateService();
|
||||
const updateService = useUpdateService();
|
||||
const deleteService = useDeleteService();
|
||||
@@ -40,14 +61,26 @@ const Services: React.FC = () => {
|
||||
const [formData, setFormData] = useState<ServiceFormData>({
|
||||
name: '',
|
||||
durationMinutes: 60,
|
||||
price: 0,
|
||||
price_cents: 0,
|
||||
description: '',
|
||||
photos: [],
|
||||
variable_pricing: false,
|
||||
deposit_enabled: false,
|
||||
deposit_type: 'amount',
|
||||
deposit_amount: null,
|
||||
deposit_amount_cents: null,
|
||||
deposit_percent: null,
|
||||
all_resources: true,
|
||||
resource_ids: [],
|
||||
// Timing fields
|
||||
prep_time: 0,
|
||||
takedown_time: 0,
|
||||
// Reminder notification fields
|
||||
reminder_enabled: false,
|
||||
reminder_hours_before: 24,
|
||||
reminder_email: true,
|
||||
reminder_sms: false,
|
||||
// Thank you email
|
||||
thank_you_email_enabled: false,
|
||||
});
|
||||
|
||||
// Photo gallery state
|
||||
@@ -211,14 +244,23 @@ const Services: React.FC = () => {
|
||||
setFormData({
|
||||
name: '',
|
||||
durationMinutes: 60,
|
||||
price: 0,
|
||||
price_cents: 0,
|
||||
description: '',
|
||||
photos: [],
|
||||
variable_pricing: false,
|
||||
deposit_enabled: false,
|
||||
deposit_type: 'amount',
|
||||
deposit_amount: null,
|
||||
deposit_amount_cents: null,
|
||||
deposit_percent: null,
|
||||
all_resources: true,
|
||||
resource_ids: [],
|
||||
prep_time: 0,
|
||||
takedown_time: 0,
|
||||
reminder_enabled: false,
|
||||
reminder_hours_before: 24,
|
||||
reminder_email: true,
|
||||
reminder_sms: false,
|
||||
thank_you_email_enabled: false,
|
||||
});
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
@@ -226,21 +268,30 @@ const Services: React.FC = () => {
|
||||
const openEditModal = (service: Service) => {
|
||||
setEditingService(service);
|
||||
// Determine deposit configuration from existing data
|
||||
const hasDeposit = (service.deposit_amount && service.deposit_amount > 0) ||
|
||||
const hasDeposit = (service.deposit_amount_cents && service.deposit_amount_cents > 0) ||
|
||||
(service.deposit_percent && service.deposit_percent > 0);
|
||||
const depositType = service.deposit_percent && service.deposit_percent > 0 ? 'percent' : 'amount';
|
||||
|
||||
setFormData({
|
||||
name: service.name,
|
||||
durationMinutes: service.durationMinutes,
|
||||
price: service.price,
|
||||
price_cents: service.price_cents || 0,
|
||||
description: service.description || '',
|
||||
photos: service.photos || [],
|
||||
variable_pricing: service.variable_pricing || false,
|
||||
deposit_enabled: hasDeposit,
|
||||
deposit_type: depositType,
|
||||
deposit_amount: service.deposit_amount || null,
|
||||
deposit_amount_cents: service.deposit_amount_cents || null,
|
||||
deposit_percent: service.deposit_percent || null,
|
||||
all_resources: service.all_resources ?? true,
|
||||
resource_ids: service.resource_ids || [],
|
||||
prep_time: service.prep_time || 0,
|
||||
takedown_time: service.takedown_time || 0,
|
||||
reminder_enabled: service.reminder_enabled || false,
|
||||
reminder_hours_before: service.reminder_hours_before || 24,
|
||||
reminder_email: service.reminder_email ?? true,
|
||||
reminder_sms: service.reminder_sms || false,
|
||||
thank_you_email_enabled: service.thank_you_email_enabled || false,
|
||||
});
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
@@ -257,17 +308,30 @@ const Services: React.FC = () => {
|
||||
const apiData = {
|
||||
name: formData.name,
|
||||
durationMinutes: formData.durationMinutes,
|
||||
price: formData.variable_pricing ? 0 : formData.price, // Price is 0 for variable pricing
|
||||
price_cents: formData.variable_pricing ? 0 : formData.price_cents, // Price is 0 for variable pricing
|
||||
description: formData.description,
|
||||
photos: formData.photos,
|
||||
variable_pricing: formData.variable_pricing,
|
||||
// Only send deposit values if deposit is enabled
|
||||
deposit_amount: formData.deposit_enabled && formData.deposit_type === 'amount'
|
||||
? formData.deposit_amount
|
||||
deposit_amount_cents: formData.deposit_enabled && formData.deposit_type === 'amount'
|
||||
? formData.deposit_amount_cents
|
||||
: null,
|
||||
deposit_percent: formData.deposit_enabled && formData.deposit_type === 'percent'
|
||||
? formData.deposit_percent
|
||||
: null,
|
||||
// Resource assignment
|
||||
all_resources: formData.all_resources,
|
||||
resource_ids: formData.all_resources ? [] : formData.resource_ids,
|
||||
// Timing fields
|
||||
prep_time: formData.prep_time,
|
||||
takedown_time: formData.takedown_time,
|
||||
// Reminder fields - only send if enabled
|
||||
reminder_enabled: formData.reminder_enabled,
|
||||
reminder_hours_before: formData.reminder_enabled ? formData.reminder_hours_before : 24,
|
||||
reminder_email: formData.reminder_enabled ? formData.reminder_email : true,
|
||||
reminder_sms: formData.reminder_enabled ? formData.reminder_sms : false,
|
||||
// Thank you email
|
||||
thank_you_email_enabled: formData.thank_you_email_enabled,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -424,10 +488,10 @@ const Services: React.FC = () => {
|
||||
<DollarSign className="h-3.5 w-3.5" />
|
||||
{service.variable_pricing ? (
|
||||
<>
|
||||
{t('services.fromPrice', 'From')} ${service.price.toFixed(2)}
|
||||
{t('services.fromPrice', 'From')} ${service.price}
|
||||
</>
|
||||
) : (
|
||||
`$${service.price.toFixed(2)}`
|
||||
`$${service.price}`
|
||||
)}
|
||||
</span>
|
||||
{service.variable_pricing && (
|
||||
@@ -446,6 +510,19 @@ const Services: React.FC = () => {
|
||||
{service.photos.length}
|
||||
</span>
|
||||
)}
|
||||
{/* Resource assignment indicator */}
|
||||
<span className="text-gray-500 dark:text-gray-400 flex items-center gap-1" title={
|
||||
service.all_resources
|
||||
? t('services.allResourcesAssigned', 'All resources can provide this service')
|
||||
: service.resource_names && service.resource_names.length > 0
|
||||
? service.resource_names.map(r => r.name).join(', ')
|
||||
: t('services.noResourcesAssigned', 'No resources assigned')
|
||||
}>
|
||||
<Users className="h-3.5 w-3.5" />
|
||||
{service.all_resources
|
||||
? t('services.allResourcesBadge', 'All')
|
||||
: service.resource_names?.length || 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -497,9 +574,9 @@ const Services: React.FC = () => {
|
||||
</span>
|
||||
<span className="font-semibold text-brand-600 dark:text-brand-400">
|
||||
{service.variable_pricing ? (
|
||||
<>From ${service.price.toFixed(2)}</>
|
||||
<>From ${service.price}</>
|
||||
) : (
|
||||
`$${service.price.toFixed(2)}`
|
||||
`$${service.price}`
|
||||
)}
|
||||
</span>
|
||||
{service.variable_pricing && service.deposit_display && (
|
||||
@@ -530,7 +607,7 @@ const Services: React.FC = () => {
|
||||
{/* Modal */}
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-lg w-full mx-4 max-h-[90vh] flex flex-col">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] flex flex-col">
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-100 dark:border-gray-700 shrink-0">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{editingService
|
||||
@@ -599,7 +676,7 @@ const Services: React.FC = () => {
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{t('services.duration', 'Duration (min)')} *
|
||||
{t('services.duration', 'Duration (Minutes)')} *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
@@ -613,23 +690,25 @@ const Services: React.FC = () => {
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{t('services.price', 'Price ($)')} {!formData.variable_pricing && '*'}
|
||||
{t('services.price', 'Price')} {!formData.variable_pricing && '*'}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.variable_pricing ? '' : formData.price}
|
||||
onChange={(e) => setFormData({ ...formData, price: parseFloat(e.target.value) || 0 })}
|
||||
required={!formData.variable_pricing}
|
||||
disabled={formData.variable_pricing}
|
||||
min={0}
|
||||
step={0.01}
|
||||
placeholder={formData.variable_pricing ? t('services.priceNA', 'N/A') : '0.00'}
|
||||
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-brand-500 ${
|
||||
formData.variable_pricing
|
||||
? 'bg-gray-100 dark:bg-gray-900 cursor-not-allowed'
|
||||
: 'bg-white dark:bg-gray-700'
|
||||
}`}
|
||||
/>
|
||||
{formData.variable_pricing ? (
|
||||
<input
|
||||
type="text"
|
||||
value=""
|
||||
disabled
|
||||
placeholder={t('services.priceNA', 'N/A')}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-white cursor-not-allowed"
|
||||
/>
|
||||
) : (
|
||||
<CurrencyInput
|
||||
value={formData.price_cents}
|
||||
onChange={(cents) => setFormData({ ...formData, price_cents: cents })}
|
||||
required={!formData.variable_pricing}
|
||||
placeholder="$0.00"
|
||||
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"
|
||||
/>
|
||||
)}
|
||||
{formData.variable_pricing && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{t('services.variablePriceNote', 'Price determined after service')}
|
||||
@@ -688,7 +767,7 @@ const Services: React.FC = () => {
|
||||
...formData,
|
||||
deposit_type: 'amount',
|
||||
deposit_percent: null,
|
||||
deposit_amount: formData.deposit_amount || 50,
|
||||
deposit_amount_cents: formData.deposit_amount_cents || 5000,
|
||||
})}
|
||||
className="w-4 h-4 text-brand-600 border-gray-300 focus:ring-brand-500"
|
||||
/>
|
||||
@@ -704,7 +783,7 @@ const Services: React.FC = () => {
|
||||
onChange={() => setFormData({
|
||||
...formData,
|
||||
deposit_type: 'percent',
|
||||
deposit_amount: null,
|
||||
deposit_amount_cents: null,
|
||||
deposit_percent: formData.deposit_percent || 25,
|
||||
})}
|
||||
className="w-4 h-4 text-brand-600 border-gray-300 focus:ring-brand-500"
|
||||
@@ -721,17 +800,15 @@ const Services: React.FC = () => {
|
||||
{(formData.variable_pricing || formData.deposit_type === 'amount') && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{t('services.depositAmount', 'Deposit Amount ($)')} *
|
||||
{t('services.depositAmount', 'Deposit Amount')} *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.deposit_amount || ''}
|
||||
onChange={(e) => setFormData({ ...formData, deposit_amount: parseFloat(e.target.value) || null })}
|
||||
<CurrencyInput
|
||||
value={formData.deposit_amount_cents || 0}
|
||||
onChange={(cents) => setFormData({ ...formData, deposit_amount_cents: cents || null })}
|
||||
required
|
||||
min={1}
|
||||
step={0.01}
|
||||
placeholder="$0.00"
|
||||
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="50.00"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -753,11 +830,9 @@ const Services: React.FC = () => {
|
||||
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="25"
|
||||
/>
|
||||
{formData.deposit_percent && formData.price > 0 && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{t('services.depositCalculated', 'Deposit: ${amount}', {
|
||||
amount: ((formData.price * formData.deposit_percent) / 100).toFixed(2)
|
||||
})}
|
||||
{formData.deposit_percent && formData.price_cents > 0 && (
|
||||
<p className="text-xs text-green-600 dark:text-green-400 mt-1">
|
||||
= ${formatCentsAsDollars(Math.round(formData.price_cents * formData.deposit_percent / 100))}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -770,6 +845,255 @@ const Services: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Resource Assignment */}
|
||||
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-700">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<label className="text-sm font-medium text-blue-900 dark:text-blue-100">
|
||||
{t('services.resourceAssignment', 'Who Can Provide This Service?')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* All Resources Toggle */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<span className="text-sm text-blue-800 dark:text-blue-200">
|
||||
{t('services.allResources', 'All Resources')}
|
||||
</span>
|
||||
<p className="text-xs text-blue-600 dark:text-blue-300">
|
||||
{t('services.allResourcesDescription', 'Any resource can be booked for this service')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFormData({
|
||||
...formData,
|
||||
all_resources: !formData.all_resources,
|
||||
resource_ids: !formData.all_resources ? [] : formData.resource_ids,
|
||||
})}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
formData.all_resources ? 'bg-blue-600' : 'bg-gray-300 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
formData.all_resources ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Specific Resource Selection */}
|
||||
{!formData.all_resources && (
|
||||
<div className="mt-3 pt-3 border-t border-blue-200 dark:border-blue-600">
|
||||
<p className="text-xs text-blue-600 dark:text-blue-300 mb-2">
|
||||
{t('services.selectSpecificResources', 'Select specific resources that can provide this service:')}
|
||||
</p>
|
||||
{resources && resources.length > 0 ? (
|
||||
<div className="space-y-2 max-h-40 overflow-y-auto">
|
||||
{resources.map((resource) => (
|
||||
<label
|
||||
key={resource.id}
|
||||
className="flex items-center gap-2 cursor-pointer p-2 rounded hover:bg-blue-100 dark:hover:bg-blue-800/30 transition-colors"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.resource_ids.includes(resource.id)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setFormData({
|
||||
...formData,
|
||||
resource_ids: [...formData.resource_ids, resource.id],
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
...formData,
|
||||
resource_ids: formData.resource_ids.filter(id => id !== resource.id),
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="w-4 h-4 text-blue-600 border-blue-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-blue-900 dark:text-blue-100">
|
||||
{resource.name}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-blue-600 dark:text-blue-300 italic">
|
||||
{t('services.noStaffResources', 'No resources available. Add resources first.')}
|
||||
</p>
|
||||
)}
|
||||
{!formData.all_resources && formData.resource_ids.length === 0 && resources && resources.length > 0 && (
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400 mt-2">
|
||||
{t('services.selectAtLeastOne', 'Select at least one resource, or enable "All Resources"')}
|
||||
</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">
|
||||
<Clock className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
<label className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{t('services.bufferTime', 'Buffer Time')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{t('services.prepTime', 'Prep Time (Minutes)')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.prep_time}
|
||||
onChange={(e) => setFormData({ ...formData, prep_time: parseInt(e.target.value) || 0 })}
|
||||
min={0}
|
||||
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"
|
||||
placeholder="0"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{t('services.prepTimeHint', 'Time needed before the appointment')}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{t('services.takedownTime', 'Takedown Time (Minutes)')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.takedown_time}
|
||||
onChange={(e) => setFormData({ ...formData, takedown_time: parseInt(e.target.value) || 0 })}
|
||||
min={0}
|
||||
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"
|
||||
placeholder="0"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{t('services.takedownTimeHint', 'Time needed after the appointment')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reminder Notifications */}
|
||||
<div className="p-4 bg-amber-50 dark:bg-amber-900/20 rounded-lg border border-amber-200 dark:border-amber-700">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bell className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
||||
<label className="text-sm font-medium text-amber-900 dark:text-amber-100">
|
||||
{t('services.reminderNotifications', 'Reminder Notifications')}
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFormData({ ...formData, reminder_enabled: !formData.reminder_enabled })}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
formData.reminder_enabled ? 'bg-amber-600' : 'bg-gray-300 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
formData.reminder_enabled ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{formData.reminder_enabled && (
|
||||
<div className="space-y-4 pt-3 border-t border-amber-200 dark:border-amber-600">
|
||||
{/* Reminder timing */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-amber-800 dark:text-amber-200 mb-1">
|
||||
{t('services.reminderTiming', 'Send reminder')}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
value={formData.reminder_hours_before}
|
||||
onChange={(e) => setFormData({ ...formData, reminder_hours_before: parseInt(e.target.value) || 24 })}
|
||||
min={1}
|
||||
max={168}
|
||||
className="w-20 px-3 py-2 border border-amber-300 dark:border-amber-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-amber-500 focus:border-amber-500"
|
||||
/>
|
||||
<span className="text-sm text-amber-800 dark:text-amber-200">
|
||||
{t('services.hoursBefore', 'hours before appointment')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reminder methods */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-amber-800 dark:text-amber-200 mb-2">
|
||||
{t('services.reminderMethod', 'Send via')}
|
||||
</label>
|
||||
<div className="flex gap-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.reminder_email}
|
||||
onChange={(e) => setFormData({ ...formData, reminder_email: e.target.checked })}
|
||||
className="w-4 h-4 text-amber-600 border-amber-300 rounded focus:ring-amber-500"
|
||||
/>
|
||||
<Mail className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
||||
<span className="text-sm text-amber-800 dark:text-amber-200">
|
||||
{t('services.email', 'Email')}
|
||||
</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.reminder_sms}
|
||||
onChange={(e) => setFormData({ ...formData, reminder_sms: e.target.checked })}
|
||||
className="w-4 h-4 text-amber-600 border-amber-300 rounded focus:ring-amber-500"
|
||||
/>
|
||||
<MessageSquare className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
||||
<span className="text-sm text-amber-800 dark:text-amber-200">
|
||||
{t('services.sms', 'Text Message')}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Thank You Email */}
|
||||
<div className="p-4 bg-pink-50 dark:bg-pink-900/20 rounded-lg border border-pink-200 dark:border-pink-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Heart className="h-4 w-4 text-pink-600 dark:text-pink-400" />
|
||||
<div>
|
||||
<label className="text-sm font-medium text-pink-900 dark:text-pink-100">
|
||||
{t('services.thankYouEmail', 'Thank You Email')}
|
||||
</label>
|
||||
<p className="text-xs text-pink-600 dark:text-pink-300">
|
||||
{t('services.thankYouEmailDescription', 'Send a follow-up email after the appointment')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFormData({ ...formData, thank_you_email_enabled: !formData.thank_you_email_enabled })}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
formData.thank_you_email_enabled ? 'bg-pink-600' : 'bg-gray-300 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
formData.thank_you_email_enabled ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
|
||||
Reference in New Issue
Block a user