Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 61 additions & 14 deletions apps/app/src/actions/people/create-employee-action.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use server";

import { db } from "@bubba/db";
import { type Employee, db } from "@bubba/db";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
import { authActionClient } from "../safe-action";
import { createEmployeeSchema } from "../schema";
Expand Down Expand Up @@ -46,20 +46,56 @@ export const createEmployeeAction = authActionClient
}

try {
// Create the employee
const employee = await db.employee.create({
data: {
name,
email,
department,
organizationId: user.organizationId,
isActive: true,
externalEmployeeId,
// First check if an employee exists (active or inactive)
const existingEmployee = await db.employee.findUnique({
where: {
email_organizationId: {
email,
organizationId: user.organizationId,
},
},
});

const portalUser = await db.portalUser.create({
data: {
let employee: Employee;

if (existingEmployee) {
if (existingEmployee.isActive) {
return {
success: false,
error:
"An employee with this email already exists in your organization",
};
}

// Reactivate the existing employee
employee = await db.employee.update({
where: { id: existingEmployee.id },
data: {
name,
department,
isActive: true,
externalEmployeeId,
organizationId: user.organizationId,
updatedAt: new Date(),
},
});
} else {
employee = await db.employee.create({
data: {
name,
email,
department,
organizationId: user.organizationId,
isActive: true,
externalEmployeeId,
},
});
}

// Update or create portalUser
const portalUser = await db.portalUser.upsert({
where: { email },
create: {
id: employee.id,
name,
email,
Expand All @@ -73,6 +109,17 @@ export const createEmployeeAction = authActionClient
},
},
},
update: {
updatedAt: new Date(),
name,
email,
organizationId: user.organizationId,
employees: {
connect: {
id: employee.id,
},
},
},
});

// Create or get the required task definitions first and store their IDs
Expand All @@ -87,7 +134,7 @@ export const createEmployeeAction = authActionClient
},
update: {},
});
})
}),
);

// Now create the employee tasks using the actual task IDs
Expand All @@ -100,7 +147,7 @@ export const createEmployeeAction = authActionClient
status: "assigned",
},
});
})
}),
);

return {
Expand Down
13 changes: 3 additions & 10 deletions apps/portal/src/app/[locale]/(app)/unauthorized/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import { auth } from "@/app/lib/auth";
import { headers } from "next/headers";

export default async function Unauthorized() {
const session = await auth.api.getSession({
headers: await headers(),
});

return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<div>
<h1>
We couldn't find an organization for you. Please contact your
administrator.
</main>
</h1>
</div>
);
}
12 changes: 1 addition & 11 deletions apps/portal/src/app/components/header.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,19 @@
import { UserMenu } from "@/app/components/user-menu";
import { getI18n } from "@/app/locales/server";
import { Skeleton } from "@bubba/ui/skeleton";
import { headers } from "next/headers";
import { Suspense } from "react";
import { auth } from "../lib/auth";
import { MobileMenu } from "./mobile-menu";

