From f628dc83a805dbd495e0b7836153f2614a3f7eae Mon Sep 17 00:00:00 2001 From: Nizzy <140507264+nizzyabi@users.noreply.github.com> Date: Fri, 10 Jan 2025 18:51:27 -0500 Subject: [PATCH] detailed customer card (#18511) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Omar López --- apps/web/app/api/customer-card/route.ts | 185 ++--------- .../pages/api/plain/customer-card-display.ts | 295 ++++++++++++++++-- .../pages/api/plain/example-cards/index.ts | 9 +- packages/lib/server/repository/user.ts | 3 + 4 files changed, 302 insertions(+), 190 deletions(-) diff --git a/apps/web/app/api/customer-card/route.ts b/apps/web/app/api/customer-card/route.ts index 0d89278cea..117e03a45f 100644 --- a/apps/web/app/api/customer-card/route.ts +++ b/apps/web/app/api/customer-card/route.ts @@ -10,12 +10,18 @@ import { userMetadata } from "@calcom/prisma/zod-utils"; const inputSchema = z.object({ customer: z.object({ + name: z.string().optional(), email: z.string().email(), username: z.string().optional(), timeZone: z.string().optional(), emailVerified: z.boolean().optional(), identityProvider: z.string().optional(), twoFactorEnabled: z.boolean().optional(), + lastActiveAt: z.string().optional(), + teamName: z.string().optional(), + teamSlug: z.string().optional(), + isOrganization: z.boolean().optional(), + stripeCustomerId: z.string().optional(), }), cardKeys: z.array(z.string()), }); @@ -53,168 +59,10 @@ async function handler(request: Request) { }, }, { - componentRow: { - rowMainContent: [ - { - componentText: { - text: "Email", - textColor: "MUTED", - }, - }, - ], - rowAsideContent: [ - { - componentText: { - text: customer.email || "Unknown", - }, - }, - ], - }, - }, - { - componentSpacer: { - spacerSize: "M", - }, - }, - { - componentRow: { - rowMainContent: [ - { - componentText: { - text: "Email Verified?", - textColor: "MUTED", - }, - }, - ], - rowAsideContent: [ - { - componentBadge: { - badgeLabel: customer.emailVerified ? "Yes" : "No", - badgeColor: customer.emailVerified ? "GREEN" : "RED", - }, - }, - ], - }, - }, - { - componentSpacer: { - spacerSize: "M", - }, - }, - { - componentRow: { - rowMainContent: [ - { - componentText: { - text: "Username", - textColor: "MUTED", - }, - }, - ], - rowAsideContent: [ - { - componentText: { - text: customer.username || "Unknown", - }, - }, - ], - }, - }, - { - componentSpacer: { - spacerSize: "M", - }, - }, - { - componentRow: { - rowMainContent: [ - { - componentText: { - text: "User ID", - textColor: "MUTED", - }, - }, - ], - rowAsideContent: [ - { - componentText: { - text: "Unknown", - }, - }, - ], - }, - }, - { - componentSpacer: { - spacerSize: "M", - }, - }, - { - componentRow: { - rowMainContent: [ - { - componentText: { - text: "Time Zone", - textColor: "MUTED", - }, - }, - ], - rowAsideContent: [ - { - componentText: { - text: customer.timeZone || "Unknown", - }, - }, - ], - }, - }, - { - componentSpacer: { - spacerSize: "M", - }, - }, - { - componentRow: { - rowMainContent: [ - { - componentText: { - text: "Two Factor Enabled?", - textColor: "MUTED", - }, - }, - ], - rowAsideContent: [ - { - componentBadge: { - badgeLabel: customer.twoFactorEnabled ? "Yes" : "No", - badgeColor: customer.twoFactorEnabled ? "GREEN" : "RED", - }, - }, - ], - }, - }, - { - componentSpacer: { - spacerSize: "M", - }, - }, - { - componentRow: { - rowMainContent: [ - { - componentText: { - text: "Identity Provider", - textColor: "MUTED", - }, - }, - ], - rowAsideContent: [ - { - componentText: { - text: customer.identityProvider || "Unknown", - }, - }, - ], + componentText: { + textSize: "L", + textColor: "MUTED", + text: "User does not exist!", }, }, ], @@ -223,18 +71,29 @@ async function handler(request: Request) { }); } + // Fetch team details including userId and team name + const teamMemberships = await UserRepository.findTeamsByUserId({ userId: user.id }); + const firstTeam = teamMemberships.teams[0] ?? null; + // Parse user metadata const parsedMetadata = userMetadata.parse(user.metadata); const cards = await Promise.all( cardExamples.map(async (cardFn) => { return cardFn( + user.name || "Unknown", user.email, user.id.toString(), user.username || "Unknown", user.timeZone, user.emailVerified, - user.twoFactorEnabled + user.twoFactorEnabled, + user.identityProvider, + user.lastActiveAt, + firstTeam?.name || "Unknown", + firstTeam?.slug || "Unknown", + firstTeam?.isOrganization || false, + (user.metadata as { stripeCustomerId?: string })?.stripeCustomerId || "Unknown" ); }) ); diff --git a/apps/web/pages/api/plain/customer-card-display.ts b/apps/web/pages/api/plain/customer-card-display.ts index 3a25e1bd2b..eb3c76d5e9 100644 --- a/apps/web/pages/api/plain/customer-card-display.ts +++ b/apps/web/pages/api/plain/customer-card-display.ts @@ -1,18 +1,63 @@ /* eslint-disable import/no-anonymous-default-export */ import type { Card } from "@pages/api/plain"; +import dayjs from "@calcom/dayjs"; + export default ( + name: string, email: string, id: string, username: string, timeZone: string, emailVerified: Date | null, - twoFactorEnabled: boolean | null + twoFactorEnabled: boolean | null, + identityProvider: string | null, + lastActiveAt: Date | null, + teamName: string | null, + teamSlug: string | null, + isOrganization: boolean | null, + stripeCustomerId: string | null ): Card => { return { key: "customer-card", timeToLiveSeconds: null, components: [ + { + componentSpacer: { + spacerSize: "M", + }, + }, + { + componentText: { + text: "Basic Information", + textColor: "NORMAL", + textSize: "L", + }, + }, + { + componentSpacer: { + spacerSize: "M", + }, + }, + { + componentRow: { + rowMainContent: [ + { + componentText: { + text: "Name", + textColor: "MUTED", + }, + }, + ], + rowAsideContent: [ + { + componentText: { + text: name || "Unknown", + }, + }, + ], + }, + }, { componentSpacer: { spacerSize: "M", @@ -42,31 +87,6 @@ export default ( spacerSize: "M", }, }, - { - componentRow: { - rowMainContent: [ - { - componentText: { - text: "Email Verified?", - textColor: "MUTED", - }, - }, - ], - rowAsideContent: [ - { - componentBadge: { - badgeLabel: emailVerified === null ? "No" : emailVerified ? "Yes" : "No", - badgeColor: emailVerified === null ? "RED" : emailVerified ? "GREEN" : "RED", - }, - }, - ], - }, - }, - { - componentSpacer: { - spacerSize: "M", - }, - }, { componentRow: { rowMainContent: [ @@ -115,6 +135,24 @@ export default ( spacerSize: "M", }, }, + { + componentDivider: { + dividerSpacingSize: "M", + }, + }, + // Account Settings Section Title + { + componentText: { + text: "Account Settings", + textColor: "NORMAL", + textSize: "L", + }, + }, + { + componentSpacer: { + spacerSize: "M", + }, + }, { componentRow: { rowMainContent: [ @@ -139,6 +177,211 @@ export default ( spacerSize: "M", }, }, + { + componentRow: { + rowMainContent: [ + { + componentText: { + text: "Identity Provider", + textColor: "MUTED", + }, + }, + ], + rowAsideContent: [ + { + componentText: { + text: identityProvider || "Unknown", + }, + }, + ], + }, + }, + { + componentSpacer: { + spacerSize: "M", + }, + }, + { + componentRow: { + rowMainContent: [ + { + componentText: { + text: "Last Active At", + textColor: "MUTED", + }, + }, + ], + rowAsideContent: [ + { + componentText: { + text: lastActiveAt ? dayjs(lastActiveAt).fromNow() : "Unknown", + }, + }, + ], + }, + }, + { + componentSpacer: { + spacerSize: "M", + }, + }, + { + componentDivider: { + dividerSpacingSize: "M", + }, + }, + // Team Information Section Title + { + componentText: { + text: "Team Information", + textColor: "NORMAL", + textSize: "L", + }, + }, + { + componentSpacer: { + spacerSize: "M", + }, + }, + { + componentRow: { + rowMainContent: [ + { + componentText: { + text: "Team Name", + textColor: "MUTED", + }, + }, + ], + rowAsideContent: [ + { + componentText: { + text: teamName || "Unknown", + }, + }, + ], + }, + }, + { + componentSpacer: { + spacerSize: "M", + }, + }, + { + componentRow: { + rowMainContent: [ + { + componentText: { + text: "Team Slug", + textColor: "MUTED", + }, + }, + ], + rowAsideContent: [ + { + componentText: { + text: teamSlug || "Unknown", + }, + }, + ], + }, + }, + { + componentSpacer: { + spacerSize: "M", + }, + }, + { + componentRow: { + rowMainContent: [ + { + componentText: { + text: "Is Organization", + textColor: "MUTED", + }, + }, + ], + rowAsideContent: [ + { + componentBadge: { + badgeLabel: isOrganization ? "Yes" : "No", + badgeColor: isOrganization ? "GREEN" : "RED", + }, + }, + ], + }, + }, + { + componentSpacer: { + spacerSize: "M", + }, + }, + { + componentDivider: { + dividerSpacingSize: "M", + }, + }, + { + componentText: { + text: "Security & Billing", + textColor: "NORMAL", + textSize: "M", + }, + }, + { + componentSpacer: { + spacerSize: "M", + }, + }, + { + componentRow: { + rowMainContent: [ + { + componentText: { + text: "Stripe Customer ID", + textColor: "MUTED", + }, + }, + ], + rowAsideContent: [ + { + componentText: { + text: stripeCustomerId || "Unknown", + }, + }, + ], + }, + }, + { + componentSpacer: { + spacerSize: "M", + }, + }, + { + componentRow: { + rowMainContent: [ + { + componentText: { + text: "Email Verified?", + textColor: "MUTED", + }, + }, + ], + rowAsideContent: [ + { + componentBadge: { + badgeLabel: emailVerified === null ? "No" : emailVerified ? "Yes" : "No", + badgeColor: emailVerified === null ? "RED" : emailVerified ? "GREEN" : "RED", + }, + }, + ], + }, + }, + { + componentSpacer: { + spacerSize: "M", + }, + }, { componentRow: { rowMainContent: [ diff --git a/apps/web/pages/api/plain/example-cards/index.ts b/apps/web/pages/api/plain/example-cards/index.ts index 5002279c40..ca688c9c8e 100644 --- a/apps/web/pages/api/plain/example-cards/index.ts +++ b/apps/web/pages/api/plain/example-cards/index.ts @@ -2,10 +2,17 @@ import type { Card } from "@pages/api/plain"; import customerCardDisplay from "@pages/api/plain/customer-card-display"; export const cardExamples: (( + name: string, email: string, id: string, username: string, timeZone: string, emailVerified: Date | null, - twoFactorEnabled: boolean | null + twoFactorEnabled: boolean | null, + identityProvider: string | null, + lastActiveAt: Date | null, + teamName: string | null, + teamSlug: string | null, + isOrganization: boolean | null, + stripeCustomerId: string | null ) => Card)[] = [customerCardDisplay]; diff --git a/packages/lib/server/repository/user.ts b/packages/lib/server/repository/user.ts index 89104f72e3..6790097004 100644 --- a/packages/lib/server/repository/user.ts +++ b/packages/lib/server/repository/user.ts @@ -72,6 +72,9 @@ const userSelect = Prisma.validator()({ movedToProfileId: true, metadata: true, isPlatformManaged: true, + lastActiveAt: true, + identityProvider: true, + teams: true, }); export type UserWithLegacySelectedCalendars = TUser & {