Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<T = any> = 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<ActionResponse> => {
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,
};
}
});
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-1.5">
<Label className="font-medium">Department</Label>
<Button
variant="ghost"
size="icon"
onClick={() => setIsEditing(true)}
className="h-5 w-5 p-0"
>
<Pencil className="h-3.5 w-3.5" />
</Button>
</div>
<p>
{DEPARTMENTS.find((d) => d.value === currentDepartment)?.label ||
currentDepartment}
</p>
</div>
);
}

return (
<div className="flex flex-col gap-2">
<Label className="font-medium">Department</Label>
<div className="flex items-center gap-2">
<Select
value={department}
onValueChange={(value) => setDepartment(value as Departments)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select department" />
</SelectTrigger>
<SelectContent>
{DEPARTMENTS.map((dept) => (
<SelectItem key={dept.value} value={dept.value}>
{dept.label}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="flex items-center gap-1">
<Button
size="icon"
variant="ghost"
onClick={handleSave}
disabled={status === "executing"}
className="h-7 w-7 p-0"
>
<Check className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="ghost"
onClick={handleCancel}
className="h-7 w-7 p-0"
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down Expand Up @@ -83,10 +86,11 @@ export function EmployeeDetails({ employeeId }: EmployeeDetailsProps) {
{formatDate(employee.createdAt.toISOString(), "MMM d, yyyy")}
</p>
</div>
<div className="flex flex-col gap-2">
<Label>Department</Label>
<p>{employee.department}</p>
</div>
<EditableDepartment
employeeId={employee.id}
currentDepartment={employee.department as Departments}
onSuccess={() => mutate()}
/>
</div>
</CardContent>
</Card>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod";
import type { Departments } from "@bubba/db";

export const employeeTaskSchema = z.object({
id: z.string(),
Expand All @@ -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<typeof employeeTaskSchema>;
export type EmployeeDetails = z.infer<typeof employeeDetailsSchema>;
export type EmployeeDetailsInput = z.infer<typeof employeeDetailsInputSchema>;
export type UpdateEmployeeDepartmentInput = z.infer<
typeof updateEmployeeDepartmentSchema
>;

export type AppError = {
code: "NOT_FOUND" | "UNAUTHORIZED" | "UNEXPECTED_ERROR";
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/components/tables/people/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Expand Down