Files
smoothschedule/frontend/src/components/booking/PaymentSection.tsx
poduck 4a66246708 Add booking flow, business hours, and dark mode support
Features:
- Complete multi-step booking flow with service selection, date/time picker,
  auth (login/signup with email verification), payment, and confirmation
- Business hours settings page for defining when business is open
- TimeBlock purpose field (BUSINESS_HOURS, CLOSURE, UNAVAILABLE)
- Service resource assignment with prep/takedown time buffers
- Availability checking respects business hours and service buffers
- Customer registration via email verification code

UI/UX:
- Full dark mode support for all booking components
- Separate first/last name fields in signup form
- Back buttons on each wizard step
- Removed auto-redirect from confirmation page

API:
- Public endpoints for services, availability, business hours
- Customer verification and registration endpoints
- Tenant lookup from X-Business-Subdomain header

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 20:20:18 -05:00

160 lines
8.8 KiB
TypeScript

import React, { useState } from 'react';
import { PublicService } from '../../hooks/useBooking';
import { CreditCard, ShieldCheck, Lock } from 'lucide-react';
interface PaymentSectionProps {
service: PublicService;
onPaymentComplete: () => void;
}
export const PaymentSection: React.FC<PaymentSectionProps> = ({ service, onPaymentComplete }) => {
const [processing, setProcessing] = useState(false);
const [cardNumber, setCardNumber] = useState('');
const [expiry, setExpiry] = useState('');
const [cvc, setCvc] = useState('');
// Convert cents to dollars
const price = service.price_cents / 100;
const deposit = (service.deposit_amount_cents || 0) / 100;
// Auto-format card number
const handleCardInput = (e: React.ChangeEvent<HTMLInputElement>) => {
let val = e.target.value.replace(/\D/g, '');
val = val.substring(0, 16);
val = val.replace(/(\d{4})/g, '$1 ').trim();
setCardNumber(val);
};
const handlePayment = (e: React.FormEvent) => {
e.preventDefault();
setProcessing(true);
// Simulate Stripe Payment Intent & Processing
setTimeout(() => {
setProcessing(false);
onPaymentComplete();
}, 2000);
};
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Payment Details Column */}
<div className="lg:col-span-2 space-y-6">
<div className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center">
<CreditCard className="w-5 h-5 mr-2 text-indigo-600 dark:text-indigo-400" />
Card Details
</h3>
<div className="flex space-x-2">
{/* Mock Card Icons */}
<div className="h-6 w-10 bg-gray-200 dark:bg-gray-600 rounded"></div>
<div className="h-6 w-10 bg-gray-200 dark:bg-gray-600 rounded"></div>
<div className="h-6 w-10 bg-gray-200 dark:bg-gray-600 rounded"></div>
</div>
</div>
<form id="payment-form" onSubmit={handlePayment} className="space-y-5">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Card Number</label>
<input
type="text"
required
value={cardNumber}
onChange={handleCardInput}
placeholder="0000 0000 0000 0000"
className="block w-full px-3 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-indigo-500 focus:border-indigo-500 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 font-mono"
/>
</div>
<div className="grid grid-cols-2 gap-5">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Expiry Date</label>
<input
type="text"
required
value={expiry}
onChange={(e) => setExpiry(e.target.value)}
placeholder="MM / YY"
className="block w-full px-3 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-indigo-500 focus:border-indigo-500 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 font-mono"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">CVC</label>
<div className="relative">
<input
type="text"
required
value={cvc}
onChange={(e) => setCvc(e.target.value)}
placeholder="123"
className="block w-full px-3 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-indigo-500 focus:border-indigo-500 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 font-mono"
/>
<Lock className="w-4 h-4 text-gray-400 dark:text-gray-500 absolute right-3 top-3.5" />
</div>
</div>
</div>
<div className="mt-4 flex items-start p-4 bg-indigo-50 dark:bg-indigo-900/30 rounded-lg">
<ShieldCheck className="w-5 h-5 text-indigo-600 dark:text-indigo-400 mt-0.5 mr-3 flex-shrink-0" />
<p className="text-sm text-indigo-800 dark:text-indigo-200">
Your payment is secure. We use Stripe to process your payment. {deposit > 0 ? <>A deposit of <strong>${deposit.toFixed(2)}</strong> will be charged now.</> : <>Full payment will be collected at your appointment.</>}
</p>
</div>
</form>
</div>
</div>
{/* Summary Column */}
<div className="lg:col-span-1">
<div className="bg-gray-50 dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 sticky top-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">Payment Summary</h3>
<div className="space-y-3 text-sm">
<div className="flex justify-between text-gray-600 dark:text-gray-400">
<span>Service Total</span>
<span>${price.toFixed(2)}</span>
</div>
<div className="flex justify-between text-gray-600 dark:text-gray-400">
<span>Tax (Estimated)</span>
<span>$0.00</span>
</div>
<div className="border-t border-gray-200 dark:border-gray-600 my-2 pt-2"></div>
<div className="flex justify-between items-center text-lg font-bold text-gray-900 dark:text-white">
<span>Total</span>
<span>${price.toFixed(2)}</span>
</div>
</div>
{deposit > 0 ? (
<div className="mt-6 bg-white dark:bg-gray-700 p-4 rounded-lg border border-gray-200 dark:border-gray-600 shadow-sm">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-gray-900 dark:text-white">Due Now (Deposit)</span>
<span className="text-lg font-bold text-indigo-600 dark:text-indigo-400">${deposit.toFixed(2)}</span>
</div>
<div className="flex justify-between items-center text-sm text-gray-500 dark:text-gray-400">
<span>Due at appointment</span>
<span>${(price - deposit).toFixed(2)}</span>
</div>
</div>
) : (
<div className="mt-6 bg-white dark:bg-gray-700 p-4 rounded-lg border border-gray-200 dark:border-gray-600 shadow-sm">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-gray-900 dark:text-white">Due at appointment</span>
<span className="text-lg font-bold text-indigo-600 dark:text-indigo-400">${price.toFixed(2)}</span>
</div>
</div>
)}
<button
type="submit"
form="payment-form"
disabled={processing}
className="w-full mt-6 py-3 px-4 bg-indigo-600 dark:bg-indigo-500 text-white rounded-lg font-semibold shadow-md hover:bg-indigo-700 dark:hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-75 disabled:cursor-not-allowed transition-all"
>
{processing ? 'Processing...' : deposit > 0 ? `Pay $${deposit.toFixed(2)} Deposit` : 'Confirm Booking'}
</button>
</div>
</div>
</div>
);
};