From 24bcb651b68262a705e47ead8891696bd43460ac Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Fri, 28 Mar 2025 12:12:52 -0400 Subject: [PATCH 01/12] change name of sheet --- .../[orgId]/employees/all/components/table/EmployeesTable.tsx | 2 +- .../sheets/{invite-user-sheet.tsx => add-employee-sheet.tsx} | 0 apps/app/src/components/tables/people/empty-states.tsx | 2 +- apps/app/src/components/tables/people/filter-toolbar.tsx | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename apps/app/src/components/sheets/{invite-user-sheet.tsx => add-employee-sheet.tsx} (100%) diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/all/components/table/EmployeesTable.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/all/components/table/EmployeesTable.tsx index 2b01773382..edcdc71666 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/all/components/table/EmployeesTable.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/all/components/table/EmployeesTable.tsx @@ -1,6 +1,6 @@ "use client"; -import { EmployeeInviteSheet } from "@/components/sheets/invite-user-sheet"; +import { EmployeeInviteSheet } from "@/components/sheets/add-employee-sheet"; import { DataTable } from "@/components/ui/data-table"; import { Plus } from "lucide-react"; import { useParams, useRouter } from "next/navigation"; diff --git a/apps/app/src/components/sheets/invite-user-sheet.tsx b/apps/app/src/components/sheets/add-employee-sheet.tsx similarity index 100% rename from apps/app/src/components/sheets/invite-user-sheet.tsx rename to apps/app/src/components/sheets/add-employee-sheet.tsx diff --git a/apps/app/src/components/tables/people/empty-states.tsx b/apps/app/src/components/tables/people/empty-states.tsx index 5c8f2d83a8..7c8836c2f2 100644 --- a/apps/app/src/components/tables/people/empty-states.tsx +++ b/apps/app/src/components/tables/people/empty-states.tsx @@ -6,7 +6,7 @@ import { Users } from "lucide-react"; import { useParams, useRouter } from "next/navigation"; import { useQueryState } from "nuqs"; import { Plus } from "lucide-react"; -import { EmployeeInviteSheet } from "@/components/sheets/invite-user-sheet"; +import { EmployeeInviteSheet } from "@/components/sheets/add-employee-sheet"; interface Props { hasFilters?: boolean; diff --git a/apps/app/src/components/tables/people/filter-toolbar.tsx b/apps/app/src/components/tables/people/filter-toolbar.tsx index 8dbec46ae5..5b7deaca0c 100644 --- a/apps/app/src/components/tables/people/filter-toolbar.tsx +++ b/apps/app/src/components/tables/people/filter-toolbar.tsx @@ -1,6 +1,6 @@ "use client"; -import { EmployeeInviteSheet } from "@/components/sheets/invite-user-sheet"; +import { EmployeeInviteSheet } from "@/components/sheets/add-employee-sheet"; import { useI18n } from "@/locales/client"; import { Button } from "@bubba/ui/button"; import { Input } from "@bubba/ui/input"; From 64606279dbbdf9f6e5b3fd4ecb15e50f61346ec1 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Fri, 28 Mar 2025 14:15:16 -0400 Subject: [PATCH 02/12] send email to employee on invite --- apps/app/src/lib/db/employee.ts | 86 +++++++++---------- apps/app/src/locales/features/people.ts | 3 +- packages/email/emails/invite-portal.tsx | 106 ++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 49 deletions(-) create mode 100644 packages/email/emails/invite-portal.tsx diff --git a/apps/app/src/lib/db/employee.ts b/apps/app/src/lib/db/employee.ts index 1e2d5f1e7b..682b880e25 100644 --- a/apps/app/src/lib/db/employee.ts +++ b/apps/app/src/lib/db/employee.ts @@ -1,18 +1,11 @@ import { db } from "@bubba/db"; import type { Employee, Departments } from "@bubba/db/types"; +import { InvitePortalEmail } from "@bubba/email/emails/invite-portal"; +import { sendEmail } from "@bubba/email/lib/resend"; -const DEFAULT_TASKS = [ - { - code: "POLICY-ACCEPT", - name: "Policy Acceptance", - description: "Review and accept company policies", - }, - { - code: "DEVICE-SECURITY", - name: "Device Security", - description: "Complete device security checklist and configuration", - }, -] as const; +if (!process.env.NEXT_PUBLIC_PORTAL_URL) { + throw new Error("NEXT_PUBLIC_PORTAL_URL is not set"); +} /** * Find an existing employee by email and organization ID @@ -120,46 +113,33 @@ export async function createOrUpdatePortalUser(params: { }); } -/** - * Create default tasks for an employee - */ -export async function createDefaultTasksForEmployee( - employeeId: string -): Promise { - // Create or get the required task definitions first and store their IDs - const requiredTasks = await Promise.all( - DEFAULT_TASKS.map(async (task) => { - return db.employeeRequiredTask.upsert({ - where: { code: task.code }, - create: { - code: task.code, - name: task.name, - description: task.description, - }, - update: {}, - }); - }) - ); - - // Now create the employee tasks using the actual task IDs - await Promise.all( - requiredTasks.map(async (task) => { - return db.employeeTask.create({ - data: { - employeeId: employeeId, - requiredTaskId: task.id, - status: "assigned", - }, - }); - }) - ); -} +const inviteEmployeeToPortal = async (params: { + email: string; + organizationName: string; + inviteLink: string; +}) => { + const { email, organizationName, inviteLink } = params; + + await sendEmail({ + to: email, + subject: `You've been invited to join ${organizationName || "an organization"} on Comp AI`, + react: InvitePortalEmail({ + email, + organizationName, + inviteLink, + }), + }); + + return { + success: true, + message: "Employee invited to portal", + }; +}; /** * Complete employee creation by handling all steps: * 1. Create/reactivate the employee * 2. Create/update portal user - * 3. Create default tasks */ export async function completeEmployeeCreation(params: { name: string; @@ -177,7 +157,17 @@ export async function completeEmployeeCreation(params: { organizationId: params.organizationId, }); - await createDefaultTasksForEmployee(employee.id); + const organization = await db.organization.findUnique({ + where: { + id: params.organizationId, + }, + }); + + await inviteEmployeeToPortal({ + email: params.email, + organizationName: organization?.name || "an organization", + inviteLink: `${process.env.NEXT_PUBLIC_PORTAL_URL}`, + }); return employee; } diff --git a/apps/app/src/locales/features/people.ts b/apps/app/src/locales/features/people.ts index 2b24e3178f..addb6d5eb3 100644 --- a/apps/app/src/locales/features/people.ts +++ b/apps/app/src/locales/features/people.ts @@ -56,7 +56,8 @@ export const people = { placeholder: "Select a department", }, submit: "Add Employee", - success: "Employee added successfully", + success: + "Employee added successfully, they will receive an email to join the portal.", error: "Failed to add employee", }, dashboard: { diff --git a/packages/email/emails/invite-portal.tsx b/packages/email/emails/invite-portal.tsx new file mode 100644 index 0000000000..2ee79cf1df --- /dev/null +++ b/packages/email/emails/invite-portal.tsx @@ -0,0 +1,106 @@ +import { + Body, + Button, + Container, + Font, + Heading, + Html, + Link, + Preview, + Section, + Tailwind, + Text, +} from "@react-email/components"; +import { Footer } from "../components/footer"; +import { Logo } from "../components/logo"; + +interface Props { + email: string; + inviteLink: string; + organizationName?: string; +} + +export const InvitePortalEmail = ({ + email, + inviteLink, + organizationName, +}: Props) => { + return ( + + + + + + + + + You've been invited to the Comp AI Portal + + + + + + You've been invited to the Comp AI Portal + + + + {organizationName + ? `${organizationName} has invited you to access their Comp AI Portal.` + : "You've been invited to access the Comp AI Portal."} + +
+ +
+ + + or copy and paste this URL into your browser{" "} + + {inviteLink} + + + +
+
+ + This invitation was intended for{" "} + {email}.{" "} + +
+ +
+ +