refactor(structure): move legacy import and remove lib leftovers
This commit is contained in:
@@ -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!",
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export type PaginatedResult<T> = {
|
||||
data: T[]
|
||||
totalItems: number
|
||||
totalPages: number
|
||||
currentPage: number
|
||||
pageSize: number
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import type { Recipient as PrismaRecipient } from "@/generated/prisma/client"
|
||||
|
||||
export type Recipient = PrismaRecipient
|
||||
@@ -1,3 +0,0 @@
|
||||
import type { User as PrismaUser } from "@/generated/prisma/client"
|
||||
|
||||
export type User = PrismaUser
|
||||
Reference in New Issue
Block a user