diff --git a/src/actions/recipient.actions.ts b/src/actions/recipient.actions.ts new file mode 100644 index 0000000..e05515b --- /dev/null +++ b/src/actions/recipient.actions.ts @@ -0,0 +1,76 @@ +"use server" + +import { revalidatePath } from "next/cache" + +import { + type CreateRecipientFormType, + createRecipientSchema, + type UpdateRecipientFormType, + updateRecipientSchema, +} from "@/schemas/recipient.schema" +import { + createRecipientUseCase, + updateRecipientUseCase, +} from "@/use-cases/recipient.use-cases" + +export async function createNewRecipient(formData: CreateRecipientFormType) { + const validatedFields = createRecipientSchema.safeParse(formData) + + if (!validatedFields.success) { + return { + success: false, + errors: validatedFields.error.flatten().fieldErrors, + } + } + + try { + const result = await createRecipientUseCase(validatedFields.data) + + if (!result.success) { + return result + } + + revalidatePath("/recipients") + + return { + success: true, + message: "Recipient created successfully", + } + } catch (error) { + console.error("Database error:", error) + return { + message: "Failed to create recipient", + } + } +} + +export async function updateRecipient(formData: UpdateRecipientFormType) { + const validatedFields = updateRecipientSchema.safeParse(formData) + + if (!validatedFields.success) { + return { + success: false, + errors: validatedFields.error.flatten().fieldErrors, + } + } + + try { + const result = await updateRecipientUseCase(validatedFields.data) + + if (!result.success) { + return result + } + + revalidatePath("/recipients") + + return { + success: true, + message: "Recipient updated successfully", + } + } catch (error) { + console.error("Database error:", error) + return { + message: "Failed to update recipient", + } + } +} diff --git a/src/app/(dashboard)/recipients/_components/recipient.form.tsx b/src/app/(dashboard)/recipients/_components/recipient.form.tsx index b6e5955..20e0b75 100644 --- a/src/app/(dashboard)/recipients/_components/recipient.form.tsx +++ b/src/app/(dashboard)/recipients/_components/recipient.form.tsx @@ -4,19 +4,18 @@ import { zodResolver } from "@hookform/resolvers/zod" import { useRouter } from "next/navigation" import { useForm } from "react-hook-form" import { toast } from "sonner" - -import { SubmitButton } from "@/components/forms/submitButton" -import { RecipientDepartment } from "@/generated/prisma/client" import { createNewRecipient, updateRecipient, -} from "@/lib/actions/recipient.actions" +} from "@/actions/recipient.actions" +import { SubmitButton } from "@/components/forms/submitButton" +import { RECIPIENT_DEPARTMENTS } from "@/lib/constants" import { type CreateRecipientFormType, recipientSchema, type UpdateRecipientFormType, -} from "@/lib/schemas/recipients.schemas" -import type { Recipient } from "@/lib/types" +} from "@/schemas/recipient.schema" +import type { Recipient } from "@/types" interface RecipientFormProps { initialData?: Recipient @@ -130,7 +129,7 @@ export default function RecipientForm({ className="w-full rounded-lg border px-4 py-2" > - {Object.keys(RecipientDepartment).map((department) => ( + {Object.keys(RECIPIENT_DEPARTMENTS).map((department) => ( diff --git a/src/lib/actions/recipient.actions.ts b/src/lib/actions/recipient.actions.ts deleted file mode 100644 index a7f71a3..0000000 --- a/src/lib/actions/recipient.actions.ts +++ /dev/null @@ -1,138 +0,0 @@ -"use server" - -import { revalidatePath } from "next/cache" - -import prisma from "@/lib/prisma" -import { - type CreateRecipientFormType, - createRecipientSchema, - type UpdateRecipientFormType, - updateRecipientSchema, -} from "@/lib/schemas/recipients.schemas" -import { RecipientService } from "@/services/recipient.service" - -export async function createNewRecipient(formData: CreateRecipientFormType) { - const validatedFields = createRecipientSchema.safeParse(formData) - - if (!validatedFields.success) { - return { - success: false, - errors: validatedFields.error.flatten().fieldErrors, - } - } - - const { username, firstName, lastName, department, email, phone } = - validatedFields.data - - try { - const existingRecipientUsername = - await RecipientService.findByUsername(username) - - if (existingRecipientUsername) { - return { - success: false, - errors: { - username: ["Username already exists"], - }, - } - } - - const existingRecipientEmail = await RecipientService.findByEmail( - email || "", - ) - - if (existingRecipientEmail) { - return { - success: false, - errors: { - email: ["Email already exists"], - }, - } - } - - await RecipientService.create({ - username: username || (firstName[0] + lastName).toLowerCase(), - firstName, - lastName, - department: department, - email: email || null, - phone: phone || null, - }) - - revalidatePath("/recipients") - - return { - success: true, - message: "Recipient created successfully", - } - } catch (error) { - console.error("Database error:", error) - return { - message: "Failed to create recipient", - } - } -} - -export async function updateRecipient(formData: UpdateRecipientFormType) { - const validatedFields = updateRecipientSchema.safeParse(formData) - - if (!validatedFields.success) { - return { - success: false, - errors: validatedFields.error.flatten().fieldErrors, - } - } - - const { id, username, firstName, lastName, department, email, phone } = - validatedFields.data - - try { - const existingRecipient = await RecipientService.findByUsername(username) - - if (existingRecipient && existingRecipient.id !== id) { - return { - success: false, - errors: { - username: ["Username already exists"], - }, - } - } - - const existingRecipientEmail = await RecipientService.findByEmail( - email || "", - ) - - if (existingRecipientEmail && existingRecipientEmail.id !== id) { - return { - success: false, - errors: { - email: ["Email already exists"], - }, - } - } - - await prisma.recipient.update({ - where: { id }, - data: { - username, - firstName, - lastName, - department: department, - email: email || null, - phone: phone || null, - }, - }) - - revalidatePath("/recipients") - - return { - success: true, - message: "Recipient updated successfully", - } - } catch (error) { - console.error("Database error:", error) - return { - message: "Failed to update recipient", - } - } -} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 4f7ae1b..c894af7 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -8,6 +8,17 @@ export const SIGN_IN_URL = "/login" export const TOKEN_EXPIRATION_SECONDS = 60 * 60 * 2 // 2 hour +export const RECIPIENT_DEPARTMENTS = { + IT: "IT", + ENGINEERING: "ENGINEERING", + LOGISTICS: "LOGISTICS", + TRAFFIC: "TRAFFIC", + DRIVER: "DRIVER", + ADMINISTRATION: "ADMINISTRATION", + SALES: "SALES", + OTHER: "OTHER", +} as const + export const ITEM_STATUS = { AVAILABLE: "AVAILABLE", ASSIGNED: "ASSIGNED", diff --git a/src/lib/schemas/recipients.schemas.ts b/src/schemas/recipient.schema.ts similarity index 87% rename from src/lib/schemas/recipients.schemas.ts rename to src/schemas/recipient.schema.ts index 7cc7615..940fc24 100644 --- a/src/lib/schemas/recipients.schemas.ts +++ b/src/schemas/recipient.schema.ts @@ -5,15 +5,15 @@ export const recipientSchema = z.object({ username: z .string() .min(1, { - error: "Username is required" + error: "Username is required", }) .nonempty("Username is required"), firstName: z.string().min(1, { - error: "First name is required" -}), + error: "First name is required", + }), lastName: z.string().min(1, { - error: "Last name is required" -}), + error: "Last name is required", + }), department: z.enum( [ "IT", @@ -26,7 +26,7 @@ export const recipientSchema = z.object({ "OTHER", ], { - error: "Department is required" + error: "Department is required", }, ), email: z.string().optional().nullable(), diff --git a/src/services/recipient.service.ts b/src/services/recipient.service.ts index e5b9e62..c9c104c 100644 --- a/src/services/recipient.service.ts +++ b/src/services/recipient.service.ts @@ -40,19 +40,39 @@ export const RecipientService = { return prisma.recipient.count() }, - findById: async (id: string): Promise => { - return prisma.recipient.findUnique({ where: { id } }) + findById: async ( + id: string, + db: Prisma.TransactionClient | typeof prisma = prisma, + ): Promise => { + return db.recipient.findUnique({ where: { id } }) }, - findByUsername: async (username: string): Promise => { - return prisma.recipient.findUnique({ where: { username } }) + findByUsername: async ( + username: string, + db: Prisma.TransactionClient | typeof prisma = prisma, + ): Promise => { + return db.recipient.findUnique({ where: { username } }) }, - findByEmail: async (email: string): Promise => { - return prisma.recipient.findUnique({ where: { email } }) + findByEmail: async ( + email: string, + db: Prisma.TransactionClient | typeof prisma = prisma, + ): Promise => { + return db.recipient.findUnique({ where: { email } }) }, - create: async (data: Prisma.RecipientCreateInput): Promise => { - return prisma.recipient.create({ data }) + create: async ( + data: Prisma.RecipientCreateInput, + db: Prisma.TransactionClient | typeof prisma = prisma, + ): Promise => { + return db.recipient.create({ data }) + }, + + update: async ( + id: string, + data: Prisma.RecipientUpdateInput, + db: Prisma.TransactionClient | typeof prisma = prisma, + ): Promise => { + return db.recipient.update({ where: { id }, data }) }, } diff --git a/src/use-cases/recipient.use-cases.ts b/src/use-cases/recipient.use-cases.ts new file mode 100644 index 0000000..720127a --- /dev/null +++ b/src/use-cases/recipient.use-cases.ts @@ -0,0 +1,155 @@ +import { Prisma } from "@/generated/prisma/client" +import prisma from "@/lib/prisma" +import type { + CreateRecipientFormType, + UpdateRecipientFormType, +} from "@/schemas/recipient.schema" +import { RecipientService } from "@/services/recipient.service" + +type FieldErrors = Record + +type RecipientUseCaseResult = + | { + success: true + } + | { + success: false + errors: FieldErrors + } + +function recipientError(errors: FieldErrors): RecipientUseCaseResult { + return { + success: false, + errors, + } +} + +function uniqueErrorFor(error: unknown): FieldErrors | null { + if ( + !(error instanceof Prisma.PrismaClientKnownRequestError) || + error.code !== "P2002" + ) { + return null + } + + const target = Array.isArray(error.meta?.target) ? error.meta.target : [] + + if (target.includes("username")) { + return { username: ["Username already exists"] } + } + + if (target.includes("email")) { + return { email: ["Email already exists"] } + } + + return { username: ["Username already exists"] } +} + +export async function createRecipientUseCase( + input: CreateRecipientFormType, +): Promise { + const { username, firstName, lastName, department, email, phone } = input + + try { + return await prisma.$transaction(async (tx) => { + const existingRecipientUsername = await RecipientService.findByUsername( + username, + tx, + ) + + if (existingRecipientUsername) { + return recipientError({ username: ["Username already exists"] }) + } + + if (email) { + const existingRecipientEmail = await RecipientService.findByEmail( + email, + tx, + ) + + if (existingRecipientEmail) { + return recipientError({ email: ["Email already exists"] }) + } + } + + await RecipientService.create( + { + username: username || (firstName[0] + lastName).toLowerCase(), + firstName, + lastName, + department, + email: email || null, + phone: phone || null, + }, + tx, + ) + + return { + success: true, + } + }) + } catch (error) { + const errors = uniqueErrorFor(error) + + if (errors) { + return recipientError(errors) + } + + throw error + } +} + +export async function updateRecipientUseCase( + input: UpdateRecipientFormType, +): Promise { + const { id, username, firstName, lastName, department, email, phone } = input + + try { + return await prisma.$transaction(async (tx) => { + const existingRecipient = await RecipientService.findByUsername( + username, + tx, + ) + + if (existingRecipient && existingRecipient.id !== id) { + return recipientError({ username: ["Username already exists"] }) + } + + if (email) { + const existingRecipientEmail = await RecipientService.findByEmail( + email, + tx, + ) + + if (existingRecipientEmail && existingRecipientEmail.id !== id) { + return recipientError({ email: ["Email already exists"] }) + } + } + + await RecipientService.update( + id, + { + username, + firstName, + lastName, + department, + email: email || null, + phone: phone || null, + }, + tx, + ) + + return { + success: true, + } + }) + } catch (error) { + const errors = uniqueErrorFor(error) + + if (errors) { + return recipientError(errors) + } + + throw error + } +}