diff --git a/apps/app/src/actions/people/create-employee-action.ts b/apps/app/src/actions/people/create-employee-action.ts
index 5ad7eba710..ab15b1074e 100644
--- a/apps/app/src/actions/people/create-employee-action.ts
+++ b/apps/app/src/actions/people/create-employee-action.ts
@@ -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";
@@ -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,
@@ -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
@@ -87,7 +134,7 @@ export const createEmployeeAction = authActionClient
},
update: {},
});
- })
+ }),
);
// Now create the employee tasks using the actual task IDs
@@ -100,7 +147,7 @@ export const createEmployeeAction = authActionClient
status: "assigned",
},
});
- })
+ }),
);
return {
diff --git a/apps/portal/src/app/[locale]/(app)/unauthorized/page.tsx b/apps/portal/src/app/[locale]/(app)/unauthorized/page.tsx
index 03b8df5f78..53a33e5ff1 100644
--- a/apps/portal/src/app/[locale]/(app)/unauthorized/page.tsx
+++ b/apps/portal/src/app/[locale]/(app)/unauthorized/page.tsx
@@ -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 (
-
-
+
+
We couldn't find an organization for you. Please contact your
administrator.
-
+
);
}
diff --git a/apps/portal/src/app/components/header.tsx b/apps/portal/src/app/components/header.tsx
index 813e9abf3f..140f836106 100644
--- a/apps/portal/src/app/components/header.tsx
+++ b/apps/portal/src/app/components/header.tsx
@@ -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 (
diff --git a/apps/portal/src/app/components/locale-switch.tsx b/apps/portal/src/app/components/locale-switch.tsx
new file mode 100644
index 0000000000..69833af5a6
--- /dev/null
+++ b/apps/portal/src/app/components/locale-switch.tsx
@@ -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 (
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/portal/src/app/components/theme-switch.tsx b/apps/portal/src/app/components/theme-switch.tsx
new file mode 100644
index 0000000000..47740f9f78
--- /dev/null
+++ b/apps/portal/src/app/components/theme-switch.tsx
@@ -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 ;
+ case "system":
+ return ;
+ default:
+ return ;
+ }
+};
+
+export const ThemeSwitch = () => {
+ const t = useI18n();
+ const { theme, setTheme, themes } = useTheme();
+
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/portal/src/app/components/user-menu.tsx b/apps/portal/src/app/components/user-menu.tsx
index 1ced7d0b28..69a14afe57 100644
--- a/apps/portal/src/app/components/user-menu.tsx
+++ b/apps/portal/src/app/components/user-menu.tsx
@@ -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> }) {
+export async function UserMenu() {
const t = await getI18n();
+
+ const session = await auth.api.getSession({
+ headers: await headers(),
+ });
+
return (
@@ -34,9 +42,34 @@ export async function UserMenu({
-
-
-
+ {" "}
+
+
+
+
+ {session?.user?.name}
+
+
+ {session?.user?.email}
+
+
+
+ Beta
+
+
+
+
+
+
{t("user_menu.theme")}
+
+
{" "}
+ {" "}
+
+
{t("user_menu.language")}
+
+
{" "}
+
+
);
diff --git a/apps/portal/src/app/locales/en.ts b/apps/portal/src/app/locales/en.ts
index 4763c1daed..fa7b92a722 100644
--- a/apps/portal/src/app/locales/en.ts
+++ b/apps/portal/src/app/locales/en.ts
@@ -19,6 +19,8 @@ export default {
learn_more_link: "https://trycomp.ai",
},
user_menu: {
+ theme: "Theme",
+ language: "Language",
sign_out: "Sign Out",
},
not_found: {
@@ -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;
diff --git a/apps/portal/src/app/locales/es.ts b/apps/portal/src/app/locales/es.ts
index fba3266930..238af973aa 100644
--- a/apps/portal/src/app/locales/es.ts
+++ b/apps/portal/src/app/locales/es.ts
@@ -7,7 +7,7 @@ export default {
otp_sent: "Contraseña de un solo uso enviada",
otp_description: "Revisa tu correo electrónico para ver la contraseña de un solo uso",
otp_try_again: "Intentar de nuevo",
- placeholder: "Tu correo electrónico del trabajo",
+ placeholder: "Tu correo del trabajo",
button: "Continuar",
},
},
@@ -19,6 +19,8 @@ export default {
learn_more_link: "https://trycomp.ai",
},
user_menu: {
+ theme: "Tema",
+ language: "Idioma",
sign_out: "Cerrar Sesión",
},
not_found: {
@@ -29,4 +31,14 @@ export default {
sidebar: {
dashboard: "Vista General del Portal del Empleado",
},
+ language: {
+ placeholder: "Seleccionar Idioma",
+ },
+ theme: {
+ options: {
+ dark: "Oscuro",
+ light: "Claro",
+ system: "Sistema",
+ },
+ },
} as const;
diff --git a/apps/portal/src/app/locales/fr.ts b/apps/portal/src/app/locales/fr.ts
index 7c2d32d872..1e707d2af2 100644
--- a/apps/portal/src/app/locales/fr.ts
+++ b/apps/portal/src/app/locales/fr.ts
@@ -1,6 +1,6 @@
export default {
auth: {
- title: "Portail Employé",
+ title: "Portail des Employés",
description: "Saisissez votre adresse e-mail pour recevoir un mot de passe à usage unique.",
options: "Plus d'options",
email: {
@@ -15,10 +15,12 @@ export default {
title: "Comp AI - Plateforme de Conformité OSS",
description:
"Obtenez la conformité SOC 2, ISO 27001 et RGPD en quelques semaines, pas en mois. Open source, inscription instantanée, essai gratuit.",
- learn_more: "Commencer l'essai gratuit et devenir conforme",
+ learn_more: "Commencer l'essai gratuit et être conforme",
learn_more_link: "https://trycomp.ai",
},
user_menu: {
+ theme: "Thème",
+ language: "Langue",
sign_out: "Se déconnecter",
},
not_found: {
@@ -27,6 +29,16 @@ export default {
return: "Retourner à la page d'accueil",
},
sidebar: {
- dashboard: "Vue d'ensemble du portail employé",
+ dashboard: "Vue d'ensemble du Portail des Employés",
+ },
+ language: {
+ placeholder: "Sélectionner la langue",
+ },
+ theme: {
+ options: {
+ dark: "Sombre",
+ light: "Clair",
+ system: "Système",
+ },
},
} as const;
diff --git a/apps/portal/src/app/locales/no.ts b/apps/portal/src/app/locales/no.ts
index 4bfe74915e..689889168f 100644
--- a/apps/portal/src/app/locales/no.ts
+++ b/apps/portal/src/app/locales/no.ts
@@ -7,7 +7,7 @@ export default {
otp_sent: "Engangspassord sendt",
otp_description: "Sjekk e-posten din for engangspassordet",
otp_try_again: "Prøv igjen",
- placeholder: "Din jobb-e-post",
+ placeholder: "Din jobbmail",
button: "Fortsett",
},
},
@@ -19,6 +19,8 @@ export default {
learn_more_link: "https://trycomp.ai",
},
user_menu: {
+ theme: "Tema",
+ language: "Språk",
sign_out: "Logg ut",
},
not_found: {
@@ -29,4 +31,14 @@ export default {
sidebar: {
dashboard: "Oversikt over ansattportal",
},
+ language: {
+ placeholder: "Velg språk",
+ },
+ theme: {
+ options: {
+ dark: "Mørk",
+ light: "Lys",
+ system: "System",
+ },
+ },
} as const;
diff --git a/apps/portal/src/app/locales/pt.ts b/apps/portal/src/app/locales/pt.ts
index ef35fa7e90..489907ccb9 100644
--- a/apps/portal/src/app/locales/pt.ts
+++ b/apps/portal/src/app/locales/pt.ts
@@ -5,7 +5,7 @@ export default {
options: "Mais opções",
email: {
otp_sent: "Senha única enviada",
- otp_description: "Verifique seu e-mail para ver a senha única",
+ otp_description: "Verifique seu e-mail para a senha única",
otp_try_again: "Tentar novamente",
placeholder: "Seu e-mail profissional",
button: "Continuar",
@@ -14,11 +14,13 @@ export default {
powered_by: {
title: "Comp AI - Plataforma de Conformidade OSS",
description:
- "Obtenha conformidade com SOC 2, ISO 27001 e GDPR em semanas, não meses. Código aberto, inscrição instantânea, teste gratuito.",
+ "Obtenha conformidade com SOC 2, ISO 27001 e GDPR em semanas, não meses. Código aberto, cadastro instantâneo, teste gratuito.",
learn_more: "Comece o Teste Gratuito e Obtenha Conformidade",
learn_more_link: "https://trycomp.ai",
},
user_menu: {
+ theme: "Tema",
+ language: "Idioma",
sign_out: "Sair",
},
not_found: {
@@ -29,4 +31,14 @@ export default {
sidebar: {
dashboard: "Visão Geral do Portal do Funcionário",
},
+ language: {
+ placeholder: "Selecionar Idioma",
+ },
+ theme: {
+ options: {
+ dark: "Escuro",
+ light: "Claro",
+ system: "Sistema",
+ },
+ },
} as const;