diff --git a/src/components/raidGroup/RaidGroupPermissionSelector.tsx b/src/components/raidGroup/RaidGroupPermissionSelector.tsx new file mode 100644 index 0000000..e29d208 --- /dev/null +++ b/src/components/raidGroup/RaidGroupPermissionSelector.tsx @@ -0,0 +1,41 @@ +import { RaidGroupPermissionType } from "@/interface/RaidGroup"; + + +export default function RaidGroupPermissionSelector({ + value, + onChange +}:{ + value?: RaidGroupPermissionType; + onChange: (e: React.ChangeEvent) => void; +}){ + const modalId = crypto.randomUUID().replaceAll("-", ""); + + + return ( +
+ { + Object.keys(RaidGroupPermissionType).map((permissionType) => ( + + )) + } +
+ ); +} diff --git a/src/hooks/AccountHooks.ts b/src/hooks/AccountHooks.ts index 611e9b2..8209300 100644 --- a/src/hooks/AccountHooks.ts +++ b/src/hooks/AccountHooks.ts @@ -1,4 +1,5 @@ import { Account } from "@/interface/Account"; +import { RaidGroupPermissionType } from "@/interface/RaidGroup"; import { api } from "@/util/AxiosUtil"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; @@ -28,6 +29,51 @@ export function useGetAccounts(page: number, pageSize: number, searchTerm?: stri }); } +export function useGetAccountsByRaidGroup(raidGroupId: string, page: number, pageSize: number, searchTerm?: string){ + return useQuery({ + queryKey: ["accounts", "raidGroup", raidGroupId, {page, pageSize, searchTerm}], + queryFn: async () => { + const params = new URLSearchParams(); + params.append("page", page.toString()); + params.append("pageSize", pageSize.toString()); + if(searchTerm){ + params.append("searchTerm", searchTerm ?? ""); + } + + const response = await api.get(`/account/raidGroup/${raidGroupId}?${params}`); + + if(response.status !== 200){ + throw new Error("Failed to get accounts"); + } + else if(response.data.errors){ + throw new Error(response.data.errors.join(", ")); + } + + return response.data as Account[]; + } + }); +} + +export function useGetRaidGroupPermissionsForAccount(raidGroupId?: string, accountId?: string){ + return useQuery({ + queryKey: ["accounts", "raidGroup", raidGroupId, "account", accountId], + queryFn: async () => { + const response = await api.get(`/account/${accountId}/raidGroup/${raidGroupId}/permission`); + + if(response.status !== 200){ + throw new Error("Failed to get permissions"); + } + else if(response.data.errors){ + throw new Error(response.data.errors.join(", ")); + } + + return response.data.permission as RaidGroupPermissionType; + }, + enabled: !!raidGroupId && !!accountId + }); + +} + export function useGetAccountsCount(searchTerm?: string){ const searchParams = new URLSearchParams(); if(searchTerm){ @@ -53,6 +99,33 @@ export function useGetAccountsCount(searchTerm?: string){ }); } +export function useGetAccountsByRaidGroupCount(raidGroupId: string, searchTerm?: string){ + const searchParams = new URLSearchParams(); + if(searchTerm){ + searchParams.append("searchTerm", searchTerm); + } + + + return useQuery({ + queryKey: [ "accounts", "raidGroup", raidGroupId, "count", searchTerm], + queryFn: async () => { + + const response = await api.get(`/account/raidGroup/${raidGroupId}/count?${searchParams}`); + + if(response.status !== 200){ + throw new Error("Failed to get accounts count"); + } + else if(response.data.errors){ + throw new Error(response.data.errors.join(", ")); + } + + return response.data.count as number; + } + }); +} + + + export function useForcePasswordReset(accountId: string){ const queryClient = useQueryClient(); @@ -165,6 +238,30 @@ export function useUpdateAccount(){ }); } +export function useUpdateRaidGroupPermissionsForAccount(raidGroupId?: string, accountId?: string){ + const queryClient = useQueryClient(); + + + return useMutation({ + mutationKey: ["updateRaidGroupPermissionsForAccount", raidGroupId, accountId], + mutationFn: async (permission: RaidGroupPermissionType) => { + const response = await api.put(`/account/${accountId}/raidGroup/${raidGroupId}/permission`, { + permission + }); + + if(response.status !== 200){ + throw new Error("Failed to update permissions"); + } + else if(response.data.errors){ + throw new Error(response.data.errors.join(", ")); + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["accounts"] }); + } + }); +} + export function useDeleteAccount(accountId: string){ const queryClient = useQueryClient(); @@ -187,3 +284,26 @@ export function useDeleteAccount(accountId: string){ }); } + +export function UseRemoveAccountFromRaidGroup(raidGroupId?: string, accountId?: string){ + const queryClient = useQueryClient(); + + + return useMutation({ + mutationKey: ["removeAccountFromRaidGroup", raidGroupId, accountId], + mutationFn: async () => { + const response = await api.delete(`/account/raidGroup/${raidGroupId}/permissions/${accountId}`); + + if(response.status !== 200){ + throw new Error("Failed to remove account from raid group"); + } + else if(response.data.errors){ + throw new Error(response.data.errors.join(", ")); + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["accounts"] }); + } + }); + +} diff --git a/src/interface/RaidGroup.ts b/src/interface/RaidGroup.ts index b704046..044e0f0 100644 --- a/src/interface/RaidGroup.ts +++ b/src/interface/RaidGroup.ts @@ -1,3 +1,10 @@ +export enum RaidGroupPermissionType { + ADMIN = "ADMIN", + LEADER = "LEADER", + RAIDER = "RAIDER" +} + + export interface RaidGroup { raidGroupId?: string; gameId: string; diff --git a/src/pages/protected/RaidGroupPage.tsx b/src/pages/protected/RaidGroupPage.tsx index f11906f..5ed229c 100644 --- a/src/pages/protected/RaidGroupPage.tsx +++ b/src/pages/protected/RaidGroupPage.tsx @@ -1,10 +1,12 @@ import TabGroup, { Tab } from "@/components/tab/TabGroup"; import { useGetRaidGroup } from "@/hooks/RaidGroupHooks"; import { RaidGroup } from "@/interface/RaidGroup"; +import RaidGroupAccountsTab from "@/ui/account/RaidGroupAccountsTab"; import RaidGroupCalendarDisplay from "@/ui/calendar/RaidGroupCalendarDisplay"; import RaidGroupHeader from "@/ui/calendar/RaidGroupHeader"; import ClassGroupsTab from "@/ui/classGroup/ClassGroupsTab"; import PersonTab from "@/ui/person/PersonTab"; +import RaidInstancesTab from "@/ui/raidInstances/RaidInstancesTab"; import RaidLayoutTab from "@/ui/raidLayout/RaidLayoutTab"; import { useEffect, useState } from "react"; import { Navigate, useParams } from "react-router"; @@ -55,6 +57,14 @@ export default function RaidGroupPage(){ { tabHeader: "Raid Layout", tabContent: + }, + { + tabHeader: "Raid Instances", + tabContent: + }, + { + tabHeader: "Users", + tabContent: } ]; diff --git a/src/ui/account/AccountsByRaidGroupLoader.tsx b/src/ui/account/AccountsByRaidGroupLoader.tsx new file mode 100644 index 0000000..0344f57 --- /dev/null +++ b/src/ui/account/AccountsByRaidGroupLoader.tsx @@ -0,0 +1,36 @@ +import DangerMessage from "@/components/message/DangerMessage"; +import { useGetAccountsByRaidGroup } from "@/hooks/AccountHooks"; +import { RaidGroup } from "@/interface/RaidGroup"; +import AccountsList from "./AccountsList"; +import AccountsListSkeleton from "./AccountsListSkeleton"; + + +export default function AccountsByRaidGroupLoader({ + page, + pageSize, + searchTerm, + raidGroup +}:{ + page: number; + pageSize: number; + searchTerm?: string; + raidGroup: RaidGroup; +}){ + const accountsQuery = useGetAccountsByRaidGroup(raidGroup.raidGroupId!, page - 1, pageSize, searchTerm); + + + if(accountsQuery.status === "pending"){ + return + } + else if(accountsQuery.status === "error"){ + return Error: {accountsQuery.error.message} + } + else{ + return ( + + ); + } +} diff --git a/src/ui/account/AccountsList.tsx b/src/ui/account/AccountsList.tsx index fd4b021..182271b 100644 --- a/src/ui/account/AccountsList.tsx +++ b/src/ui/account/AccountsList.tsx @@ -1,23 +1,28 @@ import { ButtonProps } from "@/components/button/Button"; import Table from "@/components/table/Table"; import { Account } from "@/interface/Account"; +import { RaidGroup } from "@/interface/RaidGroup"; import moment from "moment"; import { useState } from "react"; import AccountAdminButtons from "./AccountAdminButtons"; import AccountModal from "./modals/AccountModal"; import AccountPasswordRestModal from "./modals/AccountPasswordResetModal"; +import AccountRaidGroupPermissionsModal from "./modals/AccountRaidGroupPermissionsModal"; import DeleteAccountModal from "./modals/DeleteAccountModal"; import ForcePasswordResetModal from "./modals/ForcePasswordResetModal"; +import RemoveAccountFromRaidGroupModal from "./modals/RemoveAccountFromRaidGroupModal"; import RevokeRefreshTokenModal from "./modals/RevokeRefreshTokenModal"; +import RaidGroupAccountAdminButtons from "./RaidGroupAccountAdminButtons"; export interface AccountsListProps { accounts: Account[]; + raidGroup?: RaidGroup; } export default function AccountsList(props: AccountsListProps){ - const { accounts } = props; + const { accounts, raidGroup } = props; const [ selectedAccount, setSelectedAccount ] = useState(undefined); @@ -26,6 +31,8 @@ export default function AccountsList(props: AccountsListProps){ const [ displayRevokeRefreshTokenModal, setDisplayRevokeRefreshTokenModal ] = useState(false); const [ displayAccountModal, setDisplayAccountModal ] = useState(false); const [ displayDeleteAccountModal, setDisplayDeleteAccountModal ] = useState(false); + const [ displayAccountRaidGroupPermissionsModal, setDisplayAccountRaidGroupPermissionsModal ] = useState(false); + const [ displayRemoveAccountFromRaidGroupModal, setDisplayRemoveAccountFromRaidGroupModal ] = useState(false); const buttonProps: ButtonProps = { @@ -109,6 +116,20 @@ export default function AccountsList(props: AccountsListProps){ setDisplayDeleteAccountModal(true); }} /> + { + raidGroup && + { + setSelectedAccount(account); + setDisplayAccountRaidGroupPermissionsModal(true) + }} + showRemoveFromRaidGroupModal={() => { + setSelectedAccount(account); + setDisplayRemoveAccountFromRaidGroupModal(true); + }} + /> + } ]); @@ -144,6 +165,17 @@ export default function AccountsList(props: AccountsListProps){ close={() => {setDisplayDeleteAccountModal(false); setSelectedAccount(undefined);}} account={selectedAccount} /> + {setDisplayAccountRaidGroupPermissionsModal(false); setSelectedAccount(undefined);}} + account={selectedAccount} + raidGroup={raidGroup} + /> + {setDisplayRemoveAccountFromRaidGroupModal(false); setSelectedAccount(undefined);}} + account={selectedAccount} + /> ); } diff --git a/src/ui/account/RaidGroupAccountAdminButtons.tsx b/src/ui/account/RaidGroupAccountAdminButtons.tsx new file mode 100644 index 0000000..e7c173d --- /dev/null +++ b/src/ui/account/RaidGroupAccountAdminButtons.tsx @@ -0,0 +1,38 @@ +import { ButtonProps } from "@/components/button/Button"; +import DangerButton from "@/components/button/DangerButton"; +import WarningButton from "@/components/button/WarningButton"; +import { BsKeyFill, BsTrash3 } from "react-icons/bs"; + + +export default function RaidGroupAccountAdminButtons({ + buttonProps, + showRaidGroupPermissionsModal, + showRemoveFromRaidGroupModal +}:{ + buttonProps: ButtonProps; + showRaidGroupPermissionsModal: () => void; + showRemoveFromRaidGroupModal: () => void; +}){ + return ( +
+ + + + + + +
+ ); +} diff --git a/src/ui/account/RaidGroupAccountsTab.tsx b/src/ui/account/RaidGroupAccountsTab.tsx new file mode 100644 index 0000000..e5c66b1 --- /dev/null +++ b/src/ui/account/RaidGroupAccountsTab.tsx @@ -0,0 +1,90 @@ +import TextInput from "@/components/input/TextInput"; +import Pagination from "@/components/pagination/Pagination"; +import { useGetAccountsByRaidGroupCount } from "@/hooks/AccountHooks"; +import { RaidGroup } from "@/interface/RaidGroup"; +import { useEffect, useState } from "react"; +import { useDebouncedCallback } from "use-debounce"; +import AccountsByRaidGroupLoader from "./AccountsByRaidGroupLoader"; + + +export default function RaidGroupAccountsTab({ + raidGroup +}:{ + raidGroup: RaidGroup; +}){ + const [ page, setPage ] = useState(1); + const [ totalPages, setTotalPages ] = useState(1); + const [ searchTerm, setSearchTerm ] = useState(""); + const [ sentSearchTerm, setSentSearchTerm ] = useState(); + const pageSize = 10; + const modalId = crypto.randomUUID().replaceAll("-", ""); + + + const accountsCountQuery = useGetAccountsByRaidGroupCount(raidGroup.raidGroupId ?? "", sentSearchTerm); + + + const updateSearchTerm = useDebouncedCallback((newSearchTerm: string) => { + setSentSearchTerm(newSearchTerm.length ? newSearchTerm : undefined); + }, 1000); + + + useEffect(() => { + updateSearchTerm(searchTerm ?? ""); + }, [ searchTerm, updateSearchTerm ]); + + + useEffect(() => { + if(accountsCountQuery.data){ + setTotalPages(Math.ceil(accountsCountQuery.data / pageSize)); + } + }, [ accountsCountQuery.data ]); + + + return ( + <> +
+
+   +
+
+   +
+
+
+ setSearchTerm(e.target.value)} + placeholder="Search" + /> +
+
+
+ {/* Account List */} + + {/* Pagination */} +
+ +
+ + ); +} diff --git a/src/ui/account/modals/AccountRaidGroupPermissionsModal.tsx b/src/ui/account/modals/AccountRaidGroupPermissionsModal.tsx new file mode 100644 index 0000000..91c38e7 --- /dev/null +++ b/src/ui/account/modals/AccountRaidGroupPermissionsModal.tsx @@ -0,0 +1,88 @@ +import PrimaryButton from "@/components/button/PrimaryButton"; +import SecondaryButton from "@/components/button/SecondaryButton"; +import RaidBuilderModal from "@/components/modal/RaidBuilderModal"; +import RaidGroupPermissionSelector from "@/components/raidGroup/RaidGroupPermissionSelector"; +import { useGetRaidGroupPermissionsForAccount, useUpdateRaidGroupPermissionsForAccount } from "@/hooks/AccountHooks"; +import { Account } from "@/interface/Account"; +import { RaidGroup, RaidGroupPermissionType } from "@/interface/RaidGroup"; +import { useTimedModal } from "@/providers/TimedModalProvider"; +import { useEffect, useState } from "react"; + + +export default function AccountRaidGroupPermissionsModal({ + display, + close, + account, + raidGroup +}:{ + display: boolean; + close: () => void; + account?: Account; + raidGroup?: RaidGroup; +}){ + const [ currentPermission, setCurrentPermission ] = useState(); + + + const raidGroupPermissionsQuery = useGetRaidGroupPermissionsForAccount(raidGroup?.raidGroupId, account?.accountId); + const raidGroupPermissionsMutate = useUpdateRaidGroupPermissionsForAccount(raidGroup?.raidGroupId, account?.accountId); + const { addSuccessMessage, addErrorMessage } = useTimedModal(); + + useEffect(() => { + if(raidGroupPermissionsQuery.status === "success"){ + setCurrentPermission(raidGroupPermissionsQuery.data); + } + else if(raidGroupPermissionsQuery.status === "error"){ + addErrorMessage(`Error getting raid group permissions: ${raidGroupPermissionsQuery.error.message}`); + } + }, [raidGroupPermissionsQuery.status, raidGroupPermissionsQuery.data, addSuccessMessage, addErrorMessage, raidGroupPermissionsQuery.error?.message]); + + useEffect(() => { + if(raidGroupPermissionsMutate.status === "success"){ + raidGroupPermissionsMutate.reset(); + close(); + addSuccessMessage("Permissions updated successfully"); + } + else if(raidGroupPermissionsMutate.status === "error"){ + raidGroupPermissionsMutate.reset(); + addErrorMessage(`Error updating raid group permissions: ${raidGroupPermissionsMutate.error.message}`); + } + }, [ close, raidGroupPermissionsMutate, raidGroupPermissionsMutate.status, addErrorMessage, addSuccessMessage ]); + + + const updateRaidGroupPermissions = () => { + raidGroupPermissionsMutate.mutate(currentPermission ?? RaidGroupPermissionType.RAIDER); + } + + + return ( + + setCurrentPermission(e.target.value as RaidGroupPermissionType)} + /> + + } + modalFooter={ + <> + + Update + + + Cancel + + + } + /> + ); +} diff --git a/src/ui/account/modals/RemoveAccountFromRaidGroupModal.tsx b/src/ui/account/modals/RemoveAccountFromRaidGroupModal.tsx new file mode 100644 index 0000000..d652f58 --- /dev/null +++ b/src/ui/account/modals/RemoveAccountFromRaidGroupModal.tsx @@ -0,0 +1,66 @@ +import DangerButton from "@/components/button/DangerButton"; +import SecondaryButton from "@/components/button/SecondaryButton"; +import RaidBuilderModal from "@/components/modal/RaidBuilderModal"; +import { UseRemoveAccountFromRaidGroup } from "@/hooks/AccountHooks"; +import { Account } from "@/interface/Account"; +import { RaidGroup } from "@/interface/RaidGroup"; +import { useTimedModal } from "@/providers/TimedModalProvider"; +import { useEffect } from "react"; + + +export default function RemoveAccountFromRaidGroupModal({ + display, + close, + account, + raidGroup +}:{ + display: boolean; + close: () => void; + account?: Account; + raidGroup?: RaidGroup; +}){ + const removeAccountFromRaidGroupQuery = UseRemoveAccountFromRaidGroup(raidGroup?.raidGroupId ?? "", account?.accountId ?? ""); + const { addSuccessMessage, addErrorMessage } = useTimedModal(); + + + const removeAccountFromRaidGroup = () => { + removeAccountFromRaidGroupQuery.mutate(); + } + + useEffect(() => { + if(removeAccountFromRaidGroupQuery.status === "success"){ + removeAccountFromRaidGroupQuery.reset(); + addSuccessMessage(`Successfully removed ${account?.username} from the Raid Group`); + close(); + } + else if(removeAccountFromRaidGroupQuery.status === "error"){ + removeAccountFromRaidGroupQuery.reset(); + addErrorMessage(`Error removing ${account?.username} from the Raid Group: ${removeAccountFromRaidGroupQuery.error.message}`); + console.log(removeAccountFromRaidGroupQuery.error); + } + }); + + + return ( + + + Remove + + + Cancel + + + } + /> + ); +} diff --git a/src/ui/raidInstances/RaidInstancesTab.tsx b/src/ui/raidInstances/RaidInstancesTab.tsx new file mode 100644 index 0000000..4960708 --- /dev/null +++ b/src/ui/raidInstances/RaidInstancesTab.tsx @@ -0,0 +1,7 @@ +export default function RaidInstancesTab(){ + return ( +
+ Raid Instances tab +
+ ); +}