607 lines
23 KiB
Vue
607 lines
23 KiB
Vue
|
|
<script setup lang="ts">
|
||
|
|
import { Head, router, useForm } from '@inertiajs/vue3';
|
||
|
|
import {
|
||
|
|
MoreHorizontal,
|
||
|
|
Shield,
|
||
|
|
UserCog,
|
||
|
|
UserMinus,
|
||
|
|
UserPlus,
|
||
|
|
Users,
|
||
|
|
} from 'lucide-vue-next';
|
||
|
|
import { computed, ref, watch } from 'vue';
|
||
|
|
import Heading from '@/components/Heading.vue';
|
||
|
|
import { Badge } from '@/components/ui/badge';
|
||
|
|
import { Button } from '@/components/ui/button';
|
||
|
|
import {
|
||
|
|
Dialog,
|
||
|
|
DialogContent,
|
||
|
|
DialogDescription,
|
||
|
|
DialogFooter,
|
||
|
|
DialogHeader,
|
||
|
|
DialogTitle,
|
||
|
|
} from '@/components/ui/dialog';
|
||
|
|
import {
|
||
|
|
DropdownMenu,
|
||
|
|
DropdownMenuContent,
|
||
|
|
DropdownMenuItem,
|
||
|
|
DropdownMenuSeparator,
|
||
|
|
DropdownMenuTrigger,
|
||
|
|
} from '@/components/ui/dropdown-menu';
|
||
|
|
import { Input } from '@/components/ui/input';
|
||
|
|
import { Label } from '@/components/ui/label';
|
||
|
|
import {
|
||
|
|
Select,
|
||
|
|
SelectContent,
|
||
|
|
SelectItem,
|
||
|
|
SelectTrigger,
|
||
|
|
SelectValue,
|
||
|
|
} from '@/components/ui/select';
|
||
|
|
import { Switch } from '@/components/ui/switch';
|
||
|
|
import AppLayout from '@/layouts/AppLayout.vue';
|
||
|
|
import { dashboard } from '@/routes';
|
||
|
|
import { index as teamIndex } from '@/routes/team';
|
||
|
|
import type { BreadcrumbItem, TeamPageProps, TeamMember } from '@/types';
|
||
|
|
|
||
|
|
type Props = TeamPageProps;
|
||
|
|
|
||
|
|
const ROLE_OWNER = 'owner';
|
||
|
|
const ROLE_MANAGER = 'manager';
|
||
|
|
|
||
|
|
const props = defineProps<Props>();
|
||
|
|
|
||
|
|
const showInviteDialog = ref(false);
|
||
|
|
|
||
|
|
const form = useForm({
|
||
|
|
email: '',
|
||
|
|
role: 'worker',
|
||
|
|
});
|
||
|
|
|
||
|
|
function submitInvite() {
|
||
|
|
form.post(props.inviteUrl, {
|
||
|
|
onSuccess: () => {
|
||
|
|
showInviteDialog.value = false;
|
||
|
|
form.reset();
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Role Change Dialog ──
|
||
|
|
const showRoleDialog = ref(false);
|
||
|
|
const roleChangeMember = ref<TeamMember | null>(null);
|
||
|
|
const selectedRole = ref('');
|
||
|
|
const roleChangeProcessing = ref(false);
|
||
|
|
|
||
|
|
function openRoleDialog(member: TeamMember) {
|
||
|
|
roleChangeMember.value = member;
|
||
|
|
selectedRole.value = member.role;
|
||
|
|
showRoleDialog.value = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
const isRoleUnchanged = computed(
|
||
|
|
() => selectedRole.value === roleChangeMember.value?.role,
|
||
|
|
);
|
||
|
|
|
||
|
|
function submitRoleChange() {
|
||
|
|
if (!roleChangeMember.value || isRoleUnchanged.value) return;
|
||
|
|
router.patch(
|
||
|
|
roleChangeMember.value.updateRoleUrl,
|
||
|
|
{ role: selectedRole.value },
|
||
|
|
{
|
||
|
|
onStart: () => {
|
||
|
|
roleChangeProcessing.value = true;
|
||
|
|
},
|
||
|
|
onFinish: () => {
|
||
|
|
roleChangeProcessing.value = false;
|
||
|
|
},
|
||
|
|
onSuccess: () => {
|
||
|
|
showRoleDialog.value = false;
|
||
|
|
roleChangeMember.value = null;
|
||
|
|
},
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Remove Member Dialog ──
|
||
|
|
const showRemoveDialog = ref(false);
|
||
|
|
const removeMember = ref<TeamMember | null>(null);
|
||
|
|
const removeProcessing = ref(false);
|
||
|
|
|
||
|
|
function openRemoveDialog(member: TeamMember) {
|
||
|
|
removeMember.value = member;
|
||
|
|
showRemoveDialog.value = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
function submitRemove() {
|
||
|
|
if (!removeMember.value) return;
|
||
|
|
router.delete(removeMember.value.removeUrl, {
|
||
|
|
onStart: () => {
|
||
|
|
removeProcessing.value = true;
|
||
|
|
},
|
||
|
|
onFinish: () => {
|
||
|
|
removeProcessing.value = false;
|
||
|
|
},
|
||
|
|
onSuccess: () => {
|
||
|
|
showRemoveDialog.value = false;
|
||
|
|
removeMember.value = null;
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Permissions Dialog ──
|
||
|
|
const showPermissionsDialog = ref(false);
|
||
|
|
const permissionsMember = ref<TeamMember | null>(null);
|
||
|
|
const permissionsProcessing = ref(false);
|
||
|
|
|
||
|
|
function openPermissionsDialog(member: TeamMember) {
|
||
|
|
permissionsMember.value = member;
|
||
|
|
showPermissionsDialog.value = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
function togglePermission(key: string, value: boolean) {
|
||
|
|
if (!permissionsMember.value?.permissionsUrl) return;
|
||
|
|
const updatedPermissions = {
|
||
|
|
...permissionsMember.value.permissions,
|
||
|
|
[key]: value,
|
||
|
|
};
|
||
|
|
permissionsProcessing.value = true;
|
||
|
|
router.put(
|
||
|
|
permissionsMember.value.permissionsUrl,
|
||
|
|
{ permissions: updatedPermissions },
|
||
|
|
{
|
||
|
|
preserveScroll: true,
|
||
|
|
onFinish: () => {
|
||
|
|
permissionsProcessing.value = false;
|
||
|
|
},
|
||
|
|
onSuccess: () => {
|
||
|
|
if (permissionsMember.value) {
|
||
|
|
permissionsMember.value = {
|
||
|
|
...permissionsMember.value,
|
||
|
|
permissions: updatedPermissions,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
},
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Keep permissionsMember in sync when props update (e.g. Inertia partial reload)
|
||
|
|
watch(
|
||
|
|
() => props.members,
|
||
|
|
(members) => {
|
||
|
|
if (!permissionsMember.value || !showPermissionsDialog.value) return;
|
||
|
|
const updated = members.find(
|
||
|
|
(m) => m.workspace_user_id === permissionsMember.value!.workspace_user_id,
|
||
|
|
);
|
||
|
|
if (updated) {
|
||
|
|
permissionsMember.value = updated;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
);
|
||
|
|
|
||
|
|
function canShowActions(member: TeamMember): boolean {
|
||
|
|
// No actions for current user's own row
|
||
|
|
if (member.id === props.authUserId) return false;
|
||
|
|
// No actions for Owner rows (managers cannot modify owners)
|
||
|
|
if (member.role === ROLE_OWNER) return false;
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
const allMembers = computed(() => {
|
||
|
|
const active = props.members.map((m) => ({
|
||
|
|
id: `member-${m.id}`,
|
||
|
|
name: m.name,
|
||
|
|
email: m.email,
|
||
|
|
role: m.role,
|
||
|
|
date: m.joined_at,
|
||
|
|
status: m.status,
|
||
|
|
}));
|
||
|
|
const pending = props.pendingInvitations.map((inv) => ({
|
||
|
|
id: `invite-${inv.id}`,
|
||
|
|
name: null as string | null,
|
||
|
|
email: inv.email,
|
||
|
|
role: inv.role,
|
||
|
|
date: inv.invited_at,
|
||
|
|
status: inv.status,
|
||
|
|
}));
|
||
|
|
return [...active, ...pending];
|
||
|
|
});
|
||
|
|
|
||
|
|
const isEmpty = computed(
|
||
|
|
() => props.members.length <= 1 && props.pendingInvitations.length === 0,
|
||
|
|
);
|
||
|
|
|
||
|
|
const roleLabels = computed<Record<string, string>>(() => ({
|
||
|
|
owner: 'Propriétaire',
|
||
|
|
...props.roles,
|
||
|
|
}));
|
||
|
|
|
||
|
|
const statusLabels: Record<string, string> = {
|
||
|
|
active: 'Actif',
|
||
|
|
pending: 'En attente',
|
||
|
|
};
|
||
|
|
|
||
|
|
function formatDate(dateStr: string): string {
|
||
|
|
return new Date(dateStr).toLocaleDateString('fr-FR', {
|
||
|
|
year: 'numeric',
|
||
|
|
month: 'short',
|
||
|
|
day: 'numeric',
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function getMemberData(member: {
|
||
|
|
id: string;
|
||
|
|
status: string;
|
||
|
|
}): TeamMember | null {
|
||
|
|
if (member.status !== 'active') return null;
|
||
|
|
const userId = Number(member.id.replace('member-', ''));
|
||
|
|
return props.members.find((m) => m.id === userId) ?? null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const breadcrumbs: BreadcrumbItem[] = [
|
||
|
|
{ title: 'Dashboard', href: dashboard().url },
|
||
|
|
{ title: 'Équipe', href: teamIndex().url },
|
||
|
|
];
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<template>
|
||
|
|
<AppLayout :breadcrumbs="breadcrumbs">
|
||
|
|
<Head title="Équipe" />
|
||
|
|
|
||
|
|
<!-- Invite Dialog (shared across empty state and header) -->
|
||
|
|
<Dialog v-model:open="showInviteDialog">
|
||
|
|
<DialogContent>
|
||
|
|
<DialogHeader>
|
||
|
|
<DialogTitle>Inviter un membre</DialogTitle>
|
||
|
|
<DialogDescription>
|
||
|
|
Envoyez une invitation par email pour ajouter un membre
|
||
|
|
à votre équipe.
|
||
|
|
</DialogDescription>
|
||
|
|
</DialogHeader>
|
||
|
|
<form class="space-y-4" @submit.prevent="submitInvite">
|
||
|
|
<div class="space-y-2">
|
||
|
|
<Label for="email">Adresse email</Label>
|
||
|
|
<Input
|
||
|
|
id="email"
|
||
|
|
v-model="form.email"
|
||
|
|
type="email"
|
||
|
|
placeholder="nom@exemple.com"
|
||
|
|
required
|
||
|
|
/>
|
||
|
|
<p
|
||
|
|
v-if="form.errors.email"
|
||
|
|
class="text-sm text-destructive"
|
||
|
|
>
|
||
|
|
{{ form.errors.email }}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div class="space-y-2">
|
||
|
|
<Label for="role">Rôle</Label>
|
||
|
|
<Select v-model="form.role">
|
||
|
|
<SelectTrigger>
|
||
|
|
<SelectValue
|
||
|
|
placeholder="Sélectionner un rôle"
|
||
|
|
/>
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem
|
||
|
|
v-for="(label, value) in roles"
|
||
|
|
:key="value"
|
||
|
|
:value="value"
|
||
|
|
>
|
||
|
|
{{ label }}
|
||
|
|
</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
<p
|
||
|
|
v-if="form.errors.role"
|
||
|
|
class="text-sm text-destructive"
|
||
|
|
>
|
||
|
|
{{ form.errors.role }}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<DialogFooter>
|
||
|
|
<Button type="submit" :disabled="form.processing">
|
||
|
|
Envoyer l'invitation
|
||
|
|
</Button>
|
||
|
|
</DialogFooter>
|
||
|
|
</form>
|
||
|
|
</DialogContent>
|
||
|
|
</Dialog>
|
||
|
|
|
||
|
|
<!-- Role Change Dialog -->
|
||
|
|
<Dialog v-model:open="showRoleDialog">
|
||
|
|
<DialogContent>
|
||
|
|
<DialogHeader>
|
||
|
|
<DialogTitle>Changer le rôle</DialogTitle>
|
||
|
|
<DialogDescription>
|
||
|
|
Sélectionnez le nouveau rôle pour
|
||
|
|
{{ roleChangeMember?.name }}
|
||
|
|
</DialogDescription>
|
||
|
|
</DialogHeader>
|
||
|
|
<div class="space-y-4">
|
||
|
|
<div class="space-y-2">
|
||
|
|
<Label>Rôle</Label>
|
||
|
|
<Select v-model="selectedRole">
|
||
|
|
<SelectTrigger>
|
||
|
|
<SelectValue
|
||
|
|
placeholder="Sélectionner un rôle"
|
||
|
|
/>
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem
|
||
|
|
v-for="(label, value) in roles"
|
||
|
|
:key="value"
|
||
|
|
:value="value"
|
||
|
|
>
|
||
|
|
{{ label }}
|
||
|
|
</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
<DialogFooter>
|
||
|
|
<Button
|
||
|
|
:disabled="isRoleUnchanged || roleChangeProcessing"
|
||
|
|
@click="submitRoleChange"
|
||
|
|
>
|
||
|
|
Confirmer
|
||
|
|
</Button>
|
||
|
|
</DialogFooter>
|
||
|
|
</div>
|
||
|
|
</DialogContent>
|
||
|
|
</Dialog>
|
||
|
|
|
||
|
|
<!-- Permissions Dialog -->
|
||
|
|
<Dialog v-model:open="showPermissionsDialog">
|
||
|
|
<DialogContent>
|
||
|
|
<DialogHeader>
|
||
|
|
<DialogTitle>Gérer les permissions</DialogTitle>
|
||
|
|
<DialogDescription>
|
||
|
|
Configurez les permissions de
|
||
|
|
{{ permissionsMember?.name }}
|
||
|
|
</DialogDescription>
|
||
|
|
</DialogHeader>
|
||
|
|
<div class="space-y-4">
|
||
|
|
<div
|
||
|
|
v-for="(label, key) in availablePermissions"
|
||
|
|
:key="key"
|
||
|
|
class="flex items-center justify-between rounded-lg border p-3"
|
||
|
|
>
|
||
|
|
<Label :for="`perm-${key}`" class="cursor-pointer">
|
||
|
|
{{ label }}
|
||
|
|
</Label>
|
||
|
|
<Switch
|
||
|
|
:id="`perm-${key}`"
|
||
|
|
:checked="
|
||
|
|
permissionsMember?.permissions?.[key] ?? false
|
||
|
|
"
|
||
|
|
:disabled="permissionsProcessing"
|
||
|
|
@update:checked="
|
||
|
|
(val: boolean) => togglePermission(key, val)
|
||
|
|
"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</DialogContent>
|
||
|
|
</Dialog>
|
||
|
|
|
||
|
|
<!-- Remove Member Dialog -->
|
||
|
|
<Dialog v-model:open="showRemoveDialog">
|
||
|
|
<DialogContent>
|
||
|
|
<DialogHeader>
|
||
|
|
<DialogTitle>Retirer le membre</DialogTitle>
|
||
|
|
<DialogDescription>
|
||
|
|
Êtes-vous sûr de vouloir retirer
|
||
|
|
{{ removeMember?.name }} de l'espace de travail ? Cette
|
||
|
|
action est irréversible.
|
||
|
|
</DialogDescription>
|
||
|
|
</DialogHeader>
|
||
|
|
<DialogFooter>
|
||
|
|
<Button variant="outline" @click="showRemoveDialog = false">
|
||
|
|
Annuler
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
variant="destructive"
|
||
|
|
:disabled="removeProcessing"
|
||
|
|
@click="submitRemove"
|
||
|
|
>
|
||
|
|
Retirer
|
||
|
|
</Button>
|
||
|
|
</DialogFooter>
|
||
|
|
</DialogContent>
|
||
|
|
</Dialog>
|
||
|
|
|
||
|
|
<div class="flex flex-col space-y-6 p-4">
|
||
|
|
<div class="flex items-center justify-between">
|
||
|
|
<Heading
|
||
|
|
variant="small"
|
||
|
|
title="Équipe"
|
||
|
|
description="Gérer les membres de votre équipe"
|
||
|
|
/>
|
||
|
|
<Button v-if="canManageTeam" @click="showInviteDialog = true">
|
||
|
|
<UserPlus class="mr-2 h-4 w-4" />
|
||
|
|
Inviter un membre
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Empty State -->
|
||
|
|
<div
|
||
|
|
v-if="isEmpty"
|
||
|
|
class="flex flex-col items-center justify-center rounded-xl border border-sidebar-border/70 px-4 py-16 text-center dark:border-sidebar-border"
|
||
|
|
>
|
||
|
|
<Users class="mb-4 h-12 w-12 text-muted-foreground" />
|
||
|
|
<h3 class="text-lg font-medium">Aucun membre</h3>
|
||
|
|
<p class="mt-1 text-sm text-muted-foreground">
|
||
|
|
Invitez votre premier membre d'équipe
|
||
|
|
</p>
|
||
|
|
<Button
|
||
|
|
v-if="canManageTeam"
|
||
|
|
class="mt-4"
|
||
|
|
@click="showInviteDialog = true"
|
||
|
|
>
|
||
|
|
<UserPlus class="mr-2 h-4 w-4" />
|
||
|
|
Inviter un membre
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Team Table -->
|
||
|
|
<div
|
||
|
|
v-else
|
||
|
|
class="overflow-hidden rounded-xl border border-sidebar-border/70 dark:border-sidebar-border"
|
||
|
|
>
|
||
|
|
<div class="overflow-x-auto">
|
||
|
|
<table class="w-full text-sm">
|
||
|
|
<thead
|
||
|
|
class="border-b border-sidebar-border/70 bg-muted/50"
|
||
|
|
>
|
||
|
|
<tr>
|
||
|
|
<th
|
||
|
|
class="h-10 px-4 text-left align-middle font-medium"
|
||
|
|
>
|
||
|
|
Nom
|
||
|
|
</th>
|
||
|
|
<th
|
||
|
|
class="h-10 px-4 text-left align-middle font-medium"
|
||
|
|
>
|
||
|
|
Email
|
||
|
|
</th>
|
||
|
|
<th
|
||
|
|
class="h-10 px-4 text-left align-middle font-medium"
|
||
|
|
>
|
||
|
|
Rôle
|
||
|
|
</th>
|
||
|
|
<th
|
||
|
|
class="h-10 px-4 text-left align-middle font-medium"
|
||
|
|
>
|
||
|
|
Rejoint le
|
||
|
|
</th>
|
||
|
|
<th
|
||
|
|
class="h-10 px-4 text-left align-middle font-medium"
|
||
|
|
>
|
||
|
|
Statut
|
||
|
|
</th>
|
||
|
|
<th
|
||
|
|
v-if="canManageTeam"
|
||
|
|
class="h-10 w-12 px-4 text-left align-middle font-medium"
|
||
|
|
>
|
||
|
|
<span class="sr-only">Actions</span>
|
||
|
|
</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<tr
|
||
|
|
v-for="member in allMembers"
|
||
|
|
:key="member.id"
|
||
|
|
class="border-b border-sidebar-border/50 last:border-0 hover:bg-muted/50"
|
||
|
|
>
|
||
|
|
<td class="px-4 py-3 font-medium">
|
||
|
|
{{ member.name ?? '—' }}
|
||
|
|
</td>
|
||
|
|
<td class="px-4 py-3 text-muted-foreground">
|
||
|
|
{{ member.email }}
|
||
|
|
</td>
|
||
|
|
<td class="px-4 py-3">
|
||
|
|
<Badge variant="secondary">
|
||
|
|
{{
|
||
|
|
roleLabels[member.role] ??
|
||
|
|
member.role
|
||
|
|
}}
|
||
|
|
</Badge>
|
||
|
|
</td>
|
||
|
|
<td class="px-4 py-3 text-muted-foreground">
|
||
|
|
{{ formatDate(member.date) }}
|
||
|
|
</td>
|
||
|
|
<td class="px-4 py-3">
|
||
|
|
<Badge
|
||
|
|
:variant="
|
||
|
|
member.status === 'active'
|
||
|
|
? 'default'
|
||
|
|
: 'outline'
|
||
|
|
"
|
||
|
|
>
|
||
|
|
{{ statusLabels[member.status] }}
|
||
|
|
</Badge>
|
||
|
|
</td>
|
||
|
|
<td v-if="canManageTeam" class="px-4 py-3">
|
||
|
|
<template
|
||
|
|
v-if="
|
||
|
|
getMemberData(member) &&
|
||
|
|
canShowActions(
|
||
|
|
getMemberData(member)!,
|
||
|
|
)
|
||
|
|
"
|
||
|
|
>
|
||
|
|
<DropdownMenu>
|
||
|
|
<DropdownMenuTrigger as-child>
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="icon"
|
||
|
|
>
|
||
|
|
<MoreHorizontal
|
||
|
|
class="h-4 w-4"
|
||
|
|
/>
|
||
|
|
</Button>
|
||
|
|
</DropdownMenuTrigger>
|
||
|
|
<DropdownMenuContent align="end">
|
||
|
|
<DropdownMenuItem
|
||
|
|
v-if="
|
||
|
|
isOwner &&
|
||
|
|
getMemberData(member)!
|
||
|
|
.role ===
|
||
|
|
ROLE_MANAGER
|
||
|
|
"
|
||
|
|
@click="
|
||
|
|
openPermissionsDialog(
|
||
|
|
getMemberData(
|
||
|
|
member,
|
||
|
|
)!,
|
||
|
|
)
|
||
|
|
"
|
||
|
|
>
|
||
|
|
<Shield
|
||
|
|
class="mr-2 h-4 w-4"
|
||
|
|
/>
|
||
|
|
Gérer les permissions
|
||
|
|
</DropdownMenuItem>
|
||
|
|
<DropdownMenuItem
|
||
|
|
@click="
|
||
|
|
openRoleDialog(
|
||
|
|
getMemberData(
|
||
|
|
member,
|
||
|
|
)!,
|
||
|
|
)
|
||
|
|
"
|
||
|
|
>
|
||
|
|
<UserCog
|
||
|
|
class="mr-2 h-4 w-4"
|
||
|
|
/>
|
||
|
|
Changer le rôle
|
||
|
|
</DropdownMenuItem>
|
||
|
|
<DropdownMenuSeparator />
|
||
|
|
<DropdownMenuItem
|
||
|
|
class="text-destructive"
|
||
|
|
@click="
|
||
|
|
openRemoveDialog(
|
||
|
|
getMemberData(
|
||
|
|
member,
|
||
|
|
)!,
|
||
|
|
)
|
||
|
|
"
|
||
|
|
>
|
||
|
|
<UserMinus
|
||
|
|
class="mr-2 h-4 w-4"
|
||
|
|
/>
|
||
|
|
Retirer de l'espace
|
||
|
|
</DropdownMenuItem>
|
||
|
|
</DropdownMenuContent>
|
||
|
|
</DropdownMenu>
|
||
|
|
</template>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</AppLayout>
|
||
|
|
</template>
|