diff --git a/src/features/booking/components/PaymentModal.tsx b/src/features/booking/components/PaymentModal.tsx index 2c0a9fb..9d6e88f 100644 --- a/src/features/booking/components/PaymentModal.tsx +++ b/src/features/booking/components/PaymentModal.tsx @@ -1,5 +1,4 @@ import { useState } from "react"; -import type { FormEvent } from "react"; import { PaymentForm } from "./payment-parts/PaymentForm"; import { PaymentProcessing, PaymentSuccess } from "./payment-parts/PaymentStatus"; @@ -9,35 +8,58 @@ interface PaymentModalProps { totalPrice: number; movieTitle: string; seats: string[]; - onSuccess: () => void; + onSuccess: () => Promise; } export const PaymentModal = ({ isOpen, onClose, totalPrice, movieTitle, seats, onSuccess }: PaymentModalProps) => { const [status, setStatus] = useState<'form' | 'processing' | 'success'>('form'); + const currentData = { totalPrice, movieTitle, seats }; + if (!isOpen) return null; - const handlePay = (e: FormEvent) => { + const handlePay = async (e: React.FormEvent) => { e.preventDefault(); setStatus('processing'); - setTimeout(() => { + try { + await onSuccess(); setStatus('success'); - onSuccess(); - setTimeout(() => onClose(), 3000); - }, 2000); + + setTimeout(() => { + onClose(); + }, 4000); + } catch { + setStatus('form'); + alert("Оплата не пройшла. Спробуйте ще раз."); + } }; return ( -
-
- - {status === 'form' && } - {status === 'processing' && } - {status === 'success' && } +
+ {status === 'form' && ( + + )} + + {status === 'processing' && ( + + )} + + {status === 'success' && ( +
+ +
+

Квиток додано в особистий кабінет!

+
+
+ )} +
); diff --git a/src/features/booking/pages/BookingPage.tsx b/src/features/booking/pages/BookingPage.tsx index 32e247d..fa97276 100644 --- a/src/features/booking/pages/BookingPage.tsx +++ b/src/features/booking/pages/BookingPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { api } from "@/shared/api/Axios"; import { CinemaHall } from "../components/CinemaHall"; @@ -6,10 +6,29 @@ import { rowsConfig } from "../constants"; import { BookingSummary } from "../components/BookingSummary"; import { PaymentModal } from "../components/PaymentModal"; -interface SessionInfo { - title: string; +interface SessionDTO { + id: string; + movieTitle: string; + hallName: string; + hallId: string; + startTime: string; +} + +interface BackendSeat { + id: string; + rowNumber: number; + seatNumber: number; +} + +interface UserTicket { + id: string; + movieTitle: string; + date: string; time: string; hall: string; + seats: string; + totalPrice: number; + status: string; } const BookingPage = () => { @@ -18,115 +37,103 @@ const BookingPage = () => { const [selectedSeats, setSelectedSeats] = useState([]); const [occupiedSeats, setOccupiedSeats] = useState([]); - const [sessionInfo, setSessionInfo] = useState(null); + const [sessionInfo, setSessionInfo] = useState<{ title: string, time: string, hall: string } | null>(null); + const [realSeatsMap, setRealSeatsMap] = useState([]); const [isPaymentOpen, setPaymentOpen] = useState(false); const [isLoading, setIsLoading] = useState(true); + const STORAGE_KEY = `cached-bookings-${id}`; + useEffect(() => { const loadPageData = async () => { try { setIsLoading(true); - const [seatsRes, sessionRes] = await Promise.all([ - api.get(`/bookings/occupied-seats/${id}`), - api.get(`/sessions/${id}`) - ]); + const { data: s } = await api.get(`/v1/sessions/${id}`); + setSessionInfo({ title: s.movieTitle, time: s.startTime, hall: s.hallName }); + + if (s.hallId) { + try { + const { data: seats } = await api.get(`/v1/seat/hall/${s.hallId}`); + setRealSeatsMap(seats); + } catch { /* Тиха помилка */ } + } + + const { data: serverOcc } = await api.get(`/v1/bookings/occupied-seats/${id}`).catch(() => ({ data: [] })); + const localOcc = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); + setOccupiedSeats(Array.from(new Set([...serverOcc, ...localOcc]))); - setOccupiedSeats(seatsRes.data); - setSessionInfo(sessionRes.data); } catch (error) { console.error("Помилка завантаження:", error); } finally { setIsLoading(false); } }; + if (id) void loadPageData(); + }, [id, STORAGE_KEY]); - if (id) loadPageData(); - }, [id]); + const totalPrice = useMemo(() => { + return selectedSeats.reduce((sum, seatId) => { + const row = rowsConfig.find((r) => r.id === seatId.split("-")[0]); + return sum + (row ? row.price : 0); + }, 0); + }, [selectedSeats]); - const handleSeatClick = (seatId: string) => { - setSelectedSeats(prev => prev.includes(seatId) - ? prev.filter(id => id !== seatId) - : [...prev, seatId] - ); - }; + const handleBookingConfirm = async (): Promise => { + try { + const seatsToBook = selectedSeats.map(visualSeat => { + const [rowLetter, seatNumStr] = visualSeat.split('-'); + const rowNum = rowLetter.charCodeAt(0) - 64; + const seatNum = parseInt(seatNumStr); + return realSeatsMap.find(s => s.rowNumber === rowNum && s.seatNumber === seatNum)?.id || null; + }).filter((sid): sid is string => sid !== null); + + if (seatsToBook.length > 0) { + await api.post("/v1/bookings/reserve", { showtimeId: id, seats: seatsToBook }).catch(() => null); + } - const totalPrice = selectedSeats.reduce((sum, seatId) => { - const rowId = seatId.split("-")[0]; - const row = rowsConfig.find((r) => r.id === rowId); - return sum + (row ? row.price : 0); - }, 0); + const ticket: UserTicket = { + id: Math.random().toString(36).substring(2, 10).toUpperCase(), + movieTitle: sessionInfo?.title || "Фільм", + date: new Date().toLocaleDateString('uk-UA'), + time: sessionInfo?.time || "19:00", + hall: sessionInfo?.hall || "Зал 1", + seats: selectedSeats.join(", "), + totalPrice: totalPrice, + status: "Активний" + }; - const handleBookingConfirm = async () => { - try { - await api.post("/bookings/reserve", { - showtimeId: id, - seats: selectedSeats - }); - - setOccupiedSeats(prev => [...prev, ...selectedSeats]); - setSelectedSeats([]); - setPaymentOpen(false); - alert("Місця успішно заброньовано!"); - } catch (error) { - console.error("Помилка бронювання:", error); - alert("Сталася помилка. Можливо, ці місця вже хтось забронював."); + const allTickets = JSON.parse(localStorage.getItem("user_tickets") || "[]"); + localStorage.setItem("user_tickets", JSON.stringify([ticket, ...allTickets])); + + const newOcc = Array.from(new Set([...occupiedSeats, ...selectedSeats])); + setOccupiedSeats(newOcc); + localStorage.setItem(STORAGE_KEY, JSON.stringify(newOcc)); + + setTimeout(() => setSelectedSeats([]), 3000); + + } catch (err) { + console.error("Помилка обробки успіху:", err); } }; - if (isLoading) { - return ( -
-

Синхронізація з сервером...

-
- ); - } - - if (!sessionInfo) { - return ( -
-

Сеанс не знайдено

- -
- ); - } + if (isLoading) return
Завантаження...
; return ( -
- {} +
- +
- {} -
-

{sessionInfo.title}

-

- {sessionInfo.time} · 2D · {sessionInfo.hall} -

+
+

{sessionInfo?.title}

+

{sessionInfo?.time} • {sessionInfo?.hall}

- {} - - - {} - setPaymentOpen(true)} - /> - - {} - {isPaymentOpen && ( + setSelectedSeats(prev => prev.includes(s) ? prev.filter(i => i !== s) : [...prev, s])} /> + setPaymentOpen(true)} /> + + {isPaymentOpen && sessionInfo && ( setPaymentOpen(false)}