User tab working

This commit is contained in:
2025-03-09 19:49:40 -04:00
parent 8b8538a968
commit 65fb3479fd
11 changed files with 536 additions and 1 deletions

View File

@@ -0,0 +1,41 @@
import { RaidGroupPermissionType } from "@/interface/RaidGroup";
export default function RaidGroupPermissionSelector({
value,
onChange
}:{
value?: RaidGroupPermissionType;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}){
const modalId = crypto.randomUUID().replaceAll("-", "");
return (
<div
className="flex flex-row flex-wrap justify-start gap-x-4"
>
{
Object.keys(RaidGroupPermissionType).map((permissionType) => (
<label
key={permissionType}
className="whitespace-nowrap"
>
<input
type="radio"
name={`raidGroupPermissionTypeSelector${modalId}`}
value={permissionType}
onChange={onChange}
checked={value === permissionType}
/>
<span
className="ml-1"
>
{permissionType}
</span>
</label>
))
}
</div>
);
}

View File

@@ -1,4 +1,5 @@
import { Account } from "@/interface/Account"; import { Account } from "@/interface/Account";
import { RaidGroupPermissionType } from "@/interface/RaidGroup";
import { api } from "@/util/AxiosUtil"; import { api } from "@/util/AxiosUtil";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 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){ export function useGetAccountsCount(searchTerm?: string){
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
if(searchTerm){ 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){ export function useForcePasswordReset(accountId: string){
const queryClient = useQueryClient(); 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){ export function useDeleteAccount(accountId: string){
const queryClient = useQueryClient(); 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"] });
}
});
}

View File

@@ -1,3 +1,10 @@
export enum RaidGroupPermissionType {
ADMIN = "ADMIN",
LEADER = "LEADER",
RAIDER = "RAIDER"
}
export interface RaidGroup { export interface RaidGroup {
raidGroupId?: string; raidGroupId?: string;
gameId: string; gameId: string;

View File

@@ -1,10 +1,12 @@
import TabGroup, { Tab } from "@/components/tab/TabGroup"; import TabGroup, { Tab } from "@/components/tab/TabGroup";
import { useGetRaidGroup } from "@/hooks/RaidGroupHooks"; import { useGetRaidGroup } from "@/hooks/RaidGroupHooks";
import { RaidGroup } from "@/interface/RaidGroup"; import { RaidGroup } from "@/interface/RaidGroup";
import RaidGroupAccountsTab from "@/ui/account/RaidGroupAccountsTab";
import RaidGroupCalendarDisplay from "@/ui/calendar/RaidGroupCalendarDisplay"; import RaidGroupCalendarDisplay from "@/ui/calendar/RaidGroupCalendarDisplay";
import RaidGroupHeader from "@/ui/calendar/RaidGroupHeader"; import RaidGroupHeader from "@/ui/calendar/RaidGroupHeader";
import ClassGroupsTab from "@/ui/classGroup/ClassGroupsTab"; import ClassGroupsTab from "@/ui/classGroup/ClassGroupsTab";
import PersonTab from "@/ui/person/PersonTab"; import PersonTab from "@/ui/person/PersonTab";
import RaidInstancesTab from "@/ui/raidInstances/RaidInstancesTab";
import RaidLayoutTab from "@/ui/raidLayout/RaidLayoutTab"; import RaidLayoutTab from "@/ui/raidLayout/RaidLayoutTab";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Navigate, useParams } from "react-router"; import { Navigate, useParams } from "react-router";
@@ -55,6 +57,14 @@ export default function RaidGroupPage(){
{ {
tabHeader: "Raid Layout", tabHeader: "Raid Layout",
tabContent: <RaidLayoutTab raidGroup={raidGroup}/> tabContent: <RaidLayoutTab raidGroup={raidGroup}/>
},
{
tabHeader: "Raid Instances",
tabContent: <RaidInstancesTab/>
},
{
tabHeader: "Users",
tabContent: <RaidGroupAccountsTab raidGroup={raidGroup}/>
} }
]; ];

View File

@@ -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 <AccountsListSkeleton/>
}
else if(accountsQuery.status === "error"){
return <DangerMessage>Error: {accountsQuery.error.message}</DangerMessage>
}
else{
return (
<AccountsList
accounts={accountsQuery.data ?? []}
raidGroup={raidGroup}
/>
);
}
}

View File

