diff --git a/src/app/motivators/[id]/page.tsx b/src/app/motivators/[id]/page.tsx index 9ccffd8..ae862bc 100644 --- a/src/app/motivators/[id]/page.tsx +++ b/src/app/motivators/[id]/page.tsx @@ -3,6 +3,7 @@ import Link from 'next/link'; import { auth } from '@/lib/auth'; import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops'; import { getMotivatorSessionById } from '@/services/moving-motivators'; +import { getUserTeams } from '@/services/teams'; import type { ResolvedCollaborator } from '@/services/auth'; import { MotivatorBoard, MotivatorLiveWrapper } from '@/components/moving-motivators'; import { Badge, CollaboratorDisplay } from '@/components/ui'; @@ -20,7 +21,10 @@ export default async function MotivatorSessionPage({ params }: MotivatorSessionP return null; } - const session = await getMotivatorSessionById(id, authSession.user.id); + const [session, userTeams] = await Promise.all([ + getMotivatorSessionById(id, authSession.user.id), + getUserTeams(authSession.user.id), + ]); if (!session) { notFound(); @@ -81,6 +85,7 @@ export default async function MotivatorSessionPage({ params }: MotivatorSessionP shares={session.shares} isOwner={session.isOwner} canEdit={session.canEdit} + userTeams={userTeams} > diff --git a/src/app/sessions/[id]/page.tsx b/src/app/sessions/[id]/page.tsx index e33d73e..8b572c6 100644 --- a/src/app/sessions/[id]/page.tsx +++ b/src/app/sessions/[id]/page.tsx @@ -3,6 +3,7 @@ import Link from 'next/link'; import { auth } from '@/lib/auth'; import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops'; import { getSessionById } from '@/services/sessions'; +import { getUserTeams } from '@/services/teams'; import { SwotBoard } from '@/components/swot/SwotBoard'; import { SessionLiveWrapper } from '@/components/collaboration'; import { EditableSessionTitle } from '@/components/ui'; @@ -20,7 +21,10 @@ export default async function SessionPage({ params }: SessionPageProps) { return null; } - const session = await getSessionById(id, authSession.user.id); + const [session, userTeams] = await Promise.all([ + getSessionById(id, authSession.user.id), + getUserTeams(authSession.user.id), + ]); if (!session) { notFound(); @@ -80,6 +84,7 @@ export default async function SessionPage({ params }: SessionPageProps) { shares={session.shares} isOwner={session.isOwner} canEdit={session.canEdit} + userTeams={userTeams} > diff --git a/src/app/weekly-checkin/[id]/page.tsx b/src/app/weekly-checkin/[id]/page.tsx index fed2f61..aef8caa 100644 --- a/src/app/weekly-checkin/[id]/page.tsx +++ b/src/app/weekly-checkin/[id]/page.tsx @@ -3,6 +3,7 @@ import Link from 'next/link'; import { auth } from '@/lib/auth'; import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops'; import { getWeeklyCheckInSessionById } from '@/services/weekly-checkin'; +import { getUserTeams } from '@/services/teams'; import type { ResolvedCollaborator } from '@/services/auth'; import { getUserOKRsForPeriod } from '@/services/okrs'; import { getCurrentQuarterPeriod } from '@/lib/okr-utils'; @@ -23,7 +24,10 @@ export default async function WeeklyCheckInSessionPage({ params }: WeeklyCheckIn return null; } - const session = await getWeeklyCheckInSessionById(id, authSession.user.id); + const [session, userTeams] = await Promise.all([ + getWeeklyCheckInSessionById(id, authSession.user.id), + getUserTeams(authSession.user.id), + ]); if (!session) { notFound(); @@ -96,6 +100,7 @@ export default async function WeeklyCheckInSessionPage({ params }: WeeklyCheckIn shares={session.shares} isOwner={session.isOwner} canEdit={session.canEdit} + userTeams={userTeams} > diff --git a/src/app/year-review/[id]/page.tsx b/src/app/year-review/[id]/page.tsx index 7215316..71608cc 100644 --- a/src/app/year-review/[id]/page.tsx +++ b/src/app/year-review/[id]/page.tsx @@ -3,6 +3,7 @@ import Link from 'next/link'; import { auth } from '@/lib/auth'; import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops'; import { getYearReviewSessionById } from '@/services/year-review'; +import { getUserTeams } from '@/services/teams'; import type { ResolvedCollaborator } from '@/services/auth'; import { YearReviewBoard, YearReviewLiveWrapper } from '@/components/year-review'; import { Badge, CollaboratorDisplay } from '@/components/ui'; @@ -20,7 +21,10 @@ export default async function YearReviewSessionPage({ params }: YearReviewSessio return null; } - const session = await getYearReviewSessionById(id, authSession.user.id); + const [session, userTeams] = await Promise.all([ + getYearReviewSessionById(id, authSession.user.id), + getUserTeams(authSession.user.id), + ]); if (!session) { notFound(); @@ -80,6 +84,7 @@ export default async function YearReviewSessionPage({ params }: YearReviewSessio shares={session.shares} isOwner={session.isOwner} canEdit={session.canEdit} + userTeams={userTeams} > diff --git a/src/components/collaboration/SessionLiveWrapper.tsx b/src/components/collaboration/SessionLiveWrapper.tsx index 382801a..bab4b6a 100644 --- a/src/components/collaboration/SessionLiveWrapper.tsx +++ b/src/components/collaboration/SessionLiveWrapper.tsx @@ -4,9 +4,11 @@ import { useState, useCallback } from 'react'; import { useSessionLive, type LiveEvent } from '@/hooks/useSessionLive'; import { LiveIndicator } from './LiveIndicator'; import { ShareModal } from './ShareModal'; +import { shareSessionAction, removeShareAction } from '@/actions/share'; import { Button } from '@/components/ui/Button'; import { Avatar } from '@/components/ui/Avatar'; import type { ShareRole } from '@prisma/client'; +import type { TeamWithMembers } from '@/lib/share-utils'; interface ShareUser { id: string; @@ -28,6 +30,7 @@ interface SessionLiveWrapperProps { shares: Share[]; isOwner: boolean; canEdit: boolean; + userTeams?: TeamWithMembers[]; children: React.ReactNode; } @@ -38,6 +41,7 @@ export function SessionLiveWrapper({ shares, isOwner, canEdit, + userTeams = [], children, }: SessionLiveWrapperProps) { const [shareModalOpen, setShareModalOpen] = useState(false); @@ -122,10 +126,22 @@ export function SessionLiveWrapper({ setShareModalOpen(false)} - sessionId={sessionId} + title="Partager la session" + sessionSubtitle="Session" sessionTitle={sessionTitle} shares={shares} isOwner={isOwner} + userTeams={userTeams} + currentUserId={currentUserId} + onShareWithEmail={(email, role) => shareSessionAction(sessionId, email, role)} + onRemoveShare={(userId) => removeShareAction(sessionId, userId)} + helpText={ + <> + Éditeur : peut modifier les items et actions +
+ Lecteur : peut uniquement consulter + + } /> ); diff --git a/src/components/collaboration/ShareModal.tsx b/src/components/collaboration/ShareModal.tsx index f407206..7227754 100644 --- a/src/components/collaboration/ShareModal.tsx +++ b/src/components/collaboration/ShareModal.tsx @@ -1,12 +1,13 @@ 'use client'; import { useState, useTransition } from 'react'; +import Link from 'next/link'; import { Modal } from '@/components/ui/Modal'; import { Input } from '@/components/ui/Input'; import { Button } from '@/components/ui/Button'; import { Badge } from '@/components/ui/Badge'; import { Avatar } from '@/components/ui/Avatar'; -import { shareSessionAction, removeShareAction } from '@/actions/share'; +import { getTeamMembersForShare, type TeamWithMembers } from '@/lib/share-utils'; import type { ShareRole } from '@prisma/client'; interface ShareUser { @@ -19,39 +20,79 @@ interface Share { id: string; role: ShareRole; user: ShareUser; - createdAt: Date; + createdAt?: Date; } +type ShareTab = 'teamMember' | 'team' | 'email'; + interface ShareModalProps { isOpen: boolean; onClose: () => void; - sessionId: string; + title: string; + sessionSubtitle?: string; sessionTitle: string; shares: Share[]; isOwner: boolean; + userTeams?: TeamWithMembers[]; + currentUserId?: string; + onShareWithEmail: (email: string, role: ShareRole) => Promise<{ success: boolean; error?: string }>; + onShareWithTeam?: (teamId: string, role: ShareRole) => Promise<{ success: boolean; error?: string }>; + onRemoveShare: (userId: string) => Promise; + helpText?: React.ReactNode; } +const SELECT_STYLE = + 'appearance-none rounded-lg border border-border bg-card px-3 py-2.5 pr-10 text-sm text-foreground transition-colors hover:bg-card-hover focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20'; + export function ShareModal({ isOpen, onClose, - sessionId, + title, + sessionSubtitle, sessionTitle, shares, isOwner, + userTeams = [], + currentUserId = '', + onShareWithEmail, + onShareWithTeam, + onRemoveShare, + helpText, }: ShareModalProps) { + const teamMembers = getTeamMembersForShare(userTeams, currentUserId); + const hasTeamShare = !!onShareWithTeam; + const [shareType, setShareType] = useState('teamMember'); const [email, setEmail] = useState(''); + const [teamId, setTeamId] = useState(''); + const [selectedMemberId, setSelectedMemberId] = useState(''); const [role, setRole] = useState('EDITOR'); const [error, setError] = useState(null); const [isPending, startTransition] = useTransition(); + const resetForm = () => { + setEmail(''); + setTeamId(''); + setSelectedMemberId(''); + }; + async function handleShare(e: React.FormEvent) { e.preventDefault(); setError(null); startTransition(async () => { - const result = await shareSessionAction(sessionId, email, role); + let result: { success: boolean; error?: string }; + if (shareType === 'team' && onShareWithTeam) { + result = await onShareWithTeam(teamId, role); + } else { + const targetEmail = + shareType === 'teamMember' + ? teamMembers.find((m) => m.id === selectedMemberId)?.email ?? '' + : email; + result = await onShareWithEmail(targetEmail, role); + } + if (result.success) { - setEmail(''); + resetForm(); } else { setError(result.error || 'Erreur lors du partage'); } @@ -60,53 +101,172 @@ export function ShareModal({ async function handleRemove(userId: string) { startTransition(async () => { - await removeShareAction(sessionId, userId); + await onRemoveShare(userId); }); } + const tabs: { value: ShareTab; label: string; icon: string }[] = [ + { value: 'teamMember', label: 'Membre', icon: '👥' }, + ...(hasTeamShare ? [{ value: 'team' as ShareTab, label: 'Équipe', icon: '🏢' }] : []), + { value: 'email', label: 'Email', icon: '👤' }, + ]; + return ( - +
- {/* Session info */}
-

Session

+ {sessionSubtitle &&

{sessionSubtitle}

}

{sessionTitle}

- {/* Share form (only for owner) */} {isOwner && (
-
- setEmail(e.target.value)} - className="flex-1" - required - /> - +
+ {tabs.map((tab) => ( + + ))}
+ {shareType === 'email' && ( +
+ setEmail(e.target.value)} + className="flex-1" + required + /> + +
+ )} + + {shareType === 'teamMember' && ( +
+ {teamMembers.length === 0 ? ( +

+ Vous n'êtes membre d'aucune équipe ou vos équipes n'ont pas d'autres membres. + Créez une équipe depuis la page{' '} + + Équipes + + . +

+ ) : ( +
+
+ +
+ + + +
+
+ +
+ )} +
+ )} + + {shareType === 'team' && hasTeamShare && ( +
+ {userTeams.length === 0 ? ( +

+ Vous n'êtes membre d'aucune équipe. Créez une équipe depuis la page{' '} + + Équipes + + . +

+ ) : ( + <> +
+ +
+ + + +
+
+
+ +
+ + + +
+
+ + )} +
+ )} + {error &&

{error}

} - )} - {/* Current shares */}

Collaborateurs ({shares.length})

- {shares.length === 0 ? (

Aucun collaborateur pour le moment

) : ( @@ -125,7 +285,6 @@ export function ShareModal({ {share.user.name &&

{share.user.email}

}
-
{share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'} @@ -137,12 +296,7 @@ export function ShareModal({ className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive" title="Retirer l'accès" > - + - {/* Help text */} -
-

- Éditeur : peut modifier les items et actions -
- Lecteur : peut uniquement consulter -

-
+ {helpText &&
{helpText}
}
); diff --git a/src/components/moving-motivators/MotivatorLiveWrapper.tsx b/src/components/moving-motivators/MotivatorLiveWrapper.tsx index 101aee6..4bd4779 100644 --- a/src/components/moving-motivators/MotivatorLiveWrapper.tsx +++ b/src/components/moving-motivators/MotivatorLiveWrapper.tsx @@ -3,7 +3,9 @@ import { useState, useCallback } from 'react'; import { useMotivatorLive, type MotivatorLiveEvent } from '@/hooks/useMotivatorLive'; import { LiveIndicator } from '@/components/collaboration/LiveIndicator'; -import { MotivatorShareModal } from './MotivatorShareModal'; +import { ShareModal } from '@/components/collaboration/ShareModal'; +import { shareMotivatorSession, removeMotivatorShare } from '@/actions/moving-motivators'; +import type { TeamWithMembers } from '@/lib/share-utils'; import { Button } from '@/components/ui/Button'; import { Avatar } from '@/components/ui/Avatar'; import type { ShareRole } from '@prisma/client'; @@ -28,6 +30,7 @@ interface MotivatorLiveWrapperProps { shares: Share[]; isOwner: boolean; canEdit: boolean; + userTeams?: TeamWithMembers[]; children: React.ReactNode; } @@ -38,6 +41,7 @@ export function MotivatorLiveWrapper({ shares, isOwner, canEdit, + userTeams = [], children, }: MotivatorLiveWrapperProps) { const [shareModalOpen, setShareModalOpen] = useState(false); @@ -119,13 +123,25 @@ export function MotivatorLiveWrapper({
{children}
{/* Share Modal */} - setShareModalOpen(false)} - sessionId={sessionId} + title="Partager la session" + sessionSubtitle="Session Moving Motivators" sessionTitle={sessionTitle} shares={shares} isOwner={isOwner} + userTeams={userTeams} + currentUserId={currentUserId} + onShareWithEmail={(email, role) => shareMotivatorSession(sessionId, email, role)} + onRemoveShare={(userId) => removeMotivatorShare(sessionId, userId)} + helpText={ + <> + Éditeur : peut modifier les cartes et leurs positions +
+ Lecteur : peut uniquement consulter + + } /> ); diff --git a/src/components/moving-motivators/MotivatorShareModal.tsx b/src/components/moving-motivators/MotivatorShareModal.tsx deleted file mode 100644 index 1769e95..0000000 --- a/src/components/moving-motivators/MotivatorShareModal.tsx +++ /dev/null @@ -1,172 +0,0 @@ -'use client'; - -import { useState, useTransition } from 'react'; -import { Modal } from '@/components/ui/Modal'; -import { Input } from '@/components/ui/Input'; -import { Button } from '@/components/ui/Button'; -import { Badge } from '@/components/ui/Badge'; -import { Avatar } from '@/components/ui/Avatar'; -import { shareMotivatorSession, removeMotivatorShare } from '@/actions/moving-motivators'; -import type { ShareRole } from '@prisma/client'; - -interface ShareUser { - id: string; - name: string | null; - email: string; -} - -interface Share { - id: string; - role: ShareRole; - user: ShareUser; - createdAt: Date; -} - -interface MotivatorShareModalProps { - isOpen: boolean; - onClose: () => void; - sessionId: string; - sessionTitle: string; - shares: Share[]; - isOwner: boolean; -} - -export function MotivatorShareModal({ - isOpen, - onClose, - sessionId, - sessionTitle, - shares, - isOwner, -}: MotivatorShareModalProps) { - const [email, setEmail] = useState(''); - const [role, setRole] = useState('EDITOR'); - const [error, setError] = useState(null); - const [isPending, startTransition] = useTransition(); - - async function handleShare(e: React.FormEvent) { - e.preventDefault(); - setError(null); - - startTransition(async () => { - const result = await shareMotivatorSession(sessionId, email, role); - if (result.success) { - setEmail(''); - } else { - setError(result.error || 'Erreur lors du partage'); - } - }); - } - - async function handleRemove(userId: string) { - startTransition(async () => { - await removeMotivatorShare(sessionId, userId); - }); - } - - return ( - -
- {/* Session info */} -
-

Session Moving Motivators

-

{sessionTitle}

-
- - {/* Share form (only for owner) */} - {isOwner && ( -
-
- setEmail(e.target.value)} - className="flex-1" - required - /> - -
- - {error &&

{error}

} - - -
- )} - - {/* Current shares */} -
-

Collaborateurs ({shares.length})

- - {shares.length === 0 ? ( -

Aucun collaborateur pour le moment

- ) : ( -
    - {shares.map((share) => ( -
  • -
    - -
    -

    - {share.user.name || share.user.email} -

    - {share.user.name &&

    {share.user.email}

    } -
    -
    - -
    - - {share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'} - - {isOwner && ( - - )} -
    -
  • - ))} -
- )} -
- - {/* Help text */} -
-

- Éditeur : peut modifier les cartes et leurs positions -
- Lecteur : peut uniquement consulter -

-
-
-
- ); -} diff --git a/src/components/moving-motivators/index.ts b/src/components/moving-motivators/index.ts index f2a38db..7f09362 100644 --- a/src/components/moving-motivators/index.ts +++ b/src/components/moving-motivators/index.ts @@ -3,4 +3,3 @@ export { MotivatorCard, MotivatorCardStatic } from './MotivatorCard'; export { MotivatorSummary } from './MotivatorSummary'; export { InfluenceZone } from './InfluenceZone'; export { MotivatorLiveWrapper } from './MotivatorLiveWrapper'; -export { MotivatorShareModal } from './MotivatorShareModal'; diff --git a/src/components/weather/WeatherLiveWrapper.tsx b/src/components/weather/WeatherLiveWrapper.tsx index f71b629..d764256 100644 --- a/src/components/weather/WeatherLiveWrapper.tsx +++ b/src/components/weather/WeatherLiveWrapper.tsx @@ -3,7 +3,8 @@ import { useState, useCallback } from 'react'; import { useWeatherLive, type WeatherLiveEvent } from '@/hooks/useWeatherLive'; import { LiveIndicator } from '@/components/collaboration/LiveIndicator'; -import { WeatherShareModal } from './WeatherShareModal'; +import { ShareModal } from '@/components/collaboration/ShareModal'; +import { shareWeatherSession, shareWeatherSessionToTeam, removeWeatherShare } from '@/actions/weather'; import { Button } from '@/components/ui/Button'; import { Avatar } from '@/components/ui/Avatar'; import type { ShareRole } from '@prisma/client'; @@ -128,14 +129,26 @@ export function WeatherLiveWrapper({
{children}
{/* Share Modal */} - setShareModalOpen(false)} - sessionId={sessionId} + title="Partager la météo" + sessionSubtitle="Météo personnelle" sessionTitle={sessionTitle} shares={shares} isOwner={isOwner} userTeams={userTeams} + currentUserId={currentUserId} + onShareWithEmail={(email, role) => shareWeatherSession(sessionId, email, role)} + onShareWithTeam={(teamId, role) => shareWeatherSessionToTeam(sessionId, teamId, role)} + onRemoveShare={(userId) => removeWeatherShare(sessionId, userId)} + helpText={ + <> + Éditeur : peut modifier sa météo et voir celle des autres +
+ Lecteur : peut uniquement consulter + + } /> ); diff --git a/src/components/weather/WeatherShareModal.tsx b/src/components/weather/WeatherShareModal.tsx deleted file mode 100644 index 5829929..0000000 --- a/src/components/weather/WeatherShareModal.tsx +++ /dev/null @@ -1,307 +0,0 @@ -'use client'; - -import { useState, useTransition } from 'react'; -import Link from 'next/link'; -import { Modal } from '@/components/ui/Modal'; -import { Input } from '@/components/ui/Input'; -import { Button } from '@/components/ui/Button'; -import { Badge } from '@/components/ui/Badge'; -import { Avatar } from '@/components/ui/Avatar'; -import { shareWeatherSession, shareWeatherSessionToTeam, removeWeatherShare } from '@/actions/weather'; -import type { ShareRole } from '@prisma/client'; - -interface ShareUser { - id: string; - name: string | null; - email: string; -} - -interface Share { - id: string; - role: ShareRole; - user: ShareUser; - createdAt: Date; -} - -interface Team { - id: string; - name: string; - description: string | null; - userRole: 'ADMIN' | 'MEMBER'; -} - -interface WeatherShareModalProps { - isOpen: boolean; - onClose: () => void; - sessionId: string; - sessionTitle: string; - shares: Share[]; - isOwner: boolean; - userTeams?: Team[]; -} - -export function WeatherShareModal({ - isOpen, - onClose, - sessionId, - sessionTitle, - shares, - isOwner, - userTeams = [], -}: WeatherShareModalProps) { - const [shareType, setShareType] = useState<'user' | 'team'>('user'); - const [email, setEmail] = useState(''); - const [teamId, setTeamId] = useState(''); - const [role, setRole] = useState('EDITOR'); - const [error, setError] = useState(null); - const [isPending, startTransition] = useTransition(); - - async function handleShare(e: React.FormEvent) { - e.preventDefault(); - setError(null); - - startTransition(async () => { - let result; - if (shareType === 'team') { - result = await shareWeatherSessionToTeam(sessionId, teamId, role); - } else { - result = await shareWeatherSession(sessionId, email, role); - } - - if (result.success) { - setEmail(''); - setTeamId(''); - } else { - setError(result.error || 'Erreur lors du partage'); - } - }); - } - - async function handleRemove(userId: string) { - startTransition(async () => { - await removeWeatherShare(sessionId, userId); - }); - } - - return ( - -
- {/* Session info */} -
-

Météo personnelle

-

{sessionTitle}

-
- - {/* Share form (only for owner) */} - {isOwner && ( -
- {/* Share type selector */} -
- - -
- - {/* User share */} - {shareType === 'user' && ( -
- setEmail(e.target.value)} - className="flex-1" - required - /> -
- -
- - - -
-
-
- )} - - {/* Team share */} - {shareType === 'team' && ( -
- {userTeams.length === 0 ? ( -

- Vous n'êtes membre d'aucune équipe. Créez une équipe depuis la page{' '} - - Équipes - - . -

- ) : ( - <> -
- -
- - - -
-
-
- -
- - - -
-
- - )} -
- )} - - {error &&

{error}

} - - -
- )} - - {/* Current shares */} -
-

Collaborateurs ({shares.length})

- - {shares.length === 0 ? ( -

Aucun collaborateur pour le moment

- ) : ( -
    - {shares.map((share) => ( -
  • -
    - -
    -

    - {share.user.name || share.user.email} -

    - {share.user.name &&

    {share.user.email}

    } -
    -
    - -
    - - {share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'} - - {isOwner && ( - - )} -
    -
  • - ))} -
- )} -
- - {/* Help text */} -
-

- Éditeur : peut modifier sa météo et voir celle des autres -
- Lecteur : peut uniquement consulter -

-
-
-
- ); -} diff --git a/src/components/weather/index.ts b/src/components/weather/index.ts index 5873c4f..3cf0761 100644 --- a/src/components/weather/index.ts +++ b/src/components/weather/index.ts @@ -1,5 +1,4 @@ export { WeatherBoard } from './WeatherBoard'; export { WeatherCard } from './WeatherCard'; export { WeatherLiveWrapper } from './WeatherLiveWrapper'; -export { WeatherShareModal } from './WeatherShareModal'; export { WeatherInfoPanel } from './WeatherInfoPanel'; diff --git a/src/components/weekly-checkin/WeeklyCheckInLiveWrapper.tsx b/src/components/weekly-checkin/WeeklyCheckInLiveWrapper.tsx index ad5423c..2f51e33 100644 --- a/src/components/weekly-checkin/WeeklyCheckInLiveWrapper.tsx +++ b/src/components/weekly-checkin/WeeklyCheckInLiveWrapper.tsx @@ -3,7 +3,9 @@ import { useState, useCallback } from 'react'; import { useWeeklyCheckInLive, type WeeklyCheckInLiveEvent } from '@/hooks/useWeeklyCheckInLive'; import { LiveIndicator } from '@/components/collaboration/LiveIndicator'; -import { WeeklyCheckInShareModal } from './WeeklyCheckInShareModal'; +import { ShareModal } from '@/components/collaboration/ShareModal'; +import { shareWeeklyCheckInSession, removeWeeklyCheckInShare } from '@/actions/weekly-checkin'; +import type { TeamWithMembers } from '@/lib/share-utils'; import { Button } from '@/components/ui/Button'; import { Avatar } from '@/components/ui/Avatar'; import type { ShareRole } from '@prisma/client'; @@ -28,6 +30,7 @@ interface WeeklyCheckInLiveWrapperProps { shares: Share[]; isOwner: boolean; canEdit: boolean; + userTeams?: TeamWithMembers[]; children: React.ReactNode; } @@ -38,6 +41,7 @@ export function WeeklyCheckInLiveWrapper({ shares, isOwner, canEdit, + userTeams = [], children, }: WeeklyCheckInLiveWrapperProps) { const [shareModalOpen, setShareModalOpen] = useState(false); @@ -119,13 +123,25 @@ export function WeeklyCheckInLiveWrapper({
{children}
{/* Share Modal */} - setShareModalOpen(false)} - sessionId={sessionId} + title="Options de partage" + sessionSubtitle="Check-in hebdomadaire" sessionTitle={sessionTitle} shares={shares} isOwner={isOwner} + userTeams={userTeams} + currentUserId={currentUserId} + onShareWithEmail={(email, role) => shareWeeklyCheckInSession(sessionId, email, role)} + onRemoveShare={(userId) => removeWeeklyCheckInShare(sessionId, userId)} + helpText={ + <> + Éditeur : peut modifier les items et leurs catégories +
+ Lecteur : peut uniquement consulter + + } /> ); diff --git a/src/components/weekly-checkin/WeeklyCheckInShareModal.tsx b/src/components/weekly-checkin/WeeklyCheckInShareModal.tsx deleted file mode 100644 index 86d96a2..0000000 --- a/src/components/weekly-checkin/WeeklyCheckInShareModal.tsx +++ /dev/null @@ -1,172 +0,0 @@ -'use client'; - -import { useState, useTransition } from 'react'; -import { Modal } from '@/components/ui/Modal'; -import { Input } from '@/components/ui/Input'; -import { Button } from '@/components/ui/Button'; -import { Badge } from '@/components/ui/Badge'; -import { Avatar } from '@/components/ui/Avatar'; -import { shareWeeklyCheckInSession, removeWeeklyCheckInShare } from '@/actions/weekly-checkin'; -import type { ShareRole } from '@prisma/client'; - -interface ShareUser { - id: string; - name: string | null; - email: string; -} - -interface Share { - id: string; - role: ShareRole; - user: ShareUser; - createdAt: Date; -} - -interface WeeklyCheckInShareModalProps { - isOpen: boolean; - onClose: () => void; - sessionId: string; - sessionTitle: string; - shares: Share[]; - isOwner: boolean; -} - -export function WeeklyCheckInShareModal({ - isOpen, - onClose, - sessionId, - sessionTitle, - shares, - isOwner, -}: WeeklyCheckInShareModalProps) { - const [email, setEmail] = useState(''); - const [role, setRole] = useState('EDITOR'); - const [error, setError] = useState(null); - const [isPending, startTransition] = useTransition(); - - async function handleShare(e: React.FormEvent) { - e.preventDefault(); - setError(null); - - startTransition(async () => { - const result = await shareWeeklyCheckInSession(sessionId, email, role); - if (result.success) { - setEmail(''); - } else { - setError(result.error || 'Erreur lors du partage'); - } - }); - } - - async function handleRemove(userId: string) { - startTransition(async () => { - await removeWeeklyCheckInShare(sessionId, userId); - }); - } - - return ( - -
- {/* Session info */} -
-

Check-in hebdomadaire

-

{sessionTitle}

-
- - {/* Share form (only for owner) */} - {isOwner && ( -
-
- setEmail(e.target.value)} - className="flex-1" - required - /> - -
- - {error &&

{error}

} - - -
- )} - - {/* Current shares */} -
-

Collaborateurs ({shares.length})

- - {shares.length === 0 ? ( -

Aucun collaborateur pour le moment

- ) : ( -
    - {shares.map((share) => ( -
  • -
    - -
    -

    - {share.user.name || share.user.email} -

    - {share.user.name &&

    {share.user.email}

    } -
    -
    - -
    - - {share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'} - - {isOwner && ( - - )} -
    -
  • - ))} -
- )} -
- - {/* Help text */} -
-

- Éditeur : peut modifier les items et leurs catégories -
- Lecteur : peut uniquement consulter -

-
-
-
- ); -} diff --git a/src/components/weekly-checkin/index.ts b/src/components/weekly-checkin/index.ts index 4456b9b..c66e0be 100644 --- a/src/components/weekly-checkin/index.ts +++ b/src/components/weekly-checkin/index.ts @@ -2,5 +2,4 @@ export { WeeklyCheckInBoard } from './WeeklyCheckInBoard'; export { WeeklyCheckInCard } from './WeeklyCheckInCard'; export { WeeklyCheckInSection } from './WeeklyCheckInSection'; export { WeeklyCheckInLiveWrapper } from './WeeklyCheckInLiveWrapper'; -export { WeeklyCheckInShareModal } from './WeeklyCheckInShareModal'; export { CurrentQuarterOKRs } from './CurrentQuarterOKRs'; diff --git a/src/components/year-review/YearReviewLiveWrapper.tsx b/src/components/year-review/YearReviewLiveWrapper.tsx index 8686273..3945009 100644 --- a/src/components/year-review/YearReviewLiveWrapper.tsx +++ b/src/components/year-review/YearReviewLiveWrapper.tsx @@ -3,7 +3,9 @@ import { useState, useCallback } from 'react'; import { useYearReviewLive, type YearReviewLiveEvent } from '@/hooks/useYearReviewLive'; import { LiveIndicator } from '@/components/collaboration/LiveIndicator'; -import { YearReviewShareModal } from './YearReviewShareModal'; +import { ShareModal } from '@/components/collaboration/ShareModal'; +import { shareYearReviewSession, removeYearReviewShare } from '@/actions/year-review'; +import type { TeamWithMembers } from '@/lib/share-utils'; import { Button } from '@/components/ui/Button'; import { Avatar } from '@/components/ui/Avatar'; import type { ShareRole } from '@prisma/client'; @@ -28,6 +30,7 @@ interface YearReviewLiveWrapperProps { shares: Share[]; isOwner: boolean; canEdit: boolean; + userTeams?: TeamWithMembers[]; children: React.ReactNode; } @@ -38,6 +41,7 @@ export function YearReviewLiveWrapper({ shares, isOwner, canEdit, + userTeams = [], children, }: YearReviewLiveWrapperProps) { const [shareModalOpen, setShareModalOpen] = useState(false); @@ -119,13 +123,25 @@ export function YearReviewLiveWrapper({
{children}
{/* Share Modal */} - setShareModalOpen(false)} - sessionId={sessionId} + title="Partager le bilan" + sessionSubtitle="Bilan annuel" sessionTitle={sessionTitle} shares={shares} isOwner={isOwner} + userTeams={userTeams} + currentUserId={currentUserId} + onShareWithEmail={(email, role) => shareYearReviewSession(sessionId, email, role)} + onRemoveShare={(userId) => removeYearReviewShare(sessionId, userId)} + helpText={ + <> + Éditeur : peut modifier les items et leurs catégories +
+ Lecteur : peut uniquement consulter + + } /> ); diff --git a/src/components/year-review/YearReviewShareModal.tsx b/src/components/year-review/YearReviewShareModal.tsx deleted file mode 100644 index 550760a..0000000 --- a/src/components/year-review/YearReviewShareModal.tsx +++ /dev/null @@ -1,173 +0,0 @@ -'use client'; - -import { useState, useTransition } from 'react'; -import { Modal } from '@/components/ui/Modal'; -import { Input } from '@/components/ui/Input'; -import { Button } from '@/components/ui/Button'; -import { Badge } from '@/components/ui/Badge'; -import { Avatar } from '@/components/ui/Avatar'; -import { shareYearReviewSession, removeYearReviewShare } from '@/actions/year-review'; -import type { ShareRole } from '@prisma/client'; - -interface ShareUser { - id: string; - name: string | null; - email: string; -} - -interface Share { - id: string; - role: ShareRole; - user: ShareUser; - createdAt: Date; -} - -interface YearReviewShareModalProps { - isOpen: boolean; - onClose: () => void; - sessionId: string; - sessionTitle: string; - shares: Share[]; - isOwner: boolean; -} - -export function YearReviewShareModal({ - isOpen, - onClose, - sessionId, - sessionTitle, - shares, - isOwner, -}: YearReviewShareModalProps) { - const [email, setEmail] = useState(''); - const [role, setRole] = useState('EDITOR'); - const [error, setError] = useState(null); - const [isPending, startTransition] = useTransition(); - - async function handleShare(e: React.FormEvent) { - e.preventDefault(); - setError(null); - - startTransition(async () => { - const result = await shareYearReviewSession(sessionId, email, role); - if (result.success) { - setEmail(''); - } else { - setError(result.error || 'Erreur lors du partage'); - } - }); - } - - async function handleRemove(userId: string) { - startTransition(async () => { - await removeYearReviewShare(sessionId, userId); - }); - } - - return ( - -
- {/* Session info */} -
-

Bilan annuel

-

{sessionTitle}

-
- - {/* Share form (only for owner) */} - {isOwner && ( -
-
- setEmail(e.target.value)} - className="flex-1" - required - /> - -
- - {error &&

{error}

} - - -
- )} - - {/* Current shares */} -
-

Collaborateurs ({shares.length})

- - {shares.length === 0 ? ( -

Aucun collaborateur pour le moment

- ) : ( -
    - {shares.map((share) => ( -
  • -
    - -
    -

    - {share.user.name || share.user.email} -

    - {share.user.name &&

    {share.user.email}

    } -
    -
    - -
    - - {share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'} - - {isOwner && ( - - )} -
    -
  • - ))} -
- )} -
- - {/* Help text */} -
-

- Éditeur : peut modifier les items et leurs catégories -
- Lecteur : peut uniquement consulter -

-
-
-
- ); -} - diff --git a/src/components/year-review/index.ts b/src/components/year-review/index.ts index df98b92..c0c24f2 100644 --- a/src/components/year-review/index.ts +++ b/src/components/year-review/index.ts @@ -2,4 +2,3 @@ export { YearReviewBoard } from './YearReviewBoard'; export { YearReviewCard } from './YearReviewCard'; export { YearReviewSection } from './YearReviewSection'; export { YearReviewLiveWrapper } from './YearReviewLiveWrapper'; -export { YearReviewShareModal } from './YearReviewShareModal'; diff --git a/src/lib/share-utils.ts b/src/lib/share-utils.ts new file mode 100644 index 0000000..c3a722c --- /dev/null +++ b/src/lib/share-utils.ts @@ -0,0 +1,37 @@ +/** + * Shared utilities for share modals across workshop types. + */ + +export interface TeamMemberUser { + id: string; + email: string; + name: string | null; +} + +export interface TeamWithMembers { + id: string; + name: string; + description: string | null; + userRole?: 'ADMIN' | 'MEMBER'; + members?: { user: TeamMemberUser }[]; +} + +/** + * Flatten team members from all teams, dedupe by userId, exclude current user. + */ +export function getTeamMembersForShare( + userTeams: TeamWithMembers[], + currentUserId: string +): TeamMemberUser[] { + const seen = new Set(); + return ( + userTeams + .flatMap((t) => t.members ?? []) + .map((m) => m.user) + .filter((u) => { + if (u.id === currentUserId || seen.has(u.id)) return false; + seen.add(u.id); + return true; + }) + ); +}