345 lines
8.4 KiB
TypeScript
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!",
|
|
}
|
|
} |