Files
stock-manager/src/actions/import.actions.ts
T

345 lines
8.4 KiB
TypeScript

"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<Record<string, string | undefined>>(
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!",
}
}