From 9d84b45bd9916b2b6c75354cfccf64f3bf596621 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Tue, 11 Mar 2025 14:47:21 -0400 Subject: [PATCH] feat(employees): implement editable department for employee details - Add `EditableDepartment` component with inline editing functionality - Create server action `updateEmployeeDepartment` for department updates - Extend employee details types to support department update schema - Update `EmployeeDetails` to use new editable department component - Enhance people table to display department in uppercase --- .../[employeeId]/actions/update-department.ts | 80 +++++++++++ .../components/EditableDepartment.tsx | 126 ++++++++++++++++++ .../components/EmployeeDetails.tsx | 14 +- .../people/[employeeId]/types/index.ts | 9 ++ .../components/tables/people/data-table.tsx | 2 +- 5 files changed, 225 insertions(+), 6 deletions(-) create mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/actions/update-department.ts create mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/components/EditableDepartment.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/actions/update-department.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/actions/update-department.ts new file mode 100644 index 0000000000..a429fa8475 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/actions/update-department.ts @@ -0,0 +1,80 @@ +"use server"; + +import { db } from "@bubba/db"; +import type { Departments } from "@bubba/db"; +import { authActionClient } from "@/actions/safe-action"; +import { revalidatePath } from "next/cache"; +import { + type AppError, + updateEmployeeDepartmentSchema, + appErrors, +} from "../types"; +import { auth } from "@/auth"; + +export type ActionResponse = Promise< + { success: true; data: T } | { success: false; error: AppError } +>; + +export const updateEmployeeDepartment = authActionClient + .schema(updateEmployeeDepartmentSchema) + .metadata({ + name: "update-employee-department", + track: { + event: "update-employee-department", + channel: "server", + }, + }) + .action(async ({ parsedInput }): Promise => { + const { employeeId, department } = parsedInput; + + const session = await auth(); + const organizationId = session?.user.organizationId; + + if (!organizationId) { + return { + success: false, + error: appErrors.UNAUTHORIZED, + }; + } + + try { + const employee = await db.employee.findUnique({ + where: { + id: employeeId, + organizationId, + }, + }); + + if (!employee) { + return { + success: false, + error: appErrors.NOT_FOUND, + }; + } + + const updatedEmployee = await db.employee.update({ + where: { + id: employeeId, + organizationId, + }, + data: { + department: department as Departments, + }, + }); + + // Revalidate related paths + revalidatePath(`/people/${employeeId}`); + revalidatePath("/people"); + + return { + success: true, + data: updatedEmployee, + }; + } catch (error) { + console.error("Error updating employee department:", error); + return { + success: false, + error: appErrors.UNEXPECTED_ERROR, + }; + } + }); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/components/EditableDepartment.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/components/EditableDepartment.tsx new file mode 100644 index 0000000000..07ac5575b5 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/components/EditableDepartment.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { useState } from "react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@bubba/ui/select"; +import { Label } from "@bubba/ui/label"; +import { Button } from "@bubba/ui/button"; +import { toast } from "sonner"; +import { useAction } from "next-safe-action/hooks"; +import { updateEmployeeDepartment } from "../actions/update-department"; +import { Pencil, Check, X } from "lucide-react"; +import type { Departments } from "@bubba/db"; + +const DEPARTMENTS = [ + { value: "admin", label: "Admin" }, + { value: "gov", label: "Governance" }, + { value: "hr", label: "HR" }, + { value: "it", label: "IT" }, + { value: "itsm", label: "IT Service Management" }, + { value: "qms", label: "Quality Management" }, + { value: "none", label: "None" }, +]; + +interface EditableDepartmentProps { + employeeId: string; + currentDepartment: Departments; + onSuccess?: () => void; +} + +export function EditableDepartment({ + employeeId, + currentDepartment, + onSuccess, +}: EditableDepartmentProps) { + const [isEditing, setIsEditing] = useState(false); + const [department, setDepartment] = useState(currentDepartment); + + const { execute, status } = useAction(updateEmployeeDepartment, { + onSuccess: () => { + toast.success("Department updated successfully"); + setIsEditing(false); + onSuccess?.(); + }, + onError: (error) => { + toast.error(error?.error?.serverError || "Failed to update department"); + }, + }); + + const handleSave = () => { + execute({ employeeId, department }); + }; + + const handleCancel = () => { + setDepartment(currentDepartment); + setIsEditing(false); + }; + + if (!isEditing) { + return ( +
+
+ + +
+

+ {DEPARTMENTS.find((d) => d.value === currentDepartment)?.label || + currentDepartment} +

+
+ ); + } + + return ( +
+ +
+ +
+ + +
+
+
+ ); +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/components/EmployeeDetails.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/components/EmployeeDetails.tsx index d28520be9b..d15a9ae5fc 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/components/EmployeeDetails.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/components/EmployeeDetails.tsx @@ -10,13 +10,16 @@ import { Alert, AlertDescription, AlertTitle } from "@bubba/ui/alert"; import { Label } from "@bubba/ui/label"; import { formatDate } from "@/utils/format"; import { Button } from "@bubba/ui/button"; +import { EditableDepartment } from "./EditableDepartment"; +import type { Departments } from "@bubba/db"; + interface EmployeeDetailsProps { employeeId: string; } export function EmployeeDetails({ employeeId }: EmployeeDetailsProps) { const t = useI18n(); - const { employee, isLoading, error } = useEmployeeDetails(employeeId); + const { employee, isLoading, error, mutate } = useEmployeeDetails(employeeId); if (error) { if (error.code === "NOT_FOUND") { @@ -83,10 +86,11 @@ export function EmployeeDetails({ employeeId }: EmployeeDetailsProps) { {formatDate(employee.createdAt.toISOString(), "MMM d, yyyy")}

-
- -

{employee.department}

-
+ mutate()} + /> diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/types/index.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/types/index.ts index f515a1ea70..3e77155d84 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/types/index.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/types/index.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import type { Departments } from "@bubba/db"; export const employeeTaskSchema = z.object({ id: z.string(), @@ -24,9 +25,17 @@ export const employeeDetailsInputSchema = z.object({ employeeId: z.string(), }); +export const updateEmployeeDepartmentSchema = z.object({ + employeeId: z.string(), + department: z.enum(["admin", "gov", "hr", "it", "itsm", "qms", "none"]), +}); + export type EmployeeTask = z.infer; export type EmployeeDetails = z.infer; export type EmployeeDetailsInput = z.infer; +export type UpdateEmployeeDepartmentInput = z.infer< + typeof updateEmployeeDepartmentSchema +>; export type AppError = { code: "NOT_FOUND" | "UNAUTHORIZED" | "UNEXPECTED_ERROR"; diff --git a/apps/app/src/components/tables/people/data-table.tsx b/apps/app/src/components/tables/people/data-table.tsx index fa43b26e68..ddcce66b17 100644 --- a/apps/app/src/components/tables/people/data-table.tsx +++ b/apps/app/src/components/tables/people/data-table.tsx @@ -171,7 +171,7 @@ export function DataTable({ } else if (cell.column.id === "email") { cellClassName = "w-[30%] hidden md:table-cell"; } else if (cell.column.id === "department") { - cellClassName = "w-[20%]"; + cellClassName = "w-[20%] uppercase"; } else if (cell.column.id === "status") { cellClassName = "w-[20%] text-center"; }