Selector de empresas
This commit is contained in:
parent
b1e9f4bd2d
commit
0b07382919
@ -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<Company>(companies[0]);
|
||||
const { activeCompany, companies, handleSelectCompany, isChangingCompany, isDisabled } =
|
||||
useAppCompanySwitcherController();
|
||||
|
||||
if (!activeCompany) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("w-full", className)}>
|
||||
@ -42,23 +30,29 @@ export const AppCompanySwitcher = ({ className }: AppCompanySwitcherProps) => {
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<button
|
||||
aria-busy={isChangingCompany}
|
||||
aria-label="Switch active company"
|
||||
className={cn(
|
||||
"flex h-14 w-full items-center gap-3 rounded-none px-3 text-left hover:bg-muted transition-colors",
|
||||
isChangingCompany && "cursor-wait opacity-80",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
)}
|
||||
disabled={isDisabled}
|
||||
type="button"
|
||||
>
|
||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-blue-600 text-sm font-semibold text-white">
|
||||
{selectedCompany.initials}
|
||||
{activeCompany.initials}
|
||||
</span>
|
||||
|
||||
<span className="min-w-0 flex-1 group-data-[collapsible=icon]:hidden">
|
||||
<span className="block truncate text-sm font-semibold leading-5 text-foreground">
|
||||
{selectedCompany.name}
|
||||
</span>
|
||||
<span className="block truncate text-xs text-muted-foreground">
|
||||
{selectedCompany.taxName}
|
||||
{activeCompany.name}
|
||||
</span>
|
||||
{activeCompany.secondaryLabel ? (
|
||||
<span className="block truncate text-xs text-muted-foreground">
|
||||
{activeCompany.secondaryLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
|
||||
<ChevronDownIcon
|
||||
@ -75,13 +69,12 @@ export const AppCompanySwitcher = ({ className }: AppCompanySwitcherProps) => {
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{companies.map((company) => {
|
||||
const isSelected = company.id === selectedCompany.id;
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
className="gap-3 py-2"
|
||||
disabled={company.isSelected || isChangingCompany}
|
||||
key={company.id}
|
||||
onSelect={() => setSelectedCompany(company)}
|
||||
onSelect={() => handleSelectCompany(company.id)}
|
||||
>
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted text-xs font-semibold text-foreground">
|
||||
{company.initials}
|
||||
@ -89,12 +82,14 @@ export const AppCompanySwitcher = ({ className }: AppCompanySwitcherProps) => {
|
||||
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate font-medium">{company.name}</span>
|
||||
<span className="block truncate text-xs text-muted-foreground">
|
||||
{company.taxName}
|
||||
</span>
|
||||
{company.secondaryLabel ? (
|
||||
<span className="block truncate text-xs text-muted-foreground">
|
||||
{company.secondaryLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
|
||||
{isSelected ? (
|
||||
{company.isSelected ? (
|
||||
<CheckIcon aria-hidden="true" className="size-4 text-blue-600" />
|
||||
) : null}
|
||||
</DropdownMenuItem>
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from "./use-app-company-switcher-controller";
|
||||
export * from "./use-theme";
|
||||
|
||||
110
apps/web/src/lib/hooks/use-app-company-switcher-controller.ts
Normal file
110
apps/web/src/lib/hooks/use-app-company-switcher-controller.ts
Normal file
@ -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,
|
||||
};
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -37,6 +37,7 @@ export interface IdentityAuthSessionContextValue {
|
||||
refreshSession: () => Promise<AuthSession | null>;
|
||||
refreshAccessToken: () => Promise<string | null>;
|
||||
selectActiveCompany: (companyId: string) => ActiveCompany | null;
|
||||
changeActiveCompany: (companyId: string) => Promise<ActiveCompany | null>;
|
||||
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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user