Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc1d553b74 | ||
|
|
b8c2fdac44 | ||
|
|
e24c40f518 | ||
|
|
c5cefc3e61 | ||
|
|
2c57ad0288 | ||
|
|
af9bf14d9e | ||
|
|
b1a0cdc8e2 | ||
|
|
7fef3e7b85 | ||
|
|
384deab0ce | ||
|
|
22da681f8c | ||
|
|
f4f4afad39 | ||
|
|
1c05952077 |
No files matched your search
@@ -65,7 +65,6 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/yarn-playwright-install
|
|
||||||
- uses: ./.github/actions/cache-db
|
- uses: ./.github/actions/cache-db
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
working-directory: apps/api/v2
|
working-directory: apps/api/v2
|
||||||
|
|||||||
@@ -0,0 +1,410 @@
|
|||||||
|
import prismock from "../../../../tests/libs/__mocks__/prisma";
|
||||||
|
|
||||||
|
import { v4 } from "uuid";
|
||||||
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||||
|
|
||||||
|
import { getCRMContactOwnerForRRLeadSkip } from "@calcom/app-store/_utils/CRMRoundRobinSkip";
|
||||||
|
import bookingFormHandlers from "@calcom/app-store/routing-forms/appBookingFormHandler";
|
||||||
|
import { ROUTING_FORM_RESPONSE_ID_QUERY_STRING } from "@calcom/app-store/routing-forms/lib/constants";
|
||||||
|
import { RouteActionType } from "@calcom/app-store/routing-forms/zod";
|
||||||
|
import { SchedulingType } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
|
import { getTeamMemberEmailForResponseOrContactUsingUrlQuery } from "../getTeamMemberEmailFromCrm";
|
||||||
|
|
||||||
|
vi.mock("@calcom/app-store/routing-forms/appBookingFormHandler", () => ({
|
||||||
|
default: {
|
||||||
|
salesforce: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@calcom/app-store/_utils/CRMRoundRobinSkip", () => ({
|
||||||
|
getCRMContactOwnerForRRLeadSkip: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function mockGetCRMContactOwnerForRRLeadSkip({
|
||||||
|
bookerEmail,
|
||||||
|
teamMemberEmail,
|
||||||
|
}: {
|
||||||
|
bookerEmail: string;
|
||||||
|
teamMemberEmail: string;
|
||||||
|
}) {
|
||||||
|
vi.mocked(getCRMContactOwnerForRRLeadSkip).mockImplementation((_bookerEmail, _eventMetadata) => {
|
||||||
|
if (_bookerEmail === bookerEmail) {
|
||||||
|
return Promise.resolve(teamMemberEmail);
|
||||||
|
}
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockBookingFormHandler({
|
||||||
|
bookerEmail,
|
||||||
|
teamMemberEmail,
|
||||||
|
}: {
|
||||||
|
bookerEmail: string;
|
||||||
|
teamMemberEmail: string;
|
||||||
|
}) {
|
||||||
|
vi.mocked(bookingFormHandlers.salesforce).mockImplementation(
|
||||||
|
(_bookerEmail, _attributeRoutingConfig, _eventTypeId) => {
|
||||||
|
if (_bookerEmail === bookerEmail) {
|
||||||
|
return Promise.resolve({ email: teamMemberEmail });
|
||||||
|
}
|
||||||
|
return Promise.resolve({ email: null });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createRoutingFormWithResponse({
|
||||||
|
formData,
|
||||||
|
}: {
|
||||||
|
formData: {
|
||||||
|
routes: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
queryValue?: any;
|
||||||
|
action: {
|
||||||
|
eventTypeId: number;
|
||||||
|
type: RouteActionType;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
attributeRoutingConfig: {
|
||||||
|
skipContactOwner: boolean;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
const user = await prismock.user.create({
|
||||||
|
data: {
|
||||||
|
name: "Routing Form User",
|
||||||
|
username: "routing-form-user",
|
||||||
|
email: "routing-form-user@example.com",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = await prismock.app_RoutingForms_Form.create({
|
||||||
|
data: {
|
||||||
|
...formData,
|
||||||
|
name: "Test Form",
|
||||||
|
routes: formData.routes.map((route) => ({
|
||||||
|
queryValue: {
|
||||||
|
id: v4(),
|
||||||
|
type: "group",
|
||||||
|
children1: [],
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
id: v4(),
|
||||||
|
...route,
|
||||||
|
})),
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseRecord = await prismock.app_RoutingForms_FormResponse.create({
|
||||||
|
data: {
|
||||||
|
response: {},
|
||||||
|
form: {
|
||||||
|
connect: {
|
||||||
|
id: form.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { form, responseRecord };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createHostForEvent({
|
||||||
|
eventTypeData,
|
||||||
|
userData,
|
||||||
|
}: {
|
||||||
|
eventTypeData: { id: number } & Partial<{
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
length: number;
|
||||||
|
}>;
|
||||||
|
userData: {
|
||||||
|
email: string;
|
||||||
|
} & Partial<{
|
||||||
|
name: string;
|
||||||
|
username: string;
|
||||||
|
}>;
|
||||||
|
}) {
|
||||||
|
const user = await prismock.user.create({
|
||||||
|
data: {
|
||||||
|
name: "Test User",
|
||||||
|
username: "testuser",
|
||||||
|
...userData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prismock.eventType.create({
|
||||||
|
data: {
|
||||||
|
title: "Title",
|
||||||
|
slug: "slug",
|
||||||
|
length: 30,
|
||||||
|
...eventTypeData,
|
||||||
|
hosts: {
|
||||||
|
create: {
|
||||||
|
userId: user.id,
|
||||||
|
isFixed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("getTeamMemberEmailForResponseOrContactUsingUrlQuery", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockEventData = {
|
||||||
|
id: 1,
|
||||||
|
isInstantEvent: false,
|
||||||
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||||
|
metadata: null,
|
||||||
|
length: 30,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should return null when email is not provided in query", async () => {
|
||||||
|
const result = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||||
|
query: {},
|
||||||
|
eventData: mockEventData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null when scheduling type is not ROUND_ROBIN", async () => {
|
||||||
|
const bookerEmail = "booker@example.com";
|
||||||
|
const teamMemberEmail = "teamMember@example.com";
|
||||||
|
mockGetCRMContactOwnerForRRLeadSkip({ bookerEmail, teamMemberEmail });
|
||||||
|
await createHostForEvent({
|
||||||
|
eventTypeData: {
|
||||||
|
id: mockEventData.id,
|
||||||
|
},
|
||||||
|
userData: {
|
||||||
|
email: teamMemberEmail,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||||
|
query: { email: bookerEmail },
|
||||||
|
eventData: { ...mockEventData, schedulingType: SchedulingType.COLLECTIVE },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return CRM owner email when valid", async () => {
|
||||||
|
const ownerEmail = "owner@example.com";
|
||||||
|
const bookerEmail = "booker@example.com";
|
||||||
|
mockGetCRMContactOwnerForRRLeadSkip({ bookerEmail, teamMemberEmail: ownerEmail });
|
||||||
|
await createHostForEvent({
|
||||||
|
eventTypeData: {
|
||||||
|
id: mockEventData.id,
|
||||||
|
},
|
||||||
|
userData: {
|
||||||
|
email: ownerEmail,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||||
|
query: { email: "booker@example.com" },
|
||||||
|
eventData: mockEventData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe(ownerEmail);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null when CRM owner is not found", async () => {
|
||||||
|
const bookerEmail = "booker@example.com";
|
||||||
|
const teamMemberEmail = "teamMember@example.com";
|
||||||
|
mockGetCRMContactOwnerForRRLeadSkip({ bookerEmail, teamMemberEmail });
|
||||||
|
|
||||||
|
const result = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||||
|
query: { email: bookerEmail },
|
||||||
|
eventData: mockEventData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null when CRM owner is not part of event type", async () => {
|
||||||
|
const ownerEmail = "owner@example.com";
|
||||||
|
const bookerEmail = "booker@example.com";
|
||||||
|
mockGetCRMContactOwnerForRRLeadSkip({ bookerEmail, teamMemberEmail: ownerEmail });
|
||||||
|
|
||||||
|
const result = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||||
|
query: { email: bookerEmail },
|
||||||
|
eventData: mockEventData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Booking form handler", () => {
|
||||||
|
it("should return teamMember email through booking form handler when cal.routingFormResponseId and cal.salesforce.xxxx=true is provided", async () => {
|
||||||
|
const bookerEmail = "booker@example.com";
|
||||||
|
const teamMemberEmail = "owner@example.com";
|
||||||
|
mockBookingFormHandler({ bookerEmail, teamMemberEmail });
|
||||||
|
await createHostForEvent({
|
||||||
|
eventTypeData: {
|
||||||
|
id: mockEventData.id,
|
||||||
|
},
|
||||||
|
userData: {
|
||||||
|
email: teamMemberEmail,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { responseRecord } = await createRoutingFormWithResponse({
|
||||||
|
formData: {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
action: {
|
||||||
|
eventTypeId: 1,
|
||||||
|
type: RouteActionType.EventTypeRedirectUrl,
|
||||||
|
value: "/team/sales",
|
||||||
|
},
|
||||||
|
attributeRoutingConfig: { skipContactOwner: false },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||||
|
query: {
|
||||||
|
email: bookerEmail,
|
||||||
|
[ROUTING_FORM_RESPONSE_ID_QUERY_STRING]: responseRecord.id.toString(),
|
||||||
|
"cal.salesforce.xxx": "true",
|
||||||
|
},
|
||||||
|
eventData: mockEventData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe(teamMemberEmail);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null when skipContactOwner is true even when cal.routingFormResponseId and cal.salesforce.xxxx=true is provided", async () => {
|
||||||
|
const bookerEmail = "booker@example.com";
|
||||||
|
const teamMemberEmail = "owner@example.com";
|
||||||
|
mockBookingFormHandler({ bookerEmail, teamMemberEmail });
|
||||||
|
await createHostForEvent({
|
||||||
|
eventTypeData: {
|
||||||
|
id: mockEventData.id,
|
||||||
|
},
|
||||||
|
userData: {
|
||||||
|
email: teamMemberEmail,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { responseRecord } = await createRoutingFormWithResponse({
|
||||||
|
formData: {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
action: {
|
||||||
|
eventTypeId: 1,
|
||||||
|
type: RouteActionType.EventTypeRedirectUrl,
|
||||||
|
value: "/team/sales",
|
||||||
|
},
|
||||||
|
attributeRoutingConfig: { skipContactOwner: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||||
|
query: {
|
||||||
|
email: bookerEmail,
|
||||||
|
[ROUTING_FORM_RESPONSE_ID_QUERY_STRING]: responseRecord.id.toString(),
|
||||||
|
"cal.salesforce.xxx": "true",
|
||||||
|
},
|
||||||
|
eventData: mockEventData,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Because skipContactOwner is true, the booking form handler should return null
|
||||||
|
expect(result).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null when when cal.routingFormResponseId and cal.salesforce.xxxx=true is provided but the returned email isn't an event member", async () => {
|
||||||
|
const bookerEmail = "booker@example.com";
|
||||||
|
const teamMemberEmail = "owner@example.com";
|
||||||
|
mockBookingFormHandler({ bookerEmail, teamMemberEmail });
|
||||||
|
const SOME_OTHER_EVENT_ID = 200;
|
||||||
|
await createHostForEvent({
|
||||||
|
eventTypeData: {
|
||||||
|
id: SOME_OTHER_EVENT_ID,
|
||||||
|
},
|
||||||
|
userData: {
|
||||||
|
email: teamMemberEmail,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { responseRecord } = await createRoutingFormWithResponse({
|
||||||
|
formData: {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
action: {
|
||||||
|
eventTypeId: 1,
|
||||||
|
type: RouteActionType.EventTypeRedirectUrl,
|
||||||
|
value: "/team/sales",
|
||||||
|
},
|
||||||
|
attributeRoutingConfig: { skipContactOwner: false },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||||
|
query: {
|
||||||
|
email: bookerEmail,
|
||||||
|
[ROUTING_FORM_RESPONSE_ID_QUERY_STRING]: responseRecord.id.toString(),
|
||||||
|
"cal.salesforce.xxx": "true",
|
||||||
|
},
|
||||||
|
eventData: mockEventData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null when cal.routingFormResponseId is provided but cal.salesforce.xxxx is not", async () => {
|
||||||
|
const bookerEmail = "booker@example.com";
|
||||||
|
const teamMemberEmail = "owner@example.com";
|
||||||
|
mockBookingFormHandler({ bookerEmail, teamMemberEmail });
|
||||||
|
await createHostForEvent({
|
||||||
|
eventTypeData: {
|
||||||
|
id: mockEventData.id,
|
||||||
|
},
|
||||||
|
userData: {
|
||||||
|
email: teamMemberEmail,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { responseRecord } = await createRoutingFormWithResponse({
|
||||||
|
formData: {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
action: {
|
||||||
|
eventTypeId: 1,
|
||||||
|
type: RouteActionType.EventTypeRedirectUrl,
|
||||||
|
value: "/team/sales",
|
||||||
|
},
|
||||||
|
attributeRoutingConfig: { skipContactOwner: false },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||||
|
query: {
|
||||||
|
email: bookerEmail,
|
||||||
|
[ROUTING_FORM_RESPONSE_ID_QUERY_STRING]: responseRecord.id.toString(),
|
||||||
|
"cal.somethingelse.xxx": "true",
|
||||||
|
},
|
||||||
|
eventData: mockEventData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -21,6 +21,21 @@ interface EventData {
|
|||||||
length: number;
|
length: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function findUserByEmailWhoIsAHostOfEventType({
|
||||||
|
email,
|
||||||
|
eventTypeId,
|
||||||
|
}: {
|
||||||
|
email: string;
|
||||||
|
eventTypeId: number;
|
||||||
|
}) {
|
||||||
|
return prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
email,
|
||||||
|
hosts: { some: { eventTypeId } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getRoutingFormResponseIdFromQuery(query: ParsedUrlQuery) {
|
function getRoutingFormResponseIdFromQuery(query: ParsedUrlQuery) {
|
||||||
const routingFormResponseIdAsNumber = Number(query[ROUTING_FORM_RESPONSE_ID_QUERY_STRING]);
|
const routingFormResponseIdAsNumber = Number(query[ROUTING_FORM_RESPONSE_ID_QUERY_STRING]);
|
||||||
const routingFormResponseId = isNaN(routingFormResponseIdAsNumber) ? null : routingFormResponseIdAsNumber;
|
const routingFormResponseId = isNaN(routingFormResponseIdAsNumber) ? null : routingFormResponseIdAsNumber;
|
||||||
@@ -99,16 +114,11 @@ function getEnabledRoutingFormAppSlugFromQuery(query: ParsedUrlQuery) {
|
|||||||
async function getOwnerEmailFromCrm(eventData: EventData, email: string): Promise<string | null> {
|
async function getOwnerEmailFromCrm(eventData: EventData, email: string): Promise<string | null> {
|
||||||
const crmContactOwnerEmail = await getCRMContactOwnerForRRLeadSkip(email, eventData.metadata);
|
const crmContactOwnerEmail = await getCRMContactOwnerForRRLeadSkip(email, eventData.metadata);
|
||||||
if (!crmContactOwnerEmail) return null;
|
if (!crmContactOwnerEmail) return null;
|
||||||
|
|
||||||
// Determine if the contactOwner is a part of the event type
|
// Determine if the contactOwner is a part of the event type
|
||||||
const contactOwnerQuery = await prisma.user.findFirst({
|
const contactOwnerQuery = await findUserByEmailWhoIsAHostOfEventType({
|
||||||
where: {
|
email: crmContactOwnerEmail,
|
||||||
email: crmContactOwnerEmail,
|
eventTypeId: eventData.id,
|
||||||
hosts: {
|
|
||||||
some: {
|
|
||||||
eventTypeId: eventData.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
if (!contactOwnerQuery) return null;
|
if (!contactOwnerQuery) return null;
|
||||||
return crmContactOwnerEmail;
|
return crmContactOwnerEmail;
|
||||||
@@ -138,7 +148,6 @@ async function getTeamMemberEmailUsingRoutingFormHandler({
|
|||||||
const appBookingFormHandler = (await import("@calcom/app-store/routing-forms/appBookingFormHandler"))
|
const appBookingFormHandler = (await import("@calcom/app-store/routing-forms/appBookingFormHandler"))
|
||||||
.default;
|
.default;
|
||||||
const appHandler = appBookingFormHandler[crmAppSlug];
|
const appHandler = appBookingFormHandler[crmAppSlug];
|
||||||
|
|
||||||
if (!appHandler) return nullReturnValue;
|
if (!appHandler) return nullReturnValue;
|
||||||
|
|
||||||
const { email: userEmail } = await appHandler(bookerEmail, attributeRoutingConfig, eventTypeId);
|
const { email: userEmail } = await appHandler(bookerEmail, attributeRoutingConfig, eventTypeId);
|
||||||
@@ -146,16 +155,7 @@ async function getTeamMemberEmailUsingRoutingFormHandler({
|
|||||||
if (!userEmail) return nullReturnValue;
|
if (!userEmail) return nullReturnValue;
|
||||||
|
|
||||||
// Determine if the user is a part of the event type
|
// Determine if the user is a part of the event type
|
||||||
const userQuery = await prisma.user.findFirst({
|
const userQuery = await findUserByEmailWhoIsAHostOfEventType({ email: userEmail, eventTypeId });
|
||||||
where: {
|
|
||||||
email: userEmail,
|
|
||||||
hosts: {
|
|
||||||
some: {
|
|
||||||
eventTypeId: eventTypeId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userQuery) return nullReturnValue;
|
if (!userQuery) return nullReturnValue;
|
||||||
|
|
||||||
@@ -164,22 +164,21 @@ async function getTeamMemberEmailUsingRoutingFormHandler({
|
|||||||
|
|
||||||
async function getTeamMemberEmailForResponseOrContact({
|
async function getTeamMemberEmailForResponseOrContact({
|
||||||
bookerEmail,
|
bookerEmail,
|
||||||
eventTypeId,
|
|
||||||
eventData,
|
eventData,
|
||||||
routingFormResponseId,
|
routingFormResponseId,
|
||||||
chosenRoute,
|
chosenRoute,
|
||||||
crmAppSlug,
|
crmAppSlug,
|
||||||
}: {
|
}: {
|
||||||
bookerEmail: string;
|
bookerEmail: string;
|
||||||
eventTypeId: number;
|
|
||||||
eventData: EventData;
|
eventData: EventData;
|
||||||
routingFormResponseId?: number | null;
|
routingFormResponseId?: number | null;
|
||||||
/**
|
/**
|
||||||
* If provided, we won't go look for the route from DB.
|
* If provided, we won't go look for the route from DB.
|
||||||
*/
|
*/
|
||||||
chosenRoute?: LocalRoute;
|
chosenRoute?: LocalRoute;
|
||||||
crmAppSlug: string;
|
crmAppSlug?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const eventTypeId = eventData.id;
|
||||||
if (eventData.schedulingType !== SchedulingType.ROUND_ROBIN) return null;
|
if (eventData.schedulingType !== SchedulingType.ROUND_ROBIN) return null;
|
||||||
|
|
||||||
const attributeRoutingConfigGetterData = routingFormResponseId
|
const attributeRoutingConfigGetterData = routingFormResponseId
|
||||||
@@ -214,12 +213,10 @@ async function getTeamMemberEmailForResponseOrContact({
|
|||||||
|
|
||||||
export async function getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
export async function getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||||
query,
|
query,
|
||||||
eventTypeId,
|
|
||||||
eventData,
|
eventData,
|
||||||
chosenRoute,
|
chosenRoute,
|
||||||
}: {
|
}: {
|
||||||
query: ParsedUrlQuery;
|
query: ParsedUrlQuery;
|
||||||
eventTypeId: number;
|
|
||||||
eventData: EventData;
|
eventData: EventData;
|
||||||
chosenRoute?: LocalRoute;
|
chosenRoute?: LocalRoute;
|
||||||
}) {
|
}) {
|
||||||
@@ -231,13 +228,11 @@ export async function getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
|||||||
log.debug("getTeamMemberEmailForResponseOrContactUsingUrlQuery", safeStringify({ query }));
|
log.debug("getTeamMemberEmailForResponseOrContactUsingUrlQuery", safeStringify({ query }));
|
||||||
|
|
||||||
const crmAppSlug = getEnabledRoutingFormAppSlugFromQuery(query);
|
const crmAppSlug = getEnabledRoutingFormAppSlugFromQuery(query);
|
||||||
if (!crmAppSlug) return null;
|
|
||||||
|
|
||||||
const routingFormResponseId = getRoutingFormResponseIdFromQuery(query);
|
const routingFormResponseId = getRoutingFormResponseIdFromQuery(query);
|
||||||
|
|
||||||
return await getTeamMemberEmailForResponseOrContact({
|
return await getTeamMemberEmailForResponseOrContact({
|
||||||
bookerEmail: query.email,
|
bookerEmail: query.email,
|
||||||
eventTypeId,
|
|
||||||
eventData,
|
eventData,
|
||||||
routingFormResponseId,
|
routingFormResponseId,
|
||||||
chosenRoute,
|
chosenRoute,
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
orgBannerUrl: team.parent?.bannerUrl ?? "",
|
orgBannerUrl: team.parent?.bannerUrl ?? "",
|
||||||
teamMemberEmail: await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
teamMemberEmail: await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||||
query,
|
query,
|
||||||
eventTypeId,
|
|
||||||
eventData,
|
eventData,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "4.7.0",
|
"version": "4.7.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "تم الحجز بواسطة",
|
"routing_form_insights_booked_by": "تم الحجز بواسطة",
|
||||||
"routing_form_insights_created_at": "تم الإنشاء في",
|
"routing_form_insights_created_at": "تم الإنشاء في",
|
||||||
"routing_form_insights_booking_status": "حالة الحجز",
|
"routing_form_insights_booking_status": "حالة الحجز",
|
||||||
|
"routing_form_insights_booking_at": "موعد الحجز",
|
||||||
|
"routing_form_insights_submitted_at": "تاريخ التقديم",
|
||||||
"access_denied": "تم رفض الوصول",
|
"access_denied": "تم رفض الوصول",
|
||||||
"salesforce_route_to_owner": "سيكون مالك جهة الاتصال هو مضيف Round Robin إذا كان متاحاً",
|
"salesforce_route_to_owner": "سيكون مالك جهة الاتصال هو مضيف Round Robin إذا كان متاحاً",
|
||||||
"salesforce_do_not_route_to_owner": "لن يتم فرض مالك جهة الاتصال (يمكن أن يظل مضيفاً إذا تطابق مع السمات ومعايير Round Robin)",
|
"salesforce_do_not_route_to_owner": "لن يتم فرض مالك جهة الاتصال (يمكن أن يظل مضيفاً إذا تطابق مع السمات ومعايير Round Robin)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Rezervasiya edən",
|
"routing_form_insights_booked_by": "Rezervasiya edən",
|
||||||
"routing_form_insights_created_at": "Yaradılma tarixi",
|
"routing_form_insights_created_at": "Yaradılma tarixi",
|
||||||
"routing_form_insights_booking_status": "Rezervasiya statusu",
|
"routing_form_insights_booking_status": "Rezervasiya statusu",
|
||||||
|
"routing_form_insights_booking_at": "Rezervasiya tarixi",
|
||||||
|
"routing_form_insights_submitted_at": "Göndərilmə tarixi",
|
||||||
"access_denied": "Giriş qadağandır",
|
"access_denied": "Giriş qadağandır",
|
||||||
"salesforce_route_to_owner": "Əlaqə sahibi mövcud olduqda Round Robin ev sahibi olacaq",
|
"salesforce_route_to_owner": "Əlaqə sahibi mövcud olduqda Round Robin ev sahibi olacaq",
|
||||||
"salesforce_do_not_route_to_owner": "Əlaqə sahibi məcbur edilməyəcək (əgər atributlara və Round Robin meyarlarına uyğun gəlirsə, yenə də ev sahibi ola bilər)",
|
"salesforce_do_not_route_to_owner": "Əlaqə sahibi məcbur edilməyəcək (əgər atributlara və Round Robin meyarlarına uyğun gəlirsə, yenə də ev sahibi ola bilər)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Rezervoval/a",
|
"routing_form_insights_booked_by": "Rezervoval/a",
|
||||||
"routing_form_insights_created_at": "Vytvořeno",
|
"routing_form_insights_created_at": "Vytvořeno",
|
||||||
"routing_form_insights_booking_status": "Stav rezervace",
|
"routing_form_insights_booking_status": "Stav rezervace",
|
||||||
|
"routing_form_insights_booking_at": "Čas rezervace",
|
||||||
|
"routing_form_insights_submitted_at": "Čas odeslání",
|
||||||
"access_denied": "Přístup odepřen",
|
"access_denied": "Přístup odepřen",
|
||||||
"salesforce_route_to_owner": "Vlastník kontaktu bude hostem Round Robin, pokud je k dispozici",
|
"salesforce_route_to_owner": "Vlastník kontaktu bude hostem Round Robin, pokud je k dispozici",
|
||||||
"salesforce_do_not_route_to_owner": "Vlastník kontaktu nebude nucen (stále může být hostem, pokud odpovídá atributům a kritériím Round Robin)",
|
"salesforce_do_not_route_to_owner": "Vlastník kontaktu nebude nucen (stále může být hostem, pokud odpovídá atributům a kritériím Round Robin)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Booket af",
|
"routing_form_insights_booked_by": "Booket af",
|
||||||
"routing_form_insights_created_at": "Oprettet den",
|
"routing_form_insights_created_at": "Oprettet den",
|
||||||
"routing_form_insights_booking_status": "Bookingstatus",
|
"routing_form_insights_booking_status": "Bookingstatus",
|
||||||
|
"routing_form_insights_booking_at": "Booket den",
|
||||||
|
"routing_form_insights_submitted_at": "Indsendt den",
|
||||||
"access_denied": "Adgang nægtet",
|
"access_denied": "Adgang nægtet",
|
||||||
"salesforce_route_to_owner": "Kontaktens ejer vil være Round Robin-vært, hvis tilgængelig",
|
"salesforce_route_to_owner": "Kontaktens ejer vil være Round Robin-vært, hvis tilgængelig",
|
||||||
"salesforce_do_not_route_to_owner": "Kontaktens ejer vil ikke blive tvunget (kan stadig være vært, hvis det matcher attributterne og Round Robin-kriterierne)",
|
"salesforce_do_not_route_to_owner": "Kontaktens ejer vil ikke blive tvunget (kan stadig være vært, hvis det matcher attributterne og Round Robin-kriterierne)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Gebucht von",
|
"routing_form_insights_booked_by": "Gebucht von",
|
||||||
"routing_form_insights_created_at": "Erstellt am",
|
"routing_form_insights_created_at": "Erstellt am",
|
||||||
"routing_form_insights_booking_status": "Buchungsstatus",
|
"routing_form_insights_booking_status": "Buchungsstatus",
|
||||||
|
"routing_form_insights_booking_at": "Gebucht am",
|
||||||
|
"routing_form_insights_submitted_at": "Eingereicht am",
|
||||||
"access_denied": "Zugriff verweigert",
|
"access_denied": "Zugriff verweigert",
|
||||||
"salesforce_route_to_owner": "Der Kontaktinhaber wird der Round Robin Host sein, falls verfügbar",
|
"salesforce_route_to_owner": "Der Kontaktinhaber wird der Round Robin Host sein, falls verfügbar",
|
||||||
"salesforce_do_not_route_to_owner": "Der Kontaktinhaber wird nicht erzwungen (kann trotzdem Host sein, wenn die Attribute und Round Robin Kriterien übereinstimmen)",
|
"salesforce_do_not_route_to_owner": "Der Kontaktinhaber wird nicht erzwungen (kann trotzdem Host sein, wenn die Attribute und Round Robin Kriterien übereinstimmen)",
|
||||||
|
|||||||
@@ -2783,6 +2783,8 @@
|
|||||||
"routing_form_insights_booked_by": "Booked by",
|
"routing_form_insights_booked_by": "Booked by",
|
||||||
"routing_form_insights_created_at": "Created at",
|
"routing_form_insights_created_at": "Created at",
|
||||||
"routing_form_insights_booking_status": "Booking Status",
|
"routing_form_insights_booking_status": "Booking Status",
|
||||||
|
"routing_form_insights_booking_at": "Booking At",
|
||||||
|
"routing_form_insights_submitted_at": "Submitted At",
|
||||||
"access_denied": "Access Denied",
|
"access_denied": "Access Denied",
|
||||||
"salesforce_route_to_owner": "Contact owner will be the Round Robin host if available",
|
"salesforce_route_to_owner": "Contact owner will be the Round Robin host if available",
|
||||||
"salesforce_do_not_route_to_owner": "Contact owner will not be forced (can still be host if it matches the attributes and Round Robin criteria)",
|
"salesforce_do_not_route_to_owner": "Contact owner will not be forced (can still be host if it matches the attributes and Round Robin criteria)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Reservado por",
|
"routing_form_insights_booked_by": "Reservado por",
|
||||||
"routing_form_insights_created_at": "Creado el",
|
"routing_form_insights_created_at": "Creado el",
|
||||||
"routing_form_insights_booking_status": "Estado de la reserva",
|
"routing_form_insights_booking_status": "Estado de la reserva",
|
||||||
|
"routing_form_insights_booking_at": "Reservado el",
|
||||||
|
"routing_form_insights_submitted_at": "Enviado el",
|
||||||
"access_denied": "Acceso denegado",
|
"access_denied": "Acceso denegado",
|
||||||
"salesforce_route_to_owner": "El propietario del contacto será el anfitrión de Round Robin si está disponible",
|
"salesforce_route_to_owner": "El propietario del contacto será el anfitrión de Round Robin si está disponible",
|
||||||
"salesforce_do_not_route_to_owner": "El propietario del contacto no será forzado (aún puede ser anfitrión si coincide con los atributos y criterios de Round Robin)",
|
"salesforce_do_not_route_to_owner": "El propietario del contacto no será forzado (aún puede ser anfitrión si coincide con los atributos y criterios de Round Robin)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Reservado por",
|
"routing_form_insights_booked_by": "Reservado por",
|
||||||
"routing_form_insights_created_at": "Creado el",
|
"routing_form_insights_created_at": "Creado el",
|
||||||
"routing_form_insights_booking_status": "Estado de la reserva",
|
"routing_form_insights_booking_status": "Estado de la reserva",
|
||||||
|
"routing_form_insights_booking_at": "Fecha de reserva",
|
||||||
|
"routing_form_insights_submitted_at": "Fecha de envío",
|
||||||
"access_denied": "Acceso denegado",
|
"access_denied": "Acceso denegado",
|
||||||
"salesforce_route_to_owner": "El propietario del contacto será el anfitrión de Round Robin si está disponible",
|
"salesforce_route_to_owner": "El propietario del contacto será el anfitrión de Round Robin si está disponible",
|
||||||
"salesforce_do_not_route_to_owner": "No se forzará al propietario del contacto (aún puede ser anfitrión si coincide con los atributos y criterios de Round Robin)",
|
"salesforce_do_not_route_to_owner": "No se forzará al propietario del contacto (aún puede ser anfitrión si coincide con los atributos y criterios de Round Robin)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Broneeris",
|
"routing_form_insights_booked_by": "Broneeris",
|
||||||
"routing_form_insights_created_at": "Loodud",
|
"routing_form_insights_created_at": "Loodud",
|
||||||
"routing_form_insights_booking_status": "Broneeringu staatus",
|
"routing_form_insights_booking_status": "Broneeringu staatus",
|
||||||
|
"routing_form_insights_booking_at": "Broneeritud",
|
||||||
|
"routing_form_insights_submitted_at": "Esitatud",
|
||||||
"access_denied": "Juurdepääs keelatud",
|
"access_denied": "Juurdepääs keelatud",
|
||||||
"salesforce_route_to_owner": "Kontakti omanik on round robin host, kui saadaval",
|
"salesforce_route_to_owner": "Kontakti omanik on round robin host, kui saadaval",
|
||||||
"salesforce_do_not_route_to_owner": "Kontakti omanikku ei sunnita (võib siiski olla host, kui vastab atribuutidele ja round robin kriteeriumitele)",
|
"salesforce_do_not_route_to_owner": "Kontakti omanikku ei sunnita (võib siiski olla host, kui vastab atribuutidele ja round robin kriteeriumitele)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Réservé par",
|
"routing_form_insights_booked_by": "Réservé par",
|
||||||
"routing_form_insights_created_at": "Créé le",
|
"routing_form_insights_created_at": "Créé le",
|
||||||
"routing_form_insights_booking_status": "Statut de la réservation",
|
"routing_form_insights_booking_status": "Statut de la réservation",
|
||||||
|
"routing_form_insights_booking_at": "Réservé le",
|
||||||
|
"routing_form_insights_submitted_at": "Soumis le",
|
||||||
"access_denied": "Accès refusé",
|
"access_denied": "Accès refusé",
|
||||||
"salesforce_route_to_owner": "Le propriétaire du contact sera l'hôte Round Robin si disponible",
|
"salesforce_route_to_owner": "Le propriétaire du contact sera l'hôte Round Robin si disponible",
|
||||||
"salesforce_do_not_route_to_owner": "Le propriétaire du contact ne sera pas forcé (peut toujours être l'hôte si cela correspond aux attributs et aux critères Round Robin)",
|
"salesforce_do_not_route_to_owner": "Le propriétaire du contact ne sera pas forcé (peut toujours être l'hôte si cela correspond aux attributs et aux critères Round Robin)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "הוזמן על ידי",
|
"routing_form_insights_booked_by": "הוזמן על ידי",
|
||||||
"routing_form_insights_created_at": "נוצר ב-",
|
"routing_form_insights_created_at": "נוצר ב-",
|
||||||
"routing_form_insights_booking_status": "סטטוס הזמנה",
|
"routing_form_insights_booking_status": "סטטוס הזמנה",
|
||||||
|
"routing_form_insights_booking_at": "מועד ההזמנה",
|
||||||
|
"routing_form_insights_submitted_at": "מועד ההגשה",
|
||||||
"access_denied": "הגישה נדחתה",
|
"access_denied": "הגישה נדחתה",
|
||||||
"salesforce_route_to_owner": "בעל איש הקשר יהיה מארח ה-Round Robin אם זמין",
|
"salesforce_route_to_owner": "בעל איש הקשר יהיה מארח ה-Round Robin אם זמין",
|
||||||
"salesforce_do_not_route_to_owner": "בעל איש הקשר לא יהיה מאולץ (עדיין יכול להיות מארח אם תואם את המאפיינים וקריטריוני ה-Round Robin)",
|
"salesforce_do_not_route_to_owner": "בעל איש הקשר לא יהיה מאולץ (עדיין יכול להיות מארח אם תואם את המאפיינים וקריטריוני ה-Round Robin)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Foglalva általa",
|
"routing_form_insights_booked_by": "Foglalva általa",
|
||||||
"routing_form_insights_created_at": "Létrehozva",
|
"routing_form_insights_created_at": "Létrehozva",
|
||||||
"routing_form_insights_booking_status": "Foglalás állapota",
|
"routing_form_insights_booking_status": "Foglalás állapota",
|
||||||
|
"routing_form_insights_booking_at": "Foglalás időpontja",
|
||||||
|
"routing_form_insights_submitted_at": "Beküldés időpontja",
|
||||||
"access_denied": "Hozzáférés megtagadva",
|
"access_denied": "Hozzáférés megtagadva",
|
||||||
"salesforce_route_to_owner": "A kapcsolattartó tulajdonosa lesz a round robin házigazda, ha elérhető",
|
"salesforce_route_to_owner": "A kapcsolattartó tulajdonosa lesz a round robin házigazda, ha elérhető",
|
||||||
"salesforce_do_not_route_to_owner": "A kapcsolattartó tulajdonosa nem lesz kötelező (továbbra is lehet házigazda, ha megfelel a tulajdonságoknak és a round robin kritériumoknak)",
|
"salesforce_do_not_route_to_owner": "A kapcsolattartó tulajdonosa nem lesz kötelező (továbbra is lehet házigazda, ha megfelel a tulajdonságoknak és a round robin kritériumoknak)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Prenotato da",
|
"routing_form_insights_booked_by": "Prenotato da",
|
||||||
"routing_form_insights_created_at": "Creato il",
|
"routing_form_insights_created_at": "Creato il",
|
||||||
"routing_form_insights_booking_status": "Stato della prenotazione",
|
"routing_form_insights_booking_status": "Stato della prenotazione",
|
||||||
|
"routing_form_insights_booking_at": "Data prenotazione",
|
||||||
|
"routing_form_insights_submitted_at": "Data invio",
|
||||||
"access_denied": "Accesso negato",
|
"access_denied": "Accesso negato",
|
||||||
"salesforce_route_to_owner": "Il proprietario del contatto sarà l'host del Round Robin se disponibile",
|
"salesforce_route_to_owner": "Il proprietario del contatto sarà l'host del Round Robin se disponibile",
|
||||||
"salesforce_do_not_route_to_owner": "Il proprietario del contatto non sarà obbligato (può comunque essere l'host se corrisponde agli attributi e ai criteri del Round Robin)",
|
"salesforce_do_not_route_to_owner": "Il proprietario del contatto non sarà obbligato (può comunque essere l'host se corrisponde agli attributi e ai criteri del Round Robin)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "予約者",
|
"routing_form_insights_booked_by": "予約者",
|
||||||
"routing_form_insights_created_at": "作成日",
|
"routing_form_insights_created_at": "作成日",
|
||||||
"routing_form_insights_booking_status": "予約状況",
|
"routing_form_insights_booking_status": "予約状況",
|
||||||
|
"routing_form_insights_booking_at": "予約日時",
|
||||||
|
"routing_form_insights_submitted_at": "送信日時",
|
||||||
"access_denied": "アクセスが拒否されました",
|
"access_denied": "アクセスが拒否されました",
|
||||||
"salesforce_route_to_owner": "可能な場合、取引先責任者の所有者がラウンドロビンのホストになります",
|
"salesforce_route_to_owner": "可能な場合、取引先責任者の所有者がラウンドロビンのホストになります",
|
||||||
"salesforce_do_not_route_to_owner": "取引先責任者の所有者は強制されません(属性とラウンドロビンの基準に一致する場合はホストになる可能性があります)",
|
"salesforce_do_not_route_to_owner": "取引先責任者の所有者は強制されません(属性とラウンドロビンの基準に一致する場合はホストになる可能性があります)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "កក់ដោយ",
|
"routing_form_insights_booked_by": "កក់ដោយ",
|
||||||
"routing_form_insights_created_at": "បង្កើតនៅ",
|
"routing_form_insights_created_at": "បង្កើតនៅ",
|
||||||
"routing_form_insights_booking_status": "ស្ថានភាពនៃការកក់",
|
"routing_form_insights_booking_status": "ស្ថានភាពនៃការកក់",
|
||||||
|
"routing_form_insights_booking_at": "កាលបរិច្ឆេទកក់",
|
||||||
|
"routing_form_insights_submitted_at": "កាលបរិច្ឆេទដាក់ស្នើ",
|
||||||
"access_denied": "ការចូលប្រើត្រូវបានបដិសេធ",
|
"access_denied": "ការចូលប្រើត្រូវបានបដិសេធ",
|
||||||
"salesforce_route_to_owner": "ម្ចាស់ទំនាក់ទំនងនឹងជាម្ចាស់ Round Robin ប្រសិនបើមានស្រាប់។",
|
"salesforce_route_to_owner": "ម្ចាស់ទំនាក់ទំនងនឹងជាម្ចាស់ Round Robin ប្រសិនបើមានស្រាប់។",
|
||||||
"salesforce_do_not_route_to_owner": "ម្ចាស់ទំនាក់ទំនងនឹងមិនត្រូវបានបង្ខំ (អាចនៅតែជាម្ចាស់ប្រសិនបើវាដូចគ្នាជាមួយលក្ខណៈ និងលក្ខខណ្ឌ Round Robin)។",
|
"salesforce_do_not_route_to_owner": "ម្ចាស់ទំនាក់ទំនងនឹងមិនត្រូវបានបង្ខំ (អាចនៅតែជាម្ចាស់ប្រសិនបើវាដូចគ្នាជាមួយលក្ខណៈ និងលក្ខខណ្ឌ Round Robin)។",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "예약자",
|
"routing_form_insights_booked_by": "예약자",
|
||||||
"routing_form_insights_created_at": "생성 시간",
|
"routing_form_insights_created_at": "생성 시간",
|
||||||
"routing_form_insights_booking_status": "예약 상태",
|
"routing_form_insights_booking_status": "예약 상태",
|
||||||
|
"routing_form_insights_booking_at": "예약 시간",
|
||||||
|
"routing_form_insights_submitted_at": "제출 시간",
|
||||||
"access_denied": "접근 거부됨",
|
"access_denied": "접근 거부됨",
|
||||||
"salesforce_route_to_owner": "연락처 소유자가 가능하면 Round Robin 호스트가 됩니다.",
|
"salesforce_route_to_owner": "연락처 소유자가 가능하면 Round Robin 호스트가 됩니다.",
|
||||||
"salesforce_do_not_route_to_owner": "연락처 소유자가 강제되지 않습니다 (속성과 Round Robin 기준이 일치하면 여전히 호스트가 될 수 있습니다).",
|
"salesforce_do_not_route_to_owner": "연락처 소유자가 강제되지 않습니다 (속성과 Round Robin 기준이 일치하면 여전히 호스트가 될 수 있습니다).",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Geboekt door",
|
"routing_form_insights_booked_by": "Geboekt door",
|
||||||
"routing_form_insights_created_at": "Aangemaakt op",
|
"routing_form_insights_created_at": "Aangemaakt op",
|
||||||
"routing_form_insights_booking_status": "Boekingsstatus",
|
"routing_form_insights_booking_status": "Boekingsstatus",
|
||||||
|
"routing_form_insights_booking_at": "Geboekt op",
|
||||||
|
"routing_form_insights_submitted_at": "Ingediend op",
|
||||||
"access_denied": "Toegang geweigerd",
|
"access_denied": "Toegang geweigerd",
|
||||||
"salesforce_route_to_owner": "Contacteigenaar zal de Round Robin host zijn indien beschikbaar",
|
"salesforce_route_to_owner": "Contacteigenaar zal de Round Robin host zijn indien beschikbaar",
|
||||||
"salesforce_do_not_route_to_owner": "Contacteigenaar wordt niet geforceerd (kan nog steeds host zijn als het overeenkomt met de attributen en Round Robin criteria)",
|
"salesforce_do_not_route_to_owner": "Contacteigenaar wordt niet geforceerd (kan nog steeds host zijn als het overeenkomt met de attributen en Round Robin criteria)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Bestilt av",
|
"routing_form_insights_booked_by": "Bestilt av",
|
||||||
"routing_form_insights_created_at": "Opprettet den",
|
"routing_form_insights_created_at": "Opprettet den",
|
||||||
"routing_form_insights_booking_status": "Bestillingsstatus",
|
"routing_form_insights_booking_status": "Bestillingsstatus",
|
||||||
|
"routing_form_insights_booking_at": "Booket",
|
||||||
|
"routing_form_insights_submitted_at": "Sendt inn",
|
||||||
"access_denied": "Tilgang nektet",
|
"access_denied": "Tilgang nektet",
|
||||||
"salesforce_route_to_owner": "Kontaktens eier vil være Round Robin-vert hvis tilgjengelig",
|
"salesforce_route_to_owner": "Kontaktens eier vil være Round Robin-vert hvis tilgjengelig",
|
||||||
"salesforce_do_not_route_to_owner": "Kontaktens eier vil ikke bli tvunget (kan fortsatt være vert hvis det samsvarer med attributtene og Round Robin-kriteriene)",
|
"salesforce_do_not_route_to_owner": "Kontaktens eier vil ikke bli tvunget (kan fortsatt være vert hvis det samsvarer med attributtene og Round Robin-kriteriene)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Zarezerwowane przez",
|
"routing_form_insights_booked_by": "Zarezerwowane przez",
|
||||||
"routing_form_insights_created_at": "Utworzono",
|
"routing_form_insights_created_at": "Utworzono",
|
||||||
"routing_form_insights_booking_status": "Status rezerwacji",
|
"routing_form_insights_booking_status": "Status rezerwacji",
|
||||||
|
"routing_form_insights_booking_at": "Data rezerwacji",
|
||||||
|
"routing_form_insights_submitted_at": "Data przesłania",
|
||||||
"access_denied": "Odmowa dostępu",
|
"access_denied": "Odmowa dostępu",
|
||||||
"salesforce_route_to_owner": "Właściciel kontaktu będzie hostem Round Robin, jeśli jest dostępny",
|
"salesforce_route_to_owner": "Właściciel kontaktu będzie hostem Round Robin, jeśli jest dostępny",
|
||||||
"salesforce_do_not_route_to_owner": "Właściciel kontaktu nie będzie wymuszony (może nadal być hostem, jeśli spełnia atrybuty i kryteria Round Robin)",
|
"salesforce_do_not_route_to_owner": "Właściciel kontaktu nie będzie wymuszony (może nadal być hostem, jeśli spełnia atrybuty i kryteria Round Robin)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Agendado por",
|
"routing_form_insights_booked_by": "Agendado por",
|
||||||
"routing_form_insights_created_at": "Criado em",
|
"routing_form_insights_created_at": "Criado em",
|
||||||
"routing_form_insights_booking_status": "Status do Agendamento",
|
"routing_form_insights_booking_status": "Status do Agendamento",
|
||||||
|
"routing_form_insights_booking_at": "Agendado em",
|
||||||
|
"routing_form_insights_submitted_at": "Enviado em",
|
||||||
"access_denied": "Acesso negado",
|
"access_denied": "Acesso negado",
|
||||||
"salesforce_route_to_owner": "O proprietário do contato será o anfitrião do Round Robin se disponível",
|
"salesforce_route_to_owner": "O proprietário do contato será o anfitrião do Round Robin se disponível",
|
||||||
"salesforce_do_not_route_to_owner": "O proprietário do contato não será forçado (ainda pode ser anfitrião se corresponder aos atributos e critérios do Round Robin)",
|
"salesforce_do_not_route_to_owner": "O proprietário do contato não será forçado (ainda pode ser anfitrião se corresponder aos atributos e critérios do Round Robin)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Agendado por",
|
"routing_form_insights_booked_by": "Agendado por",
|
||||||
"routing_form_insights_created_at": "Criado em",
|
"routing_form_insights_created_at": "Criado em",
|
||||||
"routing_form_insights_booking_status": "Status do agendamento",
|
"routing_form_insights_booking_status": "Status do agendamento",
|
||||||
|
"routing_form_insights_booking_at": "Agendado em",
|
||||||
|
"routing_form_insights_submitted_at": "Enviado em",
|
||||||
"access_denied": "Acesso negado",
|
"access_denied": "Acesso negado",
|
||||||
"salesforce_route_to_owner": "O proprietário do contato será o anfitrião do Round Robin se disponível",
|
"salesforce_route_to_owner": "O proprietário do contato será o anfitrião do Round Robin se disponível",
|
||||||
"salesforce_do_not_route_to_owner": "O proprietário do contato não será forçado (ainda pode ser anfitrião se corresponder aos atributos e critérios do Round Robin)",
|
"salesforce_do_not_route_to_owner": "O proprietário do contato não será forçado (ainda pode ser anfitrião se corresponder aos atributos e critérios do Round Robin)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Programat de",
|
"routing_form_insights_booked_by": "Programat de",
|
||||||
"routing_form_insights_created_at": "Creat la",
|
"routing_form_insights_created_at": "Creat la",
|
||||||
"routing_form_insights_booking_status": "Status programare",
|
"routing_form_insights_booking_status": "Status programare",
|
||||||
|
"routing_form_insights_booking_at": "Programat la",
|
||||||
|
"routing_form_insights_submitted_at": "Trimis la",
|
||||||
"access_denied": "Acces refuzat",
|
"access_denied": "Acces refuzat",
|
||||||
"salesforce_route_to_owner": "Proprietarul contactului va fi gazda Round Robin dacă este disponibil",
|
"salesforce_route_to_owner": "Proprietarul contactului va fi gazda Round Robin dacă este disponibil",
|
||||||
"salesforce_do_not_route_to_owner": "Proprietarul contactului nu va fi forțat (poate fi totuși gazdă dacă se potrivesc atributele și criteriile Round Robin)",
|
"salesforce_do_not_route_to_owner": "Proprietarul contactului nu va fi forțat (poate fi totuși gazdă dacă se potrivesc atributele și criteriile Round Robin)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Rezervoval",
|
"routing_form_insights_booked_by": "Rezervoval",
|
||||||
"routing_form_insights_created_at": "Vytvorené",
|
"routing_form_insights_created_at": "Vytvorené",
|
||||||
"routing_form_insights_booking_status": "Stav rezervácie",
|
"routing_form_insights_booking_status": "Stav rezervácie",
|
||||||
|
"routing_form_insights_booking_at": "Čas rezervácie",
|
||||||
|
"routing_form_insights_submitted_at": "Čas odoslania",
|
||||||
"access_denied": "Prístup odmietnutý",
|
"access_denied": "Prístup odmietnutý",
|
||||||
"salesforce_route_to_owner": "Vlastník kontaktu bude hostiteľom metódou Round Robin, ak je k dispozícii",
|
"salesforce_route_to_owner": "Vlastník kontaktu bude hostiteľom metódou Round Robin, ak je k dispozícii",
|
||||||
"salesforce_do_not_route_to_owner": "Vlastník kontaktu nebude nútený (môže byť hostiteľom, ak zodpovedá atribútom a kritériám Round Robin)",
|
"salesforce_do_not_route_to_owner": "Vlastník kontaktu nebude nútený (môže byť hostiteľom, ak zodpovedá atribútom a kritériám Round Robin)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Rezervisao/la",
|
"routing_form_insights_booked_by": "Rezervisao/la",
|
||||||
"routing_form_insights_created_at": "Kreirano",
|
"routing_form_insights_created_at": "Kreirano",
|
||||||
"routing_form_insights_booking_status": "Status rezervacije",
|
"routing_form_insights_booking_status": "Status rezervacije",
|
||||||
|
"routing_form_insights_booking_at": "Vreme rezervacije",
|
||||||
|
"routing_form_insights_submitted_at": "Vreme podnošenja",
|
||||||
"access_denied": "Pristup odbijen",
|
"access_denied": "Pristup odbijen",
|
||||||
"salesforce_route_to_owner": "Vlasnik kontakta će biti Round Robin domaćin ako je dostupan",
|
"salesforce_route_to_owner": "Vlasnik kontakta će biti Round Robin domaćin ako je dostupan",
|
||||||
"salesforce_do_not_route_to_owner": "Vlasnik kontakta neće biti primoran (može i dalje biti domaćin ako odgovara atributima i Round Robin kriterijumima)",
|
"salesforce_do_not_route_to_owner": "Vlasnik kontakta neće biti primoran (može i dalje biti domaćin ako odgovara atributima i Round Robin kriterijumima)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Bokad av",
|
"routing_form_insights_booked_by": "Bokad av",
|
||||||
"routing_form_insights_created_at": "Skapad",
|
"routing_form_insights_created_at": "Skapad",
|
||||||
"routing_form_insights_booking_status": "Bokningsstatus",
|
"routing_form_insights_booking_status": "Bokningsstatus",
|
||||||
|
"routing_form_insights_booking_at": "Bokad den",
|
||||||
|
"routing_form_insights_submitted_at": "Skickad den",
|
||||||
"access_denied": "Åtkomst nekad",
|
"access_denied": "Åtkomst nekad",
|
||||||
"salesforce_route_to_owner": "Kontaktägaren blir Round Robin-värd om tillgänglig",
|
"salesforce_route_to_owner": "Kontaktägaren blir Round Robin-värd om tillgänglig",
|
||||||
"salesforce_do_not_route_to_owner": "Kontaktägaren kommer inte att tvingas (kan fortfarande vara värd om det matchar attributen och Round Robin-kriterierna)",
|
"salesforce_do_not_route_to_owner": "Kontaktägaren kommer inte att tvingas (kan fortfarande vara värd om det matchar attributen och Round Robin-kriterierna)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Rezervasyon yapan",
|
"routing_form_insights_booked_by": "Rezervasyon yapan",
|
||||||
"routing_form_insights_created_at": "Oluşturulma tarihi",
|
"routing_form_insights_created_at": "Oluşturulma tarihi",
|
||||||
"routing_form_insights_booking_status": "Rezervasyon durumu",
|
"routing_form_insights_booking_status": "Rezervasyon durumu",
|
||||||
|
"routing_form_insights_booking_at": "Rezervasyon tarihi",
|
||||||
|
"routing_form_insights_submitted_at": "Gönderilme tarihi",
|
||||||
"access_denied": "Erişim Reddedildi",
|
"access_denied": "Erişim Reddedildi",
|
||||||
"salesforce_route_to_owner": "Mümkünse kişi sahibi round robin ev sahibi olacak",
|
"salesforce_route_to_owner": "Mümkünse kişi sahibi round robin ev sahibi olacak",
|
||||||
"salesforce_do_not_route_to_owner": "Kişi sahibi zorlanmayacak (özellikler ve round robin kriterlerine uyuyorsa yine de ev sahibi olabilir)",
|
"salesforce_do_not_route_to_owner": "Kişi sahibi zorlanmayacak (özellikler ve round robin kriterlerine uyuyorsa yine de ev sahibi olabilir)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Заброньовано",
|
"routing_form_insights_booked_by": "Заброньовано",
|
||||||
"routing_form_insights_created_at": "Створено",
|
"routing_form_insights_created_at": "Створено",
|
||||||
"routing_form_insights_booking_status": "Статус бронювання",
|
"routing_form_insights_booking_status": "Статус бронювання",
|
||||||
|
"routing_form_insights_booking_at": "Час бронювання",
|
||||||
|
"routing_form_insights_submitted_at": "Час подання",
|
||||||
"access_denied": "Доступ заборонено",
|
"access_denied": "Доступ заборонено",
|
||||||
"salesforce_route_to_owner": "Власник контакту буде ведучим Round Robin, якщо доступний",
|
"salesforce_route_to_owner": "Власник контакту буде ведучим Round Robin, якщо доступний",
|
||||||
"salesforce_do_not_route_to_owner": "Власник контакту не буде примусово призначений (може бути ведучим, якщо відповідає атрибутам і критеріям Round Robin)",
|
"salesforce_do_not_route_to_owner": "Власник контакту не буде примусово призначений (може бути ведучим, якщо відповідає атрибутам і критеріям Round Robin)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "Được đặt bởi",
|
"routing_form_insights_booked_by": "Được đặt bởi",
|
||||||
"routing_form_insights_created_at": "Được tạo vào",
|
"routing_form_insights_created_at": "Được tạo vào",
|
||||||
"routing_form_insights_booking_status": "Trạng thái đặt chỗ",
|
"routing_form_insights_booking_status": "Trạng thái đặt chỗ",
|
||||||
|
"routing_form_insights_booking_at": "Thời gian đặt lịch",
|
||||||
|
"routing_form_insights_submitted_at": "Thời gian gửi",
|
||||||
"access_denied": "Từ chối truy cập",
|
"access_denied": "Từ chối truy cập",
|
||||||
"salesforce_route_to_owner": "Người sở hữu liên hệ sẽ là người chủ trì Round Robin nếu khả dụng",
|
"salesforce_route_to_owner": "Người sở hữu liên hệ sẽ là người chủ trì Round Robin nếu khả dụng",
|
||||||
"salesforce_do_not_route_to_owner": "Người sở hữu liên hệ sẽ không bị bắt buộc (vẫn có thể là người chủ trì nếu phù hợp với các thuộc tính và tiêu chí Round Robin)",
|
"salesforce_do_not_route_to_owner": "Người sở hữu liên hệ sẽ không bị bắt buộc (vẫn có thể là người chủ trì nếu phù hợp với các thuộc tính và tiêu chí Round Robin)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "预订者",
|
"routing_form_insights_booked_by": "预订者",
|
||||||
"routing_form_insights_created_at": "创建于",
|
"routing_form_insights_created_at": "创建于",
|
||||||
"routing_form_insights_booking_status": "预订状态",
|
"routing_form_insights_booking_status": "预订状态",
|
||||||
|
"routing_form_insights_booking_at": "预约时间",
|
||||||
|
"routing_form_insights_submitted_at": "提交时间",
|
||||||
"access_denied": "访问被拒绝",
|
"access_denied": "访问被拒绝",
|
||||||
"salesforce_route_to_owner": "如果联系人所有者可用,将作为轮询主持人",
|
"salesforce_route_to_owner": "如果联系人所有者可用,将作为轮询主持人",
|
||||||
"salesforce_do_not_route_to_owner": "不会强制指定联系人所有者(如果符合属性和轮询标准,仍可成为主持人)",
|
"salesforce_do_not_route_to_owner": "不会强制指定联系人所有者(如果符合属性和轮询标准,仍可成为主持人)",
|
||||||
|
|||||||
@@ -2779,6 +2779,8 @@
|
|||||||
"routing_form_insights_booked_by": "預約者",
|
"routing_form_insights_booked_by": "預約者",
|
||||||
"routing_form_insights_created_at": "建立時間",
|
"routing_form_insights_created_at": "建立時間",
|
||||||
"routing_form_insights_booking_status": "預約狀態",
|
"routing_form_insights_booking_status": "預約狀態",
|
||||||
|
"routing_form_insights_booking_at": "預約時間",
|
||||||
|
"routing_form_insights_submitted_at": "提交時間",
|
||||||
"access_denied": "拒絕存取",
|
"access_denied": "拒絕存取",
|
||||||
"salesforce_route_to_owner": "如果聯絡人負責人可用,將成為Round Robin主持人",
|
"salesforce_route_to_owner": "如果聯絡人負責人可用,將成為Round Robin主持人",
|
||||||
"salesforce_do_not_route_to_owner": "不會強制指定聯絡人負責人(如果符合屬性和Round Robin條件,仍可成為主持人)",
|
"salesforce_do_not_route_to_owner": "不會強制指定聯絡人負責人(如果符合屬性和Round Robin條件,仍可成為主持人)",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ To test API in your local instance, you have the following pre-requisites:
|
|||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Clone the cal.com repository">
|
<Step title="Clone the cal.com repository">
|
||||||
Please close the cal.com repository. You can do it by following the instructions provided in [Installation](/developing/local-development)
|
Please clone the cal.com repository. You can do it by following the instructions provided in [Installation](/developing/local-development)
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Add license key in .env">
|
<Step title="Add license key in .env">
|
||||||
Add a staging license key that goes in as value for `CALCOM_LICENSE_KEY` in your root `.env` file.
|
Add a staging license key that goes in as value for `CALCOM_LICENSE_KEY` in your root `.env` file.
|
||||||
|
|||||||
@@ -2781,6 +2781,8 @@ checksums:
|
|||||||
routing_form_insights_booked_by: 7574cbcc0dd68a6469b6715452e035cc
|
routing_form_insights_booked_by: 7574cbcc0dd68a6469b6715452e035cc
|
||||||
routing_form_insights_created_at: f98a3c3bc73494df9bbc14e4e47a018c
|
routing_form_insights_created_at: f98a3c3bc73494df9bbc14e4e47a018c
|
||||||
routing_form_insights_booking_status: 08b945804d902b55184884401a840d57
|
routing_form_insights_booking_status: 08b945804d902b55184884401a840d57
|
||||||
|
routing_form_insights_booking_at: ca8a804eb7068845bede9adbb6bb7d3b
|
||||||
|
routing_form_insights_submitted_at: b95063c78796a32900c35743237512c4
|
||||||
access_denied: 589456a5c4103e29c44e9df16a4eaf82
|
access_denied: 589456a5c4103e29c44e9df16a4eaf82
|
||||||
salesforce_route_to_owner: 39a88a7afb1c1e8fb9215e27ae7a7033
|
salesforce_route_to_owner: 39a88a7afb1c1e8fb9215e27ae7a7033
|
||||||
salesforce_do_not_route_to_owner: bcae6e5615c17b809c2334a541f75e78
|
salesforce_do_not_route_to_owner: bcae6e5615c17b809c2334a541f75e78
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { BarList } from "@tremor/react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
@@ -6,6 +5,7 @@ import { trpc } from "@calcom/trpc";
|
|||||||
import { ToggleGroup } from "@calcom/ui";
|
import { ToggleGroup } from "@calcom/ui";
|
||||||
|
|
||||||
import { useFilterContext } from "../context/provider";
|
import { useFilterContext } from "../context/provider";
|
||||||
|
import { BarList } from "./tremor/BarList";
|
||||||
|
|
||||||
interface FormCardProps {
|
interface FormCardProps {
|
||||||
formName: string;
|
formName: string;
|
||||||
@@ -27,7 +27,7 @@ function FormCard({ formName, fields }: FormCardProps) {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-subtle h-[400px] w-full rounded-md border">
|
<div className="border-subtle w-full rounded-md border">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="bg-subtle border-subtle flex h-12 items-center border-b">
|
<div className="bg-subtle border-subtle flex h-12 items-center border-b">
|
||||||
<h3 className="text-default px-2 text-left align-middle font-medium">{formName}</h3>
|
<h3 className="text-default px-2 text-left align-middle font-medium">{formName}</h3>
|
||||||
@@ -40,7 +40,7 @@ function FormCard({ formName, fields }: FormCardProps) {
|
|||||||
onValueChange={(value) => value && setSelectedField(value)}
|
onValueChange={(value) => value && setSelectedField(value)}
|
||||||
/>
|
/>
|
||||||
{selectedFieldData && (
|
{selectedFieldData && (
|
||||||
<div className="scrollbar-thin mt-4 h-full overflow-y-auto">
|
<div className="scrollbar-thin mt-4 h-[400px] overflow-y-auto">
|
||||||
<BarList
|
<BarList
|
||||||
data={selectedFieldData}
|
data={selectedFieldData}
|
||||||
valueFormatter={(value: number) => value.toString()}
|
valueFormatter={(value: number) => value.toString()}
|
||||||
|
|||||||
@@ -17,7 +17,22 @@ import { useCopy } from "@calcom/lib/hooks/useCopy";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { BookingStatus } from "@calcom/prisma/enums";
|
import { BookingStatus } from "@calcom/prisma/enums";
|
||||||
import { trpc, type RouterOutputs } from "@calcom/trpc";
|
import { trpc, type RouterOutputs } from "@calcom/trpc";
|
||||||
import { DataTable, useFetchMoreOnBottomReached, Badge, Avatar, Icon } from "@calcom/ui";
|
import {
|
||||||
|
DataTable,
|
||||||
|
useFetchMoreOnBottomReached,
|
||||||
|
Badge,
|
||||||
|
Avatar,
|
||||||
|
Icon,
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardTrigger,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@calcom/ui";
|
||||||
import type { BadgeProps } from "@calcom/ui/components/badge/Badge";
|
import type { BadgeProps } from "@calcom/ui/components/badge/Badge";
|
||||||
|
|
||||||
import { useFilterContext } from "../context/provider";
|
import { useFilterContext } from "../context/provider";
|
||||||
@@ -36,7 +51,7 @@ function CellWithOverflowX({ children, className }: { children: React.ReactNode;
|
|||||||
return (
|
return (
|
||||||
<div className={classNames("group relative max-w-[200px]", className)}>
|
<div className={classNames("group relative max-w-[200px]", className)}>
|
||||||
<div
|
<div
|
||||||
className="no-scrollbar overflow-x-auto whitespace-nowrap"
|
className="no-scrollbar flex gap-1 overflow-x-auto whitespace-nowrap"
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
// Only add shadow if the scroll width is greater than 200px
|
// Only add shadow if the scroll width is greater than 200px
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@@ -51,7 +66,7 @@ function CellWithOverflowX({ children, className }: { children: React.ReactNode;
|
|||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute right-0 top-0 hidden h-full w-8 bg-gradient-to-l from-white to-transparent group-hover:from-gray-100" />
|
<div className="from-default absolute right-0 top-0 hidden h-full w-8 bg-gradient-to-l to-transparent " />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -92,7 +107,7 @@ function ResponseValueCell({ value, rowId }: { value: string[]; rowId: number })
|
|||||||
if (value.length === 0) return <div className="h-6 w-[200px]" />;
|
if (value.length === 0) return <div className="h-6 w-[200px]" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-[200px] flex-wrap gap-1">
|
<CellWithOverflowX className="flex w-[200px] gap-1">
|
||||||
{value.length > 2 ? (
|
{value.length > 2 ? (
|
||||||
<>
|
<>
|
||||||
{value.slice(0, 2).map((v: string, i: number) => (
|
{value.slice(0, 2).map((v: string, i: number) => (
|
||||||
@@ -100,18 +115,20 @@ function ResponseValueCell({ value, rowId }: { value: string[]; rowId: number })
|
|||||||
{v}
|
{v}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
<div className="group/badge relative">
|
<HoverCard>
|
||||||
<Badge variant="gray">+{value.length - 2}</Badge>
|
<HoverCardTrigger>
|
||||||
<div className="bg-default invisible absolute left-0 top-full z-20 translate-y-[-8px] rounded-md p-2 opacity-0 shadow-md transition-all duration-200 group-hover/badge:visible group-hover/badge:translate-y-0 group-hover/badge:opacity-100">
|
<Badge variant="gray">+{value.length - 2}</Badge>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardContent side="bottom" align="start" className="w-fit">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{value.slice(2).map((v: string, i: number) => (
|
{value.slice(2).map((v: string, i: number) => (
|
||||||
<span key={`${cellId}-overflow-${i}-${rowId}`} className="text-sm text-gray-600">
|
<span key={`${cellId}-overflow-${i}-${rowId}`} className="text-default text-sm">
|
||||||
{v}
|
{v}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</HoverCardContent>
|
||||||
</div>
|
</HoverCard>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
value.map((v: string, i: number) => (
|
value.map((v: string, i: number) => (
|
||||||
@@ -120,7 +137,7 @@ function ResponseValueCell({ value, rowId }: { value: string[]; rowId: number })
|
|||||||
</Badge>
|
</Badge>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</CellWithOverflowX>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,12 +175,16 @@ function BookingStatusCell({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group/booking_status relative flex items-center gap-2" key={`${cellId}-booking-${rowId}`}>
|
<HoverCard>
|
||||||
<Avatar size="xs" imageSrc={booking.user.avatarUrl ?? ""} alt={booking.user.name ?? ""} />
|
<HoverCardTrigger asChild>
|
||||||
<Link href={`/booking/${booking.uid}`}>
|
<div className="flex items-center gap-2" key={`${cellId}-booking-${rowId}`}>
|
||||||
<Badge variant={badgeVariant}>{dayjs(booking.createdAt).format("MMM D, YYYY HH:mm")}</Badge>
|
<Avatar size="xs" imageSrc={booking.user.avatarUrl ?? ""} alt={booking.user.name ?? ""} />
|
||||||
</Link>
|
<Link href={`/booking/${booking.uid}`}>
|
||||||
<div className="bg-default invisible absolute left-0 top-full z-20 translate-y-[-8px] rounded-md p-2 opacity-0 shadow-md transition-all duration-200 group-hover/booking_status:visible group-hover/booking_status:translate-y-0 group-hover/booking_status:opacity-100 ">
|
<Badge variant={badgeVariant}>{dayjs(booking.createdAt).format("MMM D, YYYY HH:mm")}</Badge>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardContent>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Avatar size="sm" imageSrc={booking.user.avatarUrl ?? ""} alt={booking.user.name ?? ""} />
|
<Avatar size="sm" imageSrc={booking.user.avatarUrl ?? ""} alt={booking.user.name ?? ""} />
|
||||||
@@ -188,11 +209,13 @@ function BookingStatusCell({
|
|||||||
<Badge variant={badgeVariant}>{bookingStatusToText(booking.status)}</Badge>
|
<Badge variant={badgeVariant}>{bookingStatusToText(booking.status)}</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</HoverCardContent>
|
||||||
</div>
|
</HoverCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RoutingFormTableType = ReturnType<typeof useReactTable<RoutingFormTableRow>>;
|
||||||
|
|
||||||
export function RoutingFormResponsesTable() {
|
export function RoutingFormResponsesTable() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { filter } = useFilterContext();
|
const { filter } = useFilterContext();
|
||||||
@@ -212,16 +235,17 @@ export function RoutingFormResponsesTable() {
|
|||||||
const initialConfigIsReady = !!(initialConfig?.teamId || initialConfig?.userId || initialConfig?.isAll);
|
const initialConfigIsReady = !!(initialConfig?.teamId || initialConfig?.userId || initialConfig?.isAll);
|
||||||
const [startDate, endDate] = dateRange;
|
const [startDate, endDate] = dateRange;
|
||||||
|
|
||||||
const { data: headers } = trpc.viewer.insights.routingFormResponsesHeaders.useQuery(
|
const { data: headers, isLoading: isHeadersLoading } =
|
||||||
{
|
trpc.viewer.insights.routingFormResponsesHeaders.useQuery(
|
||||||
teamId: selectedTeamId ?? undefined,
|
{
|
||||||
isAll: isAll ?? false,
|
teamId: selectedTeamId ?? undefined,
|
||||||
routingFormId: selectedRoutingFormId ?? undefined,
|
isAll: isAll ?? false,
|
||||||
},
|
routingFormId: selectedRoutingFormId ?? undefined,
|
||||||
{
|
},
|
||||||
enabled: initialConfigIsReady,
|
{
|
||||||
}
|
enabled: initialConfigIsReady,
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const { data, fetchNextPage, isFetching, hasNextPage } =
|
const { data, fetchNextPage, isFetching, hasNextPage } =
|
||||||
trpc.viewer.insights.routingFormResponses.useInfiniteQuery(
|
trpc.viewer.insights.routingFormResponses.useInfiniteQuery(
|
||||||
@@ -251,6 +275,7 @@ export function RoutingFormResponsesTable() {
|
|||||||
const totalFetched = flatData.length;
|
const totalFetched = flatData.length;
|
||||||
|
|
||||||
const processedData = useMemo(() => {
|
const processedData = useMemo(() => {
|
||||||
|
if (isHeadersLoading) return [];
|
||||||
return flatData.map((response) => {
|
return flatData.map((response) => {
|
||||||
const row: RoutingFormTableRow = {
|
const row: RoutingFormTableRow = {
|
||||||
id: response.id,
|
id: response.id,
|
||||||
@@ -283,7 +308,7 @@ export function RoutingFormResponsesTable() {
|
|||||||
|
|
||||||
return row;
|
return row;
|
||||||
});
|
});
|
||||||
}, [flatData, headers]);
|
}, [flatData, headers, isHeadersLoading]);
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<RoutingFormTableRow>();
|
const columnHelper = createColumnHelper<RoutingFormTableRow>();
|
||||||
|
|
||||||
@@ -298,6 +323,7 @@ export function RoutingFormResponsesTable() {
|
|||||||
return <BookedByCell attendees={row.routedToBooking?.attendees || []} rowId={row.id} />;
|
return <BookedByCell attendees={row.routedToBooking?.attendees || []} rowId={row.id} />;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
...(headers?.map((header) => {
|
...(headers?.map((header) => {
|
||||||
return columnHelper.accessor(header.id, {
|
return columnHelper.accessor(header.id, {
|
||||||
id: header.id,
|
id: header.id,
|
||||||
@@ -314,10 +340,9 @@ export function RoutingFormResponsesTable() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}) ?? []),
|
}) ?? []),
|
||||||
|
|
||||||
columnHelper.accessor("routedToBooking", {
|
columnHelper.accessor("routedToBooking", {
|
||||||
id: "bookingStatus",
|
id: "bookingStatus",
|
||||||
header: t("routing_form_insights_booking_status"),
|
header: t("routing_form_insights_booking_at"),
|
||||||
size: 250,
|
size: 250,
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<div className="max-w-[250px]">
|
<div className="max-w-[250px]">
|
||||||
@@ -330,6 +355,14 @@ export function RoutingFormResponsesTable() {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
columnHelper.accessor("createdAt", {
|
||||||
|
id: "submittedAt",
|
||||||
|
header: t("routing_form_insights_submitted_at"),
|
||||||
|
size: 250,
|
||||||
|
cell: (info) => (
|
||||||
|
<div className="max-w-[250px]">{dayjs(info.getValue()).format("MMM D, YYYY HH:mm")}</div>
|
||||||
|
),
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
[headers, t, copyToClipboard]
|
[headers, t, copyToClipboard]
|
||||||
);
|
);
|
||||||
@@ -353,6 +386,52 @@ export function RoutingFormResponsesTable() {
|
|||||||
totalDBRowCount
|
totalDBRowCount
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isHeadersLoading || (isFetching && !data)) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="grid h-[75dvh]"
|
||||||
|
style={{ gridTemplateRows: "auto 1fr auto", gridTemplateAreas: "'header' 'body' 'footer'" }}>
|
||||||
|
<div
|
||||||
|
className="scrollbar-thin border-subtle relative h-full overflow-auto rounded-md border"
|
||||||
|
style={{ gridArea: "body" }}>
|
||||||
|
<Table>
|
||||||
|
<TableHeader className="bg-subtle sticky top-0 z-10">
|
||||||
|
<TableRow>
|
||||||
|
{[...Array(4)].map((_, index) => (
|
||||||
|
<TableHead key={`skeleton-header-${index}`}>
|
||||||
|
<div className="bg-subtle h-4 w-[200px] animate-pulse rounded-md" />
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{[...Array(10)].map((_, rowIndex) => (
|
||||||
|
<TableRow key={`skeleton-row-${rowIndex}`}>
|
||||||
|
{[...Array(4)].map((_, colIndex) => (
|
||||||
|
<TableCell key={`skeleton-cell-${rowIndex}-${colIndex}`}>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"bg-subtle h-6 animate-pulse rounded-md",
|
||||||
|
colIndex === 0
|
||||||
|
? "w-[200px]"
|
||||||
|
: colIndex === 2
|
||||||
|
? "w-[250px]"
|
||||||
|
: colIndex === 3
|
||||||
|
? "w-[250px]"
|
||||||
|
: "w-[200px]"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<DataTable
|
<DataTable
|
||||||
|
|||||||
@@ -51,17 +51,8 @@ export const RoutingKPICards = () => {
|
|||||||
|
|
||||||
const categories: {
|
const categories: {
|
||||||
title: string;
|
title: string;
|
||||||
index:
|
index: "active" | "total_responses" | "total_responses_without_booking" | "total_responses_with_booking";
|
||||||
| "created"
|
|
||||||
| "active"
|
|
||||||
| "total_responses"
|
|
||||||
| "total_responses_without_booking"
|
|
||||||
| "total_responses_with_booking";
|
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
|
||||||
title: t("routing_forms_created"),
|
|
||||||
index: "created",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: t("routing_forms_total_responses"),
|
title: t("routing_forms_total_responses"),
|
||||||
index: "total_responses",
|
index: "total_responses",
|
||||||
@@ -84,7 +75,7 @@ export const RoutingKPICards = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Grid numColsSm={2} numColsLg={4} className="mt-4 gap-x-4 gap-y-4">
|
<Grid numColsSm={1} numColsLg={3} className="mt-4 gap-x-4 gap-y-4">
|
||||||
{categories.map((item) => (
|
{categories.map((item) => (
|
||||||
<CardInsights key={item.title}>
|
<CardInsights key={item.title}>
|
||||||
<Text className="text-default">{item.title}</Text>
|
<Text className="text-default">{item.title}</Text>
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ export { RecentFeedbackTable } from "./RecentFeedbackTable";
|
|||||||
export { HighestNoShowHostTable } from "./HighestNoShowHostTable";
|
export { HighestNoShowHostTable } from "./HighestNoShowHostTable";
|
||||||
export { HighestRatedMembersTable } from "./HighestRatedMembersTable";
|
export { HighestRatedMembersTable } from "./HighestRatedMembersTable";
|
||||||
export { LowestRatedMembersTable } from "./LowestRatedMembersTable";
|
export { LowestRatedMembersTable } from "./LowestRatedMembersTable";
|
||||||
export { RoutingFormResponsesTable } from "./RoutingFormResponsesTable";
|
export { RoutingFormResponsesTable, type RoutingFormTableType } from "./RoutingFormResponsesTable";
|
||||||
export { FailedBookingsByField } from "./FailedBookingsByField";
|
export { FailedBookingsByField } from "./FailedBookingsByField";
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
// Tremor BarList [v0.1.1]
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { classNames } from "@calcom/lib";
|
||||||
|
|
||||||
|
type Bar<T> = T & {
|
||||||
|
key?: string;
|
||||||
|
href?: string;
|
||||||
|
value: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const focusRing = [
|
||||||
|
// base
|
||||||
|
"outline outline-offset-2 outline-0 focus-visible:outline-2",
|
||||||
|
// outline color
|
||||||
|
"outline-brand",
|
||||||
|
];
|
||||||
|
|
||||||
|
interface BarListProps<T = unknown> extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
data: Bar<T>[];
|
||||||
|
valueFormatter?: (value: number) => string;
|
||||||
|
showAnimation?: boolean;
|
||||||
|
onValueChange?: (payload: Bar<T>) => void;
|
||||||
|
sortOrder?: "ascending" | "descending" | "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function BarListInner<T>(
|
||||||
|
{
|
||||||
|
data = [],
|
||||||
|
valueFormatter = (value) => value.toString(),
|
||||||
|
showAnimation = false,
|
||||||
|
onValueChange,
|
||||||
|
sortOrder = "descending",
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: BarListProps<T>,
|
||||||
|
forwardedRef: React.ForwardedRef<HTMLDivElement>
|
||||||
|
) {
|
||||||
|
const Component = onValueChange ? "button" : "div";
|
||||||
|
const sortedData = React.useMemo(() => {
|
||||||
|
if (sortOrder === "none") {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
return [...data].sort((a, b) => {
|
||||||
|
return sortOrder === "ascending" ? a.value - b.value : b.value - a.value;
|
||||||
|
});
|
||||||
|
}, [data, sortOrder]);
|
||||||
|
|
||||||
|
const widths = React.useMemo(() => {
|
||||||
|
const maxValue = Math.max(...sortedData.map((item) => item.value), 0);
|
||||||
|
return sortedData.map((item) => (item.value === 0 ? 0 : Math.max((item.value / maxValue) * 100, 2)));
|
||||||
|
}, [sortedData]);
|
||||||
|
|
||||||
|
const rowHeight = "h-8";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={forwardedRef}
|
||||||
|
className={classNames("flex justify-between space-x-6", className)}
|
||||||
|
aria-sort={sortOrder}
|
||||||
|
tremor-id="tremor-raw"
|
||||||
|
{...props}>
|
||||||
|
<div className="relative w-full space-y-1.5">
|
||||||
|
{sortedData.map((item, index) => (
|
||||||
|
<Component
|
||||||
|
key={item.key ?? item.name}
|
||||||
|
onClick={() => {
|
||||||
|
onValueChange?.(item);
|
||||||
|
}}
|
||||||
|
className={classNames(
|
||||||
|
// base
|
||||||
|
"group w-full rounded",
|
||||||
|
// focus
|
||||||
|
focusRing,
|
||||||
|
onValueChange
|
||||||
|
? [
|
||||||
|
"!-m-0 cursor-pointer",
|
||||||
|
// hover
|
||||||
|
"hover:bg-gray-50 hover:dark:bg-gray-900",
|
||||||
|
]
|
||||||
|
: ""
|
||||||
|
)}>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
// base
|
||||||
|
"flex items-center rounded transition-all",
|
||||||
|
rowHeight,
|
||||||
|
// background color
|
||||||
|
"bg-blue-200 dark:bg-blue-900",
|
||||||
|
onValueChange ? "group-hover:bg-blue-300 group-hover:dark:bg-blue-800" : "",
|
||||||
|
// margin and duration
|
||||||
|
{
|
||||||
|
"mb-0": index === sortedData.length - 1,
|
||||||
|
"duration-800": showAnimation,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
style={{ width: `${widths[index]}%` }}>
|
||||||
|
<div className={classNames("absolute left-2 flex max-w-full pr-2")}>
|
||||||
|
{item.href ? (
|
||||||
|
<a
|
||||||
|
href={item.href}
|
||||||
|
className={classNames(
|
||||||
|
// base
|
||||||
|
"truncate whitespace-nowrap rounded text-sm",
|
||||||
|
// text color
|
||||||
|
"text-default",
|
||||||
|
// hover
|
||||||
|
"hover:underline hover:underline-offset-2",
|
||||||
|
// focus
|
||||||
|
focusRing
|
||||||
|
)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
onClick={(event) => event.stopPropagation()}>
|
||||||
|
{item.name}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<p
|
||||||
|
className={classNames(
|
||||||
|
// base
|
||||||
|
"truncate whitespace-nowrap text-sm",
|
||||||
|
// text color
|
||||||
|
"text-default"
|
||||||
|
)}>
|
||||||
|
{item.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Component>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{sortedData.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={item.key ?? item.name}
|
||||||
|
className={classNames(
|
||||||
|
"flex items-center justify-end",
|
||||||
|
rowHeight,
|
||||||
|
index === sortedData.length - 1 ? "mb-0" : "mb-1.5"
|
||||||
|
)}>
|
||||||
|
<p
|
||||||
|
className={classNames(
|
||||||
|
// base
|
||||||
|
"truncate whitespace-nowrap text-sm leading-none",
|
||||||
|
// text color
|
||||||
|
"text-default"
|
||||||
|
)}>
|
||||||
|
{valueFormatter(item.value)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BarListInner.displayName = "BarList";
|
||||||
|
|
||||||
|
const BarList = React.forwardRef(BarListInner) as <T>(
|
||||||
|
p: BarListProps<T> & { ref?: React.ForwardedRef<HTMLDivElement> }
|
||||||
|
) => ReturnType<typeof BarListInner>;
|
||||||
|
|
||||||
|
export { BarList, type BarListProps };
|
||||||
@@ -108,9 +108,9 @@ export const FilterType = ({ showRoutingFilters = false }: { showRoutingFilters?
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="z-50 w-44">
|
<DropdownMenuContent className="z-50">
|
||||||
{filterOptions?.map((option) => (
|
{filterOptions?.map((option) => (
|
||||||
<DropdownMenuItem key={option.label} className="w-44">
|
<DropdownMenuItem key={option.label} className="w-full">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
type="button"
|
type="button"
|
||||||
StartIcon={option.StartIcon}
|
StartIcon={option.StartIcon}
|
||||||
@@ -124,9 +124,11 @@ export const FilterType = ({ showRoutingFilters = false }: { showRoutingFilters?
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
childrenClassName="w-full">
|
childrenClassName="w-full">
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between whitespace-normal">
|
||||||
{t(option.label)}
|
<span className="mr-2">{t(option.label)}</span>
|
||||||
{selectedFilter?.includes(option.value) && <Icon name="check" className="h-4 w-4" />}
|
{selectedFilter?.includes(option.value) && (
|
||||||
|
<Icon name="check" className="h-4 w-4 flex-shrink-0" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -376,7 +376,13 @@ export class ProfileRepository {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
members: {
|
members: {
|
||||||
|
distinct: ["role"],
|
||||||
select: membershipSelect,
|
select: membershipSelect,
|
||||||
|
where: {
|
||||||
|
accepted: true,
|
||||||
|
// Filter out memberships that are not owned by the user
|
||||||
|
user: { profiles: { some: { id } } },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -664,25 +664,18 @@ export class UserRepository {
|
|||||||
return !!teams.length;
|
return !!teams.length;
|
||||||
}
|
}
|
||||||
static async isAdminOrOwnerOfTeam({ userId, teamId }: { userId: number; teamId: number }) {
|
static async isAdminOrOwnerOfTeam({ userId, teamId }: { userId: number; teamId: number }) {
|
||||||
const team = await prisma.team.findUnique({
|
const isAdminOrOwnerOfTeam = await prisma.membership.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: teamId,
|
userId,
|
||||||
AND: [
|
teamId,
|
||||||
{
|
role: { in: [MembershipRole.ADMIN, MembershipRole.OWNER] },
|
||||||
members: {
|
accepted: true,
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
role: { in: [MembershipRole.ADMIN, MembershipRole.OWNER] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return !!team;
|
return !!isAdminOrOwnerOfTeam;
|
||||||
}
|
}
|
||||||
static async getTimeZoneAndDefaultScheduleId({ userId }: { userId: number }) {
|
static async getTimeZoneAndDefaultScheduleId({ userId }: { userId: number }) {
|
||||||
return await prisma.user.findUnique({
|
return await prisma.user.findUnique({
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "EventType_profileId_idx" ON "EventType"("profileId");
|
||||||
@@ -163,6 +163,7 @@ model EventType {
|
|||||||
@@unique([userId, parentId])
|
@@unique([userId, parentId])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@index([teamId])
|
@@index([teamId])
|
||||||
|
@@index([profileId])
|
||||||
@@index([scheduleId])
|
@@index([scheduleId])
|
||||||
@@index([secondaryEmailId])
|
@@index([secondaryEmailId])
|
||||||
@@index([parentId])
|
@@index([parentId])
|
||||||
|
|||||||
@@ -102,23 +102,16 @@ export async function getUserFromSession(ctx: TRPCContextInner, session: Maybe<S
|
|||||||
// This helps to prevent reaching the 4MB payload limit by avoiding base64 and instead passing the avatar url
|
// This helps to prevent reaching the 4MB payload limit by avoiding base64 and instead passing the avatar url
|
||||||
|
|
||||||
const locale = user?.locale ?? ctx.locale;
|
const locale = user?.locale ?? ctx.locale;
|
||||||
|
const { members = [], ..._organization } = user.profile?.organization || {};
|
||||||
const isOrgAdmin = !!user.profile?.organization?.members.filter(
|
const isOrgAdmin = members.some((member) => ["OWNER", "ADMIN"].includes(member.role));
|
||||||
(member) => (member.role === "ADMIN" || member.role === "OWNER") && member.userId === user.id
|
|
||||||
).length;
|
|
||||||
|
|
||||||
if (isOrgAdmin) {
|
if (isOrgAdmin) {
|
||||||
logger.debug("User is an org admin", safeStringify({ userId: user.id }));
|
logger.debug("User is an org admin", safeStringify({ userId: user.id }));
|
||||||
} else {
|
} else {
|
||||||
logger.debug("User is not an org admin", safeStringify({ userId: user.id }));
|
logger.debug("User is not an org admin", safeStringify({ userId: user.id }));
|
||||||
}
|
}
|
||||||
// Want to reduce the amount of data being sent
|
|
||||||
if (isOrgAdmin && user.profile?.organization?.members) {
|
|
||||||
user.profile.organization.members = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const organization = {
|
const organization = {
|
||||||
...user.profile?.organization,
|
..._organization,
|
||||||
id: user.profile?.organization?.id ?? null,
|
id: user.profile?.organization?.id ?? null,
|
||||||
isOrgAdmin,
|
isOrgAdmin,
|
||||||
metadata: orgMetadata,
|
metadata: orgMetadata,
|
||||||
|
|||||||
-1
@@ -214,7 +214,6 @@ export const findTeamMembersMatchingAttributeLogicHandler = async ({
|
|||||||
const timeBeforeCrm = performance.now();
|
const timeBeforeCrm = performance.now();
|
||||||
const contactOwnerEmail = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
const contactOwnerEmail = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||||
query: fromEntriesWithDuplicateKeys(urlSearchParamsToForward.entries()),
|
query: fromEntriesWithDuplicateKeys(urlSearchParamsToForward.entries()),
|
||||||
eventTypeId: eventType.id,
|
|
||||||
eventData: eventType,
|
eventData: eventType,
|
||||||
chosenRoute: route,
|
chosenRoute: route,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ function ColumnVisibilityButtonComponent<TData>(
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const allColumns = table.getAllLeafColumns();
|
const allColumns = table.getAllLeafColumns();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [visibleColumns, setVisibleColumns] = useState(
|
||||||
|
() => new Set(allColumns.filter((col) => col.getIsVisible()).map((col) => col.id))
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
@@ -59,15 +62,28 @@ function ColumnVisibilityButtonComponent<TData>(
|
|||||||
const canHide = column.getCanHide();
|
const canHide = column.getCanHide();
|
||||||
if (!column.columnDef.header || typeof column.columnDef.header !== "string" || !canHide)
|
if (!column.columnDef.header || typeof column.columnDef.header !== "string" || !canHide)
|
||||||
return null;
|
return null;
|
||||||
const isVisible = column.getIsVisible();
|
const isVisible = visibleColumns.has(column.id);
|
||||||
return (
|
return (
|
||||||
<CommandItem key={column.id} onSelect={() => column.toggleVisibility(!isVisible)}>
|
<CommandItem
|
||||||
|
key={column.id}
|
||||||
|
onSelect={() => {
|
||||||
|
column.toggleVisibility(!isVisible);
|
||||||
|
setVisibleColumns((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (isVisible) {
|
||||||
|
next.delete(column.id);
|
||||||
|
} else {
|
||||||
|
next.add(column.id);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"border-subtle mr-2 flex h-4 w-4 items-center justify-center rounded-sm border",
|
"border-subtle mr-2 flex h-4 w-4 items-center justify-center rounded-sm border",
|
||||||
isVisible ? "text-emphasis" : "opacity-50 [&_svg]:invisible"
|
isVisible ? "text-emphasis" : "opacity-50 [&_svg]:invisible"
|
||||||
)}>
|
)}>
|
||||||
<Icon name="check" className={classNames("h-4 w-4")} />
|
<Icon name="check" className={classNames("h-4 w-4", !isVisible && "invisible")} />
|
||||||
</div>
|
</div>
|
||||||
{column.columnDef.header}
|
{column.columnDef.header}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
@@ -78,7 +94,10 @@ function ColumnVisibilityButtonComponent<TData>(
|
|||||||
<CommandSeparator />
|
<CommandSeparator />
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => allColumns.forEach((column) => column.toggleVisibility(true))}
|
onSelect={() => {
|
||||||
|
allColumns.forEach((column) => column.toggleVisibility(true));
|
||||||
|
setVisibleColumns(new Set(allColumns.map((col) => col.id)));
|
||||||
|
}}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"w-full justify-center text-center",
|
"w-full justify-center text-center",
|
||||||
buttonClasses({ color: "secondary" })
|
buttonClasses({ color: "secondary" })
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
|
|||||||
{...props}
|
{...props}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"shadow-dropdown w-50 bg-default border-subtle relative z-10 ml-1.5 origin-top-right rounded-md border text-sm",
|
"shadow-dropdown bg-default border-subtle relative z-10 ml-1.5 origin-top-right rounded-md border text-sm",
|
||||||
"[&>*:first-child]:mt-1 [&>*:last-child]:mb-1",
|
"[&>*:first-child]:mt-1 [&>*:last-child]:mb-1",
|
||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { classNames } from "@calcom/lib";
|
||||||
|
|
||||||
|
const HoverCard = HoverCardPrimitive.Root;
|
||||||
|
|
||||||
|
const HoverCardTrigger = HoverCardPrimitive.Trigger;
|
||||||
|
|
||||||
|
const HoverCardContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||||
|
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||||
|
<HoverCardPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={classNames(
|
||||||
|
"bg-default text-default data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-subtle z-50 w-64 rounded-md border p-2 shadow-sm outline-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
||||||
@@ -170,6 +170,8 @@ export {
|
|||||||
CommandShortcut,
|
CommandShortcut,
|
||||||
} from "./components/command";
|
} from "./components/command";
|
||||||
|
|
||||||
|
export { HoverCard, HoverCardTrigger, HoverCardContent } from "./components/hover-card";
|
||||||
|
|
||||||
export { OrgBanner } from "./components/organization-banner";
|
export { OrgBanner } from "./components/organization-banner";
|
||||||
export type { OrgBannerProps } from "./components/organization-banner";
|
export type { OrgBannerProps } from "./components/organization-banner";
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user