From 43b31eed1db2218a220a072783ae2d95583869ae Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sat, 7 Dec 2024 21:12:08 -0400 Subject: [PATCH 1/2] update info --- .../ExperienceDetail/InfoExperience.tsx | 2 +- .../ExperienceDetail/ReviewCard.tsx | 11 +- .../ExperienceDetail/ReviewSection.tsx | 1 + src/components/Filters/Filters.tsx | 101 +++++++++++------- 4 files changed, 72 insertions(+), 43 deletions(-) diff --git a/src/components/ExperienceDetail/InfoExperience.tsx b/src/components/ExperienceDetail/InfoExperience.tsx index a0e67f7..e62dabe 100644 --- a/src/components/ExperienceDetail/InfoExperience.tsx +++ b/src/components/ExperienceDetail/InfoExperience.tsx @@ -44,7 +44,7 @@ function InfoExperience({description, capacity, location, createdAt, tags } : {d export default InfoExperience; -const InfoCard = ({ icon, first, second } : { icon: string, first: string, second: string | number }) => { +export const InfoCard = ({ icon, first, second } : { icon: string, first: string, second: string | number }) => { return (
iconEmail diff --git a/src/components/ExperienceDetail/ReviewCard.tsx b/src/components/ExperienceDetail/ReviewCard.tsx index c3a0e21..726c2e0 100644 --- a/src/components/ExperienceDetail/ReviewCard.tsx +++ b/src/components/ExperienceDetail/ReviewCard.tsx @@ -5,12 +5,15 @@ interface IReview { userId: string rating: number comment: string - createdAt: string + createdAt: string, + userAvatar: string, + userName: string, } function ReviewCard({review} : {review: IReview}) { - const { rating, comment, createdAt } = review; + const { rating, comment, createdAt, userName, userAvatar } = review; + console.log(review) const getRating = (rating: number) => { switch (rating) { @@ -31,9 +34,9 @@ function ReviewCard({review} : {review: IReview}) { return (
-
+
-

Mariana Garcia Rodiguez

+

{userName ?? "Usuario"}

{getRating(rating)?.stars} {formatToShortDate(createdAt)}

diff --git a/src/components/ExperienceDetail/ReviewSection.tsx b/src/components/ExperienceDetail/ReviewSection.tsx index 0f4bc20..126159e 100644 --- a/src/components/ExperienceDetail/ReviewSection.tsx +++ b/src/components/ExperienceDetail/ReviewSection.tsx @@ -13,6 +13,7 @@ function ReviewSection({ id }: { id: string }) { const response = await fetch(`${BACK_URL}/reviews/experience/${id}`); const data = await response.json(); setReviews(data); + console.log(reviews) } catch (error) { console.error("Error fetching reviews:", error); } diff --git a/src/components/Filters/Filters.tsx b/src/components/Filters/Filters.tsx index dd01d81..749389f 100644 --- a/src/components/Filters/Filters.tsx +++ b/src/components/Filters/Filters.tsx @@ -6,6 +6,12 @@ import CategorySelect from "./CategorySelect"; import LocationInput from "./LocationInput"; import { IExperience } from "../../types/experience"; import { MagnifyingGlassIcon } from "@heroicons/react/24/solid"; +import { formatCategory, formatToShortDate } from "../../utils/getDateFormat"; +import { InfoCard } from "../ExperienceDetail/InfoExperience"; + +import imageSafari from "../../assets/img/Safari.jpg"; +import iconLocation from "../../assets/icons/icon-location.svg"; +import iconPrice from "../../assets/icons/icon-price.svg"; interface Filters { title: string; @@ -44,7 +50,8 @@ const Filters: React.FC = () => { }); const handleInputChange = (key: string, value: string) => { - const regex = /^[A-Za-z\s]*$/; // Only letters and spaces + const regex = /^[A-Za-záéíóúÁÉÍÓÚ\s]*$/; + // Only letters and spaces if (value.length > 50 || !regex.test(value)) { setInputErrors((prev) => ({ ...prev, @@ -65,27 +72,32 @@ const Filters: React.FC = () => { })); }; - const fetchExperiences = useCallback(async (queryParams: QueryParams) => { - setLoading(true); - setError(null); + const fetchExperiences = useCallback( + async (queryParams: QueryParams) => { + setLoading(true); + setError(null); - try { - const response = await axios.get( - `${BACK_URL}/experiences/get-all?${queryString.stringify(queryParams)}` - ); - console.log("API Response:", response.data); // Debugging API response - if (Array.isArray(response.data)) { - setExperiences(response.data); - } else { - setExperiences([]); // Fallback if response is not an array + try { + const response = await axios.get( + `${BACK_URL}/experiences/get-all?${queryString.stringify( + queryParams + )}` + ); + console.log("API Response:", response.data); // Debugging API response + if (Array.isArray(response.data)) { + setExperiences(response.data); + } else { + setExperiences([]); // Fallback if response is not an array + } + } catch (err) { + setError("No existen experiencias."); + setExperiences([]); // Clear experiences on error + } finally { + setLoading(false); } - } catch (err) { - setError("No existen experiencias."); - setExperiences([]); // Clear experiences on error - } finally { - setLoading(false); - } - }, [BACK_URL]); + }, + [BACK_URL] + ); const applyFilters = () => { const { title, country, city, maxPrice, category } = filters; @@ -164,7 +176,7 @@ const Filters: React.FC = () => {

-
+
{ -
- {inputErrors.title &&

{inputErrors.title}

} + {inputErrors.title && ( +

{inputErrors.title}

+ )} {inputErrors.city &&

{inputErrors.city}

} @@ -242,7 +255,8 @@ const Filters: React.FC = () => {

{error}

) : (
-

Experiencias

+

Experiencias

+
{Array.isArray(experiences) && experiences.length > 0 ? ( experiences.map((experience) => ( @@ -252,23 +266,34 @@ const Filters: React.FC = () => { > {experience.title}
-

{experience.title}

-

- {experience.location[0] + ", " + experience.location[1]} +

+

Categoria: {formatCategory(experience.tags[0])}

+
+

+ {experience.title} +

+

+ {experience.description}

-
- 💰 ${experience.price} - - 📍 {experience.location[0]}, {experience.location[1]} - - 📍 {experience.tags[0]} +
+ +
@@ -286,4 +311,4 @@ const Filters: React.FC = () => { ); }; -export default Filters; \ No newline at end of file +export default Filters; From 7da89062783cb97fbf6ddf912b34a2e777aaf772 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sat, 7 Dec 2024 21:13:49 -0400 Subject: [PATCH 2/2] feat: add dashboard tourist/provider and reviews --- .../BookingInfo/BookingInfo.tsx | 283 ++++++++++++++++++ .../ExperiencePanel/ExperiencePanel.tsx | 17 ++ .../ExperiencePanel/HeaderPanel.tsx | 16 + .../ManageCustomers/CustomerCard.tsx | 140 +++++++++ .../ManageCustomers/ManageCustomers.tsx | 182 +++++++++++ .../ProviderUser/ExperienceCardProvider.tsx | 39 +++ .../ProviderUser/PanelProvider.tsx | 50 ++++ .../TouristUser/ExperienceCard.tsx | 65 ++++ .../TouristUser/PanelTourist.tsx | 56 ++++ src/components/Reviews/Reviews.tsx | 223 +++++++------- src/components/UserProfile/DetailsCard.tsx | 8 +- src/consts/userProfileFields.ts | 2 +- src/routes/AppRoutes.tsx | 12 + src/services/booking.services.ts | 14 + src/services/review.services.ts | 24 ++ src/types/booking.ts | 13 + src/utils/getDateFormat.ts | 16 +- 17 files changed, 1045 insertions(+), 115 deletions(-) create mode 100644 src/components/ExperiencePanel/BookingInfo/BookingInfo.tsx create mode 100644 src/components/ExperiencePanel/ExperiencePanel.tsx create mode 100644 src/components/ExperiencePanel/HeaderPanel.tsx create mode 100644 src/components/ExperiencePanel/ManageCustomers/CustomerCard.tsx create mode 100644 src/components/ExperiencePanel/ManageCustomers/ManageCustomers.tsx create mode 100644 src/components/ExperiencePanel/ProviderUser/ExperienceCardProvider.tsx create mode 100644 src/components/ExperiencePanel/ProviderUser/PanelProvider.tsx create mode 100644 src/components/ExperiencePanel/TouristUser/ExperienceCard.tsx create mode 100644 src/components/ExperiencePanel/TouristUser/PanelTourist.tsx create mode 100644 src/services/review.services.ts create mode 100644 src/types/booking.ts diff --git a/src/components/ExperiencePanel/BookingInfo/BookingInfo.tsx b/src/components/ExperiencePanel/BookingInfo/BookingInfo.tsx new file mode 100644 index 0000000..bae950e --- /dev/null +++ b/src/components/ExperiencePanel/BookingInfo/BookingInfo.tsx @@ -0,0 +1,283 @@ +import { useState, useEffect } from "react"; +import { useParams } from "react-router-dom"; +import HeaderPanel from "../HeaderPanel"; +import imageSafari from "../../../assets/img/Safari.jpg"; + +//@ts-ignore +import Cookies from "js-cookie"; +import { getStatus } from "../../../utils/getDateFormat"; +import { useNavigate } from "react-router-dom"; +import { useContext } from "react"; +import { AuthContext } from "../../../contexts/auth.context"; +import reviewServices from "../../../services/review.services"; + +function BookingInfo() { + const { id } = useParams(); + const navigate = useNavigate(); + const { user } = useContext(AuthContext); + const userId = user?._id; + + const [booking, setBooking] = useState(null); + const [experience, setExperience] = useState(null); + const [loading, setLoading] = useState(true); + const [userHasReview, setUserHasReview] = useState(false); + const [reviewId, setReviewId] = useState(null); + const [showConfirmationModal, setShowConfirmationModal] = useState(false); // Estado para controlar el modal de confirmación + const [successMessage, setSuccessMessage] = useState(null); // Mensaje de éxito + + function formatDateAndTime(dateString: string) { + const date = new Date(dateString); + const day = String(date.getDate()).padStart(2, "0"); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const year = date.getFullYear(); + const formattedDate = `${day}/${month}/${year}`; + const hours = String(date.getHours()).padStart(2, "0"); + const minutes = String(date.getMinutes()).padStart(2, "0"); + const formattedTime = `${hours}:${minutes}`; + return { date: formattedDate, time: formattedTime }; + } + + const { date, time } = formatDateAndTime(booking?.bookingDate || ""); + const { text } = getStatus(booking?.status || ""); + + useEffect(() => { + const fetchBookingData = async () => { + try { + setLoading(true); + + const storedBooking = Cookies.get("booking"); + if (storedBooking) { + const parsedBooking = JSON.parse(storedBooking); + setBooking(parsedBooking); + + const experienceId = parsedBooking.experienceId; + + const experienceResponse = await fetch( + `${import.meta.env.VITE_API_URL}/experiences/${experienceId}` + ); + const experienceData = await experienceResponse.json(); + setExperience(experienceData); + + const reviewsResponse = await fetch( + `${import.meta.env.VITE_API_URL}/reviews/experience/${experienceId}` + ); + const reviews = await reviewsResponse.json(); + + // Busca si el usuario ya tiene una reseña + const userReview = reviews.find( + (review: any) => review.userId === userId + ); + if (userReview) { + setUserHasReview(true); + setReviewId(userReview.id); // Guarda el ID de la reseña + } else { + setUserHasReview(false); + } + } else { + console.error("No se encontró la reserva en las cookies."); + } + } catch (error) { + console.error("Error fetching booking or experience data:", error); + } finally { + setLoading(false); + } + }; + + if (id) fetchBookingData(); + }, [id, userId]); + + const { provider } = booking || {}; + + const handleDeleteReview = async () => { + if (!reviewId) return; + + try { + await reviewServices.delete(reviewId); // Usa el servicio actualizado + + setSuccessMessage("Reseña eliminada con éxito."); // Muestra el mensaje de éxito + setUserHasReview(false); + setReviewId(null); + setShowConfirmationModal(false); // Cierra el modal + + // Oculta el mensaje después de 3 segundos + setTimeout(() => { + setSuccessMessage(null); + }, 3000); + } catch (error) { + console.error("Error eliminando la reseña:", error); + alert("Hubo un problema al eliminar la reseña."); + } + }; + + const handleShowConfirmation = () => { + setShowConfirmationModal(true); + }; + + const handleCancelDelete = () => { + setShowConfirmationModal(false); + }; + + if (loading) { + return ( +
+
+
+ ); + } + + if (!booking || !experience) { + return

No se encontraron datos.

; + } + + return ( +
+ +
+ Imagen de Safari +

{experience.title}

+
+ + + + + + + + } + /> + navigate(`/experience/${experience.id}`)} + className="px-4 py-0.5" + > + Ver información + + } + /> + {booking.status === "CONFIRMED" && ( + + Eliminar Reseña + + ) : ( + + ) + } + /> + )} +
+
+

Datos del proveedor

+
+ + + +
+
+
+ + {/* Modal de confirmación */} + {showConfirmationModal && ( +
+
+

+ ¿Estás seguro? +

+

+ ¿Quieres eliminar esta reseña? +

+
+ + +
+
+
+ )} + + {/* Mensaje de éxito */} + {successMessage && ( +
+ {successMessage} +
+ )} +
+ ); +} + +export default BookingInfo; + +const Info = ({ first, second }: { first: string; second: string | any }) => { + return ( +
+

{first}

+

{second}

+
+ ); +}; + +const ButtonNavigate = ({ + latitude, + longitude, +}: { + latitude: string; + longitude: string; +}) => { + const openLocation = () => { + const url = `https://www.google.com/maps?q=${latitude},${longitude}`; + window.open(url, "_blank"); + }; + + return ( + + ); +}; diff --git a/src/components/ExperiencePanel/ExperiencePanel.tsx b/src/components/ExperiencePanel/ExperiencePanel.tsx new file mode 100644 index 0000000..6b4d150 --- /dev/null +++ b/src/components/ExperiencePanel/ExperiencePanel.tsx @@ -0,0 +1,17 @@ + +import { useContext } from "react" +import { AuthContext } from "../../contexts/auth.context" +import PanelTourist from "./TouristUser/PanelTourist"; +// import BookingInfo from "./BookingInfo/BookingInfo"; +import PanelProvider from "./ProviderUser/PanelProvider"; + +function ExperiencePanel() { + + const { user } = useContext(AuthContext); + console.log(user) + + + return user?.role !== 'TOURIST' ? : +} + +export default ExperiencePanel; \ No newline at end of file diff --git a/src/components/ExperiencePanel/HeaderPanel.tsx b/src/components/ExperiencePanel/HeaderPanel.tsx new file mode 100644 index 0000000..79b183d --- /dev/null +++ b/src/components/ExperiencePanel/HeaderPanel.tsx @@ -0,0 +1,16 @@ +import iconLeft from '../../assets/icons/icon-arrow-left.svg' +import { useNavigate } from 'react-router-dom'; + +function HeaderPanel({title} : {title: string}) { + const navigate = useNavigate(); + + return ( +
+ navigate(-1)} src={iconLeft} alt="icon-left" className="w-8 pl-5 cursor-pointer" /> +

{title}

+

+
+ ); +} + +export default HeaderPanel; diff --git a/src/components/ExperiencePanel/ManageCustomers/CustomerCard.tsx b/src/components/ExperiencePanel/ManageCustomers/CustomerCard.tsx new file mode 100644 index 0000000..2fe5670 --- /dev/null +++ b/src/components/ExperiencePanel/ManageCustomers/CustomerCard.tsx @@ -0,0 +1,140 @@ +import { useState, useContext } from "react"; +import { AuthContext } from "../../../contexts/auth.context"; +import bookingServices from "../../../services/booking.services"; + +type ConfirmationModalProps = { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + message: string; +}; + +function ConfirmationModal({ isOpen, onClose, onConfirm, message } : ConfirmationModalProps) { + if (!isOpen) return null; + + return ( +
+
+

{message}

+
+ + +
+
+
+ ); +} + +function CustomerCard({ activeTab, dataBooking, onBookingUpdate }: any) { + const { user } = useContext(AuthContext); + const userId = user?._id; + + const { tourist, participants, id: bookingId } = dataBooking; + const { avatar, name, email, phone } = tourist; + console.log(tourist) + + const [isModalOpen, setModalOpen] = useState(false); + const [modalMessage, setModalMessage] = useState(""); + const [modalAction, setModalAction] = useState<(() => void) | null>(null); + const [notification, setNotification] = useState<{ message: string; color: string } | null>(null); + + const showNotification = (message: string, color: string) => { + setNotification({ message, color }); + setTimeout(() => setNotification(null), 3000); + }; + + const handleConfirmBooking = async () => { + try { + await bookingServices.updateBooking(bookingId, { userId, status: "CONFIRMED" }); + showNotification("Reserva confirmada con éxito.", "bg-green-600"); + setTimeout(() => onBookingUpdate(), 1500); + } catch (error) { + console.error(error); + showNotification("Error al confirmar la reserva.", "bg-red-600"); + } finally { + setModalOpen(false); + } + }; + + const handleCancelBooking = async () => { + try { + await bookingServices.updateBooking(bookingId, { userId, status: "CANCELLED" }); + showNotification("Reserva cancelada con éxito.", "bg-primary"); + onBookingUpdate(); // Llamamos a la función para actualizar los bookings en ManageCustomers + } catch (error) { + console.error(error); + showNotification("Error al cancelar la reserva.", "bg-red-600"); + } finally { + setModalOpen(false); + } + }; + + const openConfirmModal = () => { + setModalMessage("¿Estás seguro de que deseas confirmar esta reserva?"); + setModalAction(() => handleConfirmBooking); + setModalOpen(true); + }; + + const openCancelModal = () => { + setModalMessage("¿Estás seguro de que deseas cancelar esta reserva?"); + setModalAction(() => handleCancelBooking); + setModalOpen(true); + }; + + return ( +
+
+ +
+

{name}

+

{email}

+

{phone}

+

+ {participants} {participants === 1 ? "Persona" : "Personas"} +

+
+
+ {activeTab === "PENDING" && ( +
+ + +
+ )} + setModalOpen(false)} + onConfirm={modalAction || (() => {})} + message={modalMessage} + /> + {notification && ( +
+ {notification.message} +
+ )} +
+ ); +} + +export default CustomerCard; diff --git a/src/components/ExperiencePanel/ManageCustomers/ManageCustomers.tsx b/src/components/ExperiencePanel/ManageCustomers/ManageCustomers.tsx new file mode 100644 index 0000000..85ff5db --- /dev/null +++ b/src/components/ExperiencePanel/ManageCustomers/ManageCustomers.tsx @@ -0,0 +1,182 @@ +import { useEffect, useState } from "react"; +import HeaderPanel from "../HeaderPanel"; +import CustomerCard from "./CustomerCard"; +import bookingService from "../../../services/booking.services"; +import { useParams } from "react-router-dom"; + +function ManageCustomers() { + const [bookings, setBookings] = useState([]); + const [loading, setLoading] = useState(true); + const [uniqueDates, setUniqueDates] = useState([]); + const [uniqueTimes, setUniqueTimes] = useState([]); + const [activeTab, setActiveTab] = useState("PENDING"); + const [selectedDate, setSelectedDate] = useState("Todos"); + const [selectedTime, setSelectedTime] = useState("Todos"); + const [filteredBookings, setFilteredBookings] = useState([]); + const { id } = useParams(); + + // Mover fetchBookings fuera del useEffect + const fetchBookings = async () => { + try { + setLoading(true); + const data = await bookingService.getBookingsFromExperience(id); + setBookings(data); + + const dates: string[] = Array.from( + new Set(data.map((booking: any) => booking.bookingDate.split("T")[0])) + ); + + setUniqueDates(dates); + + const times: string[] = Array.from( + new Set( + data.map((booking: any) => + new Date(booking.bookingDate).toLocaleTimeString("en-GB", { + hour: "2-digit", + minute: "2-digit", + }) + ) + ) + ); + + setUniqueTimes(times); + + setFilteredBookings(data); // Inicialmente mostramos todo + } catch (error) { + console.error("Error fetching bookings:", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (id) { + fetchBookings(); // Llamada al fetchBookings + } + }, [id]); + + useEffect(() => { + // Filtrar los bookings según el estado activo, fecha y hora seleccionados + const filterResults = () => { + let filtered = bookings.filter((booking) => booking.status === activeTab); + + if (selectedDate !== "Todos") { + filtered = filtered.filter((booking) => + booking.bookingDate.startsWith(selectedDate) + ); + } + + if (selectedTime !== "Todos") { + filtered = filtered.filter((booking) => { + const bookingTime = new Date(booking.bookingDate).toLocaleTimeString( + "en-GB", + { + hour: "2-digit", + minute: "2-digit", + } + ); + return bookingTime === selectedTime; + }); + } + + setFilteredBookings(filtered); + }; + + filterResults(); + }, [activeTab, selectedDate, selectedTime, bookings]); + + const handleBookingUpdate = () => { + if (id) { + fetchBookings(); // Vuelve a llamar a fetchBookings al actualizar + } + }; + + if (loading) { + return ( +
+
+
+ ); + } + + const tabTranslations: { [key: string]: string } = { + PENDING: "Pendientes", + CONFIRMED: "Confirmados", + CANCELLED: "Cancelados", + }; + + return ( +
+ + + {/* Filters */} +
+ + +
+ + {/* Tabs */} +
+ {["PENDING", "CONFIRMED", "CANCELLED"].map((tab) => ( + + ))} +
+ + {/* Bookings List */} +
+ {filteredBookings.length > 0 ? ( + filteredBookings.map((booking) => ( + + )) + ) : ( +

+ No hay reservas en {tabTranslations[activeTab]}. +

+ )} +
+
+ ); +} + +export default ManageCustomers; diff --git a/src/components/ExperiencePanel/ProviderUser/ExperienceCardProvider.tsx b/src/components/ExperiencePanel/ProviderUser/ExperienceCardProvider.tsx new file mode 100644 index 0000000..d694e94 --- /dev/null +++ b/src/components/ExperiencePanel/ProviderUser/ExperienceCardProvider.tsx @@ -0,0 +1,39 @@ +import imageSafari from "../../../assets/img/Safari.jpg"; +import { useNavigate } from "react-router-dom"; + +function ExperienceCardProvider({ title, experienceId , price, status } : {title: string, experienceId: string, price: number, status:string } ) { + const navigate = useNavigate(); + + return ( +
+
+ +
+
+
+

+ {title} +

+ {status ?

+ Estado: Activo +

:

+ Estado: Inactivo +

} +

Precio: ${price}

+
+ +
+ + + +
+
+
+ ); +} + +export default ExperienceCardProvider; diff --git a/src/components/ExperiencePanel/ProviderUser/PanelProvider.tsx b/src/components/ExperiencePanel/ProviderUser/PanelProvider.tsx new file mode 100644 index 0000000..10c040d --- /dev/null +++ b/src/components/ExperiencePanel/ProviderUser/PanelProvider.tsx @@ -0,0 +1,50 @@ +import { useEffect, useState, useContext } from "react"; +import HeaderPanel from "../HeaderPanel"; +import ExperienceCardProvider from "./ExperienceCardProvider"; +import { AuthContext } from "../../../contexts/auth.context"; + +function PanelProvider() { + const { user } = useContext(AuthContext); + const userId = user?._id; + const [experiences, setExperiences] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchExperiences = async () => { + try { + const response = await fetch(`${import.meta.env.VITE_API_URL}/experiences/host/${userId}`); + const data = await response.json(); + setExperiences(data); + } catch (error) { + console.error("Error fetching experiences:", error); + } finally { + setLoading(false); + } + }; + + if (userId) { + fetchExperiences(); + } + }, []); + + + + return ( +
+ + {loading ? ( +
+
+
+ ) : ( +
+ {experiences.map((experience) => ( + + ))} +
+ )} +
+ ); +} + +export default PanelProvider; diff --git a/src/components/ExperiencePanel/TouristUser/ExperienceCard.tsx b/src/components/ExperiencePanel/TouristUser/ExperienceCard.tsx new file mode 100644 index 0000000..17b66c6 --- /dev/null +++ b/src/components/ExperiencePanel/TouristUser/ExperienceCard.tsx @@ -0,0 +1,65 @@ +import imageSafari from "../../../assets/img/Safari.jpg"; +import { useNavigate } from "react-router-dom"; +import { Booking } from "../../../types/booking"; +import { getStatus } from "../../../utils/getDateFormat"; +import { formatToShortDate } from "../../../utils/getDateFormat"; + +// @ts-ignore +import Cookies from "js-cookie"; + +interface ExperienceCardProps { + idBooking: string; + bookingInfo: Booking; + // userId: string; +} + +function ExperienceCard({ idBooking, bookingInfo }: ExperienceCardProps) { + const navigate = useNavigate(); + + // experienceId posiblemente tenga que extraerlo + const { status, bookingDate, experienceTitle } = bookingInfo; + console.log(bookingInfo) + + const dateFormatted = formatToShortDate(bookingDate); + + const { text, color } = getStatus(status); + + + // Redirecciona a la informacion de la reserva para el usuario + const handleBooking = () => { + // Guardar el booking en la cookie + Cookies.set("booking", JSON.stringify(bookingInfo), { expires: 7 }); + navigate(`/booking-info/${idBooking}`); + }; + + return ( +
+
+ Imagen de Safari +
+
+

+ {experienceTitle} {/* Nombre de la experiencia */} +

+

+ Estado: {text} +

+

Fecha: {dateFormatted}

+
+ +
+
+
+ ); +} + +export default ExperienceCard; diff --git a/src/components/ExperiencePanel/TouristUser/PanelTourist.tsx b/src/components/ExperiencePanel/TouristUser/PanelTourist.tsx new file mode 100644 index 0000000..400d3cc --- /dev/null +++ b/src/components/ExperiencePanel/TouristUser/PanelTourist.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from "react"; +import ExperienceCard from "./ExperienceCard"; +import HeaderPanel from "../HeaderPanel"; +// Importa el servicio de reservas (aunque ahora no se está usando en este ejemplo) +import bookingServices from "../../../services/booking.services"; +import { AuthContext } from "../../../contexts/auth.context"; +import { useContext } from "react"; +import { Booking } from "../../../types/booking"; + +function PanelTourist() { + const { user } = useContext(AuthContext); + const userId = user?._id; + + const [bookings, setBookings] = useState([]); + const [loading, setLoading] = useState(true); // Estado para controlar la carga + + + useEffect(() => { + // TRAE LOS BOOKING DEL USUARIO + const fetchBookings = async () => { + try { + const response = await bookingServices.getBookingsFromUser(userId); + setBookings(response); + console.log(bookings) + } catch (error) { + console.error("Error fetching bookings:", error); + } finally { + setLoading(false); + } + }; + + fetchBookings(); + }, [userId]); + + return ( +
+ + + {/* Muestra el loader mientras está en carga */} + {loading ? ( +
+
+
+ ) : ( + // RENDERIZA LOS BOOKINGS +
+ {bookings?.map((booking) => ( + + ))} +
+ )} +
+ ); +} + +export default PanelTourist; diff --git a/src/components/Reviews/Reviews.tsx b/src/components/Reviews/Reviews.tsx index f3adf53..e0b557a 100644 --- a/src/components/Reviews/Reviews.tsx +++ b/src/components/Reviews/Reviews.tsx @@ -1,127 +1,131 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useContext } from "react"; +//@ts-ignore +import Cookies from "js-cookie"; +import { useParams } from "react-router-dom"; +import imageSafari from "../../assets/img/Safari.jpg"; +import { AuthContext } from "../../contexts/auth.context"; +import HeaderPanel from "../ExperiencePanel/HeaderPanel"; +import reviewServices from "../../services/review.services"; +import { useNavigate } from "react-router-dom"; const Reviews: React.FC = () => { - const [rating, setRating] = useState(''); - const [review, setReview] = useState(''); + const { id } = useParams(); + const { user } = useContext(AuthContext); + const userId = user?._id; + const navigate = useNavigate(); + + const [rating, setRating] = useState(); + const [review, setReview] = useState(""); const [loading, setLoading] = useState(false); - const [success, setSuccess] = useState(null); - const [activity, setActivity] = useState<{ title: string; image: string } | null>(null); - const [fetchError, setFetchError] = useState(null); - - - useEffect(() => { - const fetchActivity = async () => { - try { - setFetchError(null); - const response = await fetch('https://jsonplaceholder.typicode.com/photos/1'); - if (!response.ok) { - throw new Error('Failed to fetch activity details'); - } - const data = await response.json(); - setActivity({ - title: data.title || 'Default Activity Title', - image: data.url || 'https://via.placeholder.com/300x150', - }); - } catch (error: unknown) { - if (error instanceof Error) { - console.error('Error fetching activity:', error.message); - setFetchError(error.message); - } else { - console.error('Unexpected error:', error); - setFetchError('An unexpected error occurred while fetching activity details'); - } - } - }; + const [errors, setErrors] = useState<{ rating?: string; review?: string }>({ + rating: "", + review: "", + }); + const [message, setMessage] = useState(null); + + const CookieExperience = Cookies.get("experience"); + const experience = JSON.parse(CookieExperience); + + // Validación de campos + const isFormValid = () => { + let formIsValid = true; + const errors: { rating?: string; review?: string } = {}; + + if (!rating || rating < 1 || rating > 5) { + errors.rating = "La calificación debe ser entre 1 y 5."; + formIsValid = false; + } + if (!review || review.length < 10) { + errors.review = "La reseña debe tener al menos 10 caracteres."; + formIsValid = false; + } - fetchActivity(); - }, []); + setErrors(errors); + return formIsValid; + }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - setLoading(true); - setSuccess(null); - try { - const response = await fetch('https://jsonplaceholder.typicode.com/posts', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - rating, - review, - activity: activity?.title, - }), - }); - - if (!response.ok) { - throw new Error('Failed to submit review'); - } + // Validar antes de proceder + if (!isFormValid()) { + console.log("Formulario inválido: Verifica los campos."); + return; + } - const data = await response.json(); - console.log('Review submitted successfully:', data); + // Verificar que el usuario y el ID de la experiencia estén disponibles + if (!userId || !id) { + setMessage("Información de usuario o experiencia faltante."); + return; + } - setSuccess(true); - setRating(''); - setReview(''); - } catch (error: unknown) { - if (error instanceof Error) { - console.error('Error submitting review:', error.message); - setSuccess(false); + setLoading(true); + setMessage(null); // Limpiar el mensaje anterior + + try { + const data = { + experienceId: id, + userId, + rating: Number(rating), + comment: review, + }; + console.log("Datos enviados: ", data); + + const response = await reviewServices.create(data); + console.log(response); + + if (response.status === 200) { + setRating(0); + setReview(""); + setMessage("Reseña enviada con éxito!"); + console.log("Reseña enviada con éxito"); + } else if (response.message === "Review uploaded successfully.") { + setMessage("Reseña enviada con éxito!"); + setTimeout(() => navigate(-1), 2500); } else { - console.error('Unexpected error:', error); - setSuccess(false); + setMessage("Hubo un error al enviar la reseña."); } + } catch (error) { + console.error("Error al enviar la reseña:", error); + setMessage("Error al enviar la reseña. Intenta nuevamente."); } finally { setLoading(false); } }; return ( -
- -
- -

