Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 50 additions & 2 deletions src/components/Profile/ProfileView.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { cn } from "@/lib/utils";
import { RoleList } from "./RoleList";
import { ExternalLinks } from "../Socials/ExternalLinks";
import { ImageUpdate } from "./ImageUpdate";
import { UserAvatar } from "./UserAvatar";
import { UpdateName } from "./UpdateName";
import { getOfficerQuery, getOfficerByIdQuery } from "@/queries/officer";
import { useQuery } from "@tanstack/react-query";
import {
getOfficerQuery,
getOfficerByIdQuery,
updateOfficerStatusMutation,
} from "@/queries/officer";
import { useMutation, useQuery } from "@tanstack/react-query";
import { Spinner } from "../Spinner";
import { isExecutive } from "@/lib/admin";
import { Button } from "../ui/button";

type Props = {
officerId?: string;
Expand All @@ -17,6 +24,11 @@ export function ProfileView({ officerId, editable = false }: Props) {
officerId ? getOfficerByIdQuery(officerId) : getOfficerQuery
);

const { data: viewer } = useQuery(getOfficerQuery);
const isViewerExecutive = viewer ? isExecutive(viewer) : false;

const { mutate: updateStatus } = useMutation(updateOfficerStatusMutation);

if (isLoading) {
return <Spinner />;
}
Expand Down Expand Up @@ -57,6 +69,42 @@ export function ProfileView({ officerId, editable = false }: Props) {
<div className="flex flex-wrap justify-center gap-2 text-sm text-white/70">
<RoleList roles={officer.roles} showAll />
</div>

<div className="flex items-center gap-3 pt-2">
<div
className={cn(
"flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium transition-colors",
officer.isActive
? "border-green-500/20 bg-green-500/10 text-green-400"
: "border-red-500/20 bg-red-500/10 text-red-400"
)}
>
<div
className={cn(
"h-1.5 w-1.5 rounded-full shadow-[0_0_8px]",
officer.isActive
? "bg-green-400 shadow-green-500/50"
: "bg-red-400 shadow-red-500/50"
)}
/>
{officer.isActive ? "Active" : "Inactive"}
</div>
{isViewerExecutive && officerId && (
<Button
variant="ghost"
size="sm"
className="h-7 rounded-full border border-white/10 px-3 text-xs text-white/60 hover:bg-white/10 hover:text-white"
onClick={() =>
updateStatus({
officerId: officer.id,
isActive: !officer.isActive,
})
}
>
{officer.isActive ? "Deactivate" : "Activate"}
</Button>
)}
</div>
</div>

<div className="flex flex-col gap-4 pt-6">
Expand Down
23 changes: 23 additions & 0 deletions src/functions/officer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { auth } from "@/lib/firebase";
import { OfficerSchema, type Officer } from "@/schemas/officer";
import { fetchWithAuth } from "./fetch";
import { isExecutive } from "@/lib/admin";

export async function getCurrentOfficer(): Promise<Officer | null> {
const idToken = await auth.currentUser?.getIdToken();
Expand Down Expand Up @@ -98,6 +99,28 @@ export async function updateAcademicInfo(
return OfficerSchema.parse(res);
}

export async function updateOfficerStatus({
officerId,
isActive,
}: {
officerId: string;
isActive: boolean;
}): Promise<Officer> {
const currentUser = await getCurrentOfficer();
if (!currentUser) throw new Error("Current user not found");
if (!isExecutive(currentUser)) throw new Error("Unauthorized");

const officer = await fetchWithAuth(`/updateOfficer?id=${officerId}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ isActive }),
});
const res = await officer.json();
return OfficerSchema.parse(res);
}

export async function getAllOfficers(): Promise<Officer[]> {
const officers = await fetchWithAuth(`/getOfficers`, {
method: "GET",
Expand Down
8 changes: 8 additions & 0 deletions src/lib/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,11 @@ export function isAdmin(account: GetAdminLevel) {
return account.level > 1;
}
}

export function isExecutive(account: GetAdminLevel) {
if ("accessLevel" in account) {
return account.accessLevel > 2;
} else {
return account.level > 2;
}
}
22 changes: 19 additions & 3 deletions src/lib/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "@tanstack/match-sorter-utils";
import { RoleList } from "@/components/Profile/RoleList";
import { Link } from "@tanstack/react-router";
import { cn } from "@/lib/utils";

export const divisions = [
"All",
Expand Down Expand Up @@ -172,9 +173,24 @@ export const columns = [
header: () => <span className="text-white/70">Active</span>,
cell: ({ row }) => (
<div className="w-[160px] min-w-[160px]">
<span className="text-white">
{row.original.isActive ? "🟢" : "🔴"}
</span>
<div
className={cn(
"inline-flex items-center gap-2 rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors",
row.original.isActive
? "border-green-500/20 bg-green-500/10 text-green-400"
: "border-red-500/20 bg-red-500/10 text-red-400"
)}
>
<div
className={cn(
"h-1.5 w-1.5 rounded-full shadow-[0_0_8px]",
row.original.isActive
? "bg-green-400 shadow-green-500/50"
: "bg-red-400 shadow-red-500/50"
)}
/>
{row.original.isActive ? "Active" : "Inactive"}
</div>
</div>
),
}),
Expand Down
12 changes: 12 additions & 0 deletions src/queries/officer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
updateAcademicInfo,
updateOfficerImage,
updateOfficerName,
updateOfficerStatus,
} from "@/functions/officer";
import { mutationOptions, queryOptions } from "@tanstack/react-query";

Expand Down Expand Up @@ -46,3 +47,14 @@ export const updateAcademicInfoMutationOptions = mutationOptions({
context.client.invalidateQueries(getAllOfficersQuery);
},
});

export const updateOfficerStatusMutation = mutationOptions({
mutationFn: updateOfficerStatus,
onSuccess: (res, variables, __, context) => {
context.client.setQueryData(
getOfficerByIdQuery(variables.officerId).queryKey,
res
);
context.client.refetchQueries(getAllOfficersQuery);
},
});