From 2c4116871bb53591a65212dce8d6daf844e9fc06 Mon Sep 17 00:00:00 2001 From: nasmans <114198613+nasmans@users.noreply.github.com> Date: Wed, 29 Oct 2025 19:58:39 +0100 Subject: [PATCH] feat: add booking form and ticket modal --- src/components/booking/BookingForm.module.css | 100 ++++++ src/components/booking/BookingForm.tsx | 320 ++++++++++++++++++ .../booking/BookingTicketModal.module.css | 101 ++++++ src/components/booking/BookingTicketModal.tsx | 107 ++++++ src/context/ClientContext.tsx | 32 ++ src/types/css.d.ts | 4 + 6 files changed, 664 insertions(+) create mode 100644 src/components/booking/BookingForm.module.css create mode 100644 src/components/booking/BookingForm.tsx create mode 100644 src/components/booking/BookingTicketModal.module.css create mode 100644 src/components/booking/BookingTicketModal.tsx create mode 100644 src/context/ClientContext.tsx create mode 100644 src/types/css.d.ts diff --git a/src/components/booking/BookingForm.module.css b/src/components/booking/BookingForm.module.css new file mode 100644 index 0000000..647497f --- /dev/null +++ b/src/components/booking/BookingForm.module.css @@ -0,0 +1,100 @@ +.form { + display: grid; + gap: 24px; + background: #ffffff; + border-radius: 16px; + padding: 28px; + box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08); + font-family: 'Tajawal', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.fieldset { + border: 1px solid rgba(15, 23, 42, 0.08); + border-radius: 12px; + padding: 18px 20px 20px; + display: grid; + gap: 18px; +} + +.fieldset legend { + font-weight: 600; + font-size: 1rem; + padding: 0 6px; +} + +.label { + display: grid; + gap: 6px; + font-size: 0.95rem; + color: rgba(15, 23, 42, 0.78); +} + +.label input, +.label select, +.label textarea { + border: 1px solid rgba(15, 23, 42, 0.16); + border-radius: 10px; + padding: 10px 12px; + font-size: 0.95rem; + font-family: inherit; +} + +.label input[readonly], +.fieldset:disabled input, +.fieldset:disabled select { + background: rgba(148, 163, 184, 0.12); + color: rgba(15, 23, 42, 0.65); + cursor: not-allowed; +} + +.radioGroup { + display: flex; + gap: 24px; + flex-wrap: wrap; + font-size: 0.95rem; +} + +.homeOptions { + display: grid; + gap: 18px; +} + +.participants { + display: grid; + gap: 16px; +} + +.participantCard { + border: 1px solid rgba(47, 128, 237, 0.28); + border-radius: 12px; + padding: 16px; + background: rgba(47, 128, 237, 0.06); + display: grid; + gap: 12px; +} + +.participantCard h4 { + margin: 0; + font-size: 1rem; +} + +.actions { + display: flex; + justify-content: flex-end; +} + +.submitButton { + background: linear-gradient(130deg, #2f80ed, #56ccf2); + color: #ffffff; + border: none; + border-radius: 999px; + padding: 12px 26px; + font-weight: 600; + font-size: 1rem; + cursor: pointer; + transition: box-shadow 0.2s ease; +} + +.submitButton:hover { + box-shadow: 0 12px 22px rgba(47, 128, 237, 0.3); +} diff --git a/src/components/booking/BookingForm.tsx b/src/components/booking/BookingForm.tsx new file mode 100644 index 0000000..aa6721b --- /dev/null +++ b/src/components/booking/BookingForm.tsx @@ -0,0 +1,320 @@ +import React, { FormEvent, useMemo, useState } from 'react'; +import { useClientProfile } from '../../context/ClientContext'; +import styles from './BookingForm.module.css'; +import BookingTicketModal, { + BookingTicketDetails, + ParticipantDetail, +} from './BookingTicketModal'; + +export interface BookingFormProps { + onSubmitBooking?: (details: BookingTicketDetails) => void; + onShowTicketInDashboard?: (details: BookingTicketDetails) => void; +} + +interface ParticipantsState extends ParticipantDetail { + id: number; +} + +interface BookingFormState { + service: string; + date: string; + period: 'morning' | 'afternoon' | 'evening'; + visitType: 'center' | 'home'; + homeVisitTime: string; + participantsCount: number; + participants: ParticipantsState[]; + visitAddress: string; +} + +const serviceOptions: string[] = [ + 'جلسة استشارية', + 'خدمة علاجية', + 'متابعة شهرية', + 'زيارة منزلية', +]; + +const periodOptions: { value: BookingFormState['period']; label: string }[] = [ + { value: 'morning', label: 'الفترة الصباحية' }, + { value: 'afternoon', label: 'الفترة المسائية' }, + { value: 'evening', label: 'الفترة الليلية' }, +]; + +const buildParticipants = (count: number): ParticipantsState[] => { + return Array.from({ length: count }, (_, index) => ({ + id: index, + name: '', + age: '', + note: '', + })); +}; + +const BookingForm: React.FC = ({ + onSubmitBooking, + onShowTicketInDashboard, +}) => { + const client = useClientProfile(); + + const [formState, setFormState] = useState({ + service: serviceOptions[0], + date: '', + period: 'morning', + visitType: 'center', + homeVisitTime: '', + participantsCount: 1, + participants: buildParticipants(1), + visitAddress: '', + }); + + const [isTicketOpen, setIsTicketOpen] = useState(false); + + const visibleParticipants = useMemo(() => { + if (formState.participants.length === formState.participantsCount) { + return formState.participants; + } + + const next = buildParticipants(formState.participantsCount); + return next.map((participant, index) => ({ + ...participant, + ...formState.participants[index], + })); + }, [formState.participants, formState.participantsCount]); + + const updateField = (key: K, value: BookingFormState[K]) => { + setFormState((prev) => ({ + ...prev, + [key]: value, + ...(key === 'participantsCount' + ? { + participants: buildParticipants(value as number).map((participant, index) => ({ + ...participant, + ...prev.participants[index], + })), + } + : null), + })); + }; + + const updateParticipant = (index: number, key: keyof ParticipantDetail, value: string) => { + setFormState((prev) => { + const nextParticipants = [...visibleParticipants]; + nextParticipants[index] = { + ...nextParticipants[index], + [key]: value, + } as ParticipantsState; + + return { + ...prev, + participants: nextParticipants, + }; + }); + }; + + const buildTicketDetails = (): BookingTicketDetails => ({ + service: formState.service, + date: formState.date, + period: formState.period, + visitType: formState.visitType, + homeVisitTime: formState.visitType === 'home' ? formState.homeVisitTime : undefined, + visitAddress: formState.visitType === 'home' ? formState.visitAddress : undefined, + participants: + formState.visitType === 'home' + ? visibleParticipants.map(({ name, age, note }) => ({ + name, + age, + note, + })) + : [], + }); + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + const ticket = buildTicketDetails(); + onSubmitBooking?.(ticket); + setIsTicketOpen(true); + }; + + const handleOpenDashboard = () => { + const ticket = buildTicketDetails(); + onShowTicketInDashboard?.(ticket); + setIsTicketOpen(false); + }; + + return ( + <> +
+
+ بيانات العميل + + + +
+ +
+ تفاصيل الحجز + + + + + + +
+ +
+ طريقة الحضور +
+ + +
+ + {formState.visitType === 'home' && ( +
+ + + + + + +
+ {visibleParticipants.map((participant, index) => ( +
+

الفرد {index + 1}

+ + +