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 (
-
-
Employee Portal
-
-
}> - +
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;