Skip to content

feat: add Client Booking History Dashboard — view all unique clients with booking history#29775

Open
prathamesh04 wants to merge 4 commits into
calcom:mainfrom
prathamesh04:feat/clients-booking-history-dashboard
Open

feat: add Client Booking History Dashboard — view all unique clients with booking history#29775
prathamesh04 wants to merge 4 commits into
calcom:mainfrom
prathamesh04:feat/clients-booking-history-dashboard

Conversation

@prathamesh04

Copy link
Copy Markdown

What does this PR do?

Adds a Client Booking History Dashboard — a feature requested in #29772 that lets hosts view all unique clients who have booked with them, with booking history and total spend.

This is a feature gap found by comparing cal.com with competitors like Acuity Scheduling (Client Management dashboard) and Calendly (Contact profile view). Service businesses (coaches, therapists, consultants) need a consolidated client view.

Changes

New tRPC router (viewer.clients):

  • list — returns all unique clients aggregated by attendee email from the host's bookings, with total booking count and total spend
  • get — returns a single client's booking history with event type, status, dates, and payment info

New pages:

  • /bookings/clients — client list table with search, showing name, email, total bookings, total spent, last booking date
  • /bookings/clients/[email] — client detail with booking history timeline/table

Navigation:

  • "Clients" added as a sub-item under "Bookings" in the sidebar

No new database tables — uses existing AttendeeBookingPayment relations

Type of change

  • New feature (non-breaking)

How should this be tested?

  1. Log in as a host who has bookings
  2. Navigate to /bookings/clients via sidebar or URL
  3. See all unique clients with their stats
  4. Click a client to see their full booking history
  5. Use the search box to filter by name or email

Checklist

  • I have self-reviewed the code
  • Changeset added
  • Tests added
  • No new database tables needed

Closes #29772

Previously, when DATABASE_URL was not set, the Prisma client would
attempt to connect with an empty string, resulting in a cryptic
connection error. This change adds an early validation that throws
a descriptive error message guiding the user to set the environment
variable.

Closes calcom#24485
The sticky 'Back / Pay to book' bar on the booking confirmation modal
was overlapping form fields on mobile devices, preventing users from
filling in phone number and notes fields.

Replaced pb-0 with pb-24 on the modal DialogContent to add sufficient
bottom padding so the form content scrolls above the sticky bar.

Fixes calcom#29265
@github-actions

Copy link
Copy Markdown
Contributor

Welcome to Cal.diy, @prathamesh04! Thanks for opening this pull request.

A few things to keep in mind:

  • This is Cal.diy, not Cal.com. Cal.diy is a community-driven, fully open-source fork of Cal.com licensed under MIT. Your changes here will be part of Cal.diy — they will not be deployed to the Cal.com production app.
  • Please review our Contributing Guidelines if you haven't already.
  • Make sure your PR title follows the Conventional Commits format.

A maintainer will review your PR soon. Thanks for contributing!

@prathamesh04

Copy link
Copy Markdown
Author

