feat: add Client Booking History Dashboard — view all unique clients with booking history#29775
feat: add Client Booking History Dashboard — view all unique clients with booking history#29775prathamesh04 wants to merge 4 commits into
Conversation
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
|
Welcome to Cal.diy, @prathamesh04! Thanks for opening this pull request. A few things to keep in mind:
A maintainer will review your PR soon. Thanks for contributing! |
|
@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: |
📝 WalkthroughWalkthroughAdds 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 🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (2 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (2)
apps/web/modules/clients/views/clients-listing-view.tsx (1)
68-72: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider using
Intl.NumberFormatfor currency formatting.Using
Intl.NumberFormatautomatically 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 valueConsider using
Intl.NumberFormatfor currency formatting.Using
Intl.NumberFormatautomatically 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
📒 Files selected for processing (19)
.changeset/fix-clients-dashboard.md.changeset/fix-mobile-sticky-bar-overlap.md.changeset/fix-multi-seat-payment-per-attendee.mdapps/web/app/(use-page-wrapper)/(main-nav)/bookings/clients/[email]/page.tsxapps/web/app/(use-page-wrapper)/(main-nav)/bookings/clients/page.tsxapps/web/modules/bookings/components/BookEventForm/BookFormAsModal.tsxapps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsxapps/web/modules/clients/views/client-detail-view.tsxapps/web/modules/clients/views/clients-listing-view.tsxapps/web/modules/shell/navigation/Navigation.tsxpackages/app-store/mock-payment-app/lib/PaymentService.tspackages/i18n/locales/en/common.jsonpackages/prisma/index.tspackages/trpc/server/routers/viewer/_router.tsxpackages/trpc/server/routers/viewer/clients/_router.tsxpackages/trpc/server/routers/viewer/clients/get.handler.tspackages/trpc/server/routers/viewer/clients/get.schema.tspackages/trpc/server/routers/viewer/clients/list.handler.tspackages/trpc/server/routers/viewer/clients/list.schema.ts
| const emailCandidates = [ | ||
| data?.bookerEmail, | ||
| metadata?.bookerEmail, | ||
| data?.buyerEmail, | ||
| data?.customerEmail, | ||
| ].filter(Boolean) as string[]; | ||
| return emailCandidates.some((e) => e.toLowerCase() === seat.attendee.email.toLowerCase()); |
There was a problem hiding this comment.
🩺 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.
| 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.
| <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> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg 'currency' packages/trpc/server/routers/viewer/clients/get.handler.tsRepository: 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.tsRepository: 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]}")
PYRepository: 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.tsRepository: 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.
| const { data } = trpc.viewer.clients.list.useQuery( | ||
| { | ||
| searchTerm: debouncedSearchTerm, | ||
| limit: FETCH_LIMIT, | ||
| }, | ||
| { | ||
| placeholderData: keepPreviousData, | ||
| } | ||
| ); |
There was a problem hiding this comment.
🩺 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: DestructureisPending(orisLoading) from theuseQueryhook call.apps/web/modules/clients/views/clients-listing-view.tsx#L43-L45: CheckisPendingbefore renderingclients.length === 0to 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.
| bookerName: string, | ||
| _paymentOption: PaymentOption, | ||
| bookerEmail?: string | null |
There was a problem hiding this comment.
🎯 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 expandedRepository: 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.tsRepository: 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.tsRepository: 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.
| const totalSpent = bookings.reduce((sum, b) => { | ||
| return sum + b.payment.filter((p) => p.success).reduce((s, p) => s + p.amount, 0); |
There was a problem hiding this comment.
🗄️ 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 200Repository: 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 300Repository: 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.
| 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", | ||
| }, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🚀 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.
| ...(searchTerm | ||
| ? { | ||
| OR: [ | ||
| { email: { contains: searchTerm, mode: "insensitive" } }, | ||
| { name: { contains: searchTerm, mode: "insensitive" } }, | ||
| ], | ||
| } | ||
| : {}), |
There was a problem hiding this comment.
🗄️ 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.
| limit: z.number().min(1).max(100).default(50), | ||
| cursor: z.number().nullish(), |
There was a problem hiding this comment.
🎯 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.
| 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.
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 spendget— returns a single client's booking history with event type, status, dates, and payment infoNew 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/tableNavigation:
No new database tables — uses existing
Attendee⟶Booking⟶PaymentrelationsType of change
How should this be tested?
/bookings/clientsvia sidebar or URLChecklist
Closes #29772