Menggunakan rreact query untuk menangani CRUD
This commit is contained in:
parent
84490a1c70
commit
5ee59cbf20
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { NavMain } from "@/app/_components/admin/navigations/nav-main";
|
import { NavMain } from "@/app/(protected)/(admin)/_components/admin/navigations/nav-main";
|
||||||
import { NavReports } from "@/app/_components/admin/navigations/nav-report";
|
import { NavReports } from "@/app/(protected)/(admin)/_components/admin/navigations/nav-report";
|
||||||
import { NavUser } from "@/app/_components/admin/navigations/nav-user";
|
import { NavUser } from "@/app/(protected)/(admin)/_components/admin/navigations/nav-user";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
|
@ -15,7 +15,7 @@ import {
|
||||||
} from "@/app/_components/ui/sidebar";
|
} from "@/app/_components/ui/sidebar";
|
||||||
import { NavPreMain } from "./navigations/nav-pre-main";
|
import { NavPreMain } from "./navigations/nav-pre-main";
|
||||||
import { navData } from "@/prisma/data/nav";
|
import { navData } from "@/prisma/data/nav";
|
||||||
import { TeamSwitcher } from "../team-switcher";
|
import { TeamSwitcher } from "../../../../_components/team-switcher";
|
||||||
|
|
||||||
import { Profile, User } from "@/src/models/users/users.model";
|
import { Profile, User } from "@/src/models/users/users.model";
|
||||||
import { getCurrentUser } from "@/app/(protected)/(admin)/dashboard/user-management/action";
|
import { getCurrentUser } from "@/app/(protected)/(admin)/dashboard/user-management/action";
|
|
@ -4,7 +4,7 @@ import { Card, CardContent } from "@/app/_components/ui/card";
|
||||||
import { ScrollArea } from "@/app/_components/ui/scroll-area";
|
import { ScrollArea } from "@/app/_components/ui/scroll-area";
|
||||||
import { Separator } from "@/app/_components/ui/separator";
|
import { Separator } from "@/app/_components/ui/separator";
|
||||||
import { Upload } from "lucide-react";
|
import { Upload } from "lucide-react";
|
||||||
import { Badge } from "../../ui/badge";
|
import { Badge } from "../../../../../_components/ui/badge";
|
||||||
import {
|
import {
|
||||||
IconBrandGoogleAnalytics,
|
IconBrandGoogleAnalytics,
|
||||||
IconCsv,
|
IconCsv,
|
|
@ -5,8 +5,8 @@ import { ChevronDown } from "lucide-react";
|
||||||
import { Switch } from "@/app/_components/ui/switch";
|
import { Switch } from "@/app/_components/ui/switch";
|
||||||
import { Separator } from "@/app/_components/ui/separator";
|
import { Separator } from "@/app/_components/ui/separator";
|
||||||
import { ScrollArea } from "@/app/_components/ui/scroll-area";
|
import { ScrollArea } from "@/app/_components/ui/scroll-area";
|
||||||
import { ThemeSwitcher } from "../../theme-switcher";
|
import { ThemeSwitcher } from "../../../../../_components/theme-switcher";
|
||||||
import DropdownSwitcher from "../../custom-dropdown-switcher";
|
import DropdownSwitcher from "../../../../../_components/custom-dropdown-switcher";
|
||||||
import {
|
import {
|
||||||
type CookiePreferences,
|
type CookiePreferences,
|
||||||
defaultCookiePreferences,
|
defaultCookiePreferences,
|
|
@ -1,5 +1,3 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
@ -15,6 +13,7 @@ import { Checkbox } from "@/app/_components/ui/checkbox";
|
||||||
import { createUser } from "@/app/(protected)/(admin)/dashboard/user-management/action";
|
import { createUser } from "@/app/(protected)/(admin)/dashboard/user-management/action";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Mail, Lock, Loader2, X } from "lucide-react";
|
import { Mail, Lock, Loader2, X } from "lucide-react";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
|
||||||
interface AddUserDialogProps {
|
interface AddUserDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
@ -27,7 +26,6 @@ export function AddUserDialog({
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onUserAdded,
|
onUserAdded,
|
||||||
}: AddUserDialogProps) {
|
}: AddUserDialogProps) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
|
@ -39,17 +37,10 @@ export function AddUserDialog({
|
||||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const { mutate: createUserMutation, isPending } = useMutation({
|
||||||
e.preventDefault();
|
mutationKey: ["createUser"],
|
||||||
setIsLoading(true);
|
mutationFn: createUser,
|
||||||
|
onSuccess: () => {
|
||||||
try {
|
|
||||||
await createUser({
|
|
||||||
email: formData.email,
|
|
||||||
password: formData.password,
|
|
||||||
email_confirm: formData.emailConfirm,
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success("User created successfully.");
|
toast.success("User created successfully.");
|
||||||
onUserAdded();
|
onUserAdded();
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
|
@ -58,10 +49,24 @@ export function AddUserDialog({
|
||||||
password: "",
|
password: "",
|
||||||
emailConfirm: true,
|
emailConfirm: true,
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error("Failed to create user.");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createUserMutation({
|
||||||
|
email: formData.email,
|
||||||
|
password: formData.password,
|
||||||
|
email_confirm: formData.emailConfirm,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Failed to create user.");
|
toast.error("Failed to create user.");
|
||||||
} finally {
|
return;
|
||||||
setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -144,8 +149,8 @@ export function AddUserDialog({
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button type="submit" disabled={isLoading} className="w-full ">
|
<Button type="submit" disabled={isPending} className="w-full ">
|
||||||
{isLoading ? (
|
{isPending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
Creating...
|
Creating...
|
|
@ -1,5 +1,3 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import { Badge } from "@/app/_components/ui/badge";
|
import { Badge } from "@/app/_components/ui/badge";
|
||||||
import { Checkbox } from "@/app/_components/ui/checkbox";
|
import { Checkbox } from "@/app/_components/ui/checkbox";
|
|
@ -48,6 +48,7 @@ interface DataTableProps<TData, TValue> {
|
||||||
data: TData[];
|
data: TData[];
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
onRowClick?: (row: TData) => void;
|
onRowClick?: (row: TData) => void;
|
||||||
|
onActionClick?: (row: TData, action: string) => void;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,6 +57,7 @@ export function DataTable<TData, TValue>({
|
||||||
data,
|
data,
|
||||||
loading = false,
|
loading = false,
|
||||||
onRowClick,
|
onRowClick,
|
||||||
|
onActionClick,
|
||||||
pageSize = 5,
|
pageSize = 5,
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
@ -198,11 +200,38 @@ export function DataTable<TData, TValue>({
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
|
{/* {onActionClick && (
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onActionClick(row.original, "update");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onActionClick(row.original, "delete");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
)} */}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
<TableCell
|
||||||
|
colSpan={columns.length + 1}
|
||||||
|
className="h-24 text-center"
|
||||||
|
>
|
||||||
No results.
|
No results.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
|
@ -1,5 +1,3 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
@ -35,8 +33,6 @@ export function InviteUserDialog({
|
||||||
metadata: "{}",
|
metadata: "{}",
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const handleInputChange = (
|
const handleInputChange = (
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||||
) => {
|
) => {
|
||||||
|
@ -44,23 +40,10 @@ export function InviteUserDialog({
|
||||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const { mutate: inviteUserMutation, isPending } = useMutation({
|
||||||
e.preventDefault();
|
mutationKey: ["inviteUser"],
|
||||||
setIsLoading(true);
|
mutationFn: inviteUser,
|
||||||
|
onSuccess: () => {
|
||||||
let metadata = {};
|
|
||||||
try {
|
|
||||||
metadata = JSON.parse(formData.metadata);
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("Invalid JSON. Please check your metadata format.");
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await inviteUser({
|
|
||||||
email: formData.email,
|
|
||||||
});
|
|
||||||
toast.success("Invitation sent");
|
toast.success("Invitation sent");
|
||||||
onUserInvited();
|
onUserInvited();
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
|
@ -68,10 +51,24 @@ export function InviteUserDialog({
|
||||||
email: "",
|
email: "",
|
||||||
metadata: "{}",
|
metadata: "{}",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
},
|
||||||
|
onError: () => {
|
||||||
toast.error("Failed to send invitation");
|
toast.error("Failed to send invitation");
|
||||||
} finally {
|
},
|
||||||
setIsLoading(false);
|
});
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
let metadata = {};
|
||||||
|
try {
|
||||||
|
metadata = JSON.parse(formData.metadata);
|
||||||
|
inviteUserMutation({
|
||||||
|
email: formData.email,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Invalid JSON. Please check your metadata format.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -106,8 +103,8 @@ export function InviteUserDialog({
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isLoading}>
|
<Button type="submit" disabled={isPending}>
|
||||||
{isLoading ? "Sending..." : "Send Invitation"}
|
{isPending ? "Sending..." : "Send Invitation"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
|
@ -1,5 +1,3 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
@ -43,19 +41,19 @@ import {
|
||||||
} from "@/app/(protected)/(admin)/dashboard/user-management/action";
|
} from "@/app/(protected)/(admin)/dashboard/user-management/action";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
|
||||||
interface UserDetailsSheetProps {
|
interface UserDetailSheetProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
user: any;
|
user: any;
|
||||||
onUserUpdate: () => void;
|
onUserUpdate: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserDetailsSheet({
|
export function UserDetailSheet({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
user,
|
user,
|
||||||
onUserUpdate,
|
onUserUpdate,
|
||||||
}: UserDetailsSheetProps) {
|
}: UserDetailSheetProps) {
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState({
|
const [isLoading, setIsLoading] = useState({
|
||||||
deleteUser: false,
|
deleteUser: false,
|
||||||
|
@ -64,67 +62,105 @@ export function UserDetailsSheet({
|
||||||
toggleBan: false,
|
toggleBan: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDeleteUser = async () => {
|
const deleteUserMutation = useMutation({
|
||||||
|
mutationFn: () => deleteUser(user.id),
|
||||||
|
onMutate: () => {
|
||||||
setIsLoading((prev) => ({ ...prev, deleteUser: true }));
|
setIsLoading((prev) => ({ ...prev, deleteUser: true }));
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
try {
|
},
|
||||||
await deleteUser(user.id);
|
onSuccess: () => {
|
||||||
toast.success("User deleted successfully");
|
toast.success("User deleted successfully");
|
||||||
onUserUpdate();
|
onUserUpdate();
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
} catch {
|
},
|
||||||
|
onError: () => {
|
||||||
toast.error("Failed to delete user");
|
toast.error("Failed to delete user");
|
||||||
} finally {
|
},
|
||||||
|
onSettled: () => {
|
||||||
setIsLoading((prev) => ({ ...prev, deleteUser: false }));
|
setIsLoading((prev) => ({ ...prev, deleteUser: false }));
|
||||||
setIsDeleting(false);
|
setIsDeleting(false);
|
||||||
}
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const handleSendPasswordRecovery = async () => {
|
const sendPasswordRecoveryMutation = useMutation({
|
||||||
|
mutationFn: () => {
|
||||||
|
if (!user.email) {
|
||||||
|
throw new Error("User does not have an email address");
|
||||||
|
}
|
||||||
|
return sendPasswordRecovery(user.email);
|
||||||
|
},
|
||||||
|
onMutate: () => {
|
||||||
setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: true }));
|
setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: true }));
|
||||||
try {
|
},
|
||||||
if (!user.email) {
|
onSuccess: () => {
|
||||||
throw new Error("User does not have an email address");
|
|
||||||
}
|
|
||||||
await sendPasswordRecovery(user.email);
|
|
||||||
toast.success("Password recovery email sent");
|
toast.success("Password recovery email sent");
|
||||||
} catch {
|
},
|
||||||
|
onError: () => {
|
||||||
toast.error("Failed to send password recovery email");
|
toast.error("Failed to send password recovery email");
|
||||||
} finally {
|
},
|
||||||
|
onSettled: () => {
|
||||||
setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: false }));
|
setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: false }));
|
||||||
}
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const handleSendMagicLink = async () => {
|
const sendMagicLinkMutation = useMutation({
|
||||||
setIsLoading((prev) => ({ ...prev, sendMagicLink: true }));
|
mutationFn: () => {
|
||||||
try {
|
|
||||||
if (!user.email) {
|
if (!user.email) {
|
||||||
throw new Error("User does not have an email address");
|
throw new Error("User does not have an email address");
|
||||||
}
|
}
|
||||||
await sendMagicLink(user.email);
|
return sendMagicLink(user.email);
|
||||||
|
},
|
||||||
|
onMutate: () => {
|
||||||
|
setIsLoading((prev) => ({ ...prev, sendMagicLink: true }));
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
toast.success("Magic link sent successfully");
|
toast.success("Magic link sent successfully");
|
||||||
} catch {
|
},
|
||||||
|
onError: () => {
|
||||||
toast.error("Failed to send magic link");
|
toast.error("Failed to send magic link");
|
||||||
} finally {
|
},
|
||||||
|
onSettled: () => {
|
||||||
setIsLoading((prev) => ({ ...prev, sendMagicLink: false }));
|
setIsLoading((prev) => ({ ...prev, sendMagicLink: false }));
|
||||||
}
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const handleToggleBan = async () => {
|
const toggleBanMutation = useMutation({
|
||||||
setIsLoading((prev) => ({ ...prev, toggleBan: true }));
|
mutationFn: () => {
|
||||||
try {
|
|
||||||
if (user.banned_until) {
|
if (user.banned_until) {
|
||||||
await unbanUser(user.id);
|
return unbanUser(user.id);
|
||||||
} else {
|
} else {
|
||||||
await banUser(user.id);
|
return banUser(user.id);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onMutate: () => {
|
||||||
|
setIsLoading((prev) => ({ ...prev, toggleBan: true }));
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
toast.success("User ban status updated");
|
toast.success("User ban status updated");
|
||||||
onUserUpdate();
|
onUserUpdate();
|
||||||
} catch {
|
},
|
||||||
|
onError: () => {
|
||||||
toast.error("Failed to update user ban status");
|
toast.error("Failed to update user ban status");
|
||||||
} finally {
|
},
|
||||||
|
onSettled: () => {
|
||||||
setIsLoading((prev) => ({ ...prev, toggleBan: false }));
|
setIsLoading((prev) => ({ ...prev, toggleBan: false }));
|
||||||
}
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDeleteUser = () => {
|
||||||
|
deleteUserMutation.mutate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendPasswordRecovery = () => {
|
||||||
|
sendPasswordRecoveryMutation.mutate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendMagicLink = () => {
|
||||||
|
sendMagicLinkMutation.mutate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleBan = () => {
|
||||||
|
toggleBanMutation.mutate();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyItem = (item: string) => {
|
const handleCopyItem = (item: string) => {
|
|
@ -0,0 +1,449 @@
|
||||||
|
import { FormDescription } from "@/app/_components/ui/form";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import * as z from "zod";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
} from "@/app/_components/ui/sheet";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/app/_components/ui/form";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/app/_components/ui/select";
|
||||||
|
import { Input } from "@/app/_components/ui/input";
|
||||||
|
import { Button } from "@/app/_components/ui/button";
|
||||||
|
import { Textarea } from "@/app/_components/ui/textarea";
|
||||||
|
import { Calendar } from "@/app/_components/ui/calendar";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/app/_components/ui/popover";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { CalendarIcon, Loader2 } from "lucide-react";
|
||||||
|
import { Switch } from "@/app/_components/ui/switch";
|
||||||
|
import { User, UserSchema } from "@/src/models/users/users.model";
|
||||||
|
|
||||||
|
type UserProfileFormValues = z.infer<typeof UserSchema>;
|
||||||
|
|
||||||
|
interface UserProfileSheetProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
userData?: User; // Replace with your user data type
|
||||||
|
onSave: (data: UserProfileFormValues) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UserProfileSheet({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
userData,
|
||||||
|
onSave,
|
||||||
|
}: UserProfileSheetProps) {
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
// Initialize form with user data
|
||||||
|
const form = useForm<UserProfileFormValues>({
|
||||||
|
resolver: zodResolver(UserSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: userData?.email || "",
|
||||||
|
phone: userData?.phone || "",
|
||||||
|
role: userData?.role || "user",
|
||||||
|
is_anonymous: userData?.is_anonymous || false,
|
||||||
|
profile: {
|
||||||
|
username: userData?.profile?.username || "",
|
||||||
|
first_name: userData?.profile?.first_name || "",
|
||||||
|
last_name: userData?.profile?.last_name || "",
|
||||||
|
bio: userData?.profile?.bio || "",
|
||||||
|
birth_date: userData?.profile?.birth_date
|
||||||
|
? new Date(userData.profile.birth_date)
|
||||||
|
: null,
|
||||||
|
avatar: userData?.profile?.avatar || "",
|
||||||
|
address: userData?.profile?.address || {
|
||||||
|
street: "",
|
||||||
|
city: "",
|
||||||
|
state: "",
|
||||||
|
country: "",
|
||||||
|
postal_code: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onSubmit(data: UserProfileFormValues) {
|
||||||
|
try {
|
||||||
|
setIsSaving(true);
|
||||||
|
await onSave(data);
|
||||||
|
onOpenChange(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error saving user profile:", error);
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
|
<SheetContent className="w-full sm:max-w-xl overflow-y-auto">
|
||||||
|
<SheetHeader className="mb-6">
|
||||||
|
<SheetTitle>Update User Profile</SheetTitle>
|
||||||
|
<SheetDescription>
|
||||||
|
Make changes to the user profile here. Click save when you're
|
||||||
|
done.
|
||||||
|
</SheetDescription>
|
||||||
|
</SheetHeader>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
|
{/* User Information Section */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium">User Information</h3>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="email"
|
||||||
|
placeholder="email@example.com"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="phone"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Phone</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="tel"
|
||||||
|
placeholder="+1234567890"
|
||||||
|
value={field.value ?? ""}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="role"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Role</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a role" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="user">User</SelectItem>
|
||||||
|
<SelectItem value="admin">Admin</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="is_anonymous"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel className="text-base">
|
||||||
|
Anonymous User
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Make this user anonymous in the system
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Profile Information Section */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium">Profile Information</h3>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="profile.username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="username"
|
||||||
|
value={field.value ?? ""}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="profile.first_name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>First Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="John"
|
||||||
|
value={field.value ?? ""}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="profile.last_name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Last Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="Doe"
|
||||||
|
value={field.value ?? ""}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="profile.bio"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Bio</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
{...field}
|
||||||
|
placeholder="Tell us about yourself"
|
||||||
|
className="resize-none"
|
||||||
|
rows={4}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="profile.birth_date"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col">
|
||||||
|
<FormLabel>Date of birth</FormLabel>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant={"outline"}
|
||||||
|
className={cn(
|
||||||
|
"w-full pl-3 text-left font-normal",
|
||||||
|
!field.value && "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{field.value ? (
|
||||||
|
format(field.value, "PPP")
|
||||||
|
) : (
|
||||||
|
<span>Pick a date</span>
|
||||||
|
)}
|
||||||
|
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={
|
||||||
|
field.value instanceof Date
|
||||||
|
? field.value
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onSelect={field.onChange}
|
||||||
|
disabled={(date) =>
|
||||||
|
date > new Date() || date < new Date("1900-01-01")
|
||||||
|
}
|
||||||
|
initialFocus
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="profile.avatar"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Avatar URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="url"
|
||||||
|
placeholder="https://example.com/avatar.jpg"
|
||||||
|
value={field.value ?? ""}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Address Section */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium">Address</h3>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="profile.address.street"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Street Address</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="123 Main St" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="profile.address.city"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>City</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="City" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="profile.address.state"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>State</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="State" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="profile.address.country"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Country</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="Country" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="profile.address.postal_code"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Postal Code</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="Postal Code" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex justify-end space-x-4 sticky bottom-0 bg-background py-4 border-t">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isSaving}>
|
||||||
|
{isSaving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
|
{isSaving ? "Saving..." : "Save changes"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useMemo, useEffect } from "react";
|
import { useState, useMemo, useEffect, JSX } from "react";
|
||||||
import {
|
import {
|
||||||
PlusCircle,
|
PlusCircle,
|
||||||
Search,
|
Search,
|
||||||
|
@ -18,6 +18,9 @@ import {
|
||||||
Calendar,
|
Calendar,
|
||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
ListFilter,
|
ListFilter,
|
||||||
|
XCircle,
|
||||||
|
Trash2,
|
||||||
|
UserPen,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/app/_components/ui/button";
|
import { Button } from "@/app/_components/ui/button";
|
||||||
import { Input } from "@/app/_components/ui/input";
|
import { Input } from "@/app/_components/ui/input";
|
||||||
|
@ -30,33 +33,39 @@ import {
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
} from "@/app/_components/ui/dropdown-menu";
|
} from "@/app/_components/ui/dropdown-menu";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||||
import { fetchUsers } from "@/app/(protected)/(admin)/dashboard/user-management/action";
|
import { fetchUsers } from "@/app/(protected)/(admin)/dashboard/user-management/action";
|
||||||
import { User } from "@/src/models/users/users.model";
|
import { User } from "@/src/models/users/users.model";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DataTable } from "./data-table";
|
import { DataTable } from "./data-table";
|
||||||
import { InviteUserDialog } from "./invite-user";
|
import { InviteUserDialog } from "./invite-user";
|
||||||
import { AddUserDialog } from "./add-user-dialog";
|
import { AddUserDialog } from "./add-user-dialog";
|
||||||
import { UserDetailsSheet } from "./sheet";
|
import { UserDetailSheet } from "./sheet";
|
||||||
import { Avatar } from "@radix-ui/react-avatar";
|
import { Avatar } from "@radix-ui/react-avatar";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useNavigations } from "@/app/_hooks/use-navigations";
|
import { ColumnDef, HeaderContext } from "@tanstack/react-table";
|
||||||
|
import { UserProfileSheet } from "./update-user";
|
||||||
|
|
||||||
export default function UserManagement() {
|
type UserFilterOptions = {
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
|
||||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
|
||||||
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
|
||||||
const [isAddUserOpen, setIsAddUserOpen] = useState(false);
|
|
||||||
const [isInviteUserOpen, setIsInviteUserOpen] = useState(false);
|
|
||||||
|
|
||||||
// Filter states
|
|
||||||
const [filters, setFilters] = useState<{
|
|
||||||
email: string;
|
email: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
lastSignIn: string;
|
lastSignIn: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
status: string[];
|
status: string[];
|
||||||
}>({
|
};
|
||||||
|
|
||||||
|
type UserTableColumn = ColumnDef<User, User>;
|
||||||
|
|
||||||
|
export default function UserManagement() {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||||
|
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
||||||
|
const [isUpdateOpen, setIsUpdateOpen] = useState(false);
|
||||||
|
const [isAddUserOpen, setIsAddUserOpen] = useState(false);
|
||||||
|
const [isInviteUserOpen, setIsInviteUserOpen] = useState(false);
|
||||||
|
|
||||||
|
// Filter states
|
||||||
|
const [filters, setFilters] = useState<UserFilterOptions>({
|
||||||
email: "",
|
email: "",
|
||||||
phone: "",
|
phone: "",
|
||||||
lastSignIn: "",
|
lastSignIn: "",
|
||||||
|
@ -65,45 +74,27 @@ export default function UserManagement() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use React Query to fetch users
|
// Use React Query to fetch users
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
const {
|
||||||
const { isLoading, setIsLoading } = useNavigations();
|
data: users = [],
|
||||||
|
isLoading,
|
||||||
useEffect(() => {
|
refetch,
|
||||||
const fetchData = async () => {
|
isPlaceholderData,
|
||||||
try {
|
} = useQuery<User[]>({
|
||||||
setIsLoading(true);
|
queryKey: ["users"],
|
||||||
const fetchedUsers = await fetchUsers();
|
queryFn: fetchUsers,
|
||||||
setUsers(fetchedUsers);
|
placeholderData: keepPreviousData,
|
||||||
} catch (error) {
|
throwOnError: true,
|
||||||
toast.error("Failed to fetch users");
|
});
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const refetch = async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const fetchedUsers = await fetchUsers();
|
|
||||||
setUsers(fetchedUsers);
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("Failed to fetch users");
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUserClick = (user: User) => {
|
const handleUserClick = (user: User) => {
|
||||||
setSelectedUser(user);
|
setSelectedUser(user);
|
||||||
setIsSheetOpen(true);
|
setIsSheetOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUserUpdate = () => {
|
const handleUserUpdate = (user: User) => {
|
||||||
refetch();
|
setSelectedUser(user);
|
||||||
setIsSheetOpen(false);
|
setIsSheetOpen(false);
|
||||||
|
setIsUpdateOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredUsers = useMemo(() => {
|
const filteredUsers = useMemo(() => {
|
||||||
|
@ -224,10 +215,10 @@ export default function UserManagement() {
|
||||||
(Array.isArray(value) && value.length > 0)
|
(Array.isArray(value) && value.length > 0)
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
const columns = [
|
const columns: UserTableColumn[] = [
|
||||||
{
|
{
|
||||||
id: "email",
|
id: "email",
|
||||||
header: ({ column }: any) => (
|
header: ({ column }: HeaderContext<User, User>) => (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span>Email</span>
|
<span>Email</span>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
@ -257,7 +248,7 @@ export default function UserManagement() {
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
cell: ({ row }: { row: { original: User } }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Avatar className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-medium">
|
<Avatar className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-medium">
|
||||||
{row.original.profile?.avatar ? (
|
{row.original.profile?.avatar ? (
|
||||||
|
@ -285,7 +276,7 @@ export default function UserManagement() {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "phone",
|
id: "phone",
|
||||||
header: ({ column }: any) => (
|
header: ({ column }: HeaderContext<User, User>) => (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span>Phone</span>
|
<span>Phone</span>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
@ -315,11 +306,11 @@ export default function UserManagement() {
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
cell: ({ row }: { row: { original: User } }) => row.original.phone || "-",
|
cell: ({ row }) => row.original.phone || "-",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "lastSignIn",
|
id: "lastSignIn",
|
||||||
header: ({ column }: any) => (
|
header: ({ column }: HeaderContext<User, User>) => (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span>Last Sign In</span>
|
<span>Last Sign In</span>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
@ -383,7 +374,7 @@ export default function UserManagement() {
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
cell: ({ row }: { row: { original: User } }) => {
|
cell: ({ row }) => {
|
||||||
return row.original.last_sign_in_at
|
return row.original.last_sign_in_at
|
||||||
? new Date(row.original.last_sign_in_at).toLocaleString()
|
? new Date(row.original.last_sign_in_at).toLocaleString()
|
||||||
: "Never";
|
: "Never";
|
||||||
|
@ -391,7 +382,7 @@ export default function UserManagement() {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "createdAt",
|
id: "createdAt",
|
||||||
header: ({ column }: any) => (
|
header: ({ column }: HeaderContext<User, User>) => (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span>Created At</span>
|
<span>Created At</span>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
@ -444,7 +435,7 @@ export default function UserManagement() {
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
cell: ({ row }: { row: { original: User } }) => {
|
cell: ({ row }) => {
|
||||||
return row.original.created_at
|
return row.original.created_at
|
||||||
? new Date(row.original.created_at).toLocaleString()
|
? new Date(row.original.created_at).toLocaleString()
|
||||||
: "N/A";
|
: "N/A";
|
||||||
|
@ -452,7 +443,7 @@ export default function UserManagement() {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "status",
|
id: "status",
|
||||||
header: ({ column }: any) => (
|
header: ({ column }: HeaderContext<User, User>) => (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span>Status</span>
|
<span>Status</span>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
@ -514,7 +505,7 @@ export default function UserManagement() {
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
cell: ({ row }: { row: { original: User } }) => {
|
cell: ({ row }) => {
|
||||||
if (row.original.banned_until) {
|
if (row.original.banned_until) {
|
||||||
return <Badge variant="destructive">Banned</Badge>;
|
return <Badge variant="destructive">Banned</Badge>;
|
||||||
}
|
}
|
||||||
|
@ -527,17 +518,36 @@ export default function UserManagement() {
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
header: "",
|
header: "",
|
||||||
cell: ({ row }: { row: { original: User } }) => (
|
cell: ({ row }) => (
|
||||||
<Button
|
<DropdownMenu>
|
||||||
variant="ghost"
|
<DropdownMenuTrigger asChild>
|
||||||
size="icon"
|
<Button variant="ghost" size="icon">
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleUserClick(row.original);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => handleUserUpdate(row.original)}>
|
||||||
|
<UserPen className="h-4 w-4 mr-2 text-blue-500" />
|
||||||
|
Update
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
/* handle delete */
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2 text-red-500" />
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
/* handle ban */
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ShieldAlert className="h-4 w-4 mr-2 text-yellow-500" />
|
||||||
|
{row.original.banned_until != null ? "Unban" : "Ban"}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -600,34 +610,38 @@ export default function UserManagement() {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={filteredUsers}
|
data={filteredUsers}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
onRowClick={(user) => handleUserClick(user)}
|
onRowClick={(user) => handleUserClick(user)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{selectedUser && (
|
{selectedUser && (
|
||||||
<UserDetailsSheet
|
<UserDetailSheet
|
||||||
user={selectedUser}
|
user={selectedUser}
|
||||||
open={isSheetOpen}
|
open={isSheetOpen}
|
||||||
onOpenChange={setIsSheetOpen}
|
onOpenChange={setIsSheetOpen}
|
||||||
onUserUpdate={handleUserUpdate}
|
onUserUpdate={() => {}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<AddUserDialog
|
<AddUserDialog
|
||||||
open={isAddUserOpen}
|
open={isAddUserOpen}
|
||||||
onOpenChange={setIsAddUserOpen}
|
onOpenChange={setIsAddUserOpen}
|
||||||
onUserAdded={() => refetch()}
|
onUserAdded={() => refetch()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InviteUserDialog
|
<InviteUserDialog
|
||||||
open={isInviteUserOpen}
|
open={isInviteUserOpen}
|
||||||
onOpenChange={setIsInviteUserOpen}
|
onOpenChange={setIsInviteUserOpen}
|
||||||
onUserInvited={() => refetch()}
|
onUserInvited={() => refetch()}
|
||||||
/>
|
/>
|
||||||
|
{selectedUser && (
|
||||||
|
<UserProfileSheet
|
||||||
|
open={isUpdateOpen}
|
||||||
|
onOpenChange={setIsUpdateOpen}
|
||||||
|
userData={selectedUser}
|
||||||
|
onSave={async () => {}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -28,23 +28,10 @@ function calculateUserStats(users: User[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserStats() {
|
export function UserStats() {
|
||||||
const { isLoading, setIsLoading } = useNavigations();
|
const { data: users = [], isLoading } = useQuery({
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
queryKey: ["users"],
|
||||||
|
queryFn: fetchUsers,
|
||||||
useEffect(() => {
|
});
|
||||||
const fetchUserData = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
const fetchedUsers = await fetchUsers();
|
|
||||||
setUsers(fetchedUsers);
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("Failed to fetch users");
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchUserData();
|
|
||||||
}, [setIsLoading]);
|
|
||||||
|
|
||||||
const stats = calculateUserStats(users);
|
const stats = calculateUserStats(users);
|
||||||
|
|
|
@ -36,6 +36,8 @@ export async function fetchUsers(): Promise<User[]> {
|
||||||
throw new Error("Users not found");
|
throw new Error("Users not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("fetchedUsers");
|
||||||
|
|
||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import UserManagement from "@/app/_components/admin/users/user-management";
|
import UserManagement from "@/app/(protected)/(admin)/dashboard/user-management/_components/user-management";
|
||||||
import { UserStats } from "@/app/_components/admin/users/user-stats";
|
import { UserStats } from "@/app/(protected)/(admin)/dashboard/user-management/_components/user-stats";
|
||||||
|
|
||||||
export default function UsersPage() {
|
export default function UsersPage() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -26,7 +26,7 @@ import { ThemeSwitcher } from "@/app/_components/theme-switcher";
|
||||||
import { Separator } from "@/app/_components/ui/separator";
|
import { Separator } from "@/app/_components/ui/separator";
|
||||||
import { InboxDrawer } from "@/app/_components/inbox-drawer";
|
import { InboxDrawer } from "@/app/_components/inbox-drawer";
|
||||||
import FloatingActionSearchBar from "@/app/_components/floating-action-search-bar";
|
import FloatingActionSearchBar from "@/app/_components/floating-action-search-bar";
|
||||||
import { AppSidebar } from "@/app/_components/admin/app-sidebar";
|
import { AppSidebar } from "@/app/(protected)/(admin)/_components/admin/app-sidebar";
|
||||||
import { createClient } from "@/utils/supabase/server";
|
import { createClient } from "@/utils/supabase/server";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
// "use client"
|
|
||||||
|
|
||||||
// import { useState } from "react"
|
|
||||||
// import { DataTable } from "./data-table"
|
|
||||||
// import { columns, User } from "./column"
|
|
||||||
// import { useQuery } from "react-query"
|
|
||||||
// import { getUsers } from "../../user-management/action"
|
|
||||||
// import { UserDetailSheet } from "./sheet"
|
|
||||||
|
|
||||||
|
|
||||||
// export function UsersTable() {
|
|
||||||
// const [selectedUser, setSelectedUser] = useState<User | null>(null)
|
|
||||||
// const [sheetOpen, setSheetOpen] = useState(false)
|
|
||||||
|
|
||||||
// const { data: users = [], isLoading } = useQuery({
|
|
||||||
// queryKey: ["users"],
|
|
||||||
// queryFn: getUsers,
|
|
||||||
// })
|
|
||||||
|
|
||||||
// const handleRowClick = (user: User) => {
|
|
||||||
// setSelectedUser(user)
|
|
||||||
// setSheetOpen(true)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (isLoading) {
|
|
||||||
// return <div>Loading...</div>
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <div className="w-full">
|
|
||||||
// <DataTable columns={columns} data={users} onRowClick={handleRowClick} />
|
|
||||||
// {selectedUser && <UserDetailSheet user={selectedUser} open={sheetOpen} onOpenChange={setSheetOpen} />}
|
|
||||||
// </div>
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ export default function RootLayout({
|
||||||
return (
|
return (
|
||||||
<html lang="en" className={geistSans.className} suppressHydrationWarning>
|
<html lang="en" className={geistSans.className} suppressHydrationWarning>
|
||||||
<body className="bg-background text-foreground">
|
<body className="bg-background text-foreground">
|
||||||
|
<ReactQueryProvider>
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
attribute="class"
|
attribute="class"
|
||||||
defaultTheme="system"
|
defaultTheme="system"
|
||||||
|
@ -78,6 +79,7 @@ export default function RootLayout({
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</ReactQueryProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -52,6 +52,8 @@
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tanstack/eslint-plugin-query": "^5.67.2",
|
||||||
|
"@tanstack/react-query-devtools": "^5.67.2",
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.10.2",
|
||||||
"@types/react": "^19.0.2",
|
"@types/react": "^19.0.2",
|
||||||
"@types/react-dom": "19.0.2",
|
"@types/react-dom": "19.0.2",
|
||||||
|
|
|
@ -2,12 +2,31 @@
|
||||||
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
|
|
||||||
const ReactQueryProvider = ({ children }: { children: React.ReactNode }) => {
|
const ReactQueryProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
const [queryClient] = useState(() => new QueryClient());
|
const [queryClient] = useState(
|
||||||
|
() =>
|
||||||
|
new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
// Pengaturan caching global
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
gcTime: 10 * 60 * 1000,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: 1,
|
||||||
|
retryDelay: (attemptIndex) =>
|
||||||
|
Math.min(1000 * 2 ** attemptIndex, 30000), // Exponential backoff
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
{children}
|
||||||
|
{process.env.NODE_ENV === "development" && <ReactQueryDevtools />}
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue