diff --git a/server/uploads/services/6aa8001acb16345c5a340947/image.jpg b/server/uploads/services/6aa8001acb16345c5a340947/image.jpg new file mode 100644 index 0000000..add9632 Binary files /dev/null and b/server/uploads/services/6aa8001acb16345c5a340947/image.jpg differ diff --git a/server/uploads/services/6aa8001acb16345c5a340947/image.webp b/server/uploads/services/6aa8001acb16345c5a340947/image.webp new file mode 100644 index 0000000..0aff6b5 Binary files /dev/null and b/server/uploads/services/6aa8001acb16345c5a340947/image.webp differ diff --git a/txclient/src/app/components/ProductItem/ProductItem.tsx b/txclient/src/app/components/ProductItem/ProductItem.tsx index 0aebed4..6aac845 100644 --- a/txclient/src/app/components/ProductItem/ProductItem.tsx +++ b/txclient/src/app/components/ProductItem/ProductItem.tsx @@ -6,6 +6,7 @@ import { PublicServiceView } from "@models/Service.model"; import style from "./style.module.css"; import { getServiceImage } from "@core/helpers/getServiceImage"; import ServiceRatingSummary from "@components/ServiceRatingSummary/ServiceRatingSummary"; +import EditIcon from "@mui/icons-material/Edit"; export type FindResultsViewMode = "grid" | "card"; @@ -14,6 +15,8 @@ export interface ProductItemProps { width?: number | string; viewMode?: FindResultsViewMode; dragging?: boolean; + isAdmin?: boolean; + onImageEdit?: () => void; } export default function ProductItem(props: ProductItemProps) { @@ -21,6 +24,8 @@ export default function ProductItem(props: ProductItemProps) { const { product } = props; const { viewMode = "card" } = props; const { dragging = false } = props; + const { isAdmin = false } = props; + const { onImageEdit } = props; const { goTo } = useNavigation(); return ( @@ -38,7 +43,7 @@ export default function ProductItem(props: ProductItemProps) { textAlign: "center", padding: "10px", border: "solid 1px rgba(0, 0, 0, 0.1)", - cursor: "pointer", + cursor: isAdmin ? "default" : "pointer", position: "relative", }} onClick={() => { @@ -58,6 +63,11 @@ export default function ProductItem(props: ProductItemProps) { display: "block", }} /> + {isAdmin && ( + { e.stopPropagation(); onImageEdit?.(); }}> + + + )} => { + return ApiRequest.postFile("services/upload-image", data); +}; + +export const checkOrgAdmin = async (companyId: string, sessionUser: string): Promise => { + return ApiRequest.post("companies/check-admin", { companyId, sessionUser }); +}; diff --git a/txclient/src/app/landing/service/[sid]/ServiceDetailCard.tsx b/txclient/src/app/landing/service/[sid]/ServiceDetailCard.tsx new file mode 100644 index 0000000..706e58b --- /dev/null +++ b/txclient/src/app/landing/service/[sid]/ServiceDetailCard.tsx @@ -0,0 +1,59 @@ +"use client"; +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import ProductItem from "@core/app/components/ProductItem/ProductItem"; +import { PublicServiceView } from "@models/Service.model"; +import { PublicOrganizationView } from "@models/Company.model"; +import ServiceImageEditPopup from "./ServiceImageEditPopup"; +import { useSessionStore } from "@core/Store/Sesion.Store"; +import { checkOrgAdmin } from "./Service.Service"; + +interface ServiceDetailCardProps { + service: PublicServiceView; + organization: PublicOrganizationView; +} + +export default function ServiceDetailCard({ service, organization }: ServiceDetailCardProps) { + const router = useRouter(); + const SessionInfo = useSessionStore(); + const [isAdmin, setIsAdmin] = useState(false); + const [showEditPopup, setShowEditPopup] = useState(false); + + useEffect(() => { + if (!SessionInfo.userId || !organization.id || !service.id) return; + + checkOrgAdmin(organization.id, SessionInfo.userId) + .then((result) => { + setIsAdmin(result); + }) + .catch(() => { + setIsAdmin(false); + }); + }, [SessionInfo.userId, organization.id, service.id]); + + const handleImageSaved = () => { + setShowEditPopup(false); + router.refresh(); + }; + + return ( + <> + setShowEditPopup(true)} + /> + {isAdmin && ( + setShowEditPopup(false)} + serviceId={service.id} + companyId={organization.id} + currentImage={service.image} + onSaved={handleImageSaved} + /> + )} + + ); +} diff --git a/txclient/src/app/landing/service/[sid]/ServiceImageEditPopup.tsx b/txclient/src/app/landing/service/[sid]/ServiceImageEditPopup.tsx new file mode 100644 index 0000000..bf191ea --- /dev/null +++ b/txclient/src/app/landing/service/[sid]/ServiceImageEditPopup.tsx @@ -0,0 +1,105 @@ +"use client"; +import { useState } from "react"; +import { IconButton, Dialog, DialogActions, DialogContent, DialogTitle, Button } from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; +import turnosXpressTheme from "@core/app/theme/turnosXpress"; +import { ThemeProvider } from "@emotion/react"; +import ImagePicker from "@components/ImagePicker/ImagePicker"; +import { useAlert } from "@core/Store/Alert.Store"; +import { useSessionStore } from "@core/Store/Sesion.Store"; +import { uploadServiceImage } from "./Service.Service"; + +interface ServiceImageEditPopupProps { + open: boolean; + onClose: () => void; + serviceId: string; + companyId: string; + currentImage: string; + onSaved: () => void; +} + +export default function ServiceImageEditPopup(props: ServiceImageEditPopupProps) { + const { open, onClose, serviceId, companyId, currentImage, onSaved } = props; + const alert = useAlert(); + const SessionInfo = useSessionStore(); + const [loading, setLoading] = useState(false); + + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + setLoading(true); + + try { + const formData = new FormData(); + formData.append("companyId", companyId); + formData.append("serviceId", serviceId); + formData.append("sessionUser", SessionInfo.userId); + formData.append("file", file); + + uploadServiceImage(formData) + .then(() => { + alert.showSuccess("Imagen actualizada"); + onSaved(); + }) + .catch((error) => { + const formatted = error instanceof Error ? error.message : "Error al guardar la imagen"; + alert.showError(formatted || "Error al guardar la imagen"); + }) + .finally(() => { + setLoading(false); + }); + } catch (error) { + const formatted = error instanceof Error ? error.message : "Error al guardar la imagen"; + alert.showError(formatted || "Error al guardar la imagen"); + setLoading(false); + } + }; + + return ( + + + + Editar imagen del servicio + + + + + + + + + + + + + + + + ); +} diff --git a/txclient/src/app/landing/service/[sid]/page.tsx b/txclient/src/app/landing/service/[sid]/page.tsx index b42f198..cd96b32 100644 --- a/txclient/src/app/landing/service/[sid]/page.tsx +++ b/txclient/src/app/landing/service/[sid]/page.tsx @@ -2,7 +2,6 @@ import AnimatedContainer from "@components/AnimatedConainer/AnimatedContainer"; import style from "./profile.module.css"; import ApiServerService from "@services/Api.Server.Service"; import { PublicOrganizationServiceView } from "@core/Models/Service.model"; -import ProductItem from "@core/app/components/ProductItem/ProductItem"; import Appointment from "./components/Appointment/Appointment"; import OrganizationHeader from "@components/Home/OrganizationHeader/OrganizationHeader"; import ProductGallery from "@core/app/components/Home/ProductsGallery/ProductGallery"; @@ -12,6 +11,7 @@ import ServicePrivate from "@core/app/components/ServicePrivate/ServicePrivate"; import EvangelizeBanner from "@components/EvangelizeBanner/EvangelizeBanner"; import ReviewsCarousel from "@components/ReviewsCarousel/ReviewsCarousel"; import { RatingTargetType } from "@core/Models/Ratings.model"; +import ServiceDetailCard from "./ServiceDetailCard"; const GetServiceData = async (serviceId: string): Promise => { try { @@ -60,7 +60,7 @@ export default async function OrganizationPublicProfile({ params }: Props) {
- +