diff --git a/apps/web/test/lib/generateCsv.test.ts b/apps/web/test/lib/generateCsv.test.ts
index 1d5b8b327f..d6422c0acc 100644
--- a/apps/web/test/lib/generateCsv.test.ts
+++ b/apps/web/test/lib/generateCsv.test.ts
@@ -67,6 +67,7 @@ describe("generate Csv for Org Users Table @test", () => {
completedOnboarding: true,
teams: [],
attributes: [],
+ lastActiveAt: "",
};
it("should throw if no headers", () => {
diff --git a/packages/features/auth/lib/getServerSession.ts b/packages/features/auth/lib/getServerSession.ts
index 36bf09d49d..fc0e5d5402 100644
--- a/packages/features/auth/lib/getServerSession.ts
+++ b/packages/features/auth/lib/getServerSession.ts
@@ -54,10 +54,11 @@ export async function getServerSession(options: {
return cachedSession;
}
- const userFromDb = await prisma.user.findUnique({
+ const userFromDb = await prisma.user.update({
where: {
email: token.email.toLowerCase(),
},
+ data: { lastActiveAt: new Date() },
});
if (!userFromDb) {
diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts
index eda60308f4..f7955d5556 100644
--- a/packages/features/bookings/lib/handleNewBooking.ts
+++ b/packages/features/bookings/lib/handleNewBooking.ts
@@ -37,6 +37,7 @@ import {
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
import { getFullName } from "@calcom/features/form-builder/utils";
+import { UsersRepository } from "@calcom/features/users/users.repository";
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import {
@@ -986,6 +987,11 @@ async function handler(
originalRescheduledBooking,
});
+ if (booking?.userId) {
+ const usersRepository = new UsersRepository();
+ await usersRepository.updateLastActiveAt(booking.userId);
+ }
+
evt.uid = booking?.uid ?? null;
evt.oneTimePassword = booking?.oneTimePassword ?? null;
diff --git a/packages/features/ee/teams/components/MemberList.tsx b/packages/features/ee/teams/components/MemberList.tsx
index eec5f4edfd..1144b65983 100644
--- a/packages/features/ee/teams/components/MemberList.tsx
+++ b/packages/features/ee/teams/components/MemberList.tsx
@@ -378,6 +378,11 @@ export default function MemberList(props: Props) {
return filterValue.includes(rows.getValue(id));
},
},
+ {
+ id: "lastActiveAt",
+ header: "Last Active",
+ cell: ({ row }) =>
{row.original.lastActiveAt}
,
+ },
{
id: "actions",
cell: ({ row }) => {
diff --git a/packages/features/users/components/UserTable/UserListTable.tsx b/packages/features/users/components/UserTable/UserListTable.tsx
index 55504df7c4..d58cc00007 100644
--- a/packages/features/users/components/UserTable/UserListTable.tsx
+++ b/packages/features/users/components/UserTable/UserListTable.tsx
@@ -342,6 +342,11 @@ export function UserListTable() {
},
},
...generateAttributeColumns(),
+ {
+ id: "lastActiveAt",
+ header: "Last Active",
+ cell: ({ row }) => {row.original.lastActiveAt}
,
+ },
{
id: "actions",
enableHiding: false,
diff --git a/packages/features/users/components/UserTable/types.ts b/packages/features/users/components/UserTable/types.ts
index dfbff32eb6..ef64f8c16c 100644
--- a/packages/features/users/components/UserTable/types.ts
+++ b/packages/features/users/components/UserTable/types.ts
@@ -10,6 +10,7 @@ export interface UserTableUser {
accepted: boolean;
disableImpersonation: boolean;
completedOnboarding: boolean;
+ lastActiveAt: string;
teams: {
id: number;
name: string;
diff --git a/packages/features/users/users.repository.interface.ts b/packages/features/users/users.repository.interface.ts
new file mode 100644
index 0000000000..9e9f886e37
--- /dev/null
+++ b/packages/features/users/users.repository.interface.ts
@@ -0,0 +1,5 @@
+import type { User } from "@prisma/client";
+
+export interface IUsersRepository {
+ updateLastActiveAt(userId: number): Promise;
+}
diff --git a/packages/features/users/users.repository.ts b/packages/features/users/users.repository.ts
new file mode 100644
index 0000000000..279c4e55d6
--- /dev/null
+++ b/packages/features/users/users.repository.ts
@@ -0,0 +1,20 @@
+import { captureException } from "@sentry/nextjs";
+
+import db from "@calcom/prisma";
+
+import type { IUsersRepository } from "./users.repository.interface";
+
+export class UsersRepository implements IUsersRepository {
+ async updateLastActiveAt(userId: number) {
+ try {
+ const user = await db.user.update({
+ where: { id: userId },
+ data: { lastActiveAt: new Date() },
+ });
+ return user;
+ } catch (err) {
+ captureException(err);
+ throw err;
+ }
+ }
+}
diff --git a/packages/prisma/migrations/20241023041541_add_user_last_active_at/migration.sql b/packages/prisma/migrations/20241023041541_add_user_last_active_at/migration.sql
new file mode 100644
index 0000000000..6b3a05afea
--- /dev/null
+++ b/packages/prisma/migrations/20241023041541_add_user_last_active_at/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "users" ADD COLUMN "lastActiveAt" TIMESTAMP(3);
diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma
index 509690a268..0d5116740e 100644
--- a/packages/prisma/schema.prisma
+++ b/packages/prisma/schema.prisma
@@ -277,6 +277,7 @@ model User {
appTheme String?
createdDate DateTime @default(now()) @map(name: "created")
trialEndsAt DateTime?
+ lastActiveAt DateTime?
eventTypes EventType[] @relation("user_eventtype")
credentials Credential[]
teams Membership[]
diff --git a/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts b/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts
index 54f0170bac..4a5c7ed797 100644
--- a/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts
+++ b/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts
@@ -106,6 +106,7 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => {
timeZone: true,
disableImpersonation: true,
completedOnboarding: true,
+ lastActiveAt: true,
teams: {
select: {
team: {
@@ -164,6 +165,13 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => {
accepted: membership.accepted,
disableImpersonation: user.disableImpersonation,
completedOnboarding: user.completedOnboarding,
+ lastActiveAt: membership.user.lastActiveAt
+ ? new Intl.DateTimeFormat(ctx.user.locale, {
+ timeZone: ctx.user.timeZone,
+ })
+ .format(membership.user.lastActiveAt)
+ .toLowerCase()
+ : null,
avatarUrl: user.avatarUrl,
teams: user.teams
.filter((team) => team.team.id !== organizationId) // In this context we dont want to return the org team
diff --git a/packages/trpc/server/routers/viewer/teams/listMembers.handler.ts b/packages/trpc/server/routers/viewer/teams/listMembers.handler.ts
index f1dd2ae68d..ffa22a4bbb 100644
--- a/packages/trpc/server/routers/viewer/teams/listMembers.handler.ts
+++ b/packages/trpc/server/routers/viewer/teams/listMembers.handler.ts
@@ -25,6 +25,7 @@ const userSelect = Prisma.validator()({
id: true,
bio: true,
disableImpersonation: true,
+ lastActiveAt: true,
});
export const listMembersHandler = async ({ ctx, input }: ListMembersHandlerOptions) => {
@@ -92,6 +93,13 @@ export const listMembersHandler = async ({ ctx, input }: ListMembersHandlerOptio
disableImpersonation: user.disableImpersonation,
bookerUrl: getBookerBaseUrlSync(profile?.organization?.slug || ""),
teamId: member.teamId,
+ lastActiveAt: member.user.lastActiveAt
+ ? new Intl.DateTimeFormat(ctx.user.locale, {
+ timeZone: ctx.user.timeZone,
+ })
+ .format(member.user.lastActiveAt)
+ .toLowerCase()
+ : null,
};
})
);