diff --git a/collection/app/(app)/products/AddProduct.tsx b/collection/app/(app)/products/AddProduct.tsx new file mode 100644 index 0000000..7ff95fe --- /dev/null +++ b/collection/app/(app)/products/AddProduct.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { addProducts } from "@/lib/crud/products"; +import { StatusReturn } from "@/lib/types"; +import { + ActionIcon, + Alert, + Button, + Group, + InputLabel, + Modal, + NativeSelect, + Stack, + TextInput, +} from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { useDisclosure } from "@mantine/hooks"; +import React, { useState, useTransition } from "react"; +import { FaPlus, FaTrash } from "react-icons/fa6"; + +interface AddProductProps { + academicYears: string[]; + currentYear: string; +} + +type AddProductFormProps = AddProductProps & { + close: () => void; +}; + +interface AddProductForm { + products: string[]; + academicYear: string; +} + +const AddProductForm: React.FC = ({ academicYears, currentYear, close }) => { + const [isPending, startTransition] = useTransition(); + const [status, setStatus] = useState({ + status: "pending", + }); + const form = useForm({ + mode: "controlled", + initialValues: { + products: [""], + academicYear: currentYear, + }, + }); + + const submit = ({ academicYear, products }: { academicYear: string; products: string[] }) => { + startTransition(async () => { + const res = await addProducts(academicYear, products); + + if (res.status === "error") { + setStatus(res); + } else { + close(); + } + }); + }; + + return ( +
+ + { + // Display error if there is one + status.status === "error" && ( + + {status.error} + + ) + } + + + Products + form.insertListItem("products", "")}> + + + + {form.getValues().products.map((product, i) => ( + + + form.removeListItem("products", i)}> + + + + ))} + + + + +
+ ); +}; + +export const AddProduct: React.FC = (props) => { + const [opened, { open, close }] = useDisclosure(false); + return ( + <> + + + + + + ); +}; diff --git a/collection/app/(app)/products/DeleteProduct.tsx b/collection/app/(app)/products/DeleteProduct.tsx new file mode 100644 index 0000000..b661d13 --- /dev/null +++ b/collection/app/(app)/products/DeleteProduct.tsx @@ -0,0 +1,27 @@ +import { deleteProduct } from "@/lib/crud/products"; +import { Button } from "@mantine/core"; +import React, { useTransition } from "react"; +import { FaTrash } from "react-icons/fa6"; + +export const DeleteProduct = ({ productId }: { productId: number }) => { + const [isPending, startTransition] = useTransition(); + + const onClickHandler = () => { + startTransition(async () => { + await deleteProduct(productId); + }); + }; + + return ( + + ); +}; diff --git a/collection/app/(app)/products/VariantsTable.tsx b/collection/app/(app)/products/VariantsTable.tsx index 5eef05c..181ce82 100644 --- a/collection/app/(app)/products/VariantsTable.tsx +++ b/collection/app/(app)/products/VariantsTable.tsx @@ -1,10 +1,12 @@ "use client"; import TanstackTable from "@/components/tables/TanStackTable"; -import { Text } from "@mantine/core"; +import { Group, Text } from "@mantine/core"; import { createColumnHelper } from "@tanstack/react-table"; import React from "react"; +import { DeleteProduct } from "./DeleteProduct"; + interface Variant { id: number; name: string; @@ -30,11 +32,17 @@ const columns = [ interface VariantsTableProps { variants: Variant[]; + productId: number; } -export const VariantsTable: React.FC = ({ variants }) => { +export const VariantsTable: React.FC = ({ variants, productId }) => { if (!variants || variants.length === 0) { - return No variants added yet; + return ( + + No variants added yet + + + ); } return ( diff --git a/collection/app/(app)/products/YearProducts.tsx b/collection/app/(app)/products/YearProducts.tsx index d2d61e0..021ffce 100644 --- a/collection/app/(app)/products/YearProducts.tsx +++ b/collection/app/(app)/products/YearProducts.tsx @@ -26,6 +26,7 @@ export const YearProducts: React.FC = ({ products }) => { name: variant.variantName, count: variant._count.OrderItem, }))} + productId={product.id} /> diff --git a/collection/app/(app)/products/page.tsx b/collection/app/(app)/products/page.tsx index 8912e0c..aff4bb5 100644 --- a/collection/app/(app)/products/page.tsx +++ b/collection/app/(app)/products/page.tsx @@ -1,13 +1,16 @@ import TanstackTable from "@/components/tables/TanStackTable"; +import { getAcademicYear } from "@/lib/config"; +import { getAcademicYearsInDB } from "@/lib/crud/academic-year"; import { getProductsAndVariantByAcademicYearWithCounts, ProductsAndVariantsByAcademicYear, } from "@/lib/crud/products"; -import { Alert, Grid, GridCol, Stack, Title } from "@mantine/core"; +import { Alert, Grid, GridCol, Group, Stack, Title } from "@mantine/core"; import { createColumnHelper } from "@tanstack/react-table"; import React from "react"; import { FaInfoCircle } from "react-icons/fa"; +import { AddProduct } from "./AddProduct"; import { VariantsTable } from "./VariantsTable"; import { YearProducts } from "./YearProducts"; @@ -36,9 +39,14 @@ const columns = [ export const ProductsPage = async () => { const data = await getProductsAndVariantByAcademicYearWithCounts(); + const academicYears = await getAcademicYearsInDB(); + const currentYear = await getAcademicYear(); return ( - Products by Academic Year + + Products by Academic Year + + }> Variants will be added automatically on import diff --git a/collection/lib/crud/products.ts b/collection/lib/crud/products.ts index 6def318..96d1e6b 100644 --- a/collection/lib/crud/products.ts +++ b/collection/lib/crud/products.ts @@ -1,8 +1,11 @@ "use server"; +import { isValidAcademicYear } from "@docsoc/eactivities"; import { RootItem } from "@prisma/client"; +import { revalidatePath } from "next/cache"; import prisma from "../db"; +import { StatusReturn } from "../types"; export const getProductsByAcademicYear = async (): Promise> => { // group by res.academic year, just need name and id @@ -69,3 +72,86 @@ export interface ProductsAndVariantsByAcademicYear { }[]; }[]; } + +export async function addProducts(academicYear: string, products: string[]): Promise { + if (products.length === 0) { + return { + status: "error", + error: "No products provided", + }; + } + + if (!isValidAcademicYear(academicYear)) { + return { + status: "error", + error: "Invalid academic year", + }; + } + + try { + await prisma.rootItem.createMany({ + data: products + .map((product) => product.trim()) + .filter((product) => product !== "") + .map((product) => ({ + academicYear, + name: product, + })), + }); + } catch (e: any) { + if (e?.code === "P2002" && e?.meta?.target?.includes("name")) { + return { + status: "error", + error: "Product already exists", + }; + } else { + return { + status: "error", + error: e.message ?? e.toString(), + }; + } + } + + revalidatePath("/products"); + revalidatePath("/"); + + return { + status: "success", + }; +} + +export async function deleteProduct(productId: number): Promise { + try { + // Validate it has no variants + const variants = await prisma.variant.findMany({ + where: { + rootItemId: productId, + }, + }); + + if (variants.length > 0) { + return { + status: "error", + error: "Product has variants", + }; + } + + await prisma.rootItem.delete({ + where: { + id: productId, + }, + }); + } catch (e: any) { + return { + status: "error", + error: e.message ?? e.toString(), + }; + } + + revalidatePath("/products"); + revalidatePath("/"); + + return { + status: "success", + }; +}