Skip to content

Commit fc0b457

Browse files
committed
Display tenant and user IDs on detail surfaces and polish dashboard cards
1 parent 50e49a9 commit fc0b457

16 files changed

Lines changed: 116 additions & 67 deletions

File tree

application/account/BackOffice/routes/-components/DashboardRecentSignupsCard.tsx

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import type { RowKey } from "@repo/ui/components/Table";
22

33
import { t } from "@lingui/core/macro";
4-
import { useLingui } from "@lingui/react";
54
import { Trans } from "@lingui/react/macro";
6-
import { Badge } from "@repo/ui/components/Badge";
75
import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "@repo/ui/components/Empty";
86
import { Skeleton } from "@repo/ui/components/Skeleton";
97
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@repo/ui/components/Table";
@@ -14,13 +12,15 @@ import { useCallback } from "react";
1412

1513
import { SmartDateTime } from "@/shared/components/SmartDateTime";
1614
import { api } from "@/shared/lib/api/client";
17-
import { getSubscriptionPlanLabel } from "@/shared/lib/api/labels";
18-
import { getCountryFlagEmoji, getCountryName } from "@/shared/lib/countryFlag";
1915

2016
import { DashboardCardShell } from "./DashboardCardShell";
2117

18+
function getOwnerDisplayName(owner: { firstName: string | null; lastName: string | null; email: string }): string {
19+
const fullName = [owner.firstName, owner.lastName].filter((part) => part != null && part.trim() !== "").join(" ");
20+
return fullName !== "" ? fullName : owner.email;
21+
}
22+
2223
export function DashboardRecentSignupsCard() {
23-
const { i18n } = useLingui();
2424
const navigate = useNavigate();
2525
const { data, isLoading } = api.useQuery("get", "/api/back-office/dashboard/recent-signups", {
2626
params: { query: { Limit: 6 } }
@@ -64,17 +64,20 @@ export function DashboardRecentSignupsCard() {
6464
</EmptyHeader>
6565
</Empty>
6666
) : (
67-
<Table rowSize="compact" aria-label={t`Recent signups`} selectionMode="single" onActivate={handleActivate}>
67+
<Table
68+
rowSize="compact"
69+
aria-label={t`Recent signups`}
70+
selectionMode="single"
71+
onActivate={handleActivate}
72+
containerClassName="border-0 bg-transparent"
73+
>
6874
<TableHeader>
6975
<TableRow>
7076
<TableHead>
7177
<Trans>Account</Trans>
7278
</TableHead>
7379
<TableHead className="hidden md:table-cell">
74-
<Trans>Plan</Trans>
75-
</TableHead>
76-
<TableHead className="hidden md:table-cell">
77-
<Trans>Country</Trans>
80+
<Trans>Owner</Trans>
7881
</TableHead>
7982
<TableHead className="text-right">
8083
<Trans>Created</Trans>
@@ -96,16 +99,8 @@ export function DashboardRecentSignupsCard() {
9699
</div>
97100
</TableCell>
98101
<TableCell className="hidden md:table-cell">
99-
<Badge variant="secondary" className="text-xs">
100-
{getSubscriptionPlanLabel(signup.plan)}
101-
</Badge>
102-
</TableCell>
103-
<TableCell className="hidden md:table-cell">
104-
{signup.country ? (
105-
<span className="inline-flex items-center gap-1.5 text-sm">
106-
<span aria-hidden="true">{getCountryFlagEmoji(signup.country)}</span>
107-
<span className="truncate">{getCountryName(signup.country, i18n.locale)}</span>
108-
</span>
102+
{signup.owner ? (
103+
<span className="truncate text-sm">{getOwnerDisplayName(signup.owner)}</span>
109104
) : (
110105
<span className="text-muted-foreground"></span>
111106
)}

application/account/BackOffice/routes/-components/DashboardRecentStripeEventsCard.tsx

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export function DashboardRecentStripeEventsCard() {
7272
aria-label={t`Recent billing events`}
7373
selectionMode="single"
7474
onActivate={handleActivate}
75+
containerClassName="border-0 bg-transparent"
7576
>
7677
<TableHeader>
7778
<TableRow>
@@ -88,16 +89,14 @@ export function DashboardRecentStripeEventsCard() {
8889
<Trans>MRR impact</Trans>
8990
</TableHead>
9091
<TableHead className="hidden text-right md:table-cell">
91-
<Trans>Date</Trans>
92+
<Trans>Occurred</Trans>
9293
</TableHead>
9394
</TableRow>
9495
</TableHeader>
9596
<TableBody>
9697
{events.map((event, index) => {
9798
const variant = BILLING_EVENT_VARIANT[event.type];
9899
const Icon = variant.icon;
99-
const showPlanTransition =
100-
event.fromPlan != null && event.toPlan != null && event.fromPlan !== event.toPlan;
101100
const isNegativeAmount = event.amountDelta != null && event.amountDelta < 0;
102101
return (
103102
<TableRow
@@ -122,15 +121,7 @@ export function DashboardRecentStripeEventsCard() {
122121
</Badge>
123122
</TableCell>
124123
<TableCell className="hidden md:table-cell">
125-
{showPlanTransition ? (
126-
<span className="inline-flex items-center gap-1 whitespace-nowrap">
127-
<Badge variant="secondary">{getSubscriptionPlanLabel(event.fromPlan!)}</Badge>
128-
<span aria-hidden={true} className="text-muted-foreground">
129-
130-
</span>
131-
<Badge variant="secondary">{getSubscriptionPlanLabel(event.toPlan!)}</Badge>
132-
</span>
133-
) : event.toPlan != null ? (
124+
{event.toPlan != null ? (
134125
<Badge variant="secondary">{getSubscriptionPlanLabel(event.toPlan)}</Badge>
135126
) : (
136127
<span className="text-muted-foreground"></span>

application/account/BackOffice/routes/-components/DashboardSections.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ export function DashboardSections() {
2626
<DashboardTenantGrowthCard period={period} />
2727
<DashboardUserLoginsCard period={period} />
2828
</div>
29-
<div className="grid grid-cols-1 gap-4 lg:grid-cols-5">
30-
<div className="lg:col-span-2">
29+
<div className="grid grid-cols-1 gap-4 xl:grid-cols-5">
30+
<div className="xl:col-span-2">
3131
<DashboardRecentSignupsCard />
3232
</div>
33-
<div className="lg:col-span-3">
33+
<div className="xl:col-span-3">
3434
<DashboardRecentStripeEventsCard />
3535
</div>
3636
</div>

application/account/BackOffice/routes/accounts/-components/AccountBillingEventsSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export function AccountBillingEventsSection({
8181
<TableHeader>
8282
<TableRow>
8383
<TableHead>
84-
<Trans>Date</Trans>
84+
<Trans>Occurred</Trans>
8585
</TableHead>
8686
<TableHead>
8787
<Trans>Event</Trans>

application/account/BackOffice/routes/accounts/-components/AccountDetailHeader.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Badge } from "@repo/ui/components/Badge";
44
import { Skeleton } from "@repo/ui/components/Skeleton";
55
import { TenantLogo } from "@repo/ui/components/TenantLogo";
66
import { useFormatDate } from "@repo/ui/hooks/useSmartDate";
7-
import { CalendarIcon } from "lucide-react";
7+
import { CalendarIcon, HashIcon } from "lucide-react";
88

99
import type { components } from "@/shared/lib/api/client";
1010

@@ -75,6 +75,10 @@ export function AccountDetailHeader({ tenant, tenantId, isLoading }: Readonly<Ac
7575
<span className="hidden md:inline">{formatDate(tenant.createdAt)}</span>
7676
</Trans>
7777
</span>
78+
<span className="inline-flex items-center gap-1.5 font-mono">
79+
<HashIcon className="size-3.5" aria-hidden={true} />
80+
<span>{tenantId}</span>
81+
</span>
7882
</div>
7983
</>
8084
)}

application/account/BackOffice/routes/users/$userId.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ function UserDetailPage() {
6161
<SidebarInset>
6262
<AppLayout variant="center" maxWidth="64rem" browserTitle={browserTitle}>
6363
<div className="flex flex-col gap-6">
64-
<UserDetailHeader user={user} isLoading={userQuery.isLoading} />
64+
<UserDetailHeader user={user} userId={userId} isLoading={userQuery.isLoading} />
6565
<UserActivityTiles user={user} userId={userId} isLoading={userQuery.isLoading} />
6666
<Tabs value={activeTab} onValueChange={setActiveTab}>
6767
<TabsList>

application/account/BackOffice/routes/users/-components/UserDetailHeader.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@repo/ui/components/Avatar"
33
import { Badge } from "@repo/ui/components/Badge";
44
import { Skeleton } from "@repo/ui/components/Skeleton";
55
import { useFormatDate } from "@repo/ui/hooks/useSmartDate";
6-
import { CalendarIcon, CheckCircle2Icon, MailIcon, XCircleIcon } from "lucide-react";
6+
import { CalendarIcon, CheckCircle2Icon, HashIcon, MailIcon, XCircleIcon } from "lucide-react";
77

88
import type { components } from "@/shared/lib/api/client";
99

@@ -13,18 +13,19 @@ type BackOfficeUserDetailResponse = components["schemas"]["BackOfficeUserDetailR
1313

1414
interface UserDetailHeaderProps {
1515
user: BackOfficeUserDetailResponse | undefined;
16+
userId: string;
1617
isLoading: boolean;
1718
}
1819

19-
export function UserDetailHeader({ user, isLoading }: Readonly<UserDetailHeaderProps>) {
20+
export function UserDetailHeader({ user, userId, isLoading }: Readonly<UserDetailHeaderProps>) {
2021
const formatDate = useFormatDate();
2122

2223
return (
23-
<div className="flex flex-wrap items-center gap-4">
24+
<div className="flex items-center gap-4">
2425
{isLoading || !user ? (
2526
<>
2627
<Skeleton className="size-16 rounded-full" />
27-
<div className="flex min-w-0 flex-col justify-center gap-1 self-center">
28+
<div className="flex min-w-0 flex-1 flex-col justify-center gap-1 self-center">
2829
<Skeleton className="h-7 w-64" />
2930
<Skeleton className="h-4 w-48" />
3031
</div>
@@ -39,7 +40,7 @@ export function UserDetailHeader({ user, isLoading }: Readonly<UserDetailHeaderP
3940
{getUserInitials(user.firstName, user.lastName, user.email)}
4041
</AvatarFallback>
4142
</Avatar>
42-
<div className="flex min-w-0 flex-col justify-center gap-1 self-center">
43+
<div className="flex min-w-0 flex-1 flex-col justify-center gap-1 self-center">
4344
<div className="flex flex-wrap items-center gap-2">
4445
<h1 className="m-0 min-w-0 truncate leading-none">
4546
{getUserDisplayName(user.firstName, user.lastName, user.email)}
@@ -72,6 +73,10 @@ export function UserDetailHeader({ user, isLoading }: Readonly<UserDetailHeaderP
7273
<span className="hidden md:inline">{formatDate(user.createdAt)}</span>
7374
</Trans>
7475
</span>
76+
<span className="inline-flex items-center gap-1.5 font-mono">
77+
<HashIcon className="size-3.5" aria-hidden={true} />
78+
<span>{userId}</span>
79+
</span>
7580
</div>
7681
</div>
7782
</>

application/account/Core/Features/BackOffice/Dashboard/Queries/GetDashboardRecentSignups.cs

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
using Account.Features.Subscriptions.Domain;
21
using Account.Features.Tenants.Domain;
2+
using Account.Features.Users.Domain;
33
using FluentValidation;
44
using JetBrains.Annotations;
55
using SharedKernel.Cqrs;
@@ -18,12 +18,14 @@ public sealed record BackOfficeDashboardRecentSignupsResponse(BackOfficeDashboar
1818
public sealed record BackOfficeDashboardRecentSignup(
1919
TenantId TenantId,
2020
string Name,
21-
string? Country,
22-
SubscriptionPlan Plan,
2321
string? TenantLogoUrl,
24-
DateTimeOffset CreatedAt
22+
DateTimeOffset CreatedAt,
23+
BackOfficeDashboardRecentSignupOwner? Owner
2524
);
2625

26+
[PublicAPI]
27+
public sealed record BackOfficeDashboardRecentSignupOwner(UserId UserId, string? FirstName, string? LastName, string Email);
28+
2729
public sealed class GetDashboardRecentSignupsQueryValidator : AbstractValidator<GetDashboardRecentSignupsQuery>
2830
{
2931
public GetDashboardRecentSignupsQueryValidator()
@@ -32,31 +34,24 @@ public GetDashboardRecentSignupsQueryValidator()
3234
}
3335
}
3436

35-
public sealed class GetDashboardRecentSignupsHandler(
36-
ITenantRepository tenantRepository,
37-
ISubscriptionRepository subscriptionRepository
38-
) : IRequestHandler<GetDashboardRecentSignupsQuery, Result<BackOfficeDashboardRecentSignupsResponse>>
37+
public sealed class GetDashboardRecentSignupsHandler(ITenantRepository tenantRepository, IUserRepository userRepository)
38+
: IRequestHandler<GetDashboardRecentSignupsQuery, Result<BackOfficeDashboardRecentSignupsResponse>>
3939
{
4040
public async Task<Result<BackOfficeDashboardRecentSignupsResponse>> Handle(GetDashboardRecentSignupsQuery query, CancellationToken cancellationToken)
4141
{
4242
var tenants = await tenantRepository.GetMostRecentSignupsUnfilteredAsync(query.Limit, cancellationToken);
4343
var tenantIds = tenants.Select(t => t.Id).ToArray();
44-
var subscriptions = tenantIds.Length == 0
45-
? []
46-
: await subscriptionRepository.GetByTenantIdsUnfilteredAsync(tenantIds, cancellationToken);
47-
var subscriptionByTenantId = subscriptions.ToDictionary(s => s.TenantId);
44+
var ownerByTenantId = await userRepository.GetFirstOwnerByTenantIdsUnfilteredAsync(tenantIds, cancellationToken);
4845

4946
var signups = tenants.Select(tenant =>
5047
{
51-
var subscription = subscriptionByTenantId.GetValueOrDefault(tenant.Id);
52-
var country = subscription?.BillingInfo?.Address?.Country;
48+
var owner = ownerByTenantId.GetValueOrDefault(tenant.Id);
5349
return new BackOfficeDashboardRecentSignup(
5450
tenant.Id,
5551
tenant.Name,
56-
country,
57-
tenant.Plan,
5852
tenant.Logo.Url,
59-
tenant.CreatedAt
53+
tenant.CreatedAt,
54+
owner is null ? null : new BackOfficeDashboardRecentSignupOwner(owner.Id, owner.FirstName, owner.LastName, owner.Email)
6055
);
6156
}
6257
).ToArray();

application/account/Core/Features/Users/Domain/UserRepository.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ CancellationToken cancellationToken
9191
/// </summary>
9292
Task<User[]> GetCreatedSinceUnfilteredAsync(DateTimeOffset since, CancellationToken cancellationToken);
9393

94+
/// <summary>
95+
/// Returns the earliest-created Owner for each of the given tenants without applying tenant query filters.
96+
/// Used by the back-office recent signups dashboard to attribute each new tenant to the user who signed up.
97+
/// </summary>
98+
Task<Dictionary<TenantId, User>> GetFirstOwnerByTenantIdsUnfilteredAsync(TenantId[] tenantIds, CancellationToken cancellationToken);
99+
94100
/// <summary>
95101
/// Returns every non-deleted user across all tenants without applying tenant query filters.
96102
/// Used by the back-office dashboard KPI snapshot to compute period-active users (last_seen_at within
@@ -505,6 +511,26 @@ public async Task<User[]> GetAllUnfilteredAsync(CancellationToken cancellationTo
505511
return await DbSet.IgnoreQueryFilters([QueryFilterNames.Tenant]).ToArrayAsync(cancellationToken);
506512
}
507513

514+
/// <summary>
515+
/// Returns the earliest-created Owner for each of the given tenants without applying tenant query filters.
516+
/// Used by the back-office recent signups dashboard to attribute each new tenant to the user who signed up.
517+
/// </summary>
518+
public async Task<Dictionary<TenantId, User>> GetFirstOwnerByTenantIdsUnfilteredAsync(TenantId[] tenantIds, CancellationToken cancellationToken)
519+
{
520+
if (tenantIds.Length == 0) return new Dictionary<TenantId, User>();
521+
522+
// SQLite cannot translate DateTimeOffset ORDER BY clauses, so materialize the candidate Owners and pick
523+
// the earliest in memory. Bounded by the number of tenants on the dashboard recent-signups list.
524+
var owners = await DbSet
525+
.IgnoreQueryFilters([QueryFilterNames.Tenant])
526+
.Where(u => u.Role == UserRole.Owner && tenantIds.AsEnumerable().Contains(u.TenantId))
527+
.ToArrayAsync(cancellationToken);
528+
529+
return owners
530+
.GroupBy(u => u.TenantId)
531+
.ToDictionary(g => g.Key, g => g.OrderBy(u => u.CreatedAt).ThenBy(u => u.Id.Value).First());
532+
}
533+
508534
[UsedImplicitly]
509535
private sealed record UserSummaryResult(int TotalUsers, int ActiveUsers, int PendingUsers);
510536
}

application/account/WebApp/routes/account/settings/-components/AccountInfoFields.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Trans } from "@lingui/react/macro";
33
import { Badge } from "@repo/ui/components/Badge";
44
import { useFormatDate } from "@repo/ui/hooks/useSmartDate";
55
import { Link } from "@tanstack/react-router";
6+
import { HashIcon } from "lucide-react";
67

78
import { api, type Schemas, SuspensionReason, TenantState } from "@/shared/lib/api/client";
89
import { getPlanLabelWithFree } from "@/shared/lib/api/subscriptionPlan";
@@ -41,7 +42,10 @@ export function AccountInfoFields({ tenant }: Readonly<AccountInfoFieldsProps>)
4142
<span className="text-muted-foreground">
4243
<Trans>Account ID</Trans>
4344
</span>
44-
<span className="font-mono">{tenant?.id}</span>
45+
<span className="inline-flex items-center gap-1.5 font-mono">
46+
<HashIcon className="size-3.5" aria-hidden={true} />
47+
{tenant?.id}
48+
</span>
4549
</div>
4650
<div className="flex justify-between sm:flex-col sm:gap-1 md:flex-row md:justify-between md:gap-0 lg:flex-col lg:gap-1">
4751
<span className="text-muted-foreground">

0 commit comments

Comments
 (0)