export async function Header() {
const session = await auth.api.getSession({
headers: await headers(),
});

const t = await getI18n();

return (
<header className="-ml-4 -mr-4 md:m-0 z-10 px-4 md:px-0 md:border-b-[1px] flex justify-between pt-4 pb-2 md:pb-4 items-center todesktop:sticky todesktop:top-0 todesktop:bg-background todesktop:border-none sticky md:static top-0 backdrop-filter backdrop-blur-xl md:backdrop-filter md:backdrop-blur-none bg-opacity-70">
<MobileMenu />

<div className="flex space-x-2 ml-auto">
<div className="flex gap-2">Employee Portal</div>
</div>

<div className="flex space-x-2 ml-auto">
<Suspense fallback={<Skeleton className="h-8 w-8 rounded-full" />}>
<UserMenu session={session} />
<UserMenu />
</Suspense>
</div>
</header>
Expand Down
49 changes: 49 additions & 0 deletions apps/portal/src/app/components/locale-switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client";

import {
useChangeLocale,
useCurrentLocale,
useI18n,
} from "@/app/locales/client";
import { languages } from "@/app/locales/client";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@bubba/ui/select";
import { Globe } from "lucide-react";

export const LocaleSwitch = () => {
const t = useI18n();
const locale = useCurrentLocale();
const changeLocale = useChangeLocale();

return (
<div className="flex items-center relative">
<Select
defaultValue={locale}
onValueChange={(value: keyof typeof languages) => changeLocale(value)}
>
<SelectTrigger className="w-full pl-6 pr-3 py-1.5 bg-transparent outline-none capitalize h-[32px] text-xs">
<SelectValue placeholder={t("language.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{Object.entries(languages).map(([code, name]) => (
<SelectItem key={code} value={code} className="capitalize">
{name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>

<div className="absolute left-2 pointer-events-none">
<Globe size={12} />
</div>
</div>
);
};
64 changes: 64 additions & 0 deletions apps/portal/src/app/components/theme-switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use client";

import { useI18n } from "@/app/locales/client";
import { Monitor, Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";

import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@bubba/ui/select";

type Theme = "dark" | "system" | "light";

type Props = {
currentTheme?: Theme;
};

const ThemeIcon = ({ currentTheme }: Props) => {
switch (currentTheme) {
case "dark":
return <Moon size={12} />;
case "system":
return <Monitor size={12} />;
default:
return <Sun size={12} />;
}
};

export const ThemeSwitch = () => {
const t = useI18n();
const { theme, setTheme, themes } = useTheme();

return (
<div className="flex items-center relative">
<Select
defaultValue={theme}
onValueChange={(value: Theme) => setTheme(value)}
>
<SelectTrigger className="w-full pl-6 pr-3 py-1.5 bg-transparent outline-none capitalize h-[32px] text-xs">
<SelectValue placeholder={t("user_menu.theme")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{themes.map((theme) => (
<SelectItem key={theme} value={theme} className="capitalize">
{t(
`theme.options.${theme.toLowerCase() as "dark" | "system" | "light"}`,
)}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>

<div className="absolute left-2 pointer-events-none">
<ThemeIcon currentTheme={theme as Theme} />
</div>
</div>
);
};
47 changes: 40 additions & 7 deletions apps/portal/src/app/components/user-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,23 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@bubba/ui/dropdown-menu";
import type { auth } from "../lib/auth";
import { headers } from "next/headers";
import { auth } from "../lib/auth";
import { LocaleSwitch } from "./locale-switch";
import { Logout } from "./logout";
import { ThemeSwitch } from "./theme-switch";

export async function UserMenu({
session,
}: { session: Awaited<ReturnType<typeof auth.api.getSession>> }) {
export async function UserMenu() {
const t = await getI18n();

const session = await auth.api.getSession({
headers: await headers(),
});

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
Expand All @@ -34,9 +42,34 @@ export async function UserMenu({
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[240px]" sideOffset={10} align="end">
<DropdownMenuItem>
<Logout />
</DropdownMenuItem>
{" "}
<DropdownMenuLabel>
<div className="flex justify-between items-center">
<div className="flex flex-col">
<span className="truncate line-clamp-1 max-w-[155px] block">
{session?.user?.name}
</span>
<span className="truncate text-xs text-muted-foreground font-normal">
{session?.user?.email}
</span>
</div>
<div className="border py-0.5 px-3 rounded-full text-[11px] font-normal">
Beta
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<div className="flex flex-row justify-between items-center p-2">
<p className="text-sm">{t("user_menu.theme")}</p>
<ThemeSwitch />
</div>{" "}
<DropdownMenuSeparator />{" "}
<div className="flex flex-row justify-between items-center p-2">
<p className="text-sm">{t("user_menu.language")}</p>
<LocaleSwitch />
</div>{" "}
<DropdownMenuSeparator />
<Logout />
</DropdownMenuContent>
</DropdownMenu>
);
Expand Down
12 changes: 12 additions & 0 deletions apps/portal/src/app/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export default {
learn_more_link: "https://trycomp.ai",
},
user_menu: {
theme: "Theme",
language: "Language",
sign_out: "Sign Out",
},
not_found: {
Expand All @@ -29,4 +31,14 @@ export default {
sidebar: {
dashboard: "Employee Portal Overview",
},
language: {
placeholder: "Select Language",
},
theme: {
options: {
dark: "Dark",
light: "Light",
system: "System",
},
},
} as const;
Loading