diff --git a/txclient/src/Services/Header.Edit.Service.ts b/txclient/src/Services/Header.Edit.Service.ts new file mode 100644 index 0000000..5be849b --- /dev/null +++ b/txclient/src/Services/Header.Edit.Service.ts @@ -0,0 +1,19 @@ +import ApiRequest from "@services/Api.Service"; +import { UpdateCompanyParams } from "@models/Company.model"; +import { ApiVoidResult } from "@models/Api.VoidResult.type"; + +export const updateOrganizationColors = async (data: UpdateCompanyParams): Promise => { + return ApiRequest.post("companies/update", data); +}; + +export const uploadHeaderImage = async (data: FormData): Promise => { + return ApiRequest.postFile("companies/upload-header", data); +}; + +export const uploadServiceImage = async (data: FormData): Promise => { + 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/components/OrganizationHeaderEdit/HeaderEditPopup.tsx b/txclient/src/app/components/OrganizationHeaderEdit/HeaderEditPopup.tsx new file mode 100644 index 0000000..8a6907a --- /dev/null +++ b/txclient/src/app/components/OrganizationHeaderEdit/HeaderEditPopup.tsx @@ -0,0 +1,187 @@ +"use client"; +import { useState, useEffect } from "react"; +import { Grid2, IconButton, Dialog, DialogActions, DialogContent, DialogTitle } from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; +import turnosXpressTheme from "@core/app/theme/turnosXpress"; +import Button from "@mui/material/Button"; +import { ThemeProvider } from "@emotion/react"; +import { PopoverPicker } from "@components/ColorPicker/ColorPicker"; +import ImagePicker from "@components/ImagePicker/ImagePicker"; +import { useAlert } from "@core/Store/Alert.Store"; +import { useSessionStore } from "@core/Store/Sesion.Store"; +import { useOrganizationHeaderStore } from "@core/Store/OrganizationHeader.Store"; +import { updateOrganizationColors, uploadHeaderImage } from "@services/Header.Edit.Service"; +import { UpdateCompanyParams } from "@models/Company.model"; + +interface HeaderEditPopupProps { + open: boolean; + onClose: () => void; + companyId: string; + headerColor: string; + headerFontColor: string; + headerFontShadowColor: string; + headerImage: string; + onSaved: () => void; + onRefresh?: () => void; +} + +export default function HeaderEditPopup(props: HeaderEditPopupProps) { + const { open, onClose, companyId, headerColor, headerFontColor, headerFontShadowColor, headerImage, onSaved, onRefresh } = props; + const alert = useAlert(); + const SessionInfo = useSessionStore(); + const organizationHeader = useOrganizationHeaderStore(); + + const [headerColorState, setHeaderColorState] = useState(""); + const [headerFontColorState, setHeaderFontColorState] = useState(""); + const [headerFontShadowColorState, setHeaderFontShadowColorState] = useState(""); + const [loading, setLoading] = useState(false); + + // Sync local state when values change externally + useEffect(() => { + if (open) { + setHeaderColorState(headerColor || ""); + setHeaderFontColorState(headerFontColor || ""); + setHeaderFontShadowColorState(headerFontShadowColor || ""); + setLoading(false); + } + }, [open, headerColor, headerFontColor, headerFontShadowColor]); + + const handleSaveColors = async () => { + setLoading(true); + + try { + const updData: UpdateCompanyParams = { + id: companyId, + headerColor: headerColorState, + headerFontColor: headerFontColorState, + headerFontShadowColor: headerFontShadowColorState, + sessionUser: SessionInfo.userId, + }; + + await updateOrganizationColors(updData); + + organizationHeader.setHeaderColor(headerColorState); + organizationHeader.setHeaderFontColor(headerFontColorState); + organizationHeader.setHeaderFontShadowColor(headerFontShadowColorState); + + alert.showSuccess("Colores actualizados"); + onSaved(); + onRefresh?.(); + } catch (error) { + const formatted = error instanceof Error ? error.message : "Error al guardar los colores"; + alert.showError(formatted || "Error al guardar los colores"); + } finally { + setLoading(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("sessionUser", SessionInfo.userId); + formData.append("file", file); + + uploadHeaderImage(formData).then(() => { + alert.showSuccess("Imagen actualizada"); + onSaved(); + onRefresh?.(); + }).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 ( + + + + Personalizar + + + + + + + {/* Colores */} + + + { + setHeaderColorState(color); + }} + /> + + + { + setHeaderFontColorState(color); + }} + /> + + + { + setHeaderFontShadowColorState(color); + }} + /> + + + + {/* Imagen */} + + + + + + + + + + ); +} diff --git a/txclient/src/app/components/OrganizationHeaderEdit/OrganizationHeaderEdit.tsx b/txclient/src/app/components/OrganizationHeaderEdit/OrganizationHeaderEdit.tsx new file mode 100644 index 0000000..7fb172a --- /dev/null +++ b/txclient/src/app/components/OrganizationHeaderEdit/OrganizationHeaderEdit.tsx @@ -0,0 +1,116 @@ +"use client"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import HeaderEditPopup from "./HeaderEditPopup"; +import { useSessionStore } from "@core/Store/Sesion.Store"; +import { checkOrgAdmin } from "@services/Header.Edit.Service"; + +interface OrganizationHeaderEditProps { + companyId: string; + headerColor?: string; + headerFontColor?: string; + headerFontShadowColor?: string; + headerImage?: string; +} + +export default function OrganizationHeaderEdit({ + companyId, + headerColor = "", + headerFontColor = "", + headerFontShadowColor = "", + headerImage = "", +}: OrganizationHeaderEditProps) { + const router = useRouter(); + const SessionInfo = useSessionStore(); + const [isOrgAdmin, setIsOrgAdmin] = useState(false); + const [showEditPopup, setShowEditPopup] = useState(false); + + useEffect(() => { + if (!SessionInfo.userId || !companyId) { + setIsOrgAdmin(false); + const existing = document.getElementById("edit-header-btn"); + if (existing) existing.remove(); + return; + } + + let cancelled = false; + + checkOrgAdmin(companyId, SessionInfo.userId) + .then((isAdmin) => { + if (cancelled) return; + setIsOrgAdmin(isAdmin); + + const wrapper = document.getElementById("header-wrapper"); + if (!wrapper) return; + + if (isAdmin) { + const existingBtn = document.getElementById("edit-header-btn"); + if (existingBtn) existingBtn.remove(); + + const btn = document.createElement("div"); + btn.id = "edit-header-btn"; + btn.innerHTML = ` + + `; + btn.querySelector("button")!.addEventListener("click", () => + setShowEditPopup(true), + ); + wrapper.appendChild(btn); + } else { + const existingBtn = document.getElementById("edit-header-btn"); + if (existingBtn) existingBtn.remove(); + } + }) + .catch(() => { + if (cancelled) return; + setIsOrgAdmin(false); + + const existingBtn = document.getElementById("edit-header-btn"); + if (existingBtn) existingBtn.remove(); + }); + + return () => { + cancelled = true; + const existing = document.getElementById("edit-header-btn"); + if (existing) existing.remove(); + }; + }, [SessionInfo.userId, companyId]); + + return ( + <> + {isOrgAdmin && ( + setShowEditPopup(false)} + companyId={companyId} + headerColor={headerColor} + headerFontColor={headerFontColor} + headerFontShadowColor={headerFontShadowColor} + headerImage={headerImage} + onSaved={() => { + setShowEditPopup(false); + }} + onRefresh={() => router.refresh()} + /> + )} + + ); +} diff --git a/txclient/src/app/landing/org/[oid]/HeaderEditPopup.tsx b/txclient/src/app/landing/org/[oid]/HeaderEditPopup.tsx index 5e097d8..8a6907a 100644 --- a/txclient/src/app/landing/org/[oid]/HeaderEditPopup.tsx +++ b/txclient/src/app/landing/org/[oid]/HeaderEditPopup.tsx @@ -10,7 +10,7 @@ import ImagePicker from "@components/ImagePicker/ImagePicker"; import { useAlert } from "@core/Store/Alert.Store"; import { useSessionStore } from "@core/Store/Sesion.Store"; import { useOrganizationHeaderStore } from "@core/Store/OrganizationHeader.Store"; -import { updateOrganizationsById, uploadHeaderImage } from "./Org.Service"; +import { updateOrganizationColors, uploadHeaderImage } from "@services/Header.Edit.Service"; import { UpdateCompanyParams } from "@models/Company.model"; interface HeaderEditPopupProps { @@ -58,7 +58,7 @@ export default function HeaderEditPopup(props: HeaderEditPopupProps) { sessionUser: SessionInfo.userId, }; - await updateOrganizationsById(updData); + await updateOrganizationColors(updData); organizationHeader.setHeaderColor(headerColorState); organizationHeader.setHeaderFontColor(headerFontColorState); diff --git a/txclient/src/app/landing/service/[sid]/ServiceDetailCard.tsx b/txclient/src/app/landing/service/[sid]/ServiceDetailCard.tsx index 706e58b..cedc943 100644 --- a/txclient/src/app/landing/service/[sid]/ServiceDetailCard.tsx +++ b/txclient/src/app/landing/service/[sid]/ServiceDetailCard.tsx @@ -6,7 +6,7 @@ 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"; +import { checkOrgAdmin } from "@services/Header.Edit.Service"; interface ServiceDetailCardProps { service: PublicServiceView; diff --git a/txclient/src/app/landing/service/[sid]/ServiceImageEditPopup.tsx b/txclient/src/app/landing/service/[sid]/ServiceImageEditPopup.tsx index bf191ea..da44abc 100644 --- a/txclient/src/app/landing/service/[sid]/ServiceImageEditPopup.tsx +++ b/txclient/src/app/landing/service/[sid]/ServiceImageEditPopup.tsx @@ -7,7 +7,7 @@ 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"; +import { uploadServiceImage } from "@services/Header.Edit.Service"; interface ServiceImageEditPopupProps { open: boolean; diff --git a/txclient/src/app/landing/service/[sid]/page.tsx b/txclient/src/app/landing/service/[sid]/page.tsx index cd96b32..82fbbad 100644 --- a/txclient/src/app/landing/service/[sid]/page.tsx +++ b/txclient/src/app/landing/service/[sid]/page.tsx @@ -12,6 +12,7 @@ import EvangelizeBanner from "@components/EvangelizeBanner/EvangelizeBanner"; import ReviewsCarousel from "@components/ReviewsCarousel/ReviewsCarousel"; import { RatingTargetType } from "@core/Models/Ratings.model"; import ServiceDetailCard from "./ServiceDetailCard"; +import OrganizationHeaderEdit from "@components/OrganizationHeaderEdit/OrganizationHeaderEdit"; const GetServiceData = async (serviceId: string): Promise => { try { @@ -56,7 +57,16 @@ export default async function OrganizationPublicProfile({ params }: Props) {
- +
+ +
+