mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
feat(ui): reset a team member's spend from the Members tab
Every member now carries a membership row, so a member who spent with no budget is over budget the moment a member budget is added later. The only fix was POST /team/{team_id}/member/{user_id}/reset_spend, which had no UI.
The Members tab gets a Reset spend action on rows that have current cycle spend. It confirms in a dialog, posts reset_to 0 through the typed client, and refreshes the team without remounting the page so the tab stays open. A team admin does not see it on their own row because the backend rejects that reset
This commit is contained in:
parent
61d4c5b9b5
commit
c21e86a443
6 changed files with 234 additions and 20 deletions
|
|
@ -0,0 +1,17 @@
|
|||
import { useMutation } from "@tanstack/react-query";
|
||||
import { fetchClient } from "@/lib/http/api";
|
||||
|
||||
export interface ResetTeamMemberSpendParams {
|
||||
teamId: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export const resetTeamMemberSpend = async ({ teamId, userId }: ResetTeamMemberSpendParams): Promise<void> => {
|
||||
await fetchClient.POST("/team/{team_id}/member/{user_id}/reset_spend", {
|
||||
params: { path: { team_id: teamId, user_id: userId } },
|
||||
body: { reset_to: 0 },
|
||||
});
|
||||
};
|
||||
|
||||
export const useResetTeamMemberSpend = () =>
|
||||
useMutation<void, Error, ResetTeamMemberSpendParams>({ mutationFn: resetTeamMemberSpend });
|
||||
|
|
@ -30,6 +30,7 @@ export const TableIconActionButtonMap: Record<string, TableIconActionButtonBaseP
|
|||
Delete: { icon: TrashIcon, className: "hover:text-destructive" },
|
||||
Test: { icon: PlayIcon, className: "hover:text-info" },
|
||||
Regenerate: { icon: RefreshIcon, className: "hover:text-success" },
|
||||
Reset: { icon: RefreshIcon, className: "hover:text-info" },
|
||||
Up: { icon: ChevronUpIcon, className: "hover:text-info" },
|
||||
Down: { icon: ChevronDownIcon, className: "hover:text-info" },
|
||||
Open: { icon: ExternalLinkIcon, className: "hover:text-success" },
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ export interface MemberTableProps {
|
|||
roleTooltip?: string;
|
||||
extraColumns?: MemberTableColumn[];
|
||||
showDeleteForMember?: (member: Member) => boolean;
|
||||
onResetSpend?: (member: Member) => void;
|
||||
showResetSpendForMember?: (member: Member) => boolean;
|
||||
emptyText?: string;
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +75,8 @@ interface MemberColumnDeps {
|
|||
roleTooltip?: string;
|
||||
extraColumns: MemberTableColumn[];
|
||||
showDeleteForMember?: (member: Member) => boolean;
|
||||
onResetSpend?: (member: Member) => void;
|
||||
showResetSpendForMember?: (member: Member) => boolean;
|
||||
}
|
||||
|
||||
const extraColumnDef = (column: MemberTableColumn): ColumnDef<Member> => {
|
||||
|
|
@ -105,6 +109,8 @@ const buildColumns = ({
|
|||
roleTooltip,
|
||||
extraColumns,
|
||||
showDeleteForMember,
|
||||
onResetSpend,
|
||||
showResetSpendForMember,
|
||||
}: MemberColumnDeps): ColumnDef<Member>[] => [
|
||||
{
|
||||
id: "user_alias",
|
||||
|
|
@ -173,6 +179,14 @@ const buildColumns = ({
|
|||
dataTestId="edit-member"
|
||||
onClick={() => onEdit(row.original)}
|
||||
/>
|
||||
{onResetSpend && (showResetSpendForMember?.(row.original) ?? true) && (
|
||||
<TableIconActionButton
|
||||
variant="Reset"
|
||||
tooltipText="Reset spend"
|
||||
dataTestId="reset-member-spend"
|
||||
onClick={() => onResetSpend(row.original)}
|
||||
/>
|
||||
)}
|
||||
{(!showDeleteForMember || showDeleteForMember(row.original)) && (
|
||||
<TableIconActionButton
|
||||
variant="Delete"
|
||||
|
|
@ -196,6 +210,8 @@ export default function MemberTable({
|
|||
roleTooltip,
|
||||
extraColumns = [],
|
||||
showDeleteForMember,
|
||||
onResetSpend,
|
||||
showResetSpendForMember,
|
||||
emptyText,
|
||||
}: MemberTableProps) {
|
||||
const [globalFilter, setGlobalFilter] = useState("");
|
||||
|
|
@ -210,6 +226,8 @@ export default function MemberTable({
|
|||
roleTooltip,
|
||||
extraColumns,
|
||||
showDeleteForMember,
|
||||
onResetSpend,
|
||||
showResetSpendForMember,
|
||||
};
|
||||
const columns = buildColumns(columnDeps);
|
||||
const roleFilterItems = [
|
||||
|
|
|
|||
|
|
@ -678,6 +678,15 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const refreshTeamData = async () => {
|
||||
if (!accessToken) return;
|
||||
try {
|
||||
setTeamData(await teamInfoCall(accessToken, teamId));
|
||||
} catch {
|
||||
toast.fromError("Failed to load team information");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTeamInfo();
|
||||
}, [teamId, accessToken]);
|
||||
|
|
@ -1351,6 +1360,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
teamData={teamData}
|
||||
canEditTeam={canEditTeam}
|
||||
handleMemberDelete={handleMemberDelete}
|
||||
onMemberSpendReset={refreshTeamData}
|
||||
setSelectedEditMember={setSelectedEditMember}
|
||||
setIsEditMemberModalVisible={setIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={setIsAddMemberModalVisible}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { fireEvent, screen, within } from "@testing-library/react";
|
||||
import { fireEvent, screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
|
|
@ -13,6 +13,9 @@ vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
|
|||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
const { POST } = vi.hoisted(() => ({ POST: vi.fn() }));
|
||||
vi.mock("@/lib/http/api", () => ({ fetchClient: { POST } }));
|
||||
|
||||
vi.mock("@/utils/roles", () => ({
|
||||
isUserTeamAdminForSingleTeam: vi.fn(() => false),
|
||||
isProxyAdminRole: vi.fn(() => false),
|
||||
|
|
@ -26,6 +29,7 @@ const mockHandleMemberDelete = vi.fn();
|
|||
const mockSetSelectedEditMember = vi.fn();
|
||||
const mockSetIsEditMemberModalVisible = vi.fn();
|
||||
const mockSetIsAddMemberModalVisible = vi.fn();
|
||||
const mockOnMemberSpendReset = vi.fn();
|
||||
|
||||
const budgetResetIso = new Date(2026, 6, 15, 12, 0, 0).toISOString();
|
||||
|
||||
|
|
@ -121,6 +125,7 @@ describe("TeamMembersComponent", () => {
|
|||
teamData={createMockTeamData()}
|
||||
canEditTeam={false}
|
||||
handleMemberDelete={mockHandleMemberDelete}
|
||||
onMemberSpendReset={mockOnMemberSpendReset}
|
||||
setSelectedEditMember={mockSetSelectedEditMember}
|
||||
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
|
||||
|
|
@ -136,6 +141,7 @@ describe("TeamMembersComponent", () => {
|
|||
teamData={createMockTeamData()}
|
||||
canEditTeam={false}
|
||||
handleMemberDelete={mockHandleMemberDelete}
|
||||
onMemberSpendReset={mockOnMemberSpendReset}
|
||||
setSelectedEditMember={mockSetSelectedEditMember}
|
||||
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
|
||||
|
|
@ -154,6 +160,7 @@ describe("TeamMembersComponent", () => {
|
|||
teamData={createMockTeamData()}
|
||||
canEditTeam={false}
|
||||
handleMemberDelete={mockHandleMemberDelete}
|
||||
onMemberSpendReset={mockOnMemberSpendReset}
|
||||
setSelectedEditMember={mockSetSelectedEditMember}
|
||||
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
|
||||
|
|
@ -172,6 +179,7 @@ describe("TeamMembersComponent", () => {
|
|||
const props = {
|
||||
canEditTeam: false,
|
||||
handleMemberDelete: mockHandleMemberDelete,
|
||||
onMemberSpendReset: mockOnMemberSpendReset,
|
||||
setSelectedEditMember: mockSetSelectedEditMember,
|
||||
setIsEditMemberModalVisible: mockSetIsEditMemberModalVisible,
|
||||
setIsAddMemberModalVisible: mockSetIsAddMemberModalVisible,
|
||||
|
|
@ -195,6 +203,7 @@ describe("TeamMembersComponent", () => {
|
|||
teamData={createMockTeamData()}
|
||||
canEditTeam={true}
|
||||
handleMemberDelete={mockHandleMemberDelete}
|
||||
onMemberSpendReset={mockOnMemberSpendReset}
|
||||
setSelectedEditMember={mockSetSelectedEditMember}
|
||||
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
|
||||
|
|
@ -221,6 +230,7 @@ describe("TeamMembersComponent", () => {
|
|||
})}
|
||||
canEditTeam={false}
|
||||
handleMemberDelete={mockHandleMemberDelete}
|
||||
onMemberSpendReset={mockOnMemberSpendReset}
|
||||
setSelectedEditMember={mockSetSelectedEditMember}
|
||||
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
|
||||
|
|
@ -247,6 +257,7 @@ describe("TeamMembersComponent", () => {
|
|||
})}
|
||||
canEditTeam={false}
|
||||
handleMemberDelete={mockHandleMemberDelete}
|
||||
onMemberSpendReset={mockOnMemberSpendReset}
|
||||
setSelectedEditMember={mockSetSelectedEditMember}
|
||||
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
|
||||
|
|
@ -262,6 +273,7 @@ describe("TeamMembersComponent", () => {
|
|||
teamData={createMockTeamData()}
|
||||
canEditTeam={false}
|
||||
handleMemberDelete={mockHandleMemberDelete}
|
||||
onMemberSpendReset={mockOnMemberSpendReset}
|
||||
setSelectedEditMember={mockSetSelectedEditMember}
|
||||
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
|
||||
|
|
@ -280,6 +292,7 @@ describe("TeamMembersComponent", () => {
|
|||
teamData={createMockTeamData()}
|
||||
canEditTeam={false}
|
||||
handleMemberDelete={mockHandleMemberDelete}
|
||||
onMemberSpendReset={mockOnMemberSpendReset}
|
||||
setSelectedEditMember={mockSetSelectedEditMember}
|
||||
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
|
||||
|
|
@ -295,6 +308,7 @@ describe("TeamMembersComponent", () => {
|
|||
teamData={createMockTeamData()}
|
||||
canEditTeam={false}
|
||||
handleMemberDelete={mockHandleMemberDelete}
|
||||
onMemberSpendReset={mockOnMemberSpendReset}
|
||||
setSelectedEditMember={mockSetSelectedEditMember}
|
||||
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
|
||||
|
|
@ -311,6 +325,7 @@ describe("TeamMembersComponent", () => {
|
|||
teamData={createMockTeamData()}
|
||||
canEditTeam={false}
|
||||
handleMemberDelete={mockHandleMemberDelete}
|
||||
onMemberSpendReset={mockOnMemberSpendReset}
|
||||
setSelectedEditMember={mockSetSelectedEditMember}
|
||||
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
|
||||
|
|
@ -330,6 +345,7 @@ describe("TeamMembersComponent", () => {
|
|||
teamData={createMockTeamData()}
|
||||
canEditTeam={true}
|
||||
handleMemberDelete={mockHandleMemberDelete}
|
||||
onMemberSpendReset={mockOnMemberSpendReset}
|
||||
setSelectedEditMember={mockSetSelectedEditMember}
|
||||
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
|
||||
|
|
@ -364,6 +380,7 @@ describe("TeamMembersComponent", () => {
|
|||
teamData={teamData}
|
||||
canEditTeam={true}
|
||||
handleMemberDelete={mockHandleMemberDelete}
|
||||
onMemberSpendReset={mockOnMemberSpendReset}
|
||||
setSelectedEditMember={mockSetSelectedEditMember}
|
||||
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
|
||||
|
|
@ -417,6 +434,7 @@ describe("TeamMembersComponent", () => {
|
|||
teamData={createMockTeamData()}
|
||||
canEditTeam={true}
|
||||
handleMemberDelete={mockHandleMemberDelete}
|
||||
onMemberSpendReset={mockOnMemberSpendReset}
|
||||
setSelectedEditMember={mockSetSelectedEditMember}
|
||||
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
|
||||
|
|
@ -447,6 +465,7 @@ describe("TeamMembersComponent", () => {
|
|||
teamData={createMockTeamData()}
|
||||
canEditTeam={true}
|
||||
handleMemberDelete={mockHandleMemberDelete}
|
||||
onMemberSpendReset={mockOnMemberSpendReset}
|
||||
setSelectedEditMember={mockSetSelectedEditMember}
|
||||
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
|
||||
|
|
@ -466,6 +485,7 @@ describe("TeamMembersComponent", () => {
|
|||
teamData={createMockTeamData()}
|
||||
canEditTeam={true}
|
||||
handleMemberDelete={mockHandleMemberDelete}
|
||||
onMemberSpendReset={mockOnMemberSpendReset}
|
||||
setSelectedEditMember={mockSetSelectedEditMember}
|
||||
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
|
||||
|
|
@ -482,6 +502,7 @@ describe("TeamMembersComponent", () => {
|
|||
teamData={createMockTeamData()}
|
||||
canEditTeam={false}
|
||||
handleMemberDelete={mockHandleMemberDelete}
|
||||
onMemberSpendReset={mockOnMemberSpendReset}
|
||||
setSelectedEditMember={mockSetSelectedEditMember}
|
||||
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
|
||||
|
|
@ -491,4 +512,95 @@ describe("TeamMembersComponent", () => {
|
|||
expect(screen.queryByTestId("edit-member")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("delete-member")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("reset spend", () => {
|
||||
const renderEditableTab = () =>
|
||||
renderWithProviders(
|
||||
<TeamMembersComponent
|
||||
teamData={createMockTeamData()}
|
||||
canEditTeam={true}
|
||||
handleMemberDelete={mockHandleMemberDelete}
|
||||
onMemberSpendReset={mockOnMemberSpendReset}
|
||||
setSelectedEditMember={mockSetSelectedEditMember}
|
||||
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
|
||||
/>,
|
||||
);
|
||||
|
||||
it("resets the member's current cycle spend to $0 after confirming, then refreshes the team", async () => {
|
||||
const user = userEvent.setup();
|
||||
POST.mockResolvedValue({ data: {} });
|
||||
renderEditableTab();
|
||||
|
||||
const memberRow = screen.getByRole("row", { name: /user1@test\.com/ });
|
||||
await user.click(within(memberRow).getByTestId("reset-member-spend"));
|
||||
|
||||
const dialog = await screen.findByRole("dialog", { name: "Reset Team Member Spend" });
|
||||
expect(dialog).toHaveTextContent("user1@test.com");
|
||||
expect(dialog).toHaveTextContent("$100.5000");
|
||||
expect(POST).not.toHaveBeenCalled();
|
||||
|
||||
await user.click(within(dialog).getByRole("button", { name: "Reset" }));
|
||||
|
||||
await waitFor(() => expect(mockOnMemberSpendReset).toHaveBeenCalledTimes(1));
|
||||
expect(POST).toHaveBeenCalledExactlyOnceWith("/team/{team_id}/member/{user_id}/reset_spend", {
|
||||
params: { path: { team_id: "team-123", user_id: "user1@test.com" } },
|
||||
body: { reset_to: 0 },
|
||||
});
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps the dialog open and does not refresh the team when the reset fails", async () => {
|
||||
const user = userEvent.setup();
|
||||
POST.mockRejectedValue(new Error("Cannot reset your own spend. Ask a proxy admin."));
|
||||
renderEditableTab();
|
||||
|
||||
await user.click(screen.getByTestId("reset-member-spend"));
|
||||
const dialog = await screen.findByRole("dialog", { name: "Reset Team Member Spend" });
|
||||
await user.click(within(dialog).getByRole("button", { name: "Reset" }));
|
||||
|
||||
await waitFor(() => expect(POST).toHaveBeenCalledTimes(1));
|
||||
expect(mockOnMemberSpendReset).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole("dialog", { name: "Reset Team Member Spend" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not call the API when the dialog is cancelled", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderEditableTab();
|
||||
|
||||
await user.click(screen.getByTestId("reset-member-spend"));
|
||||
const dialog = await screen.findByRole("dialog", { name: "Reset Team Member Spend" });
|
||||
await user.click(within(dialog).getByRole("button", { name: "Cancel" }));
|
||||
|
||||
await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
|
||||
expect(POST).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("only offers the reset on members that have current cycle spend", () => {
|
||||
renderEditableTab();
|
||||
|
||||
expect(
|
||||
within(screen.getByRole("row", { name: /user1@test\.com/ })).getByTestId("reset-member-spend"),
|
||||
).toBeVisible();
|
||||
expect(
|
||||
within(screen.getByRole("row", { name: /user2@test\.com/ })).queryByTestId("reset-member-spend"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides the reset on the caller's own row for a team admin, since the backend rejects it", () => {
|
||||
vi.mocked(useAuthorized).mockReturnValue({ userId: "user1@test.com", userRole: "Internal User" } as never);
|
||||
vi.mocked(isProxyAdminRole).mockReturnValue(false);
|
||||
renderEditableTab();
|
||||
|
||||
expect(screen.queryByTestId("reset-member-spend")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the reset on the caller's own row for a proxy admin", () => {
|
||||
vi.mocked(useAuthorized).mockReturnValue({ userId: "user1@test.com", userRole: "Admin" } as never);
|
||||
vi.mocked(isProxyAdminRole).mockReturnValue(true);
|
||||
renderEditableTab();
|
||||
|
||||
expect(screen.getByTestId("reset-member-spend")).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
import { useResetTeamMemberSpend } from "@/app/(dashboard)/hooks/teams/useResetTeamMemberSpend";
|
||||
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { SimpleTooltip } from "@/components/ui/tooltip";
|
||||
import MemberTable from "@/components/common_components/MemberTable";
|
||||
import { Member } from "@/components/networking";
|
||||
import { parseErrorMessage } from "@/components/shared/errorUtils";
|
||||
import { DateCell, MoneyCell } from "@/components/shared/table_cells";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { isProxyAdminRole, isUserTeamAdminForSingleTeam } from "@/utils/roles";
|
||||
import { CircleHelp } from "lucide-react";
|
||||
import type { ComponentProps } from "react";
|
||||
import { useState, type ComponentProps } from "react";
|
||||
import { TeamData, TeamMembership } from "./TeamInfo";
|
||||
|
||||
export const seedMemberBudgetFields = (
|
||||
|
|
@ -31,6 +36,7 @@ interface TeamMemberTabProps {
|
|||
setSelectedEditMember: (member: Member) => void;
|
||||
setIsEditMemberModalVisible: (visible: boolean) => void;
|
||||
setIsAddMemberModalVisible: (visible: boolean) => void;
|
||||
onMemberSpendReset: () => void;
|
||||
}
|
||||
|
||||
export default function TeamMemberTab({
|
||||
|
|
@ -40,7 +46,11 @@ export default function TeamMemberTab({
|
|||
setSelectedEditMember,
|
||||
setIsEditMemberModalVisible,
|
||||
setIsAddMemberModalVisible,
|
||||
onMemberSpendReset,
|
||||
}: TeamMemberTabProps) {
|
||||
const [memberToResetSpend, setMemberToResetSpend] = useState<Member | null>(null);
|
||||
const { mutate: resetMemberSpend, isPending: isResettingSpend } = useResetTeamMemberSpend();
|
||||
|
||||
const formatNumber = (value: number | null): string => {
|
||||
if (value === null || value === undefined) return "0";
|
||||
|
||||
|
|
@ -199,24 +209,70 @@ export default function TeamMemberTab({
|
|||
},
|
||||
];
|
||||
|
||||
const handleResetSpend = () => {
|
||||
if (!memberToResetSpend?.user_id) return;
|
||||
resetMemberSpend(
|
||||
{ teamId: teamData.team_id, userId: memberToResetSpend.user_id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success("Team member spend reset to $0");
|
||||
setMemberToResetSpend(null);
|
||||
onMemberSpendReset();
|
||||
},
|
||||
onError: (error) => toast.fromError(parseErrorMessage(error)),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<MemberTable
|
||||
key={teamData.team_id}
|
||||
members={teamData.team_info.members_with_roles}
|
||||
canEdit={canEditTeam}
|
||||
onEdit={(record) => {
|
||||
const membership = teamData.team_memberships.find((tm) => tm.user_id === record.user_id);
|
||||
setSelectedEditMember(seedMemberBudgetFields(record, membership?.litellm_budget_table));
|
||||
setIsEditMemberModalVisible(true);
|
||||
}}
|
||||
onDelete={handleMemberDelete}
|
||||
onAddMember={() => setIsAddMemberModalVisible(true)}
|
||||
roleColumnTitle="Team Role"
|
||||
roleTooltip="This role applies only to this team and is independent from the user's proxy-level role."
|
||||
extraColumns={extraColumns}
|
||||
showDeleteForMember={() =>
|
||||
isProxyAdmin || (canEditTeam && !isUserTeamAdmin) || (isUserTeamAdmin && !disableTeamAdminDeleteTeamUser)
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<MemberTable
|
||||
key={teamData.team_id}
|
||||
members={teamData.team_info.members_with_roles}
|
||||
canEdit={canEditTeam}
|
||||
onEdit={(record) => {
|
||||
const membership = teamData.team_memberships.find((tm) => tm.user_id === record.user_id);
|
||||
setSelectedEditMember(seedMemberBudgetFields(record, membership?.litellm_budget_table));
|
||||
setIsEditMemberModalVisible(true);
|
||||
}}
|
||||
onDelete={handleMemberDelete}
|
||||
onAddMember={() => setIsAddMemberModalVisible(true)}
|
||||
roleColumnTitle="Team Role"
|
||||
roleTooltip="This role applies only to this team and is independent from the user's proxy-level role."
|
||||
extraColumns={extraColumns}
|
||||
showDeleteForMember={() =>
|
||||
isProxyAdmin || (canEditTeam && !isUserTeamAdmin) || (isUserTeamAdmin && !disableTeamAdminDeleteTeamUser)
|
||||
}
|
||||
onResetSpend={setMemberToResetSpend}
|
||||
showResetSpendForMember={(record) =>
|
||||
getUserCurrentCycleSpend(record.user_id) > 0 && (isProxyAdmin || record.user_id !== userId)
|
||||
}
|
||||
/>
|
||||
<Dialog open={memberToResetSpend !== null} onOpenChange={(open) => !open && setMemberToResetSpend(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Reset Team Member Spend</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p>
|
||||
Reset current cycle spend for{" "}
|
||||
<strong>{memberToResetSpend?.user_email || memberToResetSpend?.user_id}</strong> in this team to{" "}
|
||||
<strong>$0</strong>?
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Current cycle spend:{" "}
|
||||
<strong>${formatNumberWithCommas(getUserCurrentCycleSpend(memberToResetSpend?.user_id ?? null), 4)}</strong>
|
||||
. This is the value checked against the member's budget. Total spend and logs are preserved.
|
||||
</p>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setMemberToResetSpend(null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleResetSpend} disabled={isResettingSpend}>
|
||||
Reset
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue