* refactor: move booking-audit client components from packages/features to apps/web/modules This is part of the larger effort to move tRPC-dependent UI components from packages/features to apps/web/modules to eliminate circular dependencies. Changes: - Move BookingHistory.tsx and BookingHistoryPage.tsx to apps/web/modules/booking-audit/components/ - Update imports in apps/web to use the new location Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * refactor: move formbricks client from packages/features to apps/web/modules Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * refactor: move hooks and stores from packages/features to apps/web/modules - Move useAppsData hook from packages/features/apps/hooks to apps/web/modules/apps/hooks - Move onboardingStore from packages/features/ee/organizations/lib to apps/web/modules/ee/organizations/lib - Move useWelcomeModal hook from packages/features/ee/organizations/hooks to apps/web/modules/ee/organizations/hooks - Move useAgentsData hook from packages/features/ee/workflows/hooks to apps/web/modules/ee/workflows/hooks - Update all import paths in consuming files Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * refactor: move onboardingStore test file to apps/web/modules Co-Authored-By: benny@cal.com <sldisek783@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
192 lines
6.2 KiB
TypeScript
192 lines
6.2 KiB
TypeScript
import { useSession } from "next-auth/react";
|
|
import { useSearchParams, useRouter, usePathname } from "next/navigation";
|
|
import { useEffect } from "react";
|
|
import { create } from "zustand";
|
|
import { persist, StorageValue } from "zustand/middleware";
|
|
|
|
import { WEBAPP_URL, IS_TEAM_BILLING_ENABLED_CLIENT } from "@calcom/lib/constants";
|
|
import { localStorage } from "@calcom/lib/webstorage";
|
|
import { BillingPeriod, UserPermissionRole } from "@calcom/prisma/enums";
|
|
import { trpc } from "@calcom/trpc/react";
|
|
|
|
interface OnboardingAdminStoreState {
|
|
billingPeriod?: BillingPeriod;
|
|
pricePerSeat?: number | null;
|
|
seats?: number | null;
|
|
orgOwnerEmail?: string;
|
|
}
|
|
|
|
interface OnboardingUserStoreState {
|
|
name: string;
|
|
slug: string;
|
|
logo?: string | null;
|
|
bio?: string | null;
|
|
brandColor?: string | null;
|
|
bannerUrl?: string | null;
|
|
onboardingId: string | null;
|
|
invitedMembers: { email: string; name?: string }[];
|
|
teams: { id: number; name: string; slug: string | null; isBeingMigrated: boolean }[];
|
|
}
|
|
|
|
interface OnboardingStoreState extends OnboardingAdminStoreState, OnboardingUserStoreState {
|
|
// Actions for admin state
|
|
setBillingPeriod: (billingPeriod: BillingPeriod) => void;
|
|
setPricePerSeat: (price: number | null) => void;
|
|
setSeats: (seats: number | null) => void;
|
|
setOrgOwnerEmail: (email: string) => void;
|
|
|
|
// Actions for user state
|
|
setName: (name: string) => void;
|
|
setSlug: (slug: string) => void;
|
|
setLogo: (logo: string) => void;
|
|
setBio: (bio: string) => void;
|
|
setBrandColor: (brandColor: string) => void;
|
|
setBannerUrl: (bannerUrl: string) => void;
|
|
addInvitedMember: (member: { email: string; name?: string }) => void;
|
|
removeInvitedMember: (email: string) => void;
|
|
|
|
setOnboardingId: (onboardingId: string) => void;
|
|
|
|
// Actions for team state
|
|
setTeams: (teams: { id: number; name: string; slug: string | null; isBeingMigrated: boolean }[]) => void;
|
|
// Reset state
|
|
reset: (state?: Partial<OnboardingStoreState>) => void;
|
|
}
|
|
|
|
const initialState: OnboardingAdminStoreState & OnboardingUserStoreState = {
|
|
// User state
|
|
name: "",
|
|
slug: "",
|
|
logo: "",
|
|
bio: "",
|
|
brandColor: "",
|
|
bannerUrl: "",
|
|
orgOwnerEmail: "",
|
|
seats: null,
|
|
pricePerSeat: null,
|
|
billingPeriod: undefined,
|
|
onboardingId: null,
|
|
invitedMembers: [],
|
|
teams: [],
|
|
};
|
|
|
|
export const useOnboardingStore = create<OnboardingStoreState>()(
|
|
persist(
|
|
(set, get) => ({
|
|
...initialState,
|
|
|
|
// Admin actions
|
|
setBillingPeriod: (billingPeriod) => set({ billingPeriod }),
|
|
setPricePerSeat: (pricePerSeat) => set({ pricePerSeat }),
|
|
setSeats: (seats) => set({ seats }),
|
|
setOrgOwnerEmail: (orgOwnerEmail) => set({ orgOwnerEmail }),
|
|
|
|
// User actions
|
|
setName: (name) => set({ name }),
|
|
setSlug: (slug) => set({ slug }),
|
|
setLogo: (logo) => set({ logo }),
|
|
setBio: (bio) => set({ bio }),
|
|
setBrandColor: (brandColor) => set({ brandColor }),
|
|
setBannerUrl: (bannerUrl) => set({ bannerUrl }),
|
|
setOnboardingId: (onboardingId) => set({ onboardingId }),
|
|
addInvitedMember: (member) =>
|
|
set((state) => ({
|
|
invitedMembers: [...state.invitedMembers, member],
|
|
})),
|
|
removeInvitedMember: (email) =>
|
|
set((state) => ({
|
|
invitedMembers: state.invitedMembers.filter((member) => member.email !== email),
|
|
})),
|
|
|
|
// Team actions
|
|
setTeams: (teams) => set({ teams }),
|
|
|
|
reset: (state) => {
|
|
if (state) {
|
|
set(state);
|
|
} else {
|
|
// Clear the localStorage entry for this store
|
|
if (typeof window !== "undefined") {
|
|
localStorage.removeItem("org-creation-onboarding");
|
|
}
|
|
set(initialState);
|
|
}
|
|
},
|
|
}),
|
|
{
|
|
name: "org-creation-onboarding",
|
|
}
|
|
)
|
|
);
|
|
|
|
export const useOnboarding = () => {
|
|
const session = useSession();
|
|
const router = useRouter();
|
|
const path = usePathname();
|
|
const isAdmin = session.data?.user?.role === UserPermissionRole.ADMIN;
|
|
|
|
const isBillingEnabled = process.env.NEXT_PUBLIC_IS_E2E ? false : IS_TEAM_BILLING_ENABLED_CLIENT;
|
|
|
|
const searchParams = useSearchParams();
|
|
const { data: organizationOnboarding, isPending: isLoadingOrgOnboarding } =
|
|
trpc.viewer.organizations.getOrganizationOnboarding.useQuery();
|
|
const { reset } = useOnboardingStore();
|
|
useEffect(() => {
|
|
if (isLoadingOrgOnboarding) {
|
|
return;
|
|
}
|
|
|
|
// Admin on handover page should never touch the store
|
|
// The DB query returns admin's own onboarding, not the one being handed over
|
|
if (isAdmin && path?.includes("/handover")) {
|
|
return;
|
|
}
|
|
|
|
if (organizationOnboarding?.isComplete) {
|
|
reset();
|
|
}
|
|
|
|
if (organizationOnboarding) {
|
|
// Admin creating for someone else - don't sync from DB, let them proceed to handover
|
|
if (isAdmin && organizationOnboarding?.orgOwnerEmail !== session.data?.user.email) {
|
|
return;
|
|
}
|
|
|
|
// Always sync from DB to ensure UI reflects latest changes
|
|
reset({
|
|
onboardingId: organizationOnboarding.id,
|
|
billingPeriod: organizationOnboarding.billingPeriod as BillingPeriod,
|
|
pricePerSeat: organizationOnboarding.pricePerSeat,
|
|
seats: organizationOnboarding.seats,
|
|
orgOwnerEmail: organizationOnboarding.orgOwnerEmail,
|
|
name: organizationOnboarding.name,
|
|
slug: organizationOnboarding.slug,
|
|
bio: organizationOnboarding.bio,
|
|
logo: organizationOnboarding.logo,
|
|
brandColor: organizationOnboarding.brandColor,
|
|
bannerUrl: organizationOnboarding.bannerUrl,
|
|
invitedMembers: [],
|
|
teams: [],
|
|
});
|
|
}
|
|
// Note: We no longer redirect if onboardingId is missing, as regular users
|
|
// don't create the onboarding record until the final step
|
|
}, [organizationOnboarding, isLoadingOrgOnboarding, isAdmin, reset, path]);
|
|
|
|
useEffect(() => {
|
|
if (session.status === "loading") {
|
|
return;
|
|
}
|
|
if (!session.data) {
|
|
const searchString = !searchParams ? "" : `${searchParams.toString()}`;
|
|
router.push(`/auth/login?callbackUrl=${WEBAPP_URL}${path}${searchString ? `?${searchString}` : ""}`);
|
|
}
|
|
}, [session, router, path, searchParams]);
|
|
return {
|
|
useOnboardingStore,
|
|
isLoadingOrgOnboarding,
|
|
dbOnboarding: organizationOnboarding,
|
|
isBillingEnabled,
|
|
};
|
|
};
|