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";
}