mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-07-27 02:10:23 +00:00
feat(user-agreement): implement user agreement upload functionality and update related components ✨
This commit is contained in:
@ -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
|
||||||
|
@ -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",
|
||||||
|
};
|
||||||
|
}
|
47
app/(dashboard)/users/[userId]/agreement/page.tsx
Normal file
47
app/(dashboard)/users/[userId]/agreement/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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
4
app/next-auth.d.ts
vendored
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
79
components/user/user-agreement-form.tsx
Normal file
79
components/user/user-agreement-form.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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/**'),
|
||||||
|
Reference in New Issue
Block a user