@@ -1,23 +1,28 @@
import { ButtonProps } from "@/components/button/Button"; import { ButtonProps } from "@/components/button/Button";
import Table from "@/components/table/Table"; import Table from "@/components/table/Table";
import { Account } from "@/interface/Account"; import { Account } from "@/interface/Account";
import { RaidGroup } from "@/interface/RaidGroup";
import moment from "moment"; import moment from "moment";
import { useState } from "react"; import { useState } from "react";
import AccountAdminButtons from "./AccountAdminButtons"; import AccountAdminButtons from "./AccountAdminButtons";
import AccountModal from "./modals/AccountModal"; import AccountModal from "./modals/AccountModal";
import AccountPasswordRestModal from "./modals/AccountPasswordResetModal"; import AccountPasswordRestModal from "./modals/AccountPasswordResetModal";
import AccountRaidGroupPermissionsModal from "./modals/AccountRaidGroupPermissionsModal";
import DeleteAccountModal from "./modals/DeleteAccountModal"; import DeleteAccountModal from "./modals/DeleteAccountModal";
import ForcePasswordResetModal from "./modals/ForcePasswordResetModal"; import ForcePasswordResetModal from "./modals/ForcePasswordResetModal";
import RemoveAccountFromRaidGroupModal from "./modals/RemoveAccountFromRaidGroupModal";
import RevokeRefreshTokenModal from "./modals/RevokeRefreshTokenModal"; import RevokeRefreshTokenModal from "./modals/RevokeRefreshTokenModal";
import RaidGroupAccountAdminButtons from "./RaidGroupAccountAdminButtons";
export interface AccountsListProps { export interface AccountsListProps {
accounts: Account[]; accounts: Account[];
raidGroup?: RaidGroup;
} }
export default function AccountsList(props: AccountsListProps){ export default function AccountsList(props: AccountsListProps){
const { accounts } = props; const { accounts, raidGroup } = props;
const [ selectedAccount, setSelectedAccount ] = useState<Account | undefined>(undefined); const [ selectedAccount, setSelectedAccount ] = useState<Account | undefined>(undefined);
@@ -26,6 +31,8 @@ export default function AccountsList(props: AccountsListProps){
const [ displayRevokeRefreshTokenModal, setDisplayRevokeRefreshTokenModal ] = useState(false); const [ displayRevokeRefreshTokenModal, setDisplayRevokeRefreshTokenModal ] = useState(false);
const [ displayAccountModal, setDisplayAccountModal ] = useState(false); const [ displayAccountModal, setDisplayAccountModal ] = useState(false);
const [ displayDeleteAccountModal, setDisplayDeleteAccountModal ] = useState(false); const [ displayDeleteAccountModal, setDisplayDeleteAccountModal ] = useState(false);
const [ displayAccountRaidGroupPermissionsModal, setDisplayAccountRaidGroupPermissionsModal ] = useState(false);
const [ displayRemoveAccountFromRaidGroupModal, setDisplayRemoveAccountFromRaidGroupModal ] = useState(false);
const buttonProps: ButtonProps = { const buttonProps: ButtonProps = {
@@ -109,6 +116,20 @@ export default function AccountsList(props: AccountsListProps){
setDisplayDeleteAccountModal(true); setDisplayDeleteAccountModal(true);
}} }}
/> />
{
raidGroup &&
<RaidGroupAccountAdminButtons
buttonProps={buttonProps}
showRaidGroupPermissionsModal={() => {
setSelectedAccount(account);
setDisplayAccountRaidGroupPermissionsModal(true)
}}
showRemoveFromRaidGroupModal={() => {
setSelectedAccount(account);
setDisplayRemoveAccountFromRaidGroupModal(true);
}}
/>
}
</div> </div>
]); ]);
@@ -144,6 +165,17 @@ export default function AccountsList(props: AccountsListProps){
close={() => {setDisplayDeleteAccountModal(false); setSelectedAccount(undefined);}} close={() => {setDisplayDeleteAccountModal(false); setSelectedAccount(undefined);}}
account={selectedAccount} account={selectedAccount}
/> />
<AccountRaidGroupPermissionsModal
display={displayAccountRaidGroupPermissionsModal}
close={() => {setDisplayAccountRaidGroupPermissionsModal(false); setSelectedAccount(undefined);}}
account={selectedAccount}
raidGroup={raidGroup}
/>
<RemoveAccountFromRaidGroupModal
display={displayRemoveAccountFromRaidGroupModal}
close={() => {setDisplayRemoveAccountFromRaidGroupModal(false); setSelectedAccount(undefined);}}
account={selectedAccount}
/>
</> </>
); );
} }

View File

@@ -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 (
<div
className="flex flex-row gap-2"
>
<WarningButton
{...buttonProps}
onClick={showRaidGroupPermissionsModal}
>
<BsKeyFill
size={22}
/>
</WarningButton>
<DangerButton
{...buttonProps}
onClick={showRemoveFromRaidGroupModal}
>
<BsTrash3
size={22}
/>
</DangerButton>
</div>
);
}

View File

@@ -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<string>();
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 (
<>
<div
className="flex flex-row items-center justify-between w-full"
>
<div
className="flex flex-row items-center justify-center w-full"
>
&nbsp;
</div>
<div
className="flex flex-row items-center justify-end w-full"
>
&nbsp;
</div>
<div
className="flex flex-row items-center justify-end w-full"
>
<div>
<TextInput
id={`accountSearchBox${modalId}`}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search"
/>
</div>
</div>
</div>
{/* Account List */}
<AccountsByRaidGroupLoader
page={page}
pageSize={pageSize}
searchTerm={searchTerm}
raidGroup={raidGroup}
/>
{/* Pagination */}
<div
className="my-12"
>
<Pagination
currentPage={page}
totalPages={totalPages}
onChange={setPage}
/>
</div>
</>
);
}

View File

@@ -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<RaidGroupPermissionType>();
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 (
<RaidBuilderModal
display={display}
close={close}
modalHeader={`Raid Group Permissions for ${account?.username}`}
modalBody={
<div
className="flex flex-col items-center justify-center gap-4"
>
<RaidGroupPermissionSelector
value={currentPermission}
onChange={(e) => setCurrentPermission(e.target.value as RaidGroupPermissionType)}
/>
</div>
}
modalFooter={
<>
<PrimaryButton
onClick={updateRaidGroupPermissions}
>
Update
</PrimaryButton>
<SecondaryButton
onClick={close}
>
Cancel
</SecondaryButton>
</>
}
/>
);
}

View File

@@ -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 (
<RaidBuilderModal
display={display}
close={close}
modalHeader="Remove Account From Raid Group"
modalBody={`Are you sure you want to remove ${account?.username} from the Raid Group?`}
modalFooter={
<>
<DangerButton
onClick={removeAccountFromRaidGroup}
>
Remove
</DangerButton>
<SecondaryButton
onClick={close}
>
Cancel
</SecondaryButton>
</>
}
/>
);
}

View File

@@ -0,0 +1,7 @@
export default function RaidInstancesTab(){
return (
<div>
Raid Instances tab
</div>
);
}