diff --git a/server/src/Models/Companies/Companies.Interface.ts b/server/src/Models/Companies/Companies.Interface.ts index 541c8a0..fc36a54 100644 --- a/server/src/Models/Companies/Companies.Interface.ts +++ b/server/src/Models/Companies/Companies.Interface.ts @@ -380,4 +380,5 @@ export interface ICompaniesManager { sysAdminUpdate(data: SysAdminUpdateCompanyParams): Promise; sysAdminSetBanned(data: SysAdminSetCompanyBannedParams): Promise; sysAdminGetInsights(data: SysAdminCompanyInsightsParams): Promise; + checkCompanyAdmin(data: { companyId: string; sessionUser: string }): Promise; } diff --git a/server/src/Models/Companies/Companies.ts b/server/src/Models/Companies/Companies.ts index 618319b..62f5051 100644 --- a/server/src/Models/Companies/Companies.ts +++ b/server/src/Models/Companies/Companies.ts @@ -1221,6 +1221,19 @@ class CompaniesManager implements ICompaniesManager { }, }; } + + public async checkCompanyAdmin(data: { companyId: string; sessionUser: string }): Promise { + const filterData = + data.companyId.indexOf("-") >= 0 ? { slug: data.companyId } : { _id: data.companyId }; + + const company = await this.companies.findOne(filterData as any); + + if (!company) { + return false; + } + + return await EmployeesList.checkPermission(company, data.sessionUser, EmployeeRoles.ADMIN); + } } const CompaniesList = new CompaniesManager(); diff --git a/server/src/api/Companies/Companies.Controller.ts b/server/src/api/Companies/Companies.Controller.ts index 10a3614..69365d1 100644 --- a/server/src/api/Companies/Companies.Controller.ts +++ b/server/src/api/Companies/Companies.Controller.ts @@ -405,3 +405,24 @@ export class CheckCompanyNameController extends Controller { } } } + +@Route("companies/check-admin") +@Middlewares(authenticateMiddleware) +export class CheckCompanyAdminController extends Controller { + @Response(500, "Ha ocurrido un error") + @SuccessResponse(200, "Done") + @Post() + public async checkAdmin( + @Body() requestBody: { companyId: string; sessionUser: string } + ): Promise<{ isAdmin: boolean } | ApiValidationError> { + try { + const isAdmin = await new CompaniesService().checkCompanyAdmin(requestBody); + this.setStatus(200); + return { isAdmin }; + } catch (e) { + const errorOccurred: Error = e as Error; + this.setStatus(500); + return new ApiValidationError(500, errorOccurred.message); + } + } +} diff --git a/server/src/api/Companies/Companies.Service.ts b/server/src/api/Companies/Companies.Service.ts index 506aa15..cba013e 100644 --- a/server/src/api/Companies/Companies.Service.ts +++ b/server/src/api/Companies/Companies.Service.ts @@ -112,4 +112,9 @@ export class CompaniesService { const isValid = await CompaniestList.isValidName({ name }); return isValid; } + + public async checkCompanyAdmin(data: { companyId: string; sessionUser: string }): Promise { + await connect(`${process.env.DATABASE_CONNECTION}`); + return await CompaniestList.checkCompanyAdmin(data); + } } diff --git a/txclient/src/app/landing/org/[oid]/HeaderEditPopup.tsx b/txclient/src/app/landing/org/[oid]/HeaderEditPopup.tsx new file mode 100644 index 0000000..dafd3f3 --- /dev/null +++ b/txclient/src/app/landing/org/[oid]/HeaderEditPopup.tsx @@ -0,0 +1,192 @@ +"use client"; +import { useState, useEffect } from "react"; +import { Grid2, IconButton, Dialog, DialogContent, DialogTitle, Box, Divider } from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; +import SaveIcon from "@mui/icons-material/Save"; +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 { updateOrganizationsById, uploadHeaderImage } from "./Org.Service"; +import { UpdateCompanyParams } from "@models/Company.model"; + +interface HeaderEditPopupProps { + open: boolean; + onClose: () => void; + companyId: string; + onSaved: () => void; +} + +export default function HeaderEditPopup(props: HeaderEditPopupProps) { + const { open, onClose, companyId, onSaved } = props; + const alert = useAlert(); + const SessionInfo = useSessionStore(); + const organizationHeader = useOrganizationHeaderStore(); + + const [headerImage, setHeaderImage] = useState(""); + const [headerColor, setHeaderColor] = useState(""); + const [headerFontColor, setHeaderFontColor] = useState(""); + const [headerFontShadowColor, setHeaderFontShadowColor] = useState(""); + const [loading, setLoading] = useState(false); + + // Load current values from store when opening + useEffect(() => { + if (!open) return; + + // Get current values from the organization header store + const current = useOrganizationHeaderStore.getState(); + setHeaderColor(current.headerColor || ""); + setHeaderFontColor(current.headerFontColor || ""); + setHeaderFontShadowColor(current.headerFontShadowColor || ""); + setHeaderImage(""); + }, [open]); + + const handleSaveColors = async () => { + setLoading(true); + + try { + const updData: UpdateCompanyParams = { + id: companyId, + headerColor: headerColor, + headerFontColor: headerFontColor, + headerFontShadowColor: headerFontShadowColor, + sessionUser: SessionInfo.userId, + }; + + await updateOrganizationsById(updData); + + // Update the header store so colors apply immediately + organizationHeader.setHeaderColor(headerColor); + organizationHeader.setHeaderFontColor(headerFontColor); + organizationHeader.setHeaderFontShadowColor(headerFontShadowColor); + + alert.showSuccess("Colores actualizados"); + } 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 handleSaveImage = async (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); + + await uploadHeaderImage(formData); + 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); + } + }; + + return ( + + + Personalizar header + theme.palette.grey[500] }}> + + + + + + {/* Colores */} + + Colores + + + { + setHeaderColor(color); + }} + /> + + + { + setHeaderFontColor(color); + }} + /> + + + { + setHeaderFontShadowColor(color); + }} + /> + + + + + + + {/* Imagen */} + + Imagen de cabecera + + + + + + {/* Botón guardar */} + + + + + + + + + + + ); +} diff --git a/txclient/src/app/landing/org/[oid]/Org.Service.ts b/txclient/src/app/landing/org/[oid]/Org.Service.ts new file mode 100644 index 0000000..3657e02 --- /dev/null +++ b/txclient/src/app/landing/org/[oid]/Org.Service.ts @@ -0,0 +1,13 @@ +import ApiRequest from "@services/Api.Service"; +import { UpdateCompanyParams } from "@models/Company.model"; +import { ApiVoidResult } from "@models/Api.VoidResult.type"; + +export const updateOrganizationsById = async ( + data: UpdateCompanyParams +): Promise => { + return ApiRequest.post("companies/update", data); +}; + +export const uploadHeaderImage = async (data: FormData): Promise => { + return ApiRequest.postFile("companies/upload-header", data); +}; diff --git a/txclient/src/app/landing/org/[oid]/layout.tsx b/txclient/src/app/landing/org/[oid]/layout.tsx index 3d84630..32faa15 100644 --- a/txclient/src/app/landing/org/[oid]/layout.tsx +++ b/txclient/src/app/landing/org/[oid]/layout.tsx @@ -45,7 +45,9 @@ export default async function OrganizationPublicProfile({ params, children }: Pr
{orgData.id && ( <> - +
+ +
{children}
diff --git a/txclient/src/app/landing/org/[oid]/page.tsx b/txclient/src/app/landing/org/[oid]/page.tsx index 49651b0..eff0b20 100644 --- a/txclient/src/app/landing/org/[oid]/page.tsx +++ b/txclient/src/app/landing/org/[oid]/page.tsx @@ -61,6 +61,9 @@ import { useConfirmStore } from "@core/Store/Confirm.Store"; import ReviewsCarousel from "@core/app/components/ReviewsCarousel/ReviewsCarousel"; import OrganizationProfessionals from "@core/app/components/OrganizationProfessionals/OrganizationProfessionals"; import { RatingTargetType } from "@core/Models/Ratings.model"; +import HeaderEditPopup from "./HeaderEditPopup"; +import { checkOrgAdmin } from "./post.service"; +import { IconButton as MuiIconButton } from "@mui/material"; enum VIEW { POSTS = "posts", @@ -96,6 +99,47 @@ export default function OrganizationPublicProfile() { const [posts, setPosts] = useState([]); const [authorizedUsers, setAuthorizedUsers] = useState([]); const [currentPage, setCurrentPage] = useState(1); + const [isOrgAdmin, setIsOrgAdmin] = useState(false); + const [showEditPopup, setShowEditPopup] = useState(false); + + // Append edit button to header wrapper when admin + useEffect(() => { + if (!isOrgAdmin) return; + + const wrapper = document.getElementById("header-wrapper"); + if (!wrapper) return; + + const btn = document.createElement("div"); + btn.id = "edit-header-btn"; + btn.innerHTML = ` + + `; + btn.querySelector("button")!.addEventListener("click", () => setShowEditPopup(true)); + wrapper.appendChild(btn); + + return () => { + const existing = document.getElementById("edit-header-btn"); + if (existing) existing.remove(); + }; + }, [isOrgAdmin]); const SessionInfo = useSessionStore(); const eventHandler = useEventHandlerStore(); @@ -139,6 +183,22 @@ export default function OrganizationPublicProfile() { }); }, [SessionInfo, authorizedUsers]); + // Check if the logged-in user is admin of this organization + useEffect(() => { + if (!SessionInfo.userId || !oid) { + setIsOrgAdmin(false); + return; + } + + checkOrgAdmin(oid, SessionInfo.userId) + .then((isAdmin) => { + setIsOrgAdmin(isAdmin); + }) + .catch(() => { + setIsOrgAdmin(false); + }); + }, [SessionInfo.userId, oid]); + useEffect(() => { if (!OrganizationData.orgData) return; if (!oid) return; //if (!OrganizationData.orgData.id) return; @@ -531,6 +591,16 @@ export default function OrganizationPublicProfile() { return ( <> + {isOrgAdmin && oid && ( + setShowEditPopup(false)} + companyId={oid} + onSaved={() => { + setShowEditPopup(false); + }} + /> + )} {orgData && (
{showEditor && ( diff --git a/txclient/src/app/landing/org/[oid]/post.service.ts b/txclient/src/app/landing/org/[oid]/post.service.ts index 40428b3..d16dc87 100644 --- a/txclient/src/app/landing/org/[oid]/post.service.ts +++ b/txclient/src/app/landing/org/[oid]/post.service.ts @@ -65,3 +65,7 @@ export const toggleFixedPost = async (data: FixCompanyPostParams): Promise export const moveFixedPost = async (data: MoveCompanyFixedPostParams): Promise => { return ApiRequest.post("companies/move-fixed-post", data); }; + +export const checkOrgAdmin = async (companyId: string, sessionUser: string): Promise => { + return ApiRequest.post("companies/check-admin", { companyId, sessionUser }); +};