"use server" import { revalidatePath } from "next/cache" import Papa from "papaparse" import { flattenError } from "zod" import { type ImportFormType, importSchema } from "@/schemas/import.schema" import type { CreateMovementFormType } from "@/schemas/movement.schema" import { AssetService } from "@/services/asset.service" import { AssignmentService } from "@/services/assignment.service" 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 { PersonService } from "@/services/person.service" import type { Asset, Assignment, Category, ImportItem, Item, Person, } from "@/types" export async function importItems(formData: ImportFormType) { const validatedFields = importSchema.safeParse(formData) if (!validatedFields.success) { return { errors: flattenError(validatedFields.error).fieldErrors, } } const { file, categoryId } = validatedFields.data const userId = await getAuthenticatedUserId() if (!file) { return { errors: { file: ["File is required"], }, } } const fileStream = await file.text() const parsedFile = Papa.parse>( fileStream, { header: true, skipEmptyLines: true, dynamicTyping: false, }, ) const { data: rows, meta, errors: papaErrors } = parsedFile if (papaErrors.length > 0) { return { errors: { file: papaErrors.flatMap((err) => err.message), }, } } if (!rows || rows.length === 0) { return { errors: { file: ["File is empty"], }, } } const fileHeaders = meta?.fields || [] if (fileHeaders.length === 0) { return { errors: { file: ["File has no headers"], }, } } if (!fileHeaders.includes("name")) { return { errors: { file: ["Required header name is missing"], }, } } if (categoryId) { if ( fileHeaders.includes("category") || fileHeaders.includes("categoryId") ) { return { errors: { file: [ "If you select a category in the form, you must not select a category or categoryId in the file", ], }, } } else { if ( !fileHeaders.includes("category") && !fileHeaders.includes("categoryId") ) { return { errors: { file: [ "If you not select a category in the form, you must select a category or categoryId in the file", ], }, } } } } if (fileHeaders.includes("category") && fileHeaders.includes("categoryId")) { return { errors: { file: [ "Only one of category or categoryId is allowed, you must select one of them", ], } } } if ( fileHeaders.includes("assigned") && !fileHeaders.includes("firstName") && !fileHeaders.includes("lastName") ) { return { errors: { file: [ "If you select assigned, you must select firstName and lastName in the file", ], }, } } const itemsToCreate: ImportItem[] = [] const importErrors: string[] = [] for (const [index, row] of rows.entries()) { const { name, stock, serialNumber, categoryId, category, deliveryNote, assigned, firstName, lastName, } = row if (!name) { importErrors.push(`Row ${index + 2}: Name is required`) } if (!categoryId && !category) { importErrors.push(`Row ${index + 2}: Category or categoryId is required`) } if (stock && Number.isNaN(stock)) { importErrors.push(`Row ${index + 2}: Stock must be a number`) } if (serialNumber && typeof serialNumber !== "string") { importErrors.push(`Row ${index + 2}: Serial number must be a string`) } if (deliveryNote && typeof deliveryNote !== "string") { importErrors.push(`Row ${index + 2}: Delivery note must be a string`) } if (firstName && typeof firstName !== "string") { importErrors.push(`Row ${index + 2}: First name must be a string`) } if (lastName && typeof lastName !== "string") { importErrors.push(`Row ${index + 2}: Last name must be a string`) } if (assigned === "true" && !firstName && !lastName) { importErrors.push( `Row ${index + 2}: If assigned is true, firstName and lastName are required`, ) } } if (importErrors.length > 0) { return { errors: { file: importErrors, }, } } for (const row of rows) { itemsToCreate.push({ name: row.name?.trim() || "", stock: row.stock ? Number(row.stock) : row.serialNumber ? 1 : 0, serialNumber: row.serialNumber?.trim() || "", categoryId: categoryId ? categoryId : row.categoryId?.trim() || "", category: row.category?.trim() || "", deliveryNote: row.deliveryNote?.trim() || "", assigned: row.assigned?.trim() === "true", firstName: row.firstName?.trim() || "", lastName: row.lastName?.trim() || "", }) } for (const item of itemsToCreate) { const { name, stock, serialNumber, categoryId, category, deliveryNote, assigned, firstName, lastName, } = item // Reset variables at the beginning of each iteration let newItem: Item | null = null let newAsset: Asset | null = null let newCategory: Category | null = null let newPerson: Person | null = null let newAssignment: Assignment | null = null const existingCategory = categoryId ? await CategoryService.findById(categoryId) : await CategoryService.findByName(category || "") if (!existingCategory && category) { newCategory = await CategoryService.create({ name: category }) } else { newCategory = existingCategory } const existingItem = await ItemService.findByName(name) if (!existingItem) { newItem = await ItemService.create({ name, stock: assigned ? 0 : stock || 0, category: { connect: { id: categoryId ? categoryId : newCategory?.id || "" }, }, }) } else { newItem = existingItem await ItemService.update(existingItem.id, { name: existingItem.name, stock: !assigned ? (existingItem?.stock || 0) + (stock || 1) : existingItem?.stock || 0, }) } if (serialNumber) { const existingAsset = await AssetService.findBySerialNumber(serialNumber) if (!existingAsset) { newAsset = await AssetService.create({ item: { connect: { id: newItem.id }, }, serialNumber, status: assigned ? "ASSIGNED" : "AVAILABLE", deliveryNote: deliveryNote || "", }) } else { // Skip asset if already exists continue } } if (assigned && firstName && lastName) { const existingPerson = firstName ? await PersonService.findAllPaginated({ search: firstName, page: 0, pageSize: 1, }) : null if (!existingPerson || existingPerson.data.length === 0) { newPerson = await PersonService.create({ firstName, lastName, email: undefined, phone: "", department: "OTHER", }) } else { newPerson = existingPerson.data[0] } newAssignment = await AssignmentService.create({ quantity: stock || 1, notes: deliveryNote || "", itemId: newItem?.id || "", assetId: newAsset?.id || "", recipientId: newPerson?.id || "", assignmentDate: new Date(), createdBy: userId, }) } const movementData: CreateMovementFormType = { assetId: newAsset?.id || undefined, quantity: stock || 1, type: assigned ? "ASSIGNMENT" : "IN", itemId: newItem?.id || undefined, recipientId: newPerson?.id || undefined, } if (newAssignment?.id) { movementData.assignmentId = newAssignment.id } if (newPerson?.id) { movementData.recipientId = newPerson.id } await MovementService.create({ ...movementData, userId, }) } revalidatePath("/inventory/items") return { success: true, message: "Items imported successfully!", } }