mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-07-15 11:05:50 +00:00
feat(user): implement user update functionality and enhance verification page UI ✨
This commit is contained in:
@ -6,7 +6,7 @@ import { getServerSession } from "next-auth";
|
|||||||
import { authOptions } from "@/app/auth";
|
import { authOptions } from "@/app/auth";
|
||||||
import type { RejectUserFormState } from "@/components/user/user-reject-dialog";
|
import type { RejectUserFormState } from "@/components/user/user-reject-dialog";
|
||||||
import type { ApiError } from "@/lib/backend-types";
|
import type { ApiError } from "@/lib/backend-types";
|
||||||
import type { User, UserProfile } from "@/lib/types/user";
|
import type { User } from "@/lib/types/user";
|
||||||
import { handleApiResponse } from "@/utils/tryCatch";
|
import { handleApiResponse } from "@/utils/tryCatch";
|
||||||
|
|
||||||
export async function VerifyUser(_userId: string) {
|
export async function VerifyUser(_userId: string) {
|
||||||
@ -73,24 +73,6 @@ export async function getProfile() {
|
|||||||
return handleApiResponse<User>(response, "getProfile");
|
return handleApiResponse<User>(response, "getProfile");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function getProfileById(userId: string) {
|
|
||||||
const session = await getServerSession(authOptions);
|
|
||||||
const response = await fetch(
|
|
||||||
`${process.env.SARLINK_API_BASE_URL}/api/auth/users/${userId}/`,
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Token ${session?.apiToken}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return handleApiResponse<UserProfile>(response, "getProfilebyId");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function rejectUser(
|
export async function rejectUser(
|
||||||
_prevState: RejectUserFormState,
|
_prevState: RejectUserFormState,
|
||||||
formData: FormData
|
formData: FormData
|
||||||
@ -129,3 +111,62 @@ export async function rejectUser(
|
|||||||
payload: formData
|
payload: formData
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UpdateUserFormState = {
|
||||||
|
message: string;
|
||||||
|
fieldErrors?: {
|
||||||
|
id_card?: string[];
|
||||||
|
first_name?: string[];
|
||||||
|
last_name?: string[];
|
||||||
|
dob?: string[];
|
||||||
|
mobile?: string[];
|
||||||
|
address?: string[];
|
||||||
|
};
|
||||||
|
payload?: FormData;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export async function updateUser(
|
||||||
|
_prevState: UpdateUserFormState,
|
||||||
|
formData: FormData
|
||||||
|
): Promise<UpdateUserFormState> {
|
||||||
|
const userId = formData.get("userId") as string;
|
||||||
|
const data: Record<string, string | number | boolean> = {};
|
||||||
|
for (const [key, value] of formData.entries()) {
|
||||||
|
if (value !== undefined && value !== "") {
|
||||||
|
data[key] = typeof value === "number" ? value : String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("data in update user action", data)
|
||||||
|
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.SARLINK_API_BASE_URL}/api/auth/users/${userId}/update/`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Token ${session?.apiToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
console.log("response in update user action", response)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
return {
|
||||||
|
message: errorData.message || errorData.detail || "An error occurred while updating the user.",
|
||||||
|
fieldErrors: errorData.field_errors || {},
|
||||||
|
payload: formData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedUser = await response.json() as User;
|
||||||
|
revalidatePath("/users/[userId]/update", "page");
|
||||||
|
revalidatePath("/users/[userId]/verify", "page");
|
||||||
|
return {
|
||||||
|
...updatedUser,
|
||||||
|
message: "User updated successfully",
|
||||||
|
};
|
||||||
|
}
|
@ -1,3 +1,19 @@
|
|||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { authOptions } from "@/app/auth";
|
||||||
|
import ClientErrorMessage from "@/components/client-error-message";
|
||||||
|
import UserUpdateForm from "@/components/user/user-update-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({
|
export default async function UserUpdate({
|
||||||
params,
|
params,
|
||||||
@ -7,8 +23,25 @@ export default async function UserUpdate({
|
|||||||
}>;
|
}>;
|
||||||
}) {
|
}) {
|
||||||
const { userId } = await params;
|
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 (
|
return (
|
||||||
<div>UserUpdate: {userId}</div>
|
<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">Verify user</h3>
|
||||||
|
</div>
|
||||||
|
<UserUpdateForm user={user} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
|
import { PencilIcon } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getProfileById } from "@/actions/user-actions";
|
|
||||||
import ClientErrorMessage from "@/components/client-error-message";
|
import ClientErrorMessage from "@/components/client-error-message";
|
||||||
import InputReadOnly from "@/components/input-read-only";
|
import InputReadOnly from "@/components/input-read-only";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import UserRejectDialog from "@/components/user/user-reject-dialog";
|
import UserRejectDialog from "@/components/user/user-reject-dialog";
|
||||||
import { UserVerifyDialog } from "@/components/user/user-verify-dialog";
|
import { UserVerifyDialog } from "@/components/user/user-verify-dialog";
|
||||||
import { getNationalPerson } from "@/lib/person";
|
import { getNationalPerson } from "@/lib/person";
|
||||||
|
import { getProfileById } from "@/queries/users";
|
||||||
import { tryCatch } from "@/utils/tryCatch";
|
import { tryCatch } from "@/utils/tryCatch";
|
||||||
|
|
||||||
export default async function VerifyUserPage({
|
export default async function VerifyUserPage({
|
||||||
@ -44,6 +47,12 @@ export default async function VerifyUserPage({
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{dbUser && !dbUser?.verified && <UserVerifyDialog user={dbUser} />}
|
{dbUser && !dbUser?.verified && <UserVerifyDialog user={dbUser} />}
|
||||||
{dbUser && !dbUser?.verified && <UserRejectDialog user={dbUser} />}
|
{dbUser && !dbUser?.verified && <UserRejectDialog user={dbUser} />}
|
||||||
|
<Link href={'update'}>
|
||||||
|
<Button className="hover:cursor-pointer">
|
||||||
|
<PencilIcon />
|
||||||
|
Update User
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
{dbUser?.verified && (
|
{dbUser?.verified && (
|
||||||
<Badge variant={"secondary"} className="bg-lime-500">
|
<Badge variant={"secondary"} className="bg-lime-500">
|
||||||
Verified
|
Verified
|
||||||
@ -54,7 +63,7 @@ export default async function VerifyUserPage({
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-start justify-start">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-start justify-start">
|
||||||
<div id="database-information">
|
<div id="database-information">
|
||||||
<h4 className="p-2 rounded font-semibold">Database Information</h4>
|
<h4 className="p-2 rounded font-semibold">Database Information</h4>
|
||||||
<div className="shadow p-2 rounded-lg title-bg space-y-1 my-2 grid grid-cols-1 md:grid-cols-2 gap-2">
|
<div className="shadow-md p-2 bg-sarLinkOrange/10 border border-dashed border-sarLinkOrange rounded-lg space-y-1 my-2 grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
<InputReadOnly
|
<InputReadOnly
|
||||||
showCheck
|
showCheck
|
||||||
checkTrue={dbUser?.id_card === nationalData?.nic}
|
checkTrue={dbUser?.id_card === nationalData?.nic}
|
||||||
@ -88,7 +97,7 @@ export default async function VerifyUserPage({
|
|||||||
checkTrue={dbUser?.atoll.name === nationalData?.atoll_en}
|
checkTrue={dbUser?.atoll.name === nationalData?.atoll_en}
|
||||||
labelClassName="text-sarLinkOrange"
|
labelClassName="text-sarLinkOrange"
|
||||||
label="Atoll"
|
label="Atoll"
|
||||||
value={dbUser?.island?.name ?? ""}
|
value={dbUser?.atoll?.name ?? ""}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputReadOnly
|
<InputReadOnly
|
||||||
@ -116,7 +125,7 @@ export default async function VerifyUserPage({
|
|||||||
{(
|
{(
|
||||||
<div id="national-information">
|
<div id="national-information">
|
||||||
<h4 className="p-2 rounded font-semibold">National Information</h4>
|
<h4 className="p-2 rounded font-semibold">National Information</h4>
|
||||||
<div className="shadow p-2 rounded-lg title-bg space-y-1 my-2 grid grid-cols-1 md:grid-cols-2 gap-2">
|
<div className="bg-green-800/10 shadow p-2 rounded-lg border border-dashed border-green-800 space-y-1 my-2 grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
<InputReadOnly
|
<InputReadOnly
|
||||||
showCheck={false}
|
showCheck={false}
|
||||||
labelClassName="text-green-500"
|
labelClassName="text-green-500"
|
||||||
|
151
components/user/user-update-form.tsx
Normal file
151
components/user/user-update-form.tsx
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
"use client";
|
||||||
|
import { ArrowRightLeft, Loader2, MoveLeft } from "lucide-react";
|
||||||
|
import { useActionState, useEffect } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { type UpdateUserFormState, updateUser } from "@/actions/user-actions";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { FloatingLabelInput } from "@/components/ui/floating-label";
|
||||||
|
import type { UserProfile } from "@/lib/types/user";
|
||||||
|
// import {
|
||||||
|
// Select,
|
||||||
|
// SelectContent,
|
||||||
|
// SelectGroup,
|
||||||
|
// SelectItem,
|
||||||
|
// SelectLabel,
|
||||||
|
// SelectTrigger,
|
||||||
|
// SelectValue,
|
||||||
|
// } from "@/components/ui/select";
|
||||||
|
|
||||||
|
export default function UserUpdateForm({ user }: { user: UserProfile }) {
|
||||||
|
const initialState: UpdateUserFormState = {
|
||||||
|
message: "",
|
||||||
|
fieldErrors: {},
|
||||||
|
payload: new FormData(),
|
||||||
|
};
|
||||||
|
const [state, formAction, isPending] = useActionState(
|
||||||
|
updateUser,
|
||||||
|
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 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">
|
||||||
|
Update User Information
|
||||||
|
</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
|
||||||
|
defaultValue={
|
||||||
|
user?.id_card || (state.payload?.get("id_card") as string)
|
||||||
|
}
|
||||||
|
size={10}
|
||||||
|
name="id_card"
|
||||||
|
label="ID Card"
|
||||||
|
/>
|
||||||
|
<FloatingLabelInput
|
||||||
|
defaultValue={
|
||||||
|
user?.first_name || (state.payload?.get("first_name") as string)
|
||||||
|
}
|
||||||
|
name="first_name"
|
||||||
|
label="First Name"
|
||||||
|
/>
|
||||||
|
<FloatingLabelInput
|
||||||
|
defaultValue={
|
||||||
|
user?.last_name || (state.payload?.get("last_name") as string)
|
||||||
|
}
|
||||||
|
name="last_name"
|
||||||
|
label="Last Name"
|
||||||
|
/>
|
||||||
|
<FloatingLabelInput
|
||||||
|
defaultValue={
|
||||||
|
user?.address || (state.payload?.get("address") as string)
|
||||||
|
}
|
||||||
|
name="address"
|
||||||
|
label="House Name"
|
||||||
|
/>
|
||||||
|
{/* <Select>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select an island" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Islands</SelectLabel>
|
||||||
|
<SelectItem value="apple">Apple</SelectItem>
|
||||||
|
<SelectItem value="banana">Banana</SelectItem>
|
||||||
|
<SelectItem value="blueberry">Blueberry</SelectItem>
|
||||||
|
<SelectItem value="grapes">Grapes</SelectItem>
|
||||||
|
<SelectItem value="pineapple">Pineapple</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select an island" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Islands</SelectLabel>
|
||||||
|
<SelectItem value="apple">Apple</SelectItem>
|
||||||
|
<SelectItem value="banana">Banana</SelectItem>
|
||||||
|
<SelectItem value="blueberry">Blueberry</SelectItem>
|
||||||
|
<SelectItem value="grapes">Grapes</SelectItem>
|
||||||
|
<SelectItem value="pineapple">Pineapple</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select> */}
|
||||||
|
<FloatingLabelInput
|
||||||
|
defaultValue={user?.dob}
|
||||||
|
name="dob"
|
||||||
|
type="date"
|
||||||
|
label="DOB"
|
||||||
|
/>
|
||||||
|
<FloatingLabelInput
|
||||||
|
defaultValue={user?.mobile}
|
||||||
|
name="mobile"
|
||||||
|
label="Phone Number"
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
disabled={isPending}
|
||||||
|
type="submit"
|
||||||
|
variant={"secondary"}
|
||||||
|
className="col-span-2 w-full"
|
||||||
|
>
|
||||||
|
{isPending ? <Loader2 className="animate-spin" /> : "Update User"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -29,3 +29,20 @@ export async function getUsers(params: ParamProps) {
|
|||||||
|
|
||||||
return handleApiResponse<ApiResponse<UserProfile>>(response, "getUsers");
|
return handleApiResponse<ApiResponse<UserProfile>>(response, "getUsers");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function getProfileById(userId: string) {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.SARLINK_API_BASE_URL}/api/auth/users/${userId}/`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Token ${session?.apiToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return handleApiResponse<UserProfile>(response, "getProfilebyId");
|
||||||
|
}
|
Reference in New Issue
Block a user