refactor(structure): move legacy import and remove lib leftovers

This commit is contained in:
2026-06-04 22:13:26 +02:00
parent da9ae0582b
commit a7f7ace527
17 changed files with 27 additions and 220 deletions
-344
View File
@@ -1,344 +0,0 @@
"use server"
import { revalidatePath } from "next/cache"
import Papa from "papaparse"
import { flattenError } from "zod"
import { type ImportFormType, importSchema } from "@/lib/schemas/import.schemas"
import type { CreateMovementFormType } from "@/lib/schemas/movement.schemas"
import type {
Asset,
Assignment,
Category,
ImportItem,
Item,
Recipient,
} from "@/lib/types"
import { AssetService } from "@/services/asset.service"
import { AssignmentService } from "@/services/assignment.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"
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
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,
username,
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 (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`)
}
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",
username: row.username?.trim() || "",
firstName: row.firstName?.trim() || "",
lastName: row.lastName?.trim() || "",
})
}
for (const item of itemsToCreate) {
const {
name,
stock,
serialNumber,
categoryId,
category,
deliveryNote,
assigned,
username,
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 newRecipient: Recipient | 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 finalUsername =
username || `${firstName.toLowerCase()[0]}${lastName.toLowerCase()}`
const existingRecipient =
await RecipientService.findByUsername(finalUsername)
if (!existingRecipient) {
newRecipient = await RecipientService.create({
username: finalUsername,
firstName,
lastName,
email: undefined,
phone: "",
department: "OTHER",
})
} else {
newRecipient = existingRecipient
}
newAssignment = await AssignmentService.create({
quantity: stock || 1,
notes: deliveryNote || "",
itemId: newItem?.id || "",
assetId: newAsset?.id || "",
recipientId: newRecipient?.id || "",
assignmentDate: new Date(),
})
}
const movementData: CreateMovementFormType = {
assetId: newAsset?.id || undefined,
quantity: stock || 1,
type: assigned ? "ASSIGNMENT" : "IN",
itemId: newItem?.id || undefined,
recipientId: newRecipient?.id || undefined,
}
if (newAssignment?.id) {
movementData.assignmentId = newAssignment.id
}
if (newRecipient?.id) {
movementData.recipientId = newRecipient.id
}
await MovementService.create(movementData)
}
revalidatePath("/inventory/items")
return {
success: true,
message: "Items imported successfully!",
}
}
-15
View File
@@ -1,15 +0,0 @@
import {
type CreateMovementFormType,
createMovementSchema,
} from "../schemas/movement.schemas"
export async function createMovementAction(data: CreateMovementFormType) {
const validatedFields = createMovementSchema.safeParse(data)
if (!validatedFields.success) {
return {
success: false,
errors: validatedFields.error.flatten().fieldErrors,
}
}
}
+4 -5
View File
@@ -1,10 +1,9 @@
import type { Prisma } from "@/generated/prisma/client"
import type { PaginatedResult } from "@/lib/types"
import type { PaginatedResult } from "@/types"
//eslint-disable-next-line
type PaginationArgs<T> = {
model: any // prisma.<model>
where?: Prisma.Enumerable<any> // filtros
type PaginationArgs<_T> = {
model: any
where?: Prisma.Enumerable<any>
page?: number
pageSize?: number
orderBy?: any
-13
View File
@@ -1,13 +0,0 @@
import { z } from "zod"
const ACCEPTED_MIME_TYPES = ["text/csv", "text/comma-separated-values"]
export const importSchema = z.object({
file: z.file()
.refine((file) => ACCEPTED_MIME_TYPES.includes(file.type), {
error: "File must be a CSV"
}),
categoryId: z.string().optional(),
})
export type ImportFormType = z.infer<typeof importSchema>
-25
View File
@@ -1,25 +0,0 @@
import { z } from "zod"
export const movementSchema = z.object({
type: z.enum(["IN", "OUT", "ASSIGNMENT", "RETURN", "ADJUSTMENT"]),
quantity: z.coerce
.number()
.int()
.nonnegative()
.min(1, {
error: "Quantity is required"
}),
details: z.string().optional(),
notes: z.string().optional(),
itemId: z.string().optional(),
assetId: z.string().optional(),
userId: z.string(),
assignmentId: z.string().optional(),
recipientId: z.string().optional(),
})
export const createMovementSchema = movementSchema.omit({
userId: true,
})
export type CreateMovementFormType = z.infer<typeof createMovementSchema>
-34
View File
@@ -1,34 +0,0 @@
import type {
Assignment,
Asset as PrismaAsset,
ItemStatus as PrismaItemStatus,
} from "@/generated/prisma/client"
export type Asset = PrismaAsset
export type ItemStatus = PrismaItemStatus
export type NewAssetStatus = "AVAILABLE" | "ASSIGNED"
export type UpdateAssetStatus = NewAssetStatus | "RESERVED" | "IN_REPAIR"
export type AssetSummary = Pick<Asset, "id" | "serialNumber" | "status">
export type AssetWithAssignment = Asset & {
assignment: Assignment | null
}
export type AssetWithItemAndCategory = {
id: string
serialNumber: string
deliveryNote?: string | null
status: ItemStatus
item: {
id: string
name: string
category: {
id: string
name: string
}
} | null
}
-21
View File
@@ -1,21 +0,0 @@
import type { Assignment as PrismaAssignment } from "@/generated/prisma/client"
import type { Asset } from "./asset"
import type { Item } from "./item"
import type { Recipient } from "./recipient"
export type Assignment = PrismaAssignment
export type AssignmentSummary = Pick<Assignment, "id" | "quantity">
export type AssignmentWithRecipientItemAsset = Assignment & {
returnDate: Date | null
recipient: Recipient | null
item: Item | null
asset: Asset | null
}
export type AssignmentWithItemAndAsset = AssignmentSummary & {
item: Item
asset: Asset
}
-22
View File
@@ -1,22 +0,0 @@
import type { Category as PrismaCategory } from "@/generated/prisma/client"
export type Category = PrismaCategory
export type CategorySummary = Pick<Category, "id" | "name">
export type CategoryWithItemsCount = CategorySummary & {
_count: {
items: number
}
}
export interface CreateCategoryInput {
name: string
description?: string | null
}
export interface UpdateCategoryInput {
name?: string
description?: string | null
isActive?: boolean
}
-12
View File
@@ -1,12 +0,0 @@
export interface ImportItem {
name: string
stock?: number
serialNumber?: string
categoryId?: string
category?: string
deliveryNote?: string
assigned?: boolean
username?: string
firstName?: string
lastName?: string
}
-9
View File
@@ -1,9 +0,0 @@
export * from "./asset"
export * from "./assignment"
export * from "./category"
export * from "./import"
export * from "./item"
export * from "./movement"
export * from "./paginate"
export * from "./recipient"
export * from "./user"
-21
View File
@@ -1,21 +0,0 @@
import type { Category, Item as PrismaItem } from "@/generated/prisma/client"
export type Item = PrismaItem
export type ItemSummary = Pick<Item, "id" | "name" | "stock"> & {
category: Pick<Category, "id" | "name">
}
export type ItemWithoutStock = Pick<Item, "id" | "name">
export type ItemWithAssetCount = ItemSummary & {
_count: {
assets: number
}
}
export type ItemWithAssetAndMovementCount = ItemWithAssetCount & {
_count: {
movements: number
}
}
-24
View File
@@ -1,24 +0,0 @@
import type { Movement as PrismaMovement } from "@/generated/prisma/client"
import type { Asset } from "./asset"
import type { Item } from "./item"
import type { Recipient } from "./recipient"
export type Movement = PrismaMovement
export type MovementSummary = Pick<Movement, "id" | "type" | "quantity">
export type MovementWithItem = Movement & {
item: Item | null
}
export type MovementWithItemAndAsset = Movement & {
item: Item | null
asset: Asset | null
}
export type MovementWithRecipientItemAsset = Movement & {
recipient: Recipient | null
item: Item | null
asset: Asset | null
}
-7
View File
@@ -1,7 +0,0 @@
export type PaginatedResult<T> = {
data: T[]
totalItems: number
totalPages: number
currentPage: number
pageSize: number
}
-3
View File
@@ -1,3 +0,0 @@
import type { Recipient as PrismaRecipient } from "@/generated/prisma/client"
export type Recipient = PrismaRecipient
-3
View File
@@ -1,3 +0,0 @@
import type { User as PrismaUser } from "@/generated/prisma/client"
export type User = PrismaUser