feat(user-agreement): implement user agreement upload functionality and update related components

This commit is contained in:
2025-07-25 10:39:15 +05:00
parent 5fda723653
commit c2578f1c8f
9 changed files with 190 additions and 6 deletions

View File

@ -29,7 +29,7 @@ This is a web portal for SAR Link customers.
## Admin Controls ## Admin Controls
### Users ### Users
- [x] Show users table - [x] Show users table
- [ ] handle verify api no response case - [x] handle verify api no response case
- [x] Add all relavant filters for users table - [x] Add all relavant filters for users table
- [x] Verify or reject users with a custom message - [x] Verify or reject users with a custom message
- [ ] Add functionality to send custom sms to users in user:id page - [ ] Add functionality to send custom sms to users in user:id page

View File

@ -161,3 +161,48 @@ export async function updateUser(
message: "User updated successfully", message: "User updated successfully",
}; };
} }
export async function updateUserAgreement(
_prevState: UpdateUserFormState,
formData: FormData
): Promise<UpdateUserFormState> {
const userId = formData.get("userId") as string;
// Remove userId from formData before sending to API
const apiFormData = new FormData();
for (const [key, value] of formData.entries()) {
if (key !== "userId") {
apiFormData.append(key, value);
}
}
console.log({ apiFormData })
const session = await getServerSession(authOptions);
const response = await fetch(
`${process.env.SARLINK_API_BASE_URL}/api/auth/users/${userId}/agreement/`,
{
method: "PUT",
headers: {
Authorization: `Token ${session?.apiToken}`,
},
body: apiFormData,
},
);
console.log("response in update user agreement action", response)
if (!response.ok) {
const errorData = await response.json();
return {
message: errorData.message || errorData.detail || "An error occurred while updating the user agreement.",
fieldErrors: errorData.field_errors || {},
payload: formData,
}
}
const updatedUserAgreement = await response.json() as { agreement: string };
revalidatePath("/users/[userId]/update", "page");
revalidatePath("/users/[userId]/verify", "page");
revalidatePath("/users/[userId]/agreement", "page");
return {
...updatedUserAgreement,
message: "User agreement updated successfully",
};
}

View File

@ -0,0 +1,47 @@
import { redirect } from "next/navigation";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/auth";
import ClientErrorMessage from "@/components/client-error-message";
import UserAgreementForm from "@/components/user/user-agreement-form";
import { getProfileById } from "@/queries/users";
import { tryCatch } from "@/utils/tryCatch";
// import {
// Select,
// SelectContent,
// SelectGroup,
// SelectItem,
// SelectLabel,
// SelectTrigger,
// SelectValue,
// } from "@/components/ui/select";
export default async function UserUpdate({
params,
}: {
params: Promise<{
userId: string;
}>;
}) {
const { userId } = await params;
const session = await getServerSession(authOptions);
if (!session?.user?.is_admin) return null
const [error, user] = await tryCatch(getProfileById(userId));
if (error) {
if (error.message === "UNAUTHORIZED") {
redirect("/auth/signin");
} else {
return <ClientErrorMessage message={error.message} />;
}
}
return (
<div>
<div className="flex items-center justify-between text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl">Upload user user agreement</h3>
</div>
<UserAgreementForm user={user} />
</div>
);
}

View File