Añadir reseña

+
+
+
- - {fetchError ? ( -

Error: {fetchError}

- ) : !activity ? ( -

- Cargando datos de la actividad... -

- ) : ( - <> - -
- {activity.title} -
-

- {activity.title} -

- - )} + <> +
+ {experience?.title} +
+

+ {experience?.title} +

+
-
+ {errors.rating && ( +

{errors.rating}

+ )}
@@ -145,33 +152,35 @@ const Reviews: React.FC = () => { value={review} onChange={(e) => setReview(e.target.value)} placeholder="Describe tu experiencia." - className="w-full p-3 border rounded-lg text-gray-700 bg-white shadow-sm focus:outline-none focus:ring-2 focus:ring-orange-500 resize-y" + className="w-full p-3 text-gray-700 bg-white border rounded-lg shadow-sm resize-y focus:outline-none focus:ring-2 focus:ring-orange-500" rows={4} disabled={loading} > + {errors.review && ( +

{errors.review}

+ )}
-
- {success === true && ( -

- Reseña enviada con éxito. -

- )} - {success === false && ( -

- Error al enviar la reseña. Intenta de nuevo. -

- )} + {message && ( +

+ {message} +

+ )} +
); }; diff --git a/src/components/UserProfile/DetailsCard.tsx b/src/components/UserProfile/DetailsCard.tsx index b6e314a..3963001 100644 --- a/src/components/UserProfile/DetailsCard.tsx +++ b/src/components/UserProfile/DetailsCard.tsx @@ -9,11 +9,7 @@ const DetailsCard: React.FC = ({ email, location, phone, role }) => const dataMap = { email, location, phone } const handleBookingsClick = () => { - if (role === 'TOURIST') { - navigate('/mis-reservas') - } else if (role === 'PROVIDER') { - navigate('/reservas-de-clientes') - } + navigate('/my-experiences') } return ( @@ -23,7 +19,7 @@ const DetailsCard: React.FC = ({ email, location, phone, role }) => const { field, icon, title } = deet const data = dataMap[field as keyof typeof dataMap] || '' - const adjustedTitle = field === 'bookings' && role === 'PROVIDER' ? 'Reservas de Clientes' : title + const adjustedTitle = field === 'bookings' && role.toLocaleUpperCase() === 'PROVIDER' ? 'Gestionar Reservas' : title return (
  • diff --git a/src/consts/userProfileFields.ts b/src/consts/userProfileFields.ts index efd5638..cab3c17 100644 --- a/src/consts/userProfileFields.ts +++ b/src/consts/userProfileFields.ts @@ -23,7 +23,7 @@ const detailsList = [ { field: 'bookings', icon: card, - title: 'Reservas', + title: 'Mis experiencias', }, ] diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx index 80b8bb1..71aa196 100644 --- a/src/routes/AppRoutes.tsx +++ b/src/routes/AppRoutes.tsx @@ -18,6 +18,10 @@ import ConfirmationView from '../components/ConfirmationView/ConfirmationView' import { ProtectedPublicRoute } from './ProtectedPublicRoutes' import { useLocation } from 'react-router-dom'; import { useEffect } from 'react' +import ExperiencePanel from '../components/ExperiencePanel/ExperiencePanel' +import BookingInfo from '../components/ExperiencePanel/BookingInfo/BookingInfo' +import ManageCustomers from '../components/ExperiencePanel/ManageCustomers/ManageCustomers' +import Reviews from '../components/Reviews/Reviews' const AppRoutes = () => { @@ -45,6 +49,11 @@ const AppRoutes = () => { }> } /> } /> + } /> + } /> + } /> + } /> + {/* Rutas que necesitan contexto de reserva */} @@ -61,6 +70,8 @@ const AppRoutes = () => { + + 404} /> @@ -69,3 +80,4 @@ const AppRoutes = () => { export default AppRoutes + diff --git a/src/services/booking.services.ts b/src/services/booking.services.ts index cef506b..76e3bf2 100644 --- a/src/services/booking.services.ts +++ b/src/services/booking.services.ts @@ -11,6 +11,20 @@ class BookingServices { booking ) } + async getBookingsFromUser(id: unknown) { + const responde = await this.api.get(`/user/${id}`); + return responde.data; + } + + async getBookingsFromExperience(id: unknown) { + const responde = await this.api.get(`/experience/${id}`); + return responde.data; + } + + async updateBooking(bookingId: unknown, data: unknown) { + await this.api.put(`/${bookingId}`, data); + } + } const bookingServices = new BookingServices() diff --git a/src/services/review.services.ts b/src/services/review.services.ts new file mode 100644 index 0000000..794492e --- /dev/null +++ b/src/services/review.services.ts @@ -0,0 +1,24 @@ +type CreateReviewResponse = { + status: number; + message: string; + }; + + +import createApiClient from "./apiClient" + +class ReviewServices { + private api = createApiClient(`${import.meta.env.VITE_API_URL}/reviews`) + + async create(review: unknown): Promise { + const response = await this.api.post("/create", review); + return response.data; + } + + async delete(review: unknown) { + await this.api.delete(`${review}`); + } +} + +const reviewServices = new ReviewServices() + +export default reviewServices; \ No newline at end of file diff --git a/src/types/booking.ts b/src/types/booking.ts new file mode 100644 index 0000000..55f6a57 --- /dev/null +++ b/src/types/booking.ts @@ -0,0 +1,13 @@ +export interface Booking { + bookingDate: string; + experienceTitle?: string; + createdAt: string; + experienceId: string; + id: string; + participants: number; + paymentStatus: string; + status: string; + totalPrice: number; + userId: string; + } + \ No newline at end of file diff --git a/src/utils/getDateFormat.ts b/src/utils/getDateFormat.ts index c21bbf2..a99002d 100644 --- a/src/utils/getDateFormat.ts +++ b/src/utils/getDateFormat.ts @@ -18,4 +18,18 @@ export function formatCategory(category: string | string[]) { if (!categoryString) return ''; return categoryString.charAt(0).toUpperCase() + categoryString.slice(1); - } \ No newline at end of file +} + + +export const getStatus = (state: string) => { + switch (state) { + case "CONFIRMED": + return { text: "Confirmado", color: "text-green-600" }; + case "CANCELLED": + return { text: "Cancelado", color: "text-red-600" }; + case "PENDING": + return { text: "Pendiente", color: "text-gray-500" }; + default: + return { text: "Desconocido", color: "text-gray-400" }; + } + }; \ No newline at end of file