"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { groupsDelete, groupsRename } from "@/lib/client/groups"; import useGroupAudit from "@/features/groups/hooks/use-group-audit"; import useGroupInvites from "@/features/groups/hooks/use-group-invites"; import useGroupMembers from "@/features/groups/hooks/use-group-members"; import useGroupSettings from "@/features/groups/hooks/use-group-settings"; import useTags from "@/features/tags/hooks/use-tags"; import type { AuditFilterKey, ConfirmInviteDeleteTarget, ConfirmUserTarget, InviteMaxUses, JoinPolicy } from "@/features/groups/components/group-settings.types"; import { eventCategory, formatInviteExpiry, isInviteExpired } from "@/features/groups/components/group-settings.utils"; import { useGroupsContext } from "@/hooks/groups-context"; import { useNotificationsContext } from "@/hooks/notifications-context"; export default function useGroupSettingsViewModel(groupId: number) { const router = useRouter(); const { groups, activeGroupId, setActiveGroup } = useGroupsContext(); const { notify } = useNotificationsContext(); const group = useMemo(() => groups.find(item => item.id === groupId) || null, [groups, groupId]); const role = group?.role; const isAdmin = role === "GROUP_ADMIN" || role === "GROUP_OWNER"; const isOwner = role === "GROUP_OWNER"; const canViewAudit = isAdmin; const hasSyncedActiveRef = useRef(false); const lastGroupIdRef = useRef(groupId); useEffect(() => { if (lastGroupIdRef.current !== groupId) { lastGroupIdRef.current = groupId; hasSyncedActiveRef.current = false; } if (!groupId || hasSyncedActiveRef.current) return; if (activeGroupId !== groupId) setActiveGroup(groupId); hasSyncedActiveRef.current = true; }, [activeGroupId, groupId, setActiveGroup]); const { tags, addTags, removeTag } = useTags(activeGroupId === groupId ? groupId : null); const { settings, updateSettings } = useGroupSettings(activeGroupId === groupId ? groupId : null); const { members, requests, currentUserId, approve, deny, promote, demote, kick, transferOwner, leave } = useGroupMembers(activeGroupId === groupId ? groupId : null); const { links, create: createInvite, revoke: revokeInvite, revive: reviveInvite, remove: deleteInvite } = useGroupInvites(activeGroupId === groupId ? groupId : null); const { events } = useGroupAudit(activeGroupId === groupId && canViewAudit ? groupId : null); const [pendingTags, setPendingTags] = useState([]); const [toggleRemoveTags, setToggleRemoveTags] = useState([]); const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); const [renameValue, setRenameValue] = useState(group?.name || ""); const [renameModalOpen, setRenameModalOpen] = useState(false); const [confirmRenameOpen, setConfirmRenameOpen] = useState(false); const [confirmLeaveOpen, setConfirmLeaveOpen] = useState(false); const [confirmKick, setConfirmKick] = useState(null); const [confirmTransfer, setConfirmTransfer] = useState(null); const [confirmDeleteGroupOpen, setConfirmDeleteGroupOpen] = useState(false); const [deleteConfirmText, setDeleteConfirmText] = useState(""); const [tagModalOpen, setTagModalOpen] = useState(false); const [showAllTags, setShowAllTags] = useState(false); const [inviteTtlDays, setInviteTtlDays] = useState(7); const [inviteMaxUses, setInviteMaxUses] = useState("UNLIMITED"); const [auditOpen, setAuditOpen] = useState(false); const [confirmDeleteInvite, setConfirmDeleteInvite] = useState(null); const [auditFilters, setAuditFilters] = useState(["members", "tags", "settings", "entries"]); const [auditQuery, setAuditQuery] = useState(""); const [auditFrom, setAuditFrom] = useState(""); const [auditTo, setAuditTo] = useState(""); const [auditLimit, setAuditLimit] = useState(10); const [localAllowMemberTagManage, setLocalAllowMemberTagManage] = useState(settings.allowMemberTagManage); const [localJoinPolicy, setLocalJoinPolicy] = useState(settings.joinPolicy); const canManageTags = role === "GROUP_ADMIN" || role === "GROUP_OWNER" || localAllowMemberTagManage; const adminCount = members.filter(member => member.role === "GROUP_ADMIN" || member.role === "GROUP_OWNER").length; const isLastAdmin = isAdmin && adminCount <= 1; const memberCount = members.length; const showLeaveGroup = role !== "GROUP_OWNER" && !isLastAdmin; useEffect(() => { setRenameValue(group?.name || ""); }, [group?.name]); useEffect(() => { setLocalAllowMemberTagManage(settings.allowMemberTagManage); setLocalJoinPolicy(settings.joinPolicy); }, [settings.allowMemberTagManage, settings.joinPolicy]); const handleCloseRenameModal = useCallback(() => { setRenameModalOpen(false); setRenameValue(group?.name || ""); }, [group?.name]); async function handleSaveTags() { if (!pendingTags.length) return; const ok = await addTags(pendingTags); if (ok) { notify({ title: "Tags updated", message: pendingTags.join(", "), tone: "success" }); setPendingTags([]); } } async function handleConfirmDelete() { if (!toggleRemoveTags.length) return; await Promise.all(toggleRemoveTags.map(tag => removeTag(tag))); notify({ title: "Tags removed", message: toggleRemoveTags.join(", "), tone: "danger" }); setToggleRemoveTags([]); } async function handleToggleAllowMembers(value: boolean) { setLocalAllowMemberTagManage(value); const ok = await updateSettings(value, localJoinPolicy); if (ok) { notify({ title: "Tag permissions updated" }); return; } setLocalAllowMemberTagManage(settings.allowMemberTagManage); notify({ title: "Update failed", message: "Could not save tag permissions.", tone: "danger" }); } async function handleJoinPolicyChange(nextPolicy: JoinPolicy) { setLocalJoinPolicy(nextPolicy); const ok = await updateSettings(localAllowMemberTagManage, nextPolicy); if (ok) { notify({ title: "Join policy updated" }); return; } setLocalJoinPolicy(settings.joinPolicy); notify({ title: "Update failed", message: "Could not save join policy.", tone: "danger" }); } function handleOpenRenameModal() { setRenameValue(group?.name || ""); setRenameModalOpen(true); } async function handleRenameGroup() { const name = renameValue.trim(); if (!name) return; const result = await groupsRename({ name }); if ("error" in result) { notify({ title: "Rename failed", message: result.error.message, tone: "danger" }); return; } notify({ title: "Group renamed", message: name }); setConfirmRenameOpen(false); setRenameModalOpen(false); } async function handleRoleChange(targetUserId: number, nextRole: "MEMBER" | "GROUP_ADMIN" | "GROUP_OWNER") { if (!isAdmin) return; const member = members.find(item => item.userId === targetUserId); if (!member) return; if (member.role === nextRole) return; if (nextRole === "GROUP_OWNER") { if (!isOwner) return; await transferOwner(targetUserId); return; } if (nextRole === "GROUP_ADMIN") { await promote(targetUserId); return; } await demote(targetUserId); } function getInviteLinkUrl(token: string) { if (typeof window === "undefined") return token; return `${window.location.origin}/invite/${token}`; } async function handleCopyInvite(token: string) { try { await navigator.clipboard.writeText(getInviteLinkUrl(token)); notify({ title: "Invite link copied", message: "Link copied to clipboard" }); } catch { notify({ title: "Copy failed", tone: "danger" }); } } const memberLookup = useMemo(() => { return new Map(members.map(member => [member.userId, { name: member.displayName || member.email, email: member.email }])); }, [members]); function getMemberLabel(userId?: number | null) { if (!userId) return "System"; const member = memberLookup.get(userId); return member?.name || `User ${userId}`; } const filteredEvents = events.filter(event => { const category = eventCategory(event.eventType); if (!auditFilters.includes(category)) return false; if (auditQuery) { const query = auditQuery.toLowerCase(); const actor = event.actorUserId ? memberLookup.get(event.actorUserId) : null; const haystack = `${event.eventType} ${event.actorUserId ?? ""} ${actor?.name || ""} ${actor?.email || ""} ${event.requestId}`.toLowerCase(); if (!haystack.includes(query)) return false; } if (auditFrom) { const from = new Date(auditFrom).getTime(); if (!Number.isNaN(from) && new Date(event.createdAt).getTime() < from) return false; } if (auditTo) { const to = new Date(auditTo).getTime(); if (!Number.isNaN(to) && new Date(event.createdAt).getTime() > to + 24 * 60 * 60 * 1000 - 1) return false; } return true; }); const todayKey = new Date().toISOString().slice(0, 10); const logsToday = events.filter(event => event.createdAt?.slice(0, 10) === todayKey).length; const mostActiveUser = (() => { const counts = new Map(); for (const event of events) { if (!event.actorUserId) continue; counts.set(event.actorUserId, (counts.get(event.actorUserId) ?? 0) + 1); } let top: number | null = null; let topCount = 0; counts.forEach((count, userId) => { if (count > topCount) { top = userId; topCount = count; } }); if (!top) return null; const member = memberLookup.get(top); const name = member?.name || `User ${top}`; const searchValue = member?.name || member?.email || String(top); return { userId: top, count: topCount, name, searchValue }; })(); const mostActiveCount = mostActiveUser?.count ?? 0; const renameDirty = Boolean(group && renameValue.trim() !== group.name); const visibleTags = showAllTags ? tags : tags.slice(0, 5); const hasMoreTags = tags.length > 5; const tagsScrollable = showAllTags && tags.length > 15; useEffect(() => { if (!renameModalOpen && !tagModalOpen) return; function handleKeyDown(event: KeyboardEvent) { if (event.key === "Escape") { if (tagModalOpen) setTagModalOpen(false); if (renameModalOpen) handleCloseRenameModal(); } if (event.key === "Enter" && !event.shiftKey && !event.defaultPrevented) { if (renameModalOpen && renameDirty && isAdmin && renameValue.trim()) setConfirmRenameOpen(true); } } window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [renameModalOpen, tagModalOpen, renameDirty, isAdmin, renameValue, handleCloseRenameModal]); async function handleDeleteGroup() { const result = await groupsDelete(); if ("error" in result) { notify({ title: "Delete failed", message: result.error.message, tone: "danger" }); return; } setConfirmDeleteGroupOpen(false); setDeleteConfirmText(""); notify({ title: "Group deleted", tone: "danger" }); router.push("/"); } async function handleConfirmDeleteInvite() { if (!confirmDeleteInvite) return; const target = confirmDeleteInvite; setConfirmDeleteInvite(null); const ok = await deleteInvite(target.id); if (ok) notify({ title: "Invite deleted", tone: "danger" }); } async function handleConfirmLeaveGroup() { setConfirmLeaveOpen(false); const ok = await leave(); if (ok) { notify({ title: "Left group" }); router.push("/"); } } async function handleConfirmKickMember() { if (!confirmKick) return; const target = confirmKick; setConfirmKick(null); const ok = await kick(target.userId); if (ok) notify({ title: "Member removed", message: target.name, tone: "danger" }); } async function handleConfirmTransferOwnership() { if (!confirmTransfer) return; const target = confirmTransfer; setConfirmTransfer(null); const ok = await transferOwner(target.userId); if (ok) notify({ title: "Ownership transferred", message: target.name }); } return { group, role, isAdmin, isOwner, canViewAudit, canManageTags, isLastAdmin, memberCount, showLeaveGroup, tags, members, requests, currentUserId, links, events, filteredEvents, logsToday, mostActiveUser, mostActiveCount, visibleTags, hasMoreTags, tagsScrollable, pendingTags, setPendingTags, toggleRemoveTags, setToggleRemoveTags, confirmDeleteOpen, setConfirmDeleteOpen, renameValue, setRenameValue, renameModalOpen, setRenameModalOpen, renameDirty, confirmRenameOpen, setConfirmRenameOpen, confirmLeaveOpen, setConfirmLeaveOpen, confirmKick, setConfirmKick, confirmTransfer, setConfirmTransfer, confirmDeleteGroupOpen, setConfirmDeleteGroupOpen, deleteConfirmText, setDeleteConfirmText, tagModalOpen, setTagModalOpen, showAllTags, setShowAllTags, inviteTtlDays, setInviteTtlDays, inviteMaxUses, setInviteMaxUses, auditOpen, setAuditOpen, confirmDeleteInvite, setConfirmDeleteInvite, auditFilters, setAuditFilters, auditQuery, setAuditQuery, auditFrom, setAuditFrom, auditTo, setAuditTo, auditLimit, setAuditLimit, localAllowMemberTagManage, localJoinPolicy, handleSaveTags, handleConfirmDelete, handleToggleAllowMembers, handleJoinPolicyChange, handleOpenRenameModal, handleCloseRenameModal, handleRenameGroup, handleRoleChange, handleCopyInvite, formatInviteExpiry, isInviteExpired, getMemberLabel, handleDeleteGroup, handleConfirmDeleteInvite, handleConfirmLeaveGroup, handleConfirmKickMember, handleConfirmTransferOwnership, approve, deny, promote, demote, kick, transferOwner, leave, createInvite, revokeInvite, reviveInvite, deleteInvite, goBack: () => router.push("/") }; } export type GroupSettingsViewModelState = ReturnType;