@ -1,4 +1,4 @@
import { PencilIcon } from "lucide-react"; import { FileTextIcon, PencilIcon } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@ -42,7 +42,7 @@ export default async function VerifyUserPage({
return ( return (
<div> <div>
<div className="flex items-center justify-between text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4"> <div className="flex items-center justify-between text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl">Verify user</h3> <h3 className="text-sarLinkOrange text-2xl">User Information</h3>
<div className="flex gap-2"> <div className="flex gap-2">
{dbUser && !dbUser?.verified && <UserVerifyDialog user={dbUser} />} {dbUser && !dbUser?.verified && <UserVerifyDialog user={dbUser} />}
@ -53,6 +53,12 @@ export default async function VerifyUserPage({
Update User Update User
</Button> </Button>
</Link> </Link>
<Link href={'agreement'}>
<Button className="hover:cursor-pointer">
<FileTextIcon />
Update Agreement
</Button>
</Link>
{dbUser?.verified && ( {dbUser?.verified && (
<Badge variant={"secondary"} className="bg-lime-500"> <Badge variant={"secondary"} className="bg-lime-500">
Verified Verified

4
app/next-auth.d.ts vendored
View File

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
// @ts-expect-error importing unused types are required here // @ts-expect-error importing unused types are required here
import NextAuth, { DefaultSession, type User, Session } from "next-auth"; import NextAuth, { DefaultSession, Session, type User } from "next-auth";
/* eslint-enable @typescript-eslint/no-unused-vars */ /* eslint-enable @typescript-eslint/no-unused-vars */
declare module "next-auth" { declare module "next-auth" {
/** /**
@ -26,6 +27,7 @@ declare module "next-auth" {
date_joined?: string; date_joined?: string;
is_superuser?: boolean; is_superuser?: boolean;
is_admin?: boolean; is_admin?: boolean;
agreement?: string;
}; };
expires: ISODateString; expires: ISODateString;
} }

View File

@ -16,7 +16,7 @@ export function DeviceCartDrawer() {
if (devices.length === 0) return null; if (devices.length === 0) return null;
return ( return (
<div className="bg-sarLinkOrange rounded-lg shadow-2xl dark:hover:bg-orange-900 fixed bottom-20 w-80 uppercase h-auto z-20 left-1/2 transform -translate-x-1/2 hover:ring-2 hover:ring-sarLinkOrange transition-all duration-200 p-2 flex flex-col gap-2"> <div className="bg-sarLinkOrange rounded-lg shadow-2xl dark:bg-orange-900 fixed bottom-20 w-80 uppercase h-auto z-20 left-1/2 transform -translate-x-1/2 hover:ring-2 hover:ring-sarLinkOrange transition-all duration-200 p-2 flex flex-col gap-2">
<Button <Button
size={"lg"} size={"lg"}
className="w-ful" className="w-ful"

View File

@ -0,0 +1,79 @@
"use client";
import { Loader2, MoveLeft } from "lucide-react";
import { useActionState, useEffect } from "react";
import { toast } from "sonner";
import { type UpdateUserFormState, updateUserAgreement } from "@/actions/user-actions";
import { Button } from "@/components/ui/button";
import { FloatingLabelInput } from "@/components/ui/floating-label";
import type { UserProfile } from "@/lib/types/user";
export default function UserAgreementForm({ user }: { user: UserProfile }) {
const initialState: UpdateUserFormState = {
message: "",
fieldErrors: {},
payload: new FormData(),
};
const [state, formAction, isPending] = useActionState(
updateUserAgreement,
initialState,
);
useEffect(() => {
if (state.message) {
if (state.fieldErrors) {
Object.entries(state.fieldErrors).forEach(([field, errors]) => {
errors.forEach((error) => {
toast.error(`Error in ${field}: ${error}`);
});
});
} else {
toast.success("Success", {
description: "User agreement updated successfully",
closeButton: true,
});
}
}
}, [state]);
return (
<div id="user-update-form">
<Button
onClick={() => window.history.back()}
variant="outline"
className="mb-4"
>
<MoveLeft />
Go Back
</Button>
<form action={formAction}>
<h4 className="p-2 rounded font-semibold text-muted-foreground">
Upload User agreement
</h4>
<div className="border border-dashed border-sarLinkOrange p-4 rounded-lg max-w-2xl">
<fieldset
disabled={isPending}
className="space-y-1 my-2 grid grid-cols-1 md:grid-cols-2 gap-4"
>
<input type="hidden" name="userId" value={user.id} />
<FloatingLabelInput
type="file"
size={10}
name="agreement"
label="Agreement Document"
accept=".pdf"
/>
</fieldset>
<Button
disabled={isPending}
type="submit"
variant={"secondary"}
className=""
>
{isPending ? <Loader2 className="animate-spin" /> : "Update Agreement"}
</Button>
</div>
</form>
</div>
);
}

View File

@ -1,4 +1,4 @@
import { User } from "./types/user"; import type { User } from "./types/user";
export interface Links { export interface Links {
next_page: string | null; next_page: string | null;

View File

@ -2,6 +2,11 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ /* config options here */
experimental: {
serverActions: {
bodySizeLimit: '20mb',
}
},
images: { images: {
remotePatterns: [ remotePatterns: [
new URL('http://people-api.sarlink.net/images/**'), new URL('http://people-api.sarlink.net/images/**'),