feat: introduce Teams & OKRs feature with models, types, and UI components for team management and objective tracking
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 12m53s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 12m53s
This commit is contained in:
175
src/components/teams/MembersList.tsx
Normal file
175
src/components/teams/MembersList.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { getGravatarUrl } from '@/lib/gravatar';
|
||||
import { Badge } from '@/components/ui';
|
||||
import { Button } from '@/components/ui';
|
||||
import { AddMemberModal } from './AddMemberModal';
|
||||
import type { TeamMember, TeamRole } from '@/lib/types';
|
||||
import { TEAM_ROLE_LABELS } from '@/lib/types';
|
||||
|
||||
interface MembersListProps {
|
||||
members: TeamMember[];
|
||||
teamId: string;
|
||||
isAdmin: boolean;
|
||||
onMemberUpdate: () => void;
|
||||
}
|
||||
|
||||
export function MembersList({ members, teamId, isAdmin, onMemberUpdate }: MembersListProps) {
|
||||
const [addMemberOpen, setAddMemberOpen] = useState(false);
|
||||
const [updatingRole, setUpdatingRole] = useState<string | null>(null);
|
||||
const [removingMember, setRemovingMember] = useState<string | null>(null);
|
||||
|
||||
const handleRoleChange = async (userId: string, newRole: TeamRole) => {
|
||||
setUpdatingRole(userId);
|
||||
try {
|
||||
const response = await fetch(`/api/teams/${teamId}/members`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ userId, role: newRole }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
alert(error.error || 'Erreur lors de la mise à jour du rôle');
|
||||
return;
|
||||
}
|
||||
|
||||
onMemberUpdate();
|
||||
} catch (error) {
|
||||
console.error('Error updating role:', error);
|
||||
alert('Erreur lors de la mise à jour du rôle');
|
||||
} finally {
|
||||
setUpdatingRole(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveMember = async (userId: string) => {
|
||||
if (!confirm('Êtes-vous sûr de vouloir retirer ce membre de l\'équipe ?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
setRemovingMember(userId);
|
||||
try {
|
||||
const response = await fetch(`/api/teams/${teamId}/members?userId=${userId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
alert(error.error || 'Erreur lors de la suppression du membre');
|
||||
return;
|
||||
}
|
||||
|
||||
onMemberUpdate();
|
||||
} catch (error) {
|
||||
console.error('Error removing member:', error);
|
||||
alert('Erreur lors de la suppression du membre');
|
||||
} finally {
|
||||
setRemovingMember(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-foreground">Membres ({members.length})</h3>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
onClick={() => setAddMemberOpen(true)}
|
||||
className="bg-[var(--purple)] text-white hover:opacity-90 border-transparent"
|
||||
>
|
||||
Ajouter un membre
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{members.map((member) => (
|
||||
<div
|
||||
key={member.id}
|
||||
className="flex items-center gap-4 rounded-xl border border-border bg-card p-4"
|
||||
>
|
||||
{/* Avatar */}
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={getGravatarUrl(member.user.email, 96)}
|
||||
alt={member.user.name || member.user.email}
|
||||
width={48}
|
||||
height={48}
|
||||
className="rounded-full border-2 border-border"
|
||||
/>
|
||||
|
||||
{/* User Info */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-foreground truncate">
|
||||
{member.user.name || 'Sans nom'}
|
||||
</span>
|
||||
<Badge
|
||||
style={{
|
||||
backgroundColor:
|
||||
member.role === 'ADMIN'
|
||||
? 'color-mix(in srgb, var(--purple) 15%, transparent)'
|
||||
: 'color-mix(in srgb, var(--gray) 15%, transparent)',
|
||||
color: member.role === 'ADMIN' ? 'var(--purple)' : 'var(--gray)',
|
||||
}}
|
||||
>
|
||||
{TEAM_ROLE_LABELS[member.role]}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-muted truncate">{member.user.email}</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
{isAdmin && (
|
||||
<div className="flex items-center gap-2">
|
||||
{member.role === 'MEMBER' ? (
|
||||
<Button
|
||||
onClick={() => handleRoleChange(member.userId, 'ADMIN')}
|
||||
disabled={updatingRole === member.userId}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
{updatingRole === member.userId ? '...' : 'Promouvoir Admin'}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => handleRoleChange(member.userId, 'MEMBER')}
|
||||
disabled={updatingRole === member.userId}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
{updatingRole === member.userId ? '...' : 'Rétrograder'}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => handleRemoveMember(member.userId)}
|
||||
disabled={removingMember === member.userId}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
style={{
|
||||
color: 'var(--destructive)',
|
||||
borderColor: 'var(--destructive)',
|
||||
}}
|
||||
>
|
||||
{removingMember === member.userId ? '...' : 'Retirer'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{addMemberOpen && (
|
||||
<AddMemberModal
|
||||
teamId={teamId}
|
||||
existingMemberIds={members.map((m) => m.userId)}
|
||||
onClose={() => setAddMemberOpen(false)}
|
||||
onSuccess={onMemberUpdate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user