mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Refactoring.
This commit is contained in:
@@ -1,20 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
import {SettingsEmailTab} from "@/components/wrappers/dashboard/admin/admin-email-tab/settings-email-tab";
|
||||
import {SettingsStorageTab} from "@/components/wrappers/dashboard/admin/admin-storage-tab/settings-storage-tab";
|
||||
import {SettingsEmailTab} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/settings-email-tab";
|
||||
import {SettingsStorageTab} from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/settings-storage-tab";
|
||||
import {User, UserWithAccounts} from "@/db/schema/02_user";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useRouter, useSearchParams} from "next/navigation";
|
||||
import {AdminUsersTable} from "@/components/wrappers/dashboard/admin/admin-user-tab/admin-user-table";
|
||||
import {AdminUsersTable} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/admin-user-table";
|
||||
import {
|
||||
AdminOrganizationsTable
|
||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-organizations-tab/admin-organizations-table";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
|
||||
export type AdminTabsProps = {
|
||||
users: UserWithAccounts[];
|
||||
settings: Setting;
|
||||
organizations: OrganizationWithMembers[];
|
||||
};
|
||||
|
||||
export const AdminTabs = ({users, settings}: AdminTabsProps) => {
|
||||
export const AdminTabs = ({users, settings, organizations}: AdminTabsProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -35,6 +40,9 @@ export const AdminTabs = ({users, settings}: AdminTabsProps) => {
|
||||
<TabsTrigger className="w-full" value="users">
|
||||
Users
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className="w-full" value="organizations">
|
||||
Organizations
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className="w-full" value="email">
|
||||
Email
|
||||
</TabsTrigger>
|
||||
@@ -45,6 +53,9 @@ export const AdminTabs = ({users, settings}: AdminTabsProps) => {
|
||||
<TabsContent value="users">
|
||||
<AdminUsersTable users={users}/>
|
||||
</TabsContent>
|
||||
<TabsContent value="organizations">
|
||||
<AdminOrganizationsTable organizations={organizations}/>
|
||||
</TabsContent>
|
||||
<TabsContent value="email">
|
||||
<SettingsEmailTab settings={settings}/>
|
||||
</TabsContent>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"use client"
|
||||
|
||||
import {useState} from "react";
|
||||
import {Plus} from "lucide-react";
|
||||
|
||||
import {
|
||||
Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger
|
||||
} from "@/components/ui/dialog";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {AdminOrganizationForm} from "@/components/wrappers/dashboard/admin/organization/admin-organization-form";
|
||||
|
||||
type AdminOrganizationAddModalProps = {}
|
||||
|
||||
|
||||
export const AdminOrganizationAddModal = (props: AdminOrganizationAddModalProps) => {
|
||||
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus/> add
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>add organization</DialogTitle>
|
||||
<DialogDescription>
|
||||
your description
|
||||
</DialogDescription>
|
||||
<AdminOrganizationForm onSuccess={() => setOpen(false)}/>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ErrorContext } from "@better-fetch/fetch";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
||||
import { OrganizationSchema } from "@/components/wrappers/dashboard/admin/organization/organization.schema";
|
||||
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { authClient } from "@/lib/auth/auth-client";
|
||||
import { slugify } from "@/utils/slugify";
|
||||
|
||||
type AdminOrganizationFormProps = {
|
||||
onSuccess?: () => void;
|
||||
};
|
||||
|
||||
export const AdminOrganizationForm = ({ onSuccess }: AdminOrganizationFormProps) => {
|
||||
|
||||
const router = useRouter();
|
||||
const form = useZodForm({ schema: OrganizationSchema });
|
||||
|
||||
const mutationCreateOrganisation = useMutation({
|
||||
mutationFn: async ({ name }: OrganizationSchema) => {
|
||||
const slug = slugify(name);
|
||||
await authClient.organization.checkSlug(
|
||||
{
|
||||
slug: slug,
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
await authClient.organization.create(
|
||||
{
|
||||
name: name,
|
||||
slug: slug,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success("Organization created successfully.");
|
||||
router.refresh();
|
||||
onSuccess?.();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.error.message);
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
onError: (error: ErrorContext) => {
|
||||
toast.error(error.error.message);
|
||||
onSuccess?.();
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
await mutationCreateOrganisation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Name of your organization" {...field} value={field.value ?? ""} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex gap-4 justify-end">
|
||||
<ButtonWithLoading isPending={mutationCreateOrganisation.isPending}>Validate</ButtonWithLoading>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { AdminOrganizationList } from "@/components/wrappers/dashboard/admin/organization/admin-orgnization-list";
|
||||
import { AdminOrganizationAddModal } from "@/components/wrappers/dashboard/admin/organization/admin-organization-add-modal";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
|
||||
type AdminOrganizationSectionProps = {
|
||||
organizations: OrganizationWithMembers[];
|
||||
};
|
||||
|
||||
export const AdminOrganizationSection = ({ organizations }: AdminOrganizationSectionProps) => {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Add a new organization</CardTitle>
|
||||
<CardAction>
|
||||
<AdminOrganizationAddModal />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AdminOrganizationList organizations={organizations} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
"use client"
|
||||
import { DataTable } from "@/components/wrappers/common/table/data-table";
|
||||
import { organizationsListColumns } from "@/components/wrappers/dashboard/admin/organization/table-colums";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
|
||||
type AdminOrganizationListProps = {
|
||||
organizations: OrganizationWithMembers[];
|
||||
};
|
||||
|
||||
export const AdminOrganizationList = ({ organizations }: AdminOrganizationListProps) => {
|
||||
return <DataTable columns={organizationsListColumns()} data={organizations} enablePagination={true} enableSelect={false} />;
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
"use client"
|
||||
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {Trash2} from "lucide-react";
|
||||
import {toast} from "sonner";
|
||||
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {deleteOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action";
|
||||
|
||||
export type ButtonDeleteFleetProps = {
|
||||
text?: string;
|
||||
organisationId: string
|
||||
};
|
||||
|
||||
export const ButtonDeleteOrganization = (props: ButtonDeleteFleetProps) => {
|
||||
|
||||
const router = useRouter();
|
||||
const {data: organizations, refetch} = authClient.useListOrganizations();
|
||||
|
||||
|
||||
const mutationDeleteOrganisation = useMutation({
|
||||
mutationFn: () => deleteOrganizationAction({id: props.organisationId}),
|
||||
onSuccess: async (result) => {
|
||||
if (result?.data?.success) {
|
||||
await authClient.organization.setActive({
|
||||
organizationSlug: "default",
|
||||
});
|
||||
toast.success("Organization deleted!");
|
||||
router.refresh()
|
||||
refetch()
|
||||
} else {
|
||||
toast.error("An error occurred.");
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error("network error:", error);
|
||||
toast.error(error?.message || "A network error occurred.");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<ButtonWithConfirm
|
||||
title={props.text ? props.text : ""}
|
||||
description={"Are you sure you want to delete this organization?"}
|
||||
button={{
|
||||
main: {
|
||||
variant: "outline",
|
||||
icon: <Trash2 color="red"/>,
|
||||
},
|
||||
confirm: {
|
||||
className: "w-full",
|
||||
text: "Delete",
|
||||
icon: <Trash2/>,
|
||||
variant: "destructive",
|
||||
onClick: async () => {
|
||||
await mutationDeleteOrganisation.mutateAsync()
|
||||
},
|
||||
},
|
||||
cancel: {
|
||||
className: "w-full",
|
||||
text: "Cancel",
|
||||
icon: <Trash2/>,
|
||||
variant: "outline",
|
||||
},
|
||||
}}
|
||||
isPending={mutationDeleteOrganisation.isPending}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
"use server";
|
||||
|
||||
import { ServerActionResult } from "@/types/action-type";
|
||||
import { z } from "zod";
|
||||
import { auth } from "@/lib/auth/auth";
|
||||
import { MemberRoleType } from "@/types/common";
|
||||
import { Member } from "better-auth/plugins/organization";
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
|
||||
export const addMemberOrganizationAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
organizationId: z.string(),
|
||||
role: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({ parsedInput }): Promise<ServerActionResult<Member | null>> => {
|
||||
try {
|
||||
const data = await auth.api.addMember({
|
||||
body: {
|
||||
userId: parsedInput.userId,
|
||||
role: parsedInput.role as MemberRoleType,
|
||||
organizationId: parsedInput.organizationId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: data,
|
||||
actionSuccess: {
|
||||
message: "Member added successfully",
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "An error occurred while addinng member",
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
|
||||
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
|
||||
import {
|
||||
AddMemberSchema,
|
||||
AddMemberSchemaType
|
||||
} from "@/components/wrappers/dashboard/admin/organization/organization.schema";
|
||||
import {SearchInput} from "@/components/ui/search-input";
|
||||
import {
|
||||
addMemberOrganizationAction
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/add-member.action";
|
||||
import {toast} from "sonner";
|
||||
import {OrganizationWithMembers, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||
import {User} from "@/db/schema/02_user";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
|
||||
type OrganizationAddMemberFormProps = {
|
||||
onSuccessAction?: () => void;
|
||||
users: User[];
|
||||
organization: OrganizationWithMembersAndUsers;
|
||||
};
|
||||
|
||||
export const OrganizationAddMemberForm = ({onSuccessAction, users, organization}: OrganizationAddMemberFormProps) => {
|
||||
|
||||
const organizationMemberUserIds = organization.members.map((member) => member.user.id);
|
||||
const filteredUsers = users
|
||||
.filter((user) => !organizationMemberUserIds.includes(user.id))
|
||||
.map((user) => ({value: user.id, label: `${user.name} | ${user.email}`}));
|
||||
const router = useRouter();
|
||||
const form = useZodForm({schema: AddMemberSchema});
|
||||
|
||||
const mutationAddMemberOrganisation = useMutation({
|
||||
mutationFn: async (data: AddMemberSchemaType) => {
|
||||
console.log(data);
|
||||
const result = await addMemberOrganizationAction({
|
||||
userId: data.userId,
|
||||
organizationId: organization.id,
|
||||
role: "member",
|
||||
});
|
||||
console.log(result);
|
||||
toast.success("Member successfully added!");
|
||||
router.refresh();
|
||||
onSuccessAction?.();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
onSuccessAction?.();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
await mutationAddMemberOrganisation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="userId"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>User</FormLabel>
|
||||
<FormControl>
|
||||
<SearchInput
|
||||
name="userId"
|
||||
placeholder="Enter a user email"
|
||||
entries={filteredUsers}
|
||||
onSelect={(entySelected: any) => {
|
||||
console.log("Form selection:", entySelected);
|
||||
field.onChange(entySelected.value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex gap-4 justify-end">
|
||||
<ButtonWithLoading
|
||||
isPending={mutationAddMemberOrganisation.isPending}>Confirm</ButtonWithLoading>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { OrganizationAddMemberForm } from "@/components/wrappers/dashboard/admin/organization/details/organization-add-member-form";
|
||||
import { useState } from "react";
|
||||
import { UserPlus } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {OrganizationWithMembers, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||
import {User} from "@/db/schema/02_user";
|
||||
|
||||
type OrganizationAddMemberModalProps = {
|
||||
users: User[];
|
||||
organization: OrganizationWithMembersAndUsers;
|
||||
};
|
||||
|
||||
export const OrganizationAddMemberModal = ({ users, organization }: OrganizationAddMemberModalProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<UserPlus className="w-4 h-4 mr-2" />
|
||||
Add member
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add member to your organization</DialogTitle>
|
||||
<DialogDescription>Select a user to add to your organization</DialogDescription>
|
||||
</DialogHeader>
|
||||
<OrganizationAddMemberForm users={users} organization={organization} onSuccessAction={() => setOpen(!open)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { authClient } from "@/lib/auth/auth-client";
|
||||
import { toast } from "sonner";
|
||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
|
||||
type OrganizationDeleteMemberModalProps = {
|
||||
open: boolean;
|
||||
member: MemberWithUser;
|
||||
onOpenChangeAction: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export const OrganizationDeleteMemberModal = ({ member, open, onOpenChangeAction }: OrganizationDeleteMemberModalProps) => {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await authClient.organization.removeMember(
|
||||
{
|
||||
memberIdOrEmail: member.id,
|
||||
organizationId: member.organizationId,
|
||||
},
|
||||
{
|
||||
onSuccess: async (response) => {
|
||||
console.log(response);
|
||||
toast.success("Member successfully deleted!");
|
||||
onOpenChangeAction(false);
|
||||
router.refresh();
|
||||
},
|
||||
onError: async (error) => {
|
||||
console.log(error);
|
||||
toast.error("An error occurred while deleting member!");
|
||||
onOpenChangeAction(false);
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChangeAction}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure you want to delete {member.user.name } ?</AlertDialogTitle>
|
||||
<AlertDialogDescription>This action is irreversible: it will permanently delete this member’s data.</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<ButtonWithLoading onClick={async () => await mutation.mutateAsync()}>Validate</ButtonWithLoading>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {MoreHorizontal, Settings, Trash2} from "lucide-react";
|
||||
import {
|
||||
OrganizationDeleteMemberModal
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/organization-delete-member-modal";
|
||||
import {useState} from "react";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {
|
||||
OrganizationMemberChangeRoleModal
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/organization-member-change-role";
|
||||
import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||
|
||||
type OrganizationMemberCardProps = {
|
||||
member: MemberWithUser;
|
||||
organization: OrganizationWithMembersAndUsers;
|
||||
};
|
||||
|
||||
export const OrganizationMemberCard = ({member, organization}: OrganizationMemberCardProps) => {
|
||||
|
||||
const [isModalDeleteOpen, setIsModalDeleteOpen] = useState(false);
|
||||
const [isModalRoleOpen, setIsModalRoleOpen] = useState(false);
|
||||
const {data: session, isPending, error} = authClient.useSession();
|
||||
|
||||
if (isPending || error) return null;
|
||||
const isCurrentUser = session?.user?.id === member.user.id;
|
||||
const isOwner = member?.role === "owner";
|
||||
|
||||
return (
|
||||
<div key={member.id}
|
||||
className="flex flex-col md:flex-row md:items-center justify-between p-4 border rounded-lg">
|
||||
<OrganizationDeleteMemberModal member={member} open={isModalDeleteOpen}
|
||||
onOpenChangeAction={setIsModalDeleteOpen}/>
|
||||
<OrganizationMemberChangeRoleModal member={member} open={isModalRoleOpen}
|
||||
onOpenChangeAction={setIsModalRoleOpen}/>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Avatar>
|
||||
<AvatarImage src={member.user.image || ""} alt={member.user.name}/>
|
||||
<AvatarFallback>
|
||||
{member.user.name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="font-medium">{member.user.name}</div>
|
||||
<div className="text-sm text-muted-foreground">{member.user.email}</div>
|
||||
<div
|
||||
className="text-xs text-muted-foreground">Joined {new Date(member.createdAt).toLocaleDateString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-4 md:mt-0">
|
||||
<Badge variant={getRoleBadgeVariant(member.role)}>{member.role}</Badge>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreHorizontal className="w-4 h-4"/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onSelect={() => setIsModalRoleOpen(true)}>
|
||||
<Settings className="w-4 h-4 mr-2"/>
|
||||
Change role
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator/>
|
||||
<DropdownMenuItem onSelect={() => setIsModalDeleteOpen(true)} className="text-red-600">
|
||||
<Trash2 className="w-4 h-4 mr-2"/>
|
||||
Remove member
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getRoleBadgeVariant = (role: string) => {
|
||||
switch (role.toLowerCase()) {
|
||||
case "owner":
|
||||
return "default";
|
||||
case "admin":
|
||||
return "secondary";
|
||||
default:
|
||||
return "outline";
|
||||
}
|
||||
};
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import {useState} from "react";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {toast} from "sonner";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {MemberRoleType} from "@/types/common";
|
||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
import {updateMemberRoleAction} from "@/components/wrappers/dashboard/settings/update-member.action";
|
||||
import {RoleSchemaMember} from "@/components/wrappers/dashboard/settings/member.schema";
|
||||
import {
|
||||
updateMemberRoleAdminAction
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/role-member.action";
|
||||
|
||||
type OrganizationMemberChangeRoleModalProps = {
|
||||
open: boolean;
|
||||
member: MemberWithUser;
|
||||
onOpenChangeAction: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export const OrganizationMemberChangeRoleModal = (props: OrganizationMemberChangeRoleModalProps) => {
|
||||
const {member, open, onOpenChangeAction} = props;
|
||||
|
||||
const router = useRouter();
|
||||
const [role, setRole] = useState<MemberRoleType>(member.role as MemberRoleType);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateMemberRoleAdminAction({
|
||||
memberId: member.id,
|
||||
organizationId: member.organizationId,
|
||||
role: RoleSchemaMember.parse(role),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast.success("Member successfully updated");
|
||||
onOpenChangeAction(false);
|
||||
router.refresh();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log(error);
|
||||
toast.error("An error occurred while updating member");
|
||||
onOpenChangeAction(false);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChangeAction}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Change the user’s role</DialogTitle>
|
||||
<DialogDescription>Modify the role of this user within your organization.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Select defaultValue={member.role ?? ""} onValueChange={(role) => setRole(role as MemberRoleType)}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Sélectionnez un rôle"/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="member">Member</SelectItem>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
<SelectItem value="owner">Owner</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DialogFooter>
|
||||
<div className="flex gap-4 justify-end">
|
||||
<ButtonWithLoading
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
onOpenChangeAction(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</ButtonWithLoading>
|
||||
<ButtonWithLoading
|
||||
isPending={mutation.isPending}
|
||||
onClick={async () => {
|
||||
await mutation.mutateAsync();
|
||||
}}
|
||||
>
|
||||
Validate
|
||||
</ButtonWithLoading>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
"use server";
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {z} from "zod";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {Member} from "better-auth/plugins";
|
||||
import {RoleSchemaMember} from "@/components/wrappers/dashboard/settings/member.schema";
|
||||
import {db as dbClient} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
|
||||
|
||||
export const updateMemberRoleAdminAction = userAction.schema(
|
||||
z.object({
|
||||
memberId: z.string(),
|
||||
organizationId: z.string(),
|
||||
role: RoleSchemaMember,
|
||||
})
|
||||
).action(async ({parsedInput}): Promise<ServerActionResult<Member>> => {
|
||||
try {
|
||||
|
||||
const [updatedMember] = await dbClient
|
||||
.update(drizzleDb.schemas.member)
|
||||
.set(withUpdatedAt({
|
||||
role: parsedInput.role as string,
|
||||
}))
|
||||
.where(and(eq(drizzleDb.schemas.member.id, parsedInput.memberId), eq(drizzleDb.schemas.member.organizationId, parsedInput.organizationId)))
|
||||
.returning();
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: updatedMember,
|
||||
actionSuccess: {
|
||||
message: "Member has been successfully updated.",
|
||||
messageParams: {},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to update member role.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
|
||||
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
|
||||
import {
|
||||
UpdateOrganizationSchema,
|
||||
UpdateOrganizationSchemaType
|
||||
} from "@/components/wrappers/dashboard/admin/organization/organization.schema";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {toast} from "sonner";
|
||||
import {OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||
import {updateOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action";
|
||||
|
||||
type UpdateOrganizationFormProps = {
|
||||
onSuccessAction?: () => void;
|
||||
defaultValues: OrganizationWithMembersAndUsers;
|
||||
};
|
||||
|
||||
export const UpdateOrganizationForm = ({onSuccessAction, defaultValues}: UpdateOrganizationFormProps) => {
|
||||
|
||||
const router = useRouter();
|
||||
const {data: organizations, refetch} = authClient.useListOrganizations();
|
||||
|
||||
const isDefaultOrganization = defaultValues.slug == "default";
|
||||
|
||||
const form = useZodForm({
|
||||
schema: UpdateOrganizationSchema,
|
||||
defaultValues: defaultValues,
|
||||
disabled: isDefaultOrganization,
|
||||
});
|
||||
|
||||
|
||||
const mutationUpdateOrganisation = useMutation({
|
||||
mutationFn: ({name}: UpdateOrganizationSchemaType) => updateOrganizationAction({
|
||||
data: {
|
||||
name: name,
|
||||
users: [],
|
||||
slug: defaultValues.slug
|
||||
},
|
||||
organizationId: defaultValues.id,
|
||||
}),
|
||||
onSuccess: async (result) => {
|
||||
if (result?.data?.success) {
|
||||
toast.success("Organization updated successfully.");
|
||||
router.refresh();
|
||||
refetch()
|
||||
} else {
|
||||
// @ts-ignore
|
||||
const errorMsg = result?.data?.actionError?.message || result?.data?.actionError?.messageParams?.message || "Failed to update the organization.";
|
||||
toast.error(errorMsg);
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error("Mutation network error:", error);
|
||||
toast.error(error?.message || "A network error occurred.");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
await mutationUpdateOrganisation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="" {...field} value={field.value ?? ""}/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex gap-4 justify-end">
|
||||
<ButtonWithLoading disabled={isDefaultOrganization} isPending={mutationUpdateOrganisation.isPending}>Validate</ButtonWithLoading>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import {Building2, Shield, Users} from "lucide-react";
|
||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||
import {
|
||||
UpdateOrganizationForm
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/update-organization-form";
|
||||
import {
|
||||
OrganizationMemberCard
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/organization-member-card";
|
||||
import {useRouter, useSearchParams} from "next/navigation";
|
||||
import {useEffect, useState} from "react";
|
||||
import {capitalizeFirstLetter} from "@/utils/text";
|
||||
import {User} from "@/db/schema/02_user";
|
||||
import {
|
||||
OrganizationAddMemberModal
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/organization-add-member-modal";
|
||||
import {cn} from "@/lib/utils";
|
||||
|
||||
type OrganizationManagementProps = {
|
||||
organization: OrganizationWithMembersAndUsers;
|
||||
users: User[];
|
||||
};
|
||||
|
||||
export const OrganizationManagement = ({organization, users}: OrganizationManagementProps) => {
|
||||
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [tab, setTab] = useState<string>(() => searchParams.get("tab") ?? "members");
|
||||
|
||||
useEffect(() => {
|
||||
const newTab = searchParams.get("tab") ?? "members";
|
||||
setTab(newTab);
|
||||
}, [searchParams]);
|
||||
|
||||
const handleChangeTab = (value: string) => {
|
||||
router.push(`?tab=${value}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className=" space-y-8">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center justify-center w-12 h-12 dark:bg-gray-700 bg-gray-100 rounded-lg">
|
||||
<Building2 className="w-6 h-6 "/>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{capitalizeFirstLetter(organization.name)}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-3 md:mt-0">
|
||||
<OrganizationAddMemberModal organization={organization} users={users}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Members</CardTitle>
|
||||
<Users className="h-4 w-4 text-muted-foreground"/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{organization.members.length}</div>
|
||||
<p className="text-xs text-muted-foreground">Number of members</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Administrators</CardTitle>
|
||||
<Shield className="h-4 w-4 text-muted-foreground"/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div
|
||||
className="text-2xl font-bold">{organization.members.filter((m) => m.role === "admin" || m.role === "owner").length}</div>
|
||||
<p className="text-xs text-muted-foreground">With admin roles</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<Tabs className="space-y-6" value={tab} onValueChange={handleChangeTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="members">Members</TabsTrigger>
|
||||
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="members" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Organization members</CardTitle>
|
||||
<CardDescription>Manage who has access to your organization and their
|
||||
roles.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{organization.members.map((member: MemberWithUser) => (
|
||||
<OrganizationMemberCard key={member.id} member={member}
|
||||
organization={organization}/>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent value="settings" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Settings</CardTitle>
|
||||
<CardDescription>Organization configuration settings.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<UpdateOrganizationForm defaultValues={organization}/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import {z} from "zod";
|
||||
|
||||
export const AddMemberSchema = z.object({
|
||||
userId: z.string().min(1, "Invalid field"),
|
||||
});
|
||||
|
||||
export const UpdateOrganizationSchema = z.object({
|
||||
name: z.string().min(5),
|
||||
});
|
||||
|
||||
export const OrganizationSchema = z.object({
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
export const OrganizationInvitationSchema = z.object({
|
||||
email: z.string(),
|
||||
invitedByUsername: z.string(),
|
||||
invitedByEmail: z.string(),
|
||||
teamName: z.string(),
|
||||
inviteLink: z.string()
|
||||
});
|
||||
|
||||
export type OrganizationInvitationType = z.infer<typeof OrganizationInvitationSchema>;
|
||||
export type OrganizationSchema = z.infer<typeof OrganizationSchema>;
|
||||
export type UpdateOrganizationSchemaType = z.infer<typeof UpdateOrganizationSchema>;
|
||||
export type AddMemberSchemaType = z.infer<typeof AddMemberSchema>;
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
import {ColumnDef} from "@tanstack/react-table";
|
||||
import {ButtonDeleteOrganization} from "@/components/wrappers/dashboard/admin/organization/button-delete-organization";
|
||||
import Link from "next/link";
|
||||
import {Settings} from "lucide-react";
|
||||
import {buttonVariants} from "@/components/ui/button";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
|
||||
export function organizationsListColumns(): ColumnDef<OrganizationWithMembers>[] {
|
||||
|
||||
return [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
},
|
||||
{
|
||||
accessorKey: "members",
|
||||
header: "Members",
|
||||
cell: ({row}) => {
|
||||
const membersCount = row.original.members?.length;
|
||||
return <div className="flex items-center gap-3">{membersCount}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
id: "actions",
|
||||
cell: ({row}) => {
|
||||
const isDefaultOrganization = row.original.slug == "default";
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
{!isDefaultOrganization && (
|
||||
<ButtonDeleteOrganization organisationId={row.original.id}/>
|
||||
)}
|
||||
<Link className={buttonVariants({variant: "outline"})}
|
||||
href={`admin/organization/${row.original.id}`}>
|
||||
<Settings/>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
import { z } from "zod";
|
||||
import { EmailFormSchema } from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.schema";
|
||||
import { EmailFormSchema } from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
+2
-2
@@ -19,11 +19,11 @@ import {TooltipProvider} from "@/components/ui/tooltip";
|
||||
import {
|
||||
EmailFormSchema,
|
||||
EmailFormType
|
||||
} from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.schema";
|
||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
|
||||
import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input";
|
||||
import {
|
||||
updateEmailSettingsAction
|
||||
} from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.action";
|
||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.action";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
|
||||
+12
-13
@@ -1,13 +1,13 @@
|
||||
import { EmailForm } from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form";
|
||||
import { Send } from "lucide-react";
|
||||
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { sendEmail } from "@/utils/email-helper";
|
||||
import TestEmailSettings from "../../../../../../emails/TestEmailSettings";
|
||||
import { render } from "@react-email/render";
|
||||
import { toast } from "sonner";
|
||||
import {EmailForm} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form";
|
||||
import {Send} from "lucide-react";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {sendEmail} from "@/utils/email-helper";
|
||||
import {render} from "@react-email/render";
|
||||
import {toast} from "sonner";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {EmailFormType} from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.schema";
|
||||
import {EmailFormType} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
|
||||
import TestEmailSettings from "../../../../../../../emails/TestEmailSettings";
|
||||
|
||||
export type SettingsEmailTabProps = {
|
||||
settings: Setting;
|
||||
@@ -47,14 +47,13 @@ export const SettingsEmailTab = (props: SettingsEmailTabProps) => {
|
||||
onClick={async () => {
|
||||
await handleSendMailTest();
|
||||
}}
|
||||
icon={<Send />}
|
||||
text="Send email test"
|
||||
icon={<Send/>}
|
||||
size="default"
|
||||
/>
|
||||
>Send email test</ButtonWithLoading>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<EmailForm defaultValues={props.settings.smtpFrom ? props.settings as EmailFormType : undefined } />
|
||||
<EmailForm defaultValues={props.settings.smtpFrom ? props.settings as EmailFormType : undefined}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
"use client"
|
||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {AdminOrganizationList} from "@/components/wrappers/dashboard/admin/organization/admin-orgnization-list";
|
||||
|
||||
export type AdminOrganizationsTableProps = {
|
||||
organizations: OrganizationWithMembers[];
|
||||
|
||||
};
|
||||
|
||||
export const AdminOrganizationsTable = (props: AdminOrganizationsTableProps) => {
|
||||
const {organizations} = props;
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4 h-full py-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Active organizations</CardTitle>
|
||||
<CardDescription>Manage all system organizations</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AdminOrganizationList organizations={organizations} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
"use client"
|
||||
import {ColumnDef} from "@tanstack/react-table";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {updateUserAction} from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useState} from "react";
|
||||
import {Trash2} from "lucide-react";
|
||||
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/button-delete-account/delete-account.action";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {UserWithAccounts} from "@/db/schema/02_user";
|
||||
import {authClient, useSession} from "@/lib/auth/auth-client";
|
||||
import {formatFrenchDate} from "@/utils/date-formatting";
|
||||
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
|
||||
import {ButtonDeleteUser} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/button-delete-use";
|
||||
import {Organization} from "@/db/schema/03_organization";
|
||||
|
||||
export const organizationsColumnsAdmin: ColumnDef<Organization>[] = [
|
||||
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
},
|
||||
|
||||
// {
|
||||
// header: "Action",
|
||||
// id: "actions",
|
||||
// cell: ({row}) => {
|
||||
// const router = useRouter();
|
||||
// const {data: session, isPending} = useSession();
|
||||
// const isSuperAdmin = session?.user.role == "superadmin";
|
||||
//
|
||||
// return (
|
||||
// <ButtonDeleteUser
|
||||
// disabled={!isSuperAdmin || !session || session?.user.email === row.original.email}
|
||||
// userId={row.original.id}/>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
];
|
||||
+4
-6
@@ -2,7 +2,7 @@ import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
|
||||
import {Info, ShieldCheck} from "lucide-react";
|
||||
import {Switch} from "@/components/ui/switch";
|
||||
import {Label} from "@/components/ui/label";
|
||||
import {StorageS3Form} from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/storage-s3-form";
|
||||
import {StorageS3Form} from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/storage-s3-form";
|
||||
import {useState} from "react";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
@@ -11,9 +11,9 @@ import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {
|
||||
updateStorageSettingsAction
|
||||
} from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.action";
|
||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.action";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {S3FormType} from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.schema";
|
||||
import {S3FormType} from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.schema";
|
||||
|
||||
export type SettingsStorageTabProps = {
|
||||
settings: Setting;
|
||||
@@ -93,9 +93,7 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
|
||||
onClick={async () => {
|
||||
await mutation.mutateAsync();
|
||||
}}
|
||||
icon={<ShieldCheck/>}
|
||||
text="Test connexion"
|
||||
/>
|
||||
icon={<ShieldCheck/>}>Test connexion</ButtonWithLoading>
|
||||
</div>
|
||||
</div>
|
||||
{isSwitched && (
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
import { z } from "zod";
|
||||
import { db } from "@/db";
|
||||
import { S3FormSchema, StorageSwitchSchema } from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.schema";
|
||||
import { S3FormSchema, StorageSwitchSchema } from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
+2
-2
@@ -8,10 +8,10 @@ import { Button } from "@/components/ui/button";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
|
||||
import { S3FormSchema, S3FormType } from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.schema";
|
||||
import { S3FormSchema, S3FormType } from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.schema";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { updateS3SettingsAction } from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.action";
|
||||
import { updateS3SettingsAction } from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.action";
|
||||
import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input";
|
||||
|
||||
export type S3FormProps = {
|
||||
-1
@@ -66,7 +66,6 @@ export const accountsColumns: ColumnDef<{
|
||||
<div className="flex items-center gap-2">
|
||||
<ButtonWithLoading
|
||||
variant="outline"
|
||||
text=""
|
||||
disabled={row.original.provider === "credential" || table.getRowModel().rows.length <= 1}
|
||||
icon={<Unlink color="red" size={15}/>}
|
||||
onClick={async () => {
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import {UserWithAccounts} from "@/db/schema/02_user";
|
||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
||||
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/admin-user-tab/columns-users";
|
||||
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/columns-users";
|
||||
|
||||
export type AdminUsersTableProps = {
|
||||
users: UserWithAccounts[];
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import {Trash2} from "lucide-react";
|
||||
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {toast} from "sonner";
|
||||
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/button-delete-account/delete-account.action";
|
||||
|
||||
export type ButtonDeleteUserProps = {
|
||||
userId: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const ButtonDeleteUser = (props: ButtonDeleteUserProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => deleteUserAction(props.userId),
|
||||
onSuccess: async () => {
|
||||
toast.success("User deleted successfully.");
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
<ButtonWithConfirm
|
||||
title={""}
|
||||
|
||||
description="Are you sure you want to remove this user? This action cannot be undone."
|
||||
button={{
|
||||
main: {
|
||||
disabled: !!props.disabled,
|
||||
text: "",
|
||||
variant: "outline",
|
||||
size: "sm",
|
||||
icon: <Trash2 color="red" size={15}/>,
|
||||
},
|
||||
confirm: {
|
||||
className: "w-full",
|
||||
text: "Delete",
|
||||
icon: <Trash2/>,
|
||||
variant: "destructive",
|
||||
onClick: () => {
|
||||
mutation.mutate()
|
||||
},
|
||||
},
|
||||
cancel: {
|
||||
className: "w-full",
|
||||
text: "Cancel",
|
||||
icon: <Trash2/>,
|
||||
variant: "outline",
|
||||
},
|
||||
}}
|
||||
isPending={mutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+4
-22
@@ -13,6 +13,7 @@ import {UserWithAccounts} from "@/db/schema/02_user";
|
||||
import {authClient, useSession} from "@/lib/auth/auth-client";
|
||||
import {formatFrenchDate} from "@/utils/date-formatting";
|
||||
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
|
||||
import {ButtonDeleteUser} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/button-delete-use";
|
||||
|
||||
export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
|
||||
{
|
||||
@@ -98,29 +99,10 @@ export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
|
||||
const {data: session, isPending} = useSession();
|
||||
const isSuperAdmin = session?.user.role == "superadmin";
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => deleteUserAction(row.original.id),
|
||||
onSuccess: async () => {
|
||||
toast.success("User deleted successfully.");
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<ButtonWithLoading
|
||||
disabled={!isSuperAdmin || !session || session?.user.email === row.original.email}
|
||||
variant="outline"
|
||||
text=""
|
||||
icon={<Trash2 color="red" size={15}/>}
|
||||
onClick={async () => {
|
||||
await mutation.mutateAsync();
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<ButtonDeleteUser
|
||||
disabled={!isSuperAdmin || !session || session?.user.email === row.original.email}
|
||||
userId={row.original.id}/>
|
||||
);
|
||||
},
|
||||
},
|
||||
-1
@@ -73,7 +73,6 @@ export const sessionsColumns: ColumnDef<Session>[] = [
|
||||
<ButtonWithLoading
|
||||
variant="outline"
|
||||
disabled={session?.session.id === row.original.id}
|
||||
text=""
|
||||
icon={<Unlink color="red" size={15} />}
|
||||
onClick={async () => {
|
||||
await mutation.mutateAsync();
|
||||
Reference in New Issue
Block a user