* chore: Upgrade prisma to 6.7.0 * Build fixes * type fixes Signed-off-by: Omar López <zomars@me.com> * Update schema.prisma * Patching * Revert "Update schema.prisma" This reverts commit 47d8618bf89ef4d007b30084df766f17281e21a1. * Revert "Patching" This reverts commit a1d2e3040e71690a44d4324db95d73b4d68c6adb. * Revert schema changes Signed-off-by: Omar López <zomars@me.com> * WIP Signed-off-by: Omar López <zomars@me.com> * Update getPublicEvent.ts * Update imports Signed-off-by: Omar López <zomars@me.com> * Update gitignore Signed-off-by: Omar López <zomars@me.com> * update remaining imports Signed-off-by: Omar López <zomars@me.com> * Delete .cursor/config.json * Discard changes to packages/features/eventtypes/lib/getPublicEvent.ts * Update _get.ts * Update user.ts * Update .gitignore * update * Update WorkflowStepContainer.tsx * Update next-auth-custom-adapter.ts * Update getPublicEvent.ts * Update workflow.ts * Update next-auth-custom-adapter.ts * Update next-auth-options.ts * Update bookingScenario.ts * fix missing imports * upgrades prismock Signed-off-by: Omar López <zomars@me.com> * patches prismock Signed-off-by: Omar López <zomars@me.com> * Update reschedule.test.ts * Update prisma.ts * patch prismock Signed-off-by: Omar López <zomars@me.com> * fix enums imports Signed-off-by: Omar López <zomars@me.com> * Revert "Update prisma.ts" This reverts commit 64edcf8db54171ff4456c209d563b5d431d99619. * Revert "patch prismock" This reverts commit e95819113dc9d88e7130947aa120cd42710977c8. * fix patch * Fix test that overrun the boundary, it shouldn't test too much * Move prisma import to changeSMSLockState * Bring back broken test without illegal imports * Merge with main and fix filter hosts by same round robin host * Fixed buildDryRunBooking fn tests * Fix and move ooo create or update handler test * Fix packages/features/eventtypes/lib/isCurrentlyAvailable.test.ts * Fix packages/trpc/server/routers/viewer/organizations/listMembers.handler.test.ts * Mock @calcom/prisma * Fix: verify-email.test.ts * fix: Moved WebhookService test and fixed default import mock * Fix: Added missing prisma mock, handleNewBooking uses that of course * We're not testing createContext here * fix: Prisma mock fix for listMembers.test.ts * More fixes to broken testcases * Forgot to remove borked test * Prevent the need to mock a lot of dependencies by moving out buildBaseWhereCondition to its own file * Temporarily skip getCalendarEvents, needs a rewrite * Fix: turns out you can access protected in testcases * fix further mocks * Added packages/features/insights/server/buildBaseWhereCondition.ts, types * Always great to have a mock and then not use it * And one less again. * fix: confirm.handler.test, didn't mock prisma * fix: Address minor nit by @eunjae & fix ImpersonationProvider test * Updated isPrismaAvailableCheck that doesn't crash on import * fix: Get Prisma directly from the client, it usually involves the Validator and does not need 'local' inclusion * Add zod-prisma-types without the generator enabled (commented out) * Uncomment and see what happens * Change method of import as imports did not work in Input Schemas * Remove custom 'zod' booking model, it does not belong with Prisma * Fix all other global Model imports * Rewrite most schema includes AND remove barrel file * Add bookingCreateBodySchema to features/bookings * Flurry of type fixes for compatibility with new zod gen * Refactor out the custom prisma type createEventTypeInput * Work around nullable eventTypeLocations * HandlePayment type fix * More fixes, final fix remaining is CompleteEventType * Should fix a bunch more booking related type errors * Missed one * Some props missing from BookingCreateBodySchema * Fix location type in handleChildrenEventTypes * Little bit hacky imo but it works * Final type error \o/ * Forgot to include Prisma * Do not include zod-utils in booker/types * Oops, was already including Booker/types * Fix membership type, also disallow updating createdAt/updatedAt, make part of patch/post * Fix api v1 type errors * Fix EventTypeDescription typings * Remove getParserWithGeneric, use userBodySchema with UserSchema * use centralized timeZoneSchema * Implement feedback by @zomars * Couple of WIP pushes * Fix tests * Type fixes in `handleChildrenEventTypes` test * Try and parse metadata before use * Change zod-prisma-types configuration for optimal performance * Fix prisma validator error in `prisma/selects/credential` * Disable seperate relations model, hits a bug * Import absolute - this makes rollup work in @platform/libraries * Attempt at removing resolutions override * Refactor using `Prisma.validator` to `satisfies` * Build atoms using @calcom/prisma/client * Build atoms using @calcom/prisma/client * fixes * Update eventTypeSelect.ts * Adjust `eventTypeMetaDataSchemaWithUntypedApps` from `unknown` to `record(any)` * `EventTypeDescription` rely on `descriptionAsSafeHTML` instead of `description` * Add `seatsPerTimeSlot` to event type public select * Fix typing in `users-public-view` getServerSide props * Add missing `schedulingType` to prop * chore: bump platform libraries * Function return type is illegal, not sure how this passed eslint (#21567) * Merged with main * Update updateTokenObject.ts * Update handleResponse.ts * Update index.ts * Update handleChildrenEventTypes.ts * Update booking-idempotency-key.ts * Update WebhookService.test.ts * Update events.test.ts * Update queued-response.test.ts * Update events.test.ts * Update getRoutedUrl.test.ts * fix: type checks Signed-off-by: Omar López <zomars@me.com> * fixes Signed-off-by: Omar López <zomars@me.com> * chore: bump platform libraries * Update yarn.lock * more fixes Signed-off-by: Omar López <zomars@me.com> * fixes Signed-off-by: Omar López <zomars@me.com> * biuld fixes * chore: bump platform libraries * Update conferencing.repository.ts * Update conferencing.repository.ts * Update getCalendarsEvents.test.ts * Update vite.config.js * chore: bump platform libraries * Update users.ts * Discard changes to docs/api-reference/v2/openapi.json * Update vite.config.ts * updated platform libraries * Update get.handler.test.ts * Update get.handler.test.ts * Update schema.prisma * Discard changes to docs/api-reference/v2/openapi.json * Update next-auth-custom-adapter.ts * Update team.ts * Flurry of type fixes * Fix majority of insight related type errors * Type fixes for unlink of account * Make user nullable again * Fixed a bunch of unit tests and one type error * Attempted mock fix * Attempted fix for Attribute type * Ensure default import becomes prisma, but not direct usage * Import default as prisma in prisma.module * Add attributeOption to attribute type * Fix calcom/prisma mock * Refactor Prisma client imports to @calcom/prisma/client Updated all imports from '@prisma/client' to '@calcom/prisma/client' across tests and repository files for consistency and to use the correct Prisma client package. This change improves maintainability and ensures the correct client is referenced throughout the codebase. * Undo removal of max-warnings=0 to get main to merge * Remove unit tests for e2e fixtures, provide new prisma mock * Mock @calcom/prisma in event manager * Mock @calcom/prisma in event manager * Add correct format even with --no-verify * Mock prisma in CalendarManager * Add mock for permission-check.service * Better injection in PrismaApiKeyRepository imports * More mock fixes :) * Fix listMembers.handler.test * Fix User import * Appropriately adjust all types to be imported as types, there were a lot of types imported as normal deps * Why was this a thing? * Strictly speaking; Not using prismock anymore * Ditched patch file for prismock * Fix output.service.ts platform type imports, need concrete for plainToClass * Better typing and tests for unlinkConnectedAccount.handler * Small type fix * Disable calendar cache tests as they are dependent on prismock * chore: bump platform lib * getRoutedUrl test remove of unused import * Extract select to external const on getEventTypesFromDB * Direct select of userSelect from selects/user * fix type error from merging 23653 * Fixed integration tests by removing hardcoded values that were possible due to mocking, but as its now directly hitting the db no longer * fix: vite config atoms prisma client type location * revert: example app prisma client * revert: example app prisma client * bump platform libs * fix: use class instead of type for DI of PlatformBookingsService * update platform libs * remove unused variable * chore: generate prisma client for api v2 * fix: api v2 e2e * fix: atoms e2e * fix: atoms e2e * fix: atoms e2e * fix: api v2 e2e * fix: tsconfig apiv2 enums * publish libraries * Simplify check for existence teamId --------- Signed-off-by: Omar López <zomars@me.com> Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com> Co-authored-by: supalarry <laurisskraucis@gmail.com> Co-authored-by: cal.com <morgan@cal.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Benny Joo <sldisek783@gmail.com>
1191 lines
34 KiB
TypeScript
1191 lines
34 KiB
TypeScript
import { z } from "zod";
|
|
|
|
import dayjs from "@calcom/dayjs";
|
|
import {
|
|
insightsRoutingServiceInputSchema,
|
|
insightsRoutingServicePaginatedInputSchema,
|
|
routingRepositoryBaseInputSchema,
|
|
routedToPerPeriodInputSchema,
|
|
routedToPerPeriodCsvInputSchema,
|
|
bookingRepositoryBaseInputSchema,
|
|
} from "@calcom/features/insights/server/raw-data.schema";
|
|
import { getInsightsBookingService } from "@calcom/lib/di/containers/InsightsBooking";
|
|
import { getInsightsRoutingService } from "@calcom/lib/di/containers/InsightsRouting";
|
|
import type { PrismaClient } from "@calcom/prisma";
|
|
import type { Prisma } from "@calcom/prisma/client";
|
|
import authedProcedure from "@calcom/trpc/server/procedures/authedProcedure";
|
|
import { router } from "@calcom/trpc/server/trpc";
|
|
|
|
import { TRPCError } from "@trpc/server";
|
|
|
|
import { hasInsightsPermission } from "./hasInsightsPermission";
|
|
import { getTimeView, getDateRanges, type GetDateRangesParams } from "./insightsDateUtils";
|
|
import { RoutingEventsInsights } from "./routing-events";
|
|
import { VirtualQueuesInsights } from "./virtual-queues";
|
|
|
|
const UserBelongsToTeamInput = z.object({
|
|
teamId: z.coerce.number().optional().nullable(),
|
|
isAll: z.boolean().optional(),
|
|
});
|
|
|
|
type BuildBaseWhereConditionCtxType = {
|
|
userIsOwnerAdminOfParentTeam: boolean;
|
|
userOrganizationId: number | null;
|
|
insightsDb: PrismaClient;
|
|
};
|
|
|
|
interface BuildBaseWhereConditionType {
|
|
teamId?: number | null;
|
|
eventTypeId?: number;
|
|
memberUserId?: number;
|
|
userId?: number;
|
|
isAll?: boolean;
|
|
ctx: BuildBaseWhereConditionCtxType;
|
|
}
|
|
|
|
export const buildBaseWhereCondition = async ({
|
|
teamId,
|
|
eventTypeId,
|
|
memberUserId,
|
|
userId,
|
|
isAll,
|
|
ctx,
|
|
}: BuildBaseWhereConditionType): Promise<{
|
|
whereCondition: Prisma.BookingTimeStatusDenormalizedWhereInput;
|
|
}> => {
|
|
const conditions: Prisma.BookingTimeStatusDenormalizedWhereInput[] = [];
|
|
|
|
// EventType Filter
|
|
if (eventTypeId) {
|
|
conditions.push({
|
|
OR: [{ eventTypeId }, { eventParentId: eventTypeId }],
|
|
});
|
|
}
|
|
|
|
// User/Member filter
|
|
if (memberUserId) {
|
|
conditions.push({ userId: memberUserId });
|
|
}
|
|
|
|
if (userId) {
|
|
conditions.push({
|
|
teamId: null,
|
|
userId: userId,
|
|
});
|
|
}
|
|
|
|
// organization-wide queries condition
|
|
if (isAll && ctx.userIsOwnerAdminOfParentTeam && ctx.userOrganizationId) {
|
|
const teamsFromOrg = await ctx.insightsDb.team.findMany({
|
|
where: {
|
|
parentId: ctx.userOrganizationId,
|
|
},
|
|
select: {
|
|
id: true,
|
|
},
|
|
});
|
|
|
|
const teamIds = [ctx.userOrganizationId, ...teamsFromOrg.map((t) => t.id)];
|
|
const usersFromOrg =
|
|
teamsFromOrg.length > 0
|
|
? await ctx.insightsDb.membership.findMany({
|
|
where: {
|
|
team: {
|
|
id: {
|
|
in: teamIds,
|
|
},
|
|
},
|
|
accepted: true,
|
|
},
|
|
select: {
|
|
userId: true,
|
|
},
|
|
})
|
|
: [];
|
|
|
|
conditions.push({
|
|
OR: [
|
|
{
|
|
teamId: {
|
|
in: teamIds,
|
|
},
|
|
isTeamBooking: true,
|
|
},
|
|
...(usersFromOrg.length > 0
|
|
? [
|
|
{
|
|
userId: {
|
|
in: usersFromOrg.map((u) => u.userId),
|
|
},
|
|
isTeamBooking: false,
|
|
},
|
|
]
|
|
: []),
|
|
],
|
|
});
|
|
}
|
|
|
|
// Team-specific queries condition
|
|
if (!isAll && teamId) {
|
|
const usersFromTeam = await ctx.insightsDb.membership.findMany({
|
|
where: {
|
|
teamId: teamId,
|
|
accepted: true,
|
|
},
|
|
select: {
|
|
userId: true,
|
|
},
|
|
});
|
|
const userIdsFromTeam = usersFromTeam.map((u) => u.userId);
|
|
|
|
conditions.push({
|
|
OR: [
|
|
{
|
|
teamId,
|
|
isTeamBooking: true,
|
|
},
|
|
{
|
|
userId: {
|
|
in: userIdsFromTeam,
|
|
},
|
|
isTeamBooking: false,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
let whereCondition: Prisma.BookingTimeStatusDenormalizedWhereInput = {};
|
|
|
|
if (conditions.length === 1) {
|
|
whereCondition = conditions[0];
|
|
} else if (conditions.length > 1) {
|
|
whereCondition = { AND: conditions };
|
|
}
|
|
|
|
return {
|
|
whereCondition:
|
|
Object.keys(whereCondition).length === 0
|
|
? { id: -1 } // Ensure no data is returned for invalid parameters
|
|
: whereCondition,
|
|
};
|
|
};
|
|
|
|
const userBelongsToTeamProcedure = authedProcedure.use(async ({ ctx, next, getRawInput }) => {
|
|
const parse = UserBelongsToTeamInput.safeParse(await getRawInput());
|
|
if (!parse.success) {
|
|
throw new TRPCError({ code: "BAD_REQUEST" });
|
|
}
|
|
|
|
// If teamId is provided, check if user belongs to team
|
|
// If teamId is not provided, check if user belongs to any team
|
|
|
|
const membershipWhereConditional: Prisma.MembershipWhereInput = {
|
|
userId: ctx.user.id,
|
|
accepted: true,
|
|
};
|
|
|
|
if (parse.data.teamId) {
|
|
membershipWhereConditional["teamId"] = parse.data.teamId;
|
|
}
|
|
|
|
const membership = await ctx.insightsDb.membership.findFirst({
|
|
where: membershipWhereConditional,
|
|
});
|
|
|
|
let isOwnerAdminOfParentTeam = false;
|
|
|
|
// Probably we couldn't find a membership because the user is not a direct member of the team
|
|
// So that would mean ctx.user.organization is present
|
|
if ((parse.data.isAll && ctx.user.organizationId) || (!membership && ctx.user.organizationId)) {
|
|
//Look for membership type in organizationId
|
|
if (!membership && ctx.user.organizationId && parse.data.teamId) {
|
|
const isChildTeamOfOrg = await ctx.insightsDb.team.findFirst({
|
|
where: {
|
|
id: parse.data.teamId,
|
|
parentId: ctx.user.organizationId,
|
|
},
|
|
});
|
|
if (!isChildTeamOfOrg) {
|
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
}
|
|
}
|
|
|
|
const membershipOrg = await ctx.insightsDb.membership.findFirst({
|
|
where: {
|
|
userId: ctx.user.id,
|
|
teamId: ctx.user.organizationId,
|
|
accepted: true,
|
|
role: {
|
|
in: ["OWNER", "ADMIN"],
|
|
},
|
|
},
|
|
});
|
|
if (!membershipOrg) {
|
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
}
|
|
isOwnerAdminOfParentTeam = true;
|
|
}
|
|
|
|
return next({
|
|
ctx: {
|
|
user: {
|
|
...ctx.user,
|
|
isOwnerAdminOfParentTeam,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
const insightsPbacProcedure = userBelongsToTeamProcedure.use(async ({ ctx, next }) => {
|
|
const hasPermission = await hasInsightsPermission({
|
|
userId: ctx.user.id,
|
|
organizationId: ctx.user.organizationId,
|
|
});
|
|
|
|
if (!hasPermission) {
|
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
}
|
|
|
|
return next({
|
|
ctx,
|
|
});
|
|
});
|
|
|
|
const userSelect = {
|
|
id: true,
|
|
name: true,
|
|
email: true,
|
|
username: true,
|
|
avatarUrl: true,
|
|
};
|
|
|
|
const emptyResponseBookingKPIStats = {
|
|
empty: true,
|
|
created: {
|
|
count: 0,
|
|
deltaPrevious: 0,
|
|
},
|
|
completed: {
|
|
count: 0,
|
|
deltaPrevious: 0,
|
|
},
|
|
rescheduled: {
|
|
count: 0,
|
|
deltaPrevious: 0,
|
|
},
|
|
cancelled: {
|
|
count: 0,
|
|
deltaPrevious: 0,
|
|
},
|
|
rating: {
|
|
count: 0,
|
|
deltaPrevious: 0,
|
|
},
|
|
no_show: {
|
|
count: 0,
|
|
deltaPrevious: 0,
|
|
},
|
|
no_show_guest: {
|
|
count: 0,
|
|
deltaPrevious: 0,
|
|
},
|
|
csat: {
|
|
count: 0,
|
|
deltaPrevious: 0,
|
|
},
|
|
previousRange: {
|
|
startDate: dayjs().toISOString(),
|
|
endDate: dayjs().toISOString(),
|
|
},
|
|
};
|
|
|
|
export interface IResultTeamList {
|
|
id: number;
|
|
slug: string | null;
|
|
name: string | null;
|
|
logoUrl: string | null;
|
|
userId?: number;
|
|
isOrg?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Helper function to create InsightsBookingService with standardized parameters
|
|
*/
|
|
function createInsightsBookingService(
|
|
ctx: { user: { id: number; organizationId: number | null } },
|
|
input: z.infer<typeof bookingRepositoryBaseInputSchema>,
|
|
dateTarget: "createdAt" | "startTime" = "createdAt"
|
|
) {
|
|
const { scope, selectedTeamId, startDate, endDate, columnFilters } = input;
|
|
return getInsightsBookingService({
|
|
options: {
|
|
scope,
|
|
userId: ctx.user.id,
|
|
orgId: ctx.user.organizationId ?? 0,
|
|
...(selectedTeamId && { teamId: selectedTeamId }),
|
|
},
|
|
filters: {
|
|
...(columnFilters && { columnFilters }),
|
|
dateRange: {
|
|
target: dateTarget,
|
|
startDate,
|
|
endDate,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
function createInsightsRoutingService(
|
|
ctx: { insightsDb: PrismaClient; user: { id: number; organizationId: number | null } },
|
|
input: z.infer<typeof routingRepositoryBaseInputSchema>
|
|
) {
|
|
return getInsightsRoutingService({
|
|
options: {
|
|
scope: input.scope,
|
|
teamId: input.selectedTeamId,
|
|
userId: ctx.user.id,
|
|
orgId: ctx.user.organizationId,
|
|
},
|
|
filters: {
|
|
startDate: input.startDate,
|
|
endDate: input.endDate,
|
|
columnFilters: input.columnFilters,
|
|
},
|
|
});
|
|
}
|
|
|
|
export const insightsRouter = router({
|
|
bookingKPIStats: insightsPbacProcedure
|
|
.input(bookingRepositoryBaseInputSchema)
|
|
.query(async ({ ctx, input }) => {
|
|
const currentPeriodService = createInsightsBookingService(ctx, input);
|
|
|
|
// Get current period stats
|
|
const currentStats = await currentPeriodService.getBookingStats();
|
|
|
|
// Calculate previous period dates and create service for previous period
|
|
const previousPeriodDates = currentPeriodService.calculatePreviousPeriodDates();
|
|
const previousPeriodService = createInsightsBookingService(ctx, {
|
|
...input,
|
|
startDate: previousPeriodDates.startDate,
|
|
endDate: previousPeriodDates.endDate,
|
|
});
|
|
|
|
// Get previous period stats
|
|
const previousStats = await previousPeriodService.getBookingStats();
|
|
|
|
// Helper function to calculate percentage change
|
|
const getPercentage = (current: number, previous: number): number => {
|
|
if (previous === 0) return current > 0 ? 100 : 0;
|
|
return ((current - previous) / previous) * 100;
|
|
};
|
|
|
|
// Calculate percentages and CSAT
|
|
const currentCSAT =
|
|
currentStats.total_ratings > 0
|
|
? (currentStats.ratings_above_3 / currentStats.total_ratings) * 100
|
|
: 0;
|
|
const previousCSAT =
|
|
previousStats.total_ratings > 0
|
|
? (previousStats.ratings_above_3 / previousStats.total_ratings) * 100
|
|
: 0;
|
|
|
|
const currentRating = currentStats.avg_rating ? parseFloat(currentStats.avg_rating.toFixed(1)) : 0;
|
|
const previousRating = previousStats.avg_rating ? parseFloat(previousStats.avg_rating.toFixed(1)) : 0;
|
|
|
|
// Check if all metrics are zero for empty state
|
|
const isEmpty =
|
|
currentStats.total_bookings === 0 &&
|
|
currentStats.completed_bookings === 0 &&
|
|
currentStats.rescheduled_bookings === 0 &&
|
|
currentStats.cancelled_bookings === 0 &&
|
|
currentStats.no_show_host_bookings === 0 &&
|
|
currentStats.no_show_guests === 0 &&
|
|
currentRating === 0;
|
|
|
|
if (isEmpty) {
|
|
return emptyResponseBookingKPIStats;
|
|
}
|
|
|
|
return {
|
|
empty: false,
|
|
created: {
|
|
count: currentStats.total_bookings,
|
|
deltaPrevious: getPercentage(currentStats.total_bookings, previousStats.total_bookings),
|
|
},
|
|
completed: {
|
|
count: currentStats.completed_bookings,
|
|
deltaPrevious: getPercentage(
|
|
currentStats.total_bookings - currentStats.cancelled_bookings - currentStats.rescheduled_bookings,
|
|
previousStats.total_bookings -
|
|
previousStats.cancelled_bookings -
|
|
previousStats.rescheduled_bookings
|
|
),
|
|
},
|
|
rescheduled: {
|
|
count: currentStats.rescheduled_bookings,
|
|
deltaPrevious: getPercentage(currentStats.rescheduled_bookings, previousStats.rescheduled_bookings),
|
|
},
|
|
cancelled: {
|
|
count: currentStats.cancelled_bookings,
|
|
deltaPrevious: getPercentage(currentStats.cancelled_bookings, previousStats.cancelled_bookings),
|
|
},
|
|
no_show: {
|
|
count: currentStats.no_show_host_bookings,
|
|
deltaPrevious: getPercentage(
|
|
currentStats.no_show_host_bookings,
|
|
previousStats.no_show_host_bookings
|
|
),
|
|
},
|
|
no_show_guest: {
|
|
count: currentStats.no_show_guests,
|
|
deltaPrevious: getPercentage(currentStats.no_show_guests, previousStats.no_show_guests),
|
|
},
|
|
rating: {
|
|
count: currentRating,
|
|
deltaPrevious: getPercentage(currentRating, previousRating),
|
|
},
|
|
csat: {
|
|
count: currentCSAT,
|
|
deltaPrevious: getPercentage(currentCSAT, previousCSAT),
|
|
},
|
|
previousRange: {
|
|
startDate: previousPeriodDates.formattedStartDate,
|
|
endDate: previousPeriodDates.formattedEndDate,
|
|
},
|
|
};
|
|
}),
|
|
eventTrends: insightsPbacProcedure.input(bookingRepositoryBaseInputSchema).query(async ({ ctx, input }) => {
|
|
const { startDate, endDate, timeZone } = input;
|
|
|
|
// Calculate timeView and dateRanges
|
|
const timeView = getTimeView(startDate, endDate);
|
|
const dateRanges = getDateRanges({
|
|
startDate,
|
|
endDate,
|
|
timeView,
|
|
timeZone,
|
|
weekStart: ctx.user.weekStart,
|
|
});
|
|
|
|
const insightsBookingService = createInsightsBookingService(ctx, input);
|
|
try {
|
|
return await insightsBookingService.getEventTrendsStats({
|
|
timeZone,
|
|
dateRanges,
|
|
});
|
|
} catch (e) {
|
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
|
}
|
|
}),
|
|
popularEvents: insightsPbacProcedure
|
|
.input(bookingRepositoryBaseInputSchema)
|
|
.query(async ({ input, ctx }) => {
|
|
const insightsBookingService = createInsightsBookingService(ctx, input);
|
|
|
|
try {
|
|
return await insightsBookingService.getPopularEventsStats();
|
|
} catch (e) {
|
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
|
}
|
|
}),
|
|
averageEventDuration: insightsPbacProcedure
|
|
.input(bookingRepositoryBaseInputSchema)
|
|
.query(async ({ ctx, input }) => {
|
|
const { startDate, endDate, timeZone } = input;
|
|
|
|
const insightsBookingService = createInsightsBookingService(ctx, input);
|
|
|
|
try {
|
|
const timeView = getTimeView(startDate, endDate);
|
|
const dateRanges = getDateRanges({
|
|
startDate,
|
|
endDate,
|
|
timeView,
|
|
timeZone,
|
|
weekStart: ctx.user.weekStart as GetDateRangesParams["weekStart"],
|
|
});
|
|
|
|
if (!dateRanges.length) {
|
|
return [];
|
|
}
|
|
|
|
const startOfEndOf = timeView === "year" ? "year" : timeView === "month" ? "month" : "week";
|
|
|
|
const allBookings = await insightsBookingService.findAll({
|
|
select: {
|
|
eventLength: true,
|
|
createdAt: true,
|
|
},
|
|
});
|
|
|
|
const resultMap = new Map<string, { totalDuration: number; count: number }>();
|
|
|
|
// Initialize the map with all date ranges
|
|
for (const range of dateRanges) {
|
|
resultMap.set(dayjs(range.startDate).format("ll"), { totalDuration: 0, count: 0 });
|
|
}
|
|
|
|
for (const booking of allBookings) {
|
|
const periodStart = dayjs(booking.createdAt).startOf(startOfEndOf).format("ll");
|
|
if (resultMap.has(periodStart)) {
|
|
const current = resultMap.get(periodStart);
|
|
if (!current) continue;
|
|
current.totalDuration += booking.eventLength || 0;
|
|
current.count += 1;
|
|
}
|
|
}
|
|
|
|
const result = Array.from(resultMap.entries()).map(([date, { totalDuration, count }]) => ({
|
|
Date: date,
|
|
Average: count > 0 ? totalDuration / count : 0,
|
|
}));
|
|
|
|
return result;
|
|
} catch (e) {
|
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
|
}
|
|
}),
|
|
membersWithMostCancelledBookings: insightsPbacProcedure
|
|
.input(bookingRepositoryBaseInputSchema)
|
|
.query(async ({ input, ctx }) => {
|
|
const insightsBookingService = createInsightsBookingService(ctx, input);
|
|
|
|
try {
|
|
return await insightsBookingService.getMembersStatsWithCount("cancelled", "DESC");
|
|
} catch (e) {
|
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
|
}
|
|
}),
|
|
membersWithMostCompletedBookings: insightsPbacProcedure
|
|
.input(bookingRepositoryBaseInputSchema)
|
|
.query(async ({ input, ctx }) => {
|
|
const insightsBookingService = createInsightsBookingService(ctx, input, "startTime");
|
|
|
|
try {
|
|
return await insightsBookingService.getMembersStatsWithCount("accepted", "DESC");
|
|
} catch (e) {
|
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
|
}
|
|
}),
|
|
membersWithLeastCompletedBookings: insightsPbacProcedure
|
|
.input(bookingRepositoryBaseInputSchema)
|
|
.query(async ({ input, ctx }) => {
|
|
const insightsBookingService = createInsightsBookingService(ctx, input, "startTime");
|
|
|
|
try {
|
|
return await insightsBookingService.getMembersStatsWithCount("accepted", "ASC");
|
|
} catch (e) {
|
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
|
}
|
|
}),
|
|
membersWithMostBookings: insightsPbacProcedure
|
|
.input(bookingRepositoryBaseInputSchema)
|
|
.query(async ({ input, ctx }) => {
|
|
const insightsBookingService = createInsightsBookingService(ctx, input);
|
|
|
|
try {
|
|
return await insightsBookingService.getMembersStatsWithCount("all", "DESC");
|
|
} catch (e) {
|
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
|
}
|
|
}),
|
|
membersWithLeastBookings: insightsPbacProcedure
|
|
.input(bookingRepositoryBaseInputSchema)
|
|
.query(async ({ input, ctx }) => {
|
|
const insightsBookingService = createInsightsBookingService(ctx, input);
|
|
|
|
try {
|
|
return await insightsBookingService.getMembersStatsWithCount("all", "ASC");
|
|
} catch (e) {
|
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
|
}
|
|
}),
|
|
teamListForUser: authedProcedure.query(async ({ ctx }) => {
|
|
const user = ctx.user;
|
|
|
|
// Fetch user data
|
|
const userData = await ctx.insightsDb.user.findUnique({
|
|
where: {
|
|
id: user.id,
|
|
},
|
|
select: userSelect,
|
|
});
|
|
|
|
if (!userData) {
|
|
return [];
|
|
}
|
|
|
|
// Validate if user belongs to org as admin/owner
|
|
if (user.organizationId && user.organization.isOrgAdmin) {
|
|
const teamsAndOrg = await ctx.insightsDb.team.findMany({
|
|
where: {
|
|
OR: [{ parentId: user.organizationId }, { id: user.organizationId }],
|
|
},
|
|
select: {
|
|
id: true,
|
|
slug: true,
|
|
name: true,
|
|
logoUrl: true,
|
|
},
|
|
});
|
|
const teamsFromOrg = teamsAndOrg.filter((team) => team.id !== user.organizationId);
|
|
const orgTeam = teamsAndOrg.find((team) => team.id === user.organizationId);
|
|
if (!orgTeam) {
|
|
return [];
|
|
}
|
|
|
|
const result: IResultTeamList[] = [
|
|
{
|
|
...orgTeam,
|
|
isOrg: true,
|
|
},
|
|
...teamsFromOrg,
|
|
];
|
|
|
|
return result;
|
|
}
|
|
|
|
// Look if user it's admin/owner in multiple teams
|
|
const belongsToTeams = await ctx.insightsDb.membership.findMany({
|
|
where: {
|
|
team: {
|
|
slug: { not: null },
|
|
},
|
|
accepted: true,
|
|
userId: user.id,
|
|
OR: [
|
|
{
|
|
role: "ADMIN",
|
|
},
|
|
{
|
|
role: "OWNER",
|
|
},
|
|
],
|
|
},
|
|
include: {
|
|
team: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
logoUrl: true,
|
|
slug: true,
|
|
metadata: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (belongsToTeams.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const result: IResultTeamList[] = belongsToTeams.map((membership) => {
|
|
return { ...membership.team };
|
|
});
|
|
|
|
return result;
|
|
}),
|
|
userList: authedProcedure
|
|
.input(
|
|
z.object({
|
|
teamId: z.number().optional(),
|
|
isAll: z.boolean().nullable(),
|
|
})
|
|
)
|
|
.query(async ({ ctx, input }) => {
|
|
const user = ctx.user;
|
|
const { teamId, isAll } = input;
|
|
|
|
if (isAll && user.organizationId && user.organization.isOrgAdmin) {
|
|
const usersInTeam = await ctx.insightsDb.membership.findMany({
|
|
where: {
|
|
team: {
|
|
parentId: user.organizationId,
|
|
},
|
|
},
|
|
include: {
|
|
user: {
|
|
select: userSelect,
|
|
},
|
|
},
|
|
distinct: ["userId"],
|
|
});
|
|
return usersInTeam.map((membership) => membership.user);
|
|
}
|
|
|
|
if (!teamId) {
|
|
return [];
|
|
}
|
|
|
|
const membership = await ctx.insightsDb.membership.findFirst({
|
|
where: {
|
|
userId: user.id,
|
|
teamId,
|
|
accepted: true,
|
|
},
|
|
include: {
|
|
user: {
|
|
select: userSelect,
|
|
},
|
|
},
|
|
});
|
|
if (!membership) {
|
|
return [];
|
|
}
|
|
const isMember = membership && membership.role === "MEMBER";
|
|
// If user is not admin, return himself only
|
|
if (isMember) {
|
|
return [membership.user];
|
|
}
|
|
|
|
const usersInTeam = await ctx.insightsDb.membership.findMany({
|
|
where: {
|
|
teamId,
|
|
accepted: true,
|
|
},
|
|
include: {
|
|
user: {
|
|
select: userSelect,
|
|
},
|
|
},
|
|
distinct: ["userId"],
|
|
});
|
|
|
|
return usersInTeam.map((membership) => membership.user);
|
|
}),
|
|
eventTypeList: insightsPbacProcedure
|
|
.input(
|
|
z.object({
|
|
teamId: z.coerce.number().nullish(),
|
|
userId: z.coerce.number().nullish(),
|
|
isAll: z.boolean().optional(),
|
|
})
|
|
)
|
|
.query(async ({ ctx, input }) => {
|
|
const { prisma, user } = ctx;
|
|
const { teamId, userId, isAll } = input;
|
|
|
|
const eventTypeList = await getEventTypeList({
|
|
prisma,
|
|
teamId,
|
|
userId,
|
|
isAll,
|
|
user: {
|
|
id: user.id,
|
|
organizationId: user.organizationId,
|
|
isOwnerAdminOfParentTeam: user.isOwnerAdminOfParentTeam,
|
|
},
|
|
});
|
|
|
|
return eventTypeList;
|
|
}),
|
|
recentRatings: insightsPbacProcedure
|
|
.input(bookingRepositoryBaseInputSchema)
|
|
.query(async ({ ctx, input }) => {
|
|
const insightsBookingService = createInsightsBookingService(ctx, input);
|
|
return await insightsBookingService.getRecentRatingsStats();
|
|
}),
|
|
membersWithMostNoShow: insightsPbacProcedure
|
|
.input(bookingRepositoryBaseInputSchema)
|
|
.query(async ({ input, ctx }) => {
|
|
const insightsBookingService = createInsightsBookingService(ctx, input);
|
|
|
|
try {
|
|
return await insightsBookingService.getMembersStatsWithCount("noShow", "DESC");
|
|
} catch (e) {
|
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
|
}
|
|
}),
|
|
membersWithHighestRatings: insightsPbacProcedure
|
|
.input(bookingRepositoryBaseInputSchema)
|
|
.query(async ({ ctx, input }) => {
|
|
const insightsBookingService = createInsightsBookingService(ctx, input);
|
|
return await insightsBookingService.getMembersRatingStats("DESC");
|
|
}),
|
|
membersWithLowestRatings: insightsPbacProcedure
|
|
.input(bookingRepositoryBaseInputSchema)
|
|
.query(async ({ ctx, input }) => {
|
|
const insightsBookingService = createInsightsBookingService(ctx, input);
|
|
return await insightsBookingService.getMembersRatingStats("ASC");
|
|
}),
|
|
rawData: insightsPbacProcedure
|
|
.input(
|
|
bookingRepositoryBaseInputSchema.extend({
|
|
limit: z.number().max(100).optional(),
|
|
offset: z.number().optional(),
|
|
})
|
|
)
|
|
.query(async ({ ctx, input }) => {
|
|
const { limit, offset } = input;
|
|
|
|
const insightsBookingService = createInsightsBookingService(ctx, input);
|
|
|
|
try {
|
|
return await insightsBookingService.getCsvData({
|
|
limit: limit ?? 100,
|
|
offset: offset ?? 0,
|
|
});
|
|
} catch (e) {
|
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
|
}
|
|
}),
|
|
|
|
getRoutingFormsForFilters: insightsPbacProcedure
|
|
.input(z.object({ userId: z.number().optional(), teamId: z.number().optional(), isAll: z.boolean() }))
|
|
.query(async ({ ctx, input }) => {
|
|
const { teamId, isAll } = input;
|
|
return await RoutingEventsInsights.getRoutingFormsForFilters({
|
|
userId: ctx.user.id,
|
|
teamId,
|
|
isAll,
|
|
organizationId: ctx.user.organizationId ?? undefined,
|
|
});
|
|
}),
|
|
routingFormsByStatus: insightsPbacProcedure
|
|
.input(insightsRoutingServiceInputSchema)
|
|
.query(async ({ ctx, input }) => {
|
|
const insightsRoutingService = createInsightsRoutingService(ctx, input);
|
|
return await insightsRoutingService.getRoutingFormStats();
|
|
}),
|
|
routingFormResponses: insightsPbacProcedure
|
|
.input(insightsRoutingServicePaginatedInputSchema)
|
|
.query(async ({ ctx, input }) => {
|
|
const insightsRoutingService = createInsightsRoutingService(ctx, input);
|
|
return await insightsRoutingService.getTableData({
|
|
sorting: input.sorting,
|
|
limit: input.limit,
|
|
offset: input.offset,
|
|
});
|
|
}),
|
|
routingFormResponsesForDownload: insightsPbacProcedure
|
|
.input(insightsRoutingServicePaginatedInputSchema)
|
|
.query(async ({ ctx, input }) => {
|
|
const headersPromise = RoutingEventsInsights.getRoutingFormHeaders({
|
|
userId: ctx.user.id,
|
|
teamId: input.selectedTeamId,
|
|
isAll: input.scope === "org",
|
|
organizationId: ctx.user.organizationId,
|
|
routingFormId: (input.columnFilters || []).find((filter) => filter.id === "formId")?.value?.data as
|
|
| string
|
|
| undefined,
|
|
});
|
|
|
|
const insightsRoutingService = createInsightsRoutingService(ctx, input);
|
|
const dataPromise = insightsRoutingService.getTableData({
|
|
sorting: input.sorting,
|
|
limit: input.limit,
|
|
offset: input.offset,
|
|
});
|
|
|
|
return await RoutingEventsInsights.getRoutingFormPaginatedResponsesForDownload({
|
|
headersPromise,
|
|
dataPromise,
|
|
});
|
|
}),
|
|
getRoutingFormFieldOptions: insightsPbacProcedure
|
|
.input(
|
|
z.object({
|
|
userId: z.number().optional(),
|
|
teamId: z.number().optional(),
|
|
isAll: z.boolean(),
|
|
routingFormId: z.string().optional(),
|
|
})
|
|
)
|
|
.query(async ({ input, ctx }) => {
|
|
const options = await RoutingEventsInsights.getRoutingFormFieldOptions({
|
|
...input,
|
|
userId: ctx.user.id,
|
|
organizationId: ctx.user.organizationId ?? null,
|
|
});
|
|
return options;
|
|
}),
|
|
failedBookingsByField: insightsPbacProcedure
|
|
.input(insightsRoutingServiceInputSchema)
|
|
.query(async ({ ctx, input }) => {
|
|
const insightsRoutingService = createInsightsRoutingService(ctx, input);
|
|
try {
|
|
return await insightsRoutingService.getFailedBookingsByFieldData();
|
|
} catch (e) {
|
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
|
}
|
|
}),
|
|
routingFormResponsesHeaders: insightsPbacProcedure
|
|
.input(
|
|
z.object({
|
|
userId: z.number().optional(),
|
|
teamId: z.number().optional(),
|
|
isAll: z.boolean(),
|
|
routingFormId: z.string().optional(),
|
|
})
|
|
)
|
|
.query(async ({ ctx, input }) => {
|
|
const headers = await RoutingEventsInsights.getRoutingFormHeaders({
|
|
userId: ctx.user.id,
|
|
teamId: input.teamId ?? null,
|
|
isAll: input.isAll,
|
|
organizationId: ctx.user.organizationId ?? null,
|
|
routingFormId: input.routingFormId ?? null,
|
|
});
|
|
|
|
return headers || [];
|
|
}),
|
|
routedToPerPeriod: insightsPbacProcedure
|
|
.input(routedToPerPeriodInputSchema)
|
|
.query(async ({ ctx, input }) => {
|
|
const { period, limit, searchQuery, ...rest } = input;
|
|
|
|
try {
|
|
const insightsRoutingService = createInsightsRoutingService(ctx, rest);
|
|
return await insightsRoutingService.getRoutedToPerPeriodData({
|
|
period,
|
|
limit,
|
|
searchQuery,
|
|
});
|
|
} catch (e) {
|
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
|
}
|
|
}),
|
|
routedToPerPeriodCsv: insightsPbacProcedure
|
|
.input(routedToPerPeriodCsvInputSchema)
|
|
.query(async ({ ctx, input }) => {
|
|
const { period, searchQuery, ...rest } = input;
|
|
try {
|
|
const insightsRoutingService = createInsightsRoutingService(ctx, rest);
|
|
|
|
const csvData = await insightsRoutingService.getRoutedToPerPeriodCsvData({
|
|
period,
|
|
searchQuery,
|
|
});
|
|
|
|
const csvString = objectToCsv(csvData);
|
|
const downloadAs = `routed-to-${period}-${dayjs(rest.startDate).format("YYYY-MM-DD")}-${dayjs(
|
|
rest.endDate
|
|
).format("YYYY-MM-DD")}.csv`;
|
|
|
|
return { data: csvString, filename: downloadAs };
|
|
} catch (e) {
|
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
|
}
|
|
}),
|
|
getUserRelevantTeamRoutingForms: authedProcedure.query(async ({ ctx }) => {
|
|
try {
|
|
const routingForms = await VirtualQueuesInsights.getUserRelevantTeamRoutingForms({
|
|
userId: ctx.user.id,
|
|
});
|
|
|
|
return routingForms;
|
|
} catch (e) {
|
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
|
}
|
|
}),
|
|
getRoutingFunnelData: insightsPbacProcedure
|
|
.input(routingRepositoryBaseInputSchema)
|
|
.query(async ({ ctx, input }) => {
|
|
const timeView = getTimeView(input.startDate, input.endDate);
|
|
const dateRanges = getDateRanges({
|
|
startDate: input.startDate,
|
|
endDate: input.endDate,
|
|
timeZone: ctx.user.timeZone,
|
|
timeView,
|
|
weekStart: ctx.user.weekStart,
|
|
});
|
|
const insightsRoutingService = createInsightsRoutingService(ctx, input);
|
|
try {
|
|
return await insightsRoutingService.getRoutingFunnelData(dateRanges);
|
|
} catch (e) {
|
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
|
}
|
|
}),
|
|
bookingsByHourStats: insightsPbacProcedure
|
|
.input(bookingRepositoryBaseInputSchema)
|
|
.query(async ({ ctx, input }) => {
|
|
const { timeZone } = input;
|
|
const insightsBookingService = createInsightsBookingService(ctx, input, "startTime");
|
|
|
|
try {
|
|
return await insightsBookingService.getBookingsByHourStats({
|
|
timeZone,
|
|
});
|
|
} catch (e) {
|
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
|
}
|
|
}),
|
|
recentNoShowGuests: insightsPbacProcedure
|
|
.input(bookingRepositoryBaseInputSchema)
|
|
.query(async ({ ctx, input }) => {
|
|
const insightsBookingService = createInsightsBookingService(ctx, input, "startTime");
|
|
|
|
try {
|
|
return await insightsBookingService.getRecentNoShowGuests();
|
|
} catch (e) {
|
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
|
}
|
|
}),
|
|
});
|
|
|
|
export async function getEventTypeList({
|
|
prisma,
|
|
teamId,
|
|
userId,
|
|
isAll,
|
|
user,
|
|
}: {
|
|
prisma: PrismaClient;
|
|
teamId: number | null | undefined;
|
|
userId: number | null | undefined;
|
|
isAll: boolean | undefined;
|
|
user: {
|
|
id: number;
|
|
organizationId: number | null;
|
|
isOwnerAdminOfParentTeam: boolean;
|
|
};
|
|
}) {
|
|
if (!teamId && !userId && !isAll) {
|
|
return [];
|
|
}
|
|
|
|
const membershipWhereConditional: Prisma.MembershipWhereInput = {};
|
|
let childrenTeamIds: number[] = [];
|
|
|
|
if (userId && !teamId && !isAll) {
|
|
const eventTypeResult = await prisma.eventType.findMany({
|
|
select: {
|
|
id: true,
|
|
slug: true,
|
|
title: true,
|
|
teamId: true,
|
|
userId: true,
|
|
team: {
|
|
select: {
|
|
name: true,
|
|
},
|
|
},
|
|
},
|
|
where: {
|
|
userId: user.id,
|
|
teamId: null,
|
|
},
|
|
});
|
|
|
|
return eventTypeResult;
|
|
}
|
|
|
|
if (isAll && user.organizationId && user.isOwnerAdminOfParentTeam) {
|
|
const childTeams = await prisma.team.findMany({
|
|
where: {
|
|
parentId: user.organizationId,
|
|
},
|
|
select: {
|
|
id: true,
|
|
},
|
|
});
|
|
|
|
if (childTeams.length > 0) {
|
|
childrenTeamIds = childTeams.map((team) => team.id);
|
|
}
|
|
|
|
const eventTypeResult = await prisma.eventType.findMany({
|
|
select: {
|
|
id: true,
|
|
slug: true,
|
|
title: true,
|
|
teamId: true,
|
|
userId: true,
|
|
team: {
|
|
select: {
|
|
name: true,
|
|
},
|
|
},
|
|
},
|
|
where: {
|
|
OR: [
|
|
{
|
|
teamId: {
|
|
in: [user.organizationId, ...childrenTeamIds],
|
|
},
|
|
},
|
|
{
|
|
userId: user.id,
|
|
teamId: null,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
return eventTypeResult;
|
|
}
|
|
|
|
if (teamId && !isAll) {
|
|
membershipWhereConditional["teamId"] = teamId;
|
|
membershipWhereConditional["userId"] = user.id;
|
|
}
|
|
|
|
// I'm not using unique here since when userId comes from input we should look for every
|
|
// event type that user owns
|
|
const membership = await prisma.membership.findFirst({
|
|
where: membershipWhereConditional,
|
|
});
|
|
|
|
if (!membership && !user.isOwnerAdminOfParentTeam) {
|
|
throw new Error("User is not part of a team/org");
|
|
}
|
|
|
|
const eventTypeWhereConditional: Prisma.EventTypeWhereInput = {};
|
|
|
|
if (teamId && !isAll) {
|
|
eventTypeWhereConditional["teamId"] = teamId;
|
|
}
|
|
|
|
let isMember = membership?.role === "MEMBER";
|
|
if (user.isOwnerAdminOfParentTeam) {
|
|
isMember = false;
|
|
}
|
|
|
|
if (isMember) {
|
|
eventTypeWhereConditional["OR"] = [{ userId: user.id }, { users: { some: { id: user.id } } }];
|
|
// @TODO this is not working as expected
|
|
// hosts: { some: { id: user.id } },
|
|
}
|
|
|
|
const eventTypeResult = await prisma.eventType.findMany({
|
|
select: {
|
|
id: true,
|
|
slug: true,
|
|
title: true,
|
|
teamId: true,
|
|
userId: true,
|
|
team: {
|
|
select: {
|
|
name: true,
|
|
},
|
|
},
|
|
},
|
|
where: eventTypeWhereConditional,
|
|
});
|
|
|
|
return eventTypeResult;
|
|
}
|
|
|
|
function objectToCsv(data: Record<string, string>[]) {
|
|
if (!data.length) return "";
|
|
|
|
const headers = Object.keys(data[0]);
|
|
const csvRows = [
|
|
headers.join(","),
|
|
...data.map((row) =>
|
|
headers
|
|
.map((header) => {
|
|
const value = row[header]?.toString() || "";
|
|
// Escape quotes and wrap in quotes if contains comma or newline
|
|
return value.includes(",") || value.includes("\n") || value.includes('"')
|
|
? `"${value.replace(/"/g, '""')}"` // escape double quotes
|
|
: value;
|
|
})
|
|
.join(",")
|
|
),
|
|
];
|
|
|
|
return csvRows.join("\n");
|
|
}
|