fix(users): include all workshop types in user stats
This commit is contained in:
@@ -4,6 +4,31 @@ import { getAllUsersWithStats } from '@/services/auth';
|
|||||||
import { getGravatarUrl } from '@/lib/gravatar';
|
import { getGravatarUrl } from '@/lib/gravatar';
|
||||||
import { PageHeader } from '@/components/ui';
|
import { PageHeader } from '@/components/ui';
|
||||||
|
|
||||||
|
const OWNED_WORKSHOP_COUNT_KEYS = [
|
||||||
|
'sessions',
|
||||||
|
'motivatorSessions',
|
||||||
|
'yearReviewSessions',
|
||||||
|
'weeklyCheckInSessions',
|
||||||
|
'weatherSessions',
|
||||||
|
'gifMoodSessions',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const SHARED_WORKSHOP_COUNT_KEYS = [
|
||||||
|
'sharedSessions',
|
||||||
|
'sharedMotivatorSessions',
|
||||||
|
'sharedYearReviewSessions',
|
||||||
|
'sharedWeeklyCheckInSessions',
|
||||||
|
'sharedWeatherSessions',
|
||||||
|
'sharedGifMoodSessions',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function sumCountKeys(
|
||||||
|
counts: Record<string, number>,
|
||||||
|
keys: readonly string[]
|
||||||
|
): number {
|
||||||
|
return keys.reduce((acc, key) => acc + (counts[key] ?? 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
function formatRelativeTime(date: Date): string {
|
function formatRelativeTime(date: Date): string {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const diffMs = now.getTime() - date.getTime();
|
const diffMs = now.getTime() - date.getTime();
|
||||||
@@ -28,7 +53,11 @@ export default async function UsersPage() {
|
|||||||
|
|
||||||
// Calculate some global stats
|
// Calculate some global stats
|
||||||
const totalSessions = users.reduce(
|
const totalSessions = users.reduce(
|
||||||
(acc, u) => acc + u._count.sessions + u._count.motivatorSessions,
|
(acc, u) => acc + sumCountKeys(u._count, OWNED_WORKSHOP_COUNT_KEYS),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const totalSharedSessions = users.reduce(
|
||||||
|
(acc, u) => acc + sumCountKeys(u._count, SHARED_WORKSHOP_COUNT_KEYS),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
const avgSessionsPerUser = users.length > 0 ? totalSessions / users.length : 0;
|
const avgSessionsPerUser = users.length > 0 ? totalSessions / users.length : 0;
|
||||||
@@ -56,12 +85,7 @@ export default async function UsersPage() {
|
|||||||
<div className="text-sm text-muted">Moy. par user</div>
|
<div className="text-sm text-muted">Moy. par user</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border border-border bg-card p-4">
|
<div className="rounded-xl border border-border bg-card p-4">
|
||||||
<div className="text-2xl font-bold text-accent">
|
<div className="text-2xl font-bold text-accent">{totalSharedSessions}</div>
|
||||||
{users.reduce(
|
|
||||||
(acc, u) => acc + u._count.sharedSessions + u._count.sharedMotivatorSessions,
|
|
||||||
0
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-muted">Partages actifs</div>
|
<div className="text-sm text-muted">Partages actifs</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,8 +93,8 @@ export default async function UsersPage() {
|
|||||||
{/* Users List */}
|
{/* Users List */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{users.map((user) => {
|
{users.map((user) => {
|
||||||
const totalUserSessions = user._count.sessions + user._count.motivatorSessions;
|
const totalUserSessions = sumCountKeys(user._count, OWNED_WORKSHOP_COUNT_KEYS);
|
||||||
const totalShares = user._count.sharedSessions + user._count.sharedMotivatorSessions;
|
const totalShares = sumCountKeys(user._count, SHARED_WORKSHOP_COUNT_KEYS);
|
||||||
const isCurrentUser = user.id === session.user?.id;
|
const isCurrentUser = user.id === session.user?.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -115,7 +139,7 @@ export default async function UsersPage() {
|
|||||||
backgroundColor: 'color-mix(in srgb, var(--strength) 15%, transparent)',
|
backgroundColor: 'color-mix(in srgb, var(--strength) 15%, transparent)',
|
||||||
color: 'var(--strength)',
|
color: 'var(--strength)',
|
||||||
}}
|
}}
|
||||||
title="Sessions SWOT"
|
title="Ateliers créés (tous types)"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className="h-3.5 w-3.5"
|
className="h-3.5 w-3.5"
|
||||||
@@ -130,30 +154,7 @@ export default async function UsersPage() {
|
|||||||
d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"
|
d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{user._count.sessions}
|
{totalUserSessions}
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium"
|
|
||||||
style={{
|
|
||||||
backgroundColor: 'color-mix(in srgb, var(--opportunity) 15%, transparent)',
|
|
||||||
color: 'var(--opportunity)',
|
|
||||||
}}
|
|
||||||
title="Sessions Moving Motivators"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="h-3.5 w-3.5"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{user._count.motivatorSessions}
|
|
||||||
</div>
|
</div>
|
||||||
{totalShares > 0 && (
|
{totalShares > 0 && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -194,8 +194,16 @@ export async function updateUserPassword(
|
|||||||
export interface UserStats {
|
export interface UserStats {
|
||||||
sessions: number;
|
sessions: number;
|
||||||
motivatorSessions: number;
|
motivatorSessions: number;
|
||||||
|
yearReviewSessions: number;
|
||||||
|
weeklyCheckInSessions: number;
|
||||||
|
weatherSessions: number;
|
||||||
|
gifMoodSessions: number;
|
||||||
sharedSessions: number;
|
sharedSessions: number;
|
||||||
sharedMotivatorSessions: number;
|
sharedMotivatorSessions: number;
|
||||||
|
sharedYearReviewSessions: number;
|
||||||
|
sharedWeeklyCheckInSessions: number;
|
||||||
|
sharedWeatherSessions: number;
|
||||||
|
sharedGifMoodSessions: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserWithStats {
|
export interface UserWithStats {
|
||||||
@@ -208,7 +216,7 @@ export interface UserWithStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllUsersWithStats(): Promise<UserWithStats[]> {
|
export async function getAllUsersWithStats(): Promise<UserWithStats[]> {
|
||||||
const users = await prisma.user.findMany({
|
return prisma.user.findMany({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
email: true,
|
email: true,
|
||||||
@@ -218,32 +226,20 @@ export async function getAllUsersWithStats(): Promise<UserWithStats[]> {
|
|||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
sessions: true,
|
sessions: true,
|
||||||
|
motivatorSessions: true,
|
||||||
|
yearReviewSessions: true,
|
||||||
|
weeklyCheckInSessions: true,
|
||||||
|
weatherSessions: true,
|
||||||
|
gifMoodSessions: true,
|
||||||
sharedSessions: true,
|
sharedSessions: true,
|
||||||
|
sharedMotivatorSessions: true,
|
||||||
|
sharedYearReviewSessions: true,
|
||||||
|
sharedWeeklyCheckInSessions: true,
|
||||||
|
sharedWeatherSessions: true,
|
||||||
|
sharedGifMoodSessions: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get motivator counts in bulk (2 queries instead of 2*N)
|
|
||||||
const motivatorCounts = await prisma.movingMotivatorsSession.groupBy({
|
|
||||||
by: ['userId'],
|
|
||||||
_count: { id: true },
|
|
||||||
});
|
|
||||||
const sharedMotivatorCounts = await prisma.mMSessionShare.groupBy({
|
|
||||||
by: ['userId'],
|
|
||||||
_count: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
const motivatorMap = new Map(motivatorCounts.map((m) => [m.userId, m._count.id]));
|
|
||||||
const sharedMotivatorMap = new Map(sharedMotivatorCounts.map((m) => [m.userId, m._count.id]));
|
|
||||||
|
|
||||||
return users.map((user) => ({
|
|
||||||
...user,
|
|
||||||
_count: {
|
|
||||||
...user._count,
|
|
||||||
motivatorSessions: motivatorMap.get(user.id) ?? 0,
|
|
||||||
sharedMotivatorSessions: sharedMotivatorMap.get(user.id) ?? 0,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user