diff --git a/src/app/(dashboard)/settings/members/components/members-table.tsx b/src/app/(dashboard)/settings/members/components/members-table.tsx index 5cd6dd5..2cac37d 100644 --- a/src/app/(dashboard)/settings/members/components/members-table.tsx +++ b/src/app/(dashboard)/settings/members/components/members-table.tsx @@ -1,11 +1,21 @@ "use client"; +import { useRouter } from "next/navigation"; import { useState, useTransition } from "react"; -import { Loader2, Trash2 } from "lucide-react"; +import { DoorOpen, Loader2, Trash2 } from "lucide-react"; +import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { Select, SelectContent, @@ -21,7 +31,11 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { removeMemberAction, updateMemberRoleAction } from "@/lib/appwrite/team-actions"; +import { + leaveWorkspaceAction, + removeMemberAction, + updateMemberRoleAction, +} from "@/lib/appwrite/team-actions"; type Member = { id: string; @@ -51,108 +65,222 @@ export function MembersTable({ isOwner: boolean; canManage: boolean; }) { + const router = useRouter(); const [busy, setBusy] = useState(null); const [, startTransition] = useTransition(); + const [removing, setRemoving] = useState(null); + const [leaving, setLeaving] = useState(false); const setRole = (membershipId: string, role: string) => { setBusy(membershipId); startTransition(async () => { - const fd = new FormData(); - fd.set("membershipId", membershipId); - fd.set("role", role); - await updateMemberRoleAction({ ok: false }, fd); + const result = await updateMemberRoleAction({ ok: false }, formDataFor({ + membershipId, + role, + })); + if (result.ok) toast.success("Rol güncellendi."); + else toast.error(result.error ?? "Rol güncellenemedi."); setBusy(null); }); }; - const remove = (membershipId: string, name: string) => { - if (!confirm(`${name} adlı üyeyi çalışma alanından çıkarmak istediğinize emin misiniz?`)) { - return; - } - setBusy(membershipId); + const handleRemove = () => { + if (!removing) return; + setBusy(removing.id); startTransition(async () => { - const fd = new FormData(); - fd.set("membershipId", membershipId); - await removeMemberAction({ ok: false }, fd); + const result = await removeMemberAction({ ok: false }, formDataFor({ + membershipId: removing.id, + })); + if (result.ok) { + toast.success(`${removing.name} ekipten çıkarıldı.`); + setRemoving(null); + } else { + toast.error(result.error ?? "İşlem başarısız."); + } + setBusy(null); + }); + }; + + const handleLeave = () => { + setBusy("leave"); + startTransition(async () => { + const result = await leaveWorkspaceAction(); + if (result.ok) { + toast.success("Çalışma alanından ayrıldınız."); + setLeaving(false); + router.push("/dashboard"); + } else { + toast.error(result.error ?? "Ayrılma başarısız."); + } setBusy(null); }); }; return ( - - - Üyeler ({members.length}) - - - - - - İsim - Email - Rol - İşlem - - - - {members.map((m) => { - const isSelf = m.userId === currentUserId; - const isMemberOwner = m.role === "owner"; - return ( - - - {m.name} - {isSelf && ( - - Siz - - )} - - {m.email} - - {isOwner && !isMemberOwner && !isSelf ? ( - - ) : ( - - {ROLE_LABEL[m.role] ?? m.role} - - )} - - - {canManage && !isSelf && !isMemberOwner ? ( - - ) : null} - - - ); - })} - -
-
-
+ <> + + + Üyeler ({members.length}) + + + + + + İsim + Email + Rol + İşlem + + + + {members.map((m) => { + const isSelf = m.userId === currentUserId; + const isMemberOwner = m.role === "owner"; + return ( + + + {m.name} + {isSelf && ( + + Siz + + )} + + {m.email} + + {isOwner && !isMemberOwner && !isSelf ? ( + + ) : ( + + {ROLE_LABEL[m.role] ?? m.role} + + )} + + + {isSelf ? ( + + ) : canManage && !isMemberOwner ? ( + + ) : null} + + + ); + })} + +
+
+
+ + !v && busy === null && setRemoving(null)} + > + + + Üyeyi ekipten çıkar + + {removing && ( + <> + {removing.name} ({removing.email}) ekipten çıkarılacak. + Verileri silinmez ama bu çalışma alanına erişimi kalkar. + + )} + + + + + + + + + + !v && busy === null && setLeaving(false)}> + + + Çalışma alanından ayrıl + + Bu çalışma alanındaki tüm verilere erişiminiz kalkar. Tekrar davet edilmedikçe + giremezsiniz. Devam etmek istediğinize emin misiniz? + + + + + + + + + ); } + +function formDataFor(fields: Record): FormData { + const fd = new FormData(); + for (const [k, v] of Object.entries(fields)) fd.set(k, v); + return fd; +} diff --git a/src/lib/appwrite/team-actions.ts b/src/lib/appwrite/team-actions.ts index 367fa27..7c75e68 100644 --- a/src/lib/appwrite/team-actions.ts +++ b/src/lib/appwrite/team-actions.ts @@ -244,6 +244,74 @@ export async function removeMemberAction( return { ok: true }; } +/** + * Self-leave: the current user removes themselves from the active tenant. + * - If they're the only owner, the call is refused (would leave the workspace + * ownerless). + * - On success the active-tenant cookie + prefs.activeTenant are cleared so + * the next request goes through fallback to another team or onboarding. + */ +export async function leaveWorkspaceAction(): Promise { + let ctx; + try { + ctx = await requireTenant(); + } catch { + return { ok: false, error: "Yetkiniz yok." }; + } + + try { + const admin = createAdminClient(); + const memberships = await admin.teams.listMemberships(ctx.tenantId); + const me = memberships.memberships.find((m) => m.userId === ctx.user.id); + if (!me) return { ok: false, error: "Üyelik bulunamadı." }; + + if (me.roles.includes("owner")) { + const otherOwners = memberships.memberships.filter( + (m) => m.userId !== ctx.user.id && m.roles.includes("owner"), + ); + if (otherOwners.length === 0) { + return { + ok: false, + error: + "Tek sahip olduğunuz için ayrılamazsınız. Önce başka bir üyeyi sahip yapın.", + }; + } + } + + await admin.teams.deleteMembership(ctx.tenantId, me.$id); + + await logAudit({ + tenantId: ctx.tenantId, + userId: ctx.user.id, + action: "delete", + entityType: "membership", + entityId: me.$id, + changes: { self: true, userEmail: ctx.user.email }, + }); + + // Clear active-tenant pointers so the user lands somewhere safe next request. + try { + const session = await createSessionClient(); + const user = await session.account.get(); + const prefs = { ...(user.prefs as Record) }; + delete prefs.activeTenant; + await session.account.updatePrefs(prefs); + } catch { + /* ignore */ + } + try { + (await cookies()).delete(ACTIVE_TENANT_COOKIE); + } catch { + /* ignore */ + } + } catch (e) { + return { ok: false, error: appwriteError(e) }; + } + + revalidatePath("/", "layout"); + return { ok: true }; +} + export async function updateMemberRoleAction( _prev: MemberActionState, formData: FormData,