From 0c5daa7173ac456f2a5aa76432b4e8fd10438f85 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 14 Jan 2026 03:31:49 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20process=20approvals=20on=20the=20fr?= =?UTF-8?q?ontend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 7 +- .../routers/approvals/listApprovals.ts | 2 +- .../approvals/processPendingApproval.ts | 6 +- src/components/ApprovalFeed.tsx | 126 +++++++++++++----- src/lib/queries.ts | 2 +- 5 files changed, 107 insertions(+), 36 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 8ded612b..7ba0b355 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -462,7 +462,7 @@ "all": "All", "deny": "Deny", "viewDetails": "View Details", - "requestingNewDeviceApproval": "is requesting new device", + "requestingNewDeviceApproval": "requested a new device", "resetFilters": "Reset Filters", "totalBlocked": "Requests Blocked By Pangolin", "totalRequests": "Total Requests", @@ -752,8 +752,13 @@ "accessRoleUpdateSubmit": "Update Role", "accessRoleUpdated": "Role updated", "accessRoleUpdatedDescription": "The role has been successfully updated.", + "accessApprovalUpdated": "Approval processed", + "accessApprovalApprovedDescription": "Request approved.", + "accessApprovalDeniedDescription": "Request denied.", "accessRoleErrorUpdate": "Failed to update role", "accessRoleErrorUpdateDescription": "An error occurred while updating the role.", + "accessApprovalErrorUpdate": "Failed to process approval", + "accessApprovalErrorUpdateDescription": "An error occurred while processing the approval.", "accessRoleErrorNewRequired": "New role is required", "accessRoleErrorRemove": "Failed to remove role", "accessRoleErrorRemoveDescription": "An error occurred while removing the role.", diff --git a/server/private/routers/approvals/listApprovals.ts b/server/private/routers/approvals/listApprovals.ts index ca774801..6006e48b 100644 --- a/server/private/routers/approvals/listApprovals.ts +++ b/server/private/routers/approvals/listApprovals.ts @@ -45,7 +45,7 @@ const querySchema = z.strictObject({ approvalState: z .enum(["pending", "approved", "denied", "all"]) .optional() - .default("pending") + .default("all") .catch("all") }); diff --git a/server/private/routers/approvals/processPendingApproval.ts b/server/private/routers/approvals/processPendingApproval.ts index 4e456eb9..eaf583f1 100644 --- a/server/private/routers/approvals/processPendingApproval.ts +++ b/server/private/routers/approvals/processPendingApproval.ts @@ -18,7 +18,7 @@ import { z } from "zod"; import { fromError } from "zod-validation-error"; import { build } from "@server/build"; -import { approvals, clients, db, orgs } from "@server/db"; +import { approvals, clients, db, orgs, type Approval } from "@server/db"; import { getOrgTierData } from "@server/lib/billing"; import { TierId } from "@server/lib/billing/tiers"; import response from "@server/lib/response"; @@ -27,13 +27,15 @@ import type { NextFunction, Request, Response } from "express"; const paramsSchema = z.strictObject({ orgId: z.string(), - approvalId: z.int() + approvalId: z.string().transform(Number).pipe(z.int().positive()) }); const bodySchema = z.strictObject({ decision: z.enum(["approved", "denied"]) }); +export type ProcessApprovalResponse = Approval; + export async function processPendingApproval( req: Request, res: Response, diff --git a/src/components/ApprovalFeed.tsx b/src/components/ApprovalFeed.tsx index 1132129c..df045844 100644 --- a/src/components/ApprovalFeed.tsx +++ b/src/components/ApprovalFeed.tsx @@ -1,10 +1,23 @@ "use client"; -import { useTranslations } from "next-intl"; -import { useSearchParams, usePathname, useRouter } from "next/navigation"; -import { Card, CardHeader } from "./ui/card"; -import { Button } from "./ui/button"; -import { ArrowRight, Ban, Check, LaptopMinimal, RefreshCw } from "lucide-react"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; import { cn } from "@app/lib/cn"; +import { approvalFiltersSchema, approvalQueries } from "@app/lib/queries"; +import type { + ListApprovalsResponse, + ProcessApprovalResponse +} from "@server/private/routers/approvals"; +import { useQuery } from "@tanstack/react-query"; +import type { AxiosResponse } from "axios"; +import { ArrowRight, Ban, Check, LaptopMinimal, RefreshCw } from "lucide-react"; +import { useTranslations } from "next-intl"; +import Link from "next/link"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { Fragment, useActionState } from "react"; +import { Badge } from "./ui/badge"; +import { Button } from "./ui/button"; +import { Card, CardHeader } from "./ui/card"; import { Label } from "./ui/label"; import { Select, @@ -13,12 +26,7 @@ import { SelectTrigger, SelectValue } from "./ui/select"; -import { approvalFiltersSchema, approvalQueries } from "@app/lib/queries"; -import { useQuery } from "@tanstack/react-query"; -import { Fragment } from "react"; import { Separator } from "./ui/separator"; -import type { ListApprovalsResponse } from "@server/private/routers/approvals"; -import Link from "next/link"; export type ApprovalFeedProps = { orgId: string; @@ -60,7 +68,7 @@ export function ApprovalFeed({ orgId }: ApprovalFeedProps) { `${path}?${newSearch.toString()}` ); }} - value={filters.approvalState ?? "pending"} + value={filters.approvalState ?? "all"} > (
  • - + refetch()} + />
  • {index < approvals.length - 1 && }
    @@ -129,10 +141,47 @@ export function ApprovalFeed({ orgId }: ApprovalFeedProps) { type ApprovalRequestProps = { approval: ListApprovalsResponse["approvals"][number]; + orgId: string; + onSuccess?: (data: ProcessApprovalResponse) => void; }; -function ApprovalRequest({ approval }: ApprovalRequestProps) { +function ApprovalRequest({ approval, orgId, onSuccess }: ApprovalRequestProps) { const t = useTranslations(); + + const [_, formAction, isSubmitting] = useActionState(onSubmit, null); + const api = createApiClient(useEnvContext()); + + async function onSubmit(_previousState: any, formData: FormData) { + const decision = formData.get("decision"); + const res = await api + .put< + AxiosResponse + >(`/org/${orgId}/approvals/${approval.approvalId}`, { decision }) + .catch((e) => { + toast({ + variant: "destructive", + title: t("accessApprovalErrorUpdate"), + description: formatAxiosError( + e, + t("accessApprovalErrorUpdateDescription") + ) + }); + }); + if (res && res.status === 200) { + const result = res.data.data; + toast({ + variant: "default", + title: t("accessApprovalUpdated"), + description: + result.decision === "approved" + ? t("accessApprovalApprovedDescription") + : t("accessApprovalDeniedDescription") + }); + + onSuccess?.(res.data.data); + } + } + return (
    @@ -148,27 +197,42 @@ function ApprovalRequest({ approval }: ApprovalRequestProps) {
    - - + {approval.decision === "pending" && ( +
    + + +
    + )} + {approval.decision === "approved" && ( + {t("approved")} + )} + {approval.decision === "denied" && ( + {t("denied")} + )} +