Files
smoothschedule/legacy_reference/frontend/src/components/QuickAddAppointment.tsx
poduck 2e111364a2 Initial commit: SmoothSchedule multi-tenant scheduling platform
This commit includes:
- Django backend with multi-tenancy (django-tenants)
- React + TypeScript frontend with Vite
- Platform administration API with role-based access control
- Authentication system with token-based auth
- Quick login dev tools for testing different user roles
- CORS and CSRF configuration for local development
- Docker development environment setup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 01:43:20 -05:00

253 lines
9.7 KiB
TypeScript

import React, { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { CalendarPlus, Clock, User, Briefcase, MapPin, FileText, Loader2, Check } from 'lucide-react';
import { useServices } from '../hooks/useServices';
import { useResources } from '../hooks/useResources';
import { useCustomers } from '../hooks/useCustomers';
import { useCreateAppointment } from '../hooks/useAppointments';
import { format } from 'date-fns';
interface QuickAddAppointmentProps {
onSuccess?: () => void;
}
const QuickAddAppointment: React.FC<QuickAddAppointmentProps> = ({ onSuccess }) => {
const { t } = useTranslation();
const { data: services } = useServices();
const { data: resources } = useResources();
const { data: customers } = useCustomers();
const createAppointment = useCreateAppointment();
const [customerId, setCustomerId] = useState('');
const [serviceId, setServiceId] = useState('');
const [resourceId, setResourceId] = useState('');
const [date, setDate] = useState(format(new Date(), 'yyyy-MM-dd'));
const [time, setTime] = useState('09:00');
const [notes, setNotes] = useState('');
const [showSuccess, setShowSuccess] = useState(false);
// Get selected service to auto-fill duration
const selectedService = useMemo(() => {
return services?.find(s => s.id === serviceId);
}, [services, serviceId]);
// Generate time slots (every 15 minutes from 6am to 10pm)
const timeSlots = useMemo(() => {
const slots = [];
for (let hour = 6; hour <= 22; hour++) {
for (let minute = 0; minute < 60; minute += 15) {
const h = hour.toString().padStart(2, '0');
const m = minute.toString().padStart(2, '0');
slots.push(`${h}:${m}`);
}
}
return slots;
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!serviceId || !date || !time) {
return;
}
const [hours, minutes] = time.split(':').map(Number);
const startTime = new Date(date);
startTime.setHours(hours, minutes, 0, 0);
try {
await createAppointment.mutateAsync({
customerId: customerId || undefined,
customerName: customerId ? (customers?.find(c => c.id === customerId)?.name || '') : 'Walk-in',
serviceId,
resourceId: resourceId || null,
startTime,
durationMinutes: selectedService?.durationMinutes || 60,
status: 'Scheduled',
notes,
});
// Show success state
setShowSuccess(true);
setTimeout(() => setShowSuccess(false), 2000);
// Reset form
setCustomerId('');
setServiceId('');
setResourceId('');
setNotes('');
setTime('09:00');
onSuccess?.();
} catch (error) {
console.error('Failed to create appointment:', error);
}
};
const activeCustomers = customers?.filter(c => c.status === 'Active') || [];
return (
<div className="p-6 bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-brand-100 dark:bg-brand-900/30 rounded-lg">
<CalendarPlus className="h-5 w-5 text-brand-600 dark:text-brand-400" />
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('dashboard.quickAddAppointment', 'Quick Add Appointment')}
</h3>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Customer Select */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<User className="inline h-4 w-4 mr-1" />
{t('appointments.customer', 'Customer')}
</label>
<select
value={customerId}
onChange={(e) => setCustomerId(e.target.value)}
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"
>
<option value="">{t('appointments.walkIn', 'Walk-in / No customer')}</option>
{activeCustomers.map((customer) => (
<option key={customer.id} value={customer.id}>
{customer.name} {customer.email && `(${customer.email})`}
</option>
))}
</select>
</div>
{/* Service Select */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<Briefcase className="inline h-4 w-4 mr-1" />
{t('appointments.service', 'Service')} *
</label>
<select
value={serviceId}
onChange={(e) => setServiceId(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"
>
<option value="">{t('appointments.selectService', 'Select service...')}</option>
{services?.map((service) => (
<option key={service.id} value={service.id}>
{service.name} ({service.durationMinutes} min - ${service.price})
</option>
))}
</select>
</div>
{/* Resource Select (Optional) */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<MapPin className="inline h-4 w-4 mr-1" />
{t('appointments.resource', 'Resource')}
</label>
<select
value={resourceId}
onChange={(e) => setResourceId(e.target.value)}
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"
>
<option value="">{t('appointments.unassigned', 'Unassigned')}</option>
{resources?.map((resource) => (
<option key={resource.id} value={resource.id}>
{resource.name}
</option>
))}
</select>
</div>
{/* Date and Time */}
<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('appointments.date', 'Date')} *
</label>
<input
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
required
min={format(new Date(), 'yyyy-MM-dd')}
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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<Clock className="inline h-4 w-4 mr-1" />
{t('appointments.time', 'Time')} *
</label>
<select
value={time}
onChange={(e) => setTime(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"
>
{timeSlots.map((slot) => (
<option key={slot} value={slot}>
{slot}
</option>
))}
</select>
</div>
</div>
{/* Duration Display */}
{selectedService && (
<div className="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-2">
<Clock className="h-4 w-4" />
{t('appointments.duration', 'Duration')}: {selectedService.durationMinutes} {t('common.minutes', 'minutes')}
</div>
)}
{/* Notes */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<FileText className="inline h-4 w-4 mr-1" />
{t('appointments.notes', 'Notes')}
</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={2}
placeholder={t('appointments.notesPlaceholder', 'Optional notes...')}
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 resize-none"
/>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={createAppointment.isPending || !serviceId}
className={`w-full py-2.5 px-4 rounded-lg font-medium transition-all flex items-center justify-center gap-2 ${
showSuccess
? 'bg-green-600 text-white'
: 'bg-brand-600 hover:bg-brand-700 text-white disabled:opacity-50 disabled:cursor-not-allowed'
}`}
>
{createAppointment.isPending ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
{t('common.creating', 'Creating...')}
</>
) : showSuccess ? (
<>
<Check className="h-4 w-4" />
{t('common.created', 'Created!')}
</>
) : (
<>
<CalendarPlus className="h-4 w-4" />
{t('appointments.addAppointment', 'Add Appointment')}
</>
)}
</button>
</form>
</div>
);
};
export default QuickAddAppointment;