diff --git a/src/helpers/permission.ts b/src/helpers/permission.ts index ffbb3bb..73feada 100644 --- a/src/helpers/permission.ts +++ b/src/helpers/permission.ts @@ -9,6 +9,7 @@ export type PermissionString = | "productUnit" | "productCategory" | "productBrand" + | "productCode" | "productWarranty"; export type IPermission = Record>; export const permissionOperations: PermissionOperation[] = ["create", "read", "update", "delete"]; @@ -29,7 +30,8 @@ export const PERMISSIONS_LIST: PermissionString[] = [ "faqs", "productCategory", "productBrand", - "productUnit" + "products", + "productCode" ]; export const PERMISSIONS = structurePermissionsObject(PERMISSIONS_LIST); diff --git a/src/interfaces/productCode.ts b/src/interfaces/productCode.ts new file mode 100644 index 0000000..95edcea --- /dev/null +++ b/src/interfaces/productCode.ts @@ -0,0 +1,9 @@ +import { DefaultPluginProps } from "."; + +export interface ProductCodeProps extends DefaultPluginProps { + code: string; + slug?: string; + description?: string; + accountId: string; + wareHouseId?: string; +} diff --git a/src/pages/productBrand/CreateProductBrandScreen.tsx b/src/pages/productBrands/CreateProductBrandScreen.tsx similarity index 100% rename from src/pages/productBrand/CreateProductBrandScreen.tsx rename to src/pages/productBrands/CreateProductBrandScreen.tsx diff --git a/src/pages/productBrand/EditProductBrandFields.tsx b/src/pages/productBrands/EditProductBrandFields.tsx similarity index 100% rename from src/pages/productBrand/EditProductBrandFields.tsx rename to src/pages/productBrands/EditProductBrandFields.tsx diff --git a/src/pages/productBrand/ListProductBrandScreen.tsx b/src/pages/productBrands/ListProductBrandScreen.tsx similarity index 100% rename from src/pages/productBrand/ListProductBrandScreen.tsx rename to src/pages/productBrands/ListProductBrandScreen.tsx diff --git a/src/pages/productBrand/UpdateProductBrandScreen.tsx b/src/pages/productBrands/UpdateProductBrandScreen.tsx similarity index 100% rename from src/pages/productBrand/UpdateProductBrandScreen.tsx rename to src/pages/productBrands/UpdateProductBrandScreen.tsx diff --git a/src/pages/productCodes/CreateProductCodeScreen.tsx b/src/pages/productCodes/CreateProductCodeScreen.tsx new file mode 100644 index 0000000..db27bce --- /dev/null +++ b/src/pages/productCodes/CreateProductCodeScreen.tsx @@ -0,0 +1,72 @@ +import { HandlerProps } from "@/components/customFields/type"; +import { useGeneralMutation } from "@/hooks/request/useGeneralMutation"; +import { useError } from "@/hooks/useError"; +import { useFormFieldUpdate } from "@/hooks/useFormFieldUpdate"; +import { Validator } from "@/validator"; +import { useNavigate } from "react-router-dom"; +import { toast } from "sonner"; +import EditProductCodeFields from "./EditProductCodeFields"; + +const CreateProductCodeScreen = () => { + const buttonTitle = "Create"; + const defaultData = { + description: "", + code: "" + }; + const { addErrors, errors, resetError } = useError(); + const { formValues, updateFormFieldValue } = useFormFieldUpdate(defaultData); + const navigate = useNavigate(); + const { isPending, mutate } = useGeneralMutation({ + httpMethod: "post", + mutationKey: ["createProductCode"], + url: "/product-codes" + }); + const validator = new Validator({ + formData: formValues, + rules: { + description: "required|minLength:10", + code: "required:minLength:5" + } + }); + + const handleFormFieldChange = (data: HandlerProps) => { + const { key, value } = data; + updateFormFieldValue(key, value); + }; + + const onsubmitHandler = () => { + validator.validate(); + + if (validator.failed()) { + return addErrors(validator.getValidationErrorsByIndex()); + } else { + resetError(); + } + + mutate( + { payload: formValues }, + { + onSuccess() { + toast.success("Success", { + description: "Product code created" + }); + navigate("/product-codes"); + } + } + ); + }; + + return ( + } + isLoading={isPending} + /> + ); +}; + +export default CreateProductCodeScreen; diff --git a/src/pages/productCodes/EditProductCodeFields.tsx b/src/pages/productCodes/EditProductCodeFields.tsx new file mode 100644 index 0000000..75259a0 --- /dev/null +++ b/src/pages/productCodes/EditProductCodeFields.tsx @@ -0,0 +1,67 @@ +import Container from "@/components/Container"; +import PrimaryButton from "@/components/PrimaryButton"; +import InputField from "@/components/customFields/input/InputField"; +import TextAreaField from "@/components/customFields/input/TextAreaField"; +import { HandlerProps } from "@/components/customFields/type"; +import DashboardLayout from "@/components/dashboard/Layout"; +import { FC } from "react"; + +interface EditProductCodeFieldsProps { + buttonTitle: string; + formFields: Record; + onsubmitHandler: () => void; + handleFormFieldChange: (data: HandlerProps) => void; + errors?: Record; + isLoading?: boolean; + pageTitle?: string; + disabledButton?: boolean; +} +const EditProductCodeFields: FC = ({ + handleFormFieldChange, + onsubmitHandler, + formFields, + errors, + isLoading, + buttonTitle, + pageTitle, + disabledButton +}) => { + return ( + + +

{pageTitle}

+
+ + +
+ +
+
+
+
+ ); +}; + +export default EditProductCodeFields; diff --git a/src/pages/productCodes/ListProductCodesScreen.tsx b/src/pages/productCodes/ListProductCodesScreen.tsx new file mode 100644 index 0000000..55cbba8 --- /dev/null +++ b/src/pages/productCodes/ListProductCodesScreen.tsx @@ -0,0 +1,120 @@ +import Container from "@/components/Container"; +import Modal from "@/components/Modal"; +import DashboardLayout from "@/components/dashboard/Layout"; +import Table from "@/components/table/Table"; +import { useGeneralMutation } from "@/hooks/request/useGeneralMutation"; +import { useGeneralQuery } from "@/hooks/request/useGeneralQuery"; +import { useOptimisticUpdates } from "@/hooks/request/useOptimisticUpdates"; +import { GetManyProps } from "@/hooks/types"; +import { useSetQueryParam } from "@/hooks/useSetQueryParam"; +import { ModalActionButtonProps } from "@/interfaces"; +import { ProductCodeProps } from "@/interfaces/productCode"; +import { productCodeSchema } from "@/tableSchema/productCodes"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { toast } from "sonner"; + +const ListProductCodeScreen = () => { + const { removeItemFromList } = useOptimisticUpdates(); + const [selectedCode, setSelectedCode] = useState>({}); + const { queryObject } = useSetQueryParam(); + const { mutate, isPending } = useGeneralMutation({ + httpMethod: "delete", + mutationKey: ["deleteProductCode", selectedCode.id], + url: `/product-codes/${selectedCode.id}` + }); + + const { data, isFetching } = useGeneralQuery>({ + queryKey: ["productCodes", queryObject], + url: "/product-codes", + query: queryObject, + enabled: !!Object.keys(queryObject).length + }); + const [openModal, setOpenModal] = useState(false); + const navigate = useNavigate(); + const rowActions = [ + { + label: "Edit", + action: handleEditRowActionClick + }, + { + label: "Delete", + action: (data: Record) => { + setOpenModal(true); + setSelectedCode(data); + } + } + ]; + + const modalData = { + showModal: openModal, + modalTitle: (code: string) => { + console.log({ code }); + + return `Are you sure you want to delete ${code}`; + }, + modalDescription: `Deleting the product code will permanently remove it from the system. Continue?`, + actionButtons: [ + { + title: "Cancel", + action: () => setOpenModal(false), + type: "cancel" + }, + { + title: "Continue", + action: async () => { + mutate(selectedCode.id, { + onSuccess: () => { + setOpenModal(false); + toast.success("Success", { + description: "Product code deleted" + }); + }, + onSettled() { + return removeItemFromList(["productCodes", queryObject], selectedCode.id); + } + }); + }, + type: "action", + loading: isPending + } + ] as ModalActionButtonProps[] + }; + + function handleEditRowActionClick(data: Record) { + navigate(`/product-codes/${data.id}`); + } + + return ( + navigate("/product-codes/create") } + }} + > + + + + + + ); +}; + +export default ListProductCodeScreen; diff --git a/src/pages/productCodes/UpdateProductCodeScreen.tsx b/src/pages/productCodes/UpdateProductCodeScreen.tsx new file mode 100644 index 0000000..16da744 --- /dev/null +++ b/src/pages/productCodes/UpdateProductCodeScreen.tsx @@ -0,0 +1,89 @@ +import { HandlerProps } from "@/components/customFields/type"; +import { useGeneralMutation } from "@/hooks/request/useGeneralMutation"; +import { useError } from "@/hooks/useError"; +import { useFormFieldUpdate } from "@/hooks/useFormFieldUpdate"; +import { Validator } from "@/validator"; +import { useNavigate, useParams } from "react-router-dom"; +import { toast } from "sonner"; +import { useGeneralQuery } from "@/hooks/request/useGeneralQuery"; +import { objectDifference } from "@/helpers"; +import { useEffect } from "react"; +import EditProductCodeFields from "./EditProductCodeFields"; +import { ProductCodeProps } from "@/interfaces/productCode"; + +interface ValidatorProps { + code: string; + description: string; +} +const UpdateProductCodeScreen = () => { + const params = useParams<{ id: string }>(); + const categoryId = params.id; + const buttonTitle = "Update"; + const { addErrors, errors, resetError } = useError(); + const { data } = useGeneralQuery({ + queryKey: ["productCode", categoryId], + url: `/product-codes/${categoryId}` + }); + + const { formValues, updateFormFieldValue, setFormValues } = useFormFieldUpdate(data); + const navigate = useNavigate(); + const { isPending, mutate } = useGeneralMutation({ + httpMethod: "put", + mutationKey: ["updateCode", categoryId as string], + url: `/product-codes/${categoryId}` + }); + const validator = new Validator>({ + formData: formValues as ValidatorProps, + rules: { + description: "required|minLength:10", + code: "required:minLength:5" + } + }); + + const handleFormFieldChange = (data: HandlerProps) => { + const { key, value } = data; + updateFormFieldValue(key, value); + }; + + const payload = objectDifference(data, formValues); + const onsubmitHandler = () => { + validator.validate(); + + if (validator.failed()) { + return addErrors(validator.getValidationErrorsByIndex()); + } else { + resetError(); + } + + mutate( + { payload }, + { + onSuccess() { + toast.success("Success", { + description: "Product code created" + }); + navigate("/product-codes"); + } + } + ); + }; + useEffect(() => { + if (data) { + setFormValues(data); + } + }, [params.id, data]); + return ( + } + handleFormFieldChange={handleFormFieldChange} + onsubmitHandler={onsubmitHandler} + errors={errors as Record} + isLoading={isPending} + disabledButton={!Object.keys(payload).length} + /> + ); +}; + +export default UpdateProductCodeScreen; diff --git a/src/route/product.ts b/src/route/product.ts index 8ee8756..118b058 100644 --- a/src/route/product.ts +++ b/src/route/product.ts @@ -1,10 +1,13 @@ import { RoutesProps } from "@/interfaces/route"; -import CreateProductBrandScreen from "@/pages/productBrand/CreateProductBrandScreen"; -import ListProductBrandScreen from "@/pages/productBrand/ListProductBrandScreen"; -import UpdateProductBrandScreen from "@/pages/productBrand/UpdateProductBrandScreen"; +import CreateProductBrandScreen from "@/pages/productBrands/CreateProductBrandScreen"; +import ListProductBrandScreen from "@/pages/productBrands/ListProductBrandScreen"; +import UpdateProductBrandScreen from "@/pages/productBrands/UpdateProductBrandScreen"; import CreateProductCategoryScreen from "@/pages/productCategories/CreateProductCategoryScreen"; import ListProductCategoriesScreen from "@/pages/productCategories/ListProductCategoriesScreen"; import UpdateProductCategoryScreen from "@/pages/productCategories/UpdateProductCategoryScreen"; +import CreateProductCodeScreen from "@/pages/productCodes/CreateProductCodeScreen"; +import ListProductCodeScreen from "@/pages/productCodes/ListProductCodesScreen"; +import UpdateProductCodeScreen from "@/pages/productCodes/UpdateProductCodeScreen"; import CreateProductUnitScreen from "@/pages/productUnits/CreateProductUnitScreen"; import ListProductUnitsScreen from "@/pages/productUnits/ListProductUnitsScreen"; import UpdateProductUnitScreen from "@/pages/productUnits/UpdateProductUnitScreen"; @@ -68,5 +71,30 @@ const PRODUCT_UNITS_ROUTES: RoutesProps[] = [ permission: ["productUnit", "update"] } ]; +const PRODUCT_CODES_ROUTES: RoutesProps[] = [ + { + component: ListProductCodeScreen, + url: "/product-codes", + requireAuth: true, + permission: ["productCode", "read"] + }, + { + component: CreateProductCodeScreen, + url: "/product-codes/create", + requireAuth: true, + permission: ["productCode", "create"] + }, + { + component: UpdateProductCodeScreen, + url: "/product-codes/:id", + requireAuth: true, + permission: ["productCode", "update"] + } +]; -export const PRODUCT_ENDPOINTS = [...PRODUCT_CATEGORIES_ROUTES, ...PRODUCT_BRANDS_ROUTES, ...PRODUCT_UNITS_ROUTES]; +export const PRODUCT_ENDPOINTS = [ + ...PRODUCT_CATEGORIES_ROUTES, + ...PRODUCT_BRANDS_ROUTES, + ...PRODUCT_UNITS_ROUTES, + ...PRODUCT_CODES_ROUTES +]; diff --git a/src/route/sidebar.ts b/src/route/sidebar.ts index efb0396..01e6c1f 100644 --- a/src/route/sidebar.ts +++ b/src/route/sidebar.ts @@ -72,10 +72,10 @@ export const menuSidebarRoutes = (userRole?: string): MenuSidebarRoutes => ({ url: "/users", isDisabled: true }, + { - title: "Print Barcode/QrCode", - url: "/users", - isDisabled: true + title: "Product Codes", + url: "/product-codes" }, { title: "Product Categories", @@ -99,6 +99,11 @@ export const menuSidebarRoutes = (userRole?: string): MenuSidebarRoutes => ({ url: "/users", isDisabled: true }, + { + title: "Print Barcode/QrCode", + url: "/users", + isDisabled: true + }, { title: "Product Alert", url: "/users", diff --git a/src/tableSchema/productCodes.tsx b/src/tableSchema/productCodes.tsx new file mode 100644 index 0000000..02cb43f --- /dev/null +++ b/src/tableSchema/productCodes.tsx @@ -0,0 +1,27 @@ +import { ColumnDef } from "@tanstack/react-table"; +import { DataTableColumnHeader } from "@/components/table/DataTableColumnHeader"; +import { ProductCodeProps } from "@/interfaces/productCode"; +import { format } from "date-fns"; + +export const productCodeSchema: ColumnDef[] = [ + { + accessorKey: "code", + header: ({ column }) => , + cell: ({ row }) =>
{row.getValue("code")}
, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)); + }, + size: 12 + }, + { + accessorKey: "createdAt", + header: ({ column }) => , + cell: ({ row }) => { + const date: Date = row.getValue("createdAt"); + return
{format(date, "d-m-y")}
; + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)); + } + } +];