@bandhan-majumder Could you please review this PR? It implements a Client Booking History Dashboard (requested in #29772) — aggregated view of all unique clients from existing bookings, with per-client booking history and total spend. No new database tables needed.

Testing/Screenshot:
Screencast from 2026-07-14 13-35-10.webm

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds authenticated client listing and detail pages with searchable client aggregation, booking history, payment totals, localization, and navigation access. Adds tRPC schemas, handlers, and router wiring for client data. Updates multi-seat payment selection to match seat attendees, records attendee data in mock payments, and increases mobile modal bottom padding. Adds release metadata and enforces DATABASE_URL during Prisma initialization.

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR covers the client dashboard, but the list view omits the required first booking date, so it does not fully satisfy #29772. Add first booking date to the client list view and ensure the detail table exposes booking UID links, dates, event type, status, and paid amount.
Out of Scope Changes check ⚠️ Warning Several unrelated changes are included, such as mobile booking fixes, Prisma env enforcement, and extra changeset entries not needed for the client dashboard. Split unrelated fixes and release-note edits into separate PRs, keeping this PR focused on the client booking history dashboard.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly names the new Client Booking History Dashboard and matches the main feature in the changeset.
Description check ✅ Passed The description matches the client dashboard feature and its list/detail views, router, and navigation changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (2)
apps/web/modules/clients/views/clients-listing-view.tsx (1)

68-72: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider using Intl.NumberFormat for currency formatting.

Using Intl.NumberFormat automatically handles locale-specific formatting such as thousands separators, providing a more robust display than .toFixed(2).

♻️ Proposed refactor
-                  <Cell>
-                    {client.totalSpent > 0
-                      ? `${(client.totalSpent / 100).toFixed(2)} ${client.currency.toUpperCase()}`
-                      : "—"}
-                  </Cell>
+                  <Cell>
+                    {client.totalSpent > 0
+                      ? new Intl.NumberFormat(undefined, { style: "currency", currency: client.currency }).format(client.totalSpent / 100)
+                      : "—"}
+                  </Cell>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/modules/clients/views/clients-listing-view.tsx` around lines 68 -
72, Update the currency display in the clients listing cell to use
Intl.NumberFormat with currency formatting instead of manual division and
toFixed(2). Preserve the existing zero-or-negative fallback of "—", and format
positive totalSpent values using client.currency and the appropriate
locale-aware currency options.
apps/web/modules/clients/views/client-detail-view.tsx (1)

93-95: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider using Intl.NumberFormat for currency formatting.

Using Intl.NumberFormat automatically handles locale-specific formatting such as thousands separators, providing a more robust display than .toFixed(2).

♻️ Proposed refactor
-                    <Cell className="text-sm text-subtle">
-                      {paidAmount > 0 ? `${(paidAmount / 100).toFixed(2)} ${currency.toUpperCase()}` : "—"}
-                    </Cell>
+                    <Cell className="text-sm text-subtle">
+                      {paidAmount > 0 ? new Intl.NumberFormat(undefined, { style: "currency", currency }).format(paidAmount / 100) : "—"}
+                    </Cell>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/modules/clients/views/client-detail-view.tsx` around lines 93 - 95,
Update the paidAmount display in the client detail view to use Intl.NumberFormat
for currency formatting instead of toFixed(2) and manual currency text. Preserve
the existing "—" output for zero or negative amounts, and format positive values
with the current currency.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx`:
- Around line 228-234: Validate emailCandidates as strings before invoking
toLowerCase in the seat attendee matching logic. Update the filtering around the
candidate construction to retain only non-empty string values, then preserve the
existing case-insensitive comparison against seat.attendee.email.

In `@apps/web/modules/clients/views/client-detail-view.tsx`:
- Around line 46-51: Add currency context to the client detail data used by the
totalSpent display, preferably by extending the viewer.clients.get response with
a client currency, then format the value in the total_spent block using that
currency. If the response cannot provide it, derive the currency from booking
payments before rendering; ensure the displayed amount is never
currency-ambiguous.

In `@apps/web/modules/clients/views/clients-listing-view.tsx`:
- Around line 22-30: Update the clients query in
apps/web/modules/clients/views/clients-listing-view.tsx:22-30 to destructure
isPending (or isLoading), then update the empty-results rendering at
apps/web/modules/clients/views/clients-listing-view.tsx:43-45 to check that
loading state first and show the existing loading indicator before evaluating
clients.length === 0.

In `@packages/app-store/mock-payment-app/lib/PaymentService.ts`:
- Around line 12-14: Update the mock payment-service method signature to allow
bookerName to be null while keeping bookerEmail required. In the corresponding
implementation and stored payment metadata handling, preserve the provided
attendee email instead of defaulting it to an empty string.

In `@packages/trpc/server/routers/viewer/clients/get.handler.ts`:
- Around line 69-70: Update the total-spend reductions in get.handler.ts lines
69-70 and list.handler.ts lines 79-80 to include only successful, non-refunded
payments; preserve the existing payment amount aggregation for eligible payments
in both handlers.

In `@packages/trpc/server/routers/viewer/clients/list.handler.ts`:
- Around line 24-31: Update the client history query around the searchTerm
filter so search first resolves matching client emails, then aggregates all
booking and attendee records for those emails. Move the name/email filtering out
of the pre-grouped attendee-row scope, preserving complete booking counts,
spend, and date ranges for every matched email.
- Around line 19-58: Update the client-list query flow around attendeesRaw so
pagination selects distinct normalized client identifiers in the database before
loading attendee, booking, and payment aggregates. Apply the existing limit and
offset to that identifier query, then restrict the aggregate fetch to only those
identifiers while preserving the current search and booking filters; remove any
in-memory slicing that paginates the fully materialized attendee results.

In `@packages/trpc/server/routers/viewer/clients/list.schema.ts`:
- Around line 5-6: Update the pagination schema fields limit and cursor to
validate integer values in addition to their existing bounds and optionality.
Ensure limit remains within 1–100 with a default of 50, and cursor continues to
allow nullish values while rejecting negative or fractional inputs.

---

Nitpick comments:
In `@apps/web/modules/clients/views/client-detail-view.tsx`:
- Around line 93-95: Update the paidAmount display in the client detail view to
use Intl.NumberFormat for currency formatting instead of toFixed(2) and manual
currency text. Preserve the existing "—" output for zero or negative amounts,
and format positive values with the current currency.

In `@apps/web/modules/clients/views/clients-listing-view.tsx`:
- Around line 68-72: Update the currency display in the clients listing cell to
use Intl.NumberFormat with currency formatting instead of manual division and
toFixed(2). Preserve the existing zero-or-negative fallback of "—", and format
positive totalSpent values using client.currency and the appropriate
locale-aware currency options.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ac8cb74d-47d1-4ecc-bfb7-e1d6fde41003

📥 Commits

Reviewing files that changed from the base of the PR and between f004349 and a74d0cc.

📒 Files selected for processing (19)
  • .changeset/fix-clients-dashboard.md
  • .changeset/fix-mobile-sticky-bar-overlap.md
  • .changeset/fix-multi-seat-payment-per-attendee.md
  • apps/web/app/(use-page-wrapper)/(main-nav)/bookings/clients/[email]/page.tsx
  • apps/web/app/(use-page-wrapper)/(main-nav)/bookings/clients/page.tsx
  • apps/web/modules/bookings/components/BookEventForm/BookFormAsModal.tsx
  • apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx
  • apps/web/modules/clients/views/client-detail-view.tsx
  • apps/web/modules/clients/views/clients-listing-view.tsx
  • apps/web/modules/shell/navigation/Navigation.tsx
  • packages/app-store/mock-payment-app/lib/PaymentService.ts
  • packages/i18n/locales/en/common.json
  • packages/prisma/index.ts
  • packages/trpc/server/routers/viewer/_router.tsx
  • packages/trpc/server/routers/viewer/clients/_router.tsx
  • packages/trpc/server/routers/viewer/clients/get.handler.ts
  • packages/trpc/server/routers/viewer/clients/get.schema.ts
  • packages/trpc/server/routers/viewer/clients/list.handler.ts
  • packages/trpc/server/routers/viewer/clients/list.schema.ts

Comment on lines +228 to +234
const emailCandidates = [
data?.bookerEmail,
metadata?.bookerEmail,
data?.buyerEmail,
data?.customerEmail,
].filter(Boolean) as string[];
return emailCandidates.some((e) => e.toLowerCase() === seat.attendee.email.toLowerCase());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Validate email candidates before calling toLowerCase().

Payment data is arbitrary JSON, so .filter(Boolean) as string[] still permits truthy numbers or objects and can crash this page.

Proposed fix
         const emailCandidates = [
           data?.bookerEmail,
           metadata?.bookerEmail,
           data?.buyerEmail,
           data?.customerEmail,
-        ].filter(Boolean) as string[];
+        ].filter((value): value is string => typeof value === "string");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const emailCandidates = [
data?.bookerEmail,
metadata?.bookerEmail,
data?.buyerEmail,
data?.customerEmail,
].filter(Boolean) as string[];
return emailCandidates.some((e) => e.toLowerCase() === seat.attendee.email.toLowerCase());
const emailCandidates = [
data?.bookerEmail,
metadata?.bookerEmail,
data?.buyerEmail,
data?.customerEmail,
].filter((value): value is string => typeof value === "string");
return emailCandidates.some((e) => e.toLowerCase() === seat.attendee.email.toLowerCase());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx`
around lines 228 - 234, Validate emailCandidates as strings before invoking
toLowerCase in the seat attendee matching logic. Update the filtering around the
candidate construction to retain only non-empty string values, then preserve the
existing case-insensitive comparison against seat.attendee.email.

Comment on lines +46 to +51
<div>
<span className="text-subtle text-xs">{t("total_spent")}</span>
<p className="font-medium">
{client.totalSpent > 0 ? `${(client.totalSpent / 100).toFixed(2)}` : "—"}
</p>
</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg 'currency' packages/trpc/server/routers/viewer/clients/get.handler.ts

Repository: calcom/cal.diy

Length of output: 178


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## client-detail-view.tsx\n'
sed -n '1,140p' apps/web/modules/clients/views/client-detail-view.tsx

printf '\n## viewer clients get handler\n'
sed -n '1,220p' packages/trpc/server/routers/viewer/clients/get.handler.ts

printf '\n## viewer clients list handler\n'
sed -n '1,260p' packages/trpc/server/routers/viewer/clients/list.handler.ts

Repository: calcom/cal.diy

Length of output: 9104


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
for p in [
    Path("apps/web/modules/clients/views/client-detail-view.tsx"),
    Path("packages/trpc/server/routers/viewer/clients/get.handler.ts"),
    Path("packages/trpc/server/routers/viewer/clients/list.handler.ts"),
]:
    print(f"\nFILE: {p}")
    text = p.read_text()
    for i, line in enumerate(text.splitlines(), 1):
        if "currency" in line or "totalSpent" in line or "total_spent" in line or "Intl.NumberFormat" in line:
            start = max(1, i - 4)
            end = min(len(text.splitlines()), i + 4)
            print(f"-- around line {i} --")
            for j in range(start, end + 1):
                print(f"{j:4d}: {text.splitlines()[j-1]}")
PY

Repository: calcom/cal.diy

Length of output: 5335


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "currency|totalSpent|total_spent|Intl\.NumberFormat" \
  apps/web/modules/clients/views/client-detail-view.tsx \
  packages/trpc/server/routers/viewer/clients/get.handler.ts \
  packages/trpc/server/routers/viewer/clients/list.handler.ts

Repository: calcom/cal.diy

Length of output: 1718


Display totalSpent with a currency. The current viewer.clients.get payload exposes totalSpent but not a client currency, so this value is ambiguous in the UI. Add a currency field to the response and format it here, or derive the currency from the booking payments before rendering.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/modules/clients/views/client-detail-view.tsx` around lines 46 - 51,
Add currency context to the client detail data used by the totalSpent display,
preferably by extending the viewer.clients.get response with a client currency,
then format the value in the total_spent block using that currency. If the
response cannot provide it, derive the currency from booking payments before
rendering; ensure the displayed amount is never currency-ambiguous.

Comment on lines +22 to +30
const { data } = trpc.viewer.clients.list.useQuery(
{
searchTerm: debouncedSearchTerm,
limit: FETCH_LIMIT,
},
{
placeholderData: keepPreviousData,
}
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle the initial loading state to prevent a flash of the empty state. When the component mounts, data is undefined and clients defaults to [], causing the UI to briefly flash the "No clients found matching your search" message before the data arrives.

  • apps/web/modules/clients/views/clients-listing-view.tsx#L22-L30: Destructure isPending (or isLoading) from the useQuery hook call.
  • apps/web/modules/clients/views/clients-listing-view.tsx#L43-L45: Check isPending before rendering clients.length === 0 to display a loading indicator instead of the empty state.
📍 Affects 1 file
  • apps/web/modules/clients/views/clients-listing-view.tsx#L22-L30 (this comment)
  • apps/web/modules/clients/views/clients-listing-view.tsx#L43-L45
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/modules/clients/views/clients-listing-view.tsx` around lines 22 -
30, Update the clients query in
apps/web/modules/clients/views/clients-listing-view.tsx:22-30 to destructure
isPending (or isLoading), then update the empty-results rendering at
apps/web/modules/clients/views/clients-listing-view.tsx:43-45 to check that
loading state first and show the existing loading indicator before evaluating
clients.length === 0.

Comment on lines +12 to +14
bookerName: string,
_paymentOption: PaymentOption,
bookerEmail?: string | null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep outline packages/types/PaymentService.d.ts --match IAbstractPaymentService --view expanded
ast-grep outline packages/app-store/mock-payment-app/lib/PaymentService.ts \
  --match MockPaymentService --view expanded

Repository: calcom/cal.diy

Length of output: 1172


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- packages/types/PaymentService.d.ts ---\n'
sed -n '1,40p' packages/types/PaymentService.d.ts

printf '\n--- packages/app-store/mock-payment-app/lib/PaymentService.ts ---\n'
sed -n '1,110p' packages/app-store/mock-payment-app/lib/PaymentService.ts

Repository: calcom/cal.diy

Length of output: 4317


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- usages of bookerEmail/bookerName in mock payment app and payment service types ---\n'
rg -n "bookerEmail|bookerName|InputJsonValue|payment\.data|data:" packages/app-store/mock-payment-app packages/types packages/app-store -g '*.ts' -g '*.d.ts'

printf '\n--- mock payment app file tail ---\n'
sed -n '110,220p' packages/app-store/mock-payment-app/lib/PaymentService.ts

Repository: calcom/cal.diy

Length of output: 37525


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- search for payment-data lookups ---\n'
rg -n "bookerEmail|bookerName|payment.*data|data\.booker|data\?.*booker|JSON\.parse\(.*booker" packages -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx'

printf '\n--- other payment service implementations ---\n'
fd 'PaymentService.ts' packages -t f | sed 's#^`#FILE`: #'

Repository: calcom/cal.diy

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- bookerEmail/bookerName references in repo ---\n'
rg -n "bookerEmail|bookerName" packages -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx' | sed -n '1,200p'

printf '\n--- payment app implementations ---\n'
fd 'PaymentService.ts' packages -t f | sed -n '1,50p'

Repository: calcom/cal.diy

Length of output: 50371


Keep the mock payment-service signature aligned. bookerName should accept null, and bookerEmail should stay required; falling back to "" strips the attendee email from stored payment metadata. packages/app-store/mock-payment-app/lib/PaymentService.ts:12-14, 51-54

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app-store/mock-payment-app/lib/PaymentService.ts` around lines 12 -
14, Update the mock payment-service method signature to allow bookerName to be
null while keeping bookerEmail required. In the corresponding implementation and
stored payment metadata handling, preserve the provided attendee email instead
of defaulting it to an empty string.

Comment on lines +69 to +70
const totalSpent = bookings.reduce((sum, b) => {
return sum + b.payment.filter((p) => p.success).reduce((s, p) => s + p.amount, 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i 'schema\.prisma$' . --exec rg -n -C4 'model Payment|attendee|bookingId|refunded|currency' {}

ast-grep outline \
  apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx \
  --items all --match 'payment|attendee'

Repository: calcom/cal.diy

Length of output: 9187


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant handlers before reading slices
ast-grep outline packages/trpc/server/routers/viewer/clients/get.handler.ts --items all
ast-grep outline packages/trpc/server/routers/viewer/clients/list.handler.ts --items all

# Show the relevant line ranges
sed -n '1,220p' packages/trpc/server/routers/viewer/clients/get.handler.ts | cat -n
printf '\n==== list.handler.ts ====\n'
sed -n '1,220p' packages/trpc/server/routers/viewer/clients/list.handler.ts | cat -n

printf '\n==== search for attendee payment association ====\n'
rg -n "bookingSeat|paymentOption|refunded|currency|attendeeId|attendee\.|payments?" packages/trpc server packages -g '!**/node_modules/**' -g '!**/dist/**' | head -n 200

Repository: calcom/cal.diy

Length of output: 31107


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "refunded|payment\.refunded|\.refunded\b|refund" packages apps prisma -g '!**/node_modules/**' -g '!**/dist/**' | head -n 300

printf '\n==== attendee/payment association search ====\n'
rg -n "bookingSeat|attendeeId|paymentOption|Payment.*attendee|attendee.*payment" packages apps prisma -g '!**/node_modules/**' -g '!**/dist/**' | head -n 300

Repository: calcom/cal.diy

Length of output: 37589


Exclude refunded payments from client spend totals. Both handlers count every successful payment, so refunded charges still inflate spend in packages/trpc/server/routers/viewer/clients/get.handler.ts#L69-L70 and packages/trpc/server/routers/viewer/clients/list.handler.ts#L79-L80.

📍 Affects 2 files
  • packages/trpc/server/routers/viewer/clients/get.handler.ts#L69-L70 (this comment)
  • packages/trpc/server/routers/viewer/clients/list.handler.ts#L79-L80
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/trpc/server/routers/viewer/clients/get.handler.ts` around lines 69 -
70, Update the total-spend reductions in get.handler.ts lines 69-70 and
list.handler.ts lines 79-80 to include only successful, non-refunded payments;
preserve the existing payment amount aggregation for eligible payments in both
handlers.

Comment on lines +19 to +58
const attendeesRaw = await prisma.attendee.findMany({
where: {
booking: {
...whereBase,
},
...(searchTerm
? {
OR: [
{ email: { contains: searchTerm, mode: "insensitive" } },
{ name: { contains: searchTerm, mode: "insensitive" } },
],
}
: {}),
},
select: {
email: true,
name: true,
bookingId: true,
booking: {
select: {
id: true,
createdAt: true,
startTime: true,
paid: true,
payment: {
select: {
amount: true,
currency: true,
success: true,
},
},
},
},
},
orderBy: {
booking: {
startTime: "desc",
},
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Paginate before loading every attendee and payment.

limit only slices the fully materialized in-memory result. Large booking histories therefore cause unbounded database reads, allocations, sorting, and payment processing. Paginate distinct normalized client identifiers in the database, then fetch aggregates for that page.

Also applies to: 108-117

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/trpc/server/routers/viewer/clients/list.handler.ts` around lines 19
- 58, Update the client-list query flow around attendeesRaw so pagination
selects distinct normalized client identifiers in the database before loading
attendee, booking, and payment aggregates. Apply the existing limit and offset
to that identifier query, then restrict the aggregate fetch to only those
identifiers while preserving the current search and booking filters; remove any
in-memory slicing that paginates the fully materialized attendee results.

Comment on lines +24 to +31
...(searchTerm
? {
OR: [
{ email: { contains: searchTerm, mode: "insensitive" } },
{ name: { contains: searchTerm, mode: "insensitive" } },
],
}
: {}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Apply search after identifying the complete client history.

Filtering attendee rows before grouping means a name matching only one historical attendee record returns incomplete booking counts, spend, and date ranges for that email. Resolve matching client emails first, then aggregate all their bookings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/trpc/server/routers/viewer/clients/list.handler.ts` around lines 24
- 31, Update the client history query around the searchTerm filter so search
first resolves matching client emails, then aggregates all booking and attendee
records for those emails. Move the name/email filtering out of the pre-grouped
attendee-row scope, preserving complete booking counts, spend, and date ranges
for every matched email.

Comment on lines +5 to +6
limit: z.number().min(1).max(100).default(50),
cursor: z.number().nullish(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Restrict pagination values to valid integers.

Negative or fractional cursors produce incorrect slice() boundaries, while fractional limits are silently truncated.

Proposed fix
-  limit: z.number().min(1).max(100).default(50),
-  cursor: z.number().nullish(),
+  limit: z.number().int().min(1).max(100).default(50),
+  cursor: z.number().int().nonnegative().nullish(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
limit: z.number().min(1).max(100).default(50),
cursor: z.number().nullish(),
limit: z.number().int().min(1).max(100).default(50),
cursor: z.number().int().nonnegative().nullish(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/trpc/server/routers/viewer/clients/list.schema.ts` around lines 5 -
6, Update the pagination schema fields limit and cursor to validate integer
values in addition to their existing bounds and optionality. Ensure limit
remains within 1–100 with a default of 50, and cursor continues to allow nullish
values while rejecting negative or fractional inputs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Client Booking History Dashboard — view all unique clients with booking history

1 participant