feat: integrate user team retrieval into session components, enhancing sharing functionality and user experience across motivators, sessions, weekly check-ins, and year reviews
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m35s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m35s
This commit is contained in:
@@ -3,6 +3,7 @@ import Link from 'next/link';
|
|||||||
import { auth } from '@/lib/auth';
|
import { auth } from '@/lib/auth';
|
||||||
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
|
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
|
||||||
import { getMotivatorSessionById } from '@/services/moving-motivators';
|
import { getMotivatorSessionById } from '@/services/moving-motivators';
|
||||||
|
import { getUserTeams } from '@/services/teams';
|
||||||
import type { ResolvedCollaborator } from '@/services/auth';
|
import type { ResolvedCollaborator } from '@/services/auth';
|
||||||
import { MotivatorBoard, MotivatorLiveWrapper } from '@/components/moving-motivators';
|
import { MotivatorBoard, MotivatorLiveWrapper } from '@/components/moving-motivators';
|
||||||
import { Badge, CollaboratorDisplay } from '@/components/ui';
|
import { Badge, CollaboratorDisplay } from '@/components/ui';
|
||||||
@@ -20,7 +21,10 @@ export default async function MotivatorSessionPage({ params }: MotivatorSessionP
|
|||||||
return null;
|
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) {
|
if (!session) {
|
||||||
notFound();
|
notFound();
|
||||||
@@ -81,6 +85,7 @@ export default async function MotivatorSessionPage({ params }: MotivatorSessionP
|
|||||||
shares={session.shares}
|
shares={session.shares}
|
||||||
isOwner={session.isOwner}
|
isOwner={session.isOwner}
|
||||||
canEdit={session.canEdit}
|
canEdit={session.canEdit}
|
||||||
|
userTeams={userTeams}
|
||||||
>
|
>
|
||||||
<MotivatorBoard sessionId={session.id} cards={session.cards} canEdit={session.canEdit} />
|
<MotivatorBoard sessionId={session.id} cards={session.cards} canEdit={session.canEdit} />
|
||||||
</MotivatorLiveWrapper>
|
</MotivatorLiveWrapper>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Link from 'next/link';
|
|||||||
import { auth } from '@/lib/auth';
|
import { auth } from '@/lib/auth';
|
||||||
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
|
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
|
||||||
import { getSessionById } from '@/services/sessions';
|
import { getSessionById } from '@/services/sessions';
|
||||||
|
import { getUserTeams } from '@/services/teams';
|
||||||
import { SwotBoard } from '@/components/swot/SwotBoard';
|
import { SwotBoard } from '@/components/swot/SwotBoard';
|
||||||
import { SessionLiveWrapper } from '@/components/collaboration';
|
import { SessionLiveWrapper } from '@/components/collaboration';
|
||||||
import { EditableSessionTitle } from '@/components/ui';
|
import { EditableSessionTitle } from '@/components/ui';
|
||||||
@@ -20,7 +21,10 @@ export default async function SessionPage({ params }: SessionPageProps) {
|
|||||||
return null;
|
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) {
|
if (!session) {
|
||||||
notFound();
|
notFound();
|
||||||
@@ -80,6 +84,7 @@ export default async function SessionPage({ params }: SessionPageProps) {
|
|||||||
shares={session.shares}
|
shares={session.shares}
|
||||||
isOwner={session.isOwner}
|
isOwner={session.isOwner}
|
||||||
canEdit={session.canEdit}
|
canEdit={session.canEdit}
|
||||||
|
userTeams={userTeams}
|
||||||
>
|
>
|
||||||
<SwotBoard sessionId={session.id} items={session.items} actions={session.actions} />
|
<SwotBoard sessionId={session.id} items={session.items} actions={session.actions} />
|
||||||
</SessionLiveWrapper>
|
</SessionLiveWrapper>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Link from 'next/link';
|
|||||||
import { auth } from '@/lib/auth';
|
import { auth } from '@/lib/auth';
|
||||||
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
|
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
|
||||||
import { getWeeklyCheckInSessionById } from '@/services/weekly-checkin';
|
import { getWeeklyCheckInSessionById } from '@/services/weekly-checkin';
|
||||||
|
import { getUserTeams } from '@/services/teams';
|
||||||
import type { ResolvedCollaborator } from '@/services/auth';
|
import type { ResolvedCollaborator } from '@/services/auth';
|
||||||
import { getUserOKRsForPeriod } from '@/services/okrs';
|
import { getUserOKRsForPeriod } from '@/services/okrs';
|
||||||
import { getCurrentQuarterPeriod } from '@/lib/okr-utils';
|
import { getCurrentQuarterPeriod } from '@/lib/okr-utils';
|
||||||
@@ -23,7 +24,10 @@ export default async function WeeklyCheckInSessionPage({ params }: WeeklyCheckIn
|
|||||||
return null;
|
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) {
|
if (!session) {
|
||||||
notFound();
|
notFound();
|
||||||
@@ -96,6 +100,7 @@ export default async function WeeklyCheckInSessionPage({ params }: WeeklyCheckIn
|
|||||||
shares={session.shares}
|
shares={session.shares}
|
||||||
isOwner={session.isOwner}
|
isOwner={session.isOwner}
|
||||||
canEdit={session.canEdit}
|
canEdit={session.canEdit}
|
||||||
|
userTeams={userTeams}
|
||||||
>
|
>
|
||||||
<WeeklyCheckInBoard sessionId={session.id} items={session.items} />
|
<WeeklyCheckInBoard sessionId={session.id} items={session.items} />
|
||||||
</WeeklyCheckInLiveWrapper>
|
</WeeklyCheckInLiveWrapper>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Link from 'next/link';
|
|||||||
import { auth } from '@/lib/auth';
|
import { auth } from '@/lib/auth';
|
||||||
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
|
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
|
||||||
import { getYearReviewSessionById } from '@/services/year-review';
|
import { getYearReviewSessionById } from '@/services/year-review';
|
||||||
|
import { getUserTeams } from '@/services/teams';
|
||||||
import type { ResolvedCollaborator } from '@/services/auth';
|
import type { ResolvedCollaborator } from '@/services/auth';
|
||||||
import { YearReviewBoard, YearReviewLiveWrapper } from '@/components/year-review';
|
import { YearReviewBoard, YearReviewLiveWrapper } from '@/components/year-review';
|
||||||
import { Badge, CollaboratorDisplay } from '@/components/ui';
|
import { Badge, CollaboratorDisplay } from '@/components/ui';
|
||||||
@@ -20,7 +21,10 @@ export default async function YearReviewSessionPage({ params }: YearReviewSessio
|
|||||||
return null;
|
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) {
|
if (!session) {
|
||||||
notFound();
|
notFound();
|
||||||
@@ -80,6 +84,7 @@ export default async function YearReviewSessionPage({ params }: YearReviewSessio
|
|||||||
shares={session.shares}
|
shares={session.shares}
|
||||||
isOwner={session.isOwner}
|
isOwner={session.isOwner}
|
||||||
canEdit={session.canEdit}
|
canEdit={session.canEdit}
|
||||||
|
userTeams={userTeams}
|
||||||
>
|
>
|
||||||
<YearReviewBoard sessionId={session.id} items={session.items} />
|
<YearReviewBoard sessionId={session.id} items={session.items} />
|
||||||
</YearReviewLiveWrapper>
|
</YearReviewLiveWrapper>
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import { useState, useCallback } from 'react';
|
|||||||
import { useSessionLive, type LiveEvent } from '@/hooks/useSessionLive';
|
import { useSessionLive, type LiveEvent } from '@/hooks/useSessionLive';
|
||||||
import { LiveIndicator } from './LiveIndicator';
|
import { LiveIndicator } from './LiveIndicator';
|
||||||
import { ShareModal } from './ShareModal';
|
import { ShareModal } from './ShareModal';
|
||||||
|
import { shareSessionAction, removeShareAction } from '@/actions/share';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Avatar } from '@/components/ui/Avatar';
|
import { Avatar } from '@/components/ui/Avatar';
|
||||||
import type { ShareRole } from '@prisma/client';
|
import type { ShareRole } from '@prisma/client';
|
||||||
|
import type { TeamWithMembers } from '@/lib/share-utils';
|
||||||
|
|
||||||
interface ShareUser {
|
interface ShareUser {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -28,6 +30,7 @@ interface SessionLiveWrapperProps {
|
|||||||
shares: Share[];
|
shares: Share[];
|
||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
|
userTeams?: TeamWithMembers[];
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +41,7 @@ export function SessionLiveWrapper({
|
|||||||
shares,
|
shares,
|
||||||
isOwner,
|
isOwner,
|
||||||
canEdit,
|
canEdit,
|
||||||
|
userTeams = [],
|
||||||
children,
|
children,
|
||||||
}: SessionLiveWrapperProps) {
|
}: SessionLiveWrapperProps) {
|
||||||
const [shareModalOpen, setShareModalOpen] = useState(false);
|
const [shareModalOpen, setShareModalOpen] = useState(false);
|
||||||
@@ -122,10 +126,22 @@ export function SessionLiveWrapper({
|
|||||||
<ShareModal
|
<ShareModal
|
||||||
isOpen={shareModalOpen}
|
isOpen={shareModalOpen}
|
||||||
onClose={() => setShareModalOpen(false)}
|
onClose={() => setShareModalOpen(false)}
|
||||||
sessionId={sessionId}
|
title="Partager la session"
|
||||||
|
sessionSubtitle="Session"
|
||||||
sessionTitle={sessionTitle}
|
sessionTitle={sessionTitle}
|
||||||
shares={shares}
|
shares={shares}
|
||||||
isOwner={isOwner}
|
isOwner={isOwner}
|
||||||
|
userTeams={userTeams}
|
||||||
|
currentUserId={currentUserId}
|
||||||
|
onShareWithEmail={(email, role) => shareSessionAction(sessionId, email, role)}
|
||||||
|
onRemoveShare={(userId) => removeShareAction(sessionId, userId)}
|
||||||
|
helpText={
|
||||||
|
<>
|
||||||
|
<strong>Éditeur</strong> : peut modifier les items et actions
|
||||||
|
<br />
|
||||||
|
<strong>Lecteur</strong> : peut uniquement consulter
|
||||||
|
</>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useTransition } from 'react';
|
import { useState, useTransition } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
import { Modal } from '@/components/ui/Modal';
|
import { Modal } from '@/components/ui/Modal';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
import { Avatar } from '@/components/ui/Avatar';
|
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';
|
import type { ShareRole } from '@prisma/client';
|
||||||
|
|
||||||
interface ShareUser {
|
interface ShareUser {
|
||||||
@@ -19,39 +20,79 @@ interface Share {
|
|||||||
id: string;
|
id: string;
|
||||||
role: ShareRole;
|
role: ShareRole;
|
||||||
user: ShareUser;
|
user: ShareUser;
|
||||||
createdAt: Date;
|
createdAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ShareTab = 'teamMember' | 'team' | 'email';
|
||||||
|
|
||||||
interface ShareModalProps {
|
interface ShareModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
sessionId: string;
|
title: string;
|
||||||
|
sessionSubtitle?: string;
|
||||||
sessionTitle: string;
|
sessionTitle: string;
|
||||||
shares: Share[];
|
shares: Share[];
|
||||||
isOwner: boolean;
|
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<unknown>;
|
||||||
|
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({
|
export function ShareModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
sessionId,
|
title,
|
||||||
|
sessionSubtitle,
|
||||||
sessionTitle,
|
sessionTitle,
|
||||||
shares,
|
shares,
|
||||||
isOwner,
|
isOwner,
|
||||||
|
userTeams = [],
|
||||||
|
currentUserId = '',
|
||||||
|
onShareWithEmail,
|
||||||
|
onShareWithTeam,
|
||||||
|
onRemoveShare,
|
||||||
|
helpText,
|
||||||
}: ShareModalProps) {
|
}: ShareModalProps) {
|
||||||
|
const teamMembers = getTeamMembersForShare(userTeams, currentUserId);
|
||||||
|
const hasTeamShare = !!onShareWithTeam;
|
||||||
|
const [shareType, setShareType] = useState<ShareTab>('teamMember');
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
const [teamId, setTeamId] = useState('');
|
||||||
|
const [selectedMemberId, setSelectedMemberId] = useState('');
|
||||||
const [role, setRole] = useState<ShareRole>('EDITOR');
|
const [role, setRole] = useState<ShareRole>('EDITOR');
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setEmail('');
|
||||||
|
setTeamId('');
|
||||||
|
setSelectedMemberId('');
|
||||||
|
};
|
||||||
|
|
||||||
async function handleShare(e: React.FormEvent) {
|
async function handleShare(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
startTransition(async () => {
|
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) {
|
if (result.success) {
|
||||||
setEmail('');
|
resetForm();
|
||||||
} else {
|
} else {
|
||||||
setError(result.error || 'Erreur lors du partage');
|
setError(result.error || 'Erreur lors du partage');
|
||||||
}
|
}
|
||||||
@@ -60,22 +101,47 @@ export function ShareModal({
|
|||||||
|
|
||||||
async function handleRemove(userId: string) {
|
async function handleRemove(userId: string) {
|
||||||
startTransition(async () => {
|
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 (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} title="Partager la session">
|
<Modal isOpen={isOpen} onClose={onClose} title={title}>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Session info */}
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted">Session</p>
|
{sessionSubtitle && <p className="text-sm text-muted">{sessionSubtitle}</p>}
|
||||||
<p className="font-medium text-foreground">{sessionTitle}</p>
|
<p className="font-medium text-foreground">{sessionTitle}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Share form (only for owner) */}
|
|
||||||
{isOwner && (
|
{isOwner && (
|
||||||
<form onSubmit={handleShare} className="space-y-4">
|
<form onSubmit={handleShare} className="space-y-4">
|
||||||
|
<div className="flex gap-2 border-b border-border pb-3 flex-wrap">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setShareType(tab.value);
|
||||||
|
resetForm();
|
||||||
|
}}
|
||||||
|
className={`flex-1 min-w-0 rounded-lg px-4 py-2 text-sm font-medium transition-colors ${
|
||||||
|
shareType === tab.value
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'bg-card-hover text-muted hover:text-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.icon} {tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{shareType === 'email' && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
@@ -85,28 +151,122 @@ export function ShareModal({
|
|||||||
className="flex-1"
|
className="flex-1"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<select
|
<select value={role} onChange={(e) => setRole(e.target.value as ShareRole)} className={SELECT_STYLE}>
|
||||||
value={role}
|
|
||||||
onChange={(e) => setRole(e.target.value as ShareRole)}
|
|
||||||
className="rounded-lg border border-border bg-input px-3 py-2 text-sm text-foreground"
|
|
||||||
>
|
|
||||||
<option value="EDITOR">Éditeur</option>
|
<option value="EDITOR">Éditeur</option>
|
||||||
<option value="VIEWER">Lecteur</option>
|
<option value="VIEWER">Lecteur</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{shareType === 'teamMember' && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{teamMembers.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted">
|
||||||
|
Vous n'êtes membre d'aucune équipe ou vos équipes n'ont pas d'autres membres.
|
||||||
|
Créez une équipe depuis la page{' '}
|
||||||
|
<Link href="/teams" className="text-primary hover:underline">
|
||||||
|
Équipes
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<select
|
||||||
|
value={selectedMemberId}
|
||||||
|
onChange={(e) => setSelectedMemberId(e.target.value)}
|
||||||
|
className={`w-full ${SELECT_STYLE}`}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Sélectionner un membre</option>
|
||||||
|
{teamMembers.map((m) => (
|
||||||
|
<option key={m.id} value={m.id}>
|
||||||
|
{m.name || m.email} {m.name && `(${m.email})`}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2">
|
||||||
|
<svg className="h-4 w-4 text-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<select value={role} onChange={(e) => setRole(e.target.value as ShareRole)} className={SELECT_STYLE}>
|
||||||
|
<option value="EDITOR">Éditeur</option>
|
||||||
|
<option value="VIEWER">Lecteur</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{shareType === 'team' && hasTeamShare && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{userTeams.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted">
|
||||||
|
Vous n'êtes membre d'aucune équipe. Créez une équipe depuis la page{' '}
|
||||||
|
<Link href="/teams" className="text-primary hover:underline">
|
||||||
|
Équipes
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="relative">
|
||||||
|
<select
|
||||||
|
value={teamId}
|
||||||
|
onChange={(e) => setTeamId(e.target.value)}
|
||||||
|
className={`w-full ${SELECT_STYLE}`}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Sélectionner une équipe</option>
|
||||||
|
{userTeams.map((team) => (
|
||||||
|
<option key={team.id} value={team.id}>
|
||||||
|
{team.name} {team.userRole === 'ADMIN' && '(Admin)'}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2">
|
||||||
|
<svg className="h-4 w-4 text-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<select value={role} onChange={(e) => setRole(e.target.value as ShareRole)} className={`w-full ${SELECT_STYLE}`}>
|
||||||
|
<option value="EDITOR">Éditeur</option>
|
||||||
|
<option value="VIEWER">Lecteur</option>
|
||||||
|
</select>
|
||||||
|
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2">
|
||||||
|
<svg className="h-4 w-4 text-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||||
|
|
||||||
<Button type="submit" disabled={isPending || !email} className="w-full">
|
<Button
|
||||||
{isPending ? 'Partage...' : 'Partager'}
|
type="submit"
|
||||||
|
disabled={
|
||||||
|
isPending ||
|
||||||
|
(shareType === 'email' && !email) ||
|
||||||
|
(shareType === 'teamMember' && !selectedMemberId) ||
|
||||||
|
(shareType === 'team' && !teamId)
|
||||||
|
}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{isPending ? 'Partage...' : shareType === 'team' ? "Partager à l'équipe" : 'Partager'}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Current shares */}
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-medium text-foreground">Collaborateurs ({shares.length})</p>
|
<p className="text-sm font-medium text-foreground">Collaborateurs ({shares.length})</p>
|
||||||
|
|
||||||
{shares.length === 0 ? (
|
{shares.length === 0 ? (
|
||||||
<p className="text-sm text-muted">Aucun collaborateur pour le moment</p>
|
<p className="text-sm text-muted">Aucun collaborateur pour le moment</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -125,7 +285,6 @@ export function ShareModal({
|
|||||||
{share.user.name && <p className="text-xs text-muted">{share.user.email}</p>}
|
{share.user.name && <p className="text-xs text-muted">{share.user.email}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge variant={share.role === 'EDITOR' ? 'primary' : 'default'}>
|
<Badge variant={share.role === 'EDITOR' ? 'primary' : 'default'}>
|
||||||
{share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'}
|
{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"
|
className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive"
|
||||||
title="Retirer l'accès"
|
title="Retirer l'accès"
|
||||||
>
|
>
|
||||||
<svg
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
className="h-4 w-4"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z"
|
d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z"
|
||||||
@@ -158,14 +312,7 @@ export function ShareModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Help text */}
|
{helpText && <div className="rounded-lg bg-primary/5 p-3">{helpText}</div>}
|
||||||
<div className="rounded-lg bg-primary/5 p-3">
|
|
||||||
<p className="text-xs text-muted">
|
|
||||||
<strong>Éditeur</strong> : peut modifier les items et actions
|
|
||||||
<br />
|
|
||||||
<strong>Lecteur</strong> : peut uniquement consulter
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { useMotivatorLive, type MotivatorLiveEvent } from '@/hooks/useMotivatorLive';
|
import { useMotivatorLive, type MotivatorLiveEvent } from '@/hooks/useMotivatorLive';
|
||||||
import { LiveIndicator } from '@/components/collaboration/LiveIndicator';
|
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 { Button } from '@/components/ui/Button';
|
||||||
import { Avatar } from '@/components/ui/Avatar';
|
import { Avatar } from '@/components/ui/Avatar';
|
||||||
import type { ShareRole } from '@prisma/client';
|
import type { ShareRole } from '@prisma/client';
|
||||||
@@ -28,6 +30,7 @@ interface MotivatorLiveWrapperProps {
|
|||||||
shares: Share[];
|
shares: Share[];
|
||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
|
userTeams?: TeamWithMembers[];
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +41,7 @@ export function MotivatorLiveWrapper({
|
|||||||
shares,
|
shares,
|
||||||
isOwner,
|
isOwner,
|
||||||
canEdit,
|
canEdit,
|
||||||
|
userTeams = [],
|
||||||
children,
|
children,
|
||||||
}: MotivatorLiveWrapperProps) {
|
}: MotivatorLiveWrapperProps) {
|
||||||
const [shareModalOpen, setShareModalOpen] = useState(false);
|
const [shareModalOpen, setShareModalOpen] = useState(false);
|
||||||
@@ -119,13 +123,25 @@ export function MotivatorLiveWrapper({
|
|||||||
<div className={!canEdit ? 'pointer-events-none opacity-90' : ''}>{children}</div>
|
<div className={!canEdit ? 'pointer-events-none opacity-90' : ''}>{children}</div>
|
||||||
|
|
||||||
{/* Share Modal */}
|
{/* Share Modal */}
|
||||||
<MotivatorShareModal
|
<ShareModal
|
||||||
isOpen={shareModalOpen}
|
isOpen={shareModalOpen}
|
||||||
onClose={() => setShareModalOpen(false)}
|
onClose={() => setShareModalOpen(false)}
|
||||||
sessionId={sessionId}
|
title="Partager la session"
|
||||||
|
sessionSubtitle="Session Moving Motivators"
|
||||||
sessionTitle={sessionTitle}
|
sessionTitle={sessionTitle}
|
||||||
shares={shares}
|
shares={shares}
|
||||||
isOwner={isOwner}
|
isOwner={isOwner}
|
||||||
|
userTeams={userTeams}
|
||||||
|
currentUserId={currentUserId}
|
||||||
|
onShareWithEmail={(email, role) => shareMotivatorSession(sessionId, email, role)}
|
||||||
|
onRemoveShare={(userId) => removeMotivatorShare(sessionId, userId)}
|
||||||
|
helpText={
|
||||||
|
<>
|
||||||
|
<strong>Éditeur</strong> : peut modifier les cartes et leurs positions
|
||||||
|
<br />
|
||||||
|
<strong>Lecteur</strong> : peut uniquement consulter
|
||||||
|
</>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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<ShareRole>('EDITOR');
|
|
||||||
const [error, setError] = useState<string | null>(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 (
|
|
||||||
<Modal isOpen={isOpen} onClose={onClose} title="Partager la session">
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Session info */}
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-muted">Session Moving Motivators</p>
|
|
||||||
<p className="font-medium text-foreground">{sessionTitle}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Share form (only for owner) */}
|
|
||||||
{isOwner && (
|
|
||||||
<form onSubmit={handleShare} className="space-y-4">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
type="email"
|
|
||||||
placeholder="Email de l'utilisateur"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
className="flex-1"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
value={role}
|
|
||||||
onChange={(e) => setRole(e.target.value as ShareRole)}
|
|
||||||
className="rounded-lg border border-border bg-input px-3 py-2 text-sm text-foreground"
|
|
||||||
>
|
|
||||||
<option value="EDITOR">Éditeur</option>
|
|
||||||
<option value="VIEWER">Lecteur</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
||||||
|
|
||||||
<Button type="submit" disabled={isPending || !email} className="w-full">
|
|
||||||
{isPending ? 'Partage...' : 'Partager'}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Current shares */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-sm font-medium text-foreground">Collaborateurs ({shares.length})</p>
|
|
||||||
|
|
||||||
{shares.length === 0 ? (
|
|
||||||
<p className="text-sm text-muted">Aucun collaborateur pour le moment</p>
|
|
||||||
) : (
|
|
||||||
<ul className="space-y-2">
|
|
||||||
{shares.map((share) => (
|
|
||||||
<li
|
|
||||||
key={share.id}
|
|
||||||
className="flex items-center justify-between rounded-lg border border-border bg-card p-3"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Avatar email={share.user.email} name={share.user.name} size={32} />
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-foreground">
|
|
||||||
{share.user.name || share.user.email}
|
|
||||||
</p>
|
|
||||||
{share.user.name && <p className="text-xs text-muted">{share.user.email}</p>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge variant={share.role === 'EDITOR' ? 'primary' : 'default'}>
|
|
||||||
{share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'}
|
|
||||||
</Badge>
|
|
||||||
{isOwner && (
|
|
||||||
<button
|
|
||||||
onClick={() => handleRemove(share.user.id)}
|
|
||||||
disabled={isPending}
|
|
||||||
className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive"
|
|
||||||
title="Retirer l'accès"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
className="h-4 w-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Help text */}
|
|
||||||
<div className="rounded-lg bg-primary/5 p-3">
|
|
||||||
<p className="text-xs text-muted">
|
|
||||||
<strong>Éditeur</strong> : peut modifier les cartes et leurs positions
|
|
||||||
<br />
|
|
||||||
<strong>Lecteur</strong> : peut uniquement consulter
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -3,4 +3,3 @@ export { MotivatorCard, MotivatorCardStatic } from './MotivatorCard';
|
|||||||
export { MotivatorSummary } from './MotivatorSummary';
|
export { MotivatorSummary } from './MotivatorSummary';
|
||||||
export { InfluenceZone } from './InfluenceZone';
|
export { InfluenceZone } from './InfluenceZone';
|
||||||
export { MotivatorLiveWrapper } from './MotivatorLiveWrapper';
|
export { MotivatorLiveWrapper } from './MotivatorLiveWrapper';
|
||||||
export { MotivatorShareModal } from './MotivatorShareModal';
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { useWeatherLive, type WeatherLiveEvent } from '@/hooks/useWeatherLive';
|
import { useWeatherLive, type WeatherLiveEvent } from '@/hooks/useWeatherLive';
|
||||||
import { LiveIndicator } from '@/components/collaboration/LiveIndicator';
|
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 { Button } from '@/components/ui/Button';
|
||||||
import { Avatar } from '@/components/ui/Avatar';
|
import { Avatar } from '@/components/ui/Avatar';
|
||||||
import type { ShareRole } from '@prisma/client';
|
import type { ShareRole } from '@prisma/client';
|
||||||
@@ -128,14 +129,26 @@ export function WeatherLiveWrapper({
|
|||||||
<div className={!canEdit ? 'pointer-events-none opacity-90' : ''}>{children}</div>
|
<div className={!canEdit ? 'pointer-events-none opacity-90' : ''}>{children}</div>
|
||||||
|
|
||||||
{/* Share Modal */}
|
{/* Share Modal */}
|
||||||
<WeatherShareModal
|
<ShareModal
|
||||||
isOpen={shareModalOpen}
|
isOpen={shareModalOpen}
|
||||||
onClose={() => setShareModalOpen(false)}
|
onClose={() => setShareModalOpen(false)}
|
||||||
sessionId={sessionId}
|
title="Partager la météo"
|
||||||
|
sessionSubtitle="Météo personnelle"
|
||||||
sessionTitle={sessionTitle}
|
sessionTitle={sessionTitle}
|
||||||
shares={shares}
|
shares={shares}
|
||||||
isOwner={isOwner}
|
isOwner={isOwner}
|
||||||
userTeams={userTeams}
|
userTeams={userTeams}
|
||||||
|
currentUserId={currentUserId}
|
||||||
|
onShareWithEmail={(email, role) => shareWeatherSession(sessionId, email, role)}
|
||||||
|
onShareWithTeam={(teamId, role) => shareWeatherSessionToTeam(sessionId, teamId, role)}
|
||||||
|
onRemoveShare={(userId) => removeWeatherShare(sessionId, userId)}
|
||||||
|
helpText={
|
||||||
|
<>
|
||||||
|
<strong>Éditeur</strong> : peut modifier sa météo et voir celle des autres
|
||||||
|
<br />
|
||||||
|
<strong>Lecteur</strong> : peut uniquement consulter
|
||||||
|
</>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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<ShareRole>('EDITOR');
|
|
||||||
const [error, setError] = useState<string | null>(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 (
|
|
||||||
<Modal isOpen={isOpen} onClose={onClose} title="Partager la météo">
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Session info */}
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-muted">Météo personnelle</p>
|
|
||||||
<p className="font-medium text-foreground">{sessionTitle}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Share form (only for owner) */}
|
|
||||||
{isOwner && (
|
|
||||||
<form onSubmit={handleShare} className="space-y-4">
|
|
||||||
{/* Share type selector */}
|
|
||||||
<div className="flex gap-2 border-b border-border pb-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setShareType('user');
|
|
||||||
setEmail('');
|
|
||||||
setTeamId('');
|
|
||||||
}}
|
|
||||||
className={`flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-colors ${
|
|
||||||
shareType === 'user'
|
|
||||||
? 'bg-primary text-primary-foreground'
|
|
||||||
: 'bg-card-hover text-muted hover:text-foreground'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
👤 Utilisateur
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setShareType('team');
|
|
||||||
setEmail('');
|
|
||||||
setTeamId('');
|
|
||||||
}}
|
|
||||||
className={`flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-colors ${
|
|
||||||
shareType === 'team'
|
|
||||||
? 'bg-primary text-primary-foreground'
|
|
||||||
: 'bg-card-hover text-muted hover:text-foreground'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
👥 Équipe
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* User share */}
|
|
||||||
{shareType === 'user' && (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
type="email"
|
|
||||||
placeholder="Email de l'utilisateur"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
className="flex-1"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<div className="relative">
|
|
||||||
<select
|
|
||||||
value={role}
|
|
||||||
onChange={(e) => setRole(e.target.value as ShareRole)}
|
|
||||||
className="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"
|
|
||||||
>
|
|
||||||
<option value="EDITOR">Éditeur</option>
|
|
||||||
<option value="VIEWER">Lecteur</option>
|
|
||||||
</select>
|
|
||||||
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2">
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4 text-muted"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Team share */}
|
|
||||||
{shareType === 'team' && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{userTeams.length === 0 ? (
|
|
||||||
<p className="text-sm text-muted">
|
|
||||||
Vous n'êtes membre d'aucune équipe. Créez une équipe depuis la page{' '}
|
|
||||||
<Link href="/teams" className="text-primary hover:underline">
|
|
||||||
Équipes
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="relative">
|
|
||||||
<select
|
|
||||||
value={teamId}
|
|
||||||
onChange={(e) => setTeamId(e.target.value)}
|
|
||||||
className="w-full 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"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<option value="">Sélectionner une équipe</option>
|
|
||||||
{userTeams.map((team) => (
|
|
||||||
<option key={team.id} value={team.id}>
|
|
||||||
{team.name} {team.userRole === 'ADMIN' && '(Admin)'}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2">
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4 text-muted"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative">
|
|
||||||
<select
|
|
||||||
value={role}
|
|
||||||
onChange={(e) => setRole(e.target.value as ShareRole)}
|
|
||||||
className="w-full 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"
|
|
||||||
>
|
|
||||||
<option value="EDITOR">Éditeur</option>
|
|
||||||
<option value="VIEWER">Lecteur</option>
|
|
||||||
</select>
|
|
||||||
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2">
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4 text-muted"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={isPending || (shareType === 'user' && !email) || (shareType === 'team' && !teamId)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{isPending ? 'Partage...' : shareType === 'team' ? "Partager à l'équipe" : 'Partager'}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Current shares */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-sm font-medium text-foreground">Collaborateurs ({shares.length})</p>
|
|
||||||
|
|
||||||
{shares.length === 0 ? (
|
|
||||||
<p className="text-sm text-muted">Aucun collaborateur pour le moment</p>
|
|
||||||
) : (
|
|
||||||
<ul className="space-y-2">
|
|
||||||
{shares.map((share) => (
|
|
||||||
<li
|
|
||||||
key={share.id}
|
|
||||||
className="flex items-center justify-between rounded-lg border border-border bg-card p-3"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Avatar email={share.user.email} name={share.user.name} size={32} />
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-foreground">
|
|
||||||
{share.user.name || share.user.email}
|
|
||||||
</p>
|
|
||||||
{share.user.name && <p className="text-xs text-muted">{share.user.email}</p>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge variant={share.role === 'EDITOR' ? 'primary' : 'default'}>
|
|
||||||
{share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'}
|
|
||||||
</Badge>
|
|
||||||
{isOwner && (
|
|
||||||
<button
|
|
||||||
onClick={() => handleRemove(share.user.id)}
|
|
||||||
disabled={isPending}
|
|
||||||
className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive"
|
|
||||||
title="Retirer l'accès"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
className="h-4 w-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Help text */}
|
|
||||||
<div className="rounded-lg bg-primary/5 p-3">
|
|
||||||
<p className="text-xs text-muted">
|
|
||||||
<strong>Éditeur</strong> : peut modifier sa météo et voir celle des autres
|
|
||||||
<br />
|
|
||||||
<strong>Lecteur</strong> : peut uniquement consulter
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
export { WeatherBoard } from './WeatherBoard';
|
export { WeatherBoard } from './WeatherBoard';
|
||||||
export { WeatherCard } from './WeatherCard';
|
export { WeatherCard } from './WeatherCard';
|
||||||
export { WeatherLiveWrapper } from './WeatherLiveWrapper';
|
export { WeatherLiveWrapper } from './WeatherLiveWrapper';
|
||||||
export { WeatherShareModal } from './WeatherShareModal';
|
|
||||||
export { WeatherInfoPanel } from './WeatherInfoPanel';
|
export { WeatherInfoPanel } from './WeatherInfoPanel';
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { useWeeklyCheckInLive, type WeeklyCheckInLiveEvent } from '@/hooks/useWeeklyCheckInLive';
|
import { useWeeklyCheckInLive, type WeeklyCheckInLiveEvent } from '@/hooks/useWeeklyCheckInLive';
|
||||||
import { LiveIndicator } from '@/components/collaboration/LiveIndicator';
|
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 { Button } from '@/components/ui/Button';
|
||||||
import { Avatar } from '@/components/ui/Avatar';
|
import { Avatar } from '@/components/ui/Avatar';
|
||||||
import type { ShareRole } from '@prisma/client';
|
import type { ShareRole } from '@prisma/client';
|
||||||
@@ -28,6 +30,7 @@ interface WeeklyCheckInLiveWrapperProps {
|
|||||||
shares: Share[];
|
shares: Share[];
|
||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
|
userTeams?: TeamWithMembers[];
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +41,7 @@ export function WeeklyCheckInLiveWrapper({
|
|||||||
shares,
|
shares,
|
||||||
isOwner,
|
isOwner,
|
||||||
canEdit,
|
canEdit,
|
||||||
|
userTeams = [],
|
||||||
children,
|
children,
|
||||||
}: WeeklyCheckInLiveWrapperProps) {
|
}: WeeklyCheckInLiveWrapperProps) {
|
||||||
const [shareModalOpen, setShareModalOpen] = useState(false);
|
const [shareModalOpen, setShareModalOpen] = useState(false);
|
||||||
@@ -119,13 +123,25 @@ export function WeeklyCheckInLiveWrapper({
|
|||||||
<div className={!canEdit ? 'pointer-events-none opacity-90' : ''}>{children}</div>
|
<div className={!canEdit ? 'pointer-events-none opacity-90' : ''}>{children}</div>
|
||||||
|
|
||||||
{/* Share Modal */}
|
{/* Share Modal */}
|
||||||
<WeeklyCheckInShareModal
|
<ShareModal
|
||||||
isOpen={shareModalOpen}
|
isOpen={shareModalOpen}
|
||||||
onClose={() => setShareModalOpen(false)}
|
onClose={() => setShareModalOpen(false)}
|
||||||
sessionId={sessionId}
|
title="Options de partage"
|
||||||
|
sessionSubtitle="Check-in hebdomadaire"
|
||||||
sessionTitle={sessionTitle}
|
sessionTitle={sessionTitle}
|
||||||
shares={shares}
|
shares={shares}
|
||||||
isOwner={isOwner}
|
isOwner={isOwner}
|
||||||
|
userTeams={userTeams}
|
||||||
|
currentUserId={currentUserId}
|
||||||
|
onShareWithEmail={(email, role) => shareWeeklyCheckInSession(sessionId, email, role)}
|
||||||
|
onRemoveShare={(userId) => removeWeeklyCheckInShare(sessionId, userId)}
|
||||||
|
helpText={
|
||||||
|
<>
|
||||||
|
<strong>Éditeur</strong> : peut modifier les items et leurs catégories
|
||||||
|
<br />
|
||||||
|
<strong>Lecteur</strong> : peut uniquement consulter
|
||||||
|
</>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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<ShareRole>('EDITOR');
|
|
||||||
const [error, setError] = useState<string | null>(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 (
|
|
||||||
<Modal isOpen={isOpen} onClose={onClose} title="Options de partage">
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Session info */}
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-muted">Check-in hebdomadaire</p>
|
|
||||||
<p className="font-medium text-foreground">{sessionTitle}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Share form (only for owner) */}
|
|
||||||
{isOwner && (
|
|
||||||
<form onSubmit={handleShare} className="space-y-4">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
type="email"
|
|
||||||
placeholder="Email de l'utilisateur"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
className="flex-1"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
value={role}
|
|
||||||
onChange={(e) => setRole(e.target.value as ShareRole)}
|
|
||||||
className="rounded-lg border border-border bg-input px-3 py-2 text-sm text-foreground"
|
|
||||||
>
|
|
||||||
<option value="EDITOR">Éditeur</option>
|
|
||||||
<option value="VIEWER">Lecteur</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
||||||
|
|
||||||
<Button type="submit" disabled={isPending || !email} className="w-full">
|
|
||||||
{isPending ? 'Partage...' : 'Partager'}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Current shares */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-sm font-medium text-foreground">Collaborateurs ({shares.length})</p>
|
|
||||||
|
|
||||||
{shares.length === 0 ? (
|
|
||||||
<p className="text-sm text-muted">Aucun collaborateur pour le moment</p>
|
|
||||||
) : (
|
|
||||||
<ul className="space-y-2">
|
|
||||||
{shares.map((share) => (
|
|
||||||
<li
|
|
||||||
key={share.id}
|
|
||||||
className="flex items-center justify-between rounded-lg border border-border bg-card p-3"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Avatar email={share.user.email} name={share.user.name} size={32} />
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-foreground">
|
|
||||||
{share.user.name || share.user.email}
|
|
||||||
</p>
|
|
||||||
{share.user.name && <p className="text-xs text-muted">{share.user.email}</p>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge variant={share.role === 'EDITOR' ? 'primary' : 'default'}>
|
|
||||||
{share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'}
|
|
||||||
</Badge>
|
|
||||||
{isOwner && (
|
|
||||||
<button
|
|
||||||
onClick={() => handleRemove(share.user.id)}
|
|
||||||
disabled={isPending}
|
|
||||||
className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive"
|
|
||||||
title="Retirer l'accès"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
className="h-4 w-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Help text */}
|
|
||||||
<div className="rounded-lg bg-primary/5 p-3">
|
|
||||||
<p className="text-xs text-muted">
|
|
||||||
<strong>Éditeur</strong> : peut modifier les items et leurs catégories
|
|
||||||
<br />
|
|
||||||
<strong>Lecteur</strong> : peut uniquement consulter
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,5 +2,4 @@ export { WeeklyCheckInBoard } from './WeeklyCheckInBoard';
|
|||||||
export { WeeklyCheckInCard } from './WeeklyCheckInCard';
|
export { WeeklyCheckInCard } from './WeeklyCheckInCard';
|
||||||
export { WeeklyCheckInSection } from './WeeklyCheckInSection';
|
export { WeeklyCheckInSection } from './WeeklyCheckInSection';
|
||||||
export { WeeklyCheckInLiveWrapper } from './WeeklyCheckInLiveWrapper';
|
export { WeeklyCheckInLiveWrapper } from './WeeklyCheckInLiveWrapper';
|
||||||
export { WeeklyCheckInShareModal } from './WeeklyCheckInShareModal';
|
|
||||||
export { CurrentQuarterOKRs } from './CurrentQuarterOKRs';
|
export { CurrentQuarterOKRs } from './CurrentQuarterOKRs';
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { useYearReviewLive, type YearReviewLiveEvent } from '@/hooks/useYearReviewLive';
|
import { useYearReviewLive, type YearReviewLiveEvent } from '@/hooks/useYearReviewLive';
|
||||||
import { LiveIndicator } from '@/components/collaboration/LiveIndicator';
|
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 { Button } from '@/components/ui/Button';
|
||||||
import { Avatar } from '@/components/ui/Avatar';
|
import { Avatar } from '@/components/ui/Avatar';
|
||||||
import type { ShareRole } from '@prisma/client';
|
import type { ShareRole } from '@prisma/client';
|
||||||
@@ -28,6 +30,7 @@ interface YearReviewLiveWrapperProps {
|
|||||||
shares: Share[];
|
shares: Share[];
|
||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
|
userTeams?: TeamWithMembers[];
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +41,7 @@ export function YearReviewLiveWrapper({
|
|||||||
shares,
|
shares,
|
||||||
isOwner,
|
isOwner,
|
||||||
canEdit,
|
canEdit,
|
||||||
|
userTeams = [],
|
||||||
children,
|
children,
|
||||||
}: YearReviewLiveWrapperProps) {
|
}: YearReviewLiveWrapperProps) {
|
||||||
const [shareModalOpen, setShareModalOpen] = useState(false);
|
const [shareModalOpen, setShareModalOpen] = useState(false);
|
||||||
@@ -119,13 +123,25 @@ export function YearReviewLiveWrapper({
|
|||||||
<div className={!canEdit ? 'pointer-events-none opacity-90' : ''}>{children}</div>
|
<div className={!canEdit ? 'pointer-events-none opacity-90' : ''}>{children}</div>
|
||||||
|
|
||||||
{/* Share Modal */}
|
{/* Share Modal */}
|
||||||
<YearReviewShareModal
|
<ShareModal
|
||||||
isOpen={shareModalOpen}
|
isOpen={shareModalOpen}
|
||||||
onClose={() => setShareModalOpen(false)}
|
onClose={() => setShareModalOpen(false)}
|
||||||
sessionId={sessionId}
|
title="Partager le bilan"
|
||||||
|
sessionSubtitle="Bilan annuel"
|
||||||
sessionTitle={sessionTitle}
|
sessionTitle={sessionTitle}
|
||||||
shares={shares}
|
shares={shares}
|
||||||
isOwner={isOwner}
|
isOwner={isOwner}
|
||||||
|
userTeams={userTeams}
|
||||||
|
currentUserId={currentUserId}
|
||||||
|
onShareWithEmail={(email, role) => shareYearReviewSession(sessionId, email, role)}
|
||||||
|
onRemoveShare={(userId) => removeYearReviewShare(sessionId, userId)}
|
||||||
|
helpText={
|
||||||
|
<>
|
||||||
|
<strong>Éditeur</strong> : peut modifier les items et leurs catégories
|
||||||
|
<br />
|
||||||
|
<strong>Lecteur</strong> : peut uniquement consulter
|
||||||
|
</>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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<ShareRole>('EDITOR');
|
|
||||||
const [error, setError] = useState<string | null>(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 (
|
|
||||||
<Modal isOpen={isOpen} onClose={onClose} title="Partager le bilan">
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Session info */}
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-muted">Bilan annuel</p>
|
|
||||||
<p className="font-medium text-foreground">{sessionTitle}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Share form (only for owner) */}
|
|
||||||
{isOwner && (
|
|
||||||
<form onSubmit={handleShare} className="space-y-4">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
type="email"
|
|
||||||
placeholder="Email de l'utilisateur"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
className="flex-1"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
value={role}
|
|
||||||
onChange={(e) => setRole(e.target.value as ShareRole)}
|
|
||||||
className="rounded-lg border border-border bg-input px-3 py-2 text-sm text-foreground"
|
|
||||||
>
|
|
||||||
<option value="EDITOR">Éditeur</option>
|
|
||||||
<option value="VIEWER">Lecteur</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
||||||
|
|
||||||
<Button type="submit" disabled={isPending || !email} className="w-full">
|
|
||||||
{isPending ? 'Partage...' : 'Partager'}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Current shares */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-sm font-medium text-foreground">Collaborateurs ({shares.length})</p>
|
|
||||||
|
|
||||||
{shares.length === 0 ? (
|
|
||||||
<p className="text-sm text-muted">Aucun collaborateur pour le moment</p>
|
|
||||||
) : (
|
|
||||||
<ul className="space-y-2">
|
|
||||||
{shares.map((share) => (
|
|
||||||
<li
|
|
||||||
key={share.id}
|
|
||||||
className="flex items-center justify-between rounded-lg border border-border bg-card p-3"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Avatar email={share.user.email} name={share.user.name} size={32} />
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-foreground">
|
|
||||||
{share.user.name || share.user.email}
|
|
||||||
</p>
|
|
||||||
{share.user.name && <p className="text-xs text-muted">{share.user.email}</p>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge variant={share.role === 'EDITOR' ? 'primary' : 'default'}>
|
|
||||||
{share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'}
|
|
||||||
</Badge>
|
|
||||||
{isOwner && (
|
|
||||||
<button
|
|
||||||
onClick={() => handleRemove(share.user.id)}
|
|
||||||
disabled={isPending}
|
|
||||||
className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive"
|
|
||||||
title="Retirer l'accès"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
className="h-4 w-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Help text */}
|
|
||||||
<div className="rounded-lg bg-primary/5 p-3">
|
|
||||||
<p className="text-xs text-muted">
|
|
||||||
<strong>Éditeur</strong> : peut modifier les items et leurs catégories
|
|
||||||
<br />
|
|
||||||
<strong>Lecteur</strong> : peut uniquement consulter
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -2,4 +2,3 @@ export { YearReviewBoard } from './YearReviewBoard';
|
|||||||
export { YearReviewCard } from './YearReviewCard';
|
export { YearReviewCard } from './YearReviewCard';
|
||||||
export { YearReviewSection } from './YearReviewSection';
|
export { YearReviewSection } from './YearReviewSection';
|
||||||
export { YearReviewLiveWrapper } from './YearReviewLiveWrapper';
|
export { YearReviewLiveWrapper } from './YearReviewLiveWrapper';
|
||||||
export { YearReviewShareModal } from './YearReviewShareModal';
|
|
||||||
|
|||||||
37
src/lib/share-utils.ts
Normal file
37
src/lib/share-utils.ts
Normal file
@@ -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<string>();
|
||||||
|
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;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user