refactor: rename Recipient to Person, remove username, add userId FK
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'MANAGER', 'STAFF', 'VIEWER');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RecipientDepartment" AS ENUM ('IT', 'ENGINEERING', 'LOGISTICS', 'TRAFFIC', 'DRIVER', 'ADMINISTRATION', 'SALES', 'OTHER');
|
||||
CREATE TYPE "PersonDepartment" AS ENUM ('IT', 'ENGINEERING', 'LOGISTICS', 'TRAFFIC', 'DRIVER', 'ADMINISTRATION', 'SALES', 'OTHER');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ItemStatus" AS ENUM ('AVAILABLE', 'ASSIGNED', 'RESERVED', 'IN_REPAIR', 'BROKEN', 'STOLEN', 'DISPOSED');
|
||||
@@ -26,19 +26,19 @@ CREATE TABLE "User" (
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Recipient" (
|
||||
CREATE TABLE "Person" (
|
||||
"id" TEXT NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"firstName" TEXT NOT NULL,
|
||||
"lastName" TEXT NOT NULL,
|
||||
"department" "RecipientDepartment",
|
||||
"department" "PersonDepartment",
|
||||
"email" TEXT,
|
||||
"phone" TEXT,
|
||||
"userId" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Recipient_pkey" PRIMARY KEY ("id")
|
||||
CONSTRAINT "Person_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
@@ -126,16 +126,16 @@ CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Recipient_username_key" ON "Recipient"("username");
|
||||
CREATE UNIQUE INDEX "Person_email_key" ON "Person"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Recipient_email_key" ON "Recipient"("email");
|
||||
CREATE UNIQUE INDEX "Person_userId_key" ON "Person"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Recipient_lastName_firstName_idx" ON "Recipient"("lastName", "firstName");
|
||||
CREATE INDEX "Person_lastName_firstName_idx" ON "Person"("lastName", "firstName");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Recipient_department_idx" ON "Recipient"("department");
|
||||
CREATE INDEX "Person_department_idx" ON "Person"("department");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Category_name_key" ON "Category"("name");
|
||||
@@ -191,6 +191,9 @@ CREATE INDEX "Movement_type_idx" ON "Movement"("type");
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Movement_userId_idx" ON "Movement"("userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Person" ADD CONSTRAINT "Person_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Item" ADD CONSTRAINT "Item_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
@@ -204,7 +207,7 @@ ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_itemId_fkey" FOREIGN KEY ("i
|
||||
ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "Asset"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Recipient"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Person"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -216,7 +219,7 @@ ALTER TABLE "Movement" ADD CONSTRAINT "Movement_itemId_fkey" FOREIGN KEY ("itemI
|
||||
ALTER TABLE "Movement" ADD CONSTRAINT "Movement_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "Asset"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Movement" ADD CONSTRAINT "Movement_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Recipient"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "Movement" ADD CONSTRAINT "Movement_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Movement" ADD CONSTRAINT "Movement_assignmentId_fkey" FOREIGN KEY ("assignmentId") REFERENCES "Assignment"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
+18
-16
@@ -33,9 +33,10 @@ model User {
|
||||
updatedAt DateTime @updatedAt
|
||||
movements Movement[]
|
||||
assignments Assignment[]
|
||||
person Person?
|
||||
}
|
||||
|
||||
enum RecipientDepartment {
|
||||
enum PersonDepartment {
|
||||
IT
|
||||
ENGINEERING
|
||||
LOGISTICS
|
||||
@@ -46,17 +47,18 @@ enum RecipientDepartment {
|
||||
OTHER
|
||||
}
|
||||
|
||||
model Recipient {
|
||||
id String @id @default(uuid())
|
||||
username String @unique
|
||||
model Person {
|
||||
id String @id @default(uuid())
|
||||
firstName String
|
||||
lastName String
|
||||
department RecipientDepartment?
|
||||
email String? @unique
|
||||
department PersonDepartment?
|
||||
email String? @unique
|
||||
phone String?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
userId String? @unique
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
assignments Assignment[]
|
||||
movements Movement[]
|
||||
|
||||
@@ -128,11 +130,11 @@ model Assignment {
|
||||
quantity Int?
|
||||
notes String?
|
||||
itemId String?
|
||||
item Item? @relation(fields: [itemId], references: [id])
|
||||
item Item? @relation(fields: [itemId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
assetId String? @unique
|
||||
asset Asset? @relation(fields: [assetId], references: [id])
|
||||
asset Asset? @relation(fields: [assetId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
recipientId String?
|
||||
recipient Recipient? @relation(fields: [recipientId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
recipient Person? @relation(fields: [recipientId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
assignmentDate DateTime @default(now())
|
||||
returnDate DateTime?
|
||||
createdBy String
|
||||
@@ -163,15 +165,15 @@ model Movement {
|
||||
details String?
|
||||
notes String?
|
||||
itemId String?
|
||||
item Item? @relation(fields: [itemId], references: [id])
|
||||
item Item? @relation(fields: [itemId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
assetId String?
|
||||
asset Asset? @relation(fields: [assetId], references: [id])
|
||||
asset Asset? @relation(fields: [assetId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
previousStock Int?
|
||||
newStock Int?
|
||||
recipientId String?
|
||||
recipient Recipient? @relation(fields: [recipientId], references: [id])
|
||||
recipient Person? @relation(fields: [recipientId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
assignmentId String?
|
||||
assignment Assignment? @relation(fields: [assignmentId], references: [id])
|
||||
assignment Assignment? @relation(fields: [assignmentId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@ -12,14 +12,14 @@ import { getAuthenticatedUserId } from "@/services/auth.service"
|
||||
import { CategoryService } from "@/services/category.service"
|
||||
import { ItemService } from "@/services/item.service"
|
||||
import { MovementService } from "@/services/movement.service"
|
||||
import { RecipientService } from "@/services/recipient.service"
|
||||
import { PersonService } from "@/services/person.service"
|
||||
import type {
|
||||
Asset,
|
||||
Assignment,
|
||||
Category,
|
||||
ImportItem,
|
||||
Item,
|
||||
Recipient,
|
||||
Person,
|
||||
} from "@/types"
|
||||
|
||||
export async function importItems(formData: ImportFormType) {
|
||||
@@ -123,7 +123,7 @@ export async function importItems(formData: ImportFormType) {
|
||||
file: [
|
||||
"Only one of category or categoryId is allowed, you must select one of them",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,6 @@ export async function importItems(formData: ImportFormType) {
|
||||
category,
|
||||
deliveryNote,
|
||||
assigned,
|
||||
username,
|
||||
firstName,
|
||||
lastName,
|
||||
} = row
|
||||
@@ -178,10 +177,6 @@ export async function importItems(formData: ImportFormType) {
|
||||
importErrors.push(`Row ${index + 2}: Delivery note must be a string`)
|
||||
}
|
||||
|
||||
if (username && typeof username !== "string") {
|
||||
importErrors.push(`Row ${index + 2}: Username must be a string`)
|
||||
}
|
||||
|
||||
if (firstName && typeof firstName !== "string") {
|
||||
importErrors.push(`Row ${index + 2}: First name must be a string`)
|
||||
}
|
||||
@@ -214,7 +209,6 @@ export async function importItems(formData: ImportFormType) {
|
||||
category: row.category?.trim() || "",
|
||||
deliveryNote: row.deliveryNote?.trim() || "",
|
||||
assigned: row.assigned?.trim() === "true",
|
||||
username: row.username?.trim() || "",
|
||||
firstName: row.firstName?.trim() || "",
|
||||
lastName: row.lastName?.trim() || "",
|
||||
})
|
||||
@@ -229,7 +223,6 @@ export async function importItems(formData: ImportFormType) {
|
||||
category,
|
||||
deliveryNote,
|
||||
assigned,
|
||||
username,
|
||||
firstName,
|
||||
lastName,
|
||||
} = item
|
||||
@@ -238,7 +231,7 @@ export async function importItems(formData: ImportFormType) {
|
||||
let newItem: Item | null = null
|
||||
let newAsset: Asset | null = null
|
||||
let newCategory: Category | null = null
|
||||
let newRecipient: Recipient | null = null
|
||||
let newPerson: Person | null = null
|
||||
let newAssignment: Assignment | null = null
|
||||
|
||||
const existingCategory = categoryId
|
||||
@@ -290,14 +283,16 @@ export async function importItems(formData: ImportFormType) {
|
||||
}
|
||||
|
||||
if (assigned && firstName && lastName) {
|
||||
const finalUsername =
|
||||
username || `${firstName.toLowerCase()[0]}${lastName.toLowerCase()}`
|
||||
const existingRecipient =
|
||||
await RecipientService.findByUsername(finalUsername)
|
||||
const existingPerson = firstName
|
||||
? await PersonService.findAllPaginated({
|
||||
search: firstName,
|
||||
page: 0,
|
||||
pageSize: 1,
|
||||
})
|
||||
: null
|
||||
|
||||
if (!existingRecipient) {
|
||||
newRecipient = await RecipientService.create({
|
||||
username: finalUsername,
|
||||
if (!existingPerson || existingPerson.data.length === 0) {
|
||||
newPerson = await PersonService.create({
|
||||
firstName,
|
||||
lastName,
|
||||
email: undefined,
|
||||
@@ -305,7 +300,7 @@ export async function importItems(formData: ImportFormType) {
|
||||
department: "OTHER",
|
||||
})
|
||||
} else {
|
||||
newRecipient = existingRecipient
|
||||
newPerson = existingPerson.data[0]
|
||||
}
|
||||
|
||||
newAssignment = await AssignmentService.create({
|
||||
@@ -313,7 +308,7 @@ export async function importItems(formData: ImportFormType) {
|
||||
notes: deliveryNote || "",
|
||||
itemId: newItem?.id || "",
|
||||
assetId: newAsset?.id || "",
|
||||
recipientId: newRecipient?.id || "",
|
||||
recipientId: newPerson?.id || "",
|
||||
assignmentDate: new Date(),
|
||||
createdBy: userId,
|
||||
})
|
||||
@@ -324,15 +319,15 @@ export async function importItems(formData: ImportFormType) {
|
||||
quantity: stock || 1,
|
||||
type: assigned ? "ASSIGNMENT" : "IN",
|
||||
itemId: newItem?.id || undefined,
|
||||
recipientId: newRecipient?.id || undefined,
|
||||
recipientId: newPerson?.id || undefined,
|
||||
}
|
||||
|
||||
if (newAssignment?.id) {
|
||||
movementData.assignmentId = newAssignment.id
|
||||
}
|
||||
|
||||
if (newRecipient?.id) {
|
||||
movementData.recipientId = newRecipient.id
|
||||
if (newPerson?.id) {
|
||||
movementData.recipientId = newPerson.id
|
||||
}
|
||||
|
||||
await MovementService.create({
|
||||
|
||||
@@ -4,22 +4,22 @@ import { revalidatePath } from "next/cache"
|
||||
import { flattenError } from "zod"
|
||||
import { getI18n } from "@/i18n/server"
|
||||
import {
|
||||
buildCreateRecipientSchema,
|
||||
buildUpdateRecipientSchema,
|
||||
type CreateRecipientFormType,
|
||||
type UpdateRecipientFormType,
|
||||
} from "@/schemas/recipient.schema"
|
||||
buildCreatePersonSchema,
|
||||
buildUpdatePersonSchema,
|
||||
type CreatePersonFormType,
|
||||
type UpdatePersonFormType,
|
||||
} from "@/schemas/person.schema"
|
||||
import {
|
||||
createRecipientUseCase,
|
||||
updateRecipientUseCase,
|
||||
} from "@/use-cases/recipient.use-cases"
|
||||
createPersonUseCase,
|
||||
updatePersonUseCase,
|
||||
} from "@/use-cases/person.use-cases"
|
||||
|
||||
import { localizeRecipientFieldErrors } from "./recipient.messages"
|
||||
import { localizePersonFieldErrors } from "./person.messages"
|
||||
|
||||
export async function createNewRecipient(formData: CreateRecipientFormType) {
|
||||
export async function createNewPerson(formData: CreatePersonFormType) {
|
||||
const { dictionary } = await getI18n()
|
||||
const copy = dictionary.inventory.recipients
|
||||
const validatedFields = buildCreateRecipientSchema(copy.schema).safeParse(
|
||||
const copy = dictionary.inventory.people
|
||||
const validatedFields = buildCreatePersonSchema(copy.schema).safeParse(
|
||||
formData,
|
||||
)
|
||||
|
||||
@@ -31,17 +31,17 @@ export async function createNewRecipient(formData: CreateRecipientFormType) {
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await createRecipientUseCase(validatedFields.data)
|
||||
const result = await createPersonUseCase(validatedFields.data)
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
...result,
|
||||
errors: localizeRecipientFieldErrors(result.errors, copy.actions),
|
||||
errors: localizePersonFieldErrors(result.errors, copy.actions),
|
||||
message: copy.actions.createFailure,
|
||||
}
|
||||
}
|
||||
|
||||
revalidatePath("/recipients")
|
||||
revalidatePath("/people")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -56,10 +56,10 @@ export async function createNewRecipient(formData: CreateRecipientFormType) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateRecipient(formData: UpdateRecipientFormType) {
|
||||
export async function updatePerson(formData: UpdatePersonFormType) {
|
||||
const { dictionary } = await getI18n()
|
||||
const copy = dictionary.inventory.recipients
|
||||
const validatedFields = buildUpdateRecipientSchema(copy.schema).safeParse(
|
||||
const copy = dictionary.inventory.people
|
||||
const validatedFields = buildUpdatePersonSchema(copy.schema).safeParse(
|
||||
formData,
|
||||
)
|
||||
|
||||
@@ -71,17 +71,17 @@ export async function updateRecipient(formData: UpdateRecipientFormType) {
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await updateRecipientUseCase(validatedFields.data)
|
||||
const result = await updatePersonUseCase(validatedFields.data)
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
...result,
|
||||
errors: localizeRecipientFieldErrors(result.errors, copy.actions),
|
||||
errors: localizePersonFieldErrors(result.errors, copy.actions),
|
||||
message: copy.actions.updateFailure,
|
||||
}
|
||||
}
|
||||
|
||||
revalidatePath("/recipients")
|
||||
revalidatePath("/people")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { Dictionary } from "@/i18n/dictionaries"
|
||||
|
||||
type PersonActionCopy = Dictionary["inventory"]["people"]["actions"]
|
||||
|
||||
type FieldErrors = Record<string, string[]>
|
||||
|
||||
const personErrorMessageKeys = {
|
||||
"Email already exists": "duplicateEmail",
|
||||
} as const satisfies Record<string, keyof PersonActionCopy>
|
||||
|
||||
function isPersonErrorMessage(
|
||||
message: string,
|
||||
): message is keyof typeof personErrorMessageKeys {
|
||||
return message in personErrorMessageKeys
|
||||
}
|
||||
|
||||
function localizePersonMessage(
|
||||
message: string,
|
||||
copy: PersonActionCopy,
|
||||
): string {
|
||||
if (!isPersonErrorMessage(message)) return message
|
||||
|
||||
return copy[personErrorMessageKeys[message]]
|
||||
}
|
||||
|
||||
export function localizePersonFieldErrors(
|
||||
errors: FieldErrors | undefined,
|
||||
copy: PersonActionCopy,
|
||||
): FieldErrors | undefined {
|
||||
if (!errors) return undefined
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(errors).map(([field, messages]) => [
|
||||
field,
|
||||
messages.map((message) => localizePersonMessage(message, copy)),
|
||||
]),
|
||||
)
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { Dictionary } from "@/i18n/dictionaries"
|
||||
|
||||
type RecipientActionCopy = Dictionary["inventory"]["recipients"]["actions"]
|
||||
|
||||
type FieldErrors = Record<string, string[]>
|
||||
|
||||
const recipientErrorMessageKeys = {
|
||||
"Username already exists": "duplicateUsername",
|
||||
"Email already exists": "duplicateEmail",
|
||||
} as const satisfies Record<string, keyof RecipientActionCopy>
|
||||
|
||||
function isRecipientErrorMessage(
|
||||
message: string,
|
||||
): message is keyof typeof recipientErrorMessageKeys {
|
||||
return message in recipientErrorMessageKeys
|
||||
}
|
||||
|
||||
function localizeRecipientMessage(
|
||||
message: string,
|
||||
copy: RecipientActionCopy,
|
||||
): string {
|
||||
if (!isRecipientErrorMessage(message)) return message
|
||||
|
||||
return copy[recipientErrorMessageKeys[message]]
|
||||
}
|
||||
|
||||
export function localizeRecipientFieldErrors(
|
||||
errors: FieldErrors | undefined,
|
||||
copy: RecipientActionCopy,
|
||||
): FieldErrors | undefined {
|
||||
if (!errors) return undefined
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(errors).map(([field, messages]) => [
|
||||
field,
|
||||
messages.map((message) => localizeRecipientMessage(message, copy)),
|
||||
]),
|
||||
)
|
||||
}
|
||||
@@ -318,6 +318,80 @@ export const en = {
|
||||
idRequired: "Assignment ID is required",
|
||||
},
|
||||
},
|
||||
people: {
|
||||
list: {
|
||||
title: "People",
|
||||
addLabel: "Add Person",
|
||||
empty: "No people found.",
|
||||
columns: {
|
||||
name: "Name",
|
||||
email: "Email",
|
||||
phone: "Phone",
|
||||
department: "Department",
|
||||
actions: "Actions",
|
||||
},
|
||||
actions: {
|
||||
view: "View person",
|
||||
edit: "Edit person",
|
||||
},
|
||||
},
|
||||
detail: {
|
||||
notFound: "Person not found",
|
||||
labels: {
|
||||
email: "Email",
|
||||
phone: "Phone",
|
||||
department: "Department",
|
||||
},
|
||||
},
|
||||
new: {
|
||||
title: "Add Person",
|
||||
},
|
||||
edit: {
|
||||
title: "Edit Person",
|
||||
notFound: "Person not found",
|
||||
},
|
||||
form: {
|
||||
firstNameLabel: "First Name",
|
||||
firstNamePlaceholder: "First name",
|
||||
lastNameLabel: "Last Name",
|
||||
lastNamePlaceholder: "Last name",
|
||||
departmentLabel: "Department",
|
||||
departmentPlaceholder: "Select a department",
|
||||
emailLabel: "Email",
|
||||
emailPlaceholder: "Email",
|
||||
phoneLabel: "Phone",
|
||||
phonePlaceholder: "Phone",
|
||||
createSubmit: "Create Person",
|
||||
updateSubmit: "Update Person",
|
||||
},
|
||||
fallback: {
|
||||
unknownDepartment: "Unknown department",
|
||||
},
|
||||
departments: {
|
||||
IT: "IT",
|
||||
ENGINEERING: "Engineering",
|
||||
LOGISTICS: "Logistics",
|
||||
TRAFFIC: "Traffic",
|
||||
DRIVER: "Driver",
|
||||
ADMINISTRATION: "Administration",
|
||||
SALES: "Sales",
|
||||
OTHER: "Other",
|
||||
},
|
||||
actions: {
|
||||
createSuccess: "Person created successfully",
|
||||
createFailure: "Failed to create person",
|
||||
updateSuccess: "Person updated successfully",
|
||||
updateFailure: "Failed to update person",
|
||||
duplicateEmail: "Email already exists",
|
||||
},
|
||||
schema: {
|
||||
firstNameRequired: "First name is required",
|
||||
lastNameRequired: "Last name is required",
|
||||
departmentRequired: "Department is required",
|
||||
emailInvalid: "Email format is invalid",
|
||||
idRequired: "ID is required",
|
||||
},
|
||||
},
|
||||
recipients: {
|
||||
list: {
|
||||
title: "Recipients",
|
||||
|
||||
@@ -323,6 +323,80 @@ export const es = {
|
||||
idRequired: "El ID de asignación es obligatorio",
|
||||
},
|
||||
},
|
||||
people: {
|
||||
list: {
|
||||
title: "Personas",
|
||||
addLabel: "Agregar persona",
|
||||
empty: "No se encontraron personas.",
|
||||
columns: {
|
||||
name: "Nombre",
|
||||
email: "Correo electrónico",
|
||||
phone: "Teléfono",
|
||||
department: "Departamento",
|
||||
actions: "Acciones",
|
||||
},
|
||||
actions: {
|
||||
view: "Ver persona",
|
||||
edit: "Editar persona",
|
||||
},
|
||||
},
|
||||
detail: {
|
||||
notFound: "Persona no encontrada",
|
||||
labels: {
|
||||
email: "Correo electrónico",
|
||||
phone: "Teléfono",
|
||||
department: "Departamento",
|
||||
},
|
||||
},
|
||||
new: {
|
||||
title: "Agregar persona",
|
||||
},
|
||||
edit: {
|
||||
title: "Editar persona",
|
||||
notFound: "Persona no encontrada",
|
||||
},
|
||||
form: {
|
||||
firstNameLabel: "Nombre",
|
||||
firstNamePlaceholder: "Nombre",
|
||||
lastNameLabel: "Apellido",
|
||||
lastNamePlaceholder: "Apellido",
|
||||
departmentLabel: "Departamento",
|
||||
departmentPlaceholder: "Selecciona un departamento",
|
||||
emailLabel: "Correo electrónico",
|
||||
emailPlaceholder: "Correo electrónico",
|
||||
phoneLabel: "Teléfono",
|
||||
phonePlaceholder: "Teléfono",
|
||||
createSubmit: "Crear persona",
|
||||
updateSubmit: "Actualizar persona",
|
||||
},
|
||||
fallback: {
|
||||
unknownDepartment: "Departamento desconocido",
|
||||
},
|
||||
departments: {
|
||||
IT: "IT",
|
||||
ENGINEERING: "Ingeniería",
|
||||
LOGISTICS: "Logística",
|
||||
TRAFFIC: "Tráfico",
|
||||
DRIVER: "Chofer",
|
||||
ADMINISTRATION: "Administración",
|
||||
SALES: "Ventas",
|
||||
OTHER: "Otro",
|
||||
},
|
||||
actions: {
|
||||
createSuccess: "Persona creada correctamente",
|
||||
createFailure: "Error al crear la persona",
|
||||
updateSuccess: "Persona actualizada correctamente",
|
||||
updateFailure: "Error al actualizar la persona",
|
||||
duplicateEmail: "El correo electrónico ya existe",
|
||||
},
|
||||
schema: {
|
||||
firstNameRequired: "El nombre es obligatorio",
|
||||
lastNameRequired: "El apellido es obligatorio",
|
||||
departmentRequired: "El departamento es obligatorio",
|
||||
emailInvalid: "El correo electrónico no es válido",
|
||||
idRequired: "El ID es obligatorio",
|
||||
},
|
||||
},
|
||||
recipients: {
|
||||
list: {
|
||||
title: "Destinatarios",
|
||||
|
||||
@@ -8,7 +8,7 @@ export const SIGN_IN_URL = "/login"
|
||||
|
||||
export const TOKEN_EXPIRATION_SECONDS = 60 * 60 * 2 // 2 hour
|
||||
|
||||
export const RECIPIENT_DEPARTMENTS = {
|
||||
export const PERSON_DEPARTMENTS = {
|
||||
IT: "IT",
|
||||
ENGINEERING: "ENGINEERING",
|
||||
LOGISTICS: "LOGISTICS",
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import type { Dictionary } from "@/i18n/dictionaries"
|
||||
|
||||
export type PersonSchemaCopy = Dictionary["inventory"]["people"]["schema"]
|
||||
|
||||
const defaultPersonSchemaCopy: PersonSchemaCopy = {
|
||||
firstNameRequired: "First name is required",
|
||||
lastNameRequired: "Last name is required",
|
||||
departmentRequired: "Department is required",
|
||||
emailInvalid: "Email format is invalid",
|
||||
idRequired: "ID is required",
|
||||
}
|
||||
|
||||
const personDepartments = [
|
||||
"IT",
|
||||
"ENGINEERING",
|
||||
"TRAFFIC",
|
||||
"DRIVER",
|
||||
"LOGISTICS",
|
||||
"ADMINISTRATION",
|
||||
"SALES",
|
||||
"OTHER",
|
||||
] as const
|
||||
|
||||
function buildPersonBaseSchema(copy: PersonSchemaCopy) {
|
||||
return z.object({
|
||||
id: z.string().optional(),
|
||||
firstName: z.string().min(1, {
|
||||
error: copy.firstNameRequired,
|
||||
}),
|
||||
lastName: z.string().min(1, {
|
||||
error: copy.lastNameRequired,
|
||||
}),
|
||||
department: z.enum(personDepartments, {
|
||||
error: copy.departmentRequired,
|
||||
}),
|
||||
email: z.string().optional().nullable(),
|
||||
phone: z.string().optional().nullable(),
|
||||
userId: z.string().uuid().optional().nullable(),
|
||||
})
|
||||
}
|
||||
|
||||
export const personSchema = buildPersonBaseSchema(defaultPersonSchemaCopy)
|
||||
|
||||
export function buildCreatePersonSchema(copy: PersonSchemaCopy) {
|
||||
return buildPersonBaseSchema(copy).superRefine((data, ctx) => {
|
||||
if (data.email && !z.string().email().safeParse(data.email).success) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: copy.emailInvalid,
|
||||
path: ["email"],
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const createPersonSchema = buildCreatePersonSchema(
|
||||
defaultPersonSchemaCopy,
|
||||
)
|
||||
|
||||
export type CreatePersonFormType = z.infer<typeof createPersonSchema>
|
||||
|
||||
export function buildUpdatePersonSchema(copy: PersonSchemaCopy) {
|
||||
return buildPersonBaseSchema(copy).extend({
|
||||
id: z.string().nonempty(copy.idRequired),
|
||||
})
|
||||
}
|
||||
|
||||
export const updatePersonSchema = buildUpdatePersonSchema(
|
||||
defaultPersonSchemaCopy,
|
||||
)
|
||||
|
||||
export type UpdatePersonFormType = z.infer<typeof updatePersonSchema>
|
||||
@@ -1,80 +0,0 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import type { Dictionary } from "@/i18n/dictionaries"
|
||||
|
||||
export type RecipientSchemaCopy =
|
||||
Dictionary["inventory"]["recipients"]["schema"]
|
||||
|
||||
const defaultRecipientSchemaCopy: RecipientSchemaCopy = {
|
||||
usernameRequired: "Username is required",
|
||||
firstNameRequired: "First name is required",
|
||||
lastNameRequired: "Last name is required",
|
||||
departmentRequired: "Department is required",
|
||||
emailInvalid: "Email format is invalid",
|
||||
idRequired: "ID is required",
|
||||
}
|
||||
|
||||
const recipientDepartments = [
|
||||
"IT",
|
||||
"ENGINEERING",
|
||||
"TRAFFIC",
|
||||
"DRIVER",
|
||||
"LOGISTICS",
|
||||
"ADMINISTRATION",
|
||||
"SALES",
|
||||
"OTHER",
|
||||
] as const
|
||||
|
||||
function buildRecipientBaseSchema(copy: RecipientSchemaCopy) {
|
||||
return z.object({
|
||||
id: z.string().optional(),
|
||||
username: z.string().min(1, {
|
||||
error: copy.usernameRequired,
|
||||
}),
|
||||
firstName: z.string().min(1, {
|
||||
error: copy.firstNameRequired,
|
||||
}),
|
||||
lastName: z.string().min(1, {
|
||||
error: copy.lastNameRequired,
|
||||
}),
|
||||
department: z.enum(recipientDepartments, {
|
||||
error: copy.departmentRequired,
|
||||
}),
|
||||
email: z.string().optional().nullable(),
|
||||
phone: z.string().optional().nullable(),
|
||||
})
|
||||
}
|
||||
|
||||
export const recipientSchema = buildRecipientBaseSchema(
|
||||
defaultRecipientSchemaCopy,
|
||||
)
|
||||
|
||||
export function buildCreateRecipientSchema(copy: RecipientSchemaCopy) {
|
||||
return buildRecipientBaseSchema(copy).superRefine((data, ctx) => {
|
||||
if (data.email && !z.string().email().safeParse(data.email).success) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: copy.emailInvalid,
|
||||
path: ["email"],
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const createRecipientSchema = buildCreateRecipientSchema(
|
||||
defaultRecipientSchemaCopy,
|
||||
)
|
||||
|
||||
export type CreateRecipientFormType = z.infer<typeof createRecipientSchema>
|
||||
|
||||
export function buildUpdateRecipientSchema(copy: RecipientSchemaCopy) {
|
||||
return buildRecipientBaseSchema(copy).extend({
|
||||
id: z.string().nonempty(copy.idRequired),
|
||||
})
|
||||
}
|
||||
|
||||
export const updateRecipientSchema = buildUpdateRecipientSchema(
|
||||
defaultRecipientSchemaCopy,
|
||||
)
|
||||
|
||||
export type UpdateRecipientFormType = z.infer<typeof updateRecipientSchema>
|
||||
@@ -81,7 +81,7 @@ export const AssignmentService = {
|
||||
},
|
||||
})
|
||||
},
|
||||
findAllByRecipient: async (
|
||||
findAllByPerson: async (
|
||||
recipientId: string,
|
||||
): Promise<AssignmentWithRecipientItemAsset[]> => {
|
||||
return prisma.assignment.findMany({
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { Prisma, Person } from "@/generated/prisma/client"
|
||||
import { paginate } from "@/lib/paginate"
|
||||
import prisma from "@/lib/prisma"
|
||||
|
||||
export const PersonService = {
|
||||
findAll: async (): Promise<Person[]> => {
|
||||
return prisma.person.findMany({
|
||||
orderBy: {
|
||||
firstName: "asc",
|
||||
},
|
||||
})
|
||||
},
|
||||
findAllPaginated: async ({
|
||||
page = 0,
|
||||
pageSize,
|
||||
search,
|
||||
}: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
search?: string
|
||||
}) => {
|
||||
return paginate<Person>({
|
||||
model: prisma.person,
|
||||
page,
|
||||
pageSize,
|
||||
where: {
|
||||
...(search
|
||||
? {
|
||||
OR: [
|
||||
{ email: { contains: search, mode: "insensitive" } },
|
||||
{ firstName: { contains: search, mode: "insensitive" } },
|
||||
{ lastName: { contains: search, mode: "insensitive" } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
})
|
||||
},
|
||||
findAllPeopleCount: async (): Promise<number> => {
|
||||
return prisma.person.count()
|
||||
},
|
||||
|
||||
findById: async (
|
||||
id: string,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<Person | null> => {
|
||||
return db.person.findUnique({ where: { id } })
|
||||
},
|
||||
|
||||
findByEmail: async (
|
||||
email: string,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<Person | null> => {
|
||||
return db.person.findUnique({ where: { email } })
|
||||
},
|
||||
|
||||
create: async (
|
||||
data: Prisma.PersonCreateInput,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<Person> => {
|
||||
return db.person.create({ data })
|
||||
},
|
||||
|
||||
update: async (
|
||||
id: string,
|
||||
data: Prisma.PersonUpdateInput,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<Person> => {
|
||||
return db.person.update({ where: { id }, data })
|
||||
},
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import type { Prisma, Recipient } from "@/generated/prisma/client"
|
||||
import { paginate } from "@/lib/paginate"
|
||||
import prisma from "@/lib/prisma"
|
||||
|
||||
export const RecipientService = {
|
||||
findAll: async (): Promise<Recipient[]> => {
|
||||
return prisma.recipient.findMany({
|
||||
orderBy: {
|
||||
firstName: "asc",
|
||||
},
|
||||
})
|
||||
},
|
||||
findAllPaginated: async ({
|
||||
page = 0,
|
||||
pageSize,
|
||||
search,
|
||||
}: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
search?: string
|
||||
}) => {
|
||||
return paginate<Recipient>({
|
||||
model: prisma.recipient,
|
||||
page,
|
||||
pageSize,
|
||||
where: {
|
||||
...(search
|
||||
? {
|
||||
OR: [
|
||||
{ username: { contains: search, mode: "insensitive" } },
|
||||
{ firstName: { contains: search, mode: "insensitive" } },
|
||||
{ lastName: { contains: search, mode: "insensitive" } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
})
|
||||
},
|
||||
findAllRecipientsCount: async (): Promise<number> => {
|
||||
return prisma.recipient.count()
|
||||
},
|
||||
|
||||
findById: async (
|
||||
id: string,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<Recipient | null> => {
|
||||
return db.recipient.findUnique({ where: { id } })
|
||||
},
|
||||
|
||||
findByUsername: async (
|
||||
username: string,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<Recipient | null> => {
|
||||
return db.recipient.findUnique({ where: { username } })
|
||||
},
|
||||
|
||||
findByEmail: async (
|
||||
email: string,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<Recipient | null> => {
|
||||
return db.recipient.findUnique({ where: { email } })
|
||||
},
|
||||
|
||||
create: async (
|
||||
data: Prisma.RecipientCreateInput,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<Recipient> => {
|
||||
return db.recipient.create({ data })
|
||||
},
|
||||
|
||||
update: async (
|
||||
id: string,
|
||||
data: Prisma.RecipientUpdateInput,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<Recipient> => {
|
||||
return db.recipient.update({ where: { id }, data })
|
||||
},
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import type { Assignment as PrismaAssignment } from "@/generated/prisma/client"
|
||||
|
||||
import type { Asset } from "./asset"
|
||||
import type { Item } from "./item"
|
||||
import type { Recipient } from "./recipient"
|
||||
import type { Person } from "./person"
|
||||
|
||||
export type Assignment = PrismaAssignment
|
||||
|
||||
@@ -10,7 +10,7 @@ export type AssignmentSummary = Pick<Assignment, "id" | "quantity">
|
||||
|
||||
export type AssignmentWithRecipientItemAsset = Assignment & {
|
||||
returnDate: Date | null
|
||||
recipient: Recipient | null
|
||||
recipient: Person | null
|
||||
item: Item | null
|
||||
asset: Asset | null
|
||||
}
|
||||
@@ -6,7 +6,6 @@ export interface ImportItem {
|
||||
category?: string
|
||||
deliveryNote?: string
|
||||
assigned?: boolean
|
||||
username?: string
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,5 +5,5 @@ export * from "./import"
|
||||
export * from "./item"
|
||||
export * from "./movement"
|
||||
export * from "./paginate"
|
||||
export * from "./recipient"
|
||||
export * from "./person"
|
||||
export * from "./user"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import type { Person as PrismaPerson } from "@/generated/prisma/client"
|
||||
|
||||
export type Person = PrismaPerson
|
||||
@@ -1,3 +0,0 @@
|
||||
import type { Recipient as PrismaRecipient } from "@/generated/prisma/client"
|
||||
|
||||
export type Recipient = PrismaRecipient
|
||||
@@ -0,0 +1,127 @@
|
||||
import { Prisma } from "@/generated/prisma/client"
|
||||
import prisma from "@/lib/prisma"
|
||||
import type {
|
||||
CreatePersonFormType,
|
||||
UpdatePersonFormType,
|
||||
} from "@/schemas/person.schema"
|
||||
import { PersonService } from "@/services/person.service"
|
||||
|
||||
type FieldErrors = Record<string, string[]>
|
||||
|
||||
type PersonUseCaseResult =
|
||||
| {
|
||||
success: true
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
errors: FieldErrors
|
||||
}
|
||||
|
||||
function personError(errors: FieldErrors): PersonUseCaseResult {
|
||||
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("email")) {
|
||||
return { email: ["Email already exists"] }
|
||||
}
|
||||
|
||||
return { email: ["Email already exists"] }
|
||||
}
|
||||
|
||||
export async function createPersonUseCase(
|
||||
input: CreatePersonFormType,
|
||||
): Promise<PersonUseCaseResult> {
|
||||
const { firstName, lastName, department, email, phone, userId } = input
|
||||
|
||||
try {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
if (email) {
|
||||
const existingPersonEmail = await PersonService.findByEmail(email, tx)
|
||||
|
||||
if (existingPersonEmail) {
|
||||
return personError({ email: ["Email already exists"] })
|
||||
}
|
||||
}
|
||||
|
||||
await PersonService.create(
|
||||
{
|
||||
firstName,
|
||||
lastName,
|
||||
department,
|
||||
email: email || null,
|
||||
phone: phone || null,
|
||||
...(userId ? { user: { connect: { id: userId } } } : {}),
|
||||
},
|
||||
tx,
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
const errors = uniqueErrorFor(error)
|
||||
|
||||
if (errors) {
|
||||
return personError(errors)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function updatePersonUseCase(
|
||||
input: UpdatePersonFormType,
|
||||
): Promise<PersonUseCaseResult> {
|
||||
const { id, firstName, lastName, department, email, phone, userId } = input
|
||||
|
||||
try {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
if (email) {
|
||||
const existingPersonEmail = await PersonService.findByEmail(email, tx)
|
||||
|
||||
if (existingPersonEmail && existingPersonEmail.id !== id) {
|
||||
return personError({ email: ["Email already exists"] })
|
||||
}
|
||||
}
|
||||
|
||||
await PersonService.update(
|
||||
id,
|
||||
{
|
||||
firstName,
|
||||
lastName,
|
||||
department,
|
||||
email: email || null,
|
||||
phone: phone || null,
|
||||
...(userId ? { user: { connect: { id: userId } } } : { userId: null }),
|
||||
},
|
||||
tx,
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
const errors = uniqueErrorFor(error)
|
||||
|
||||
if (errors) {
|
||||
return personError(errors)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
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<string, string[]>
|
||||
|
||||
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<RecipientUseCaseResult> {
|
||||
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<RecipientUseCaseResult> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type {
|
||||
PrismaClient,
|
||||
RecipientDepartment,
|
||||
PersonDepartment,
|
||||
UserRole,
|
||||
} from "@/generated/prisma/client"
|
||||
|
||||
@@ -48,24 +48,22 @@ export async function createTestCategory(
|
||||
})
|
||||
}
|
||||
|
||||
export async function createTestRecipient(
|
||||
export async function createTestPerson(
|
||||
prisma: PrismaClient,
|
||||
overrides: Partial<{
|
||||
username: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
department: RecipientDepartment
|
||||
department: PersonDepartment
|
||||
email: string | null
|
||||
phone: string | null
|
||||
}> = {},
|
||||
) {
|
||||
const suffix = nextSuffix()
|
||||
|
||||
return prisma.recipient.create({
|
||||
return prisma.person.create({
|
||||
data: {
|
||||
username: overrides.username ?? `test-recipient-${suffix}`,
|
||||
firstName: overrides.firstName ?? "Test",
|
||||
lastName: overrides.lastName ?? "Recipient",
|
||||
lastName: overrides.lastName ?? `Person-${suffix}`,
|
||||
department: overrides.department ?? "OTHER",
|
||||
email: overrides.email ?? null,
|
||||
phone: overrides.phone ?? null,
|
||||
|
||||
@@ -18,7 +18,7 @@ const TABLES_TO_TRUNCATE = [
|
||||
"Asset",
|
||||
"Item",
|
||||
"Category",
|
||||
"Recipient",
|
||||
"Person",
|
||||
"User",
|
||||
]
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"
|
||||
import type { PrismaClient } from "@/generated/prisma/client"
|
||||
import {
|
||||
createTestItem,
|
||||
createTestRecipient,
|
||||
createTestPerson,
|
||||
createTestUser,
|
||||
} from "../helpers/factories"
|
||||
import {
|
||||
@@ -80,7 +80,7 @@ describe("asset use-cases", () => {
|
||||
|
||||
it("creates an assigned asset with assignment and ASSIGNMENT movement", async () => {
|
||||
const actor = await createTestUser(prisma)
|
||||
const recipient = await createTestRecipient(prisma)
|
||||
const recipient = await createTestPerson(prisma)
|
||||
const item = await createTestItem(prisma, { stock: 0 })
|
||||
|
||||
const result = await createAssetUseCase({
|
||||
@@ -133,7 +133,7 @@ describe("asset use-cases", () => {
|
||||
|
||||
it("moves an available asset to assigned and back to available", async () => {
|
||||
const actor = await createTestUser(prisma)
|
||||
const recipient = await createTestRecipient(prisma)
|
||||
const recipient = await createTestPerson(prisma)
|
||||
const item = await createTestItem(prisma, { stock: 0 })
|
||||
|
||||
const created = await createAssetUseCase({
|
||||
@@ -230,7 +230,7 @@ describe("asset use-cases", () => {
|
||||
|
||||
it("returns an active assignment without restoring stock when an assigned asset moves to a terminal status", async () => {
|
||||
const actor = await createTestUser(prisma)
|
||||
const recipient = await createTestRecipient(prisma)
|
||||
const recipient = await createTestPerson(prisma)
|
||||
const item = await createTestItem(prisma, { stock: 0 })
|
||||
|
||||
const created = await createAssetUseCase({
|
||||
|
||||
@@ -2,7 +2,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"
|
||||
import type { PrismaClient } from "@/generated/prisma/client"
|
||||
import {
|
||||
createTestItem,
|
||||
createTestRecipient,
|
||||
createTestPerson,
|
||||
createTestUser,
|
||||
} from "../helpers/factories"
|
||||
import {
|
||||
@@ -38,7 +38,7 @@ afterAll(async () => {
|
||||
describe("assignment use-cases", () => {
|
||||
it("creates an assignment, decrements stock, and records an ASSIGNMENT movement", async () => {
|
||||
const actor = await createTestUser(prisma)
|
||||
const recipient = await createTestRecipient(prisma)
|
||||
const recipient = await createTestPerson(prisma)
|
||||
const item = await createTestItem(prisma, { stock: 5 })
|
||||
|
||||
const assignmentDate = new Date("2026-01-01T00:00:00.000Z")
|
||||
@@ -88,7 +88,7 @@ describe("assignment use-cases", () => {
|
||||
|
||||
it("rejects assignment creation when item stock is insufficient", async () => {
|
||||
const actor = await createTestUser(prisma)
|
||||
const recipient = await createTestRecipient(prisma)
|
||||
const recipient = await createTestPerson(prisma)
|
||||
const item = await createTestItem(prisma, { stock: 1 })
|
||||
|
||||
const result = await createAssignmentUseCase({
|
||||
@@ -114,7 +114,7 @@ describe("assignment use-cases", () => {
|
||||
|
||||
it("returns an assignment, restores stock, closes it, and records a RETURN movement", async () => {
|
||||
const actor = await createTestUser(prisma)
|
||||
const recipient = await createTestRecipient(prisma)
|
||||
const recipient = await createTestPerson(prisma)
|
||||
const item = await createTestItem(prisma, { stock: 4 })
|
||||
|
||||
const created = await createAssignmentUseCase({
|
||||
@@ -172,7 +172,7 @@ describe("assignment use-cases", () => {
|
||||
|
||||
it("rejects returning the same assignment twice", async () => {
|
||||
const actor = await createTestUser(prisma)
|
||||
const recipient = await createTestRecipient(prisma)
|
||||
const recipient = await createTestPerson(prisma)
|
||||
const item = await createTestItem(prisma, { stock: 2 })
|
||||
|
||||
const created = await createAssignmentUseCase({
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"
|
||||
import type { PrismaClient } from "@/generated/prisma/client"
|
||||
import { createTestPerson, createTestUser } from "../helpers/factories"
|
||||
import {
|
||||
resetIntegrationTestDatabase,
|
||||
startIntegrationTestDatabase,
|
||||
stopIntegrationTestDatabase,
|
||||
} from "../helpers/test-db"
|
||||
|
||||
let prisma: PrismaClient
|
||||
let createPersonUseCase: typeof import("@/use-cases/person.use-cases").createPersonUseCase
|
||||
let updatePersonUseCase: typeof import("@/use-cases/person.use-cases").updatePersonUseCase
|
||||
|
||||
beforeAll(async () => {
|
||||
await startIntegrationTestDatabase()
|
||||
|
||||
const prismaModule = await import("@/lib/prisma")
|
||||
const personUseCases = await import("@/use-cases/person.use-cases")
|
||||
|
||||
prisma = prismaModule.prisma
|
||||
createPersonUseCase = personUseCases.createPersonUseCase
|
||||
updatePersonUseCase = personUseCases.updatePersonUseCase
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await resetIntegrationTestDatabase(prisma)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await prisma?.$disconnect()
|
||||
await stopIntegrationTestDatabase()
|
||||
})
|
||||
|
||||
describe("person use-cases", () => {
|
||||
it("creates a person and normalizes empty optional contact fields to null", async () => {
|
||||
await expect(
|
||||
createPersonUseCase({
|
||||
firstName: "Person",
|
||||
lastName: "One",
|
||||
department: "IT",
|
||||
email: "",
|
||||
phone: "",
|
||||
}),
|
||||
).resolves.toEqual({ success: true })
|
||||
|
||||
await expect(
|
||||
prisma.person.findFirstOrThrow({
|
||||
where: { firstName: "Person", lastName: "One" },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
firstName: "Person",
|
||||
lastName: "One",
|
||||
department: "IT",
|
||||
email: null,
|
||||
phone: null,
|
||||
userId: null,
|
||||
})
|
||||
})
|
||||
|
||||
it("creates a person with linked userId", async () => {
|
||||
const user = await createTestUser(prisma)
|
||||
|
||||
await expect(
|
||||
createPersonUseCase({
|
||||
firstName: "Linked",
|
||||
lastName: "Person",
|
||||
department: "ENGINEERING",
|
||||
email: "linked@example.test",
|
||||
phone: null,
|
||||
userId: user.id,
|
||||
}),
|
||||
).resolves.toEqual({ success: true })
|
||||
|
||||
await expect(
|
||||
prisma.person.findFirstOrThrow({
|
||||
where: { firstName: "Linked" },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
firstName: "Linked",
|
||||
lastName: "Person",
|
||||
department: "ENGINEERING",
|
||||
email: "linked@example.test",
|
||||
userId: user.id,
|
||||
})
|
||||
})
|
||||
|
||||
it("rejects duplicate emails on create", async () => {
|
||||
await createTestPerson(prisma, {
|
||||
email: "existing@example.test",
|
||||
})
|
||||
|
||||
await expect(
|
||||
createPersonUseCase({
|
||||
firstName: "Duplicate",
|
||||
lastName: "Email",
|
||||
department: "OTHER",
|
||||
email: "existing@example.test",
|
||||
phone: null,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
success: false,
|
||||
errors: { email: ["Email already exists"] },
|
||||
})
|
||||
|
||||
await expect(prisma.person.count()).resolves.toBe(1)
|
||||
})
|
||||
|
||||
it("updates a person and rejects duplicate emails", async () => {
|
||||
const person = await createTestPerson(prisma, {
|
||||
email: "person@example.test",
|
||||
phone: "111111111",
|
||||
})
|
||||
const other = await createTestPerson(prisma, {
|
||||
email: "other@example.test",
|
||||
})
|
||||
|
||||
await expect(
|
||||
updatePersonUseCase({
|
||||
id: person.id,
|
||||
firstName: "Edited",
|
||||
lastName: "Person",
|
||||
department: "ENGINEERING",
|
||||
email: "edited@example.test",
|
||||
phone: "222222222",
|
||||
}),
|
||||
).resolves.toEqual({ success: true })
|
||||
|
||||
await expect(
|
||||
prisma.person.findUniqueOrThrow({ where: { id: person.id } }),
|
||||
).resolves.toMatchObject({
|
||||
firstName: "Edited",
|
||||
lastName: "Person",
|
||||
department: "ENGINEERING",
|
||||
email: "edited@example.test",
|
||||
phone: "222222222",
|
||||
})
|
||||
|
||||
await expect(
|
||||
updatePersonUseCase({
|
||||
id: person.id,
|
||||
firstName: "Edited",
|
||||
lastName: "Person",
|
||||
department: "ENGINEERING",
|
||||
email: other.email,
|
||||
phone: "222222222",
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
success: false,
|
||||
errors: { email: ["Email already exists"] },
|
||||
})
|
||||
|
||||
await expect(
|
||||
prisma.person.findUniqueOrThrow({ where: { id: person.id } }),
|
||||
).resolves.toMatchObject({
|
||||
email: "edited@example.test",
|
||||
})
|
||||
await expect(prisma.person.count()).resolves.toBe(2)
|
||||
})
|
||||
|
||||
it("searches by email and name in paginated results", async () => {
|
||||
await createTestPerson(prisma, {
|
||||
firstName: "Alice",
|
||||
lastName: "Smith",
|
||||
email: "alice@company.com",
|
||||
})
|
||||
await createTestPerson(prisma, {
|
||||
firstName: "Bob",
|
||||
lastName: "Jones",
|
||||
email: "bob@other.com",
|
||||
})
|
||||
|
||||
const { PersonService } = await import("@/services/person.service")
|
||||
|
||||
const emailResults = await PersonService.findAllPaginated({
|
||||
search: "company",
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
expect(emailResults.data).toHaveLength(1)
|
||||
expect(emailResults.data[0].firstName).toBe("Alice")
|
||||
|
||||
const nameResults = await PersonService.findAllPaginated({
|
||||
search: "Bob",
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
expect(nameResults.data).toHaveLength(1)
|
||||
expect(nameResults.data[0].firstName).toBe("Bob")
|
||||
})
|
||||
})
|
||||
@@ -1,170 +0,0 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"
|
||||
import type { PrismaClient } from "@/generated/prisma/client"
|
||||
import { createTestRecipient } from "../helpers/factories"
|
||||
import {
|
||||
resetIntegrationTestDatabase,
|
||||
startIntegrationTestDatabase,
|
||||
stopIntegrationTestDatabase,
|
||||
} from "../helpers/test-db"
|
||||
|
||||
let prisma: PrismaClient
|
||||
let createRecipientUseCase: typeof import("@/use-cases/recipient.use-cases").createRecipientUseCase
|
||||
let updateRecipientUseCase: typeof import("@/use-cases/recipient.use-cases").updateRecipientUseCase
|
||||
|
||||
beforeAll(async () => {
|
||||
await startIntegrationTestDatabase()
|
||||
|
||||
const prismaModule = await import("@/lib/prisma")
|
||||
const recipientUseCases = await import("@/use-cases/recipient.use-cases")
|
||||
|
||||
prisma = prismaModule.prisma
|
||||
createRecipientUseCase = recipientUseCases.createRecipientUseCase
|
||||
updateRecipientUseCase = recipientUseCases.updateRecipientUseCase
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await resetIntegrationTestDatabase(prisma)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await prisma?.$disconnect()
|
||||
await stopIntegrationTestDatabase()
|
||||
})
|
||||
|
||||
describe("recipient use-cases", () => {
|
||||
it("creates a recipient and normalizes empty optional contact fields to null", async () => {
|
||||
await expect(
|
||||
createRecipientUseCase({
|
||||
username: "recipient-one",
|
||||
firstName: "Recipient",
|
||||
lastName: "One",
|
||||
department: "IT",
|
||||
email: "",
|
||||
phone: "",
|
||||
}),
|
||||
).resolves.toEqual({ success: true })
|
||||
|
||||
await expect(
|
||||
prisma.recipient.findUniqueOrThrow({
|
||||
where: { username: "recipient-one" },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
username: "recipient-one",
|
||||
firstName: "Recipient",
|
||||
lastName: "One",
|
||||
department: "IT",
|
||||
email: null,
|
||||
phone: null,
|
||||
})
|
||||
})
|
||||
|
||||
it("rejects duplicate usernames and duplicate emails on create", async () => {
|
||||
await createTestRecipient(prisma, {
|
||||
username: "existing-recipient",
|
||||
email: "existing-recipient@example.test",
|
||||
})
|
||||
|
||||
await expect(
|
||||
createRecipientUseCase({
|
||||
username: "existing-recipient",
|
||||
firstName: "Duplicate",
|
||||
lastName: "Username",
|
||||
department: "OTHER",
|
||||
email: "unique-recipient@example.test",
|
||||
phone: null,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
success: false,
|
||||
errors: { username: ["Username already exists"] },
|
||||
})
|
||||
|
||||
await expect(
|
||||
createRecipientUseCase({
|
||||
username: "unique-recipient",
|
||||
firstName: "Duplicate",
|
||||
lastName: "Email",
|
||||
department: "OTHER",
|
||||
email: "existing-recipient@example.test",
|
||||
phone: null,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
success: false,
|
||||
errors: { email: ["Email already exists"] },
|
||||
})
|
||||
|
||||
await expect(prisma.recipient.count()).resolves.toBe(1)
|
||||
})
|
||||
|
||||
it("updates a recipient and rejects duplicate usernames or emails", async () => {
|
||||
const recipient = await createTestRecipient(prisma, {
|
||||
username: "editable-recipient",
|
||||
email: "editable-recipient@example.test",
|
||||
phone: "111111111",
|
||||
})
|
||||
const other = await createTestRecipient(prisma, {
|
||||
username: "other-recipient",
|
||||
email: "other-recipient@example.test",
|
||||
})
|
||||
|
||||
await expect(
|
||||
updateRecipientUseCase({
|
||||
id: recipient.id,
|
||||
username: "edited-recipient",
|
||||
firstName: "Edited",
|
||||
lastName: "Recipient",
|
||||
department: "ENGINEERING",
|
||||
email: "edited-recipient@example.test",
|
||||
phone: "222222222",
|
||||
}),
|
||||
).resolves.toEqual({ success: true })
|
||||
|
||||
await expect(
|
||||
prisma.recipient.findUniqueOrThrow({ where: { id: recipient.id } }),
|
||||
).resolves.toMatchObject({
|
||||
username: "edited-recipient",
|
||||
firstName: "Edited",
|
||||
lastName: "Recipient",
|
||||
department: "ENGINEERING",
|
||||
email: "edited-recipient@example.test",
|
||||
phone: "222222222",
|
||||
})
|
||||
|
||||
await expect(
|
||||
updateRecipientUseCase({
|
||||
id: recipient.id,
|
||||
username: other.username,
|
||||
firstName: "Edited",
|
||||
lastName: "Recipient",
|
||||
department: "ENGINEERING",
|
||||
email: "new-recipient@example.test",
|
||||
phone: "222222222",
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
success: false,
|
||||
errors: { username: ["Username already exists"] },
|
||||
})
|
||||
|
||||
await expect(
|
||||
updateRecipientUseCase({
|
||||
id: recipient.id,
|
||||
username: "edited-recipient",
|
||||
firstName: "Edited",
|
||||
lastName: "Recipient",
|
||||
department: "ENGINEERING",
|
||||
email: other.email,
|
||||
phone: "222222222",
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
success: false,
|
||||
errors: { email: ["Email already exists"] },
|
||||
})
|
||||
|
||||
await expect(
|
||||
prisma.recipient.findUniqueOrThrow({ where: { id: recipient.id } }),
|
||||
).resolves.toMatchObject({
|
||||
username: "edited-recipient",
|
||||
email: "edited-recipient@example.test",
|
||||
})
|
||||
await expect(prisma.recipient.count()).resolves.toBe(2)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user