From 0b073829196a48cffaf57cf45939a80e5980ae5c Mon Sep 17 00:00:00 2001 From: david Date: Mon, 6 Jul 2026 17:33:45 +0200 Subject: [PATCH] Selector de empresas --- apps/web/src/layout/app-company-switcher.tsx | 57 +++++---- apps/web/src/lib/hooks/index.ts | 1 + .../use-app-company-switcher-controller.ts | 110 ++++++++++++++++++ .../use-company-selection-page-controller.ts | 6 +- .../context/identity-auth-session.context.tsx | 24 ++-- 5 files changed, 157 insertions(+), 41 deletions(-) create mode 100644 apps/web/src/lib/hooks/use-app-company-switcher-controller.ts diff --git a/apps/web/src/layout/app-company-switcher.tsx b/apps/web/src/layout/app-company-switcher.tsx index 925d3484..41c3f943 100644 --- a/apps/web/src/layout/app-company-switcher.tsx +++ b/apps/web/src/layout/app-company-switcher.tsx @@ -9,32 +9,20 @@ import { } from "@repo/shadcn-ui/components"; import { cn } from "@repo/shadcn-ui/lib/utils"; import { CheckIcon, ChevronDownIcon } from "lucide-react"; -import * as React from "react"; -interface Company { - id: string; - name: string; - taxName: string; - initials: string; -} - -const companies: Company[] = [ - { id: "company-1", name: "Mi Empresa", taxName: "Mi Empresa S.L.", initials: "ME" }, - { id: "company-2", name: "Comercial Norte", taxName: "Comercial Norte S.L.", initials: "CN" }, - { - id: "company-3", - name: "Servicios Globales", - taxName: "Servicios Globales S.L.", - initials: "SG", - }, -]; +import { useAppCompanySwitcherController } from "../lib/hooks"; interface AppCompanySwitcherProps { className?: string; } export const AppCompanySwitcher = ({ className }: AppCompanySwitcherProps) => { - const [selectedCompany, setSelectedCompany] = React.useState(companies[0]); + const { activeCompany, companies, handleSelectCompany, isChangingCompany, isDisabled } = + useAppCompanySwitcherController(); + + if (!activeCompany) { + return null; + } return (
@@ -42,23 +30,29 @@ export const AppCompanySwitcher = ({ className }: AppCompanySwitcherProps) => { - {selectedCompany.initials} + {activeCompany.initials} - {selectedCompany.name} - - - {selectedCompany.taxName} + {activeCompany.name} + {activeCompany.secondaryLabel ? ( + + {activeCompany.secondaryLabel} + + ) : null} { {companies.map((company) => { - const isSelected = company.id === selectedCompany.id; - return ( setSelectedCompany(company)} + onSelect={() => handleSelectCompany(company.id)} > {company.initials} @@ -89,12 +82,14 @@ export const AppCompanySwitcher = ({ className }: AppCompanySwitcherProps) => { {company.name} - - {company.taxName} - + {company.secondaryLabel ? ( + + {company.secondaryLabel} + + ) : null} - {isSelected ? ( + {company.isSelected ? ( diff --git a/apps/web/src/lib/hooks/index.ts b/apps/web/src/lib/hooks/index.ts index 767b68bf..baed263c 100644 --- a/apps/web/src/lib/hooks/index.ts +++ b/apps/web/src/lib/hooks/index.ts @@ -1 +1,2 @@ +export * from "./use-app-company-switcher-controller"; export * from "./use-theme"; diff --git a/apps/web/src/lib/hooks/use-app-company-switcher-controller.ts b/apps/web/src/lib/hooks/use-app-company-switcher-controller.ts new file mode 100644 index 00000000..eba1d378 --- /dev/null +++ b/apps/web/src/lib/hooks/use-app-company-switcher-controller.ts @@ -0,0 +1,110 @@ +import { useIdentityAuthSession } from "@erp/identity/client"; +import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers"; +import { useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; + +function resolveCompanyDisplayName(legalName: string, tradeName: string | null) { + return tradeName?.trim() || legalName; +} + +function resolveCompanySecondaryLabel(legalName: string, tradeName: string | null) { + if (tradeName?.trim()) { + return legalName; + } + + return null; +} + +function resolveCompanyInitials(name: string) { + const words = name + .trim() + .split(/\s+/) + .filter(Boolean); + + if (words.length === 0) { + return "--"; + } + + return words + .slice(0, 2) + .map((word) => word.charAt(0).toUpperCase()) + .join(""); +} + +function resolveErrorMessage(error: unknown) { + if (error instanceof Error && error.message.trim().length > 0) { + return error.message; + } + + return "Please try again in a few seconds."; +} + +export function useAppCompanySwitcherController() { + const queryClient = useQueryClient(); + const { activeCompany, availableCompanies, changeActiveCompany } = useIdentityAuthSession(); + const [isChangingCompany, setIsChangingCompany] = useState(false); + + const activeCompanyView = activeCompany + ? { + id: activeCompany.id, + name: resolveCompanyDisplayName(activeCompany.legalName, activeCompany.tradeName), + secondaryLabel: resolveCompanySecondaryLabel( + activeCompany.legalName, + activeCompany.tradeName + ), + initials: resolveCompanyInitials( + resolveCompanyDisplayName(activeCompany.legalName, activeCompany.tradeName) + ), + } + : null; + + const companyOptions = availableCompanies.map((company) => { + const name = resolveCompanyDisplayName(company.legalName, company.tradeName); + + return { + id: company.id, + name, + secondaryLabel: resolveCompanySecondaryLabel(company.legalName, company.tradeName), + initials: resolveCompanyInitials(name), + isSelected: company.id === activeCompany?.id, + }; + }); + + const handleSelectCompany = async (companyId: string) => { + if (isChangingCompany || companyId === activeCompany?.id) { + return; + } + + setIsChangingCompany(true); + + try { + const nextActiveCompany = await changeActiveCompany(companyId); + + if (!nextActiveCompany) { + showErrorToast("Unable to switch company", "The selected company is not available."); + return; + } + + await queryClient.cancelQueries(); + queryClient.removeQueries({ type: "inactive" }); + await queryClient.resetQueries({ type: "active" }); + + showSuccessToast( + "Active company updated", + `${resolveCompanyDisplayName(nextActiveCompany.legalName, nextActiveCompany.tradeName)} is now active.` + ); + } catch (error) { + showErrorToast("Unable to switch company", resolveErrorMessage(error)); + } finally { + setIsChangingCompany(false); + } + }; + + return { + activeCompany: activeCompanyView, + companies: companyOptions, + isChangingCompany, + isDisabled: isChangingCompany || companyOptions.length <= 1, + handleSelectCompany, + }; +} diff --git a/modules/identity/src/web/company-selection/controllers/use-company-selection-page-controller.ts b/modules/identity/src/web/company-selection/controllers/use-company-selection-page-controller.ts index 4e4328da..692da6f6 100644 --- a/modules/identity/src/web/company-selection/controllers/use-company-selection-page-controller.ts +++ b/modules/identity/src/web/company-selection/controllers/use-company-selection-page-controller.ts @@ -24,14 +24,14 @@ export function useCompanySelectionPageController() { isAuthenticated, isLoading, logout, - selectActiveCompany, + changeActiveCompany, getAuthenticatedRedirectPath, } = useIdentityAuthSession(); const returnTo = useMemo(() => resolveSafeReturnTo(searchParams.get("returnTo")), [searchParams]); - const handleSelectCompany = (companyId: string) => { - const selectedCompany = selectActiveCompany(companyId); + const handleSelectCompany = async (companyId: string) => { + const selectedCompany = await changeActiveCompany(companyId); if (!selectedCompany) { return; diff --git a/modules/identity/src/web/context/identity-auth-session.context.tsx b/modules/identity/src/web/context/identity-auth-session.context.tsx index 35d077fe..1602f002 100644 --- a/modules/identity/src/web/context/identity-auth-session.context.tsx +++ b/modules/identity/src/web/context/identity-auth-session.context.tsx @@ -37,6 +37,7 @@ export interface IdentityAuthSessionContextValue { refreshSession: () => Promise; refreshAccessToken: () => Promise; selectActiveCompany: (companyId: string) => ActiveCompany | null; + changeActiveCompany: (companyId: string) => Promise; getAuthenticatedRedirectPath: (params?: { returnTo?: string | null; fallbackPath?: string; @@ -175,6 +176,7 @@ export function IdentityAuthSessionProvider({ children }: { children: React.Reac }); authTokenStorage.setTokens(refreshedTokens); + await hydrateCompanySession(refreshedTokens.accessToken); if (session) { const refreshedSession: AuthSession = { @@ -203,7 +205,7 @@ export function IdentityAuthSessionProvider({ children }: { children: React.Reac clearSession(); return null; } - }, [clearSession, dataSource, refreshSessionMutation, session]); + }, [clearSession, dataSource, hydrateCompanySession, refreshSessionMutation, session]); const refreshAccessToken = useCallback(async () => { const refreshedSession = await refreshSession(); @@ -212,15 +214,14 @@ export function IdentityAuthSessionProvider({ children }: { children: React.Reac const selectActiveCompany = useCallback( (companyId: string) => { + if (companySession.activeCompany?.id === companyId) { + return companySession.activeCompany; + } + const nextActiveCompany = companySession.availableCompanies.find((company) => company.id === companyId) ?? null; if (!nextActiveCompany) { - activeCompanyStorage.clear(); - setCompanySession((currentSession) => ({ - ...currentSession, - activeCompany: null, - })); return null; } @@ -232,7 +233,14 @@ export function IdentityAuthSessionProvider({ children }: { children: React.Reac return nextActiveCompany; }, - [companySession.availableCompanies] + [companySession.activeCompany, companySession.availableCompanies] + ); + + const changeActiveCompany = useCallback( + async (companyId: string) => { + return selectActiveCompany(companyId); + }, + [selectActiveCompany] ); const getAuthenticatedRedirectPath = useCallback( @@ -280,10 +288,12 @@ export function IdentityAuthSessionProvider({ children }: { children: React.Reac refreshSession, refreshAccessToken, selectActiveCompany, + changeActiveCompany, getAuthenticatedRedirectPath, clearSession, }), [ + changeActiveCompany, clearSession, companySession, getAuthenticatedRedirectPath,