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 (
+
+
+
+ );
+}
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) {