diff --git a/app/(customer)/dashboard/[slug]/projects/[projectId]/page.tsx b/app/(customer)/dashboard/[slug]/projects/[projectId]/page.tsx index d7a24ec5..3677171b 100644 --- a/app/(customer)/dashboard/[slug]/projects/[projectId]/page.tsx +++ b/app/(customer)/dashboard/[slug]/projects/[projectId]/page.tsx @@ -8,11 +8,21 @@ import {ButtonDeleteProject} from "@/components/wrappers/dashboard/projects/Butt import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination"; import {ProjectDatabaseCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard"; import {notFound} from "next/navigation"; +import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie"; -export default async function RoutePage(props: PageParams<{ projectId: string }>) { +export default async function RoutePage(props: PageParams<{ slug:string, projectId: string }>) { + + const {slug: organizationSlug, projectId} = await props.params + + + const currentOrganizationSlug = await getCurrentOrganizationSlug() + + if(currentOrganizationSlug != organizationSlug) { + notFound() + } + - const {projectId} = await props.params const project = await prisma.project.findUnique({ where: { @@ -33,7 +43,7 @@ export default async function RoutePage(props: PageParams<{ projectId: string }> {project.name} + href={`/dashboard/${currentOrganizationSlug}/projects/${project.id}/edit`}> @@ -51,6 +61,7 @@ export default async function RoutePage(props: PageParams<{ projectId: string }> {project.databases.length > 0 ? ) { +export default async function RoutePage(props: PageParams<{slug: string}>) { + const {slug: organizationSlug} = await props.params + const currentOrganizationSlug = await getCurrentOrganizationSlug() + if(currentOrganizationSlug != organizationSlug) { + notFound() + } + const availableDatabases = await prisma.database.findMany({ where: { @@ -20,7 +28,7 @@ export default async function RoutePage(props: PageParams<{}>) { const organization = await prisma.organization.findFirst({ where: { - slug: 'default', + slug: currentOrganizationSlug, } }) diff --git a/app/(customer)/dashboard/[slug]/projects/page.tsx b/app/(customer)/dashboard/[slug]/projects/page.tsx index 9c270ba8..1b0e3d19 100644 --- a/app/(customer)/dashboard/[slug]/projects/page.tsx +++ b/app/(customer)/dashboard/[slug]/projects/page.tsx @@ -47,7 +47,7 @@ export default async function RoutePage(props: PageParams<{slug: string}>) { {projects.length > 0 && ( - + @@ -59,6 +59,7 @@ export default async function RoutePage(props: PageParams<{slug: string}>) { {projects.length > 0 ? ) { /> : Create new Project diff --git a/app/(customer)/dashboard/[slug]/settings/edit/page.tsx b/app/(customer)/dashboard/[slug]/settings/edit/page.tsx new file mode 100644 index 00000000..b1d34b54 --- /dev/null +++ b/app/(customer)/dashboard/[slug]/settings/edit/page.tsx @@ -0,0 +1,53 @@ +import {PageParams} from "@/types/next"; +import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; +import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie"; +import {prisma} from "@/prisma"; +import {requiredCurrentUser} from "@/auth/current-user"; +import {notFound} from "next/navigation"; +import {OrganizationForm} from "@/components/wrappers/dashboard/organization/OrganizationForm/OrganizationForm"; + + +export default async function RoutePage(props: PageParams<{ + slug: string; +}>) { + const {slug: organizationSlug} = await props.params + const currentOrganizationSlug = await getCurrentOrganizationSlug() + + if(currentOrganizationSlug != organizationSlug) { + notFound() + } + + const organization = await prisma.organization.findUnique({ + where:{ + slug: currentOrganizationSlug, + }, + include:{ + users:{ + include:{ + user:{} + } + } + } + }) + + const users = await prisma.user.findMany({ + where: { + deleted: {not: true}, + } + }) + + + + return( + + + + Edit {organization.name} + + + + + + + ) +} \ No newline at end of file diff --git a/app/(customer)/dashboard/[slug]/settings/page.tsx b/app/(customer)/dashboard/[slug]/settings/page.tsx index 7f181736..72854787 100644 --- a/app/(customer)/dashboard/[slug]/settings/page.tsx +++ b/app/(customer)/dashboard/[slug]/settings/page.tsx @@ -4,11 +4,14 @@ import {Page, PageActions, PageContent, PageDescription, PageHeader, PageTitle} import {requiredCurrentUser} from "@/auth/current-user"; import {SettingsTabs} from "@/components/wrappers/dashboard/settings/SettingsTabs/SettingsTabs"; import {getCurrentOrganizationId, getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie"; -import {Button} from "@/components/ui/button"; +import {Button, buttonVariants} from "@/components/ui/button"; import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm"; import { DeleteOrganizationButton } from "@/components/wrappers/dashboard/organization/DeleteOrganization/DeleteOrganizationButton"; +import {GearIcon} from "@radix-ui/react-icons"; +import Link from "next/link"; +import {EditButtonSettings} from "@/components/wrappers/dashboard/settings/EditButtonSettings/EditButtonSettings"; export default async function RoutePage(props: PageParams<{}>) { @@ -17,7 +20,7 @@ export default async function RoutePage(props: PageParams<{}>) { const currentOrganizationSlug = await getCurrentOrganizationSlug() const organization = await prisma.organization.findUnique({ - where:{ + where: { slug: currentOrganizationSlug, } }) @@ -35,6 +38,7 @@ export default async function RoutePage(props: PageParams<{}>) { }) + const settings = await prisma.settings.findUnique({ where: { name: "system" @@ -44,12 +48,16 @@ export default async function RoutePage(props: PageParams<{}>) { return ( - + Settings + {organization.slug != "default" ? + + : null} - {/**/} - + {organization.slug != "default" ? + + : null} diff --git a/app/(customer)/dashboard/admin/page.tsx b/app/(customer)/dashboard/admin/page.tsx index c157058f..788d6c1d 100644 --- a/app/(customer)/dashboard/admin/page.tsx +++ b/app/(customer)/dashboard/admin/page.tsx @@ -38,7 +38,6 @@ export default async function RoutePage(props: PageParams<{}>) { - ) diff --git a/src/components/wrappers/common/cards-with-pagination.tsx b/src/components/wrappers/common/cards-with-pagination.tsx index 466fd63c..3c2dca82 100644 --- a/src/components/wrappers/common/cards-with-pagination.tsx +++ b/src/components/wrappers/common/cards-with-pagination.tsx @@ -7,6 +7,7 @@ import {PaginationNavigation} from "@/components/wrappers/common/pagination/pagi export type cardsWithPaginationProps = { className?: string + organizationSlug?:string data: Array<{}>; cardItem: React.ComponentType; cardsPerPage?: number @@ -18,7 +19,7 @@ export type cardsWithPaginationProps = { export const CardsWithPagination = (props: cardsWithPaginationProps) => { - const {className, data, cardItem, cardsPerPage = 5, numberOfColumns = 1, maxVisiblePages = 3} = props + const {className,organizationSlug, data, cardItem, cardsPerPage = 5, numberOfColumns = 1, maxVisiblePages = 3} = props const CardItem = cardItem @@ -45,7 +46,7 @@ export const CardsWithPagination = (props: cardsWithPaginationProps) => {
{currentCards.map((card, key) => ( - + ))}
{ const {currentUser, users} = props; - return (
@@ -22,7 +21,7 @@ export const AdminUsersTable = (props: AdminUsersTableProps) => {
[] = [ +export const usersColumnsAdmin: ColumnDef[] = [ { accessorKey: "role", header: "Role", @@ -55,10 +55,10 @@ export const usersColumns: ColumnDef[] = [ header: "Email" }, { - accessorKey: "createdAt", - header: "Created At", + accessorKey: "updatedAt", + header: "Updated At", cell: ({row}) => { - return new Date(row.getValue("createdAt")).toLocaleString("fr-FR"); + return new Date(row.getValue("updatedAt")).toLocaleString("fr-FR"); }, }, { diff --git a/src/components/wrappers/dashboard/organization/OrganizationForm/OrganizationForm.tsx b/src/components/wrappers/dashboard/organization/OrganizationForm/OrganizationForm.tsx new file mode 100644 index 00000000..8ed9f951 --- /dev/null +++ b/src/components/wrappers/dashboard/organization/OrganizationForm/OrganizationForm.tsx @@ -0,0 +1,162 @@ +"use client"; + +import {Card, CardContent, CardHeader} from "@/components/ui/card"; +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, + useZodForm +} from "@/components/ui/form"; +import {Input} from "@/components/ui/input"; +import {Form} from "@/components/ui/form" +import {Button} from "@/components/ui/button"; +import {useMutation} from "@tanstack/react-query"; +import {useRouter} from "next/navigation"; +import {Organization, User} from "@prisma/client" +import {MultiSelect} from "@/components/wrappers/common/multiSelect/MultiSelect"; +import { + OrganizationFormSchema, + OrganizationFormType +} from "@/components/wrappers/dashboard/organization/OrganizationForm/organization-form.schema"; +import { + createOrganizationAction, + updateOrganizationAction +} from "@/components/wrappers/dashboard/organization/organization.action"; +import {toast} from "sonner"; + + +export type organizationFormProps = { + defaultValues?: Organization; + users: User[] + +} + +export const OrganizationForm = (props: organizationFormProps) => { + + const router = useRouter(); + const isCreate = !Boolean(props.defaultValues) + + + const formatUsersList = (users: User[]) => { + return users.map(user => ({ + value: user.id, + label: `${user.name} | ${user.email}`, + })); + }; + + const formatDefaultUsers = (users: OrganizationFormType['users']): string[] => { + console.log(users) + return users.map(user => user.userId ); + }; + + const formattedDefaultValues = { + ...props.defaultValues, + users: !isCreate ? formatDefaultUsers(props.defaultValues?.users) : [] + } + + + const form = useZodForm({ + schema: OrganizationFormSchema, + defaultValues: formattedDefaultValues, + + }); + + const mutation = useMutation({ + mutationFn: async (values: OrganizationFormType) => { + console.log(values) + const organization = await updateOrganizationAction({data: values, organizationId: props.defaultValues.id}) + console.log(organization) + if (organization.data.success) { + toast.success(organization.data.actionSuccess.message); + router.push(`/dashboard/${organization.data.value.slug}/settings`); + router.refresh() + } else { + toast.success(organization.data.actionError.message); + } + + + + } + }) + + return ( + + + + + +
{ + await mutation.mutateAsync(values); + }} + > + ( + + Name + + + + + + )} + /> + ( + + Slug + + { + const value = e.target.value.replaceAll(" ", "-").toLowerCase() + field.onChange(value) + }}/> + + + + )} + /> + ( + + Databases + + + + + Select users you want to add to this organization + + + ) + } + /> + + +
+
+ ) +} \ No newline at end of file diff --git a/src/components/wrappers/dashboard/organization/OrganizationForm/organization-form.schema.ts b/src/components/wrappers/dashboard/organization/OrganizationForm/organization-form.schema.ts new file mode 100644 index 00000000..9d136cc5 --- /dev/null +++ b/src/components/wrappers/dashboard/organization/OrganizationForm/organization-form.schema.ts @@ -0,0 +1,14 @@ +import {z} from "zod"; + + +export const OrganizationFormSchema = z.object({ + name: z.string().min(5, 'Name must be at least 5 characters long').max(40, 'Name must be at most 40 characters long'), + slug: z.string() + .regex(/^[a-zA-Z0-9_-]*$/, 'Slug can only contain letters, numbers, underscores, and hyphens') + .min(5, 'Slug must be at least 5 characters long') + .max(20, 'Slug must be at most 20 characters long'), + users: z.array(z.string()), + +}); + +export type OrganizationFormType = z.infer; diff --git a/src/components/wrappers/dashboard/organization/organization.action.ts b/src/components/wrappers/dashboard/organization/organization.action.ts index 4e67e318..4b22e4c6 100644 --- a/src/components/wrappers/dashboard/organization/organization.action.ts +++ b/src/components/wrappers/dashboard/organization/organization.action.ts @@ -8,6 +8,10 @@ import {Organization} from "@prisma/client"; import {z} from "zod"; import {v4 as uuidv4} from "uuid"; import {db} from "@/db"; +import { + OrganizationFormSchema +} from "@/components/wrappers/dashboard/organization/OrganizationForm/organization-form.schema"; +import {ProjectSchema} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm.schema"; const verifySlugUniqueness = async (slug: string) => { @@ -71,6 +75,95 @@ export const createOrganizationAction = userAction +export const updateOrganizationAction = userAction + .schema( + z.object({ + data: OrganizationFormSchema, + organizationId: z.string() + })) + .action(async ({parsedInput, ctx}): Promise> => { + try { + + const newUserList = parsedInput.data.users + + const organization = await prisma.organization.findFirst({ + where:{ + id: parsedInput.organizationId, + }, + include: { + users : {} + } + }) + + const existingItemIds = organization.users.map((user) => user.userId); + const usersToAdd = newUserList.filter( + (id) => !existingItemIds.includes(id) + ); + const usersToRemove = existingItemIds.filter( + (id) => !newUserList.includes(id) + ); + + console.log(usersToAdd); + console.log(usersToRemove); + + + if (usersToAdd.length > 0) { + + await prisma.userOrganization.createMany({ + data: usersToAdd.map((userId) => ({ + userId: userId, + organizationId: organization.id, + })), + skipDuplicates: true, // Optional: to avoid duplicate insertion errors + }); + } + if (usersToRemove.length > 0) { + await prisma.userOrganization.deleteMany({ + where: { + userId: { in: usersToRemove }, + }, + + }); + } + + const updatedOrganization = await prisma.organization.update({ + where:{ + id: organization.id + }, + data:{ + name: parsedInput.data.name, + slug: parsedInput.data.slug, + } + }) + + + return { + success: true, + value: updatedOrganization, + actionSuccess: { + message: "Organization has been successfully updated.", + messageParams: {organizationId: updatedOrganization.id}, + }, + }; + + } catch (error) { + return { + success: false, + actionError: { + message: "Failed to update organization.", + status: 500, + cause: error.message ?? "Unknown error", + messageParams: {message: "Error updating the organization"}, + }, + }; + } + + }); + + + + + export const deleteOrganizationAction = userAction .schema(z.string()) .action(async ({parsedInput, ctx}): Promise> => { diff --git a/src/components/wrappers/dashboard/projects/ProjectCard/ProjectCard.tsx b/src/components/wrappers/dashboard/projects/ProjectCard/ProjectCard.tsx index f80a9a98..20ce73e3 100644 --- a/src/components/wrappers/dashboard/projects/ProjectCard/ProjectCard.tsx +++ b/src/components/wrappers/dashboard/projects/ProjectCard/ProjectCard.tsx @@ -4,16 +4,17 @@ import {Card, CardContent, CardHeader} from "@/components/ui/card"; import Link from "next/link"; export type projectCardProps = { - data: any + data: any, + organizationSlug?: string } export const ProjectCard = (props: projectCardProps) => { - const {data: project} = props; + const {data: project, organizationSlug} = props; return ( - +
{project.name} diff --git a/src/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard.tsx b/src/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard.tsx index 3dce8129..235dcf7e 100644 --- a/src/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard.tsx +++ b/src/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard.tsx @@ -10,14 +10,15 @@ import {formatDateLastContact} from "@/utils/date-formatting"; export type projectDatabaseCardProps = { data: Database, extendedProps: any + organizationSlug: string } export const ProjectDatabaseCard = (props: projectDatabaseCardProps) => { - const {data: database, extendedProps: extendedProps} = props; + const {organizationSlug,data: database, extendedProps: extendedProps} = props; return ( - + ) diff --git a/src/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm.tsx b/src/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm.tsx index c8c23fce..a9e20c4c 100644 --- a/src/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm.tsx +++ b/src/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm.tsx @@ -77,7 +77,7 @@ export const ProjectForm = (props: projectFormProps) => { if (project.data.success) { toast.success(project.data.actionSuccess.message); - router.push(`/dashboard/projects/${project.data.value.id}`); + router.push(`/dashboard/${props.organization.slug}/projects/${project.data.value.id}`); router.refresh() } else { toast.success(project.data.actionError.message); diff --git a/src/components/wrappers/dashboard/settings/EditButtonSettings/EditButtonSettings.tsx b/src/components/wrappers/dashboard/settings/EditButtonSettings/EditButtonSettings.tsx new file mode 100644 index 00000000..1cd1ce5b --- /dev/null +++ b/src/components/wrappers/dashboard/settings/EditButtonSettings/EditButtonSettings.tsx @@ -0,0 +1,21 @@ +"use client" +import {buttonVariants} from "@/components/ui/button"; +import {GearIcon} from "@radix-ui/react-icons"; +import Link from "next/link"; +import {usePathname} from "next/navigation"; + +export type EditButtonSettings = {} + +export const EditButtonSettings= (props:EditButtonSettings) => { + + const pathname = usePathname(); + + + return( + + + + ) +} \ No newline at end of file diff --git a/src/components/wrappers/dashboard/settings/SettingsTabs/SettingsTabs.tsx b/src/components/wrappers/dashboard/settings/SettingsTabs/SettingsTabs.tsx index fd91319a..4f85f938 100644 --- a/src/components/wrappers/dashboard/settings/SettingsTabs/SettingsTabs.tsx +++ b/src/components/wrappers/dashboard/settings/SettingsTabs/SettingsTabs.tsx @@ -2,7 +2,7 @@ import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs"; import {DataTableWithPagination} from "@/components/wrappers/common/table/data-table-with-pagination"; -import {usersColumns} from "@/components/wrappers/dashboard/settings/SettingsUsersTab/columns-users"; +import {usersColumns} from "@/components/wrappers/dashboard/settings/SettingsUsersTab/columns-users-settings"; import {User, Settings} from "@prisma/client"; import {backupColumns} from "@/features/backup/columns"; import {SettingsEmailTab} from "@/components/wrappers/dashboard/admin/AdminEmailTab/SettingsEmailTab"; @@ -21,34 +21,6 @@ export const SettingsTabs = (props: SettingsTabsProps) => { const {currentUser, users} = props; return ( - - // - // - // Info - // Users - // Email - // Storage - // - // - //
- //
- //
- //
- //
- //
- //
- //
- // - // - // - // - // - // - // - // - // - // - ) } diff --git a/src/components/wrappers/dashboard/settings/SettingsUsersTab/SettingsUsersTab.tsx b/src/components/wrappers/dashboard/settings/SettingsUsersTab/SettingsUsersTab.tsx index 27b78d40..7bf1601b 100644 --- a/src/components/wrappers/dashboard/settings/SettingsUsersTab/SettingsUsersTab.tsx +++ b/src/components/wrappers/dashboard/settings/SettingsUsersTab/SettingsUsersTab.tsx @@ -1,6 +1,6 @@ import {User} from "@prisma/client"; import {DataTableWithPagination} from "@/components/wrappers/common/table/data-table-with-pagination"; -import {usersColumns} from "@/components/wrappers/dashboard/settings/SettingsUsersTab/columns-users"; +import {usersColumns} from "@/components/wrappers/dashboard/settings/SettingsUsersTab/columns-users-settings"; import {UsersDataTable} from "@/components/wrappers/dashboard/admin/admin-user-table"; diff --git a/src/components/wrappers/dashboard/settings/SettingsUsersTab/columns-users-settings.tsx b/src/components/wrappers/dashboard/settings/SettingsUsersTab/columns-users-settings.tsx new file mode 100644 index 00000000..09f46bf3 --- /dev/null +++ b/src/components/wrappers/dashboard/settings/SettingsUsersTab/columns-users-settings.tsx @@ -0,0 +1,64 @@ +"use client" + +import {ColumnDef} from "@tanstack/react-table" +import {Badge} from "@/components/ui/badge"; +import {User} from "@prisma/client"; +import {updateUserAction} from "@/components/wrappers/dashboard/profile/UserForm/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/ButtonDeleteAccount/delete-account.action"; +import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading"; + +export const usersColumns: ColumnDef[] = [ + { + accessorKey: "role", + header: "Role", + cell: ({row}) => { + const router = useRouter(); + const [role, setRole] = useState(row.getValue("role")) + + const updateMutation = useMutation({ + mutationFn: () => updateUserAction({id: row.original.id, data: {role: role}}), + onSuccess: () => { + toast.success(`User updated successfully.`); + router.refresh() + + }, + onError: () => { + toast.error(`An error occurred while updating user information.`); + }, + }); + + const handleUpdateRole = async () => { + const nextRole = role === "admin" ? "pending" + : role === "pending" ? "user" + : "admin"; + setRole(nextRole); + await updateMutation.mutateAsync() + }; + + return handleUpdateRole()} + variant="outline">{role} + }, + }, + { + accessorKey: "name", + header: "Name" + }, + { + accessorKey: "email", + header: "Email" + }, + { + accessorKey: "updatedAt", + header: "Updated At", + cell: ({row}) => { + return new Date(row.getValue("updatedAt")).toLocaleString("fr-FR"); + }, + } +] \ No newline at end of file