first version
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
"use server"
|
||||
|
||||
import { revalidatePath } from "next/cache"
|
||||
|
||||
import {
|
||||
createAssignment,
|
||||
updateAssignment,
|
||||
} from "@/lib/actions/assignament.actions"
|
||||
import {
|
||||
CreateAssetFormType,
|
||||
createAssetSchema,
|
||||
UpdateAssetFormType,
|
||||
updateAssetSchema,
|
||||
} from "@/lib/schemas/asset.schemas"
|
||||
import { AssetService } from "@/services/asset.service"
|
||||
import { AssignmentService } from "@/services/assignment.service"
|
||||
import { ItemService } from "@/services/item.service"
|
||||
import { MovementService } from "@/services/movement.service"
|
||||
|
||||
export async function createAssetAction(formData: CreateAssetFormType) {
|
||||
try {
|
||||
const validatedFields = createAssetSchema.safeParse(formData)
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return {
|
||||
errors: validatedFields.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
const { itemId, serialNumber, deliveryNote, status, notes, recipientId } =
|
||||
validatedFields.data
|
||||
|
||||
const item = await ItemService.findByIdWithCategory(itemId)
|
||||
|
||||
if (!item) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { itemId: ["Item not found"] },
|
||||
}
|
||||
}
|
||||
|
||||
const existentAsset = await AssetService.findBySerialNumber(serialNumber)
|
||||
|
||||
if (existentAsset) {
|
||||
return {
|
||||
success: false,
|
||||
errors: {
|
||||
serialNumber: ["This serial number already exists"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const newAsset = await AssetService.create({
|
||||
item: { connect: { id: itemId } },
|
||||
serialNumber,
|
||||
deliveryNote,
|
||||
status,
|
||||
notes,
|
||||
})
|
||||
|
||||
await MovementService.create({
|
||||
itemId,
|
||||
assetId: newAsset?.id,
|
||||
quantity: 1,
|
||||
type: status === "ASSIGNED" ? "ASSIGNMENT" : "IN",
|
||||
})
|
||||
|
||||
if (status === "AVAILABLE") {
|
||||
await ItemService.update(itemId, {
|
||||
stock: item.stock + 1,
|
||||
name: item.name,
|
||||
category: { connect: { id: item.category?.id } },
|
||||
})
|
||||
}
|
||||
|
||||
if (status === "ASSIGNED" && recipientId) {
|
||||
await AssignmentService.create({
|
||||
notes: "",
|
||||
itemId,
|
||||
assetId: newAsset?.id,
|
||||
quantity: 1,
|
||||
recipientId,
|
||||
assignmentDate: new Date(),
|
||||
})
|
||||
}
|
||||
|
||||
revalidatePath("/inventory/assets")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Asset created successfully",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return {
|
||||
success: false,
|
||||
message: "Error creating asset",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateAssetAction(formData: UpdateAssetFormType) {
|
||||
const validatedFields = updateAssetSchema.safeParse(formData)
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return {
|
||||
errors: validatedFields.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
const { id, itemId, serialNumber, deliveryNote, status, notes, recipientId } =
|
||||
validatedFields.data
|
||||
|
||||
try {
|
||||
const item = await ItemService.findByIdWithCategory(itemId)
|
||||
|
||||
if (!item) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { itemId: ["Item not found"] },
|
||||
}
|
||||
}
|
||||
|
||||
const existentAsset = await AssetService.findBySerialNumber(serialNumber)
|
||||
|
||||
if (
|
||||
existentAsset &&
|
||||
id !== existentAsset.id &&
|
||||
existentAsset.serialNumber === serialNumber
|
||||
) {
|
||||
return {
|
||||
success: false,
|
||||
errors: {
|
||||
serialNumber: ["This serial number already exists"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
await AssetService.update(id, {
|
||||
item: { connect: { id: itemId } },
|
||||
serialNumber,
|
||||
deliveryNote,
|
||||
status,
|
||||
notes,
|
||||
})
|
||||
|
||||
await MovementService.create({
|
||||
itemId,
|
||||
assetId: id,
|
||||
quantity: 1,
|
||||
type: status === "ASSIGNED" ? "ASSIGNMENT" : "IN",
|
||||
})
|
||||
|
||||
if (status === "AVAILABLE") {
|
||||
await ItemService.update(itemId, {
|
||||
stock: item.stock + 1,
|
||||
name: item.name,
|
||||
category: { connect: { id: item.category?.id } },
|
||||
})
|
||||
}
|
||||
|
||||
if (status === "ASSIGNED" && recipientId) {
|
||||
if (!recipientId) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { recipientId: ["Recipient is required for assignment"] },
|
||||
}
|
||||
}
|
||||
|
||||
if (existentAsset?.assignment) {
|
||||
await updateAssignment({
|
||||
id: existentAsset.assignment.id,
|
||||
recipientId,
|
||||
quantity: 1,
|
||||
})
|
||||
} else {
|
||||
await createAssignment({
|
||||
itemId,
|
||||
assetId: id,
|
||||
quantity: 1,
|
||||
recipientId,
|
||||
assignmentDate: new Date(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Asset updated successfully",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return {
|
||||
success: false,
|
||||
message: "Error updating asset",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
"use server"
|
||||
|
||||
import { revalidatePath } from "next/cache"
|
||||
|
||||
import prisma from "@/lib/prisma"
|
||||
import { AssetService } from "@/services/asset.service"
|
||||
import { AssignmentService } from "@/services/assignment.service"
|
||||
import { getAuthenticatedUserId } from "@/services/auth.service"
|
||||
import { ItemService } from "@/services/item.service"
|
||||
import { MovementService } from "@/services/movement.service"
|
||||
|
||||
import {
|
||||
assignmentSchema,
|
||||
CreateAssignmentFormType,
|
||||
ReturnAssignmentFormType,
|
||||
UpdateAssignmentFormType,
|
||||
updateAssignmentSchema,
|
||||
} from "../schemas/assignment.schemas"
|
||||
|
||||
export async function getAssignments() {
|
||||
return await AssignmentService.findAllWithRecipient()
|
||||
}
|
||||
|
||||
export async function getAssignmentById(id: string) {
|
||||
return await AssignmentService.findById(id)
|
||||
}
|
||||
|
||||
export async function createAssignment(formData: CreateAssignmentFormType) {
|
||||
const createdBy = await getAuthenticatedUserId()
|
||||
|
||||
const validatedFields = assignmentSchema.safeParse({
|
||||
...formData,
|
||||
createdBy,
|
||||
})
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return {
|
||||
success: false,
|
||||
errors: validatedFields.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
const { itemId, assetId, quantity, recipientId } = validatedFields.data
|
||||
|
||||
if (!itemId || !recipientId || quantity <= 0) return
|
||||
|
||||
try {
|
||||
const item = await ItemService.findById(itemId)
|
||||
|
||||
if (!item) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { itemId: ["Item not found"] },
|
||||
}
|
||||
}
|
||||
|
||||
if (item.stock < quantity) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { quantity: ["Item does not have enough stock"] },
|
||||
}
|
||||
}
|
||||
|
||||
if (assetId) {
|
||||
const asset = await AssetService.findById(assetId)
|
||||
|
||||
if (!asset) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { assetId: ["Asset not found"] },
|
||||
}
|
||||
}
|
||||
|
||||
if (asset.itemId !== item.id) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { assetId: ["Asset does not belong to item"] },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await ItemService.updateStock(itemId, -quantity)
|
||||
|
||||
if (assetId && recipientId) {
|
||||
await AssetService.update(assetId, {
|
||||
status: "ASSIGNED",
|
||||
})
|
||||
}
|
||||
|
||||
const createdAssignment = await AssignmentService.create({
|
||||
itemId,
|
||||
assetId: assetId || undefined,
|
||||
quantity,
|
||||
recipientId,
|
||||
})
|
||||
|
||||
await MovementService.create({
|
||||
itemId,
|
||||
assetId: assetId || undefined,
|
||||
quantity,
|
||||
type: "ASSIGNMENT",
|
||||
recipientId,
|
||||
assignmentId: createdAssignment.id,
|
||||
})
|
||||
|
||||
revalidatePath("/assignments")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Assignment created successfully",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return {
|
||||
success: false,
|
||||
errors: { error: ["Error creating assignment"] },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateAssignment(formData: UpdateAssignmentFormType) {
|
||||
const validatedFields = updateAssignmentSchema.safeParse(formData)
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return {
|
||||
success: false,
|
||||
errors: validatedFields.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { id, itemId, assetId, quantity } = validatedFields.data
|
||||
|
||||
const assignment = await prisma.assignment.findUnique({
|
||||
where: { id },
|
||||
})
|
||||
|
||||
if (!assignment) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { id: ["Assignment not found"] },
|
||||
}
|
||||
}
|
||||
|
||||
if (itemId) {
|
||||
const item = await prisma.item.findUnique({
|
||||
where: { id: itemId },
|
||||
})
|
||||
|
||||
if (!item) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { itemId: ["Item not found"] },
|
||||
}
|
||||
}
|
||||
|
||||
if (item.stock < quantity) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { quantity: ["Item does not have enough stock"] },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (assetId) {
|
||||
const asset = await prisma.asset.findUnique({
|
||||
where: { id: assetId },
|
||||
})
|
||||
|
||||
if (!asset) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { assetId: ["Asset not found"] },
|
||||
}
|
||||
}
|
||||
|
||||
if (asset.itemId !== itemId) {
|
||||
await prisma.asset.update({
|
||||
where: { id: assetId },
|
||||
data: { itemId },
|
||||
})
|
||||
|
||||
return {
|
||||
success: false,
|
||||
errors: { assetId: ["Asset does not belong to item"] },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.assignment.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...validatedFields.data,
|
||||
itemId,
|
||||
assetId,
|
||||
quantity,
|
||||
returnDate: new Date(),
|
||||
},
|
||||
})
|
||||
|
||||
revalidatePath("/assignments")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Assignment updated successfully",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return {
|
||||
success: false,
|
||||
errors: { error: ["Error creating assignment"] },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function returnAssignment(formData: ReturnAssignmentFormType) {
|
||||
const { id } = formData
|
||||
|
||||
const assignment = await AssignmentService.findById(id)
|
||||
|
||||
if (!assignment) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { id: ["Assignment not found"] },
|
||||
}
|
||||
}
|
||||
|
||||
if (assignment.returnDate) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { id: ["Assignment already returned"] },
|
||||
}
|
||||
}
|
||||
|
||||
if (assignment.itemId && assignment.quantity) {
|
||||
await ItemService.update(assignment.itemId, {
|
||||
stock: { increment: assignment.quantity || 1 },
|
||||
})
|
||||
}
|
||||
|
||||
if (assignment.assetId) {
|
||||
await AssetService.update(assignment.assetId, {
|
||||
status: "AVAILABLE",
|
||||
})
|
||||
}
|
||||
|
||||
await AssignmentService.delete(id)
|
||||
|
||||
await MovementService.create({
|
||||
type: "RETURN",
|
||||
quantity: assignment.quantity || 1,
|
||||
itemId: assignment.itemId || undefined,
|
||||
assetId: assignment.assetId || undefined,
|
||||
assignmentId: id,
|
||||
})
|
||||
|
||||
revalidatePath("/assignments")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Assignment returned successfully",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
"use server"
|
||||
|
||||
import { AuthError } from "next-auth"
|
||||
|
||||
import { signIn } from "@/lib/auth"
|
||||
import { SignInFormType } from "@/lib/schemas/auth.schemas"
|
||||
|
||||
export async function signInAction(values: SignInFormType) {
|
||||
const { username, password } = values
|
||||
|
||||
try {
|
||||
await signIn("credentials", {
|
||||
username,
|
||||
password,
|
||||
redirect: false,
|
||||
})
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return { error: error.cause?.err?.message }
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
"use server"
|
||||
|
||||
import { revalidatePath } from "next/cache"
|
||||
|
||||
import {
|
||||
CreateCategoryFormType,
|
||||
createCategorySchema,
|
||||
UpdateCategoryFormType,
|
||||
updateCategorySchema,
|
||||
} from "@/lib/schemas/category.schemas"
|
||||
import { CategoryService } from "@/services/category.service"
|
||||
|
||||
export async function createCategoryAction(formData: CreateCategoryFormType) {
|
||||
const validatedFields = createCategorySchema.safeParse(formData)
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return {
|
||||
success: false,
|
||||
errors: validatedFields.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const existingCategory = await CategoryService.findByName(
|
||||
validatedFields.data.name,
|
||||
)
|
||||
|
||||
if (existingCategory) {
|
||||
return {
|
||||
success: false,
|
||||
errors: {
|
||||
name: ["Category already exists"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
await CategoryService.create(validatedFields.data)
|
||||
|
||||
revalidatePath("/inventory/categories")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Category created successfully",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to create category",
|
||||
errors: {
|
||||
name: ["Category already exists"],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateCategoryAction(formData: UpdateCategoryFormType) {
|
||||
const validatedFields = updateCategorySchema.safeParse(formData)
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return {
|
||||
success: false,
|
||||
errors: validatedFields.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
const { id, name } = validatedFields.data
|
||||
|
||||
try {
|
||||
const existingCategory = await CategoryService.findById(id)
|
||||
|
||||
if (!existingCategory) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { id: ["Category not found"] },
|
||||
}
|
||||
}
|
||||
|
||||
if (existingCategory.name === name) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { name: ["Category name is the same as the old one"] },
|
||||
}
|
||||
}
|
||||
|
||||
if (await CategoryService.findByName(name)) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { name: ["Category already exists"] },
|
||||
}
|
||||
}
|
||||
|
||||
await CategoryService.update(id, {
|
||||
name,
|
||||
})
|
||||
|
||||
revalidatePath("/inventory/categories")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Category updated successfully",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to update category",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCategoryAction(formData: FormData) {
|
||||
const { id } = Object.fromEntries(formData) as { id: string }
|
||||
|
||||
try {
|
||||
const existingCategory = await CategoryService.findAllWithItemsCount()
|
||||
const category = existingCategory.find((category) => category.id === id)
|
||||
|
||||
if (!category) {
|
||||
return {
|
||||
success: false,
|
||||
errors: {
|
||||
id: ["Category not found"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (category._count.items && category._count.items > 0) {
|
||||
return {
|
||||
success: false,
|
||||
errors: {
|
||||
id: ["Category has items"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
await CategoryService.delete(id)
|
||||
|
||||
revalidatePath("/inventory/categories")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Category deleted successfully",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to delete category",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
"use server"
|
||||
|
||||
import { revalidatePath } from "next/cache"
|
||||
import Papa from "papaparse"
|
||||
|
||||
import { ImportFormType, importSchema } from "@/lib/schemas/import.schemas"
|
||||
import { ImportItem } 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: validatedFields.error.flatten().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.map((err) => err.message).flat(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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 && isNaN(Number(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" ? true : false,
|
||||
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
|
||||
let newAsset
|
||||
let newCategory
|
||||
let newRecipient
|
||||
let newAssignment
|
||||
|
||||
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: any = {
|
||||
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!",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
"use server"
|
||||
|
||||
import { revalidatePath } from "next/cache"
|
||||
|
||||
import prisma from "@/lib/prisma"
|
||||
import {
|
||||
CreateItemFormType,
|
||||
createItemSchema,
|
||||
UpdateItemFormType,
|
||||
updateItemSchema,
|
||||
} from "@/lib/schemas/item.schemas"
|
||||
import { getAuthenticatedUserId } from "@/services/auth.service"
|
||||
import { ItemService } from "@/services/item.service"
|
||||
|
||||
export async function createItemAction(formData: CreateItemFormType) {
|
||||
const validatedFields = createItemSchema.safeParse(formData)
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return {
|
||||
errors: validatedFields.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
const { name, categoryId, stock } = validatedFields.data
|
||||
|
||||
if (stock < 0) {
|
||||
return {
|
||||
success: false,
|
||||
errors: {
|
||||
stock: ["Stock cannot be negative"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const existingItem = await ItemService.findByName(name)
|
||||
|
||||
if (existingItem) {
|
||||
return {
|
||||
success: false,
|
||||
errors: {
|
||||
name: ["An item with this name already exists"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const item = await ItemService.create({
|
||||
name,
|
||||
category: { connect: { id: categoryId } },
|
||||
stock: stock || 0,
|
||||
})
|
||||
|
||||
if (stock > 0) {
|
||||
await prisma.movement.create({
|
||||
data: {
|
||||
type: "IN",
|
||||
itemId: item.id,
|
||||
userId: await getAuthenticatedUserId(),
|
||||
quantity: stock,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
revalidatePath("/inventory/items")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Item created successfully!",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return {
|
||||
error: "Error creating item",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateItemAction(formData: UpdateItemFormType) {
|
||||
const validatedFields = updateItemSchema.safeParse(formData)
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return {
|
||||
errors: validatedFields.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
const { id, stock, name, categoryId } = validatedFields.data
|
||||
|
||||
try {
|
||||
const existingItem = await ItemService.findByIdWithAssetCount(id)
|
||||
|
||||
if (!existingItem) {
|
||||
return {
|
||||
success: false,
|
||||
errors: {
|
||||
id: ["Item not found"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const existingItemByName = await ItemService.findByName(name)
|
||||
|
||||
if (existingItemByName && existingItemByName.id !== id) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { name: ["An item with this name already exists"] },
|
||||
}
|
||||
}
|
||||
|
||||
await ItemService.update(id, {
|
||||
stock: stock || existingItem.stock,
|
||||
name: name || existingItem.name,
|
||||
category: { connect: { id: categoryId } },
|
||||
})
|
||||
|
||||
const quantity = stock - existingItem.stock
|
||||
|
||||
if (stock && stock > existingItem.stock) {
|
||||
await prisma.movement.create({
|
||||
data: {
|
||||
type: "IN",
|
||||
itemId: id,
|
||||
quantity,
|
||||
userId: await getAuthenticatedUserId(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Item updated successfully!",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return {
|
||||
error: "Failed to update item",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteItemAction(formData: FormData) {
|
||||
const { id } = Object.fromEntries(formData) as { id: string }
|
||||
|
||||
try {
|
||||
const existingItem = await ItemService.findByIdWithAssetCount(id)
|
||||
|
||||
if (!existingItem) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { id: ["Item not found"] },
|
||||
}
|
||||
}
|
||||
|
||||
if (existingItem._count.assets > 0) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { id: ["Item has assets, you cannot delete it"] },
|
||||
}
|
||||
}
|
||||
|
||||
if (existingItem.stock > 0) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { id: ["Item has stock, you cannot delete it"] },
|
||||
}
|
||||
}
|
||||
|
||||
await ItemService.delete(id)
|
||||
|
||||
revalidatePath("/inventory/items")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Item deleted successfully!",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return {
|
||||
error: "Failed to delete item",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
"use server"
|
||||
|
||||
import { revalidatePath } from "next/cache"
|
||||
|
||||
import prisma from "@/lib/prisma"
|
||||
import {
|
||||
CreateRecipientFormType,
|
||||
createRecipientSchema,
|
||||
UpdateRecipientFormType,
|
||||
updateRecipientSchema,
|
||||
} from "@/lib/schemas/recipients.schemas"
|
||||
import { RecipientService } from "@/services/recipient.service"
|
||||
|
||||
export async function createNewRecipient(formData: CreateRecipientFormType) {
|
||||
const validatedFields = createRecipientSchema.safeParse(formData)
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return {
|
||||
success: false,
|
||||
errors: validatedFields.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
const { username, firstName, lastName, department, email, phone } =
|
||||
validatedFields.data
|
||||
|
||||
try {
|
||||
const existingRecipientUsername =
|
||||
await RecipientService.findByUsername(username)
|
||||
|
||||
if (existingRecipientUsername) {
|
||||
return {
|
||||
success: false,
|
||||
errors: {
|
||||
username: ["Username already exists"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const existingRecipientEmail = await RecipientService.findByEmail(
|
||||
email || "",
|
||||
)
|
||||
|
||||
if (existingRecipientEmail) {
|
||||
return {
|
||||
success: false,
|
||||
errors: {
|
||||
email: ["Email already exists"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
await RecipientService.create({
|
||||
username: username || (firstName[0] + lastName).toLowerCase(),
|
||||
firstName,
|
||||
lastName,
|
||||
department: department,
|
||||
email: email || null,
|
||||
phone: phone || null,
|
||||
})
|
||||
|
||||
revalidatePath("/recipients")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Recipient created successfully",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return {
|
||||
message: "Failed to create recipient",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateRecipient(formData: UpdateRecipientFormType) {
|
||||
const validatedFields = updateRecipientSchema.safeParse(formData)
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return {
|
||||
success: false,
|
||||
errors: validatedFields.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
const { id, username, firstName, lastName, department, email, phone } =
|
||||
validatedFields.data
|
||||
|
||||
try {
|
||||
const existingRecipient = await RecipientService.findByUsername(username)
|
||||
|
||||
if (existingRecipient && existingRecipient.id !== id) {
|
||||
return {
|
||||
success: false,
|
||||
errors: {
|
||||
username: ["Username already exists"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const existingRecipientEmail = await RecipientService.findByEmail(
|
||||
email || "",
|
||||
)
|
||||
|
||||
if (existingRecipientEmail && existingRecipientEmail.id !== id) {
|
||||
return {
|
||||
success: false,
|
||||
errors: {
|
||||
email: ["Email already exists"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.recipient.update({
|
||||
where: { id },
|
||||
data: {
|
||||
username,
|
||||
firstName,
|
||||
lastName,
|
||||
department: department,
|
||||
email: email || null,
|
||||
phone: phone || null,
|
||||
},
|
||||
})
|
||||
|
||||
revalidatePath("/recipients")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Recipient updated successfully",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return {
|
||||
message: "Failed to update recipient",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import NextAuth, { type DefaultSession } from "next-auth"
|
||||
import Credentials from "next-auth/providers/credentials"
|
||||
import { ZodError } from "zod"
|
||||
|
||||
import { UserRole } from "@/generated/prisma/client"
|
||||
import { SIGN_IN_URL, TOKEN_EXPIRATION_SECONDS } from "@/lib/constants"
|
||||
import { signInSchema } from "@/lib/schemas/auth.schemas"
|
||||
import { verifyPassword } from "@/lib/security"
|
||||
import { getUserByUsername } from "@/services/user.service"
|
||||
|
||||
declare module "next-auth" {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
interface Session {
|
||||
user: {
|
||||
id: string
|
||||
role: UserRole
|
||||
} & DefaultSession["user"]
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
interface User {
|
||||
role: UserRole
|
||||
}
|
||||
}
|
||||
|
||||
export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
maxAge: TOKEN_EXPIRATION_SECONDS,
|
||||
},
|
||||
providers: [
|
||||
Credentials({
|
||||
credentials: {
|
||||
username: {},
|
||||
password: {},
|
||||
},
|
||||
authorize: async (credentials) => {
|
||||
try {
|
||||
const { data, success } = signInSchema.safeParse(credentials)
|
||||
|
||||
if (!success) throw new Error("Invalid username or password")
|
||||
|
||||
const user = await getUserByUsername(data.username)
|
||||
|
||||
if (!user) {
|
||||
throw new Error("Invalid username or password")
|
||||
}
|
||||
|
||||
if (!(await verifyPassword(data.password, user.password)))
|
||||
throw new Error("Invalid username or password")
|
||||
|
||||
return {
|
||||
...user,
|
||||
role: user.role,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
return null
|
||||
}
|
||||
throw new Error("Invalid username or password")
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
pages: {
|
||||
signIn: SIGN_IN_URL,
|
||||
},
|
||||
callbacks: {
|
||||
async session({ session, token }) {
|
||||
if (!token) return session
|
||||
session.user.id = token.sub as string
|
||||
session.user.name = token.name as string
|
||||
session.user.email = token.email as string
|
||||
session.user.role = token.role as UserRole
|
||||
return session
|
||||
},
|
||||
async jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.sub = user.id
|
||||
token.name = user.name
|
||||
token.email = user.email
|
||||
token.role = user.role
|
||||
}
|
||||
return token
|
||||
},
|
||||
authorized: async ({ auth }) => {
|
||||
return !!auth
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
export const SITE_URL =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:3000"
|
||||
: process.env.DOMAIN
|
||||
|
||||
export const SIGN_IN_URL = "/login"
|
||||
|
||||
export const TOKEN_EXPIRATION_SECONDS = 60 * 60 * 2 // 2 hour
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
import { Prisma } from "@/generated/prisma/client"
|
||||
import { PaginatedResult } from "@/lib/types"
|
||||
|
||||
//eslint-disable-next-line
|
||||
type PaginationArgs<T> = {
|
||||
model: any // prisma.<model>
|
||||
where?: Prisma.Enumerable<any> // filtros
|
||||
page?: number
|
||||
pageSize?: number
|
||||
orderBy?: any
|
||||
include?: Prisma.Enumerable<any>
|
||||
select?: Prisma.Enumerable<any>
|
||||
}
|
||||
|
||||
export async function paginate<T>({
|
||||
model,
|
||||
where = {},
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
orderBy = { createdAt: "desc" },
|
||||
include,
|
||||
select,
|
||||
}: PaginationArgs<T>): Promise<PaginatedResult<T>> {
|
||||
const skip = (page - 1) * pageSize
|
||||
|
||||
const [data, totalItems] = await Promise.all([
|
||||
model.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: pageSize,
|
||||
orderBy,
|
||||
include,
|
||||
select,
|
||||
}),
|
||||
model.count({ where }),
|
||||
])
|
||||
|
||||
return {
|
||||
data,
|
||||
totalItems,
|
||||
totalPages: Math.ceil(totalItems / pageSize),
|
||||
currentPage: page,
|
||||
pageSize,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { PrismaClient } from "@/generated/prisma/client"
|
||||
|
||||
const globalForPrisma = global as unknown as {
|
||||
prisma: PrismaClient
|
||||
}
|
||||
|
||||
const prisma = globalForPrisma.prisma || new PrismaClient()
|
||||
|
||||
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma
|
||||
|
||||
export default prisma
|
||||
@@ -0,0 +1,23 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const assetSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
itemId: z.string().min(1, { message: "Item is required" }),
|
||||
serialNumber: z.string().min(1, { message: "Serial number is required" }),
|
||||
deliveryNote: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
recipientId: z.string().optional(),
|
||||
})
|
||||
|
||||
export const createAssetSchema = assetSchema.extend({
|
||||
status: z.enum(["AVAILABLE", "ASSIGNED"]),
|
||||
})
|
||||
|
||||
export type CreateAssetFormType = z.infer<typeof createAssetSchema>
|
||||
|
||||
export const updateAssetSchema = assetSchema.extend({
|
||||
id: z.string().min(1, { message: "ID is required" }),
|
||||
status: z.enum(["AVAILABLE", "ASSIGNED", "RESERVED", "IN_REPAIR"]),
|
||||
})
|
||||
|
||||
export type UpdateAssetFormType = z.infer<typeof updateAssetSchema>
|
||||
@@ -0,0 +1,43 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const assignmentSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
quantity: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.min(1, { message: "Quantity is required" }),
|
||||
notes: z.string().optional(),
|
||||
itemId: z.string().min(1, { message: "Item is required" }).optional(),
|
||||
assetId: z.string().optional(),
|
||||
recipientId: z.string().min(1, { message: "Recipient is required" }),
|
||||
assignmentDate: z.date().optional(),
|
||||
returnDate: z.date().optional(),
|
||||
})
|
||||
|
||||
export const createAssignmentSchema = assignmentSchema.omit({
|
||||
id: true,
|
||||
returnDate: true,
|
||||
})
|
||||
export type CreateAssignmentFormType = z.infer<typeof createAssignmentSchema>
|
||||
|
||||
export const updateAssignmentSchema = assignmentSchema
|
||||
.omit({
|
||||
returnDate: true,
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.itemId && !data.assetId) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Asset ID is required when item ID is provided",
|
||||
path: ["assetId"],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export type UpdateAssignmentFormType = z.infer<typeof updateAssignmentSchema>
|
||||
|
||||
export const returnAssignmentSchema = z.object({
|
||||
id: z.string().min(1, { message: "Assignment ID is required" }),
|
||||
})
|
||||
export type ReturnAssignmentFormType = z.infer<typeof returnAssignmentSchema>
|
||||
@@ -0,0 +1,14 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const signInSchema = z.object({
|
||||
username: z
|
||||
.string()
|
||||
.min(1, { message: "Invalid username" })
|
||||
.nonempty("Username is required"),
|
||||
password: z
|
||||
.string()
|
||||
.min(3, { message: "Password is too short" })
|
||||
.nonempty("Password is required"),
|
||||
})
|
||||
|
||||
export type SignInFormType = z.infer<typeof signInSchema>
|
||||
@@ -0,0 +1,18 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const createCategorySchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(3, {
|
||||
message: "Name is required and must be at least 3 characters long",
|
||||
})
|
||||
.nonempty("Name is required and must be at least 3 characters long"),
|
||||
})
|
||||
|
||||
export type CreateCategoryFormType = z.infer<typeof createCategorySchema>
|
||||
|
||||
export const updateCategorySchema = createCategorySchema.extend({
|
||||
id: z.string().nonempty("ID is required"),
|
||||
})
|
||||
|
||||
export type UpdateCategoryFormType = z.infer<typeof updateCategorySchema>
|
||||
@@ -0,0 +1,10 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const importSchema = z.object({
|
||||
file: z.instanceof(File).refine((file) => file.type === "text/csv", {
|
||||
message: "File must be a CSV",
|
||||
}),
|
||||
categoryId: z.string().optional(),
|
||||
})
|
||||
|
||||
export type ImportFormType = z.infer<typeof importSchema>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const createItemSchema = z.object({
|
||||
name: z.string().min(1, { message: "Name is required" }),
|
||||
categoryId: z.string().min(1, { message: "Category is required" }),
|
||||
stock: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.min(0, { message: "Stock is required" }),
|
||||
})
|
||||
|
||||
export type CreateItemFormType = z.infer<typeof createItemSchema>
|
||||
|
||||
export const updateItemSchema = createItemSchema.extend({
|
||||
id: z.string().min(1, { message: "Item is required" }),
|
||||
})
|
||||
|
||||
export type UpdateItemFormType = z.infer<typeof updateItemSchema>
|
||||
|
||||
export const getItemByIdSchema = z.object({
|
||||
id: z.string().min(1, { message: "Item is required" }),
|
||||
})
|
||||
|
||||
export type GetItemByIdFormType = z.infer<typeof getItemByIdSchema>
|
||||
@@ -0,0 +1,23 @@
|
||||
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, { message: "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>
|
||||
@@ -0,0 +1,48 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const recipientSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
username: z
|
||||
.string()
|
||||
.min(1, { message: "Username is required" })
|
||||
.nonempty("Username is required"),
|
||||
firstName: z.string().min(1, { message: "First name is required" }),
|
||||
lastName: z.string().min(1, { message: "Last name is required" }),
|
||||
department: z.enum(
|
||||
[
|
||||
"IT",
|
||||
"ENGINEERING",
|
||||
"TRAFFIC",
|
||||
"DRIVER",
|
||||
"LOGISTICS",
|
||||
"ADMINISTRATION",
|
||||
"SALES",
|
||||
"OTHER",
|
||||
],
|
||||
{
|
||||
message: "Department is required",
|
||||
},
|
||||
),
|
||||
email: z.string().optional().nullable(),
|
||||
phone: z.string().optional().nullable(),
|
||||
})
|
||||
|
||||
export const createRecipientSchema = recipientSchema.superRefine(
|
||||
(data, ctx) => {
|
||||
if (data.email && !z.string().email().safeParse(data.email).success) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Email format is invalid",
|
||||
path: ["email"],
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export type CreateRecipientFormType = z.infer<typeof createRecipientSchema>
|
||||
|
||||
export const updateRecipientSchema = recipientSchema.extend({
|
||||
id: z.string().nonempty("ID is required"),
|
||||
})
|
||||
|
||||
export type UpdateRecipientFormType = z.infer<typeof updateRecipientSchema>
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as bcrypt from "bcryptjs"
|
||||
|
||||
export const runtime = "nodejs"
|
||||
|
||||
const saltRounds: number = 10
|
||||
|
||||
export async function verifyPassword(
|
||||
plainPassword: string,
|
||||
hashedPassword: string,
|
||||
): Promise<boolean> {
|
||||
const isMatch = await bcrypt.compare(plainPassword, hashedPassword)
|
||||
if (!isMatch) {
|
||||
return false
|
||||
}
|
||||
return isMatch
|
||||
}
|
||||
|
||||
export async function getPasswordHash(password: string): Promise<string> {
|
||||
if (!password) {
|
||||
throw new Error("Password is required")
|
||||
}
|
||||
const salt = await bcrypt.genSalt(saltRounds)
|
||||
const hash = await bcrypt.hash(password, salt)
|
||||
return hash
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
Asset as PrismaAsset,
|
||||
Assignment,
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Assignment as PrismaAssignment } from "@/generated/prisma/client"
|
||||
|
||||
import { Asset } from "./asset"
|
||||
import { Item } from "./item"
|
||||
import { 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
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { 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
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export interface ImportItem {
|
||||
name: string
|
||||
stock?: number
|
||||
serialNumber?: string
|
||||
categoryId?: string
|
||||
category?: string
|
||||
deliveryNote?: string
|
||||
assigned?: boolean
|
||||
username?: string
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
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"
|
||||
@@ -0,0 +1,21 @@
|
||||
import { 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Movement as PrismaMovement } from "@/generated/prisma/client"
|
||||
|
||||
import { Asset } from "./asset"
|
||||
import { Item } from "./item"
|
||||
import { 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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export type PaginatedResult<T> = {
|
||||
data: T[]
|
||||
totalItems: number
|
||||
totalPages: number
|
||||
currentPage: number
|
||||
pageSize: number
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Recipient as PrismaRecipient } from "@/generated/prisma/client"
|
||||
|
||||
export type Recipient = PrismaRecipient
|
||||
@@ -0,0 +1,3 @@
|
||||
import { User as PrismaUser } from "@/generated/prisma/client"
|
||||
|
||||
export type User = PrismaUser
|
||||
@@ -0,0 +1,14 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function formatDate(date: Date) {
|
||||
return new Intl.DateTimeFormat("es-ES", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
}).format(date)
|
||||
}
|
||||
Reference in New Issue
Block a user