Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/modules/onboarding/services/OnboardingService.ts
T

268 lines
9.0 KiB
TypeScript

import { OnboardingFilter, OnboardingSession } from "@toolctrl/db/prisma-browser";
import { getAppConfiguration } from "~/utils/db/appConfiguration.db.server";
import { getAllEntities } from "~/utils/db/entities/entities.db.server";
import { getAllRoles } from "~/utils/db/permissions/roles.db.server";
import { getAllSubscriptionProducts } from "~/utils/db/subscriptionProducts.db.server";
import { adminGetAllTenants, getAllTenantUsers } from "~/utils/db/tenants.db.server";
import { adminGetAllUsers, UserSimple } from "~/utils/db/users.db.server";
import { createOnboardingSession, getOnboarding, getOnboardings, OnboardingWithDetails, updateOnboarding } from "../db/onboarding.db.server";
import { getOnboardingSession, getOnboardingSessionsByUser, OnboardingSessionWithDetails } from "../db/onboardingSessions.db.server";
import { OnboardingCandidateDto } from "../dtos/OnboardingCandidateDto";
import { OnboardingFilterMetadataDto } from "../dtos/OnboardingFilterMetadataDto";
import { OnboardingFilterType } from "../dtos/OnboardingFilterTypes";
import OnboardingFiltersService from "./OnboardingFiltersService";
import OnboardingSessionService from "./OnboardingSessionService";
import { evaluateOnboardingStepGates, getVisibleOnboardingSteps } from "./step-registry/gating.server";
import { getOnboardingStepDefinitions } from "./step-registry/registry.server";
import { reservedTenantRouteSlugs } from "~/utils/app/PublicRouteSegments";
import { getActiveTenantSubscriptions } from "~/utils/services/.server/subscriptionService";
async function getUserActiveOnboarding({
userId,
tenantId,
request,
}: {
userId: string;
tenantId: string | null;
request: Request;
}): Promise<OnboardingSessionWithDetails | null> {
const appConfiguration = await getAppConfiguration({ request });
if (!appConfiguration.onboarding.enabled) {
return null;
}
let currentSession: OnboardingSessionWithDetails | null = null;
const allUserOnboardings = await getOnboardingSessionsByUser({ userId, tenantId });
const activeOnboardings = allUserOnboardings.filter((f) => f.status === "active" || f.status === "started");
// If there is an active onboarding, return it
if (activeOnboardings.length > 0) {
currentSession = activeOnboardings[0];
}
// If there is no active onboarding, check if this user belongs to an active onboarding
else {
const onboardingSessions = await generateMatchingOnboardings({ userId, tenantId });
await Promise.all(
onboardingSessions.map(async (session) => {
if (!currentSession && (session?.status === "active" || session?.status === "started")) {
currentSession = await getOnboardingSession(session.id);
}
})
);
}
if (!currentSession?.onboarding.active) {
return null;
}
const visibleSteps = getVisibleOnboardingSteps(
await evaluateOnboardingStepGates({
request,
params: getOnboardingGateParams({ request, tenantId }),
subscription: await getOnboardingGateSubscription(tenantId),
definitions: getOnboardingStepDefinitions(),
})
);
if (visibleSteps.length === 0) {
return null;
}
if (currentSession.status === "active") {
await OnboardingSessionService.started({ session: currentSession, request });
}
return currentSession;
}
function getOnboardingGateParams({ request, tenantId }: { request: Request; tenantId: string | null }) {
const pathname = new URL(request.url).pathname;
const legacyTenant = pathname.match(/^\/app\/([^/]+)/)?.[1];
const hostAwareTenant = pathname.match(/^\/([^/]+)/)?.[1];
const tenant =
legacyTenant ??
(hostAwareTenant && !(reservedTenantRouteSlugs as readonly string[]).includes(hostAwareTenant) ? hostAwareTenant : undefined);
return {
...(tenant ? { tenant } : {}),
...(tenantId ? { tenantId } : {}),
};
}
async function getOnboardingGateSubscription(tenantId: string | null) {
if (!tenantId) {
return null;
}
return getActiveTenantSubscriptions(tenantId);
}
async function generateMatchingOnboardings({ userId, tenantId }: { userId: string; tenantId: string | null }): Promise<(OnboardingSession | undefined)[]> {
const allOnboardings = await getOnboardings({
realtime: true,
active: true,
});
return await Promise.all(
allOnboardings.map(async (onboarding) => {
const existingSession = onboarding.sessions.find((f) => f.userId === userId && f.tenantId === tenantId);
if (existingSession) {
return;
}
if (onboarding.filters.length === 0) {
return;
}
const matchingFilters = await getMatchingFilters({ userId, tenantId, filters: onboarding.filters });
if (matchingFilters.length === onboarding.filters.length) {
return await createOnboardingSession(onboarding, {
userId,
tenantId,
status: "active",
matchingFilters,
createdRealtime: true,
});
}
})
);
}
async function getMatchingFilters({
userId,
tenantId,
filters,
}: {
userId: string;
tenantId: string | null;
filters: OnboardingFilter[];
}): Promise<OnboardingFilter[]> {
const matchedFilters: OnboardingFilter[] = [];
await Promise.all(
filters.map(async (filter) => {
if (await OnboardingFiltersService.matches({ userId, tenantId, filter })) {
matchedFilters.push(filter);
}
})
);
return matchedFilters;
}
async function setOnboardingStatus(id: string, active: boolean): Promise<void> {
const onboarding = await getOnboarding(id);
if (!onboarding || onboarding.active === active) {
return;
}
await updateOnboarding(id, { active });
// await updateOnboardingSessions(
// onboarding.sessions.map((m) => m.id),
// { status: "active" }
// );
}
async function getCandidates(onboarding: OnboardingWithDetails): Promise<OnboardingCandidateDto[]> {
if (onboarding.filters.length === 0) {
return [];
}
const candidates: OnboardingCandidateDto[] = [];
const allUsers = await adminGetAllUsers();
const allTenantUsers = await getAllTenantUsers();
const allTenants = await adminGetAllTenants();
await Promise.all(
allUsers.items.map(async (user) => {
if (!user.admin) {
return;
}
const matchingFilters = await getMatchingFilters({ userId: user.id, tenantId: null, filters: onboarding.filters });
if (matchingFilters.length === onboarding.filters.length) {
candidates.push({
user: user as UserSimple,
tenant: null,
matchingFilters: matchingFilters.map((m) => {
return {
type: m.type as OnboardingFilterType,
value: m.value,
};
}),
});
}
})
);
await Promise.all(
allTenantUsers.map(async (tenantUser) => {
const tenant = allTenants.find((f) => f.id === tenantUser.tenantId);
if (!tenant) {
return;
}
const matchingFilters = await getMatchingFilters({ userId: tenantUser.userId, tenantId: tenantUser.tenantId, filters: onboarding.filters });
if (matchingFilters.length === onboarding.filters.length) {
candidates.push({
user: tenantUser.user as UserSimple,
tenant: { id: tenant.id, name: tenant.name, slug: tenant.slug },
matchingFilters: matchingFilters.map((m) => {
return {
type: m.type as OnboardingFilterType,
value: m.value,
};
}),
});
}
})
);
return candidates.sort((a, b) => {
if (a.tenant && !b.tenant) {
return 1;
}
if (!a.tenant && b.tenant) {
return -1;
}
if (a.tenant && b.tenant) {
return a.tenant.name.localeCompare(b.tenant.name);
}
return a.user.email.localeCompare(b.user.email);
});
}
async function getMetadata(): Promise<OnboardingFilterMetadataDto> {
return {
users: (await adminGetAllUsers()).items,
tenants: await adminGetAllTenants(),
entities: await getAllEntities({ tenantId: null }),
subscriptionProducts: await getAllSubscriptionProducts(),
roles: await getAllRoles(),
};
}
export type OnboardingSummaryDto = {
onboardings: { all: number; active: number };
sessions: { all: number; active: number; dismissed: number; completed: number };
};
async function getSummary(): Promise<OnboardingSummaryDto> {
const allOnboardings = await getOnboardings({});
const summary: OnboardingSummaryDto = {
onboardings: { all: 0, active: 0 },
sessions: { all: 0, active: 0, dismissed: 0, completed: 0 },
};
allOnboardings.forEach((onboarding) => {
summary.onboardings.all++;
if (onboarding.active) {
summary.onboardings.active++;
}
onboarding.sessions.forEach((session) => {
summary.sessions.all++;
if (session.status === "active" || session.status === "started") {
summary.sessions.active++;
} else if (session.status === "completed") {
summary.sessions.completed++;
} else if (session.status === "dismissed") {
summary.sessions.dismissed++;
}
});
});
return summary;
}
export default {
getUserActiveOnboarding,
getMatchingFilters,
setOnboardingStatus,
getCandidates,
getMetadata,
getSummary,
};