diff --git a/src/app/sessions/WorkshopTabs.tsx b/src/app/sessions/WorkshopTabs.tsx index d8eecab..2571c73 100644 --- a/src/app/sessions/WorkshopTabs.tsx +++ b/src/app/sessions/WorkshopTabs.tsx @@ -28,6 +28,7 @@ import { const TYPE_TABS = [ { value: 'all' as const, icon: '📋', label: 'Tous' }, + { value: 'team' as const, icon: '🏢', label: 'Équipe' }, ...WORKSHOPS.map((w) => ({ value: w.id, icon: w.icon, label: w.labelShort })), ]; @@ -131,6 +132,7 @@ interface WorkshopTabsProps { yearReviewSessions: YearReviewSession[]; weeklyCheckInSessions: WeeklyCheckInSession[]; weatherSessions: WeatherSession[]; + teamCollabSessions?: (AnySession & { isTeamCollab?: true })[]; } // Helper to get resolved collaborator from any session @@ -197,6 +199,7 @@ export function WorkshopTabs({ yearReviewSessions, weeklyCheckInSessions, weatherSessions, + teamCollabSessions = [], }: WorkshopTabsProps) { const searchParams = useSearchParams(); const router = useRouter(); @@ -219,7 +222,7 @@ export function WorkshopTabs({ router.push(`/sessions${params.toString() ? `?${params.toString()}` : ''}`); }; - // Combine and sort all sessions + // Combine and sort all sessions (exclude team collab from main list - they're shown separately) const allSessions: AnySession[] = [ ...swotSessions, ...motivatorSessions, @@ -232,7 +235,9 @@ export function WorkshopTabs({ const filteredSessions = activeTab === 'all' || activeTab === 'byPerson' ? allSessions - : activeTab === 'swot' + : activeTab === 'team' + ? teamCollabSessions + : activeTab === 'swot' ? swotSessions : activeTab === 'motivators' ? motivatorSessions @@ -242,9 +247,11 @@ export function WorkshopTabs({ ? weeklyCheckInSessions : weatherSessions; - // Separate by ownership + // Separate by ownership (for non-team tab: owned, shared, teamCollab) const ownedSessions = filteredSessions.filter((s) => s.isOwner); - const sharedSessions = filteredSessions.filter((s) => !s.isOwner); + const sharedSessions = filteredSessions.filter((s) => !s.isOwner && !(s as AnySession & { isTeamCollab?: boolean }).isTeamCollab); + const teamCollabFiltered = + activeTab === 'all' ? teamCollabSessions : activeTab === 'team' ? teamCollabSessions : []; // Group by person (all sessions - owned and shared) const sessionsByPerson = groupByPerson(allSessions); @@ -270,6 +277,15 @@ export function WorkshopTabs({ label="Par personne" count={sessionsByPerson.size} /> + {teamCollabSessions.length > 0 && ( + setActiveTab('team')} + icon="🏢" + label="Équipe" + count={teamCollabSessions.length} + /> + )} @@ -312,6 +329,28 @@ export function WorkshopTabs({ })} ) + ) : activeTab === 'team' ? ( + teamCollabSessions.length === 0 ? ( +
+ Aucun atelier de vos collaborateurs d'équipe (non partagés avec vous) +
+ ) : ( +
+
+

+ 🏢 Ateliers de l'équipe – non partagés ({teamCollabSessions.length}) +

+

+ En tant qu'admin d'équipe, vous voyez les ateliers de vos collaborateurs qui ne vous sont pas encore partagés. +

+
+ {teamCollabSessions.map((s) => ( + + ))} +
+
+
+ ) ) : filteredSessions.length === 0 ? (
Aucun atelier de ce type pour le moment
) : ( @@ -343,6 +382,20 @@ export function WorkshopTabs({ )} + + {/* Team collab sessions (non-shared) - grayed out, admin view only */} + {activeTab === 'all' && teamCollabFiltered.length > 0 && ( +
+

+ 🏢 Équipe – non partagés ({teamCollabFiltered.length}) +

+
+ {teamCollabFiltered.map((s) => ( + + ))} +
+
+ )} )} @@ -470,7 +523,7 @@ function TabButton({ ); } -function SessionCard({ session }: { session: AnySession }) { +function SessionCard({ session, isTeamCollab = false }: { session: AnySession; isTeamCollab?: boolean }) { const [showDeleteModal, setShowDeleteModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [isPending, startTransition] = useTransition(); @@ -562,11 +615,8 @@ function SessionCard({ session }: { session: AnySession }) { const editParticipantLabel = workshop.participantLabel; - return ( - <> -
- - + const cardContent = ( + {/* Accent bar */}
)} - + ); + + return ( + <> +
+ {isTeamCollab ? ( +
+ {cardContent} +
+ ) : ( + {cardContent} + )} {/* Action buttons - only for owner */} {session.isOwner && ( diff --git a/src/app/sessions/page.tsx b/src/app/sessions/page.tsx index fa37acc..3e8e778 100644 --- a/src/app/sessions/page.tsx +++ b/src/app/sessions/page.tsx @@ -1,10 +1,25 @@ import { Suspense } from 'react'; import { auth } from '@/lib/auth'; -import { getSessionsByUserId } from '@/services/sessions'; -import { getMotivatorSessionsByUserId } from '@/services/moving-motivators'; -import { getYearReviewSessionsByUserId } from '@/services/year-review'; -import { getWeeklyCheckInSessionsByUserId } from '@/services/weekly-checkin'; -import { getWeatherSessionsByUserId } from '@/services/weather'; +import { + getSessionsByUserId, + getTeamCollaboratorSessionsForAdmin as getTeamSwotSessions, +} from '@/services/sessions'; +import { + getMotivatorSessionsByUserId, + getTeamCollaboratorSessionsForAdmin as getTeamMotivatorSessions, +} from '@/services/moving-motivators'; +import { + getYearReviewSessionsByUserId, + getTeamCollaboratorSessionsForAdmin as getTeamYearReviewSessions, +} from '@/services/year-review'; +import { + getWeeklyCheckInSessionsByUserId, + getTeamCollaboratorSessionsForAdmin as getTeamWeeklyCheckInSessions, +} from '@/services/weekly-checkin'; +import { + getWeatherSessionsByUserId, + getTeamCollaboratorSessionsForAdmin as getTeamWeatherSessions, +} from '@/services/weather'; import { Card } from '@/components/ui'; import { withWorkshopType } from '@/lib/workshops'; import { WorkshopTabs } from './WorkshopTabs'; @@ -36,15 +51,30 @@ export default async function SessionsPage() { return null; } - // Fetch SWOT, Moving Motivators, Year Review, Weekly Check-in, and Weather sessions - const [swotSessions, motivatorSessions, yearReviewSessions, weeklyCheckInSessions, weatherSessions] = - await Promise.all([ - getSessionsByUserId(session.user.id), - getMotivatorSessionsByUserId(session.user.id), - getYearReviewSessionsByUserId(session.user.id), - getWeeklyCheckInSessionsByUserId(session.user.id), - getWeatherSessionsByUserId(session.user.id), - ]); + // Fetch sessions (owned + shared) and team collab sessions (for team admins, non-shared) + const [ + swotSessions, + motivatorSessions, + yearReviewSessions, + weeklyCheckInSessions, + weatherSessions, + teamSwotSessions, + teamMotivatorSessions, + teamYearReviewSessions, + teamWeeklyCheckInSessions, + teamWeatherSessions, + ] = await Promise.all([ + getSessionsByUserId(session.user.id), + getMotivatorSessionsByUserId(session.user.id), + getYearReviewSessionsByUserId(session.user.id), + getWeeklyCheckInSessionsByUserId(session.user.id), + getWeatherSessionsByUserId(session.user.id), + getTeamSwotSessions(session.user.id), + getTeamMotivatorSessions(session.user.id), + getTeamYearReviewSessions(session.user.id), + getTeamWeeklyCheckInSessions(session.user.id), + getTeamWeatherSessions(session.user.id), + ]); // Add workshopType to each session for unified display const allSwotSessions = withWorkshopType(swotSessions, 'swot'); @@ -53,6 +83,12 @@ export default async function SessionsPage() { const allWeeklyCheckInSessions = withWorkshopType(weeklyCheckInSessions, 'weekly-checkin'); const allWeatherSessions = withWorkshopType(weatherSessions, 'weather'); + const teamSwotWithType = withWorkshopType(teamSwotSessions, 'swot'); + const teamMotivatorWithType = withWorkshopType(teamMotivatorSessions, 'motivators'); + const teamYearReviewWithType = withWorkshopType(teamYearReviewSessions, 'year-review'); + const teamWeeklyCheckInWithType = withWorkshopType(teamWeeklyCheckInSessions, 'weekly-checkin'); + const teamWeatherWithType = withWorkshopType(teamWeatherSessions, 'weather'); + // Combine and sort by updatedAt const allSessions = [ ...allSwotSessions, @@ -99,6 +135,13 @@ export default async function SessionsPage() { yearReviewSessions={allYearReviewSessions} weeklyCheckInSessions={allWeeklyCheckInSessions} weatherSessions={allWeatherSessions} + teamCollabSessions={[ + ...teamSwotWithType, + ...teamMotivatorWithType, + ...teamYearReviewWithType, + ...teamWeeklyCheckInWithType, + ...teamWeatherWithType, + ]} /> )} diff --git a/src/lib/workshops.ts b/src/lib/workshops.ts index bae092a..5d14f3d 100644 --- a/src/lib/workshops.ts +++ b/src/lib/workshops.ts @@ -13,12 +13,13 @@ export const WORKSHOP_TYPE_IDS = [ export type WorkshopTypeId = (typeof WORKSHOP_TYPE_IDS)[number]; -export type WorkshopTabType = WorkshopTypeId | 'all' | 'byPerson'; +export type WorkshopTabType = WorkshopTypeId | 'all' | 'byPerson' | 'team'; export const VALID_TAB_PARAMS: WorkshopTabType[] = [ 'all', ...WORKSHOP_TYPE_IDS, 'byPerson', + 'team', ]; export interface WorkshopConfig { diff --git a/src/services/moving-motivators.ts b/src/services/moving-motivators.ts index c197198..bafab3f 100644 --- a/src/services/moving-motivators.ts +++ b/src/services/moving-motivators.ts @@ -1,5 +1,6 @@ import { prisma } from '@/services/database'; import { resolveCollaborator } from '@/services/auth'; +import { getTeamMemberIdsForAdminTeams } from '@/services/teams'; import type { ShareRole, MotivatorType } from '@prisma/client'; // ============================================ @@ -76,6 +77,43 @@ export async function getMotivatorSessionsByUserId(userId: string) { return sessionsWithResolved; } +/** Sessions owned by team members (where user is team admin) that are NOT shared with the user. */ +export async function getTeamCollaboratorSessionsForAdmin(userId: string) { + const teamMemberIds = await getTeamMemberIdsForAdminTeams(userId); + if (teamMemberIds.length === 0) return []; + + const sessions = await prisma.movingMotivatorsSession.findMany({ + where: { + userId: { in: teamMemberIds }, + shares: { none: { userId } }, + }, + include: { + user: { select: { id: true, name: true, email: true } }, + shares: { + include: { + user: { select: { id: true, name: true, email: true } }, + }, + }, + _count: { select: { cards: true } }, + }, + orderBy: { updatedAt: 'desc' }, + }); + + const withRole = sessions.map((s) => ({ + ...s, + isOwner: false as const, + role: 'VIEWER' as const, + isTeamCollab: true as const, + })); + + return Promise.all( + withRole.map(async (s) => ({ + ...s, + resolvedParticipant: await resolveCollaborator(s.participant), + })) + ); +} + export async function getMotivatorSessionById(sessionId: string, userId: string) { // Check if user owns the session OR has it shared const session = await prisma.movingMotivatorsSession.findFirst({ diff --git a/src/services/sessions.ts b/src/services/sessions.ts index b8ee70b..566dd9b 100644 --- a/src/services/sessions.ts +++ b/src/services/sessions.ts @@ -1,5 +1,6 @@ import { prisma } from '@/services/database'; import { resolveCollaborator } from '@/services/auth'; +import { getTeamMemberIdsForAdminTeams } from '@/services/teams'; import type { SwotCategory, ShareRole } from '@prisma/client'; // ============================================ @@ -78,6 +79,48 @@ export async function getSessionsByUserId(userId: string) { return sessionsWithResolved; } +/** Sessions owned by team members (where user is team admin) that are NOT shared with the user. */ +export async function getTeamCollaboratorSessionsForAdmin(userId: string) { + const teamMemberIds = await getTeamMemberIdsForAdminTeams(userId); + if (teamMemberIds.length === 0) return []; + + const sessions = await prisma.session.findMany({ + where: { + userId: { in: teamMemberIds }, + shares: { none: { userId } }, + }, + include: { + user: { select: { id: true, name: true, email: true } }, + shares: { + include: { + user: { select: { id: true, name: true, email: true } }, + }, + }, + _count: { + select: { + items: true, + actions: true, + }, + }, + }, + orderBy: { updatedAt: 'desc' }, + }); + + const withRole = sessions.map((s) => ({ + ...s, + isOwner: false as const, + role: 'VIEWER' as const, + isTeamCollab: true as const, + })); + + return Promise.all( + withRole.map(async (s) => ({ + ...s, + resolvedCollaborator: await resolveCollaborator(s.collaborator), + })) + ); +} + export async function getSessionById(sessionId: string, userId: string) { // Check if user owns the session OR has it shared const session = await prisma.session.findFirst({ diff --git a/src/services/teams.ts b/src/services/teams.ts index 0d0774f..5c3f753 100644 --- a/src/services/teams.ts +++ b/src/services/teams.ts @@ -244,6 +244,28 @@ export async function getTeamMember(teamId: string, userId: string) { }); } +/** Returns user IDs of all members in teams where the given user is ADMIN (excluding self). */ +export async function getTeamMemberIdsForAdminTeams(userId: string): Promise { + const adminTeams = await prisma.teamMember.findMany({ + where: { + userId, + role: 'ADMIN', + }, + select: { teamId: true }, + }); + if (adminTeams.length === 0) return []; + + const members = await prisma.teamMember.findMany({ + where: { + teamId: { in: adminTeams.map((t) => t.teamId) }, + userId: { not: userId }, + }, + select: { userId: true }, + distinct: ['userId'], + }); + return members.map((m) => m.userId); +} + export async function getTeamMemberById(teamMemberId: string) { return prisma.teamMember.findUnique({ where: { id: teamMemberId }, diff --git a/src/services/weather.ts b/src/services/weather.ts index a58cf5d..72e10d4 100644 --- a/src/services/weather.ts +++ b/src/services/weather.ts @@ -1,4 +1,5 @@ import { prisma } from '@/services/database'; +import { getTeamMemberIdsForAdminTeams } from '@/services/teams'; import type { ShareRole } from '@prisma/client'; // ============================================ @@ -67,6 +68,36 @@ export async function getWeatherSessionsByUserId(userId: string) { return allSessions; } +/** Sessions owned by team members (where user is team admin) that are NOT shared with the user. */ +export async function getTeamCollaboratorSessionsForAdmin(userId: string) { + const teamMemberIds = await getTeamMemberIdsForAdminTeams(userId); + if (teamMemberIds.length === 0) return []; + + const sessions = await prisma.weatherSession.findMany({ + where: { + userId: { in: teamMemberIds }, + shares: { none: { userId } }, + }, + include: { + user: { select: { id: true, name: true, email: true } }, + shares: { + include: { + user: { select: { id: true, name: true, email: true } }, + }, + }, + _count: { select: { entries: true } }, + }, + orderBy: { updatedAt: 'desc' }, + }); + + return sessions.map((s) => ({ + ...s, + isOwner: false as const, + role: 'VIEWER' as const, + isTeamCollab: true as const, + })); +} + export async function getWeatherSessionById(sessionId: string, userId: string) { // Check if user owns the session OR has it shared const session = await prisma.weatherSession.findFirst({ diff --git a/src/services/weekly-checkin.ts b/src/services/weekly-checkin.ts index d4875ae..337c5e3 100644 --- a/src/services/weekly-checkin.ts +++ b/src/services/weekly-checkin.ts @@ -1,5 +1,6 @@ import { prisma } from '@/services/database'; import { resolveCollaborator } from '@/services/auth'; +import { getTeamMemberIdsForAdminTeams } from '@/services/teams'; import type { ShareRole, WeeklyCheckInCategory, Emotion } from '@prisma/client'; // ============================================ @@ -76,6 +77,43 @@ export async function getWeeklyCheckInSessionsByUserId(userId: string) { return sessionsWithResolved; } +/** Sessions owned by team members (where user is team admin) that are NOT shared with the user. */ +export async function getTeamCollaboratorSessionsForAdmin(userId: string) { + const teamMemberIds = await getTeamMemberIdsForAdminTeams(userId); + if (teamMemberIds.length === 0) return []; + + const sessions = await prisma.weeklyCheckInSession.findMany({ + where: { + userId: { in: teamMemberIds }, + shares: { none: { userId } }, + }, + include: { + user: { select: { id: true, name: true, email: true } }, + shares: { + include: { + user: { select: { id: true, name: true, email: true } }, + }, + }, + _count: { select: { items: true } }, + }, + orderBy: { updatedAt: 'desc' }, + }); + + const withRole = sessions.map((s) => ({ + ...s, + isOwner: false as const, + role: 'VIEWER' as const, + isTeamCollab: true as const, + })); + + return Promise.all( + withRole.map(async (s) => ({ + ...s, + resolvedParticipant: await resolveCollaborator(s.participant), + })) + ); +} + export async function getWeeklyCheckInSessionById(sessionId: string, userId: string) { // Check if user owns the session OR has it shared const session = await prisma.weeklyCheckInSession.findFirst({ diff --git a/src/services/year-review.ts b/src/services/year-review.ts index 4656007..40a187b 100644 --- a/src/services/year-review.ts +++ b/src/services/year-review.ts @@ -1,5 +1,6 @@ import { prisma } from '@/services/database'; import { resolveCollaborator } from '@/services/auth'; +import { getTeamMemberIdsForAdminTeams } from '@/services/teams'; import type { ShareRole, YearReviewCategory } from '@prisma/client'; // ============================================ @@ -76,6 +77,43 @@ export async function getYearReviewSessionsByUserId(userId: string) { return sessionsWithResolved; } +/** Sessions owned by team members (where user is team admin) that are NOT shared with the user. */ +export async function getTeamCollaboratorSessionsForAdmin(userId: string) { + const teamMemberIds = await getTeamMemberIdsForAdminTeams(userId); + if (teamMemberIds.length === 0) return []; + + const sessions = await prisma.yearReviewSession.findMany({ + where: { + userId: { in: teamMemberIds }, + shares: { none: { userId } }, + }, + include: { + user: { select: { id: true, name: true, email: true } }, + shares: { + include: { + user: { select: { id: true, name: true, email: true } }, + }, + }, + _count: { select: { items: true } }, + }, + orderBy: { updatedAt: 'desc' }, + }); + + const withRole = sessions.map((s) => ({ + ...s, + isOwner: false as const, + role: 'VIEWER' as const, + isTeamCollab: true as const, + })); + + return Promise.all( + withRole.map(async (s) => ({ + ...s, + resolvedParticipant: await resolveCollaborator(s.participant), + })) + ); +} + export async function getYearReviewSessionById(sessionId: string, userId: string) { // Check if user owns the session OR has it shared const session = await prisma.yearReviewSession.findFirst({